1use crate::identity::AgentIdentity;
2use crate::muragent::MuragentError;
3use crate::muragent::dsse::{DsseEnvelope, sign as dsse_sign, verify as dsse_verify};
4use crate::skill::manifest::SkillManifest;
5use crate::skill::scan::scan_unicode;
6use crate::skill::serialize_canonical;
7
8pub const SKILL_PAYLOAD_TYPE: &str = "application/vnd.mur.skill+yaml";
9
10#[derive(Debug)]
11pub enum SignError {
12 Parse(crate::skill::ParseError),
13 Muragent(MuragentError),
14}
15
16impl std::fmt::Display for SignError {
17 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
18 match self {
19 SignError::Parse(e) => write!(f, "parse: {e}"),
20 SignError::Muragent(e) => write!(f, "sign: {e}"),
21 }
22 }
23}
24
25impl std::error::Error for SignError {}
26
27impl From<crate::skill::ParseError> for SignError {
28 fn from(e: crate::skill::ParseError) -> Self {
29 SignError::Parse(e)
30 }
31}
32
33impl From<MuragentError> for SignError {
34 fn from(e: MuragentError) -> Self {
35 SignError::Muragent(e)
36 }
37}
38
39pub fn sign_manifest(m: &SkillManifest, identity: &AgentIdentity) -> Result<String, SignError> {
40 let yaml = serialize_canonical(m)?;
41 let (normalised, _) = scan_unicode(&yaml);
42 let envelope = dsse_sign(SKILL_PAYLOAD_TYPE, &normalised, identity)?;
43 let s = serde_json::to_string(&envelope)
44 .map_err(|e| MuragentError::Other(format!("envelope json: {e}")))?;
45 Ok(s)
46}
47
48pub fn verify_manifest(m: &SkillManifest, envelope_json: &str) -> Result<(), SignError> {
49 let envelope: DsseEnvelope = serde_json::from_str(envelope_json)
50 .map_err(|e| MuragentError::Other(format!("envelope parse: {e}")))?;
51
52 dsse_verify(&envelope, SKILL_PAYLOAD_TYPE)?;
53
54 use base64::Engine;
55 use base64::engine::general_purpose::STANDARD as B64;
56 let signed_bytes = B64
57 .decode(&envelope.payload)
58 .map_err(|e| MuragentError::Other(format!("payload base64: {e}")))?;
59 let signed_str = String::from_utf8(signed_bytes)
60 .map_err(|e| MuragentError::Other(format!("payload utf8: {e}")))?;
61
62 let yaml = serialize_canonical(m)?;
63 let (normalised, _) = scan_unicode(&yaml);
64 if signed_str != normalised {
65 return Err(SignError::Muragent(MuragentError::InvalidSignature(
66 "manifest content does not match signed payload".into(),
67 )));
68 }
69 Ok(())
70}
71
72#[cfg(test)]
73mod tests {
74 use super::*;
75 use crate::skill::parse_canonical;
76
77 fn sample() -> SkillManifest {
78 let yaml = r#"
79name: signed
80version: 1.0.0
81publisher: human:t
82description: d
83category: context
84content:
85 abstract: a
86 context: b
87"#;
88 parse_canonical(yaml).unwrap()
89 }
90
91 #[test]
92 fn sign_then_verify() {
93 let id = AgentIdentity::generate();
94 let m = sample();
95 let env = sign_manifest(&m, &id).unwrap();
96 verify_manifest(&m, &env).unwrap();
97 }
98
99 #[test]
100 fn tampered_manifest_fails_verify() {
101 let id = AgentIdentity::generate();
102 let m = sample();
103 let env = sign_manifest(&m, &id).unwrap();
104 let mut tampered = m.clone();
105 tampered.description = "evil".into();
106 assert!(verify_manifest(&tampered, &env).is_err());
107 }
108
109 #[test]
110 fn wrong_payload_type_rejected() {
111 let id = AgentIdentity::generate();
112 let m = sample();
113 let env = sign_manifest(&m, &id).unwrap();
114 let mut e: DsseEnvelope = serde_json::from_str(&env).unwrap();
115 e.payload_type = "application/vnd.in-toto+json".into();
116 let bad = serde_json::to_string(&e).unwrap();
117 assert!(verify_manifest(&m, &bad).is_err());
118 }
119}