openzeppelin_relayer/models/signer/
response.rs

1//! API response models for signer endpoints.
2//!
3//! This module handles outgoing HTTP responses for signer operations, providing:
4//!
5//! - **Response Models**: Structures for returning signer data via API
6//! - **Data Sanitization**: Ensures sensitive information is not exposed
7//! - **Domain Conversion**: Transformation from domain/repository objects to API responses
8//!
9//! Serves as the exit point for signer data to external clients, ensuring
10//! proper data formatting and security considerations.
11
12use crate::models::{Signer, SignerConfig, SignerRepoModel, SignerType};
13use serde::{Deserialize, Serialize};
14use utoipa::ToSchema;
15
16/// Signer configuration response
17/// Does not include sensitive information like private keys
18#[derive(Debug, Serialize, Deserialize, ToSchema, PartialEq, Eq)]
19#[serde(untagged)]
20#[serde(rename_all = "lowercase")]
21pub enum SignerConfigResponse {
22    #[serde(rename = "plain")]
23    Vault {
24        address: String,
25        namespace: Option<String>,
26        key_name: String,
27        mount_point: Option<String>,
28        // role_id: Option<String>, hidden from response due to security concerns
29        // secret_id: Option<String>, hidden from response due to security concerns
30    },
31    #[serde(rename = "vault_transit")]
32    VaultTransit {
33        key_name: String,
34        address: String,
35        namespace: Option<String>,
36        pubkey: String,
37        mount_point: Option<String>,
38        // role_id: Option<String>, hidden from response due to security concerns
39        // secret_id: Option<String>, hidden from response due to security concerns
40    },
41    #[serde(rename = "aws_kms")]
42    AwsKms {
43        region: Option<String>,
44        key_id: String,
45    },
46    Turnkey {
47        api_public_key: String,
48        organization_id: String,
49        private_key_id: String,
50        public_key: String,
51        // api_private_key: Option<String>, hidden from response due to security concerns
52    },
53    Cdp {
54        api_key_id: String,
55        account_address: String,
56        // api_key_secret: SecretString, hidden from response due to security concerns
57        // wallet_secret: SecretString, hidden from response due to security concerns
58    },
59    #[serde(rename = "google_cloud_kms")]
60    GoogleCloudKms {
61        service_account: GoogleCloudKmsSignerServiceAccountResponseConfig,
62        key: GoogleCloudKmsSignerKeyResponseConfig,
63    },
64    Plain {},
65}
66
67#[derive(Debug, Serialize, Deserialize, ToSchema, PartialEq, Eq)]
68pub struct GoogleCloudKmsSignerServiceAccountResponseConfig {
69    pub project_id: String,
70    pub client_id: String,
71    pub auth_uri: String,
72    pub token_uri: String,
73    pub auth_provider_x509_cert_url: String,
74    pub client_x509_cert_url: String,
75    pub universe_domain: String,
76    // pub private_key: Option<String>, hidden from response due to security concerns
77    // pub private_key_id: Option<String>, hidden from response due to security concerns
78    // pub client_email: Option<String>, hidden from response due to security concerns
79}
80
81#[derive(Debug, Serialize, Deserialize, ToSchema, PartialEq, Eq)]
82pub struct GoogleCloudKmsSignerKeyResponseConfig {
83    pub location: String,
84    pub key_ring_id: String,
85    pub key_id: String,
86    pub key_version: u32,
87}
88
89impl From<SignerConfig> for SignerConfigResponse {
90    fn from(config: SignerConfig) -> Self {
91        match config {
92            SignerConfig::Local(_) => SignerConfigResponse::Plain {},
93            SignerConfig::Vault(c) => SignerConfigResponse::Vault {
94                address: c.address,
95                namespace: c.namespace,
96                key_name: c.key_name,
97                mount_point: c.mount_point,
98            },
99            SignerConfig::VaultTransit(c) => SignerConfigResponse::VaultTransit {
100                key_name: c.key_name,
101                address: c.address,
102                namespace: c.namespace,
103                pubkey: c.pubkey,
104                mount_point: c.mount_point,
105            },
106            SignerConfig::AwsKms(c) => SignerConfigResponse::AwsKms {
107                region: c.region,
108                key_id: c.key_id,
109            },
110            SignerConfig::Turnkey(c) => SignerConfigResponse::Turnkey {
111                api_public_key: c.api_public_key,
112                organization_id: c.organization_id,
113                private_key_id: c.private_key_id,
114                public_key: c.public_key,
115            },
116            SignerConfig::Cdp(c) => SignerConfigResponse::Cdp {
117                api_key_id: c.api_key_id,
118                account_address: c.account_address,
119            },
120            SignerConfig::GoogleCloudKms(c) => SignerConfigResponse::GoogleCloudKms {
121                service_account: GoogleCloudKmsSignerServiceAccountResponseConfig {
122                    project_id: (*c.service_account.project_id.to_str()).clone(),
123                    client_id: (*c.service_account.client_id.to_str()).clone(),
124                    auth_uri: (*c.service_account.auth_uri.to_str()).clone(),
125                    token_uri: (*c.service_account.token_uri.to_str()).clone(),
126                    auth_provider_x509_cert_url: (*c
127                        .service_account
128                        .auth_provider_x509_cert_url
129                        .to_str())
130                    .clone(),
131                    client_x509_cert_url: (*c.service_account.client_x509_cert_url.to_str())
132                        .clone(),
133                    universe_domain: (*c.service_account.universe_domain.to_str()).clone(),
134                },
135                key: GoogleCloudKmsSignerKeyResponseConfig {
136                    location: (*c.key.location.to_str()).clone(),
137                    key_ring_id: (*c.key.key_ring_id.to_str()).clone(),
138                    key_id: (*c.key.key_id.to_str()).clone(),
139                    key_version: c.key.key_version,
140                },
141            },
142        }
143    }
144}
145
146#[derive(Debug, Serialize, Deserialize, ToSchema)]
147pub struct SignerResponse {
148    /// The unique identifier of the signer
149    pub id: String,
150    /// The type of signer (local, aws_kms, google_cloud_kms, vault, etc.)
151    pub r#type: SignerType,
152    /// Non-secret configuration details
153    pub config: SignerConfigResponse,
154}
155
156impl From<SignerRepoModel> for SignerResponse {
157    fn from(repo_model: SignerRepoModel) -> Self {
158        // Convert to domain model
159        let domain_signer = Signer::from(repo_model);
160
161        Self {
162            id: domain_signer.id.clone(),
163            r#type: domain_signer.signer_type(),
164            config: SignerConfigResponse::from(domain_signer.config),
165        }
166    }
167}
168
169impl From<Signer> for SignerResponse {
170    fn from(signer: Signer) -> Self {
171        Self {
172            id: signer.id.clone(),
173            r#type: signer.signer_type(),
174            config: SignerConfigResponse::from(signer.config),
175        }
176    }
177}
178
179#[cfg(test)]
180mod tests {
181    use super::*;
182    use crate::models::{LocalSignerConfigStorage, SignerConfigStorage};
183    use secrets::SecretVec;
184
185    #[test]
186    fn test_signer_response_from_repo_model() {
187        let repo_model = SignerRepoModel {
188            id: "test-signer".to_string(),
189            config: SignerConfigStorage::Local(LocalSignerConfigStorage {
190                raw_key: SecretVec::new(32, |v| v.copy_from_slice(&[1; 32])),
191            }),
192        };
193
194        let response = SignerResponse::from(repo_model);
195
196        assert_eq!(response.id, "test-signer");
197        assert_eq!(response.r#type, SignerType::Local);
198        assert_eq!(response.config, SignerConfigResponse::Plain {});
199    }
200
201    #[test]
202    fn test_signer_response_from_domain_model() {
203        use crate::models::signer::{AwsKmsSignerConfig, SignerConfig};
204
205        let aws_config = AwsKmsSignerConfig {
206            key_id: "test-key-id".to_string(),
207            region: Some("us-east-1".to_string()),
208        };
209
210        let signer = crate::models::Signer::new(
211            "domain-signer".to_string(),
212            SignerConfig::AwsKms(aws_config),
213        );
214
215        let response = SignerResponse::from(signer);
216
217        assert_eq!(response.id, "domain-signer");
218        assert_eq!(response.r#type, SignerType::AwsKms);
219        assert_eq!(
220            response.config,
221            SignerConfigResponse::AwsKms {
222                region: Some("us-east-1".to_string()),
223                key_id: "test-key-id".to_string(),
224            }
225        );
226    }
227
228    #[test]
229    fn test_signer_type_mapping_from_config() {
230        let test_cases = vec![
231            (
232                SignerConfigStorage::Local(LocalSignerConfigStorage {
233                    raw_key: SecretVec::new(32, |v| v.copy_from_slice(&[1; 32])),
234                }),
235                SignerType::Local,
236                SignerConfigResponse::Plain {},
237            ),
238            (
239                SignerConfigStorage::AwsKms(crate::models::AwsKmsSignerConfigStorage {
240                    region: Some("us-east-1".to_string()),
241                    key_id: "test-key".to_string(),
242                }),
243                SignerType::AwsKms,
244                SignerConfigResponse::AwsKms {
245                    region: Some("us-east-1".to_string()),
246                    key_id: "test-key".to_string(),
247                },
248            ),
249        ];
250
251        for (config, expected_type, expected_config) in test_cases {
252            let repo_model = SignerRepoModel {
253                id: "test".to_string(),
254                config,
255            };
256
257            let response = SignerResponse::from(repo_model);
258            assert_eq!(
259                response.r#type, expected_type,
260                "Type mapping failed for {:?}",
261                expected_type
262            );
263            assert_eq!(response.config, expected_config);
264        }
265    }
266
267    #[test]
268    fn test_response_serialization() {
269        let response = SignerResponse {
270            id: "test-signer".to_string(),
271            r#type: SignerType::Local,
272            config: SignerConfigResponse::Plain {},
273        };
274
275        let json = serde_json::to_string(&response).unwrap();
276        assert!(json.contains("\"id\":\"test-signer\""));
277        assert!(json.contains("\"type\":\"local\""));
278    }
279
280    #[test]
281    fn test_response_deserialization() {
282        let json = r#"{
283            "id": "test-signer",
284            "type": "aws_kms",
285            "config": {
286                "region": "us-east-1",
287                "key_id": "test-key-id"
288            }
289        }"#;
290
291        let response: SignerResponse = serde_json::from_str(json).unwrap();
292        assert_eq!(response.id, "test-signer");
293        assert_eq!(response.r#type, SignerType::AwsKms);
294        assert_eq!(
295            response.config,
296            SignerConfigResponse::AwsKms {
297                region: Some("us-east-1".to_string()),
298                key_id: "test-key-id".to_string(),
299            }
300        );
301    }
302
303    #[test]
304    fn test_response_deserialization_all_types() {
305        let json = r#"{"id": "test", "type": "google_cloud_kms", "config": {"service_account": {"project_id": "proj", "client_id": "cid", "auth_uri": "auth", "token_uri": "token", "auth_provider_x509_cert_url": "cert", "client_x509_cert_url": "client_cert", "universe_domain": "domain"}, "key": {"location": "loc", "key_ring_id": "ring", "key_id": "key", "key_version": 1}}}"#;
306
307        let response: SignerResponse = serde_json::from_str(json).unwrap();
308        assert_eq!(response.r#type, SignerType::GoogleCloudKms);
309    }
310
311    #[test]
312    fn test_cdp_signer_response_conversion() {
313        use crate::models::signer::{CdpSignerConfig, SignerConfig};
314        use crate::models::SecretString;
315
316        let cdp_config = CdpSignerConfig {
317            api_key_id: "test-api-key-id".to_string(),
318            api_key_secret: SecretString::new("secret"),
319            wallet_secret: SecretString::new("wallet-secret"),
320            account_address: "0x742d35Cc6634C0532925a3b844Bc454e4438f44f".to_string(),
321        };
322
323        let signer =
324            crate::models::Signer::new("cdp-signer".to_string(), SignerConfig::Cdp(cdp_config));
325
326        let response = SignerResponse::from(signer);
327
328        assert_eq!(response.id, "cdp-signer");
329        assert_eq!(response.r#type, SignerType::Cdp);
330        assert_eq!(
331            response.config,
332            SignerConfigResponse::Cdp {
333                api_key_id: "test-api-key-id".to_string(),
334                account_address: "0x742d35Cc6634C0532925a3b844Bc454e4438f44f".to_string(),
335            }
336        );
337    }
338
339    #[test]
340    fn test_cdp_response_serialization() {
341        let response = SignerResponse {
342            id: "test-cdp-signer".to_string(),
343            r#type: SignerType::Cdp,
344            config: SignerConfigResponse::Cdp {
345                api_key_id: "test-api-key-id".to_string(),
346                account_address: "0x742d35Cc6634C0532925a3b844Bc454e4438f44f".to_string(),
347            },
348        };
349
350        let json = serde_json::to_string(&response).unwrap();
351        assert!(json.contains("\"id\":\"test-cdp-signer\""));
352        assert!(json.contains("\"type\":\"cdp\""));
353        assert!(json.contains("\"api_key_id\":\"test-api-key-id\""));
354        assert!(json.contains("\"account_address\":\"0x742d35Cc6634C0532925a3b844Bc454e4438f44f\""));
355
356        // Verify that secrets are not included
357        assert!(!json.contains("api_key_secret"));
358        assert!(!json.contains("wallet_secret"));
359    }
360
361    #[test]
362    fn test_cdp_response_deserialization() {
363        let json = r#"{
364            "id": "test-cdp-signer",
365            "type": "cdp",
366            "config": {
367                "api_key_id": "test-api-key-id",
368                "account_address": "0x742d35Cc6634C0532925a3b844Bc454e4438f44f"
369            }
370        }"#;
371
372        let response: SignerResponse = serde_json::from_str(json).unwrap();
373        assert_eq!(response.id, "test-cdp-signer");
374        assert_eq!(response.r#type, SignerType::Cdp);
375        assert_eq!(
376            response.config,
377            SignerConfigResponse::Cdp {
378                api_key_id: "test-api-key-id".to_string(),
379                account_address: "0x742d35Cc6634C0532925a3b844Bc454e4438f44f".to_string(),
380            }
381        );
382    }
383}