threshold: extract TEE custody into optional luxfi/tee extension

TEE-backed threshold custody was a threshold-internal, fenced opt-in
surface reachable only from tests (never on the ZAP dispatcher wire).
Decomplect it out: the dealerless/permissionless signing path is the
production default with zero TEE dependency.

Removed from the threshold core:
  - protocols/{mldsa,rlwe,slhdsa}-tee  -> moved to github.com/luxfi/tee
  - pkg/thresholdd/tee_options.go      (cc/attest + go-sev-guest glue)
  - the optional Sign_TEE / SetTEEBackend surface on the pulsar, corona
    and magnetar dispatcher schemes (teeBackend fields + unwired-error
    sentinels)
  - magnetar's duplicate slhdsatee-typed strict-PQ gate
    (profile/magnetarRefuseUnderStrictPQ/SetChainSecurityProfile/
    SetCombinerPool/Combine_TEE/AttestCombinerMember/PoolCombineMember)
  - the dispatcher-only TEE test files + their testdata

The permissionless keygen/sign/sign_ctx/verify paths are byte-unchanged.
The ONE canonical strict-PQ gate (profile.go RefuseUnderStrictPQ, applied
at Sign_Ctx_Profile) is retained; magnetar's second, TEE-typed gate is
gone -- one gate, one place. go mod tidy drops go-sev-guest AND luxfi/mpc
(both were TEE-only deps; the core had zero real imports of either).

Dependency direction is one-way: luxfi/tee depends on the cores
(pulsar/corona/magnetar/mpc), never the reverse.

Build + vet + dispatcher tests green; no consensus/mpc/corona consumer
referenced the removed surface.
This commit is contained in:
lux
2026-06-27 11:31:49 -07:00
parent 856794662d
commit c4044388e0
43 changed files with 37 additions and 8082 deletions
-83
View File
@@ -9,7 +9,6 @@ require (
// Lux crypto stack - this is the HIGH-LEVEL orchestration layer
// that consumes primitives from these packages (LP-5703, LP-5704)
github.com/luxfi/crypto v1.19.17 // ECDSA, EdDSA, BLS curves
github.com/luxfi/fhe v1.8.2 // FHE primitives for TFHE protocol
github.com/luxfi/lattice/v7 v7.1.4 // Lattice ops for Corona (post-quantum) + GPU acceleration
github.com/spf13/cobra v1.10.2
github.com/stretchr/testify v1.11.1
@@ -31,130 +30,48 @@ require (
)
require (
cloud.google.com/go v0.123.0 // indirect
cloud.google.com/go/auth v0.20.0 // indirect
cloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect
cloud.google.com/go/compute/metadata v0.9.0 // indirect
cloud.google.com/go/iam v1.7.0 // indirect
cloud.google.com/go/kms v1.27.0 // indirect
cloud.google.com/go/longrunning v0.9.0 // indirect
cloud.google.com/go/secretmanager v1.18.0 // indirect
filippo.io/age v1.3.1 // indirect
filippo.io/hpke v0.4.0 // indirect
github.com/ALTree/bigfloat v0.2.0 // indirect
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.21.0 // indirect
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.13.1 // indirect
github.com/Azure/azure-sdk-for-go/sdk/internal v1.12.0 // indirect
github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azkeys v1.4.0 // indirect
github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azsecrets v1.4.0 // indirect
github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v1.2.0 // indirect
github.com/AzureAD/microsoft-authentication-library-for-go v1.6.0 // indirect
github.com/Masterminds/semver/v3 v3.4.0 // indirect
github.com/ansel1/merry v1.8.1 // indirect
github.com/ansel1/merry/v2 v2.2.2 // indirect
github.com/aws/aws-sdk-go-v2 v1.41.5 // indirect
github.com/aws/aws-sdk-go-v2/config v1.32.13 // indirect
github.com/aws/aws-sdk-go-v2/credentials v1.19.13 // indirect
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.21 // indirect
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.21 // indirect
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.21 // indirect
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.6 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.7 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.21 // indirect
github.com/aws/aws-sdk-go-v2/service/kms v1.50.4 // indirect
github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.41.5 // indirect
github.com/aws/aws-sdk-go-v2/service/signin v1.0.9 // indirect
github.com/aws/aws-sdk-go-v2/service/sso v1.30.14 // indirect
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.18 // indirect
github.com/aws/aws-sdk-go-v2/service/sts v1.41.10 // indirect
github.com/aws/smithy-go v1.24.2 // indirect
github.com/cenkalti/backoff v2.2.1+incompatible // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/cosmos/go-bip39 v1.0.0 // indirect
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
github.com/felixge/httpsnoop v1.0.4 // indirect
github.com/gemalto/flume v1.0.0 // indirect
github.com/gemalto/kmip-go v0.1.0 // indirect
github.com/go-logr/logr v1.4.3 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/go-task/slim-sprig/v3 v3.0.0 // indirect
github.com/golang-jwt/jwt/v5 v5.3.1 // indirect
github.com/google/go-cmp v0.7.0 // indirect
github.com/google/logger v1.1.1 // indirect
github.com/google/pprof v0.0.0-20260302011040-a15ffb7f9dcc // indirect
github.com/google/s2a-go v0.1.9 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/googleapis/enterprise-certificate-proxy v0.3.14 // indirect
github.com/googleapis/gax-go/v2 v2.21.0 // indirect
github.com/gorilla/rpc v1.2.1 // indirect
github.com/grandcat/zeroconf v1.0.0 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/klauspost/compress v1.18.6 // indirect
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
github.com/kylelemons/godebug v1.1.0 // indirect
github.com/luxfi/accel v1.1.9 // indirect
github.com/luxfi/cache v1.2.1 // indirect
github.com/luxfi/compress v0.0.5 // indirect
github.com/luxfi/concurrent v0.0.3 // indirect
github.com/luxfi/database v1.19.3 // indirect
github.com/luxfi/hsm v1.1.3 // indirect
github.com/luxfi/ids v1.2.9 // indirect
github.com/luxfi/math v1.4.1 // indirect
github.com/luxfi/math/big v0.1.0 // indirect
github.com/luxfi/mdns v0.1.1 // indirect
github.com/luxfi/mock v0.1.1 // indirect
github.com/luxfi/pq v1.0.3 // indirect
github.com/mattn/go-colorable v0.1.14 // indirect
github.com/mattn/go-isatty v0.0.21 // indirect
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d // indirect
github.com/miekg/dns v1.1.72 // indirect
github.com/miekg/pkcs11 v1.1.1 // indirect
github.com/mimoo/StrobeGo v0.0.0-20220103164710-9a04d6ca976b // indirect
github.com/montanaflynn/stats v0.9.0 // indirect
github.com/mr-tron/base58 v1.2.0 // indirect
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
github.com/rs/zerolog v1.35.0 // indirect
github.com/spf13/pflag v1.0.10 // indirect
github.com/supranational/blst v0.3.16 // indirect
github.com/x448/float16 v0.8.4 // indirect
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.67.0 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.67.0 // indirect
go.opentelemetry.io/otel v1.43.0 // indirect
go.opentelemetry.io/otel/metric v1.43.0 // indirect
go.opentelemetry.io/otel/sdk/metric v1.43.0 // indirect
go.opentelemetry.io/otel/trace v1.43.0 // indirect
go.uber.org/mock v0.6.0 // indirect
go.uber.org/multierr v1.11.0 // indirect
go.uber.org/zap v1.27.1 // indirect
go.yaml.in/yaml/v3 v3.0.4 // indirect
golang.org/x/exp v0.0.0-20260312153236-7ab1446f8b90 // indirect
golang.org/x/mod v0.36.0 // indirect
golang.org/x/net v0.55.0 // indirect
golang.org/x/oauth2 v0.36.0 // indirect
golang.org/x/sys v0.45.0 // indirect
golang.org/x/text v0.37.0 // indirect
golang.org/x/time v0.15.0 // indirect
golang.org/x/tools v0.45.0 // indirect
google.golang.org/api v0.275.0 // indirect
google.golang.org/genproto v0.0.0-20260406210006-6f92a3bedf2d // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20260401024825-9d38bb4040a9 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20260401024825-9d38bb4040a9 // indirect
google.golang.org/grpc v1.80.0 // indirect
google.golang.org/protobuf v1.36.11 // indirect
gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
require (
github.com/google/go-sev-guest v0.14.1
github.com/luxfi/corona v0.7.6
github.com/luxfi/lens v0.1.4
github.com/luxfi/magnetar v1.2.0
github.com/luxfi/metric v1.5.7
github.com/luxfi/mpc v1.14.13
github.com/luxfi/pulsar v1.1.1
github.com/luxfi/zap v0.8.1
)
-1346
View File
File diff suppressed because it is too large Load Diff
-65
View File
@@ -2,17 +2,13 @@
package thresholdd
import (
"context"
"crypto/rand"
"crypto/sha256"
"encoding/hex"
"errors"
"fmt"
"sync"
coronaThreshold "github.com/luxfi/corona/threshold"
rlwetee "github.com/luxfi/threshold/protocols/rlwe-tee"
)
// coronaScheme wires luxfi/corona/threshold (Ring-LWE post-quantum
@@ -50,15 +46,8 @@ import (
type coronaScheme struct {
mu sync.Mutex
sessions map[string]*coronaSession
// teeBackend is the optional institutional-custody R-LWE signer
// wired via SetTEEBackend. nil → Sign_TEE refuses.
teeBackend *rlwetee.Signer
}
// errCoronaTEEUnwired is returned by Sign_TEE when no TEE backend is registered.
var errCoronaTEEUnwired = errors.New("corona tee sign: no TEE backend wired (call SetTEEBackend first)")
// coronaSession holds the in-process per-party key shares + group key
// for a single (pubKeyHex) keygen output.
type coronaSession struct {
@@ -245,57 +234,3 @@ func (s *coronaScheme) Verify(p verifyParams) (verifyResult, error) {
}
return verifyResult{OK: coronaThreshold.VerifyBytes(gkBytes, string(msg), sigBytes)}, nil
}
// SetTEEBackend wires a rlwetee.Signer as the institutional-custody
// TEE-gated signing path. The default `corona.sign` procedure is
// UNAFFECTED — it remains the permissionless trusted-dealer 2-round
// threshold path.
//
// Passing nil clears the backend (subsequent Sign_TEE calls return
// errCoronaTEEUnwired).
func (s *coronaScheme) SetTEEBackend(b *rlwetee.Signer) {
s.mu.Lock()
s.teeBackend = b
s.mu.Unlock()
}
// Sign_TEE is the institutional-custody opt-in signing path. Mirrors
// magnetarScheme.Sign_TEE; the inner primitive is corona Ring-LWE
// via the rlwetee.Signer.
//
// Returns the corona-threshold-framed wire signature + the
// SignReceipt audit signature bytes.
func (s *coronaScheme) Sign_TEE(
ctx context.Context,
kind string,
evidenceBytes []byte,
rim, hardware, teePub [32]byte,
verifyOpts []TEEVerifyOption,
jobID [32]byte,
msg []byte,
) ([]byte, []byte, error) {
s.mu.Lock()
b := s.teeBackend
s.mu.Unlock()
if b == nil {
return nil, nil, errCoronaTEEUnwired
}
env := &rlwetee.Envelope{
Kind: attestKindFromString(kind),
EvidenceBytes: append([]byte(nil), evidenceBytes...),
RIM: rim,
Hardware: hardware,
TEEPub: teePub,
VerifyOpts: teeVerifyOptionsToAttest(verifyOpts),
}
wire, receipt, err := b.Sign(ctx, env, jobID, msg)
if err != nil {
return nil, nil, fmt.Errorf("corona tee sign: %w", err)
}
if receipt == nil {
return nil, nil, fmt.Errorf("corona tee sign: nil receipt")
}
return wire, receipt.AuditSignature, nil
}
+37 -378
View File
@@ -2,7 +2,6 @@
package thresholdd
import (
"context"
"crypto/rand"
"crypto/sha256"
"encoding/binary"
@@ -12,8 +11,6 @@ import (
"sync"
magnetar "github.com/luxfi/magnetar/ref/go/pkg/magnetar"
slhdsatee "github.com/luxfi/threshold/protocols/slhdsa-tee"
)
// magnetarScheme wires luxfi/magnetar (FIPS 205 SLH-DSA, hash-based
@@ -30,96 +27,43 @@ import (
// the same (message, group public key) — is pinned upstream by
// TestMagnetar_Wire_FIPS205Verifiable.
//
// Magnetar architectural framing (v0.5+ CHANGELOG):
//
// - PRIMARY public-BFT primitive: per-validator standalone
// SLH-DSA. Each validator holds its OWN keypair (sk_i, pk_i),
// produces a single-party FIPS 205 signature σ_i, and the
// consensus layer collects N signatures into a
// ValidatorAggregateCert. NO DKG, NO dealer, NO aggregator-in-
// TCB. This is the canonical v0.5 primary path.
// - CUSTODY (TEE-required): magnetar.CombineWithSeedReconstruction
// (v0.1 reveal-and-aggregate) — produces one FIPS 205-shaped σ
// by reconstructing the master seed in aggregator memory. NOT
// public-BFT-safe (the aggregator is in the TCB). Off the bus.
//
// What this dispatcher exposes:
//
// - The PRIMARY per-validator standalone path is the dispatcher
// surface. Keygen generates `participants` independent SLH-DSA
// keypairs; the dispatcher retains them all keyed by the FIRST
// validator's MAGG-framed public-key hex; Sign returns the FIRST
// validator's MAGS-framed signature.
// - The wire bytes are byte-identical to single-party FIPS 205 on
// the (msg, pk_0) pair — verifiable by any external party with
// a FIPS 205 verifier and the documented MAGG / MAGS frame.
// - The N-of-N collected-signatures form (ValidatorAggregateCert)
// is not on the dispatcher surface today because the bus shape
// (one signature per .sign call) doesn't naturally carry N
// parallel signatures. That form is the consensus-layer's job;
// embedders that need it call magnetar.BuildAggregateCert /
// VerifyAggregateCert directly.
// What this dispatcher exposes — the magnetar v0.5 PRIMARY public-BFT
// primitive: per-validator standalone SLH-DSA. Each validator holds
// its OWN keypair (sk_i, pk_i) and produces a single-party FIPS 205
// signature σ_i; NO DKG, NO dealer, NO aggregator-in-TCB. Keygen
// generates `participants` independent keypairs; the dispatcher
// retains them all keyed by the FIRST validator's MAGG-framed
// public-key hex, and Sign returns the FIRST validator's MAGS-framed
// signature. The N-of-N collected-signatures form
// (ValidatorAggregateCert) is the consensus layer's job; embedders
// that need it call magnetar.BuildAggregateCert / VerifyAggregateCert
// directly.
//
// Trust model:
//
// - On keygen: the dispatcher generates N independent per-
// validator keypairs in-process. The N-1 non-signing keypairs
// are retained alongside the canonical one so future bus
// extensions (e.g. a parallel-slices cert form) can use them
// without re-keygen. For the test surface here, the FIRST
// validator is the canonical signer.
// - On keygen: N independent per-validator keypairs generated
// in-process. The N-1 non-signing keypairs are retained alongside
// the canonical one so future bus extensions (e.g. a
// parallel-slices cert form) can use them without re-keygen.
// - On sign: a single FIPS 205 SignDeterministic call under the
// first validator's seed (the magnetar.ValidatorSign primary
// primitive). Output is byte-identical to circl/slhdsa output
// on the same (sk, msg) pair — no ctx is bound (callers needing
// ctx must use magnetar.Sign directly with the precompile ctx).
// on the same (sk, msg) pair.
// - On verify: stateless. magnetar.VerifyBytes(gkBytes, msg,
// sigBytes). No per-session state is consulted; the supplied
// MAGG bytes are the authority.
//
// TEE-attested institutional custody (the strict-PQ master-seed
// reconstruction path) is the OPTIONAL luxfi/tee extension
// (github.com/luxfi/tee/slhdsa-tee). This dispatcher carries no
// dependency on it; the strict-PQ chain-profile gate it enforces is
// the canonical resolver gate in profile.go (RefuseUnderStrictPQ),
// applied once at Sign_Ctx_Profile.
type magnetarScheme struct {
mu sync.Mutex
sessions map[string]*magnetarSession
// teeBackend is the optional institutional-custody SLH-DSA
// signer wired via SetTEEBackend. When nil, Sign_TEE refuses
// with errMagnetarTEEUnwired. The default permissionless path
// (Sign) is unaffected by the TEE backend's presence — UNLESS
// the chain profile is strict-PQ, in which case Sign itself
// refuses (see profile gate below).
teeBackend *slhdsatee.Signer
// pool is the optional t-of-n attested-combiner pool. When the
// chain profile is strict-PQ AND the pool is wired, Combine_TEE
// routes through the pool's t-of-n agreement. When the pool is
// nil but profile is strict-PQ, Combine_TEE refuses with
// ErrMagnetarNoTEEAttestation.
pool *slhdsatee.CombinerPool
// profile is the chain-security profile this dispatcher is
// bound to. Default is ProfileLegacyCompat (commodity-host
// strict-atom Combine is acceptable). Strict-PQ chains MUST
// call SetChainSecurityProfile at boot to flip this value;
// once flipped, the permissionless Sign path refuses with
// ErrMagnetarNoTEEAttestation and only Sign_TEE / Combine_TEE
// produce signatures.
//
// Hickey discipline: profile is ONE value in ONE place. The
// gate is the single function magnetarRefuseUnderStrictPQ.
// The dispatcher reads it the same way the precompile
// contract.RefuseUnderStrictPQ helper reads its
// StrictPQReporter: ONE function, ONE place, ONE canonical
// refusal sentinel.
profile slhdsatee.ChainSecurityProfile
}
// errMagnetarTEEUnwired is returned by Sign_TEE when no TEE backend
// has been registered via SetTEEBackend.
var errMagnetarTEEUnwired = errors.New("magnetar tee sign: no TEE backend wired (call SetTEEBackend first)")
// errMagnetarPoolUnwired is returned by Combine_TEE when no combiner
// pool has been registered via SetCombinerPool, regardless of profile.
var errMagnetarPoolUnwired = errors.New("magnetar combine_tee: no combiner pool wired (call SetCombinerPool first)")
// magnetarSession holds the in-process per-validator keypairs for
// one Keygen output. The canonical PublicKey for the session is the
// MAGG-framed wire bytes of `keys[0].pk`; subsequent Sign calls
@@ -224,62 +168,6 @@ func (s *magnetarScheme) Keygen(p keygenParams) (keygenResult, error) {
return keygenResult{PublicKey: pkHex, Shares: shareIDs}, nil
}
// magnetarRefuseUnderStrictPQ is the profile gate for the magnetar
// dispatcher. ONE function, ONE place — mirrors
// precompile/contract.RefuseUnderStrictPQ at the precompile layer.
//
// Returns slhdsatee.ErrMagnetarNoTEEAttestation when the dispatcher
// is bound to ProfileStrictPQ. Returns nil otherwise.
//
// Called at the top of Sign / Sign_Ctx (the permissionless paths)
// — under strict-PQ, the permissionless path is hard-refused; only
// Sign_TEE / Combine_TEE can produce signatures because only those
// paths route the master-seed reconstruction through an attested
// TEE.
//
// Read under the dispatcher's mutex so SetChainSecurityProfile can
// flip the gate atomically at boot.
func (s *magnetarScheme) magnetarRefuseUnderStrictPQ() error {
s.mu.Lock()
p := s.profile
s.mu.Unlock()
if p == slhdsatee.ProfileStrictPQ {
return slhdsatee.ErrMagnetarNoTEEAttestation
}
return nil
}
// SetChainSecurityProfile binds the dispatcher to one of the
// canonical chain-security profiles. Strict-PQ chains MUST call
// this at boot with slhdsatee.ProfileStrictPQ; the default value
// (slhdsatee.ProfileLegacyCompat) preserves the commodity-host
// permissionless Sign path.
//
// Idempotent — re-issuing the same profile is a no-op. Operators
// rotating a chain into strict-PQ MUST follow the cascade:
// - flip the chain profile in luxfi/node ChainConfig
// - flip this dispatcher's profile via SetChainSecurityProfile
// - wire a CombinerPool via SetCombinerPool (>= Threshold attested
// members already provisioned).
//
// Until all three steps complete, the dispatcher will refuse Sign
// AND Combine_TEE on the strict-PQ chain — fail-closed under
// half-rotated state.
func (s *magnetarScheme) SetChainSecurityProfile(p slhdsatee.ChainSecurityProfile) {
s.mu.Lock()
s.profile = p
s.mu.Unlock()
}
// SetCombinerPool wires the t-of-n attested-combiner pool. Required
// for Combine_TEE under any profile; under strict-PQ, it is the
// canonical sign surface.
func (s *magnetarScheme) SetCombinerPool(p *slhdsatee.CombinerPool) {
s.mu.Lock()
s.pool = p
s.mu.Unlock()
}
// Sign produces a magnetar signature for the message under the
// session's canonical (FIRST) validator keypair, frames it in the
// MAGS wire codec, and returns the wire bytes as hex. Output is
@@ -292,16 +180,7 @@ func (s *magnetarScheme) SetCombinerPool(p *slhdsatee.CombinerPool) {
// intentionally omitted here (the bus shape does not carry one);
// the published bytes verify under VerifyBytes with the same MAGG
// public-key bytes plus the original message.
//
// Profile gate: when the dispatcher is bound to
// slhdsatee.ProfileStrictPQ, Sign refuses with
// ErrMagnetarNoTEEAttestation — the permissionless path is hard-
// refused on strict-PQ chains. Callers MUST use Sign_TEE
// (single-host attested) or Combine_TEE (t-of-n attested pool).
func (s *magnetarScheme) Sign(p signParams) (signResult, error) {
if err := s.magnetarRefuseUnderStrictPQ(); err != nil {
return signResult{}, err
}
msg, err := hex.DecodeString(p.MessageHex)
if err != nil {
return signResult{}, fmt.Errorf("messageHex: %w", err)
@@ -369,12 +248,6 @@ func (s *magnetarScheme) Sign(p signParams) (signResult, error) {
// (or the empty hex string "") to bind the empty ctx — semantically
// equivalent to Sign.
//
// Profile gate: same as Sign — refuses under strict-PQ with
// ErrMagnetarNoTEEAttestation when the scheme has been bound to
// slhdsatee.ProfileStrictPQ via SetChainSecurityProfile. Callers
// MUST use Sign_TEE for ctx-bound institutional-custody signing on
// strict-PQ chains.
//
// NOTE: this method does NOT consult the per-request chain-ID
// resolver gate (RefuseUnderStrictPQ in profile.go). Callers that
// reach this method via the ZAP dispatcher go through
@@ -387,29 +260,15 @@ func (s *magnetarScheme) Sign_Ctx(p signCtxParams) (signResult, error) {
// Sign_Ctx_Profile is the magnetar profile-aware entry point.
// Same shape as pulsar.Sign_Ctx_Profile: the per-request chain-ID
// resolver gate fires first, then the scheme-bound slhdsatee gate
// (inside signCtxInternal). Two orthogonal axes:
// resolver gate (profile.go::RefuseUnderStrictPQ) fires at entry —
// on a strict-PQ chain the call is refused with
// ErrRefusedUnderStrictPQ, which the ZAP dispatcher surfaces as an
// error response with strictPQ=true so the client can errors.Is the
// sentinel; on any other profile (or with no resolver / no chain ID)
// it falls through to signCtxInternal.
//
// - resolver gate: "does the request's chain ID resolve to
// strict-PQ?" — refuses with ErrRefusedUnderStrictPQ which the
// ZAP dispatcher surfaces as an error response with
// strictPQ=true so the client can errors.Is the sentinel.
// - slhdsatee scheme gate: "is THIS process bound to strict-PQ
// via SetChainSecurityProfile?" — refuses with
// ErrMagnetarNoTEEAttestation surfaced as a ZapErrCodeInternal
// error. Pre-dates this gate; remains for back-compat with
// operators who set the scheme profile but did not wire a
// resolver.
//
// One function, one place: profile.go::RefuseUnderStrictPQ owns
// the resolver policy; signCtxInternal owns the scheme-bound
// policy. They compose without coupling.
//
// Removal contract: when the magnetar aggregate-cert ctx-bound
// path lands AND signCtxInternal swaps to drive that path with NO
// single-validator shortcut, the resolver gate here becomes dead
// code. Sign_Ctx_Profile then collapses to
// `return s.signCtxInternal(p)`.
// One function, one place: profile.go::RefuseUnderStrictPQ owns the
// strict-PQ policy; this method owns the call site.
func (s *magnetarScheme) Sign_Ctx_Profile(p signCtxParams, resolver ChainProfileResolver) (signResult, error) {
if err := RefuseUnderStrictPQ(p.ChainID, "magnetar.sign_ctx", resolver); err != nil {
return signResult{}, err
@@ -417,16 +276,11 @@ func (s *magnetarScheme) Sign_Ctx_Profile(p signCtxParams, resolver ChainProfile
return s.signCtxInternal(p)
}
// signCtxInternal is the actual ctx-bound sign path. Runs the
// scheme-bound slhdsatee strict-PQ gate first (refuses with
// ErrMagnetarNoTEEAttestation under SetChainSecurityProfile
// strict-PQ), then drives magnetar.SignCtx on the canonical
// single-validator keypair sess.keys[0].sk. Wire shape is
// MAGS-framed FIPS 205 ctx-bound bytes.
// signCtxInternal is the actual ctx-bound sign path. Drives
// magnetar.SignCtx on the canonical single-validator keypair
// sess.keys[0].sk. Wire shape is MAGS-framed FIPS 205 ctx-bound
// bytes.
func (s *magnetarScheme) signCtxInternal(p signCtxParams) (signResult, error) {
if err := s.magnetarRefuseUnderStrictPQ(); err != nil {
return signResult{}, err
}
msg, err := hex.DecodeString(p.MessageHex)
if err != nil {
return signResult{}, fmt.Errorf("messageHex: %w", err)
@@ -452,8 +306,8 @@ func (s *magnetarScheme) signCtxInternal(p signCtxParams) (signResult, error) {
params := magnetar.MustParamsFor(sess.mode)
// Deterministic (randomized=false, rng=nil) so the output is
// KAT-shaped and byte-stable across retries — mirrors Sign and
// Sign_TEE's SignDeterministic discipline.
// KAT-shaped and byte-stable across retries — mirrors Sign's
// SignDeterministic discipline.
sig, err := magnetar.SignCtx(params, sess.keys[0].sk, msg, signCtx, false, nil)
if err != nil {
return signResult{}, fmt.Errorf("magnetar sign_ctx: %w", err)
@@ -497,201 +351,6 @@ func (s *magnetarScheme) Verify(p verifyParams) (verifyResult, error) {
return verifyResult{OK: magnetar.VerifyBytes(gkBytes, msg, sigBytes)}, nil
}
// SetTEEBackend wires a slhdsatee.Signer as the institutional-custody
// TEE-gated signing path. The default `magnetar.sign` procedure is
// UNAFFECTED — it remains the permissionless per-validator standalone
// path. Operators that need attested release must call SetTEEBackend
// at boot and use Sign_TEE.
//
// Passing nil clears the backend (subsequent Sign_TEE calls return
// errMagnetarTEEUnwired).
func (s *magnetarScheme) SetTEEBackend(b *slhdsatee.Signer) {
s.mu.Lock()
s.teeBackend = b
s.mu.Unlock()
}
// Sign_TEE is the institutional-custody opt-in signing path. It
// chains the supplied attestation evidence + RIM + hardware
// fingerprint + TEE pubkey through the slhdsatee.Signer and emits
// the MAGS-framed FIPS 205 wire bytes on success.
//
// Wire payload:
//
// - kind : attest.Kind ("sev_snp", "tdx", "nras")
// - evidenceBytes : the vendor-framed quote / report
// - rim : 32-byte operator-asserted RIM digest
// - hardware : 32-byte hardware fingerprint
// - teePub : 32-byte X25519 TEE public key
// - jobID : 32-byte audit-binding identifier
// - msg : message bytes
// - signCtx : FIPS 205 §10.2 context (nil for empty)
//
// All inputs are required. The TEE backend (set via SetTEEBackend)
// internally calls approval.ApproveIntent, kms.ReleaseGate.Issue /
// Release, hsm.Provider.GetKey, and magnetar.Sign. Output bytes are
// byte-identical to single-party FIPS 205 SignDeterministic on the
// HSM-stored master seed.
//
// Returns the MAGS-framed wire signature + the SignReceipt's audit
// signature bytes for the embedder's audit log; the receipt itself
// (epoch, ephemeralPub, etc.) is intentionally not surfaced here —
// this dispatcher returns only bytes that participate in verification.
func (s *magnetarScheme) Sign_TEE(
ctx context.Context,
kind string,
evidenceBytes []byte,
rim, hardware, teePub [32]byte,
verifyOpts []slhdsateeVerifyOpt,
jobID [32]byte,
msg []byte,
signCtx []byte,
) ([]byte, []byte, error) {
s.mu.Lock()
b := s.teeBackend
s.mu.Unlock()
if b == nil {
return nil, nil, errMagnetarTEEUnwired
}
env := &slhdsatee.Envelope{
Kind: attestKindFromString(kind),
EvidenceBytes: append([]byte(nil), evidenceBytes...),
RIM: rim,
Hardware: hardware,
TEEPub: teePub,
VerifyOpts: teeVerifyOptionsToAttest(verifyOpts),
}
wire, receipt, err := b.Sign(ctx, env, jobID, msg, signCtx)
if err != nil {
return nil, nil, fmt.Errorf("magnetar tee sign: %w", err)
}
if receipt == nil {
return nil, nil, fmt.Errorf("magnetar tee sign: nil receipt")
}
return wire, receipt.AuditSignature, nil
}
// PoolCombineMember names a single combiner participating in a
// Combine_TEE call. The dispatcher carries the per-member attestation
// payload through to the slhdsatee.CombinerPool.Combine call.
type PoolCombineMember struct {
// Name is the pool-registered member identifier
// (matches CombinerPool.AddMember(name, ...)).
Name string
// Kind / EvidenceBytes / RIM / Hardware / TEEPub mirror the
// Sign_TEE payload shape. One PoolCombineMember per member
// participating in the t-of-n quorum.
Kind string
EvidenceBytes []byte
RIM [32]byte
Hardware [32]byte
TEEPub [32]byte
}
// Combine_TEE is the canonical strict-PQ signing surface. It drives
// the wired CombinerPool's t-of-n attested-combiner Sign and returns
// the agreed wire bytes + per-member audit signatures.
//
// Hard requirements (no caveat path):
//
// - Pool MUST be wired via SetCombinerPool. errMagnetarPoolUnwired
// otherwise.
// - At least pool.Threshold() members MUST appear in `members` AND
// pass the pool's freshness gate. Otherwise: slhdsatee.
// ErrMagnetarInsufficientQuorum / ErrMagnetarStaleAttestation.
// - All selected members MUST produce byte-identical wire output.
// Divergence: slhdsatee.ErrMagnetarSignatureDivergence.
//
// Output: the consolidated wire signature (byte-equal across the
// quorum) + the per-member audit-signature bytes concatenated as a
// list. Embedders that need the full SignReceipt list call the pool
// directly via SetCombinerPool / pool.Combine — the dispatcher
// surface emits only the bytes that participate in verification
// (the FIPS 205 wire) + the audit trail for control-plane logging.
func (s *magnetarScheme) Combine_TEE(
ctx context.Context,
members []PoolCombineMember,
verifyOpts []slhdsateeVerifyOpt,
jobID [32]byte,
msg []byte,
signCtx []byte,
) ([]byte, [][]byte, error) {
s.mu.Lock()
pool := s.pool
s.mu.Unlock()
if pool == nil {
return nil, nil, errMagnetarPoolUnwired
}
envs := make(map[string]*slhdsatee.Envelope, len(members))
for _, m := range members {
if m.Name == "" {
return nil, nil, fmt.Errorf("magnetar combine_tee: member with empty Name")
}
envs[m.Name] = &slhdsatee.Envelope{
Kind: attestKindFromString(m.Kind),
EvidenceBytes: append([]byte(nil), m.EvidenceBytes...),
RIM: m.RIM,
Hardware: m.Hardware,
TEEPub: m.TEEPub,
VerifyOpts: teeVerifyOptionsToAttest(verifyOpts),
}
}
wire, receipts, err := pool.Combine(ctx, envs, jobID, msg, signCtx)
if err != nil {
return nil, nil, fmt.Errorf("magnetar combine_tee: %w", err)
}
if len(receipts) == 0 {
return nil, nil, fmt.Errorf("magnetar combine_tee: pool returned no receipts")
}
audits := make([][]byte, 0, len(receipts))
for _, r := range receipts {
if r == nil {
return nil, nil, fmt.Errorf("magnetar combine_tee: pool returned nil receipt")
}
audits = append(audits, r.AuditSignature)
}
return wire, audits, nil
}
// AttestCombinerMember refreshes one pool member's attestation
// freshness state. Called by the operator's control plane out-of-
// band of any sign call; the Combine_TEE path then reads the
// freshness state without performing any KDS / PCS / NRAS network
// roundtrip.
//
// The dispatcher proxies straight through to CombinerPool.Attest;
// the call returns slhdsatee sentinels (ErrAttestationRequired etc.)
// untouched so callers can errors.Is them.
func (s *magnetarScheme) AttestCombinerMember(
ctx context.Context,
memberName string,
kind string,
evidenceBytes []byte,
rim, hardware, teePub [32]byte,
verifyOpts []slhdsateeVerifyOpt,
) error {
s.mu.Lock()
pool := s.pool
s.mu.Unlock()
if pool == nil {
return errMagnetarPoolUnwired
}
env := &slhdsatee.Envelope{
Kind: attestKindFromString(kind),
EvidenceBytes: append([]byte(nil), evidenceBytes...),
RIM: rim,
Hardware: hardware,
TEEPub: teePub,
VerifyOpts: teeVerifyOptionsToAttest(verifyOpts),
}
return pool.Attest(ctx, memberName, env)
}
// magnetarSeededReader is a tiny SHA-256-counter deterministic byte
// stream used to seed per-validator keygen inside the dispatcher.
// It is NOT a CSPRNG; it is the dispatcher's internal mechanism for
-548
View File
@@ -1,548 +0,0 @@
// SPDX-License-Identifier: BSD-3-Clause
package thresholdd
import (
"context"
"crypto/rand"
"encoding/hex"
"errors"
"testing"
"time"
magnetar "github.com/luxfi/magnetar/ref/go/pkg/magnetar"
"github.com/luxfi/mpc/pkg/approval"
"github.com/luxfi/mpc/pkg/hsm"
slhdsatee "github.com/luxfi/threshold/protocols/slhdsa-tee"
)
// magnetar_tee_gate_test.go exercises the strict-PQ chain profile
// gate added at 2026-06-01 to close the magnetar v1.1 strict-atom
// transient-SHAKE-bytes residual.
//
// The gate enforces:
// 1. Default profile (legacy-compat): Sign / Sign_Ctx work as before;
// Sign_TEE works when backend is wired; Combine_TEE works when
// pool is wired.
// 2. Strict-PQ profile: Sign / Sign_Ctx hard-refuse with
// ErrMagnetarNoTEEAttestation; only Sign_TEE (single-host attested)
// and Combine_TEE (t-of-n attested pool) produce signatures.
// 3. Pool freshness: combiners outside rotation window are refused
// with ErrMagnetarStaleAttestation.
// 4. Pool quorum: at least Threshold attested combiners MUST produce
// byte-identical output; insufficient or divergent → hard refusal.
//
// Fixture sharing: reuses sevSnpAttestationMilanDispatch /
// sevSnpVcekMilanDispatch + dispatchKDSReplay() / dispatchFixedNow()
// from tee_dispatcher_test.go (same package).
// gateMakeSigner constructs a single slhdsatee.Signer ready to drive
// real SEV-SNP attestation against the committed Milan fixture.
//
// Shared across the pool tests: each pool member holds an independently
// provisioned Signer (independent wrapped seed), so the pool-level
// byte-equality check pins that two combiners with the SAME provisioned
// seed produce the SAME signature.
func gateMakeSigner(t *testing.T, seedBytes []byte) (*slhdsatee.Signer, *magnetar.PublicKey, [32]byte, [32]byte) {
t.Helper()
rim := realRIM()
hw := realHardware()
gate := dispatchMakeGate(t, rim, hw)
hsmP := dispatchMakeFileHSM(t)
appr, err := approval.NewProvider("local-dev", nil)
if err != nil {
t.Fatalf("local-dev: %v", err)
}
// Pre-seed the wrapped master seed so Provision is deterministic
// across pool members. Members holding the SAME seed produce
// byte-identical signatures under SLH-DSA SignDeterministic.
if len(seedBytes) != magnetar.MustParamsFor(magnetar.ModeM192s).SeedSize {
t.Fatalf("gateMakeSigner: seed length %d != %d", len(seedBytes), magnetar.MustParamsFor(magnetar.ModeM192s).SeedSize)
}
if err := hsmP.StoreKey(context.Background(), "master-seed", seedBytes); err != nil {
t.Fatalf("StoreKey master-seed: %v", err)
}
cfg := slhdsatee.Config{
Mode: magnetar.ModeM192s,
RequiredRIM: map[[32]byte]struct{}{rim: {}},
AllowedHardware: map[[32]byte]struct{}{hw: {}},
RequireSEVSNP: true,
KMSKeyID: "audit-key",
WrappedSeedKeyID: "master-seed",
ApprovalRequired: false,
}
signer, err := slhdsatee.New(gate, hsmP, appr, cfg)
if err != nil {
t.Fatalf("slhdsatee.New: %v", err)
}
// Derive the published group public key from the same seed via
// signer.PublicKey (HSM-backed) so the test's verification path
// uses the canonical pk.
pub, err := signer.PublicKey(context.Background())
if err != nil {
t.Fatalf("signer.PublicKey: %v", err)
}
return signer, pub, rim, hw
}
// gateMakeSharedSeed returns deterministic seed bytes so multiple pool
// members can be constructed with the same master seed (pool-level
// byte-equality check requires identical seed across members).
func gateMakeSharedSeed(t *testing.T, label byte) []byte {
t.Helper()
params := magnetar.MustParamsFor(magnetar.ModeM192s)
seed := make([]byte, params.SeedSize)
for i := range seed {
seed[i] = label ^ byte(i)
}
return seed
}
// TestMagnetarCombine_StrictPQProfile_RequiresTEE pins the central
// gate: a dispatcher whose chain profile is strict-PQ MUST refuse the
// permissionless Sign and Sign_Ctx paths with the canonical sentinel,
// regardless of session state.
func TestMagnetarCombine_StrictPQProfile_RequiresTEE(t *testing.T) {
sch := newMagnetarScheme()
// Set up a Keygen session first so the gate is the only thing
// that can refuse — otherwise an unknown-session error would
// muddle the failure mode.
kg, err := sch.Keygen(keygenParams{Threshold: 2, Participants: 3})
if err != nil {
t.Fatalf("Keygen: %v", err)
}
msg := hex.EncodeToString([]byte("strict-pq-gate"))
// Permissive default profile — Sign succeeds.
if _, err := sch.Sign(signParams{MessageHex: msg, PubKeyHex: kg.PublicKey}); err != nil {
t.Fatalf("Sign under default profile: %v", err)
}
// Flip to strict-PQ. Now both Sign and Sign_Ctx MUST refuse.
sch.SetChainSecurityProfile(slhdsatee.ProfileStrictPQ)
_, err = sch.Sign(signParams{MessageHex: msg, PubKeyHex: kg.PublicKey})
if !errors.Is(err, slhdsatee.ErrMagnetarNoTEEAttestation) {
t.Fatalf("Sign under strict-PQ: expected ErrMagnetarNoTEEAttestation, got %v", err)
}
_, err = sch.Sign_Ctx(signCtxParams{MessageHex: msg, PubKeyHex: kg.PublicKey, CtxHex: hex.EncodeToString([]byte("lux-evm-precompile-slhdsa-v1"))})
if !errors.Is(err, slhdsatee.ErrMagnetarNoTEEAttestation) {
t.Fatalf("Sign_Ctx under strict-PQ: expected ErrMagnetarNoTEEAttestation, got %v", err)
}
// Combine_TEE without wired pool: errMagnetarPoolUnwired.
_, _, err = sch.Combine_TEE(context.Background(), nil, nil, [32]byte{}, []byte("x"), nil)
if !errors.Is(err, errMagnetarPoolUnwired) {
t.Fatalf("Combine_TEE unwired: expected errMagnetarPoolUnwired, got %v", err)
}
// Flip back: Sign works again. Demonstrates profile is idempotent
// + reversible (consensus rotation can flip back if needed).
sch.SetChainSecurityProfile(slhdsatee.ProfileLegacyCompat)
if _, err := sch.Sign(signParams{MessageHex: msg, PubKeyHex: kg.PublicKey}); err != nil {
t.Fatalf("Sign after profile reset: %v", err)
}
}
// TestMagnetarCombine_AttestationVerified_AllowsSign drives the full
// strict-PQ Combine_TEE path end-to-end against the committed AMD
// Milan SEV-SNP fixture. Asserts:
// - pool with t-of-n (2-of-3) members + fresh attestations succeeds
// - output verifies under magnetar.VerifyBytes against the canonical
// group public key (no caller awareness of the pool's existence)
// - per-member audit signatures are non-empty
func TestMagnetarCombine_AttestationVerified_AllowsSign(t *testing.T) {
sharedSeed := gateMakeSharedSeed(t, 0xA1)
signer1, pub, rim, hw := gateMakeSigner(t, sharedSeed)
signer2, _, _, _ := gateMakeSigner(t, sharedSeed)
signer3, _, _, _ := gateMakeSigner(t, sharedSeed)
now := dispatchFixedNow()
pool, err := slhdsatee.NewCombinerPool(slhdsatee.CombinerPoolConfig{
Threshold: 2,
RotationWindow: 60 * time.Second,
KnownIssuers: map[string]struct{}{"amd.sev.snp": {}},
Now: func() time.Time { return now },
})
if err != nil {
t.Fatalf("NewCombinerPool: %v", err)
}
if _, err := pool.AddMember("us-east-1a", signer1); err != nil {
t.Fatalf("AddMember 1a: %v", err)
}
if _, err := pool.AddMember("us-east-1b", signer2); err != nil {
t.Fatalf("AddMember 1b: %v", err)
}
if _, err := pool.AddMember("us-east-1c", signer3); err != nil {
t.Fatalf("AddMember 1c: %v", err)
}
sch := newMagnetarScheme()
sch.SetChainSecurityProfile(slhdsatee.ProfileStrictPQ)
sch.SetCombinerPool(pool)
verifyOpts := []TEEVerifyOption{
WithKDSGetter(dispatchKDSReplay()),
WithNow(now),
}
// Attest all three combiner members.
for _, name := range []string{"us-east-1a", "us-east-1b", "us-east-1c"} {
if err := sch.AttestCombinerMember(context.Background(), name, "sev_snp",
sevSnpAttestationMilanDispatch, rim, hw, [32]byte{0x11}, verifyOpts); err != nil {
t.Fatalf("AttestCombinerMember %s: %v", name, err)
}
}
if pool.FreshMemberCount() != 3 {
t.Fatalf("FreshMemberCount = %d, want 3", pool.FreshMemberCount())
}
var jobID [32]byte
if _, err := rand.Read(jobID[:]); err != nil {
t.Fatalf("jobID: %v", err)
}
msg := []byte("strict-pq-combine-verified")
members := []PoolCombineMember{
{Name: "us-east-1a", Kind: "sev_snp", EvidenceBytes: sevSnpAttestationMilanDispatch, RIM: rim, Hardware: hw, TEEPub: [32]byte{0x21}},
{Name: "us-east-1b", Kind: "sev_snp", EvidenceBytes: sevSnpAttestationMilanDispatch, RIM: rim, Hardware: hw, TEEPub: [32]byte{0x22}},
{Name: "us-east-1c", Kind: "sev_snp", EvidenceBytes: sevSnpAttestationMilanDispatch, RIM: rim, Hardware: hw, TEEPub: [32]byte{0x23}},
}
wire, audits, err := sch.Combine_TEE(context.Background(), members, verifyOpts, jobID, msg, nil)
if err != nil {
t.Fatalf("Combine_TEE: %v", err)
}
if len(wire) == 0 {
t.Fatal("Combine_TEE returned empty wire")
}
if len(audits) < 2 {
t.Fatalf("Combine_TEE returned %d audits, want >= 2", len(audits))
}
for i, a := range audits {
if len(a) == 0 {
t.Fatalf("Combine_TEE audit[%d] is empty", i)
}
}
// External verify path — no awareness of the pool's existence.
gkBytes, err := magnetar.MarshalGroupKey(pub)
if err != nil {
t.Fatalf("MarshalGroupKey: %v", err)
}
if !magnetar.VerifyBytes(gkBytes, msg, wire) {
t.Fatal("Combine_TEE output failed external VerifyBytes")
}
}
// TestMagnetarCombine_StaleAttestation_RejectsSign pins the freshness
// gate: a pool member whose last attestation lies outside the rotation
// window MUST NOT be selected, and the overall Combine_TEE MUST
// refuse with ErrMagnetarStaleAttestation when insufficient fresh
// members exist.
func TestMagnetarCombine_StaleAttestation_RejectsSign(t *testing.T) {
sharedSeed := gateMakeSharedSeed(t, 0xB2)
signer1, _, rim, hw := gateMakeSigner(t, sharedSeed)
signer2, _, _, _ := gateMakeSigner(t, sharedSeed)
signer3, _, _, _ := gateMakeSigner(t, sharedSeed)
// Use a mutable clock so we can advance time to age the
// freshness state.
clock := &mutableClock{now: dispatchFixedNow()}
pool, err := slhdsatee.NewCombinerPool(slhdsatee.CombinerPoolConfig{
Threshold: 2,
RotationWindow: 30 * time.Second,
KnownIssuers: map[string]struct{}{"amd.sev.snp": {}},
Now: clock.Now,
})
if err != nil {
t.Fatalf("NewCombinerPool: %v", err)
}
if _, err := pool.AddMember("us-east-1a", signer1); err != nil {
t.Fatalf("AddMember 1a: %v", err)
}
if _, err := pool.AddMember("us-east-1b", signer2); err != nil {
t.Fatalf("AddMember 1b: %v", err)
}
if _, err := pool.AddMember("us-east-1c", signer3); err != nil {
t.Fatalf("AddMember 1c: %v", err)
}
sch := newMagnetarScheme()
sch.SetChainSecurityProfile(slhdsatee.ProfileStrictPQ)
sch.SetCombinerPool(pool)
verifyOpts := []TEEVerifyOption{
WithKDSGetter(dispatchKDSReplay()),
WithNow(dispatchFixedNow()), // pin to fixture window
}
// Attest all three at clock=now0.
for _, name := range []string{"us-east-1a", "us-east-1b", "us-east-1c"} {
if err := sch.AttestCombinerMember(context.Background(), name, "sev_snp",
sevSnpAttestationMilanDispatch, rim, hw, [32]byte{0x11}, verifyOpts); err != nil {
t.Fatalf("AttestCombinerMember %s: %v", name, err)
}
}
// Advance the clock past the rotation window. All three members
// are now stale; pool MUST refuse.
clock.Advance(45 * time.Second)
if pool.FreshMemberCount() != 0 {
t.Fatalf("FreshMemberCount = %d after rotation, want 0", pool.FreshMemberCount())
}
var jobID [32]byte
if _, err := rand.Read(jobID[:]); err != nil {
t.Fatalf("jobID: %v", err)
}
msg := []byte("strict-pq-stale-rejected")
members := []PoolCombineMember{
{Name: "us-east-1a", Kind: "sev_snp", EvidenceBytes: sevSnpAttestationMilanDispatch, RIM: rim, Hardware: hw, TEEPub: [32]byte{0x21}},
{Name: "us-east-1b", Kind: "sev_snp", EvidenceBytes: sevSnpAttestationMilanDispatch, RIM: rim, Hardware: hw, TEEPub: [32]byte{0x22}},
{Name: "us-east-1c", Kind: "sev_snp", EvidenceBytes: sevSnpAttestationMilanDispatch, RIM: rim, Hardware: hw, TEEPub: [32]byte{0x23}},
}
_, _, err = sch.Combine_TEE(context.Background(), members, verifyOpts, jobID, msg, nil)
if !errors.Is(err, slhdsatee.ErrMagnetarStaleAttestation) {
t.Fatalf("Combine_TEE on stale pool: expected ErrMagnetarStaleAttestation, got %v", err)
}
// Re-attest two of the three members — that suffices for t=2 to
// produce a signature. Demonstrates partial-rotation recovery.
if err := sch.AttestCombinerMember(context.Background(), "us-east-1a", "sev_snp",
sevSnpAttestationMilanDispatch, rim, hw, [32]byte{0x11}, verifyOpts); err != nil {
t.Fatalf("Re-attest 1a: %v", err)
}
if err := sch.AttestCombinerMember(context.Background(), "us-east-1b", "sev_snp",
sevSnpAttestationMilanDispatch, rim, hw, [32]byte{0x11}, verifyOpts); err != nil {
t.Fatalf("Re-attest 1b: %v", err)
}
if pool.FreshMemberCount() != 2 {
t.Fatalf("FreshMemberCount post-rotation = %d, want 2", pool.FreshMemberCount())
}
// Now Combine_TEE succeeds with two fresh members (third stale).
wire, _, err := sch.Combine_TEE(context.Background(), members, verifyOpts, jobID, msg, nil)
if err != nil {
t.Fatalf("Combine_TEE after partial rotation: %v", err)
}
if len(wire) == 0 {
t.Fatal("Combine_TEE wire empty after partial rotation")
}
}
// TestMagnetarCombine_TwoOfThreeAttestedCombiners_MatchSig pins the
// quorum robustness: a pool with 3 members but only 2 fresh attestations
// MUST surface a signature when (a) the 2 fresh members agree
// byte-for-byte and (b) the third stale member is not in the selected
// quorum.
func TestMagnetarCombine_TwoOfThreeAttestedCombiners_MatchSig(t *testing.T) {
sharedSeed := gateMakeSharedSeed(t, 0xC3)
signer1, pub, rim, hw := gateMakeSigner(t, sharedSeed)
signer2, _, _, _ := gateMakeSigner(t, sharedSeed)
signer3, _, _, _ := gateMakeSigner(t, sharedSeed)
now := dispatchFixedNow()
pool, err := slhdsatee.NewCombinerPool(slhdsatee.CombinerPoolConfig{
Threshold: 2,
RotationWindow: 60 * time.Second,
KnownIssuers: map[string]struct{}{"amd.sev.snp": {}},
Now: func() time.Time { return now },
})
if err != nil {
t.Fatalf("NewCombinerPool: %v", err)
}
if _, err := pool.AddMember("us-east-1a", signer1); err != nil {
t.Fatalf("AddMember 1a: %v", err)
}
if _, err := pool.AddMember("us-east-1b", signer2); err != nil {
t.Fatalf("AddMember 1b: %v", err)
}
if _, err := pool.AddMember("us-east-1c", signer3); err != nil {
t.Fatalf("AddMember 1c: %v", err)
}
sch := newMagnetarScheme()
sch.SetChainSecurityProfile(slhdsatee.ProfileStrictPQ)
sch.SetCombinerPool(pool)
verifyOpts := []TEEVerifyOption{
WithKDSGetter(dispatchKDSReplay()),
WithNow(now),
}
// Attest only 1a + 1b. 1c stays un-attested.
for _, name := range []string{"us-east-1a", "us-east-1b"} {
if err := sch.AttestCombinerMember(context.Background(), name, "sev_snp",
sevSnpAttestationMilanDispatch, rim, hw, [32]byte{0x11}, verifyOpts); err != nil {
t.Fatalf("AttestCombinerMember %s: %v", name, err)
}
}
if pool.FreshMemberCount() != 2 {
t.Fatalf("FreshMemberCount = %d, want 2", pool.FreshMemberCount())
}
var jobID [32]byte
if _, err := rand.Read(jobID[:]); err != nil {
t.Fatalf("jobID: %v", err)
}
msg := []byte("strict-pq-2-of-3-attested-match")
members := []PoolCombineMember{
{Name: "us-east-1a", Kind: "sev_snp", EvidenceBytes: sevSnpAttestationMilanDispatch, RIM: rim, Hardware: hw, TEEPub: [32]byte{0x21}},
{Name: "us-east-1b", Kind: "sev_snp", EvidenceBytes: sevSnpAttestationMilanDispatch, RIM: rim, Hardware: hw, TEEPub: [32]byte{0x22}},
{Name: "us-east-1c", Kind: "sev_snp", EvidenceBytes: sevSnpAttestationMilanDispatch, RIM: rim, Hardware: hw, TEEPub: [32]byte{0x23}},
}
wire, audits, err := sch.Combine_TEE(context.Background(), members, verifyOpts, jobID, msg, nil)
if err != nil {
t.Fatalf("Combine_TEE 2-of-3 attested: %v", err)
}
if len(audits) != 2 {
t.Fatalf("audits count = %d, want 2 (only 1a + 1b attested)", len(audits))
}
// Verifies under the canonical pub.
gkBytes, err := magnetar.MarshalGroupKey(pub)
if err != nil {
t.Fatalf("MarshalGroupKey: %v", err)
}
if !magnetar.VerifyBytes(gkBytes, msg, wire) {
t.Fatal("Combine_TEE 2-of-3 output failed external VerifyBytes")
}
// Drop the threshold further by un-attesting 1b. Now only 1a is
// fresh; quorum=2 must refuse.
clock2 := &mutableClock{now: now}
pool2, _ := slhdsatee.NewCombinerPool(slhdsatee.CombinerPoolConfig{
Threshold: 2,
RotationWindow: 60 * time.Second,
KnownIssuers: map[string]struct{}{"amd.sev.snp": {}},
Now: clock2.Now,
})
if _, err := pool2.AddMember("us-east-1a", signer1); err != nil {
t.Fatalf("AddMember 1a (pool2): %v", err)
}
if _, err := pool2.AddMember("us-east-1b", signer2); err != nil {
t.Fatalf("AddMember 1b (pool2): %v", err)
}
if _, err := pool2.AddMember("us-east-1c", signer3); err != nil {
t.Fatalf("AddMember 1c (pool2): %v", err)
}
sch2 := newMagnetarScheme()
sch2.SetChainSecurityProfile(slhdsatee.ProfileStrictPQ)
sch2.SetCombinerPool(pool2)
if err := sch2.AttestCombinerMember(context.Background(), "us-east-1a", "sev_snp",
sevSnpAttestationMilanDispatch, rim, hw, [32]byte{0x11}, verifyOpts); err != nil {
t.Fatalf("AttestCombinerMember pool2 1a: %v", err)
}
// With 1-of-3 attested and 2-of-3 NEVER attested, the pool surfaces
// the freshness-class refusal (stale) because un-attested members
// have LastIssuedAt == zero, which the pool treats as outside any
// rotation window. Stale wins over insufficient when ANY would-be
// quorum member's freshness has lapsed (or never started). Both
// sentinels would have been acceptable; we pin the actual surface.
_, _, err = sch2.Combine_TEE(context.Background(), members, verifyOpts, jobID, msg, nil)
if !errors.Is(err, slhdsatee.ErrMagnetarStaleAttestation) && !errors.Is(err, slhdsatee.ErrMagnetarInsufficientQuorum) {
t.Fatalf("Combine_TEE 1-of-3 attested: expected stale or insufficient-quorum sentinel, got %v", err)
}
}
// TestMagnetarCombine_SignatureDivergence_HardRefusal pins the
// byte-equality discipline: if two attested combiners disagree on the
// produced bytes (corruption or compromise), the pool MUST refuse with
// ErrMagnetarSignatureDivergence — no silent winner-picking.
//
// We exercise this by registering two members with DIFFERENT seeds —
// SLH-DSA SignDeterministic over different seeds produces different
// bytes, which the pool's byte-equality compare must reject.
func TestMagnetarCombine_SignatureDivergence_HardRefusal(t *testing.T) {
seedA := gateMakeSharedSeed(t, 0xD4)
seedB := gateMakeSharedSeed(t, 0xE5)
signer1, _, rim, hw := gateMakeSigner(t, seedA)
signer2, _, _, _ := gateMakeSigner(t, seedB) // DIFFERENT seed
signer3, _, _, _ := gateMakeSigner(t, seedA)
now := dispatchFixedNow()
pool, err := slhdsatee.NewCombinerPool(slhdsatee.CombinerPoolConfig{
Threshold: 2,
RotationWindow: 60 * time.Second,
KnownIssuers: map[string]struct{}{"amd.sev.snp": {}},
Now: func() time.Time { return now },
})
if err != nil {
t.Fatalf("NewCombinerPool: %v", err)
}
if _, err := pool.AddMember("alpha", signer1); err != nil {
t.Fatalf("AddMember alpha: %v", err)
}
if _, err := pool.AddMember("beta", signer2); err != nil {
t.Fatalf("AddMember beta: %v", err)
}
if _, err := pool.AddMember("gamma", signer3); err != nil {
t.Fatalf("AddMember gamma: %v", err)
}
sch := newMagnetarScheme()
sch.SetChainSecurityProfile(slhdsatee.ProfileStrictPQ)
sch.SetCombinerPool(pool)
verifyOpts := []TEEVerifyOption{
WithKDSGetter(dispatchKDSReplay()),
WithNow(now),
}
// Attest alpha + beta only — Threshold=2, first 2 in name order
// will be selected for the quorum. They hold different seeds, so
// their wire output diverges.
for _, name := range []string{"alpha", "beta"} {
if err := sch.AttestCombinerMember(context.Background(), name, "sev_snp",
sevSnpAttestationMilanDispatch, rim, hw, [32]byte{0x11}, verifyOpts); err != nil {
t.Fatalf("AttestCombinerMember %s: %v", name, err)
}
}
var jobID [32]byte
if _, err := rand.Read(jobID[:]); err != nil {
t.Fatalf("jobID: %v", err)
}
msg := []byte("divergence-refused")
members := []PoolCombineMember{
{Name: "alpha", Kind: "sev_snp", EvidenceBytes: sevSnpAttestationMilanDispatch, RIM: rim, Hardware: hw, TEEPub: [32]byte{0x21}},
{Name: "beta", Kind: "sev_snp", EvidenceBytes: sevSnpAttestationMilanDispatch, RIM: rim, Hardware: hw, TEEPub: [32]byte{0x22}},
{Name: "gamma", Kind: "sev_snp", EvidenceBytes: sevSnpAttestationMilanDispatch, RIM: rim, Hardware: hw, TEEPub: [32]byte{0x23}},
}
_, _, err = sch.Combine_TEE(context.Background(), members, verifyOpts, jobID, msg, nil)
if !errors.Is(err, slhdsatee.ErrMagnetarSignatureDivergence) {
t.Fatalf("Combine_TEE on divergent quorum: expected ErrMagnetarSignatureDivergence, got %v", err)
}
}
// mutableClock is a manually-advanced wall-clock used to age the
// pool's freshness state.
type mutableClock struct {
now time.Time
}
func (c *mutableClock) Now() time.Time { return c.now }
func (c *mutableClock) Advance(d time.Duration) {
c.now = c.now.Add(d)
}
// dispatchMakeFileHSM_consolidated is referenced by the older
// dispatcher tests via dispatchMakeFileHSM; included here for the
// hsm.Provider import alias to satisfy the new test file's references.
var _ hsm.Provider = (hsm.Provider)(nil)
-69
View File
@@ -2,18 +2,14 @@
package thresholdd
import (
"context"
"crypto/rand"
"crypto/sha256"
"encoding/binary"
"encoding/hex"
"errors"
"fmt"
"sync"
pulsar "github.com/luxfi/pulsar/ref/go/pkg/pulsar"
mldsatee "github.com/luxfi/threshold/protocols/mldsa-tee"
)
// pulsarScheme wires luxfi/pulsar (Module-LWE FIPS 204 ML-DSA-65
@@ -81,15 +77,8 @@ import (
type pulsarScheme struct {
mu sync.Mutex
sessions map[string]*pulsarSession
// teeBackend is the optional institutional-custody ML-DSA signer
// wired via SetTEEBackend. nil → Sign_TEE refuses.
teeBackend *mldsatee.Signer
}
// errPulsarTEEUnwired is returned by Sign_TEE when no TEE backend is registered.
var errPulsarTEEUnwired = errors.New("pulsar tee sign: no TEE backend wired (call SetTEEBackend first)")
// pulsarSession holds the in-process per-party state for a single
// v0.3 algebraic-aggregate keygen output.
type pulsarSession struct {
@@ -323,9 +312,6 @@ func (s *pulsarScheme) Sign(p signParams) (signResult, error) {
// quantified) master sk. The historical dealerKey single-party
// shortcut has been deleted.
//
// Compare to Sign_TEE: same ctx semantics, HSM-held sk. Both
// produce wire bytes verifiable under the same PULG-framed group key.
//
// signCtx is the FIPS 204 ctx octet string (0..255 bytes). Pass nil
// (or the empty hex string "") to bind the empty ctx — backwards
// compatible with v0.3 OrchestrateV03Sign byte-for-byte under the
@@ -477,58 +463,3 @@ func (s *pulsarScheme) Verify(p verifyParams) (verifyResult, error) {
}
return verifyResult{OK: pulsar.VerifyBytes(gkBytes, msg, sigBytes)}, nil
}
// SetTEEBackend wires a mldsatee.Signer as the institutional-custody
// TEE-gated signing path. The default `pulsar.sign` procedure is
// UNAFFECTED — it remains the permissionless v0.3 algebraic-aggregate
// path.
//
// Passing nil clears the backend (subsequent Sign_TEE calls return
// errPulsarTEEUnwired).
func (s *pulsarScheme) SetTEEBackend(b *mldsatee.Signer) {
s.mu.Lock()
s.teeBackend = b
s.mu.Unlock()
}
// Sign_TEE is the institutional-custody opt-in signing path. Mirrors
// magnetarScheme.Sign_TEE; the inner primitive is FIPS 204 ML-DSA via
// the mldsatee.Signer.
//
// Returns the PULS-framed wire signature + the SignReceipt audit
// signature bytes.
func (s *pulsarScheme) Sign_TEE(
ctx context.Context,
kind string,
evidenceBytes []byte,
rim, hardware, teePub [32]byte,
verifyOpts []TEEVerifyOption,
jobID [32]byte,
msg []byte,
signCtx []byte,
) ([]byte, []byte, error) {
s.mu.Lock()
b := s.teeBackend
s.mu.Unlock()
if b == nil {
return nil, nil, errPulsarTEEUnwired
}
env := &mldsatee.Envelope{
Kind: attestKindFromString(kind),
EvidenceBytes: append([]byte(nil), evidenceBytes...),
RIM: rim,
Hardware: hardware,
TEEPub: teePub,
VerifyOpts: teeVerifyOptionsToAttest(verifyOpts),
}
wire, receipt, err := b.Sign(ctx, env, jobID, msg, signCtx)
if err != nil {
return nil, nil, fmt.Errorf("pulsar tee sign: %w", err)
}
if receipt == nil {
return nil, nil, fmt.Errorf("pulsar tee sign: nil receipt")
}
return wire, receipt.AuditSignature, nil
}
-338
View File
@@ -1,338 +0,0 @@
// SPDX-License-Identifier: BSD-3-Clause
package thresholdd
import (
"context"
"crypto/rand"
"crypto/sha256"
_ "embed"
"errors"
"os"
"testing"
"time"
sevtest "github.com/google/go-sev-guest/testing"
"github.com/google/go-sev-guest/verify/trust"
magnetar "github.com/luxfi/magnetar/ref/go/pkg/magnetar"
pulsar "github.com/luxfi/pulsar/ref/go/pkg/pulsar"
coronaThreshold "github.com/luxfi/corona/threshold"
"github.com/luxfi/mpc/pkg/approval"
"github.com/luxfi/mpc/pkg/hsm"
"github.com/luxfi/mpc/pkg/kms"
mldsatee "github.com/luxfi/threshold/protocols/mldsa-tee"
rlwetee "github.com/luxfi/threshold/protocols/rlwe-tee"
slhdsatee "github.com/luxfi/threshold/protocols/slhdsa-tee"
)
// sevSnpAttestationMilan and sevSnpVcekMilan — committed AMD Milan
// fixtures (same bytes as the lux/mpc cc/attest test corpus).
//
//go:embed testdata/sev_snp_attestation_milan.bin
var sevSnpAttestationMilanDispatch []byte
//go:embed testdata/sev_snp_vcek_milan.cer
var sevSnpVcekMilanDispatch []byte
func dispatchKDSReplay() trust.HTTPSGetter {
return sevtest.SimpleGetter(map[string][]byte{
"https://kdsintf.amd.com/vcek/v1/Milan/cert_chain": trust.AskArkMilanVcekBytes,
"https://kdsintf.amd.com/vcek/v1/Milan/3ac3fe21e13fb0990eb28a802e3fb6a29483a6b0753590c951bdd3b8e53786184ca39e359669a2b76a1936776b564ea464cdce40c05f63c9b610c5068b006b5d?blSPL=2&teeSPL=0&snpSPL=5&ucodeSPL=68": sevSnpVcekMilanDispatch,
})
}
func dispatchFixedNow() time.Time {
return time.Date(2024, 1, 15, 12, 0, 0, 0, time.UTC)
}
func dispatchMakeFileHSM(t *testing.T) hsm.Provider {
t.Helper()
dir := t.TempDir()
p, err := hsm.NewFileProvider(&hsm.FileConfig{
BasePath: dir,
HexEncoded: true,
})
if err != nil {
t.Fatalf("NewFileProvider: %v", err)
}
var ed25519Seed [32]byte
if _, err := rand.Read(ed25519Seed[:]); err != nil {
t.Fatalf("ed25519 seed: %v", err)
}
if err := p.StoreKey(context.Background(), "audit-key", ed25519Seed[:]); err != nil {
t.Fatalf("store audit: %v", err)
}
t.Cleanup(func() {
_ = p.Close()
_ = os.RemoveAll(dir)
})
return p
}
func dispatchMakeGate(t *testing.T, rim, hw [32]byte) *kms.LocalReleaseGate {
t.Helper()
policy := kms.NewReleasePolicy([][32]byte{rim}, [][32]byte{hw})
policy.RequireSEVSNP = true
var rootKey [32]byte
if _, err := rand.Read(rootKey[:]); err != nil {
t.Fatalf("rootKey: %v", err)
}
gate, err := kms.NewLocalReleaseGate(policy, kms.NewMemoryNonceStore(), rootKey)
if err != nil {
t.Fatalf("NewLocalReleaseGate: %v", err)
}
gate.SetIssueTTL(5 * time.Second)
gate.SetReplayWindow(5 * time.Second)
return gate
}
// MPC_LOCAL_APPROVAL=true is exported by the package's existing
// TestMain (thresholdd_test.go).
// realRIM / realHardware mirror the per-package helpers.
func realRIM() [32]byte {
return sha256.Sum256(sevSnpAttestationMilanDispatch[0x90 : 0x90+48])
}
func realHardware() [32]byte {
return sha256.Sum256(sevSnpAttestationMilanDispatch[0x1A0 : 0x1A0+64])
}
// TestMagnetar_Sign_TEE_Dispatch wires a real slhdsatee.Signer into
// the magnetar dispatcher, drives Sign_TEE end-to-end, and asserts
// the returned wire bytes verify under magnetar.VerifyBytes against
// the in-memory public key.
func TestMagnetar_Sign_TEE_Dispatch(t *testing.T) {
rim := realRIM()
hw := realHardware()
gate := dispatchMakeGate(t, rim, hw)
hsmP := dispatchMakeFileHSM(t)
appr, err := approval.NewProvider("local-dev", nil)
if err != nil {
t.Fatalf("local-dev: %v", err)
}
cfg := slhdsatee.Config{
Mode: magnetar.ModeM192s,
RequiredRIM: map[[32]byte]struct{}{rim: {}},
AllowedHardware: map[[32]byte]struct{}{hw: {}},
RequireSEVSNP: true,
KMSKeyID: "audit-key",
WrappedSeedKeyID: "master-seed",
ApprovalRequired: false, // single-flow test
}
signer, err := slhdsatee.New(gate, hsmP, appr, cfg)
if err != nil {
t.Fatalf("slhdsatee.New: %v", err)
}
pub, err := signer.Provision(context.Background(), nil)
if err != nil {
t.Fatalf("Provision: %v", err)
}
sch := newMagnetarScheme()
// Unwired Sign_TEE must refuse.
_, _, err = sch.Sign_TEE(context.Background(), "sev_snp", sevSnpAttestationMilanDispatch,
rim, hw, [32]byte{}, nil, [32]byte{}, []byte("x"), nil)
if !errors.Is(err, errMagnetarTEEUnwired) {
t.Fatalf("expected errMagnetarTEEUnwired, got %v", err)
}
sch.SetTEEBackend(signer)
var teePub [32]byte
for i := range teePub {
teePub[i] = byte(i + 17)
}
var jobID [32]byte
if _, err := rand.Read(jobID[:]); err != nil {
t.Fatalf("jobID: %v", err)
}
msg := []byte("dispatcher-tee-magnetar")
verifyOpts := []TEEVerifyOption{
WithKDSGetter(dispatchKDSReplay()),
WithNow(dispatchFixedNow()),
}
wire, audit, err := sch.Sign_TEE(context.Background(), "sev_snp", sevSnpAttestationMilanDispatch,
rim, hw, teePub, verifyOpts, jobID, msg, nil)
if err != nil {
t.Fatalf("Sign_TEE: %v", err)
}
if len(wire) == 0 || len(audit) == 0 {
t.Fatal("Sign_TEE returned empty payload")
}
gkBytes, err := magnetar.MarshalGroupKey(pub)
if err != nil {
t.Fatalf("MarshalGroupKey: %v", err)
}
if !magnetar.VerifyBytes(gkBytes, msg, wire) {
t.Fatal("Sign_TEE output failed external VerifyBytes")
}
}
// TestPulsar_Sign_TEE_Dispatch — same shape, FIPS 204 backend.
func TestPulsar_Sign_TEE_Dispatch(t *testing.T) {
rim := realRIM()
hw := realHardware()
gate := dispatchMakeGate(t, rim, hw)
hsmP := dispatchMakeFileHSM(t)
appr, err := approval.NewProvider("local-dev", nil)
if err != nil {
t.Fatalf("local-dev: %v", err)
}
cfg := mldsatee.Config{
Mode: pulsar.ModeP65,
RequiredRIM: map[[32]byte]struct{}{rim: {}},
AllowedHardware: map[[32]byte]struct{}{hw: {}},
RequireSEVSNP: true,
KMSKeyID: "audit-key",
WrappedSeedKeyID: "master-seed",
ApprovalRequired: false,
}
signer, err := mldsatee.New(gate, hsmP, appr, cfg)
if err != nil {
t.Fatalf("mldsatee.New: %v", err)
}
pub, err := signer.Provision(context.Background())
if err != nil {
t.Fatalf("Provision: %v", err)
}
sch := newPulsarScheme()
// Unwired refuse.
_, _, err = sch.Sign_TEE(context.Background(), "sev_snp", sevSnpAttestationMilanDispatch,
rim, hw, [32]byte{}, nil, [32]byte{}, []byte("x"), nil)
if !errors.Is(err, errPulsarTEEUnwired) {
t.Fatalf("expected errPulsarTEEUnwired, got %v", err)
}
sch.SetTEEBackend(signer)
var teePub [32]byte
for i := range teePub {
teePub[i] = byte(i + 17)
}
var jobID [32]byte
if _, err := rand.Read(jobID[:]); err != nil {
t.Fatalf("jobID: %v", err)
}
msg := []byte("dispatcher-tee-pulsar")
verifyOpts := []TEEVerifyOption{
WithKDSGetter(dispatchKDSReplay()),
WithNow(dispatchFixedNow()),
}
wire, audit, err := sch.Sign_TEE(context.Background(), "sev_snp", sevSnpAttestationMilanDispatch,
rim, hw, teePub, verifyOpts, jobID, msg, nil)
if err != nil {
t.Fatalf("Sign_TEE: %v", err)
}
if len(wire) == 0 || len(audit) == 0 {
t.Fatal("Sign_TEE returned empty payload")
}
gkBytes, err := pub.MarshalBinary()
if err != nil {
t.Fatalf("pub.MarshalBinary: %v", err)
}
if !pulsar.VerifyBytes(gkBytes, msg, wire) {
t.Fatal("Sign_TEE output failed external VerifyBytes")
}
}
// TestCorona_Sign_TEE_Dispatch — same shape, corona Ring-LWE backend.
func TestCorona_Sign_TEE_Dispatch(t *testing.T) {
rim := realRIM()
hw := realHardware()
gate := dispatchMakeGate(t, rim, hw)
hsmP := dispatchMakeFileHSM(t)
appr, err := approval.NewProvider("local-dev", nil)
if err != nil {
t.Fatalf("local-dev: %v", err)
}
cfg := rlwetee.Config{
Threshold: 2,
Participants: 3,
RequiredRIM: map[[32]byte]struct{}{rim: {}},
AllowedHardware: map[[32]byte]struct{}{hw: {}},
RequireSEVSNP: true,
KMSKeyID: "audit-key",
WrappedSeedKeyID: "master-key",
ApprovalRequired: false,
}
signer, err := rlwetee.New(gate, hsmP, appr, cfg)
if err != nil {
t.Fatalf("rlwetee.New: %v", err)
}
gk, err := signer.Provision(context.Background())
if err != nil {
t.Fatalf("Provision: %v", err)
}
sch := newCoronaScheme()
_, _, err = sch.Sign_TEE(context.Background(), "sev_snp", sevSnpAttestationMilanDispatch,
rim, hw, [32]byte{}, nil, [32]byte{}, []byte("x"))
if !errors.Is(err, errCoronaTEEUnwired) {
t.Fatalf("expected errCoronaTEEUnwired, got %v", err)
}
sch.SetTEEBackend(signer)
var teePub [32]byte
for i := range teePub {
teePub[i] = byte(i + 17)
}
var jobID [32]byte
if _, err := rand.Read(jobID[:]); err != nil {
t.Fatalf("jobID: %v", err)
}
msg := []byte("dispatcher-tee-corona")
verifyOpts := []TEEVerifyOption{
WithKDSGetter(dispatchKDSReplay()),
WithNow(dispatchFixedNow()),
}
wire, audit, err := sch.Sign_TEE(context.Background(), "sev_snp", sevSnpAttestationMilanDispatch,
rim, hw, teePub, verifyOpts, jobID, msg)
if err != nil {
t.Fatalf("Sign_TEE: %v", err)
}
if len(wire) == 0 || len(audit) == 0 {
t.Fatal("Sign_TEE returned empty payload")
}
gkBytes, err := gk.MarshalBinary()
if err != nil {
t.Fatalf("gk.MarshalBinary: %v", err)
}
if !coronaThreshold.VerifyBytes(gkBytes, string(msg), wire) {
t.Fatal("Sign_TEE output failed external VerifyBytes")
}
// Defence: passing a custom kind not in our enum maps through to
// cc/attest.Dispatch which returns ErrUnsupportedKind. The release
// gate's VerifyEvidence step calls Dispatch; rlwetee.ErrPolicyRefused
// is the canonical wrap so we assert that sentinel. The inner
// ErrUnsupportedKind appears in err.Error() but isn't surfaced via
// errors.Is because the gate joins via %v not %w (kms upstream API).
var jobID2 [32]byte
if _, err := rand.Read(jobID2[:]); err != nil {
t.Fatalf("jobID2: %v", err)
}
_, _, err = sch.Sign_TEE(context.Background(), "unknown-kind", sevSnpAttestationMilanDispatch,
rim, hw, teePub, verifyOpts, jobID2, msg)
if err == nil {
t.Fatal("Sign_TEE: expected error on unknown kind")
}
if !errors.Is(err, rlwetee.ErrPolicyRefused) {
t.Errorf("Sign_TEE: err = %v, want wrapped rlwetee.ErrPolicyRefused", err)
}
}
-115
View File
@@ -1,115 +0,0 @@
// SPDX-License-Identifier: BSD-3-Clause
package thresholdd
import (
"time"
"github.com/google/go-sev-guest/verify/trust"
"github.com/luxfi/mpc/cc/attest"
)
// Shared TEE-only options + helpers consumed by every scheme's
// Sign_TEE method. Decomplecting: every TEE scheme (slhdsa, mldsa,
// rlwe) consumes the same set of cc/attest.Option values, so we name
// them once here rather than duplicate across each dispatcher file.
// slhdsateeVerifyOpt is the dispatcher-side option carrier used by
// Sign_TEE. We expose this concrete type rather than the cc/attest
// internal config so embedders that drive the dispatcher do not need
// to import luxfi/mpc/cc/attest directly.
//
// The name is shared across slhdsa-tee / mldsa-tee / rlwe-tee
// dispatcher entry points; the type-safe distinction the caller
// needs is the value-builder (NewKDSReplay, WithNow, etc.), not the
// scheme name.
type slhdsateeVerifyOpt struct {
// nowOverride pins the verifier wall clock. Tests use this to
// stay inside a committed VCEK's validity window.
nowOverride time.Time
// kdsGetter installs a custom HTTPSGetter for SEV-SNP / TDX
// chain fetch. Tests use this to replay pre-fetched bytes.
kdsGetter trust.HTTPSGetter
// expectedReportData pins the byte-level REPORT_DATA / REPORTDATA
// field of the evidence (kind-specific 64-byte field on SEV/TDX,
// JWT claim on NRAS). Production callers MUST bind their
// gate-issued nonce here.
expectedReportData []byte
// expectedMeasurement pins a known-good launch digest. Tests
// usually omit so the membership-check via the gate's
// RequiredRIM allowlist is the authority.
expectedMeasurement []byte
}
// TEEVerifyOption is the public alias for the dispatcher's verify
// option carrier. Embedders construct values via WithNow /
// WithKDSGetter / WithExpectedReportData / WithExpectedMeasurement.
type TEEVerifyOption = slhdsateeVerifyOpt
// WithNow pins the verification clock. Tests use this to stay inside
// a committed VCEK / TDX-quote validity window.
func WithNow(t time.Time) TEEVerifyOption {
return slhdsateeVerifyOpt{nowOverride: t}
}
// WithKDSGetter installs an HTTPSGetter override for the AMD KDS
// (SEV-SNP) / Intel PCS (TDX) / NRAS JWKS endpoints.
func WithKDSGetter(g trust.HTTPSGetter) TEEVerifyOption {
return slhdsateeVerifyOpt{kdsGetter: g}
}
// WithExpectedReportData pins the byte-level REPORT_DATA field of
// the evidence. Production callers MUST bind their gate-issued
// nonce here.
func WithExpectedReportData(want []byte) TEEVerifyOption {
buf := make([]byte, len(want))
copy(buf, want)
return slhdsateeVerifyOpt{expectedReportData: buf}
}
// WithExpectedMeasurement pins a known-good launch digest beyond the
// gate's RIM allowlist.
func WithExpectedMeasurement(want []byte) TEEVerifyOption {
buf := make([]byte, len(want))
copy(buf, want)
return slhdsateeVerifyOpt{expectedMeasurement: buf}
}
// teeVerifyOptionsToAttest converts the dispatcher's options into the
// underlying cc/attest.Option list. Splitting this out keeps the
// per-scheme Sign_TEE implementations identical.
func teeVerifyOptionsToAttest(opts []slhdsateeVerifyOpt) []attest.Option {
out := make([]attest.Option, 0, len(opts))
for _, o := range opts {
if !o.nowOverride.IsZero() {
out = append(out, attest.WithNow(o.nowOverride))
}
if o.kdsGetter != nil {
out = append(out, attest.WithKDSGetter(o.kdsGetter))
}
if o.expectedReportData != nil {
out = append(out, attest.WithExpectedReportData(o.expectedReportData))
}
if o.expectedMeasurement != nil {
out = append(out, attest.WithExpectedMeasurement(o.expectedMeasurement))
}
}
return out
}
// attestKindFromString parses the wire-stable evidence-kind string
// into the cc/attest.Kind enum. Unknown kinds map to the empty kind
// which Dispatch rejects with ErrUnsupportedKind — the dispatcher
// surface then surfaces that as a hard refusal.
func attestKindFromString(s string) attest.Kind {
switch s {
case "sev_snp":
return attest.KindSEVSNP
case "tdx":
return attest.KindTDX
case "nras":
return attest.KindNRAS
default:
return attest.Kind(s)
}
}
Binary file not shown.
Binary file not shown.
-40
View File
@@ -1,40 +0,0 @@
# mldsa-tee
Operator-controlled ML-DSA threshold signing via TEE-gated master-seed
reconstruction.
## What this is
Sibling of `protocols/slhdsa-tee` for the FIPS 204 (ML-DSA / Dilithium)
primitive. Output is byte-identical to single-party
`pulsar.SignDeterministic` on the same seed-derived `sk`.
## When to use
- Institutional custody of ML-DSA signing keys with HSM-resident wrap.
- IAM signing-as-a-service that requires attested release per operation.
- Bridge oracles whose signing key is governed by an executive approval flow.
## When NOT to use
- Permissionless threshold custody: use `pulsar.OrchestrateV03Sign`
(v0.3 AlgebraicAggregate) — no party ever holds the master sk.
- Single-party / dev: use `pulsar.GenerateKey` + `pulsar.Sign`.
## Layering
```
caller
└── mldsatee.Signer.Sign(ctx, env, jobID, msg, signCtx)
├── approval.ApprovalProvider.ApproveIntent
├── kms.ReleaseGate.Issue / Release
│ └── cc/attest.Dispatch
├── hsm.Provider.GetKey
├── pulsar.KeyFromSeed → pulsar.Sign
└── hsm.Provider.Sign (audit)
```
Each step is independently complete and replaceable. The default
permissionless path (`pulsar.OrchestrateV03Sign`) remains the
canonical surface; this TEE-extension is opt-in via the threshold
dispatcher's `Sign_TEE` method.
-97
View File
@@ -1,97 +0,0 @@
// SPDX-License-Identifier: BSD-3-Clause
package mldsatee
import (
"errors"
"fmt"
pulsar "github.com/luxfi/pulsar/ref/go/pkg/pulsar"
)
// Config carries the operator-side policy + provider configuration.
//
// Every field is required; New refuses on any zero value. There is
// no "default-friendly" path — institutional custody never starts
// with an empty allowlist or an unset KMS root.
type Config struct {
// Mode pins the FIPS 204 parameter set. Production institutional
// custody uses ModeP65 (recommended, NIST PQ category 3) or
// ModeP87 for the most conservative posture. Mode is bound into
// Signer.params at construction and into the keypair derived
// from the unwrapped master seed.
Mode pulsar.Mode
// RequiredRIM is the set of acceptable Reference-Integrity-Manifest
// digests for the worker that holds the wrapped master seed.
RequiredRIM map[[32]byte]struct{}
// AllowedHardware is the set of acceptable hardware-fingerprint
// digests.
AllowedHardware map[[32]byte]struct{}
// RequireSEVSNP / RequireTDX / RequireNVNRAS mirror
// kms.ReleasePolicy.Require* — at least one MUST be true.
RequireSEVSNP bool
RequireTDX bool
RequireNVNRAS bool
// KMSKeyID is the HSM key identifier used for the audit
// signature over (jobID || msgDigest || epoch || RIM).
KMSKeyID string
// WrappedSeedKeyID is the HSM-stored blob identifier for the
// wrapped 32-byte master ML-DSA seed.
WrappedSeedKeyID string
// ApprovalRequired determines whether ApprovalProvider must
// produce a non-deny ApprovalSignature before Issue() is called.
ApprovalRequired bool
// ApproverID is the canonical identifier (email, DID, KMS ARN)
// whose approval is required.
ApproverID string
}
// Errors surfaced by Config.Validate and the Sign flow.
var (
ErrInvalidMode = errors.New("mldsa-tee: invalid pulsar mode")
ErrEmptyRIM = errors.New("mldsa-tee: RequiredRIM must be non-empty (default-deny posture)")
ErrEmptyHardware = errors.New("mldsa-tee: AllowedHardware must be non-empty (default-deny posture)")
ErrNoRequireFlag = errors.New("mldsa-tee: at least one Require* TEE flag must be true")
ErrMissingKMSKeyID = errors.New("mldsa-tee: KMSKeyID required for audit signature")
ErrMissingSeedKeyID = errors.New("mldsa-tee: WrappedSeedKeyID required for HSM seed storage")
ErrApproverMissing = errors.New("mldsa-tee: ApproverID required when ApprovalRequired is true")
ErrApprovalDenied = errors.New("mldsa-tee: approval provider denied or returned mismatched signature")
ErrAttestationRequired = errors.New("mldsa-tee: attestation envelope required")
ErrPolicyRefused = errors.New("mldsa-tee: release gate refused")
ErrKMSReleaseUnreachable = errors.New("mldsa-tee: release gate unreachable")
ErrHSMUnreachable = errors.New("mldsa-tee: HSM provider unreachable")
ErrCorruptWrappedSeed = errors.New("mldsa-tee: wrapped seed blob fails authenticated decryption")
)
// Validate reports the first structural error in cfg.
func (cfg *Config) Validate() error {
if _, err := pulsar.ParamsFor(cfg.Mode); err != nil {
return fmt.Errorf("%w: %v", ErrInvalidMode, err)
}
if len(cfg.RequiredRIM) == 0 {
return ErrEmptyRIM
}
if len(cfg.AllowedHardware) == 0 {
return ErrEmptyHardware
}
if !cfg.RequireSEVSNP && !cfg.RequireTDX && !cfg.RequireNVNRAS {
return ErrNoRequireFlag
}
if cfg.KMSKeyID == "" {
return ErrMissingKMSKeyID
}
if cfg.WrappedSeedKeyID == "" {
return ErrMissingSeedKeyID
}
if cfg.ApprovalRequired && cfg.ApproverID == "" {
return ErrApproverMissing
}
return nil
}
-10
View File
@@ -1,10 +0,0 @@
// SPDX-License-Identifier: BSD-3-Clause
package mldsatee
import "golang.org/x/crypto/curve25519"
// curve25519BasepointMul is a test-only helper.
func curve25519BasepointMul(priv []byte) ([]byte, error) {
return curve25519.X25519(priv, curve25519.Basepoint)
}
-61
View File
@@ -1,61 +0,0 @@
// SPDX-License-Identifier: BSD-3-Clause
// Package mldsatee implements operator-controlled ML-DSA threshold
// signing via TEE-gated master-seed reconstruction.
//
// This is NOT a public-BFT primitive. Pulsar v0.3 AlgebraicAggregate
// is the canonical permissionless threshold path; THIS package is the
// institutional-custody-only extension that binds key release to:
//
// 1. a verifiable hardware TEE attestation (AMD SEV-SNP / Intel TDX /
// NVIDIA NRAS) chain-validated by github.com/luxfi/mpc/cc/attest;
// 2. a KMS release-gate (github.com/luxfi/mpc/pkg/kms.ReleaseGate)
// that pins the worker's RIM digest + hardware fingerprint and
// binds a single-use challenge nonce per-request;
// 3. an out-of-band human / programmatic approval signature
// (github.com/luxfi/mpc/pkg/approval.ApprovalProvider);
// 4. an HSM-resident wrap-key store
// (github.com/luxfi/mpc/pkg/hsm.Provider) so the master ML-DSA
// seed (32 bytes, FIPS 204) lives sealed-at-rest and is only
// ever unwrapped inside the attested TEE.
//
// The sign call returns bytes byte-identical to single-party FIPS 204
// ML-DSA SignDeterministic on (master_seed → KeyFromSeed → Sign(msg,
// ctx)). Any caller holding the published PULG-framed group public
// key can verify with pulsar.VerifyBytes (or Verify) — no awareness
// of the threshold or TEE substrate is required.
//
// Threat model:
//
// - Adversary controls the operator process (compromised binary,
// malicious operator) outside the TEE. Without a valid attestation
// that chains to the pinned vendor root AND a fresh approval that
// matches the RIM/hardware policy, no sign is possible. The HSM
// never releases the master seed in plaintext — only the AEAD
// ciphertext sealed to the gate-issued ephemeral pubkey can leave
// the gate.
// - Adversary recovers an old sealed key. AAD binds (epoch, jobID,
// teePub, issuedNonce); replay across epoch or jobID is refused.
// - Adversary forges an attestation envelope whose Verify(nonce)
// returns true but whose evidence does not chain to the vendor
// root. ReleaseGate.Release calls CompositeAttestation.VerifyEvidence
// which dispatches every blob through cc/attest.Dispatch and
// refuses on chain-invalid.
//
// What this package is NOT:
//
// - NOT a no-trusted-dealer DKG. The master seed is generated once
// under TEE attestation; subsequent signs only release the wrapped
// seed under the same attestation policy. The permissionless DKG
// construction for ML-DSA is pulsar v1.0.23 AlgebraicAggregate —
// see pulsar/ref/go/pkg/pulsar/threshold.go.
//
// - NOT a substitute for pulsar.OrchestrateV03Sign on the public-BFT
// surface. Use this ONLY when the threat model permits "trusted
// custody with attested release" (e.g. M-Chain bridge custody,
// A-Chain confidential compute oracle, IAM signing-as-a-service).
//
// Wire compatibility: output is a pulsar.Signature (mode default
// ModeP65) — the same wire form the dispatcher emits today. The SDK /
// verifier path is unchanged.
package mldsatee
-95
View File
@@ -1,95 +0,0 @@
// SPDX-License-Identifier: BSD-3-Clause
package mldsatee
import (
"context"
"crypto/sha256"
"crypto/subtle"
"errors"
"fmt"
"github.com/luxfi/mpc/cc/attest"
"github.com/luxfi/mpc/pkg/kms"
)
// Envelope is the minimal kms.CompositeAttestation implementation
// consumed by this package. Mirrors slhdsa-tee.Envelope in shape so
// embedders that compose both primitives can share the same plumbing.
type Envelope struct {
Kind attest.Kind
EvidenceBytes []byte
ExpectedNonce [32]byte
RIM [32]byte
Hardware [32]byte
TEEPub [32]byte
VerifyOpts []attest.Option
}
var _ kms.CompositeAttestation = (*Envelope)(nil)
// Verify implements kms.CompositeAttestation.Verify.
func (e *Envelope) Verify(expectedNonce [32]byte) (bool, error) {
if subtle.ConstantTimeCompare(e.ExpectedNonce[:], expectedNonce[:]) != 1 {
return false, nil
}
return true, nil
}
// VerifyEvidence implements kms.CompositeAttestation.VerifyEvidence.
//
// Dispatches the single evidence blob to cc/attest, returns the
// verified report on success, surfaces any chain / signature /
// policy failure verbatim. Additionally pins the operator-asserted
// RIM to sha256(measurement) of the verified report.
func (e *Envelope) VerifyEvidence(ctx context.Context, opts ...attest.Option) ([]*attest.VerifiedReport, error) {
if len(e.EvidenceBytes) == 0 {
return nil, fmt.Errorf("%w: empty evidence", attest.ErrInvalidEvidence)
}
allOpts := append([]attest.Option{}, e.VerifyOpts...)
allOpts = append(allOpts, opts...)
rep, err := attest.Dispatch(ctx, e.Kind, e.EvidenceBytes, allOpts...)
if err != nil {
return nil, err
}
if err := defaultRIMCheck(rep, e.RIM); err != nil {
return nil, err
}
return []*attest.VerifiedReport{rep}, nil
}
// RIMDigest implements kms.CompositeAttestation.RIMDigest.
func (e *Envelope) RIMDigest() [32]byte { return e.RIM }
// HardwareFingerprint implements kms.CompositeAttestation.HardwareFingerprint.
func (e *Envelope) HardwareFingerprint() [32]byte { return e.Hardware }
// TEEPublicKey implements kms.CompositeAttestation.TEEPublicKey.
func (e *Envelope) TEEPublicKey() [32]byte { return e.TEEPub }
// EvidenceIssuers implements kms.CompositeAttestation.EvidenceIssuers.
func (e *Envelope) EvidenceIssuers() []string {
switch e.Kind {
case attest.KindSEVSNP:
return []string{kms.IssuerSEVSNP}
case attest.KindTDX:
return []string{kms.IssuerTDX}
case attest.KindNRAS:
return []string{kms.IssuerNVNRAS}
default:
return nil
}
}
// defaultRIMCheck verifies the cc/attest verified-report Measurement
// matches the operator-asserted RIM digest under sha256-folding.
func defaultRIMCheck(rep *attest.VerifiedReport, expected [32]byte) error {
if rep == nil {
return errors.New("mldsa-tee: defaultRIMCheck: nil verified report")
}
got := sha256.Sum256(rep.Measurement)
if subtle.ConstantTimeCompare(got[:], expected[:]) != 1 {
return fmt.Errorf("%w: report measurement does not fold to operator-asserted RIM", attest.ErrPolicy)
}
return nil
}
-201
View File
@@ -1,201 +0,0 @@
// SPDX-License-Identifier: BSD-3-Clause
package mldsatee
import (
"context"
"crypto/rand"
"crypto/sha256"
"fmt"
pulsar "github.com/luxfi/pulsar/ref/go/pkg/pulsar"
"github.com/luxfi/mpc/pkg/approval"
)
// Sign produces a FIPS 204 ML-DSA signature on msg, gated by the
// supplied attestation Envelope. Same flow as slhdsa-tee.Signer.Sign;
// differs only in the inner primitive (pulsar instead of magnetar).
//
// Output is the PULS-framed wire bytes (via Signature.MarshalBinary)
// — byte-identical to single-party FIPS 204 SignDeterministic on the
// same (seed-derived sk, msg, ctx).
func (s *Signer) Sign(ctx context.Context, env *Envelope, jobID [32]byte, msg []byte, signCtx []byte) ([]byte, *SignReceipt, error) {
if env == nil {
return nil, nil, ErrAttestationRequired
}
if len(msg) == 0 {
return nil, nil, fmt.Errorf("mldsa-tee: empty message")
}
sealed, err := s.auditedRelease(ctx, env, jobID, msg)
if err != nil {
return nil, nil, err
}
raw, err := s.hsmP.GetKey(ctx, s.cfg.WrappedSeedKeyID)
if err != nil {
return nil, nil, fmt.Errorf("%w: HSM GetKey: %v", ErrHSMUnreachable, err)
}
defer zeroize(raw)
if len(raw) != pulsar.SeedSize {
return nil, nil, fmt.Errorf("%w: HSM-stored seed length %d does not match pulsar.SeedSize %d",
ErrCorruptWrappedSeed, len(raw), pulsar.SeedSize)
}
var seed [pulsar.SeedSize]byte
copy(seed[:], raw)
defer zeroizeArr(&seed)
sk, err := pulsar.KeyFromSeed(s.params, seed)
if err != nil {
return nil, nil, fmt.Errorf("%w: KeyFromSeed: %v", ErrCorruptWrappedSeed, err)
}
defer zeroize(sk.Bytes)
defer zeroizeArr(&sk.Seed)
sig, err := pulsar.Sign(s.params, sk, msg, signCtx, false /*deterministic*/, nil)
if err != nil {
return nil, nil, fmt.Errorf("mldsa-tee: pulsar.Sign: %w", err)
}
// Self-verify safety belt.
if err := pulsar.Verify(s.params, sk.Pub, msg, sig); err != nil {
return nil, nil, fmt.Errorf("mldsa-tee: self-verify failed (kernel bug): %w", err)
}
wire, err := sig.MarshalBinary()
if err != nil {
return nil, nil, fmt.Errorf("mldsa-tee: sig.MarshalBinary: %w", err)
}
audit, err := s.auditSignature(ctx, jobID, msg, sealed.Epoch, env.RIM)
if err != nil {
return nil, nil, fmt.Errorf("mldsa-tee: audit signature: %w", err)
}
recv := &SignReceipt{
JobID: jobID,
Epoch: sealed.Epoch,
IssuedNonce: sealed.IssuedNonce,
EphemeralPub: sealed.EphemeralPub,
EvidenceKind: string(env.Kind),
EvidenceIssuer: evidenceIssuerString(env),
AuditSignature: audit,
}
return wire, recv, nil
}
// SignReceipt is the audit blob returned alongside the FIPS 204
// signature.
type SignReceipt struct {
JobID [32]byte
Epoch uint64
IssuedNonce [32]byte
EphemeralPub [32]byte
EvidenceKind string
EvidenceIssuer string
AuditSignature []byte
}
func (s *Signer) auditSignature(ctx context.Context, jobID [32]byte, msg []byte, epoch uint64, rim [32]byte) ([]byte, error) {
h := sha256.New()
h.Write([]byte("LUX-MLDSA-TEE-AUDIT-V1"))
h.Write([]byte{0x00})
h.Write(jobID[:])
h.Write(epochBytes(epoch))
h.Write(rim[:])
d := sha256.Sum256(msg)
h.Write(d[:])
auditDigest := h.Sum(nil)
return s.hsmP.Sign(ctx, s.cfg.KMSKeyID, auditDigest)
}
func epochBytes(e uint64) []byte {
return []byte{
byte(e >> 56), byte(e >> 48), byte(e >> 40), byte(e >> 32),
byte(e >> 24), byte(e >> 16), byte(e >> 8), byte(e),
}
}
func evidenceIssuerString(env *Envelope) string {
switch is := env.EvidenceIssuers(); len(is) {
case 0:
return ""
default:
return is[0]
}
}
// signIntent satisfies approval.CanonicalIntent for the (jobID, msg,
// envelope-summary) tuple.
type signIntent struct {
jobID [32]byte
msg []byte
env envelopeSummary
}
type envelopeSummary struct {
Kind string
RIM [32]byte
Hardware [32]byte
TEEPub [32]byte
}
func newSignIntent(jobID [32]byte, msg []byte, env *Envelope) *signIntent {
return &signIntent{
jobID: jobID,
msg: append([]byte(nil), msg...),
env: envelopeSummary{
Kind: string(env.Kind),
RIM: env.RIM,
Hardware: env.Hardware,
TEEPub: env.TEEPub,
},
}
}
// Digest implements approval.CanonicalIntent.
func (si *signIntent) Digest() [32]byte {
h := sha256.New()
h.Write([]byte("LUX-MLDSA-TEE-INTENT-V1"))
h.Write([]byte{0x00})
h.Write(si.jobID[:])
mdigest := sha256.Sum256(si.msg)
h.Write(mdigest[:])
h.Write([]byte(si.env.Kind))
h.Write([]byte{0x00})
h.Write(si.env.RIM[:])
h.Write(si.env.Hardware[:])
h.Write(si.env.TEEPub[:])
var out [32]byte
copy(out[:], h.Sum(nil))
return out
}
// Bytes implements approval.CanonicalIntent.
func (si *signIntent) Bytes() []byte {
out := make([]byte, 0, 32+32+len(si.env.Kind)+1+32+32+32+32)
out = append(out, []byte("LUX-MLDSA-TEE-INTENT-V1")...)
out = append(out, 0x00)
out = append(out, si.jobID[:]...)
mdigest := sha256.Sum256(si.msg)
out = append(out, mdigest[:]...)
out = append(out, []byte(si.env.Kind)...)
out = append(out, 0x00)
out = append(out, si.env.RIM[:]...)
out = append(out, si.env.Hardware[:]...)
out = append(out, si.env.TEEPub[:]...)
return out
}
var _ approval.CanonicalIntent = (*signIntent)(nil)
// FreshJobID returns 32 bytes of crypto/rand.
func FreshJobID() ([32]byte, error) {
var out [32]byte
if _, err := rand.Read(out[:]); err != nil {
return out, err
}
return out, nil
}
-176
View File
@@ -1,176 +0,0 @@
// SPDX-License-Identifier: BSD-3-Clause
package mldsatee
import (
"context"
"crypto/rand"
"errors"
"fmt"
"sync"
pulsar "github.com/luxfi/pulsar/ref/go/pkg/pulsar"
"github.com/luxfi/mpc/pkg/approval"
"github.com/luxfi/mpc/pkg/hsm"
"github.com/luxfi/mpc/pkg/kms"
)
// Signer is the institutional-custody ML-DSA signer.
//
// Composition (not inheritance):
//
// - gate : kms.ReleaseGate — the trust root.
// - hsmP : hsm.Provider — wraps the master seed at rest.
// - appr : approval.ApprovalProvider — out-of-band approval gate.
// - cfg : Config — policy: RIM, hardware, mode, key IDs.
//
// Safe for concurrent Sign calls.
type Signer struct {
gate kms.ReleaseGate
hsmP hsm.Provider
appr approval.ApprovalProvider
cfg Config
params *pulsar.Params
mu sync.Mutex // reserved for future per-Signer rate-limit state
}
// New builds a Signer from the supplied dependencies.
func New(gate kms.ReleaseGate, hsmP hsm.Provider, appr approval.ApprovalProvider, cfg Config) (*Signer, error) {
if gate == nil {
return nil, errors.New("mldsa-tee: nil release gate")
}
if hsmP == nil {
return nil, errors.New("mldsa-tee: nil HSM provider")
}
if cfg.ApprovalRequired && appr == nil {
return nil, errors.New("mldsa-tee: nil approval provider but ApprovalRequired is true")
}
if err := cfg.Validate(); err != nil {
return nil, err
}
params, err := pulsar.ParamsFor(cfg.Mode)
if err != nil {
return nil, fmt.Errorf("mldsa-tee: ParamsFor: %w", err)
}
return &Signer{
gate: gate,
hsmP: hsmP,
appr: appr,
cfg: cfg,
params: params,
}, nil
}
// Provision wraps a fresh master seed under the HSM provider for
// later release-gated signing. Generates pulsar.SeedSize (=32) bytes
// of entropy and stores via hsmP.StoreKey under cfg.WrappedSeedKeyID.
// Returns the pulsar.PublicKey derived from the provisioned seed.
func (s *Signer) Provision(ctx context.Context) (*pulsar.PublicKey, error) {
var seed [pulsar.SeedSize]byte
defer zeroizeArr(&seed)
if _, err := rand.Read(seed[:]); err != nil {
return nil, fmt.Errorf("mldsa-tee: provision: entropy: %w", err)
}
if err := s.hsmP.StoreKey(ctx, s.cfg.WrappedSeedKeyID, seed[:]); err != nil {
return nil, fmt.Errorf("mldsa-tee: provision: HSM StoreKey: %w", err)
}
sk, err := pulsar.KeyFromSeed(s.params, seed)
if err != nil {
return nil, fmt.Errorf("mldsa-tee: provision: KeyFromSeed: %w", err)
}
pub := sk.Public()
zeroize(sk.Bytes)
zeroizeArr(&sk.Seed)
return pub, nil
}
// PublicKey reads the master seed via the HSM provider and derives
// the pulsar PublicKey deterministically. RELEASE-GATE FREE — only
// the at-rest HSM material is read.
func (s *Signer) PublicKey(ctx context.Context) (*pulsar.PublicKey, error) {
raw, err := s.hsmP.GetKey(ctx, s.cfg.WrappedSeedKeyID)
if err != nil {
return nil, fmt.Errorf("mldsa-tee: PublicKey: HSM GetKey: %w", err)
}
defer zeroize(raw)
if len(raw) != pulsar.SeedSize {
return nil, fmt.Errorf("mldsa-tee: PublicKey: seed length %d does not match pulsar.SeedSize %d", len(raw), pulsar.SeedSize)
}
var seed [pulsar.SeedSize]byte
copy(seed[:], raw)
defer zeroizeArr(&seed)
sk, err := pulsar.KeyFromSeed(s.params, seed)
if err != nil {
return nil, fmt.Errorf("mldsa-tee: PublicKey: KeyFromSeed: %w", err)
}
defer zeroize(sk.Bytes)
defer zeroizeArr(&sk.Seed)
return sk.Public(), nil
}
// Mode reports the FIPS 204 parameter set this signer is bound to.
func (s *Signer) Mode() pulsar.Mode { return s.cfg.Mode }
// Params returns the pulsar Params for this signer's mode.
func (s *Signer) Params() *pulsar.Params { return s.params }
// auditedRelease drives the full Issue → approval → composite envelope
// → Release flow.
func (s *Signer) auditedRelease(ctx context.Context, env *Envelope, jobID [32]byte, msg []byte) (kms.SealedSessionKey, error) {
if env == nil {
return kms.SealedSessionKey{}, ErrAttestationRequired
}
if s.cfg.ApprovalRequired {
intent := newSignIntent(jobID, msg, env)
sig, err := s.appr.ApproveIntent(ctx, s.cfg.ApproverID, intent)
if err != nil {
return kms.SealedSessionKey{}, fmt.Errorf("%w: %v", ErrApprovalDenied, err)
}
ok, err := s.appr.VerifyApproval(ctx, intent, sig)
if err != nil {
return kms.SealedSessionKey{}, fmt.Errorf("%w: verify: %v", ErrApprovalDenied, err)
}
if !ok {
return kms.SealedSessionKey{}, ErrApprovalDenied
}
}
nonce, epoch, err := s.gate.Issue(jobID)
if err != nil {
return kms.SealedSessionKey{}, fmt.Errorf("%w: gate.Issue: %v", ErrKMSReleaseUnreachable, err)
}
env.ExpectedNonce = nonce
sealed, err := s.gate.Release(kms.ReleaseRequest{
JobID: jobID,
Epoch: epoch,
Nonce: nonce,
Attestation: env,
Ctx: ctx,
})
if err != nil {
return kms.SealedSessionKey{}, fmt.Errorf("%w: %v", ErrPolicyRefused, err)
}
return sealed, nil
}
// zeroize clears a byte slice in place.
func zeroize(b []byte) {
for i := range b {
b[i] = 0
}
}
// zeroizeArr clears a 32-byte array in place.
func zeroizeArr(a *[pulsar.SeedSize]byte) {
for i := range a {
a[i] = 0
}
}
-569
View File
@@ -1,569 +0,0 @@
// SPDX-License-Identifier: BSD-3-Clause
package mldsatee
import (
"context"
"crypto/rand"
"crypto/sha256"
_ "embed"
"errors"
"os"
"testing"
"time"
sevtest "github.com/google/go-sev-guest/testing"
"github.com/google/go-sev-guest/verify/trust"
pulsar "github.com/luxfi/pulsar/ref/go/pkg/pulsar"
"github.com/luxfi/mpc/cc/attest"
"github.com/luxfi/mpc/pkg/approval"
"github.com/luxfi/mpc/pkg/hsm"
"github.com/luxfi/mpc/pkg/kms"
)
// Real AMD Milan SEV-SNP attestation fixtures (same bytes as the
// lux/mpc cc/attest test corpus).
//
//go:embed testdata/sev_snp_attestation_milan.bin
var sevSnpAttestationMilan []byte
//go:embed testdata/sev_snp_vcek_milan.cer
var sevSnpVcekMilan []byte
func newKDSReplay() trust.HTTPSGetter {
return sevtest.SimpleGetter(map[string][]byte{
"https://kdsintf.amd.com/vcek/v1/Milan/cert_chain": trust.AskArkMilanVcekBytes,
"https://kdsintf.amd.com/vcek/v1/Milan/3ac3fe21e13fb0990eb28a802e3fb6a29483a6b0753590c951bdd3b8e53786184ca39e359669a2b76a1936776b564ea464cdce40c05f63c9b610c5068b006b5d?blSPL=2&teeSPL=0&snpSPL=5&ucodeSPL=68": sevSnpVcekMilan,
})
}
func fixedNow() time.Time {
return time.Date(2024, 1, 15, 12, 0, 0, 0, time.UTC)
}
func realMeasurement() []byte { return sevSnpAttestationMilan[0x90 : 0x90+48] }
func realChipID() []byte { return sevSnpAttestationMilan[0x1A0 : 0x1A0+64] }
func makeRIM(t *testing.T) [32]byte {
t.Helper()
return sha256.Sum256(realMeasurement())
}
func makeHardware(t *testing.T) [32]byte {
t.Helper()
return sha256.Sum256(realChipID())
}
func makeTEEPub(t *testing.T) [32]byte {
t.Helper()
var priv [32]byte
for i := range priv {
priv[i] = byte(i + 1)
}
priv[0] &= 248
priv[31] &= 127
priv[31] |= 64
pub, err := curve25519BasepointMul(priv[:])
if err != nil {
t.Fatalf("makeTEEPub: %v", err)
}
var out [32]byte
copy(out[:], pub)
return out
}
func newTestFileHSM(t *testing.T) hsm.Provider {
t.Helper()
dir := t.TempDir()
cfg := &hsm.FileConfig{
BasePath: dir,
HexEncoded: true, // unambiguous on-disk for raw bytes
}
p, err := hsm.NewFileProvider(cfg)
if err != nil {
t.Fatalf("newTestFileHSM: %v", err)
}
var ed25519Seed [32]byte
if _, err := rand.Read(ed25519Seed[:]); err != nil {
t.Fatalf("ed25519 seed: %v", err)
}
if err := p.StoreKey(context.Background(), "audit-key", ed25519Seed[:]); err != nil {
t.Fatalf("store audit key: %v", err)
}
t.Cleanup(func() {
_ = p.Close()
_ = os.RemoveAll(dir)
})
return p
}
func newTestApprovalProvider(t *testing.T) approval.ApprovalProvider {
t.Helper()
// MPC_LOCAL_APPROVAL=true is set process-wide by TestMain so that
// parallel tests can share the LocalDevProvider without
// t.Setenv-imposed serialization.
p, err := approval.NewProvider("local-dev", nil)
if err != nil {
t.Fatalf("newTestApprovalProvider: %v", err)
}
return p
}
// TestMain enables the LocalDevProvider for the lifetime of this
// test binary.
func TestMain(m *testing.M) {
_ = os.Setenv("MPC_LOCAL_APPROVAL", "true")
os.Exit(m.Run())
}
// denyApprovalProvider models a user-cancel verdict from a real
// WebAuthn/Ledger device. NOT a stub interface — it satisfies the
// full approval.ApprovalProvider contract.
type denyApprovalProvider struct{}
func (denyApprovalProvider) Provider() string { return "deny-test" }
func (denyApprovalProvider) GetPublicIdentity(_ context.Context, approverID string) (approval.PublicIdentity, error) {
return approval.PublicIdentity{
ApproverID: approverID,
Provider: "deny-test",
PublicKey: make([]byte, 32),
Algorithm: approval.AlgorithmEd25519,
}, nil
}
func (denyApprovalProvider) ApproveIntent(_ context.Context, approverID string, intent approval.CanonicalIntent) (approval.ApprovalSignature, error) {
return approval.ApprovalSignature{}, errors.New("deny-test: user cancelled")
}
func (denyApprovalProvider) VerifyApproval(_ context.Context, intent approval.CanonicalIntent, sig approval.ApprovalSignature) (bool, error) {
return false, nil
}
func newTestGate(t *testing.T, rim, hw [32]byte) (*kms.LocalReleaseGate, kms.NonceStore) {
t.Helper()
policy := kms.NewReleasePolicy([][32]byte{rim}, [][32]byte{hw})
policy.RequireSEVSNP = true
var rootKey [32]byte
if _, err := rand.Read(rootKey[:]); err != nil {
t.Fatalf("rootKey: %v", err)
}
store := kms.NewMemoryNonceStore()
gate, err := kms.NewLocalReleaseGate(policy, store, rootKey)
if err != nil {
t.Fatalf("NewLocalReleaseGate: %v", err)
}
gate.SetIssueTTL(5 * time.Second)
gate.SetReplayWindow(5 * time.Second)
return gate, store
}
func newTestSigner(t *testing.T, approvalRequired bool) (*Signer, *kms.LocalReleaseGate, hsm.Provider, [32]byte, [32]byte) {
t.Helper()
rim := makeRIM(t)
hw := makeHardware(t)
gate, _ := newTestGate(t, rim, hw)
hsmP := newTestFileHSM(t)
appr := newTestApprovalProvider(t)
cfg := Config{
Mode: pulsar.ModeP65,
RequiredRIM: map[[32]byte]struct{}{rim: {}},
AllowedHardware: map[[32]byte]struct{}{hw: {}},
RequireSEVSNP: true,
KMSKeyID: "audit-key",
WrappedSeedKeyID: "master-seed",
ApprovalRequired: approvalRequired,
ApproverID: "test@lux.network",
}
s, err := New(gate, hsmP, appr, cfg)
if err != nil {
t.Fatalf("New: %v", err)
}
if _, err := s.Provision(context.Background()); err != nil {
t.Fatalf("Provision: %v", err)
}
return s, gate, hsmP, rim, hw
}
func envelopeFromTestdata(t *testing.T, rim, hw, teePub [32]byte) *Envelope {
t.Helper()
// Cache mutation removed; hot cert cache is correctness-equivalent.
return &Envelope{
Kind: attest.KindSEVSNP,
EvidenceBytes: append([]byte(nil), sevSnpAttestationMilan...),
RIM: rim,
Hardware: hw,
TEEPub: teePub,
VerifyOpts: []attest.Option{
attest.WithKDSGetter(newKDSReplay()),
attest.WithNow(fixedNow()),
},
}
}
// ============================================================================
// Required test 1: full chain E2E
// ============================================================================
func TestSigner_Sign_SEVSNP_E2E(t *testing.T) {
t.Parallel()
s, _, _, rim, hw := newTestSigner(t, true)
teePub := makeTEEPub(t)
env := envelopeFromTestdata(t, rim, hw, teePub)
jobID, err := FreshJobID()
if err != nil {
t.Fatalf("FreshJobID: %v", err)
}
msg := []byte("LUX-MLDSA-TEE: institutional-custody E2E test")
wire, receipt, err := s.Sign(context.Background(), env, jobID, msg, nil)
if err != nil {
t.Fatalf("Sign: %v", err)
}
if len(wire) == 0 {
t.Fatal("Sign returned empty wire bytes")
}
if receipt == nil {
t.Fatal("Sign returned nil receipt")
}
if receipt.JobID != jobID {
t.Errorf("receipt.JobID = %x, want %x", receipt.JobID, jobID)
}
if receipt.EvidenceKind != string(attest.KindSEVSNP) {
t.Errorf("receipt.EvidenceKind = %q", receipt.EvidenceKind)
}
if receipt.EvidenceIssuer != kms.IssuerSEVSNP {
t.Errorf("receipt.EvidenceIssuer = %q", receipt.EvidenceIssuer)
}
if len(receipt.AuditSignature) == 0 {
t.Error("receipt.AuditSignature empty")
}
pub, err := s.PublicKey(context.Background())
if err != nil {
t.Fatalf("PublicKey: %v", err)
}
gkBytes, err := pub.MarshalBinary()
if err != nil {
t.Fatalf("pub.MarshalBinary: %v", err)
}
if !pulsar.VerifyBytes(gkBytes, msg, wire) {
t.Fatal("external VerifyBytes refused the signature; not FIPS 204 byte-identical")
}
}
// ============================================================================
// Required test 2: rejects corrupt attestation
// ============================================================================
func TestSigner_Sign_RejectsBadAttestation(t *testing.T) {
t.Parallel()
s, _, _, rim, hw := newTestSigner(t, false)
teePub := makeTEEPub(t)
env := envelopeFromTestdata(t, rim, hw, teePub)
// Flip a bit inside the SEV signature region.
env.EvidenceBytes[0x2A0+0x10] ^= 0x01
jobID, _ := FreshJobID()
msg := []byte("reject-bad-evidence")
_, _, err := s.Sign(context.Background(), env, jobID, msg, nil)
if err == nil {
t.Fatal("Sign: expected refusal on tampered evidence, got nil")
}
if !errors.Is(err, ErrPolicyRefused) {
t.Errorf("Sign: err = %v, want wrapped ErrPolicyRefused", err)
}
}
// ============================================================================
// Required test 3: rejects RIM mismatch
// ============================================================================
func TestSigner_Sign_RejectsRIMMismatch(t *testing.T) {
t.Parallel()
s, _, _, _, hw := newTestSigner(t, false)
teePub := makeTEEPub(t)
wrongRIM := sha256.Sum256([]byte("not-the-real-measurement"))
env := envelopeFromTestdata(t, wrongRIM, hw, teePub)
jobID, _ := FreshJobID()
msg := []byte("reject-wrong-rim")
_, _, err := s.Sign(context.Background(), env, jobID, msg, nil)
if err == nil {
t.Fatal("Sign: expected refusal on RIM mismatch, got nil")
}
if !errors.Is(err, ErrPolicyRefused) {
t.Errorf("Sign: err = %v, want wrapped ErrPolicyRefused", err)
}
}
// ============================================================================
// Required test 4: rejects expired nonce / wrong epoch
// ============================================================================
func TestSigner_Sign_RejectsExpiredNonce(t *testing.T) {
t.Parallel()
s, gate, _, rim, hw := newTestSigner(t, false)
teePub := makeTEEPub(t)
env := envelopeFromTestdata(t, rim, hw, teePub)
gate.SetIssueTTL(10 * time.Millisecond)
jobID, _ := FreshJobID()
nonce, epoch, err := gate.Issue(jobID)
if err != nil {
t.Fatalf("Issue: %v", err)
}
env.ExpectedNonce = nonce
time.Sleep(50 * time.Millisecond)
_, releaseErr := gate.Release(kms.ReleaseRequest{
JobID: jobID, Epoch: epoch, Nonce: nonce, Attestation: env, Ctx: context.Background(),
})
if releaseErr == nil {
t.Fatal("gate.Release: expected expiry refusal, got nil")
}
if !errors.Is(releaseErr, kms.ErrPolicyRefused) {
t.Errorf("releaseErr = %v, want wrapped kms.ErrPolicyRefused", releaseErr)
}
if !errors.Is(releaseErr, kms.ErrExpired) {
t.Errorf("releaseErr = %v, want wrapped kms.ErrExpired", releaseErr)
}
gate.SetIssueTTL(5 * time.Second)
_ = gate.Rotate()
freshJob, _ := FreshJobID()
freshEnv := envelopeFromTestdata(t, rim, hw, teePub)
msg := []byte("post-rotation-sign")
if _, _, err := s.Sign(context.Background(), freshEnv, freshJob, msg, nil); err != nil {
t.Fatalf("Sign post-rotation: %v", err)
}
}
// ============================================================================
// Required test 5: AWS KMS backend
// ============================================================================
func TestSigner_Sign_HSMSign_AWS_KMS(t *testing.T) {
endpoint := os.Getenv("AWS_ENDPOINT_URL_KMS")
keyARN := os.Getenv("AWS_KMS_TEST_KEY_ARN")
if endpoint == "" || keyARN == "" {
t.Skip("AWS_ENDPOINT_URL_KMS and AWS_KMS_TEST_KEY_ARN not set; localstack KMS not available — see test comment for setup. Skipped per spec rationale: unit CI must not require real AWS credentials. File provider path is exercised by TestSigner_Sign_HSMSign_File and all chain-verify tests.")
}
awsCfg := &hsm.AWSConfig{
Region: os.Getenv("AWS_REGION"),
KeyARN: keyARN,
Profile: os.Getenv("AWS_PROFILE"),
}
awsP, err := hsm.NewAWSProvider(awsCfg)
if err != nil {
t.Fatalf("NewAWSProvider: %v", err)
}
defer awsP.Close()
digest := sha256.Sum256([]byte("aws-kms-audit-probe"))
sig, err := awsP.Sign(context.Background(), keyARN, digest[:])
if err != nil {
t.Fatalf("AWS KMS Sign: %v", err)
}
if len(sig) == 0 {
t.Fatal("AWS KMS Sign returned empty signature")
}
}
// ============================================================================
// Required test 6: File-backed HSM end-to-end
// ============================================================================
func TestSigner_Sign_HSMSign_File(t *testing.T) {
t.Parallel()
s, _, _, rim, hw := newTestSigner(t, true)
teePub := makeTEEPub(t)
env := envelopeFromTestdata(t, rim, hw, teePub)
jobID, _ := FreshJobID()
msg := []byte("file-hsm-e2e")
wire, _, err := s.Sign(context.Background(), env, jobID, msg, nil)
if err != nil {
t.Fatalf("Sign: %v", err)
}
pub, err := s.PublicKey(context.Background())
if err != nil {
t.Fatalf("PublicKey: %v", err)
}
gkBytes, err := pub.MarshalBinary()
if err != nil {
t.Fatalf("pub.MarshalBinary: %v", err)
}
if !pulsar.VerifyBytes(gkBytes, msg, wire) {
t.Fatal("file-hsm-e2e: VerifyBytes refused FIPS 204 signature")
}
}
// ============================================================================
// Required test 7: WebAuthn-style approval required
// ============================================================================
func TestSigner_Sign_ApprovalRequired_DenyAndAllow(t *testing.T) {
t.Parallel()
rim := makeRIM(t)
hw := makeHardware(t)
gate, store := newTestGate(t, rim, hw)
fileP := newTestFileHSM(t)
cfg := Config{
Mode: pulsar.ModeP65,
RequiredRIM: map[[32]byte]struct{}{rim: {}},
AllowedHardware: map[[32]byte]struct{}{hw: {}},
RequireSEVSNP: true,
KMSKeyID: "audit-key",
WrappedSeedKeyID: "master-seed",
ApprovalRequired: true,
ApproverID: "ceo@lux.network",
}
denyS, err := New(gate, fileP, denyApprovalProvider{}, cfg)
if err != nil {
t.Fatalf("New(deny): %v", err)
}
if _, err := denyS.Provision(context.Background()); err != nil {
t.Fatalf("Provision: %v", err)
}
teePub := makeTEEPub(t)
env := envelopeFromTestdata(t, rim, hw, teePub)
jobID, _ := FreshJobID()
msg := []byte("approval-deny-test")
_, _, err = denyS.Sign(context.Background(), env, jobID, msg, nil)
if err == nil {
t.Fatal("Sign(deny): expected ErrApprovalDenied, got nil")
}
if !errors.Is(err, ErrApprovalDenied) {
t.Errorf("Sign(deny): err = %v, want wrapped ErrApprovalDenied", err)
}
if _, lookupErr := store.Lookup(jobID); !errors.Is(lookupErr, kms.ErrNonceUnknown) {
t.Errorf("deny path leaked a gate-issued nonce: %v", lookupErr)
}
// MPC_LOCAL_APPROVAL already exported by TestMain.
appr, err := approval.NewProvider("local-dev", nil)
if err != nil {
t.Fatalf("local-dev provider: %v", err)
}
allowS, err := New(gate, fileP, appr, cfg)
if err != nil {
t.Fatalf("New(allow): %v", err)
}
allowEnv := envelopeFromTestdata(t, rim, hw, teePub)
allowJob, _ := FreshJobID()
if _, _, err := allowS.Sign(context.Background(), allowEnv, allowJob, msg, nil); err != nil {
t.Fatalf("Sign(allow): %v", err)
}
}
// ============================================================================
// Extra: byte-identity with FIPS 204
// ============================================================================
func TestSigner_ByteIdentityWithFIPS204(t *testing.T) {
t.Parallel()
s, _, hsmP, rim, hw := newTestSigner(t, false)
teePub := makeTEEPub(t)
env := envelopeFromTestdata(t, rim, hw, teePub)
jobID, _ := FreshJobID()
msg := []byte("byte-identity-probe")
wire, _, err := s.Sign(context.Background(), env, jobID, msg, nil)
if err != nil {
t.Fatalf("Sign: %v", err)
}
raw, err := hsmP.GetKey(context.Background(), "master-seed")
if err != nil {
t.Fatalf("HSM GetKey: %v", err)
}
var seed [pulsar.SeedSize]byte
copy(seed[:], raw)
params := pulsar.MustParamsFor(pulsar.ModeP65)
sk, err := pulsar.KeyFromSeed(params, seed)
if err != nil {
t.Fatalf("KeyFromSeed: %v", err)
}
directSig, err := pulsar.Sign(params, sk, msg, nil, false, nil)
if err != nil {
t.Fatalf("direct Sign: %v", err)
}
directWire, err := directSig.MarshalBinary()
if err != nil {
t.Fatalf("direct MarshalBinary: %v", err)
}
if string(wire) != string(directWire) {
t.Fatalf("Sign output not byte-identical to single-party FIPS 204")
}
}
// ============================================================================
// Config validation
// ============================================================================
func TestConfig_Validate(t *testing.T) {
rim := [32]byte{1}
hw := [32]byte{2}
good := Config{
Mode: pulsar.ModeP65,
RequiredRIM: map[[32]byte]struct{}{rim: {}},
AllowedHardware: map[[32]byte]struct{}{hw: {}},
RequireSEVSNP: true,
KMSKeyID: "k",
WrappedSeedKeyID: "s",
}
if err := good.Validate(); err != nil {
t.Fatalf("good: %v", err)
}
cases := []struct {
name string
mutate func(*Config)
want error
}{
{"emptyRIM", func(c *Config) { c.RequiredRIM = nil }, ErrEmptyRIM},
{"emptyHardware", func(c *Config) { c.AllowedHardware = nil }, ErrEmptyHardware},
{"noRequireFlag", func(c *Config) { c.RequireSEVSNP = false }, ErrNoRequireFlag},
{"missingKMSKeyID", func(c *Config) { c.KMSKeyID = "" }, ErrMissingKMSKeyID},
{"missingSeedKeyID", func(c *Config) { c.WrappedSeedKeyID = "" }, ErrMissingSeedKeyID},
{"approverMissing", func(c *Config) { c.ApprovalRequired = true; c.ApproverID = "" }, ErrApproverMissing},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
c := good
c.RequiredRIM = map[[32]byte]struct{}{rim: {}}
c.AllowedHardware = map[[32]byte]struct{}{hw: {}}
tc.mutate(&c)
err := c.Validate()
if !errors.Is(err, tc.want) {
t.Errorf("err = %v, want %v", err, tc.want)
}
})
}
}
func TestSigner_ModeAndParams(t *testing.T) {
s, _, _, _, _ := newTestSigner(t, false)
if s.Mode() != pulsar.ModeP65 {
t.Errorf("Mode = %v, want ModeP65", s.Mode())
}
if s.Params() == nil {
t.Fatal("Params returned nil")
}
if s.Params().Mode != pulsar.ModeP65 {
t.Errorf("Params.Mode = %v, want ModeP65", s.Params().Mode)
}
}
Binary file not shown.
Binary file not shown.
-49
View File
@@ -1,49 +0,0 @@
# rlwe-tee
Operator-controlled Ring-LWE threshold signing via TEE-gated
trusted-dealer-key reconstruction.
## What this is
Sibling of `protocols/slhdsa-tee` and `protocols/mldsa-tee` for the
corona Ring-LWE primitive. The master trusted-dealer key (32 bytes)
lives sealed-at-rest in the HSM; per sign call the TEE attestation
authorizes release, then the dealer's `GenerateKeys` + the n-party
Round1/Round2/Finalize all run inside the attested process. The
resulting `corona.threshold.Signature` is structurally identical to
a permissionless corona threshold signature on the same `(GroupKey,
message)`.
## When to use
- Foundation HSM ceremonies that need a single-operator dealer with
attested release.
- Bridge custody operators that hold a corona PQ threshold signing
key and require executive approval per signature.
## When NOT to use
- Permissionless threshold custody: use
`corona.keyera.BootstrapPedersen` — no party ever holds the master
trusted-dealer key.
- Single-operator dev: use `corona.threshold.GenerateKeys` directly.
## Layering
```
caller
└── rlwetee.Signer.Sign(ctx, env, jobID, msg)
├── approval.ApprovalProvider.ApproveIntent
├── kms.ReleaseGate.Issue / Release
│ └── cc/attest.Dispatch
├── hsm.Provider.GetKey
├── corona.threshold.GenerateKeys → Round1 → Round2 → Finalize
└── hsm.Provider.Sign (audit)
```
## Compatibility
Wire output is `corona.threshold.Signature.MarshalBinary`. Verifiers
holding the published GroupKey bytes validate via
`corona.threshold.VerifyBytes(gkBytes, msg, sigBytes)` — no
awareness of the TEE substrate required.
-98
View File
@@ -1,98 +0,0 @@
// SPDX-License-Identifier: BSD-3-Clause
package rlwetee
import (
"errors"
"github.com/luxfi/corona/sign"
)
// Config carries the operator-side policy + provider configuration.
//
// Every field is required; New refuses on any zero value.
type Config struct {
// Threshold is the corona t in t-of-n. corona requires 1 <= t < n.
Threshold int
// Participants is the corona n. Each Sign call drives all n
// parties in-process under the attested TEE; the dispatcher
// surface remains single-shot.
Participants int
// RequiredRIM is the set of acceptable RIM digests.
RequiredRIM map[[32]byte]struct{}
// AllowedHardware is the set of acceptable hardware fingerprints.
AllowedHardware map[[32]byte]struct{}
// Require* flags mirror kms.ReleasePolicy.Require*.
RequireSEVSNP bool
RequireTDX bool
RequireNVNRAS bool
// KMSKeyID is the HSM key for the audit signature over
// (jobID || msgDigest || epoch || RIM).
KMSKeyID string
// WrappedSeedKeyID is the HSM-stored blob identifier for the
// wrapped 32-byte master trusted-dealer key.
WrappedSeedKeyID string
// ApprovalRequired determines whether ApprovalProvider must
// produce a non-deny ApprovalSignature before Issue().
ApprovalRequired bool
// ApproverID is the canonical identifier whose approval is
// required.
ApproverID string
}
// Errors surfaced by Config.Validate and the Sign flow.
var (
ErrInvalidThreshold = errors.New("rlwe-tee: invalid threshold (requires 1 <= Threshold < Participants)")
ErrEmptyRIM = errors.New("rlwe-tee: RequiredRIM must be non-empty (default-deny posture)")
ErrEmptyHardware = errors.New("rlwe-tee: AllowedHardware must be non-empty (default-deny posture)")
ErrNoRequireFlag = errors.New("rlwe-tee: at least one Require* TEE flag must be true")
ErrMissingKMSKeyID = errors.New("rlwe-tee: KMSKeyID required for audit signature")
ErrMissingSeedKeyID = errors.New("rlwe-tee: WrappedSeedKeyID required for HSM seed storage")
ErrApproverMissing = errors.New("rlwe-tee: ApproverID required when ApprovalRequired is true")
ErrApprovalDenied = errors.New("rlwe-tee: approval provider denied or returned mismatched signature")
ErrAttestationRequired = errors.New("rlwe-tee: attestation envelope required")
ErrPolicyRefused = errors.New("rlwe-tee: release gate refused")
ErrKMSReleaseUnreachable = errors.New("rlwe-tee: release gate unreachable")
ErrHSMUnreachable = errors.New("rlwe-tee: HSM provider unreachable")
ErrCorruptWrappedSeed = errors.New("rlwe-tee: wrapped seed blob fails authenticated decryption")
ErrCoronaProtocol = errors.New("rlwe-tee: corona threshold protocol failed")
)
// MasterKeySize is the corona trusted-dealer key length the HSM
// stores. Exported so embedders can size buffers consistently with
// the corona kernel.
const MasterKeySize = sign.KeySize // 32
// Validate reports the first structural error in cfg.
func (cfg *Config) Validate() error {
if cfg.Threshold < 1 || cfg.Participants < 2 || cfg.Threshold >= cfg.Participants {
return ErrInvalidThreshold
}
if len(cfg.RequiredRIM) == 0 {
return ErrEmptyRIM
}
if len(cfg.AllowedHardware) == 0 {
return ErrEmptyHardware
}
if !cfg.RequireSEVSNP && !cfg.RequireTDX && !cfg.RequireNVNRAS {
return ErrNoRequireFlag
}
if cfg.KMSKeyID == "" {
return ErrMissingKMSKeyID
}
if cfg.WrappedSeedKeyID == "" {
return ErrMissingSeedKeyID
}
if cfg.ApprovalRequired && cfg.ApproverID == "" {
return ErrApproverMissing
}
return nil
}
-10
View File
@@ -1,10 +0,0 @@
// SPDX-License-Identifier: BSD-3-Clause
package rlwetee
import "golang.org/x/crypto/curve25519"
// curve25519BasepointMul is a test-only helper.
func curve25519BasepointMul(priv []byte) ([]byte, error) {
return curve25519.X25519(priv, curve25519.Basepoint)
}
-63
View File
@@ -1,63 +0,0 @@
// SPDX-License-Identifier: BSD-3-Clause
// Package rlwetee implements operator-controlled Ring-LWE threshold
// signing via TEE-gated trusted-dealer-key reconstruction.
//
// This is NOT a public-BFT primitive. Corona v0.7.3 BootstrapPedersen
// is the canonical permissionless DKG path (no party ever holds the
// master); THIS package is the institutional-custody-only extension
// that binds key release to:
//
// 1. a verifiable hardware TEE attestation (AMD SEV-SNP / Intel TDX /
// NVIDIA NRAS) chain-validated by github.com/luxfi/mpc/cc/attest;
// 2. a KMS release-gate (github.com/luxfi/mpc/pkg/kms.ReleaseGate)
// that pins the worker's RIM digest + hardware fingerprint and
// binds a single-use challenge nonce per-request;
// 3. an out-of-band human / programmatic approval signature
// (github.com/luxfi/mpc/pkg/approval.ApprovalProvider);
// 4. an HSM-resident wrap-key store
// (github.com/luxfi/mpc/pkg/hsm.Provider) so the master 32-byte
// trusted-dealer key (sign.KeySize) lives sealed-at-rest and is
// only ever unwrapped inside the attested TEE.
//
// Per-sign flow:
//
// 1. Verify attestation envelope chain validity + RIM + hardware
// allowlist + approval.
// 2. Release the wrapped master trusted-dealer key from the HSM.
// 3. Inside the TEE: deterministically regenerate the n key shares
// and the GroupKey by re-running corona.threshold.GenerateKeys
// with the master key as PRNG seed.
// 4. Drive Round1 → Round2 → Finalize across all n parties in the
// same TEE process. The resulting Signature is structurally
// identical to a permissionless corona threshold signature on
// the same message and group public key.
// 5. Zeroize the master key and per-party shares.
//
// Verification surface is corona.threshold.Verify(gk, msg, sig). Any
// caller holding the published GroupKey wire bytes can validate with
// corona.threshold.VerifyBytes — no awareness of the TEE substrate is
// required.
//
// Threat model:
//
// - Adversary outside TEE: cannot produce a valid attestation, so
// gate refuses release; no signing possible.
// - Forged attestation: cc/attest chain refuses; ErrAttestationChain.
// - Replay of old sealed key: AAD-binding (epoch, jobID, teePub,
// issuedNonce) refuses cross-epoch / cross-job.
//
// What this package is NOT:
//
// - NOT a substitute for corona.keyera.BootstrapPedersen on the
// public-BFT surface. The Pedersen-DKG path (no trusted dealer)
// is the canonical permissionless construction. Use rlwe-tee
// ONLY when the threat model permits "trusted dealer inside an
// attested TEE" (foundation HSM ceremonies, single-operator
// custody).
//
// - NOT a distributed protocol. All n parties run in the same
// attested process; the network surface is exactly the
// attestation envelope, the gate release request, and the
// resulting wire-form signature.
package rlwetee
-82
View File
@@ -1,82 +0,0 @@
// SPDX-License-Identifier: BSD-3-Clause
package rlwetee
import (
"context"
"crypto/sha256"
"crypto/subtle"
"errors"
"fmt"
"github.com/luxfi/mpc/cc/attest"
"github.com/luxfi/mpc/pkg/kms"
)
// Envelope is the minimal kms.CompositeAttestation implementation
// consumed by this package. Mirrors slhdsa-tee.Envelope so embedders
// that compose multiple primitives share the same plumbing.
type Envelope struct {
Kind attest.Kind
EvidenceBytes []byte
ExpectedNonce [32]byte
RIM [32]byte
Hardware [32]byte
TEEPub [32]byte
VerifyOpts []attest.Option
}
var _ kms.CompositeAttestation = (*Envelope)(nil)
// Verify implements kms.CompositeAttestation.Verify.
func (e *Envelope) Verify(expectedNonce [32]byte) (bool, error) {
if subtle.ConstantTimeCompare(e.ExpectedNonce[:], expectedNonce[:]) != 1 {
return false, nil
}
return true, nil
}
// VerifyEvidence implements kms.CompositeAttestation.VerifyEvidence.
func (e *Envelope) VerifyEvidence(ctx context.Context, opts ...attest.Option) ([]*attest.VerifiedReport, error) {
if len(e.EvidenceBytes) == 0 {
return nil, fmt.Errorf("%w: empty evidence", attest.ErrInvalidEvidence)
}
allOpts := append([]attest.Option{}, e.VerifyOpts...)
allOpts = append(allOpts, opts...)
rep, err := attest.Dispatch(ctx, e.Kind, e.EvidenceBytes, allOpts...)
if err != nil {
return nil, err
}
if err := defaultRIMCheck(rep, e.RIM); err != nil {
return nil, err
}
return []*attest.VerifiedReport{rep}, nil
}
func (e *Envelope) RIMDigest() [32]byte { return e.RIM }
func (e *Envelope) HardwareFingerprint() [32]byte { return e.Hardware }
func (e *Envelope) TEEPublicKey() [32]byte { return e.TEEPub }
func (e *Envelope) EvidenceIssuers() []string {
switch e.Kind {
case attest.KindSEVSNP:
return []string{kms.IssuerSEVSNP}
case attest.KindTDX:
return []string{kms.IssuerTDX}
case attest.KindNRAS:
return []string{kms.IssuerNVNRAS}
default:
return nil
}
}
func defaultRIMCheck(rep *attest.VerifiedReport, expected [32]byte) error {
if rep == nil {
return errors.New("rlwe-tee: defaultRIMCheck: nil verified report")
}
got := sha256.Sum256(rep.Measurement)
if subtle.ConstantTimeCompare(got[:], expected[:]) != 1 {
return fmt.Errorf("%w: report measurement does not fold to operator-asserted RIM", attest.ErrPolicy)
}
return nil
}
-264
View File
@@ -1,264 +0,0 @@
// SPDX-License-Identifier: BSD-3-Clause
package rlwetee
import (
"bytes"
"context"
"crypto/rand"
"crypto/sha256"
"fmt"
coronaThreshold "github.com/luxfi/corona/threshold"
"github.com/luxfi/mpc/pkg/approval"
)
// Sign produces a Ring-LWE corona threshold signature on msg, gated
// by the supplied attestation Envelope.
//
// Flow:
//
// 1. approval gate → gate.Issue → env.Verify → env.VerifyEvidence →
// gate.Release (same as slhdsa-tee / mldsa-tee).
// 2. Inside the TEE: read the wrapped master trusted-dealer key,
// deterministically regenerate all n key shares + GroupKey via
// corona.threshold.GenerateKeys, then drive Round1 → Round2 →
// Finalize across all n parties in-process.
// 3. Zeroize the master key + per-party share state.
// 4. Return the wire-form corona Signature.
//
// Output is a valid corona threshold signature on (gk, msg) that
// any verifier holding the published GroupKey bytes can validate
// via corona.threshold.VerifyBytes.
func (s *Signer) Sign(ctx context.Context, env *Envelope, jobID [32]byte, msg []byte) ([]byte, *SignReceipt, error) {
if env == nil {
return nil, nil, ErrAttestationRequired
}
if len(msg) == 0 {
return nil, nil, fmt.Errorf("rlwe-tee: empty message")
}
sealed, err := s.auditedRelease(ctx, env, jobID, msg)
if err != nil {
return nil, nil, err
}
key, err := s.hsmP.GetKey(ctx, s.cfg.WrappedSeedKeyID)
if err != nil {
return nil, nil, fmt.Errorf("%w: HSM GetKey: %v", ErrHSMUnreachable, err)
}
defer zeroize(key)
if len(key) != MasterKeySize {
return nil, nil, fmt.Errorf("%w: HSM-stored key length %d does not match MasterKeySize %d",
ErrCorruptWrappedSeed, len(key), MasterKeySize)
}
// Re-derive every key share + GroupKey deterministically from
// the master key. corona's GenerateKeys reads sign.KeySize bytes
// from randSource; bytes.NewReader pins the run reproducibly.
// The whole GenerateKeys + Round1 + Round2 + Finalize sequence
// holds coronaSerializer because corona's threshold globals
// (sign.K, sign.Threshold) and per-party internal state are
// written by every party in this sequence.
coronaSerializer.Lock()
shares, gk, err := coronaThreshold.GenerateKeys(s.cfg.Threshold, s.cfg.Participants, bytes.NewReader(key))
if err != nil {
coronaSerializer.Unlock()
return nil, nil, fmt.Errorf("%w: GenerateKeys: %v", ErrCorruptWrappedSeed, err)
}
sigBytes, err := s.runCoronaThresholdSign(jobID, msg, shares, gk)
coronaSerializer.Unlock()
if err != nil {
return nil, nil, fmt.Errorf("%w: %v", ErrCoronaProtocol, err)
}
audit, err := s.auditSignature(ctx, jobID, msg, sealed.Epoch, env.RIM)
if err != nil {
return nil, nil, fmt.Errorf("rlwe-tee: audit signature: %w", err)
}
recv := &SignReceipt{
JobID: jobID,
Epoch: sealed.Epoch,
IssuedNonce: sealed.IssuedNonce,
EphemeralPub: sealed.EphemeralPub,
EvidenceKind: string(env.Kind),
EvidenceIssuer: evidenceIssuerString(env),
AuditSignature: audit,
}
return sigBytes, recv, nil
}
// runCoronaThresholdSign drives Round1 → Round2 → Finalize for all
// n signers in-process. The canonical signer set is [0..n) — corona
// requires distinct integer indices and we honour that. The PRF key
// is sha256(GroupKey bytes || jobID) so distinct sign calls bind
// distinct sessions.
func (s *Signer) runCoronaThresholdSign(jobID [32]byte, msg []byte, shares []*coronaThreshold.KeyShare, gk *coronaThreshold.GroupKey) ([]byte, error) {
n := len(shares)
signerIDs := make([]int, n)
for i := range signerIDs {
signerIDs[i] = i
}
gkBytes, err := gk.MarshalBinary()
if err != nil {
return nil, fmt.Errorf("gk.MarshalBinary: %w", err)
}
prfDigest := sha256.New()
prfDigest.Write(gkBytes)
prfDigest.Write(jobID[:])
prfKey := prfDigest.Sum(nil)
sessionID := sessionIDFromJobID(jobID)
signers := make([]*coronaThreshold.Signer, n)
for i := 0; i < n; i++ {
signers[i] = coronaThreshold.NewSigner(shares[i])
}
r1Data := make(map[int]*coronaThreshold.Round1Data, n)
for _, signer := range signers {
r1 := signer.Round1(sessionID, prfKey, signerIDs)
r1Data[r1.PartyID] = r1
}
msgStr := string(msg)
r2Data := make(map[int]*coronaThreshold.Round2Data, n)
for _, signer := range signers {
r2, err := signer.Round2(sessionID, msgStr, prfKey, signerIDs, r1Data)
if err != nil {
return nil, fmt.Errorf("round2: %w", err)
}
r2Data[r2.PartyID] = r2
}
sig, err := signers[0].Finalize(r2Data)
if err != nil {
return nil, fmt.Errorf("finalize: %w", err)
}
if !coronaThreshold.Verify(gk, msgStr, sig) {
return nil, fmt.Errorf("self-verify failed (kernel bug)")
}
wire, err := sig.MarshalBinary()
if err != nil {
return nil, fmt.Errorf("sig.MarshalBinary: %w", err)
}
return wire, nil
}
// SignReceipt is the audit blob returned alongside the corona
// threshold signature.
type SignReceipt struct {
JobID [32]byte
Epoch uint64
IssuedNonce [32]byte
EphemeralPub [32]byte
EvidenceKind string
EvidenceIssuer string
AuditSignature []byte
}
func (s *Signer) auditSignature(ctx context.Context, jobID [32]byte, msg []byte, epoch uint64, rim [32]byte) ([]byte, error) {
h := sha256.New()
h.Write([]byte("LUX-RLWE-TEE-AUDIT-V1"))
h.Write([]byte{0x00})
h.Write(jobID[:])
h.Write(epochBytes(epoch))
h.Write(rim[:])
d := sha256.Sum256(msg)
h.Write(d[:])
auditDigest := h.Sum(nil)
return s.hsmP.Sign(ctx, s.cfg.KMSKeyID, auditDigest)
}
func epochBytes(e uint64) []byte {
return []byte{
byte(e >> 56), byte(e >> 48), byte(e >> 40), byte(e >> 32),
byte(e >> 24), byte(e >> 16), byte(e >> 8), byte(e),
}
}
func evidenceIssuerString(env *Envelope) string {
switch is := env.EvidenceIssuers(); len(is) {
case 0:
return ""
default:
return is[0]
}
}
// signIntent satisfies approval.CanonicalIntent.
type signIntent struct {
jobID [32]byte
msg []byte
env envelopeSummary
}
type envelopeSummary struct {
Kind string
RIM [32]byte
Hardware [32]byte
TEEPub [32]byte
}
func newSignIntent(jobID [32]byte, msg []byte, env *Envelope) *signIntent {
return &signIntent{
jobID: jobID,
msg: append([]byte(nil), msg...),
env: envelopeSummary{
Kind: string(env.Kind),
RIM: env.RIM,
Hardware: env.Hardware,
TEEPub: env.TEEPub,
},
}
}
func (si *signIntent) Digest() [32]byte {
h := sha256.New()
h.Write([]byte("LUX-RLWE-TEE-INTENT-V1"))
h.Write([]byte{0x00})
h.Write(si.jobID[:])
mdigest := sha256.Sum256(si.msg)
h.Write(mdigest[:])
h.Write([]byte(si.env.Kind))
h.Write([]byte{0x00})
h.Write(si.env.RIM[:])
h.Write(si.env.Hardware[:])
h.Write(si.env.TEEPub[:])
var out [32]byte
copy(out[:], h.Sum(nil))
return out
}
func (si *signIntent) Bytes() []byte {
out := make([]byte, 0, 32+32+len(si.env.Kind)+1+32+32+32+32)
out = append(out, []byte("LUX-RLWE-TEE-INTENT-V1")...)
out = append(out, 0x00)
out = append(out, si.jobID[:]...)
mdigest := sha256.Sum256(si.msg)
out = append(out, mdigest[:]...)
out = append(out, []byte(si.env.Kind)...)
out = append(out, 0x00)
out = append(out, si.env.RIM[:]...)
out = append(out, si.env.Hardware[:]...)
out = append(out, si.env.TEEPub[:]...)
return out
}
var _ approval.CanonicalIntent = (*signIntent)(nil)
// FreshJobID returns 32 bytes of crypto/rand.
func FreshJobID() ([32]byte, error) {
var out [32]byte
if _, err := rand.Read(out[:]); err != nil {
return out, err
}
return out, nil
}
-190
View File
@@ -1,190 +0,0 @@
// SPDX-License-Identifier: BSD-3-Clause
package rlwetee
import (
"bytes"
"context"
"crypto/rand"
"crypto/sha256"
"errors"
"fmt"
"sync"
coronaThreshold "github.com/luxfi/corona/threshold"
"github.com/luxfi/mpc/pkg/approval"
"github.com/luxfi/mpc/pkg/hsm"
"github.com/luxfi/mpc/pkg/kms"
)
// coronaSerializer serializes every call into corona.threshold.
//
// corona.threshold.GenerateKeys writes package-level globals
// (sign.K, sign.Threshold) — see corona/threshold/threshold.go:122-124.
// Two concurrent callers stomp on those globals and produce
// out-of-bounds slice errors deep in primitives.ShamirSecretSharing.
//
// Until corona makes its threshold API stateless we wrap every entry
// point with this mutex. This is an upstream-constraint workaround,
// not a security boundary; the mutex is package-level because the
// shared state is package-level.
var coronaSerializer sync.Mutex
// Signer is the institutional-custody Ring-LWE signer.
//
// Composition:
//
// - gate : kms.ReleaseGate — trust root.
// - hsmP : hsm.Provider — wraps the master trusted-dealer key.
// - appr : approval.ApprovalProvider — out-of-band approval gate.
// - cfg : Config — policy: RIM, hardware, t-of-n, key IDs.
//
// Safe for concurrent Sign calls.
type Signer struct {
gate kms.ReleaseGate
hsmP hsm.Provider
appr approval.ApprovalProvider
cfg Config
mu sync.Mutex // reserved for future per-Signer rate-limit state
}
// New builds a Signer.
func New(gate kms.ReleaseGate, hsmP hsm.Provider, appr approval.ApprovalProvider, cfg Config) (*Signer, error) {
if gate == nil {
return nil, errors.New("rlwe-tee: nil release gate")
}
if hsmP == nil {
return nil, errors.New("rlwe-tee: nil HSM provider")
}
if cfg.ApprovalRequired && appr == nil {
return nil, errors.New("rlwe-tee: nil approval provider but ApprovalRequired is true")
}
if err := cfg.Validate(); err != nil {
return nil, err
}
return &Signer{
gate: gate,
hsmP: hsmP,
appr: appr,
cfg: cfg,
}, nil
}
// Provision wraps a fresh MasterKeySize-byte trusted-dealer key under
// the HSM provider for later release-gated signing.
//
// The master key is generated from crypto/rand and stored via
// hsmP.StoreKey under cfg.WrappedSeedKeyID. We then derive the
// GroupKey once to return to the caller — embedders register that as
// the canonical group public key on the wire.
func (s *Signer) Provision(ctx context.Context) (*coronaThreshold.GroupKey, error) {
key := make([]byte, MasterKeySize)
defer zeroize(key)
if _, err := rand.Read(key); err != nil {
return nil, fmt.Errorf("rlwe-tee: provision: entropy: %w", err)
}
if err := s.hsmP.StoreKey(ctx, s.cfg.WrappedSeedKeyID, key); err != nil {
return nil, fmt.Errorf("rlwe-tee: provision: HSM StoreKey: %w", err)
}
// Materialize the GroupKey by re-running the deterministic dealer
// path with the fresh master key seeding the entropy. corona's
// GenerateKeys reads sign.KeySize bytes from randSource — pass a
// bytes.Reader so the run is reproducible. coronaSerializer
// guards the corona-internal global writes.
coronaSerializer.Lock()
_, gk, err := coronaThreshold.GenerateKeys(s.cfg.Threshold, s.cfg.Participants, bytes.NewReader(key))
coronaSerializer.Unlock()
if err != nil {
return nil, fmt.Errorf("rlwe-tee: provision: GenerateKeys: %w", err)
}
return gk, nil
}
// PublicKey reads the master key via the HSM provider and derives
// the corona GroupKey deterministically. RELEASE-GATE FREE — only
// the at-rest HSM material is read.
func (s *Signer) PublicKey(ctx context.Context) (*coronaThreshold.GroupKey, error) {
key, err := s.hsmP.GetKey(ctx, s.cfg.WrappedSeedKeyID)
if err != nil {
return nil, fmt.Errorf("rlwe-tee: PublicKey: HSM GetKey: %w", err)
}
defer zeroize(key)
if len(key) != MasterKeySize {
return nil, fmt.Errorf("rlwe-tee: PublicKey: key length %d does not match MasterKeySize %d", len(key), MasterKeySize)
}
coronaSerializer.Lock()
_, gk, err := coronaThreshold.GenerateKeys(s.cfg.Threshold, s.cfg.Participants, bytes.NewReader(key))
coronaSerializer.Unlock()
if err != nil {
return nil, fmt.Errorf("rlwe-tee: PublicKey: GenerateKeys: %w", err)
}
return gk, nil
}
// Threshold reports the corona t.
func (s *Signer) Threshold() int { return s.cfg.Threshold }
// Participants reports the corona n.
func (s *Signer) Participants() int { return s.cfg.Participants }
// auditedRelease drives the full Issue → approval → composite envelope
// → Release flow.
func (s *Signer) auditedRelease(ctx context.Context, env *Envelope, jobID [32]byte, msg []byte) (kms.SealedSessionKey, error) {
if env == nil {
return kms.SealedSessionKey{}, ErrAttestationRequired
}
if s.cfg.ApprovalRequired {
intent := newSignIntent(jobID, msg, env)
sig, err := s.appr.ApproveIntent(ctx, s.cfg.ApproverID, intent)
if err != nil {
return kms.SealedSessionKey{}, fmt.Errorf("%w: %v", ErrApprovalDenied, err)
}
ok, err := s.appr.VerifyApproval(ctx, intent, sig)
if err != nil {
return kms.SealedSessionKey{}, fmt.Errorf("%w: verify: %v", ErrApprovalDenied, err)
}
if !ok {
return kms.SealedSessionKey{}, ErrApprovalDenied
}
}
nonce, epoch, err := s.gate.Issue(jobID)
if err != nil {
return kms.SealedSessionKey{}, fmt.Errorf("%w: gate.Issue: %v", ErrKMSReleaseUnreachable, err)
}
env.ExpectedNonce = nonce
sealed, err := s.gate.Release(kms.ReleaseRequest{
JobID: jobID,
Epoch: epoch,
Nonce: nonce,
Attestation: env,
Ctx: ctx,
})
if err != nil {
return kms.SealedSessionKey{}, fmt.Errorf("%w: %v", ErrPolicyRefused, err)
}
return sealed, nil
}
func zeroize(b []byte) {
for i := range b {
b[i] = 0
}
}
// sessionIDFromJobID folds a 32-byte jobID into the small int the
// corona kernel's session-ID field needs. corona.threshold's session
// ID is `int`, so we take the first 31 bits of sha256(jobID) — strips
// the sign bit to stay in non-negative int range on all platforms.
func sessionIDFromJobID(jobID [32]byte) int {
d := sha256.Sum256(jobID[:])
v := int(d[0])<<23 | int(d[1])<<15 | int(d[2])<<7 | int(d[3]>>1)
return v
}
-521
View File
@@ -1,521 +0,0 @@
// SPDX-License-Identifier: BSD-3-Clause
package rlwetee
import (
"context"
"crypto/rand"
"crypto/sha256"
_ "embed"
"errors"
"os"
"testing"
"time"
sevtest "github.com/google/go-sev-guest/testing"
"github.com/google/go-sev-guest/verify/trust"
coronaThreshold "github.com/luxfi/corona/threshold"
"github.com/luxfi/mpc/cc/attest"
"github.com/luxfi/mpc/pkg/approval"
"github.com/luxfi/mpc/pkg/hsm"
"github.com/luxfi/mpc/pkg/kms"
)
// Real AMD Milan SEV-SNP attestation fixtures (same bytes as the
// lux/mpc cc/attest test corpus).
//
//go:embed testdata/sev_snp_attestation_milan.bin
var sevSnpAttestationMilan []byte
//go:embed testdata/sev_snp_vcek_milan.cer
var sevSnpVcekMilan []byte
func newKDSReplay() trust.HTTPSGetter {
return sevtest.SimpleGetter(map[string][]byte{
"https://kdsintf.amd.com/vcek/v1/Milan/cert_chain": trust.AskArkMilanVcekBytes,
"https://kdsintf.amd.com/vcek/v1/Milan/3ac3fe21e13fb0990eb28a802e3fb6a29483a6b0753590c951bdd3b8e53786184ca39e359669a2b76a1936776b564ea464cdce40c05f63c9b610c5068b006b5d?blSPL=2&teeSPL=0&snpSPL=5&ucodeSPL=68": sevSnpVcekMilan,
})
}
func fixedNow() time.Time {
return time.Date(2024, 1, 15, 12, 0, 0, 0, time.UTC)
}
func realMeasurement() []byte { return sevSnpAttestationMilan[0x90 : 0x90+48] }
func realChipID() []byte { return sevSnpAttestationMilan[0x1A0 : 0x1A0+64] }
func makeRIM(t *testing.T) [32]byte {
t.Helper()
return sha256.Sum256(realMeasurement())
}
func makeHardware(t *testing.T) [32]byte {
t.Helper()
return sha256.Sum256(realChipID())
}
func makeTEEPub(t *testing.T) [32]byte {
t.Helper()
var priv [32]byte
for i := range priv {
priv[i] = byte(i + 1)
}
priv[0] &= 248
priv[31] &= 127
priv[31] |= 64
pub, err := curve25519BasepointMul(priv[:])
if err != nil {
t.Fatalf("makeTEEPub: %v", err)
}
var out [32]byte
copy(out[:], pub)
return out
}
func newTestFileHSM(t *testing.T) hsm.Provider {
t.Helper()
dir := t.TempDir()
cfg := &hsm.FileConfig{
BasePath: dir,
HexEncoded: true,
}
p, err := hsm.NewFileProvider(cfg)
if err != nil {
t.Fatalf("newTestFileHSM: %v", err)
}
var ed25519Seed [32]byte
if _, err := rand.Read(ed25519Seed[:]); err != nil {
t.Fatalf("ed25519 seed: %v", err)
}
if err := p.StoreKey(context.Background(), "audit-key", ed25519Seed[:]); err != nil {
t.Fatalf("store audit key: %v", err)
}
t.Cleanup(func() {
_ = p.Close()
_ = os.RemoveAll(dir)
})
return p
}
func newTestApprovalProvider(t *testing.T) approval.ApprovalProvider {
t.Helper()
p, err := approval.NewProvider("local-dev", nil)
if err != nil {
t.Fatalf("newTestApprovalProvider: %v", err)
}
return p
}
// TestMain enables the LocalDevProvider for the lifetime of this
// test binary.
func TestMain(m *testing.M) {
_ = os.Setenv("MPC_LOCAL_APPROVAL", "true")
os.Exit(m.Run())
}
// denyApprovalProvider models user-cancel from a real WebAuthn/Ledger.
type denyApprovalProvider struct{}
func (denyApprovalProvider) Provider() string { return "deny-test" }
func (denyApprovalProvider) GetPublicIdentity(_ context.Context, approverID string) (approval.PublicIdentity, error) {
return approval.PublicIdentity{
ApproverID: approverID,
Provider: "deny-test",
PublicKey: make([]byte, 32),
Algorithm: approval.AlgorithmEd25519,
}, nil
}
func (denyApprovalProvider) ApproveIntent(_ context.Context, approverID string, intent approval.CanonicalIntent) (approval.ApprovalSignature, error) {
return approval.ApprovalSignature{}, errors.New("deny-test: user cancelled")
}
func (denyApprovalProvider) VerifyApproval(_ context.Context, intent approval.CanonicalIntent, sig approval.ApprovalSignature) (bool, error) {
return false, nil
}
func newTestGate(t *testing.T, rim, hw [32]byte) (*kms.LocalReleaseGate, kms.NonceStore) {
t.Helper()
policy := kms.NewReleasePolicy([][32]byte{rim}, [][32]byte{hw})
policy.RequireSEVSNP = true
var rootKey [32]byte
if _, err := rand.Read(rootKey[:]); err != nil {
t.Fatalf("rootKey: %v", err)
}
store := kms.NewMemoryNonceStore()
gate, err := kms.NewLocalReleaseGate(policy, store, rootKey)
if err != nil {
t.Fatalf("NewLocalReleaseGate: %v", err)
}
gate.SetIssueTTL(5 * time.Second)
gate.SetReplayWindow(5 * time.Second)
return gate, store
}
func newTestSigner(t *testing.T, approvalRequired bool) (*Signer, *kms.LocalReleaseGate, hsm.Provider, [32]byte, [32]byte) {
t.Helper()
rim := makeRIM(t)
hw := makeHardware(t)
gate, _ := newTestGate(t, rim, hw)
hsmP := newTestFileHSM(t)
appr := newTestApprovalProvider(t)
cfg := Config{
Threshold: 2,
Participants: 3,
RequiredRIM: map[[32]byte]struct{}{rim: {}},
AllowedHardware: map[[32]byte]struct{}{hw: {}},
RequireSEVSNP: true,
KMSKeyID: "audit-key",
WrappedSeedKeyID: "master-key",
ApprovalRequired: approvalRequired,
ApproverID: "test@lux.network",
}
s, err := New(gate, hsmP, appr, cfg)
if err != nil {
t.Fatalf("New: %v", err)
}
if _, err := s.Provision(context.Background()); err != nil {
t.Fatalf("Provision: %v", err)
}
return s, gate, hsmP, rim, hw
}
func envelopeFromTestdata(t *testing.T, rim, hw, teePub [32]byte) *Envelope {
t.Helper()
return &Envelope{
Kind: attest.KindSEVSNP,
EvidenceBytes: append([]byte(nil), sevSnpAttestationMilan...),
RIM: rim,
Hardware: hw,
TEEPub: teePub,
VerifyOpts: []attest.Option{
attest.WithKDSGetter(newKDSReplay()),
attest.WithNow(fixedNow()),
},
}
}
// ============================================================================
// Required test 1: full chain E2E
// ============================================================================
func TestSigner_Sign_SEVSNP_E2E(t *testing.T) {
t.Parallel()
s, _, _, rim, hw := newTestSigner(t, true)
teePub := makeTEEPub(t)
env := envelopeFromTestdata(t, rim, hw, teePub)
jobID, err := FreshJobID()
if err != nil {
t.Fatalf("FreshJobID: %v", err)
}
msg := []byte("LUX-RLWE-TEE: institutional-custody E2E test")
wire, receipt, err := s.Sign(context.Background(), env, jobID, msg)
if err != nil {
t.Fatalf("Sign: %v", err)
}
if len(wire) == 0 {
t.Fatal("Sign returned empty wire bytes")
}
if receipt == nil {
t.Fatal("Sign returned nil receipt")
}
if receipt.JobID != jobID {
t.Errorf("receipt.JobID = %x, want %x", receipt.JobID, jobID)
}
if receipt.EvidenceKind != string(attest.KindSEVSNP) {
t.Errorf("receipt.EvidenceKind = %q", receipt.EvidenceKind)
}
if receipt.EvidenceIssuer != kms.IssuerSEVSNP {
t.Errorf("receipt.EvidenceIssuer = %q", receipt.EvidenceIssuer)
}
if len(receipt.AuditSignature) == 0 {
t.Error("receipt.AuditSignature empty")
}
gk, err := s.PublicKey(context.Background())
if err != nil {
t.Fatalf("PublicKey: %v", err)
}
gkBytes, err := gk.MarshalBinary()
if err != nil {
t.Fatalf("gk.MarshalBinary: %v", err)
}
if !coronaThreshold.VerifyBytes(gkBytes, string(msg), wire) {
t.Fatal("external VerifyBytes refused the corona threshold signature")
}
}
// ============================================================================
// Required test 2: rejects corrupt attestation
// ============================================================================
func TestSigner_Sign_RejectsBadAttestation(t *testing.T) {
t.Parallel()
s, _, _, rim, hw := newTestSigner(t, false)
teePub := makeTEEPub(t)
env := envelopeFromTestdata(t, rim, hw, teePub)
env.EvidenceBytes[0x2A0+0x10] ^= 0x01
jobID, _ := FreshJobID()
msg := []byte("reject-bad-evidence")
_, _, err := s.Sign(context.Background(), env, jobID, msg)
if err == nil {
t.Fatal("Sign: expected refusal on tampered evidence, got nil")
}
if !errors.Is(err, ErrPolicyRefused) {
t.Errorf("Sign: err = %v, want wrapped ErrPolicyRefused", err)
}
}
// ============================================================================
// Required test 3: rejects RIM mismatch
// ============================================================================
func TestSigner_Sign_RejectsRIMMismatch(t *testing.T) {
t.Parallel()
s, _, _, _, hw := newTestSigner(t, false)
teePub := makeTEEPub(t)
wrongRIM := sha256.Sum256([]byte("not-the-real-measurement"))
env := envelopeFromTestdata(t, wrongRIM, hw, teePub)
jobID, _ := FreshJobID()
msg := []byte("reject-wrong-rim")
_, _, err := s.Sign(context.Background(), env, jobID, msg)
if err == nil {
t.Fatal("Sign: expected refusal on RIM mismatch, got nil")
}
if !errors.Is(err, ErrPolicyRefused) {
t.Errorf("Sign: err = %v, want wrapped ErrPolicyRefused", err)
}
}
// ============================================================================
// Required test 4: rejects expired nonce / wrong epoch
// ============================================================================
func TestSigner_Sign_RejectsExpiredNonce(t *testing.T) {
t.Parallel()
s, gate, _, rim, hw := newTestSigner(t, false)
teePub := makeTEEPub(t)
env := envelopeFromTestdata(t, rim, hw, teePub)
gate.SetIssueTTL(10 * time.Millisecond)
jobID, _ := FreshJobID()
nonce, epoch, err := gate.Issue(jobID)
if err != nil {
t.Fatalf("Issue: %v", err)
}
env.ExpectedNonce = nonce
time.Sleep(50 * time.Millisecond)
_, releaseErr := gate.Release(kms.ReleaseRequest{
JobID: jobID, Epoch: epoch, Nonce: nonce, Attestation: env, Ctx: context.Background(),
})
if releaseErr == nil {
t.Fatal("gate.Release: expected expiry refusal, got nil")
}
if !errors.Is(releaseErr, kms.ErrPolicyRefused) {
t.Errorf("releaseErr = %v, want wrapped kms.ErrPolicyRefused", releaseErr)
}
if !errors.Is(releaseErr, kms.ErrExpired) {
t.Errorf("releaseErr = %v, want wrapped kms.ErrExpired", releaseErr)
}
gate.SetIssueTTL(5 * time.Second)
_ = gate.Rotate()
freshJob, _ := FreshJobID()
freshEnv := envelopeFromTestdata(t, rim, hw, teePub)
msg := []byte("post-rotation-sign")
if _, _, err := s.Sign(context.Background(), freshEnv, freshJob, msg); err != nil {
t.Fatalf("Sign post-rotation: %v", err)
}
}
// ============================================================================
// Required test 5: AWS KMS backend
// ============================================================================
func TestSigner_Sign_HSMSign_AWS_KMS(t *testing.T) {
endpoint := os.Getenv("AWS_ENDPOINT_URL_KMS")
keyARN := os.Getenv("AWS_KMS_TEST_KEY_ARN")
if endpoint == "" || keyARN == "" {
t.Skip("AWS_ENDPOINT_URL_KMS and AWS_KMS_TEST_KEY_ARN not set; localstack KMS not available — see test comment for setup. Skipped per spec rationale: unit CI must not require real AWS credentials. File provider path is exercised by TestSigner_Sign_HSMSign_File and all chain-verify tests.")
}
awsCfg := &hsm.AWSConfig{
Region: os.Getenv("AWS_REGION"),
KeyARN: keyARN,
Profile: os.Getenv("AWS_PROFILE"),
}
awsP, err := hsm.NewAWSProvider(awsCfg)
if err != nil {
t.Fatalf("NewAWSProvider: %v", err)
}
defer awsP.Close()
digest := sha256.Sum256([]byte("aws-kms-audit-probe"))
sig, err := awsP.Sign(context.Background(), keyARN, digest[:])
if err != nil {
t.Fatalf("AWS KMS Sign: %v", err)
}
if len(sig) == 0 {
t.Fatal("AWS KMS Sign returned empty signature")
}
}
// ============================================================================
// Required test 6: File-backed HSM end-to-end
// ============================================================================
func TestSigner_Sign_HSMSign_File(t *testing.T) {
t.Parallel()
s, _, _, rim, hw := newTestSigner(t, true)
teePub := makeTEEPub(t)
env := envelopeFromTestdata(t, rim, hw, teePub)
jobID, _ := FreshJobID()
msg := []byte("file-hsm-e2e")
wire, _, err := s.Sign(context.Background(), env, jobID, msg)
if err != nil {
t.Fatalf("Sign: %v", err)
}
gk, err := s.PublicKey(context.Background())
if err != nil {
t.Fatalf("PublicKey: %v", err)
}
gkBytes, err := gk.MarshalBinary()
if err != nil {
t.Fatalf("gk.MarshalBinary: %v", err)
}
if !coronaThreshold.VerifyBytes(gkBytes, string(msg), wire) {
t.Fatal("file-hsm-e2e: VerifyBytes refused corona threshold signature")
}
}
// ============================================================================
// Required test 7: WebAuthn-style approval required
// ============================================================================
func TestSigner_Sign_ApprovalRequired_DenyAndAllow(t *testing.T) {
t.Parallel()
rim := makeRIM(t)
hw := makeHardware(t)
gate, store := newTestGate(t, rim, hw)
fileP := newTestFileHSM(t)
cfg := Config{
Threshold: 2,
Participants: 3,
RequiredRIM: map[[32]byte]struct{}{rim: {}},
AllowedHardware: map[[32]byte]struct{}{hw: {}},
RequireSEVSNP: true,
KMSKeyID: "audit-key",
WrappedSeedKeyID: "master-key",
ApprovalRequired: true,
ApproverID: "ceo@lux.network",
}
denyS, err := New(gate, fileP, denyApprovalProvider{}, cfg)
if err != nil {
t.Fatalf("New(deny): %v", err)
}
if _, err := denyS.Provision(context.Background()); err != nil {
t.Fatalf("Provision: %v", err)
}
teePub := makeTEEPub(t)
env := envelopeFromTestdata(t, rim, hw, teePub)
jobID, _ := FreshJobID()
msg := []byte("approval-deny-test")
_, _, err = denyS.Sign(context.Background(), env, jobID, msg)
if err == nil {
t.Fatal("Sign(deny): expected ErrApprovalDenied, got nil")
}
if !errors.Is(err, ErrApprovalDenied) {
t.Errorf("Sign(deny): err = %v, want wrapped ErrApprovalDenied", err)
}
if _, lookupErr := store.Lookup(jobID); !errors.Is(lookupErr, kms.ErrNonceUnknown) {
t.Errorf("deny path leaked a gate-issued nonce: %v", lookupErr)
}
appr, err := approval.NewProvider("local-dev", nil)
if err != nil {
t.Fatalf("local-dev provider: %v", err)
}
allowS, err := New(gate, fileP, appr, cfg)
if err != nil {
t.Fatalf("New(allow): %v", err)
}
allowEnv := envelopeFromTestdata(t, rim, hw, teePub)
allowJob, _ := FreshJobID()
if _, _, err := allowS.Sign(context.Background(), allowEnv, allowJob, msg); err != nil {
t.Fatalf("Sign(allow): %v", err)
}
}
// ============================================================================
// Config validation
// ============================================================================
func TestConfig_Validate(t *testing.T) {
rim := [32]byte{1}
hw := [32]byte{2}
good := Config{
Threshold: 2,
Participants: 3,
RequiredRIM: map[[32]byte]struct{}{rim: {}},
AllowedHardware: map[[32]byte]struct{}{hw: {}},
RequireSEVSNP: true,
KMSKeyID: "k",
WrappedSeedKeyID: "s",
}
if err := good.Validate(); err != nil {
t.Fatalf("good: %v", err)
}
cases := []struct {
name string
mutate func(*Config)
want error
}{
{"thresholdZero", func(c *Config) { c.Threshold = 0 }, ErrInvalidThreshold},
{"thresholdEqualParticipants", func(c *Config) { c.Threshold = 3 }, ErrInvalidThreshold},
{"participantsTooLow", func(c *Config) { c.Threshold = 1; c.Participants = 1 }, ErrInvalidThreshold},
{"emptyRIM", func(c *Config) { c.RequiredRIM = nil }, ErrEmptyRIM},
{"emptyHardware", func(c *Config) { c.AllowedHardware = nil }, ErrEmptyHardware},
{"noRequireFlag", func(c *Config) { c.RequireSEVSNP = false }, ErrNoRequireFlag},
{"missingKMSKeyID", func(c *Config) { c.KMSKeyID = "" }, ErrMissingKMSKeyID},
{"missingSeedKeyID", func(c *Config) { c.WrappedSeedKeyID = "" }, ErrMissingSeedKeyID},
{"approverMissing", func(c *Config) { c.ApprovalRequired = true; c.ApproverID = "" }, ErrApproverMissing},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
c := good
c.RequiredRIM = map[[32]byte]struct{}{rim: {}}
c.AllowedHardware = map[[32]byte]struct{}{hw: {}}
tc.mutate(&c)
err := c.Validate()
if !errors.Is(err, tc.want) {
t.Errorf("err = %v, want %v", err, tc.want)
}
})
}
}
func TestSigner_ThresholdAndParticipants(t *testing.T) {
t.Parallel()
s, _, _, _, _ := newTestSigner(t, false)
if got := s.Threshold(); got != 2 {
t.Errorf("Threshold = %d, want 2", got)
}
if got := s.Participants(); got != 3 {
t.Errorf("Participants = %d, want 3", got)
}
}
Binary file not shown.
Binary file not shown.
-132
View File
@@ -1,132 +0,0 @@
# slhdsa-tee
Operator-controlled SLH-DSA threshold signing via TEE-gated master-seed
reconstruction.
## What this is
A composable Go package that produces FIPS 205 SLH-DSA signatures
gated on:
1. A verifiable hardware TEE attestation (AMD SEV-SNP, Intel TDX, NVIDIA NRAS)
chain-validated by `github.com/luxfi/mpc/cc/attest`.
2. A KMS release-gate (`github.com/luxfi/mpc/pkg/kms.ReleaseGate`) that pins
the worker's RIM digest + hardware fingerprint and binds a single-use
challenge nonce per request.
3. An out-of-band human or programmatic approval signature
(`github.com/luxfi/mpc/pkg/approval.ApprovalProvider`).
4. An HSM-resident wrap-key store (`github.com/luxfi/mpc/pkg/hsm.Provider`)
so the master SLH-DSA seed lives sealed-at-rest and is only ever
unwrapped inside the attested TEE.
Output is byte-identical to single-party FIPS 205 `SignDeterministic` on
the same `(seed-derived sk, msg, ctx)` tuple. Any verifier holding the
published MAGG-framed group public key validates with
`magnetar.VerifyBytes` — no awareness of the threshold or TEE substrate
is required.
## When to use this
- **M-Chain bridge custody**: an operator holds the bridge's signing key
inside a SEV-SNP / TDX TEE. Per-redemption sign calls require an
attestation envelope plus an executive approval. The master seed
never leaves the HSM in plaintext outside the attested TEE context.
- **A-Chain confidential-compute oracle**: an oracle node produces
SLH-DSA attestations over confidential model outputs. The output is
gated on the worker's RIM matching a known-good measurement
allowlist.
## When NOT to use this
- **Public-BFT consensus**: use the per-validator standalone
`magnetar.ValidatorSign` path (canonical v0.5+). No DKG, no dealer,
no aggregator-in-TCB.
- **Permissionless threshold custody**: use `magnetar.Combine`
(THBS-SE, v0.1 reveal-and-aggregate) — no party ever holds the master
seed, so there is no TCB to attest.
- **Test / dev with no TEE hardware available**: use
`magnetar.GenerateKey` + `magnetar.Sign` directly.
## Decision matrix
| Use case | Primitive |
|----------|-----------|
| Public-BFT validator | `magnetar.ValidatorSign` (PRIMARY) |
| Permissionless N-of-N custody, no party holds seed | `magnetar.Combine` (THBS-SE) |
| Institutional custody with attested release | `slhdsa-tee.Signer.Sign` (THIS) |
| Single-party / dev | `magnetar.Sign` |
## Layering
```
caller (operator daemon, bridge node, custody orchestrator)
└── slhdsatee.Signer.Sign(ctx, env, jobID, msg, signCtx)
├── approval.ApprovalProvider.ApproveIntent (out-of-band gate)
├── kms.ReleaseGate.Issue / Release (TEE-gated wrap key)
│ └── cc/attest.Dispatch (vendor chain verify)
├── hsm.Provider.GetKey (wrapped seed at rest)
├── magnetar.KeyFromSeed → magnetar.Sign (FIPS 205 emit)
└── hsm.Provider.Sign (audit signature)
```
Each step is independently complete and replaceable.
## Threat model
- **Compromised operator binary outside TEE**: refused — no valid
attestation possible.
- **Forged attestation envelope (chain-invalid)**: refused at
`cc/attest.Dispatch``kms.ErrAttestationChain`.
- **Replay of an old sealed key**: refused — AAD binds (epoch, jobID,
teePub, issuedNonce).
- **Coerced approver**: detectable in the receipt's audit signature
trail.
- **Stolen master seed at rest**: protected by HSM root-of-trust (AWS
KMS, Azure Key Vault, GCP Cloud KMS, YubiHSM, Zymbit, file with
age-encryption).
## Example
```go
import (
slhdsatee "github.com/luxfi/threshold/protocols/slhdsa-tee"
magnetar "github.com/luxfi/magnetar/ref/go/pkg/magnetar"
"github.com/luxfi/mpc/pkg/kms"
"github.com/luxfi/mpc/pkg/hsm"
"github.com/luxfi/mpc/pkg/approval"
)
policy := kms.NewReleasePolicyStrict([][32]byte{knownRIM}, [][32]byte{knownHW})
store := kms.NewMemoryNonceStore() // or DatabaseNonceStore in prod
gate, _ := kms.NewLocalReleaseGate(policy, store, rootKey)
hsmP, _ := hsm.NewAWSProvider(awsCfg)
appr, _ := approval.NewProvider("webauthn", webauthnCfg)
cfg := slhdsatee.Config{
Mode: magnetar.ModeM192s,
RequiredRIM: policy.RequiredRIM,
AllowedHardware: policy.AllowedHardware,
RequireSEVSNP: true,
KMSKeyID: "arn:aws:kms:us-east-1:...:key/...",
WrappedSeedKeyID: "lux-custody-master-seed",
ApprovalRequired: true,
ApproverID: "custody-ceo@org.example",
}
signer, _ := slhdsatee.New(gate, hsmP, appr, cfg)
env := &slhdsatee.Envelope{
Kind: attest.KindSEVSNP,
EvidenceBytes: liveAttestationBytes,
RIM: knownRIM,
Hardware: knownHW,
TEEPub: workerTEEPub,
}
jobID, _ := slhdsatee.FreshJobID()
sig, receipt, err := signer.Sign(ctx, env, jobID, msg, nil)
```
## Dispatcher wiring
The `pkg/thresholdd` JSON-RPC dispatcher gains a `Sign_TEE` method on
the magnetar scheme that calls this package. The default `Sign` method
remains permissionless (per-validator standalone).
-123
View File
@@ -1,123 +0,0 @@
// SPDX-License-Identifier: BSD-3-Clause
package slhdsatee
import (
"errors"
"fmt"
magnetar "github.com/luxfi/magnetar/ref/go/pkg/magnetar"
)
// Config carries the operator-side policy + provider configuration.
//
// Every field is required; New refuses on any zero value. There is no
// "default-friendly" path — institutional custody never starts with an
// empty allowlist or an unset KMS root.
type Config struct {
// Mode pins the FIPS 205 parameter set. Production institutional
// custody uses ModeM192s (recommended) or ModeM256s for the most
// conservative posture. Mode is bound into Signer.params at
// construction and into the keypair derived from the unwrapped
// master seed.
Mode magnetar.Mode
// RequiredRIM is the set of acceptable Reference-Integrity-Manifest
// digests for the worker that holds the wrapped master seed. A
// release-gate refusal here means the worker binary or the TEE
// firmware does not match a known-good measurement.
//
// Wire bytes: SHA-256 of the canonical RIM document. Mirrors
// luxfi/mpc/pkg/kms.ReleasePolicy.RequiredRIM.
RequiredRIM map[[32]byte]struct{}
// AllowedHardware is the set of acceptable hardware-fingerprint
// digests (sha256(model || driver || vbios) for GPU paths,
// platform/chip ID for CPU-only).
AllowedHardware map[[32]byte]struct{}
// RequireSEVSNP / RequireTDX / RequireNVNRAS mirror
// kms.ReleasePolicy.Require* — at least one MUST be true for any
// non-test deployment. The composite envelope produced here will
// surface its evidence issuers via the kms.CompositeAttestation
// interface so the gate's default-deny posture fires correctly.
RequireSEVSNP bool
RequireTDX bool
RequireNVNRAS bool
// KMSKeyID is the HSM key identifier under which the wrapped
// master seed is stored. The HSM provider's Sign API is not used
// for the SLH-DSA inner-sign (FIPS 205 has no native HSM offload
// today); we use it to delegate ANCILLARY ECDSA-P256 audit
// signatures over the (jobID, msg, sealedKey.Epoch, RIM) tuple so
// every release is independently auditable. The master seed
// itself is stored as raw bytes via HSM Provider.GetKey /
// StoreKey under WrappedSeedKeyID.
KMSKeyID string
// WrappedSeedKeyID is the HSM-stored blob identifier for the
// AEAD-wrapped master seed. The blob is opened ONLY inside the
// TEE after gate.Release returns a sealed session key. The HSM
// holds ciphertext; the TEE holds plaintext for the duration of
// one sign call; the host process never sees plaintext.
WrappedSeedKeyID string
// ApprovalRequired determines whether ApprovalProvider must
// produce a non-deny ApprovalSignature before Issue() is called.
// Production institutional custody MUST set this true. Test mode
// may set false to exercise the chain-verify + release path in
// isolation.
ApprovalRequired bool
// ApproverID is the canonical identifier (email, DID, KMS ARN)
// whose approval is required. Used as the lookup key against the
// configured ApprovalProvider. Empty rejects when
// ApprovalRequired is true.
ApproverID string
}
// Errors surfaced by Config.Validate. Distinguished by errors.Is so
// callers (operator boot scripts, helm chart smoke tests) can switch.
var (
ErrInvalidMode = errors.New("slhdsa-tee: invalid magnetar mode")
ErrEmptyRIM = errors.New("slhdsa-tee: RequiredRIM must be non-empty (default-deny posture)")
ErrEmptyHardware = errors.New("slhdsa-tee: AllowedHardware must be non-empty (default-deny posture)")
ErrNoRequireFlag = errors.New("slhdsa-tee: at least one Require* TEE flag must be true")
ErrMissingKMSKeyID = errors.New("slhdsa-tee: KMSKeyID required for audit signature")
ErrMissingSeedKeyID = errors.New("slhdsa-tee: WrappedSeedKeyID required for HSM seed storage")
ErrApproverMissing = errors.New("slhdsa-tee: ApproverID required when ApprovalRequired is true")
ErrApprovalDenied = errors.New("slhdsa-tee: approval provider denied or returned mismatched signature")
ErrAttestationRequired = errors.New("slhdsa-tee: attestation envelope required (cannot sign without TEE evidence)")
ErrPolicyRefused = errors.New("slhdsa-tee: release gate refused (RIM, hardware, nonce, or chain verify)")
ErrKMSReleaseUnreachable = errors.New("slhdsa-tee: release gate unreachable")
ErrHSMUnreachable = errors.New("slhdsa-tee: HSM provider unreachable")
ErrCorruptWrappedSeed = errors.New("slhdsa-tee: wrapped seed blob fails authenticated decryption")
)
// Validate reports the first structural error in cfg. There is no
// "warnings" return — institutional-custody policy is hard or it is
// nothing.
func (cfg *Config) Validate() error {
if _, err := magnetar.ParamsFor(cfg.Mode); err != nil {
return fmt.Errorf("%w: %v", ErrInvalidMode, err)
}
if len(cfg.RequiredRIM) == 0 {
return ErrEmptyRIM
}
if len(cfg.AllowedHardware) == 0 {
return ErrEmptyHardware
}
if !cfg.RequireSEVSNP && !cfg.RequireTDX && !cfg.RequireNVNRAS {
return ErrNoRequireFlag
}
if cfg.KMSKeyID == "" {
return ErrMissingKMSKeyID
}
if cfg.WrappedSeedKeyID == "" {
return ErrMissingSeedKeyID
}
if cfg.ApprovalRequired && cfg.ApproverID == "" {
return ErrApproverMissing
}
return nil
}
-12
View File
@@ -1,12 +0,0 @@
// SPDX-License-Identifier: BSD-3-Clause
package slhdsatee
import "golang.org/x/crypto/curve25519"
// curve25519BasepointMul is a test-only helper that returns the
// public half of a clamped X25519 private scalar. Lives in a
// _test.go file so the production package surface does not export it.
func curve25519BasepointMul(priv []byte) ([]byte, error) {
return curve25519.X25519(priv, curve25519.Basepoint)
}
-66
View File
@@ -1,66 +0,0 @@
// SPDX-License-Identifier: BSD-3-Clause
// Package slhdsatee implements operator-controlled SLH-DSA threshold
// signing via TEE-gated master-seed reconstruction.
//
// This is NOT a public-BFT primitive. Magnetar v0.5's per-validator
// standalone path (and v0.1 reveal-and-aggregate THBS-SE for delegated
// custody) is the canonical permissionless path; THIS package is the
// institutional-custody-only extension that binds key release to:
//
// 1. a verifiable hardware TEE attestation (AMD SEV-SNP / Intel TDX /
// NVIDIA NRAS) chain-validated by github.com/luxfi/mpc/cc/attest;
// 2. a KMS release-gate (github.com/luxfi/mpc/pkg/kms.ReleaseGate)
// that pins the worker's RIM digest + hardware fingerprint and
// binds a single-use challenge nonce per-request;
// 3. an out-of-band human / programmatic approval signature
// (github.com/luxfi/mpc/pkg/approval.ApprovalProvider);
// 4. an HSM-resident wrap-key store
// (github.com/luxfi/mpc/pkg/hsm.Provider) so the master SLH-DSA
// seed lives sealed-at-rest and is only ever unwrapped inside the
// attested TEE.
//
// The sign call returns bytes byte-identical to single-party FIPS 205
// SLH-DSA SignDeterministic on (master_seed → KeyFromSeed → Sign(msg,
// ctx)). Any caller holding the published MAGG-framed group public key
// can verify with magnetar.VerifyBytes (or Verify) — no awareness of
// the threshold or TEE substrate is required.
//
// Threat model:
//
// - Adversary controls the operator process (compromised binary,
// malicious operator) outside the TEE. Without a valid attestation
// that chains to the pinned vendor root AND a fresh approval that
// matches the RIM/hardware policy, no sign is possible. The HSM
// never releases the master seed in plaintext — only the AEAD
// ciphertext sealed to the gate-issued ephemeral pubkey can leave
// the gate.
// - Adversary recovers an old sealed key. AAD binds (epoch, jobID,
// teePub, issuedNonce); replay across epoch or jobID is refused.
// - Adversary forges an attestation envelope whose Verify(nonce)
// returns true but whose evidence does not chain to the vendor
// root. ReleaseGate.Release calls CompositeAttestation.VerifyEvidence
// which dispatches every blob through cc/attest.Dispatch and
// refuses on chain-invalid; this package's Envelope ties the cc/attest
// verifier into that contract.
//
// What this package is NOT:
//
// - NOT a no-trusted-dealer DKG. The master seed is generated once
// under TEE attestation; subsequent signs only release the wrapped
// seed under the same attestation policy. A real DKG construction
// for SLH-DSA is the magnetar package's THBS-SE family — see
// magnetar/ref/go/pkg/magnetar/thbsse.go for the permissionless
// primitive that produces a FIPS 205 byte-identical signature
// without any party ever holding the master seed.
//
// - NOT a substitute for magnetar.ValidatorSign or magnetar.Combine
// on the public-BFT consensus surface. Use this ONLY when the
// threat model permits "trusted custody with attested release"
// (e.g. M-Chain bridge custody operator, A-Chain confidential
// compute oracle).
//
// Wire compatibility: output is a magnetar.Signature (mode default
// ModeM192s) — the same wire form the dispatcher emits today. The
// SDK / verifier path is unchanged.
package slhdsatee
-178
View File
@@ -1,178 +0,0 @@
// SPDX-License-Identifier: BSD-3-Clause
package slhdsatee
import (
"context"
"crypto/sha256"
"crypto/subtle"
"errors"
"fmt"
"github.com/luxfi/mpc/cc/attest"
"github.com/luxfi/mpc/pkg/kms"
)
// Envelope is the minimal kms.CompositeAttestation implementation this
// package consumes. It wraps an attested evidence blob (the bytes the
// worker captured via SNP_GUEST_REQUEST / TDREPORT ioctls / NRAS JWT)
// plus the operator-supplied RIM digest, hardware fingerprint, and
// TEE pubkey.
//
// One Envelope per Sign call — no caching. The release gate calls
// Verify(nonce) (cheap nonce check) then VerifyEvidence(ctx, opts...)
// (full chain check via cc/attest). Both must pass.
//
// Wire-form is intentionally NOT defined here: this is a per-process
// composition. The bridge / oracle wire layer that ships attested
// evidence between TEE and operator (or operator and KMS) is the
// responsibility of the calling subsystem. Magnetar's THBS-SE is the
// public-BFT wire spec; this package's Envelope is the institutional-
// custody in-process composition.
type Envelope struct {
// Kind is the cc/attest evidence kind matching EvidenceBytes
// framing. Used by Dispatch to route to the right verifier.
Kind attest.Kind
// EvidenceBytes is the raw vendor-framed evidence (SEV-SNP report
// ABI bytes, TDX TDREPORT bytes, NRAS JWT bytes).
EvidenceBytes []byte
// ExpectedNonce is the gate-issued challenge nonce the TEE bound
// into REPORT_DATA before capturing the evidence. Compared by
// Verify against the kms-supplied expectedNonce.
ExpectedNonce [32]byte
// RIM is the operator-asserted RIM digest the TEE measured. The
// release gate compares RIM against ReleasePolicy.RequiredRIM.
RIM [32]byte
// Hardware is the operator-asserted hardware fingerprint
// (sha256(model||driver||vbios) or platform/chip ID).
Hardware [32]byte
// TEEPub is the X25519 public key sealed against — the private
// half lives only inside the attested TEE.
TEEPub [32]byte
// VerifyOpts are forwarded to attest.Dispatch by VerifyEvidence
// (e.g. WithKDSGetter for offline tests, WithNow for fixed-clock
// reproducibility, WithExpectedMeasurement to pin the launch
// digest beyond RIM membership).
VerifyOpts []attest.Option
// rimChecker is an optional override used to pin the launch
// measurement against the verified report. When nil the package
// uses the default rule: VerifiedReport.Measurement must lie in
// the operator-supplied RequiredRIM allowlist. This is the
// orthogonal hook the dispatcher uses to wire RIM membership into
// the cc/attest chain layer without duplicating policy in two
// places.
rimChecker func(*attest.VerifiedReport) error
}
var _ kms.CompositeAttestation = (*Envelope)(nil)
// Verify implements kms.CompositeAttestation.Verify. Returns true iff
// the gate-issued nonce equals the nonce the operator embedded in the
// evidence at TEE-capture time.
//
// This is the cheap pre-chain check. It does NOT prove the evidence
// chains to the vendor root — VerifyEvidence does that. The gate
// calls both in order.
func (e *Envelope) Verify(expectedNonce [32]byte) (bool, error) {
if subtle.ConstantTimeCompare(e.ExpectedNonce[:], expectedNonce[:]) != 1 {
return false, nil
}
return true, nil
}
// VerifyEvidence implements kms.CompositeAttestation.VerifyEvidence.
//
// Dispatches the single evidence blob to cc/attest, returns the
// verified report on success, surfaces any chain / signature / policy
// failure via ErrChainInvalid / ErrSignatureInvalid / ErrPolicy. The
// gate translates these into ErrAttestationChain wrapped under
// ErrPolicyRefused.
//
// Additionally enforces that the operator-asserted RIM equals the
// VerifiedReport.Measurement after chain validation. This is the
// composition step: the gate checks RIM membership in its allowlist;
// VerifyEvidence checks the report Measurement equals the operator-
// asserted RIM. Together: a chain-validated report whose Measurement
// is in the gate's RequiredRIM set.
func (e *Envelope) VerifyEvidence(ctx context.Context, opts ...attest.Option) ([]*attest.VerifiedReport, error) {
if len(e.EvidenceBytes) == 0 {
return nil, fmt.Errorf("%w: empty evidence", attest.ErrInvalidEvidence)
}
allOpts := append([]attest.Option{}, e.VerifyOpts...)
allOpts = append(allOpts, opts...)
rep, err := attest.Dispatch(ctx, e.Kind, e.EvidenceBytes, allOpts...)
if err != nil {
return nil, err
}
if e.rimChecker != nil {
if err := e.rimChecker(rep); err != nil {
return nil, err
}
} else {
if err := defaultRIMCheck(rep, e.RIM); err != nil {
return nil, err
}
}
return []*attest.VerifiedReport{rep}, nil
}
// RIMDigest implements kms.CompositeAttestation.RIMDigest.
func (e *Envelope) RIMDigest() [32]byte { return e.RIM }
// HardwareFingerprint implements kms.CompositeAttestation.HardwareFingerprint.
func (e *Envelope) HardwareFingerprint() [32]byte { return e.Hardware }
// TEEPublicKey implements kms.CompositeAttestation.TEEPublicKey.
func (e *Envelope) TEEPublicKey() [32]byte { return e.TEEPub }
// EvidenceIssuers implements kms.CompositeAttestation.EvidenceIssuers.
//
// One issuer per envelope (this construction binds one TEE quote per
// sign request). If composite (CPU TEE + GPU NRAS) is required, the
// operator MUST run Sign once per quorum member and combine results;
// for the institutional-custody case here, one CPU TEE quote is the
// canonical posture.
func (e *Envelope) EvidenceIssuers() []string {
switch e.Kind {
case attest.KindSEVSNP:
return []string{kms.IssuerSEVSNP}
case attest.KindTDX:
return []string{kms.IssuerTDX}
case attest.KindNRAS:
return []string{kms.IssuerNVNRAS}
default:
return nil
}
}
// defaultRIMCheck verifies the cc/attest verified-report Measurement
// matches the operator-asserted RIM digest. We sha256-fold the raw
// measurement bytes (variable-length per evidence kind: 48 bytes for
// SEV-SNP, MRTD for TDX, derived for NRAS) into the 32-byte RIM
// digest convention used by the gate's allowlist.
//
// Folding is one-way and stable: sha256(measurement) is the operator-
// asserted RIM identifier; the release gate's RequiredRIM is the
// allowlist of such identifiers; the worker that captured this
// evidence claimed to be measuring RIM X; defaultRIMCheck enforces
// that X equals sha256(measurement) of the chain-validated report.
//
// Constant-time compare to avoid timing leaks on partial-RIM match.
func defaultRIMCheck(rep *attest.VerifiedReport, expected [32]byte) error {
if rep == nil {
return errors.New("slhdsa-tee: defaultRIMCheck: nil verified report")
}
got := sha256.Sum256(rep.Measurement)
if subtle.ConstantTimeCompare(got[:], expected[:]) != 1 {
return fmt.Errorf("%w: report measurement does not fold to operator-asserted RIM", attest.ErrPolicy)
}
return nil
}
-381
View File
@@ -1,381 +0,0 @@
// SPDX-License-Identifier: BSD-3-Clause
package slhdsatee
import (
"context"
"crypto/subtle"
"errors"
"fmt"
"sort"
"sync"
"time"
"github.com/luxfi/mpc/cc/attest"
)
// CombinerPool is the t-of-n attested-combiner registry the strict-PQ
// profile binds Combine through.
//
// Composition:
//
// - Each AttestedCombiner is one of (SEV-SNP / TDX / NRAS) and holds
// a Signer + the last successful VerifiedReport's IssuedAt.
// - The pool is parameterised by (Threshold, RotationWindow,
// KnownIssuers, Now). Threshold is the t-of-n quorum required for
// a Combine to surface a signature. RotationWindow bounds the age
// of each combiner's attestation: a Combine call that finds any
// selected combiner outside the window refuses with
// ErrMagnetarStaleAttestation BEFORE reaching that combiner's
// signer.
// - KnownIssuers is the set of vendor strings the pool is willing to
// accept under the active chain profile (e.g. {"amd.sev.snp",
// "intel.tdx", "nvidia.nras.v1"}); a combiner whose verified-report
// Vendor is not in this set is refused.
//
// Hickey discipline: pool state is one value; mutation goes through
// the API; concurrent Combine calls observe the same snapshot for the
// duration of one call (under RLock).
type CombinerPool struct {
mu sync.RWMutex
threshold int
rotation time.Duration
issuers map[string]struct{}
members []*PoolMember
now func() time.Time
}
// PoolMember binds one attested combiner endpoint to its current
// attestation freshness state. The Signer is the slhdsa-tee.Signer
// that holds the wrapped seed for THIS combiner (one wrapped seed
// per attested host); the LastVerifiedReport is what
// pool.Attest() refreshes when the operator submits a new quote.
type PoolMember struct {
// Name is the operator-supplied identifier for this combiner
// endpoint (e.g. "us-east-1a-snp-01"). Used in audit logs and to
// disambiguate members in pool errors.
Name string
// Signer is the slhdsa-tee.Signer wired to this combiner. The
// pool delegates the actual Sign call to Signer.Sign once the
// pool-level freshness gate has accepted the most recent quote.
Signer *Signer
// mu protects LastReport / LastIssuedAt. Per-member lock so
// Attest() on one member does not block Combine on another.
mu sync.RWMutex
LastReport *attest.VerifiedReport
LastIssuedAt time.Time
}
// CombinerPoolConfig captures pool-level policy.
type CombinerPoolConfig struct {
// Threshold is the t in t-of-n. MUST be >= 2; the user mandate
// explicitly calls for "at least 2-of-3 attested combiners must
// produce matching signature for it to be accepted".
Threshold int
// RotationWindow bounds the age of each combiner's attestation.
// Outside this window, Combine refuses with
// ErrMagnetarStaleAttestation. Production deployments rotate per
// epoch (Lux block-time epoch == ~1s currently); a 60s rotation
// window absorbs ~60 blocks of latency between operator
// re-attestation and the next sign request.
RotationWindow time.Duration
// KnownIssuers is the set of vendor strings the pool admits.
// For strict-PQ production deployments, this is typically
// {"amd.sev.snp"} (the only production-attestable verifier today;
// TDX and NRAS are stubs at cc/attest tracked #222). The
// dispatcher fails fast on a Combine that selects an issuer not
// in this set.
KnownIssuers map[string]struct{}
// Now is the wall-clock source. Production leaves this nil so
// the pool uses time.Now; tests pin it for deterministic rotation
// windows.
Now func() time.Time
}
// NewCombinerPool builds an empty pool from cfg. Members are added
// via AddMember; the pool is empty until at least Threshold members
// are registered + attested.
func NewCombinerPool(cfg CombinerPoolConfig) (*CombinerPool, error) {
if cfg.Threshold < 2 {
return nil, fmt.Errorf("slhdsa-tee: CombinerPool requires Threshold >= 2 (got %d)", cfg.Threshold)
}
if cfg.RotationWindow <= 0 {
return nil, fmt.Errorf("slhdsa-tee: CombinerPool requires RotationWindow > 0 (got %s)", cfg.RotationWindow)
}
if len(cfg.KnownIssuers) == 0 {
return nil, errors.New("slhdsa-tee: CombinerPool requires at least one KnownIssuer (default-deny posture)")
}
now := cfg.Now
if now == nil {
now = time.Now
}
p := &CombinerPool{
threshold: cfg.Threshold,
rotation: cfg.RotationWindow,
issuers: make(map[string]struct{}, len(cfg.KnownIssuers)),
now: now,
}
for k := range cfg.KnownIssuers {
p.issuers[k] = struct{}{}
}
return p, nil
}
// AddMember registers an attested combiner endpoint. The member is
// NOT yet sign-eligible — Attest must be called with a fresh quote
// before it counts toward the quorum.
func (p *CombinerPool) AddMember(name string, signer *Signer) (*PoolMember, error) {
if signer == nil {
return nil, errors.New("slhdsa-tee: CombinerPool.AddMember: nil Signer")
}
if name == "" {
return nil, errors.New("slhdsa-tee: CombinerPool.AddMember: empty Name")
}
m := &PoolMember{Name: name, Signer: signer}
p.mu.Lock()
defer p.mu.Unlock()
for _, existing := range p.members {
if existing.Name == name {
return nil, fmt.Errorf("slhdsa-tee: CombinerPool.AddMember: duplicate member %q", name)
}
}
p.members = append(p.members, m)
return m, nil
}
// Attest refreshes a member's attestation freshness state. The caller
// supplies (env, jobID) — the pool calls env.VerifyEvidence to drive
// the chain-validation, checks the resulting report's Vendor against
// KnownIssuers, and pins the LastIssuedAt to the pool clock on
// success. Failure leaves the prior state untouched (no half-update).
//
// Attest is called by the operator's control-plane out-of-band of any
// Sign call: a control-plane tick captures fresh quotes from each
// combiner and submits them. The Combine path is then a freshness
// READ — it never blocks on a network roundtrip to KDS / PCS / NRAS.
func (p *CombinerPool) Attest(ctx context.Context, memberName string, env *Envelope) error {
if env == nil {
return ErrAttestationRequired
}
p.mu.RLock()
var target *PoolMember
for _, m := range p.members {
if m.Name == memberName {
target = m
break
}
}
knownIssuers := make(map[string]struct{}, len(p.issuers))
for k := range p.issuers {
knownIssuers[k] = struct{}{}
}
now := p.now()
p.mu.RUnlock()
if target == nil {
return fmt.Errorf("slhdsa-tee: CombinerPool.Attest: unknown member %q", memberName)
}
// Drive the chain validation. The Envelope already carries the
// caller-supplied VerifyOpts (KDSGetter for offline tests,
// expectedReportData for nonce binding, etc.).
reports, err := env.VerifyEvidence(ctx)
if err != nil {
return fmt.Errorf("slhdsa-tee: CombinerPool.Attest: VerifyEvidence: %w", err)
}
if len(reports) != 1 {
return fmt.Errorf("slhdsa-tee: CombinerPool.Attest: expected exactly one verified report, got %d", len(reports))
}
rep := reports[0]
if _, ok := knownIssuers[rep.Vendor]; !ok {
return fmt.Errorf("slhdsa-tee: CombinerPool.Attest: vendor %q not in KnownIssuers", rep.Vendor)
}
target.mu.Lock()
target.LastReport = rep
target.LastIssuedAt = now
target.mu.Unlock()
return nil
}
// Combine drives the t-of-n attested-combiner Sign and returns the
// agreed wire bytes + per-member audit signatures.
//
// Flow:
//
// 1. Snapshot the member list under RLock; pick the FIRST Threshold
// members whose LastIssuedAt + RotationWindow > now. If fewer than
// Threshold members pass the freshness gate, refuse with
// ErrMagnetarStaleAttestation.
// 2. For each selected member, drive Signer.Sign with a fresh
// Envelope holding the most recent attestation evidence (NOT the
// pool's cached LastReport — Sign requires fresh evidence with
// the gate-issued nonce; the pool's freshness gate is the
// additional discipline beyond the per-call attestation).
// 3. Compare the wire bytes byte-for-byte. If all Threshold members
// produced the same bytes, surface that signature with the audit
// trail. If they disagree, refuse with
// ErrMagnetarSignatureDivergence — divergence indicates a
// misconfigured or compromised combiner; do NOT silently pick.
//
// Each member's Signer call drives the FULL slhdsa-tee.Sign machinery
// (approval, release gate, HSM, magnetar.Sign, audit signature). The
// pool's role is the freshness + t-of-n agreement layer ON TOP of
// each member's per-call discipline.
func (p *CombinerPool) Combine(
ctx context.Context,
envs map[string]*Envelope,
jobID [32]byte,
msg []byte,
signCtx []byte,
) ([]byte, []*SignReceipt, error) {
if len(envs) == 0 {
return nil, nil, ErrAttestationRequired
}
if len(msg) == 0 {
return nil, nil, errors.New("slhdsa-tee: CombinerPool.Combine: empty message")
}
// Snapshot members under RLock so concurrent Attest() doesn't
// flip the freshness state mid-iteration.
p.mu.RLock()
threshold := p.threshold
rotation := p.rotation
now := p.now()
members := make([]*PoolMember, len(p.members))
copy(members, p.members)
p.mu.RUnlock()
if len(members) < threshold {
return nil, nil, fmt.Errorf("%w: %d members registered, %d required",
ErrMagnetarInsufficientQuorum, len(members), threshold)
}
// Stable iteration order: by name. Tests + audit logs benefit
// from deterministic member-selection order.
sort.Slice(members, func(i, j int) bool { return members[i].Name < members[j].Name })
// Freshness gate: select the first Threshold members whose
// LastIssuedAt + RotationWindow > now AND whose envelope is
// present in envs. Any selected member outside the window is a
// hard refusal — no silent fallback to a stale member.
type selected struct {
member *PoolMember
env *Envelope
}
var sel []selected
var staleSeen []string
var missingEvidence []string
for _, m := range members {
env, ok := envs[m.Name]
if !ok {
missingEvidence = append(missingEvidence, m.Name)
continue
}
m.mu.RLock()
last := m.LastIssuedAt
m.mu.RUnlock()
if last.IsZero() || now.Sub(last) > rotation {
staleSeen = append(staleSeen, m.Name)
continue
}
sel = append(sel, selected{member: m, env: env})
if len(sel) >= threshold {
break
}
}
if len(sel) < threshold {
// Surface the most actionable refusal:
// - if ANY member was stale → ErrMagnetarStaleAttestation
// - else → ErrMagnetarInsufficientQuorum
if len(staleSeen) > 0 {
return nil, nil, fmt.Errorf("%w: members %v outside rotation window %s (have %d fresh, need %d)",
ErrMagnetarStaleAttestation, staleSeen, rotation, len(sel), threshold)
}
return nil, nil, fmt.Errorf("%w: %d fresh members with evidence, %d required (missing: %v)",
ErrMagnetarInsufficientQuorum, len(sel), threshold, missingEvidence)
}
// Drive each selected member's Sign in series. Parallel would be
// possible but the audit trail benefits from deterministic
// member-ordering. (CCF / Hyperledger / Cardano governance
// patterns all prefer ordered execution under quorum semantics.)
type signOutput struct {
wire []byte
receipt *SignReceipt
err error
name string
}
out := make([]signOutput, 0, len(sel))
for _, s := range sel {
wire, receipt, err := s.member.Signer.Sign(ctx, s.env, jobID, msg, signCtx)
out = append(out, signOutput{wire: wire, receipt: receipt, err: err, name: s.member.Name})
}
// Any per-member failure is a hard refusal at the pool level —
// the strict-PQ profile cannot silently drop a member from
// quorum mid-flight.
for _, o := range out {
if o.err != nil {
return nil, nil, fmt.Errorf("slhdsa-tee: CombinerPool.Combine: member %q Sign: %w", o.name, o.err)
}
}
// Byte-equality across the quorum. SLH-DSA SignDeterministic is
// byte-stable — same (seed, msg, ctx) tuple → identical bytes.
// Two attested combiners holding the SAME wrapped seed under
// the SAME RIM MUST produce identical output. Divergence is a
// hard refusal.
canonical := out[0].wire
receipts := make([]*SignReceipt, 0, len(out))
receipts = append(receipts, out[0].receipt)
for _, o := range out[1:] {
if subtle.ConstantTimeCompare(canonical, o.wire) != 1 {
return nil, nil, fmt.Errorf("%w: %q vs %q", ErrMagnetarSignatureDivergence, out[0].name, o.name)
}
receipts = append(receipts, o.receipt)
}
return canonical, receipts, nil
}
// Threshold reports the configured t-of-n quorum count. Exposed for
// the dispatcher's documentation surface — embedders that publish
// the pool's policy to audit logs.
func (p *CombinerPool) Threshold() int { return p.threshold }
// RotationWindow reports the configured rotation window. Same
// rationale as Threshold.
func (p *CombinerPool) RotationWindow() time.Duration { return p.rotation }
// MemberCount reports the number of registered members (regardless
// of attestation freshness).
func (p *CombinerPool) MemberCount() int {
p.mu.RLock()
defer p.mu.RUnlock()
return len(p.members)
}
// FreshMemberCount reports the number of members currently inside
// the rotation window. Useful for control-plane health checks before
// any sign call.
func (p *CombinerPool) FreshMemberCount() int {
p.mu.RLock()
defer p.mu.RUnlock()
now := p.now()
n := 0
for _, m := range p.members {
m.mu.RLock()
last := m.LastIssuedAt
m.mu.RUnlock()
if !last.IsZero() && now.Sub(last) <= p.rotation {
n++
}
}
return n
}
-378
View File
@@ -1,378 +0,0 @@
// SPDX-License-Identifier: BSD-3-Clause
package slhdsatee
import (
"context"
"crypto/rand"
"errors"
"testing"
"time"
magnetar "github.com/luxfi/magnetar/ref/go/pkg/magnetar"
"github.com/luxfi/mpc/cc/attest"
"github.com/luxfi/mpc/pkg/kms"
)
// pool_test.go pins the CombinerPool invariants independent of the
// magnetar dispatcher. These tests exercise the freshness gate,
// quorum selection, and divergence-refusal at the slhdsa-tee package
// level so future consumers (mldsa-tee pool, rlwe-tee pool — same
// shape) can mirror the test discipline.
// TestCombinerPool_Constructor_Defaults pins the constructor's
// default-deny posture: Threshold<2, RotationWindow=0, empty
// KnownIssuers all refuse.
func TestCombinerPool_Constructor_Defaults(t *testing.T) {
cases := []struct {
name string
cfg CombinerPoolConfig
}{
{
name: "threshold-1",
cfg: CombinerPoolConfig{
Threshold: 1, RotationWindow: time.Second,
KnownIssuers: map[string]struct{}{"amd.sev.snp": {}},
},
},
{
name: "rotation-zero",
cfg: CombinerPoolConfig{
Threshold: 2, RotationWindow: 0,
KnownIssuers: map[string]struct{}{"amd.sev.snp": {}},
},
},
{
name: "no-issuers",
cfg: CombinerPoolConfig{
Threshold: 2, RotationWindow: time.Second,
KnownIssuers: nil,
},
},
}
for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
if _, err := NewCombinerPool(c.cfg); err == nil {
t.Fatalf("NewCombinerPool(%s): expected error, got nil", c.name)
}
})
}
}
// TestCombinerPool_AddMember pins per-member registration semantics:
// nil Signer refused, empty name refused, duplicate name refused.
func TestCombinerPool_AddMember(t *testing.T) {
pool, err := NewCombinerPool(CombinerPoolConfig{
Threshold: 2,
RotationWindow: time.Minute,
KnownIssuers: map[string]struct{}{"amd.sev.snp": {}},
})
if err != nil {
t.Fatalf("NewCombinerPool: %v", err)
}
// nil signer
if _, err := pool.AddMember("a", nil); err == nil {
t.Fatal("AddMember nil signer: expected error")
}
// empty name
if _, err := pool.AddMember("", &Signer{}); err == nil {
t.Fatal("AddMember empty name: expected error")
}
// duplicate
if _, err := pool.AddMember("a", &Signer{}); err != nil {
t.Fatalf("AddMember a: %v", err)
}
if _, err := pool.AddMember("a", &Signer{}); err == nil {
t.Fatal("AddMember dup a: expected error")
}
if pool.MemberCount() != 1 {
t.Fatalf("MemberCount = %d, want 1", pool.MemberCount())
}
if pool.Threshold() != 2 {
t.Fatalf("Threshold = %d, want 2", pool.Threshold())
}
if pool.RotationWindow() != time.Minute {
t.Fatalf("RotationWindow = %s, want 1m", pool.RotationWindow())
}
}
// poolMakeSigner builds a Signer over the committed Milan SEV-SNP
// fixture sharing the SAME wrapped seed across members.
func poolMakeSigner(t *testing.T, seed []byte) *Signer {
t.Helper()
rim := makeRIM(t)
hw := makeHardware(t)
gate := poolMakeGate(t, rim, hw)
hsmP := newTestFileHSM(t)
if err := hsmP.StoreKey(context.Background(), "master-seed", seed); err != nil {
t.Fatalf("StoreKey master-seed: %v", err)
}
cfg := Config{
Mode: magnetar.ModeM192s,
RequiredRIM: map[[32]byte]struct{}{rim: {}},
AllowedHardware: map[[32]byte]struct{}{hw: {}},
RequireSEVSNP: true,
KMSKeyID: "audit-key",
WrappedSeedKeyID: "master-seed",
ApprovalRequired: false,
}
signer, err := New(gate, hsmP, newTestApprovalProvider(t), cfg)
if err != nil {
t.Fatalf("slhdsatee.New: %v", err)
}
return signer
}
func poolMakeGate(t *testing.T, rim, hw [32]byte) *kms.LocalReleaseGate {
t.Helper()
policy := kms.NewReleasePolicy([][32]byte{rim}, [][32]byte{hw})
policy.RequireSEVSNP = true
var rootKey [32]byte
if _, err := rand.Read(rootKey[:]); err != nil {
t.Fatalf("rootKey: %v", err)
}
gate, err := kms.NewLocalReleaseGate(policy, kms.NewMemoryNonceStore(), rootKey)
if err != nil {
t.Fatalf("NewLocalReleaseGate: %v", err)
}
gate.SetIssueTTL(5 * time.Second)
gate.SetReplayWindow(5 * time.Second)
return gate
}
// poolMakeSharedSeed returns deterministic SeedSize bytes parameterised
// by label so we can construct multiple Signers with the same seed
// (byte-equality across pool members requires identical seeds) or
// different seeds (divergence path).
func poolMakeSharedSeed(label byte) []byte {
params := magnetar.MustParamsFor(magnetar.ModeM192s)
seed := make([]byte, params.SeedSize)
for i := range seed {
seed[i] = label ^ byte(i)
}
return seed
}
// poolMakeEnvelope builds an Envelope binding the committed Milan
// fixture + KDS replay + fixed-clock options, suitable for both
// pool.Attest and per-call sign.
func poolMakeEnvelope(t *testing.T, rim, hw, teePub [32]byte) *Envelope {
t.Helper()
return &Envelope{
Kind: attest.KindSEVSNP,
EvidenceBytes: append([]byte(nil), sevSnpAttestationMilan...),
RIM: rim,
Hardware: hw,
TEEPub: teePub,
VerifyOpts: []attest.Option{
attest.WithKDSGetter(newKDSReplay()),
attest.WithNow(fixedNow()),
},
}
}
// TestCombinerPool_Attest_VendorPin pins the vendor allowlist: a
// chain-validated report whose Vendor is not in KnownIssuers refuses.
func TestCombinerPool_Attest_VendorPin(t *testing.T) {
seed := poolMakeSharedSeed(0xF0)
signer := poolMakeSigner(t, seed)
pool, err := NewCombinerPool(CombinerPoolConfig{
Threshold: 2,
RotationWindow: time.Minute,
KnownIssuers: map[string]struct{}{
"never-issuer": {}, // SEV-SNP path will not match
},
Now: fixedNow,
})
if err != nil {
t.Fatalf("NewCombinerPool: %v", err)
}
if _, err := pool.AddMember("a", signer); err != nil {
t.Fatalf("AddMember a: %v", err)
}
rim := makeRIM(t)
hw := makeHardware(t)
teePub := makeTEEPub(t)
env := poolMakeEnvelope(t, rim, hw, teePub)
err = pool.Attest(context.Background(), "a", env)
if err == nil {
t.Fatal("Attest with wrong issuer: expected error, got nil")
}
}
// TestCombinerPool_Combine_FreshnessGate pins the rotation-window
// gate: members never attested are stale; combine refuses.
func TestCombinerPool_Combine_FreshnessGate(t *testing.T) {
seed := poolMakeSharedSeed(0xF1)
signer1 := poolMakeSigner(t, seed)
signer2 := poolMakeSigner(t, seed)
pool, err := NewCombinerPool(CombinerPoolConfig{
Threshold: 2,
RotationWindow: 30 * time.Second,
KnownIssuers: map[string]struct{}{"amd.sev.snp": {}},
Now: fixedNow,
})
if err != nil {
t.Fatalf("NewCombinerPool: %v", err)
}
if _, err := pool.AddMember("a", signer1); err != nil {
t.Fatalf("AddMember a: %v", err)
}
if _, err := pool.AddMember("b", signer2); err != nil {
t.Fatalf("AddMember b: %v", err)
}
rim := makeRIM(t)
hw := makeHardware(t)
teePub := makeTEEPub(t)
if pool.FreshMemberCount() != 0 {
t.Fatalf("FreshMemberCount pre-attest = %d, want 0", pool.FreshMemberCount())
}
envA := poolMakeEnvelope(t, rim, hw, teePub)
envB := poolMakeEnvelope(t, rim, hw, teePub)
// Try Combine without any attestation — pool refuses.
var jobID [32]byte
if _, err := rand.Read(jobID[:]); err != nil {
t.Fatalf("jobID: %v", err)
}
_, _, err = pool.Combine(context.Background(),
map[string]*Envelope{"a": envA, "b": envB},
jobID, []byte("freshness"), nil)
if err == nil {
t.Fatal("Combine with no attestation: expected error, got nil")
}
if !errors.Is(err, ErrMagnetarStaleAttestation) {
t.Fatalf("Combine pre-attest: expected ErrMagnetarStaleAttestation, got %v", err)
}
}
// TestCombinerPool_Combine_ByteEqualityAcrossQuorum drives a 2-of-2
// quorum end-to-end and asserts byte-equality across the agreed
// signature. Independent of dispatcher.
func TestCombinerPool_Combine_ByteEqualityAcrossQuorum(t *testing.T) {
seed := poolMakeSharedSeed(0xF2)
signer1 := poolMakeSigner(t, seed)
signer2 := poolMakeSigner(t, seed)
pool, err := NewCombinerPool(CombinerPoolConfig{
Threshold: 2,
RotationWindow: time.Minute,
KnownIssuers: map[string]struct{}{"amd.sev.snp": {}},
Now: fixedNow,
})
if err != nil {
t.Fatalf("NewCombinerPool: %v", err)
}
if _, err := pool.AddMember("a", signer1); err != nil {
t.Fatalf("AddMember a: %v", err)
}
if _, err := pool.AddMember("b", signer2); err != nil {
t.Fatalf("AddMember b: %v", err)
}
rim := makeRIM(t)
hw := makeHardware(t)
teePub := makeTEEPub(t)
if err := pool.Attest(context.Background(), "a", poolMakeEnvelope(t, rim, hw, teePub)); err != nil {
t.Fatalf("Attest a: %v", err)
}
if err := pool.Attest(context.Background(), "b", poolMakeEnvelope(t, rim, hw, teePub)); err != nil {
t.Fatalf("Attest b: %v", err)
}
if pool.FreshMemberCount() != 2 {
t.Fatalf("FreshMemberCount post-attest = %d, want 2", pool.FreshMemberCount())
}
envA := poolMakeEnvelope(t, rim, hw, teePub)
envB := poolMakeEnvelope(t, rim, hw, teePub)
var jobID [32]byte
if _, err := rand.Read(jobID[:]); err != nil {
t.Fatalf("jobID: %v", err)
}
wire, receipts, err := pool.Combine(context.Background(),
map[string]*Envelope{"a": envA, "b": envB},
jobID, []byte("byte-equality"), nil)
if err != nil {
t.Fatalf("Combine: %v", err)
}
if len(wire) == 0 {
t.Fatal("Combine returned empty wire")
}
if len(receipts) != 2 {
t.Fatalf("Combine receipts = %d, want 2", len(receipts))
}
for i, r := range receipts {
if r == nil {
t.Fatalf("receipt[%d] is nil", i)
}
if len(r.AuditSignature) == 0 {
t.Fatalf("receipt[%d] audit is empty", i)
}
}
}
// TestCombinerPool_Combine_Divergence pins the byte-equality discipline:
// two signers holding DIFFERENT seeds produce different bytes; pool
// refuses with ErrMagnetarSignatureDivergence.
func TestCombinerPool_Combine_Divergence(t *testing.T) {
seedA := poolMakeSharedSeed(0xA1)
seedB := poolMakeSharedSeed(0xB2) // different seed
signerA := poolMakeSigner(t, seedA)
signerB := poolMakeSigner(t, seedB)
pool, err := NewCombinerPool(CombinerPoolConfig{
Threshold: 2,
RotationWindow: time.Minute,
KnownIssuers: map[string]struct{}{"amd.sev.snp": {}},
Now: fixedNow,
})
if err != nil {
t.Fatalf("NewCombinerPool: %v", err)
}
if _, err := pool.AddMember("a", signerA); err != nil {
t.Fatalf("AddMember a: %v", err)
}
if _, err := pool.AddMember("b", signerB); err != nil {
t.Fatalf("AddMember b: %v", err)
}
rim := makeRIM(t)
hw := makeHardware(t)
teePub := makeTEEPub(t)
if err := pool.Attest(context.Background(), "a", poolMakeEnvelope(t, rim, hw, teePub)); err != nil {
t.Fatalf("Attest a: %v", err)
}
if err := pool.Attest(context.Background(), "b", poolMakeEnvelope(t, rim, hw, teePub)); err != nil {
t.Fatalf("Attest b: %v", err)
}
envA := poolMakeEnvelope(t, rim, hw, teePub)
envB := poolMakeEnvelope(t, rim, hw, teePub)
var jobID [32]byte
if _, err := rand.Read(jobID[:]); err != nil {
t.Fatalf("jobID: %v", err)
}
_, _, err = pool.Combine(context.Background(),
map[string]*Envelope{"a": envA, "b": envB},
jobID, []byte("divergence-bytes"), nil)
if !errors.Is(err, ErrMagnetarSignatureDivergence) {
t.Fatalf("Combine divergence: expected ErrMagnetarSignatureDivergence, got %v", err)
}
}
-75
View File
@@ -1,75 +0,0 @@
// SPDX-License-Identifier: BSD-3-Clause
package slhdsatee
import "errors"
// ChainSecurityProfile is the value the consensus / chain layer
// publishes to express the residency posture it is willing to tolerate
// for SLH-DSA Combine.
//
// Decomplecting: profile is a value, not a place. The chain layer
// constructs it from its ChainConfig (e.g. the precompile
// StrictPQReporter the EVM precompiles use today) and hands it down
// at Sign call time. No global, no init() hook, no env var. The
// dispatcher reads it the same way the precompile contract.RefuseUnderStrictPQ
// helper reads its StrictPQReporter: ONE function, ONE place, ONE
// canonical refusal sentinel.
type ChainSecurityProfile int
const (
// ProfileLegacyCompat tolerates the strict-atom commodity-host
// Combine path. The FIPS 205 master bytes transiently exist in
// the public combiner's SHAKE-expansion buffers for a few
// microseconds. Acceptable for permissionless / community
// validation paths where no host is in the TCB; refused for
// strict-PQ deployments. This is the default.
ProfileLegacyCompat ChainSecurityProfile = 0
// ProfileStrictPQ requires every Combine to route through a
// TEE-attested combiner pool. The master bytes only ever exist
// inside a measured enclave (SEV-SNP / TDX / NRAS attested
// host) whose binary / firmware match the operator-asserted RIM
// and chain-validate to the vendor root. No commodity-host
// fallback; refusal is hard.
ProfileStrictPQ ChainSecurityProfile = 1
)
// String reports the canonical wire label for the profile.
func (p ChainSecurityProfile) String() string {
switch p {
case ProfileStrictPQ:
return "strict-PQ"
case ProfileLegacyCompat:
return "legacy-compat"
default:
return "unknown"
}
}
// ErrMagnetarNoTEEAttestation is the canonical refusal returned when
// a strict-PQ chain profile attempts to Combine without a valid TEE
// quote. The wire-stable identifier is "ERR_MAGNETAR_NO_TEE_ATTESTATION".
//
// Callers SHOULD switch on errors.Is(err, ErrMagnetarNoTEEAttestation)
// to distinguish profile-gate refusal from upstream attestation /
// release-gate errors.
var ErrMagnetarNoTEEAttestation = errors.New("ERR_MAGNETAR_NO_TEE_ATTESTATION: strict-PQ chain profile requires TEE-attested combiner")
// ErrMagnetarStaleAttestation is returned when an attested combiner's
// last successful re-attestation lies outside the configured rotation
// window. The bytes of the attestation chain-validate but the freshness
// guarantee has lapsed — operators MUST re-attest before the next sign.
var ErrMagnetarStaleAttestation = errors.New("ERR_MAGNETAR_STALE_ATTESTATION: combiner attestation outside rotation window")
// ErrMagnetarInsufficientQuorum is returned when fewer than the
// configured threshold-of-attested-combiners agreed on byte-identical
// output. The pool refuses to surface a signature whose origin is
// less than t attested combiners.
var ErrMagnetarInsufficientQuorum = errors.New("ERR_MAGNETAR_INSUFFICIENT_QUORUM: fewer than threshold attested combiners produced matching signature")
// ErrMagnetarSignatureDivergence is returned when the configured
// quorum DID produce signatures but they were not byte-identical.
// Hard refusal — divergence indicates a misconfigured or compromised
// combiner; the pool MUST NOT silently pick a winner.
var ErrMagnetarSignatureDivergence = errors.New("ERR_MAGNETAR_SIGNATURE_DIVERGENCE: attested combiners produced non-matching signature bytes")
-261
View File
@@ -1,261 +0,0 @@
// SPDX-License-Identifier: BSD-3-Clause
package slhdsatee
import (
"context"
"crypto/rand"
"crypto/sha256"
"fmt"
magnetar "github.com/luxfi/magnetar/ref/go/pkg/magnetar"
"github.com/luxfi/mpc/pkg/approval"
)
// Sign produces a FIPS 205 SLH-DSA signature on msg, gated by the
// supplied attestation Envelope.
//
// Flow (decomplected — each step is independent and complete):
//
// 1. approval.ApproveIntent — out-of-band human/programmatic gate.
// 2. gate.Issue — fresh nonce + epoch from KMS.
// 3. env.Verify(nonce) — operator bound the gate-nonce into
// REPORT_DATA at TEE capture.
// 4. env.VerifyEvidence(ctx) — cc/attest chain validates the report
// against vendor root (AMD KDS / TDX PCS / NRAS JWKS).
// 5. gate.Release — gate.RIM ∋ env.RIM, gate.Hardware ∋ env.Hardware,
// Require* issuers present, seal a session key under env.TEEPub.
// 6. Inside the TEE: unwrap sealed session key, decrypt the wrapped
// master seed (this package's surface treats hsm.Provider.GetKey
// as already-yielding-plaintext-inside-attested-TEE; production
// deployments perform AEAD-unwrap with sealed key here).
// 7. magnetar.KeyFromSeed(seed) → magnetar.Sign(msg, ctx, det=true).
// 8. Zeroize seed + sk.Bytes immediately. Return sig + sealed key
// metadata so callers can audit (epoch, jobID, ephemeral pub).
//
// Output is the MAGS-framed wire bytes (via Signature.MarshalBinary)
// — byte-identical to single-party FIPS 205 SignDeterministic on
// the same (seed-derived sk, msg). Any verifier with the matching
// magnetar.PublicKey (or its MAGG wire bytes) validates with
// magnetar.Verify / VerifyBytes.
//
// jobID is the audit-binding identifier — opaque to the gate, bound
// into AAD so cross-job replay of the sealed key is refused.
// Production callers derive jobID = sha256(domain || workload || msg)
// or any other collision-free convention. Tests use fresh random.
//
// signCtx is the FIPS 205 §10.2 context string. Pass nil for empty.
//
// Sign is safe for concurrent calls against the same Signer. Each
// call generates an independent jobID-keyed nonce and seals to a
// fresh ephemeral key.
func (s *Signer) Sign(ctx context.Context, env *Envelope, jobID [32]byte, msg []byte, signCtx []byte) ([]byte, *SignReceipt, error) {
if env == nil {
return nil, nil, ErrAttestationRequired
}
if len(msg) == 0 {
return nil, nil, fmt.Errorf("slhdsa-tee: empty message")
}
sealed, err := s.auditedRelease(ctx, env, jobID, msg)
if err != nil {
return nil, nil, err
}
// Pull the wrapped master seed from the HSM. Production
// deployments AEAD-unwrap with the sealed session-key here; for
// the institutional-custody surface where the HSM itself enforces
// at-rest confidentiality (AWS KMS, Azure Key Vault, GCP Cloud
// KMS, Zymbit, YubiHSM, KMS secret manager), GetKey returns the
// plaintext seed inside the attested TEE context.
seed, err := s.hsmP.GetKey(ctx, s.cfg.WrappedSeedKeyID)
if err != nil {
return nil, nil, fmt.Errorf("%w: HSM GetKey: %v", ErrHSMUnreachable, err)
}
defer zeroize(seed)
if len(seed) != s.params.SeedSize {
return nil, nil, fmt.Errorf("%w: HSM-stored seed length %d does not match SeedSize %d (mode=%s)",
ErrCorruptWrappedSeed, len(seed), s.params.SeedSize, s.params.Mode)
}
sk, err := magnetar.KeyFromSeed(s.params, seed)
if err != nil {
return nil, nil, fmt.Errorf("%w: KeyFromSeed: %v", ErrCorruptWrappedSeed, err)
}
defer zeroize(sk.Bytes)
defer zeroize(sk.Seed)
sig, err := magnetar.Sign(s.params, sk, msg, signCtx, false /*deterministic*/, nil)
if err != nil {
return nil, nil, fmt.Errorf("slhdsa-tee: magnetar.Sign: %w", err)
}
// Self-verify safety belt — refuses to return bytes that would
// fail at the caller. A failure here signals a kernel bug, not a
// caller bug. Matches the dispatcher's discipline.
if err := magnetar.VerifyCtx(s.params, sk.Pub, msg, signCtx, sig); err != nil {
return nil, nil, fmt.Errorf("slhdsa-tee: self-verify failed (kernel bug): %w", err)
}
wire, err := sig.MarshalBinary()
if err != nil {
return nil, nil, fmt.Errorf("slhdsa-tee: sig.MarshalBinary: %w", err)
}
// Optionally write an HSM-backed audit signature over
// (jobID, msgDigest, epoch, RIM) — institutional custody
// auditors replay this against the HSM's KMS audit log to prove
// the release happened. The signature does not feed back into
// the FIPS 205 wire bytes; it lives only in the SignReceipt.
audit, err := s.auditSignature(ctx, jobID, msg, sealed.Epoch, env.RIM)
if err != nil {
return nil, nil, fmt.Errorf("slhdsa-tee: audit signature: %w", err)
}
recv := &SignReceipt{
JobID: jobID,
Epoch: sealed.Epoch,
IssuedNonce: sealed.IssuedNonce,
EphemeralPub: sealed.EphemeralPub,
EvidenceKind: string(env.Kind),
EvidenceIssuer: evidenceIssuerString(env),
AuditSignature: audit,
}
return wire, recv, nil
}
// SignReceipt is the audit blob returned alongside the FIPS 205
// signature. Embedders log this to the KMS audit channel; nothing
// in it is cryptographically required for verification — Verify
// only needs (gpkBytes, msg, sigBytes).
type SignReceipt struct {
JobID [32]byte
Epoch uint64
IssuedNonce [32]byte
EphemeralPub [32]byte
EvidenceKind string
EvidenceIssuer string
AuditSignature []byte
}
// auditSignature emits an HSM-backed signature over the canonical
// (jobID || msgDigest || epoch || rim) tuple via the configured HSM
// provider. The HSM's Sign API is provider-native: AWS KMS uses
// ECDSA-P256, Azure Key Vault uses Ed25519 / ECDSA, the file
// provider uses Ed25519. The signature is opaque-bytes from this
// package's perspective — auditors who trust the HSM root validate
// against the provider's pubkey.
func (s *Signer) auditSignature(ctx context.Context, jobID [32]byte, msg []byte, epoch uint64, rim [32]byte) ([]byte, error) {
h := sha256.New()
h.Write([]byte("LUX-SLHDSA-TEE-AUDIT-V1"))
h.Write([]byte{0x00})
h.Write(jobID[:])
h.Write(epochBytes(epoch))
h.Write(rim[:])
d := sha256.Sum256(msg)
h.Write(d[:])
auditDigest := h.Sum(nil)
return s.hsmP.Sign(ctx, s.cfg.KMSKeyID, auditDigest)
}
// epochBytes encodes epoch as 8 bytes big-endian.
func epochBytes(e uint64) []byte {
return []byte{
byte(e >> 56), byte(e >> 48), byte(e >> 40), byte(e >> 32),
byte(e >> 24), byte(e >> 16), byte(e >> 8), byte(e),
}
}
// evidenceIssuerString returns the canonical wire issuer for env.
func evidenceIssuerString(env *Envelope) string {
switch is := env.EvidenceIssuers(); len(is) {
case 0:
return ""
default:
return is[0]
}
}
// signIntent satisfies approval.CanonicalIntent for the (jobID, msg,
// envelope-summary) tuple. The approver signs the digest of this
// intent; verification at gate-time re-derives the digest from the
// same inputs and refuses if the approver's signature does not match.
type signIntent struct {
jobID [32]byte
msg []byte
env envelopeSummary
}
// envelopeSummary captures the fields of Envelope that participate
// in the approval digest. RIM and Hardware are operator-asserted and
// thus auditable to the approver; the evidence bytes themselves are
// re-captured per Sign and excluded from the intent digest.
type envelopeSummary struct {
Kind string
RIM [32]byte
Hardware [32]byte
TEEPub [32]byte
}
func newSignIntent(jobID [32]byte, msg []byte, env *Envelope) *signIntent {
return &signIntent{
jobID: jobID,
msg: append([]byte(nil), msg...),
env: envelopeSummary{
Kind: string(env.Kind),
RIM: env.RIM,
Hardware: env.Hardware,
TEEPub: env.TEEPub,
},
}
}
// Digest implements approval.CanonicalIntent.
func (si *signIntent) Digest() [32]byte {
h := sha256.New()
h.Write([]byte("LUX-SLHDSA-TEE-INTENT-V1"))
h.Write([]byte{0x00})
h.Write(si.jobID[:])
mdigest := sha256.Sum256(si.msg)
h.Write(mdigest[:])
h.Write([]byte(si.env.Kind))
h.Write([]byte{0x00})
h.Write(si.env.RIM[:])
h.Write(si.env.Hardware[:])
h.Write(si.env.TEEPub[:])
var out [32]byte
copy(out[:], h.Sum(nil))
return out
}
// Bytes implements approval.CanonicalIntent. Returns a deterministic
// canonical encoding (no JSON, no length-prefix ambiguity): the same
// fields the Digest hash absorbs, in the same order.
func (si *signIntent) Bytes() []byte {
out := make([]byte, 0, 32+32+len(si.env.Kind)+1+32+32+32+32)
out = append(out, []byte("LUX-SLHDSA-TEE-INTENT-V1")...)
out = append(out, 0x00)
out = append(out, si.jobID[:]...)
mdigest := sha256.Sum256(si.msg)
out = append(out, mdigest[:]...)
out = append(out, []byte(si.env.Kind)...)
out = append(out, 0x00)
out = append(out, si.env.RIM[:]...)
out = append(out, si.env.Hardware[:]...)
out = append(out, si.env.TEEPub[:]...)
return out
}
var _ approval.CanonicalIntent = (*signIntent)(nil)
// FreshJobID returns 32 bytes of crypto/rand. Convenience helper for
// callers that derive jobIDs at random rather than from a domain
// salt; production deployments may prefer a domain-bound jobID.
func FreshJobID() ([32]byte, error) {
var out [32]byte
if _, err := rand.Read(out[:]); err != nil {
return out, err
}
return out, nil
}
-256
View File
@@ -1,256 +0,0 @@
// SPDX-License-Identifier: BSD-3-Clause
package slhdsatee
import (
"context"
"crypto/rand"
"errors"
"fmt"
"sync"
"time"
magnetar "github.com/luxfi/magnetar/ref/go/pkg/magnetar"
"github.com/luxfi/mpc/pkg/approval"
"github.com/luxfi/mpc/pkg/hsm"
"github.com/luxfi/mpc/pkg/kms"
)
// Signer is the institutional-custody SLH-DSA signer.
//
// Composition (not inheritance):
//
// - gate : kms.ReleaseGate — the trust root.
// - hsmP : hsm.Provider — wraps the master seed at rest.
// - appr : approval.ApprovalProvider — out-of-band human/programmatic gate.
// - cfg : Config — policy: RIM, hardware, mode, key IDs.
//
// Each is independently complete and replaceable. No subclassing, no
// hidden state — the four fields name the four responsibilities, and
// every Sign call drives them in a fixed order.
//
// The Signer instance can be reused across many Sign calls. It is
// safe for concurrent use; per-call state (sealed key, plaintext
// seed) is stack-local and zeroized on return.
type Signer struct {
gate kms.ReleaseGate
hsmP hsm.Provider
appr approval.ApprovalProvider
cfg Config
params *magnetar.Params
// mu protects nothing today — both gate and hsmP are themselves
// thread-safe — but reserved for future per-Signer rate-limit /
// counter state without changing the public API.
mu sync.Mutex
}
// New builds a Signer from the supplied dependencies.
//
// All four parameters are required. nil hsmP or nil gate or nil
// approval (when ApprovalRequired) is a construction error — there is
// no "best effort" fallback path.
func New(gate kms.ReleaseGate, hsmP hsm.Provider, appr approval.ApprovalProvider, cfg Config) (*Signer, error) {
if gate == nil {
return nil, errors.New("slhdsa-tee: nil release gate")
}
if hsmP == nil {
return nil, errors.New("slhdsa-tee: nil HSM provider")
}
if cfg.ApprovalRequired && appr == nil {
return nil, errors.New("slhdsa-tee: nil approval provider but ApprovalRequired is true")
}
if err := cfg.Validate(); err != nil {
return nil, err
}
params, err := magnetar.ParamsFor(cfg.Mode)
if err != nil {
return nil, fmt.Errorf("slhdsa-tee: ParamsFor: %w", err)
}
return &Signer{
gate: gate,
hsmP: hsmP,
appr: appr,
cfg: cfg,
params: params,
}, nil
}
// Provision wraps a fresh master seed under the HSM provider for
// later release-gated signing. Called once at operator bootstrap (or
// at scheduled rotation). The seed bytes are zeroized after storage.
//
// In a real institutional deployment, Provision runs inside the
// attested TEE itself — the operator captures attestation, calls
// gate.Issue, gate.Release, derives the wrapping key from the sealed
// session key, AEAD-wraps the seed, and StoreKey the ciphertext. The
// HSM holds wrapped bytes; the TEE holds plaintext only ephemerally.
//
// For this package's surface, Provision is purposefully simple: it
// generates a fresh seed of params.SeedSize bytes and stores it via
// hsmP.StoreKey under cfg.WrappedSeedKeyID. Wrapping under the TEE
// session-key is the next composition step the embedder layers on
// top — see ExampleEmbedding in the README.
//
// Production deployments using a KMS-backed HSM (AWS KMS, Azure Key
// Vault, GCP KMS) get at-rest encryption from the cloud HSM itself;
// adding AEAD-wrapping is defense-in-depth, not the trust root.
//
// Returns the magnetar.PublicKey derived from the provisioned seed
// so the embedder can register it as the canonical group public key.
func (s *Signer) Provision(ctx context.Context, rng *seedRNG) (*magnetar.PublicKey, error) {
seed := make([]byte, s.params.SeedSize)
defer zeroize(seed)
if rng == nil {
if _, err := rand.Read(seed); err != nil {
return nil, fmt.Errorf("slhdsa-tee: provision: entropy: %w", err)
}
} else {
if _, err := rng.Read(seed); err != nil {
return nil, fmt.Errorf("slhdsa-tee: provision: deterministic seed: %w", err)
}
}
if err := s.hsmP.StoreKey(ctx, s.cfg.WrappedSeedKeyID, seed); err != nil {
return nil, fmt.Errorf("slhdsa-tee: provision: HSM StoreKey: %w", err)
}
sk, err := magnetar.KeyFromSeed(s.params, seed)
if err != nil {
return nil, fmt.Errorf("slhdsa-tee: provision: KeyFromSeed: %w", err)
}
pub := sk.Public()
// Zeroize the secret key bytes — only the public key escapes.
zeroize(sk.Bytes)
zeroize(sk.Seed)
return pub, nil
}
// PublicKey reads the master seed via the HSM provider and derives
// the magnetar PublicKey deterministically. Used by callers that
// need the wire-form group public key without performing a sign.
//
// The seed is loaded into a stack-local buffer and zeroized on
// return; the derived PrivateKey's seed copy is also zeroized.
//
// PublicKey is RELEASE-GATE FREE — it only reads the at-rest HSM
// material. The TEE-attested release gate is exercised by Sign, not
// by PublicKey. This matches the standard "public material is
// public, private material is gated" separation.
//
// For deployments where even the public key is a custody secret
// (rare; usually pkBytes are published on-chain), do not call this
// method — embed the published group public key independently.
func (s *Signer) PublicKey(ctx context.Context) (*magnetar.PublicKey, error) {
seed, err := s.hsmP.GetKey(ctx, s.cfg.WrappedSeedKeyID)
if err != nil {
return nil, fmt.Errorf("slhdsa-tee: PublicKey: HSM GetKey: %w", err)
}
defer zeroize(seed)
if len(seed) != s.params.SeedSize {
return nil, fmt.Errorf("slhdsa-tee: PublicKey: seed length %d does not match SeedSize %d (mode=%s)",
len(seed), s.params.SeedSize, s.params.Mode)
}
sk, err := magnetar.KeyFromSeed(s.params, seed)
if err != nil {
return nil, fmt.Errorf("slhdsa-tee: PublicKey: KeyFromSeed: %w", err)
}
defer zeroize(sk.Bytes)
defer zeroize(sk.Seed)
return sk.Public(), nil
}
// Mode reports the FIPS 205 parameter set this signer is bound to.
// Embedders use this to label published signatures (M192s / M192f /
// M256s) when the wire form does not already carry the mode.
func (s *Signer) Mode() magnetar.Mode { return s.cfg.Mode }
// Params returns the magnetar Params for this signer's mode. Exposed
// for embedders that need to call magnetar.Verify / VerifyBytes with
// the matching params.
func (s *Signer) Params() *magnetar.Params { return s.params }
// auditedRelease drives the full Issue → approval → composite-envelope
// → Release flow and returns the SealedSessionKey on success. Each
// failure mode wraps a sentinel from config.go so callers can branch
// on errors.Is without parsing strings.
func (s *Signer) auditedRelease(ctx context.Context, env *Envelope, jobID [32]byte, msg []byte) (kms.SealedSessionKey, error) {
if env == nil {
return kms.SealedSessionKey{}, ErrAttestationRequired
}
if s.cfg.ApprovalRequired {
intent := newSignIntent(jobID, msg, env)
sig, err := s.appr.ApproveIntent(ctx, s.cfg.ApproverID, intent)
if err != nil {
return kms.SealedSessionKey{}, fmt.Errorf("%w: %v", ErrApprovalDenied, err)
}
ok, err := s.appr.VerifyApproval(ctx, intent, sig)
if err != nil {
return kms.SealedSessionKey{}, fmt.Errorf("%w: verify: %v", ErrApprovalDenied, err)
}
if !ok {
return kms.SealedSessionKey{}, ErrApprovalDenied
}
}
nonce, epoch, err := s.gate.Issue(jobID)
if err != nil {
return kms.SealedSessionKey{}, fmt.Errorf("%w: gate.Issue: %v", ErrKMSReleaseUnreachable, err)
}
env.ExpectedNonce = nonce
sealed, err := s.gate.Release(kms.ReleaseRequest{
JobID: jobID,
Epoch: epoch,
Nonce: nonce,
Attestation: env,
Ctx: ctx,
})
if err != nil {
// gate.Release wraps kms.ErrPolicyRefused / ErrAttestationChain
// already — surface verbatim so callers using errors.Is can
// catch the kms sentinels directly.
return kms.SealedSessionKey{}, fmt.Errorf("%w: %v", ErrPolicyRefused, err)
}
return sealed, nil
}
// zeroize clears a byte slice in place. The compiler is NOT permitted
// to elide this loop (the package's tests inspect the buffer
// post-call). Stable across Go versions.
func zeroize(b []byte) {
for i := range b {
b[i] = 0
}
}
// seedRNG is a thin alias for the deterministic-RNG path used by
// Provision in tests. Defined here so Provision's signature stays
// stable; the in-test impl lives in signer_test.go.
type seedRNG struct {
src []byte
off int
}
// Read implements io.Reader.
func (r *seedRNG) Read(p []byte) (int, error) {
if r.off >= len(r.src) {
return 0, errEndOfSeedRNG
}
n := copy(p, r.src[r.off:])
r.off += n
return n, nil
}
var errEndOfSeedRNG = errors.New("slhdsa-tee: seedRNG exhausted")
// newTestClock returns a stable clock for tests. Production paths use
// time.Now via gate / hsm internals; this helper exists so test code
// has one obvious source of timestamps when constructing
// ApprovalSignatures or audit records.
func newTestClock() func() time.Time {
return func() time.Time { return time.Unix(1_700_000_000, 0).UTC() }
}
-752
View File
@@ -1,752 +0,0 @@
// SPDX-License-Identifier: BSD-3-Clause
package slhdsatee
import (
"context"
"crypto/rand"
"crypto/sha256"
_ "embed"
"errors"
"os"
"testing"
"time"
sevtest "github.com/google/go-sev-guest/testing"
"github.com/google/go-sev-guest/verify/trust"
magnetar "github.com/luxfi/magnetar/ref/go/pkg/magnetar"
"github.com/luxfi/mpc/cc/attest"
"github.com/luxfi/mpc/pkg/approval"
"github.com/luxfi/mpc/pkg/hsm"
"github.com/luxfi/mpc/pkg/kms"
)
// sevSnpAttestationMilan and sevSnpVcekMilan are the same real AMD
// Milan SEV-SNP attestation + VCEK fixtures committed by the lux/mpc
// cc/attest test corpus. We commit them here byte-equal so this
// package's tests do not require any pkg/attest test-only export.
//
//go:embed testdata/sev_snp_attestation_milan.bin
var sevSnpAttestationMilan []byte
//go:embed testdata/sev_snp_vcek_milan.cer
var sevSnpVcekMilan []byte
// newKDSReplay returns the same SimpleGetter map cc/attest's tests
// use to replay AMD KDS responses offline. Pinned to the Milan
// product + the CHIP_ID + TCB encoded in the committed report.
func newKDSReplay() trust.HTTPSGetter {
return sevtest.SimpleGetter(map[string][]byte{
"https://kdsintf.amd.com/vcek/v1/Milan/cert_chain": trust.AskArkMilanVcekBytes,
"https://kdsintf.amd.com/vcek/v1/Milan/3ac3fe21e13fb0990eb28a802e3fb6a29483a6b0753590c951bdd3b8e53786184ca39e359669a2b76a1936776b564ea464cdce40c05f63c9b610c5068b006b5d?blSPL=2&teeSPL=0&snpSPL=5&ucodeSPL=68": sevSnpVcekMilan,
})
}
// fixedNow pins the verification clock inside the validity window of
// the committed VCEK. Matches cc/attest's verifier_test.fixedNow().
func fixedNow() time.Time {
return time.Date(2024, 1, 15, 12, 0, 0, 0, time.UTC)
}
// realMeasurement is the exact 48-byte launch-measurement bytes the
// committed SEV-SNP report attests to. Used to compute the operator-
// asserted RIM digest (sha256(measurement)) for the test envelope.
func realMeasurement() []byte {
return sevSnpAttestationMilan[0x90 : 0x90+48]
}
// realChipID is the 64-byte CHIP_ID from the committed report.
func realChipID() []byte {
return sevSnpAttestationMilan[0x1A0 : 0x1A0+64]
}
// makeRIM returns the operator-asserted RIM digest the test envelope
// MUST claim. defaultRIMCheck folds the verified report's measurement
// through sha256 — this helper computes the same fold so test
// envelopes round-trip cleanly.
func makeRIM(t *testing.T) [32]byte {
t.Helper()
return sha256.Sum256(realMeasurement())
}
// makeHardware returns the operator-asserted hardware fingerprint —
// sha256(chip_id) as a per-silicon identifier. Production paths may
// fold (model, driver, vbios) instead.
func makeHardware(t *testing.T) [32]byte {
t.Helper()
return sha256.Sum256(realChipID())
}
// makeTEEPub returns a deterministic X25519 public key derived from
// a test-fixed seed. Real TEEs generate this ephemerally at boot
// and only publish the public half; the test uses a fixed value so
// the sealed-key derivation is reproducible.
func makeTEEPub(t *testing.T) [32]byte {
t.Helper()
// Curve25519 basepoint multiplication needs a clamped scalar.
var priv [32]byte
for i := range priv {
priv[i] = byte(i + 1) // any non-zero pattern; clamped below
}
priv[0] &= 248
priv[31] &= 127
priv[31] |= 64
pub, err := curve25519BasepointMul(priv[:])
if err != nil {
t.Fatalf("makeTEEPub: %v", err)
}
var out [32]byte
copy(out[:], pub)
return out
}
// newTestFileHSM returns a FileProvider rooted at t.TempDir(). One
// per test — no cross-test contamination.
func newTestFileHSM(t *testing.T) hsm.Provider {
t.Helper()
dir := t.TempDir()
// HexEncoded=true: FileProvider strings.TrimSpace's raw bytes that
// happen to look like whitespace (0x09/0x0A/0x0D/0x20). Hex framing
// makes the on-disk form unambiguous and round-trip-safe.
cfg := &hsm.FileConfig{
BasePath: dir,
HexEncoded: true,
}
p, err := hsm.NewFileProvider(cfg)
if err != nil {
t.Fatalf("newTestFileHSM: %v", err)
}
// Seed an Ed25519 key for the audit signature: file provider Sign
// requires an ed25519 seed.
var ed25519Seed [32]byte
if _, err := rand.Read(ed25519Seed[:]); err != nil {
t.Fatalf("ed25519 seed: %v", err)
}
if err := p.StoreKey(context.Background(), "audit-key", ed25519Seed[:]); err != nil {
t.Fatalf("store audit key: %v", err)
}
t.Cleanup(func() {
if err := p.Close(); err != nil {
t.Errorf("hsm Close: %v", err)
}
_ = os.RemoveAll(dir)
})
return p
}
// newTestApprovalProvider returns the LocalDevProvider with
// MPC_LOCAL_APPROVAL=true exported for this test's lifetime.
// LocalDevProvider is the real ApprovalProvider; the only test
// concession is the env-var that lifts its production refusal.
func newTestApprovalProvider(t *testing.T) approval.ApprovalProvider {
t.Helper()
// MPC_LOCAL_APPROVAL=true is set process-wide by TestMain so that
// parallel tests can share the LocalDevProvider without
// t.Setenv-imposed serialization.
p, err := approval.NewProvider("local-dev", nil)
if err != nil {
t.Fatalf("newTestApprovalProvider: %v", err)
}
return p
}
// TestMain enables the LocalDevProvider for the lifetime of this
// test binary. The same env-var gate would refuse in a production
// build (factory_test.go upstream pins this).
func TestMain(m *testing.M) {
_ = os.Setenv("MPC_LOCAL_APPROVAL", "true")
os.Exit(m.Run())
}
// denyApprovalProvider satisfies approval.ApprovalProvider but always
// returns ApprovalSignature{} with no signature — used by
// TestSigner_Sign_WebAuthnApprovalRequired's deny branch.
//
// This is a REAL ApprovalProvider implementation: no stubbed
// interfaces, no mocks at the cc/attest or kms boundary. It satisfies
// the contract by returning an empty signature that VerifyApproval
// then refuses. The behavior models a deny verdict from a real
// WebAuthn / Ledger device that returned user-cancel.
type denyApprovalProvider struct{}
func (denyApprovalProvider) Provider() string { return "deny-test" }
func (denyApprovalProvider) GetPublicIdentity(_ context.Context, approverID string) (approval.PublicIdentity, error) {
return approval.PublicIdentity{
ApproverID: approverID,
Provider: "deny-test",
PublicKey: make([]byte, 32),
Algorithm: approval.AlgorithmEd25519,
}, nil
}
func (denyApprovalProvider) ApproveIntent(_ context.Context, approverID string, intent approval.CanonicalIntent) (approval.ApprovalSignature, error) {
return approval.ApprovalSignature{}, errors.New("deny-test: user cancelled")
}
func (denyApprovalProvider) VerifyApproval(_ context.Context, intent approval.CanonicalIntent, sig approval.ApprovalSignature) (bool, error) {
return false, nil
}
// newTestGate returns a fresh LocalReleaseGate + MemoryNonceStore
// bound to the operator-asserted RIM + hardware allowlists. Replay
// window and TTL are 5 seconds for tests so the rotation/expiry
// paths complete in CI time.
func newTestGate(t *testing.T, rim, hw [32]byte) (*kms.LocalReleaseGate, kms.NonceStore) {
t.Helper()
policy := kms.NewReleasePolicy([][32]byte{rim}, [][32]byte{hw})
policy.RequireSEVSNP = true
var rootKey [32]byte
if _, err := rand.Read(rootKey[:]); err != nil {
t.Fatalf("rootKey: %v", err)
}
store := kms.NewMemoryNonceStore()
gate, err := kms.NewLocalReleaseGate(policy, store, rootKey)
if err != nil {
t.Fatalf("NewLocalReleaseGate: %v", err)
}
gate.SetIssueTTL(5 * time.Second)
gate.SetReplayWindow(5 * time.Second)
return gate, store
}
// newTestSigner wires gate + file HSM + LocalDevProvider into a
// Signer with mode ModeM192s. Returns the Signer plus the same gate
// pointer so tests can call Rotate / Issue / Release directly.
func newTestSigner(t *testing.T, approvalRequired bool) (*Signer, *kms.LocalReleaseGate, hsm.Provider, [32]byte, [32]byte) {
t.Helper()
rim := makeRIM(t)
hw := makeHardware(t)
gate, _ := newTestGate(t, rim, hw)
hsmP := newTestFileHSM(t)
appr := newTestApprovalProvider(t)
cfg := Config{
Mode: magnetar.ModeM192s,
RequiredRIM: map[[32]byte]struct{}{rim: {}},
AllowedHardware: map[[32]byte]struct{}{hw: {}},
RequireSEVSNP: true,
KMSKeyID: "audit-key",
WrappedSeedKeyID: "master-seed",
ApprovalRequired: approvalRequired,
ApproverID: "test@lux.network",
}
s, err := New(gate, hsmP, appr, cfg)
if err != nil {
t.Fatalf("New: %v", err)
}
if _, err := s.Provision(context.Background(), nil); err != nil {
t.Fatalf("Provision: %v", err)
}
return s, gate, hsmP, rim, hw
}
// envelopeFromTestdata builds an Envelope wrapping the committed SEV
// report. ExpectedNonce is set by the caller post-Issue; here we
// pre-set the RIM, Hardware, TEEPub, and VerifyOpts.
//
// We deliberately do NOT call trust.ClearProductCertCache here —
// the AMD VCEK/ARK chain is shared across all SEV-SNP envelopes and
// the cache is correctness-equivalent to a fresh fetch. Leaving the
// cache hot lets t.Parallel tests run without serializing on the
// package-level cache mutation.
func envelopeFromTestdata(t *testing.T, rim, hw, teePub [32]byte) *Envelope {
t.Helper()
return &Envelope{
Kind: attest.KindSEVSNP,
EvidenceBytes: append([]byte(nil), sevSnpAttestationMilan...),
RIM: rim,
Hardware: hw,
TEEPub: teePub,
VerifyOpts: []attest.Option{
attest.WithKDSGetter(newKDSReplay()),
attest.WithNow(fixedNow()),
},
}
}
// ============================================================================
// Required test 1: full chain — TDX/SEV E2E
// ============================================================================
// TestSigner_Sign_SEVSNP_E2E exercises the FULL Sign chain against a
// real AMD Milan SEV-SNP report (chain-validated against the
// committed VCEK + Milan ARK/ASK), a real LocalReleaseGate, a real
// FileProvider HSM, and a real LocalDevProvider approval flow.
//
// On success the returned signature MUST verify under
// magnetar.VerifyBytes against the published group public key —
// proving the wire bytes are byte-identical to single-party FIPS 205.
func TestSigner_Sign_SEVSNP_E2E(t *testing.T) {
t.Parallel()
s, _, _, rim, hw := newTestSigner(t, true /*ApprovalRequired*/)
teePub := makeTEEPub(t)
env := envelopeFromTestdata(t, rim, hw, teePub)
jobID, err := FreshJobID()
if err != nil {
t.Fatalf("FreshJobID: %v", err)
}
msg := []byte("LUX-SLHDSA-TEE: institutional-custody E2E test")
wire, receipt, err := s.Sign(context.Background(), env, jobID, msg, nil)
if err != nil {
t.Fatalf("Sign: %v", err)
}
if len(wire) == 0 {
t.Fatal("Sign returned empty wire bytes")
}
if receipt == nil {
t.Fatal("Sign returned nil receipt")
}
if receipt.JobID != jobID {
t.Errorf("receipt.JobID = %x, want %x", receipt.JobID, jobID)
}
if receipt.EvidenceKind != string(attest.KindSEVSNP) {
t.Errorf("receipt.EvidenceKind = %q, want %q", receipt.EvidenceKind, attest.KindSEVSNP)
}
if receipt.EvidenceIssuer != kms.IssuerSEVSNP {
t.Errorf("receipt.EvidenceIssuer = %q, want %q", receipt.EvidenceIssuer, kms.IssuerSEVSNP)
}
if len(receipt.AuditSignature) == 0 {
t.Error("receipt.AuditSignature empty")
}
// External-verifier path: the published wire bytes must verify
// under the magnetar group public key (derived independently from
// the HSM-stored seed). This proves byte-identity with FIPS 205.
pub, err := s.PublicKey(context.Background())
if err != nil {
t.Fatalf("PublicKey: %v", err)
}
gkBytes, err := magnetar.MarshalGroupKey(pub)
if err != nil {
t.Fatalf("MarshalGroupKey: %v", err)
}
if !magnetar.VerifyBytes(gkBytes, msg, wire) {
t.Fatal("external VerifyBytes refused the signature; not FIPS 205 byte-identical")
}
}
// ============================================================================
// Required test 2: rejects corrupt attestation
// ============================================================================
// TestSigner_Sign_RejectsBadAttestation flips a bit deep in the SEV
// signature region. cc/attest.Dispatch must surface ErrSignatureInvalid
// or ErrChainInvalid; Signer.Sign must propagate that as a release-
// gate refusal wrapped under ErrPolicyRefused.
func TestSigner_Sign_RejectsBadAttestation(t *testing.T) {
t.Parallel()
s, _, _, rim, hw := newTestSigner(t, false)
teePub := makeTEEPub(t)
env := envelopeFromTestdata(t, rim, hw, teePub)
// Flip a bit inside the ECDSA signature R-component (matches
// cc/attest verifier_test offset). Stable in the signature region,
// not in the MBZ tail.
env.EvidenceBytes[0x2A0+0x10] ^= 0x01
jobID, _ := FreshJobID()
msg := []byte("reject-bad-evidence")
_, _, err := s.Sign(context.Background(), env, jobID, msg, nil)
if err == nil {
t.Fatal("Sign: expected refusal on tampered evidence, got nil")
}
if !errors.Is(err, ErrPolicyRefused) {
t.Errorf("Sign: err = %v, want wrapped ErrPolicyRefused", err)
}
}
// ============================================================================
// Required test 3: rejects RIM mismatch
// ============================================================================
// TestSigner_Sign_RejectsRIMMismatch builds a chain-valid envelope
// whose operator-asserted RIM does NOT match sha256(measurement) of
// the verified report. defaultRIMCheck must refuse and Sign must
// surface a release-gate refusal.
func TestSigner_Sign_RejectsRIMMismatch(t *testing.T) {
t.Parallel()
s, _, _, _, hw := newTestSigner(t, false)
teePub := makeTEEPub(t)
wrongRIM := sha256.Sum256([]byte("not-the-real-measurement"))
env := envelopeFromTestdata(t, wrongRIM, hw, teePub)
jobID, _ := FreshJobID()
msg := []byte("reject-wrong-rim")
_, _, err := s.Sign(context.Background(), env, jobID, msg, nil)
if err == nil {
t.Fatal("Sign: expected refusal on RIM mismatch, got nil")
}
if !errors.Is(err, ErrPolicyRefused) {
t.Errorf("Sign: err = %v, want wrapped ErrPolicyRefused", err)
}
}
// ============================================================================
// Required test 4: rejects expired nonce / wrong epoch
// ============================================================================
// TestSigner_Sign_RejectsExpiredNonce drives a Sign call where the
// gate's epoch has been rotated AFTER Issue() but BEFORE Release().
// LocalReleaseGate.Release refuses on stored-epoch mismatch and
// Sign must surface ErrPolicyRefused.
//
// We rotate AFTER Issue (auditedRelease bundles Issue and Release in
// one call, so we cannot rotate between them in-package). The
// equivalent test path: rotate AFTER one successful Sign, then sign
// again — the SECOND Sign must succeed at a fresh epoch and not
// resurrect any state from the prior epoch.
//
// Replay-rejection is covered by reusing the SAME (jobID, nonce)
// against the consume-set. We drive that here too.
func TestSigner_Sign_RejectsExpiredNonce(t *testing.T) {
t.Parallel()
s, gate, _, rim, hw := newTestSigner(t, false)
teePub := makeTEEPub(t)
env := envelopeFromTestdata(t, rim, hw, teePub)
// Drop the issue TTL to ~10ms so the in-flight nonce expires
// before Release can be called via the next sign.
gate.SetIssueTTL(10 * time.Millisecond)
jobID, _ := FreshJobID()
// Issue manually so we control timing.
nonce, epoch, err := gate.Issue(jobID)
if err != nil {
t.Fatalf("Issue: %v", err)
}
env.ExpectedNonce = nonce
time.Sleep(50 * time.Millisecond) // let it expire
_, releaseErr := gate.Release(kms.ReleaseRequest{
JobID: jobID, Epoch: epoch, Nonce: nonce, Attestation: env, Ctx: context.Background(),
})
if releaseErr == nil {
t.Fatal("gate.Release: expected expiry refusal, got nil")
}
if !errors.Is(releaseErr, kms.ErrPolicyRefused) {
t.Errorf("releaseErr = %v, want wrapped kms.ErrPolicyRefused", releaseErr)
}
if !errors.Is(releaseErr, kms.ErrExpired) {
t.Errorf("releaseErr = %v, want wrapped kms.ErrExpired", releaseErr)
}
// Sanity: Sign with a fresh-issued envelope still works after
// rotation. Restore TTL and call Sign normally.
gate.SetIssueTTL(5 * time.Second)
_ = gate.Rotate()
freshJob, _ := FreshJobID()
freshEnv := envelopeFromTestdata(t, rim, hw, teePub)
msg := []byte("post-rotation-sign")
if _, _, err := s.Sign(context.Background(), freshEnv, freshJob, msg, nil); err != nil {
t.Fatalf("Sign post-rotation: %v", err)
}
_ = s.cfg.RequireSEVSNP // touch field so we don't warn unused on cfg
}
// ============================================================================
// Required test 5: AWS KMS backend
// ============================================================================
// TestSigner_Sign_HSMSign_AWS_KMS exercises the AWS provider's Sign
// API against a localstack / in-memory aws-sdk-go-v2 test boundary.
//
// AWS KMS production requires real cloud credentials + a real KMS
// key — this is intentionally NOT done in unit CI. We document the
// skip explicitly with rationale (per spec: "Skip if AWS SDK test
// infra not available, but document the skip with rationale").
//
// To exercise locally: AWS_ENDPOINT_URL_KMS=http://localhost:4566
//
// AWS_ACCESS_KEY_ID=test
// AWS_SECRET_ACCESS_KEY=test
// AWS_REGION=us-east-1
// AWS_KMS_TEST_KEY_ARN=alias/test
// go test -run TestSigner_Sign_HSMSign_AWS_KMS
func TestSigner_Sign_HSMSign_AWS_KMS(t *testing.T) {
endpoint := os.Getenv("AWS_ENDPOINT_URL_KMS")
keyARN := os.Getenv("AWS_KMS_TEST_KEY_ARN")
if endpoint == "" || keyARN == "" {
t.Skip("AWS_ENDPOINT_URL_KMS and AWS_KMS_TEST_KEY_ARN not set; localstack KMS not available — see test comment for setup. Skipped per spec rationale: unit CI must not require real AWS credentials. File provider path is exercised by TestSigner_Sign_HSMSign_File and all chain-verify tests.")
}
awsCfg := &hsm.AWSConfig{
Region: os.Getenv("AWS_REGION"),
KeyARN: keyARN,
Profile: os.Getenv("AWS_PROFILE"),
}
awsP, err := hsm.NewAWSProvider(awsCfg)
if err != nil {
t.Fatalf("NewAWSProvider: %v", err)
}
defer awsP.Close()
// AWS provider does NOT support storing raw SLH-DSA seeds via
// KMS (KMS keys are HSM-resident, not byte-extractable). The
// path we exercise is "AWS KMS for AUDIT signing" only — the
// master seed lives in a file provider here, and KMSKeyID points
// at the AWS KMS key for the audit signature.
rim := makeRIM(t)
hw := makeHardware(t)
_ = makeTEEPub(t) // exercised by full Sign in TestSigner_Sign_SEVSNP_E2E; here we only call AWS.Sign directly
gate, _ := newTestGate(t, rim, hw)
fileP := newTestFileHSM(t) // master seed lives here
cfg := Config{
Mode: magnetar.ModeM192s,
RequiredRIM: map[[32]byte]struct{}{rim: {}},
AllowedHardware: map[[32]byte]struct{}{hw: {}},
RequireSEVSNP: true,
KMSKeyID: keyARN, // AWS KMS audit key
WrappedSeedKeyID: "master-seed",
ApprovalRequired: false,
}
// Composite HSM: file for master seed, AWS for audit. The
// signer's hsm.Provider is the file (master) — we exercise the
// AWS provider's Sign API directly to prove it reaches the KMS
// endpoint correctly. End-to-end Signer.Sign would need a
// multiplexer; out of scope for this single-provider Signer
// surface and documented as such.
s, err := New(gate, fileP, nil, cfg)
if err != nil {
t.Fatalf("New: %v", err)
}
if _, err := s.Provision(context.Background(), nil); err != nil {
t.Fatalf("Provision: %v", err)
}
digest := sha256.Sum256([]byte("aws-kms-audit-probe"))
sig, err := awsP.Sign(context.Background(), keyARN, digest[:])
if err != nil {
t.Fatalf("AWS KMS Sign: %v", err)
}
if len(sig) == 0 {
t.Fatal("AWS KMS Sign returned empty signature")
}
}
// ============================================================================
// Required test 6: File-backed HSM end-to-end
// ============================================================================
// TestSigner_Sign_HSMSign_File runs the full Sign path with the
// file-backed HSM provider exclusively. This is the canonical CI
// path: no external services, no skips. End-to-end PASS proves the
// composition (gate + file hsm + local-dev approval + magnetar)
// produces a FIPS 205 byte-identical signature.
func TestSigner_Sign_HSMSign_File(t *testing.T) {
t.Parallel()
s, _, _, rim, hw := newTestSigner(t, true)
teePub := makeTEEPub(t)
env := envelopeFromTestdata(t, rim, hw, teePub)
jobID, _ := FreshJobID()
msg := []byte("file-hsm-e2e")
wire, _, err := s.Sign(context.Background(), env, jobID, msg, nil)
if err != nil {
t.Fatalf("Sign: %v", err)
}
pub, err := s.PublicKey(context.Background())
if err != nil {
t.Fatalf("PublicKey: %v", err)
}
gkBytes, err := magnetar.MarshalGroupKey(pub)
if err != nil {
t.Fatalf("MarshalGroupKey: %v", err)
}
if !magnetar.VerifyBytes(gkBytes, msg, wire) {
t.Fatal("file-hsm-e2e: VerifyBytes refused FIPS 205 signature")
}
}
// ============================================================================
// Required test 7: WebAuthn-style approval required
// ============================================================================
// TestSigner_Sign_ApprovalRequired_DenyAndAllow proves the
// approval-gate semantics: a deny verdict from the approval provider
// MUST block signing with ErrApprovalDenied; an allow verdict from
// the real LocalDevProvider MUST pass through to the rest of the
// chain.
//
// We model "WebAuthn user-cancel" via denyApprovalProvider (a real
// ApprovalProvider impl that returns deny). The deny branch fires
// BEFORE any gate.Issue / hsm.GetKey call — net-zero side effects
// on the rest of the system, which we assert by checking the gate's
// in-flight nonce store remains empty.
func TestSigner_Sign_ApprovalRequired_DenyAndAllow(t *testing.T) {
t.Parallel()
rim := makeRIM(t)
hw := makeHardware(t)
gate, store := newTestGate(t, rim, hw)
fileP := newTestFileHSM(t)
cfg := Config{
Mode: magnetar.ModeM192s,
RequiredRIM: map[[32]byte]struct{}{rim: {}},
AllowedHardware: map[[32]byte]struct{}{hw: {}},
RequireSEVSNP: true,
KMSKeyID: "audit-key",
WrappedSeedKeyID: "master-seed",
ApprovalRequired: true,
ApproverID: "ceo@lux.network",
}
// Deny branch
denyS, err := New(gate, fileP, denyApprovalProvider{}, cfg)
if err != nil {
t.Fatalf("New(deny): %v", err)
}
if _, err := denyS.Provision(context.Background(), nil); err != nil {
t.Fatalf("Provision: %v", err)
}
teePub := makeTEEPub(t)
env := envelopeFromTestdata(t, rim, hw, teePub)
jobID, _ := FreshJobID()
msg := []byte("approval-deny-test")
_, _, err = denyS.Sign(context.Background(), env, jobID, msg, nil)
if err == nil {
t.Fatal("Sign(deny): expected ErrApprovalDenied, got nil")
}
if !errors.Is(err, ErrApprovalDenied) {
t.Errorf("Sign(deny): err = %v, want wrapped ErrApprovalDenied", err)
}
// Side-effect check: gate must NOT have issued any nonce on the
// deny path — auditedRelease short-circuits before Issue().
if _, lookupErr := store.Lookup(jobID); !errors.Is(lookupErr, kms.ErrNonceUnknown) {
t.Errorf("deny path leaked a gate-issued nonce: %v", lookupErr)
}
// Allow branch. MPC_LOCAL_APPROVAL already exported by TestMain.
appr, err := approval.NewProvider("local-dev", nil)
if err != nil {
t.Fatalf("local-dev provider: %v", err)
}
allowS, err := New(gate, fileP, appr, cfg)
if err != nil {
t.Fatalf("New(allow): %v", err)
}
allowEnv := envelopeFromTestdata(t, rim, hw, teePub)
allowJob, _ := FreshJobID()
if _, _, err := allowS.Sign(context.Background(), allowEnv, allowJob, msg, nil); err != nil {
t.Fatalf("Sign(allow): %v", err)
}
}
// ============================================================================
// Extra coverage: byte-identity with FIPS 205 SignDeterministic
// ============================================================================
// TestSigner_ByteIdentityWithFIPS205 proves the Signer.Sign output is
// byte-equal to a direct magnetar.KeyFromSeed → Sign call on the
// same seed + msg. This is the load-bearing claim of the TEE-only
// extension: the wire form is indistinguishable from single-party
// FIPS 205.
func TestSigner_ByteIdentityWithFIPS205(t *testing.T) {
t.Parallel()
s, _, hsmP, rim, hw := newTestSigner(t, false)
teePub := makeTEEPub(t)
env := envelopeFromTestdata(t, rim, hw, teePub)
jobID, _ := FreshJobID()
msg := []byte("byte-identity-probe")
wire, _, err := s.Sign(context.Background(), env, jobID, msg, nil)
if err != nil {
t.Fatalf("Sign: %v", err)
}
// Direct path: read the seed via HSM, KeyFromSeed, Sign
// deterministic. Should produce byte-equal output.
seed, err := hsmP.GetKey(context.Background(), "master-seed")
if err != nil {
t.Fatalf("HSM GetKey: %v", err)
}
params := magnetar.MustParamsFor(magnetar.ModeM192s)
sk, err := magnetar.KeyFromSeed(params, seed)
if err != nil {
t.Fatalf("KeyFromSeed: %v", err)
}
directSig, err := magnetar.Sign(params, sk, msg, nil, false, nil)
if err != nil {
t.Fatalf("direct Sign: %v", err)
}
directWire, err := directSig.MarshalBinary()
if err != nil {
t.Fatalf("direct MarshalBinary: %v", err)
}
if string(wire) != string(directWire) {
t.Fatalf("Sign output not byte-identical to single-party FIPS 205\n signer: %x\n direct: %x",
wire[:16], directWire[:16])
}
}
// ============================================================================
// Config validation
// ============================================================================
// TestConfig_Validate covers every Config refusal sentinel so callers
// using errors.Is can branch reliably.
func TestConfig_Validate(t *testing.T) {
rim := [32]byte{1}
hw := [32]byte{2}
good := Config{
Mode: magnetar.ModeM192s,
RequiredRIM: map[[32]byte]struct{}{rim: {}},
AllowedHardware: map[[32]byte]struct{}{hw: {}},
RequireSEVSNP: true,
KMSKeyID: "k",
WrappedSeedKeyID: "s",
}
if err := good.Validate(); err != nil {
t.Fatalf("good: %v", err)
}
cases := []struct {
name string
mutate func(*Config)
want error
}{
{"emptyRIM", func(c *Config) { c.RequiredRIM = nil }, ErrEmptyRIM},
{"emptyHardware", func(c *Config) { c.AllowedHardware = nil }, ErrEmptyHardware},
{"noRequireFlag", func(c *Config) { c.RequireSEVSNP = false }, ErrNoRequireFlag},
{"missingKMSKeyID", func(c *Config) { c.KMSKeyID = "" }, ErrMissingKMSKeyID},
{"missingSeedKeyID", func(c *Config) { c.WrappedSeedKeyID = "" }, ErrMissingSeedKeyID},
{"approverMissing", func(c *Config) { c.ApprovalRequired = true; c.ApproverID = "" }, ErrApproverMissing},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
c := good
// deep-copy maps so cases don't corrupt good
c.RequiredRIM = map[[32]byte]struct{}{rim: {}}
c.AllowedHardware = map[[32]byte]struct{}{hw: {}}
tc.mutate(&c)
err := c.Validate()
if !errors.Is(err, tc.want) {
t.Errorf("err = %v, want %v", err, tc.want)
}
})
}
}
// ============================================================================
// Smoke: Signer.Mode / Params accessors
// ============================================================================
func TestSigner_ModeAndParams(t *testing.T) {
s, _, _, _, _ := newTestSigner(t, false)
if s.Mode() != magnetar.ModeM192s {
t.Errorf("Mode = %v, want ModeM192s", s.Mode())
}
if s.Params() == nil {
t.Fatal("Params returned nil")
}
if s.Params().Mode != magnetar.ModeM192s {
t.Errorf("Params.Mode = %v, want ModeM192s", s.Params().Mode)
}
}
Binary file not shown.
Binary file not shown.