auths_keri/crypto.rs
1//! KERI pre-rotation commitment functions.
2//!
3//! Key commitment hashes for pre-rotation are computed here.
4//! SAID computation lives in `said.rs`.
5
6use subtle::ConstantTimeEq;
7
8use crate::keys::KeriPublicKey;
9use crate::types::Said;
10
11/// Compute the next-key commitment digest for pre-rotation.
12///
13/// The commitment is the Blake3-256 digest of the next verkey, CESR-encoded with
14/// the `E` derivation code. It goes in the current event's `n` field and must be
15/// satisfied by the next rotation's `k`.
16///
17/// The key is typed (`KeriPublicKey`) so its curve travels with it ā the curve is
18/// required to encode the verkey, and a typed key makes a "key without a curve"
19/// unrepresentable at the call site.
20///
21/// Args:
22/// * `key` - The next public key (Ed25519 or P-256), carrying its curve.
23///
24/// Usage:
25/// ```
26/// use auths_keri::{compute_next_commitment, KeriPublicKey};
27/// let commitment = compute_next_commitment(&KeriPublicKey::Ed25519([0u8; 32]));
28/// assert_eq!(commitment.as_str().len(), 44);
29/// assert!(commitment.as_str().starts_with('E'));
30/// ```
31pub fn compute_next_commitment(key: &KeriPublicKey) -> Said {
32 // keripy: the next-key commitment is Diger(ser=verfer.qb64b) ā the Blake3-256
33 // digest of the CESR-qualified verkey *text*, itself CESR-encoded (`Eā¦`). The
34 // typed `key` carries the curve needed to produce that qualified form.
35 #[allow(clippy::expect_used)] // INVARIANT: a valid KeriPublicKey always CESR-encodes
36 let qb64 = key
37 .to_qb64()
38 .expect("a valid KeriPublicKey always CESR-encodes");
39 let hash = blake3::hash(qb64.as_bytes());
40 #[allow(clippy::expect_used)] // INVARIANT: a 32-byte Blake3 digest always CESR-encodes
41 let said = crate::cesr_encode::encode_blake3_digest(hash.as_bytes())
42 .expect("32-byte Blake3 digest always encodes");
43 Said::new_unchecked(said)
44}
45
46/// Verify that a public key satisfies a commitment.
47///
48/// Args:
49/// * `key` - The next public key to check, carrying its curve.
50/// * `commitment` - The commitment `Said` from a previous event's `n` field.
51///
52/// Usage:
53/// ```
54/// use auths_keri::{compute_next_commitment, verify_commitment, KeriPublicKey};
55/// let key = KeriPublicKey::Ed25519([1u8; 32]);
56/// let c = compute_next_commitment(&key);
57/// assert!(verify_commitment(&key, &c));
58/// assert!(!verify_commitment(&KeriPublicKey::Ed25519([2u8; 32]), &c));
59/// ```
60// Defense-in-depth: both values are derived from public data, but constant-time
61// comparison prevents timing side-channels on commitment verification.
62pub fn verify_commitment(key: &KeriPublicKey, commitment: &Said) -> bool {
63 let computed = compute_next_commitment(key);
64 computed
65 .as_str()
66 .as_bytes()
67 .ct_eq(commitment.as_str().as_bytes())
68 .into()
69}
70
71#[cfg(test)]
72mod tests {
73 use super::*;
74
75 #[test]
76 fn commitment_verification_works() {
77 let key = KeriPublicKey::Ed25519([1u8; 32]);
78 let commitment = compute_next_commitment(&key);
79 assert!(verify_commitment(&key, &commitment));
80 assert!(!verify_commitment(
81 &KeriPublicKey::Ed25519([2u8; 32]),
82 &commitment
83 ));
84 }
85
86 #[test]
87 fn commitment_is_deterministic() {
88 let key = KeriPublicKey::Ed25519([42u8; 32]);
89 let c1 = compute_next_commitment(&key);
90 let c2 = compute_next_commitment(&key);
91 assert_eq!(c1, c2);
92 assert!(c1.as_str().starts_with('E'));
93 }
94
95 #[test]
96 fn commitment_has_correct_length() {
97 let commitment = compute_next_commitment(&KeriPublicKey::Ed25519([0u8; 32]));
98 // 'E' + 43 chars of base64url
99 assert_eq!(commitment.as_str().len(), 44);
100 }
101}