1use base64::engine::general_purpose::URL_SAFE_NO_PAD;
10use base64::Engine;
11use ed25519_dalek::{Signature, Verifier, VerifyingKey};
12use eyre::{bail, ensure, Context, Result};
13use sha2::{Digest, Sha256};
14
15const ED25519_PUB_MULTICODEC: [u8; 2] = [0xed, 0x01];
17
18#[derive(Debug, Clone)]
20pub struct ManifestVerification {
21 pub signer_id: String,
23 pub bundle_hash: [u8; 32],
25}
26
27pub fn derive_signer_id_did_key(pubkey: &[u8; 32]) -> String {
41 let mut multicodec_key = Vec::with_capacity(2 + 32);
43 multicodec_key.extend_from_slice(&ED25519_PUB_MULTICODEC);
44 multicodec_key.extend_from_slice(pubkey);
45
46 let encoded = bs58::encode(&multicodec_key).into_string();
48
49 format!("did:key:z{}", encoded)
50}
51
52pub fn canonicalize_manifest(manifest_json: &serde_json::Value) -> Result<Vec<u8>> {
63 let mut signing_view = manifest_json.clone();
65 if let Some(obj) = signing_view.as_object_mut() {
66 obj.remove("signature");
67 obj.retain(|k, _| !k.starts_with('_'));
76 }
77
78 let canonical_bytes = serde_json_canonicalizer::to_vec(&signing_view)
80 .context("failed to canonicalize manifest JSON")?;
81
82 Ok(canonical_bytes)
83}
84
85pub fn compute_bundle_hash(canonical_bytes: &[u8]) -> [u8; 32] {
95 let mut hasher = Sha256::new();
96 hasher.update(canonical_bytes);
97 hasher.finalize().into()
98}
99
100pub fn compute_signing_payload(canonical_bytes: &[u8]) -> [u8; 32] {
111 compute_bundle_hash(canonical_bytes)
113}
114
115pub fn decode_public_key(encoded: &str) -> Result<[u8; 32]> {
123 let bytes = URL_SAFE_NO_PAD
124 .decode(encoded)
125 .context("invalid base64url encoding for public key")?;
126
127 ensure!(
128 bytes.len() == 32,
129 "invalid public key length: expected 32 bytes, got {}",
130 bytes.len()
131 );
132
133 let mut key = [0u8; 32];
134 key.copy_from_slice(&bytes);
135 Ok(key)
136}
137
138pub fn decode_signature(encoded: &str) -> Result<[u8; 64]> {
146 let bytes = URL_SAFE_NO_PAD
147 .decode(encoded)
148 .context("invalid base64url encoding for signature")?;
149
150 ensure!(
151 bytes.len() == 64,
152 "invalid signature length: expected 64 bytes, got {}",
153 bytes.len()
154 );
155
156 let mut sig = [0u8; 64];
157 sig.copy_from_slice(&bytes);
158 Ok(sig)
159}
160
161pub fn verify_ed25519(
171 signature_bytes: &[u8; 64],
172 public_key_bytes: &[u8; 32],
173 message: &[u8],
174) -> Result<()> {
175 let verifying_key =
176 VerifyingKey::from_bytes(public_key_bytes).context("invalid Ed25519 public key")?;
177
178 let signature = Signature::from_bytes(signature_bytes);
179
180 verifying_key
181 .verify(message, &signature)
182 .context("Ed25519 signature verification failed")?;
183
184 Ok(())
185}
186
187pub fn verify_manifest_signature(
204 manifest_json: &serde_json::Value,
205) -> Result<ManifestVerification> {
206 let signature_obj = manifest_json
208 .get("signature")
209 .ok_or_else(|| eyre::eyre!("manifest is missing required 'signature' field"))?;
210
211 let algorithm = signature_obj
213 .get("algorithm")
214 .and_then(|v| v.as_str())
215 .ok_or_else(|| eyre::eyre!("signature missing 'algorithm' field"))?;
216
217 ensure!(
221 algorithm == "ed25519",
222 "unsupported signature algorithm: '{}', expected 'ed25519'",
223 algorithm
224 );
225
226 let public_key_b64 = signature_obj
227 .get("publicKey")
228 .and_then(|v| v.as_str())
229 .ok_or_else(|| eyre::eyre!("signature missing 'publicKey' field"))?;
230
231 let signature_b64 = signature_obj
232 .get("signature")
233 .and_then(|v| v.as_str())
234 .ok_or_else(|| eyre::eyre!("signature missing 'signature' field"))?;
235
236 let public_key_bytes = decode_public_key(public_key_b64)?;
238 let signature_bytes = decode_signature(signature_b64)?;
239
240 let canonical_bytes = canonicalize_manifest(manifest_json)?;
242
243 let signing_payload = compute_signing_payload(&canonical_bytes);
245
246 verify_ed25519(&signature_bytes, &public_key_bytes, &signing_payload)?;
248
249 let derived_signer_id = derive_signer_id_did_key(&public_key_bytes);
251
252 let manifest_signer_id = manifest_json
255 .get("signerId")
256 .and_then(|v| v.as_str())
257 .ok_or_else(|| eyre::eyre!("bundle manifest is missing required signerId field"))?;
258
259 if manifest_signer_id != derived_signer_id {
260 bail!(
261 "signerId mismatch: manifest declares '{}' but signature public key derives to '{}'",
262 manifest_signer_id,
263 derived_signer_id
264 );
265 }
266
267 let bundle_hash = signing_payload;
269
270 Ok(ManifestVerification {
271 signer_id: derived_signer_id,
272 bundle_hash,
273 })
274}
275
276pub fn format_bundle_hash(hash: &[u8; 32]) -> String {
278 hex::encode(hash)
279}
280
281pub fn sign_manifest_json(
297 manifest_json: &mut serde_json::Value,
298 signing_key: &ed25519_dalek::SigningKey,
299) -> Result<String> {
300 use ed25519_dalek::Signer;
301
302 let verifying_key = signing_key.verifying_key();
303 let signer_id = derive_signer_id_did_key(verifying_key.as_bytes());
304
305 if let Some(obj) = manifest_json.as_object_mut() {
308 obj.insert(
309 "signerId".to_string(),
310 serde_json::Value::String(signer_id.clone()),
311 );
312 }
313
314 let canonical_bytes = canonicalize_manifest(manifest_json)?;
316
317 let signing_payload = compute_signing_payload(&canonical_bytes);
319
320 let signature = signing_key.sign(&signing_payload);
322
323 let public_key_b64 = URL_SAFE_NO_PAD.encode(verifying_key.as_bytes());
325 let signature_b64 = URL_SAFE_NO_PAD.encode(signature.to_bytes());
326
327 if let Some(obj) = manifest_json.as_object_mut() {
329 obj.insert(
330 "signature".to_string(),
331 serde_json::json!({
332 "algorithm": "ed25519",
333 "publicKey": public_key_b64,
334 "signature": signature_b64
335 }),
336 );
337 }
338
339 Ok(signer_id)
340}
341
342#[cfg(test)]
343mod tests {
344 use super::*;
345 use ed25519_dalek::{Signer, SigningKey};
346
347 fn create_test_manifest(
349 package: &str,
350 version: &str,
351 signer_id: Option<&str>,
352 ) -> serde_json::Value {
353 let mut manifest = serde_json::json!({
354 "version": "1.0",
355 "package": package,
356 "appVersion": version,
357 "minRuntimeVersion": "1.0.0",
358 "resources": [
359 {
360 "role": "executable",
361 "path": "app.wasm",
362 "hash": "a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2",
363 "size": 1024
364 }
365 ]
366 });
367
368 if let Some(sid) = signer_id {
369 manifest["signerId"] = serde_json::Value::String(sid.to_string());
370 }
371
372 manifest
373 }
374
375 fn sign_manifest(manifest: &mut serde_json::Value, signing_key: &SigningKey) -> Result<()> {
377 let canonical_bytes = canonicalize_manifest(manifest)?;
379
380 let signing_payload = compute_signing_payload(&canonical_bytes);
382
383 let signature = signing_key.sign(&signing_payload);
385
386 let public_key_b64 = URL_SAFE_NO_PAD.encode(signing_key.verifying_key().as_bytes());
388 let signature_b64 = URL_SAFE_NO_PAD.encode(signature.to_bytes());
389
390 manifest["signature"] = serde_json::json!({
392 "algorithm": "ed25519",
393 "publicKey": public_key_b64,
394 "signature": signature_b64
395 });
396
397 Ok(())
398 }
399
400 #[test]
401 fn test_derive_signer_id_did_key() {
402 let pubkey: [u8; 32] = [
404 0x3b, 0x6a, 0x27, 0xbc, 0xce, 0xb6, 0xa4, 0x2d, 0x62, 0xa3, 0xa8, 0xd0, 0x2a, 0x6f,
405 0x0d, 0x73, 0x65, 0x32, 0x15, 0x77, 0x1d, 0xe2, 0x43, 0xa6, 0x3a, 0xc0, 0x48, 0xa1,
406 0x8b, 0x59, 0xda, 0x29,
407 ];
408
409 let signer_id = derive_signer_id_did_key(&pubkey);
410
411 assert!(signer_id.starts_with("did:key:z"));
413 assert!(signer_id.len() > 10);
414
415 let signer_id_2 = derive_signer_id_did_key(&pubkey);
417 assert_eq!(signer_id, signer_id_2);
418 }
419
420 #[test]
421 fn test_canonicalize_manifest_removes_signature() {
422 let manifest = serde_json::json!({
423 "version": "1.0",
424 "package": "com.example.app",
425 "signature": {
426 "algorithm": "ed25519",
427 "publicKey": "test",
428 "signature": "test"
429 }
430 });
431
432 let canonical = canonicalize_manifest(&manifest).unwrap();
433 let canonical_str = String::from_utf8(canonical).unwrap();
434
435 assert!(!canonical_str.contains("signature"));
437 assert!(canonical_str.contains("package"));
439 assert!(canonical_str.contains("version"));
440 }
441
442 #[test]
443 fn test_canonicalize_manifest_removes_transient_fields() {
444 let manifest = serde_json::json!({
445 "version": "1.0",
446 "package": "com.example.app",
447 "_binary": "some_value",
448 "_overwrite": true
449 });
450
451 let canonical = canonicalize_manifest(&manifest).unwrap();
452 let canonical_str = String::from_utf8(canonical).unwrap();
453
454 assert!(!canonical_str.contains("_binary"));
456 assert!(!canonical_str.contains("_overwrite"));
457 }
458
459 #[test]
460 fn test_canonicalize_manifest_key_ordering() {
461 let manifest = serde_json::json!({
463 "z_field": "last",
464 "a_field": "first",
465 "m_field": "middle"
466 });
467
468 let canonical = canonicalize_manifest(&manifest).unwrap();
469 let canonical_str = String::from_utf8(canonical).unwrap();
470
471 let a_pos = canonical_str.find("a_field").unwrap();
473 let m_pos = canonical_str.find("m_field").unwrap();
474 let z_pos = canonical_str.find("z_field").unwrap();
475
476 assert!(a_pos < m_pos);
477 assert!(m_pos < z_pos);
478 }
479
480 #[test]
481 fn test_compute_bundle_hash() {
482 let canonical_bytes = b"test manifest content";
483 let hash = compute_bundle_hash(canonical_bytes);
484
485 assert_eq!(hash.len(), 32);
487
488 let hash_2 = compute_bundle_hash(canonical_bytes);
490 assert_eq!(hash, hash_2);
491
492 let different_bytes = b"different content";
494 let different_hash = compute_bundle_hash(different_bytes);
495 assert_ne!(hash, different_hash);
496 }
497
498 #[test]
499 fn test_decode_public_key_valid() {
500 let signing_key = SigningKey::generate(&mut rand::thread_rng());
502 let public_key = signing_key.verifying_key();
503
504 let encoded = URL_SAFE_NO_PAD.encode(public_key.as_bytes());
506 let decoded = decode_public_key(&encoded).unwrap();
507
508 assert_eq!(decoded, *public_key.as_bytes());
509 }
510
511 #[test]
512 fn test_decode_public_key_invalid_length() {
513 let short_key = URL_SAFE_NO_PAD.encode(&[0u8; 16]);
514 let result = decode_public_key(&short_key);
515 assert!(result.is_err());
516 assert!(result
517 .unwrap_err()
518 .to_string()
519 .contains("invalid public key length"));
520 }
521
522 #[test]
523 fn test_decode_signature_valid() {
524 let sig_bytes = [0u8; 64];
525 let encoded = URL_SAFE_NO_PAD.encode(&sig_bytes);
526 let decoded = decode_signature(&encoded).unwrap();
527 assert_eq!(decoded, sig_bytes);
528 }
529
530 #[test]
531 fn test_decode_signature_invalid_length() {
532 let short_sig = URL_SAFE_NO_PAD.encode(&[0u8; 32]);
533 let result = decode_signature(&short_sig);
534 assert!(result.is_err());
535 assert!(result
536 .unwrap_err()
537 .to_string()
538 .contains("invalid signature length"));
539 }
540
541 #[test]
542 fn test_verify_ed25519_valid_signature() {
543 let signing_key = SigningKey::generate(&mut rand::thread_rng());
544 let message = b"test message";
545
546 let signature = signing_key.sign(message);
547
548 let result = verify_ed25519(
549 &signature.to_bytes(),
550 signing_key.verifying_key().as_bytes(),
551 message,
552 );
553
554 assert!(result.is_ok());
555 }
556
557 #[test]
558 fn test_verify_ed25519_invalid_signature() {
559 let signing_key = SigningKey::generate(&mut rand::thread_rng());
560 let message = b"test message";
561 let wrong_message = b"wrong message";
562
563 let signature = signing_key.sign(message);
565
566 let result = verify_ed25519(
567 &signature.to_bytes(),
568 signing_key.verifying_key().as_bytes(),
569 wrong_message,
570 );
571
572 assert!(result.is_err());
573 }
574
575 #[test]
576 fn test_verify_manifest_signature_valid() {
577 let signing_key = SigningKey::generate(&mut rand::thread_rng());
578 let signer_id = derive_signer_id_did_key(signing_key.verifying_key().as_bytes());
579
580 let mut manifest = create_test_manifest("com.example.app", "1.0.0", Some(&signer_id));
581
582 sign_manifest(&mut manifest, &signing_key).unwrap();
583
584 let result = verify_manifest_signature(&manifest);
585 assert!(result.is_ok());
586
587 let verification = result.unwrap();
588 assert_eq!(verification.signer_id, signer_id);
589 }
590
591 #[test]
592 fn test_verify_manifest_signature_missing_signature() {
593 let manifest = create_test_manifest("com.example.app", "1.0.0", None);
594
595 let result = verify_manifest_signature(&manifest);
596 assert!(result.is_err());
597 assert!(result
598 .unwrap_err()
599 .to_string()
600 .contains("missing required 'signature' field"));
601 }
602
603 #[test]
604 fn test_verify_manifest_signature_wrong_algorithm() {
605 let signing_key = SigningKey::generate(&mut rand::thread_rng());
606 let signer_id = derive_signer_id_did_key(signing_key.verifying_key().as_bytes());
607 let mut manifest = create_test_manifest("com.example.app", "1.0.0", Some(&signer_id));
608 manifest["signature"] = serde_json::json!({
609 "algorithm": "rsa",
610 "publicKey": "test",
611 "signature": "test"
612 });
613
614 let result = verify_manifest_signature(&manifest);
615 assert!(result.is_err());
616 assert!(result
617 .unwrap_err()
618 .to_string()
619 .contains("unsupported signature algorithm"));
620 }
621
622 #[test]
623 fn test_verify_manifest_signature_missing_signer_id() {
624 let signing_key = SigningKey::generate(&mut rand::thread_rng());
625 let mut manifest = create_test_manifest("com.example.app", "1.0.0", None);
627 sign_manifest(&mut manifest, &signing_key).unwrap();
628
629 let result = verify_manifest_signature(&manifest);
630 assert!(result.is_err());
631 assert!(result
632 .unwrap_err()
633 .to_string()
634 .contains("missing required signerId field"));
635 }
636
637 #[test]
638 fn test_verify_manifest_signature_signer_id_mismatch() {
639 let signing_key = SigningKey::generate(&mut rand::thread_rng());
640
641 let mut manifest = create_test_manifest(
643 "com.example.app",
644 "1.0.0",
645 Some("did:key:z6MkWRONGKEY123456789"),
646 );
647
648 sign_manifest(&mut manifest, &signing_key).unwrap();
649
650 let result = verify_manifest_signature(&manifest);
651 assert!(result.is_err());
652 assert!(result
653 .unwrap_err()
654 .to_string()
655 .contains("signerId mismatch"));
656 }
657
658 #[test]
659 fn test_verify_manifest_signature_invalid_signature() {
660 let signing_key = SigningKey::generate(&mut rand::thread_rng());
661 let signer_id = derive_signer_id_did_key(signing_key.verifying_key().as_bytes());
662
663 let mut manifest = create_test_manifest("com.example.app", "1.0.0", Some(&signer_id));
664
665 sign_manifest(&mut manifest, &signing_key).unwrap();
667
668 manifest["appVersion"] = serde_json::Value::String("2.0.0".to_string());
670
671 let result = verify_manifest_signature(&manifest);
672 assert!(result.is_err());
673 assert!(result
674 .unwrap_err()
675 .to_string()
676 .contains("signature verification failed"));
677 }
678
679 #[test]
680 fn test_signer_id_derivation_is_stable() {
681 let signing_key = SigningKey::generate(&mut rand::thread_rng());
683 let verifying_key = signing_key.verifying_key();
684 let pubkey = verifying_key.as_bytes();
685
686 let signer_id_1 = derive_signer_id_did_key(pubkey);
687 let signer_id_2 = derive_signer_id_did_key(pubkey);
688 let signer_id_3 = derive_signer_id_did_key(pubkey);
689
690 assert_eq!(signer_id_1, signer_id_2);
691 assert_eq!(signer_id_2, signer_id_3);
692 }
693
694 #[test]
695 fn test_bundle_hash_is_deterministic() {
696 let signing_key = SigningKey::generate(&mut rand::thread_rng());
697 let signer_id = derive_signer_id_did_key(signing_key.verifying_key().as_bytes());
698
699 let manifest = create_test_manifest("com.example.app", "1.0.0", Some(&signer_id));
700
701 let canonical_1 = canonicalize_manifest(&manifest).unwrap();
703 let hash_1 = compute_bundle_hash(&canonical_1);
704
705 let canonical_2 = canonicalize_manifest(&manifest).unwrap();
706 let hash_2 = compute_bundle_hash(&canonical_2);
707
708 assert_eq!(hash_1, hash_2);
709 assert_eq!(canonical_1, canonical_2);
710 }
711
712 #[test]
713 fn test_format_bundle_hash() {
714 let hash: [u8; 32] = [
715 0xa1, 0xb2, 0xc3, 0xd4, 0xe5, 0xf6, 0xa7, 0xb8, 0xc9, 0xd0, 0xe1, 0xf2, 0xa3, 0xb4,
716 0xc5, 0xd6, 0xe7, 0xf8, 0xa9, 0xb0, 0xc1, 0xd2, 0xe3, 0xf4, 0xa5, 0xb6, 0xc7, 0xd8,
717 0xe9, 0xf0, 0xa1, 0xb2,
718 ];
719
720 let formatted = format_bundle_hash(&hash);
721
722 assert_eq!(formatted.len(), 64);
724 assert!(formatted.chars().all(|c| c.is_ascii_hexdigit()));
725 assert!(formatted.chars().all(|c| !c.is_uppercase()));
726 }
727}