mirror of
https://github.com/luxfi/corona.git
synced 2026-07-27 02:50:34 +00:00
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:
@@ -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
|
||||
|
||||
@@ -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=
|
||||
|
||||
+6
-71
@@ -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) {
|
||||
|
||||
+25
-15
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user