mur_common/muragent/
dsse.rs1use crate::identity::AgentIdentity;
6use crate::muragent::MuragentError;
7use ed25519_dalek::{Signature, Signer, VerifyingKey};
8use serde::{Deserialize, Serialize};
9
10pub fn pae(payload_type: &str, payload: &str) -> Vec<u8> {
14 let mut out = b"DSSEv1 ".to_vec();
15 out.extend_from_slice(payload_type.len().to_string().as_bytes());
16 out.push(b' ');
17 out.extend_from_slice(payload_type.as_bytes());
18 out.push(b' ');
19 out.extend_from_slice(payload.len().to_string().as_bytes());
20 out.push(b' ');
21 out.extend_from_slice(payload.as_bytes());
22 out
23}
24
25#[derive(Debug, Clone, Serialize, Deserialize)]
26pub struct DsseEnvelope {
27 #[serde(rename = "payloadType")]
28 pub payload_type: String,
29 pub payload: String,
30 pub signatures: Vec<DsseSignature>,
31}
32
33#[derive(Debug, Clone, Serialize, Deserialize)]
34pub struct DsseSignature {
35 pub keyid: String,
36 #[serde(rename = "publicKey")]
37 pub public_key: String,
38 pub sig: String,
39}
40
41pub fn sign(
43 payload_type: &str,
44 payload_json: &str,
45 identity: &AgentIdentity,
46) -> Result<DsseEnvelope, MuragentError> {
47 use base64::{Engine, engine::general_purpose::STANDARD as B64};
48
49 let pae_bytes = pae(payload_type, payload_json);
50 let signing_key = identity.signing_key();
51 let signature: Signature = signing_key.sign(&pae_bytes);
52
53 let verifying_key = signing_key.verifying_key();
54 let pubkey_bytes = verifying_key.as_bytes();
55 let keyid = keyid_from_pubkey(pubkey_bytes);
56
57 let envelope = DsseEnvelope {
58 payload_type: payload_type.to_string(),
59 payload: B64.encode(payload_json.as_bytes()),
60 signatures: vec![DsseSignature {
61 keyid,
62 public_key: B64.encode(pubkey_bytes),
63 sig: B64.encode(signature.to_bytes()),
64 }],
65 };
66
67 Ok(envelope)
68}
69
70pub fn verify(envelope: &DsseEnvelope, expected_payload_type: &str) -> Result<(), MuragentError> {
73 use base64::{Engine, engine::general_purpose::STANDARD as B64};
74
75 if envelope.payload_type != expected_payload_type {
76 return Err(MuragentError::DsseError(format!(
77 "payload type mismatch: expected '{}', got '{}'",
78 expected_payload_type, envelope.payload_type
79 )));
80 }
81
82 let payload_bytes = B64
83 .decode(&envelope.payload)
84 .map_err(|e| MuragentError::DsseError(format!("payload base64: {e}")))?;
85 let payload_str = String::from_utf8(payload_bytes)
86 .map_err(|e| MuragentError::DsseError(format!("payload utf-8: {e}")))?;
87
88 if envelope.signatures.is_empty() {
89 return Err(MuragentError::DsseError("no signatures in envelope".into()));
90 }
91
92 let sig_entry = &envelope.signatures[0];
93 let pae_bytes = pae(expected_payload_type, &payload_str);
94
95 let pubkey_bytes = B64
96 .decode(&sig_entry.public_key)
97 .map_err(|e| MuragentError::DsseError(format!("public_key base64: {e}")))?;
98 let pubkey_arr: [u8; 32] = pubkey_bytes
99 .as_slice()
100 .try_into()
101 .map_err(|_| MuragentError::DsseError("public_key not 32 bytes".into()))?;
102 let verifying_key = VerifyingKey::from_bytes(&pubkey_arr)
103 .map_err(|e| MuragentError::DsseError(format!("pubkey decode: {e}")))?;
104
105 let sig_bytes = B64
106 .decode(&sig_entry.sig)
107 .map_err(|e| MuragentError::DsseError(format!("sig base64: {e}")))?;
108 let sig_arr: [u8; 64] = sig_bytes
109 .as_slice()
110 .try_into()
111 .map_err(|_| MuragentError::DsseError("sig not 64 bytes".into()))?;
112 let signature = Signature::from_bytes(&sig_arr);
113
114 verifying_key
115 .verify_strict(&pae_bytes, &signature)
116 .map_err(|e| MuragentError::InvalidSignature(format!("Ed25519 verify_strict: {e}")))?;
117
118 Ok(())
119}
120
121fn keyid_from_pubkey(pubkey: &[u8; 32]) -> String {
123 use sha2::Digest;
124 let hash = sha2::Sha256::digest(pubkey);
125 let hex = format!("{:x}", hash);
126 format!("ed25519-{}", &hex[..8])
127}
128
129#[cfg(test)]
130mod tests {
131 use super::*;
132 use crate::identity::AgentIdentity;
133
134 #[test]
135 fn pae_is_deterministic() {
136 let a = pae("application/vnd.in-toto+json", r#"{"a":1}"#);
137 let b = pae("application/vnd.in-toto+json", r#"{"a":1}"#);
138 assert_eq!(a, b);
139 }
140
141 #[test]
142 fn pae_byte_lengths_not_char_counts() {
143 let pae_bytes = pae("type", "café");
145 let pae_str = String::from_utf8(pae_bytes).unwrap();
146 assert!(
148 pae_str.contains(" 5 café"),
149 "payload length should be 5 bytes, got: {pae_str}"
150 );
151 }
152
153 #[test]
154 fn sign_and_verify_roundtrip() {
155 let identity = AgentIdentity::generate();
156 let payload = r#"{"manifest_sha256":"abc123"}"#;
157 let envelope = sign("application/vnd.in-toto+json", payload, &identity).unwrap();
158 verify(&envelope, "application/vnd.in-toto+json").unwrap();
159 }
160
161 #[test]
162 fn verify_rejects_wrong_payload_type() {
163 let identity = AgentIdentity::generate();
164 let envelope = sign("application/vnd.in-toto+json", "{}", &identity).unwrap();
165 assert!(verify(&envelope, "wrong/type").is_err());
166 }
167
168 #[test]
169 fn verify_rejects_tampered_payload() {
170 let identity = AgentIdentity::generate();
171 let mut envelope = sign("application/vnd.in-toto+json", r#"{"a":1}"#, &identity).unwrap();
172 use base64::{Engine, engine::general_purpose::STANDARD as B64};
173 envelope.payload = B64.encode(r#"{"a":2}"#);
174 assert!(verify(&envelope, "application/vnd.in-toto+json").is_err());
175 }
176
177 #[test]
178 fn verify_rejects_empty_signatures() {
179 use base64::{Engine, engine::general_purpose::STANDARD as B64};
180 let envelope = DsseEnvelope {
181 payload_type: "application/vnd.in-toto+json".into(),
182 payload: B64.encode("{}"),
183 signatures: vec![],
184 };
185 assert!(verify(&envelope, "application/vnd.in-toto+json").is_err());
186 }
187}