MetamorphicCrypto.VrfP256 (metamorphic_crypto v0.8.0)

Copy Markdown View Source

Verifiable Random Function — ECVRF-P256-SHA256-TAI (RFC 9381, suite 0x01).

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.

This is the NIST P-256 sibling of MetamorphicCrypto.Vrf (Edwards25519). It implements RFC 9381 ciphersuite 0x01: the P-256 curve, SHA-256, and try-and-increment hash-to-curve. It exists so that MetamorphicLog's KEYTRANS layer can offer the on-spec IETF KT_128_SHA256_P256 cipher suite, whose VRF is defined as ECVRF-P256-SHA256-TAI.

See MetamorphicCrypto.Vrf for the general VRF concept and the index privacy role in key transparency; the two modules share an identical API shape and result contract, differing only in curve, lengths, and suite octet.

Post-quantum posture (honest framing)

This VRF is classical, and protects only index privacy

Like the Edwards25519 VRF, this is classical (elliptic-curve discrete log) and protects exactly one property: KEYTRANS 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. Raw byte lengths (before base64):

secret key  (SK) : 32 bytes   (big-endian secret scalar x)
public key  (Y)  : 33 bytes   (SEC1 compressed point)
proof       (pi) : 81 bytes   = Gamma(33) || c(16) || s(32)
output      (beta): 32 bytes  (a SHA-256 digest)

Correctness is pinned byte-for-byte by RFC 9381 Appendix B.1 test vectors.

Verification result shape

verify/3 distinguishes a cryptographic rejection from a structural (malformed-input) error:

  • {:ok, output_b64} — the proof is valid; output_b64 is the VRF output.
  • :invalid — a cryptographic rejection (wrong key, tampered input/proof, or a degenerate/non-canonical key, Gamma, or s).
  • {:error, reason} — a structural failure (wrong-length key/proof, invalid base64).

Summary

Types

An ECVRF-P256 keypair, both fields base64-encoded (32-byte scalar / 33-byte SEC1 point).

Functions

Generate a fresh VRF keypair from the OS CSPRNG.

Output (beta) length in bytes (32).

Proof (pi) length in bytes (81).

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 (33).

Secret-key length in bytes (32).

RFC 9381 ciphersuite octet for ECVRF-P256-SHA256-TAI (0x01).

Verify a VRF proof pi for alpha under public_key (all base64).

Types

keypair()

@type keypair() :: %{secret_key: String.t(), public_key: String.t()}

An ECVRF-P256 keypair, both fields base64-encoded (32-byte scalar / 33-byte SEC1 point).

Functions

generate_keypair()

@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 big-endian scalar; handle it as secret material.

Example

%{secret_key: sk, public_key: pk} = MetamorphicCrypto.VrfP256.generate_keypair()

output_len()

@spec output_len() :: pos_integer()

Output (beta) length in bytes (32).

proof_len()

@spec proof_len() :: pos_integer()

Proof (pi) length in bytes (81).

proof_to_hash(proof_b64)

@spec proof_to_hash(proof_b64 :: String.t()) ::
  {:ok, String.t()} | {:error, String.t()}

Recover the VRF output beta (base64) directly from a proof pi, without verifying it.

Only the Gamma -> beta mapping; it does not check that pi is valid. Use it only on a proof already verified with verify/3, or whose provenance you independently trust. Returns {:ok, output_b64} or {:error, reason}.

proof_to_hash!(proof_b64)

@spec proof_to_hash!(proof_b64 :: String.t()) :: String.t()

Same as proof_to_hash/1 but returns the output directly, raising on invalid input.

prove(secret_key_b64, alpha_b64)

@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.

Returns {:ok, proof_b64} or {:error, reason}. The construction is deterministic (RFC 6979 nonces): the same key and alpha always yield the same proof.

Example

%{secret_key: sk} = MetamorphicCrypto.VrfP256.generate_keypair()
{:ok, pi} = MetamorphicCrypto.VrfP256.prove(sk, Base.encode64("identity index"))

prove!(secret_key_b64, alpha_b64)

@spec prove!(secret_key_b64 :: String.t(), alpha_b64 :: String.t()) :: String.t()

Same as prove/2 but returns the proof directly, raising on invalid input.

public_key(secret_key_b64)

@spec public_key(secret_key_b64 :: String.t()) ::
  {:ok, String.t()} | {:error, String.t()}

Derive the base64 public key for a base64 secret_key.

Returns {:ok, public_key_b64} or {:error, reason} (wrong length, a non-canonical/zero scalar, or invalid base64).

Example

%{secret_key: sk, public_key: pk} = MetamorphicCrypto.VrfP256.generate_keypair()
{:ok, ^pk} = MetamorphicCrypto.VrfP256.public_key(sk)

public_key_len()

@spec public_key_len() :: pos_integer()

Public-key length in bytes (33).

secret_key_len()

@spec secret_key_len() :: pos_integer()

Secret-key length in bytes (32).

suite()

@spec suite() :: 1

RFC 9381 ciphersuite octet for ECVRF-P256-SHA256-TAI (0x01).

verify(public_key_b64, alpha_b64, proof_b64)

@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_b64 is the VRF output.
  • :invalid — a cryptographic rejection (wrong key, tampered input/proof, degenerate key/Gamma/s).
  • {:error, reason} — a structural failure (wrong-length key/proof, invalid base64).

Example

%{secret_key: sk, public_key: pk} = MetamorphicCrypto.VrfP256.generate_keypair()
alpha = Base.encode64("identity index")
{:ok, pi} = MetamorphicCrypto.VrfP256.prove(sk, alpha)
{:ok, beta} = MetamorphicCrypto.VrfP256.verify(pk, alpha, pi)
:invalid = MetamorphicCrypto.VrfP256.verify(pk, Base.encode64("other"), pi)