1use ed25519_dalek::{Signature as EdSignature, Signer as _, SigningKey, Verifier, VerifyingKey};
11use hmac::{Hmac, Mac};
12use pqcrypto_mldsa::mldsa65 as mldsa;
13use pqcrypto_traits::sign::{DetachedSignature, PublicKey as _, SecretKey as _};
14use sha2::{Digest, Sha256};
15
16use crate::canonical::canonical_json;
17use crate::types::{
18 AgentIdentity, DelegationCert, HumanRoot, HybridPrivateKey, HybridPublicKey, HybridSignature,
19 KeyRotationStatement, ProofBundle, ReceiptPartySignature, RevocationList, RevocationPush,
20 SessionToken, TransactionReceipt, VerifyResult, WitnessEntry,
21};
22use serde_json::json;
23
24type HmacSha256 = Hmac<Sha256>;
25
26pub fn derive_id(pub_key: &HybridPublicKey) -> String {
32 let mut hasher = Sha256::new();
33 hasher.update(&pub_key.ed25519);
34 hasher.update(&pub_key.ml_dsa_65);
35 let digest = hasher.finalize();
36 hex::encode(&digest[..16])
37}
38
39pub fn generate_hybrid_keypair() -> (HybridPublicKey, HybridPrivateKey) {
45 use rand_core::OsRng;
46 let mut seed = [0u8; 32];
47 use rand_core::RngCore;
48 OsRng.fill_bytes(&mut seed);
49 let ed_sk = SigningKey::from_bytes(&seed);
50 let ed_pk = ed_sk.verifying_key();
51
52 let (ml_pk, ml_sk) = mldsa::keypair();
53
54 (
55 HybridPublicKey {
56 ed25519: ed_pk.to_bytes().to_vec(),
57 ml_dsa_65: ml_pk.as_bytes().to_vec(),
58 },
59 HybridPrivateKey {
60 ed25519: seed.to_vec(),
61 ml_dsa_65: ml_sk.as_bytes().to_vec(),
62 },
63 )
64}
65
66pub fn generate_human_root() -> (HumanRoot, HybridPrivateKey) {
68 let (pub_key, priv_key) = generate_hybrid_keypair();
69 let id = derive_id(&pub_key);
70 (
71 HumanRoot {
72 id,
73 public_key: pub_key,
74 created_at: now_unix(),
75 anchors: None,
76 },
77 priv_key,
78 )
79}
80
81pub fn generate_agent(name: &str, agent_type: &str) -> (AgentIdentity, HybridPrivateKey) {
83 let (pub_key, priv_key) = generate_hybrid_keypair();
84 let id = derive_id(&pub_key);
85 (
86 AgentIdentity {
87 id,
88 public_key: pub_key,
89 name: name.to_string(),
90 agent_type: agent_type.to_string(),
91 created_at: now_unix(),
92 },
93 priv_key,
94 )
95}
96
97fn now_unix() -> i64 {
98 use std::time::{SystemTime, UNIX_EPOCH};
99 SystemTime::now()
100 .duration_since(UNIX_EPOCH)
101 .unwrap_or_default()
102 .as_secs() as i64
103}
104
105pub fn delegation_sign_bytes(cert: &DelegationCert) -> Vec<u8> {
116 let signable = json!({
117 "cert_id": cert.cert_id,
118 "constraints": cert.constraints,
119 "expires_at": cert.expires_at,
120 "issued_at": cert.issued_at,
121 "issuer_id": cert.issuer_id,
122 "issuer_pub_key": {
123 "ed25519": crate::canonical::base64_std_encode(&cert.issuer_pub_key.ed25519),
124 "ml_dsa_65": crate::canonical::base64_std_encode(&cert.issuer_pub_key.ml_dsa_65),
125 },
126 "scope": cert.scope,
127 "subject_id": cert.subject_id,
128 "subject_pub_key": {
129 "ed25519": crate::canonical::base64_std_encode(&cert.subject_pub_key.ed25519),
130 "ml_dsa_65": crate::canonical::base64_std_encode(&cert.subject_pub_key.ml_dsa_65),
131 },
132 "version": cert.version,
133 });
134 canonical_json(&signable)
135}
136
137pub fn challenge_sign_bytes(challenge: &[u8], ts: i64) -> Vec<u8> {
141 challenge_sign_bytes_with_stream(challenge, ts, &[], &[], 0)
142}
143
144pub fn challenge_sign_bytes_with_session_context(
147 challenge: &[u8],
148 ts: i64,
149 session_context: &[u8],
150) -> Vec<u8> {
151 challenge_sign_bytes_with_stream(challenge, ts, session_context, &[], 0)
152}
153
154pub fn challenge_sign_bytes_with_stream(
161 challenge: &[u8],
162 ts: i64,
163 session_context: &[u8],
164 stream_id: &[u8],
165 stream_seq: i64,
166) -> Vec<u8> {
167 let stream_len = if stream_id.is_empty() {
168 0
169 } else {
170 stream_id.len() + 8
171 };
172 let mut out = Vec::with_capacity(challenge.len() + 8 + session_context.len() + stream_len);
173 out.extend_from_slice(challenge);
174 out.extend_from_slice(&(ts as u64).to_be_bytes());
175 out.extend_from_slice(session_context);
176 if !stream_id.is_empty() {
177 out.extend_from_slice(stream_id);
178 out.extend_from_slice(&(stream_seq as u64).to_be_bytes());
179 }
180 out
181}
182
183pub fn revocation_sign_bytes(list: &RevocationList) -> Vec<u8> {
185 let signable = json!({
186 "issuer_id": list.issuer_id,
187 "revoked_certs": list.revoked_certs,
188 "updated_at": list.updated_at,
189 });
190 canonical_json(&signable)
191}
192
193pub fn key_rotation_sign_bytes(stmt: &KeyRotationStatement) -> Vec<u8> {
195 let signable = json!({
196 "new_id": stmt.new_id,
197 "new_pub_key": {
198 "ed25519": crate::canonical::base64_std_encode(&stmt.new_pub_key.ed25519),
199 "ml_dsa_65": crate::canonical::base64_std_encode(&stmt.new_pub_key.ml_dsa_65),
200 },
201 "old_id": stmt.old_id,
202 "old_pub_key": {
203 "ed25519": crate::canonical::base64_std_encode(&stmt.old_pub_key.ed25519),
204 "ml_dsa_65": crate::canonical::base64_std_encode(&stmt.old_pub_key.ml_dsa_65),
205 },
206 "reason": stmt.reason,
207 "rotated_at": stmt.rotated_at,
208 "version": stmt.version,
209 });
210 canonical_json(&signable)
211}
212
213pub fn sign_both(msg: &[u8], priv_key: &HybridPrivateKey) -> HybridSignature {
219 let mut ed_seed = [0u8; 32];
220 ed_seed.copy_from_slice(&priv_key.ed25519[..32]);
221 let ed_sk = SigningKey::from_bytes(&ed_seed);
222 let ed_sig = ed_sk.sign(msg);
223
224 let ml_sk =
225 mldsa::SecretKey::from_bytes(&priv_key.ml_dsa_65).expect("ML-DSA-65 secret key malformed");
226 let ml_sig = mldsa::detached_sign(msg, &ml_sk);
227
228 HybridSignature {
229 ed25519: ed_sig.to_bytes().to_vec(),
230 ml_dsa_65: ml_sig.as_bytes().to_vec(),
231 }
232}
233
234pub fn verify_both(
236 msg: &[u8],
237 sig: &HybridSignature,
238 pub_key: &HybridPublicKey,
239) -> Result<(), String> {
240 if pub_key.ed25519.len() != 32 {
241 return Err(format!(
242 "Ed25519 public key wrong length: {}",
243 pub_key.ed25519.len()
244 ));
245 }
246 if pub_key.ml_dsa_65.len() != 1952 {
247 return Err(format!(
248 "ML-DSA-65 public key wrong length: {}",
249 pub_key.ml_dsa_65.len()
250 ));
251 }
252 if sig.ed25519.len() != 64 {
253 return Err(format!(
254 "Ed25519 signature wrong length: {}",
255 sig.ed25519.len()
256 ));
257 }
258 if sig.ml_dsa_65.len() != 3309 {
259 return Err(format!(
260 "ML-DSA-65 signature wrong length: {}",
261 sig.ml_dsa_65.len()
262 ));
263 }
264
265 let mut ed_pk_bytes = [0u8; 32];
266 ed_pk_bytes.copy_from_slice(&pub_key.ed25519);
267 let ed_pk = VerifyingKey::from_bytes(&ed_pk_bytes)
268 .map_err(|_| "Ed25519 public key invalid".to_string())?;
269 let ed_sig = EdSignature::from_slice(&sig.ed25519)
270 .map_err(|_| "Ed25519 signature invalid".to_string())?;
271 ed_pk
272 .verify(msg, &ed_sig)
273 .map_err(|_| "Ed25519 signature invalid".to_string())?;
274
275 let ml_pk = mldsa::PublicKey::from_bytes(&pub_key.ml_dsa_65)
276 .map_err(|_| "ML-DSA-65 public key malformed".to_string())?;
277 let ml_sig = mldsa::DetachedSignature::from_bytes(&sig.ml_dsa_65)
278 .map_err(|_| "ML-DSA-65 signature malformed".to_string())?;
279 mldsa::verify_detached_signature(&ml_sig, msg, &ml_pk)
280 .map_err(|_| "ML-DSA-65 signature invalid".to_string())?;
281
282 Ok(())
283}
284
285pub fn issue_delegation(cert: &mut DelegationCert, issuer_priv: &HybridPrivateKey) {
290 cert.signature = sign_both(&delegation_sign_bytes(cert), issuer_priv);
291}
292
293pub fn verify_delegation_signature(cert: &DelegationCert) -> bool {
294 verify_delegation_signature_e(cert).is_ok()
295}
296
297pub fn verify_delegation_signature_e(cert: &DelegationCert) -> Result<(), String> {
298 verify_both(
299 &delegation_sign_bytes(cert),
300 &cert.signature,
301 &cert.issuer_pub_key,
302 )
303}
304
305pub fn sign_challenge(challenge: &[u8], ts: i64, agent_priv: &HybridPrivateKey) -> HybridSignature {
306 sign_challenge_with_session_context(challenge, ts, &[], agent_priv)
307}
308
309pub fn sign_challenge_with_session_context(
310 challenge: &[u8],
311 ts: i64,
312 session_context: &[u8],
313 agent_priv: &HybridPrivateKey,
314) -> HybridSignature {
315 assert!(
316 session_context.is_empty() || session_context.len() == 32,
317 "session_context must be 32 bytes"
318 );
319 sign_both(
320 &challenge_sign_bytes_with_session_context(challenge, ts, session_context),
321 agent_priv,
322 )
323}
324
325pub fn sign_challenge_with_stream(
326 challenge: &[u8],
327 ts: i64,
328 session_context: &[u8],
329 stream_id: &[u8],
330 stream_seq: i64,
331 agent_priv: &HybridPrivateKey,
332) -> HybridSignature {
333 assert!(
334 session_context.is_empty() || session_context.len() == 32,
335 "session_context must be 32 bytes"
336 );
337 assert_eq!(stream_id.len(), 32, "stream_id must be 32 bytes");
338 assert!(stream_seq >= 1, "stream_seq must be >=1");
339 sign_both(
340 &challenge_sign_bytes_with_stream(challenge, ts, session_context, stream_id, stream_seq),
341 agent_priv,
342 )
343}
344
345pub fn verify_challenge_signature(
346 challenge: &[u8],
347 ts: i64,
348 sig: &HybridSignature,
349 agent_pub: &HybridPublicKey,
350) -> Result<(), String> {
351 verify_challenge_signature_with_stream(challenge, ts, &[], &[], 0, sig, agent_pub)
352}
353
354pub fn verify_challenge_signature_with_session_context(
355 challenge: &[u8],
356 ts: i64,
357 session_context: &[u8],
358 sig: &HybridSignature,
359 agent_pub: &HybridPublicKey,
360) -> Result<(), String> {
361 verify_challenge_signature_with_stream(challenge, ts, session_context, &[], 0, sig, agent_pub)
362}
363
364pub fn verify_challenge_signature_with_stream(
365 challenge: &[u8],
366 ts: i64,
367 session_context: &[u8],
368 stream_id: &[u8],
369 stream_seq: i64,
370 sig: &HybridSignature,
371 agent_pub: &HybridPublicKey,
372) -> Result<(), String> {
373 if !session_context.is_empty() && session_context.len() != 32 {
374 return Err(format!(
375 "session_context must be 32 bytes, got {}",
376 session_context.len()
377 ));
378 }
379 if !stream_id.is_empty() && stream_id.len() != 32 {
380 return Err(format!(
381 "stream_id must be 32 bytes, got {}",
382 stream_id.len()
383 ));
384 }
385 if !stream_id.is_empty() && stream_seq < 1 {
386 return Err(format!("stream_seq must be >=1, got {}", stream_seq));
387 }
388 verify_both(
389 &challenge_sign_bytes_with_stream(challenge, ts, session_context, stream_id, stream_seq),
390 sig,
391 agent_pub,
392 )
393}
394
395pub fn issue_revocation_list(list: &mut RevocationList, issuer_priv: &HybridPrivateKey) {
396 list.signature = sign_both(&revocation_sign_bytes(list), issuer_priv);
397}
398
399pub fn verify_revocation_list(list: &RevocationList, issuer_pub: &HybridPublicKey) -> bool {
400 verify_both(&revocation_sign_bytes(list), &list.signature, issuer_pub).is_ok()
401}
402
403pub fn revocation_push_sign_bytes(push: &RevocationPush) -> Vec<u8> {
405 let signable = json!({
406 "entries": push.entries,
407 "issuer_id": push.issuer_id,
408 "pushed_at": push.pushed_at,
409 "seq_no": push.seq_no,
410 });
411 canonical_json(&signable)
412}
413
414pub fn issue_revocation_push(push: &mut RevocationPush, issuer_priv: &HybridPrivateKey) {
415 push.signature = sign_both(&revocation_push_sign_bytes(push), issuer_priv);
416}
417
418pub fn verify_revocation_push(push: &RevocationPush, issuer_pub: &HybridPublicKey) -> bool {
419 verify_both(
420 &revocation_push_sign_bytes(push),
421 &push.signature,
422 issuer_pub,
423 )
424 .is_ok()
425}
426
427pub fn witness_entry_sign_bytes(entry: &WitnessEntry) -> Vec<u8> {
429 let signable = json!({
430 "entry_data": crate::canonical::base64_std_encode(&entry.entry_data),
431 "prev_hash": crate::canonical::base64_std_encode(&entry.prev_hash),
432 "timestamp": entry.timestamp,
433 "witness_id": entry.witness_id,
434 });
435 canonical_json(&signable)
436}
437
438pub fn issue_witness_entry(entry: &mut WitnessEntry, witness_priv: &HybridPrivateKey) {
439 entry.signature = sign_both(&witness_entry_sign_bytes(entry), witness_priv);
440}
441
442pub fn verify_witness_entry(entry: &WitnessEntry, witness_pub: &HybridPublicKey) -> bool {
443 verify_both(
444 &witness_entry_sign_bytes(entry),
445 &entry.signature,
446 witness_pub,
447 )
448 .is_ok()
449}
450
451pub fn issue_key_rotation_statement(
452 stmt: &mut KeyRotationStatement,
453 old_priv: &HybridPrivateKey,
454 new_priv: &HybridPrivateKey,
455) {
456 let bytes = key_rotation_sign_bytes(stmt);
457 stmt.signature_old = sign_both(&bytes, old_priv);
458 stmt.signature_new = sign_both(&bytes, new_priv);
459}
460
461pub fn verify_key_rotation_statement(stmt: &KeyRotationStatement) -> Result<(), String> {
462 if stmt.version != 1 {
463 return Err(format!(
464 "version_mismatch: unsupported version {}",
465 stmt.version
466 ));
467 }
468 if stmt.old_id != derive_id(&stmt.old_pub_key) {
469 return Err("old_id does not match old_pub_key".to_string());
470 }
471 if stmt.new_id != derive_id(&stmt.new_pub_key) {
472 return Err("new_id does not match new_pub_key".to_string());
473 }
474 if stmt.old_id == stmt.new_id {
475 return Err("old_id and new_id must differ".to_string());
476 }
477 if !is_key_rotation_reason_known(&stmt.reason) {
478 return Err(format!("unknown key rotation reason: {}", stmt.reason));
479 }
480 let bytes = key_rotation_sign_bytes(stmt);
481 verify_both(&bytes, &stmt.signature_old, &stmt.old_pub_key)
482 .map_err(|e| format!("old signature invalid: {}", e))?;
483 verify_both(&bytes, &stmt.signature_new, &stmt.new_pub_key)
484 .map_err(|e| format!("new signature invalid: {}", e))?;
485 Ok(())
486}
487
488fn is_key_rotation_reason_known(reason: &str) -> bool {
489 matches!(
490 reason,
491 "routine" | "compromise_suspected" | "device_lost" | "recovery" | "other"
492 )
493}
494
495pub fn generate_challenge() -> Vec<u8> {
497 use rand_core::{OsRng, RngCore};
498 let mut b = [0u8; 32];
499 OsRng.fill_bytes(&mut b);
500 b.to_vec()
501}
502
503pub fn transaction_receipt_sign_bytes(receipt: &TransactionReceipt) -> Vec<u8> {
510 let mut parties: Vec<serde_json::Value> = receipt
511 .parties
512 .iter()
513 .map(|p| {
514 json!({
515 "agent_id": p.agent_id,
516 "agent_pub_key": {
517 "ed25519": crate::canonical::base64_std_encode(&p.agent_pub_key.ed25519),
518 "ml_dsa_65": crate::canonical::base64_std_encode(&p.agent_pub_key.ml_dsa_65),
519 },
520 "party_id": p.party_id,
521 "role": p.role,
522 })
523 })
524 .collect();
525 parties.sort_by(|a, b| {
526 let a_id = a["party_id"].as_str().unwrap_or("");
527 let b_id = b["party_id"].as_str().unwrap_or("");
528 a_id.cmp(b_id)
529 });
530 let signable = json!({
531 "created_at": receipt.created_at,
532 "parties": parties,
533 "terms_canonical_json": crate::canonical::base64_std_encode(&receipt.terms_canonical_json),
534 "terms_schema_uri": receipt.terms_schema_uri,
535 "transaction_id": receipt.transaction_id,
536 "version": receipt.version,
537 });
538 canonical_json(&signable)
539}
540
541pub fn sign_transaction_receipt_party(
543 receipt: &TransactionReceipt,
544 party_id: &str,
545 agent_priv: &HybridPrivateKey,
546) -> ReceiptPartySignature {
547 let data = transaction_receipt_sign_bytes(receipt);
548 let sig = sign_both(&data, agent_priv);
549 ReceiptPartySignature {
550 party_id: party_id.to_string(),
551 signature: sig,
552 }
553}
554
555pub fn chain_hash(chain: &[DelegationCert]) -> Vec<u8> {
563 let mut hasher = Sha256::new();
564 for cert in chain {
565 hasher.update(delegation_sign_bytes(cert));
566 }
567 hasher.finalize().to_vec()
568}
569
570pub fn session_token_sign_bytes(token: &SessionToken) -> Vec<u8> {
573 let mut scope = token.granted_scope.clone();
574 scope.sort();
575 let signable = json!({
576 "agent_id": token.agent_id,
577 "agent_pub_key": {
578 "ed25519": crate::canonical::base64_std_encode(&token.agent_pub_key.ed25519),
579 "ml_dsa_65": crate::canonical::base64_std_encode(&token.agent_pub_key.ml_dsa_65),
580 },
581 "chain_hash": crate::canonical::base64_std_encode(&token.chain_hash),
582 "granted_scope": scope,
583 "human_id": token.human_id,
584 "issued_at": token.issued_at,
585 "session_id": token.session_id,
586 "valid_until": token.valid_until,
587 "version": token.version,
588 });
589 canonical_json(&signable)
590}
591
592pub fn issue_session_token(
595 bundle: &ProofBundle,
596 result: &VerifyResult,
597 session_id: &str,
598 issued_at: i64,
599 valid_until: i64,
600 session_secret: &[u8],
601) -> Result<SessionToken, String> {
602 if session_secret.is_empty() {
603 return Err("session_secret must not be empty".to_string());
604 }
605 if session_id.is_empty() {
606 return Err("session_id must not be empty".to_string());
607 }
608 if valid_until <= issued_at {
609 return Err("valid_until must be strictly after issued_at".to_string());
610 }
611 let mut scope = result.granted_scope.clone();
612 scope.sort();
613 let mut token = SessionToken {
614 version: 1,
615 session_id: session_id.to_string(),
616 agent_id: result.agent_id.clone(),
617 agent_pub_key: bundle.agent_pub_key.clone(),
618 human_id: result.human_id.clone(),
619 granted_scope: scope,
620 issued_at,
621 valid_until,
622 chain_hash: chain_hash(&bundle.delegations),
623 mac: Vec::new(),
624 };
625 let signable = session_token_sign_bytes(&token);
626 let mut mac =
627 HmacSha256::new_from_slice(session_secret).map_err(|e| format!("init HMAC: {}", e))?;
628 mac.update(&signable);
629 token.mac = mac.finalize().into_bytes().to_vec();
630 Ok(token)
631}
632
633pub fn verify_session_token_e(
636 token: &SessionToken,
637 session_secret: &[u8],
638 now: i64,
639) -> Result<(), String> {
640 if session_secret.is_empty() {
641 return Err("session_secret must not be empty".to_string());
642 }
643 if token.version != 1 {
644 return Err(format!(
645 "version_mismatch: unsupported version {}",
646 token.version
647 ));
648 }
649 if token.chain_hash.len() != 32 {
650 return Err(format!(
651 "chain_hash must be 32 bytes, got {}",
652 token.chain_hash.len()
653 ));
654 }
655 if token.mac.len() != 32 {
656 return Err(format!("mac must be 32 bytes, got {}", token.mac.len()));
657 }
658 let mut mac =
659 HmacSha256::new_from_slice(session_secret).map_err(|e| format!("init HMAC: {}", e))?;
660 mac.update(&session_token_sign_bytes(token));
661 mac.verify_slice(&token.mac)
662 .map_err(|_| "session_token MAC invalid".to_string())?;
663 if now < token.issued_at {
664 return Err("session_token not yet valid".to_string());
665 }
666 if now > token.valid_until {
667 return Err("session_token expired".to_string());
668 }
669 Ok(())
670}
671
672pub fn verify_session_token(token: &SessionToken, session_secret: &[u8], now: i64) -> bool {
673 verify_session_token_e(token, session_secret, now).is_ok()
674}