1use content_addressable::ContentId;
11use ed25519_dalek::VerifyingKey;
12use serde::{Deserialize, Serialize};
13
14use crate::agent_key::{AgentKey, CertChain, Issuer, SerdeSig};
15use crate::UserPublic;
16use crate::{MeshError, Result};
17
18const ASSERTION_DOMAIN: &[u8] = b"agent-mesh/provenance/assertion/v1\0";
20
21#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
24pub struct Assertion {
25 author: CertChain,
26 sig: SerdeSig,
27}
28
29fn message(cid: &ContentId) -> Vec<u8> {
30 let mut m = ASSERTION_DOMAIN.to_vec();
31 m.extend_from_slice(&cid.to_bytes());
32 m
33}
34
35impl Assertion {
36 #[must_use]
38 pub fn sign(author: &AgentKey, cid: &ContentId) -> Self {
39 Self {
40 author: author.cert().clone(),
41 sig: SerdeSig(author.sign(&message(cid))),
42 }
43 }
44
45 #[must_use]
47 pub fn author(&self) -> &CertChain {
48 &self.author
49 }
50
51 #[must_use]
54 pub fn signature(&self) -> &SerdeSig {
55 &self.sig
56 }
57
58 #[must_use]
62 pub fn from_parts(author: CertChain, sig: SerdeSig) -> Self {
63 Self { author, sig }
64 }
65
66 pub fn verify(&self, cid: &ContentId, trust_root: &[u8; 32], generation: u64) -> Result<()> {
77 self.author.verify_at(generation)?;
78 if root_agent(&self.author) != trust_root {
79 return Err(MeshError::InvalidCertChain(
80 "author chain is not rooted at the trust root".into(),
81 ));
82 }
83 VerifyingKey::from_bytes(&self.author.agent_pubkey)
84 .and_then(|vk| vk.verify_strict(&message(cid), &self.sig.0))
85 .map_err(|_| MeshError::BadSignature)
86 }
87
88 pub fn verify_user(&self, cid: &ContentId, user: &UserPublic, generation: u64) -> Result<()> {
97 self.author.verify_at(generation)?;
98 match &self.author.issuer {
99 Issuer::User(u) if u == user => {}
100 _ => {
101 return Err(MeshError::InvalidCertChain(
102 "author is not certified directly by this user".into(),
103 ))
104 }
105 }
106 VerifyingKey::from_bytes(&self.author.agent_pubkey)
107 .and_then(|vk| vk.verify_strict(&message(cid), &self.sig.0))
108 .map_err(|_| MeshError::BadSignature)
109 }
110}
111
112fn root_agent(cert: &CertChain) -> &[u8; 32] {
114 match &cert.issuer {
115 Issuer::User(_) => &cert.agent_pubkey,
116 Issuer::Agent { parent, .. } => root_agent(parent),
117 }
118}
119
120#[cfg(test)]
121mod tests {
122 use super::*;
123 use crate::{AgentMetadata, Caveats, UserKey};
124 use ed25519_dalek::Signature;
125
126 fn meta(role: &str) -> AgentMetadata {
127 AgentMetadata {
128 role: role.into(),
129 host: "h".into(),
130 capabilities: vec![],
131 issued_at: "2026-10-15T00:00:00Z".into(),
132 expires_at: None,
133 caveats: Caveats::top(),
134 }
135 }
136
137 fn cid(n: u8) -> ContentId {
138 ContentId::from_canonical_bytes(&[n])
139 }
140
141 fn flock(user: &UserKey) -> (AgentKey, AgentKey) {
143 let server = AgentKey::issue(user, meta("server"));
144 let author = server.delegate(meta("author")).unwrap();
145 (server, author)
146 }
147
148 #[test]
149 fn verifies_for_any_reader_holding_the_chain() {
150 let (server, author) = flock(&UserKey::generate());
151 let a = Assertion::sign(&author, &cid(1));
152 let wire = serde_json::to_vec(&a).unwrap();
154 let got: Assertion = serde_json::from_slice(&wire).unwrap();
155 got.verify(&cid(1), &server.public_bytes(), 0).unwrap();
156 }
157
158 #[test]
159 fn one_changed_byte_fails() {
160 let (server, author) = flock(&UserKey::generate());
161 let a = Assertion::sign(&author, &cid(1));
162 let root = server.public_bytes();
163 assert!(a.verify(&cid(2), &root, 0).is_err(), "changed CID");
164 let mut bytes = a.sig.0.to_bytes();
165 bytes[0] ^= 1;
166 let mut bad = a.clone();
167 bad.sig = SerdeSig(Signature::from_bytes(&bytes));
168 assert!(bad.verify(&cid(1), &root, 0).is_err(), "changed signature");
169 }
170
171 #[test]
172 fn other_flock_server_fails_even_for_the_same_user() {
173 let user = UserKey::generate();
174 let (_s1, author1) = flock(&user);
175 let (s2, _author2) = flock(&user);
176 let a = Assertion::sign(&author1, &cid(1));
177 assert!(a.verify(&cid(1), &s2.public_bytes(), 0).is_err());
178 }
179
180 #[test]
181 fn domain_tag_separates_from_a_bare_cid_signature() {
182 let (server, author) = flock(&UserKey::generate());
183 let c = cid(1);
184 let a = Assertion {
185 author: author.cert().clone(),
186 sig: SerdeSig(author.sign(&c.to_bytes())),
187 };
188 assert!(a.verify(&c, &server.public_bytes(), 0).is_err());
189 }
190
191 fn seed_of(k: &AgentKey) -> [u8; 32] {
192 k.signing_key_bytes()
193 }
194
195 #[test]
196 fn intermediate_match_with_other_root_agent_is_refused() {
197 let user = UserKey::generate();
199 let other = AgentKey::issue(&user, meta("other"));
200 let pinned = AgentKey::issue(&user, meta("pinned"));
201 let ch = other.possession_challenge(pinned.public_bytes());
202 let proof = pinned.sign(&ch.signing_bytes());
203 let cert = other
204 .delegate_external(&ch, &proof, meta("pinned"))
205 .unwrap();
206 let pinned_via_other = AgentKey::from_seed_and_cert(&seed_of(&pinned), cert).unwrap();
207 let author = pinned_via_other.delegate(meta("author")).unwrap();
208 let a = Assertion::sign(&author, &cid(1));
209 assert!(a.verify(&cid(1), &pinned.public_bytes(), 0).is_err());
210 a.verify(&cid(1), &other.public_bytes(), 0).unwrap();
211 }
212
213 #[test]
214 fn author_leaf_as_trust_root_is_refused() {
215 let (_server, author) = flock(&UserKey::generate());
216 let a = Assertion::sign(&author, &cid(1));
217 assert!(a.verify(&cid(1), &author.public_bytes(), 0).is_err());
218 }
219
220 #[test]
221 fn bounded_generation_valid_at_n_refused_at_n_plus_1() {
222 let user = UserKey::generate();
223 let mut m = meta("server");
224 m.caveats.valid_for_generation = crate::Scope::only([5u64]);
225 let server = AgentKey::issue(&user, m.clone());
226 let author = server.delegate(m).unwrap();
227 let a = Assertion::sign(&author, &cid(1));
228 a.verify(&cid(1), &server.public_bytes(), 5).unwrap();
229 assert!(a.verify(&cid(1), &server.public_bytes(), 6).is_err());
230 assert!(a.verify(&cid(1), &server.public_bytes(), 4).is_err());
231 }
232
233 #[test]
234 fn verify_user_accepts_a_device_of_that_user_only() {
235 let user = UserKey::generate();
236 let dev = AgentKey::issue(&user, meta("operator"));
237 let c = cid(7);
238 let a = Assertion::sign(&dev, &c);
239 a.verify_user(&c, &user.public(), 0).unwrap();
240 assert!(a.verify_user(&c, &UserKey::generate().public(), 0).is_err());
242 assert!(a.verify_user(&cid(8), &user.public(), 0).is_err());
243 let child = dev.delegate(meta("x")).unwrap();
244 assert!(Assertion::sign(&child, &c)
245 .verify_user(&c, &user.public(), 0)
246 .is_err());
247 }
248}