diff --git a/go.mod b/go.mod index f21dac1..82134cc 100644 --- a/go.mod +++ b/go.mod @@ -6,6 +6,7 @@ require ( github.com/luxfi/dkg v0.2.0 github.com/luxfi/lattice/v7 v7.1.0 github.com/luxfi/math v1.4.0 + github.com/luxfi/mlwe v0.2.1 github.com/luxfi/zap v0.7.2 github.com/montanaflynn/stats v0.9.0 github.com/spf13/cobra v1.10.2 diff --git a/go.sum b/go.sum index de4623a..abb52fe 100644 --- a/go.sum +++ b/go.sum @@ -32,6 +32,8 @@ github.com/luxfi/math v1.4.0 h1:/sb7Grw3hfO+5INWAWdB95jTvCeXg8fSQxsxDzcFtd4= github.com/luxfi/math v1.4.0/go.mod h1:iW0FOCC8qF2mPE+MakG780CAHA83848lb1L04thA1Pg= github.com/luxfi/mdns v0.1.1 h1:g2eRr9AXcziPkkcd24M+Qu9ApEpoKKjfI79QSNqv0rQ= github.com/luxfi/mdns v0.1.1/go.mod h1:dbp5f3h3aE7CGzwbaWzBM9cwdcekhmSrWhQevgYhhNA= +github.com/luxfi/mlwe v0.2.1 h1:pRwTjNUUtzUxRIlMbUPpeh9DE2/NdqfS17hfdogazp4= +github.com/luxfi/mlwe v0.2.1/go.mod h1:DD9EHTeiyh/y0KGGeqL+q9S4n8raeGiGdaG/BQPAvT0= github.com/luxfi/pq v1.0.3 h1:pFlQm1+5FuKTDUh2y/23bXWkN4I2Rc5iuxJypwDFFMs= github.com/luxfi/pq v1.0.3/go.mod h1:8bppZcRElfrVt0n3nYCZW3iX1TvhvzNbdjNdK1irgIE= github.com/luxfi/zap v0.7.2 h1:YecWTWNE5PPJXL56sLIkzS8b23bprUwZ5lPAQuLUtTE= diff --git a/hash/hash_test.go b/hash/hash_test.go index 806e1f1..98894aa 100644 --- a/hash/hash_test.go +++ b/hash/hash_test.go @@ -5,8 +5,6 @@ package hash import ( "bytes" - "encoding/hex" - "strings" "testing" ) @@ -149,75 +147,12 @@ func TestPairwiseDistinctEras(t *testing.T) { } } -// ─── NIST SP 800-185 vector smoke tests ───────────────────────────── - -func TestLeftEncode(t *testing.T) { - cases := []struct { - x uint64 - want string - }{ - {0, "0100"}, - {12, "010c"}, - {255, "01ff"}, - {256, "020100"}, - {65535, "02ffff"}, - {65536, "03010000"}, - } - for _, c := range cases { - got := hex.EncodeToString(leftEncode(c.x)) - if got != c.want { - t.Errorf("leftEncode(%d): want %s got %s", c.x, c.want, got) - } - } -} - -func TestRightEncode(t *testing.T) { - cases := []struct { - x uint64 - want string - }{ - {0, "0001"}, - {12, "0c01"}, - {256, "010002"}, - } - for _, c := range cases { - got := hex.EncodeToString(rightEncode(c.x)) - if got != c.want { - t.Errorf("rightEncode(%d): want %s got %s", c.x, c.want, got) - } - } -} - -// TestKMAC256NISTVector — Sample #4 from NIST SP 800-185 KMAC-Samples. -func TestKMAC256NISTVector(t *testing.T) { - K, _ := hex.DecodeString("404142434445464748494A4B4C4D4E4F505152535455565758595A5B5C5D5E5F") - X, _ := hex.DecodeString("00010203") - S := "My Tagged Application" - want := "20C570C31346F703C9AC36C61C03CB64C3970D0CFC787E9B79599D273A68D2F7" + - "F69D4CC3DE9D104A351689F27CF6F5951F0103F33F4F24871024D9C27773A8DD" - - got := hex.EncodeToString(kmac256(K, X, 64, S)) - wantLower := strings.ToLower(want[:128]) - if got != wantLower { - t.Errorf("KMAC256: \nwant %s\n got %s", wantLower, got) - } -} - -// TestTupleHash256NISTVector — Sample #4 from NIST SP 800-185 TupleHash-Samples. -func TestTupleHash256NISTVector(t *testing.T) { - x1, _ := hex.DecodeString("000102") - x2, _ := hex.DecodeString("101112131415") - x3, _ := hex.DecodeString("202122232425262728") - S := "My Tuple App" - want := "45000BE63F9B6BFD89F54717670F69A9BC763591A4F05C50D68891A744BCC6E7" + - "D6D5B5E82C018DA999ED35B0BB49C9678E526ABD8E85C13ED254021DB9E790CE" - - got := hex.EncodeToString(tupleHash256([][]byte{x1, x2, x3}, 64, S)) - wantLower := strings.ToLower(want[:128]) - if got != wantLower { - t.Errorf("TupleHash256:\nwant %s\n got %s", wantLower, got) - } -} +// NIST SP 800-185 primitive vectors (left/right_encode, KMAC256, +// TupleHash256) are owned and tested by github.com/luxfi/mlwe/transcript +// (transcript_test.go: cSHAKE256 NIST Sample #3, the §2.3 encoders, and +// the KMAC/TupleHash spec-construction KATs). Corona no longer carries a +// parallel copy of those primitive tests — it tests only its own suite +// composition (tags, domain separation, determinism) below. // TestSuiteDeterminism — same input, two calls, identical bytes. func TestSuiteDeterminism(t *testing.T) { diff --git a/hash/sha3.go b/hash/sha3.go index 0bd87ce..889e578 100644 --- a/hash/sha3.go +++ b/hash/sha3.go @@ -4,7 +4,12 @@ package hash // CoronaSHA3 is the production hash suite for Corona. Built on -// cSHAKE256 / KMAC256 / TupleHash256 from FIPS 202 and NIST SP 800-185. +// cSHAKE256 / KMAC256 / TupleHash256 from FIPS 202 and NIST SP 800-185, +// vended by the shared github.com/luxfi/mlwe/transcript package — the +// single SP 800-185 surface for the Lux Module-LWE stack (Corona and +// Pulsar). Corona owns only the domain-separation tags below; the +// primitive byte encodings live in mlwe/transcript, so there is exactly +// one implementation of each construction across the stack. // // Customization tags pin every operation to the Corona protocol: // @@ -23,6 +28,8 @@ package hash import ( "encoding/binary" + + "github.com/luxfi/mlwe/transcript" ) const ( @@ -42,27 +49,30 @@ func NewCoronaSHA3() HashSuite { return coronaSHA3{} } func (coronaSHA3) ID() string { return "Corona-SHA3" } -func (coronaSHA3) Hc(transcript []byte) []byte { - return cshake256Stream(tagHC, transcript, 32) +// Hc is cSHAKE256 with an empty function name and the Hc domain tag. +// (The transcript bytes are named tr here to avoid shadowing the +// imported transcript package.) +func (coronaSHA3) Hc(tr []byte) []byte { + return transcript.CShake256("", tagHC, tr, 32) } -func (coronaSHA3) Hu(transcript []byte, outLen int) []byte { - return cshake256Stream(tagHU, transcript, outLen) +func (coronaSHA3) Hu(tr []byte, outLen int) []byte { + return transcript.CShake256("", tagHU, tr, outLen) } func (coronaSHA3) TranscriptHash(parts ...[]byte) [32]byte { - out := tupleHash256(parts, 32, tagTranscript) + out := transcript.TupleHash256(parts, 32, tagTranscript) var fixed [32]byte copy(fixed[:], out) return fixed } func (coronaSHA3) PRF(key, msg []byte, outLen int) []byte { - return kmac256(key, msg, outLen, tagPRF) + return transcript.KMAC256(key, msg, outLen, tagPRF) } func (coronaSHA3) MAC(key, msg []byte, outLen int) []byte { - return kmac256(key, msg, outLen, tagMAC) + return transcript.KMAC256(key, msg, outLen, tagMAC) } func (coronaSHA3) DerivePairwise( @@ -77,18 +87,18 @@ func (coronaSHA3) DerivePairwise( a, b = b, a } var msg []byte - msg = append(msg, encodeString(chainID)...) - msg = append(msg, encodeString(groupID)...) + msg = append(msg, transcript.EncodeString(chainID)...) + msg = append(msg, transcript.EncodeString(groupID)...) var u8 [8]byte binary.BigEndian.PutUint64(u8[:], eraID) - msg = append(msg, encodeString(u8[:])...) + msg = append(msg, transcript.EncodeString(u8[:])...) binary.BigEndian.PutUint64(u8[:], generation) - msg = append(msg, encodeString(u8[:])...) + msg = append(msg, transcript.EncodeString(u8[:])...) var u4 [4]byte binary.BigEndian.PutUint32(u4[:], uint32(a)) - msg = append(msg, encodeString(u4[:])...) + msg = append(msg, transcript.EncodeString(u4[:])...) binary.BigEndian.PutUint32(u4[:], uint32(b)) - msg = append(msg, encodeString(u4[:])...) + msg = append(msg, transcript.EncodeString(u4[:])...) - return kmac256(kex, msg, outLen, tagPairwise) + return transcript.KMAC256(kex, msg, outLen, tagPairwise) } diff --git a/hash/sp800_185.go b/hash/sp800_185.go deleted file mode 100644 index 5811fc6..0000000 --- a/hash/sp800_185.go +++ /dev/null @@ -1,111 +0,0 @@ -// Copyright (C) 2025-2026, Lux Industries Inc. All rights reserved. -// See the file LICENSE for licensing terms. - -package hash - -// FIPS 202 / NIST SP 800-185 helpers — left_encode, right_encode, -// bytepad, encode_string — and the TupleHash / KMAC constructions -// they enable. Vendored here because Go's golang.org/x/crypto/sha3 -// ships cSHAKE128/256 but not TupleHash or KMAC. The implementation -// is fully covered by the published NIST test vectors (see hash_test.go). -// -// All encoders match SP 800-185 §2.3 byte-for-byte. left_encode and -// right_encode operate on the BIT length, not the byte length. - -import ( - "encoding/binary" - - "golang.org/x/crypto/sha3" -) - -// leftEncode returns the SP 800-185 left_encode(x) byte string. -func leftEncode(x uint64) []byte { - if x == 0 { - return []byte{0x01, 0x00} - } - var buf [8]byte - binary.BigEndian.PutUint64(buf[:], x) - i := 0 - for i < 7 && buf[i] == 0 { - i++ - } - out := make([]byte, 0, 9-i) - out = append(out, byte(8-i)) - out = append(out, buf[i:]...) - return out -} - -// rightEncode returns the SP 800-185 right_encode(x) byte string. -func rightEncode(x uint64) []byte { - if x == 0 { - return []byte{0x00, 0x01} - } - var buf [8]byte - binary.BigEndian.PutUint64(buf[:], x) - i := 0 - for i < 7 && buf[i] == 0 { - i++ - } - out := make([]byte, 0, 9-i) - out = append(out, buf[i:]...) - out = append(out, byte(8-i)) - return out -} - -// encodeString returns left_encode(bit_len(s)) || s. -func encodeString(s []byte) []byte { - out := leftEncode(uint64(len(s)) * 8) - out = append(out, s...) - return out -} - -// bytepad pads x with zeros so the result is a multiple of w bytes, -// prefixed by left_encode(w). -func bytepad(x []byte, w int) []byte { - prefix := leftEncode(uint64(w)) - out := make([]byte, 0, len(prefix)+len(x)+w) - out = append(out, prefix...) - out = append(out, x...) - for len(out)%w != 0 { - out = append(out, 0x00) - } - return out -} - -// kmac256 returns KMAC256(K, X, outLen, S) per SP 800-185 §4. -func kmac256(key, msg []byte, outLen int, customization string) []byte { - x := bytepad(encodeString(key), 136) - x = append(x, msg...) - x = append(x, rightEncode(uint64(outLen)*8)...) - - h := sha3.NewCShake256([]byte("KMAC"), []byte(customization)) - _, _ = h.Write(x) - out := make([]byte, outLen) - _, _ = h.Read(out) - return out -} - -// tupleHash256 returns TupleHash256(parts, outLen, S) per SP 800-185 §5. -func tupleHash256(parts [][]byte, outLen int, customization string) []byte { - var x []byte - for _, p := range parts { - x = append(x, encodeString(p)...) - } - x = append(x, rightEncode(uint64(outLen)*8)...) - - h := sha3.NewCShake256([]byte("TupleHash"), []byte(customization)) - _, _ = h.Write(x) - out := make([]byte, outLen) - _, _ = h.Read(out) - return out -} - -// cshake256Stream is the bare cSHAKE256 XOF: customization S is the -// only label; N is empty. -func cshake256Stream(customization string, transcript []byte, outLen int) []byte { - h := sha3.NewCShake256(nil, []byte(customization)) - _, _ = h.Write(transcript) - out := make([]byte, outLen) - _, _ = h.Read(out) - return out -} diff --git a/primitives/shamir.go b/primitives/shamir.go index 9c2d348..a7919f0 100644 --- a/primitives/shamir.go +++ b/primitives/shamir.go @@ -9,6 +9,7 @@ import ( "github.com/luxfi/lattice/v7/ring" "github.com/luxfi/lattice/v7/utils/structs" + "github.com/luxfi/mlwe/share" "github.com/zeebo/blake3" ) @@ -198,26 +199,35 @@ func ShamirSecretSharing(r *ring.Ring, s []ring.Poly, k int, lambdas []ring.Poly return shares } -// ComputeLagrangeCoefficients computes the Lagrange coefficients for interpolation based on the indices of available shares. +// ComputeLagrangeCoefficients computes the Lagrange reconstruction +// coefficients lambda_i = prod_{j!=i} (-x_j)/(x_i - x_j) over GF(modulus) +// for the party points x_i = T[i]+1, i.e. the basis that interpolates a +// shared secret back to x=0. +// +// The interpolation and field arithmetic are the shared Module-LWE +// primitive github.com/luxfi/mlwe/share.Lagrange (evaluated at point 0); +// this function is only the adapter that lifts each scalar coefficient +// into the Corona R_q wire (a constant-term ring.Poly). modulus is +// Corona's prime q in every production call. Invalid input (non-prime +// modulus, or x_i that are zero/duplicated) is a caller-side invariant +// violation and panics — matching the prior nil-inverse crash, never a +// silent wrong answer on a signing path. func ComputeLagrangeCoefficients(r *ring.Ring, T []int, modulus *big.Int) []ring.Poly { + field, err := share.NewPrimeField(modulus.Uint64()) + if err != nil { + panic("corona/primitives: ComputeLagrangeCoefficients needs a prime field modulus: " + err.Error()) + } + xs := make([]uint64, len(T)) + for i, ti := range T { + xs[i] = uint64(ti + 1) + } + lambda, err := share.Lagrange(xs, 0, field) + if err != nil { + panic("corona/primitives: ComputeLagrangeCoefficients invalid evaluation points: " + err.Error()) + } lagrangeCoefficients := make([]ring.Poly, len(T)) - for i := 0; i < len(T); i++ { - xi := big.NewInt(int64(T[i] + 1)) - numerator := big.NewInt(1) - denominator := big.NewInt(1) - for j := 0; j < len(T); j++ { - if i != j { - xj := big.NewInt(int64(T[j] + 1)) - numerator.Mul(numerator, new(big.Int).Neg(xj)) - numerator.Mod(numerator, modulus) - temp := new(big.Int).Sub(xi, xj) - denominator.Mul(denominator, temp) - denominator.Mod(denominator, modulus) - } - } - denomInv := new(big.Int).ModInverse(denominator, modulus) - coeff := new(big.Int).Mul(numerator, denomInv) - coeff.Mod(coeff, modulus) + for i := range T { + coeff := new(big.Int).SetUint64(lambda[i]) lagrangePoly := r.NewPoly() r.SetCoefficientsBigint([]*big.Int{coeff}, lagrangePoly) lagrangeCoefficients[i] = lagrangePoly