diff --git a/crypto/vrf/ecvrf/UPSTREAM.md b/crypto/vrf/ecvrf/UPSTREAM.md new file mode 100644 index 000000000000..89f2b10f4e80 --- /dev/null +++ b/crypto/vrf/ecvrf/UPSTREAM.md @@ -0,0 +1,80 @@ +# Upstream Provenance — `crypto/vrf/ecvrf/` + +Spec reference: WorldLand Rockies v.1.0 spec §8.7. + +## Source + +- **Upstream**: [`github.com/ProtonMail/go-ecvrf`](https://github.com/ProtonMail/go-ecvrf) +- **Upstream commit**: `dce739d6b120640fb237f6ca18538fd1cefa91c9` +- **Upstream license**: MIT (see `LICENSE` in upstream tree) +- **Cipher suite**: ECVRF-EDWARDS25519-SHA512-TAI (RFC 9381 §5, + `suite_string = 0x03`). + +## Why ProtonMail/go-ecvrf + +The originally specified upstream (Algorand `go-algorand/crypto/vrf/`) is +a CGo wrapper around libsodium's `vrf_ietfdraft03`, which implements +ECVRF-EDWARDS25519-SHA512-Elligator2 (`suite_string = 0x04`). That suite +is **incompatible** with the TAI variant mandated by spec §3 — the two +schemes produce different `ProofToHash` outputs for the same `(sk, alpha)`. + +ProtonMail's library is a small, pure-Go (no CGo) implementation of +exactly the right TAI suite, validated against the published RFC test +vectors. WorldLand's VCT use of the VRF supplies a fully public `alpha = +H_{h-1} || h`, so the Try-And-Increment timing-side-channel concern +flagged in RFC 9381 §7.5 does not apply (there is no secret in the +hashed material). + +The spec was amended on `feature/vrf-library`'s parent (commit +`fc2b948` on `docs/rockies-v1.0-spec`) to record this decision in §8.7. + +## Modifications from upstream + +The following changes were applied during the fork: + +1. **Package documentation** updated to reference RFC 9381 (the published + IETF document) instead of `draft-irtf-cfrg-vrf-10`. The on-the-wire + format is identical. +2. **Typed errors** (`crypto/vrf/ecvrf/errors.go`) per spec §8.4. Each + error message carries the `ecvrf:` prefix for greppability. +3. **`Verify()` API** changed from `(verified bool, beta []byte, err error)` + to `(beta []byte, err error)`. A semantically invalid (but + structurally well-formed) proof now returns `ErrInvalidVRFProof` + rather than `(false, nil, nil)`. This eliminates a footgun where + callers could ignore the bool and treat a forged proof as valid. +4. **`Public()`** changed from `(*PublicKey, error)` to `*PublicKey`. It + cannot fail (the public key was derived from a valid scalar at key + construction time). +5. **`NewPublicKey()`** now eagerly decodes the curve point and rejects + malformed encodings at construction. Upstream deferred the check to + the first `Verify()` call. +6. **`ProofToHash()`** exposed as a top-level function (not just an + internal helper). The VRF precompile (spec §4.5) needs this entry + point because it returns beta but does the verification through the + library's `Verify`. +7. **Cofactor multiplication in `proofToHash`** moved into the + helper itself (rather than the caller), simplifying the call sites. +8. **Constants exposed**: `ProofSize`, `PublicKeySize`, `BetaSize`, + `SeedSize`, `SuiteID` — needed by the precompile, header schema, and + registry layers. +9. **Comments** rewritten to cite RFC 9381 sections rather than the + draft. Algorithm steps are unchanged. + +The *cryptographic algorithm* (every line of curve arithmetic and every +hash input) is unchanged from upstream and from the RFC; modifications +are restricted to API hygiene and documentation. Validation: the full +RFC 9381 §B.3 test corpus passes byte-for-byte (see `ecvrf_test.go`). + +## Re-baseline procedure + +Should ProtonMail publish a security fix for the upstream, the diff +must be re-applied here. Steps: + +1. Fetch the new upstream commit; record the hash in this file. +2. Diff `upstream/ecvrf/ecvrf.go` against this package's `ecvrf.go`. +3. Apply algorithmic changes only; preserve the modifications listed + above (typed errors, two-state Verify return, etc.). +4. Re-run `go test ./crypto/vrf/...` and confirm the RFC 9381 §B.3 + vectors still pass. +5. Update this file's `Upstream commit` line and submit a PR + referencing the upstream advisory. diff --git a/crypto/vrf/ecvrf/ecvrf.go b/crypto/vrf/ecvrf/ecvrf.go new file mode 100644 index 000000000000..7b21e3a30ee7 --- /dev/null +++ b/crypto/vrf/ecvrf/ecvrf.go @@ -0,0 +1,340 @@ +// Copyright 2026 The WorldLand / go-ethereum Authors +// Portions Copyright (c) 2021 ProtonMail, MIT-licensed (see UPSTREAM.md). +// This file is part of the WorldLand library. +// +// The WorldLand library is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// The WorldLand library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. + +// Package ecvrf implements ECVRF-EDWARDS25519-SHA512-TAI as specified by +// RFC 9381 §5 (suite_string = 0x03), the cipher suite mandated by WorldLand +// Rockies v.1.0 spec §3. +// +// The implementation is derived from ProtonMail/go-ecvrf (MIT). See +// UPSTREAM.md for the exact upstream commit and a summary of WorldLand +// modifications. +package ecvrf + +import ( + "crypto/rand" + "crypto/sha512" + "crypto/subtle" + "io" + + "filippo.io/edwards25519" +) + +// Suite/protocol constants. Sizes match RFC 9381 §5 for ED25519-SHA512-TAI. +const ( + // SeedSize is the RFC 8032 seed length (32 bytes). + SeedSize = 32 + // PublicKeySize is the canonical Ed25519 point encoding (32 bytes). + PublicKeySize = 32 + // PrivateKeySize is the RFC 8032 private-key encoding: seed || public. + PrivateKeySize = SeedSize + PublicKeySize + // IntermediateSize is the truncated challenge length c (16 bytes, + // per RFC 9381 §5.5 cLen for ED25519-SHA512-TAI). + IntermediateSize = 16 + // ProofSize is gamma || c || s = 32 + 16 + 32 = 80 bytes. + ProofSize = PublicKeySize + IntermediateSize + SeedSize + // BetaSize is the SHA-512 ProofToHash output (64 bytes). + BetaSize = 64 + + // SuiteID is the RFC 9381 suite_string for ECVRF-EDWARDS25519-SHA512-TAI. + // Note: this is the *RFC* suite byte, not the WorldLand crypto-agility + // scheme byte (which is 0x01; see crypto/vrf/registry.go and spec §8.3). + SuiteID = 0x03 +) + +// Domain-separation tags from RFC 9381 §5.4. +const ( + dstHashToCurve = 0x01 + dstHashPoints = 0x02 + dstProofToHash = 0x03 +) + +// PrivateKey is a VRF private key. It bundles the RFC 8032 seed, derived +// public point, and the precomputed secret scalar so Prove() does not redo +// the SHA-512 + clamp on every call. +type PrivateKey struct { + seed []byte // 32 bytes (RFC 8032 SK) + pk []byte // 32 bytes (canonical encoding of A = x*B) + x *edwards25519.Scalar +} + +// PublicKey is a VRF public key (a canonical Ed25519 point encoding). +type PublicKey struct { + pk []byte + point *edwards25519.Point +} + +// GenerateKey samples a fresh keypair. If rnd is nil, crypto/rand is used. +// Calling with a 32-byte deterministic reader reproduces an RFC 8032 seed. +func GenerateKey(rnd io.Reader) (*PrivateKey, error) { + if rnd == nil { + rnd = rand.Reader + } + seed := make([]byte, SeedSize) + if _, err := io.ReadFull(rnd, seed); err != nil { + return nil, err + } + return privateKeyFromSeed(seed) +} + +// NewPrivateKey deserialises a 64-byte (seed || pk) RFC 8032 private key. +// The supplied pk is checked against the value derived from the seed. +func NewPrivateKey(skBytes []byte) (*PrivateKey, error) { + if len(skBytes) != PrivateKeySize { + return nil, ErrInvalidPrivateKey + } + sk, err := privateKeyFromSeed(skBytes[:SeedSize]) + if err != nil { + return nil, err + } + if subtle.ConstantTimeCompare(sk.pk, skBytes[SeedSize:]) != 1 { + return nil, ErrInvalidPrivateKey + } + return sk, nil +} + +func privateKeyFromSeed(seed []byte) (*PrivateKey, error) { + // RFC 8032 §5.1.5: x = clamp(SHA512(seed)[:32]). + h := sha512.Sum512(seed) + x, err := edwards25519.NewScalar().SetBytesWithClamping(h[:32]) + if err != nil { + return nil, ErrInvalidPrivateKey + } + A := (&edwards25519.Point{}).ScalarBaseMult(x) + cp := make([]byte, SeedSize) + copy(cp, seed) + return &PrivateKey{seed: cp, pk: A.Bytes(), x: x}, nil +} + +// Public returns the matching public key. +func (sk *PrivateKey) Public() *PublicKey { + pkCopy := make([]byte, PublicKeySize) + copy(pkCopy, sk.pk) + point, _ := (&edwards25519.Point{}).SetBytes(sk.pk) // safe: derived from valid scalar + return &PublicKey{pk: pkCopy, point: point} +} + +// Bytes serialises the private key as seed || pk. +func (sk *PrivateKey) Bytes() []byte { + out := make([]byte, PrivateKeySize) + copy(out, sk.seed) + copy(out[SeedSize:], sk.pk) + return out +} + +// Seed returns a copy of the RFC 8032 seed. +func (sk *PrivateKey) Seed() []byte { + out := make([]byte, SeedSize) + copy(out, sk.seed) + return out +} + +// NewPublicKey deserialises and validates a 32-byte Ed25519 public key. +// Unlike the upstream, we eagerly decode the point so malformed keys are +// rejected at construction (rather than when first verified). +func NewPublicKey(pkBytes []byte) (*PublicKey, error) { + if len(pkBytes) != PublicKeySize { + return nil, ErrInvalidPublicKey + } + point, err := (&edwards25519.Point{}).SetBytes(pkBytes) + if err != nil { + return nil, ErrInvalidPublicKey + } + cp := make([]byte, PublicKeySize) + copy(cp, pkBytes) + return &PublicKey{pk: cp, point: point}, nil +} + +// Bytes returns a copy of the canonical public-key encoding. +func (pk *PublicKey) Bytes() []byte { + out := make([]byte, PublicKeySize) + copy(out, pk.pk) + return out +} + +// Prove computes (beta, pi) = ECVRF_prove(sk, alpha) per RFC 9381 §5.1. +// Determinism follows from sk and alpha: both are protocol-fixed in the +// VCT use (alpha = H_{h-1} || uint64_be(h); see spec §4.2). +func (sk *PrivateKey) Prove(alpha []byte) (beta, proof []byte, err error) { + H, err := hashToCurveTAI(sk.pk, alpha) + if err != nil { + return nil, nil, err + } + + // gamma = x * H + gamma := (&edwards25519.Point{}).ScalarMult(sk.x, H) + + // k = ECVRF_nonce_generation_RFC8032(sk, H) (§5.4.2.2) + kHash := generateNonceHash(sk.seed, H.Bytes()) + k, err := edwards25519.NewScalar().SetUniformBytes(kHash) + if err != nil { + return nil, nil, err + } + + // c = ECVRF_hash_points(H, gamma, k*B, k*H) truncated to 16 bytes. + c := hashPoints( + H, + gamma, + (&edwards25519.Point{}).ScalarBaseMult(k), + (&edwards25519.Point{}).ScalarMult(k, H), + ) + cScal, err := cToScalar(c) + if err != nil { + return nil, nil, err + } + + // s = (k + c*x) mod q + s := edwards25519.NewScalar().Add(k, edwards25519.NewScalar().Multiply(cScal, sk.x)) + + proof = make([]byte, ProofSize) + copy(proof, gamma.Bytes()) + copy(proof[PublicKeySize:], c) + copy(proof[PublicKeySize+IntermediateSize:], s.Bytes()) + + return proofToHashFromGamma(gamma), proof, nil +} + +// Verify implements RFC 9381 §5.3. It returns the recovered beta on +// success, or one of the typed errors (ErrInvalidProofLength / Point / +// Scalar / VRFProof) on failure. +// +// We map the upstream's three-state (verified, beta, err) return into a +// two-state (beta, err) form: a structurally well-formed but semantically +// invalid proof now returns ErrInvalidVRFProof. Callers can use errors.Is +// to distinguish bad-input from bad-signature without inspecting bools. +func (pk *PublicKey) Verify(alpha, proof []byte) ([]byte, error) { + if len(proof) != ProofSize { + return nil, ErrInvalidProofLength + } + gamma, err := (&edwards25519.Point{}).SetBytes(proof[:PublicKeySize]) + if err != nil { + return nil, ErrInvalidProofPoint + } + c, err := cToScalar(proof[PublicKeySize : PublicKeySize+IntermediateSize]) + if err != nil { + // 16-byte zero-padded value is always canonical; this branch is + // defensive, hence we map to ErrInvalidProofScalar for consistency. + return nil, ErrInvalidProofScalar + } + s, err := edwards25519.NewScalar().SetCanonicalBytes(proof[PublicKeySize+IntermediateSize:]) + if err != nil { + return nil, ErrInvalidProofScalar + } + + H, err := hashToCurveTAI(pk.pk, alpha) + if err != nil { + return nil, err + } + + // U = s*B - c*Y + U := (&edwards25519.Point{}).Subtract( + (&edwards25519.Point{}).ScalarBaseMult(s), + (&edwards25519.Point{}).ScalarMult(c, pk.point), + ) + // V = s*H - c*Gamma + V := (&edwards25519.Point{}).Subtract( + (&edwards25519.Point{}).ScalarMult(s, H), + (&edwards25519.Point{}).ScalarMult(c, gamma), + ) + + cPrime := hashPoints(H, gamma, U, V) + if subtle.ConstantTimeCompare(cPrime, proof[PublicKeySize:PublicKeySize+IntermediateSize]) != 1 { + return nil, ErrInvalidVRFProof + } + return proofToHashFromGamma(gamma), nil +} + +// ProofToHash extracts the VRF output beta from a proof, without verifying +// it (RFC 9381 §5.2). Callers MUST run Verify separately when authenticity +// matters; this helper exists for the precompile (§4.5) which performs the +// verify step itself and only needs to surface beta on success. +// +// Even so, ProofToHash refuses obviously malformed inputs (wrong length, +// non-curve gamma) so misuse cannot crash callers. +func ProofToHash(proof []byte) ([]byte, error) { + if len(proof) != ProofSize { + return nil, ErrInvalidProofLength + } + gamma, err := (&edwards25519.Point{}).SetBytes(proof[:PublicKeySize]) + if err != nil { + return nil, ErrInvalidProofPoint + } + return proofToHashFromGamma(gamma), nil +} + +// --- internal helpers (RFC 9381 §5.4) --- + +// hashToCurveTAI implements ECVRF_hash_to_curve_try_and_increment +// (RFC 9381 §5.4.1.1): try ctr = 0, 1, ... until SHA-512(suite || 0x01 || +// pk || alpha || ctr || 0x00)[:32] decodes to a non-identity point, then +// multiply by the cofactor. +func hashToCurveTAI(pk, alpha []byte) (*edwards25519.Point, error) { + for ctr := 0; ctr < 256; ctr++ { + h := sha512.New() + h.Write([]byte{SuiteID, dstHashToCurve}) + h.Write(pk) + h.Write(alpha) + h.Write([]byte{byte(ctr), 0x00}) + + p, err := (&edwards25519.Point{}).SetBytes(h.Sum(nil)[:PublicKeySize]) + if err == nil && p.Equal(edwards25519.NewIdentityPoint()) == 0 { + return (&edwards25519.Point{}).MultByCofactor(p), nil + } + } + return nil, ErrHashToCurveFailed +} + +// generateNonceHash computes the 64-byte SHA-512 nonce input per +// RFC 9381 §5.4.2.2 (ECVRF_nonce_generation_RFC8032). The 64-byte output +// is later reduced mod q via SetUniformBytes. +func generateNonceHash(seed, h []byte) []byte { + skHash := sha512.New() + skHash.Write(seed) + out := skHash.Sum(nil) + + nonceHash := sha512.New() + nonceHash.Write(out[SeedSize:]) // upper half of SHA512(sk_seed) + nonceHash.Write(h) + return nonceHash.Sum(nil) +} + +// hashPoints implements ECVRF_hash_points (RFC 9381 §5.4.3): truncated +// SHA-512 challenge over (suite, 0x02, P_1, ..., P_n, 0x00). +func hashPoints(points ...*edwards25519.Point) []byte { + h := sha512.New() + h.Write([]byte{SuiteID, dstHashPoints}) + for _, p := range points { + h.Write(p.Bytes()) + } + h.Write([]byte{0x00}) + return h.Sum(nil)[:IntermediateSize] +} + +// proofToHashFromGamma implements RFC 9381 §5.2 once gamma is decoded. +// beta = SHA-512(suite || 0x03 || cofactor*gamma || 0x00). +func proofToHashFromGamma(gamma *edwards25519.Point) []byte { + gammaC := (&edwards25519.Point{}).MultByCofactor(gamma) + h := sha512.New() + h.Write([]byte{SuiteID, dstProofToHash}) + h.Write(gammaC.Bytes()) + h.Write([]byte{0x00}) + return h.Sum(nil) +} + +// cToScalar zero-pads the 16-byte challenge into a canonical 32-byte scalar. +// 16 leading bytes are always < 2^252+... so the result is canonical. +func cToScalar(c []byte) (*edwards25519.Scalar, error) { + buf := make([]byte, SeedSize) + copy(buf, c) + return edwards25519.NewScalar().SetCanonicalBytes(buf) +} diff --git a/crypto/vrf/ecvrf/ecvrf_test.go b/crypto/vrf/ecvrf/ecvrf_test.go new file mode 100644 index 000000000000..7f4c3003f2c4 --- /dev/null +++ b/crypto/vrf/ecvrf/ecvrf_test.go @@ -0,0 +1,486 @@ +// Copyright 2026 The WorldLand / go-ethereum Authors +// This file is part of the WorldLand library. +// +// The WorldLand library is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// The WorldLand library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. + +// RFC 9381 Appendix B.3 test vectors for ECVRF-EDWARDS25519-SHA512-TAI. +// +// Vectors transcribed verbatim from RFC 9381 (August 2023). They are +// identical to draft-irtf-cfrg-vrf-10 Appendix A.3 Examples 7, 8, 9 and to +// the ProtonMail/go-ecvrf upstream test corpus (commit +// dce739d6b120640fb237f6ca18538fd1cefa91c9), which is the upstream we forked +// for this package per spec §8.7. + +package ecvrf + +import ( + "bytes" + "crypto/ed25519" + "crypto/sha256" + "encoding/hex" + "errors" + "strings" + "testing" + + "filippo.io/edwards25519" +) + +// rfcVector groups the published RFC 9381 §B.3 fields plus the intermediate +// values used to exercise the internal helpers. +type rfcVector struct { + name string + // Externally visible fields. + sk string // 32-byte ed25519 seed (RFC 8032 SK) + pk string // 32-byte ed25519 public key + alpha string // VRF input + pi string // 80-byte VRF proof (gamma || c || s) + beta string // 64-byte VRF output (ProofToHash(gamma)) + // Intermediate values for white-box tests. + x string // secret scalar x = clamp(SHA512(sk)[:32]) (RFC 8032 §5.1.5) + h string // hashToCurve(pk, alpha) encoded + k string // 64-byte nonce SHA-512 output prior to scalar reduction + u string // U = k*B + v string // V = k*H +} + +// rfc9381B3 is the full RFC 9381 §B.3 test corpus for +// ECVRF-EDWARDS25519-SHA512-TAI (3 vectors). +var rfc9381B3 = []rfcVector{ + { + name: "RFC9381_B3_Example1", + sk: "9d61b19deffd5a60ba844af492ec2cc44449c5697b326919703bac031cae7f60", + pk: "d75a980182b10ab7d54bfed3c964073a0ee172f3daa62325af021a68f707511a", + alpha: "", + pi: "8657106690b5526245a92b003bb079ccd1a92130477671f6fc01ad16f26f723f" + + "5e8bd1839b414219e8626d393787a192241fc442e6569e96c462f62b8079b9ed" + + "83ff2ee21c90c7c398802fdeebea4001", + beta: "90cf1df3b703cce59e2a35b925d411164068269d7b2d29f3301c03dd757876ff" + + "66b71dda49d2de59d03450451af026798e8f81cd2e333de5cdf4f3e140fdd8ae", + x: "307c83864f2833cb427a2ef1c00a013cfdff2768d980c0a3a520f006904de94f", + h: "91bbed02a99461df1ad4c6564a5f5d829d0b90cfc7903e7a5797bd658abf3318", + k: "7100f3d9eadb6dc4743b029736ff283f5be494128df128df2817106f345b8594" + + "b6d6da2d6fb0b4c0257eb337675d96eab49cf39e66cc2c9547c2bf8b2a6afae4", + u: "aef27c725be964c6a9bf4c45ca8e35df258c1878b838f37d9975523f09034071", + v: "5016572f71466c646c119443455d6cb9b952f07d060ec8286d678615d55f954f", + }, + { + name: "RFC9381_B3_Example2", + sk: "4ccd089b28ff96da9db6c346ec114e0f5b8a319f35aba624da8cf6ed4fb8a6fb", + pk: "3d4017c3e843895a92b70aa74d1b7ebc9c982ccf2ec4968cc0cd55f12af4660c", + alpha: "72", + pi: "f3141cd382dc42909d19ec5110469e4feae18300e94f304590abdced48aed593" + + "f7eaf3eb2f1a968cba3f6e23b386aeeaab7b1ea44a256e811892e13eeae7c9f6" + + "ea8992557453eac11c4d5476b1f35a08", + beta: "eb4440665d3891d668e7e0fcaf587f1b4bd7fbfe99d0eb2211ccec90496310eb" + + "5e33821bc613efb94db5e5b54c70a848a0bef4553a41befc57663b56373a5031", + x: "68bd9ed75882d52815a97585caf4790a7f6c6b3b7f821c5e259a24b02e502e51", + h: "5b659fc3d4e9263fd9a4ed1d022d75eaacc20df5e09f9ea937502396598dc551", + k: "42589bbf0c485c3c91c1621bb4bfe04aed7be76ee48f9b00793b2342acb9c167" + + "cab856f9f9d4febc311330c20b0a8afd3743d05433e8be8d32522ecdc16cc5ce", + u: "1dcb0a4821a2c48bf53548228b7f170962988f6d12f5439f31987ef41f034ab3", + v: "fd03c0bf498c752161bae4719105a074630a2aa5f200ff7b3995f7bfb1513423", + }, + { + name: "RFC9381_B3_Example3", + sk: "c5aa8df43f9f837bedb7442f31dcb7b166d38535076f094b85ce3a2e0b4458f7", + pk: "fc51cd8e6218a1a38da47ed00230f0580816ed13ba3303ac5deb911548908025", + alpha: "af82", + pi: "9bc0f79119cc5604bf02d23b4caede71393cedfbb191434dd016d30177ccbf80" + + "e29dc513c01c3a980e0e545bcd848222d08a6c3e3665ff5a4cab13a643bef812" + + "e284c6b2ee063a2cb4f456794723ad0a", + beta: "645427e5d00c62a23fb703732fa5d892940935942101e456ecca7bb217c61c45" + + "2118fec1219202a0edcf038bb6373241578be7217ba85a2687f7a0310b2df19f", + x: "909a8b755ed902849023a55b15c23d11ba4d7f4ec5c2f51b1325a181991ea95c", + h: "bf4339376f5542811de615e3313d2b36f6f53c0acfebb482159711201192576a", + k: "38b868c335ccda94a088428cbf3ec8bc7955bfaffe1f3bd2aa2c59fc31a0febc" + + "59d0e1af3715773ce11b3bbdd7aba8e3505d4b9de6f7e4a96e67e0d6bb6d6c3a", + u: "2bae73e15a64042fcebf062abe7e432b2eca6744f3e8265bc38e009cd577ecd5", + v: "88cba1cb0d4f9b649d9a86026b69de076724a93a65c349c988954f0961c5d506", + }, +} + +func mustHex(t *testing.T, s string) []byte { + t.Helper() + b, err := hex.DecodeString(s) + if err != nil { + t.Fatalf("decoding %q: %v", s, err) + } + return b +} + +// invalidPointBytes returns 32 bytes that filippo.io/edwards25519 SetBytes +// rejects (no valid x coordinate). About half of random 32-byte strings +// are non-curve, so a deterministic seeded search terminates quickly. +func invalidPointBytes(t *testing.T) []byte { + t.Helper() + for seed := uint32(0); seed < 1024; seed++ { + h := sha256.Sum256([]byte{byte(seed), byte(seed >> 8), byte(seed >> 16), byte(seed >> 24)}) + if _, err := (&edwards25519.Point{}).SetBytes(h[:]); err != nil { + return append([]byte{}, h[:]...) + } + } + t.Fatal("no invalid Ed25519 point encoding found in 1024 SHA-256 candidates") + return nil +} + +// TestRFC9381_B3_Prove verifies the canonical Prove() output of every vector +// in RFC 9381 §B.3 byte-for-byte (proof and beta). +func TestRFC9381_B3_Prove(t *testing.T) { + for _, v := range rfc9381B3 { + v := v + t.Run(v.name, func(t *testing.T) { + fullSK := append(mustHex(t, v.sk), mustHex(t, v.pk)...) + sk, err := NewPrivateKey(fullSK) + if err != nil { + t.Fatalf("NewPrivateKey: %v", err) + } + + beta, proof, err := sk.Prove(mustHex(t, v.alpha)) + if err != nil { + t.Fatalf("Prove: %v", err) + } + if got, want := hex.EncodeToString(proof), v.pi; got != want { + t.Errorf("proof mismatch:\n got=%s\nwant=%s", got, want) + } + if got, want := hex.EncodeToString(beta), v.beta; got != want { + t.Errorf("beta mismatch:\n got=%s\nwant=%s", got, want) + } + if len(proof) != ProofSize { + t.Errorf("proof len = %d, want %d", len(proof), ProofSize) + } + if len(beta) != BetaSize { + t.Errorf("beta len = %d, want %d", len(beta), BetaSize) + } + }) + } +} + +// TestRFC9381_B3_Verify verifies that the canonical proofs from the RFC +// validate and recover the published beta. +func TestRFC9381_B3_Verify(t *testing.T) { + for _, v := range rfc9381B3 { + v := v + t.Run(v.name, func(t *testing.T) { + pk, err := NewPublicKey(mustHex(t, v.pk)) + if err != nil { + t.Fatalf("NewPublicKey: %v", err) + } + beta, err := pk.Verify(mustHex(t, v.alpha), mustHex(t, v.pi)) + if err != nil { + t.Fatalf("Verify: %v", err) + } + if got, want := hex.EncodeToString(beta), v.beta; got != want { + t.Errorf("beta mismatch:\n got=%s\nwant=%s", got, want) + } + }) + } +} + +// TestRFC9381_B3_GenerateKey verifies that seeding GenerateKey from each +// vector's RFC-8032 seed yields the correct (sk, pk, x) tuple. +func TestRFC9381_B3_GenerateKey(t *testing.T) { + for _, v := range rfc9381B3 { + v := v + t.Run(v.name, func(t *testing.T) { + sk, err := GenerateKey(bytes.NewReader(mustHex(t, v.sk))) + if err != nil { + t.Fatalf("GenerateKey: %v", err) + } + gotPK := sk.Public().Bytes() + if !bytes.Equal(gotPK, mustHex(t, v.pk)) { + t.Errorf("public key mismatch:\n got=%x\nwant=%s", gotPK, v.pk) + } + + // Cross-check against stdlib ed25519 derivation. + edKey := ed25519.NewKeyFromSeed(mustHex(t, v.sk)) + if !bytes.Equal(edKey[32:], gotPK) { + t.Errorf("ed25519 stdlib pk mismatch:\n got=%x\nwant=%x", gotPK, edKey[32:]) + } + }) + } +} + +// TestRFC9381_B3_HashToCurveTAI verifies the hashToCurveTAI helper against +// the RFC's published intermediate H values. +func TestRFC9381_B3_HashToCurveTAI(t *testing.T) { + for _, v := range rfc9381B3 { + v := v + t.Run(v.name, func(t *testing.T) { + h, err := hashToCurveTAI(mustHex(t, v.pk), mustHex(t, v.alpha)) + if err != nil { + t.Fatalf("hashToCurveTAI: %v", err) + } + if got, want := hex.EncodeToString(h.Bytes()), v.h; got != want { + t.Errorf("H mismatch:\n got=%s\nwant=%s", got, want) + } + }) + } +} + +// TestRFC9381_B3_NonceHash verifies the SHA512(SHA512(sk)[32:] || H) nonce +// derivation (RFC 9381 §5.4.2.2). +func TestRFC9381_B3_NonceHash(t *testing.T) { + for _, v := range rfc9381B3 { + v := v + t.Run(v.name, func(t *testing.T) { + got := generateNonceHash(mustHex(t, v.sk), mustHex(t, v.h)) + if want := mustHex(t, v.k); !bytes.Equal(got, want) { + t.Errorf("k mismatch:\n got=%x\nwant=%x", got, want) + } + }) + } +} + +// TestRFC9381_B3_ProofToHash verifies the standalone ProofToHash helper +// against published betas (the precompile path uses this). +func TestRFC9381_B3_ProofToHash(t *testing.T) { + for _, v := range rfc9381B3 { + v := v + t.Run(v.name, func(t *testing.T) { + beta, err := ProofToHash(mustHex(t, v.pi)) + if err != nil { + t.Fatalf("ProofToHash: %v", err) + } + if got, want := hex.EncodeToString(beta), v.beta; got != want { + t.Errorf("beta mismatch:\n got=%s\nwant=%s", got, want) + } + }) + } +} + +// TestRoundTrip exercises the keygen→prove→verify flow with random keys. +func TestRoundTrip(t *testing.T) { + sk, err := GenerateKey(nil) + if err != nil { + t.Fatalf("GenerateKey: %v", err) + } + pk := sk.Public() + + alpha := []byte("worldland-vct-alpha") + beta1, proof, err := sk.Prove(alpha) + if err != nil { + t.Fatalf("Prove: %v", err) + } + if len(proof) != ProofSize { + t.Fatalf("proof len = %d", len(proof)) + } + + beta2, err := pk.Verify(alpha, proof) + if err != nil { + t.Fatalf("Verify rejected our own proof: %v", err) + } + if !bytes.Equal(beta1, beta2) { + t.Fatalf("beta mismatch: prove=%x verify=%x", beta1, beta2) + } +} + +// TestVerifyRejectsForgery: every single-bit flip of a valid proof must +// cause Verify to return ErrInvalidVRFProof. This is the unforgeability +// property — we sweep all 80*8 = 640 bit positions of the proof. +func TestVerifyRejectsForgery(t *testing.T) { + sk, err := GenerateKey(nil) + if err != nil { + t.Fatalf("GenerateKey: %v", err) + } + pk := sk.Public() + + alpha := []byte("forgery-test") + _, proof, err := sk.Prove(alpha) + if err != nil { + t.Fatalf("Prove: %v", err) + } + + for i := 0; i < ProofSize; i++ { + for j := uint(0); j < 8; j++ { + tampered := make([]byte, ProofSize) + copy(tampered, proof) + tampered[i] ^= 1 << j + + beta, err := pk.Verify(alpha, tampered) + if err == nil { + t.Fatalf("forgery accepted at byte %d bit %d (beta=%x)", i, j, beta) + } + } + } +} + +// TestVerifyRejectsWrongAlpha: changing alpha alone must invalidate the +// proof (RFC 9381 §3.1 unique-output property). +func TestVerifyRejectsWrongAlpha(t *testing.T) { + sk, err := GenerateKey(nil) + if err != nil { + t.Fatalf("GenerateKey: %v", err) + } + pk := sk.Public() + + _, proof, err := sk.Prove([]byte("alpha-A")) + if err != nil { + t.Fatalf("Prove: %v", err) + } + if _, err := pk.Verify([]byte("alpha-B"), proof); !errors.Is(err, ErrInvalidVRFProof) { + t.Fatalf("wrong-alpha verify err = %v, want ErrInvalidVRFProof", err) + } +} + +// TestVerifyRejectsWrongPK: the proof must be bound to the claimed pk. +func TestVerifyRejectsWrongPK(t *testing.T) { + sk1, _ := GenerateKey(nil) + sk2, _ := GenerateKey(nil) + pk2 := sk2.Public() + + _, proof, err := sk1.Prove([]byte("alpha")) + if err != nil { + t.Fatalf("Prove: %v", err) + } + if _, err := pk2.Verify([]byte("alpha"), proof); !errors.Is(err, ErrInvalidVRFProof) { + t.Fatalf("wrong-pk verify err = %v, want ErrInvalidVRFProof", err) + } +} + +// TestProveDeterministic: identical inputs must yield identical outputs +// (RFC 9381 §3.1 full-uniqueness — the basis of VCT determinism per spec +// §4.2). +func TestProveDeterministic(t *testing.T) { + sk, _ := GenerateKey(nil) + alpha := []byte("deterministic-input") + + beta1, proof1, _ := sk.Prove(alpha) + beta2, proof2, _ := sk.Prove(alpha) + + if !bytes.Equal(beta1, beta2) { + t.Errorf("nondeterministic beta") + } + if !bytes.Equal(proof1, proof2) { + t.Errorf("nondeterministic proof") + } +} + +// --- error-path coverage (spec §8.4: typed errors required) --- + +func TestNewPrivateKey_BadLength(t *testing.T) { + for _, n := range []int{0, 1, 31, 32, 63, 65, 128} { + _, err := NewPrivateKey(make([]byte, n)) + if !errors.Is(err, ErrInvalidPrivateKey) { + t.Errorf("len=%d err=%v, want ErrInvalidPrivateKey", n, err) + } + } +} + +func TestNewPublicKey_BadLength(t *testing.T) { + for _, n := range []int{0, 1, 31, 33, 64} { + _, err := NewPublicKey(make([]byte, n)) + if !errors.Is(err, ErrInvalidPublicKey) { + t.Errorf("len=%d err=%v, want ErrInvalidPublicKey", n, err) + } + } +} + +func TestNewPublicKey_NotOnCurve(t *testing.T) { + bad := invalidPointBytes(t) + if _, err := NewPublicKey(bad); !errors.Is(err, ErrInvalidPublicKey) { + t.Errorf("non-curve point err = %v, want ErrInvalidPublicKey", err) + } +} + +func TestVerify_BadProofLength(t *testing.T) { + pk, _ := NewPublicKey(mustHex(t, rfc9381B3[0].pk)) + for _, n := range []int{0, 1, 79, 81, 160} { + _, err := pk.Verify([]byte("alpha"), make([]byte, n)) + if !errors.Is(err, ErrInvalidProofLength) { + t.Errorf("len=%d err=%v, want ErrInvalidProofLength", n, err) + } + } +} + +func TestVerify_BadGammaPoint(t *testing.T) { + pk, _ := NewPublicKey(mustHex(t, rfc9381B3[0].pk)) + bad := make([]byte, ProofSize) + copy(bad[:32], invalidPointBytes(t)) + if _, err := pk.Verify([]byte("alpha"), bad); !errors.Is(err, ErrInvalidProofPoint) { + t.Errorf("bad gamma err = %v, want ErrInvalidProofPoint", err) + } +} + +func TestVerify_NonCanonicalScalar(t *testing.T) { + // s field is 32 bytes at proof[48:80]; setting it all-0xff exceeds the + // group order and must be rejected as non-canonical (RFC 9381 §5.3). + pk, _ := NewPublicKey(mustHex(t, rfc9381B3[0].pk)) + bad := mustHex(t, rfc9381B3[0].pi) + for i := 48; i < 80; i++ { + bad[i] = 0xff + } + if _, err := pk.Verify([]byte(""), bad); !errors.Is(err, ErrInvalidProofScalar) { + t.Errorf("non-canonical s err = %v, want ErrInvalidProofScalar", err) + } +} + +func TestProofToHash_BadLength(t *testing.T) { + for _, n := range []int{0, 79, 81} { + if _, err := ProofToHash(make([]byte, n)); !errors.Is(err, ErrInvalidProofLength) { + t.Errorf("len=%d err=%v, want ErrInvalidProofLength", n, err) + } + } +} + +func TestProofToHash_BadGamma(t *testing.T) { + bad := make([]byte, ProofSize) + copy(bad[:32], invalidPointBytes(t)) + if _, err := ProofToHash(bad); !errors.Is(err, ErrInvalidProofPoint) { + t.Errorf("bad gamma err = %v, want ErrInvalidProofPoint", err) + } +} + +// TestErrorMessages: the spec §8.4 mandates typed errors, but messages +// should also be greppable. Validate prefix. +func TestErrorMessages(t *testing.T) { + errs := []error{ + ErrInvalidPrivateKey, ErrInvalidPublicKey, ErrInvalidProofLength, + ErrInvalidProofPoint, ErrInvalidProofScalar, ErrInvalidVRFProof, + ErrHashToCurveFailed, + } + for _, e := range errs { + if e == nil { + t.Errorf("nil error in error set") + continue + } + if !strings.HasPrefix(e.Error(), "ecvrf: ") { + t.Errorf("error %q missing 'ecvrf:' prefix", e.Error()) + } + } +} + +// BenchmarkProve / BenchmarkVerify are required by spec §5.1 to validate the +// <1ms target on reference hardware (4-core, 16 GB). +func BenchmarkProve(b *testing.B) { + sk, err := GenerateKey(nil) + if err != nil { + b.Fatal(err) + } + alpha := []byte("benchmark-alpha") + b.ResetTimer() + for i := 0; i < b.N; i++ { + _, _, _ = sk.Prove(alpha) + } +} + +func BenchmarkVerify(b *testing.B) { + sk, err := GenerateKey(nil) + if err != nil { + b.Fatal(err) + } + pk := sk.Public() + alpha := []byte("benchmark-alpha") + _, proof, _ := sk.Prove(alpha) + b.ResetTimer() + for i := 0; i < b.N; i++ { + _, _ = pk.Verify(alpha, proof) + } +} diff --git a/crypto/vrf/ecvrf/errors.go b/crypto/vrf/ecvrf/errors.go new file mode 100644 index 000000000000..af9ac1fc8076 --- /dev/null +++ b/crypto/vrf/ecvrf/errors.go @@ -0,0 +1,28 @@ +// Copyright 2026 The WorldLand / go-ethereum Authors +// This file is part of the WorldLand library. +// +// The WorldLand library is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// The WorldLand library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. + +package ecvrf + +import "errors" + +// Typed errors required by spec §8.4. Higher layers (consensus engine, +// precompile) wrap these with block-height / DID context as needed. +var ( + ErrInvalidPrivateKey = errors.New("ecvrf: invalid private key") + ErrInvalidPublicKey = errors.New("ecvrf: invalid public key") + ErrInvalidProofLength = errors.New("ecvrf: invalid proof length") + ErrInvalidProofPoint = errors.New("ecvrf: invalid proof curve point") + ErrInvalidProofScalar = errors.New("ecvrf: invalid proof scalar") + ErrInvalidVRFProof = errors.New("ecvrf: VRF proof failed verification") + ErrHashToCurveFailed = errors.New("ecvrf: hash-to-curve exhausted counter") +) diff --git a/crypto/vrf/registry.go b/crypto/vrf/registry.go new file mode 100644 index 000000000000..f7e16603bdb1 --- /dev/null +++ b/crypto/vrf/registry.go @@ -0,0 +1,111 @@ +// Copyright 2026 The WorldLand / go-ethereum Authors +// This file is part of the WorldLand library. +// +// The WorldLand library is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// The WorldLand library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. + +// Package vrf is the WorldLand VRF crypto-agility hub. The block header's +// VRFScheme byte (spec §4.1, §8.3) selects which underlying VRF cipher +// suite is used to verify a proof. All scheme dispatch is concentrated in +// this file: future hardforks adding a new VRF (e.g. a post-quantum scheme) +// only need to register it here. +package vrf + +import ( + "errors" + "fmt" + + "github.com/cryptoecc/WorldLand/crypto/vrf/ecvrf" +) + +// SchemeID is the WorldLand crypto-agility byte stored in +// types.Header.VRFScheme. It is independent of the underlying RFC suite +// byte (which is, e.g., 0x03 for ECVRF-EDWARDS25519-SHA512-TAI inside +// RFC 9381's hash domain separation). +type SchemeID uint8 + +const ( + // SchemeECVRFEdwards25519SHA512TAI is the v.1.0 default scheme, + // implementing RFC 9381 ECVRF-EDWARDS25519-SHA512-TAI. + SchemeECVRFEdwards25519SHA512TAI SchemeID = 0x01 + + // 0x02..0xFF are reserved for future schemes (e.g. PQ-VRF). Each + // future scheme MUST add a case to schemeRegistry below in a single, + // reviewable change. +) + +// ErrUnknownScheme is returned for any SchemeID that has not been +// registered. Callers should treat this as a hard validation failure. +var ErrUnknownScheme = errors.New("vrf: unknown scheme id") + +// Verifier is the minimal interface every registered VRF scheme must +// implement. ProofSize is the canonical proof length for that scheme; +// PublicKeySize is the canonical public-key length. Higher layers use +// these to slice the precompile input (spec §4.5) and to validate header +// field lengths (spec §4.1). +type Verifier interface { + // Name returns a human-readable identifier (used in logs/RPC). + Name() string + // ProofSize is the byte length of a valid proof. + ProofSize() int + // PublicKeySize is the byte length of a valid public key. + PublicKeySize() int + // Verify checks (pk, alpha, proof) and returns the VRF output beta + // on success, or a typed error on failure. + Verify(pk, alpha, proof []byte) (beta []byte, err error) + // ProofToHash extracts beta from proof without verifying. Callers + // MUST run Verify when authenticity matters. + ProofToHash(proof []byte) (beta []byte, err error) +} + +// LookupScheme returns the Verifier registered for id, or +// (nil, ErrUnknownScheme). +func LookupScheme(id SchemeID) (Verifier, error) { + if v, ok := schemeRegistry[id]; ok { + return v, nil + } + return nil, fmt.Errorf("%w: 0x%02x", ErrUnknownScheme, byte(id)) +} + +// schemeRegistry is populated via init() below. To add a new scheme: +// 1. Implement the Verifier interface in a sibling sub-package. +// 2. Reserve a new SchemeID constant above. +// 3. Add a single registration line to init(). +var schemeRegistry = map[SchemeID]Verifier{} + +func register(id SchemeID, v Verifier) { + if _, ok := schemeRegistry[id]; ok { + panic(fmt.Sprintf("vrf: duplicate registration for scheme 0x%02x", byte(id))) + } + schemeRegistry[id] = v +} + +func init() { + register(SchemeECVRFEdwards25519SHA512TAI, ecvrfTAIVerifier{}) +} + +// ecvrfTAIVerifier adapts the ecvrf package to the Verifier interface. +type ecvrfTAIVerifier struct{} + +func (ecvrfTAIVerifier) Name() string { return "ECVRF-EDWARDS25519-SHA512-TAI" } +func (ecvrfTAIVerifier) ProofSize() int { return ecvrf.ProofSize } +func (ecvrfTAIVerifier) PublicKeySize() int { return ecvrf.PublicKeySize } + +func (ecvrfTAIVerifier) Verify(pk, alpha, proof []byte) ([]byte, error) { + publicKey, err := ecvrf.NewPublicKey(pk) + if err != nil { + return nil, err + } + return publicKey.Verify(alpha, proof) +} + +func (ecvrfTAIVerifier) ProofToHash(proof []byte) ([]byte, error) { + return ecvrf.ProofToHash(proof) +} diff --git a/crypto/vrf/registry_test.go b/crypto/vrf/registry_test.go new file mode 100644 index 000000000000..a6a5566f6baf --- /dev/null +++ b/crypto/vrf/registry_test.go @@ -0,0 +1,111 @@ +// Copyright 2026 The WorldLand / go-ethereum Authors +// This file is part of the WorldLand library. +// +// The WorldLand library is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +package vrf + +import ( + "bytes" + "encoding/hex" + "errors" + "testing" + + "github.com/cryptoecc/WorldLand/crypto/vrf/ecvrf" +) + +func mustHex(t *testing.T, s string) []byte { + t.Helper() + b, err := hex.DecodeString(s) + if err != nil { + t.Fatalf("hex %q: %v", s, err) + } + return b +} + +// TestLookupScheme_Default confirms SchemeID 0x01 dispatches to the RFC 9381 +// TAI implementation and that constants stay aligned with the ecvrf +// package (spec §8.3). +func TestLookupScheme_Default(t *testing.T) { + v, err := LookupScheme(SchemeECVRFEdwards25519SHA512TAI) + if err != nil { + t.Fatalf("LookupScheme: %v", err) + } + if got, want := v.Name(), "ECVRF-EDWARDS25519-SHA512-TAI"; got != want { + t.Errorf("Name() = %q, want %q", got, want) + } + if v.ProofSize() != ecvrf.ProofSize { + t.Errorf("ProofSize() = %d, want %d", v.ProofSize(), ecvrf.ProofSize) + } + if v.PublicKeySize() != ecvrf.PublicKeySize { + t.Errorf("PublicKeySize() = %d, want %d", v.PublicKeySize(), ecvrf.PublicKeySize) + } +} + +// TestLookupScheme_Unknown checks every unregistered scheme byte returns +// the typed ErrUnknownScheme. +func TestLookupScheme_Unknown(t *testing.T) { + for id := 0; id < 256; id++ { + if SchemeID(id) == SchemeECVRFEdwards25519SHA512TAI { + continue + } + _, err := LookupScheme(SchemeID(id)) + if !errors.Is(err, ErrUnknownScheme) { + t.Errorf("scheme 0x%02x err = %v, want ErrUnknownScheme", id, err) + } + } +} + +// TestRegistryVerify_RFC9381Vector1 dispatches an RFC 9381 §B.3 Example 1 +// verification through the registry to confirm the wiring is correct +// end-to-end (header byte → scheme → verify → beta). +func TestRegistryVerify_RFC9381Vector1(t *testing.T) { + pk := mustHex(t, "d75a980182b10ab7d54bfed3c964073a0ee172f3daa62325af021a68f707511a") + pi := mustHex(t, + "8657106690b5526245a92b003bb079ccd1a92130477671f6fc01ad16f26f723f"+ + "5e8bd1839b414219e8626d393787a192241fc442e6569e96c462f62b8079b9ed"+ + "83ff2ee21c90c7c398802fdeebea4001") + wantBeta := mustHex(t, + "90cf1df3b703cce59e2a35b925d411164068269d7b2d29f3301c03dd757876ff"+ + "66b71dda49d2de59d03450451af026798e8f81cd2e333de5cdf4f3e140fdd8ae") + + v, err := LookupScheme(SchemeECVRFEdwards25519SHA512TAI) + if err != nil { + t.Fatalf("LookupScheme: %v", err) + } + beta, err := v.Verify(pk, []byte{}, pi) + if err != nil { + t.Fatalf("Verify: %v", err) + } + if !bytes.Equal(beta, wantBeta) { + t.Fatalf("beta mismatch:\n got=%x\nwant=%x", beta, wantBeta) + } + + // ProofToHash standalone path agrees. + beta2, err := v.ProofToHash(pi) + if err != nil { + t.Fatalf("ProofToHash: %v", err) + } + if !bytes.Equal(beta2, wantBeta) { + t.Fatalf("ProofToHash beta mismatch") + } +} + +// TestRegistryVerify_BadInputs confirms the registry forwards typed +// errors from the underlying suite. +func TestRegistryVerify_BadInputs(t *testing.T) { + v, _ := LookupScheme(SchemeECVRFEdwards25519SHA512TAI) + + // Wrong public-key length. + if _, err := v.Verify(make([]byte, 16), []byte{}, make([]byte, ecvrf.ProofSize)); !errors.Is(err, ecvrf.ErrInvalidPublicKey) { + t.Errorf("short pk err = %v, want ErrInvalidPublicKey", err) + } + // Wrong proof length. + pk := mustHex(t, "d75a980182b10ab7d54bfed3c964073a0ee172f3daa62325af021a68f707511a") + if _, err := v.Verify(pk, []byte{}, make([]byte, 79)); !errors.Is(err, ecvrf.ErrInvalidProofLength) { + t.Errorf("short proof err = %v, want ErrInvalidProofLength", err) + } +} diff --git a/go.mod b/go.mod index fd8e402fe9b7..a1cd71299054 100644 --- a/go.mod +++ b/go.mod @@ -70,6 +70,8 @@ require ( gopkg.in/natefinch/npipe.v2 v2.0.0-20160621034901-c1b8fa8bdcce ) +require filippo.io/edwards25519 v1.0.0 + require ( github.com/Azure/azure-sdk-for-go/sdk/azcore v0.21.1 // indirect github.com/Azure/azure-sdk-for-go/sdk/internal v0.8.3 // indirect diff --git a/go.sum b/go.sum index 0ffc432e76ca..50dcd29a5bb6 100644 --- a/go.sum +++ b/go.sum @@ -18,6 +18,8 @@ cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiy cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos= collectd.org v0.3.0/go.mod h1:A/8DzQBkF6abtvrT2j/AU/4tiBgJWYyh0y/oB/4MlWE= dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= +filippo.io/edwards25519 v1.0.0 h1:0wAIcmJUqRdI8IJ/3eGi5/HwXZWPujYXXlkrQogz0Ek= +filippo.io/edwards25519 v1.0.0/go.mod h1:N1IkdkCkiLB6tki+MYJoSx2JTY9NUlxZE7eHn5EwJns= github.com/Azure/azure-sdk-for-go/sdk/azcore v0.21.1 h1:qoVeMsc9/fh/yhxVaA0obYjVH/oI/ihrOoMwsLS9KSA= github.com/Azure/azure-sdk-for-go/sdk/azcore v0.21.1/go.mod h1:fBF9PQNqB8scdgpZ3ufzaLntG0AG7C1WjPMsiFOmfHM= github.com/Azure/azure-sdk-for-go/sdk/internal v0.8.3 h1:E+m3SkZCN0Bf5q7YdTs5lSm2CYY3CK4spn5OmUIiQtk= @@ -466,8 +468,6 @@ golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPh golang.org/x/crypto v0.0.0-20200820211705-5c72a883971a/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20201221181555-eec23a3978ad/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= -golang.org/x/crypto v0.0.0-20210921155107-089bfa567519 h1:7I4JAnoQBe7ZtJcBaYHi5UtiO8tQHbUSXxL+pnGRANg= -golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.12.0 h1:tFM/ta59kqch6LlvYnPa0yx5a83cL2nHflFhYKvv9Yk= golang.org/x/crypto v0.12.0/go.mod h1:NF0Gs7EO5K4qLn+Ylc+fih8BSTeIjAP05siRnAh98yw= golang.org/x/exp v0.0.0-20180321215751-8460e604b9de/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= @@ -499,8 +499,7 @@ golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.6.0-dev.0.20211013180041-c96bc1413d57 h1:LQmS1nU0twXLA96Kt7U9qtHJEbBk3z6Q0V4UXjZkpr4= -golang.org/x/mod v0.6.0-dev.0.20211013180041-c96bc1413d57/go.mod h1:3p9vT2HGsQu2K1YbXdKPJLVgG5VJdoTa1poYQBtP1AY= +golang.org/x/mod v0.8.0 h1:LUYupSeNrTNCGzR/hVBk2NHZO4hXcVaW1k4Qx7rjPx8= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -527,8 +526,6 @@ golang.org/x/net v0.0.0-20210220033124-5f55cee0dc0d/go.mod h1:m0MpNAwzfU5UDzcl9v golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210610132358-84b48f89b13b/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.0.0-20220607020251-c690dde0001d h1:4SFsTMi4UahlKoloni7L4eYzhFRifURQLw+yv0QDCx8= -golang.org/x/net v0.0.0-20220607020251-c690dde0001d/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.14.0 h1:BONx9s002vGdD9umnlX1Po8vOZmrgH34qlHcD1MfK14= golang.org/x/net v0.14.0/go.mod h1:PpSgVXXLK0OxS0F31C1/tv6XNguvCrnXIDrFMspZIUI= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= @@ -543,7 +540,6 @@ golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20210220032951-036812b2e83c h1:5KslGYwFpkhGh+Q16bwMP3cOontH8FOep7tGV86Y7SQ= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -587,14 +583,10 @@ golang.org/x/sys v0.0.0-20210316164454-77fc1eacc6aa/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20210324051608-47abb6519492/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210420205809-ac73e9fd8988/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.9.0 h1:KS/R3tvhPqvJvwcKfnBHJwwthS11LRhmM5D59eEXa0s= -golang.org/x/sys v0.9.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.11.0 h1:eG7RXZHdqOJ1i+0lgLgCpSXAp6M3LYlAo6osgSi0xOM= golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= -golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 h1:JGgROgKl9N8DuW20oFS5gxc+lE67/N3FcwmBPMe7ArY= -golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.11.0 h1:F9tnn/DA/Im8nCwm+fX+1/eBwi4qFjRT++MhtVC4ZX0= golang.org/x/term v0.11.0/go.mod h1:zC9APTIj3jG3FdV/Ons+XE1riIZXG4aZ4GTHiPZJPIU= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= @@ -604,7 +596,6 @@ golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.12.0 h1:k+n5B8goJNdU7hSvEtMUz3d1Q6D/XW4COJSJR6fN0mc= golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= @@ -641,8 +632,7 @@ golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapK golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200108203644-89082a384178/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= -golang.org/x/tools v0.1.8-0.20211029000441-d6a9af8af023 h1:0c3L82FDQ5rt1bjTBlchS8t6RQ6299/+5bWMnRLh+uI= -golang.org/x/tools v0.1.8-0.20211029000441-d6a9af8af023/go.mod h1:nABZi5QlRsZVlzPpHl034qft6wpY4eDcsTt5AaioBiU= +golang.org/x/tools v0.6.0 h1:BOw41kyTf3PuCW1pVQf8+Cyg8pMlkYB1oo9iJ6D/lKM= golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=