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}