Skip to main content

metamorphic_log/
vrf.rs

1//! Layer-3a: the swappable **verifiable random function (VRF)** abstraction.
2//!
3//! A VRF is the engine behind CONIKS-style *index privacy* ([`crate::coniks`]):
4//! it maps a (private) identity index to a pseudorandom value `beta` together
5//! with a proof `pi` that `beta` was computed correctly under a published VRF
6//! public key. The directory places each identity at the tree position derived
7//! from `beta`, so the position is verifiable and non-equivocable, yet the
8//! directory never has to reveal which identities it holds.
9//!
10//! ## Why a trait
11//!
12//! The VRF construction is deliberately **pluggable** behind the [`Vrf`] trait,
13//! for two reasons spelled out in the project's VRF research (#304):
14//!
15//! 1. **A post-quantum future.** Today's default is classical
16//!    ([`Ecvrf`], RFC 9381 ECVRF-edwards25519-SHA512-TAI). There is no audited,
17//!    production-grade lattice VRF yet, so a post-quantum VRF is **not built**.
18//!    When one exists, it becomes another `Vrf` implementation; nothing else in
19//!    the engine changes.
20//! 2. **A hybrid path that is safe to design in now.** The combined output of a
21//!    classical and a post-quantum VRF can be mixed via SHA3-512 so the result
22//!    stays pseudorandom if *either* half is secure (closing the
23//!    harvest-now/decrypt-later de-anonymisation exposure), while *uniqueness*
24//!    stays anchored on the audited classical half. That output combiner —
25//!    [`hybrid_output`] — is implemented here because it needs no lattice
26//!    crypto; only the post-quantum `Vrf` half it would consume is missing.
27//!
28//! The trait is intentionally **byte-oriented and object-safe**
29//! ([`VrfSecretKey`] / [`VrfPublicKey`] / [`VrfProof`] / [`VrfOutput`] are
30//! opaque byte wrappers), so a namespace can hold a `Box<dyn Vrf>` and swap
31//! constructions without the CONIKS layer caring which one is in use.
32//!
33//! ## Post-quantum posture (honest framing)
34//!
35//! The default VRF is **classical**. Index-privacy is the *only* property in
36//! this engine that is not post-quantum from day one. Integrity, authenticity,
37//! confidentiality, and the SHA3-512 commitments ([`crate::commitment`]) do not
38//! depend on the VRF. The primitives are not FIPS-validated.
39
40use core::fmt;
41
42use crate::error::{Error, Result};
43
44/// VRF output (`beta`): the 64-byte pseudorandom value a verified proof yields.
45///
46/// CONIKS derives the private tree index from this value (see
47/// [`VrfOutput::index`]).
48#[derive(Clone, PartialEq, Eq, Hash)]
49pub struct VrfOutput([u8; 64]);
50
51impl VrfOutput {
52    /// Wrap a raw 64-byte VRF output.
53    #[must_use]
54    pub fn from_bytes(bytes: [u8; 64]) -> Self {
55        Self(bytes)
56    }
57
58    /// The raw 64-byte output.
59    #[must_use]
60    pub fn as_bytes(&self) -> &[u8; 64] {
61        &self.0
62    }
63
64    /// The 256-bit (32-byte) tree index derived from this output: the first 32
65    /// bytes of `beta`, consumed most-significant-bit-first as the root-to-leaf
66    /// path in the CONIKS prefix tree.
67    ///
68    /// Because `beta` is pseudorandom and unique per `(key, input)`, the derived
69    /// index is a stable, verifiable, privacy-preserving position: an observer
70    /// who sees the index learns nothing about the identity, and the directory
71    /// cannot move an identity to a different position without a fresh VRF
72    /// proof.
73    #[must_use]
74    pub fn index(&self) -> [u8; 32] {
75        let mut index = [0u8; 32];
76        index.copy_from_slice(&self.0[..32]);
77        index
78    }
79}
80
81// Avoid leaking output bytes through `Debug` in logs; show only the type.
82impl fmt::Debug for VrfOutput {
83    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
84        f.write_str("VrfOutput(..)")
85    }
86}
87
88/// An opaque VRF secret key. The concrete byte encoding is defined by the
89/// [`Vrf`] implementation. Treat the bytes as secret material.
90#[derive(Clone)]
91pub struct VrfSecretKey(Vec<u8>);
92
93/// An opaque VRF public key. The concrete byte encoding is defined by the
94/// [`Vrf`] implementation.
95#[derive(Clone, Debug, PartialEq, Eq, Hash)]
96pub struct VrfPublicKey(Vec<u8>);
97
98/// An opaque VRF proof (`pi`). The concrete byte encoding is defined by the
99/// [`Vrf`] implementation.
100#[derive(Clone, Debug, PartialEq, Eq, Hash)]
101pub struct VrfProof(Vec<u8>);
102
103macro_rules! byte_wrapper {
104    ($t:ty, $what:literal) => {
105        impl $t {
106            #[doc = concat!("Wrap raw ", $what, " bytes.")]
107            #[must_use]
108            pub fn from_bytes(bytes: Vec<u8>) -> Self {
109                Self(bytes)
110            }
111
112            #[doc = concat!("Borrow the raw ", $what, " bytes.")]
113            #[must_use]
114            pub fn as_bytes(&self) -> &[u8] {
115                &self.0
116            }
117
118            #[doc = concat!("Consume into the raw ", $what, " bytes.")]
119            #[must_use]
120            pub fn into_bytes(self) -> Vec<u8> {
121                self.0
122            }
123        }
124    };
125}
126
127byte_wrapper!(VrfSecretKey, "secret-key");
128byte_wrapper!(VrfPublicKey, "public-key");
129byte_wrapper!(VrfProof, "proof");
130
131// Avoid leaking secret-key bytes through `Debug`.
132impl fmt::Debug for VrfSecretKey {
133    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
134        f.write_str("VrfSecretKey(..)")
135    }
136}
137
138/// A swappable verifiable random function.
139///
140/// Implementations are stateless strategy objects (the keys are passed in), so
141/// a single instance can serve a whole namespace. All methods are byte-oriented
142/// and the trait is object-safe, so callers can hold a `Box<dyn Vrf>`.
143pub trait Vrf {
144    /// A stable identifier for the construction. For RFC 9381 suites this is the
145    /// ciphersuite octet (e.g. `0x03` for ECVRF-edwards25519-SHA512-TAI); a
146    /// future composite/hybrid construction uses its own reserved identifier.
147    /// It is mixed into CONIKS domain separation so proofs are bound to the
148    /// exact VRF construction and cannot be reinterpreted under another.
149    fn suite_id(&self) -> u8;
150
151    /// Generate a fresh keypair from the OS CSPRNG, as `(secret, public)`.
152    fn generate_keypair(&self) -> (VrfSecretKey, VrfPublicKey);
153
154    /// Derive the public key for a secret key.
155    ///
156    /// # Errors
157    /// Returns [`Error::Vrf`] if the secret key is structurally invalid.
158    fn derive_public_key(&self, secret_key: &VrfSecretKey) -> Result<VrfPublicKey>;
159
160    /// Produce a proof `pi` that binds `alpha` to its VRF output under
161    /// `secret_key`.
162    ///
163    /// # Errors
164    /// Returns [`Error::Vrf`] if the secret key is structurally invalid or the
165    /// proof cannot be produced.
166    fn prove(&self, secret_key: &VrfSecretKey, alpha: &[u8]) -> Result<VrfProof>;
167
168    /// Verify a proof and, on success, return the VRF output.
169    ///
170    /// Returns `Ok(Some(output))` if the proof is valid, `Ok(None)` if it is
171    /// well-formed but cryptographically invalid (wrong key, tampered input, or
172    /// forgery).
173    ///
174    /// # Errors
175    /// Returns [`Error::Vrf`] if `public_key` or `proof` is structurally invalid
176    /// (e.g. the wrong byte length).
177    fn verify(
178        &self,
179        public_key: &VrfPublicKey,
180        alpha: &[u8],
181        proof: &VrfProof,
182    ) -> Result<Option<VrfOutput>>;
183
184    /// Recover the VRF output from a proof **without** verifying it. Only safe
185    /// on a proof already verified with [`Vrf::verify`] (which returns the
186    /// output directly) or whose provenance is independently trusted.
187    ///
188    /// # Errors
189    /// Returns [`Error::Vrf`] if the proof is structurally invalid.
190    fn proof_to_output(&self, proof: &VrfProof) -> Result<VrfOutput>;
191}
192
193/// Classical **ECVRF-edwards25519-SHA512-TAI** (RFC 9381 ciphersuite `0x03`),
194/// the default CONIKS VRF.
195///
196/// This is a thin adapter over [`metamorphic_crypto`]'s audited `vrf` primitive
197/// (which is itself built on the in-tree `curve25519-dalek` backend and locked
198/// to RFC 9381's official test vectors). No cryptography lives here — only the
199/// opaque-byte ↔ primitive plumbing.
200///
201/// RFC 9381's sibling suite `ECVRF-edwards25519-SHA512-ELL2` (`0x04`,
202/// constant-time Elligator2 hash-to-curve) is a designed-in future addition: it
203/// lands when the released curve backend exposes a conformant hash-to-curve
204/// (curve25519-dalek 5.x). Because [`Vrf::suite_id`] is bound into CONIKS domain
205/// separation, adding it is purely additive and never invalidates a `0x03`
206/// proof. The two suites are interchangeable behind this trait; index privacy as
207/// observed by a verifier is identical.
208#[derive(Debug, Clone, Copy, Default)]
209pub struct Ecvrf;
210
211impl Vrf for Ecvrf {
212    fn suite_id(&self) -> u8 {
213        metamorphic_crypto::ECVRF_EDWARDS25519_SHA512_TAI_SUITE
214    }
215
216    fn generate_keypair(&self) -> (VrfSecretKey, VrfPublicKey) {
217        let (sk, pk) = metamorphic_crypto::ecvrf_generate_keypair();
218        (VrfSecretKey(sk.to_vec()), VrfPublicKey(pk.to_vec()))
219    }
220
221    fn derive_public_key(&self, secret_key: &VrfSecretKey) -> Result<VrfPublicKey> {
222        let pk = metamorphic_crypto::ecvrf_public_key(secret_key.as_bytes())
223            .map_err(|e| Error::Vrf(e.to_string()))?;
224        Ok(VrfPublicKey(pk.to_vec()))
225    }
226
227    fn prove(&self, secret_key: &VrfSecretKey, alpha: &[u8]) -> Result<VrfProof> {
228        let pi = metamorphic_crypto::ecvrf_prove(secret_key.as_bytes(), alpha)
229            .map_err(|e| Error::Vrf(e.to_string()))?;
230        Ok(VrfProof(pi.to_vec()))
231    }
232
233    fn verify(
234        &self,
235        public_key: &VrfPublicKey,
236        alpha: &[u8],
237        proof: &VrfProof,
238    ) -> Result<Option<VrfOutput>> {
239        let beta = metamorphic_crypto::ecvrf_verify(public_key.as_bytes(), alpha, proof.as_bytes())
240            .map_err(|e| Error::Vrf(e.to_string()))?;
241        Ok(beta.map(VrfOutput))
242    }
243
244    fn proof_to_output(&self, proof: &VrfProof) -> Result<VrfOutput> {
245        let beta = metamorphic_crypto::ecvrf_proof_to_hash(proof.as_bytes())
246            .map_err(|e| Error::Vrf(e.to_string()))?;
247        Ok(VrfOutput(beta))
248    }
249}
250
251/// Domain-separation tag for the designed-in hybrid VRF output combiner.
252pub const HYBRID_OUTPUT_DST: &str = "metamorphic.app/vrf-hybrid-output/v1";
253
254/// Combine a classical and a post-quantum VRF output into a single hybrid output
255/// (the **designed-in**, not-yet-load-bearing hybrid path from #304).
256///
257/// ```text
258/// hybrid_beta = SHA3-512_with_context(
259///     "metamorphic.app/vrf-hybrid-output/v1",
260///     classical_beta (64) || pq_beta (64),
261/// )
262/// ```
263///
264/// ## Why this is safe to ship before a post-quantum VRF exists
265///
266/// This function is *only* the output mixer; it does not, by itself, make a
267/// hybrid VRF. A full hybrid VRF additionally requires a post-quantum [`Vrf`]
268/// implementation whose proof is verified **alongside** the classical one
269/// (strict-AND), and that PQ half does not exist yet (no audited lattice VRF).
270/// The mixer is defined now so the wire/derivation format is fixed in advance:
271///
272/// - **Privacy is belt-and-suspenders.** SHA3-512 over both halves stays
273///   pseudorandom if *either* input is secret, so a future quantum break of the
274///   classical curve does not retroactively de-anonymise recorded transcripts.
275/// - **Uniqueness stays anchored on the audited classical half.** We never claim
276///   the (future, unaudited) lattice half contributes uniqueness; a hybrid VRF
277///   built on this mixer must take the *classical* proof as the authority for
278///   uniqueness. This keeps the one cryptographic property with no standardized
279///   combiner resting on standardized, audited crypto.
280///
281/// When an audited PQ VRF lands, the hybrid construction is: verify both proofs
282/// (strict-AND), then derive the index from `hybrid_output(classical, pq)`.
283#[must_use]
284pub fn hybrid_output(classical: &VrfOutput, pq: &VrfOutput) -> VrfOutput {
285    let mut framed = [0u8; 128];
286    framed[..64].copy_from_slice(classical.as_bytes());
287    framed[64..].copy_from_slice(pq.as_bytes());
288    VrfOutput(metamorphic_crypto::hash::sha3_512_with_context(
289        HYBRID_OUTPUT_DST,
290        &framed,
291    ))
292}
293
294#[cfg(all(test, not(target_arch = "wasm32")))]
295mod tests {
296    use super::*;
297
298    #[test]
299    fn ecvrf_suite_id_is_tai() {
300        assert_eq!(Ecvrf.suite_id(), 0x03);
301    }
302
303    #[test]
304    fn prove_verify_roundtrip_through_trait() {
305        let vrf = Ecvrf;
306        let (sk, pk) = vrf.generate_keypair();
307        let alpha = b"alice@example.com";
308        let pi = vrf.prove(&sk, alpha).unwrap();
309        let out = vrf.verify(&pk, alpha, &pi).unwrap();
310        assert_eq!(out, Some(vrf.proof_to_output(&pi).unwrap()));
311    }
312
313    #[test]
314    fn derive_public_key_matches_keygen() {
315        let vrf = Ecvrf;
316        let (sk, pk) = vrf.generate_keypair();
317        assert_eq!(vrf.derive_public_key(&sk).unwrap(), pk);
318    }
319
320    #[test]
321    fn verify_rejects_tampered_input() {
322        let vrf = Ecvrf;
323        let (sk, pk) = vrf.generate_keypair();
324        let pi = vrf.prove(&sk, b"original").unwrap();
325        assert_eq!(vrf.verify(&pk, b"tampered", &pi).unwrap(), None);
326    }
327
328    #[test]
329    fn verify_rejects_wrong_key() {
330        let vrf = Ecvrf;
331        let (sk, _pk) = vrf.generate_keypair();
332        let (_sk2, pk2) = vrf.generate_keypair();
333        let pi = vrf.prove(&sk, b"x").unwrap();
334        assert_eq!(vrf.verify(&pk2, b"x", &pi).unwrap(), None);
335    }
336
337    #[test]
338    fn structural_errors_surface_as_vrf_error() {
339        let vrf = Ecvrf;
340        let bad_pk = VrfPublicKey::from_bytes(vec![0u8; 31]);
341        let pi = VrfProof::from_bytes(vec![0u8; 80]);
342        assert!(matches!(vrf.verify(&bad_pk, b"x", &pi), Err(Error::Vrf(_))));
343    }
344
345    #[test]
346    fn index_is_first_32_bytes_of_output() {
347        let mut beta = [0u8; 64];
348        for (i, b) in beta.iter_mut().enumerate() {
349            *b = i as u8;
350        }
351        let out = VrfOutput::from_bytes(beta);
352        assert_eq!(&out.index()[..], &beta[..32]);
353    }
354
355    #[test]
356    fn hybrid_output_is_deterministic_and_order_sensitive() {
357        let a = VrfOutput::from_bytes([1u8; 64]);
358        let b = VrfOutput::from_bytes([2u8; 64]);
359        assert_eq!(hybrid_output(&a, &b), hybrid_output(&a, &b));
360        // Swapping the halves changes the output (classical/PQ roles are fixed).
361        assert_ne!(hybrid_output(&a, &b), hybrid_output(&b, &a));
362    }
363
364    #[test]
365    fn hybrid_output_matches_documented_framing() {
366        let a = VrfOutput::from_bytes([7u8; 64]);
367        let b = VrfOutput::from_bytes([9u8; 64]);
368        let mut framed = Vec::new();
369        framed.extend_from_slice(a.as_bytes());
370        framed.extend_from_slice(b.as_bytes());
371        let expected = metamorphic_crypto::hash::sha3_512_with_context(HYBRID_OUTPUT_DST, &framed);
372        assert_eq!(hybrid_output(&a, &b).as_bytes(), &expected);
373    }
374
375    #[test]
376    fn vrf_is_object_safe() {
377        // Compiles only if `Vrf` is object-safe — the property the CONIKS layer
378        // relies on to hold a `Box<dyn Vrf>` per namespace.
379        let vrf: Box<dyn Vrf> = Box::new(Ecvrf);
380        assert_eq!(vrf.suite_id(), 0x03);
381    }
382}