1use crate::effects::EffectRow;
13use hmac::{Hmac, Mac};
14use sha2::{Digest, Sha256};
15
16type HmacSha256 = Hmac<Sha256>;
17
18pub const ED25519_SIG_PREFIX: &str = "ed25519:";
20
21#[must_use]
26pub fn sign_ed25519(payload: &str, signing_key: &ed25519_dalek::SigningKey) -> String {
27 use ed25519_dalek::Signer;
28 let sig = signing_key.sign(payload.as_bytes());
29 format!("{ED25519_SIG_PREFIX}{}", encode_hex(&sig.to_bytes()))
30}
31
32#[must_use]
34pub fn verify_ed25519(
35 payload: &str,
36 signature: &str,
37 verifying_key: &ed25519_dalek::VerifyingKey,
38) -> bool {
39 use ed25519_dalek::Verifier;
40 let Some(hex) = signature.strip_prefix(ED25519_SIG_PREFIX) else {
41 return false;
42 };
43 let Some(bytes) = decode_hex(hex) else {
44 return false;
45 };
46 let Ok(bytes): Result<[u8; 64], _> = bytes.try_into() else {
47 return false;
48 };
49 let sig = ed25519_dalek::Signature::from_bytes(&bytes);
50 verifying_key.verify(payload.as_bytes(), &sig).is_ok()
51}
52
53fn encode_hex(bytes: &[u8]) -> String {
54 use std::fmt::Write;
55 bytes.iter().fold(String::new(), |mut acc, b| {
56 let _ = write!(acc, "{b:02x}");
57 acc
58 })
59}
60
61fn decode_hex(hex: &str) -> Option<Vec<u8>> {
62 if hex.len() % 2 != 0 {
63 return None;
64 }
65 let bytes = hex.as_bytes();
66 let mut out = Vec::with_capacity(hex.len() / 2);
67 for chunk in bytes.chunks_exact(2) {
68 let hi = hex_val(chunk[0])?;
69 let lo = hex_val(chunk[1])?;
70 out.push((hi << 4) | lo);
71 }
72 Some(out)
73}
74
75const fn hex_val(b: u8) -> Option<u8> {
76 match b {
77 b'0'..=b'9' => Some(b - b'0'),
78 b'a'..=b'f' => Some(b - b'a' + 10),
79 b'A'..=b'F' => Some(b - b'A' + 10),
80 _ => None,
81 }
82}
83
84#[must_use]
90pub fn sign_hmac(payload: &str, key: &[u8]) -> Option<String> {
91 let Ok(mut mac) = HmacSha256::new_from_slice(key) else {
92 return None;
93 };
94 mac.update(payload.as_bytes());
95 Some(format!("{:x}", mac.finalize().into_bytes()))
96}
97
98#[must_use]
102pub fn verify_hmac(payload: &str, signature: &str, key: &[u8]) -> bool {
103 if signature.is_empty() {
104 return false;
105 }
106 let Some(expected) = decode_hex(signature) else {
107 return false;
108 };
109 let Ok(mut mac) = HmacSha256::new_from_slice(key) else {
110 return false;
111 };
112 mac.update(payload.as_bytes());
113 mac.verify_slice(&expected).is_ok()
115}
116
117#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
119pub struct ToolManifest {
120 pub tool_name: String,
122 pub version: String,
124 pub publisher: String,
126 pub description: String,
128 #[serde(default)]
130 pub effects: EffectSummary,
131 pub capabilities: Vec<String>,
133 pub trust_level: f32,
135 pub requires_human_review: bool,
137 pub created_at: i64,
139 #[serde(default)]
142 pub signature: String,
143}
144
145#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)]
147pub struct EffectSummary {
148 #[serde(default)]
150 pub reads: Vec<String>,
151 #[serde(default)]
153 pub writes: Vec<String>,
154 #[serde(default)]
156 pub spawns: bool,
157}
158
159impl EffectSummary {
160 #[must_use]
162 pub fn from_effect_row(effects: &EffectRow) -> Self {
163 Self {
164 reads: effects.reads.iter().map(|r| format!("{r:?}")).collect(),
165 writes: effects.writes.iter().map(|r| format!("{r:?}")).collect(),
166 spawns: effects.spawns,
167 }
168 }
169
170 #[must_use]
172 pub fn has_destructive_effects(&self) -> bool {
173 !self.writes.is_empty() || self.spawns
174 }
175}
176
177impl ToolManifest {
178 #[must_use]
180 pub fn new(tool_name: &str, version: &str, publisher: &str, description: &str) -> Self {
181 Self {
182 tool_name: tool_name.to_string(),
183 version: version.to_string(),
184 publisher: publisher.to_string(),
185 description: description.to_string(),
186 effects: EffectSummary::default(),
187 capabilities: Vec::new(),
188 trust_level: 0.5,
189 requires_human_review: false,
190 created_at: chrono::Utc::now().timestamp(),
191 signature: String::new(),
192 }
193 }
194
195 #[must_use]
197 pub fn with_effects(mut self, effects: EffectSummary) -> Self {
198 self.effects = effects;
199 self
200 }
201
202 #[must_use]
204 pub fn with_capabilities(mut self, caps: Vec<String>) -> Self {
205 self.capabilities = caps;
206 self
207 }
208
209 #[must_use]
211 pub const fn with_trust(mut self, trust: f32) -> Self {
212 self.trust_level = trust.clamp(0.0, 1.0);
213 self
214 }
215
216 #[must_use]
218 pub const fn require_review(mut self) -> Self {
219 self.requires_human_review = true;
220 self
221 }
222
223 #[must_use]
225 pub fn signing_payload(&self) -> String {
226 let without_sig = Self {
228 signature: String::new(),
229 ..self.clone()
230 };
231 serde_json::to_string(&without_sig).unwrap_or_default()
232 }
233
234 #[must_use]
238 pub fn sign(mut self, key: &[u8]) -> Self {
239 let payload = self.signing_payload();
240 if let Some(sig) = sign_hmac(&payload, key) {
241 self.signature = sig;
242 }
243 self
244 }
245
246 #[must_use]
250 pub fn verify_signature(&self, key: &[u8]) -> bool {
251 verify_hmac(&self.signing_payload(), &self.signature, key)
252 }
253
254 #[must_use]
259 pub fn signature_scheme(&self) -> &'static str {
260 if self.signature.starts_with(ED25519_SIG_PREFIX) {
261 "ed25519"
262 } else if self.signature.is_empty() {
263 "none"
264 } else {
265 "hmac-sha256"
266 }
267 }
268
269 #[must_use]
273 pub fn sign_ed25519(mut self, signing_key: &ed25519_dalek::SigningKey) -> Self {
274 self.signature = sign_ed25519(&self.signing_payload(), signing_key);
275 self
276 }
277
278 #[must_use]
280 pub fn verify_signature_ed25519(&self, verifying_key: &ed25519_dalek::VerifyingKey) -> bool {
281 verify_ed25519(&self.signing_payload(), &self.signature, verifying_key)
282 }
283
284 #[must_use]
286 pub fn has_capability(&self, cap: &str) -> bool {
287 self.capabilities.iter().any(|c| c == cap)
288 }
289
290 #[must_use]
292 pub fn is_destructive(&self) -> bool {
293 self.effects.has_destructive_effects()
294 }
295}
296
297#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
299pub struct TrustScope {
300 pub name: String,
302 pub min_trust: f32,
304 #[serde(default)]
306 pub allowed_tools: Vec<String>,
307 #[serde(default)]
309 pub denied_tools: Vec<String>,
310 pub allow_destructive: bool,
312 pub allow_network: bool,
314 pub allow_filesystem_write: bool,
316 pub require_review: bool,
318 pub max_calls_per_minute: u32,
320}
321
322impl Default for TrustScope {
323 fn default() -> Self {
324 Self {
325 name: "default".into(),
326 min_trust: 0.5,
327 allowed_tools: Vec::new(),
328 denied_tools: Vec::new(),
329 allow_destructive: false,
330 allow_network: false,
331 allow_filesystem_write: false,
332 require_review: false,
333 max_calls_per_minute: 60,
334 }
335 }
336}
337
338impl TrustScope {
339 #[must_use]
341 pub fn strict() -> Self {
342 Self {
343 name: "strict".into(),
344 min_trust: 0.8,
345 allowed_tools: vec!["memory.search".into(), "memory.recall".into()],
346 denied_tools: vec!["file.".into(), "process.".into(), "network.".into()],
347 allow_destructive: false,
348 allow_network: false,
349 allow_filesystem_write: false,
350 require_review: true,
351 max_calls_per_minute: 10,
352 }
353 }
354
355 #[must_use]
357 pub fn permissive() -> Self {
358 Self {
359 name: "permissive".into(),
360 min_trust: 0.3,
361 allow_destructive: true,
362 allow_network: true,
363 allow_filesystem_write: true,
364 require_review: false,
365 max_calls_per_minute: 200,
366 ..Self::default()
367 }
368 }
369
370 #[must_use]
372 pub fn is_tool_allowed(&self, manifest: &ToolManifest) -> bool {
373 if manifest.trust_level < self.min_trust {
375 return false;
376 }
377
378 if self
380 .denied_tools
381 .iter()
382 .any(|p| manifest.tool_name.starts_with(p))
383 {
384 return false;
385 }
386
387 if !self.allowed_tools.is_empty()
389 && !self
390 .allowed_tools
391 .iter()
392 .any(|p| manifest.tool_name.starts_with(p))
393 {
394 return false;
395 }
396
397 if manifest.is_destructive() && !self.allow_destructive {
399 return false;
400 }
401
402 if manifest.has_capability("network_access") && !self.allow_network {
404 return false;
405 }
406
407 if manifest.has_capability("filesystem_write") && !self.allow_filesystem_write {
409 return false;
410 }
411
412 if self.require_review && !manifest.requires_human_review {
414 }
417
418 true
419 }
420}
421
422pub struct ToolAttestationRegistry {
428 manifests: ahash::AHashMap<String, ToolManifest>,
430 signing_key: Vec<u8>,
432 ed25519_key: Option<ed25519_dalek::VerifyingKey>,
434 external_scope: TrustScope,
436 trusted_publishers: Vec<String>,
438}
439
440impl ToolAttestationRegistry {
441 #[must_use]
443 pub fn new(signing_key: Vec<u8>) -> Self {
444 Self {
445 manifests: ahash::AHashMap::new(),
446 signing_key,
447 ed25519_key: None,
448 external_scope: TrustScope::default(),
449 trusted_publishers: vec!["whitemagic-core".into()],
450 }
451 }
452
453 #[must_use]
457 pub fn new_ed25519(verifying_key: ed25519_dalek::VerifyingKey) -> Self {
458 Self {
459 manifests: ahash::AHashMap::new(),
460 signing_key: Vec::new(),
461 ed25519_key: Some(verifying_key),
462 external_scope: TrustScope::default(),
463 trusted_publishers: vec!["whitemagic-core".into()],
464 }
465 }
466
467 #[must_use]
469 pub const fn with_ed25519_key(mut self, verifying_key: ed25519_dalek::VerifyingKey) -> Self {
470 self.ed25519_key = Some(verifying_key);
471 self
472 }
473
474 #[must_use]
476 pub fn with_legacy_hmac_key(mut self, key: Vec<u8>) -> Self {
477 self.signing_key = key;
478 self
479 }
480
481 #[must_use]
484 fn verify_manifest(&self, manifest: &ToolManifest) -> bool {
485 match manifest.signature_scheme() {
486 "ed25519" => self
487 .ed25519_key
488 .as_ref()
489 .is_some_and(|key| manifest.verify_signature_ed25519(key)),
490 "hmac-sha256" => {
491 !self.signing_key.is_empty() && manifest.verify_signature(&self.signing_key)
492 }
493 _ => false,
494 }
495 }
496
497 #[must_use]
499 pub fn with_external_scope(mut self, scope: TrustScope) -> Self {
500 self.external_scope = scope;
501 self
502 }
503
504 pub fn trust_publisher(&mut self, publisher: &str) {
506 if !self.trusted_publishers.contains(&publisher.to_string()) {
507 self.trusted_publishers.push(publisher.to_string());
508 }
509 }
510
511 pub fn register(&mut self, manifest: ToolManifest) -> bool {
516 if !self.verify_manifest(&manifest) {
518 return false;
519 }
520
521 if !self.trusted_publishers.contains(&manifest.publisher) {
523 return false;
524 }
525
526 self.manifests.insert(manifest.tool_name.clone(), manifest);
527 true
528 }
529
530 pub fn register_unsigned(&mut self, manifest: ToolManifest) -> bool {
534 if !self.trusted_publishers.contains(&manifest.publisher) {
535 return false;
536 }
537 self.manifests.insert(manifest.tool_name.clone(), manifest);
538 true
539 }
540
541 #[must_use]
543 pub fn get(&self, tool_name: &str) -> Option<&ToolManifest> {
544 self.manifests.get(tool_name)
545 }
546
547 #[must_use]
549 pub fn is_tool_allowed(&self, tool_name: &str) -> bool {
550 let Some(manifest) = self.manifests.get(tool_name) else {
551 return false; };
553
554 if manifest.publisher == "whitemagic-core" {
556 return true;
557 }
558
559 self.external_scope.is_tool_allowed(manifest)
561 }
562
563 #[must_use]
565 pub fn verify_provenance(&self, manifest: &ToolManifest) -> bool {
566 self.verify_manifest(manifest) && self.trusted_publishers.contains(&manifest.publisher)
567 }
568
569 #[must_use]
571 pub fn registered_tools(&self) -> Vec<String> {
572 self.manifests.keys().cloned().collect()
573 }
574
575 #[must_use]
577 pub fn len(&self) -> usize {
578 self.manifests.len()
579 }
580
581 #[must_use]
583 pub fn is_empty(&self) -> bool {
584 self.manifests.is_empty()
585 }
586}
587
588#[must_use]
590pub fn manifest_hash(manifest: &ToolManifest) -> String {
591 let payload = manifest.signing_payload();
592 let mut hasher = Sha256::new();
593 hasher.update(payload.as_bytes());
594 format!("{:x}", hasher.finalize())
595}
596
597#[cfg(test)]
600mod tests {
601 use super::*;
602
603 const TEST_KEY: &[u8] = b"test_signing_key_123";
604
605 fn make_manifest(name: &str, publisher: &str) -> ToolManifest {
606 ToolManifest::new(name, "1.0.0", publisher, "A test tool")
607 .with_trust(0.8)
608 .with_capabilities(vec!["read_only".into()])
609 }
610
611 #[test]
612 fn manifest_sign_and_verify() {
613 let manifest = make_manifest("memory.search", "whitemagic-core").sign(TEST_KEY);
614 assert!(
615 manifest.verify_signature(TEST_KEY),
616 "Signed manifest should verify"
617 );
618 }
619
620 #[test]
621 fn manifest_tamper_detected() {
622 let manifest = make_manifest("memory.search", "whitemagic-core").sign(TEST_KEY);
623 let tampered = ToolManifest {
624 description: "Tampered description".into(),
625 ..manifest
626 };
627 assert!(
628 !tampered.verify_signature(TEST_KEY),
629 "Tampered manifest should fail verification"
630 );
631 }
632
633 #[test]
634 fn manifest_unsigned_fails_verification() {
635 let manifest = make_manifest("memory.search", "whitemagic-core");
636 assert!(!manifest.verify_signature(TEST_KEY));
637 }
638
639 #[test]
640 fn manifest_wrong_key_fails() {
641 let manifest = make_manifest("memory.search", "whitemagic-core").sign(TEST_KEY);
642 assert!(!manifest.verify_signature(b"wrong_key"));
643 }
644
645 #[test]
646 fn manifest_has_capability() {
647 let manifest = make_manifest("memory.search", "whitemagic-core")
648 .with_capabilities(vec!["read_only".into(), "search".into()]);
649 assert!(manifest.has_capability("read_only"));
650 assert!(manifest.has_capability("search"));
651 assert!(!manifest.has_capability("network_access"));
652 }
653
654 #[test]
655 fn manifest_destructive_detection() {
656 let manifest = ToolManifest::new("file.write", "1.0.0", "whitemagic-core", "Write file")
657 .with_effects(EffectSummary {
658 writes: vec!["Filesystem".into()],
659 ..Default::default()
660 });
661 assert!(manifest.is_destructive());
662
663 let read_only = ToolManifest::new("memory.search", "1.0.0", "whitemagic-core", "Search")
664 .with_effects(EffectSummary {
665 reads: vec!["Galaxy".into()],
666 ..Default::default()
667 });
668 assert!(!read_only.is_destructive());
669 }
670
671 #[test]
672 fn trust_scope_default_allows_trusted() {
673 let scope = TrustScope::default();
674 let manifest = make_manifest("memory.search", "whitemagic-core").with_trust(0.6);
675 assert!(scope.is_tool_allowed(&manifest));
676 }
677
678 #[test]
679 fn trust_scope_blocks_low_trust() {
680 let scope = TrustScope::default();
681 let manifest = make_manifest("memory.search", "external").with_trust(0.2);
682 assert!(!scope.is_tool_allowed(&manifest));
683 }
684
685 #[test]
686 fn trust_scope_strict_blocks_destructive() {
687 let scope = TrustScope::strict();
688 let manifest = ToolManifest::new("file.write", "1.0.0", "external", "Write file")
689 .with_trust(0.9)
690 .with_effects(EffectSummary {
691 writes: vec!["Filesystem".into()],
692 ..Default::default()
693 });
694 assert!(!scope.is_tool_allowed(&manifest));
695 }
696
697 #[test]
698 fn trust_scope_strict_blocks_network() {
699 let scope = TrustScope::strict();
700 let manifest = ToolManifest::new("http.fetch", "1.0.0", "external", "Fetch URL")
701 .with_trust(0.9)
702 .with_capabilities(vec!["network_access".into()]);
703 assert!(!scope.is_tool_allowed(&manifest));
704 }
705
706 #[test]
707 fn trust_scope_denied_list_takes_precedence() {
708 let scope = TrustScope {
709 allowed_tools: vec!["memory.".into()],
710 denied_tools: vec!["memory.delete".into()],
711 ..TrustScope::permissive()
712 };
713 let manifest = make_manifest("memory.delete", "external").with_trust(0.9);
714 assert!(!scope.is_tool_allowed(&manifest));
715 }
716
717 #[test]
718 fn registry_register_signed_manifest() {
719 let mut registry = ToolAttestationRegistry::new(TEST_KEY.to_vec());
720 let manifest = make_manifest("memory.search", "whitemagic-core").sign(TEST_KEY);
721 assert!(registry.register(manifest));
722 assert_eq!(registry.len(), 1);
723 }
724
725 #[test]
726 fn registry_rejects_invalid_signature() {
727 let mut registry = ToolAttestationRegistry::new(TEST_KEY.to_vec());
728 let manifest = make_manifest("memory.search", "whitemagic-core").sign(b"wrong_key");
729 assert!(!registry.register(manifest));
730 assert_eq!(registry.len(), 0);
731 }
732
733 #[test]
734 fn registry_rejects_untrusted_publisher() {
735 let mut registry = ToolAttestationRegistry::new(TEST_KEY.to_vec());
736 let manifest = make_manifest("memory.search", "untrusted").sign(TEST_KEY);
737 assert!(!registry.register(manifest));
738 }
739
740 #[test]
741 fn registry_allows_trusted_publisher() {
742 let mut registry = ToolAttestationRegistry::new(TEST_KEY.to_vec());
743 registry.trust_publisher("external:acme");
744 let manifest = make_manifest("custom.tool", "external:acme").sign(TEST_KEY);
745 assert!(registry.register(manifest));
746 }
747
748 #[test]
749 fn registry_internal_tools_bypass_scope() {
750 let mut registry = ToolAttestationRegistry::new(TEST_KEY.to_vec())
751 .with_external_scope(TrustScope::strict());
752 let manifest = ToolManifest::new("file.write", "1.0.0", "whitemagic-core", "Write")
753 .with_trust(0.5)
754 .with_effects(EffectSummary {
755 writes: vec!["Filesystem".into()],
756 ..Default::default()
757 })
758 .sign(TEST_KEY);
759 registry.register(manifest);
760
761 assert!(registry.is_tool_allowed("file.write"));
763 }
764
765 #[test]
766 fn registry_external_tools_checked_against_scope() {
767 let mut registry = ToolAttestationRegistry::new(TEST_KEY.to_vec())
768 .with_external_scope(TrustScope::strict());
769 registry.trust_publisher("external:acme");
770 let manifest = ToolManifest::new("file.write", "1.0.0", "external:acme", "Write")
771 .with_trust(0.9)
772 .with_effects(EffectSummary {
773 writes: vec!["Filesystem".into()],
774 ..Default::default()
775 })
776 .sign(TEST_KEY);
777 registry.register(manifest);
778
779 assert!(!registry.is_tool_allowed("file.write"));
781 }
782
783 #[test]
784 fn registry_unknown_tool_not_allowed() {
785 let registry = ToolAttestationRegistry::new(TEST_KEY.to_vec());
786 assert!(!registry.is_tool_allowed("unknown.tool"));
787 }
788
789 #[test]
790 fn registry_verify_provenance() {
791 let registry = ToolAttestationRegistry::new(TEST_KEY.to_vec());
792 let manifest = make_manifest("memory.search", "whitemagic-core").sign(TEST_KEY);
793 assert!(registry.verify_provenance(&manifest));
794
795 let untrusted = make_manifest("memory.search", "untrusted").sign(TEST_KEY);
796 assert!(!registry.verify_provenance(&untrusted));
797 }
798
799 #[test]
800 fn manifest_hash_deterministic() {
801 let m1 = make_manifest("memory.search", "whitemagic-core");
802 let m2 = make_manifest("memory.search", "whitemagic-core");
803 assert_eq!(manifest_hash(&m1), manifest_hash(&m2));
804 }
805
806 #[test]
807 fn manifest_hash_changes_with_content() {
808 let m1 = make_manifest("memory.search", "whitemagic-core");
809 let m2 = make_manifest("memory.search", "whitemagic-core").with_trust(0.9);
810 assert_ne!(manifest_hash(&m1), manifest_hash(&m2));
811 }
812
813 #[test]
814 fn registry_registered_tools_list() {
815 let mut registry = ToolAttestationRegistry::new(TEST_KEY.to_vec());
816 registry.register(make_manifest("memory.search", "whitemagic-core").sign(TEST_KEY));
817 registry.register(make_manifest("memory.recall", "whitemagic-core").sign(TEST_KEY));
818
819 let tools = registry.registered_tools();
820 assert_eq!(tools.len(), 2);
821 assert!(tools.contains(&"memory.search".to_string()));
822 assert!(tools.contains(&"memory.recall".to_string()));
823 }
824
825 #[test]
826 fn trust_scope_permissive_allows_most() {
827 let scope = TrustScope::permissive();
828 let manifest = ToolManifest::new("file.write", "1.0.0", "external", "Write")
829 .with_trust(0.5)
830 .with_effects(EffectSummary {
831 writes: vec!["Filesystem".into()],
832 ..Default::default()
833 })
834 .with_capabilities(vec!["filesystem_write".into()]);
835 assert!(scope.is_tool_allowed(&manifest));
836 }
837
838 fn ed25519_key() -> ed25519_dalek::SigningKey {
841 ed25519_dalek::SigningKey::from_bytes(&[7u8; 32])
842 }
843
844 #[test]
845 fn manifest_sign_and_verify_ed25519() {
846 let key = ed25519_key();
847 let manifest = make_manifest("memory.search", "whitemagic-core").sign_ed25519(&key);
848 assert!(manifest.verify_signature_ed25519(&key.verifying_key()));
849 assert_eq!(manifest.signature_scheme(), "ed25519");
850 }
851
852 #[test]
853 fn manifest_ed25519_tamper_detected() {
854 let key = ed25519_key();
855 let manifest = make_manifest("memory.search", "whitemagic-core").sign_ed25519(&key);
856 let tampered = ToolManifest {
857 description: "Tampered".into(),
858 ..manifest
859 };
860 assert!(!tampered.verify_signature_ed25519(&key.verifying_key()));
861 }
862
863 #[test]
864 fn manifest_ed25519_wrong_key_fails() {
865 let key = ed25519_key();
866 let other = ed25519_dalek::SigningKey::from_bytes(&[9u8; 32]);
867 let manifest = make_manifest("memory.search", "whitemagic-core").sign_ed25519(&key);
868 assert!(!manifest.verify_signature_ed25519(&other.verifying_key()));
869 }
870
871 #[test]
872 fn manifest_ed25519_signature_not_valid_as_hmac() {
873 let key = ed25519_key();
874 let manifest = make_manifest("memory.search", "whitemagic-core").sign_ed25519(&key);
875 assert!(!manifest.verify_signature(TEST_KEY));
876 }
877
878 #[test]
879 fn manifest_hmac_signature_scheme_unchanged() {
880 let manifest = make_manifest("memory.search", "whitemagic-core").sign(TEST_KEY);
881 assert_eq!(manifest.signature_scheme(), "hmac-sha256");
882 assert!(manifest.verify_signature(TEST_KEY));
883 assert!(!manifest.verify_signature_ed25519(&ed25519_key().verifying_key()));
884 }
885
886 #[test]
887 fn unsigned_manifest_scheme_none() {
888 let manifest = make_manifest("memory.search", "whitemagic-core");
889 assert_eq!(manifest.signature_scheme(), "none");
890 }
891
892 #[test]
895 fn registry_ed25519_first_registers_ed25519_manifest() {
896 let key = ed25519_key();
897 let mut registry = ToolAttestationRegistry::new_ed25519(key.verifying_key());
898 let manifest = make_manifest("memory.search", "whitemagic-core").sign_ed25519(&key);
899 assert!(registry.register(manifest.clone()));
900 assert!(registry.verify_provenance(&manifest));
901 }
902
903 #[test]
904 fn registry_ed25519_first_rejects_legacy_hmac_without_key() {
905 let key = ed25519_key();
906 let mut registry = ToolAttestationRegistry::new_ed25519(key.verifying_key());
907 let manifest = make_manifest("memory.search", "whitemagic-core").sign(TEST_KEY);
908 assert!(
909 !registry.register(manifest),
910 "legacy HMAC must fail closed when no legacy key is configured"
911 );
912 assert_eq!(registry.len(), 0);
913 }
914
915 #[test]
916 fn registry_migration_window_accepts_both_schemes() {
917 let key = ed25519_key();
918 let mut registry = ToolAttestationRegistry::new_ed25519(key.verifying_key())
919 .with_legacy_hmac_key(TEST_KEY.to_vec());
920 let modern = make_manifest("memory.search", "whitemagic-core").sign_ed25519(&key);
921 let legacy = make_manifest("memory.recall", "whitemagic-core").sign(TEST_KEY);
922 assert!(registry.register(modern));
923 assert!(registry.register(legacy));
924 assert_eq!(registry.len(), 2);
925 }
926
927 #[test]
928 fn registry_ed25519_rejects_wrong_issuer_key() {
929 let key = ed25519_key();
930 let other = ed25519_dalek::SigningKey::from_bytes(&[9u8; 32]);
931 let mut registry = ToolAttestationRegistry::new_ed25519(other.verifying_key());
932 let manifest = make_manifest("memory.search", "whitemagic-core").sign_ed25519(&key);
933 assert!(!registry.register(manifest));
934 }
935
936 #[test]
937 fn registry_ed25519_rejects_tampered_manifest() {
938 let key = ed25519_key();
939 let mut registry = ToolAttestationRegistry::new_ed25519(key.verifying_key());
940 let tampered = ToolManifest {
941 description: "Tampered".into(),
942 ..make_manifest("memory.search", "whitemagic-core").sign_ed25519(&key)
943 };
944 assert!(!registry.register(tampered));
945 }
946
947 #[test]
948 fn registry_hmac_only_rejects_ed25519_manifest() {
949 let key = ed25519_key();
950 let mut registry = ToolAttestationRegistry::new(TEST_KEY.to_vec());
951 let manifest = make_manifest("memory.search", "whitemagic-core").sign_ed25519(&key);
952 assert!(
953 !registry.register(manifest),
954 "an ed25519 manifest needs an ed25519 issuer key"
955 );
956 }
957
958 #[test]
959 fn registry_rejects_unsigned_manifest_fail_closed() {
960 let mut registry = ToolAttestationRegistry::new(TEST_KEY.to_vec());
961 let manifest = make_manifest("memory.search", "whitemagic-core");
962 assert!(!registry.register(manifest));
963 }
964
965 #[test]
966 fn verify_hmac_rejects_forgeries_and_malformed_hex() {
967 let good = sign_hmac("payload", TEST_KEY).unwrap();
968 assert!(verify_hmac("payload", &good, TEST_KEY));
969 assert!(!verify_hmac("payload", &good, b"wrong"));
970 assert!(!verify_hmac("payload!", &good, TEST_KEY));
971 assert!(!verify_hmac("payload", "", TEST_KEY));
972 assert!(!verify_hmac("payload", "zz", TEST_KEY));
973 }
974}