mirror of
https://github.com/luxfi/crypto.git
synced 2026-07-27 07:43:44 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8c8af9b545 | ||
|
|
0ed2a574f4 | ||
|
|
c626fd7d5d | ||
|
|
b436812dd3 | ||
|
|
e24964b0ad | ||
|
|
59fe62c06d | ||
|
|
c436347937 | ||
|
|
482faf9bea | ||
|
|
92efe562a9 | ||
|
|
a6cc9670f7 | ||
|
|
447c43314d | ||
|
|
733b315ce2 | ||
|
|
7181e9ccbc | ||
|
|
8e04abc877 | ||
|
|
429ab6b8ad | ||
|
|
0faac6d2e7 | ||
|
|
e63630ed80 | ||
|
|
f94599dcee | ||
|
|
ae88198fcf | ||
|
|
5bacec4869 | ||
|
|
aa7a640e9d | ||
|
|
3fa72fc0f6 | ||
|
|
e8403d217e | ||
|
|
af332c0910 | ||
|
|
7b260cae20 | ||
|
|
08eacb0237 |
@@ -0,0 +1,9 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="1280" height="640" viewBox="0 0 1280 640" role="img" aria-label="crypto">
|
||||
<rect width="1280" height="640" fill="#0A0A0A"/>
|
||||
<svg x="96" y="215" width="210" height="210" viewBox="17 26 66 66"><path d="M50 88 L17.09 31 L82.91 31 Z" fill="#fff"/></svg>
|
||||
<text x="378" y="276" font-family="Inter,system-ui,-apple-system,sans-serif" font-size="78" font-weight="800" letter-spacing="-2" fill="#ffffff">crypto</text>
|
||||
<text x="378" y="322" font-family="Inter,system-ui,sans-serif" font-size="30" fill="#ffffff" opacity=".66">Core cryptographic primitives for Lux: high-performance hash functions…</text>
|
||||
<rect x="378" y="338" width="806" height="3" rx="1.5" fill="#ffffff" opacity=".9"/>
|
||||
<text x="378" y="390" font-family="Inter,system-ui,sans-serif" font-size="24" font-weight="600" fill="#ffffff" opacity=".5">github.com/luxfi</text>
|
||||
<text x="1184" y="390" text-anchor="end" font-family="Inter,system-ui,sans-serif" font-size="24" font-weight="600" fill="#ffffff" opacity=".5">lux.network</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.0 KiB |
@@ -19,7 +19,7 @@ concurrency:
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: lux-build-amd64
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v5
|
||||
@@ -56,7 +56,7 @@ jobs:
|
||||
environment:
|
||||
name: github-pages
|
||||
url: ${{ steps.deployment.outputs.page_url }}
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: lux-build-amd64
|
||||
needs: build
|
||||
steps:
|
||||
- name: Deploy to GitHub Pages
|
||||
|
||||
@@ -33,3 +33,4 @@ target/
|
||||
test-results/
|
||||
tmp/
|
||||
*.eco
|
||||
*.test
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
<p align="center"><img src=".github/hero.svg" alt="crypto" width="880"></p>
|
||||
|
||||
# Lux Crypto
|
||||
|
||||
Cryptographic primitives for the Lux Network -- post-quantum signatures, key encapsulation, BLS aggregation, threshold signing, ring signatures, and EVM-compatible secp256k1.
|
||||
|
||||
@@ -1,168 +0,0 @@
|
||||
// Copyright (C) 2019-2025, Lux Industries Inc. All rights reserved.
|
||||
// See the file LICENSE for licensing terms.
|
||||
|
||||
package aggregated
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"sync"
|
||||
|
||||
"github.com/luxfi/crypto/bls"
|
||||
"github.com/luxfi/crypto/cggmp21"
|
||||
"github.com/luxfi/ids"
|
||||
log "github.com/luxfi/log"
|
||||
)
|
||||
|
||||
// BLSManager manages BLS signature operations
|
||||
type BLSManager struct {
|
||||
log log.Logger
|
||||
mu sync.RWMutex
|
||||
}
|
||||
|
||||
// NewBLSManager creates a new BLS manager
|
||||
func NewBLSManager(log log.Logger) *BLSManager {
|
||||
return &BLSManager{
|
||||
log: log,
|
||||
}
|
||||
}
|
||||
|
||||
// CreateKeyPair generates a new BLS key pair
|
||||
func (m *BLSManager) CreateKeyPair() (*bls.SecretKey, *bls.PublicKey, error) {
|
||||
sk, err := bls.NewSecretKey()
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
pk := sk.PublicKey()
|
||||
return sk, pk, nil
|
||||
}
|
||||
|
||||
// Sign creates a BLS signature
|
||||
func (m *BLSManager) Sign(sk *bls.SecretKey, message []byte) (*bls.Signature, error) {
|
||||
sig, err := sk.Sign(message)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return sig, nil
|
||||
}
|
||||
|
||||
// ThresholdManager manages threshold signature operations
|
||||
// Actual threshold operations use github.com/luxfi/threshold
|
||||
type ThresholdManager struct {
|
||||
log log.Logger
|
||||
mu sync.RWMutex
|
||||
}
|
||||
|
||||
// NewThresholdManager creates a new threshold manager
|
||||
func NewThresholdManager(log log.Logger) *ThresholdManager {
|
||||
return &ThresholdManager{
|
||||
log: log,
|
||||
}
|
||||
}
|
||||
|
||||
// CGGMP21Manager manages CGGMP21 threshold signature operations
|
||||
type CGGMP21Manager struct {
|
||||
log log.Logger
|
||||
parties map[int]*cggmp21.Party
|
||||
config *cggmp21.Config
|
||||
mu sync.RWMutex
|
||||
}
|
||||
|
||||
// NewCGGMP21Manager creates a new CGGMP21 manager
|
||||
func NewCGGMP21Manager(log log.Logger) *CGGMP21Manager {
|
||||
return &CGGMP21Manager{
|
||||
log: log,
|
||||
parties: make(map[int]*cggmp21.Party),
|
||||
}
|
||||
}
|
||||
|
||||
// InitializeParty creates a new CGGMP21 party
|
||||
func (m *CGGMP21Manager) InitializeParty(
|
||||
partyID ids.NodeID,
|
||||
index int,
|
||||
config *cggmp21.Config,
|
||||
) error {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
|
||||
party, err := cggmp21.NewParty(partyID, index, config, m.log)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
m.parties[index] = party
|
||||
m.config = config
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetParty retrieves a CGGMP21 party by index
|
||||
func (m *CGGMP21Manager) GetParty(index int) (*cggmp21.Party, error) {
|
||||
m.mu.RLock()
|
||||
defer m.mu.RUnlock()
|
||||
|
||||
party, exists := m.parties[index]
|
||||
if !exists {
|
||||
return nil, errors.New("party not found")
|
||||
}
|
||||
|
||||
return party, nil
|
||||
}
|
||||
|
||||
// FeeCollector manages fee collection for signature operations
|
||||
type FeeCollector struct {
|
||||
collectedFees map[SignatureType]uint64
|
||||
mu sync.RWMutex
|
||||
}
|
||||
|
||||
// NewFeeCollector creates a new fee collector
|
||||
func NewFeeCollector() FeeCollector {
|
||||
return FeeCollector{
|
||||
collectedFees: make(map[SignatureType]uint64),
|
||||
}
|
||||
}
|
||||
|
||||
// CollectFee records a fee collection
|
||||
func (fc *FeeCollector) CollectFee(sigType SignatureType, amount uint64) {
|
||||
fc.mu.Lock()
|
||||
defer fc.mu.Unlock()
|
||||
|
||||
fc.collectedFees[sigType] += amount
|
||||
}
|
||||
|
||||
// GetCollectedFees returns total collected fees by type
|
||||
func (fc *FeeCollector) GetCollectedFees() map[SignatureType]uint64 {
|
||||
fc.mu.RLock()
|
||||
defer fc.mu.RUnlock()
|
||||
|
||||
fees := make(map[SignatureType]uint64)
|
||||
for k, v := range fc.collectedFees {
|
||||
fees[k] = v
|
||||
}
|
||||
|
||||
return fees
|
||||
}
|
||||
|
||||
// GetTotalFees returns the total of all collected fees
|
||||
func (fc *FeeCollector) GetTotalFees() uint64 {
|
||||
fc.mu.RLock()
|
||||
defer fc.mu.RUnlock()
|
||||
|
||||
var total uint64
|
||||
for _, amount := range fc.collectedFees {
|
||||
total += amount
|
||||
}
|
||||
|
||||
return total
|
||||
}
|
||||
|
||||
// ResetFees resets the fee collection
|
||||
func (fc *FeeCollector) ResetFees() map[SignatureType]uint64 {
|
||||
fc.mu.Lock()
|
||||
defer fc.mu.Unlock()
|
||||
|
||||
oldFees := fc.collectedFees
|
||||
fc.collectedFees = make(map[SignatureType]uint64)
|
||||
|
||||
return oldFees
|
||||
}
|
||||
@@ -1,582 +0,0 @@
|
||||
// Copyright (C) 2019-2025, Lux Industries Inc. All rights reserved.
|
||||
// See the file LICENSE for licensing terms.
|
||||
|
||||
package aggregated
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/luxfi/crypto/bls"
|
||||
"github.com/luxfi/ids"
|
||||
log "github.com/luxfi/log"
|
||||
)
|
||||
|
||||
// ThresholdPublicKey wraps raw bytes for threshold public keys.
|
||||
// Threshold key generation and signing are handled by github.com/luxfi/threshold.
|
||||
type ThresholdPublicKey struct {
|
||||
Bytes []byte
|
||||
}
|
||||
|
||||
// Equal checks if two threshold public keys are equal
|
||||
func (pk *ThresholdPublicKey) Equal(other *ThresholdPublicKey) bool {
|
||||
if pk == nil || other == nil {
|
||||
return pk == other
|
||||
}
|
||||
if len(pk.Bytes) != len(other.Bytes) {
|
||||
return false
|
||||
}
|
||||
for i := range pk.Bytes {
|
||||
if pk.Bytes[i] != other.Bytes[i] {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// ThresholdSignature wraps raw bytes for threshold signatures.
|
||||
// Threshold signing and combination are handled by github.com/luxfi/threshold.
|
||||
type ThresholdSignature struct {
|
||||
Bytes []byte
|
||||
}
|
||||
|
||||
// Verify performs a basic non-empty check. Full cryptographic verification
|
||||
// requires the threshold public key and is done via github.com/luxfi/threshold.
|
||||
func (ts *ThresholdSignature) Verify(message []byte) bool {
|
||||
return len(ts.Bytes) > 0
|
||||
}
|
||||
|
||||
// Import log field helpers
|
||||
var (
|
||||
logUint8 = log.Uint8
|
||||
logUint64 = log.Uint64
|
||||
logBool = log.Bool
|
||||
logString = log.String
|
||||
)
|
||||
|
||||
// SignatureType represents the type of aggregated signature
|
||||
type SignatureType uint8
|
||||
|
||||
const (
|
||||
SignatureTypeBLS SignatureType = iota
|
||||
SignatureTypeCorona
|
||||
SignatureTypeCGGMP21
|
||||
)
|
||||
|
||||
// SignatureConfig contains configuration for signature aggregation
|
||||
type SignatureConfig struct {
|
||||
// Network-wide signature type preference
|
||||
PreferredType SignatureType `json:"preferredType"`
|
||||
|
||||
// Enable specific signature types
|
||||
EnableBLS bool `json:"enableBLS"`
|
||||
EnableCorona bool `json:"enableCorona"`
|
||||
EnableCGGMP21 bool `json:"enableCGGMP21"`
|
||||
|
||||
// Fee configuration (in nLUX - nano LUX)
|
||||
BLSFee uint64 `json:"blsFee"` // 0 = free
|
||||
CoronaFee uint64 `json:"coronaFee"` // Premium for enhanced privacy
|
||||
CGGMP21Fee uint64 `json:"cggmp21Fee"` // Premium for threshold signatures
|
||||
|
||||
// Performance settings
|
||||
ParallelAggregation bool `json:"parallelAggregation"`
|
||||
MaxSignersPerRound int `json:"maxSignersPerRound"`
|
||||
|
||||
// Security settings
|
||||
MinSigners int `json:"minSigners"`
|
||||
ThresholdRatio float64 `json:"thresholdRatio"` // e.g., 0.67 for 2/3
|
||||
}
|
||||
|
||||
// AggregatedSignature represents an aggregated signature with metadata
|
||||
type AggregatedSignature struct {
|
||||
Type SignatureType `json:"type"`
|
||||
Signature []byte `json:"signature"`
|
||||
SignerIDs []ids.NodeID `json:"signerIds,omitempty"`
|
||||
SignerCount int `json:"signerCount"`
|
||||
ThresholdPubKeys []*ThresholdPublicKey `json:"thresholdPubKeys,omitempty"` // For Corona threshold
|
||||
AggregateKey []byte `json:"aggregateKey,omitempty"` // For BLS
|
||||
ThresholdRequired int `json:"threshold,omitempty"` // For threshold schemes
|
||||
TotalFee uint64 `json:"totalFee"`
|
||||
}
|
||||
|
||||
// SignatureAggregator manages network-wide signature aggregation
|
||||
type SignatureAggregator struct {
|
||||
config SignatureConfig
|
||||
log log.Logger
|
||||
|
||||
// Signature managers
|
||||
blsManager *BLSManager
|
||||
thresholdManager *ThresholdManager
|
||||
cggmpManager *CGGMP21Manager
|
||||
|
||||
// Active aggregation sessions
|
||||
sessions map[string]*AggregationSession
|
||||
|
||||
// Fee collector
|
||||
feeCollector FeeCollector
|
||||
|
||||
mu sync.RWMutex
|
||||
}
|
||||
|
||||
// AggregationSession represents an active signature aggregation
|
||||
type AggregationSession struct {
|
||||
SessionID string
|
||||
Message []byte
|
||||
SignatureType SignatureType
|
||||
|
||||
// Collected signatures
|
||||
BLSSignatures []*bls.Signature
|
||||
BLSPublicKeys []*bls.PublicKey
|
||||
ThresholdSignatures []*ThresholdSignature
|
||||
ThresholdPubKeys []*ThresholdPublicKey
|
||||
|
||||
// Signers
|
||||
Signers map[ids.NodeID]bool
|
||||
SignerCount int
|
||||
|
||||
// Status
|
||||
StartTime int64
|
||||
Completed bool
|
||||
Result *AggregatedSignature
|
||||
}
|
||||
|
||||
// NewSignatureAggregator creates a new signature aggregator
|
||||
func NewSignatureAggregator(config SignatureConfig, log log.Logger) (*SignatureAggregator, error) {
|
||||
sa := &SignatureAggregator{
|
||||
config: config,
|
||||
log: log,
|
||||
sessions: make(map[string]*AggregationSession),
|
||||
}
|
||||
|
||||
// Initialize signature managers based on config
|
||||
if config.EnableBLS {
|
||||
sa.blsManager = NewBLSManager(log)
|
||||
}
|
||||
|
||||
if config.EnableCorona {
|
||||
sa.thresholdManager = NewThresholdManager(log)
|
||||
}
|
||||
|
||||
if config.EnableCGGMP21 {
|
||||
sa.cggmpManager = NewCGGMP21Manager(log)
|
||||
}
|
||||
|
||||
sa.feeCollector = NewFeeCollector()
|
||||
|
||||
log.Info("Signature aggregator initialized",
|
||||
logUint8("preferredType", uint8(config.PreferredType)),
|
||||
logBool("blsEnabled", config.EnableBLS),
|
||||
logBool("coronaEnabled", config.EnableCorona),
|
||||
logBool("cggmp21Enabled", config.EnableCGGMP21),
|
||||
logUint64("blsFee", config.BLSFee),
|
||||
logUint64("coronaFee", config.CoronaFee),
|
||||
)
|
||||
|
||||
return sa, nil
|
||||
}
|
||||
|
||||
// StartAggregation starts a new signature aggregation session
|
||||
func (sa *SignatureAggregator) StartAggregation(
|
||||
sessionID string,
|
||||
message []byte,
|
||||
sigType SignatureType,
|
||||
expectedSigners int,
|
||||
) error {
|
||||
sa.mu.Lock()
|
||||
defer sa.mu.Unlock()
|
||||
|
||||
if _, exists := sa.sessions[sessionID]; exists {
|
||||
return errors.New("session already exists")
|
||||
}
|
||||
|
||||
// Validate signature type is enabled
|
||||
switch sigType {
|
||||
case SignatureTypeBLS:
|
||||
if !sa.config.EnableBLS {
|
||||
return errors.New("BLS signatures not enabled")
|
||||
}
|
||||
case SignatureTypeCorona:
|
||||
if !sa.config.EnableCorona {
|
||||
return errors.New("Corona signatures not enabled")
|
||||
}
|
||||
case SignatureTypeCGGMP21:
|
||||
if !sa.config.EnableCGGMP21 {
|
||||
return errors.New("CGGMP21 signatures not enabled")
|
||||
}
|
||||
default:
|
||||
return errors.New("unknown signature type")
|
||||
}
|
||||
|
||||
session := &AggregationSession{
|
||||
SessionID: sessionID,
|
||||
Message: message,
|
||||
SignatureType: sigType,
|
||||
Signers: make(map[ids.NodeID]bool),
|
||||
StartTime: getCurrentTime(),
|
||||
}
|
||||
|
||||
sa.sessions[sessionID] = session
|
||||
|
||||
sa.log.Debug("Started aggregation session",
|
||||
log.String("sessionID", sessionID),
|
||||
log.Uint8("type", uint8(sigType)),
|
||||
log.Int("expectedSigners", expectedSigners),
|
||||
)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// AddSignature adds a signature to an aggregation session
|
||||
func (sa *SignatureAggregator) AddSignature(
|
||||
sessionID string,
|
||||
signerID ids.NodeID,
|
||||
signature []byte,
|
||||
publicKey []byte,
|
||||
) error {
|
||||
sa.mu.Lock()
|
||||
defer sa.mu.Unlock()
|
||||
|
||||
session, exists := sa.sessions[sessionID]
|
||||
if !exists {
|
||||
return errors.New("session not found")
|
||||
}
|
||||
|
||||
if session.Completed {
|
||||
return errors.New("session already completed")
|
||||
}
|
||||
|
||||
// Check if signer already contributed
|
||||
if session.Signers[signerID] {
|
||||
return errors.New("signer already contributed")
|
||||
}
|
||||
|
||||
// Add signature based on type
|
||||
switch session.SignatureType {
|
||||
case SignatureTypeBLS:
|
||||
return sa.addBLSSignature(session, signerID, signature, publicKey)
|
||||
|
||||
case SignatureTypeCorona:
|
||||
return sa.addCoronaSignature(session, signerID, signature, publicKey)
|
||||
|
||||
case SignatureTypeCGGMP21:
|
||||
return errors.New("CGGMP21 uses different protocol flow")
|
||||
|
||||
default:
|
||||
return errors.New("unknown signature type")
|
||||
}
|
||||
}
|
||||
|
||||
// addBLSSignature adds a BLS signature to the session
|
||||
func (sa *SignatureAggregator) addBLSSignature(
|
||||
session *AggregationSession,
|
||||
signerID ids.NodeID,
|
||||
signature []byte,
|
||||
publicKey []byte,
|
||||
) error {
|
||||
// Parse BLS signature and public key
|
||||
sig, err := bls.SignatureFromBytes(signature)
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid BLS signature: %w", err)
|
||||
}
|
||||
|
||||
pk, err := bls.PublicKeyFromCompressedBytes(publicKey)
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid BLS public key: %w", err)
|
||||
}
|
||||
|
||||
// Verify individual signature
|
||||
valid := bls.Verify(pk, sig, session.Message)
|
||||
if !valid {
|
||||
return fmt.Errorf("BLS signature verification failed")
|
||||
}
|
||||
|
||||
// Add to session
|
||||
session.BLSSignatures = append(session.BLSSignatures, sig)
|
||||
session.BLSPublicKeys = append(session.BLSPublicKeys, pk)
|
||||
session.Signers[signerID] = true
|
||||
session.SignerCount++
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// addCoronaSignature adds a Corona threshold signature share to the session
|
||||
// Actual threshold operations are coordinated through github.com/luxfi/threshold
|
||||
func (sa *SignatureAggregator) addCoronaSignature(
|
||||
session *AggregationSession,
|
||||
signerID ids.NodeID,
|
||||
signature []byte,
|
||||
publicKey []byte,
|
||||
) error {
|
||||
// Create threshold signature placeholder
|
||||
// Actual deserialization would use github.com/luxfi/threshold
|
||||
thresholdSig := &ThresholdSignature{
|
||||
Bytes: signature,
|
||||
}
|
||||
|
||||
// Create threshold public key placeholder
|
||||
pk := &ThresholdPublicKey{
|
||||
Bytes: publicKey,
|
||||
}
|
||||
|
||||
// Add to participant list if not already present
|
||||
inList := false
|
||||
for _, existingPK := range session.ThresholdPubKeys {
|
||||
if existingPK.Equal(pk) {
|
||||
inList = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !inList {
|
||||
session.ThresholdPubKeys = append(session.ThresholdPubKeys, pk)
|
||||
}
|
||||
|
||||
// Store signature share
|
||||
session.ThresholdSignatures = append(session.ThresholdSignatures, thresholdSig)
|
||||
session.Signers[signerID] = true
|
||||
session.SignerCount++
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// FinalizeAggregation completes the aggregation and returns the result
|
||||
func (sa *SignatureAggregator) FinalizeAggregation(
|
||||
sessionID string,
|
||||
requiredSigners int,
|
||||
) (*AggregatedSignature, error) {
|
||||
sa.mu.Lock()
|
||||
defer sa.mu.Unlock()
|
||||
|
||||
session, exists := sa.sessions[sessionID]
|
||||
if !exists {
|
||||
return nil, errors.New("session not found")
|
||||
}
|
||||
|
||||
if session.Completed {
|
||||
return session.Result, nil
|
||||
}
|
||||
|
||||
// Check minimum signers
|
||||
if session.SignerCount < requiredSigners {
|
||||
return nil, fmt.Errorf("insufficient signers: %d < %d", session.SignerCount, requiredSigners)
|
||||
}
|
||||
|
||||
var result *AggregatedSignature
|
||||
var err error
|
||||
|
||||
switch session.SignatureType {
|
||||
case SignatureTypeBLS:
|
||||
result, err = sa.finalizeBLS(session)
|
||||
|
||||
case SignatureTypeCorona:
|
||||
result, err = sa.finalizeCorona(session)
|
||||
|
||||
default:
|
||||
return nil, errors.New("unknown signature type")
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Calculate total fee
|
||||
result.TotalFee = sa.calculateFee(session.SignatureType, session.SignerCount)
|
||||
|
||||
// Mark session as completed
|
||||
session.Completed = true
|
||||
session.Result = result
|
||||
|
||||
sa.log.Info("Finalized aggregation",
|
||||
log.String("sessionID", sessionID),
|
||||
log.Uint8("type", uint8(session.SignatureType)),
|
||||
log.Int("signers", session.SignerCount),
|
||||
log.Uint64("totalFee", result.TotalFee),
|
||||
)
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// finalizeBLS aggregates BLS signatures
|
||||
func (sa *SignatureAggregator) finalizeBLS(session *AggregationSession) (*AggregatedSignature, error) {
|
||||
if len(session.BLSSignatures) == 0 {
|
||||
return nil, errors.New("no BLS signatures to aggregate")
|
||||
}
|
||||
|
||||
// Aggregate signatures
|
||||
aggSig, err := bls.AggregateSignatures(session.BLSSignatures)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("BLS aggregation failed: %w", err)
|
||||
}
|
||||
|
||||
// Aggregate public keys
|
||||
aggPK, err := bls.AggregatePublicKeys(session.BLSPublicKeys)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("BLS public key aggregation failed: %w", err)
|
||||
}
|
||||
|
||||
// Verify aggregate signature
|
||||
valid := bls.Verify(aggPK, aggSig, session.Message)
|
||||
if !valid {
|
||||
return nil, fmt.Errorf("aggregate signature verification failed")
|
||||
}
|
||||
|
||||
sigBytes := bls.SignatureToBytes(aggSig)
|
||||
pkBytes := bls.PublicKeyToCompressedBytes(aggPK)
|
||||
|
||||
// Extract signer IDs
|
||||
signerIDs := make([]ids.NodeID, 0, len(session.Signers))
|
||||
for id := range session.Signers {
|
||||
signerIDs = append(signerIDs, id)
|
||||
}
|
||||
|
||||
return &AggregatedSignature{
|
||||
Type: SignatureTypeBLS,
|
||||
Signature: sigBytes,
|
||||
SignerIDs: signerIDs,
|
||||
SignerCount: session.SignerCount,
|
||||
AggregateKey: pkBytes,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// finalizeCorona aggregates threshold signature shares
|
||||
// Actual threshold aggregation uses github.com/luxfi/threshold protocols
|
||||
func (sa *SignatureAggregator) finalizeCorona(session *AggregationSession) (*AggregatedSignature, error) {
|
||||
if len(session.ThresholdSignatures) == 0 {
|
||||
return nil, errors.New("no threshold signatures collected")
|
||||
}
|
||||
|
||||
// Aggregate threshold signature shares
|
||||
// In production, this would use github.com/luxfi/threshold to combine shares
|
||||
// For now, concatenate the signature bytes as a placeholder
|
||||
var sigBytes []byte
|
||||
for _, sig := range session.ThresholdSignatures {
|
||||
sigBytes = append(sigBytes, sig.Bytes...)
|
||||
}
|
||||
|
||||
// Verify threshold signature
|
||||
// Actual verification would use the threshold package
|
||||
if len(sigBytes) == 0 {
|
||||
return nil, fmt.Errorf("threshold signature aggregation failed")
|
||||
}
|
||||
|
||||
return &AggregatedSignature{
|
||||
Type: SignatureTypeCorona,
|
||||
Signature: sigBytes,
|
||||
SignerCount: session.SignerCount,
|
||||
ThresholdPubKeys: session.ThresholdPubKeys,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// calculateFee calculates the total fee for signature aggregation
|
||||
func (sa *SignatureAggregator) calculateFee(sigType SignatureType, signerCount int) uint64 {
|
||||
var feePerSigner uint64
|
||||
|
||||
switch sigType {
|
||||
case SignatureTypeBLS:
|
||||
feePerSigner = sa.config.BLSFee // 0 for free
|
||||
case SignatureTypeCorona:
|
||||
feePerSigner = sa.config.CoronaFee // Premium fee
|
||||
case SignatureTypeCGGMP21:
|
||||
feePerSigner = sa.config.CGGMP21Fee // Premium fee
|
||||
default:
|
||||
feePerSigner = 0
|
||||
}
|
||||
|
||||
return feePerSigner * uint64(signerCount)
|
||||
}
|
||||
|
||||
// VerifyAggregatedSignature verifies an aggregated signature
|
||||
func (sa *SignatureAggregator) VerifyAggregatedSignature(
|
||||
message []byte,
|
||||
aggSig *AggregatedSignature,
|
||||
) error {
|
||||
switch aggSig.Type {
|
||||
case SignatureTypeBLS:
|
||||
return sa.verifyBLSAggregate(message, aggSig)
|
||||
|
||||
case SignatureTypeCorona:
|
||||
return sa.verifyCoronaAggregate(message, aggSig)
|
||||
|
||||
case SignatureTypeCGGMP21:
|
||||
return errors.New("CGGMP21 verification not implemented")
|
||||
|
||||
default:
|
||||
return errors.New("unknown signature type")
|
||||
}
|
||||
}
|
||||
|
||||
// verifyBLSAggregate verifies a BLS aggregate signature
|
||||
func (sa *SignatureAggregator) verifyBLSAggregate(message []byte, aggSig *AggregatedSignature) error {
|
||||
sig, err := bls.SignatureFromBytes(aggSig.Signature)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
pk, err := bls.PublicKeyFromCompressedBytes(aggSig.AggregateKey)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
valid := bls.Verify(pk, sig, message)
|
||||
if !valid {
|
||||
return errors.New("BLS signature verification failed")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// verifyCoronaAggregate verifies a Corona ring signature
|
||||
func (sa *SignatureAggregator) verifyCoronaAggregate(message []byte, aggSig *AggregatedSignature) error {
|
||||
// For now, simplified verification
|
||||
// In production, deserialize and verify properly
|
||||
if len(aggSig.Signature) < 256 {
|
||||
return errors.New("invalid ring signature")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetSessionStatus returns the status of an aggregation session
|
||||
func (sa *SignatureAggregator) GetSessionStatus(sessionID string) (map[string]interface{}, error) {
|
||||
sa.mu.RLock()
|
||||
defer sa.mu.RUnlock()
|
||||
|
||||
session, exists := sa.sessions[sessionID]
|
||||
if !exists {
|
||||
return nil, errors.New("session not found")
|
||||
}
|
||||
|
||||
status := map[string]interface{}{
|
||||
"sessionID": session.SessionID,
|
||||
"signatureType": session.SignatureType,
|
||||
"signerCount": session.SignerCount,
|
||||
"completed": session.Completed,
|
||||
"startTime": session.StartTime,
|
||||
}
|
||||
|
||||
if session.Completed && session.Result != nil {
|
||||
status["totalFee"] = session.Result.TotalFee
|
||||
}
|
||||
|
||||
return status, nil
|
||||
}
|
||||
|
||||
// Cleanup removes old sessions
|
||||
func (sa *SignatureAggregator) Cleanup(maxAge int64) {
|
||||
sa.mu.Lock()
|
||||
defer sa.mu.Unlock()
|
||||
|
||||
currentTime := getCurrentTime()
|
||||
|
||||
for sessionID, session := range sa.sessions {
|
||||
if currentTime-session.StartTime > maxAge {
|
||||
delete(sa.sessions, sessionID)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Helper function
|
||||
func getCurrentTime() int64 {
|
||||
return time.Now().Unix()
|
||||
}
|
||||
@@ -0,0 +1,136 @@
|
||||
# attestation
|
||||
|
||||
Remote-attestation quote verification for TEEs (Intel SGX DCAP v3, Intel TDX
|
||||
DCAP v4, AMD SEV-SNP). Given a raw hardware quote and a pinned trust policy,
|
||||
this package cryptographically checks the claim: "I am running the expected
|
||||
code inside a genuine enclave, and my confidentiality key is bound to that
|
||||
enclave." A quote either passes all four soundness checks or is rejected with
|
||||
a specific error — nothing is skipped.
|
||||
|
||||
## Scope
|
||||
|
||||
**Does:**
|
||||
|
||||
- Parse Intel SGX (DCAP v3), Intel TDX (DCAP v4), and AMD SEV-SNP quote wire
|
||||
formats into a canonical `Quote` shape (`vendor.go`).
|
||||
- Verify the attestation-key certificate chain to a **pinned** vendor root
|
||||
(`attestation.go:74-89`, `vendor.go:49-68`).
|
||||
- Verify the ECDSA signature over the exact signed region of the quote
|
||||
(SGX/TDX: header ‖ report body under P-256/SHA-256; SNP: `report[0:0x2A0]`
|
||||
under P-384/SHA-384) (`vendor.go:136-139`, `vendor.go:207-209`,
|
||||
`vendor.go:257-259`).
|
||||
- Enforce a caller-supplied enclave measurement allow-list (`MRENCLAVE` for
|
||||
SGX, `MRTD` for TDX/SNP) (`attestation.go:99-102`).
|
||||
- Enforce the operator-key binding: `report_data[0:32] == sha256(operatorPub)`
|
||||
(`attestation.go:57-60`, `vendor.go:273-277`).
|
||||
|
||||
**Does not:**
|
||||
|
||||
- Fetch PCK / VCEK certificates from Intel PCS or AMD KDS. Callers supply the
|
||||
cert chain in the quote or as a separate parameter (`VerifySNP` takes
|
||||
`vcekDER` + `askDER` explicitly, `vendor.go:237`).
|
||||
- Manage the pinned root store or measurement allow-list. Callers construct
|
||||
`*x509.CertPool` and `map[[32]byte]bool` / `map[[48]byte]bool` themselves
|
||||
(`attestation.go:35-38`).
|
||||
- Rotate or revoke roots. Pin lifecycle is upstream policy.
|
||||
|
||||
## Public API
|
||||
|
||||
Verified against HEAD.
|
||||
|
||||
| Symbol | Location | Purpose |
|
||||
|---------------------------------|-----------------------------|------------------------------------------------------------------------|
|
||||
| `Quote` | `attestation.go:26-32` | Canonical parsed quote (measurement, report data, leaf, chain, sig). |
|
||||
| `Policy` | `attestation.go:35-38` | Trust policy: pinned roots + allowed 32-byte measurements. |
|
||||
| `BindKey(operatorPub) [32]byte` | `attestation.go:58-60` | The value an enclave must place in `ReportData` to bind a pub key. |
|
||||
| `Verify(q, policy, boundKey)` | `attestation.go:66-109` | Verify a canonical `Quote` against policy + operator key binding. |
|
||||
| `VerifySGX(...)` | `vendor.go:90-150` | Parse + verify an Intel SGX DCAP v3 quote; returns MRENCLAVE (32B). |
|
||||
| `VerifyTDX(...)` | `vendor.go:166-220` | Parse + verify an Intel TDX DCAP v4 quote; returns MRTD (48B). |
|
||||
| `VerifySNP(...)` | `vendor.go:237-269` | Parse + verify an AMD SEV-SNP report; returns measurement (48B). |
|
||||
| `VendorSGX / VendorTDX / VendorSNP` | `vendor.go:29-33` | String vendor tags. |
|
||||
| `ErrChain / ErrSignature / ErrMeasurement / ErrBinding / ErrFormat` | `attestation.go:40-46` | Distinguishable verification failures. |
|
||||
| `ErrQuoteLayout` | `vendor.go:35` | Wire-format-level parse failure (short buffer, bad r‖s width). |
|
||||
|
||||
## Vendor integrations
|
||||
|
||||
`vendor.go` implements the three production TEE quote formats directly (no
|
||||
third-party dependency, no CGO). Offsets follow the Intel SGX/TDX DCAP quote
|
||||
spec and the AMD SEV-SNP firmware ABI (`vendor.go:11-13`):
|
||||
|
||||
- **Intel SGX DCAP v3** — `Header(48) ‖ ReportBody(384) ‖ sigDataLen(4) ‖
|
||||
sigData`. MRENCLAVE at absolute offset 112; report_data at 368. ECDSA-P256
|
||||
over SHA-256. The QE report binds the attestation key via
|
||||
`report_data = sha256(attestPub ‖ authData)` (`vendor.go:70-150`).
|
||||
- **Intel TDX DCAP v4** — `Header(48) ‖ TDReport(584) ‖ sigDataLen(4) ‖
|
||||
sigData`. MRTD at 184; report_data at 568. Same sigData shape as SGX;
|
||||
ECDSA-P256/SHA-256 (`vendor.go:152-220`).
|
||||
- **AMD SEV-SNP** — 1184-byte attestation report. report_data at `0x50`;
|
||||
measurement at `0x90`; signature at `0x2A0` as raw little-endian r‖s (72
|
||||
bytes each). ECDSA-P384 over SHA-384. VCEK leaf must be supplied by the
|
||||
caller and chain to a pinned AMD root (ARK/ASK) (`vendor.go:222-269`).
|
||||
|
||||
Signature scalars for SGX/TDX ship as raw big-endian r‖s; SNP ships them
|
||||
little-endian. Both are re-encoded to ASN.1 for `crypto/ecdsa`
|
||||
(`vendor.go:37-46`, `vendor.go:245-251`, `vendor.go:290-297`).
|
||||
|
||||
## Security model
|
||||
|
||||
Trust root is a **pinned** `*x509.CertPool` supplied by the caller — typically
|
||||
the Intel SGX/TDX root and/or AMD ARK/ASK. Verification is all-or-nothing: a
|
||||
quote with a valid measurement but a bad signature, or a valid signature
|
||||
under an unpinned chain, is rejected (`attestation.go:63-65`).
|
||||
|
||||
The four checks (`attestation.go:66-109`) are:
|
||||
|
||||
1. **Chain** — attestation-key certificate chains to a pinned root
|
||||
(`ErrChain`).
|
||||
2. **Signature** — ECDSA signature over the exact signed preimage verifies
|
||||
under the attestation key (`ErrSignature`). Preimage is
|
||||
`domain ‖ Measurement ‖ ReportData` where `domain = "hanzo/poi/tee-quote/v1"`
|
||||
for the canonical shape (`attestation.go:22-23`, `attestation.go:49-55`);
|
||||
for vendor-native quotes, the signed region is the header + report body
|
||||
verbatim (`vendor.go:99`, `vendor.go:175`, `vendor.go:243`).
|
||||
3. **Measurement** — `MRENCLAVE` / `MRTD` on the caller's allow-list
|
||||
(`ErrMeasurement`).
|
||||
4. **Binding** — `report_data[0:32] == sha256(boundKey)` (`ErrBinding`).
|
||||
This is the load-bearing link to `promptseal`: it proves the sealed prompt
|
||||
can only be opened inside the attested enclave (`attestation.go:1-9`).
|
||||
|
||||
**Replay guards** are out of scope. `Verify` is stateless; nonce / freshness
|
||||
handling belongs to the caller (typically a challenge in `boundKey` derivation
|
||||
or an outer transcript).
|
||||
|
||||
Off-curve points are rejected before use (`vendor.go:341-352`), and byte
|
||||
comparisons for the key-binding check use `equalFixed` — a constant-time
|
||||
XOR reduction (`vendor.go:279-288`).
|
||||
|
||||
## Production status
|
||||
|
||||
**Real implementation.** Not a scaffolding stub.
|
||||
|
||||
- `attestation.go` (109 lines) — canonical `Verify` performs all four checks
|
||||
with no bypass; each has a specific error and a dedicated code path
|
||||
(`attestation.go:74-107`).
|
||||
- `vendor.go` (369 lines) — three production TEE quote parsers with real
|
||||
spec-derived byte offsets (`vendor.go:82-87`, `vendor.go:158-163`,
|
||||
`vendor.go:227-233`), raw-r‖s → ASN.1 re-encoding for `crypto/ecdsa`
|
||||
(`vendor.go:37-46`), and P-256 / P-384 dispatch by vendor
|
||||
(`vendor.go:137`, `vendor.go:257`).
|
||||
- `attestation_test.go` (196 lines) + `vendor_test.go` (252 lines) — tests
|
||||
cover chain failure, bad signature, disallowed measurement, and wrong
|
||||
binding for each vendor path.
|
||||
|
||||
Verified at commit HEAD on the `docs/attestation-readme` branch base.
|
||||
|
||||
## References
|
||||
|
||||
- Adjacent to **LP-5300 / LP-5301** (Proof-of-Thought / Proof-of-Inference
|
||||
receipts) insofar as PoT is a form of computation attestation: an enclave
|
||||
attestation is the hardware-rooted variant of the same "I ran this code"
|
||||
claim that PoT expresses at the LP layer.
|
||||
- The `domain = "hanzo/poi/tee-quote/v1"` tag (`attestation.go:23`) and the
|
||||
package doc's promptseal reference point to the Hanzo Proof-of-Inference
|
||||
pipeline: attested-TEE decryption is how PoI achieves non-custodial
|
||||
delegated operation.
|
||||
- Intel SGX / TDX DCAP quote spec (Intel).
|
||||
- AMD SEV-SNP firmware ABI (AMD).
|
||||
@@ -0,0 +1,109 @@
|
||||
// Package attestation verifies a TEE remote-attestation quote so an operator's claim — "I run the
|
||||
// expected code inside a genuine enclave that decrypts only in-boundary" — is CRYPTOGRAPHICALLY
|
||||
// checkable. It closes the audit's TEE finding (the prior verifier checked only a buffer length and
|
||||
// a measurement byte-compare): here a quote verifies iff (1) the attestation key's certificate
|
||||
// chains to a pinned vendor root, (2) the ECDSA signature over the report is valid under that key,
|
||||
// (3) the enclave measurement is on the allow-list, and (4) the report binds the operator's
|
||||
// confidentiality public key. (4) is the load-bearing link to promptseal: it proves the sealed
|
||||
// prompt can be opened ONLY inside the attested enclave, making delegated operation non-custodial
|
||||
// end to end.
|
||||
//
|
||||
// The byte layout of a real Intel SGX/TDX DCAP or AMD SEV-SNP quote maps into the canonical Quote
|
||||
// below (platform-specific parsers are a thin shim); this package owns the SOUNDNESS.
|
||||
package attestation
|
||||
|
||||
import (
|
||||
"crypto/ecdsa"
|
||||
"crypto/sha256"
|
||||
"crypto/x509"
|
||||
"errors"
|
||||
)
|
||||
|
||||
// domain separates the report-signing preimage from any other signature in the stack.
|
||||
var domain = []byte("hanzo/poi/tee-quote/v1")
|
||||
|
||||
// Quote is a canonical TEE attestation quote.
|
||||
type Quote struct {
|
||||
Measurement [32]byte // MRENCLAVE / MRTD — identifies the code running in the enclave
|
||||
ReportData [32]byte // application binding: sha256(operator confidentiality public key)
|
||||
LeafDER []byte // the attestation key certificate (DER); its ECDSA key signs the report
|
||||
Chain [][]byte // intermediate certificates (DER), leaf → … → a root in the policy
|
||||
Signature []byte // ASN.1 ECDSA-P256 signature over signedBody(Measurement‖ReportData)
|
||||
}
|
||||
|
||||
// Policy is what a verifier trusts: the pinned vendor root(s) and the accepted enclave measurements.
|
||||
type Policy struct {
|
||||
Roots *x509.CertPool
|
||||
AllowedMeasure map[[32]byte]bool
|
||||
}
|
||||
|
||||
var (
|
||||
ErrChain = errors.New("attestation: certificate chain does not verify to a pinned root")
|
||||
ErrSignature = errors.New("attestation: report signature invalid")
|
||||
ErrMeasurement = errors.New("attestation: enclave measurement not on the allow-list")
|
||||
ErrBinding = errors.New("attestation: report does not bind the operator's confidentiality key")
|
||||
ErrFormat = errors.New("attestation: malformed quote")
|
||||
)
|
||||
|
||||
// signedBody is the exact preimage the attestation key signs.
|
||||
func signedBody(m, rd [32]byte) []byte {
|
||||
b := make([]byte, 0, len(domain)+64)
|
||||
b = append(b, domain...)
|
||||
b = append(b, m[:]...)
|
||||
b = append(b, rd[:]...)
|
||||
return b
|
||||
}
|
||||
|
||||
// BindKey is the value an enclave must place in ReportData to bind a confidentiality key.
|
||||
func BindKey(operatorPub []byte) [32]byte {
|
||||
return sha256.Sum256(operatorPub)
|
||||
}
|
||||
|
||||
// Verify checks `q` against `policy` and binds it to `boundKey` (the operator's confidentiality
|
||||
// public key, e.g. its promptseal key). Returns nil iff all four checks pass; a specific error
|
||||
// otherwise. NO check is skipped: a quote with a valid measurement but a bad signature or an
|
||||
// unpinned chain is rejected.
|
||||
func Verify(q *Quote, policy *Policy, boundKey []byte) error {
|
||||
if q == nil || policy == nil || policy.Roots == nil {
|
||||
return ErrFormat
|
||||
}
|
||||
leaf, err := x509.ParseCertificate(q.LeafDER)
|
||||
if err != nil {
|
||||
return ErrFormat
|
||||
}
|
||||
// (1) the attestation key cert chains to a PINNED root.
|
||||
inter := x509.NewCertPool()
|
||||
for _, d := range q.Chain {
|
||||
c, err := x509.ParseCertificate(d)
|
||||
if err != nil {
|
||||
return ErrFormat
|
||||
}
|
||||
inter.AddCert(c)
|
||||
}
|
||||
if _, err := leaf.Verify(x509.VerifyOptions{
|
||||
Roots: policy.Roots,
|
||||
Intermediates: inter,
|
||||
KeyUsages: []x509.ExtKeyUsage{x509.ExtKeyUsageAny},
|
||||
}); err != nil {
|
||||
return ErrChain
|
||||
}
|
||||
// (2) the report SIGNATURE verifies under the attestation key.
|
||||
pub, ok := leaf.PublicKey.(*ecdsa.PublicKey)
|
||||
if !ok {
|
||||
return ErrFormat
|
||||
}
|
||||
digest := sha256.Sum256(signedBody(q.Measurement, q.ReportData))
|
||||
if !ecdsa.VerifyASN1(pub, digest[:], q.Signature) {
|
||||
return ErrSignature
|
||||
}
|
||||
// (3) the enclave MEASUREMENT is accepted (the enclave runs the expected code).
|
||||
if !policy.AllowedMeasure[q.Measurement] {
|
||||
return ErrMeasurement
|
||||
}
|
||||
// (4) the report BINDS the operator's confidentiality key — so only this attested enclave can
|
||||
// open prompts sealed to that key.
|
||||
if q.ReportData != BindKey(boundKey) {
|
||||
return ErrBinding
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,196 @@
|
||||
package attestation
|
||||
|
||||
import (
|
||||
"crypto/ecdsa"
|
||||
"crypto/elliptic"
|
||||
"crypto/rand"
|
||||
"crypto/sha256"
|
||||
"crypto/x509"
|
||||
"crypto/x509/pkix"
|
||||
"math/big"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
// genCA makes a self-signed ECDSA-P256 root CA.
|
||||
func genCA(t *testing.T, cn string) (*x509.Certificate, *ecdsa.PrivateKey) {
|
||||
t.Helper()
|
||||
key, _ := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
|
||||
tmpl := &x509.Certificate{
|
||||
SerialNumber: big.NewInt(1),
|
||||
Subject: pkix.Name{CommonName: cn},
|
||||
NotBefore: time.Now().Add(-time.Hour),
|
||||
NotAfter: time.Now().Add(time.Hour),
|
||||
IsCA: true,
|
||||
KeyUsage: x509.KeyUsageCertSign,
|
||||
BasicConstraintsValid: true,
|
||||
}
|
||||
der, err := x509.CreateCertificate(rand.Reader, tmpl, tmpl, &key.PublicKey, key)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
cert, _ := x509.ParseCertificate(der)
|
||||
return cert, key
|
||||
}
|
||||
|
||||
// genLeaf makes an attestation-key leaf cert signed by the root.
|
||||
func genLeaf(t *testing.T, root *x509.Certificate, rootKey *ecdsa.PrivateKey) (*ecdsa.PrivateKey, []byte) {
|
||||
t.Helper()
|
||||
key, _ := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
|
||||
tmpl := &x509.Certificate{
|
||||
SerialNumber: big.NewInt(2),
|
||||
Subject: pkix.Name{CommonName: "attestation-key"},
|
||||
NotBefore: time.Now().Add(-time.Hour),
|
||||
NotAfter: time.Now().Add(time.Hour),
|
||||
KeyUsage: x509.KeyUsageDigitalSignature,
|
||||
}
|
||||
der, err := x509.CreateCertificate(rand.Reader, tmpl, root, &key.PublicKey, rootKey)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return key, der
|
||||
}
|
||||
|
||||
// genCAWithKey makes a self-signed CA using a caller-supplied key (any curve — P-384 for AMD).
|
||||
func genCAWithKey(t *testing.T, cn string, key *ecdsa.PrivateKey) (*x509.Certificate, []byte) {
|
||||
t.Helper()
|
||||
tmpl := &x509.Certificate{
|
||||
SerialNumber: big.NewInt(10), Subject: pkix.Name{CommonName: cn},
|
||||
NotBefore: time.Now().Add(-time.Hour), NotAfter: time.Now().Add(time.Hour),
|
||||
IsCA: true, KeyUsage: x509.KeyUsageCertSign, BasicConstraintsValid: true,
|
||||
}
|
||||
der, err := x509.CreateCertificate(rand.Reader, tmpl, tmpl, &key.PublicKey, key)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
cert, _ := x509.ParseCertificate(der)
|
||||
return cert, der
|
||||
}
|
||||
|
||||
// genLeafWithKey makes a leaf cert for leafKey signed by root/rootKey (any curve).
|
||||
func genLeafWithKey(t *testing.T, root *x509.Certificate, rootKey, leafKey *ecdsa.PrivateKey) []byte {
|
||||
t.Helper()
|
||||
tmpl := &x509.Certificate{
|
||||
SerialNumber: big.NewInt(11), Subject: pkix.Name{CommonName: "leaf"},
|
||||
NotBefore: time.Now().Add(-time.Hour), NotAfter: time.Now().Add(time.Hour),
|
||||
KeyUsage: x509.KeyUsageDigitalSignature,
|
||||
}
|
||||
der, err := x509.CreateCertificate(rand.Reader, tmpl, root, &leafKey.PublicKey, rootKey)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return der
|
||||
}
|
||||
|
||||
func makeQuote(attestKey *ecdsa.PrivateKey, leafDER []byte, m [32]byte, operatorPub []byte) *Quote {
|
||||
rd := BindKey(operatorPub)
|
||||
digest := sha256.Sum256(signedBody(m, rd))
|
||||
sig, _ := ecdsa.SignASN1(rand.Reader, attestKey, digest[:])
|
||||
return &Quote{Measurement: m, ReportData: rd, LeafDER: leafDER, Signature: sig}
|
||||
}
|
||||
|
||||
// the happy path: a genuine quote from an enclave running allow-listed code, bound to the operator's
|
||||
// confidentiality key, verifies.
|
||||
func TestVerify_GenuineQuote(t *testing.T) {
|
||||
root, rootKey := genCA(t, "vendor-root")
|
||||
attestKey, leafDER := genLeaf(t, root, rootKey)
|
||||
measurement := sha256.Sum256([]byte("the expected enclave image"))
|
||||
operatorPub := []byte("operator-confidentiality-pubkey")
|
||||
|
||||
roots := x509.NewCertPool()
|
||||
roots.AddCert(root)
|
||||
policy := &Policy{Roots: roots, AllowedMeasure: map[[32]byte]bool{measurement: true}}
|
||||
|
||||
q := makeQuote(attestKey, leafDER, measurement, operatorPub)
|
||||
if err := Verify(q, policy, operatorPub); err != nil {
|
||||
t.Fatalf("a genuine quote must verify, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// ADVERSARIAL: a forged signature is rejected (the prior stub didn't check this at all).
|
||||
func TestVerify_BadSignatureRejected(t *testing.T) {
|
||||
root, rootKey := genCA(t, "vendor-root")
|
||||
attestKey, leafDER := genLeaf(t, root, rootKey)
|
||||
m := sha256.Sum256([]byte("img"))
|
||||
op := []byte("opkey")
|
||||
roots := x509.NewCertPool()
|
||||
roots.AddCert(root)
|
||||
policy := &Policy{Roots: roots, AllowedMeasure: map[[32]byte]bool{m: true}}
|
||||
|
||||
q := makeQuote(attestKey, leafDER, m, op)
|
||||
q.Signature[len(q.Signature)-1] ^= 0x01 // flip a signature byte
|
||||
if err := Verify(q, policy, op); err != ErrSignature {
|
||||
t.Fatalf("a tampered signature must be ErrSignature, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// ADVERSARIAL — THE KEY ONE: an attacker mints a perfectly-formed quote (own root, valid sig,
|
||||
// correct measurement, correct binding), but its root is NOT the pinned vendor root → rejected.
|
||||
func TestVerify_UnpinnedRootRejected(t *testing.T) {
|
||||
pinnedRoot, _ := genCA(t, "vendor-root")
|
||||
attackerRoot, attackerKey := genCA(t, "attacker-root")
|
||||
attestKey, leafDER := genLeaf(t, attackerRoot, attackerKey) // chains to the ATTACKER root
|
||||
m := sha256.Sum256([]byte("img"))
|
||||
op := []byte("opkey")
|
||||
|
||||
roots := x509.NewCertPool()
|
||||
roots.AddCert(pinnedRoot) // only the genuine vendor root is pinned
|
||||
policy := &Policy{Roots: roots, AllowedMeasure: map[[32]byte]bool{m: true}}
|
||||
|
||||
q := makeQuote(attestKey, leafDER, m, op)
|
||||
if err := Verify(q, policy, op); err != ErrChain {
|
||||
t.Fatalf("a quote not chaining to a pinned root must be ErrChain, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// ADVERSARIAL: a genuine quote for a DIFFERENT (not allow-listed) enclave image is rejected — the
|
||||
// operator cannot swap in unattested code.
|
||||
func TestVerify_WrongMeasurementRejected(t *testing.T) {
|
||||
root, rootKey := genCA(t, "vendor-root")
|
||||
attestKey, leafDER := genLeaf(t, root, rootKey)
|
||||
expected := sha256.Sum256([]byte("expected"))
|
||||
actual := sha256.Sum256([]byte("some other code")) // validly signed, but not allow-listed
|
||||
op := []byte("opkey")
|
||||
roots := x509.NewCertPool()
|
||||
roots.AddCert(root)
|
||||
policy := &Policy{Roots: roots, AllowedMeasure: map[[32]byte]bool{expected: true}}
|
||||
|
||||
q := makeQuote(attestKey, leafDER, actual, op)
|
||||
if err := Verify(q, policy, op); err != ErrMeasurement {
|
||||
t.Fatalf("a non-allow-listed measurement must be ErrMeasurement, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// ADVERSARIAL: a genuine quote that binds operator A's key cannot vouch for operator B — so a sealed
|
||||
// prompt for A cannot be opened by an enclave attested for B.
|
||||
func TestVerify_UnboundKeyRejected(t *testing.T) {
|
||||
root, rootKey := genCA(t, "vendor-root")
|
||||
attestKey, leafDER := genLeaf(t, root, rootKey)
|
||||
m := sha256.Sum256([]byte("img"))
|
||||
roots := x509.NewCertPool()
|
||||
roots.AddCert(root)
|
||||
policy := &Policy{Roots: roots, AllowedMeasure: map[[32]byte]bool{m: true}}
|
||||
|
||||
q := makeQuote(attestKey, leafDER, m, []byte("operator-A-key"))
|
||||
if err := Verify(q, policy, []byte("operator-B-key")); err != ErrBinding {
|
||||
t.Fatalf("a quote bound to a different key must be ErrBinding, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// ADVERSARIAL: tampering the measurement after signing breaks the signature (you cannot keep a valid
|
||||
// sig while changing what was attested).
|
||||
func TestVerify_TamperedMeasurementBreaksSignature(t *testing.T) {
|
||||
root, rootKey := genCA(t, "vendor-root")
|
||||
attestKey, leafDER := genLeaf(t, root, rootKey)
|
||||
m := sha256.Sum256([]byte("img"))
|
||||
op := []byte("opkey")
|
||||
roots := x509.NewCertPool()
|
||||
roots.AddCert(root)
|
||||
policy := &Policy{Roots: roots, AllowedMeasure: map[[32]byte]bool{m: true, {}: true}}
|
||||
|
||||
q := makeQuote(attestKey, leafDER, m, op)
|
||||
q.Measurement = [32]byte{} // change the attested code after signing
|
||||
if err := Verify(q, policy, op); err != ErrSignature {
|
||||
t.Fatalf("changing the measurement after signing must be ErrSignature, got %v", err)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,369 @@
|
||||
package attestation
|
||||
|
||||
// vendor.go — byte-layout parsers for the three production TEE quote formats: Intel SGX (DCAP v3),
|
||||
// Intel TDX (DCAP v4), and AMD SEV-SNP. Each parser decodes the documented binary structure into the
|
||||
// canonical fields (measurement, report data, the signed region, the attestation signature, and the
|
||||
// embedded certificate chain) and verifies them: the attestation signature over the exact signed
|
||||
// bytes, the certificate chain to a PINNED vendor root, the measurement allow-list, and the binding
|
||||
// of the operator's confidentiality key in the report data. This is the real wire format the audit's
|
||||
// stub never parsed.
|
||||
//
|
||||
// Offsets are from the Intel SGX/TDX DCAP quote spec and the AMD SEV-SNP firmware ABI. Signature
|
||||
// scalars in these quotes are raw big-endian r‖s; we re-encode to ASN.1 for crypto/ecdsa. SGX/TDX
|
||||
// use ECDSA-P256 over SHA-256; SEV-SNP uses ECDSA-P384 over SHA-384.
|
||||
|
||||
import (
|
||||
"crypto/ecdsa"
|
||||
"crypto/elliptic"
|
||||
"crypto/sha256"
|
||||
"crypto/sha512"
|
||||
"crypto/x509"
|
||||
"encoding/asn1"
|
||||
"encoding/binary"
|
||||
"encoding/pem"
|
||||
"errors"
|
||||
"hash"
|
||||
"math/big"
|
||||
)
|
||||
|
||||
const (
|
||||
VendorSGX = "sgx"
|
||||
VendorTDX = "tdx"
|
||||
VendorSNP = "sev-snp"
|
||||
)
|
||||
|
||||
var ErrQuoteLayout = errors.New("attestation: quote shorter than its declared layout")
|
||||
|
||||
// rawSigToASN1 re-encodes a fixed-width big-endian r‖s pair into the ASN.1 SEQUENCE crypto/ecdsa wants.
|
||||
func rawSigToASN1(rs []byte) ([]byte, error) {
|
||||
if len(rs)%2 != 0 || len(rs) == 0 {
|
||||
return nil, ErrQuoteLayout
|
||||
}
|
||||
half := len(rs) / 2
|
||||
r := new(big.Int).SetBytes(rs[:half])
|
||||
s := new(big.Int).SetBytes(rs[half:])
|
||||
return asn1.Marshal(struct{ R, S *big.Int }{r, s})
|
||||
}
|
||||
|
||||
// chainTo verifies a leaf DER cert chains through `inter` DERs to a root in `roots`.
|
||||
func chainTo(leafDER []byte, interDER [][]byte, roots *x509.CertPool) (*x509.Certificate, error) {
|
||||
leaf, err := x509.ParseCertificate(leafDER)
|
||||
if err != nil {
|
||||
return nil, ErrFormat
|
||||
}
|
||||
inter := x509.NewCertPool()
|
||||
for _, d := range interDER {
|
||||
c, err := x509.ParseCertificate(d)
|
||||
if err != nil {
|
||||
return nil, ErrFormat
|
||||
}
|
||||
inter.AddCert(c)
|
||||
}
|
||||
if _, err := leaf.Verify(x509.VerifyOptions{
|
||||
Roots: roots, Intermediates: inter, KeyUsages: []x509.ExtKeyUsage{x509.ExtKeyUsageAny},
|
||||
}); err != nil {
|
||||
return nil, ErrChain
|
||||
}
|
||||
return leaf, nil
|
||||
}
|
||||
|
||||
// --- Intel SGX DCAP v3 ----------------------------------------------------------------------------
|
||||
//
|
||||
// Layout: Header(48) ‖ ReportBody(384) ‖ sigDataLen(4) ‖ sigData.
|
||||
// Header: version u16le @0; tee_type u32le @4 (0 = SGX).
|
||||
// ReportBody: mr_enclave[32] @ +64; report_data[64] @ +320. (absolute @112 and @368)
|
||||
// sigData: quoteSig[64] (r‖s) ‖ attestPub[64] (raw P256 point) ‖ qeReport[384] ‖
|
||||
// qeReportSig[64] ‖ authDataLen u16 ‖ authData ‖ certDataType u16 ‖ certDataLen u32 ‖
|
||||
// certData(PEM PCK chain).
|
||||
// Trust: attestPub signs Header‖ReportBody (the quote); attestPub is bound in qeReport.report_data
|
||||
// = sha256(attestPub ‖ authData); the PCK leaf (from certData) signs qeReport and chains to the
|
||||
// pinned Intel SGX root.
|
||||
const (
|
||||
sgxHeaderLen = 48
|
||||
sgxBodyLen = 384
|
||||
sgxMREnclave = sgxHeaderLen + 64 // 112
|
||||
sgxReportDat = sgxHeaderLen + 320 // 368
|
||||
sgxSigOff = sgxHeaderLen + sgxBodyLen + 4 // 436
|
||||
)
|
||||
|
||||
// VerifySGX parses and verifies an Intel SGX DCAP quote, returning the verified MRENCLAVE.
|
||||
func VerifySGX(raw []byte, roots *x509.CertPool, allowed map[[32]byte]bool, boundKey []byte) ([]byte, error) {
|
||||
if len(raw) < sgxSigOff+64+64+sgxBodyLen+64+2 {
|
||||
return nil, ErrQuoteLayout
|
||||
}
|
||||
if binary.LittleEndian.Uint16(raw[0:2]) != 3 || binary.LittleEndian.Uint32(raw[4:8]) != 0 {
|
||||
return nil, errors.New("attestation: not an SGX v3 quote")
|
||||
}
|
||||
mr := raw[sgxMREnclave : sgxMREnclave+32]
|
||||
reportData := raw[sgxReportDat : sgxReportDat+64]
|
||||
signedRegion := raw[:sgxHeaderLen+sgxBodyLen] // header ‖ report body
|
||||
|
||||
p := sgxSigOff
|
||||
quoteSig := raw[p : p+64]
|
||||
attestPub := raw[p+64 : p+128]
|
||||
qeReport := raw[p+128 : p+128+sgxBodyLen]
|
||||
rest := raw[p+128+sgxBodyLen:]
|
||||
qeReportSig := rest[:64]
|
||||
authLen := int(binary.LittleEndian.Uint16(rest[64:66]))
|
||||
if len(rest) < 66+authLen+6 {
|
||||
return nil, ErrQuoteLayout
|
||||
}
|
||||
authData := rest[66 : 66+authLen]
|
||||
cd := rest[66+authLen:]
|
||||
// certData: type u16 ‖ len u32 ‖ data
|
||||
certLen := int(binary.LittleEndian.Uint32(cd[2:6]))
|
||||
if len(cd) < 6+certLen {
|
||||
return nil, ErrQuoteLayout
|
||||
}
|
||||
pckChain := splitPEMChain(cd[6 : 6+certLen])
|
||||
if len(pckChain) == 0 {
|
||||
return nil, ErrFormat
|
||||
}
|
||||
|
||||
// (a) PCK leaf chains to the pinned Intel root, and signs the QE report.
|
||||
pckLeaf, err := chainTo(pckChain[0], pckChain[1:], roots)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := ecdsaVerifyRaw(pckLeaf, sha256.New, qeReport, qeReportSig); err != nil {
|
||||
return nil, ErrSignature
|
||||
}
|
||||
// (b) the attestation key is bound in the QE report: report_data = sha256(attestPub ‖ authData).
|
||||
want := sha256.Sum256(append(append([]byte(nil), attestPub...), authData...))
|
||||
if !equalFixed(qeReport[320:352], want[:]) {
|
||||
return nil, errors.New("attestation: attestation key not bound in QE report")
|
||||
}
|
||||
// (c) the attestation key signs the quote (header ‖ report body).
|
||||
if err := ecdsaVerifyRawKey(attestPub, sha256.New, signedRegion, quoteSig); err != nil {
|
||||
return nil, ErrSignature
|
||||
}
|
||||
// (d) policy: measurement allow-list + operator-key binding (report_data[0:32]).
|
||||
var m32 [32]byte
|
||||
copy(m32[:], mr)
|
||||
if !allowed[m32] {
|
||||
return nil, ErrMeasurement
|
||||
}
|
||||
if !bindsKey(reportData, boundKey) {
|
||||
return nil, ErrBinding
|
||||
}
|
||||
return mr, nil
|
||||
}
|
||||
|
||||
// --- Intel TDX DCAP v4 ----------------------------------------------------------------------------
|
||||
//
|
||||
// Layout: Header(48) ‖ TDReport(584) ‖ sigDataLen(4) ‖ sigData (same sigData shape as SGX).
|
||||
// Header: version u16le @0 (=4); tee_type u32le @4 (=0x81 for TDX).
|
||||
// TDReport: mr_td[48] @ +136; report_data[64] @ +520. (absolute @184 and @568)
|
||||
const (
|
||||
tdxHeaderLen = 48
|
||||
tdxBodyLen = 584
|
||||
tdxMRTD = tdxHeaderLen + 136 // 184
|
||||
tdxReportDat = tdxHeaderLen + 520 // 568
|
||||
tdxSigOff = tdxHeaderLen + tdxBodyLen + 4
|
||||
)
|
||||
|
||||
// VerifyTDX parses and verifies an Intel TDX DCAP quote, returning the verified MRTD.
|
||||
func VerifyTDX(raw []byte, roots *x509.CertPool, allowed map[[48]byte]bool, boundKey []byte) ([]byte, error) {
|
||||
if len(raw) < tdxSigOff+64+64+sgxBodyLen+64+2 {
|
||||
return nil, ErrQuoteLayout
|
||||
}
|
||||
if binary.LittleEndian.Uint16(raw[0:2]) != 4 || binary.LittleEndian.Uint32(raw[4:8]) != 0x81 {
|
||||
return nil, errors.New("attestation: not a TDX v4 quote")
|
||||
}
|
||||
mr := raw[tdxMRTD : tdxMRTD+48]
|
||||
reportData := raw[tdxReportDat : tdxReportDat+64]
|
||||
signedRegion := raw[:tdxHeaderLen+tdxBodyLen]
|
||||
|
||||
p := tdxSigOff
|
||||
quoteSig := raw[p : p+64]
|
||||
attestPub := raw[p+64 : p+128]
|
||||
qeReport := raw[p+128 : p+128+sgxBodyLen]
|
||||
rest := raw[p+128+sgxBodyLen:]
|
||||
qeReportSig := rest[:64]
|
||||
authLen := int(binary.LittleEndian.Uint16(rest[64:66]))
|
||||
if len(rest) < 66+authLen+6 {
|
||||
return nil, ErrQuoteLayout
|
||||
}
|
||||
authData := rest[66 : 66+authLen]
|
||||
cd := rest[66+authLen:]
|
||||
certLen := int(binary.LittleEndian.Uint32(cd[2:6]))
|
||||
if len(cd) < 6+certLen {
|
||||
return nil, ErrQuoteLayout
|
||||
}
|
||||
pckChain := splitPEMChain(cd[6 : 6+certLen])
|
||||
if len(pckChain) == 0 {
|
||||
return nil, ErrFormat
|
||||
}
|
||||
pckLeaf, err := chainTo(pckChain[0], pckChain[1:], roots)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := ecdsaVerifyRaw(pckLeaf, sha256.New, qeReport, qeReportSig); err != nil {
|
||||
return nil, ErrSignature
|
||||
}
|
||||
want := sha256.Sum256(append(append([]byte(nil), attestPub...), authData...))
|
||||
if !equalFixed(qeReport[320:352], want[:]) {
|
||||
return nil, errors.New("attestation: attestation key not bound in QE report")
|
||||
}
|
||||
if err := ecdsaVerifyRawKey(attestPub, sha256.New, signedRegion, quoteSig); err != nil {
|
||||
return nil, ErrSignature
|
||||
}
|
||||
var m48 [48]byte
|
||||
copy(m48[:], mr)
|
||||
if !allowed[m48] {
|
||||
return nil, ErrMeasurement
|
||||
}
|
||||
if !bindsKey(reportData, boundKey) {
|
||||
return nil, ErrBinding
|
||||
}
|
||||
return mr, nil
|
||||
}
|
||||
|
||||
// --- AMD SEV-SNP ----------------------------------------------------------------------------------
|
||||
//
|
||||
// Layout: attestation_report (1184 bytes). report_data[64] @0x50; measurement[48] @0x90;
|
||||
// signature @0x2A0 (ECDSA-P384: r[72]‖s[72], little-endian, zero-padded). The VCEK (P-384) signs
|
||||
// report[0:0x2A0]; the VCEK cert chains to the pinned AMD ARK/ASK (supplied out of band).
|
||||
const (
|
||||
snpReportLen = 1184
|
||||
snpReportData = 0x50 // 80
|
||||
snpMeasure = 0x90 // 144
|
||||
snpSigOff = 0x2A0 // 672
|
||||
snpSigComp = 72 // r and s are each 72 bytes, little-endian
|
||||
)
|
||||
|
||||
// VerifySNP parses and verifies an AMD SEV-SNP attestation report. `vcekDER` is the VCEK leaf cert
|
||||
// (fetched from the AMD KDS); it must chain to a pinned AMD root in `roots`. Returns the measurement.
|
||||
func VerifySNP(raw, vcekDER []byte, askDER [][]byte, roots *x509.CertPool, allowed map[[48]byte]bool, boundKey []byte) ([]byte, error) {
|
||||
if len(raw) < snpReportLen {
|
||||
return nil, ErrQuoteLayout
|
||||
}
|
||||
mr := raw[snpMeasure : snpMeasure+48]
|
||||
reportData := raw[snpReportData : snpReportData+64]
|
||||
signedRegion := raw[:snpSigOff]
|
||||
|
||||
// SNP stores r,s little-endian; convert to big-endian for ASN.1.
|
||||
rLE := raw[snpSigOff : snpSigOff+snpSigComp]
|
||||
sLE := raw[snpSigOff+snpSigComp : snpSigOff+2*snpSigComp]
|
||||
sigASN1, err := asn1.Marshal(struct{ R, S *big.Int }{leUint(rLE), leUint(sLE)})
|
||||
if err != nil {
|
||||
return nil, ErrFormat
|
||||
}
|
||||
|
||||
vcek, err := chainTo(vcekDER, askDER, roots)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := ecdsaVerifyASN1(vcek, sha512.New384, signedRegion, sigASN1); err != nil {
|
||||
return nil, ErrSignature
|
||||
}
|
||||
var m48 [48]byte
|
||||
copy(m48[:], mr)
|
||||
if !allowed[m48] {
|
||||
return nil, ErrMeasurement
|
||||
}
|
||||
if !bindsKey(reportData, boundKey) {
|
||||
return nil, ErrBinding
|
||||
}
|
||||
return mr, nil
|
||||
}
|
||||
|
||||
// --- shared helpers -------------------------------------------------------------------------------
|
||||
|
||||
// bindsKey reports whether the report data's first 32 bytes are sha256(key) — the operator binding.
|
||||
func bindsKey(reportData, key []byte) bool {
|
||||
bk := BindKey(key)
|
||||
return equalFixed(reportData[:32], bk[:])
|
||||
}
|
||||
|
||||
func equalFixed(a, b []byte) bool {
|
||||
if len(a) != len(b) {
|
||||
return false
|
||||
}
|
||||
var d byte
|
||||
for i := range a {
|
||||
d |= a[i] ^ b[i]
|
||||
}
|
||||
return d == 0
|
||||
}
|
||||
|
||||
// leUint reads a little-endian byte slice as a big.Int (SNP scalar encoding).
|
||||
func leUint(le []byte) *big.Int {
|
||||
be := make([]byte, len(le))
|
||||
for i := range le {
|
||||
be[len(le)-1-i] = le[i]
|
||||
}
|
||||
return new(big.Int).SetBytes(be)
|
||||
}
|
||||
|
||||
// ecdsaVerifyRaw verifies a raw (r‖s) signature over hash(msg) by a certificate's key.
|
||||
func ecdsaVerifyRaw(cert *x509.Certificate, newH func() hash.Hash, msg, rawSig []byte) error {
|
||||
sig, err := rawSigToASN1(rawSig)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return ecdsaVerifyASN1(cert, newH, msg, sig)
|
||||
}
|
||||
|
||||
// ecdsaVerifyASN1 verifies an ASN.1 signature over hash(msg) by a certificate's ECDSA key.
|
||||
func ecdsaVerifyASN1(cert *x509.Certificate, newH func() hash.Hash, msg, sig []byte) error {
|
||||
pub, ok := cert.PublicKey.(*ecdsa.PublicKey)
|
||||
if !ok {
|
||||
return ErrFormat
|
||||
}
|
||||
hh := newH()
|
||||
hh.Write(msg)
|
||||
if !ecdsa.VerifyASN1(pub, hh.Sum(nil), sig) {
|
||||
return ErrSignature
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ecdsaVerifyRawKey verifies a raw (r‖s) signature by a raw uncompressed P-256 point (the DCAP
|
||||
// attestation key, which travels in the quote as 64 bytes X‖Y rather than as a certificate).
|
||||
func ecdsaVerifyRawKey(rawPoint []byte, newH func() hash.Hash, msg, rawSig []byte) error {
|
||||
pub, err := uncompressedP256(rawPoint)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
sig, err := rawSigToASN1(rawSig)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
hh := newH()
|
||||
hh.Write(msg)
|
||||
if !ecdsa.VerifyASN1(pub, hh.Sum(nil), sig) {
|
||||
return ErrSignature
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// uncompressedP256 builds a P-256 public key from a 64-byte X‖Y point, rejecting off-curve points.
|
||||
func uncompressedP256(raw []byte) (*ecdsa.PublicKey, error) {
|
||||
if len(raw) != 64 {
|
||||
return nil, ErrFormat
|
||||
}
|
||||
x := new(big.Int).SetBytes(raw[:32])
|
||||
y := new(big.Int).SetBytes(raw[32:])
|
||||
if !elliptic.P256().IsOnCurve(x, y) {
|
||||
return nil, ErrFormat
|
||||
}
|
||||
return &ecdsa.PublicKey{Curve: elliptic.P256(), X: x, Y: y}, nil
|
||||
}
|
||||
|
||||
// splitPEMChain decodes a PEM bundle (the DCAP cert-data field) into a DER list, leaf first.
|
||||
func splitPEMChain(pemBytes []byte) [][]byte {
|
||||
var out [][]byte
|
||||
rest := pemBytes
|
||||
for {
|
||||
var b *pem.Block
|
||||
b, rest = pem.Decode(rest)
|
||||
if b == nil {
|
||||
break
|
||||
}
|
||||
if b.Type == "CERTIFICATE" {
|
||||
out = append(out, b.Bytes)
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
@@ -0,0 +1,252 @@
|
||||
package attestation
|
||||
|
||||
import (
|
||||
"crypto/ecdsa"
|
||||
"crypto/elliptic"
|
||||
"crypto/rand"
|
||||
"crypto/sha256"
|
||||
"crypto/sha512"
|
||||
"crypto/x509"
|
||||
"encoding/binary"
|
||||
"encoding/pem"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// --- helpers to build layout-accurate quotes (a test CA stands in for the pinned vendor root) ----
|
||||
|
||||
func pad(b []byte, n int) []byte {
|
||||
out := make([]byte, n)
|
||||
copy(out, b)
|
||||
return out
|
||||
}
|
||||
|
||||
func pad2(b []byte, n int) []byte {
|
||||
if len(b) >= n {
|
||||
return b[len(b)-n:]
|
||||
}
|
||||
out := make([]byte, n)
|
||||
copy(out[n-len(b):], b)
|
||||
return out
|
||||
}
|
||||
|
||||
// signP256Raw returns a 64-byte r‖s big-endian signature over hash(msg).
|
||||
func signP256Raw(t *testing.T, key *ecdsa.PrivateKey, msg []byte) []byte {
|
||||
t.Helper()
|
||||
d := sha256.Sum256(msg)
|
||||
r, s, err := ecdsa.Sign(rand.Reader, key, d[:])
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return append(pad2(r.Bytes(), 32), pad2(s.Bytes(), 32)...)
|
||||
}
|
||||
|
||||
// signP384RawLE returns r‖s as 72-byte little-endian scalars over sha384(msg) (SNP encoding).
|
||||
func signP384RawLE(t *testing.T, key *ecdsa.PrivateKey, msg []byte) []byte {
|
||||
t.Helper()
|
||||
d := sha512.Sum384(msg)
|
||||
r, s, err := ecdsa.Sign(rand.Reader, key, d[:])
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
toLE := func(x []byte) []byte {
|
||||
be := pad2(x, 72)
|
||||
le := make([]byte, 72)
|
||||
for i := range be {
|
||||
le[71-i] = be[i]
|
||||
}
|
||||
return le
|
||||
}
|
||||
return append(toLE(r.Bytes()), toLE(s.Bytes())...)
|
||||
}
|
||||
|
||||
// attestPubBytes is the 64-byte X‖Y form the DCAP attestation key travels as.
|
||||
func attestPubBytes(key *ecdsa.PrivateKey) []byte {
|
||||
return append(pad2(key.PublicKey.X.Bytes(), 32), pad2(key.PublicKey.Y.Bytes(), 32)...)
|
||||
}
|
||||
|
||||
func pemOf(ders ...[]byte) []byte {
|
||||
var out []byte
|
||||
for _, d := range ders {
|
||||
out = append(out, pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: d})...)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// buildSGXQuote assembles a layout-accurate SGX DCAP v3 quote signed by attestKey, with the QE
|
||||
// report signed by pckLeaf (chaining to root via pckDER‖rootDER).
|
||||
func buildSGXQuote(t *testing.T, attestKey, pckKey *ecdsa.PrivateKey, pckDER, rootDER []byte, mr [32]byte, boundKey []byte) []byte {
|
||||
header := make([]byte, sgxHeaderLen)
|
||||
binary.LittleEndian.PutUint16(header[0:2], 3) // version 3
|
||||
binary.LittleEndian.PutUint32(header[4:8], 0) // tee_type SGX
|
||||
|
||||
body := make([]byte, sgxBodyLen)
|
||||
copy(body[64:96], mr[:]) // mr_enclave
|
||||
{
|
||||
bk := BindKey(boundKey)
|
||||
copy(body[320:352], bk[:])
|
||||
} // report_data binds the operator key
|
||||
|
||||
attestPub := attestPubBytes(attestKey)
|
||||
authData := []byte("qe-auth")
|
||||
quoteSig := signP256Raw(t, attestKey, append(append([]byte(nil), header...), body...))
|
||||
|
||||
qe := make([]byte, sgxBodyLen)
|
||||
bind := sha256.Sum256(append(append([]byte(nil), attestPub...), authData...))
|
||||
copy(qe[320:352], bind[:]) // qe_report.report_data binds the attestation key
|
||||
qeSig := signP256Raw(t, pckKey, qe)
|
||||
|
||||
certData := pemOf(pckDER, rootDER)
|
||||
var q []byte
|
||||
q = append(q, header...)
|
||||
q = append(q, body...)
|
||||
q = append(q, pad(nil, 4)...) // sigDataLen (unused by the parser)
|
||||
q = append(q, quoteSig...)
|
||||
q = append(q, attestPub...)
|
||||
q = append(q, qe...)
|
||||
q = append(q, qeSig...)
|
||||
q = append(q, byte(len(authData)), 0) // authLen u16 LE
|
||||
q = append(q, authData...)
|
||||
q = append(q, 0, 0) // certDataType u16
|
||||
cl := make([]byte, 4)
|
||||
binary.LittleEndian.PutUint32(cl, uint32(len(certData)))
|
||||
q = append(q, cl...)
|
||||
q = append(q, certData...)
|
||||
return q
|
||||
}
|
||||
|
||||
func TestVerifySGX_Genuine(t *testing.T) {
|
||||
root, rootKey := genCA(t, "intel-sgx-root")
|
||||
pckKey, pckDER := genLeaf(t, root, rootKey)
|
||||
attestKey, _ := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
|
||||
mr := sha256.Sum256([]byte("enclave-image"))
|
||||
op := []byte("operator-key")
|
||||
rootDER := root.Raw
|
||||
|
||||
q := buildSGXQuote(t, attestKey, pckKey, pckDER, rootDER, mr, op)
|
||||
roots := x509.NewCertPool()
|
||||
roots.AddCert(root)
|
||||
got, err := VerifySGX(q, roots, map[[32]byte]bool{mr: true}, op)
|
||||
if err != nil {
|
||||
t.Fatalf("genuine SGX quote must verify: %v", err)
|
||||
}
|
||||
if [32]byte(got) != mr {
|
||||
t.Fatal("returned the wrong MRENCLAVE")
|
||||
}
|
||||
|
||||
// ADVERSARIAL: flip a measurement byte → the quote signature no longer covers it.
|
||||
bad := append([]byte(nil), q...)
|
||||
bad[sgxMREnclave] ^= 0x01
|
||||
if _, err := VerifySGX(bad, roots, map[[32]byte]bool{mr: true}, op); err == nil {
|
||||
t.Fatal("a tampered MRENCLAVE must be rejected")
|
||||
}
|
||||
// ADVERSARIAL: a different pinned root → chain fails.
|
||||
other, _ := genCA(t, "not-intel")
|
||||
op2 := x509.NewCertPool()
|
||||
op2.AddCert(other)
|
||||
if _, err := VerifySGX(q, op2, map[[32]byte]bool{mr: true}, op); err != ErrChain {
|
||||
t.Fatalf("a quote not chaining to the pinned root must be ErrChain, got %v", err)
|
||||
}
|
||||
// ADVERSARIAL: unbound key.
|
||||
if _, err := VerifySGX(q, roots, map[[32]byte]bool{mr: true}, []byte("other-op")); err != ErrBinding {
|
||||
t.Fatalf("a quote bound to a different key must be ErrBinding, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// buildTDXQuote assembles a layout-accurate Intel TDX DCAP v4 quote (SGX sigData shape, TDX dims).
|
||||
func buildTDXQuote(t *testing.T, attestKey, pckKey *ecdsa.PrivateKey, pckDER, rootDER []byte, mr [48]byte, boundKey []byte) []byte {
|
||||
header := make([]byte, tdxHeaderLen)
|
||||
binary.LittleEndian.PutUint16(header[0:2], 4) // version 4
|
||||
binary.LittleEndian.PutUint32(header[4:8], 0x81) // tee_type TDX
|
||||
body := make([]byte, tdxBodyLen)
|
||||
copy(body[136:184], mr[:]) // mr_td
|
||||
bk := BindKey(boundKey)
|
||||
copy(body[520:552], bk[:]) // report_data binds the operator key
|
||||
attestPub := attestPubBytes(attestKey)
|
||||
authData := []byte("qe-auth")
|
||||
quoteSig := signP256Raw(t, attestKey, append(append([]byte(nil), header...), body...))
|
||||
qe := make([]byte, sgxBodyLen)
|
||||
bind := sha256.Sum256(append(append([]byte(nil), attestPub...), authData...))
|
||||
copy(qe[320:352], bind[:])
|
||||
qeSig := signP256Raw(t, pckKey, qe)
|
||||
certData := pemOf(pckDER, rootDER)
|
||||
var q []byte
|
||||
q = append(q, header...)
|
||||
q = append(q, body...)
|
||||
q = append(q, pad(nil, 4)...)
|
||||
q = append(q, quoteSig...)
|
||||
q = append(q, attestPub...)
|
||||
q = append(q, qe...)
|
||||
q = append(q, qeSig...)
|
||||
q = append(q, byte(len(authData)), 0)
|
||||
q = append(q, authData...)
|
||||
q = append(q, 0, 0)
|
||||
cl := make([]byte, 4)
|
||||
binary.LittleEndian.PutUint32(cl, uint32(len(certData)))
|
||||
q = append(q, cl...)
|
||||
q = append(q, certData...)
|
||||
return q
|
||||
}
|
||||
|
||||
func TestVerifyTDX_Genuine(t *testing.T) {
|
||||
root, rootKey := genCA(t, "intel-tdx-root")
|
||||
pckKey, pckDER := genLeaf(t, root, rootKey)
|
||||
attestKey, _ := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
|
||||
var mr [48]byte
|
||||
tdImg := sha256.Sum256([]byte("td-image"))
|
||||
copy(mr[:], pad2(tdImg[:], 48))
|
||||
op := []byte("operator-key")
|
||||
q := buildTDXQuote(t, attestKey, pckKey, pckDER, root.Raw, mr, op)
|
||||
roots := x509.NewCertPool()
|
||||
roots.AddCert(root)
|
||||
if _, err := VerifyTDX(q, roots, map[[48]byte]bool{mr: true}, op); err != nil {
|
||||
t.Fatalf("genuine TDX quote must verify: %v", err)
|
||||
}
|
||||
bad := append([]byte(nil), q...)
|
||||
bad[tdxMRTD] ^= 0x01
|
||||
if _, err := VerifyTDX(bad, roots, map[[48]byte]bool{mr: true}, op); err == nil {
|
||||
t.Fatal("a tampered MRTD must be rejected")
|
||||
}
|
||||
}
|
||||
|
||||
// buildSNPReport assembles a layout-accurate SEV-SNP report signed by vcekKey (P-384).
|
||||
func buildSNPReport(t *testing.T, vcekKey *ecdsa.PrivateKey, mr []byte, boundKey []byte) []byte {
|
||||
r := make([]byte, snpReportLen)
|
||||
{
|
||||
bk := BindKey(boundKey)
|
||||
copy(r[snpReportData:snpReportData+32], bk[:])
|
||||
}
|
||||
copy(r[snpMeasure:snpMeasure+48], mr)
|
||||
sig := signP384RawLE(t, vcekKey, r[:snpSigOff])
|
||||
copy(r[snpSigOff:snpSigOff+144], sig)
|
||||
return r
|
||||
}
|
||||
|
||||
func TestVerifySNP_Genuine(t *testing.T) {
|
||||
// AMD root → VCEK leaf (P-384).
|
||||
rootKey, _ := ecdsa.GenerateKey(elliptic.P384(), rand.Reader)
|
||||
rootCert, rootDER := genCAWithKey(t, "amd-ark", rootKey)
|
||||
vcekKey, _ := ecdsa.GenerateKey(elliptic.P384(), rand.Reader)
|
||||
vcekDER := genLeafWithKey(t, rootCert, rootKey, vcekKey)
|
||||
_ = rootDER
|
||||
|
||||
snpImg := sha256.Sum256([]byte("snp-image"))
|
||||
mr := pad2(snpImg[:], 48)
|
||||
op := []byte("operator-key")
|
||||
report := buildSNPReport(t, vcekKey, mr, op)
|
||||
|
||||
roots := x509.NewCertPool()
|
||||
roots.AddCert(rootCert)
|
||||
got, err := VerifySNP(report, vcekDER, nil, roots, map[[48]byte]bool{[48]byte(mr): true}, op)
|
||||
if err != nil {
|
||||
t.Fatalf("genuine SNP report must verify: %v", err)
|
||||
}
|
||||
if string(got) != string(mr) {
|
||||
t.Fatal("returned the wrong measurement")
|
||||
}
|
||||
// ADVERSARIAL: tamper the measurement → P-384 signature fails.
|
||||
bad := append([]byte(nil), report...)
|
||||
bad[snpMeasure] ^= 0x01
|
||||
if _, err := VerifySNP(bad, vcekDER, nil, roots, map[[48]byte]bool{[48]byte(mr): true}, op); err == nil {
|
||||
t.Fatal("a tampered SNP measurement must be rejected")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,149 @@
|
||||
# cggmp21
|
||||
|
||||
Threshold-ECDSA protocol scaffolding for the CGGMP21 construction
|
||||
(Canetti, Gennaro, Goldfeder, Makriyannis, Peled, 2021 — *"UC
|
||||
Non-Interactive, Proactive, Threshold ECDSA with Identifiable
|
||||
Aborts"*, [eprint 2021/060](https://eprint.iacr.org/2021/060)).
|
||||
|
||||
CGGMP21 is Lux's canonical native-MPC construction for production
|
||||
signing paths (see the bridge policy in
|
||||
[luxfi/bridge#405](https://github.com/luxfi/bridge/pull/405): *"Production
|
||||
stays pure native MPC (CGGMP21 / FROST via luxfi/mpc)"*). This
|
||||
package holds the in-tree types + protocol-round skeleton used by
|
||||
higher-level threshold code; the fully-verified production signer
|
||||
lives at [`github.com/luxfi/threshold`](../threshold) and consumes
|
||||
the same primitives.
|
||||
|
||||
## Scope
|
||||
|
||||
This package provides:
|
||||
|
||||
- Party / session state machines for the four-round CGGMP21 flow
|
||||
(Commit → Reveal → Multiply → Open).
|
||||
- A Paillier keypair + additively-homomorphic encryption used by the
|
||||
MtA (multiplicative-to-additive) share-conversion step, including
|
||||
a Schnorr-style ZK-proof-of-plaintext-knowledge skeleton.
|
||||
- ECDSA signature and identifiable-abort record types.
|
||||
- A `VerifySignature` helper for the final aggregated signature.
|
||||
|
||||
This package does **not** provide:
|
||||
|
||||
- A production-ready `Finalize` — the current `Finalize` intentionally
|
||||
returns an error and delegates to `github.com/luxfi/threshold` for
|
||||
the full MtA reconciliation, consistency checks, and
|
||||
identifiable-abort proofs. Do not call this package's
|
||||
`Finalize` from signing paths.
|
||||
- The full CGGMP21 ZK proof suite. `ProveKnowledge` /
|
||||
`VerifyKnowledge` here are a simplified Schnorr-style sketch (with
|
||||
a non-Fiat-Shamir placeholder challenge) intended to illustrate
|
||||
shape; production code MUST use the proofs specified in the CGGMP21
|
||||
paper (Πenc, Πlog*, Πaff-g, Πmul, Πdec, etc.).
|
||||
- Distributed key-generation networking. `Party.KeyGen` populates
|
||||
local share state only; a transport layer supplies peer shares.
|
||||
- Dispute resolution or on-chain glue — those live above this package
|
||||
in the bridge / threshold layers.
|
||||
|
||||
## Public API
|
||||
|
||||
Verified against `cggmp21.go` and `paillier.go` at file time.
|
||||
|
||||
### Core types
|
||||
|
||||
| Symbol | Purpose |
|
||||
| --- | --- |
|
||||
| `Signature` | ECDSA `(R, S)` output. |
|
||||
| `Config` | Threshold `t`, total parties `n`, curve, round timeout. |
|
||||
| `Party` | Per-node state (secret share, Paillier keys, sessions). |
|
||||
| `SigningSession` | Per-session state across all four rounds. |
|
||||
| `ECPoint` | `(X, Y)` on the configured curve. |
|
||||
| `Round2Message`, `Round3Message`, `Round4Message` | Wire messages between rounds. |
|
||||
| `IdentifiableAbort` | Blame record for a misbehaving party. |
|
||||
|
||||
### Party lifecycle
|
||||
|
||||
| Symbol | Purpose |
|
||||
| --- | --- |
|
||||
| `NewParty(id, index, cfg, log)` | Constructor. Generates a 2048-bit Paillier keypair. |
|
||||
| `(*Party).KeyGen(parties)` | Local DKG step — samples `x_i`, computes `[x_i]G`. |
|
||||
| `(*Party).InitiateSign(sessionID, message)` | Opens a signing session; hashes message with SHA-256. |
|
||||
| `(*Party).Round1_Commitment(sessionID)` | Samples `k_i`, `γ_i`; returns `H(i, [γ_i]G)`. |
|
||||
| `(*Party).Round2_Reveal(sessionID, commitments)` | Reveals `[γ_i]G` after receiving peer commitments. |
|
||||
| `(*Party).Round3_Multiply(sessionID, reveals)` | Verifies commitments, computes `δ_i = k_i·γ_i` and `χ_i = x_i·k_i`. |
|
||||
| `(*Party).Round4_Open(sessionID, round3msgs)` | Broadcasts `δ_i` and collects peer `[δ_j]G`. |
|
||||
| `(*Party).Finalize(sessionID, round4msgs)` | **Stub** — returns an error; use `luxfi/threshold`. |
|
||||
|
||||
### Helpers
|
||||
|
||||
| Symbol | Purpose |
|
||||
| --- | --- |
|
||||
| `VerifySignature(pubKey, message, sig)` | Verifies a completed threshold signature against a public key. |
|
||||
| `ScalarBaseMult(curve, k)` | Convenience wrapper returning an `*ECPoint`. |
|
||||
|
||||
### Paillier (in `paillier.go`)
|
||||
|
||||
| Symbol | Purpose |
|
||||
| --- | --- |
|
||||
| `PaillierPublicKey`, `PaillierPrivateKey` | Keypair types. |
|
||||
| `GeneratePaillierKeyPair(bits)` | Samples two primes, returns keypair. |
|
||||
| `(*PaillierPublicKey).Encrypt(m)` | Standard Paillier encryption. |
|
||||
| `(*PaillierPrivateKey).Decrypt(c)` | Standard Paillier decryption. |
|
||||
| `(*PaillierPublicKey).Add(c1, c2)` | Homomorphic addition (`c1·c2 mod n²`). |
|
||||
| `(*PaillierPublicKey).Multiply(c, k)` | Homomorphic constant-multiplication (`c^k mod n²`). |
|
||||
| `L(x, n)` | Standard Paillier `L(x) = (x−1)/n`. |
|
||||
| `ZKProof`, `ProveKnowledge`, `VerifyKnowledge` | Schnorr-style plaintext-knowledge proof **sketch** — see caveat above. |
|
||||
|
||||
## Paillier usage
|
||||
|
||||
CGGMP21 uses Paillier as the additively-homomorphic backbone of the
|
||||
MtA conversion — every `Party` holds a private Paillier key and the
|
||||
peers' public keys, so a share `k_i` can be multiplied against a
|
||||
peer's `γ_j` under encryption and the result reshared additively.
|
||||
The keypair is generated eagerly inside `NewParty` at 2048 bits; the
|
||||
current tree does not gate that on a config option.
|
||||
|
||||
## Security notes
|
||||
|
||||
- **Threshold model.** `t`-of-`n` with the identifiable-abort variant
|
||||
from CGGMP21: any party deviating from the protocol can be
|
||||
cryptographically blamed via `IdentifiableAbort`; the honest
|
||||
majority does not lose their key material on abort.
|
||||
- **No key extraction on abort.** Aborts surface at message-level
|
||||
proof failure, not by revealing secret shares.
|
||||
- **Curve.** Configured through `Config.Curve` — production callers
|
||||
should pass `secp256k1` (or the equivalent `luxfi/crypto/secp256k1`)
|
||||
and never mix curves across a session.
|
||||
- **Randomness.** All sampling uses `crypto/rand`; do not substitute
|
||||
a deterministic source for testing outside of dedicated test
|
||||
builds.
|
||||
|
||||
## Operational constraints
|
||||
|
||||
Per the bridge signing policy
|
||||
([luxfi/bridge#405](https://github.com/luxfi/bridge/pull/405)):
|
||||
|
||||
- **Non-custodial by default.** Production signing paths must remain
|
||||
pure native MPC — CGGMP21 (this package + `luxfi/threshold`) for
|
||||
ECDSA and FROST (via `luxfi/mpc`) for Schnorr / Ed25519.
|
||||
- **No cosigner path.** Cosigner / MPC-provider fallback is
|
||||
explicitly opt-in and must not be reachable from the default bridge
|
||||
configuration.
|
||||
- **`Finalize` is not wired.** Any caller reaching this package's
|
||||
`Finalize` is a bug — route through `luxfi/threshold`.
|
||||
|
||||
## References
|
||||
|
||||
- Canetti, Gennaro, Goldfeder, Makriyannis, Peled. *UC
|
||||
Non-Interactive, Proactive, Threshold ECDSA with Identifiable
|
||||
Aborts.* CCS 2020 / eprint 2021/060.
|
||||
<https://eprint.iacr.org/2021/060>
|
||||
- Sibling packages in this repo:
|
||||
- [`../threshold`](../threshold) — production threshold-signing
|
||||
entrypoint that this package feeds.
|
||||
- [`../bls`](../bls) — BLS threshold (separate construction, not
|
||||
ECDSA).
|
||||
- Ecosystem:
|
||||
- [`luxfi/mpc`](https://github.com/luxfi/mpc) — hosts the FROST
|
||||
Schnorr/Ed25519 side of the native-MPC stack.
|
||||
- [`luxfi/bridge`](https://github.com/luxfi/bridge) — canonical
|
||||
consumer; PR #405 sets the production-signing policy this
|
||||
package supports.
|
||||
BIN
Binary file not shown.
@@ -17,14 +17,14 @@ require (
|
||||
github.com/google/gofuzz v1.2.0
|
||||
github.com/jedisct1/go-minisign v0.0.0-20241212093149-d2f9f49435c7
|
||||
github.com/luxfi/accel v1.2.4
|
||||
github.com/luxfi/age v1.5.0
|
||||
github.com/luxfi/cache v1.2.1
|
||||
github.com/luxfi/age v1.6.0
|
||||
github.com/luxfi/cache v1.3.1
|
||||
github.com/luxfi/crypto/ipa v1.2.4
|
||||
github.com/luxfi/geth v1.16.98
|
||||
github.com/luxfi/ids v1.2.10
|
||||
github.com/luxfi/log v1.4.1
|
||||
github.com/luxfi/geth v1.20.1
|
||||
github.com/luxfi/ids v1.3.2
|
||||
github.com/luxfi/log v1.4.3
|
||||
github.com/luxfi/mock v0.1.1
|
||||
github.com/luxfi/precompile v0.5.37
|
||||
github.com/luxfi/precompile v0.19.3
|
||||
github.com/mr-tron/base58 v1.2.0
|
||||
github.com/stretchr/testify v1.11.1
|
||||
github.com/supranational/blst v0.3.16
|
||||
@@ -41,30 +41,30 @@ require (
|
||||
github.com/bits-and-blooms/bitset v1.24.4 // indirect
|
||||
github.com/gorilla/rpc v1.2.1 // indirect
|
||||
github.com/holiman/uint256 v1.3.2 // indirect
|
||||
github.com/klauspost/compress v1.18.5 // indirect
|
||||
github.com/klauspost/compress v1.18.6 // indirect
|
||||
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
|
||||
github.com/luxfi/atomic v1.0.0 // indirect
|
||||
github.com/luxfi/codec v1.1.4 // indirect
|
||||
github.com/luxfi/compress v0.0.5 // indirect
|
||||
github.com/luxfi/concurrent v0.0.3 // indirect
|
||||
github.com/luxfi/consensus v1.25.0 // indirect
|
||||
github.com/luxfi/constants v1.5.8 // indirect
|
||||
github.com/luxfi/container v0.0.4 // indirect
|
||||
github.com/luxfi/database v1.18.3 // indirect
|
||||
github.com/luxfi/math v1.4.1 // indirect
|
||||
github.com/luxfi/codec v1.2.1 // indirect
|
||||
github.com/luxfi/compress v0.1.1 // indirect
|
||||
github.com/luxfi/concurrent v0.1.1 // indirect
|
||||
github.com/luxfi/consensus v1.36.2 // indirect
|
||||
github.com/luxfi/constants v1.6.2 // indirect
|
||||
github.com/luxfi/container v0.2.1 // indirect
|
||||
github.com/luxfi/database v1.21.1 // indirect
|
||||
github.com/luxfi/math v1.5.1 // indirect
|
||||
github.com/luxfi/math/big v0.1.0 // indirect
|
||||
github.com/luxfi/metric v1.5.7 // indirect
|
||||
github.com/luxfi/p2p v1.21.1 // indirect
|
||||
github.com/luxfi/metric v1.8.1 // indirect
|
||||
github.com/luxfi/p2p v1.22.1 // indirect
|
||||
github.com/luxfi/pq v1.0.3 // indirect
|
||||
github.com/luxfi/runtime v1.1.0 // indirect
|
||||
github.com/luxfi/runtime v1.3.1 // indirect
|
||||
github.com/luxfi/sampler v1.1.0 // indirect
|
||||
github.com/luxfi/utils v1.1.5 // indirect
|
||||
github.com/luxfi/validators v1.2.0 // indirect
|
||||
github.com/luxfi/utils v1.3.1 // indirect
|
||||
github.com/luxfi/validators v1.3.1 // indirect
|
||||
github.com/luxfi/version v1.0.1 // indirect
|
||||
github.com/luxfi/vm v1.2.0 // indirect
|
||||
github.com/luxfi/warp v1.18.6 // indirect
|
||||
github.com/luxfi/vm v1.3.1 // indirect
|
||||
github.com/luxfi/warp v1.24.1 // indirect
|
||||
github.com/mattn/go-colorable v0.1.14 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/mattn/go-isatty v0.0.21 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
|
||||
github.com/zeebo/assert v1.3.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20260312153236-7ab1446f8b90 // indirect
|
||||
@@ -73,3 +73,5 @@ require (
|
||||
gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
|
||||
replace github.com/luxfi/geth => /Users/z/work/lux/geth
|
||||
|
||||
@@ -93,8 +93,8 @@ github.com/jessevdk/go-flags v0.0.0-20141203071132-1679536dcc89/go.mod h1:4FA24M
|
||||
github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
|
||||
github.com/jrick/logrotate v1.0.0/go.mod h1:LNinyqDIJnpAur+b8yyulnQw/wDuN1+BYKlTRt3OuAQ=
|
||||
github.com/kkdai/bstream v0.0.0-20161212061736-f391b8402d23/go.mod h1:J+Gs4SYgM6CZQHDETBtE9HaSEkGmuNXF86RwHhHUvq4=
|
||||
github.com/klauspost/compress v1.18.5 h1:/h1gH5Ce+VWNLSWqPzOVn6XBO+vJbCNGvjoaGBFW2IE=
|
||||
github.com/klauspost/compress v1.18.5/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ=
|
||||
github.com/klauspost/compress v1.18.6 h1:2jupLlAwFm95+YDR+NwD2MEfFO9d4z4Prjl1XXDjuao=
|
||||
github.com/klauspost/compress v1.18.6/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ=
|
||||
github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=
|
||||
github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
|
||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||
@@ -107,8 +107,8 @@ github.com/leanovate/gopter v0.2.11 h1:vRjThO1EKPb/1NsDXuDrzldR28RLkBflWYcU9CvzW
|
||||
github.com/leanovate/gopter v0.2.11/go.mod h1:aK3tzZP/C+p1m3SPRE4SYZFGP7jjkuSI4f7Xvpt0S9c=
|
||||
github.com/luxfi/accel v1.2.4 h1:5VbIHyEvvfobn2zBiTFODxDw1CeqxCepZOLlvkuf9yQ=
|
||||
github.com/luxfi/accel v1.2.4/go.mod h1:ISIwAX+ZfsL/S5nsP2JvfldXN6Nc+QzoWf6Jtaq+xsQ=
|
||||
github.com/luxfi/age v1.5.0 h1:G69HbSV4R3vKEH9B0CulnRaMdSdf4RalMgP8xKmxHeI=
|
||||
github.com/luxfi/age v1.5.0/go.mod h1:iAYAxgvrXxcy746+Ovh/eWWDuF9teJLNcCSSOX9RYW0=
|
||||
github.com/luxfi/age v1.6.0 h1:KMD8gSOP4NVCb7NWSlRcgBZNV2xm2a+qQWPyPmiX6f4=
|
||||
github.com/luxfi/age v1.6.0/go.mod h1:7cu9CIyikgyAvr5MlXFapEDQ15yBaHOSdKkK5lG04WE=
|
||||
github.com/luxfi/atomic v1.0.0 h1:xUV60MuzRvXngaQ1sM0yVC2v4TRoLlUGkkH7M9PS4yw=
|
||||
github.com/luxfi/atomic v1.0.0/go.mod h1:0G2mTlQ6TXWHICUHrUUPu1/qAiIyR4gSZ2tva9ci/bI=
|
||||
github.com/luxfi/cache v1.2.1 h1:kAzOS55/hmYeNKR+0HAKv4ma48Y6JjkI8UQeqdZ8bfI=
|
||||
@@ -127,14 +127,14 @@ github.com/luxfi/container v0.0.4 h1:BXhF82WyfqVP5mjlNcr7tP0Fcnvl0Ap1rkiu+rq5XuM
|
||||
github.com/luxfi/container v0.0.4/go.mod h1:Z3SpmMF5d4t77MM0nHYXURpn+EMVaeu1fhbd/3BGaek=
|
||||
github.com/luxfi/crypto/ipa v1.2.4 h1:6xfwhI9/HrcDkF3Ti5/NxsNQIWbwYDJmRSNIHRQ/xfU=
|
||||
github.com/luxfi/crypto/ipa v1.2.4/go.mod h1:43J6f6rcfUMrZt4cQectMOZb6Ps+fAEj8ZTPC3Kk+gE=
|
||||
github.com/luxfi/database v1.18.3 h1:gg+xwhKUxXa7fDoOD8IS91E71QqoEtcDCl2nfS61Jgg=
|
||||
github.com/luxfi/database v1.18.3/go.mod h1:sv0pYCGKlK1aNJTICxFUDpVWCJTigoLlshHmV/1pg7c=
|
||||
github.com/luxfi/database v1.19.3 h1:flss7/VSGf29l1yZvPwazbtp87X5qfnZGPCU5ObqttQ=
|
||||
github.com/luxfi/database v1.19.3/go.mod h1:S/LvmfzNYWVNslcEcZwDrntqUO2ksaL8ql1nRmLUA/Q=
|
||||
github.com/luxfi/geth v1.16.98 h1:w187TtKuGStf3tm2bshuHVKBv2Frjx0lT54kQVXyNHA=
|
||||
github.com/luxfi/geth v1.16.98/go.mod h1:6kEzSExdk9CPQDPXALt6P3HfQqBq7KF1Jrrv9gBpxbU=
|
||||
github.com/luxfi/ids v1.2.10 h1:f1WILZE199ayMuqnEyB2WP1qfMZkmozOQXSVYtB3e5k=
|
||||
github.com/luxfi/ids v1.2.10/go.mod h1:QBIwy3OHvrtskbUqKh1+OYRa6PsyR7f7oNX33sOfK7w=
|
||||
github.com/luxfi/log v1.4.1 h1:rIfFRodb9jrD/w7KayaUk0Oc+37PaQQdKEEMJCjR8gw=
|
||||
github.com/luxfi/log v1.4.1/go.mod h1:64IE3xRMJcpkQwnPUfJw3pDj7wU0kRS7BZ9wM7R72jk=
|
||||
github.com/luxfi/log v1.4.3 h1:xkUKRWvQ4ZwvlUC2e0/RTtHYZOYSMvSQ9W9lbjwBmiI=
|
||||
github.com/luxfi/log v1.4.3/go.mod h1:myIkufyiQomSQH34K981kbz6cG4WUoerRUh7F4XhlQI=
|
||||
github.com/luxfi/math v1.4.1 h1:1t9bCCsEqnl9yIKrShlbs80DBKyYTWdnzkVfBqEeO7Q=
|
||||
github.com/luxfi/math v1.4.1/go.mod h1:QvbRxauQyE1w4lvbcLSe6c8yeJz2Zj1Bq1rayGgs2tA=
|
||||
github.com/luxfi/math/big v0.1.0 h1:Vz4c0RsZVPdIKPsHPgAJChH/R3p15WHRUz7LkLf+NIQ=
|
||||
@@ -145,9 +145,9 @@ github.com/luxfi/mock v0.1.1 h1:0HEtIjg1J6CWz+IUyP6rsGqNWTcmxjFnSQIhaDuARwY=
|
||||
github.com/luxfi/mock v0.1.1/go.mod h1:jo35akl3Vtd8LbzDts8VJ0jmSVycrd1/eBi6g6t5hKU=
|
||||
github.com/luxfi/p2p v1.21.1 h1:gmz1JMDhzHIL3dQlhwIDvR4OlFuhNVfnWUl/ipYhAIo=
|
||||
github.com/luxfi/p2p v1.21.1/go.mod h1:SsNPR5fPGWWNem9plGWhSmRqyDoysJ3kPAN0zG0g3iw=
|
||||
github.com/luxfi/pq v1.0.3 h1:pFlQm1+5FuKTDUh2y/23bXWkN4I2Rc5iuxJypwDFFMs=
|
||||
github.com/luxfi/pq v1.0.3 h1:ksw1dmfTR0dqqNMRS7BjGcprCO2Fhc+3Iiq2/NMuONw=
|
||||
github.com/luxfi/pq v1.0.3/go.mod h1:8bppZcRElfrVt0n3nYCZW3iX1TvhvzNbdjNdK1irgIE=
|
||||
github.com/luxfi/precompile v0.5.37 h1:2v0zTZtU3cP/hlCA1602Bz//mK+9jjUkYCBsc3KU67w=
|
||||
github.com/luxfi/precompile v0.5.37 h1:Yh3dJ+dYuFuzsJgIBRmAJXNsmGEb0BUJN5fzZe3yYeE=
|
||||
github.com/luxfi/precompile v0.5.37/go.mod h1:z1ZLWPKPdZXqIQZOSdVObM8nTIxrblP6a+fqf3O7OHs=
|
||||
github.com/luxfi/runtime v1.1.0 h1:6TrvzAmZVCTVbR1ebntHTO3/kVBaogPUSkxdDMnrTiw=
|
||||
github.com/luxfi/runtime v1.1.0/go.mod h1:Mfv2zlXqvfRFMS+/zXgG1TieyP9VnvtVzOGB437+o4Y=
|
||||
@@ -165,8 +165,8 @@ github.com/luxfi/warp v1.18.6 h1:+ly7mHz77ig4yUyLax32aAjk7tiQQ6eygzOSDNfYRbQ=
|
||||
github.com/luxfi/warp v1.18.6/go.mod h1:OBN23yiGl+E4qupkPHuetBIEqsI8q5leE8WJH8soFjY=
|
||||
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
|
||||
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
|
||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/mattn/go-isatty v0.0.21 h1:xYae+lCNBP7QuW4PUnNG61ffM4hVIfm+zUzDuSzYLGs=
|
||||
github.com/mattn/go-isatty v0.0.21/go.mod h1:ZXfXG4SQHsB/w3ZeOYbR0PrPwLy+n6xiMrJlRFqopa4=
|
||||
github.com/minio/sha256-simd v1.0.1 h1:6kaan5IFmwTNynnKKpDHe6FWHohJOHhCPchzK49dzMM=
|
||||
github.com/minio/sha256-simd v1.0.1/go.mod h1:Pz6AKMiUdngCLpeTL/RJY1M9rUuPMYujV5xJjtbRSN8=
|
||||
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
|
||||
@@ -239,7 +239,6 @@ golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7w
|
||||
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200519105757-fe76b779f299/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200814200057-3d37ad5750ed/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.45.0 h1:dO4czNzziLiiXplLQgBCEpCvXQ3dnkn0SdaZSYdQ+FY=
|
||||
golang.org/x/sys v0.45.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
|
||||
+1
-1
@@ -12,7 +12,7 @@
|
||||
// Wired to fill the family-disjoint gap on the KEM side, parallel to
|
||||
// the Pulsar / Corona / Magnetar trio on the signature side:
|
||||
//
|
||||
// Signature stack: Pulsar (MLWE) Corona (RLWE) Magnetar (hash)
|
||||
// Signature stack: Pulsar (MLWE) Corona (MLWE) Magnetar (hash)
|
||||
// KEM stack: ML-KEM (MLWE) — HQC (code)
|
||||
//
|
||||
// Spec: NIST IR 8528 "Status Report on the Fourth Round of the NIST
|
||||
|
||||
BIN
Binary file not shown.
@@ -1,20 +0,0 @@
|
||||
//go:build cgo
|
||||
// +build cgo
|
||||
|
||||
package mldsa
|
||||
|
||||
// This file contains CGO-optimized implementations that are only compiled
|
||||
// when CGO is explicitly enabled with CGO_ENABLED=1
|
||||
//
|
||||
// CGO optimizations reserved - currently using pure Go implementation.
|
||||
// Future optimizations could include:
|
||||
// - ML-DSA-44/65/87 from NIST reference implementation
|
||||
// - CRYSTALS-Dilithium optimized implementations
|
||||
// - AVX2/AVX512 optimizations for x86_64
|
||||
// - NEON optimizations for ARM64
|
||||
// - Batch verification optimizations
|
||||
//
|
||||
// The pure Go implementation provides:
|
||||
// - Full ML-DSA compliance with FIPS 204
|
||||
// - Deterministic signatures
|
||||
// - Cross-platform compatibility
|
||||
@@ -1,409 +0,0 @@
|
||||
package mldsa
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/rand"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestMLDSAKeyGeneration(t *testing.T) {
|
||||
modes := []struct {
|
||||
name string
|
||||
mode Mode
|
||||
pubSize int
|
||||
privSize int
|
||||
sigSize int
|
||||
}{
|
||||
{"ML-DSA-44", MLDSA44, MLDSA44PublicKeySize, MLDSA44PrivateKeySize, MLDSA44SignatureSize},
|
||||
{"ML-DSA-65", MLDSA65, MLDSA65PublicKeySize, MLDSA65PrivateKeySize, MLDSA65SignatureSize},
|
||||
{"ML-DSA-87", MLDSA87, MLDSA87PublicKeySize, MLDSA87PrivateKeySize, MLDSA87SignatureSize},
|
||||
}
|
||||
|
||||
for _, tt := range modes {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
// Test key generation
|
||||
privKey, err := GenerateKey(rand.Reader, tt.mode)
|
||||
if err != nil {
|
||||
t.Fatalf("GenerateKey failed: %v", err)
|
||||
}
|
||||
|
||||
// Check key sizes
|
||||
privBytes := privKey.Bytes()
|
||||
if len(privBytes) != tt.privSize {
|
||||
t.Errorf("Private key size mismatch: got %d, want %d", len(privBytes), tt.privSize)
|
||||
}
|
||||
|
||||
pubBytes := privKey.PublicKey.Bytes()
|
||||
if len(pubBytes) != tt.pubSize {
|
||||
t.Errorf("Public key size mismatch: got %d, want %d", len(pubBytes), tt.pubSize)
|
||||
}
|
||||
|
||||
// Test nil reader
|
||||
_, err = GenerateKey(nil, tt.mode)
|
||||
if err == nil {
|
||||
t.Error("GenerateKey should fail with nil reader")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestMLDSASignVerify(t *testing.T) {
|
||||
modes := []Mode{MLDSA44, MLDSA65, MLDSA87}
|
||||
|
||||
for _, mode := range modes {
|
||||
t.Run(mode.String(), func(t *testing.T) {
|
||||
privKey, err := GenerateKey(rand.Reader, mode)
|
||||
if err != nil {
|
||||
t.Fatalf("GenerateKey failed: %v", err)
|
||||
}
|
||||
|
||||
message := []byte("Test message for ML-DSA signature")
|
||||
|
||||
// Sign message
|
||||
signature, err := privKey.Sign(rand.Reader, message, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("Sign failed: %v", err)
|
||||
}
|
||||
|
||||
// Verify signature
|
||||
valid := privKey.PublicKey.Verify(message, signature, nil)
|
||||
if !valid {
|
||||
t.Error("Valid signature failed verification")
|
||||
}
|
||||
|
||||
// Test invalid signature
|
||||
signature[0] ^= 0xFF
|
||||
valid = privKey.PublicKey.Verify(message, signature, nil)
|
||||
if valid {
|
||||
t.Error("Invalid signature passed verification")
|
||||
}
|
||||
signature[0] ^= 0xFF // restore
|
||||
|
||||
// Test wrong message
|
||||
wrongMessage := []byte("Wrong message")
|
||||
valid = privKey.PublicKey.Verify(wrongMessage, signature, nil)
|
||||
if valid {
|
||||
t.Error("Signature verified with wrong message")
|
||||
}
|
||||
|
||||
// Test empty message
|
||||
emptyMessage := []byte{}
|
||||
emptySig, err := privKey.Sign(rand.Reader, emptyMessage, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("Sign empty message failed: %v", err)
|
||||
}
|
||||
valid = privKey.PublicKey.Verify(emptyMessage, emptySig, nil)
|
||||
if !valid {
|
||||
t.Error("Empty message signature failed verification")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestMLDSAKeySerialization(t *testing.T) {
|
||||
modes := []Mode{MLDSA44, MLDSA65, MLDSA87}
|
||||
|
||||
for _, mode := range modes {
|
||||
t.Run(mode.String(), func(t *testing.T) {
|
||||
// Generate original key
|
||||
origKey, err := GenerateKey(rand.Reader, mode)
|
||||
if err != nil {
|
||||
t.Fatalf("GenerateKey failed: %v", err)
|
||||
}
|
||||
|
||||
// Serialize and deserialize private key using FromBytes functions
|
||||
privBytes := origKey.Bytes()
|
||||
newPrivKey, err := PrivateKeyFromBytes(privBytes, mode)
|
||||
if err != nil {
|
||||
t.Fatalf("PrivateKeyFromBytes failed: %v", err)
|
||||
}
|
||||
|
||||
// Check keys are equal
|
||||
if !bytes.Equal(origKey.Bytes(), newPrivKey.Bytes()) {
|
||||
t.Error("Private key serialization failed")
|
||||
}
|
||||
|
||||
// Serialize and deserialize public key
|
||||
pubBytes := origKey.PublicKey.Bytes()
|
||||
newPubKey, err := PublicKeyFromBytes(pubBytes, mode)
|
||||
if err != nil {
|
||||
t.Fatalf("PublicKeyFromBytes failed: %v", err)
|
||||
}
|
||||
|
||||
if !bytes.Equal(origKey.PublicKey.Bytes(), newPubKey.Bytes()) {
|
||||
t.Error("Public key serialization failed")
|
||||
}
|
||||
|
||||
// Test signature with deserialized keys
|
||||
message := []byte("Test serialization")
|
||||
signature, err := newPrivKey.Sign(rand.Reader, message, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("Sign with deserialized key failed: %v", err)
|
||||
}
|
||||
|
||||
valid := newPubKey.Verify(message, signature, nil)
|
||||
if !valid {
|
||||
t.Error("Verification with deserialized key failed")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestMLDSADeterministicSignature(t *testing.T) {
|
||||
modes := []Mode{MLDSA44, MLDSA65, MLDSA87}
|
||||
|
||||
for _, mode := range modes {
|
||||
t.Run(mode.String(), func(t *testing.T) {
|
||||
privKey, err := GenerateKey(rand.Reader, mode)
|
||||
if err != nil {
|
||||
t.Fatalf("GenerateKey failed: %v", err)
|
||||
}
|
||||
|
||||
message := []byte("Randomized signature test")
|
||||
|
||||
// ML-DSA requires a random source - test that nil rand is rejected
|
||||
_, err = privKey.Sign(nil, message, nil)
|
||||
if err == nil {
|
||||
t.Error("Sign with nil rand should fail")
|
||||
}
|
||||
|
||||
// Sign same message multiple times with proper rand
|
||||
sig1, err := privKey.Sign(rand.Reader, message, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("First sign failed: %v", err)
|
||||
}
|
||||
|
||||
sig2, err := privKey.Sign(rand.Reader, message, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("Second sign failed: %v", err)
|
||||
}
|
||||
|
||||
// Note: The circl ML-DSA implementation may produce the same signature
|
||||
// for the same message when using the same random state.
|
||||
// This is implementation-specific behavior and doesn't indicate
|
||||
// a security issue - the randomness is properly used internally.
|
||||
t.Logf("Signature 1 length: %d, Signature 2 length: %d", len(sig1), len(sig2))
|
||||
|
||||
// Both signatures should verify
|
||||
if !privKey.PublicKey.Verify(message, sig1, nil) {
|
||||
t.Error("First signature failed verification")
|
||||
}
|
||||
if !privKey.PublicKey.Verify(message, sig2, nil) {
|
||||
t.Error("Second signature failed verification")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestMLDSAEdgeCases(t *testing.T) {
|
||||
t.Run("InvalidMode", func(t *testing.T) {
|
||||
_, err := GenerateKey(rand.Reader, Mode(99))
|
||||
if err == nil {
|
||||
t.Error("GenerateKey should fail with invalid mode")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("NilPublicKey", func(t *testing.T) {
|
||||
var pubKey *PublicKey
|
||||
valid := pubKey.Verify([]byte("test"), []byte("sig"), nil)
|
||||
if valid {
|
||||
t.Error("Nil public key should not verify")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("NilPrivateKey", func(t *testing.T) {
|
||||
var privKey *PrivateKey
|
||||
_, err := privKey.Sign(rand.Reader, []byte("test"), nil)
|
||||
if err == nil {
|
||||
t.Error("Nil private key should not sign")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("EmptySignature", func(t *testing.T) {
|
||||
privKey, _ := GenerateKey(rand.Reader, MLDSA44)
|
||||
valid := privKey.PublicKey.Verify([]byte("test"), []byte{}, nil)
|
||||
if valid {
|
||||
t.Error("Empty signature should not verify")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("WrongSizeSignature", func(t *testing.T) {
|
||||
privKey, _ := GenerateKey(rand.Reader, MLDSA44)
|
||||
// Wrong size signature
|
||||
wrongSig := make([]byte, 100)
|
||||
rand.Read(wrongSig)
|
||||
valid := privKey.PublicKey.Verify([]byte("test"), wrongSig, nil)
|
||||
if valid {
|
||||
t.Error("Wrong size signature should not verify")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestMLDSALargeMessage(t *testing.T) {
|
||||
modes := []Mode{MLDSA44, MLDSA65, MLDSA87}
|
||||
|
||||
for _, mode := range modes {
|
||||
t.Run(mode.String(), func(t *testing.T) {
|
||||
privKey, err := GenerateKey(rand.Reader, mode)
|
||||
if err != nil {
|
||||
t.Fatalf("GenerateKey failed: %v", err)
|
||||
}
|
||||
|
||||
// Test with large message (1MB)
|
||||
largeMessage := make([]byte, 1024*1024)
|
||||
rand.Read(largeMessage)
|
||||
|
||||
signature, err := privKey.Sign(rand.Reader, largeMessage, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("Sign large message failed: %v", err)
|
||||
}
|
||||
|
||||
valid := privKey.PublicKey.Verify(largeMessage, signature, nil)
|
||||
if !valid {
|
||||
t.Error("Large message signature failed verification")
|
||||
}
|
||||
|
||||
// Modify one byte in the middle
|
||||
largeMessage[512*1024] ^= 0xFF
|
||||
valid = privKey.PublicKey.Verify(largeMessage, signature, nil)
|
||||
if valid {
|
||||
t.Error("Modified large message passed verification")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestMLDSAConcurrency(t *testing.T) {
|
||||
privKey, err := GenerateKey(rand.Reader, MLDSA44)
|
||||
if err != nil {
|
||||
t.Fatalf("GenerateKey failed: %v", err)
|
||||
}
|
||||
|
||||
message := []byte("Concurrent test message")
|
||||
|
||||
// Test concurrent signing
|
||||
t.Run("ConcurrentSign", func(t *testing.T) {
|
||||
const numGoroutines = 10
|
||||
done := make(chan bool, numGoroutines)
|
||||
|
||||
for i := 0; i < numGoroutines; i++ {
|
||||
go func(id int) {
|
||||
msg := append(message, byte(id))
|
||||
sig, err := privKey.Sign(rand.Reader, msg, nil)
|
||||
if err != nil {
|
||||
t.Errorf("Goroutine %d: Sign failed: %v", id, err)
|
||||
}
|
||||
valid := privKey.PublicKey.Verify(msg, sig, nil)
|
||||
if !valid {
|
||||
t.Errorf("Goroutine %d: Verification failed", id)
|
||||
}
|
||||
done <- true
|
||||
}(i)
|
||||
}
|
||||
|
||||
for i := 0; i < numGoroutines; i++ {
|
||||
<-done
|
||||
}
|
||||
})
|
||||
|
||||
// Test concurrent verification
|
||||
t.Run("ConcurrentVerify", func(t *testing.T) {
|
||||
signature, _ := privKey.Sign(rand.Reader, message, nil)
|
||||
const numGoroutines = 10
|
||||
done := make(chan bool, numGoroutines)
|
||||
|
||||
for i := 0; i < numGoroutines; i++ {
|
||||
go func(id int) {
|
||||
valid := privKey.PublicKey.Verify(message, signature, nil)
|
||||
if !valid {
|
||||
t.Errorf("Goroutine %d: Verification failed", id)
|
||||
}
|
||||
done <- true
|
||||
}(i)
|
||||
}
|
||||
|
||||
for i := 0; i < numGoroutines; i++ {
|
||||
<-done
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func BenchmarkMLDSAKeyGen(b *testing.B) {
|
||||
modes := []struct {
|
||||
name string
|
||||
mode Mode
|
||||
}{
|
||||
{"ML-DSA-44", MLDSA44},
|
||||
{"ML-DSA-65", MLDSA65},
|
||||
{"ML-DSA-87", MLDSA87},
|
||||
}
|
||||
|
||||
for _, m := range modes {
|
||||
b.Run(m.name, func(b *testing.B) {
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
_, err := GenerateKey(rand.Reader, m.mode)
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkMLDSASign(b *testing.B) {
|
||||
modes := []struct {
|
||||
name string
|
||||
mode Mode
|
||||
}{
|
||||
{"ML-DSA-44", MLDSA44},
|
||||
{"ML-DSA-65", MLDSA65},
|
||||
{"ML-DSA-87", MLDSA87},
|
||||
}
|
||||
|
||||
message := []byte("Benchmark message for ML-DSA signature performance")
|
||||
|
||||
for _, m := range modes {
|
||||
privKey, _ := GenerateKey(rand.Reader, m.mode)
|
||||
|
||||
b.Run(m.name, func(b *testing.B) {
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
_, err := privKey.Sign(rand.Reader, message, nil)
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkMLDSAVerify(b *testing.B) {
|
||||
modes := []struct {
|
||||
name string
|
||||
mode Mode
|
||||
}{
|
||||
{"ML-DSA-44", MLDSA44},
|
||||
{"ML-DSA-65", MLDSA65},
|
||||
{"ML-DSA-87", MLDSA87},
|
||||
}
|
||||
|
||||
message := []byte("Benchmark message for ML-DSA verification performance")
|
||||
|
||||
for _, m := range modes {
|
||||
privKey, _ := GenerateKey(rand.Reader, m.mode)
|
||||
signature, _ := privKey.Sign(rand.Reader, message, nil)
|
||||
|
||||
b.Run(m.name, func(b *testing.B) {
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
valid := privKey.PublicKey.Verify(message, signature, nil)
|
||||
if !valid {
|
||||
b.Fatal("Verification failed")
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Helper functions are now in mldsa.go
|
||||
@@ -1,396 +0,0 @@
|
||||
package mldsa
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/rand"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestOptimizedKeyGeneration(t *testing.T) {
|
||||
modes := []Mode{MLDSA44, MLDSA65, MLDSA87}
|
||||
|
||||
for _, mode := range modes {
|
||||
t.Run(mode.String(), func(t *testing.T) {
|
||||
// Test optimized key generation
|
||||
privKey, err := GenerateKeyOptimized(rand.Reader, mode)
|
||||
if err != nil {
|
||||
t.Fatalf("GenerateKeyOptimized failed: %v", err)
|
||||
}
|
||||
|
||||
// Verify key sizes
|
||||
var expectedPrivSize, expectedPubSize int
|
||||
switch mode {
|
||||
case MLDSA44:
|
||||
expectedPrivSize = MLDSA44PrivateKeySize
|
||||
expectedPubSize = MLDSA44PublicKeySize
|
||||
case MLDSA65:
|
||||
expectedPrivSize = MLDSA65PrivateKeySize
|
||||
expectedPubSize = MLDSA65PublicKeySize
|
||||
case MLDSA87:
|
||||
expectedPrivSize = MLDSA87PrivateKeySize
|
||||
expectedPubSize = MLDSA87PublicKeySize
|
||||
}
|
||||
|
||||
if len(privKey.Bytes()) != expectedPrivSize {
|
||||
t.Errorf("Private key size mismatch: got %d, want %d", len(privKey.Bytes()), expectedPrivSize)
|
||||
}
|
||||
if len(privKey.PublicKey.Bytes()) != expectedPubSize {
|
||||
t.Errorf("Public key size mismatch: got %d, want %d", len(privKey.PublicKey.Bytes()), expectedPubSize)
|
||||
}
|
||||
|
||||
// Test nil reader
|
||||
_, err = GenerateKeyOptimized(nil, mode)
|
||||
if err == nil {
|
||||
t.Error("GenerateKeyOptimized should fail with nil reader")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// Test invalid mode
|
||||
_, err := GenerateKeyOptimized(rand.Reader, Mode(99))
|
||||
if err == nil {
|
||||
t.Error("GenerateKeyOptimized should fail with invalid mode")
|
||||
}
|
||||
}
|
||||
|
||||
func TestOptimizedSign(t *testing.T) {
|
||||
modes := []Mode{MLDSA44, MLDSA65, MLDSA87}
|
||||
|
||||
for _, mode := range modes {
|
||||
t.Run(mode.String(), func(t *testing.T) {
|
||||
privKey, err := GenerateKey(rand.Reader, mode)
|
||||
if err != nil {
|
||||
t.Fatalf("GenerateKey failed: %v", err)
|
||||
}
|
||||
|
||||
message := []byte("Test message for optimized signing")
|
||||
|
||||
// Test optimized signing
|
||||
signature, err := privKey.OptimizedSign(rand.Reader, message, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("OptimizedSign failed: %v", err)
|
||||
}
|
||||
|
||||
// Verify signature
|
||||
valid := privKey.PublicKey.Verify(message, signature, nil)
|
||||
if !valid {
|
||||
t.Error("Optimized signature failed verification")
|
||||
}
|
||||
|
||||
// Test with different message
|
||||
wrongMessage := []byte("Wrong message")
|
||||
valid = privKey.PublicKey.Verify(wrongMessage, signature, nil)
|
||||
if valid {
|
||||
t.Error("Signature verified with wrong message")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestBatchDSA(t *testing.T) {
|
||||
modes := []Mode{MLDSA44, MLDSA65, MLDSA87}
|
||||
|
||||
for _, mode := range modes {
|
||||
t.Run(mode.String(), func(t *testing.T) {
|
||||
numKeys := 5
|
||||
batch, err := NewBatchDSA(mode, numKeys)
|
||||
if err != nil {
|
||||
t.Fatalf("NewBatchDSA failed: %v", err)
|
||||
}
|
||||
|
||||
// Prepare messages
|
||||
messages := make([][]byte, numKeys)
|
||||
for i := range messages {
|
||||
messages[i] = []byte(string(rune('A'+i)) + " Test message for batch signing")
|
||||
}
|
||||
|
||||
// Batch sign
|
||||
signatures, err := batch.SignBatch(messages)
|
||||
if err != nil {
|
||||
t.Fatalf("SignBatch failed: %v", err)
|
||||
}
|
||||
|
||||
if len(signatures) != numKeys {
|
||||
t.Errorf("Expected %d signatures, got %d", numKeys, len(signatures))
|
||||
}
|
||||
|
||||
// Batch verify
|
||||
results, err := batch.VerifyBatch(messages, signatures)
|
||||
if err != nil {
|
||||
t.Fatalf("VerifyBatch failed: %v", err)
|
||||
}
|
||||
|
||||
for i, valid := range results {
|
||||
if !valid {
|
||||
t.Errorf("Signature %d failed verification", i)
|
||||
}
|
||||
}
|
||||
|
||||
// Test with tampered signature
|
||||
signatures[0][0] ^= 0xFF
|
||||
results, err = batch.VerifyBatch(messages, signatures)
|
||||
if err != nil {
|
||||
t.Fatalf("VerifyBatch failed: %v", err)
|
||||
}
|
||||
if results[0] {
|
||||
t.Error("Tampered signature passed verification")
|
||||
}
|
||||
signatures[0][0] ^= 0xFF // restore
|
||||
|
||||
// Test mismatched counts
|
||||
wrongMessages := make([][]byte, numKeys-1)
|
||||
_, err = batch.SignBatch(wrongMessages)
|
||||
if err == nil {
|
||||
t.Error("SignBatch should fail with mismatched message count")
|
||||
}
|
||||
|
||||
_, err = batch.VerifyBatch(wrongMessages, signatures)
|
||||
if err == nil {
|
||||
t.Error("VerifyBatch should fail with mismatched count")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestPrecomputedMLDSA(t *testing.T) {
|
||||
modes := []Mode{MLDSA44, MLDSA65, MLDSA87}
|
||||
|
||||
for _, mode := range modes {
|
||||
t.Run(mode.String(), func(t *testing.T) {
|
||||
privKey, err := GenerateKey(rand.Reader, mode)
|
||||
if err != nil {
|
||||
t.Fatalf("GenerateKey failed: %v", err)
|
||||
}
|
||||
|
||||
precomputed := NewPrecomputedMLDSA(privKey)
|
||||
|
||||
message := []byte("Test message for caching")
|
||||
|
||||
// First sign (cache miss)
|
||||
sig1, err := precomputed.SignCached(message)
|
||||
if err != nil {
|
||||
t.Fatalf("First SignCached failed: %v", err)
|
||||
}
|
||||
|
||||
// Second sign (cache hit)
|
||||
sig2, err := precomputed.SignCached(message)
|
||||
if err != nil {
|
||||
t.Fatalf("Second SignCached failed: %v", err)
|
||||
}
|
||||
|
||||
// Cached signatures should be identical
|
||||
if !bytes.Equal(sig1, sig2) {
|
||||
t.Error("Cached signatures are not identical")
|
||||
}
|
||||
|
||||
// Both should verify
|
||||
valid := privKey.PublicKey.Verify(message, sig1, nil)
|
||||
if !valid {
|
||||
t.Error("First cached signature failed verification")
|
||||
}
|
||||
|
||||
valid = privKey.PublicKey.Verify(message, sig2, nil)
|
||||
if !valid {
|
||||
t.Error("Second cached signature failed verification")
|
||||
}
|
||||
|
||||
// Different message should produce different signature
|
||||
message2 := []byte("Different message")
|
||||
sig3, err := precomputed.SignCached(message2)
|
||||
if err != nil {
|
||||
t.Fatalf("SignCached for different message failed: %v", err)
|
||||
}
|
||||
|
||||
if bytes.Equal(sig1, sig3) {
|
||||
t.Error("Different messages produced same signature")
|
||||
}
|
||||
|
||||
valid = privKey.PublicKey.Verify(message2, sig3, nil)
|
||||
if !valid {
|
||||
t.Error("Third cached signature failed verification")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestSignatureBufferPool(t *testing.T) {
|
||||
// Test getting and putting buffers
|
||||
buf1 := getSignatureBuffer(MLDSA44SignatureSize)
|
||||
if len(buf1) != MLDSA44SignatureSize {
|
||||
t.Errorf("Expected buffer size %d, got %d", MLDSA44SignatureSize, len(buf1))
|
||||
}
|
||||
|
||||
// Return buffer to pool
|
||||
putSignatureBuffer(buf1)
|
||||
|
||||
// Get buffer again (should get same or similar buffer from pool)
|
||||
buf2 := getSignatureBuffer(MLDSA44SignatureSize)
|
||||
if len(buf2) != MLDSA44SignatureSize {
|
||||
t.Errorf("Expected buffer size %d, got %d", MLDSA44SignatureSize, len(buf2))
|
||||
}
|
||||
putSignatureBuffer(buf2)
|
||||
|
||||
// Test with larger size than pool default
|
||||
largeBuf := getSignatureBuffer(MLDSA87SignatureSize * 2)
|
||||
if len(largeBuf) != MLDSA87SignatureSize*2 {
|
||||
t.Errorf("Expected buffer size %d, got %d", MLDSA87SignatureSize*2, len(largeBuf))
|
||||
}
|
||||
// Small buffers should not be pooled
|
||||
smallBuf := make([]byte, 100)
|
||||
putSignatureBuffer(smallBuf) // Should not panic
|
||||
}
|
||||
|
||||
func TestHelperFunctions(t *testing.T) {
|
||||
t.Run("SetBytes", func(t *testing.T) {
|
||||
// Test PrivateKey SetBytes
|
||||
privKey := NewPrivateKey(MLDSA44)
|
||||
data := make([]byte, MLDSA44PrivateKeySize)
|
||||
rand.Read(data)
|
||||
privKey.SetBytes(data)
|
||||
if !bytes.Equal(privKey.Bytes(), data) {
|
||||
t.Error("PrivateKey SetBytes failed")
|
||||
}
|
||||
|
||||
// Test PublicKey SetBytes
|
||||
pubKey := NewPublicKey(MLDSA44)
|
||||
pubData := make([]byte, MLDSA44PublicKeySize)
|
||||
rand.Read(pubData)
|
||||
pubKey.SetBytes(pubData)
|
||||
if !bytes.Equal(pubKey.Bytes(), pubData) {
|
||||
t.Error("PublicKey SetBytes failed")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("GetKeySizes", func(t *testing.T) {
|
||||
// Test private key sizes
|
||||
if size := getPrivateKeySize(MLDSA44); size != MLDSA44PrivateKeySize {
|
||||
t.Errorf("getPrivateKeySize(MLDSA44) = %d, want %d", size, MLDSA44PrivateKeySize)
|
||||
}
|
||||
if size := getPrivateKeySize(MLDSA65); size != MLDSA65PrivateKeySize {
|
||||
t.Errorf("getPrivateKeySize(MLDSA65) = %d, want %d", size, MLDSA65PrivateKeySize)
|
||||
}
|
||||
if size := getPrivateKeySize(MLDSA87); size != MLDSA87PrivateKeySize {
|
||||
t.Errorf("getPrivateKeySize(MLDSA87) = %d, want %d", size, MLDSA87PrivateKeySize)
|
||||
}
|
||||
if size := getPrivateKeySize(Mode(99)); size != 0 {
|
||||
t.Errorf("getPrivateKeySize(invalid) = %d, want 0", size)
|
||||
}
|
||||
|
||||
// Test public key sizes
|
||||
if size := getPublicKeySize(MLDSA44); size != MLDSA44PublicKeySize {
|
||||
t.Errorf("getPublicKeySize(MLDSA44) = %d, want %d", size, MLDSA44PublicKeySize)
|
||||
}
|
||||
if size := getPublicKeySize(MLDSA65); size != MLDSA65PublicKeySize {
|
||||
t.Errorf("getPublicKeySize(MLDSA65) = %d, want %d", size, MLDSA65PublicKeySize)
|
||||
}
|
||||
if size := getPublicKeySize(MLDSA87); size != MLDSA87PublicKeySize {
|
||||
t.Errorf("getPublicKeySize(MLDSA87) = %d, want %d", size, MLDSA87PublicKeySize)
|
||||
}
|
||||
if size := getPublicKeySize(Mode(99)); size != 0 {
|
||||
t.Errorf("getPublicKeySize(invalid) = %d, want 0", size)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func BenchmarkOptimizedKeyGen(b *testing.B) {
|
||||
modes := []struct {
|
||||
name string
|
||||
mode Mode
|
||||
}{
|
||||
{"ML-DSA-44", MLDSA44},
|
||||
{"ML-DSA-65", MLDSA65},
|
||||
{"ML-DSA-87", MLDSA87},
|
||||
}
|
||||
|
||||
for _, m := range modes {
|
||||
b.Run(m.name+"-Standard", func(b *testing.B) {
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
_, err := GenerateKey(rand.Reader, m.mode)
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
b.Run(m.name+"-Optimized", func(b *testing.B) {
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
_, err := GenerateKeyOptimized(rand.Reader, m.mode)
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkOptimizedSign(b *testing.B) {
|
||||
modes := []struct {
|
||||
name string
|
||||
mode Mode
|
||||
}{
|
||||
{"ML-DSA-44", MLDSA44},
|
||||
{"ML-DSA-65", MLDSA65},
|
||||
{"ML-DSA-87", MLDSA87},
|
||||
}
|
||||
|
||||
message := []byte("Benchmark message for optimized signing")
|
||||
|
||||
for _, m := range modes {
|
||||
privKey, _ := GenerateKey(rand.Reader, m.mode)
|
||||
|
||||
b.Run(m.name+"-Standard", func(b *testing.B) {
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
_, err := privKey.Sign(rand.Reader, message, nil)
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
b.Run(m.name+"-Optimized", func(b *testing.B) {
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
_, err := privKey.OptimizedSign(rand.Reader, message, nil)
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkBatchOperations(b *testing.B) {
|
||||
numKeys := 10
|
||||
batch, _ := NewBatchDSA(MLDSA65, numKeys)
|
||||
|
||||
messages := make([][]byte, numKeys)
|
||||
for i := range messages {
|
||||
messages[i] = make([]byte, 32)
|
||||
rand.Read(messages[i])
|
||||
}
|
||||
|
||||
b.Run("BatchSign", func(b *testing.B) {
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
_, err := batch.SignBatch(messages)
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
signatures, _ := batch.SignBatch(messages)
|
||||
|
||||
b.Run("BatchVerify", func(b *testing.B) {
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
_, err := batch.VerifyBatch(messages, signatures)
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
BIN
Binary file not shown.
+172
@@ -0,0 +1,172 @@
|
||||
# `poi` — Proof-of-Inference verifier primitive
|
||||
|
||||
Freivalds-over-`F_p` matrix-product verification, plus a Merkle transcript and
|
||||
wire codec for on-chain openings. This is the canonical Go verifier that
|
||||
`luxfi/precompile/aivmbridge/computeproof.go` consumes and that
|
||||
`hanzo-engine/src/poi.rs` mirrors on the prover side.
|
||||
|
||||
See **LP-5300** (Thinking Chains / Proof-of-Thought) for the receipt model this
|
||||
serves, and **LP-5301** (AIVMBridge) for the precompile that calls this code
|
||||
in-EVM.
|
||||
|
||||
## Overview
|
||||
|
||||
Proof-of-AI binds mint to genuine computation. An LLM forward pass is ~95%
|
||||
matrix multiplications `C = A·B`. Recomputing every `C` on-chain is
|
||||
prohibitive; Freivalds (1977) instead samples a random vector `r` and checks
|
||||
|
||||
A·(B·r) == C·r
|
||||
|
||||
in `O(t·k + k·n + t·n)` — an order cheaper than the `O(t·k·n)` recompute — and
|
||||
catches any fabricated `C` (a claimed output the prover never multiplied for)
|
||||
with probability `>= 1 - 1/p` per vector.
|
||||
|
||||
The verifier operates on the **exact-integer** accumulator of the engine's
|
||||
int8 matmul path (`i8·i8 -> i32` promoted to `int64`). Working on the exact
|
||||
accumulator, not on floats, means the check is bit-exact with **zero
|
||||
false-reject across CPU / GPU / backends** — the determinism the whole scheme
|
||||
relies on.
|
||||
|
||||
## Mathematical setup
|
||||
|
||||
- Field: `F_p` with prime modulus `P = 2^61 - 1` (a Mersenne prime; see
|
||||
`freivalds.go` — `const P uint64 = (1 << 61) - 1`).
|
||||
- Signed inputs are reduced into `[0, p)` by `toField`; a single Freivalds
|
||||
intermediate stays within 61 bits, so a 128-bit multiply never overflows.
|
||||
- Soundness: `1/p ~ 2^-61` per challenge vector. `k` independent vectors give
|
||||
a soundness error of `(1/p)^k`. `k = 2` yields `~2^-122`, which is what the
|
||||
precompile ships with.
|
||||
- Challenge derivation is Fiat–Shamir: `DeriveChallenges(seed, n, k)` folds
|
||||
keccak of `seed || (j, i)` into `[0, p)`. In production the seed is
|
||||
`keccak(beacon || root || index)` (see `OpeningSeed`), so a prover cannot
|
||||
anticipate its challenges before committing the transcript root.
|
||||
|
||||
## Public API
|
||||
|
||||
Freivalds core (`freivalds.go`):
|
||||
|
||||
| symbol | signature | purpose |
|
||||
| --- | --- | --- |
|
||||
| `P` | `const uint64 = (1 << 61) - 1` | Field modulus. |
|
||||
| `Mat` | `struct { Rows, Cols int; Data []int64 }` | Row-major integer matrix. |
|
||||
| `NewMat` | `func(rows, cols int, data []int64) Mat` | Constructor (panics on length mismatch). |
|
||||
| `Verify` | `func(a, b, c Mat, r []uint64) bool` | One-vector Freivalds check. |
|
||||
| `VerifyMulti` | `func(a, b, c Mat, challenges [][]uint64) bool` | k-vector check; false if any fails. |
|
||||
| `DeriveChallenges` | `func(seed []byte, n, k int) [][]uint64` | Fiat–Shamir challenge derivation. |
|
||||
|
||||
Transcript + Merkle (`transcript.go`):
|
||||
|
||||
| symbol | signature | purpose |
|
||||
| --- | --- | --- |
|
||||
| `DomainMatmulLeaf` | `[]byte("hanzo/poi/matmul-leaf/v1")` | Leaf domain tag. |
|
||||
| `ExactMatmul` | `func(a, b Mat) Mat` | Whole-K `int64` exact matmul (the proof-bearing GEMM). |
|
||||
| `MatmulLeaf` | `func(a, b, c Mat) [32]byte` | `keccak(DOMAIN || mat(A) || mat(B) || mat(C))`. |
|
||||
| `MerkleRoot` | `func(leaves [][32]byte) [32]byte` | Duplicate-last-on-odd keccak fold. |
|
||||
| `MerkleProof` | `func(leaves [][32]byte, index int) [][32]byte` | Sibling path, bottom-up. |
|
||||
| `MerkleVerify` | `func(leaf, root [32]byte, index int, proof [][32]byte) bool` | Inclusion check (matches `AICoinMiner._verifyMerkle`). |
|
||||
| `Opening` | `struct { Index int; A, B, C Mat; Proof [][32]byte }` | What a prover reveals for one matmul. |
|
||||
| `ProofTranscript` | `struct` w/ `Matmul`, `CommitClaimed`, `Len`, `Root`, `Open` | Append-only commitment to a forward pass's matmuls. |
|
||||
| `NewTranscript` | `func() *ProofTranscript` | Constructor. |
|
||||
| `ChallengeIndex` | `func(beacon []byte, root [32]byte, length int) int` | `keccak(beacon || root) mod length`. |
|
||||
| `OpeningSeed` | `func(beacon []byte, root [32]byte, index int) []byte` | `keccak(beacon || root || BE64(index))`. |
|
||||
| `VerifyOpening` | `func(root [32]byte, beacon []byte, op Opening, k int) bool` | Merkle inclusion **and** k-vector Freivalds. |
|
||||
|
||||
Wire codec + precompile payload (`wire.go`):
|
||||
|
||||
| symbol | signature | purpose |
|
||||
| --- | --- | --- |
|
||||
| `MaxOpeningDim` | `const = 4096` | Per-dimension bound (see below). |
|
||||
| `ErrShortOpening`, `ErrOpeningDims` | `error` | Decode errors. |
|
||||
| `EncodeOpening` | `func(root [32]byte, beacon []byte, op Opening) []byte` | Canonical wire frame. |
|
||||
| `DecodeOpening` | `func(b []byte) (root [32]byte, beacon []byte, op Opening, err error)` | Bounds-checked parse. |
|
||||
| `CheckOpening` | `func(input []byte, k int) (included, freivaldsOK bool, err error)` | Precompile payload: decode + verify. |
|
||||
| `CheckDecoded` | `func(root [32]byte, beacon []byte, op Opening, k int) (bool, bool, error)` | Same, on an already-parsed opening. |
|
||||
| `FieldOps` | `func(op Opening, k int) uint64` | `k · (t·k + k·n + t·n)` — drives precompile gas. |
|
||||
|
||||
## Wire codec
|
||||
|
||||
`EncodeOpening` produces (all integers big-endian):
|
||||
|
||||
root[32] | index u64 | beaconLen u32 | beacon | mat(A) | mat(B) | mat(C) | proofLen u32 | proof[32]*
|
||||
|
||||
mat(m) = rows u32 | cols u32 | data[i] as int64-BE (rows*cols entries)
|
||||
|
||||
`DecodeOpening` reverses it, bounds-checking every length. The proof-length
|
||||
field is capped at 64 (a `2^64`-leaf tree is absurd) to prevent OOM from a
|
||||
malicious frame; each matrix dimension is capped at `MaxOpeningDim`.
|
||||
|
||||
## Bounds
|
||||
|
||||
`MaxOpeningDim = 4096`. A full LLM layer is challenged in **slices** no larger
|
||||
than this, so a malicious frame cannot allocate unboundedly inside the
|
||||
precompile. In practice a real-model opening is on the order of a few hundred
|
||||
per side, well under the cap; the constant exists to make the on-chain
|
||||
decoder's worst-case allocation finite and known.
|
||||
|
||||
## Worked example
|
||||
|
||||
Prover commits two matmuls, opens the second, encodes it, hands the bytes to a
|
||||
verifier that decodes and checks:
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
|
||||
"github.com/luxfi/crypto/poi"
|
||||
)
|
||||
|
||||
func main() {
|
||||
// Prover side: build the exact-integer operands (the int8 accumulators in
|
||||
// production; here small int64 values for a runnable demo).
|
||||
A := poi.NewMat(2, 3, []int64{1, 2, 3, 4, 5, 6})
|
||||
B := poi.NewMat(3, 2, []int64{7, 8, 9, 10, 11, 12})
|
||||
|
||||
// Commit two matmuls to the transcript.
|
||||
tr := poi.NewTranscript()
|
||||
_ = tr.Matmul(A, B) // index 0
|
||||
_ = tr.Matmul(A, B) // index 1
|
||||
root := tr.Root()
|
||||
|
||||
// Challenger picks an opening (in production: keccak(beacon || root) mod N).
|
||||
beacon := []byte("beacon:demo")
|
||||
idx := poi.ChallengeIndex(beacon, root, tr.Len())
|
||||
op := tr.Open(idx)
|
||||
|
||||
// Prover ships bytes.
|
||||
wire := poi.EncodeOpening(root, beacon, op)
|
||||
|
||||
// Verifier side: decode + check with k=2 challenge vectors.
|
||||
rRoot, rBeacon, rOp, err := poi.DecodeOpening(wire)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
included, freivaldsOK, err := poi.CheckDecoded(rRoot, rBeacon, rOp, 2)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
fmt.Printf("root=%s included=%v freivaldsOK=%v\n", hex.EncodeToString(rRoot[:8]), included, freivaldsOK)
|
||||
// Expected: included=true freivaldsOK=true
|
||||
}
|
||||
```
|
||||
|
||||
Fraud path: mutate `rOp.C.Data[0]` before `CheckDecoded` and the second
|
||||
return flips to `false` with probability `>= 1 - (1/p)^2 ~ 1 - 2^-122`. The
|
||||
on-chain gate reads that as `included && !freivaldsOK` and slashes.
|
||||
|
||||
## References
|
||||
|
||||
- **LP-5300** — Thinking Chains / Cognitive Consensus / Proof-of-Thought.
|
||||
§Security is what this package operationalizes: the audit-ability promise
|
||||
is only real because a third party can decode an opening and rerun
|
||||
`CheckDecoded` against the on-chain root.
|
||||
- **LP-5301** — AIVMBridge. The `computeproof` precompile consumes exactly
|
||||
the wire format above.
|
||||
- **`luxfi/precompile/aivmbridge/computeproof.go`** — line 17,
|
||||
`import "github.com/luxfi/crypto/poi"`. That file is the sole in-tree caller
|
||||
of `CheckOpening`; keep the two in lockstep when changing the wire.
|
||||
- **`hanzo-engine/src/poi.rs`** — the Rust prover-side mirror. Byte-for-byte
|
||||
identical serialization and keccak fold; an opening produced there verifies
|
||||
here (and on-chain) without re-hashing.
|
||||
@@ -0,0 +1,94 @@
|
||||
package poi
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
// Adversarial (red-team) tests mirroring the Rust battery — each attack the soundness theorems
|
||||
// forbid is exercised against the canonical Go verifier the chain consumes.
|
||||
|
||||
// THE KILLER: a forger crafts C' = A·B + Δ with Δ orthogonal to a GUESSED challenge r1, so
|
||||
// Freivalds passes for r1 — but the real challenge is beacon-derived and unpredictable, so a fresh
|
||||
// r2 catches it. This is why OpeningSeed binds blockhash(commitBlock): the prover cannot pre-fit.
|
||||
func TestAttack_PreFitChallenge(t *testing.T) {
|
||||
a := NewMat(1, 1, []int64{3})
|
||||
n := 4
|
||||
b := NewMat(1, n, []int64{5, -7, 11, -2})
|
||||
honest := ExactMatmul(a, b)
|
||||
r1 := []uint64{1000, 2000, 3000, 4000} // the attacker's guess
|
||||
forged := NewMat(1, n, append([]int64(nil), honest.Data...))
|
||||
forged.Data[0] += int64(r1[1]) // Δ = [r1[1], -r1[0], 0, 0], so Δ·r1 = 0
|
||||
forged.Data[1] -= int64(r1[0])
|
||||
if !Verify(a, b, forged, r1) {
|
||||
t.Fatal("forgery must survive the GUESSED challenge r1 (the premise of the attack)")
|
||||
}
|
||||
r2 := []uint64{217, 999983, 31337, 42} // a beacon-fresh challenge the attacker can't predict
|
||||
if Verify(a, b, forged, r2) {
|
||||
t.Fatal("the forgery must be CAUGHT by a challenge the attacker could not predict")
|
||||
}
|
||||
}
|
||||
|
||||
// A griefer's fabricated opening that was never committed under the honest root cannot prove fraud:
|
||||
// CheckOpening reports not-included, so the gate's provesFraud (= included && !ok) is false.
|
||||
func TestAttack_GrieferCannotFrame(t *testing.T) {
|
||||
s := uint64(0x6717)
|
||||
a := randMat(2, 4, &s)
|
||||
b := randMat(4, 2, &s)
|
||||
honest := NewTranscript()
|
||||
honest.CommitClaimed(a, b, ExactMatmul(a, b)) // the honest prover's root
|
||||
fake := ExactMatmul(a, b)
|
||||
fake.Data[0]++
|
||||
// the griefer encodes an opening over the honest root but with the fabricated C
|
||||
enc := EncodeOpening(honest.Root(), []byte("beacon"), Opening{Index: 0, A: a, B: b, C: fake, Proof: nil})
|
||||
included, ok, err := CheckOpening(enc, 2)
|
||||
if err != nil {
|
||||
t.Fatalf("CheckOpening: %v", err)
|
||||
}
|
||||
if included { // fabricated C is not the committed leaf → not included → cannot slash
|
||||
t.Fatal("a fabricated opening must NOT be included under the honest root")
|
||||
}
|
||||
_ = ok
|
||||
}
|
||||
|
||||
// The wire decoder must be robust: arbitrary/truncated/oversized frames error, never panic.
|
||||
func TestAttack_WireFuzz(t *testing.T) {
|
||||
seeds := []uint64{1, 2, 3, 7, 99, 12345, 0xDEADBEEF, 0xFFFFFFFFFFFFFFFF}
|
||||
for _, sd := range seeds {
|
||||
s := sd
|
||||
// pseudo-random byte frames of varied length
|
||||
for _, n := range []int{0, 1, 5, 33, 64, 200, 1000} {
|
||||
buf := make([]byte, n)
|
||||
for i := range buf {
|
||||
s = s*6364136223846793005 + 1442695040888963407
|
||||
buf[i] = byte(s >> 56)
|
||||
}
|
||||
// must not panic; an error or a clean (false,false) is acceptable.
|
||||
func() {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
t.Fatalf("DecodeOpening panicked on fuzzed frame (seed=%d len=%d): %v", sd, n, r)
|
||||
}
|
||||
}()
|
||||
_, _, _ = CheckOpening(buf, 2)
|
||||
}()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// A frame declaring an over-large matrix is rejected (bounds allocation, not OOM).
|
||||
func TestAttack_OversizedDimsRejected(t *testing.T) {
|
||||
// hand-build a frame: root(32) index(8) beaconLen(4)=0 then A header rows=cols=MaxOpeningDim+1
|
||||
buf := make([]byte, 32+8+4)
|
||||
// A header
|
||||
big := uint32(MaxOpeningDim + 1)
|
||||
hdr := make([]byte, 8)
|
||||
hdr[0] = byte(big >> 24)
|
||||
hdr[1] = byte(big >> 16)
|
||||
hdr[2] = byte(big >> 8)
|
||||
hdr[3] = byte(big)
|
||||
copy(hdr[4:], hdr[0:4])
|
||||
buf = append(buf, hdr...)
|
||||
if _, _, err := CheckOpening(buf, 2); err == nil {
|
||||
t.Fatal("an over-large matrix dimension must be rejected")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,125 @@
|
||||
package poi
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"runtime"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
// TestScale_VerificationThroughput measures how Proof-of-Inference VERIFICATION scales to
|
||||
// 1 / 10 / 100 / 1000 nodes. The key property: a node verifies an opening with NO coordination with
|
||||
// any other node (VerifyOpening is a pure function of the opening). So the network is embarrassingly
|
||||
// parallel — N nodes contribute N× aggregate verification capacity at CONSTANT per-proof latency —
|
||||
// and security strengthens with N (fraud is caught if ANY honest node opens the bad matmul). We
|
||||
// measure real single-node latency on a realistic opening, validate near-linear speedup across this
|
||||
// box's cores, and project the per-node-independent network throughput for each N.
|
||||
func TestScale_VerificationThroughput(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("scale benchmark skipped in -short")
|
||||
}
|
||||
// a realistic challenged slice: a 128-deep matmul (the expensive part Freivalds shines on).
|
||||
op := sampleScaleOpening(64, 128, 64)
|
||||
root := op.root
|
||||
beacon := []byte("beacon:scale")
|
||||
|
||||
// 1) single-node latency: time VerifyOpening over many iterations.
|
||||
const iters = 2000
|
||||
warm := 0
|
||||
start := time.Now()
|
||||
for i := 0; i < iters; i++ {
|
||||
if VerifyOpening(root, beacon, op.opening, 2) {
|
||||
warm++
|
||||
}
|
||||
}
|
||||
single := time.Since(start) / iters
|
||||
if warm != iters {
|
||||
t.Fatalf("the sample opening must verify on every iteration (got %d/%d)", warm, iters)
|
||||
}
|
||||
perNode := float64(time.Second) / float64(single) // proofs/sec per node
|
||||
|
||||
// 2) measured local parallel speedup (validates the independence claim up to cores).
|
||||
cores := runtime.GOMAXPROCS(0)
|
||||
var done int64
|
||||
var wg sync.WaitGroup
|
||||
dur := 300 * time.Millisecond
|
||||
pstart := time.Now()
|
||||
for w := 0; w < cores; w++ {
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
for time.Since(pstart) < dur {
|
||||
if VerifyOpening(root, beacon, op.opening, 2) {
|
||||
atomic.AddInt64(&done, 1)
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
wg.Wait()
|
||||
parallel := float64(done) / time.Since(pstart).Seconds()
|
||||
|
||||
// 3) the scaling table. Network throughput is per-node-INDEPENDENT: each of the N nodes is a
|
||||
// separate machine running VerifyOpening with NO shared state, so aggregate capacity is N× the
|
||||
// single-node rate at constant per-proof latency. (The local multi-goroutine number below is
|
||||
// bounded by this one box's SHARED Go allocator/GC — an artifact of co-locating the goroutines,
|
||||
// NOT present across separate nodes — so it is a floor, not the network figure.)
|
||||
t.Logf("single-node verify latency: %v (%.0f proofs/sec/node)", single, perNode)
|
||||
t.Logf("this box, %d goroutines (shared GC, a local floor): %.0f proofs/sec", cores, parallel)
|
||||
t.Logf("%-8s %-22s %-18s %-26s", "nodes", "aggregate proofs/sec", "per-proof latency", "P(fraud caught, 10% honest)")
|
||||
for _, n := range []int{1, 10, 100, 1000} {
|
||||
agg := perNode * float64(n)
|
||||
// security: a fabricated matmul is caught if ANY honest node opens it; with even 10% of the
|
||||
// N nodes independently spot-checking, the miss probability is 0.9^n → 0.
|
||||
pCaught := 1.0 - pow(0.9, n)
|
||||
t.Logf("%-8d %-22.0f %-18v %.6f", n, agg, single, pCaught)
|
||||
}
|
||||
|
||||
// soundness sanity: a fabricated opening is rejected (so the throughput is over REAL checks).
|
||||
if VerifyOpening(root, beacon, op.fraud, 2) {
|
||||
t.Fatal("a fabricated opening must NOT verify")
|
||||
}
|
||||
}
|
||||
|
||||
func pow(b float64, n int) float64 {
|
||||
r := 1.0
|
||||
for i := 0; i < n; i++ {
|
||||
r *= b
|
||||
}
|
||||
return r
|
||||
}
|
||||
|
||||
type scaleOpening struct {
|
||||
root [32]byte
|
||||
opening Opening
|
||||
fraud Opening
|
||||
}
|
||||
|
||||
// sampleScaleOpening builds a single-matmul transcript of the given dims and returns an honest
|
||||
// opening plus a fabricated one (one output entry flipped).
|
||||
func sampleScaleOpening(t, k, n int) scaleOpening {
|
||||
s := uint64(0x5ca1e)
|
||||
a := randMat(t, k, &s)
|
||||
b := randMat(k, n, &s)
|
||||
tr := NewTranscript()
|
||||
tr.Matmul(a, b)
|
||||
honest := tr.Open(0)
|
||||
|
||||
fakeC := ExactMatmul(a, b)
|
||||
fakeC.Data[7]++
|
||||
ftr := NewTranscript()
|
||||
ftr.CommitClaimed(a, b, fakeC)
|
||||
|
||||
return scaleOpening{root: tr.Root(), opening: honest, fraud: ftr.Open(0)}
|
||||
}
|
||||
|
||||
// A normal Go benchmark for `go test -bench`, reporting ns/op for one verification.
|
||||
func BenchmarkVerifyOpening(b *testing.B) {
|
||||
op := sampleScaleOpening(64, 128, 64)
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
_ = VerifyOpening(op.root, []byte("beacon"), op.opening, 2)
|
||||
}
|
||||
_ = fmt.Sprint
|
||||
}
|
||||
@@ -0,0 +1,211 @@
|
||||
// Proof-of-Inference TRANSCRIPT (Go mirror of hanzo-engine/src/poi_transcript.rs).
|
||||
//
|
||||
// This is the wire that makes "no valid compute proof, no mint" real: a prover commits each
|
||||
// proof-bearing matmul as a keccak Merkle leaf over its exact-integer (A,B,C); the Merkle root is
|
||||
// the transcriptRoot posted on-chain. The off-chain watcher (and the computeattest precompile)
|
||||
// challenge a beacon-selected matmul, the prover OPENS it, and VerifyOpening (1) checks the
|
||||
// operands were the committed ones (Merkle inclusion, the same index-fold as
|
||||
// AICoinMiner._verifyMerkle) and (2) Freivalds-verifies C = A·B (Verify, this package). A
|
||||
// fabricated output fails (2); a swapped-in output fails (1). This file is what gives the Verify
|
||||
// primitive a production caller — the watcher imports it and slashes a bond on a failed opening.
|
||||
//
|
||||
// Byte-for-byte identical to the Rust prover: same DOMAIN tag, same canonical Mat serialization,
|
||||
// same keccak fold, same DeriveChallenges. An opening produced by the engine verifies here and
|
||||
// on-chain without re-hashing.
|
||||
package poi
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
|
||||
"github.com/luxfi/crypto"
|
||||
)
|
||||
|
||||
// DomainMatmulLeaf tags a matmul leaf, disjoint from any other keccak leaf in the bridge.
|
||||
var DomainMatmulLeaf = []byte("hanzo/poi/matmul-leaf/v1")
|
||||
|
||||
// ExactMatmul is the whole-K i64 exact-integer product C = A·B — the proof-bearing GEMM whose
|
||||
// output Freivalds checks with zero false-reject (mirrors poi::exact_matmul).
|
||||
func ExactMatmul(a, b Mat) Mat {
|
||||
if a.Cols != b.Rows {
|
||||
panic("poi: ExactMatmul dim mismatch a.Cols != b.Rows")
|
||||
}
|
||||
t, k, n := a.Rows, a.Cols, b.Cols
|
||||
c := make([]int64, t*n)
|
||||
for i := 0; i < t; i++ {
|
||||
for j := 0; j < n; j++ {
|
||||
var acc int64
|
||||
for x := 0; x < k; x++ {
|
||||
acc += a.Data[i*k+x] * b.Data[x*n+j]
|
||||
}
|
||||
c[i*n+j] = acc
|
||||
}
|
||||
}
|
||||
return NewMat(t, n, c)
|
||||
}
|
||||
|
||||
// matBytes is the canonical serialization: BE32(rows) ‖ BE32(cols) ‖ data[i] as BE64.
|
||||
func matBytes(m Mat) []byte {
|
||||
b := make([]byte, 0, 8+len(m.Data)*8)
|
||||
var u32 [4]byte
|
||||
binary.BigEndian.PutUint32(u32[:], uint32(m.Rows))
|
||||
b = append(b, u32[:]...)
|
||||
binary.BigEndian.PutUint32(u32[:], uint32(m.Cols))
|
||||
b = append(b, u32[:]...)
|
||||
var u64 [8]byte
|
||||
for _, x := range m.Data {
|
||||
binary.BigEndian.PutUint64(u64[:], uint64(x))
|
||||
b = append(b, u64[:]...)
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
||||
// MatmulLeaf binds one matmul: keccak(DOMAIN ‖ mat(A) ‖ mat(B) ‖ mat(C)).
|
||||
func MatmulLeaf(a, b, c Mat) [32]byte {
|
||||
buf := make([]byte, 0, len(DomainMatmulLeaf)+8)
|
||||
buf = append(buf, DomainMatmulLeaf...)
|
||||
buf = append(buf, matBytes(a)...)
|
||||
buf = append(buf, matBytes(b)...)
|
||||
buf = append(buf, matBytes(c)...)
|
||||
var out [32]byte
|
||||
copy(out[:], crypto.Keccak256(buf))
|
||||
return out
|
||||
}
|
||||
|
||||
func hashPair(l, r [32]byte) [32]byte {
|
||||
var buf [64]byte
|
||||
copy(buf[:32], l[:])
|
||||
copy(buf[32:], r[:])
|
||||
var out [32]byte
|
||||
copy(out[:], crypto.Keccak256(buf[:]))
|
||||
return out
|
||||
}
|
||||
|
||||
// MerkleRoot folds leaves into a keccak root; odd levels duplicate the last node; the index fold
|
||||
// matches AICoinMiner._verifyMerkle (even → keccak(node‖sib), odd → keccak(sib‖node)).
|
||||
func MerkleRoot(leaves [][32]byte) [32]byte {
|
||||
if len(leaves) == 0 {
|
||||
panic("poi: MerkleRoot over empty transcript")
|
||||
}
|
||||
level := append([][32]byte(nil), leaves...)
|
||||
for len(level) > 1 {
|
||||
if len(level)%2 == 1 {
|
||||
level = append(level, level[len(level)-1])
|
||||
}
|
||||
next := make([][32]byte, 0, len(level)/2)
|
||||
for i := 0; i < len(level); i += 2 {
|
||||
next = append(next, hashPair(level[i], level[i+1]))
|
||||
}
|
||||
level = next
|
||||
}
|
||||
return level[0]
|
||||
}
|
||||
|
||||
// MerkleProof returns the bottom-up sibling path for the leaf at index.
|
||||
func MerkleProof(leaves [][32]byte, index int) [][32]byte {
|
||||
var proof [][32]byte
|
||||
level := append([][32]byte(nil), leaves...)
|
||||
idx := index
|
||||
for len(level) > 1 {
|
||||
if len(level)%2 == 1 {
|
||||
level = append(level, level[len(level)-1])
|
||||
}
|
||||
var sib int
|
||||
if idx%2 == 0 {
|
||||
sib = idx + 1
|
||||
} else {
|
||||
sib = idx - 1
|
||||
}
|
||||
proof = append(proof, level[sib])
|
||||
next := make([][32]byte, 0, len(level)/2)
|
||||
for i := 0; i < len(level); i += 2 {
|
||||
next = append(next, hashPair(level[i], level[i+1]))
|
||||
}
|
||||
level = next
|
||||
idx /= 2
|
||||
}
|
||||
return proof
|
||||
}
|
||||
|
||||
// MerkleVerify checks a leaf's inclusion under root at index — the exact fold of _verifyMerkle.
|
||||
func MerkleVerify(leaf, root [32]byte, index int, proof [][32]byte) bool {
|
||||
node := leaf
|
||||
idx := index
|
||||
for _, sib := range proof {
|
||||
if idx%2 == 0 {
|
||||
node = hashPair(node, sib)
|
||||
} else {
|
||||
node = hashPair(sib, node)
|
||||
}
|
||||
idx /= 2
|
||||
}
|
||||
return node == root
|
||||
}
|
||||
|
||||
// Opening is what a prover reveals to answer a challenge.
|
||||
type Opening struct {
|
||||
Index int
|
||||
A, B, C Mat
|
||||
Proof [][32]byte
|
||||
}
|
||||
|
||||
// ProofTranscript is an append-only commitment to a forward pass's proof-bearing matmuls.
|
||||
type ProofTranscript struct {
|
||||
leaves [][32]byte
|
||||
ops [][3]Mat
|
||||
}
|
||||
|
||||
func NewTranscript() *ProofTranscript { return &ProofTranscript{} }
|
||||
|
||||
// Matmul runs an exact-integer matmul, commits it, and returns C.
|
||||
func (t *ProofTranscript) Matmul(a, b Mat) Mat {
|
||||
c := ExactMatmul(a, b)
|
||||
t.leaves = append(t.leaves, MatmulLeaf(a, b, c))
|
||||
t.ops = append(t.ops, [3]Mat{a, b, c})
|
||||
return c
|
||||
}
|
||||
|
||||
// CommitClaimed commits a matmul whose C is supplied by the caller (a claimed fast-path output).
|
||||
func (t *ProofTranscript) CommitClaimed(a, b, c Mat) {
|
||||
t.leaves = append(t.leaves, MatmulLeaf(a, b, c))
|
||||
t.ops = append(t.ops, [3]Mat{a, b, c})
|
||||
}
|
||||
|
||||
func (t *ProofTranscript) Len() int { return len(t.leaves) }
|
||||
|
||||
// Root is the transcriptRoot the prover posts on-chain.
|
||||
func (t *ProofTranscript) Root() [32]byte { return MerkleRoot(t.leaves) }
|
||||
|
||||
// Open reveals the matmul at index with its inclusion proof.
|
||||
func (t *ProofTranscript) Open(index int) Opening {
|
||||
o := t.ops[index]
|
||||
return Opening{Index: index, A: o[0], B: o[1], C: o[2], Proof: MerkleProof(t.leaves, index)}
|
||||
}
|
||||
|
||||
// ChallengeIndex selects which matmul to open: keccak(beacon ‖ root) mod len. Unpredictable
|
||||
// before the prover commits root.
|
||||
func ChallengeIndex(beacon []byte, root [32]byte, length int) int {
|
||||
buf := append(append([]byte(nil), beacon...), root[:]...)
|
||||
h := crypto.Keccak256(buf)
|
||||
return int(binary.BigEndian.Uint64(h[:8]) % uint64(length))
|
||||
}
|
||||
|
||||
// OpeningSeed binds the Freivalds challenge to (beacon, root, index): keccak(beacon ‖ root ‖ BE64(index)).
|
||||
func OpeningSeed(beacon []byte, root [32]byte, index int) []byte {
|
||||
var u64 [8]byte
|
||||
binary.BigEndian.PutUint64(u64[:], uint64(index))
|
||||
buf := append(append(append([]byte(nil), beacon...), root[:]...), u64[:]...)
|
||||
return crypto.Keccak256(buf)
|
||||
}
|
||||
|
||||
// VerifyOpening is THE CHALLENGER: (1) the revealed operands are the committed ones (Merkle
|
||||
// inclusion under root), and (2) C = A·B (Freivalds, k beacon-bound vectors). True iff both.
|
||||
// This is the production caller of Verify — the watcher slashes a bond when this returns false.
|
||||
func VerifyOpening(root [32]byte, beacon []byte, op Opening, k int) bool {
|
||||
leaf := MatmulLeaf(op.A, op.B, op.C)
|
||||
if !MerkleVerify(leaf, root, op.Index, op.Proof) {
|
||||
return false
|
||||
}
|
||||
seed := OpeningSeed(beacon, root, op.Index)
|
||||
challenges := DeriveChallenges(seed, op.B.Cols, k)
|
||||
return VerifyMulti(op.A, op.B, op.C, challenges)
|
||||
}
|
||||
@@ -0,0 +1,143 @@
|
||||
package poi
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/hex"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// a tiny deterministic generator so the Go and Rust tests build identical int8-ish matrices.
|
||||
func lcg(s *uint64) int64 {
|
||||
*s = *s*6364136223846793005 + 1442695040888963407
|
||||
return int64((*s>>33)%127) - 63
|
||||
}
|
||||
|
||||
func randMat(rows, cols int, s *uint64) Mat {
|
||||
d := make([]int64, rows*cols)
|
||||
for i := range d {
|
||||
d[i] = lcg(s)
|
||||
}
|
||||
return NewMat(rows, cols, d)
|
||||
}
|
||||
|
||||
func TestExactMatmul(t *testing.T) {
|
||||
a := NewMat(2, 2, []int64{1, 2, 3, 4})
|
||||
b := NewMat(2, 2, []int64{5, 6, 7, 8})
|
||||
c := ExactMatmul(a, b) // [[19,22],[43,50]]
|
||||
want := []int64{19, 22, 43, 50}
|
||||
for i := range want {
|
||||
if c.Data[i] != want[i] {
|
||||
t.Fatalf("ExactMatmul[%d]=%d want %d", i, c.Data[i], want[i])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// The headline property: an honest forward pass answers a beacon-selected matmul challenge, and a
|
||||
// fabricated output is CAUGHT — VerifyOpening is the production caller of Verify.
|
||||
func TestHonestOpeningVerifies(t *testing.T) {
|
||||
s := uint64(0xA1CE)
|
||||
tr := NewTranscript()
|
||||
acts := randMat(4, 64, &s)
|
||||
w1 := randMat(64, 48, &s)
|
||||
h := tr.Matmul(acts, w1)
|
||||
tr.Matmul(h, randMat(48, 32, &s))
|
||||
tr.Matmul(randMat(4, 32, &s), randMat(32, 16, &s))
|
||||
|
||||
root := tr.Root()
|
||||
beacon := []byte("beacon:block-9001")
|
||||
idx := ChallengeIndex(beacon, root, tr.Len())
|
||||
op := tr.Open(idx)
|
||||
if !VerifyOpening(root, beacon, op, 2) {
|
||||
t.Fatal("honest forward pass must pass the opened-matmul challenge")
|
||||
}
|
||||
// every matmul, whichever the beacon picks, must verify
|
||||
for i := 0; i < tr.Len(); i++ {
|
||||
if !VerifyOpening(root, beacon, tr.Open(i), 2) {
|
||||
t.Fatalf("honest matmul %d must verify", i)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestFabricatedOutputCaught(t *testing.T) {
|
||||
s := uint64(0xF00D)
|
||||
tr := NewTranscript()
|
||||
a := randMat(4, 64, &s)
|
||||
b := randMat(64, 32, &s)
|
||||
fake := ExactMatmul(a, b)
|
||||
fake.Data[5]++ // a single output entry never produced by A·B
|
||||
tr.CommitClaimed(a, b, fake)
|
||||
if VerifyOpening(tr.Root(), []byte("beacon:catch"), tr.Open(0), 2) {
|
||||
t.Fatal("a fabricated output (C != A*B) must be CAUGHT")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSwappedRevealCaught(t *testing.T) {
|
||||
s := uint64(0x5151)
|
||||
tr := NewTranscript()
|
||||
a := randMat(4, 48, &s)
|
||||
b := randMat(48, 24, &s)
|
||||
bad := ExactMatmul(a, b)
|
||||
bad.Data[3] -= 2
|
||||
tr.CommitClaimed(a, b, bad) // committed over a wrong C
|
||||
good := ExactMatmul(a, b)
|
||||
op := Opening{Index: 0, A: a, B: b, C: good, Proof: MerkleProof(tr.leaves, 0)}
|
||||
if VerifyOpening(tr.Root(), []byte("beacon:swap"), op, 2) {
|
||||
t.Fatal("revealing operands never committed must fail Merkle inclusion")
|
||||
}
|
||||
}
|
||||
|
||||
func TestMerkleInclusionIndexFold(t *testing.T) {
|
||||
leaves := make([][32]byte, 3)
|
||||
for i := range leaves {
|
||||
leaves[i] = MatmulLeaf(NewMat(1, 1, []int64{int64(i)}), NewMat(1, 1, []int64{1}), NewMat(1, 1, []int64{int64(i)}))
|
||||
}
|
||||
root := MerkleRoot(leaves)
|
||||
for i, leaf := range leaves {
|
||||
if !MerkleVerify(leaf, root, i, MerkleProof(leaves, i)) {
|
||||
t.Fatalf("leaf %d must verify", i)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// CROSS-LANGUAGE PARITY: this exact (seed,n,k) and its first values are pinned identically in the
|
||||
// Rust test (hanzo-engine/src/poi.rs::test_derive_challenges_keccak_golden). If either side's
|
||||
// keccak/encoding drifts, one of the two test suites breaks — the parity is enforced, not assumed.
|
||||
func TestDeriveChallengesGolden(t *testing.T) {
|
||||
seed := []byte("poi-parity-seed")
|
||||
ch := DeriveChallenges(seed, 4, 2)
|
||||
if len(ch) != 2 || len(ch[0]) != 4 {
|
||||
t.Fatalf("shape: got %dx%d", len(ch), len(ch[0]))
|
||||
}
|
||||
// emit the golden so it can be pinned in Rust; assert the structural invariants here.
|
||||
for j := range ch {
|
||||
for i := range ch[j] {
|
||||
if ch[j][i] >= P {
|
||||
t.Fatalf("challenge ch[%d][%d]=%d not reduced mod P", j, i, ch[j][i])
|
||||
}
|
||||
}
|
||||
}
|
||||
// pinned values (computed by this canonical Go keccak path; Rust must match byte-for-byte)
|
||||
wantHex := goldenHex(ch)
|
||||
if wantHex != poiGoldenParity {
|
||||
t.Fatalf("golden derive-challenges drifted:\n got %s\n want %s", wantHex, poiGoldenParity)
|
||||
}
|
||||
}
|
||||
|
||||
// goldenHex serializes the challenge matrix as big-endian u64s for a stable cross-language pin.
|
||||
func goldenHex(ch [][]uint64) string {
|
||||
var b bytes.Buffer
|
||||
for _, row := range ch {
|
||||
for _, v := range row {
|
||||
var u [8]byte
|
||||
for k := 0; k < 8; k++ {
|
||||
u[7-k] = byte(v >> (8 * k))
|
||||
}
|
||||
b.Write(u[:])
|
||||
}
|
||||
}
|
||||
return hex.EncodeToString(b.Bytes())
|
||||
}
|
||||
|
||||
// poiGoldenParity is the canonical DeriveChallenges([]byte("poi-parity-seed"),4,2) value. It is
|
||||
// recomputed and asserted on every run (TestDeriveChallengesGolden) and mirrored in Rust.
|
||||
const poiGoldenParity = "1b3c3bb2aa818c7e0b75620eafd2e35206f000322286987412a71a33da4219920934404c5bc1635a1b934a274b2eaa4801b82451d55b616d142238c54b6a7d51"
|
||||
+180
@@ -0,0 +1,180 @@
|
||||
// Proof-of-Inference opening WIRE CODEC — the bytes a prover/challenger hands to the
|
||||
// `aivmbridge` compute-proof precompile (and that the off-chain watcher exchanges). One encoding,
|
||||
// shared by the Go prover, the precompile, and the Solidity reference (ComputeWitnessLib), so a
|
||||
// real-model-sized opening is verified NATIVELY on-chain (cheap) instead of in gas-bounded EVM
|
||||
// bytecode. CheckOpening is exactly the precompile's payload: decode → (Merkle inclusion,
|
||||
// Freivalds) — the two bits the on-chain gate needs to tell an honest opening from a fraud.
|
||||
package poi
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrShortOpening = errors.New("poi: opening wire frame truncated")
|
||||
ErrOpeningDims = errors.New("poi: opening matrix dims overflow or mismatch")
|
||||
)
|
||||
|
||||
// MaxOpeningDim bounds any opened matrix dimension so a malicious frame cannot allocate
|
||||
// unboundedly inside the precompile. A full layer is challenged in slices ≤ this.
|
||||
const MaxOpeningDim = 4096
|
||||
|
||||
// EncodeOpening serializes (root, beacon, op) into the canonical wire frame:
|
||||
//
|
||||
// root[32] | index u64 | beaconLen u32 | beacon | mat(A) | mat(B) | mat(C) | proofLen u32 | proof[32]*
|
||||
// mat(m) = rows u32 | cols u32 | data[i] int64-BE (rows*cols entries)
|
||||
func EncodeOpening(root [32]byte, beacon []byte, op Opening) []byte {
|
||||
out := make([]byte, 0, 32+8+4+len(beacon)+matWire(op.A)+matWire(op.B)+matWire(op.C)+4+len(op.Proof)*32)
|
||||
out = append(out, root[:]...)
|
||||
out = appendU64(out, uint64(op.Index))
|
||||
out = appendU32(out, uint32(len(beacon)))
|
||||
out = append(out, beacon...)
|
||||
out = appendMat(out, op.A)
|
||||
out = appendMat(out, op.B)
|
||||
out = appendMat(out, op.C)
|
||||
out = appendU32(out, uint32(len(op.Proof)))
|
||||
for _, p := range op.Proof {
|
||||
out = append(out, p[:]...)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// DecodeOpening parses the wire frame produced by EncodeOpening, bounds-checking every length.
|
||||
func DecodeOpening(b []byte) (root [32]byte, beacon []byte, op Opening, err error) {
|
||||
p := 0
|
||||
read := func(n int) ([]byte, bool) {
|
||||
if p+n > len(b) {
|
||||
return nil, false
|
||||
}
|
||||
s := b[p : p+n]
|
||||
p += n
|
||||
return s, true
|
||||
}
|
||||
s, ok := read(32)
|
||||
if !ok {
|
||||
return root, nil, op, ErrShortOpening
|
||||
}
|
||||
copy(root[:], s)
|
||||
if s, ok = read(8); !ok {
|
||||
return root, nil, op, ErrShortOpening
|
||||
}
|
||||
op.Index = int(binary.BigEndian.Uint64(s))
|
||||
if s, ok = read(4); !ok {
|
||||
return root, nil, op, ErrShortOpening
|
||||
}
|
||||
bl := int(binary.BigEndian.Uint32(s))
|
||||
if beacon, ok = read(bl); !ok {
|
||||
return root, nil, op, ErrShortOpening
|
||||
}
|
||||
if op.A, err = readMat(b, &p); err != nil {
|
||||
return root, nil, op, err
|
||||
}
|
||||
if op.B, err = readMat(b, &p); err != nil {
|
||||
return root, nil, op, err
|
||||
}
|
||||
if op.C, err = readMat(b, &p); err != nil {
|
||||
return root, nil, op, err
|
||||
}
|
||||
if s, ok = read(4); !ok {
|
||||
return root, nil, op, ErrShortOpening
|
||||
}
|
||||
pl := int(binary.BigEndian.Uint32(s))
|
||||
if pl > 64 { // a 2^64-leaf tree is absurd; bounds the proof
|
||||
return root, nil, op, ErrOpeningDims
|
||||
}
|
||||
op.Proof = make([][32]byte, pl)
|
||||
for i := 0; i < pl; i++ {
|
||||
if s, ok = read(32); !ok {
|
||||
return root, nil, op, ErrShortOpening
|
||||
}
|
||||
copy(op.Proof[i][:], s)
|
||||
}
|
||||
return root, beacon, op, nil
|
||||
}
|
||||
|
||||
// CheckOpening is THE PRECOMPILE PAYLOAD. It decodes an opening and returns the two facts the gate
|
||||
// needs: `included` (the operands are committed under root — Merkle inclusion) and `freivaldsOK`
|
||||
// (C == A·B for k beacon-bound challenges). The contract derives:
|
||||
// - a genuine opening = included && freivaldsOK (verifyOpening), and
|
||||
// - a fraud proof = included && !freivaldsOK (provesFraud)
|
||||
//
|
||||
// so one native call serves both the honest-attest and the slash paths.
|
||||
func CheckOpening(input []byte, k int) (included bool, freivaldsOK bool, err error) {
|
||||
root, beacon, op, err := DecodeOpening(input)
|
||||
if err != nil {
|
||||
return false, false, err
|
||||
}
|
||||
return CheckDecoded(root, beacon, op, k)
|
||||
}
|
||||
|
||||
// CheckDecoded is CheckOpening on an ALREADY-PARSED opening — the precompile decodes once (to
|
||||
// charge gas by the Freivalds work) and then runs the check without re-parsing.
|
||||
func CheckDecoded(root [32]byte, beacon []byte, op Opening, k int) (included bool, freivaldsOK bool, err error) {
|
||||
if op.A.Cols != op.B.Rows || op.B.Cols != op.C.Cols || op.A.Rows != op.C.Rows {
|
||||
return false, false, ErrOpeningDims
|
||||
}
|
||||
included = MerkleVerify(MatmulLeaf(op.A, op.B, op.C), root, op.Index, op.Proof)
|
||||
if !included {
|
||||
return false, false, nil // not the prover's matmul — neither attest nor fraud
|
||||
}
|
||||
seed := OpeningSeed(beacon, root, op.Index)
|
||||
freivaldsOK = VerifyMulti(op.A, op.B, op.C, DeriveChallenges(seed, op.B.Cols, k))
|
||||
return included, freivaldsOK, nil
|
||||
}
|
||||
|
||||
// FieldOps is the Freivalds work an opening costs — drives the precompile gas (O(tk+kn+tn) per
|
||||
// challenge vector). Pure arithmetic on the decoded dims; safe to call before the heavy check.
|
||||
func FieldOps(op Opening, k int) uint64 {
|
||||
t, kk, n := uint64(op.A.Rows), uint64(op.A.Cols), uint64(op.B.Cols)
|
||||
return uint64(k) * (t*kk + kk*n + t*n)
|
||||
}
|
||||
|
||||
// --- internal wire helpers ---------------------------------------------------
|
||||
|
||||
func appendU32(b []byte, v uint32) []byte {
|
||||
var u [4]byte
|
||||
binary.BigEndian.PutUint32(u[:], v)
|
||||
return append(b, u[:]...)
|
||||
}
|
||||
|
||||
func appendU64(b []byte, v uint64) []byte {
|
||||
var u [8]byte
|
||||
binary.BigEndian.PutUint64(u[:], v)
|
||||
return append(b, u[:]...)
|
||||
}
|
||||
|
||||
func matWire(m Mat) int { return 8 + len(m.Data)*8 }
|
||||
|
||||
func appendMat(b []byte, m Mat) []byte {
|
||||
b = appendU32(b, uint32(m.Rows))
|
||||
b = appendU32(b, uint32(m.Cols))
|
||||
var u [8]byte
|
||||
for _, x := range m.Data {
|
||||
binary.BigEndian.PutUint64(u[:], uint64(x))
|
||||
b = append(b, u[:]...)
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
||||
func readMat(b []byte, p *int) (Mat, error) {
|
||||
if *p+8 > len(b) {
|
||||
return Mat{}, ErrShortOpening
|
||||
}
|
||||
rows := int(binary.BigEndian.Uint32(b[*p : *p+4]))
|
||||
cols := int(binary.BigEndian.Uint32(b[*p+4 : *p+8]))
|
||||
*p += 8
|
||||
if rows <= 0 || cols <= 0 || rows > MaxOpeningDim || cols > MaxOpeningDim {
|
||||
return Mat{}, ErrOpeningDims
|
||||
}
|
||||
n := rows * cols
|
||||
if *p+n*8 > len(b) {
|
||||
return Mat{}, ErrShortOpening
|
||||
}
|
||||
data := make([]int64, n)
|
||||
for i := 0; i < n; i++ {
|
||||
data[i] = int64(binary.BigEndian.Uint64(b[*p : *p+8]))
|
||||
*p += 8
|
||||
}
|
||||
return NewMat(rows, cols, data), nil
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
package poi
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func sampleOpening() (root [32]byte, beacon []byte, op Opening) {
|
||||
s := uint64(0xC0DE)
|
||||
tr := NewTranscript()
|
||||
tr.Matmul(randMat(2, 3, &s), randMat(3, 2, &s))
|
||||
tr.Matmul(randMat(4, 8, &s), randMat(8, 5, &s))
|
||||
tr.Matmul(randMat(2, 2, &s), randMat(2, 2, &s))
|
||||
root = tr.Root()
|
||||
beacon = []byte("beacon:wire")
|
||||
op = tr.Open(ChallengeIndex(beacon, root, tr.Len()))
|
||||
return
|
||||
}
|
||||
|
||||
func TestEncodeDecodeRoundTrip(t *testing.T) {
|
||||
root, beacon, op := sampleOpening()
|
||||
enc := EncodeOpening(root, beacon, op)
|
||||
gotRoot, gotBeacon, gotOp, err := DecodeOpening(enc)
|
||||
if err != nil {
|
||||
t.Fatalf("decode: %v", err)
|
||||
}
|
||||
if gotRoot != root || !bytes.Equal(gotBeacon, beacon) || gotOp.Index != op.Index {
|
||||
t.Fatal("round-trip header mismatch")
|
||||
}
|
||||
if gotOp.A.Rows != op.A.Rows || gotOp.B.Cols != op.B.Cols || gotOp.C.Rows != op.C.Rows {
|
||||
t.Fatal("round-trip matrix shape mismatch")
|
||||
}
|
||||
for i := range op.C.Data {
|
||||
if gotOp.C.Data[i] != op.C.Data[i] {
|
||||
t.Fatalf("round-trip C[%d] mismatch", i)
|
||||
}
|
||||
}
|
||||
if len(gotOp.Proof) != len(op.Proof) {
|
||||
t.Fatal("round-trip proof length mismatch")
|
||||
}
|
||||
}
|
||||
|
||||
// CheckOpening is the precompile payload: honest opening → included && freivaldsOK.
|
||||
func TestCheckOpening_Honest(t *testing.T) {
|
||||
root, beacon, op := sampleOpening()
|
||||
enc := EncodeOpening(root, beacon, op)
|
||||
included, ok, err := CheckOpening(enc, 2)
|
||||
if err != nil || !included || !ok {
|
||||
t.Fatalf("honest opening: included=%v ok=%v err=%v (want true,true,nil)", included, ok, err)
|
||||
}
|
||||
}
|
||||
|
||||
// A fabricated output: included (committed) but Freivalds fails → the precompile reports fraud.
|
||||
func TestCheckOpening_Fraud(t *testing.T) {
|
||||
s := uint64(0xF00D)
|
||||
tr := NewTranscript()
|
||||
a := randMat(3, 5, &s)
|
||||
b := randMat(5, 4, &s)
|
||||
fake := ExactMatmul(a, b)
|
||||
fake.Data[7]++
|
||||
tr.CommitClaimed(a, b, fake)
|
||||
enc := EncodeOpening(tr.Root(), []byte("beacon:fraud"), tr.Open(0))
|
||||
included, ok, err := CheckOpening(enc, 2)
|
||||
if err != nil || !included || ok {
|
||||
t.Fatalf("fraud opening: included=%v ok=%v err=%v (want true,false,nil)", included, ok, err)
|
||||
}
|
||||
}
|
||||
|
||||
// A made-up opening not committed under the root → not included (a griefer cannot fake a slash).
|
||||
func TestCheckOpening_NotIncluded(t *testing.T) {
|
||||
s := uint64(0x9999)
|
||||
a := randMat(2, 2, &s)
|
||||
b := randMat(2, 2, &s)
|
||||
c := ExactMatmul(a, b)
|
||||
var bogusRoot [32]byte
|
||||
bogusRoot[0] = 0xAB
|
||||
enc := EncodeOpening(bogusRoot, []byte("beacon:grief"), Opening{Index: 0, A: a, B: b, C: c, Proof: nil})
|
||||
included, _, err := CheckOpening(enc, 2)
|
||||
if err != nil || included {
|
||||
t.Fatalf("uncommitted opening: included=%v err=%v (want false,nil)", included, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDecodeOpening_Truncated(t *testing.T) {
|
||||
root, beacon, op := sampleOpening()
|
||||
enc := EncodeOpening(root, beacon, op)
|
||||
if _, _, _, err := DecodeOpening(enc[:len(enc)-4]); err == nil {
|
||||
t.Fatal("truncated frame must error, not panic")
|
||||
}
|
||||
if _, _, _, err := DecodeOpening(enc[:10]); err == nil {
|
||||
t.Fatal("tiny frame must error")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
// Package promptseal is the POST-QUANTUM confidentiality envelope for Proof-of-Inference: it seals a
|
||||
// user's prompt to an operator's registered public key so the prompt is NEVER plaintext on the wire
|
||||
// and only the chosen operator — inside its compute boundary — can open it. This closes the audit's
|
||||
// G9: the default inference path handed the operator the plaintext prompt by hash; now the operator
|
||||
// registry carries a recipient KEM key and the requester seals to it.
|
||||
//
|
||||
// PQ: the KEM is X-Wing — the standardized HYBRID of X25519 and ML-KEM-768 (RFC 9180 HPKE, over
|
||||
// github.com/cloudflare/circl). Hybrid means the seal stays confidential if EITHER X25519 OR
|
||||
// ML-KEM-768 holds, so it is secure against a future quantum adversary (ML-KEM-768) while keeping
|
||||
// classical security today (X25519) — the same strict-PQ posture as the staking layer (ML-DSA). The
|
||||
// rest of the proof system is already post-quantum: keccak commitments (PQ-secure) and the
|
||||
// information-theoretic Freivalds soundness bound (holds against an unbounded adversary). The
|
||||
// associated data (aad) binds the ciphertext to its context (the intentID), so a sealed prompt
|
||||
// cannot be replayed under a different request.
|
||||
package promptseal
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"errors"
|
||||
|
||||
"github.com/cloudflare/circl/hpke"
|
||||
)
|
||||
|
||||
// Info domain-separates this use of HPKE from any other in the stack.
|
||||
var info = []byte("hanzo/poi/prompt-seal/v1")
|
||||
|
||||
// suite: X-Wing (X25519 + ML-KEM-768) KEM, HKDF-SHA256 KDF, ChaCha20-Poly1305 AEAD.
|
||||
var suite = hpke.NewSuite(hpke.KEM_XWING, hpke.KDF_HKDF_SHA256, hpke.AEAD_ChaCha20Poly1305)
|
||||
|
||||
// kemScheme is the X-Wing hybrid KEM.
|
||||
var kemScheme = hpke.KEM_XWING.Scheme()
|
||||
|
||||
// encLen is the hybrid encapsulated-key length prefixed to every sealed blob (X25519 share ‖
|
||||
// ML-KEM-768 ciphertext).
|
||||
var encLen = kemScheme.CiphertextSize()
|
||||
|
||||
var (
|
||||
ErrShortSealed = errors.New("promptseal: sealed blob shorter than the encapsulated key")
|
||||
ErrBadKey = errors.New("promptseal: malformed operator key")
|
||||
)
|
||||
|
||||
// GenerateOperatorKey returns an operator's (publicKey, privateKey) for its registry entry. The
|
||||
// public key is published on-chain; the private key never leaves the operator's compute boundary.
|
||||
func GenerateOperatorKey() (pub, priv []byte, err error) {
|
||||
pk, sk, err := kemScheme.GenerateKeyPair()
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
pub, err = pk.MarshalBinary()
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
priv, err = sk.MarshalBinary()
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
return pub, priv, nil
|
||||
}
|
||||
|
||||
// SealPrompt seals `prompt` to an operator's `operatorPub`, binding `aad` (context, e.g. intentID).
|
||||
// Output is `enc ‖ ciphertext`. Semantically secure: two seals of the same prompt differ, and the
|
||||
// blob reveals nothing about the prompt to anyone without the operator's private key.
|
||||
func SealPrompt(operatorPub, prompt, aad []byte) ([]byte, error) {
|
||||
pk, err := kemScheme.UnmarshalBinaryPublicKey(operatorPub)
|
||||
if err != nil {
|
||||
return nil, ErrBadKey
|
||||
}
|
||||
sender, err := suite.NewSender(pk, info)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
enc, sealer, err := sender.Setup(rand.Reader)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ct, err := sealer.Seal(prompt, aad)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return append(append([]byte(nil), enc...), ct...), nil
|
||||
}
|
||||
|
||||
// OpenPrompt opens a sealed blob with the operator's `operatorPriv` and the same `aad`. Returns an
|
||||
// error if the key is wrong, the blob was tampered, or the aad does not match (AEAD authentication).
|
||||
func OpenPrompt(operatorPriv, sealed, aad []byte) ([]byte, error) {
|
||||
if len(sealed) < encLen {
|
||||
return nil, ErrShortSealed
|
||||
}
|
||||
sk, err := kemScheme.UnmarshalBinaryPrivateKey(operatorPriv)
|
||||
if err != nil {
|
||||
return nil, ErrBadKey
|
||||
}
|
||||
receiver, err := suite.NewReceiver(sk, info)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
opener, err := receiver.Setup(sealed[:encLen])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return opener.Open(sealed[encLen:], aad)
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
package promptseal
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestSealRoundTrip(t *testing.T) {
|
||||
pub, priv, err := GenerateOperatorKey()
|
||||
if err != nil {
|
||||
t.Fatalf("keygen: %v", err)
|
||||
}
|
||||
prompt := []byte("what is the airspeed velocity of an unladen swallow?")
|
||||
aad := []byte("intent:0xabc123")
|
||||
sealed, err := SealPrompt(pub, prompt, aad)
|
||||
if err != nil {
|
||||
t.Fatalf("seal: %v", err)
|
||||
}
|
||||
got, err := OpenPrompt(priv, sealed, aad)
|
||||
if err != nil {
|
||||
t.Fatalf("open: %v", err)
|
||||
}
|
||||
if !bytes.Equal(got, prompt) {
|
||||
t.Fatal("round-trip mismatch")
|
||||
}
|
||||
}
|
||||
|
||||
// ADVERSARIAL: a different operator (or any third party) cannot open a prompt sealed to someone else.
|
||||
func TestWrongKeyCannotOpen(t *testing.T) {
|
||||
pubA, _, _ := GenerateOperatorKey()
|
||||
_, privB, _ := GenerateOperatorKey() // a DIFFERENT operator
|
||||
sealed, err := SealPrompt(pubA, []byte("secret prompt"), []byte("ctx"))
|
||||
if err != nil {
|
||||
t.Fatalf("seal: %v", err)
|
||||
}
|
||||
if _, err := OpenPrompt(privB, sealed, []byte("ctx")); err == nil {
|
||||
t.Fatal("a non-recipient operator must NOT be able to open the prompt")
|
||||
}
|
||||
}
|
||||
|
||||
// ADVERSARIAL: flipping any ciphertext byte makes Open fail (AEAD integrity).
|
||||
func TestTamperedCiphertextRejected(t *testing.T) {
|
||||
pub, priv, _ := GenerateOperatorKey()
|
||||
sealed, _ := SealPrompt(pub, []byte("integrity matters"), []byte("ctx"))
|
||||
for _, i := range []int{0, len(sealed) / 2, len(sealed) - 1} {
|
||||
bad := append([]byte(nil), sealed...)
|
||||
bad[i] ^= 0x01
|
||||
if _, err := OpenPrompt(priv, bad, []byte("ctx")); err == nil {
|
||||
t.Fatalf("a tampered byte at %d must be rejected", i)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ADVERSARIAL: the aad binds context — opening under a different aad (a replay to another request)
|
||||
// fails authentication.
|
||||
func TestWrongAADRejected(t *testing.T) {
|
||||
pub, priv, _ := GenerateOperatorKey()
|
||||
sealed, _ := SealPrompt(pub, []byte("bound to intent A"), []byte("intent:A"))
|
||||
if _, err := OpenPrompt(priv, sealed, []byte("intent:B")); err == nil {
|
||||
t.Fatal("a sealed prompt must not open under a different aad (no cross-request replay)")
|
||||
}
|
||||
}
|
||||
|
||||
// ADVERSARIAL: a passive observer learns nothing. The sealed blob does not contain the plaintext,
|
||||
// and two seals of the SAME prompt differ (semantic security / fresh ephemeral key each time).
|
||||
func TestObserverLearnsNothing(t *testing.T) {
|
||||
pub, _, _ := GenerateOperatorKey()
|
||||
prompt := []byte("a very distinctive plaintext marker XYZZY")
|
||||
s1, _ := SealPrompt(pub, prompt, []byte("ctx"))
|
||||
s2, _ := SealPrompt(pub, prompt, []byte("ctx"))
|
||||
if bytes.Contains(s1, prompt) {
|
||||
t.Fatal("the sealed blob must not contain the plaintext")
|
||||
}
|
||||
if bytes.Equal(s1, s2) {
|
||||
t.Fatal("two seals of the same prompt must differ (semantic security)")
|
||||
}
|
||||
}
|
||||
|
||||
func TestMalformedKeyRejected(t *testing.T) {
|
||||
pub, _, _ := GenerateOperatorKey()
|
||||
if _, err := SealPrompt([]byte{1, 2, 3}, []byte("x"), nil); err == nil {
|
||||
t.Fatal("a malformed operator public key must be rejected")
|
||||
}
|
||||
sealed, _ := SealPrompt(pub, []byte("x"), nil)
|
||||
if _, err := OpenPrompt([]byte{1, 2, 3}, sealed, nil); err == nil {
|
||||
t.Fatal("a malformed operator private key must be rejected")
|
||||
}
|
||||
}
|
||||
+8
-158
@@ -5,7 +5,6 @@ import (
|
||||
"testing"
|
||||
|
||||
"github.com/luxfi/crypto/threshold"
|
||||
_ "github.com/luxfi/crypto/threshold/bls" // Register BLS scheme
|
||||
)
|
||||
|
||||
func TestSigner(t *testing.T) {
|
||||
@@ -21,15 +20,12 @@ func TestSigner(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatalf("BLS sign failed: %v", err)
|
||||
}
|
||||
|
||||
if !signer.VerifyBLS(message, sig) {
|
||||
t.Fatalf("BLS verification failed")
|
||||
}
|
||||
|
||||
if signer.VerifyBLS([]byte("wrong"), sig) {
|
||||
t.Fatalf("BLS should not verify wrong message")
|
||||
}
|
||||
|
||||
t.Logf("BLS signature size: %d bytes", len(sig))
|
||||
t.Logf("BLS public key size: %d bytes", len(signer.GetBLSPublicKey()))
|
||||
})
|
||||
@@ -39,204 +35,58 @@ func TestSigner(t *testing.T) {
|
||||
if len(seed) != 32 {
|
||||
t.Fatalf("Threshold seed should be 32 bytes, got %d", len(seed))
|
||||
}
|
||||
t.Logf("Threshold seed: %x...", seed[:8])
|
||||
})
|
||||
|
||||
// Uninitialized threshold state exercises ONLY the crypto/threshold
|
||||
// interface — no concrete scheme. The BLS-backed integration test lives
|
||||
// in luxfi/threshold/scheme/bls (importing crypto/signer from there keeps
|
||||
// crypto free of any luxfi/threshold dependency — no module cycle).
|
||||
t.Run("ThresholdNotInitialized", func(t *testing.T) {
|
||||
if signer.HasThresholdKey() {
|
||||
t.Fatal("Signer should not have threshold key initially")
|
||||
}
|
||||
|
||||
if signer.ThresholdSchemeID() != threshold.SchemeUnknown {
|
||||
t.Fatalf("Expected SchemeUnknown, got %v", signer.ThresholdSchemeID())
|
||||
}
|
||||
|
||||
if signer.ThresholdIndex() != -1 {
|
||||
t.Fatalf("Expected index -1, got %d", signer.ThresholdIndex())
|
||||
}
|
||||
|
||||
if signer.ThresholdPublicShare() != nil {
|
||||
t.Fatal("Expected nil public share")
|
||||
}
|
||||
|
||||
if signer.ThresholdGroupKey() != nil {
|
||||
t.Fatal("Expected nil group key")
|
||||
}
|
||||
|
||||
// Operations should fail without threshold key
|
||||
ctx := context.Background()
|
||||
_, err := signer.SignThresholdShare(ctx, message, []int{0})
|
||||
if err != threshold.ErrNotInitialized {
|
||||
if _, err := signer.SignThresholdShare(ctx, message, []int{0}); err != threshold.ErrNotInitialized {
|
||||
t.Fatalf("Expected ErrNotInitialized, got %v", err)
|
||||
}
|
||||
|
||||
_, err = signer.AggregateThresholdShares(ctx, message, nil)
|
||||
if err != threshold.ErrNotInitialized {
|
||||
if _, err := signer.AggregateThresholdShares(ctx, message, nil); err != threshold.ErrNotInitialized {
|
||||
t.Fatalf("Expected ErrNotInitialized, got %v", err)
|
||||
}
|
||||
|
||||
err = signer.VerifyThresholdShare(message, nil, nil)
|
||||
if err != threshold.ErrNotInitialized {
|
||||
if err := signer.VerifyThresholdShare(message, nil, nil); err != threshold.ErrNotInitialized {
|
||||
t.Fatalf("Expected ErrNotInitialized, got %v", err)
|
||||
}
|
||||
|
||||
if signer.VerifyThreshold(message, nil) {
|
||||
t.Fatal("Expected false without threshold key")
|
||||
}
|
||||
|
||||
if signer.VerifyThresholdBytes(message, nil) {
|
||||
t.Fatal("Expected false without threshold key")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestSignerWithThreshold(t *testing.T) {
|
||||
// Get BLS scheme
|
||||
scheme, err := threshold.GetScheme(threshold.SchemeBLS)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to get BLS scheme: %v", err)
|
||||
}
|
||||
|
||||
// Create key shares using trusted dealer
|
||||
config := threshold.DealerConfig{
|
||||
Threshold: 1, // 2-of-3
|
||||
TotalParties: 3,
|
||||
}
|
||||
|
||||
dealer, err := scheme.NewTrustedDealer(config)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create dealer: %v", err)
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
shares, groupKey, err := dealer.GenerateShares(ctx)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to generate shares: %v", err)
|
||||
}
|
||||
|
||||
// Create signers with threshold key shares
|
||||
signers := make([]*Signer, len(shares))
|
||||
for i, share := range shares {
|
||||
signer, err := NewSignerWithThreshold(share)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create signer %d: %v", i, err)
|
||||
}
|
||||
signers[i] = signer
|
||||
}
|
||||
|
||||
t.Run("ThresholdKeyInfo", func(t *testing.T) {
|
||||
for i, signer := range signers {
|
||||
if !signer.HasThresholdKey() {
|
||||
t.Fatalf("Signer %d should have threshold key", i)
|
||||
}
|
||||
|
||||
if signer.ThresholdSchemeID() != threshold.SchemeBLS {
|
||||
t.Fatalf("Signer %d: expected BLS, got %v", i, signer.ThresholdSchemeID())
|
||||
}
|
||||
|
||||
if signer.ThresholdIndex() != i {
|
||||
t.Fatalf("Signer %d: expected index %d, got %d", i, i, signer.ThresholdIndex())
|
||||
}
|
||||
|
||||
if len(signer.ThresholdPublicShare()) == 0 {
|
||||
t.Fatalf("Signer %d: empty public share", i)
|
||||
}
|
||||
|
||||
if !signer.ThresholdGroupKey().Equal(groupKey) {
|
||||
t.Fatalf("Signer %d: group key mismatch", i)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("ThresholdSigning", func(t *testing.T) {
|
||||
message := []byte("Test threshold message")
|
||||
participants := []int{0, 1} // 2 of 3
|
||||
|
||||
// Create signature shares
|
||||
signatureShares := make([]threshold.SignatureShare, len(participants))
|
||||
for i, idx := range participants {
|
||||
share, err := signers[idx].SignThresholdShare(ctx, message, participants)
|
||||
if err != nil {
|
||||
t.Fatalf("Signer %d failed to sign: %v", idx, err)
|
||||
}
|
||||
signatureShares[i] = share
|
||||
|
||||
// Verify individual share - each share should verify against its public share.
|
||||
// Current BLS threshold uses uniform shares (same secret); full Shamir
|
||||
// polynomial evaluation is in luxfi/threshold.
|
||||
err = signers[0].VerifyThresholdShare(message, share, signers[idx].ThresholdPublicShare())
|
||||
if err != nil {
|
||||
t.Fatalf("Share verification failed for signer %d: %v", idx, err)
|
||||
}
|
||||
}
|
||||
|
||||
// Aggregate shares
|
||||
signature, err := signers[0].AggregateThresholdShares(ctx, message, signatureShares)
|
||||
if err != nil {
|
||||
t.Fatalf("Aggregation failed: %v", err)
|
||||
}
|
||||
|
||||
// The BLS threshold implementation in this package uses uniform shares.
|
||||
// Full Shamir polynomial evaluation and Lagrange interpolation are
|
||||
// provided by luxfi/threshold/protocols/frost.
|
||||
t.Logf("Threshold signature size: %d bytes", len(signature.Bytes()))
|
||||
t.Logf("Note: Full threshold signature verification requires Lagrange interpolation")
|
||||
|
||||
// Verify a single signer's share works individually
|
||||
// (all shares use the same secret in the uniform-share scheme).
|
||||
singleShare := []threshold.SignatureShare{signatureShares[0]}
|
||||
singleSig, err := signers[0].AggregateThresholdShares(ctx, message, singleShare)
|
||||
if err != nil {
|
||||
t.Fatalf("Single share aggregation failed: %v", err)
|
||||
}
|
||||
|
||||
// Single signature should verify against the group key
|
||||
// (each share signs with the full secret in the uniform-share scheme).
|
||||
if !signers[0].VerifyThreshold(message, singleSig) {
|
||||
t.Log("Single signature does not verify (expected with uniform shares)")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("SetThresholdKeyShare", func(t *testing.T) {
|
||||
// Create a new signer without threshold key
|
||||
signer, err := NewSigner()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if signer.HasThresholdKey() {
|
||||
t.Fatal("New signer should not have threshold key")
|
||||
}
|
||||
|
||||
// Set threshold key
|
||||
err = signer.SetThresholdKeyShare(shares[0])
|
||||
if err != nil {
|
||||
t.Fatalf("SetThresholdKeyShare failed: %v", err)
|
||||
}
|
||||
|
||||
if !signer.HasThresholdKey() {
|
||||
t.Fatal("Signer should have threshold key after setting")
|
||||
}
|
||||
|
||||
if signer.ThresholdIndex() != 0 {
|
||||
t.Fatalf("Expected index 0, got %d", signer.ThresholdIndex())
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func BenchmarkSigner(b *testing.B) {
|
||||
signer, err := NewSigner()
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
|
||||
message := []byte("Benchmark message for performance testing")
|
||||
|
||||
b.Run("BLS_Sign", func(b *testing.B) {
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
_, err := signer.SignBLS(message)
|
||||
if err != nil {
|
||||
if _, err := signer.SignBLS(message); err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
BIN
Binary file not shown.
@@ -1,883 +0,0 @@
|
||||
// Copyright (C) 2025, Lux Industries Inc. All rights reserved.
|
||||
// See the file LICENSE for licensing terms.
|
||||
|
||||
// Package bls implements BLS threshold signatures.
|
||||
//
|
||||
// BLS threshold signatures provide:
|
||||
// - Non-interactive signature aggregation
|
||||
// - Constant-size signatures regardless of threshold
|
||||
// - Efficient verification
|
||||
//
|
||||
// This implementation uses Shamir's Secret Sharing for key distribution
|
||||
// and Lagrange interpolation for signature combination.
|
||||
package bls
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
|
||||
"github.com/cloudflare/circl/ecc/bls12381"
|
||||
"github.com/cloudflare/circl/ecc/bls12381/ff"
|
||||
"github.com/luxfi/crypto/bls"
|
||||
"github.com/luxfi/crypto/threshold"
|
||||
)
|
||||
|
||||
func init() {
|
||||
threshold.RegisterScheme(&Scheme{})
|
||||
}
|
||||
|
||||
// Constants for BLS threshold scheme.
|
||||
const (
|
||||
// KeyShareSize is the serialized size of a BLS key share.
|
||||
// 32 bytes secret + 48 bytes public share + 48 bytes group key + 4 bytes metadata
|
||||
KeyShareSize = 132
|
||||
|
||||
// SignatureShareSize is the serialized size of a signature share.
|
||||
// 96 bytes signature + 4 bytes index
|
||||
SignatureShareSize = 100
|
||||
|
||||
// SignatureSize is the serialized size of the final signature.
|
||||
SignatureSize = 96
|
||||
|
||||
// PublicKeySize is the serialized size of the group public key.
|
||||
PublicKeySize = 48
|
||||
)
|
||||
|
||||
// Scheme implements the BLS threshold signature scheme.
|
||||
type Scheme struct{}
|
||||
|
||||
// ID returns the scheme identifier.
|
||||
func (s *Scheme) ID() threshold.SchemeID {
|
||||
return threshold.SchemeBLS
|
||||
}
|
||||
|
||||
// Name returns the human-readable name.
|
||||
func (s *Scheme) Name() string {
|
||||
return "BLS Threshold"
|
||||
}
|
||||
|
||||
// KeyShareSize returns the serialized key share size.
|
||||
func (s *Scheme) KeyShareSize() int {
|
||||
return KeyShareSize
|
||||
}
|
||||
|
||||
// SignatureShareSize returns the serialized signature share size.
|
||||
func (s *Scheme) SignatureShareSize() int {
|
||||
return SignatureShareSize
|
||||
}
|
||||
|
||||
// SignatureSize returns the serialized final signature size.
|
||||
func (s *Scheme) SignatureSize() int {
|
||||
return SignatureSize
|
||||
}
|
||||
|
||||
// PublicKeySize returns the serialized public key size.
|
||||
func (s *Scheme) PublicKeySize() int {
|
||||
return PublicKeySize
|
||||
}
|
||||
|
||||
// NewDKG creates a new distributed key generation instance.
|
||||
// BLS DKG is not yet implemented here. Use threshold/protocols/frost for production DKG.
|
||||
func (s *Scheme) NewDKG(config threshold.DKGConfig) (threshold.DKG, error) {
|
||||
return nil, fmt.Errorf("BLS DKG not yet implemented: use threshold/protocols/frost for production DKG")
|
||||
}
|
||||
|
||||
// NewTrustedDealer creates a trusted dealer for centralized key generation.
|
||||
func (s *Scheme) NewTrustedDealer(config threshold.DealerConfig) (threshold.TrustedDealer, error) {
|
||||
if err := config.Validate(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &TrustedDealer{
|
||||
config: config,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// NewSigner creates a signer from a key share.
|
||||
func (s *Scheme) NewSigner(share threshold.KeyShare) (threshold.Signer, error) {
|
||||
ks, ok := share.(*KeyShare)
|
||||
if !ok {
|
||||
return nil, threshold.ErrInvalidKeyShare
|
||||
}
|
||||
return &Signer{
|
||||
keyShare: ks,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// NewAggregator creates a signature aggregator for the given group key.
|
||||
func (s *Scheme) NewAggregator(groupKey threshold.PublicKey) (threshold.Aggregator, error) {
|
||||
pk, ok := groupKey.(*PublicKey)
|
||||
if !ok {
|
||||
return nil, threshold.ErrInvalidPublicKey
|
||||
}
|
||||
return &Aggregator{
|
||||
groupKey: pk,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// NewVerifier creates a signature verifier for the given group key.
|
||||
func (s *Scheme) NewVerifier(groupKey threshold.PublicKey) (threshold.Verifier, error) {
|
||||
pk, ok := groupKey.(*PublicKey)
|
||||
if !ok {
|
||||
return nil, threshold.ErrInvalidPublicKey
|
||||
}
|
||||
return &Verifier{
|
||||
groupKey: pk,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// ParseKeyShare deserializes a key share from bytes.
|
||||
func (s *Scheme) ParseKeyShare(data []byte) (threshold.KeyShare, error) {
|
||||
if len(data) < KeyShareSize {
|
||||
return nil, threshold.ErrDataTooShort
|
||||
}
|
||||
return parseKeyShare(data)
|
||||
}
|
||||
|
||||
// ParsePublicKey deserializes a group public key from bytes.
|
||||
func (s *Scheme) ParsePublicKey(data []byte) (threshold.PublicKey, error) {
|
||||
if len(data) != PublicKeySize {
|
||||
return nil, threshold.ErrInvalidPublicKey
|
||||
}
|
||||
|
||||
pk, err := bls.PublicKeyFromCompressedBytes(data)
|
||||
if err != nil {
|
||||
return nil, threshold.ErrInvalidPublicKey
|
||||
}
|
||||
|
||||
return &PublicKey{
|
||||
key: pk,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// ParseSignatureShare deserializes a signature share from bytes.
|
||||
func (s *Scheme) ParseSignatureShare(data []byte) (threshold.SignatureShare, error) {
|
||||
if len(data) < SignatureShareSize {
|
||||
return nil, threshold.ErrDataTooShort
|
||||
}
|
||||
return parseSignatureShare(data)
|
||||
}
|
||||
|
||||
// ParseSignature deserializes a final signature from bytes.
|
||||
func (s *Scheme) ParseSignature(data []byte) (threshold.Signature, error) {
|
||||
if len(data) != SignatureSize {
|
||||
return nil, threshold.ErrInvalidSignature
|
||||
}
|
||||
|
||||
sig, err := bls.SignatureFromBytes(data)
|
||||
if err != nil {
|
||||
return nil, threshold.ErrInvalidSignature
|
||||
}
|
||||
|
||||
return &Signature{
|
||||
sig: sig,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// PublicKey represents a BLS threshold group public key.
|
||||
type PublicKey struct {
|
||||
key *bls.PublicKey
|
||||
}
|
||||
|
||||
// Bytes serializes the public key.
|
||||
func (pk *PublicKey) Bytes() []byte {
|
||||
return bls.PublicKeyToCompressedBytes(pk.key)
|
||||
}
|
||||
|
||||
// Equal returns true if the public keys are identical.
|
||||
func (pk *PublicKey) Equal(other threshold.PublicKey) bool {
|
||||
otherPK, ok := other.(*PublicKey)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
return bytes.Equal(pk.Bytes(), otherPK.Bytes())
|
||||
}
|
||||
|
||||
// SchemeID returns the scheme identifier.
|
||||
func (pk *PublicKey) SchemeID() threshold.SchemeID {
|
||||
return threshold.SchemeBLS
|
||||
}
|
||||
|
||||
// KeyShare represents a party's BLS key share.
|
||||
type KeyShare struct {
|
||||
index int
|
||||
threshold int
|
||||
totalParties int
|
||||
secretShare *bls.SecretKey
|
||||
publicShare *bls.PublicKey
|
||||
groupKey *PublicKey
|
||||
}
|
||||
|
||||
// Index returns the party index.
|
||||
func (ks *KeyShare) Index() int {
|
||||
return ks.index
|
||||
}
|
||||
|
||||
// GroupKey returns the group public key.
|
||||
func (ks *KeyShare) GroupKey() threshold.PublicKey {
|
||||
return ks.groupKey
|
||||
}
|
||||
|
||||
// PublicShare returns this party's public key share.
|
||||
func (ks *KeyShare) PublicShare() []byte {
|
||||
return bls.PublicKeyToCompressedBytes(ks.publicShare)
|
||||
}
|
||||
|
||||
// Threshold returns the signing threshold.
|
||||
func (ks *KeyShare) Threshold() int {
|
||||
return ks.threshold
|
||||
}
|
||||
|
||||
// TotalParties returns the total number of parties.
|
||||
func (ks *KeyShare) TotalParties() int {
|
||||
return ks.totalParties
|
||||
}
|
||||
|
||||
// Bytes serializes the key share.
|
||||
func (ks *KeyShare) Bytes() []byte {
|
||||
buf := make([]byte, KeyShareSize)
|
||||
|
||||
// Secret share (32 bytes)
|
||||
copy(buf[0:32], bls.SecretKeyToBytes(ks.secretShare))
|
||||
|
||||
// Public share (48 bytes)
|
||||
copy(buf[32:80], bls.PublicKeyToCompressedBytes(ks.publicShare))
|
||||
|
||||
// Group key (48 bytes)
|
||||
copy(buf[80:128], ks.groupKey.Bytes())
|
||||
|
||||
// Metadata (4 bytes: index, threshold, total)
|
||||
buf[128] = byte(ks.index)
|
||||
buf[129] = byte(ks.threshold)
|
||||
buf[130] = byte(ks.totalParties >> 8)
|
||||
buf[131] = byte(ks.totalParties)
|
||||
|
||||
return buf
|
||||
}
|
||||
|
||||
// SchemeID returns the scheme identifier.
|
||||
func (ks *KeyShare) SchemeID() threshold.SchemeID {
|
||||
return threshold.SchemeBLS
|
||||
}
|
||||
|
||||
// parseKeyShare deserializes a key share.
|
||||
func parseKeyShare(data []byte) (*KeyShare, error) {
|
||||
if len(data) < KeyShareSize {
|
||||
return nil, threshold.ErrDataTooShort
|
||||
}
|
||||
|
||||
// Secret share
|
||||
sk, err := bls.SecretKeyFromBytes(data[0:32])
|
||||
if err != nil {
|
||||
return nil, threshold.ErrInvalidKeyShare
|
||||
}
|
||||
|
||||
// Public share
|
||||
pk, err := bls.PublicKeyFromCompressedBytes(data[32:80])
|
||||
if err != nil {
|
||||
return nil, threshold.ErrInvalidKeyShare
|
||||
}
|
||||
|
||||
// Group key
|
||||
gk, err := bls.PublicKeyFromCompressedBytes(data[80:128])
|
||||
if err != nil {
|
||||
return nil, threshold.ErrInvalidKeyShare
|
||||
}
|
||||
|
||||
return &KeyShare{
|
||||
index: int(data[128]),
|
||||
threshold: int(data[129]),
|
||||
totalParties: int(data[130])<<8 | int(data[131]),
|
||||
secretShare: sk,
|
||||
publicShare: pk,
|
||||
groupKey: &PublicKey{key: gk},
|
||||
}, nil
|
||||
}
|
||||
|
||||
// SignatureShare represents a BLS signature share.
|
||||
type SignatureShare struct {
|
||||
index int
|
||||
sig *bls.Signature
|
||||
}
|
||||
|
||||
// Index returns the party index.
|
||||
func (ss *SignatureShare) Index() int {
|
||||
return ss.index
|
||||
}
|
||||
|
||||
// Bytes serializes the signature share.
|
||||
func (ss *SignatureShare) Bytes() []byte {
|
||||
buf := make([]byte, SignatureShareSize)
|
||||
binary.BigEndian.PutUint32(buf[0:4], uint32(ss.index))
|
||||
copy(buf[4:], bls.SignatureToBytes(ss.sig))
|
||||
return buf
|
||||
}
|
||||
|
||||
// SchemeID returns the scheme identifier.
|
||||
func (ss *SignatureShare) SchemeID() threshold.SchemeID {
|
||||
return threshold.SchemeBLS
|
||||
}
|
||||
|
||||
// parseSignatureShare deserializes a signature share.
|
||||
func parseSignatureShare(data []byte) (*SignatureShare, error) {
|
||||
if len(data) < SignatureShareSize {
|
||||
return nil, threshold.ErrDataTooShort
|
||||
}
|
||||
|
||||
index := int(binary.BigEndian.Uint32(data[0:4]))
|
||||
sig, err := bls.SignatureFromBytes(data[4:100])
|
||||
if err != nil {
|
||||
return nil, threshold.ErrInvalidSignatureShare
|
||||
}
|
||||
|
||||
return &SignatureShare{
|
||||
index: index,
|
||||
sig: sig,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Signature represents a final BLS threshold signature.
|
||||
type Signature struct {
|
||||
sig *bls.Signature
|
||||
}
|
||||
|
||||
// Bytes serializes the signature.
|
||||
func (s *Signature) Bytes() []byte {
|
||||
return bls.SignatureToBytes(s.sig)
|
||||
}
|
||||
|
||||
// SchemeID returns the scheme identifier.
|
||||
func (s *Signature) SchemeID() threshold.SchemeID {
|
||||
return threshold.SchemeBLS
|
||||
}
|
||||
|
||||
// TrustedDealer generates key shares using a trusted dealer.
|
||||
type TrustedDealer struct {
|
||||
config threshold.DealerConfig
|
||||
}
|
||||
|
||||
// GenerateShares creates all key shares and the group public key.
|
||||
func (d *TrustedDealer) GenerateShares(ctx context.Context) ([]threshold.KeyShare, threshold.PublicKey, error) {
|
||||
rng := d.config.Rand
|
||||
if rng == nil {
|
||||
rng = rand.Reader
|
||||
}
|
||||
|
||||
// Generate master secret key
|
||||
masterSK, err := bls.NewSecretKey()
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
// Compute group public key
|
||||
groupPK := masterSK.PublicKey()
|
||||
|
||||
// Generate polynomial coefficients for Shamir secret sharing
|
||||
// f(x) = a_0 + a_1*x + a_2*x^2 + ... + a_{t-1}*x^{t-1}
|
||||
// where a_0 = masterSK
|
||||
// For t-of-n threshold, we need degree t-1 (so t points uniquely determine it)
|
||||
coefficients := make([]*bls.SecretKey, d.config.Threshold)
|
||||
coefficients[0] = masterSK
|
||||
|
||||
for i := 1; i < d.config.Threshold; i++ {
|
||||
coef, err := bls.NewSecretKey()
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
coefficients[i] = coef
|
||||
}
|
||||
|
||||
// Generate shares by evaluating polynomial at points 1, 2, ..., n
|
||||
shares := make([]threshold.KeyShare, d.config.TotalParties)
|
||||
for i := 0; i < d.config.TotalParties; i++ {
|
||||
// Evaluate polynomial at x = i+1
|
||||
shareSecret := evaluatePolynomial(coefficients, i+1)
|
||||
|
||||
// Compute public share
|
||||
sharePK := shareSecret.PublicKey()
|
||||
|
||||
shares[i] = &KeyShare{
|
||||
index: i,
|
||||
threshold: d.config.Threshold,
|
||||
totalParties: d.config.TotalParties,
|
||||
secretShare: shareSecret,
|
||||
publicShare: sharePK,
|
||||
groupKey: &PublicKey{key: groupPK},
|
||||
}
|
||||
}
|
||||
|
||||
return shares, &PublicKey{key: groupPK}, nil
|
||||
}
|
||||
|
||||
// evaluatePolynomial evaluates a polynomial at a given point using BLS12-381 scalar field arithmetic.
|
||||
// f(x) = a_0 + a_1*x + a_2*x^2 + ... + a_t*x^t using Horner's method.
|
||||
func evaluatePolynomial(coefficients []*bls.SecretKey, x int) *bls.SecretKey {
|
||||
if len(coefficients) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Convert coefficients to scalars
|
||||
scalars := make([]*ff.Scalar, len(coefficients))
|
||||
for i, coef := range coefficients {
|
||||
scalars[i] = new(ff.Scalar)
|
||||
scalars[i].SetBytes(bls.SecretKeyToBytes(coef))
|
||||
}
|
||||
|
||||
// Convert x to scalar
|
||||
xScalar := new(ff.Scalar)
|
||||
xScalar.SetUint64(uint64(x))
|
||||
|
||||
// Evaluate using Horner's method: f(x) = a_n + x*(a_{n-1} + x*(a_{n-2} + ...))
|
||||
result := new(ff.Scalar)
|
||||
result.Set(scalars[len(scalars)-1])
|
||||
|
||||
for i := len(scalars) - 2; i >= 0; i-- {
|
||||
// result = result * x + a_i
|
||||
result.Mul(result, xScalar)
|
||||
result.Add(result, scalars[i])
|
||||
}
|
||||
|
||||
// Convert back to SecretKey
|
||||
resultBytes, _ := result.MarshalBinary()
|
||||
sk, err := bls.SecretKeyFromBytes(resultBytes)
|
||||
if err != nil {
|
||||
// Fallback to constant term if conversion fails
|
||||
return coefficients[0]
|
||||
}
|
||||
return sk
|
||||
}
|
||||
|
||||
// computeLagrangeCoefficients computes Lagrange coefficients at x=0 for the given indices.
|
||||
// λ_j(0) = Π_{i≠j} (0 - x_i) / (x_j - x_i) = Π_{i≠j} x_i / (x_i - x_j)
|
||||
func computeLagrangeCoefficients(indices []int) []*ff.Scalar {
|
||||
n := len(indices)
|
||||
coeffs := make([]*ff.Scalar, n)
|
||||
|
||||
for j := 0; j < n; j++ {
|
||||
xj := new(ff.Scalar)
|
||||
xj.SetUint64(uint64(indices[j]))
|
||||
|
||||
// numerator = Π_{i≠j} x_i
|
||||
numerator := new(ff.Scalar)
|
||||
numerator.SetOne()
|
||||
|
||||
// denominator = Π_{i≠j} (x_j - x_i)
|
||||
denominator := new(ff.Scalar)
|
||||
denominator.SetOne()
|
||||
|
||||
for i := 0; i < n; i++ {
|
||||
if i == j {
|
||||
continue
|
||||
}
|
||||
xi := new(ff.Scalar)
|
||||
xi.SetUint64(uint64(indices[i]))
|
||||
|
||||
// numerator *= x_i
|
||||
numerator.Mul(numerator, xi)
|
||||
|
||||
// diff = x_j - x_i
|
||||
diff := new(ff.Scalar)
|
||||
diff.Set(xj)
|
||||
diff.Sub(diff, xi)
|
||||
|
||||
// denominator *= diff
|
||||
denominator.Mul(denominator, diff)
|
||||
}
|
||||
|
||||
// λ_j = numerator / denominator = numerator * denominator^(-1)
|
||||
coeffs[j] = new(ff.Scalar)
|
||||
coeffs[j].Inv(denominator)
|
||||
coeffs[j].Mul(coeffs[j], numerator)
|
||||
}
|
||||
|
||||
return coeffs
|
||||
}
|
||||
|
||||
// isScalarOne checks if a scalar equals 1.
|
||||
func isScalarOne(s *ff.Scalar) bool {
|
||||
one := new(ff.Scalar)
|
||||
one.SetOne()
|
||||
return s.IsEqual(one) == 1
|
||||
}
|
||||
|
||||
// aggregateWithLagrange aggregates signature shares with Lagrange coefficients.
|
||||
// Uses circl's G2 for scalar multiplication on signature points.
|
||||
func aggregateWithLagrange(shares []*SignatureShare, coeffs []*ff.Scalar) (*Signature, error) {
|
||||
if len(shares) != len(coeffs) {
|
||||
return nil, errors.New("shares and coefficients length mismatch")
|
||||
}
|
||||
|
||||
// Use circl's G2 for scalar multiplication
|
||||
// circl bls12381 uses G2 for signatures
|
||||
var result bls12381G2
|
||||
result.SetIdentity()
|
||||
|
||||
for i, share := range shares {
|
||||
// Get signature bytes and parse as G2 point
|
||||
sigBytes := bls.SignatureToBytes(share.sig)
|
||||
|
||||
var sigPoint bls12381G2
|
||||
if err := sigPoint.SetBytes(sigBytes); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Multiply signature by Lagrange coefficient
|
||||
scaled := sigPoint.ScalarMult(coeffs[i])
|
||||
|
||||
// Add to result
|
||||
result.Add(&result, scaled)
|
||||
}
|
||||
|
||||
// Convert back to bls.Signature
|
||||
resultBytes := result.BytesCompressed()
|
||||
sig, err := bls.SignatureFromBytes(resultBytes)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &Signature{sig: sig}, nil
|
||||
}
|
||||
|
||||
// bls12381G2 wraps circl's G2 for signature point operations.
|
||||
type bls12381G2 struct {
|
||||
point bls12381.G2
|
||||
}
|
||||
|
||||
func (g *bls12381G2) SetIdentity() {
|
||||
g.point.SetIdentity()
|
||||
}
|
||||
|
||||
func (g *bls12381G2) SetBytes(data []byte) error {
|
||||
return g.point.SetBytes(data)
|
||||
}
|
||||
|
||||
func (g *bls12381G2) Add(a, b *bls12381G2) {
|
||||
g.point.Add(&a.point, &b.point)
|
||||
}
|
||||
|
||||
func (g *bls12381G2) ScalarMult(scalar *ff.Scalar) *bls12381G2 {
|
||||
result := new(bls12381G2)
|
||||
result.point.ScalarMult(scalar, &g.point)
|
||||
return result
|
||||
}
|
||||
|
||||
func (g *bls12381G2) BytesCompressed() []byte {
|
||||
return g.point.BytesCompressed()
|
||||
}
|
||||
|
||||
// DKG implements distributed key generation for BLS threshold.
|
||||
type DKG struct {
|
||||
config threshold.DKGConfig
|
||||
round int
|
||||
secretShare *bls.SecretKey
|
||||
publicShare *bls.PublicKey
|
||||
groupKey *PublicKey
|
||||
complete bool
|
||||
}
|
||||
|
||||
// Round1 generates the first round DKG message.
|
||||
func (d *DKG) Round1(ctx context.Context) (threshold.DKGMessage, error) {
|
||||
if d.round != 0 {
|
||||
return nil, threshold.ErrInvalidRound
|
||||
}
|
||||
|
||||
rng := d.config.Rand
|
||||
if rng == nil {
|
||||
rng = rand.Reader
|
||||
}
|
||||
|
||||
// Generate secret share
|
||||
sk, err := bls.NewSecretKey()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
d.secretShare = sk
|
||||
d.publicShare = sk.PublicKey()
|
||||
|
||||
// Create commitment message
|
||||
msg := &DKGMessage{
|
||||
round: 1,
|
||||
fromParty: d.config.PartyIndex,
|
||||
data: bls.PublicKeyToCompressedBytes(d.publicShare),
|
||||
}
|
||||
|
||||
d.round = 1
|
||||
return msg, nil
|
||||
}
|
||||
|
||||
// Round2 processes Round1 messages and generates Round2 messages.
|
||||
func (d *DKG) Round2(ctx context.Context, round1Messages map[int]threshold.DKGMessage) (threshold.DKGMessage, error) {
|
||||
if d.round != 1 {
|
||||
return nil, threshold.ErrInvalidRound
|
||||
}
|
||||
|
||||
// Verify we have messages from all parties
|
||||
if len(round1Messages) < d.config.TotalParties-1 {
|
||||
return nil, threshold.ErrMissingMessage
|
||||
}
|
||||
|
||||
// In a full implementation, we would:
|
||||
// 1. Verify commitments
|
||||
// 2. Generate Feldman VSS shares
|
||||
// 3. Create encrypted shares for each party
|
||||
|
||||
msg := &DKGMessage{
|
||||
round: 2,
|
||||
fromParty: d.config.PartyIndex,
|
||||
data: bls.PublicKeyToCompressedBytes(d.publicShare),
|
||||
}
|
||||
|
||||
d.round = 2
|
||||
return msg, nil
|
||||
}
|
||||
|
||||
// Round3 processes Round2 messages and generates the final key share.
|
||||
func (d *DKG) Round3(ctx context.Context, round2Messages map[int]threshold.DKGMessage) (threshold.KeyShare, error) {
|
||||
if d.round != 2 {
|
||||
return nil, threshold.ErrInvalidRound
|
||||
}
|
||||
|
||||
// In a full implementation, we would:
|
||||
// 1. Verify received shares
|
||||
// 2. Combine shares to get our secret share
|
||||
// 3. Compute group public key from commitments
|
||||
|
||||
// For now, create a key share with our generated secret
|
||||
keyShare := &KeyShare{
|
||||
index: d.config.PartyIndex,
|
||||
threshold: d.config.Threshold,
|
||||
totalParties: d.config.TotalParties,
|
||||
secretShare: d.secretShare,
|
||||
publicShare: d.publicShare,
|
||||
groupKey: &PublicKey{key: d.publicShare}, // Simplified - should be aggregated
|
||||
}
|
||||
|
||||
d.complete = true
|
||||
return keyShare, nil
|
||||
}
|
||||
|
||||
// NumRounds returns the number of DKG rounds.
|
||||
func (d *DKG) NumRounds() int {
|
||||
return 3
|
||||
}
|
||||
|
||||
// GroupKey returns the group public key after DKG completes.
|
||||
func (d *DKG) GroupKey() threshold.PublicKey {
|
||||
if !d.complete {
|
||||
return nil
|
||||
}
|
||||
return d.groupKey
|
||||
}
|
||||
|
||||
// DKGMessage represents a DKG protocol message.
|
||||
type DKGMessage struct {
|
||||
round int
|
||||
fromParty int
|
||||
data []byte
|
||||
}
|
||||
|
||||
// Round returns the round number.
|
||||
func (m *DKGMessage) Round() int {
|
||||
return m.round
|
||||
}
|
||||
|
||||
// FromParty returns the sender's party index.
|
||||
func (m *DKGMessage) FromParty() int {
|
||||
return m.fromParty
|
||||
}
|
||||
|
||||
// Bytes serializes the message.
|
||||
func (m *DKGMessage) Bytes() []byte {
|
||||
buf := make([]byte, 8+len(m.data))
|
||||
binary.BigEndian.PutUint32(buf[0:4], uint32(m.round))
|
||||
binary.BigEndian.PutUint32(buf[4:8], uint32(m.fromParty))
|
||||
copy(buf[8:], m.data)
|
||||
return buf
|
||||
}
|
||||
|
||||
// Signer creates BLS signature shares.
|
||||
type Signer struct {
|
||||
keyShare *KeyShare
|
||||
}
|
||||
|
||||
// Index returns the party index.
|
||||
func (s *Signer) Index() int {
|
||||
return s.keyShare.index
|
||||
}
|
||||
|
||||
// PublicShare returns this party's public key share.
|
||||
func (s *Signer) PublicShare() []byte {
|
||||
return s.keyShare.PublicShare()
|
||||
}
|
||||
|
||||
// NonceGen generates a nonce commitment.
|
||||
// BLS threshold doesn't require nonces for aggregation.
|
||||
func (s *Signer) NonceGen(ctx context.Context) (threshold.NonceCommitment, threshold.NonceState, error) {
|
||||
// BLS is non-interactive, no nonce needed
|
||||
return nil, nil, nil
|
||||
}
|
||||
|
||||
// SignShare creates a signature share for the given message.
|
||||
func (s *Signer) SignShare(ctx context.Context, message []byte, signers []int, nonce threshold.NonceState) (threshold.SignatureShare, error) {
|
||||
// Sign with the secret share
|
||||
sig, err := s.keyShare.secretShare.Sign(message)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &SignatureShare{
|
||||
index: s.keyShare.index,
|
||||
sig: sig,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// KeyShare returns the underlying key share.
|
||||
func (s *Signer) KeyShare() threshold.KeyShare {
|
||||
return s.keyShare
|
||||
}
|
||||
|
||||
// Aggregator combines BLS signature shares.
|
||||
type Aggregator struct {
|
||||
groupKey *PublicKey
|
||||
}
|
||||
|
||||
// Aggregate combines signature shares into a final signature using Lagrange interpolation.
|
||||
// For t-of-n threshold signatures, this computes the group signature by multiplying
|
||||
// each signature share by its Lagrange coefficient and aggregating.
|
||||
func (a *Aggregator) Aggregate(ctx context.Context, message []byte, shares []threshold.SignatureShare, commitments []threshold.NonceCommitment) (threshold.Signature, error) {
|
||||
if len(shares) == 0 {
|
||||
return nil, threshold.ErrInsufficientShares
|
||||
}
|
||||
|
||||
// Get participant indices for Lagrange interpolation
|
||||
indices := make([]int, len(shares))
|
||||
sigShares := make([]*SignatureShare, len(shares))
|
||||
for i, share := range shares {
|
||||
ss, ok := share.(*SignatureShare)
|
||||
if !ok {
|
||||
return nil, threshold.ErrInvalidSignatureShare
|
||||
}
|
||||
sigShares[i] = ss
|
||||
indices[i] = ss.index + 1 // Lagrange uses 1-indexed points
|
||||
}
|
||||
|
||||
// Compute Lagrange coefficients at x=0 for each participant
|
||||
lagrangeCoeffs := computeLagrangeCoefficients(indices)
|
||||
|
||||
// For each signature share, multiply by its Lagrange coefficient
|
||||
// In BLS, "multiplication" of signature by scalar is done by exponentiating
|
||||
// the signature point by the scalar. However, blst doesn't expose this directly.
|
||||
//
|
||||
// For BLS threshold with Lagrange interpolation, we use the property that:
|
||||
// σ = Σ λ_i * σ_i (where λ_i are Lagrange coefficients)
|
||||
//
|
||||
// Since we can't directly scale G2 points with blst's public API,
|
||||
// we use the aggregate-then-verify approach that works when all signers
|
||||
// are honest and using proper Shamir shares.
|
||||
//
|
||||
// For a proper implementation with scalar multiplication on G2, you would need
|
||||
// to use lower-level blst or circl APIs.
|
||||
blsSigs := make([]*bls.Signature, len(shares))
|
||||
for i := range sigShares {
|
||||
blsSigs[i] = sigShares[i].sig
|
||||
}
|
||||
|
||||
// If lagrange coefficients are all 1 (n-of-n case), simple aggregation works
|
||||
allOnes := true
|
||||
for _, coef := range lagrangeCoeffs {
|
||||
if !isScalarOne(coef) {
|
||||
allOnes = false
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if allOnes {
|
||||
// n-of-n case: simple aggregation
|
||||
aggSig, err := bls.AggregateSignatures(blsSigs)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &Signature{sig: aggSig}, nil
|
||||
}
|
||||
|
||||
// For t-of-n (t < n), we need weighted aggregation
|
||||
// Use circl's G2 for scalar multiplication
|
||||
return aggregateWithLagrange(sigShares, lagrangeCoeffs)
|
||||
}
|
||||
|
||||
// VerifyShare verifies a single signature share.
|
||||
func (a *Aggregator) VerifyShare(message []byte, share threshold.SignatureShare, publicShare []byte) error {
|
||||
ss, ok := share.(*SignatureShare)
|
||||
if !ok {
|
||||
return threshold.ErrInvalidSignatureShare
|
||||
}
|
||||
|
||||
pk, err := bls.PublicKeyFromCompressedBytes(publicShare)
|
||||
if err != nil {
|
||||
return threshold.ErrInvalidPublicKey
|
||||
}
|
||||
|
||||
if !bls.Verify(pk, ss.sig, message) {
|
||||
return threshold.ErrSignatureVerificationFailed
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GroupKey returns the group public key.
|
||||
func (a *Aggregator) GroupKey() threshold.PublicKey {
|
||||
return a.groupKey
|
||||
}
|
||||
|
||||
// Verifier verifies BLS threshold signatures.
|
||||
type Verifier struct {
|
||||
groupKey *PublicKey
|
||||
}
|
||||
|
||||
// Verify checks if a signature is valid.
|
||||
func (v *Verifier) Verify(message []byte, signature threshold.Signature) bool {
|
||||
sig, ok := signature.(*Signature)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
|
||||
return bls.Verify(v.groupKey.key, sig.sig, message)
|
||||
}
|
||||
|
||||
// VerifyBytes verifies a serialized signature.
|
||||
func (v *Verifier) VerifyBytes(message, signature []byte) bool {
|
||||
sig, err := bls.SignatureFromBytes(signature)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
return bls.Verify(v.groupKey.key, sig, message)
|
||||
}
|
||||
|
||||
// GroupKey returns the group public key.
|
||||
func (v *Verifier) GroupKey() threshold.PublicKey {
|
||||
return v.groupKey
|
||||
}
|
||||
|
||||
// Ensure interfaces are implemented
|
||||
var (
|
||||
_ threshold.Scheme = (*Scheme)(nil)
|
||||
_ threshold.PublicKey = (*PublicKey)(nil)
|
||||
_ threshold.KeyShare = (*KeyShare)(nil)
|
||||
_ threshold.SignatureShare = (*SignatureShare)(nil)
|
||||
_ threshold.Signature = (*Signature)(nil)
|
||||
_ threshold.TrustedDealer = (*TrustedDealer)(nil)
|
||||
_ threshold.DKG = (*DKG)(nil)
|
||||
_ threshold.DKGMessage = (*DKGMessage)(nil)
|
||||
_ threshold.Signer = (*Signer)(nil)
|
||||
_ threshold.Aggregator = (*Aggregator)(nil)
|
||||
_ threshold.Verifier = (*Verifier)(nil)
|
||||
)
|
||||
|
||||
// Compilation check for unused variables
|
||||
var _ io.Reader = rand.Reader
|
||||
var _ = errors.New
|
||||
@@ -1,478 +0,0 @@
|
||||
// Copyright (C) 2025, Lux Industries Inc. All rights reserved.
|
||||
// See the file LICENSE for licensing terms.
|
||||
|
||||
package bls
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/luxfi/crypto/threshold"
|
||||
)
|
||||
|
||||
func TestSchemeRegistration(t *testing.T) {
|
||||
// Verify scheme is registered via init()
|
||||
if !threshold.HasScheme(threshold.SchemeBLS) {
|
||||
t.Fatal("BLS scheme not registered")
|
||||
}
|
||||
|
||||
scheme, err := threshold.GetScheme(threshold.SchemeBLS)
|
||||
if err != nil {
|
||||
t.Fatalf("GetScheme failed: %v", err)
|
||||
}
|
||||
|
||||
if scheme.ID() != threshold.SchemeBLS {
|
||||
t.Errorf("scheme ID = %v, want %v", scheme.ID(), threshold.SchemeBLS)
|
||||
}
|
||||
|
||||
if scheme.Name() != "BLS Threshold" {
|
||||
t.Errorf("scheme Name = %v, want %v", scheme.Name(), "BLS Threshold")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSchemeConstants(t *testing.T) {
|
||||
scheme := &Scheme{}
|
||||
|
||||
if scheme.KeyShareSize() != KeyShareSize {
|
||||
t.Errorf("KeyShareSize = %d, want %d", scheme.KeyShareSize(), KeyShareSize)
|
||||
}
|
||||
|
||||
if scheme.SignatureShareSize() != SignatureShareSize {
|
||||
t.Errorf("SignatureShareSize = %d, want %d", scheme.SignatureShareSize(), SignatureShareSize)
|
||||
}
|
||||
|
||||
if scheme.SignatureSize() != SignatureSize {
|
||||
t.Errorf("SignatureSize = %d, want %d", scheme.SignatureSize(), SignatureSize)
|
||||
}
|
||||
|
||||
if scheme.PublicKeySize() != PublicKeySize {
|
||||
t.Errorf("PublicKeySize = %d, want %d", scheme.PublicKeySize(), PublicKeySize)
|
||||
}
|
||||
}
|
||||
|
||||
func TestTrustedDealerKeyGeneration(t *testing.T) {
|
||||
scheme := &Scheme{}
|
||||
|
||||
config := threshold.DealerConfig{
|
||||
Threshold: 2, // 3-of-5 threshold
|
||||
TotalParties: 5,
|
||||
}
|
||||
|
||||
dealer, err := scheme.NewTrustedDealer(config)
|
||||
if err != nil {
|
||||
t.Fatalf("NewTrustedDealer failed: %v", err)
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
shares, groupKey, err := dealer.GenerateShares(ctx)
|
||||
if err != nil {
|
||||
t.Fatalf("GenerateShares failed: %v", err)
|
||||
}
|
||||
|
||||
// Verify correct number of shares
|
||||
if len(shares) != config.TotalParties {
|
||||
t.Errorf("got %d shares, want %d", len(shares), config.TotalParties)
|
||||
}
|
||||
|
||||
// Verify each share
|
||||
for i, share := range shares {
|
||||
if share.Index() != i {
|
||||
t.Errorf("share %d has index %d", i, share.Index())
|
||||
}
|
||||
|
||||
if share.Threshold() != config.Threshold {
|
||||
t.Errorf("share %d threshold = %d, want %d", i, share.Threshold(), config.Threshold)
|
||||
}
|
||||
|
||||
if share.TotalParties() != config.TotalParties {
|
||||
t.Errorf("share %d totalParties = %d, want %d", i, share.TotalParties(), config.TotalParties)
|
||||
}
|
||||
|
||||
if share.SchemeID() != threshold.SchemeBLS {
|
||||
t.Errorf("share %d schemeID = %v, want %v", i, share.SchemeID(), threshold.SchemeBLS)
|
||||
}
|
||||
|
||||
// Verify group key matches
|
||||
if !share.GroupKey().Equal(groupKey) {
|
||||
t.Errorf("share %d group key mismatch", i)
|
||||
}
|
||||
}
|
||||
|
||||
// Verify group key
|
||||
if groupKey == nil {
|
||||
t.Fatal("groupKey is nil")
|
||||
}
|
||||
|
||||
if groupKey.SchemeID() != threshold.SchemeBLS {
|
||||
t.Errorf("groupKey schemeID = %v, want %v", groupKey.SchemeID(), threshold.SchemeBLS)
|
||||
}
|
||||
}
|
||||
|
||||
func TestKeyShareSerialization(t *testing.T) {
|
||||
scheme := &Scheme{}
|
||||
|
||||
config := threshold.DealerConfig{
|
||||
Threshold: 1,
|
||||
TotalParties: 3,
|
||||
}
|
||||
|
||||
dealer, err := scheme.NewTrustedDealer(config)
|
||||
if err != nil {
|
||||
t.Fatalf("NewTrustedDealer failed: %v", err)
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
shares, _, err := dealer.GenerateShares(ctx)
|
||||
if err != nil {
|
||||
t.Fatalf("GenerateShares failed: %v", err)
|
||||
}
|
||||
|
||||
// Test serialization/deserialization
|
||||
for i, share := range shares {
|
||||
data := share.Bytes()
|
||||
if len(data) != KeyShareSize {
|
||||
t.Errorf("share %d bytes length = %d, want %d", i, len(data), KeyShareSize)
|
||||
}
|
||||
|
||||
parsed, err := scheme.ParseKeyShare(data)
|
||||
if err != nil {
|
||||
t.Errorf("ParseKeyShare failed for share %d: %v", i, err)
|
||||
continue
|
||||
}
|
||||
|
||||
if parsed.Index() != share.Index() {
|
||||
t.Errorf("parsed index = %d, want %d", parsed.Index(), share.Index())
|
||||
}
|
||||
|
||||
if parsed.Threshold() != share.Threshold() {
|
||||
t.Errorf("parsed threshold = %d, want %d", parsed.Threshold(), share.Threshold())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestSignerCreation(t *testing.T) {
|
||||
scheme := &Scheme{}
|
||||
|
||||
config := threshold.DealerConfig{
|
||||
Threshold: 1,
|
||||
TotalParties: 3,
|
||||
}
|
||||
|
||||
dealer, err := scheme.NewTrustedDealer(config)
|
||||
if err != nil {
|
||||
t.Fatalf("NewTrustedDealer failed: %v", err)
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
shares, _, err := dealer.GenerateShares(ctx)
|
||||
if err != nil {
|
||||
t.Fatalf("GenerateShares failed: %v", err)
|
||||
}
|
||||
|
||||
// Create signers from shares
|
||||
for i, share := range shares {
|
||||
signer, err := scheme.NewSigner(share)
|
||||
if err != nil {
|
||||
t.Errorf("NewSigner failed for share %d: %v", i, err)
|
||||
continue
|
||||
}
|
||||
|
||||
if signer.Index() != i {
|
||||
t.Errorf("signer index = %d, want %d", signer.Index(), i)
|
||||
}
|
||||
|
||||
// Verify public share is not empty
|
||||
pubShare := signer.PublicShare()
|
||||
if len(pubShare) == 0 {
|
||||
t.Errorf("signer %d has empty public share", i)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestSigningAndAggregation(t *testing.T) {
|
||||
scheme := &Scheme{}
|
||||
|
||||
config := threshold.DealerConfig{
|
||||
Threshold: 1, // 2-of-3 threshold
|
||||
TotalParties: 3,
|
||||
}
|
||||
|
||||
dealer, err := scheme.NewTrustedDealer(config)
|
||||
if err != nil {
|
||||
t.Fatalf("NewTrustedDealer failed: %v", err)
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
shares, groupKey, err := dealer.GenerateShares(ctx)
|
||||
if err != nil {
|
||||
t.Fatalf("GenerateShares failed: %v", err)
|
||||
}
|
||||
|
||||
// Create signers
|
||||
signers := make([]threshold.Signer, len(shares))
|
||||
for i, share := range shares {
|
||||
signer, err := scheme.NewSigner(share)
|
||||
if err != nil {
|
||||
t.Fatalf("NewSigner failed: %v", err)
|
||||
}
|
||||
signers[i] = signer
|
||||
}
|
||||
|
||||
// Create aggregator
|
||||
aggregator, err := scheme.NewAggregator(groupKey)
|
||||
if err != nil {
|
||||
t.Fatalf("NewAggregator failed: %v", err)
|
||||
}
|
||||
|
||||
// Sign a message with first 2 signers (threshold+1)
|
||||
message := []byte("test message for threshold signing")
|
||||
participantIndices := []int{0, 1}
|
||||
|
||||
signatureShares := make([]threshold.SignatureShare, len(participantIndices))
|
||||
for i, idx := range participantIndices {
|
||||
share, err := signers[idx].SignShare(ctx, message, participantIndices, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("SignShare failed for signer %d: %v", idx, err)
|
||||
}
|
||||
signatureShares[i] = share
|
||||
}
|
||||
|
||||
// Verify individual shares
|
||||
for i, idx := range participantIndices {
|
||||
err := aggregator.VerifyShare(message, signatureShares[i], signers[idx].PublicShare())
|
||||
if err != nil {
|
||||
t.Errorf("VerifyShare failed for signer %d: %v", idx, err)
|
||||
}
|
||||
}
|
||||
|
||||
// Aggregate shares
|
||||
signature, err := aggregator.Aggregate(ctx, message, signatureShares, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("Aggregate failed: %v", err)
|
||||
}
|
||||
|
||||
// Verify signature is not nil
|
||||
if signature == nil {
|
||||
t.Fatal("signature is nil")
|
||||
}
|
||||
|
||||
// Verify scheme ID
|
||||
if signature.SchemeID() != threshold.SchemeBLS {
|
||||
t.Errorf("signature schemeID = %v, want %v", signature.SchemeID(), threshold.SchemeBLS)
|
||||
}
|
||||
}
|
||||
|
||||
func TestVerifier(t *testing.T) {
|
||||
scheme := &Scheme{}
|
||||
|
||||
config := threshold.DealerConfig{
|
||||
Threshold: 1,
|
||||
TotalParties: 3,
|
||||
}
|
||||
|
||||
dealer, err := scheme.NewTrustedDealer(config)
|
||||
if err != nil {
|
||||
t.Fatalf("NewTrustedDealer failed: %v", err)
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
shares, groupKey, err := dealer.GenerateShares(ctx)
|
||||
if err != nil {
|
||||
t.Fatalf("GenerateShares failed: %v", err)
|
||||
}
|
||||
|
||||
// Create verifier
|
||||
verifier, err := scheme.NewVerifier(groupKey)
|
||||
if err != nil {
|
||||
t.Fatalf("NewVerifier failed: %v", err)
|
||||
}
|
||||
|
||||
// Verify group key matches
|
||||
if !verifier.GroupKey().Equal(groupKey) {
|
||||
t.Error("verifier group key mismatch")
|
||||
}
|
||||
|
||||
// Create a signer and sign
|
||||
signer, err := scheme.NewSigner(shares[0])
|
||||
if err != nil {
|
||||
t.Fatalf("NewSigner failed: %v", err)
|
||||
}
|
||||
|
||||
message := []byte("test message")
|
||||
share, err := signer.SignShare(ctx, message, []int{0}, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("SignShare failed: %v", err)
|
||||
}
|
||||
|
||||
// Create aggregator and aggregate (single share for test)
|
||||
aggregator, err := scheme.NewAggregator(groupKey)
|
||||
if err != nil {
|
||||
t.Fatalf("NewAggregator failed: %v", err)
|
||||
}
|
||||
|
||||
sig, err := aggregator.Aggregate(ctx, message, []threshold.SignatureShare{share}, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("Aggregate failed: %v", err)
|
||||
}
|
||||
|
||||
// Verify the signature
|
||||
if !verifier.Verify(message, sig) {
|
||||
t.Error("Verify returned false, expected true")
|
||||
}
|
||||
|
||||
// Verify with bytes
|
||||
if !verifier.VerifyBytes(message, sig.Bytes()) {
|
||||
t.Error("VerifyBytes returned false, expected true")
|
||||
}
|
||||
|
||||
// Verify wrong message fails
|
||||
if verifier.Verify([]byte("wrong message"), sig) {
|
||||
t.Error("Verify returned true for wrong message, expected false")
|
||||
}
|
||||
}
|
||||
|
||||
func TestDKGConfig(t *testing.T) {
|
||||
scheme := &Scheme{}
|
||||
|
||||
// NewDKG is intentionally unimplemented — it must return an error
|
||||
// directing callers to threshold/protocols/frost for production DKG.
|
||||
config := threshold.DKGConfig{
|
||||
Threshold: 1,
|
||||
TotalParties: 3,
|
||||
PartyIndex: 0,
|
||||
}
|
||||
|
||||
_, err := scheme.NewDKG(config)
|
||||
if err == nil {
|
||||
t.Fatal("NewDKG should return an error (not implemented)")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSignatureShareSerialization(t *testing.T) {
|
||||
scheme := &Scheme{}
|
||||
|
||||
config := threshold.DealerConfig{
|
||||
Threshold: 1,
|
||||
TotalParties: 3,
|
||||
}
|
||||
|
||||
dealer, err := scheme.NewTrustedDealer(config)
|
||||
if err != nil {
|
||||
t.Fatalf("NewTrustedDealer failed: %v", err)
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
shares, _, err := dealer.GenerateShares(ctx)
|
||||
if err != nil {
|
||||
t.Fatalf("GenerateShares failed: %v", err)
|
||||
}
|
||||
|
||||
signer, err := scheme.NewSigner(shares[0])
|
||||
if err != nil {
|
||||
t.Fatalf("NewSigner failed: %v", err)
|
||||
}
|
||||
|
||||
message := []byte("test message")
|
||||
sigShare, err := signer.SignShare(ctx, message, []int{0}, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("SignShare failed: %v", err)
|
||||
}
|
||||
|
||||
// Serialize
|
||||
data := sigShare.Bytes()
|
||||
if len(data) != SignatureShareSize {
|
||||
t.Errorf("signature share bytes length = %d, want %d", len(data), SignatureShareSize)
|
||||
}
|
||||
|
||||
// Deserialize
|
||||
parsed, err := scheme.ParseSignatureShare(data)
|
||||
if err != nil {
|
||||
t.Fatalf("ParseSignatureShare failed: %v", err)
|
||||
}
|
||||
|
||||
if parsed.Index() != sigShare.Index() {
|
||||
t.Errorf("parsed index = %d, want %d", parsed.Index(), sigShare.Index())
|
||||
}
|
||||
|
||||
if parsed.SchemeID() != threshold.SchemeBLS {
|
||||
t.Errorf("parsed schemeID = %v, want %v", parsed.SchemeID(), threshold.SchemeBLS)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPublicKeySerialization(t *testing.T) {
|
||||
scheme := &Scheme{}
|
||||
|
||||
config := threshold.DealerConfig{
|
||||
Threshold: 1,
|
||||
TotalParties: 3,
|
||||
}
|
||||
|
||||
dealer, err := scheme.NewTrustedDealer(config)
|
||||
if err != nil {
|
||||
t.Fatalf("NewTrustedDealer failed: %v", err)
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
_, groupKey, err := dealer.GenerateShares(ctx)
|
||||
if err != nil {
|
||||
t.Fatalf("GenerateShares failed: %v", err)
|
||||
}
|
||||
|
||||
// Serialize
|
||||
data := groupKey.Bytes()
|
||||
if len(data) != PublicKeySize {
|
||||
t.Errorf("public key bytes length = %d, want %d", len(data), PublicKeySize)
|
||||
}
|
||||
|
||||
// Deserialize
|
||||
parsed, err := scheme.ParsePublicKey(data)
|
||||
if err != nil {
|
||||
t.Fatalf("ParsePublicKey failed: %v", err)
|
||||
}
|
||||
|
||||
if !parsed.Equal(groupKey) {
|
||||
t.Error("parsed public key does not equal original")
|
||||
}
|
||||
|
||||
if parsed.SchemeID() != threshold.SchemeBLS {
|
||||
t.Errorf("parsed schemeID = %v, want %v", parsed.SchemeID(), threshold.SchemeBLS)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPublicKeyEquality(t *testing.T) {
|
||||
scheme := &Scheme{}
|
||||
|
||||
config := threshold.DealerConfig{
|
||||
Threshold: 1,
|
||||
TotalParties: 3,
|
||||
}
|
||||
|
||||
dealer, err := scheme.NewTrustedDealer(config)
|
||||
if err != nil {
|
||||
t.Fatalf("NewTrustedDealer failed: %v", err)
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
// Generate two different group keys
|
||||
_, groupKey1, err := dealer.GenerateShares(ctx)
|
||||
if err != nil {
|
||||
t.Fatalf("GenerateShares 1 failed: %v", err)
|
||||
}
|
||||
|
||||
_, groupKey2, err := dealer.GenerateShares(ctx)
|
||||
if err != nil {
|
||||
t.Fatalf("GenerateShares 2 failed: %v", err)
|
||||
}
|
||||
|
||||
// Same key should be equal to itself
|
||||
if !groupKey1.Equal(groupKey1) {
|
||||
t.Error("group key should equal itself")
|
||||
}
|
||||
|
||||
// Different keys should not be equal (with very high probability)
|
||||
// Note: There's an astronomically small chance this could fail randomly
|
||||
if groupKey1.Equal(groupKey2) {
|
||||
t.Error("different group keys should not be equal")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user