symbi_runtime/integrations/schemapin/
native_client.rs1use async_trait::async_trait;
6use chrono::Utc;
7use schemapin::crypto::{calculate_key_id, generate_key_pair, sign_data, verify_signature};
8use sha2::Digest;
9
10use super::types::{
11 SchemaPinError, SignArgs, SignatureInfo, SigningResult, VerificationResult, VerifyArgs,
12};
13
14#[async_trait]
16pub trait SchemaPinClient: Send + Sync {
17 async fn verify_schema(&self, args: VerifyArgs) -> Result<VerificationResult, SchemaPinError>;
19
20 async fn sign_schema(&self, args: SignArgs) -> Result<SigningResult, SchemaPinError>;
22
23 async fn check_available(&self) -> Result<bool, SchemaPinError>;
25
26 async fn get_version(&self) -> Result<String, SchemaPinError>;
28}
29
30pub struct NativeSchemaPinClient {
32 }
34
35impl NativeSchemaPinClient {
36 pub fn new() -> Self {
38 Self {}
39 }
40
41 async fn fetch_public_key(&self, public_key_url: &str) -> Result<String, SchemaPinError> {
48 let response = reqwest::get(public_key_url)
49 .await
50 .map_err(|e| SchemaPinError::IoError {
51 reason: format!("Failed to fetch public key from {}: {}", public_key_url, e),
52 })?;
53
54 if !response.status().is_success() {
55 return Err(SchemaPinError::IoError {
56 reason: format!("HTTP error {} when fetching public key", response.status()),
57 });
58 }
59
60 let body = response.text().await.map_err(|e| SchemaPinError::IoError {
61 reason: format!("Failed to read public key response: {}", e),
62 })?;
63
64 let trimmed = body.trim();
66 if trimmed.starts_with('{') {
67 if let Ok(json) = serde_json::from_str::<serde_json::Value>(trimmed) {
68 if let Some(pem) = json.get("public_key_pem").and_then(|v| v.as_str()) {
69 return Ok(pem.to_string());
70 }
71 return Err(SchemaPinError::IoError {
72 reason: format!(
73 "JSON response from {} does not contain a 'public_key_pem' field",
74 public_key_url
75 ),
76 });
77 }
78 }
79
80 Ok(body)
81 }
82
83 async fn read_file(&self, path: &str) -> Result<Vec<u8>, SchemaPinError> {
85 tokio::fs::read(path)
86 .await
87 .map_err(|_e| SchemaPinError::SchemaFileNotFound {
88 path: path.to_string(),
89 })
90 }
91}
92
93impl Default for NativeSchemaPinClient {
94 fn default() -> Self {
95 Self::new()
96 }
97}
98
99#[async_trait]
100impl SchemaPinClient for NativeSchemaPinClient {
101 async fn verify_schema(&self, args: VerifyArgs) -> Result<VerificationResult, SchemaPinError> {
102 if args.schema_path.is_empty() {
104 return Err(SchemaPinError::InvalidArguments {
105 args: vec!["schema_path cannot be empty".to_string()],
106 });
107 }
108
109 if args.public_key_url.is_empty() {
110 return Err(SchemaPinError::InvalidArguments {
111 args: vec!["public_key_url cannot be empty".to_string()],
112 });
113 }
114
115 if !args.public_key_url.starts_with("http://")
117 && !args.public_key_url.starts_with("https://")
118 {
119 return Err(SchemaPinError::InvalidPublicKeyUrl {
120 url: args.public_key_url.clone(),
121 });
122 }
123
124 let schema_data = self.read_file(&args.schema_path).await?;
126
127 let public_key_pem = self.fetch_public_key(&args.public_key_url).await?;
129
130 let key_id = calculate_key_id(&public_key_pem).map_err(|e| SchemaPinError::IoError {
132 reason: format!("Failed to calculate key ID: {}", e),
133 })?;
134
135 let schema_hash = {
137 let mut hasher = sha2::Sha256::new();
138 hasher.update(&schema_data);
139 hex::encode(hasher.finalize())
140 };
141
142 let embedded_signature: Option<String> =
145 serde_json::from_slice::<serde_json::Value>(&schema_data)
146 .ok()
147 .and_then(|v| {
148 v.get("signature")
149 .and_then(|s| s.as_str())
150 .map(String::from)
151 });
152
153 if let Some(ref sig) = embedded_signature {
154 let mut schema_value: serde_json::Value = serde_json::from_slice(&schema_data)
157 .map_err(|e| SchemaPinError::IoError {
158 reason: format!("Failed to parse schema JSON: {}", e),
159 })?;
160 if let Some(obj) = schema_value.as_object_mut() {
161 obj.remove("signature");
162 }
163 let canonical_payload =
164 serde_json::to_vec(&schema_value).map_err(|e| SchemaPinError::IoError {
165 reason: format!("Failed to serialize canonical schema: {}", e),
166 })?;
167
168 match verify_signature(&public_key_pem, &canonical_payload, sig) {
169 Ok(true) => {
170 tracing::info!(
171 "Schema signature verified successfully for {}",
172 args.schema_path
173 );
174 Ok(VerificationResult {
175 success: true,
176 message: "Schema signature verified successfully using native Rust implementation".to_string(),
177 schema_hash: Some(schema_hash),
178 public_key_url: Some(args.public_key_url.clone()),
179 signature: Some(SignatureInfo {
180 algorithm: "ECDSA_P256".to_string(),
181 signature: sig.clone(),
182 key_fingerprint: Some(key_id),
183 valid: true,
184 }),
185 metadata: None,
186 timestamp: Some(Utc::now().to_rfc3339()),
187 })
188 }
189 Ok(false) => {
190 tracing::warn!(
191 "Schema signature verification failed: signature invalid for {}",
192 args.schema_path
193 );
194 Ok(VerificationResult {
195 success: false,
196 message: "Schema signature verification failed: signature is invalid"
197 .to_string(),
198 schema_hash: Some(schema_hash),
199 public_key_url: Some(args.public_key_url.clone()),
200 signature: Some(SignatureInfo {
201 algorithm: "ECDSA_P256".to_string(),
202 signature: sig.clone(),
203 key_fingerprint: Some(key_id),
204 valid: false,
205 }),
206 metadata: None,
207 timestamp: Some(Utc::now().to_rfc3339()),
208 })
209 }
210 Err(e) => {
211 tracing::warn!(
212 "Schema signature verification error for {}: {}",
213 args.schema_path,
214 e
215 );
216 Ok(VerificationResult {
217 success: false,
218 message: format!("Schema signature verification error: {}", e),
219 schema_hash: Some(schema_hash),
220 public_key_url: Some(args.public_key_url.clone()),
221 signature: Some(SignatureInfo {
222 algorithm: "ECDSA_P256".to_string(),
223 signature: sig.clone(),
224 key_fingerprint: Some(key_id),
225 valid: false,
226 }),
227 metadata: None,
228 timestamp: Some(Utc::now().to_rfc3339()),
229 })
230 }
231 }
232 } else {
233 tracing::warn!(
235 "Schema verification failed for {}: no signature provided for verification",
236 args.schema_path
237 );
238 Ok(VerificationResult {
239 success: false,
240 message: "No signature provided for verification".to_string(),
241 schema_hash: Some(schema_hash),
242 public_key_url: Some(args.public_key_url.clone()),
243 signature: None,
244 metadata: None,
245 timestamp: Some(Utc::now().to_rfc3339()),
246 })
247 }
248 }
249
250 async fn sign_schema(&self, args: SignArgs) -> Result<SigningResult, SchemaPinError> {
251 if args.schema_path.is_empty() {
253 return Err(SchemaPinError::InvalidArguments {
254 args: vec!["schema_path cannot be empty".to_string()],
255 });
256 }
257
258 if args.private_key_path.is_empty() {
259 return Err(SchemaPinError::InvalidArguments {
260 args: vec!["private_key_path cannot be empty".to_string()],
261 });
262 }
263
264 let schema_data = self.read_file(&args.schema_path).await?;
266
267 let private_key_pem = tokio::fs::read_to_string(&args.private_key_path)
269 .await
270 .map_err(|_| SchemaPinError::PrivateKeyNotFound {
271 path: args.private_key_path.clone(),
272 })?;
273
274 let signature = sign_data(&private_key_pem, &schema_data).map_err(|e| {
276 SchemaPinError::SigningFailed {
277 reason: format!("Failed to sign schema: {}", e),
278 }
279 })?;
280
281 let mut hasher = sha2::Sha256::new();
283 hasher.update(&schema_data);
284 let schema_hash = hex::encode(hasher.finalize());
285
286 let key_pair = generate_key_pair().map_err(|e| SchemaPinError::SigningFailed {
289 reason: format!("Failed to generate key pair for ID calculation: {}", e),
290 })?;
291
292 let key_id = calculate_key_id(&key_pair.public_key_pem).map_err(|e| {
293 SchemaPinError::SigningFailed {
294 reason: format!("Failed to calculate key ID: {}", e),
295 }
296 })?;
297
298 let output_path = args
300 .output_path
301 .unwrap_or_else(|| format!("{}.signed", args.schema_path));
302
303 Ok(SigningResult {
304 success: true,
305 message: "Schema signed successfully using native Rust implementation".to_string(),
306 schema_hash: Some(schema_hash),
307 signed_schema_path: Some(output_path),
308 signature: Some(SignatureInfo {
309 algorithm: "ECDSA_P256".to_string(),
310 signature,
311 key_fingerprint: Some(key_id),
312 valid: true,
313 }),
314 metadata: None,
315 timestamp: Some(Utc::now().to_rfc3339()),
316 })
317 }
318
319 async fn check_available(&self) -> Result<bool, SchemaPinError> {
320 Ok(true)
322 }
323
324 async fn get_version(&self) -> Result<String, SchemaPinError> {
325 Ok("schemapin-native v1.1.4 (Rust implementation)".to_string())
326 }
327}
328
329pub struct MockNativeSchemaPinClient {
331 should_succeed: bool,
332 mock_result: Option<VerificationResult>,
333}
334
335impl MockNativeSchemaPinClient {
336 pub fn new_success() -> Self {
338 Self {
339 should_succeed: true,
340 mock_result: None,
341 }
342 }
343
344 pub fn new_failure() -> Self {
346 Self {
347 should_succeed: false,
348 mock_result: None,
349 }
350 }
351
352 pub fn with_result(result: VerificationResult) -> Self {
354 Self {
355 should_succeed: result.success,
356 mock_result: Some(result),
357 }
358 }
359}
360
361#[async_trait]
362impl SchemaPinClient for MockNativeSchemaPinClient {
363 async fn verify_schema(&self, _args: VerifyArgs) -> Result<VerificationResult, SchemaPinError> {
364 if let Some(ref result) = self.mock_result {
365 if result.success {
366 Ok(result.clone())
367 } else {
368 Err(SchemaPinError::VerificationFailed {
369 reason: result.message.clone(),
370 })
371 }
372 } else if self.should_succeed {
373 Ok(VerificationResult {
374 success: true,
375 message: "Mock verification successful".to_string(),
376 schema_hash: Some("mock_native_hash_123".to_string()),
377 public_key_url: Some("https://mock.example.com/pubkey".to_string()),
378 signature: Some(SignatureInfo {
379 algorithm: "ECDSA_P256".to_string(),
380 signature: "mock_native_signature".to_string(),
381 key_fingerprint: Some("sha256:mock_fingerprint".to_string()),
382 valid: true,
383 }),
384 metadata: None,
385 timestamp: Some(Utc::now().to_rfc3339()),
386 })
387 } else {
388 Err(SchemaPinError::VerificationFailed {
389 reason: "Mock verification failed".to_string(),
390 })
391 }
392 }
393
394 async fn sign_schema(&self, _args: SignArgs) -> Result<SigningResult, SchemaPinError> {
395 if self.should_succeed {
396 Ok(SigningResult {
397 success: true,
398 message: "Mock native signing successful".to_string(),
399 schema_hash: Some("mock_native_signed_hash_456".to_string()),
400 signed_schema_path: Some("/mock/path/signed_schema.json".to_string()),
401 signature: Some(SignatureInfo {
402 algorithm: "ECDSA_P256".to_string(),
403 signature: "mock_native_signature_data".to_string(),
404 key_fingerprint: Some("sha256:mock_native_fingerprint".to_string()),
405 valid: true,
406 }),
407 metadata: None,
408 timestamp: Some(Utc::now().to_rfc3339()),
409 })
410 } else {
411 Err(SchemaPinError::SigningFailed {
412 reason: "Mock native signing failed".to_string(),
413 })
414 }
415 }
416
417 async fn check_available(&self) -> Result<bool, SchemaPinError> {
418 Ok(true) }
420
421 async fn get_version(&self) -> Result<String, SchemaPinError> {
422 Ok("schemapin-cli v1.0.0 (mock)".to_string())
423 }
424}
425
426#[cfg(test)]
427mod tests {
428 use super::*;
429
430 #[tokio::test]
431 async fn test_native_client_creation() {
432 let client = NativeSchemaPinClient::new();
433 let available = client.check_available().await.unwrap();
434 assert!(available);
435
436 let version = client.get_version().await.unwrap();
437 assert!(version.contains("schemapin-native"));
438 }
439
440 #[tokio::test]
441 async fn test_mock_native_client_success() {
442 let client = MockNativeSchemaPinClient::new_success();
443 let args = VerifyArgs::new(
444 "/path/to/schema.json".to_string(),
445 "https://example.com/pubkey".to_string(),
446 );
447
448 let result = client.verify_schema(args).await.unwrap();
449 assert!(result.success);
450 assert_eq!(result.message, "Mock verification successful");
451 }
452
453 #[tokio::test]
454 async fn test_mock_native_client_failure() {
455 let client = MockNativeSchemaPinClient::new_failure();
456 let args = VerifyArgs::new(
457 "/path/to/schema.json".to_string(),
458 "https://example.com/pubkey".to_string(),
459 );
460
461 let result = client.verify_schema(args).await;
462 assert!(result.is_err());
463
464 if let Err(SchemaPinError::VerificationFailed { reason }) = result {
465 assert_eq!(reason, "Mock verification failed");
466 } else {
467 panic!("Expected VerificationFailed error");
468 }
469 }
470}