zap: decomplect onto luxfi/pq.Mode gate

Drop local SecurityProfile/Profile* constants, ProfileFromString,
IsPostQuantum, IsPQAware, RequireAttestationForProfile,
ErrClassicalAuthForbidden. The strict-PQ Mode vocabulary lives in
github.com/luxfi/pq as the single source of truth; warp, dex, fhe,
evm, zap all dispatch through pq.ValidateMode against the
PQEvidencer interface.

Attestation now implements pq.PQEvidencer:

  func (a *Attestation) HasPQEvidence() bool {
      return a != nil && len(a.PubKey) > 0 && len(a.Sig) > 0
  }

Tests integrate with pq.ValidateMode(pq.ModeStrictPQ, att, verify):
missing attestation under strict-PQ returns
pq.ErrClassicalAuthForbidden; verifier errors propagate via
errors.Is.

TranscriptHash + AttestationContext + TLSCertFingerprintFromBytes
unchanged — domain-separated SHAKE256-384 over the ZAP-PQ-V1
transcript stays the canonical channel-binding digest.
This commit is contained in:
Hanzo AI
2026-05-12 19:25:53 -07:00
parent bd7d6a392d
commit 0ceb69d5fd
4 changed files with 97 additions and 289 deletions
+4 -3
View File
@@ -4,7 +4,8 @@ go 1.26
require (
github.com/luxfi/mdns v0.1.0
golang.org/x/crypto v0.25.0
github.com/luxfi/pq v1.0.3
golang.org/x/crypto v0.41.0
)
require (
@@ -12,8 +13,8 @@ require (
github.com/grandcat/zeroconf v1.0.0 // indirect
github.com/miekg/dns v1.1.62 // indirect
golang.org/x/mod v0.18.0 // indirect
golang.org/x/net v0.27.0 // indirect
golang.org/x/net v0.42.0 // indirect
golang.org/x/sync v0.7.0 // indirect
golang.org/x/sys v0.22.0 // indirect
golang.org/x/sys v0.35.0 // indirect
golang.org/x/tools v0.22.0 // indirect
)
+8 -6
View File
@@ -4,6 +4,8 @@ github.com/grandcat/zeroconf v1.0.0 h1:uHhahLBKqwWBV6WZUDAT71044vwOTL+McW0mBJvo6
github.com/grandcat/zeroconf v1.0.0/go.mod h1:lTKmG1zh86XyCoUeIHSA4FJMBwCJiQmGfcP2PdzytEs=
github.com/luxfi/mdns v0.1.0 h1:VB3mQcETc9j5SY1S6lAgFtuGr/rjWuDgPYnxS+OKWMQ=
github.com/luxfi/mdns v0.1.0/go.mod h1:/3dheKVjUk2yiS/ocH1IDzeLXOIe+kpVsErIGDOZdiQ=
github.com/luxfi/pq v1.0.3 h1:pFlQm1+5FuKTDUh2y/23bXWkN4I2Rc5iuxJypwDFFMs=
github.com/luxfi/pq v1.0.3/go.mod h1:8bppZcRElfrVt0n3nYCZW3iX1TvhvzNbdjNdK1irgIE=
github.com/miekg/dns v1.1.27/go.mod h1:KNUDUusw/aVsxyTYZM1oqvCicbwhgbNgztCETuNZ7xM=
github.com/miekg/dns v1.1.62 h1:cN8OuEF1/x5Rq6Np+h1epln8OiyPWV+lROx9LxcGgIQ=
github.com/miekg/dns v1.1.62/go.mod h1:mvDlcItzm+br7MToIKqkglaGhlFMHJ9DTNNWONWXbNQ=
@@ -11,8 +13,8 @@ github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.25.0 h1:ypSNr+bnYL2YhwoMt2zPxHFmbAN1KZs/njMG3hxUp30=
golang.org/x/crypto v0.25.0/go.mod h1:T+wALwcMOSE0kXgUAnPAHqTLW+XHgcELELW8VaDgm/M=
golang.org/x/crypto v0.41.0 h1:WKYxWedPGCTVVl5+WHSSrOBT0O8lx32+zxmHxijgXp4=
golang.org/x/crypto v0.41.0/go.mod h1:pO5AFd7FA68rFak7rOAGVuygIISepHftHnr8dr6+sUc=
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
golang.org/x/mod v0.18.0 h1:5+9lSbEzPSdWkH32vYPBwEpX8KwDbM52Ud9xBUvNlb0=
golang.org/x/mod v0.18.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
@@ -20,16 +22,16 @@ golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190923162816-aa69164e4478/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.27.0 h1:5K3Njcw06/l2y9vpGCSdcxWOYHOUk3dVNGDXN+FvAys=
golang.org/x/net v0.27.0/go.mod h1:dDi0PyhWNoiUOrAS8uXv/vnScO4wnHQO4mj9fn/RytE=
golang.org/x/net v0.42.0 h1:jzkYrhi3YQWD6MLBJcsklgQsoAcw89EcZbJw8Z614hs=
golang.org/x/net v0.42.0/go.mod h1:FF1RA5d3u7nAYA4z2TkclSCKh68eSXtiFwcWQpPXdt8=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M=
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190924154521-2837fb4f24fe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI=
golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI=
golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/tools v0.0.0-20191216052735-49a3e744a425/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.22.0 h1:gqSGLZqv+AI9lIQzniJ0nZDRG5GBPsSi+DRNHWNz6yA=
+48 -159
View File
@@ -1,137 +1,66 @@
// Copyright (C) 2019-2026, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
// pq_attestation.go — strict-PQ identity binding on ZAP handshakes.
// pq_attestation.go — ZAP channel-binding ML-DSA-65 attestation.
//
// Go 1.26's crypto/tls ships X25519MLKEM768 / SecP256r1MLKEM768 /
// SecP384r1MLKEM1024 as TLS 1.3 hybrid post-quantum key exchange
// groups — enabled by default. ZAP's TLS-wrapped transport already
// gets a quantum-secure session key OUT OF THE BOX via that
// hybrid KEX.
// Go 1.26's crypto/tls ships X25519MLKEM768 hybrid PQ key exchange
// by default; every ZAP TLS transport gets a quantum-secure session
// key out of the box. The remaining gap is identity: TLS cert
// signatures are still classical ECDSA / Ed25519 because Go doesn't
// yet ship ML-DSA cert sigs.
//
// What standard TLS does NOT yet ship is post-quantum certificate
// signatures: the cert authenticating each TLS endpoint is still
// classical ECDSA (or Ed25519 when Go ships it for TLS cert use).
// A quantum adversary that compromises an ECDSA cert key can
// impersonate the endpoint even though the session-key KEX was
// post-quantum.
// This file binds the TLS endpoint to the peer's strict-PQ identity
// at the application layer. After the TLS handshake completes,
// each peer sends an Attestation as the first ZAP message:
//
// This package closes that gap by binding the TLS cert to the
// peer's post-quantum identity at the application layer: each
// peer sends an Attestation as the first ZAP message after the
// handshake completes. The attestation is a FIPS 204 ML-DSA-65
// signature over a SHAKE256-384 transcript that includes:
// Attestation = {
// PubKey: peer's FIPS 204 ML-DSA-65 public key (1952 bytes),
// Sig: mldsa65.Sign(privKey, TranscriptHash(ctx)),
// }
//
// - the TLS cert fingerprint (so a stolen cert can't be paired
// with a different PQ identity),
// - the chain id (so a replay across chains fails),
// - a per-session nonce (so a replay within a chain fails).
// TranscriptHash = SHAKE256-384(
// "ZAP-PQ-V1"
// || TLS cert fingerprint (stolen cert can't be paired with a
// different PQ identity)
// || chain id (cross-chain replay protection)
// || peer ML-KEM-768 pubkey (binds to the same KEM key the TLS
// handshake used)
// || timestamp (intra-chain replay window)
// || per-session nonce (cross-session replay protection)
// )
//
// The verifier checks the signature against the peer's PQ public
// key AND that the public key is a member of the chain's
// validator set. Either check failing closes the connection.
// Profile dispatch routes through lux/pq.ValidateMode:
//
// This package is verifier-agnostic on purpose: the actual
// ML-DSA-65 verification lives in luxfi/crypto/mldsa, but zap
// avoids the dep (zap is at the bottom of the dependency tree
// and shouldn't carry crypto transitively). Callers supply an
// AttestationVerifier that wraps mldsa65.Verify.
// profile, _ := pq.ModeFromString(cfg.ZAPProfile)
// verify := func() error {
// hash := zap.TranscriptHash(ctx)
// return verifier(att.PubKey, att.Sig, hash[:])
// }
// if err := pq.ValidateMode(profile, att, verify); err != nil {
// // strict-PQ refused a missing attestation, or the verifier
// // returned a non-nil error
// }
//
// Same gate, same sentinel, same mode vocabulary as lux/warp,
// lx/dex, lux/fhe, luxfi/evm.
package zap
import (
"crypto/sha256"
"encoding/binary"
"errors"
"fmt"
"golang.org/x/crypto/sha3"
)
// SecurityProfile is the ZAP security posture a deployment pins.
// Matches the shape used by lux/warp, lx/dex, lux/fhe, luxfi/evm
// so downstream consumers see one vocabulary across every layer.
type SecurityProfile int
const (
// ProfileClassical accepts ZAP connections without a PQ
// Attestation. TLS handshake is the only identity check.
// Suitable for legacy deployments before any peer has
// generated ML-DSA validator material.
ProfileClassical SecurityProfile = iota
// ProfileHybrid validates an Attestation WHEN the peer sends
// one, but accepts connections without one (falling back to
// TLS identity alone with a stale-PQ warning). Safe migration
// middle.
ProfileHybrid
// ProfileStrictPQ REFUSES every ZAP connection whose peer
// does NOT present a valid Attestation bound to the TLS cert
// fingerprint. Canonical Liquid / strict Lux / strict Zoo
// profile.
ProfileStrictPQ
)
// String returns the canonical wire name. Audit pipelines match
// on these strings; renaming here breaks every downstream consumer.
func (p SecurityProfile) String() string {
switch p {
case ProfileClassical:
return "classical"
case ProfileHybrid:
return "hybrid"
case ProfileStrictPQ:
return "strict-pq"
default:
return "unknown"
}
}
// IsPostQuantum reports whether this profile REFUSES connections
// missing a PQ Attestation. Only ProfileStrictPQ returns true.
func (p SecurityProfile) IsPostQuantum() bool {
return p == ProfileStrictPQ
}
// IsPQAware reports whether this profile VALIDATES an attestation
// when the peer presents one. Both ProfileHybrid and
// ProfileStrictPQ return true; ProfileClassical ignores
// attestations even when they're sent.
func (p SecurityProfile) IsPQAware() bool {
return p == ProfileHybrid || p == ProfileStrictPQ
}
// ProfileFromString parses an operator-supplied profile string.
// Refuses unknown values rather than defaulting.
func ProfileFromString(s string) (SecurityProfile, error) {
switch s {
case "classical":
return ProfileClassical, nil
case "hybrid":
return ProfileHybrid, nil
case "strict-pq":
return ProfileStrictPQ, nil
default:
return ProfileClassical, fmt.Errorf("zap: unknown profile %q (want classical|hybrid|strict-pq)", s)
}
}
// ErrClassicalAuthForbidden is returned when a strict-PQ ZAP
// connection is missing a PQ Attestation. Name and shape match
// lux/warp, lx/dex, luxfi/evm so audit pipelines can grep one
// identifier across every strict-PQ refusal site in the system.
var ErrClassicalAuthForbidden = errors.New(
"zap: classical authentication forbidden under strict-PQ profile (PQ Attestation required)")
// Attestation is the wire shape a ZAP peer presents after the
// TLS handshake completes. PubKey + Sig are opaque bytes from
// zap's perspective; the AttestationVerifier owns the format
// (FIPS 204 ML-DSA-65 pubkey 1952 bytes, signature 3293 bytes
// for Liquid; the same wire format works for ML-DSA-87 with
// different byte counts for high-value Zoo chains).
// for Liquid; ML-DSA-87 with different byte counts for
// high-value Zoo chains).
type Attestation struct {
// PubKey is the peer's strict-PQ public key. Verifier
// PubKey is the peer's strict-PQ public key. The verifier
// confirms membership in the chain's validator set BEFORE
// trusting the signature.
PubKey []byte
@@ -139,6 +68,14 @@ type Attestation struct {
Sig []byte
}
// HasPQEvidence implements pq.PQEvidencer. A non-nil Attestation
// with a non-empty Sig counts as evidence — the gate then
// dispatches to the verifier, which actually checks the
// signature + validator-set membership.
func (a *Attestation) HasPQEvidence() bool {
return a != nil && len(a.PubKey) > 0 && len(a.Sig) > 0
}
// AttestationContext bundles the inputs a verifier needs to
// rebuild the transcript hash. Same inputs on both peers; the
// PQ signature anchors the binding.
@@ -212,57 +149,9 @@ func TLSCertFingerprintFromBytes(certDER []byte) [32]byte {
return sha256.Sum256(certDER)
}
// AttestationVerifier validates a peer's PQ attestation. Callers
// (e.g. lux/node, lux/kms) supply this — typically a one-line
// wrapper over mldsa65.PublicKeyFromBytes(...).VerifySignature(...)
// that ALSO confirms the public key is a member of the chain's
// validator set.
type AttestationVerifier func(pubKey, sig, transcriptHash []byte) error
// RequireAttestationForProfile is the single seam every ZAP
// transport-init should call BEFORE trusting a connection's
// peer identity.
//
// - ProfileClassical: returns nil regardless (TLS identity OK).
// - ProfileHybrid: returns nil regardless — if the peer sent
// an attestation, validate it via the verifier; if not, fall
// back to TLS identity alone (with a stale-PQ warning logged
// by the caller).
// - ProfileStrictPQ: returns ErrClassicalAuthForbidden if the
// attestation is nil. Otherwise calls the verifier; verifier
// errors propagate.
func RequireAttestationForProfile(
profile SecurityProfile,
att *Attestation,
ctx *AttestationContext,
verify AttestationVerifier,
) error {
if !profile.IsPQAware() {
// Classical profile: never validate, never refuse.
return nil
}
if att == nil {
if profile.IsPostQuantum() {
return ErrClassicalAuthForbidden
}
// Hybrid + nil attestation: accept, caller decides
// whether to log a stale-PQ warning.
return nil
}
if verify == nil {
return errors.New("zap: nil AttestationVerifier supplied to RequireAttestationForProfile")
}
if ctx == nil {
return errors.New("zap: nil AttestationContext")
}
hash := TranscriptHash(ctx)
return verify(att.PubKey, att.Sig, hash[:])
}
// leftEncode is the SP 800-185 §2.3.1 left_encode operation —
// length-prefix framing so concatenated fields can't be
// ambiguously parsed. Local copy avoids a dep on luxfi/ids /
// luxfi/consensus from a leaf package.
// ambiguously parsed.
func leftEncode(x uint64) []byte {
if x == 0 {
return []byte{0x01, 0x00}
+37 -121
View File
@@ -6,64 +6,31 @@ package zap
import (
"errors"
"testing"
"github.com/luxfi/pq"
)
func TestSecurityProfile_StringAndPredicates(t *testing.T) {
func TestAttestation_HasPQEvidence(t *testing.T) {
for _, tc := range []struct {
profile SecurityProfile
wantString string
wantPostQuantum bool
wantPQAware bool
name string
att *Attestation
want bool
}{
{ProfileClassical, "classical", false, false},
{ProfileHybrid, "hybrid", false, true},
{ProfileStrictPQ, "strict-pq", true, true},
{SecurityProfile(99), "unknown", false, false},
{"nil", nil, false},
{"empty", &Attestation{}, false},
{"pub-only", &Attestation{PubKey: []byte{0x01}}, false},
{"sig-only", &Attestation{Sig: []byte{0x01}}, false},
{"both", &Attestation{PubKey: []byte{0x01}, Sig: []byte{0x02}}, true},
} {
if tc.profile.String() != tc.wantString {
t.Errorf("%d.String() = %q, want %q",
tc.profile, tc.profile.String(), tc.wantString)
}
if tc.profile.IsPostQuantum() != tc.wantPostQuantum {
t.Errorf("%s.IsPostQuantum() = %t, want %t",
tc.profile, tc.profile.IsPostQuantum(), tc.wantPostQuantum)
}
if tc.profile.IsPQAware() != tc.wantPQAware {
t.Errorf("%s.IsPQAware() = %t, want %t",
tc.profile, tc.profile.IsPQAware(), tc.wantPQAware)
got := tc.att.HasPQEvidence()
if got != tc.want {
t.Errorf("%s: HasPQEvidence() = %t, want %t", tc.name, got, tc.want)
}
}
}
func TestProfileFromString(t *testing.T) {
for _, tc := range []struct {
input string
want SecurityProfile
wantErr bool
}{
{"classical", ProfileClassical, false},
{"hybrid", ProfileHybrid, false},
{"strict-pq", ProfileStrictPQ, false},
{"", 0, true},
{"PQ", 0, true},
{"strict_pq", 0, true},
} {
got, err := ProfileFromString(tc.input)
if (err != nil) != tc.wantErr {
t.Errorf("ProfileFromString(%q) err=%v, wantErr=%t",
tc.input, err, tc.wantErr)
continue
}
if !tc.wantErr && got != tc.want {
t.Errorf("ProfileFromString(%q) = %s, want %s",
tc.input, got, tc.want)
}
}
}
// TestTranscriptHash_Determinism pins that the same inputs always
// produce the same hash — required for verifier <-> signer
// agreement on the digest.
// TestTranscriptHash_Determinism pins same-inputs-same-output —
// required for verifier ↔ signer agreement on the digest.
func TestTranscriptHash_Determinism(t *testing.T) {
ctx := &AttestationContext{
TLSCertFingerprint: [32]byte{0x01, 0x02, 0x03},
@@ -72,18 +39,14 @@ func TestTranscriptHash_Determinism(t *testing.T) {
Timestamp: 1700000000,
Nonce: [32]byte{0xde, 0xad, 0xbe, 0xef},
}
h1 := TranscriptHash(ctx)
h2 := TranscriptHash(ctx)
if h1 != h2 {
if TranscriptHash(ctx) != TranscriptHash(ctx) {
t.Fatal("TranscriptHash is not deterministic")
}
}
// TestTranscriptHash_FieldDistinction pins that changing ANY
// transcript field changes the digest. This is the property an
// attacker can't bypass: they can't reuse a signature across
// different chains, different sessions, or different cert
// bindings.
// transcript field changes the digest — an attacker can't reuse
// a signature across different chains, sessions, or cert bindings.
func TestTranscriptHash_FieldDistinction(t *testing.T) {
base := &AttestationContext{
TLSCertFingerprint: [32]byte{0x01},
@@ -102,78 +65,31 @@ func TestTranscriptHash_FieldDistinction(t *testing.T) {
} {
ctx := *base
mutate(&ctx)
hm := TranscriptHash(&ctx)
if hm == h0 {
if TranscriptHash(&ctx) == h0 {
t.Errorf("TranscriptHash unchanged after mutation %+v", ctx)
}
}
}
func TestRequireAttestationForProfile_Classical(t *testing.T) {
// Classical never refuses, never calls verifier.
called := false
verify := func(_, _, _ []byte) error { called = true; return nil }
if err := RequireAttestationForProfile(ProfileClassical, nil, nil, verify); err != nil {
t.Errorf("Classical refused nil attestation: %v", err)
// TestValidateMode_StrictPQ pins integration with pq.ValidateMode:
// the canonical strict-PQ gate refuses a missing attestation,
// calls the verifier when one is present, and propagates errors
// verbatim.
func TestValidateMode_StrictPQ(t *testing.T) {
// Missing attestation → ErrClassicalAuthForbidden.
if err := pq.ValidateMode(pq.ModeStrictPQ, (*Attestation)(nil), nil); !errors.Is(err, pq.ErrClassicalAuthForbidden) {
t.Errorf("StrictPQ accepted nil attestation: %v", err)
}
if err := RequireAttestationForProfile(ProfileClassical,
&Attestation{PubKey: []byte("x"), Sig: []byte("y")}, nil, verify); err != nil {
t.Errorf("Classical refused attestation: %v", err)
}
if called {
t.Error("Classical called the verifier (should never)")
}
}
func TestRequireAttestationForProfile_StrictPQRefusesMissing(t *testing.T) {
verify := func(_, _, _ []byte) error { return nil }
err := RequireAttestationForProfile(ProfileStrictPQ, nil, nil, verify)
if !errors.Is(err, ErrClassicalAuthForbidden) {
t.Errorf("StrictPQ accepted nil attestation: err=%v", err)
}
}
func TestRequireAttestationForProfile_HybridAcceptsMissing(t *testing.T) {
verify := func(_, _, _ []byte) error { return nil }
if err := RequireAttestationForProfile(ProfileHybrid, nil, nil, verify); err != nil {
t.Errorf("Hybrid refused nil attestation: %v", err)
}
}
func TestRequireAttestationForProfile_VerifierCalled(t *testing.T) {
var seenPub, seenSig, seenHash []byte
verify := func(p, s, h []byte) error {
seenPub, seenSig, seenHash = p, s, h
return nil
}
att := &Attestation{PubKey: []byte("pub"), Sig: []byte("sig")}
ctx := &AttestationContext{TLSCertFingerprint: [32]byte{0x42}}
expected := TranscriptHash(ctx)
if err := RequireAttestationForProfile(ProfileStrictPQ, att, ctx, verify); err != nil {
t.Fatalf("verifier returned err: %v", err)
}
if string(seenPub) != "pub" || string(seenSig) != "sig" {
t.Errorf("verifier got pub=%q sig=%q", seenPub, seenSig)
}
if string(seenHash) != string(expected[:]) {
t.Error("verifier got wrong transcript hash")
}
}
func TestRequireAttestationForProfile_VerifierErrorPropagates(t *testing.T) {
want := errors.New("verifier failed")
verify := func(_, _, _ []byte) error { return want }
// Present attestation + verifier returning nil → accept.
verify := pq.Verify(func() error { return nil })
att := &Attestation{PubKey: []byte("p"), Sig: []byte("s")}
ctx := &AttestationContext{}
if err := RequireAttestationForProfile(ProfileStrictPQ, att, ctx, verify); !errors.Is(err, want) {
if err := pq.ValidateMode(pq.ModeStrictPQ, att, verify); err != nil {
t.Errorf("StrictPQ refused valid attestation: %v", err)
}
// Present attestation + verifier returning error → propagate.
want := errors.New("verifier failed")
verifyErr := pq.Verify(func() error { return want })
if err := pq.ValidateMode(pq.ModeStrictPQ, att, verifyErr); !errors.Is(err, want) {
t.Errorf("verifier error not propagated: %v", err)
}
}
func TestRequireAttestationForProfile_NilVerifier(t *testing.T) {
att := &Attestation{PubKey: []byte("p"), Sig: []byte("s")}
ctx := &AttestationContext{}
if err := RequireAttestationForProfile(ProfileStrictPQ, att, ctx, nil); err == nil {
t.Error("StrictPQ accepted nil verifier")
}
}