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