1use ed25519_dalek::{Signer, SigningKey, Verifier, VerifyingKey};
15use serde_json::{Value, json};
16use sha2::{Digest, Sha256};
17use thiserror::Error;
18
19use crate::canonical::canonical;
20use crate::signing::{b64decode, b64encode, make_key_id};
21
22pub const CARD_SCHEMA_VERSION: &str = "v3.1";
23pub const DID_METHOD: &str = "did:wire";
24
25pub fn did_for_with_key(handle: &str, public_key: &[u8]) -> String {
40 if handle.starts_with("did:") {
41 return handle.to_string();
42 }
43 let suffix = crate::signing::fingerprint(public_key);
44 format!("{DID_METHOD}:{handle}-{suffix}")
45}
46
47pub fn did_for(handle: &str) -> String {
53 if handle.starts_with("did:") {
54 handle.to_string()
55 } else {
56 format!("{DID_METHOD}:{handle}")
57 }
58}
59
60pub fn bare_handle(handle: &str) -> &str {
72 handle.split_once('@').map(|(n, _)| n).unwrap_or(handle)
73}
74
75pub fn display_handle_from_did(did: &str) -> &str {
79 let stripped = did.strip_prefix("did:wire:").unwrap_or(did);
80 if let Some(idx) = stripped.rfind('-') {
83 let suffix = &stripped[idx + 1..];
84 if suffix.len() == 8 && suffix.chars().all(|c| c.is_ascii_hexdigit()) {
85 return &stripped[..idx];
86 }
87 }
88 stripped
89}
90
91pub type AgentCard = Value;
94
95#[derive(Debug, Error)]
96pub enum CardError {
97 #[error("missing field: {0}")]
98 MissingField(&'static str),
99 #[error("verify_keys is empty or malformed")]
100 NoVerifyKeys,
101 #[error("signature decode failed")]
102 BadSignature,
103 #[error("signature did not verify")]
104 SignatureRejected,
105}
106
107pub fn build_agent_card(
118 handle: &str,
119 public_key: &[u8],
120 name: Option<&str>,
121 capabilities: Option<Vec<String>>,
122 max_body_kb: Option<u64>,
123) -> AgentCard {
124 let display_name = name
125 .map(str::to_string)
126 .unwrap_or_else(|| capitalize(handle));
127 let caps = capabilities.unwrap_or_else(|| vec!["wire/v3.1".to_string()]);
128 let body_kb = max_body_kb.unwrap_or(64);
129
130 let key_id = make_key_id(handle, public_key);
131 let key_id_full = format!("ed25519:{key_id}");
132
133 json!({
134 "schema_version": CARD_SCHEMA_VERSION,
135 "did": did_for_with_key(handle, public_key),
136 "handle": handle,
137 "name": display_name,
138 "capabilities": caps,
139 "verify_keys": {
140 key_id_full: {
141 "key": b64encode(public_key),
142 "alg": "ed25519",
143 "active": true,
144 }
145 },
146 "policies": {
147 "max_message_body_kb": body_kb,
148 }
149 })
150}
151
152fn capitalize(s: &str) -> String {
154 let mut chars = s.chars();
155 match chars.next() {
156 Some(c) => c.to_uppercase().collect::<String>() + chars.as_str(),
157 None => String::new(),
158 }
159}
160
161pub fn card_canonical(card: &AgentCard) -> Vec<u8> {
163 canonical(card, false)
164}
165
166pub fn sign_agent_card(card: &AgentCard, private_key: &[u8]) -> AgentCard {
169 let mut sk_bytes = [0u8; 32];
170 sk_bytes.copy_from_slice(&private_key[..32]);
171 let sk = SigningKey::from_bytes(&sk_bytes);
172 let sig = sk.sign(&card_canonical(card));
173 let mut out = card.as_object().cloned().unwrap_or_default();
174 out.insert(
175 "signature".into(),
176 Value::String(b64encode(&sig.to_bytes())),
177 );
178 Value::Object(out)
179}
180
181pub fn verify_agent_card(card: &AgentCard) -> Result<(), CardError> {
184 let signature_b64 = card
185 .get("signature")
186 .and_then(Value::as_str)
187 .ok_or(CardError::MissingField("signature"))?;
188
189 let verify_keys = card
190 .get("verify_keys")
191 .and_then(Value::as_object)
192 .ok_or(CardError::MissingField("verify_keys"))?;
193
194 let (_kid, key_record) = verify_keys.iter().next().ok_or(CardError::NoVerifyKeys)?;
195 let pk_b64 = key_record
196 .get("key")
197 .and_then(Value::as_str)
198 .ok_or(CardError::MissingField("verify_keys[*].key"))?;
199 let pk_bytes = b64decode(pk_b64).map_err(|_| CardError::BadSignature)?;
200 if pk_bytes.len() != 32 {
201 return Err(CardError::BadSignature);
202 }
203 let mut pk_arr = [0u8; 32];
204 pk_arr.copy_from_slice(&pk_bytes);
205 let vk = VerifyingKey::from_bytes(&pk_arr).map_err(|_| CardError::BadSignature)?;
206
207 let sig_bytes = b64decode(signature_b64).map_err(|_| CardError::BadSignature)?;
208 if sig_bytes.len() != 64 {
209 return Err(CardError::BadSignature);
210 }
211 let mut sig_arr = [0u8; 64];
212 sig_arr.copy_from_slice(&sig_bytes);
213 let sig = ed25519_dalek::Signature::from_bytes(&sig_arr);
214
215 vk.verify(&card_canonical(card), &sig)
216 .map_err(|_| CardError::SignatureRejected)
217}
218
219pub fn compute_sas(public_key_a: &[u8], public_key_b: &[u8]) -> String {
225 let (lo, hi) = if public_key_a <= public_key_b {
226 (public_key_a, public_key_b)
227 } else {
228 (public_key_b, public_key_a)
229 };
230 let mut h = Sha256::new();
231 h.update(lo);
232 h.update(hi);
233 let digest = h.finalize();
234 let n = u32::from_be_bytes([digest[28], digest[29], digest[30], digest[31]]);
236 format!("{:06}", n % 1_000_000)
237}
238
239#[cfg(test)]
240mod tests {
241 use super::*;
242 use crate::signing::generate_keypair;
243
244 #[test]
245 fn did_for_handle() {
246 assert_eq!(did_for("paul"), "did:wire:paul");
247 }
248
249 #[test]
250 fn did_for_already_did_passthrough() {
251 assert_eq!(did_for("did:wire:paul"), "did:wire:paul");
252 assert_eq!(did_for("did:key:abc"), "did:key:abc");
253 }
254
255 #[test]
256 fn did_method_constant() {
257 assert_eq!(DID_METHOD, "did:wire");
258 }
259
260 #[test]
261 fn build_minimal_card() {
262 let (_, pk) = generate_keypair();
263 let card = build_agent_card("paul", &pk, None, None, None);
264 assert_eq!(card["schema_version"], CARD_SCHEMA_VERSION);
265 let did = card["did"].as_str().unwrap();
267 assert!(did.starts_with("did:wire:paul-"), "got: {did}");
268 assert_eq!(did.len(), "did:wire:paul-".len() + 8);
269 assert_eq!(card["handle"], "paul");
270 assert_eq!(card["name"], "Paul");
271 let vks = card["verify_keys"].as_object().unwrap();
272 assert_eq!(vks.len(), 1);
273 assert_eq!(card["policies"]["max_message_body_kb"], 64);
274 }
275
276 #[test]
277 fn build_card_with_overrides() {
278 let (_, pk) = generate_keypair();
279 let card = build_agent_card(
280 "carol",
281 &pk,
282 Some("Carol's Agent"),
283 Some(vec!["custom-cap".to_string()]),
284 Some(128),
285 );
286 assert_eq!(card["name"], "Carol's Agent");
287 assert_eq!(card["capabilities"], json!(["custom-cap"]));
288 assert_eq!(card["policies"]["max_message_body_kb"], 128);
289 }
290
291 #[test]
292 fn build_card_does_not_carry_v02_fields() {
293 let (_, pk) = generate_keypair();
294 let card = build_agent_card("paul", &pk, None, None, None);
295 let obj = card.as_object().unwrap();
296 for v02 in [
297 "registries",
298 "onboard_endpoint",
299 "wire_raw_url_template",
300 "revoked_at",
301 ] {
302 assert!(
303 !obj.contains_key(v02),
304 "v0.2+ field {v02} leaked into v0.1 card"
305 );
306 }
307 }
308
309 #[test]
310 fn card_canonical_excludes_signature() {
311 let v = json!({"schema_version": "v3.1", "did": "did:wire:paul", "signature": "sig"});
312 let bytes = card_canonical(&v);
313 assert!(!String::from_utf8_lossy(&bytes).contains("signature"));
314 }
315
316 #[test]
317 fn card_canonical_sort_keys_stable() {
318 let a = json!({"b": 1, "a": 2, "did": "did:wire:paul"});
319 let b = json!({"did": "did:wire:paul", "a": 2, "b": 1});
320 assert_eq!(card_canonical(&a), card_canonical(&b));
321 }
322
323 #[test]
324 fn sign_verify_roundtrip() {
325 let (sk, pk) = generate_keypair();
326 let card = build_agent_card("paul", &pk, None, None, None);
327 let signed = sign_agent_card(&card, &sk);
328 assert!(signed.get("signature").is_some());
329 verify_agent_card(&signed).unwrap();
330 }
331
332 #[test]
333 fn verify_rejects_unsigned_card() {
334 let (_, pk) = generate_keypair();
335 let card = build_agent_card("paul", &pk, None, None, None);
336 let err = verify_agent_card(&card).unwrap_err();
337 assert!(matches!(err, CardError::MissingField("signature")));
338 }
339
340 #[test]
341 fn verify_rejects_tampered_card() {
342 let (sk, pk) = generate_keypair();
343 let mut signed = sign_agent_card(&build_agent_card("paul", &pk, None, None, None), &sk);
344 signed["name"] = json!("TamperedName");
345 let err = verify_agent_card(&signed).unwrap_err();
346 assert!(matches!(err, CardError::SignatureRejected));
347 }
348
349 #[test]
350 fn verify_rejects_card_with_no_verify_keys() {
351 let (sk, _) = generate_keypair();
352 let card = json!({"schema_version": "v3.1", "did": "did:wire:paul", "verify_keys": {}});
353 let signed = sign_agent_card(&card, &sk);
354 let err = verify_agent_card(&signed).unwrap_err();
355 assert!(matches!(err, CardError::NoVerifyKeys));
356 }
357
358 #[test]
359 fn compute_sas_is_6_digits() {
360 let (_, a) = generate_keypair();
361 let (_, b) = generate_keypair();
362 let sas = compute_sas(&a, &b);
363 assert_eq!(sas.len(), 6);
364 assert!(sas.chars().all(|c| c.is_ascii_digit()));
365 }
366
367 #[test]
368 fn compute_sas_bilateral_symmetric() {
369 let (_, a) = generate_keypair();
370 let (_, b) = generate_keypair();
371 assert_eq!(compute_sas(&a, &b), compute_sas(&b, &a));
372 }
373
374 #[test]
375 fn compute_sas_changes_with_inputs() {
376 let (_, a) = generate_keypair();
377 let (_, b) = generate_keypair();
378 let (_, c) = generate_keypair();
379 assert_ne!(compute_sas(&a, &b), compute_sas(&a, &c));
380 }
381}