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
127pub fn verify_payload_sig(pubkey: &[u8], sig_b64: &str, payload: &str) -> Result<(), CertError> {
135 verify_did_cert(pubkey, sig_b64, payload)
136}
137
138fn verify_did_cert(pubkey: &[u8], cert_b64: &str, payload_did: &str) -> Result<(), CertError> {
139 if pubkey.len() != 32 {
140 return Err(CertError::BadKey);
141 }
142 let mut pk_arr = [0u8; 32];
143 pk_arr.copy_from_slice(pubkey);
144 let vk = VerifyingKey::from_bytes(&pk_arr).map_err(|_| CertError::BadKey)?;
145
146 let sig_bytes = b64decode(cert_b64).map_err(|_| CertError::BadEncoding)?;
147 if sig_bytes.len() != 64 {
148 return Err(CertError::BadLength);
149 }
150 let mut sig_arr = [0u8; 64];
151 sig_arr.copy_from_slice(&sig_bytes);
152 let sig = ed25519_dalek::Signature::from_bytes(&sig_arr);
153
154 vk.verify(payload_did.as_bytes(), &sig)
155 .map_err(|_| CertError::Rejected)
156}
157
158#[cfg(test)]
159mod tests {
160 use super::*;
161 use crate::agent_card::{did_for_op, did_for_org, did_for_with_key};
162 use crate::signing::generate_keypair;
163
164 #[test]
165 fn sign_verify_op_cert_roundtrip() {
166 let (op_sk, op_pk) = generate_keypair();
167 let (_, session_pk) = generate_keypair();
168 let session_did = did_for_with_key("vesper-valley", &session_pk);
169 let cert = sign_did_cert(&op_sk, &session_did).unwrap();
170 verify_op_cert(&op_pk, &cert, &session_did).unwrap();
171 }
172
173 #[test]
174 fn sign_verify_member_cert_roundtrip() {
175 let (org_sk, org_pk) = generate_keypair();
176 let (_, op_pk) = generate_keypair();
177 let op_did = did_for_op("darby", &op_pk);
178 let cert = sign_did_cert(&org_sk, &op_did).unwrap();
179 verify_member_cert(&org_pk, &cert, &op_did).unwrap();
180 }
181
182 #[test]
183 fn verify_op_cert_rejects_wrong_session_did() {
184 let (op_sk, op_pk) = generate_keypair();
188 let (_, sk_a) = generate_keypair();
189 let (_, sk_b) = generate_keypair();
190 let did_a = did_for_with_key("session-a", &sk_a);
191 let did_b = did_for_with_key("session-b", &sk_b);
192 let cert = sign_did_cert(&op_sk, &did_a).unwrap();
193 assert_eq!(
194 verify_op_cert(&op_pk, &cert, &did_b),
195 Err(CertError::Rejected)
196 );
197 }
198
199 #[test]
200 fn verify_member_cert_rejects_wrong_op_did() {
201 let (org_sk, org_pk) = generate_keypair();
205 let (_, op_a_pk) = generate_keypair();
206 let (_, op_b_pk) = generate_keypair();
207 let op_a = did_for_op("darby", &op_a_pk);
208 let op_b = did_for_op("willard", &op_b_pk);
209 let cert = sign_did_cert(&org_sk, &op_a).unwrap();
210 assert_eq!(
211 verify_member_cert(&org_pk, &cert, &op_b),
212 Err(CertError::Rejected)
213 );
214 }
215
216 #[test]
217 fn verify_op_cert_rejects_wrong_op_key() {
218 let (alice_sk, _) = generate_keypair();
221 let (_, bob_pk) = generate_keypair();
222 let (_, session_pk) = generate_keypair();
223 let session_did = did_for_with_key("s", &session_pk);
224 let cert = sign_did_cert(&alice_sk, &session_did).unwrap();
225 assert_eq!(
226 verify_op_cert(&bob_pk, &cert, &session_did),
227 Err(CertError::Rejected)
228 );
229 }
230
231 #[test]
232 fn verify_op_cert_rejects_bad_base64() {
233 let (_, pk) = generate_keypair();
234 assert_eq!(
235 verify_op_cert(&pk, "not-base64!", "did:wire:s"),
236 Err(CertError::BadEncoding)
237 );
238 }
239
240 #[test]
241 fn verify_op_cert_rejects_short_cert() {
242 let (_, pk) = generate_keypair();
243 let short = b64encode(&[0u8; 32]);
244 assert_eq!(
245 verify_op_cert(&pk, &short, "did:wire:s"),
246 Err(CertError::BadLength)
247 );
248 }
249
250 #[test]
251 fn verify_op_cert_rejects_short_pubkey() {
252 let (sk, _) = generate_keypair();
253 let cert = sign_did_cert(&sk, "did:wire:s").unwrap();
254 let short_pk = vec![0u8; 16];
255 assert_eq!(
256 verify_op_cert(&short_pk, &cert, "did:wire:s"),
257 Err(CertError::BadKey)
258 );
259 }
260
261 #[test]
262 fn sign_did_cert_rejects_short_signing_key() {
263 let short_sk = vec![0u8; 16];
264 assert_eq!(
265 sign_did_cert(&short_sk, "did:wire:s"),
266 Err(CertError::BadKey)
267 );
268 }
269
270 #[test]
271 fn op_and_org_cert_signing_are_indistinguishable_at_byte_level() {
272 let (op_sk, _op_pk) = generate_keypair();
278 let (_, session_pk) = generate_keypair();
279 let session_did = did_for_with_key("s", &session_pk);
280
281 let (org_sk, _org_pk) = generate_keypair();
282 let (_, op_pk) = generate_keypair();
283 let op_did = did_for_op("darby", &op_pk);
284
285 let op_cert = sign_did_cert(&op_sk, &session_did).unwrap();
286 let member_cert = sign_did_cert(&org_sk, &op_did).unwrap();
287
288 assert_eq!(b64decode(&op_cert).unwrap().len(), 64);
290 assert_eq!(b64decode(&member_cert).unwrap().len(), 64);
291 }
292
293 #[test]
294 fn succession_cert_roundtrip_and_binding() {
295 let (old_sk, old_pk) = generate_keypair();
298 let (_, new_pk) = generate_keypair();
299 let old_did = did_for_op("darby", &old_pk);
300 let new_did = did_for_op("darby", &new_pk);
301 let cert = sign_succession_cert(&old_sk, "op", &old_did, &new_did).unwrap();
302 verify_succession_cert(&old_pk, &cert, "op", &old_did, &new_did).unwrap();
303
304 let (_, attacker_pk) = generate_keypair();
306 let attacker_did = did_for_op("darby", &attacker_pk);
307 assert_eq!(
308 verify_succession_cert(&old_pk, &cert, "op", &old_did, &attacker_did),
309 Err(CertError::Rejected)
310 );
311 assert_eq!(
313 verify_succession_cert(&old_pk, &cert, "org", &old_did, &new_did),
314 Err(CertError::Rejected)
315 );
316 assert_eq!(
318 verify_succession_cert(&new_pk, &cert, "op", &old_did, &new_did),
319 Err(CertError::Rejected)
320 );
321 }
322
323 #[test]
324 fn succession_cert_is_domain_separated_from_op_cert() {
325 let (old_sk, old_pk) = generate_keypair();
329 let (_, new_pk) = generate_keypair();
330 let old_did = did_for_op("darby", &old_pk);
331 let new_did = did_for_op("darby", &new_pk);
332
333 let succ = sign_succession_cert(&old_sk, "op", &old_did, &new_did).unwrap();
334 assert_eq!(
337 verify_op_cert(&old_pk, &succ, &new_did),
338 Err(CertError::Rejected)
339 );
340
341 let op_cert = sign_did_cert(&old_sk, &new_did).unwrap();
343 assert_eq!(
344 verify_succession_cert(&old_pk, &op_cert, "op", &old_did, &new_did),
345 Err(CertError::Rejected)
346 );
347 }
348
349 #[test]
350 fn org_did_payload_is_not_confused_with_member_cert_subject() {
351 let (org_sk, org_pk) = generate_keypair();
356 let (_, org_pk_for_did) = generate_keypair();
357 let org_did = did_for_org("slanchaai", &org_pk_for_did);
358 let (_, op_pk) = generate_keypair();
359 let op_did = did_for_op("darby", &op_pk);
360
361 let bogus = sign_did_cert(&org_sk, &org_did).unwrap();
364 assert_eq!(
365 verify_member_cert(&org_pk, &bogus, &op_did),
366 Err(CertError::Rejected)
367 );
368 }
369}