mirror of
https://github.com/luxfi/consensus.git
synced 2026-09-24 00:26:58 +00:00
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.
205 lines
6.1 KiB
Go
205 lines
6.1 KiB
Go
package pq
|
|
|
|
import (
|
|
"context"
|
|
"crypto/sha256"
|
|
"errors"
|
|
"fmt"
|
|
"sync"
|
|
|
|
"github.com/luxfi/consensus/protocol/quasar"
|
|
"github.com/luxfi/crypto/bls"
|
|
"github.com/luxfi/crypto/mldsa"
|
|
"github.com/luxfi/ids"
|
|
coronaThreshold "github.com/luxfi/threshold/protocols/corona"
|
|
)
|
|
|
|
// Engine defines the post-quantum consensus engine
|
|
type Engine interface {
|
|
// Start starts the engine
|
|
Start(context.Context, uint32) error
|
|
|
|
// Stop stops the engine
|
|
Stop(context.Context) error
|
|
|
|
// HealthCheck performs a health check
|
|
HealthCheck(context.Context) (interface{}, error)
|
|
|
|
// IsBootstrapped returns whether the engine is bootstrapped
|
|
IsBootstrapped() bool
|
|
|
|
// VerifyQuantumSignature verifies a post-quantum signature
|
|
VerifyQuantumSignature([]byte, []byte, []byte) error
|
|
|
|
// GenerateQuantumProof generates a quantum-resistant proof
|
|
GenerateQuantumProof(context.Context, ids.ID) ([]byte, error)
|
|
}
|
|
|
|
// PostQuantum implements post-quantum consensus engine
|
|
type PostQuantum struct {
|
|
bootstrapped bool
|
|
algorithm string // ML-DSA, ML-KEM, etc.
|
|
|
|
mu sync.RWMutex
|
|
// Real signing engine, optional. When set, GenerateQuantumProof
|
|
// produces a serialized QuasarCert via TripleSignRound1.
|
|
signer *quasar.Signer
|
|
// Verification keys, optional. When set, VerifyQuantumSignature
|
|
// performs real BLS+Corona+ML-DSA verification on a serialized
|
|
// QuasarCert.
|
|
blsAggKey *bls.PublicKey
|
|
rtGroupKey *coronaThreshold.GroupKey
|
|
mldsaPubKey *mldsa.PublicKey
|
|
}
|
|
|
|
// New creates a new post-quantum consensus engine
|
|
func New() *PostQuantum {
|
|
return &PostQuantum{
|
|
bootstrapped: false,
|
|
algorithm: "ML-DSA-65", // Default to ML-DSA-65
|
|
}
|
|
}
|
|
|
|
// AttachSigner wires a real PQ signer for proof generation.
|
|
func (pq *PostQuantum) AttachSigner(s *quasar.Signer) {
|
|
pq.mu.Lock()
|
|
defer pq.mu.Unlock()
|
|
pq.signer = s
|
|
}
|
|
|
|
// AttachVerifyKeys wires real PQ verification keys. blsAggKey is required;
|
|
// rtGroupKey and mldsaPubKey are optional (skipped if nil).
|
|
func (pq *PostQuantum) AttachVerifyKeys(blsAggKey *bls.PublicKey, rtGroupKey *coronaThreshold.GroupKey, mldsaPubKey *mldsa.PublicKey) {
|
|
pq.mu.Lock()
|
|
defer pq.mu.Unlock()
|
|
pq.blsAggKey = blsAggKey
|
|
pq.rtGroupKey = rtGroupKey
|
|
pq.mldsaPubKey = mldsaPubKey
|
|
}
|
|
|
|
// Start starts the engine
|
|
func (pq *PostQuantum) Start(ctx context.Context, requestID uint32) error {
|
|
pq.bootstrapped = true
|
|
return nil
|
|
}
|
|
|
|
// Stop stops the engine
|
|
func (pq *PostQuantum) Stop(ctx context.Context) error {
|
|
return nil
|
|
}
|
|
|
|
// HealthCheck performs a health check
|
|
func (pq *PostQuantum) HealthCheck(ctx context.Context) (interface{}, error) {
|
|
return map[string]interface{}{
|
|
"healthy": true,
|
|
"algorithm": pq.algorithm,
|
|
}, nil
|
|
}
|
|
|
|
// IsBootstrapped returns whether the engine is bootstrapped
|
|
func (pq *PostQuantum) IsBootstrapped() bool {
|
|
return pq.bootstrapped
|
|
}
|
|
|
|
// VerifyQuantumSignature verifies a serialized QuasarCert.
|
|
//
|
|
// signature must be the bytes produced by QuasarCert.MarshalBinary.
|
|
// message is the digest the cert commits to.
|
|
// publicKey is reserved for future single-key paths; current implementation
|
|
// uses verification keys attached via AttachVerifyKeys.
|
|
func (pq *PostQuantum) VerifyQuantumSignature(message, signature, publicKey []byte) error {
|
|
pq.mu.RLock()
|
|
blsAggKey := pq.blsAggKey
|
|
rtGroupKey := pq.rtGroupKey
|
|
mldsaPubKey := pq.mldsaPubKey
|
|
pq.mu.RUnlock()
|
|
|
|
// BLS is the OPTIONAL classical fast-path leg; a pure-PQ cert carries
|
|
// none. Require at least one verify key (BLS, Corona, or ML-DSA) so a
|
|
// pure-PQ posture (Corona / ML-DSA only) is verifiable, while a fully
|
|
// keyless call is still rejected. The per-leg fail-closed guard in
|
|
// QuasarCert.VerifyWithRealKeys ensures finality rests on ≥1 checked
|
|
// signature.
|
|
if blsAggKey == nil && rtGroupKey == nil && mldsaPubKey == nil {
|
|
return errors.New("pq: no verify key attached (need BLS, Corona, or ML-DSA)")
|
|
}
|
|
|
|
cert := &quasar.QuasarCert{}
|
|
if err := cert.UnmarshalBinary(signature); err != nil {
|
|
return fmt.Errorf("pq: decode QuasarCert: %w", err)
|
|
}
|
|
|
|
var mldsaKeys []*mldsa.PublicKey
|
|
if mldsaPubKey != nil {
|
|
mldsaKeys = []*mldsa.PublicKey{mldsaPubKey}
|
|
}
|
|
|
|
if !cert.VerifyWithRealKeys(message, blsAggKey, rtGroupKey, mldsaKeys) {
|
|
return errors.New("pq: QuasarCert verification failed")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// GenerateQuantumProof generates a serialized QuasarCert for the given block.
|
|
// Requires AttachSigner to have been called with a fully-configured signer.
|
|
func (pq *PostQuantum) GenerateQuantumProof(ctx context.Context, blockID ids.ID) ([]byte, error) {
|
|
pq.mu.RLock()
|
|
s := pq.signer
|
|
pq.mu.RUnlock()
|
|
if s == nil {
|
|
return nil, errors.New("pq: no signer attached")
|
|
}
|
|
|
|
// Pick any configured validator.
|
|
validatorID, err := pickValidator(s)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Build canonical message: SHA-256(blockID || "lux-pq-engine-v1").
|
|
h := sha256.New()
|
|
h.Write(blockID[:])
|
|
h.Write([]byte("lux-pq-engine-v1"))
|
|
msg := h.Sum(nil)
|
|
|
|
prfKeyArr := sha256.Sum256(blockID[:])
|
|
prfKey := prfKeyArr[:]
|
|
sessionID := int(blockID[0])<<24 | int(blockID[1])<<16 | int(blockID[2])<<8 | int(blockID[3])
|
|
|
|
sig, _, err := s.TripleSignRound1(ctx, validatorID, msg, sessionID, prfKey)
|
|
if err != nil {
|
|
// Fall back to legacy single-key path for signers without BLS
|
|
// threshold configuration.
|
|
legacy, lerr := s.SignMessageWithContext(ctx, validatorID, msg)
|
|
if lerr != nil {
|
|
return nil, fmt.Errorf("pq: sign block: %w", err)
|
|
}
|
|
sig = legacy
|
|
}
|
|
|
|
cert := &quasar.QuasarCert{
|
|
BLS: append([]byte(nil), sig.BLS...),
|
|
}
|
|
if len(sig.MLDSA) > 0 {
|
|
cert.MLDSARollup = quasar.EncodeMLDSASigs([][]byte{sig.MLDSA})
|
|
}
|
|
return cert.MarshalBinary()
|
|
}
|
|
|
|
// pickValidator returns any validator ID configured on the signer.
|
|
func pickValidator(s *quasar.Signer) (string, error) {
|
|
// Use exported helpers: AddValidator stores into validators map but
|
|
// there is no public iterator, so use any configured BLS key.
|
|
// For test-driver paths the caller knows which validator they added;
|
|
// we expose a helper here that callers can override by attaching
|
|
// a signer that already has at least one validator.
|
|
if s == nil {
|
|
return "", errors.New("pq: nil signer")
|
|
}
|
|
id := s.AnyValidatorID()
|
|
if id == "" {
|
|
return "", errors.New("pq: signer has no configured validators")
|
|
}
|
|
return id, nil
|
|
}
|