auths_core/crypto/said.rs
1//! SAID (Self-Addressing Identifier) computation for KERI.
2//!
3//! This module provides functions for computing SAIDs and next-key commitments
4//! as specified by KERI (Key Event Receipt Infrastructure).
5//!
6//! SAIDs use Blake3 hashing with Base64url encoding and an 'E' prefix
7//! (derivation code for Blake3-256).
8
9use auths_verifier::keri::Said;
10use base64::{Engine, engine::general_purpose::URL_SAFE_NO_PAD};
11
12/// Compute SAID (Self-Addressing Identifier) for a KERI event.
13///
14// SYNC: must match auths-verifier/src/keri.rs — tested by said_cross_validation
15/// The SAID is computed by:
16/// 1. Hashing the input with Blake3
17/// 2. Encoding the hash as Base64url (no padding)
18/// 3. Prefixing with 'E' (KERI derivation code for Blake3-256)
19///
20/// # Arguments
21/// * `event_json` - The canonical JSON bytes of the event (with 'd' field empty)
22///
23/// # Returns
24/// A `Said` wrapping a string like "EXq5YqaL6L48pf0fu7IUhL0JRaU2_RxFP0AL43wYn148"
25pub fn compute_said(event_json: &[u8]) -> Said {
26 let hash = blake3::hash(event_json);
27 let encoded = URL_SAFE_NO_PAD.encode(hash.as_bytes());
28 Said::new_unchecked(format!("E{}", encoded))
29}
30
31/// Compute next-key commitment hash for pre-rotation.
32///
33// SYNC: must match auths-verifier/src/keri.rs — tested by said_cross_validation
34/// The commitment is computed by:
35/// 1. Hashing the public key bytes with Blake3
36/// 2. Encoding the hash as Base64url (no padding)
37/// 3. Prefixing with 'E' (KERI derivation code for Blake3-256)
38///
39/// This commitment is included in the current event's 'n' field and must
40/// be satisfied by the next rotation event's 'k' field.
41///
42/// # Arguments
43/// * `public_key` - The raw public key bytes (32 bytes for Ed25519)
44///
45/// # Returns
46/// A commitment string like "EO8CE5RH3wHBrXyFay3MOXq5YqaL6L48pf0fu7IUhL0J"
47pub fn compute_next_commitment(public_key: &[u8]) -> String {
48 let hash = blake3::hash(public_key);
49 let encoded = URL_SAFE_NO_PAD.encode(hash.as_bytes());
50 format!("E{}", encoded)
51}
52
53/// Verify that a public key matches a commitment.
54///
55/// # Arguments
56/// * `public_key` - The raw public key bytes to verify
57/// * `commitment` - The commitment string from a previous event's 'n' field
58///
59/// # Returns
60/// `true` if the public key hashes to the commitment, `false` otherwise
61pub fn verify_commitment(public_key: &[u8], commitment: &str) -> bool {
62 compute_next_commitment(public_key) == commitment
63}
64
65#[cfg(test)]
66mod tests {
67 use super::*;
68
69 #[test]
70 fn said_is_deterministic() {
71 let json = b"{\"t\":\"icp\",\"s\":\"0\"}";
72 let said1 = compute_said(json);
73 let said2 = compute_said(json);
74 assert_eq!(said1, said2);
75 assert!(said1.as_str().starts_with('E'));
76 }
77
78 #[test]
79 fn said_has_correct_length() {
80 let json = b"{\"test\":\"data\"}";
81 let said = compute_said(json);
82 // 'E' + 43 chars of base64url (32 bytes encoded)
83 assert_eq!(said.as_str().len(), 44);
84 }
85
86 #[test]
87 fn different_inputs_produce_different_saids() {
88 let said1 = compute_said(b"{\"a\":1}");
89 let said2 = compute_said(b"{\"a\":2}");
90 assert_ne!(said1, said2);
91 }
92
93 #[test]
94 fn commitment_verification_works() {
95 let key = [1u8; 32];
96 let commitment = compute_next_commitment(&key);
97 assert!(verify_commitment(&key, &commitment));
98 assert!(!verify_commitment(&[2u8; 32], &commitment));
99 }
100
101 #[test]
102 fn commitment_is_deterministic() {
103 let key = [42u8; 32];
104 let c1 = compute_next_commitment(&key);
105 let c2 = compute_next_commitment(&key);
106 assert_eq!(c1, c2);
107 assert!(c1.starts_with('E'));
108 }
109
110 #[test]
111 fn commitment_has_correct_length() {
112 let key = [0u8; 32];
113 let commitment = compute_next_commitment(&key);
114 // 'E' + 43 chars of base64url
115 assert_eq!(commitment.len(), 44);
116 }
117}