Skip to main content

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}