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