diff --git a/crates/vrf_fun/RUSTSEC-0000-0000.md b/crates/vrf_fun/RUSTSEC-0000-0000.md new file mode 100644 index 000000000..d22f131e7 --- /dev/null +++ b/crates/vrf_fun/RUSTSEC-0000-0000.md @@ -0,0 +1,43 @@ +```toml +[advisory] +id = "RUSTSEC-0000-0000" +package = "vrf_fun" +date = "2026-02-27" +url = "https://github.com/LLFourn/secp256kfun/pull/244" +categories = ["crypto-failure"] +keywords = ["nonce-reuse", "key-recovery", "vrf"] +license = "CC-BY-4.0" + +[affected.functions] +"vrf_fun::Vrf::prove" = ["= 0.12.0"] +"vrf_fun::rfc9381::tai::prove" = ["= 0.12.0"] +"vrf_fun::rfc9381::sswu::prove" = ["= 0.12.0"] +"vrf_fun::rfc9381::Rfc9381Transcript::gen_rng" = ["= 0.12.0"] + +[versions] +patched = [] +``` + +# RFC 9381 VRF nonce reuse allows secret key recovery + +The `ProverTranscript` implementation for `Rfc9381Transcript` computed the +nonce seed from a fresh hasher rather than the transcript state. The nonce +depended only on a fixed domain separator and the secret key, not on the VRF +input: + +``` +nonce = H("vrf-nonce-gen" || secret_key) +``` + +Because the nonce was identical across all proofs made with the same key, +any two VRF proofs on different inputs leaked the secret key via: + +``` +x = (s1 - s2) / (c1 - c2) +``` + +where `(c1, s1)` and `(c2, s2)` are the challenge-response pairs from the +two proofs. + +The fix incorporates the transcript state (which includes the public key, +hash-to-curve point, and gamma) into the nonce derivation.