mirror of
https://github.com/luxfi/corona.git
synced 2026-07-27 02:50:34 +00:00
Leftover from Corona's "Pulsar-R" lineage. Pulsar (M-LWE) and Corona
(R-LWE) are independent constructions with separate hardness assumptions,
so their cSHAKE personalisation strings must be distinct.
Changes (Go, non-luxcpp):
- hash tags: PULSAR-HC-v1 -> CORONA-HC-v1, etc. (HU, TRANSCRIPT, PRF, MAC, PAIRWISE)
- profile IDs: "Pulsar-SHA3" -> "Corona-SHA3", "Pulsar-BLAKE3" -> "Corona-BLAKE3"
- context strings: pulsar.dkg2.A.v1 -> corona.dkg2.A.v1, etc.
- env vars: PULSAR_RESHARE_KAT_PATH -> CORONA_RESHARE_KAT_PATH, etc.
- struct names: pulsarSHA3 -> coronaSHA3
- KAT derive roots: sign_e2e_pulsar -> sign_e2e_corona
What's preserved (different scope):
- luxcpp/crypto/pulsar/* path references in comments (separate repo,
out of scope; the C++ side will rename in its own commit)
- Cross-runtime KAT files on disk (will regenerate next CI run)
All 11 packages test green: dkg, dkg2, hash, keyera, networking,
primitives, reshare, sign, threshold, utils, wire.
404 lines
13 KiB
Go
404 lines
13 KiB
Go
package sign
|
|
|
|
import (
|
|
"bytes"
|
|
"math/big"
|
|
|
|
"github.com/luxfi/corona/hash"
|
|
"github.com/luxfi/corona/primitives"
|
|
"github.com/luxfi/corona/utils"
|
|
|
|
"github.com/luxfi/lattice/v7/ring"
|
|
"github.com/luxfi/lattice/v7/utils/sampling"
|
|
"github.com/luxfi/lattice/v7/utils/structs"
|
|
)
|
|
|
|
// Party struct holds all state and methods for a party in the protocol.
|
|
//
|
|
// Suite is the hash profile this party uses for every primitives.* call.
|
|
// NewParty defaults it to hash.Default() (Corona-SHA3). Operators that need
|
|
// to interoperate with old transcripts can override with NewCoronaBLAKE3().
|
|
type Party struct {
|
|
ID int
|
|
Ring *ring.Ring
|
|
RingXi *ring.Ring
|
|
RingNu *ring.Ring
|
|
UniformSampler *ring.UniformSampler
|
|
SkShare structs.Vector[ring.Poly]
|
|
Seed map[int][][]byte
|
|
R structs.Matrix[ring.Poly]
|
|
C ring.Poly
|
|
H structs.Vector[ring.Poly]
|
|
Lambda ring.Poly
|
|
D structs.Matrix[ring.Poly]
|
|
MACKeys map[int][]byte
|
|
MACs map[int][]byte
|
|
Suite hash.HashSuite
|
|
}
|
|
|
|
// NewParty initializes a new Party instance with the production hash suite
|
|
// (Corona-SHA3). To use a different suite, set Party.Suite after construction
|
|
// or call NewPartyWithSuite.
|
|
func NewParty(id int, r *ring.Ring, r_xi *ring.Ring, r_nu *ring.Ring, sampler *ring.UniformSampler) *Party {
|
|
return NewPartyWithSuite(id, r, r_xi, r_nu, sampler, hash.Default())
|
|
}
|
|
|
|
// NewPartyWithSuite initializes a Party with an explicit hash suite. Pass
|
|
// nil to resolve to the production default at call time.
|
|
func NewPartyWithSuite(id int, r *ring.Ring, r_xi *ring.Ring, r_nu *ring.Ring, sampler *ring.UniformSampler, suite hash.HashSuite) *Party {
|
|
return &Party{
|
|
ID: id,
|
|
Ring: r,
|
|
RingXi: r_xi,
|
|
RingNu: r_nu,
|
|
UniformSampler: sampler,
|
|
MACKeys: make(map[int][]byte),
|
|
MACs: make(map[int][]byte),
|
|
Suite: hash.Resolve(suite),
|
|
}
|
|
}
|
|
|
|
// Gen generates the secret shares, seeds, MAC keys, and the public parameter b
|
|
func Gen(r *ring.Ring, r_xi *ring.Ring, uniformSampler *ring.UniformSampler, trustedDealerKey []byte, lagrangeCoefficients structs.Vector[ring.Poly]) (structs.Matrix[ring.Poly], map[int]structs.Vector[ring.Poly], map[int][][]byte, map[int]map[int][]byte, structs.Vector[ring.Poly]) {
|
|
A := utils.SamplePolyMatrix(r, M, N, uniformSampler, true, true)
|
|
|
|
precomputeSize := (K * K * KeySize) + (r.N() * N * (K - 1) * len(r.Modulus().Bytes())) + (K * (K - 1) * KeySize)
|
|
utils.PrecomputeRandomness(precomputeSize, trustedDealerKey)
|
|
|
|
prng, _ := sampling.NewKeyedPRNG(trustedDealerKey)
|
|
gaussianParams := ring.DiscreteGaussian{Sigma: SigmaE, Bound: BoundE}
|
|
gaussianSampler := ring.NewGaussianSampler(prng, r, gaussianParams, false)
|
|
|
|
s := utils.SamplePolyVector(r, N, gaussianSampler, false, false)
|
|
skShares := primitives.ShamirSecretSharing(r, s, Threshold, lagrangeCoefficients)
|
|
|
|
for _, skShare := range skShares {
|
|
utils.ConvertVectorToNTT(r, skShare)
|
|
}
|
|
utils.ConvertVectorToNTT(r, s)
|
|
|
|
e := utils.SamplePolyVector(r, M, gaussianSampler, true, true)
|
|
b := utils.InitializeVector(r, M)
|
|
utils.MatrixVectorMul(r, A, s, b)
|
|
utils.VectorAdd(r, b, e, b)
|
|
|
|
// Round b
|
|
utils.ConvertVectorFromNTT(r, b)
|
|
bTilde := utils.RoundVector(r, r_xi, b, Xi)
|
|
|
|
seeds := make(map[int][][]byte)
|
|
MACKeys := make(map[int]map[int][]byte)
|
|
MACKeys[0] = make(map[int][]byte)
|
|
|
|
for i := 0; i < K; i++ {
|
|
seeds[i] = make([][]byte, K)
|
|
for j := 0; j < K; j++ {
|
|
seeds[i][j] = utils.GetRandomBytes(KeySize)
|
|
if i != j {
|
|
if MACKeys[j] == nil {
|
|
MACKeys[j] = make(map[int][]byte)
|
|
}
|
|
if MACKeys[i][j] == nil && MACKeys[j][i] == nil {
|
|
MACKeys[i][j] = utils.GetRandomBytes(KeySize)
|
|
MACKeys[j][i] = MACKeys[i][j]
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return A, skShares, seeds, MACKeys, bTilde
|
|
}
|
|
|
|
// SignRound1 performs the first round of signing
|
|
func (party *Party) SignRound1(A structs.Matrix[ring.Poly], sid int, PRFKey []byte, T []int) (structs.Matrix[ring.Poly], map[int][]byte) {
|
|
r := party.Ring
|
|
|
|
// CRIT-1 fix (2026-05-03): mix sid into the per-party PRNG seed.
|
|
// Previously this was PRNGKey(SkShare) which produced byte-identical
|
|
// R, E, D across every Sign call of the same Setup → multi-Sign leaked
|
|
// R. PRNGKeyForRound domain-separates by sid; per-block sid
|
|
// monotonicity (LP-020 Quasar = block height) gives uniqueness for
|
|
// free. See LP-073 §5.8 (amended) and red audit response.
|
|
skHash := primitives.PRNGKeyForRound(party.Suite, party.SkShare, int64(sid))
|
|
prng, _ := sampling.NewKeyedPRNG(skHash)
|
|
gaussianParams := ring.DiscreteGaussian{Sigma: SigmaStar, Bound: BoundStar}
|
|
gaussianSampler := ring.NewGaussianSampler(prng, r, gaussianParams, false)
|
|
r_star := utils.SamplePolyVector(r, N, gaussianSampler, true, true)
|
|
e_star := utils.SamplePolyVector(r, M, gaussianSampler, true, true)
|
|
|
|
// Initialize R_i and E_i
|
|
gaussianParams = ring.DiscreteGaussian{Sigma: SigmaE, Bound: BoundE}
|
|
gaussianSampler = ring.NewGaussianSampler(prng, r, gaussianParams, false)
|
|
R_i := utils.SamplePolyMatrix(r, N, Dbar, gaussianSampler, true, true)
|
|
E_i := utils.SamplePolyMatrix(r, M, Dbar, gaussianSampler, true, true)
|
|
|
|
concatenatedR := utils.InitializeMatrix(r, N, Dbar+1)
|
|
for i := range concatenatedR {
|
|
concatenatedR[i] = append([]ring.Poly{r_star[i]}, R_i[i]...)
|
|
}
|
|
party.R = concatenatedR
|
|
|
|
// Ensure concatenatedE is properly initialized
|
|
concatenatedE := utils.InitializeMatrix(r, M, Dbar+1)
|
|
for i := range concatenatedE {
|
|
concatenatedE[i] = append([]ring.Poly{e_star[i]}, E_i[i]...)
|
|
}
|
|
|
|
D := utils.InitializeMatrix(r, M, Dbar+1)
|
|
|
|
utils.MatrixMatrixMul(r, A, concatenatedR, D)
|
|
utils.MatrixAdd(r, concatenatedE, D, D)
|
|
|
|
party.D = D
|
|
|
|
// Generate MACs for each party
|
|
MACs := make(map[int][]byte)
|
|
for _, j := range T {
|
|
if j != party.ID {
|
|
MACs[j] = primitives.GenerateMAC(party.Suite, D, party.MACKeys[j], party.ID, sid, T, j, false)
|
|
}
|
|
}
|
|
|
|
return D, MACs
|
|
}
|
|
|
|
// SignRound2Preprocess verifies the MACs received in round 1 and performs the minimum eigenvalue check
|
|
func (party *Party) SignRound2Preprocess(A structs.Matrix[ring.Poly], b structs.Vector[ring.Poly], D map[int]structs.Matrix[ring.Poly], MACs map[int]map[int][]byte, sid int, T []int) (bool, structs.Matrix[ring.Poly], []byte) {
|
|
transcriptHash := primitives.Hash(party.Suite, A, b, D, sid, T)
|
|
|
|
for _, j := range T {
|
|
if j != party.ID {
|
|
MAC := MACs[j][party.ID]
|
|
expectedMAC := primitives.GenerateMAC(party.Suite, D[j], party.MACKeys[j], party.ID, sid, T, j, true)
|
|
if !bytes.Equal(MAC, expectedMAC) {
|
|
return false, nil, nil
|
|
}
|
|
}
|
|
}
|
|
|
|
DSum := utils.InitializeMatrix(party.Ring, M, Dbar+1)
|
|
for _, D_j := range D {
|
|
utils.MatrixAdd(party.Ring, D_j, DSum, DSum)
|
|
}
|
|
|
|
if !FullRankCheck(DSum, party.Ring) {
|
|
return false, nil, nil
|
|
}
|
|
|
|
return true, DSum, transcriptHash
|
|
}
|
|
|
|
// SignRound2 performs the second round of signing.
|
|
//
|
|
// transcriptHash is the digest returned by SignRound2Preprocess; it
|
|
// binds (A, b, D, sid, T) under the active hash suite.
|
|
func (party *Party) SignRound2(A structs.Matrix[ring.Poly], bTilde structs.Vector[ring.Poly], DSum structs.Matrix[ring.Poly], sid int, mu string, T []int, PRFKey []byte, transcriptHash []byte) structs.Vector[ring.Poly] {
|
|
r := party.Ring
|
|
r_nu := party.RingNu
|
|
partyID := party.ID
|
|
concatR := party.R
|
|
seeds := party.Seed
|
|
|
|
s_i := party.SkShare
|
|
lambda := party.Lambda
|
|
|
|
onePoly := r.NewMonomialXi(0)
|
|
r.NTT(onePoly, onePoly)
|
|
r.MForm(onePoly, onePoly)
|
|
|
|
u := structs.Vector[ring.Poly]{}
|
|
oneSlice := structs.Vector[ring.Poly]{onePoly}
|
|
if Dbar > 0 {
|
|
h_u := primitives.GaussianHash(party.Suite, r, transcriptHash, mu, SigmaU, BoundU, Dbar)
|
|
u = append(oneSlice, h_u...)
|
|
}
|
|
|
|
h := utils.InitializeVector(r, M)
|
|
utils.MatrixVectorMul(r, DSum, u, h)
|
|
|
|
utils.ConvertVectorFromNTT(r, h)
|
|
roundedH := utils.RoundVector(r, r_nu, h, Nu)
|
|
party.H = roundedH
|
|
|
|
c := primitives.LowNormHash(party.Suite, r, A, bTilde, roundedH, mu, Kappa)
|
|
party.C = c
|
|
|
|
seed_i := party.Seed[party.ID]
|
|
mask := utils.InitializeVector(r, N)
|
|
for _, j := range T {
|
|
mask_j := primitives.PRF(party.Suite, r, seed_i[j], PRFKey, mu, transcriptHash, N)
|
|
utils.VectorAdd(r, mask, mask_j, mask)
|
|
}
|
|
|
|
maskPrime := utils.InitializeVector(r, N)
|
|
for _, j := range T {
|
|
mask_j := primitives.PRF(party.Suite, r, seeds[j][partyID], PRFKey, mu, transcriptHash, N)
|
|
utils.VectorAdd(r, maskPrime, mask_j, maskPrime)
|
|
}
|
|
|
|
z_i := utils.InitializeVector(r, N)
|
|
|
|
utils.MatrixVectorMul(r, concatR, u, z_i)
|
|
|
|
utils.VectorAdd(r, z_i, maskPrime, z_i)
|
|
|
|
s_c_lambda := utils.InitializeVector(r, N)
|
|
|
|
utils.VectorPolyMul(r, s_i, lambda, s_c_lambda)
|
|
utils.VectorPolyMul(r, s_c_lambda, c, s_c_lambda)
|
|
utils.VectorAdd(r, z_i, s_c_lambda, z_i)
|
|
utils.VectorSub(r, z_i, mask, z_i)
|
|
|
|
return z_i
|
|
}
|
|
|
|
// SignFinalize finalizes the signature
|
|
func (party *Party) SignFinalize(z map[int]structs.Vector[ring.Poly], A structs.Matrix[ring.Poly], bTilde structs.Vector[ring.Poly]) (ring.Poly, structs.Vector[ring.Poly], structs.Vector[ring.Poly]) {
|
|
r := party.Ring
|
|
r_xi := party.RingXi
|
|
r_nu := party.RingNu
|
|
c := party.C
|
|
h := party.H
|
|
|
|
z_sum := utils.InitializeVector(r, N)
|
|
|
|
for _, z_j := range z {
|
|
utils.VectorAdd(r, z_sum, z_j, z_sum)
|
|
}
|
|
|
|
Az_bc := utils.InitializeVector(r, M)
|
|
utils.MatrixVectorMul(r, A, z_sum, Az_bc)
|
|
bc := utils.InitializeVector(r, M)
|
|
|
|
b := utils.RestoreVector(r, r_xi, bTilde, Xi)
|
|
utils.ConvertVectorToNTT(r, b)
|
|
|
|
utils.VectorPolyMul(r, b, c, bc)
|
|
utils.VectorSub(r, Az_bc, bc, Az_bc)
|
|
|
|
utils.ConvertVectorFromNTT(r, Az_bc)
|
|
roundedAz_bc := utils.RoundVector(r, r_nu, Az_bc, Nu)
|
|
|
|
Delta := utils.InitializeVector(r_nu, M)
|
|
utils.VectorSub(r_nu, h, roundedAz_bc, Delta)
|
|
|
|
return party.C, z_sum, Delta
|
|
}
|
|
|
|
// Verify verifies the correctness of the signature using the production
|
|
// hash suite (Corona-SHA3). For non-default suites use VerifyWithSuite.
|
|
// Note: This function does not modify its inputs - it creates copies where needed.
|
|
func Verify(r *ring.Ring, r_xi *ring.Ring, r_nu *ring.Ring, z structs.Vector[ring.Poly], A structs.Matrix[ring.Poly], mu string, bTilde structs.Vector[ring.Poly], c ring.Poly, roundedDelta structs.Vector[ring.Poly]) bool {
|
|
return VerifyWithSuite(nil, r, r_xi, r_nu, z, A, mu, bTilde, c, roundedDelta)
|
|
}
|
|
|
|
// VerifyWithSuite is the suite-explicit form of Verify. suite=nil resolves
|
|
// to the production default (Corona-SHA3).
|
|
func VerifyWithSuite(suite hash.HashSuite, r *ring.Ring, r_xi *ring.Ring, r_nu *ring.Ring, z structs.Vector[ring.Poly], A structs.Matrix[ring.Poly], mu string, bTilde structs.Vector[ring.Poly], c ring.Poly, roundedDelta structs.Vector[ring.Poly]) bool {
|
|
// Make a copy of z to avoid modifying the input signature
|
|
zCopy := make(structs.Vector[ring.Poly], len(z))
|
|
for i := range z {
|
|
zCopy[i] = *z[i].CopyNew()
|
|
}
|
|
|
|
Az_bc := utils.InitializeVector(r, M)
|
|
utils.MatrixVectorMul(r, A, zCopy, Az_bc)
|
|
bc := utils.InitializeVector(r, M)
|
|
|
|
b := utils.RestoreVector(r, r_xi, bTilde, Xi)
|
|
utils.ConvertVectorToNTT(r, b)
|
|
|
|
utils.VectorPolyMul(r, b, c, bc)
|
|
utils.VectorSub(r, Az_bc, bc, Az_bc)
|
|
|
|
utils.ConvertVectorFromNTT(r, Az_bc)
|
|
roundedAz_bc := utils.RoundVector(r, r_nu, Az_bc, Nu)
|
|
|
|
Az_bc_Delta := utils.InitializeVector(r_nu, M)
|
|
utils.VectorAdd(r_nu, roundedAz_bc, roundedDelta, Az_bc_Delta)
|
|
|
|
computedC := primitives.LowNormHash(suite, r, A, bTilde, Az_bc_Delta, mu, Kappa)
|
|
if !r.Equal(c, computedC) {
|
|
return false
|
|
}
|
|
|
|
Delta := utils.RestoreVector(r, r_nu, roundedDelta, Nu)
|
|
utils.ConvertVectorFromNTT(r, zCopy)
|
|
|
|
return CheckL2Norm(r, Delta, zCopy)
|
|
}
|
|
|
|
// CheckL2Norm checks if the L2 norm of the vector of Delta is less than or equal to Bsquare
|
|
func CheckL2Norm(r *ring.Ring, Delta structs.Vector[ring.Poly], z structs.Vector[ring.Poly]) bool {
|
|
sumSquares := big.NewInt(0)
|
|
qBig := new(big.Int).SetUint64(Q)
|
|
halfQ := new(big.Int).Div(qBig, big.NewInt(2))
|
|
|
|
DeltaCoeffsBigInt := make(structs.Vector[[]*big.Int], r.N())
|
|
for i, polyCoeffs := range Delta {
|
|
DeltaCoeffsBigInt[i] = make([]*big.Int, r.N())
|
|
r.PolyToBigint(polyCoeffs, 1, DeltaCoeffsBigInt[i])
|
|
}
|
|
|
|
for _, polyCoeffs := range DeltaCoeffsBigInt {
|
|
for _, coeff := range polyCoeffs {
|
|
if coeff.Cmp(halfQ) > 0 {
|
|
coeff.Sub(coeff, qBig)
|
|
}
|
|
coeffSquare := new(big.Int).Mul(coeff, coeff)
|
|
sumSquares.Add(sumSquares, coeffSquare)
|
|
}
|
|
}
|
|
|
|
zCoeffsBigInt := make(structs.Vector[[]*big.Int], r.N())
|
|
for i, polyCoeffs := range z {
|
|
zCoeffsBigInt[i] = make([]*big.Int, r.N())
|
|
r.PolyToBigint(polyCoeffs, 1, zCoeffsBigInt[i])
|
|
}
|
|
|
|
for _, polyCoeffs := range zCoeffsBigInt {
|
|
for _, coeff := range polyCoeffs {
|
|
if coeff.Cmp(halfQ) > 0 {
|
|
coeff.Sub(coeff, qBig)
|
|
}
|
|
coeffSquare := new(big.Int).Mul(coeff, coeff)
|
|
sumSquares.Add(sumSquares, coeffSquare)
|
|
}
|
|
}
|
|
|
|
// Internal norm bound check; intermediate values stay private. Logging
|
|
// them at any level (even debug) creates a side-channel for validator
|
|
// monitoring stacks that index logs.
|
|
BsquareInt, _ := new(big.Int).SetString(Bsquare, 10)
|
|
return sumSquares.Cmp(BsquareInt) <= 0
|
|
}
|
|
|
|
// FullRankCheck checks if the given matrix is full-rank, ignoring the first column
|
|
func FullRankCheck(D structs.Matrix[ring.Poly], r *ring.Ring) bool {
|
|
phi := r.N()
|
|
q := r.Modulus()
|
|
submatrices := make([][][]*big.Int, phi)
|
|
for i := range submatrices {
|
|
submatrices[i] = make([][]*big.Int, len(D))
|
|
for row := range submatrices[i] {
|
|
submatrices[i][row] = make([]*big.Int, len(D[0])-1)
|
|
}
|
|
}
|
|
for row := range D {
|
|
for col := 1; col < len(D[row]); col++ {
|
|
coeffs := make([]*big.Int, phi)
|
|
r.PolyToBigint(D[row][col], 1, coeffs)
|
|
for i := 0; i < phi; i++ {
|
|
coeff := coeffs[i].Mod(coeffs[i], q)
|
|
submatrices[i][row][col-1] = coeff
|
|
}
|
|
}
|
|
}
|
|
for i := range submatrices {
|
|
if !utils.GaussianEliminationModQ(submatrices[i], q) {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}
|