mirror of
https://github.com/luxfi/pulsar.git
synced 2026-09-24 00:25:52 +00:00
pulsar: dealerless RSS FIPS-leg Sign API (fail-closed, quorum-gated)
MithrilKey.Sign reconstructs key material from any T-quorum's holdings, BCC-signs (hint from public w', no c·s2/c·t0 leak), and FAIL-CLOSED self-verifies against FIPS-204 before emitting. Output verifies under stock circl mldsa65.Verify. Proven for multiple qualifying quorums under one group key. Full pulsar suite green (65s).
This commit is contained in:
@@ -31,6 +31,7 @@ package pulsar
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
|
||||
"github.com/luxfi/dkg/rss"
|
||||
"golang.org/x/crypto/sha3"
|
||||
@@ -257,3 +258,38 @@ func (mk *MithrilKey) ReconstructKeyMaterial(active []int) (*mldsaKeyMaterial, e
|
||||
km.a = mk.a
|
||||
return km, nil
|
||||
}
|
||||
|
||||
// Sign produces a byte-stock-FIPS-204 ML-DSA signature on (message, ctx) under
|
||||
// the dealerless RSS group key, from any T active parties. It reconstructs the
|
||||
// key material from the quorum's holdings (balanced partition), runs the BCC
|
||||
// signer — whose hint is recovered from the PUBLIC w' = A·z − c·t1·2^d, so it
|
||||
// emits NONE of c·s2 / c·t0 / r0 — and FAIL-CLOSED self-verifies the result
|
||||
// against the FIPS-204 verifier before returning. The returned *Signature
|
||||
// verifies under unmodified cloudflare/circl mldsa{65,87}.Verify.
|
||||
//
|
||||
// This is the threshold-gated SIGNING act: it requires a T-quorum and rebuilds
|
||||
// the secret at the signing coordinator. The dealerless KEYGEN never does this —
|
||||
// no party holds the full key at keygen. (A signing-time no-reconstruct variant
|
||||
// is the Mithril 3-round hyperball protocol, where each party emits only its
|
||||
// partial z_i = y_i + c·s_{1,I}; that is the signing-side hardening follow-up.)
|
||||
func (mk *MithrilKey) Sign(active []int, message, ctx []byte, rng io.Reader, maxAttempts int) (*Signature, error) {
|
||||
km, err := mk.ReconstructKeyMaterial(active)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
sigBytes, _, err := bccSign(km, mk.Mode, message, ctx, rng, maxAttempts)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("pulsar: mithril RSS sign: %w", err)
|
||||
}
|
||||
sig := &Signature{Mode: mk.Mode, Bytes: sigBytes}
|
||||
// Fail-closed: never emit a signature the FIPS-204 verifier rejects.
|
||||
params, err := ParamsFor(mk.Mode)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
pk := &PublicKey{Mode: mk.Mode, Bytes: mk.pub}
|
||||
if err := VerifyCtx(params, pk, message, ctx, sig); err != nil {
|
||||
return nil, fmt.Errorf("pulsar: mithril RSS sign self-verify failed (refusing to emit): %w", err)
|
||||
}
|
||||
return sig, nil
|
||||
}
|
||||
|
||||
@@ -146,6 +146,34 @@ func TestMithrilRSSAnyQuorumSameKey(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// TestMithrilRSSSignAPI exercises the production-facing dealerless FIPS-leg
|
||||
// signer: MithrilKey.Sign reconstructs from a quorum, BCC-signs, fail-closed
|
||||
// self-verifies, and the result verifies under stock circl mldsa65.Verify.
|
||||
func TestMithrilRSSSignAPI(t *testing.T) {
|
||||
mk, err := MithrilRSSKeygen(ModeP65, 3, 5, mithrilSeeds(5))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
msg := []byte("dealerless RSS FIPS leg — Sign API")
|
||||
ctx := []byte("quasar-pulsar-leg")
|
||||
// Two different qualifying quorums both sign valid signatures under the
|
||||
// same public key.
|
||||
for _, active := range [][]int{{0, 1, 2}, {2, 3, 4}, {0, 2, 4}} {
|
||||
rng := newBCCDeterministicRNG(fmt.Sprintf("sign/%v", active))
|
||||
sig, err := mk.Sign(active, msg, ctx, rng, 1<<20)
|
||||
if err != nil {
|
||||
t.Fatalf("quorum %v Sign: %v", active, err)
|
||||
}
|
||||
var pkC mldsa65.PublicKey
|
||||
if err := pkC.UnmarshalBinary(mk.pub); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !mldsa65.Verify(&pkC, msg, ctx, sig.Bytes) {
|
||||
t.Fatalf("quorum %v: stock circl rejected the Sign output", active)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestMithrilRSSRejectsBadCommittee(t *testing.T) {
|
||||
for _, tn := range [][2]int{{1, 2}, {3, 2}, {2, 7}} {
|
||||
if _, err := MithrilRSSKeygen(ModeP65, tn[0], tn[1], mithrilSeeds(maxI(tn[1], 7))); err == nil {
|
||||
|
||||
Reference in New Issue
Block a user