Files
consensus/protocol/quasar/pure_pq_cert_test.go
zooqueen dca3110ddb fix(quasar): make BLS optional on QuasarCert.VerifyWithRealKeys (fully-PQ finality)
FULLY-PQ AUDIT FINDING (critical, finality path): VerifyWithRealKeys
unconditionally required a valid BLS-12-381 aggregate (types.go: 'if
len(c.BLS)==0 return false' + mandatory bls.Verify), wired into production
at engine/pq/engine.go and pkg/wire/policies.go. This contradicted the
cert's own contract ('empty in pure-PQ', HasClassicalFastPath) and the
sibling VerkleWitness verifier, and meant a quantum break of BLS-12-381
could forge ANY cert that passed verification, regardless of the PQ legs.
No pure-PQ QuasarCert could finalise through this surface.

Fix (decomplected, mirrors VerkleWitness + the field's documented contract):
- BLS is now ONE OPTIONAL leg, verified iff the cert carries BLS bytes,
  exactly like the Corona/ML-DSA legs.
- Fail-closed guard: count verified legs; accept only if >=1 leg actually
  verified, so removing the mandatory-BLS gate can never let an
  evidence-free cert pass (no fail-open).
- engine/pq pre-check relaxed from 'BLS key required' to '>=1 verify key
  (BLS, Corona, or ML-DSA) required'.
- QuantumPolicy SigQuasar wire format keeps its by-design BLS+Corona dual
  shape; pure-PQ finality runs through the WeightedQuorumCert / Polaris
  path, not SigQuasar.

Regression tests (pure_pq_cert_test.go, both pass):
- TestVerifyWithRealKeys_PurePQ_NoBLS: pure-PQ cert (BLS empty, real
  ML-DSA-65 leg) verifies with a nil BLS key; tampered leg rejected.
- TestVerifyWithRealKeys_FailClosed_NoLeg: empty cert + rollup-without-keys
  both rejected.

Full protocol/quasar suite green; engine/pq green; full consensus build clean.
2026-06-21 15:25:13 -07:00

90 lines
3.2 KiB
Go

// Copyright (C) 2025-2026, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package quasar
import (
"crypto/rand"
"testing"
"github.com/luxfi/crypto/mldsa"
)
// TestVerifyWithRealKeys_PurePQ_NoBLS is the regression guard for the
// fully-PQ finality property: a QuasarCert that carries NO BLS leg
// (pure-PQ posture) MUST finalise on its lattice leg alone. Before the
// fix, VerifyWithRealKeys hard-required BLS (`if len(c.BLS)==0 return
// false`), which meant a quantum break of BLS-12-381 could forge any
// cert that passed verification — the classical fast path was load-
// bearing, not optional. BLS is now one OPTIONAL leg; a pure-PQ cert
// (ML-DSA-65 identity leg, BLS empty) verifies with a nil BLS key.
func TestVerifyWithRealKeys_PurePQ_NoBLS(t *testing.T) {
msg := []byte("pure-pq-finality-digest-32-bytes")
// One real per-validator ML-DSA-65 (FIPS 204) signature — the PQ leg.
sk, err := mldsa.GenerateKey(rand.Reader, mldsa.MLDSA65)
if err != nil {
t.Fatalf("mldsa.GenerateKey: %v", err)
}
sig, err := sk.Sign(rand.Reader, msg, nil)
if err != nil {
t.Fatalf("mldsa.Sign: %v", err)
}
pk := sk.PublicKey
cert := &QuasarCert{
// BLS intentionally empty: pure-PQ posture.
MLDSARollup: EncodeMLDSASigs([][]byte{sig}),
Validators: 1,
}
// The fix: pure-PQ cert verifies with a NIL BLS key.
if !cert.VerifyWithRealKeys(msg, nil, nil, []*mldsa.PublicKey{pk}) {
t.Fatal("pure-PQ QuasarCert (no BLS) must verify on its ML-DSA leg with a nil BLS key")
}
// HasClassicalFastPath reports the documented contract: no BLS here.
if cert.HasClassicalFastPath() {
t.Fatal("pure-PQ cert must report HasClassicalFastPath()=false")
}
// Tampered PQ leg must still be rejected — the leg is cryptographically
// checked, not merely present.
badMsg := []byte("a-different-digest-not-what-signed")
if cert.VerifyWithRealKeys(badMsg, nil, nil, []*mldsa.PublicKey{pk}) {
t.Fatal("pure-PQ cert must reject when the ML-DSA leg does not verify against the message")
}
}
// TestVerifyWithRealKeys_FailClosed_NoLeg guards the safety side of making
// BLS optional: a cert with NO verifiable leg (every leg empty, or bytes
// present but no key supplied) MUST be rejected. Finality may never rest
// on the ABSENCE of evidence.
func TestVerifyWithRealKeys_FailClosed_NoLeg(t *testing.T) {
msg := []byte("fail-closed-finality-digest-3232")
// Empty cert: no BLS, no Corona, no rollup → zero verified legs.
empty := &QuasarCert{Validators: 1}
if empty.VerifyWithRealKeys(msg, nil, nil, nil) {
t.Fatal("empty cert (no legs) must be REJECTED — fail-closed, never finalise on absence of evidence")
}
// Rollup bytes present but no ML-DSA keys supplied → cannot verify the
// only leg → reject (not silently skip into a 0-leg accept).
sk, err := mldsa.GenerateKey(rand.Reader, mldsa.MLDSA65)
if err != nil {
t.Fatalf("mldsa.GenerateKey: %v", err)
}
sig, err := sk.Sign(rand.Reader, msg, nil)
if err != nil {
t.Fatalf("mldsa.Sign: %v", err)
}
rollupNoKeys := &QuasarCert{
MLDSARollup: EncodeMLDSASigs([][]byte{sig}),
Validators: 1,
}
if rollupNoKeys.VerifyWithRealKeys(msg, nil, nil, nil) {
t.Fatal("cert with rollup bytes but no ML-DSA keys must be REJECTED, not accepted on a skipped leg")
}
}