1use ed25519_dalek::{Signer, SigningKey, Verifier, VerifyingKey};
29use thiserror::Error;
30
31use crate::signing::{b64decode, b64encode};
32
33#[derive(Debug, Error, PartialEq, Eq)]
34pub enum CertError {
35 #[error("certificate base64 decode failed")]
36 BadEncoding,
37 #[error("certificate length is not 64 bytes")]
38 BadLength,
39 #[error("public key length is not 32 bytes")]
40 BadKey,
41 #[error("signature did not verify")]
42 Rejected,
43}
44
45pub fn sign_did_cert(signing_key: &[u8], payload_did: &str) -> Result<String, CertError> {
51 if signing_key.len() < 32 {
52 return Err(CertError::BadKey);
53 }
54 let mut sk_bytes = [0u8; 32];
55 sk_bytes.copy_from_slice(&signing_key[..32]);
56 let sk = SigningKey::from_bytes(&sk_bytes);
57 let sig = sk.sign(payload_did.as_bytes());
58 Ok(b64encode(&sig.to_bytes()))
59}
60
61pub fn verify_op_cert(
67 op_pubkey: &[u8],
68 op_cert_b64: &str,
69 session_did: &str,
70) -> Result<(), CertError> {
71 verify_did_cert(op_pubkey, op_cert_b64, session_did)
72}
73
74pub fn verify_member_cert(
79 org_pubkey: &[u8],
80 member_cert_b64: &str,
81 op_did: &str,
82) -> Result<(), CertError> {
83 verify_did_cert(org_pubkey, member_cert_b64, op_did)
84}
85
86fn succession_payload(kind: &str, old_did: &str, new_did: &str) -> String {
91 format!("wire-succession-v1|{kind}|{old_did}|{new_did}")
92}
93
94pub fn sign_succession_cert(
101 old_signing_key: &[u8],
102 kind: &str,
103 old_did: &str,
104 new_did: &str,
105) -> Result<String, CertError> {
106 sign_did_cert(old_signing_key, &succession_payload(kind, old_did, new_did))
107}
108
109pub fn verify_succession_cert(
114 old_pubkey: &[u8],
115 cert_b64: &str,
116 kind: &str,
117 old_did: &str,
118 new_did: &str,
119) -> Result<(), CertError> {
120 verify_did_cert(
121 old_pubkey,
122 cert_b64,
123 &succession_payload(kind, old_did, new_did),
124 )
125}
126
127fn verify_did_cert(pubkey: &[u8], cert_b64: &str, payload_did: &str) -> Result<(), CertError> {
128 if pubkey.len() != 32 {
129 return Err(CertError::BadKey);
130 }
131 let mut pk_arr = [0u8; 32];
132 pk_arr.copy_from_slice(pubkey);
133 let vk = VerifyingKey::from_bytes(&pk_arr).map_err(|_| CertError::BadKey)?;
134
135 let sig_bytes = b64decode(cert_b64).map_err(|_| CertError::BadEncoding)?;
136 if sig_bytes.len() != 64 {
137 return Err(CertError::BadLength);
138 }
139 let mut sig_arr = [0u8; 64];
140 sig_arr.copy_from_slice(&sig_bytes);
141 let sig = ed25519_dalek::Signature::from_bytes(&sig_arr);
142
143 vk.verify(payload_did.as_bytes(), &sig)
144 .map_err(|_| CertError::Rejected)
145}
146
147#[cfg(test)]
148mod tests {
149 use super::*;
150 use crate::agent_card::{did_for_op, did_for_org, did_for_with_key};
151 use crate::signing::generate_keypair;
152
153 #[test]
154 fn sign_verify_op_cert_roundtrip() {
155 let (op_sk, op_pk) = generate_keypair();
156 let (_, session_pk) = generate_keypair();
157 let session_did = did_for_with_key("vesper-valley", &session_pk);
158 let cert = sign_did_cert(&op_sk, &session_did).unwrap();
159 verify_op_cert(&op_pk, &cert, &session_did).unwrap();
160 }
161
162 #[test]
163 fn sign_verify_member_cert_roundtrip() {
164 let (org_sk, org_pk) = generate_keypair();
165 let (_, op_pk) = generate_keypair();
166 let op_did = did_for_op("darby", &op_pk);
167 let cert = sign_did_cert(&org_sk, &op_did).unwrap();
168 verify_member_cert(&org_pk, &cert, &op_did).unwrap();
169 }
170
171 #[test]
172 fn verify_op_cert_rejects_wrong_session_did() {
173 let (op_sk, op_pk) = generate_keypair();
177 let (_, sk_a) = generate_keypair();
178 let (_, sk_b) = generate_keypair();
179 let did_a = did_for_with_key("session-a", &sk_a);
180 let did_b = did_for_with_key("session-b", &sk_b);
181 let cert = sign_did_cert(&op_sk, &did_a).unwrap();
182 assert_eq!(
183 verify_op_cert(&op_pk, &cert, &did_b),
184 Err(CertError::Rejected)
185 );
186 }
187
188 #[test]
189 fn verify_member_cert_rejects_wrong_op_did() {
190 let (org_sk, org_pk) = generate_keypair();
194 let (_, op_a_pk) = generate_keypair();
195 let (_, op_b_pk) = generate_keypair();
196 let op_a = did_for_op("darby", &op_a_pk);
197 let op_b = did_for_op("willard", &op_b_pk);
198 let cert = sign_did_cert(&org_sk, &op_a).unwrap();
199 assert_eq!(
200 verify_member_cert(&org_pk, &cert, &op_b),
201 Err(CertError::Rejected)
202 );
203 }
204
205 #[test]
206 fn verify_op_cert_rejects_wrong_op_key() {
207 let (alice_sk, _) = generate_keypair();
210 let (_, bob_pk) = generate_keypair();
211 let (_, session_pk) = generate_keypair();
212 let session_did = did_for_with_key("s", &session_pk);
213 let cert = sign_did_cert(&alice_sk, &session_did).unwrap();
214 assert_eq!(
215 verify_op_cert(&bob_pk, &cert, &session_did),
216 Err(CertError::Rejected)
217 );
218 }
219
220 #[test]
221 fn verify_op_cert_rejects_bad_base64() {
222 let (_, pk) = generate_keypair();
223 assert_eq!(
224 verify_op_cert(&pk, "not-base64!", "did:wire:s"),
225 Err(CertError::BadEncoding)
226 );
227 }
228
229 #[test]
230 fn verify_op_cert_rejects_short_cert() {
231 let (_, pk) = generate_keypair();
232 let short = b64encode(&[0u8; 32]);
233 assert_eq!(
234 verify_op_cert(&pk, &short, "did:wire:s"),
235 Err(CertError::BadLength)
236 );
237 }
238
239 #[test]
240 fn verify_op_cert_rejects_short_pubkey() {
241 let (sk, _) = generate_keypair();
242 let cert = sign_did_cert(&sk, "did:wire:s").unwrap();
243 let short_pk = vec![0u8; 16];
244 assert_eq!(
245 verify_op_cert(&short_pk, &cert, "did:wire:s"),
246 Err(CertError::BadKey)
247 );
248 }
249
250 #[test]
251 fn sign_did_cert_rejects_short_signing_key() {
252 let short_sk = vec![0u8; 16];
253 assert_eq!(
254 sign_did_cert(&short_sk, "did:wire:s"),
255 Err(CertError::BadKey)
256 );
257 }
258
259 #[test]
260 fn op_and_org_cert_signing_are_indistinguishable_at_byte_level() {
261 let (op_sk, _op_pk) = generate_keypair();
267 let (_, session_pk) = generate_keypair();
268 let session_did = did_for_with_key("s", &session_pk);
269
270 let (org_sk, _org_pk) = generate_keypair();
271 let (_, op_pk) = generate_keypair();
272 let op_did = did_for_op("darby", &op_pk);
273
274 let op_cert = sign_did_cert(&op_sk, &session_did).unwrap();
275 let member_cert = sign_did_cert(&org_sk, &op_did).unwrap();
276
277 assert_eq!(b64decode(&op_cert).unwrap().len(), 64);
279 assert_eq!(b64decode(&member_cert).unwrap().len(), 64);
280 }
281
282 #[test]
283 fn succession_cert_roundtrip_and_binding() {
284 let (old_sk, old_pk) = generate_keypair();
287 let (_, new_pk) = generate_keypair();
288 let old_did = did_for_op("darby", &old_pk);
289 let new_did = did_for_op("darby", &new_pk);
290 let cert = sign_succession_cert(&old_sk, "op", &old_did, &new_did).unwrap();
291 verify_succession_cert(&old_pk, &cert, "op", &old_did, &new_did).unwrap();
292
293 let (_, attacker_pk) = generate_keypair();
295 let attacker_did = did_for_op("darby", &attacker_pk);
296 assert_eq!(
297 verify_succession_cert(&old_pk, &cert, "op", &old_did, &attacker_did),
298 Err(CertError::Rejected)
299 );
300 assert_eq!(
302 verify_succession_cert(&old_pk, &cert, "org", &old_did, &new_did),
303 Err(CertError::Rejected)
304 );
305 assert_eq!(
307 verify_succession_cert(&new_pk, &cert, "op", &old_did, &new_did),
308 Err(CertError::Rejected)
309 );
310 }
311
312 #[test]
313 fn succession_cert_is_domain_separated_from_op_cert() {
314 let (old_sk, old_pk) = generate_keypair();
318 let (_, new_pk) = generate_keypair();
319 let old_did = did_for_op("darby", &old_pk);
320 let new_did = did_for_op("darby", &new_pk);
321
322 let succ = sign_succession_cert(&old_sk, "op", &old_did, &new_did).unwrap();
323 assert_eq!(
326 verify_op_cert(&old_pk, &succ, &new_did),
327 Err(CertError::Rejected)
328 );
329
330 let op_cert = sign_did_cert(&old_sk, &new_did).unwrap();
332 assert_eq!(
333 verify_succession_cert(&old_pk, &op_cert, "op", &old_did, &new_did),
334 Err(CertError::Rejected)
335 );
336 }
337
338 #[test]
339 fn org_did_payload_is_not_confused_with_member_cert_subject() {
340 let (org_sk, org_pk) = generate_keypair();
345 let (_, org_pk_for_did) = generate_keypair();
346 let org_did = did_for_org("slanchaai", &org_pk_for_did);
347 let (_, op_pk) = generate_keypair();
348 let op_did = did_for_op("darby", &op_pk);
349
350 let bogus = sign_did_cert(&org_sk, &org_did).unwrap();
353 assert_eq!(
354 verify_member_cert(&org_pk, &bogus, &op_did),
355 Err(CertError::Rejected)
356 );
357 }
358}