mirror of
https://github.com/luxfi/corona.git
synced 2026-09-24 00:25:54 +00:00
Part of the Lux threshold-crypto security rip (killing trusted-dealer / full-key-reconstruction footguns). - Rename threshold.GenerateKeys -> threshold.GenerateKeysTrustedDealer. The function samples the full secret and sets sign.K / sign.Threshold in one process: it IS the trusted dealer. The new name mirrors keyera.BootstrapTrustedDealer so the trust model is explicit and greppable. Doc comment now states it is for test/KAT/CT/CLI/oracle use only, and that production chain keygen uses keyera.Bootstrap (dealerless Pedersen DKG). Stays exported; every caller updated (cli, ct/dudect, same-package tests) plus stale comment refs in cmd, keyera, wire, reshare. - Delete utils.ReconstructSecret. It Lagrange-interpolates the full secret from shares and had zero callers anywhere in the repo: a reconstruction-shaped footgun left dead. CompareSecrets and the section retained. - Include threshold/minority_soundness_test.go: an adversarial negative pinning that a strict sub-quorum cannot assemble a signature that verifies under the group key (uncancelled PRF mask fails the L2-norm gate). A sub-quorum forgery would be a catastrophic finality break.
248 lines
6.5 KiB
Go
248 lines
6.5 KiB
Go
// Copyright (C) 2025-2026, Lux Industries Inc. All rights reserved.
|
|
// See the file LICENSE for licensing terms.
|
|
|
|
package threshold
|
|
|
|
import (
|
|
"bytes"
|
|
"crypto/rand"
|
|
"errors"
|
|
"testing"
|
|
)
|
|
|
|
// TestSignatureRoundtrip generates a real threshold signature, marshals
|
|
// it, unmarshals it, and verifies the parsed signature against the
|
|
// parsed group key over the same message. Byte-equal serialization is
|
|
// enforced.
|
|
//
|
|
// NOTE: cannot t.Parallel — GenerateKeysTrustedDealer mutates sign.K / sign.Threshold
|
|
// package globals (see threshold.go:131-132). The wire codec itself is
|
|
// pure; this is a kernel-side limitation we accept rather than refactor
|
|
// out from under sibling agents who own pulsar/.
|
|
func TestSignatureWireRoundtrip(t *testing.T) {
|
|
const tThr, n = 1, 2
|
|
shares, gk, err := GenerateKeysTrustedDealer(tThr, n, rand.Reader)
|
|
if err != nil {
|
|
t.Fatalf("GenerateKeysTrustedDealer: %v", err)
|
|
}
|
|
|
|
// Two-party signing (smallest committee that exercises the protocol).
|
|
signers := []int{0, 1}
|
|
prfKey := make([]byte, 32)
|
|
if _, err := rand.Read(prfKey); err != nil {
|
|
t.Fatalf("rand.Read prfKey: %v", err)
|
|
}
|
|
|
|
sigA := NewSigner(shares[0])
|
|
sigB := NewSigner(shares[1])
|
|
|
|
const sessionID = 42
|
|
r1A, err := sigA.Round1(sessionID, prfKey, signers)
|
|
if err != nil {
|
|
t.Fatalf("Round1 A: %v", err)
|
|
}
|
|
r1B, err := sigB.Round1(sessionID, prfKey, signers)
|
|
if err != nil {
|
|
t.Fatalf("Round1 B: %v", err)
|
|
}
|
|
r1Data := map[int]*Round1Data{0: r1A, 1: r1B}
|
|
|
|
msg := "corona-wire-roundtrip-test"
|
|
r2A, err := sigA.Round2(sessionID, msg, prfKey, signers, r1Data)
|
|
if err != nil {
|
|
t.Fatalf("Round2 A: %v", err)
|
|
}
|
|
r2B, err := sigB.Round2(sessionID, msg, prfKey, signers, r1Data)
|
|
if err != nil {
|
|
t.Fatalf("Round2 B: %v", err)
|
|
}
|
|
r2Data := map[int]*Round2Data{0: r2A, 1: r2B}
|
|
|
|
sig, err := sigA.Finalize(r2Data)
|
|
if err != nil {
|
|
t.Fatalf("Finalize: %v", err)
|
|
}
|
|
|
|
if !Verify(gk, msg, sig) {
|
|
t.Fatal("baseline Verify failed before serialization")
|
|
}
|
|
|
|
// MarshalBinary + UnmarshalBinary roundtrip for Signature.
|
|
sigBytes, err := sig.MarshalBinary()
|
|
if err != nil {
|
|
t.Fatalf("sig.MarshalBinary: %v", err)
|
|
}
|
|
var sigParsed Signature
|
|
if err := sigParsed.UnmarshalBinary(sigBytes); err != nil {
|
|
t.Fatalf("sig.UnmarshalBinary: %v", err)
|
|
}
|
|
|
|
// Second marshal must be byte-identical to the first.
|
|
sigBytes2, err := sigParsed.MarshalBinary()
|
|
if err != nil {
|
|
t.Fatalf("sigParsed.MarshalBinary: %v", err)
|
|
}
|
|
if !bytes.Equal(sigBytes, sigBytes2) {
|
|
t.Fatalf("Signature re-marshal not byte-equal: first %d bytes, second %d bytes", len(sigBytes), len(sigBytes2))
|
|
}
|
|
|
|
// MarshalBinary + UnmarshalBinary roundtrip for GroupKey.
|
|
gkBytes, err := gk.MarshalBinary()
|
|
if err != nil {
|
|
t.Fatalf("gk.MarshalBinary: %v", err)
|
|
}
|
|
var gkParsed GroupKey
|
|
if err := gkParsed.UnmarshalBinary(gkBytes); err != nil {
|
|
t.Fatalf("gk.UnmarshalBinary: %v", err)
|
|
}
|
|
|
|
gkBytes2, err := gkParsed.MarshalBinary()
|
|
if err != nil {
|
|
t.Fatalf("gkParsed.MarshalBinary: %v", err)
|
|
}
|
|
if !bytes.Equal(gkBytes, gkBytes2) {
|
|
t.Fatalf("GroupKey re-marshal not byte-equal: first %d, second %d", len(gkBytes), len(gkBytes2))
|
|
}
|
|
|
|
// Stateless VerifyBytes path — the surface threshold/pkg/thresholdd
|
|
// uses.
|
|
if !VerifyBytes(gkBytes, msg, sigBytes) {
|
|
t.Fatal("VerifyBytes returned false on a valid signature")
|
|
}
|
|
|
|
// Negative: corrupted signature MUST not verify.
|
|
corrupted := append([]byte(nil), sigBytes...)
|
|
corrupted[len(corrupted)-1] ^= 0x01
|
|
if VerifyBytes(gkBytes, msg, corrupted) {
|
|
t.Fatal("VerifyBytes returned true on a corrupted signature")
|
|
}
|
|
|
|
// Negative: wrong message MUST not verify.
|
|
if VerifyBytes(gkBytes, msg+"-tampered", sigBytes) {
|
|
t.Fatal("VerifyBytes returned true on a tampered message")
|
|
}
|
|
}
|
|
|
|
// TestSignatureWireRejectMalformed ensures the wire codec rejects
|
|
// short, magic-mismatched, and version-mismatched frames before any
|
|
// allocation.
|
|
func TestSignatureWireRejectMalformed(t *testing.T) {
|
|
cases := []struct {
|
|
name string
|
|
buf []byte
|
|
want error
|
|
}{
|
|
{"empty", []byte{}, ErrWireFrameTooShort},
|
|
{"too-short", make([]byte, 4), ErrWireFrameTooShort},
|
|
{
|
|
"wrong-magic",
|
|
func() []byte {
|
|
b := make([]byte, 4+2+4*3)
|
|
// magic bytes != wireMagicSig
|
|
b[0] = 0xff
|
|
b[1] = 0xff
|
|
b[2] = 0xff
|
|
b[3] = 0xff
|
|
// version=1
|
|
b[5] = 0x01
|
|
return b
|
|
}(),
|
|
ErrWireMagicMismatch,
|
|
},
|
|
{
|
|
"wrong-version",
|
|
func() []byte {
|
|
b := make([]byte, 4+2+4*3)
|
|
// correct magic 0x434F5253
|
|
b[0] = 0x43
|
|
b[1] = 0x4F
|
|
b[2] = 0x52
|
|
b[3] = 0x53
|
|
// version=999
|
|
b[4] = 0x03
|
|
b[5] = 0xe7
|
|
return b
|
|
}(),
|
|
ErrWireVersionMismatch,
|
|
},
|
|
}
|
|
|
|
for _, tc := range cases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
var sig Signature
|
|
err := sig.UnmarshalBinary(tc.buf)
|
|
if err == nil {
|
|
t.Fatalf("expected error %v, got nil", tc.want)
|
|
}
|
|
if !errors.Is(err, tc.want) {
|
|
t.Fatalf("expected %v, got %v", tc.want, err)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestGroupKeyWireRejectMalformed mirrors the Signature rejection
|
|
// suite for GroupKey.
|
|
func TestGroupKeyWireRejectMalformed(t *testing.T) {
|
|
cases := []struct {
|
|
name string
|
|
buf []byte
|
|
want error
|
|
}{
|
|
{"empty", []byte{}, ErrWireFrameTooShort},
|
|
{
|
|
"sig-magic-into-groupkey",
|
|
func() []byte {
|
|
// Signature magic but feed into GroupKey parser — must
|
|
// be rejected as mismatch, not accepted as anything.
|
|
b := make([]byte, 4+2+4*2)
|
|
b[0] = 0x43
|
|
b[1] = 0x4F
|
|
b[2] = 0x52
|
|
b[3] = 0x53
|
|
b[5] = 0x01
|
|
return b
|
|
}(),
|
|
ErrWireMagicMismatch,
|
|
},
|
|
}
|
|
for _, tc := range cases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
var gk GroupKey
|
|
err := gk.UnmarshalBinary(tc.buf)
|
|
if err == nil {
|
|
t.Fatalf("expected error %v, got nil", tc.want)
|
|
}
|
|
if !errors.Is(err, tc.want) {
|
|
t.Fatalf("expected %v, got %v", tc.want, err)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestVerifyBytesRejectsCrossWire ensures VerifyBytes rejects a
|
|
// GroupKey-magic frame in the Signature slot and vice versa — domain
|
|
// separation must be effective.
|
|
func TestVerifyBytesRejectsCrossWire(t *testing.T) {
|
|
shares, gk, err := GenerateKeysTrustedDealer(1, 2, rand.Reader)
|
|
if err != nil {
|
|
t.Fatalf("GenerateKeysTrustedDealer: %v", err)
|
|
}
|
|
_ = shares
|
|
gkBytes, err := gk.MarshalBinary()
|
|
if err != nil {
|
|
t.Fatalf("gk.MarshalBinary: %v", err)
|
|
}
|
|
// Feed gkBytes in the signature slot — VerifyBytes must reject.
|
|
if VerifyBytes(gkBytes, "msg", gkBytes) {
|
|
t.Fatal("VerifyBytes accepted GroupKey magic in signature slot")
|
|
}
|
|
// Feed empty bytes — also reject.
|
|
if VerifyBytes(gkBytes, "msg", nil) {
|
|
t.Fatal("VerifyBytes accepted nil signature")
|
|
}
|
|
if VerifyBytes(nil, "msg", gkBytes) {
|
|
t.Fatal("VerifyBytes accepted nil group key")
|
|
}
|
|
}
|