Verifiable Random Function — ECVRF-EDWARDS25519-SHA512-TAI (RFC 9381, suite
0x03).
Thin, idiomatic wrappers over the audited metamorphic-crypto Rust core — the
same primitive used by the browser WASM build, so a proof produced or verified
here behaves byte-identically across native Rust, WASM, and this NIF.
A VRF is a keyed function whose owner can compute, for any input alpha, a
pseudorandom output beta and a proof pi that beta was computed
correctly under their public key. Anyone with the public key can verify pi,
but cannot compute beta for a new input themselves, and cannot learn alpha
from (pi, beta) alone. It backs MetamorphicLog's CONIKS-style
key-transparency layer, mapping a (private) identity index to a pseudorandom
tree position — giving index privacy together with a verifiable,
non-equivocable mapping.
This is the Edwards25519 suite; see MetamorphicCrypto.VrfP256 for the NIST
P-256 sibling (KT_128_SHA256_P256).
Post-quantum posture (honest framing)
This VRF is classical, and protects only index privacy
ECVRF is classical (elliptic-curve discrete log) — the one place in the transparency stack whose security is not post-quantum. It protects exactly one property: index privacy. Integrity, authenticity, and the hash-based commitments are post-quantum and do not rely on this VRF. Not FIPS-validated.
Encoding & wire format (base64 in, base64 out)
All arguments and results cross as base64 strings, matching the WASM wire format. Raw byte lengths (before base64):
secret key (SK) : 32 bytes (an Ed25519-style seed)
public key (Y) : 32 bytes (compressed Edwards point)
proof (pi) : 80 bytes = Gamma(32) || c(16) || s(32)
output (beta): 64 bytes (a SHA-512 digest)Correctness is pinned byte-for-byte by RFC 9381 Appendix B test vectors, so independent implementations interoperate exactly.
Verification result shape
verify/3 distinguishes a cryptographic rejection from a structural
(malformed-input) error:
{:ok, output_b64}— the proof is valid;output_b64is the VRF outputbeta(identical toproof_to_hash/1on the same proof).:invalid— a cryptographic rejection: wrong key, tamperedalpha, tampered proof, or a non-canonical/degenerate key orGamma. The inputs were well-formed but the proof does not verify.{:error, reason}— a structural failure: a wrong-length key/proof or invalid base64. Nothing was cryptographically decided.
Summary
Functions
Generate a fresh VRF keypair from the OS CSPRNG.
Output (beta) length in bytes (64).
Proof (pi) length in bytes (80).
Recover the VRF output beta (base64) directly from a proof pi, without
verifying it.
Same as proof_to_hash/1 but returns the output directly, raising on invalid
input.
Produce a VRF proof pi (base64) for base64 alpha under a base64
secret_key.
Same as prove/2 but returns the proof directly, raising on invalid input.
Derive the base64 public key for a base64 secret_key.
Public-key length in bytes (32).
Secret-key length in bytes (32).
RFC 9381 ciphersuite octet for ECVRF-EDWARDS25519-SHA512-TAI (0x03).
Verify a VRF proof pi for alpha under public_key (all base64).
Types
Functions
@spec generate_keypair() :: keypair()
Generate a fresh VRF keypair from the OS CSPRNG.
Returns %{secret_key: base64, public_key: base64}. The secret key is a
32-byte seed; handle it as secret material.
Example
%{secret_key: sk, public_key: pk} = MetamorphicCrypto.Vrf.generate_keypair()
@spec output_len() :: pos_integer()
Output (beta) length in bytes (64).
@spec proof_len() :: pos_integer()
Proof (pi) length in bytes (80).
Recover the VRF output beta (base64) directly from a proof pi, without
verifying it.
This is only the Gamma -> beta mapping; it does not check that pi is a
valid proof for any (public_key, alpha). Use it only on a proof already
verified with verify/3, or whose provenance you independently trust. Returns
{:ok, output_b64} or {:error, reason}.
Same as proof_to_hash/1 but returns the output directly, raising on invalid
input.
@spec prove(secret_key_b64 :: String.t(), alpha_b64 :: String.t()) :: {:ok, String.t()} | {:error, String.t()}
Produce a VRF proof pi (base64) for base64 alpha under a base64
secret_key.
The proof both authenticates the output and lets any verifier recompute it.
Returns {:ok, proof_b64} or {:error, reason}. TAI ECVRF is deterministic:
the same key and alpha always yield the same proof.
Example
%{secret_key: sk} = MetamorphicCrypto.Vrf.generate_keypair()
{:ok, pi} = MetamorphicCrypto.Vrf.prove(sk, Base.encode64("identity index"))
Same as prove/2 but returns the proof directly, raising on invalid input.
Derive the base64 public key for a base64 secret_key.
Returns {:ok, public_key_b64} or {:error, reason} (wrong length / invalid
base64).
Example
%{secret_key: sk, public_key: pk} = MetamorphicCrypto.Vrf.generate_keypair()
{:ok, ^pk} = MetamorphicCrypto.Vrf.public_key(sk)
@spec public_key_len() :: pos_integer()
Public-key length in bytes (32).
@spec secret_key_len() :: pos_integer()
Secret-key length in bytes (32).
@spec suite() :: 3
RFC 9381 ciphersuite octet for ECVRF-EDWARDS25519-SHA512-TAI (0x03).
@spec verify( public_key_b64 :: String.t(), alpha_b64 :: String.t(), proof_b64 :: String.t() ) :: {:ok, String.t()} | :invalid | {:error, String.t()}
Verify a VRF proof pi for alpha under public_key (all base64).
Returns:
{:ok, output_b64}— the proof is valid;output_b64is the VRF output.:invalid— a cryptographic rejection (wrong key, tampered input/proof, degenerate key orGamma). Well-formed inputs, but the proof does not verify.{:error, reason}— a structural failure (wrong-length key/proof, invalid base64).
Example
%{secret_key: sk, public_key: pk} = MetamorphicCrypto.Vrf.generate_keypair()
alpha = Base.encode64("identity index")
{:ok, pi} = MetamorphicCrypto.Vrf.prove(sk, alpha)
{:ok, beta} = MetamorphicCrypto.Vrf.verify(pk, alpha, pi)
:invalid = MetamorphicCrypto.Vrf.verify(pk, Base.encode64("other"), pi)