refactor(hash,primitives): route SP 800-185 + Lagrange onto luxfi/mlwe v0.2.1

Byte-preserving de-dup: Corona's SP 800-185 transcript surface
(hash/sha3.go; deletes the local hash/sp800_185.go) and
ComputeLagrangeCoefficients (primitives/shamir.go) now consume the
shared github.com/luxfi/mlwe base — the single Module-LWE primitive
surface for the Lux stack (Corona + Pulsar).

Routed onto mlwe:
  - hash/sha3.go: cSHAKE256 / KMAC256 / TupleHash256 / EncodeString
    -> github.com/luxfi/mlwe/transcript. Corona retains ONLY its
    domain-separation tags; the byte encodings live in mlwe/transcript.
  - primitives/shamir.go: ComputeLagrangeCoefficients delegates the
    field arithmetic + interpolation to github.com/luxfi/mlwe/share
    (NewPrimeField + Lagrange at point 0); Corona keeps the adapter
    that lifts each scalar into the R_q constant-term ring.Poly.

Deliberately NOT routed (byte-incompatible / audited):
  - Lattigo 48-bit ring stays Lattigo (re-rolling audited Lattigo forbidden).
  - The seeded Shamir deal (ShamirSecretSharing) stays Corona-specific
    (its sampler is byte-incompatible with mlwe.Split).

Corona remains Module-LWE (Ringtail/Raccoon line).

Byte-parity gate: every Corona KAT oracle emits byte-identical output
old (Corona-local impls) == new (mlwe-routed). Go byte-parity is the
correctness gate; the C++ cross-runtime byte gate is a post-merge CI
confirmation (see merge commit CI-GATE note).
This commit is contained in:
zeekay
2026-06-28 13:02:56 -07:00
parent 17ac4f350e
commit 9e6a6cab8c
6 changed files with 62 additions and 215 deletions
+1
View File
@@ -6,6 +6,7 @@ require (
github.com/luxfi/dkg v0.2.0 github.com/luxfi/dkg v0.2.0
github.com/luxfi/lattice/v7 v7.1.0 github.com/luxfi/lattice/v7 v7.1.0
github.com/luxfi/math v1.4.0 github.com/luxfi/math v1.4.0
github.com/luxfi/mlwe v0.2.1
github.com/luxfi/zap v0.7.2 github.com/luxfi/zap v0.7.2
github.com/montanaflynn/stats v0.9.0 github.com/montanaflynn/stats v0.9.0
github.com/spf13/cobra v1.10.2 github.com/spf13/cobra v1.10.2
+2
View File
@@ -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/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 h1:g2eRr9AXcziPkkcd24M+Qu9ApEpoKKjfI79QSNqv0rQ=
github.com/luxfi/mdns v0.1.1/go.mod h1:dbp5f3h3aE7CGzwbaWzBM9cwdcekhmSrWhQevgYhhNA= 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 h1:pFlQm1+5FuKTDUh2y/23bXWkN4I2Rc5iuxJypwDFFMs=
github.com/luxfi/pq v1.0.3/go.mod h1:8bppZcRElfrVt0n3nYCZW3iX1TvhvzNbdjNdK1irgIE= github.com/luxfi/pq v1.0.3/go.mod h1:8bppZcRElfrVt0n3nYCZW3iX1TvhvzNbdjNdK1irgIE=
github.com/luxfi/zap v0.7.2 h1:YecWTWNE5PPJXL56sLIkzS8b23bprUwZ5lPAQuLUtTE= github.com/luxfi/zap v0.7.2 h1:YecWTWNE5PPJXL56sLIkzS8b23bprUwZ5lPAQuLUtTE=
+6 -71
View File
@@ -5,8 +5,6 @@ package hash
import ( import (
"bytes" "bytes"
"encoding/hex"
"strings"
"testing" "testing"
) )
@@ -149,75 +147,12 @@ func TestPairwiseDistinctEras(t *testing.T) {
} }
} }
// ─── NIST SP 800-185 vector smoke tests ───────────────────────────── // NIST SP 800-185 primitive vectors (left/right_encode, KMAC256,
// TupleHash256) are owned and tested by github.com/luxfi/mlwe/transcript
func TestLeftEncode(t *testing.T) { // (transcript_test.go: cSHAKE256 NIST Sample #3, the §2.3 encoders, and
cases := []struct { // the KMAC/TupleHash spec-construction KATs). Corona no longer carries a
x uint64 // parallel copy of those primitive tests — it tests only its own suite
want string // composition (tags, domain separation, determinism) below.
}{
{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)
}
}
// TestSuiteDeterminism — same input, two calls, identical bytes. // TestSuiteDeterminism — same input, two calls, identical bytes.
func TestSuiteDeterminism(t *testing.T) { func TestSuiteDeterminism(t *testing.T) {
+25 -15
View File
@@ -4,7 +4,12 @@
package hash package hash
// CoronaSHA3 is the production hash suite for Corona. Built on // 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: // Customization tags pin every operation to the Corona protocol:
// //
@@ -23,6 +28,8 @@ package hash
import ( import (
"encoding/binary" "encoding/binary"
"github.com/luxfi/mlwe/transcript"
) )
const ( const (
@@ -42,27 +49,30 @@ func NewCoronaSHA3() HashSuite { return coronaSHA3{} }
func (coronaSHA3) ID() string { return "Corona-SHA3" } func (coronaSHA3) ID() string { return "Corona-SHA3" }
func (coronaSHA3) Hc(transcript []byte) []byte { // Hc is cSHAKE256 with an empty function name and the Hc domain tag.
return cshake256Stream(tagHC, transcript, 32) // (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 { func (coronaSHA3) Hu(tr []byte, outLen int) []byte {
return cshake256Stream(tagHU, transcript, outLen) return transcript.CShake256("", tagHU, tr, outLen)
} }
func (coronaSHA3) TranscriptHash(parts ...[]byte) [32]byte { func (coronaSHA3) TranscriptHash(parts ...[]byte) [32]byte {
out := tupleHash256(parts, 32, tagTranscript) out := transcript.TupleHash256(parts, 32, tagTranscript)
var fixed [32]byte var fixed [32]byte
copy(fixed[:], out) copy(fixed[:], out)
return fixed return fixed
} }
func (coronaSHA3) PRF(key, msg []byte, outLen int) []byte { 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 { 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( func (coronaSHA3) DerivePairwise(
@@ -77,18 +87,18 @@ func (coronaSHA3) DerivePairwise(
a, b = b, a a, b = b, a
} }
var msg []byte var msg []byte
msg = append(msg, encodeString(chainID)...) msg = append(msg, transcript.EncodeString(chainID)...)
msg = append(msg, encodeString(groupID)...) msg = append(msg, transcript.EncodeString(groupID)...)
var u8 [8]byte var u8 [8]byte
binary.BigEndian.PutUint64(u8[:], eraID) binary.BigEndian.PutUint64(u8[:], eraID)
msg = append(msg, encodeString(u8[:])...) msg = append(msg, transcript.EncodeString(u8[:])...)
binary.BigEndian.PutUint64(u8[:], generation) binary.BigEndian.PutUint64(u8[:], generation)
msg = append(msg, encodeString(u8[:])...) msg = append(msg, transcript.EncodeString(u8[:])...)
var u4 [4]byte var u4 [4]byte
binary.BigEndian.PutUint32(u4[:], uint32(a)) binary.BigEndian.PutUint32(u4[:], uint32(a))
msg = append(msg, encodeString(u4[:])...) msg = append(msg, transcript.EncodeString(u4[:])...)
binary.BigEndian.PutUint32(u4[:], uint32(b)) 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)
} }
-111
View File
@@ -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
}
+28 -18
View File
@@ -9,6 +9,7 @@ import (
"github.com/luxfi/lattice/v7/ring" "github.com/luxfi/lattice/v7/ring"
"github.com/luxfi/lattice/v7/utils/structs" "github.com/luxfi/lattice/v7/utils/structs"
"github.com/luxfi/mlwe/share"
"github.com/zeebo/blake3" "github.com/zeebo/blake3"
) )
@@ -198,26 +199,35 @@ func ShamirSecretSharing(r *ring.Ring, s []ring.Poly, k int, lambdas []ring.Poly
return shares 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 { 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)) lagrangeCoefficients := make([]ring.Poly, len(T))
for i := 0; i < len(T); i++ { for i := range T {
xi := big.NewInt(int64(T[i] + 1)) coeff := new(big.Int).SetUint64(lambda[i])
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)
lagrangePoly := r.NewPoly() lagrangePoly := r.NewPoly()
r.SetCoefficientsBigint([]*big.Int{coeff}, lagrangePoly) r.SetCoefficientsBigint([]*big.Int{coeff}, lagrangePoly)
lagrangeCoefficients[i] = lagrangePoly lagrangeCoefficients[i] = lagrangePoly