Skip to main content

mur_common/skill/
sign.rs

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}