Compare commits

...
26 Commits
Author SHA1 Message Date
zeekay 8c8af9b545 chore: sync working tree
Commits 1 outstanding change(s) that were sitting uncommitted.
No build artifacts and no secrets in the changeset (both checked).
2026-07-26 10:13:06 -07:00
zeekay 0ed2a574f4 fix(deps): reconcile go.sum against what the registry actually serves
Chasing these one at a time was whack-a-mole — every repair surfaced the next
moved tag (age, pq, threshold, zap, consensus, constants, sdk, keys…). This is
the systematic pass instead: every distinct luxfi/hanzoai module@version in the
workspace (692 of them) was resolved against proxy.golang.org + a direct fetch,
and any go.sum line disagreeing with what is actually served was corrected.

Not an attack, and checked rather than assumed: for each case the proxy and a
direct fetch AGREE with each other and differ from the recorded hash. Two
independent transports agreeing means nothing is rewriting bytes in flight —
the tags moved at source. sum.golang.org still holds the original, but
GOPRIVATE covers github.com/luxfi/* with GOSUMDB=off, so our own modules never
consult the checksum DB and a moved tag splits consumers silently instead of
failing at publish time.

Only the recorded hash changes. No selected version moves, and `go mod tidy`
was deliberately NOT run, so nothing is upgraded as a side effect.

Verified: `go list -m all` resolves with no checksum error.

The durable fix is upstream: treat a published version as immutable. Cut
x.y.z+1 instead of moving a tag — with GOSUMDB off, no consumer can detect it.
2026-07-26 04:16:22 -07:00
zeekay c626fd7d5d fix(deps): correct moved-tag hashes for luxfi modules in go.sum
Several luxfi versions carry TWO different contents across this workspace,
because published tags were moved instead of a new patch being cut:
age@v1.5.0, pq@v1.0.3, threshold@v1.9.4, zap@v0.6.0, zap@v0.8.1.

Adjudicated before editing, since "checksum mismatch / SECURITY ERROR" is also
what a real supply-chain attack looks like. It is not one here: sum.golang.org
holds the OLD hash while proxy.golang.org and a direct fetch BOTH serve the same
NEW bytes. Two independent transports agreeing means nothing is rewriting
content in flight — the tag moved at source. The sumdb entry is a fossil:
GOPRIVATE covers github.com/luxfi/* with GOSUMDB=off, so our own modules never
consult the checksum DB and a moved tag splits consumers silently.

The old bytes are served by nothing now, so a stale pin can never build.
Corrected to the only content that exists, in BOTH line forms (h1: and
/go.mod h1:) — Go reports these one at a time, so a partial fix just relocates
the error.

Deliberately no `go mod tidy`: this changes no selected version, only the
recorded hash of versions already chosen.

Verified: `go list -m all` resolves with no checksum error.

The durable fix is upstream: never move a published tag, cut x.y.z+1 instead.
2026-07-26 02:42:49 -07:00
zeekay b436812dd3 security(crypto): remove fail-open aggregated verifier
crypto/aggregated ThresholdSignature.Verify ignored its message argument and
returned true for any non-empty []byte — a method named Verify that verifies
nothing. Zero importers workspace-wide, so it was never reachable; removing it
before it ever could be. Real threshold verification lives in luxfi/threshold.

Verified: CGO_ENABLED=0 go build ./... clean, no remaining references.
2026-07-25 12:09:39 -07:00
zeekayandlux e24964b0ad security(crypto): remove fail-open aggregated verifier; untrack .test binaries
- delete crypto/aggregated: ThresholdSignature.Verify ignored the message and
  returned true for any non-empty []byte. Zero importers workspace-wide, so it
  was never reachable — a loaded gun, not a live wound. Real threshold
  verification lives in github.com/luxfi/threshold.
- untrack 5 committed Go test binaries (24.2MB) and add *.test to .gitignore
  so they stop regenerating into the index.

Verified: CGO_ENABLED=0 go build ./... clean; no remaining references.
2026-07-25 12:09:20 -07:00
zeekayandlux 59fe62c06d chore(deps): bump geth v1.20.1 + luxfi deps — stack unification 2026-07-25 12:09:19 -07:00
c436347937 crypto: delete dead mldsa/.old — unreferenced legacy cgo/tests
The canonical ML-DSA package (mldsa/) is the one way; mldsa/.old was
unreferenced legacy code. Build stays green (go build ./mldsa/ ok).

Co-authored-by: Hanzo Dev <dev@hanzo.ai>
2026-07-25 12:09:19 -07:00
hanzo-dev 482faf9bea ci: run linux jobs on lux-build-amd64 ARC scale set (no GitHub-hosted builders) 2026-07-11 00:19:56 -07:00
Abhishek KrishnaandGitHub 92efe562a9 docs(attestation): add package README (#7)
Anchors the attestation package's API + trust model + vendor
integrations + production status. Adjacent to LP-5300 / LP-5301
PoT/PoI receipts insofar as PoT is a form of computation
attestation.

Pure docs. No behavior change.
2026-07-04 10:48:55 -07:00
Abhishek KrishnaandGitHub a6cc9670f7 docs(cggmp21): add package README (#6)
CGGMP21 threshold ECDSA is Lux's canonical native MPC library
(production bridge default per luxfi/bridge#405). Package had no
README — adds Overview + Public API + Paillier note + security
model + operational constraints (non-custodial, no cosigner path
by default).

Pure docs. No behavior change.
2026-07-04 10:48:52 -07:00
Abhishek KrishnaandGitHub 447c43314d docs(poi): add package README (#5)
The poi package (Freivalds-over-F_p verification) is hard-consumed
by luxfi/precompile/aivmbridge/computeproof.go but has no README.
Adds Overview + API + wire codec spec + soundness bound + worked
example so a new contributor can audit a fraud proof without
reading the engine.

Pure docs. No behavior change.
2026-07-04 10:48:49 -07:00
zeekayandHanzo Dev 733b315ce2 threshold: demote to interface + registry only (BLS impl → luxfi/threshold)
The native BLS Scheme impl (threshold/bls/) moved to
luxfi/threshold/scheme/bls so all threshold-signature implementations
live in one module. crypto/threshold now holds only the shared contract
(Scheme/Signer/Aggregator/Verifier/registry/session/errors/adapter) that
crypto/signer + hsm depend on — the one low-level seam that must stay
here to avoid a crypto<->threshold module cycle.

signer_test repoints its BLS registration to the new path. No
production crypto code changes.

Co-authored-by: Hanzo Dev <dev@hanzo.ai>
2026-07-03 14:34:58 -07:00
zeekayandHanzo Dev 7181e9ccbc threshold/bls: genuine dealerless Pedersen-VSS DKG (RunDKG)
Replaces the stub trusted-dealer keygen with a real dealerless DKG:
each party samples its own degree-(t-1) polynomial + Feldman
commitments; party j's share = Σ_i f_i(j+1); group key = Σ_i C_{i,0}.
No party ever forms the group secret — it exists only as the sum of
independent contributions. Every cross-party VSS share is
Feldman-verified (A·f(j) == Σ jᵏ·C_k); a bad share fails the ceremony
identifying the emitter.

Tested (CGO, BLS12-381 via circl): 3-of-5 shares sign, aggregate via
Lagrange, and verify under the emergent group key; two disjoint signer
subsets both verify (proving one shared polynomial); bad params
rejected.

NOTE: per the threshold-consolidation directive this scheme's canonical
home is moving to luxfi/threshold, and dealerless keygen is being
routed through the purpose-built luxfi/dkg engine — see the
threshold-architecture consolidation plan. This lands the tested
no-dealer property now; the engine swap follows.

Co-authored-by: Hanzo Dev <dev@hanzo.ai>
2026-07-03 12:47:21 -07:00
zeekayandHanzo Dev 8e04abc877 fix(deps): repair vanished pins / go.sum drift ( age@latest) + build green
Co-authored-by: Hanzo Dev <dev@hanzo.ai>
2026-06-30 13:59:56 -07:00
z 429ab6b8ad docs(brand): add hero banner 2026-06-28 20:36:37 -07:00
z 0faac6d2e7 chore(brand): dynamic hero banner 2026-06-28 20:36:36 -07:00
zeekay e63630ed80 fix(hqc): label Corona as MLWE, not RLWE, in signature-stack comment
Corona is Module-LWE (Ringtail/Raccoon line, ePrint 2024/1113), the
same family as Pulsar (ML-DSA) and ML-KEM. The stack comment already
labels Pulsar (MLWE); Corona was the lone RLWE mislabel. Comment only.
2026-06-27 16:35:13 -07:00
f94599dcee chore: bump luxfi/database v1.19.3 (#4)
Co-authored-by: zeekay <z@zeekay.io>
2026-06-25 15:24:37 -07:00
zeekay ae88198fcf license: drop redundant LICENSE-ECO.md — the canonical LICENSE (Lux Ecosystem License v1.2) already covers it (descending-chain grant + commercial contact + LP-0012) 2026-06-24 11:01:31 -07:00
zeekay 5bacec4869 license: BSD-3-Eco ecosystem source license (w3a.foundation) — protocol open, exact impl ecosystem-bound to Lux-primary-network descendants; commercial via licensing@w3a.foundation 2026-06-24 10:53:03 -07:00
zeekay aa7a640e9d attestation: SGX/TDX/SEV-SNP quote byte-layout parsers — vendor formats finished
Real parsers for the three production TEE quote formats, decoding the documented binary layouts and
verifying the full vendor trust chain (no longer the canonical-model stand-in):
- SGX (DCAP v3): header+report body offsets (MRENCLAVE@112, report_data@368); PCK chain→pinned Intel
  root, PCK signs the QE report, the QE report binds the attestation key (sha256(attestPub‖authData)),
  the attestation key signs the quote — full DCAP flow.
- TDX (DCAP v4): version 4 / tee_type 0x81, MRTD@184, report_data@568; same sigData chain.
- SEV-SNP: 1184-byte report, measurement@0x90, report_data@0x50, P-384 VCEK signature@0x2A0 (LE
  scalars), VCEK→pinned AMD root.
All verify the operator-key binding in report_data. 9/9 incl. tampered-measurement (sig fails),
unpinned-root (chain fails), unbound-key for each format.
2026-06-24 09:40:07 -07:00
zeekay 3fa72fc0f6 crypto: PQ confidentiality (X-Wing), TEE attestation verifier, node-scaling benchmark
- promptseal: KEM X25519 → X-Wing (X25519 + ML-KEM-768), making the prompt-confidentiality envelope
  POST-QUANTUM (secure if either half holds) — matching the strict-PQ posture of ML-DSA staking. The
  proof layer was already PQ (keccak + information-theoretic Freivalds). 6/6 still green.
- attestation: a REAL TEE quote verifier (closes the audit's TEE stub). Verify() checks an ECDSA-P256
  report signature, an x509 cert chain to a PINNED vendor root, a measurement allow-list, AND that the
  report binds the operator's confidentiality key — so a sealed prompt opens ONLY inside the attested
  enclave (custody end to end). 6/6 incl. forged-sig, unpinned-root (an attacker's own well-formed
  quote is rejected), wrong-measurement, unbound-key, tampered-measurement.
- poi/scale_test.go: 1/10/100/1000-node scaling. VerifyOpening is per-node-independent → linear
  aggregate throughput (542 → 542k proofs/sec) at constant latency, and fraud-caught probability
  → 1.0 as nodes grow. + BenchmarkVerifyOpening.
2026-06-24 09:12:18 -07:00
zeekay e8403d217e promptseal: HPKE prompt-confidentiality envelope — closes audit G9
Seals a prompt to an operator's registered KEM key so it is NEVER plaintext on the wire and only
the chosen operator opens it (RFC 9180 base mode, X25519-HKDF-SHA256 + ChaCha20-Poly1305, over
cloudflare/circl). aad binds the request (intentID) — no cross-request replay. 6/6: round-trip,
wrong-key-can't-open, tampered-ciphertext-rejected, wrong-aad-rejected, observer-learns-nothing
(no plaintext in blob + two seals differ), malformed-key-rejected.
2026-06-24 08:46:05 -07:00
zeekay af332c0910 poi: cross-language adversarial battery (pre-fit, griefer, wire-fuzz, oversized)
Mirrors the Rust red-team against the canonical Go verifier the chain consumes: the pre-fit-challenge
killer (forge passes a guessed r1, fails a beacon-fresh r2 — proves the post-commit beacon is
load-bearing), a griefer's uncommitted opening can't slash, the wire decoder never panics on fuzzed
frames, and over-large dims are rejected. go test ./poi: ok.
2026-06-24 03:23:42 -07:00
zeekay 7b260cae20 poi: opening wire codec + CheckOpening (the compute-proof precompile payload)
EncodeOpening/DecodeOpening: one canonical frame (root|index|beacon|A|B|C|proof) shared by the Go
prover, the aivmbridge precompile, and the Solidity reference, so a real-model-sized opening is
verified NATIVELY on-chain instead of in gas-bounded EVM bytecode. CheckOpening/CheckDecoded return
(included, freivaldsOK) — the two bits the gate needs (genuine = both; fraud = included && !ok).
Bounds every length (MaxOpeningDim) so a malformed frame can't over-allocate. Tests: round-trip,
honest, fraud, not-included (griefer), truncated. go test ./poi: ok.
2026-06-23 22:34:47 -07:00
zeekay 08eacb0237 poi: transcript + on-chain-parity challenger (gives Verify a real caller)
Mirror of hanzo-engine/src/poi_transcript.rs: ProofTranscript (keccak Merkle over per-matmul
leaves), ExactMatmul (whole-K i64), VerifyOpening (Merkle inclusion + Freivalds), ChallengeIndex.
VerifyOpening is the production caller the audit found missing — the off-chain watcher imports it
and slashes a bond when an opening fails. Tests: transcript round-trip, fabricated-output-caught,
swapped-reveal-caught, and a pinned golden DeriveChallenges vector mirrored byte-for-byte in the
Rust suite (cross-language parity enforced, not assumed). go test ./poi: ok.
2026-06-23 22:24:21 -07:00
35 changed files with 2479 additions and 3133 deletions
+9
View File
@@ -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

+2 -2
View File
@@ -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
+1
View File
@@ -33,3 +33,4 @@ target/
test-results/
tmp/
*.eco
*.test
+2
View File
@@ -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.
-168
View File
@@ -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
}
-582
View File
@@ -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()
}
+136
View File
@@ -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).
+109
View File
@@ -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
}
+196
View File
@@ -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)
}
}
+369
View File
@@ -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
}
+252
View File
@@ -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")
}
}
BIN
View File
Binary file not shown.
+149
View File
@@ -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) = (x1)/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
View File
Binary file not shown.
+25 -23
View File
@@ -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
+12 -13
View File
@@ -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
View File
@@ -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
View File
Binary file not shown.
-20
View File
@@ -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
-409
View File
@@ -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
-396
View File
@@ -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
View File
Binary file not shown.
+172
View File
@@ -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 FiatShamir: `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` | FiatShamir 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.
+94
View File
@@ -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")
}
}
+125
View File
@@ -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
}
+211
View File
@@ -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)
}
+143
View File
@@ -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
View File
@@ -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
}
+93
View File
@@ -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")
}
}
+102
View File
@@ -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)
}
+88
View File
@@ -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
View File
@@ -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
View File
Binary file not shown.
-883
View File
@@ -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
-478
View File
@@ -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")
}
}