mirror of
https://github.com/luxfi/pulsar.git
synced 2026-09-24 00:25:52 +00:00
audit 2026-06: TEE + GPU + public-permissionless-chain safety
Fresh four-dimension audit at v1.1.1 (commit 6a5606f). BLOCKERS.md stays at "submission-ready. No open items." for existing claims; two forward-looking v1.2 extensions file under the new Forward-looking section. Bottom line per dimension: 1. Public-permissionless-chain safety — no re-opener. Slashable equivocation extractable via AbortEvidence (4-field TLV + wire-stable codec); rogue-key bound via FIPS 204 tr = SHAKE-256(pk, 64) → μ; adaptive corruption gap is a research direction documented as openq in spec/pulsar.tex; front-running ruled out by Round-1 commit-and-reveal + ML-KEM-768-derived pairwise MAC keys; PULSAR-V04-CTX v0.4 ctx-bound μ derivation stays load-bearing via single canonical deriveMuCtx helper. 2. TEE — luxfi/threshold/protocols/mldsa-tee already ships the custody-mode TEE-gated master-seed surface (AMD SEV-SNP / Intel TDX / NVIDIA NRAS via mpc/cc/attest + KMS ReleaseGate + HSM wrap-key + approval). Adds in-pulsar AttestationContext(setup, msg) helper for the permissionless-quorum-gating case: canonical 32-byte TBS digest a validator's TEE quote should bind to. No wire change, no ABI break. Five unit tests pin stability, nil-safety, full input sensitivity, and customisation-tag domain separation. 3. GPU — pulsar/gpu (UseAccelerator) is wired to luxfi/lattice/v7/gpu dispatcher; lux-private/gpu-kernels/ops/lattice/ntt_mldsa ships the byte-equal-to-PQClean NTT across 5 backends (CUDA/HIP/Metal/Vulkan/ WGSL). The substrate (lux_lattice_ntt_mldsa_batch + the existing stubbed lux_mldsa_verify_batch in accel/internal/capi/capi.go:503-531) is the v1.2 forward-looking item; routing through Round-2 batched NTT call sites is mechanical once the ABI ships. Byte-equality regression guard TestPulsar_GPU_ByteEqual already in place; passes vacuously today, pins divergence the moment GPU dispatch engages. 4. Cross-validation against pulsar-mptc legacy — clean. No orphan imports. All "MPTC" refs are to the NIST competition category. Build sanity: go build / go vet clean; full ref/go/pkg/pulsar suite green (14.9s, 1.4s -race on new tests); GPU subpackage + interop tests green. Files: - AUDIT-2026-06.md (new) — comprehensive audit per dimension. - BLOCKERS.md — adds Forward-looking (v1.2) section with PULSAR-V12-TEE-BIND + PULSAR-V12-GPU-NTT-WIRE. Existing Closed section untouched. - ref/go/pkg/pulsar/attest.go (new) — AttestationContext helper (one pure function over already-transmitted Round-1 inputs). - ref/go/pkg/pulsar/attest_test.go (new) — stability + nil-safety + input sensitivity + domain-separation pins.
This commit is contained in:
@@ -0,0 +1,494 @@
|
||||
# Pulsar 2026-06 fresh four-dimension audit
|
||||
|
||||
> Re-audit of `github.com/luxfi/pulsar` at `04cdc24` against the public-
|
||||
> permissionless-chain deployment dimension, TEE integration, GPU
|
||||
> integration, and orphan-reference hygiene. Issued even though
|
||||
> `BLOCKERS.md` reads "submission-ready. No open items." — the brief
|
||||
> is to look fresh and re-open if anything surfaces. **Nothing
|
||||
> surfaces that re-opens an Open finding.** Two forward-looking items
|
||||
> are filed for v1.2 (PULSAR-V12-TEE-BIND, PULSAR-V12-GPU-NTT-WIRE);
|
||||
> both are extensions, not gaps in existing claims.
|
||||
|
||||
`luxfi/pulsar` ships a FIPS 204 ML-DSA-65 threshold signature. v1.1.1
|
||||
closed PULSAR-V04-CTX (FIPS 204 §5.4 ctx-bound threshold sign with
|
||||
single canonical μ-derivation helper). The current head builds
|
||||
clean, `go vet` clean, the load-bearing tests (`TestPulsar_Wire_*`,
|
||||
`TestOrchestrateV03*`, `TestPrecompile_PulsarMPC*`,
|
||||
`TestAlgebraic_FullCycle_*`, `TestPulsar_GPU_ByteEqual`) all pass.
|
||||
|
||||
The four-dimension audit covers:
|
||||
|
||||
1. Public-permissionless-chain safety.
|
||||
2. TEE integration (NVIDIA Confidential Compute / AMD SEV-SNP / Apple
|
||||
Secure Enclave).
|
||||
3. GPU acceleration (NTT batching against
|
||||
`lux-private/gpu-kernels/ops/lattice/{ntt,intt}_mldsa`).
|
||||
4. Cross-validation against legacy `pulsar-mptc`.
|
||||
|
||||
The audit's bottom line per dimension is at the head of each section.
|
||||
|
||||
## 1. Public-permissionless-chain safety
|
||||
|
||||
**Bottom line.** No re-opener. The v0.3 algebraic-aggregate threshold
|
||||
path is the canonical permissionless surface; nothing in the codebase
|
||||
at v1.1.1 contradicts the safety claims already in `spec/pulsar.tex`,
|
||||
`docs/threat-model.md`, and the v1.0.20 + v1.0.21 + v1.0.22 + v1.0.23 +
|
||||
v1.1.0 + v1.1.1 closure record.
|
||||
|
||||
### 1.1 Slashable equivocation
|
||||
|
||||
Verified at
|
||||
`ref/go/pkg/pulsar/abort.go:43-49` and `ref/go/pkg/pulsar/abort.go:101-125`.
|
||||
|
||||
Equivocation evidence is a 4-field TLV blob `(commit1, commit2,
|
||||
broadcast_sig1, broadcast_sig2)` with kind-tagged structural
|
||||
validation, ENCODED canonical TBS bytes for the accuser identity
|
||||
signature (`TranscriptForComplaint` at `abort.go:310-325`), and an
|
||||
adversarial uniqueness check that rejects `commit1 == commit2` via
|
||||
`ErrEvidenceDuplicate`. `MarshalAbortEvidence` / `UnmarshalAbortEvidence`
|
||||
give a wire-stable codec (kind || accuser || accused || epoch ||
|
||||
len(ev) || ev || len(sig) || sig) that any chain validator,
|
||||
audit observer, or slashing module can verify against the
|
||||
accuser's published identity key. Double-signing on a Round-1 commit
|
||||
is therefore extractable to a `ComplaintEquivocation` complaint that
|
||||
binds to (accuser, accused, epoch) — exactly the slashable evidence
|
||||
the host chain requires.
|
||||
|
||||
### 1.2 Rogue-key / key-substitution
|
||||
|
||||
The threshold path inherits FIPS 204's standard key-binding hash
|
||||
`tr = SHAKE-256(pk, 64)` (`mldsa_keyderive.go:34, 164-167`). `tr` is
|
||||
the SOLE μ-input besides `M'`:
|
||||
|
||||
```
|
||||
μ = SHAKE-256(tr || M') where M' = 0x00 || |ctx| || ctx || M
|
||||
```
|
||||
|
||||
The single canonical μ-derivation lives at `threshold_v03.go:1294-1303`
|
||||
(`deriveMuCtx`) — one function, two callers (`round2EmitFull` and
|
||||
`AlgebraicAggregateCtx`). Every threshold signer is therefore
|
||||
key-prefix-bound to the **group** pk. A rogue-key attack on the
|
||||
threshold path reduces to a rogue-key attack on FIPS 204 ML-DSA
|
||||
itself, against which key-prefix-binding is the standard FIPS 204
|
||||
defense (FIPS 204 §5.4 step 2, mirrored in cloudflare/circl's verifier
|
||||
that pulsar's `Verify` dispatches into).
|
||||
|
||||
The DKG path (`threshold_v03.go:264-371`,
|
||||
`DealAlgebraicV03Shares`) Shamir-shares the FIPS 204-derived (s1, s2,
|
||||
t0) over the polynomial ring per-coefficient with a fresh random
|
||||
polynomial under the FIPS 204-derived A; rogue contributions during
|
||||
DKG would land in the algebraic aggregate as deviations detectable
|
||||
by the v0.3 abort path (`MACFailure` complaint, `abort.go:149-166`).
|
||||
|
||||
### 1.3 Adaptive corruption
|
||||
|
||||
The static-corruption proof is the MPTC baseline; spec/pulsar.tex
|
||||
§"Adaptive corruption proof" calls out the static-to-adaptive gap as
|
||||
a known open research direction (mirroring the same open question
|
||||
in the `boschini2024corona` baseline). This is a research gap, not
|
||||
a deployment gap: every PQ threshold signature in the published
|
||||
literature inherits the same static-vs-adaptive complexity-leveraging
|
||||
loss. Lux validator-set governance restricts mid-round corruption via
|
||||
epoch-bound committee selection (see consensus's epoch beacon), which
|
||||
is the deployed mitigation pulsar.tex Theorem reshare-pkinv assumes.
|
||||
**No re-opener.** Tracked in `spec/pulsar.tex` openq blocks.
|
||||
|
||||
### 1.4 Front-running / aggregation safety
|
||||
|
||||
Verified at `threshold_v03.go:757-934` (Round2Sign + AlgebraicAggregateCtx).
|
||||
|
||||
The per-party contribution is `z_i = y_i + c·λ_i·s_{1,i}` where:
|
||||
|
||||
- `y_i` is per-party fresh randomness drawn from the FIPS 204
|
||||
`(-γ_1', γ_1']` uniform distribution (`threshold_v03.go:660-675`,
|
||||
`polyDeriveUniformBounded`),
|
||||
- `c` is the public challenge derived from `(μ, w1)` (single SHAKE-256
|
||||
pass, `threshold_v03.go:861-869`), and
|
||||
- `λ_i` is the Lagrange coefficient for party i over the quorum.
|
||||
|
||||
`y_i` masks `s_{1,i}` under M-LWE (the v0.3 information-theoretic
|
||||
sketch is at `threshold_v03.go:749-756`). An adversary observing a
|
||||
prefix `t-1` of contributions cannot bias the aggregate's c-derivation:
|
||||
`c` depends on the public group pk via `tr`, the public ctx, the
|
||||
public message, and the public packed `w1` — none of which the
|
||||
adversary can adapt against the aggregator after broadcast.
|
||||
|
||||
The Round-1 commit (`threshold_v03.go:700-703`,
|
||||
`s.myCommit = transcriptHash32(tagV03R1, wPacked || tau1)`) hash-commits
|
||||
to `w_i` BEFORE Round-2 reveals; this is the standard commit-and-reveal
|
||||
defense against an adversary biasing its own `y_i` after observing
|
||||
others'. The MAC layer (`tau1` KMAC256, key per-pair from
|
||||
`SymmetricSession` ML-KEM-768) authenticates each commit-and-reveal
|
||||
under each pairwise identity-derived key, ruling out cross-quorum
|
||||
front-running by an unauthenticated bystander.
|
||||
|
||||
### 1.5 v0.4 ctx-bound wire-shape ambiguity
|
||||
|
||||
PULSAR-V04-CTX closed (CHANGELOG `bfee719`, BLOCKERS.md). Verified
|
||||
at v1.1.1 head:
|
||||
|
||||
- `deriveMuCtx` (`threshold_v03.go:1294-1303`) is the SINGLE source of
|
||||
truth for μ derivation.
|
||||
- `round2EmitFull` (`threshold_v03.go:858`) and `AlgebraicAggregateCtx`
|
||||
(`threshold_v03.go:1143`) both call it.
|
||||
- `NewAlgebraicThresholdSignerCtx`, `AlgebraicAggregateCtx`, and
|
||||
`OrchestrateV03SignCtx` validate `len(ctx) <= 255` at the API
|
||||
boundary (`orchestrate.go:122`).
|
||||
- `OrchestrateV03Sign(msg)` is a thin wrapper for
|
||||
`OrchestrateV03SignCtx(nil, msg)`; empty ctx byte-identical to
|
||||
historical v0.3 output.
|
||||
|
||||
`TestOrchestrateV03SignCtx_VerifyMatchesFIPS204` cross-checks against
|
||||
`cloudflare/circl` `mldsa65.VerifyCtx`. No re-opener.
|
||||
|
||||
### 1.6 Constant-time discipline
|
||||
|
||||
- `Verify` constant-time over valid-signature class:
|
||||
`ct/dudect/dudect_verify.c` exercises the dudect harness on the
|
||||
valid-sig population (which is the operationally meaningful CT
|
||||
class; see `ct/dudect/README.md` lines 14-25 for the rationale).
|
||||
- `Combine` / `AlgebraicAggregateCtx` constant-time in per-party
|
||||
shares: `ct/dudect/dudect_combine.c`. A timing leak here is HARD
|
||||
FAIL by the v0.3 contract.
|
||||
- `Sign` (single-party FIPS 204 rejection-sampling): explicitly NOT
|
||||
constant-time per FIPS 204 §3.3. Inherited from the underlying
|
||||
primitive. Documented in `ct/dudect/README.md` lines 31-37.
|
||||
- Jasmin `#[ct]` CT type-checked across the whole jasmin/ corpus per
|
||||
commit `04cdc24` ("All `#[ct]` contracts and `#declassify` pragmas
|
||||
preserved (CT type-checked at compile). Full Jasmin corpus: 23/23
|
||||
compile (was 20/23).").
|
||||
|
||||
### 1.7 Memory-zeroization
|
||||
|
||||
Verified at `ref/go/pkg/pulsar/zeroize.go` (best-effort byte/seed/u16
|
||||
wipe with the standard Go-no-memzero caveat in the file header).
|
||||
Master-seed lifetime in `DealAlgebraicV03Shares` is bounded by a
|
||||
`defer zeroizeKeyMaterial(km)` at `threshold_v03.go:308`; the
|
||||
algebraic-aggregate path does NOT materialise sk at any quorum party
|
||||
by construction.
|
||||
|
||||
### 1.8 Wire codec
|
||||
|
||||
Pulsar v1.1.0 / `wire.go` ships canonical PULS / PULG framed
|
||||
signatures + group-keys with magic-byte domain separation from
|
||||
corona's CORS / CORG. `TestPulsar_Wire_FIPS204Verifiable` cross-checks
|
||||
that a PULS-framed sig, with the frame stripped, verifies under
|
||||
unmodified `mldsa65.Verify`. No re-opener.
|
||||
|
||||
## 2. TEE integration
|
||||
|
||||
**Bottom line.** Custody-mode TEE integration is **shipped** in
|
||||
`github.com/luxfi/threshold/protocols/mldsa-tee` (master-seed-bound,
|
||||
chain-validated AMD SEV-SNP / Intel TDX / NVIDIA NRAS attestation via
|
||||
`mpc/cc/attest`, KMS release-gate, HSM-resident wrap-key, approval
|
||||
provider). The output is byte-identical to single-party FIPS 204
|
||||
ML-DSA `SignDeterministic` and verifies via `pulsar.VerifyBytes`.
|
||||
|
||||
The permissionless-BFT v0.3 path does **not** require TEE integration
|
||||
for safety (no party materialises sk by construction). What it CAN
|
||||
benefit from — and what is filed as v1.2 forward-looking — is an
|
||||
optional binding so a validator's Round-1 commit binds an external TEE
|
||||
attestation digest. The chain can then enforce attestation-presence as
|
||||
a per-validator-set policy at the consensus layer without touching
|
||||
pulsar's wire shape.
|
||||
|
||||
### 2.1 What ships today
|
||||
|
||||
`luxfi/threshold/protocols/mldsa-tee`:
|
||||
|
||||
```
|
||||
pulsar.OrchestrateV03Sign — permissionless-BFT; no TEE required
|
||||
mldsatee.Signer.Sign — custody-mode; TEE required
|
||||
```
|
||||
|
||||
The custody surface chains the four-layer release gate
|
||||
(`doc.go:9-20`):
|
||||
|
||||
1. Verifiable hardware attestation chain-validated against the pinned
|
||||
vendor root (AMD SEV-SNP Milan, NVIDIA NRAS, Intel TDX) via
|
||||
`mpc/cc/attest.Dispatch`.
|
||||
2. KMS `ReleaseGate` that pins the worker's RIM digest + hardware
|
||||
fingerprint and binds a single-use challenge nonce per-request.
|
||||
3. Out-of-band human / programmatic approval signature via
|
||||
`mpc/pkg/approval.ApprovalProvider`.
|
||||
4. HSM-resident wrap-key store via `mpc/pkg/hsm.Provider` so the
|
||||
FIPS 204 master ML-DSA seed (32 bytes) is sealed-at-rest and only
|
||||
ever unwrapped inside the attested TEE.
|
||||
|
||||
The output is the PULS-framed pulsar.Signature bytes. The SDK / chain
|
||||
verifier path is identical to the permissionless case.
|
||||
|
||||
### 2.2 Apple Secure Enclave
|
||||
|
||||
`mpc/cc/attest` dispatches every attestation blob through a per-kind
|
||||
verifier. Apple Secure Enclave (DCAP-equivalent) attestation lives in
|
||||
`mpc/cc/attest/apple` IFF it is registered with the dispatcher. As of
|
||||
the audit head, `mpc/cc/attest` ships AMD SEV-SNP + Intel TDX + NVIDIA
|
||||
NRAS verifiers. Apple Secure Enclave is **outside the audit scope** —
|
||||
it lives at the OS-level keychain layer, not at the validator-process
|
||||
layer where pulsar runs. If a future deployment needs Secure Enclave-
|
||||
backed validator key custody, the natural binding is to register
|
||||
`attest.Kind = AppleSecureEnclave` with the
|
||||
`CompositeAttestation.VerifyEvidence` dispatcher and reuse the same
|
||||
`mldsatee.Envelope` shape unchanged.
|
||||
|
||||
### 2.3 Forward-looking — PULSAR-V12-TEE-BIND (v1.2)
|
||||
|
||||
For the permissionless-BFT v0.3 path, the audit recommends shipping a
|
||||
small in-pulsar helper that constructs the canonical TBS bytes a
|
||||
validator's TEE quote should bind to. This is **not** a wire change —
|
||||
the binding lives inside the consensus envelope that carries the
|
||||
Round-1 commit, not inside pulsar's `AlgebraicRound1Message`. The
|
||||
helper makes attestation-binding unambiguous and gives the chain ONE
|
||||
canonical recipe.
|
||||
|
||||
Proposed surface (no wire change, no ABI change):
|
||||
|
||||
```go
|
||||
// AttestationContext returns the canonical TBS bytes a validator's
|
||||
// TEE quote should bind to when participating in a public-
|
||||
// permissionless Pulsar quorum that the host chain wants gated by
|
||||
// attestation presence.
|
||||
//
|
||||
// The bound material is exactly what an attested validator promises
|
||||
// is owned by its TEE-resident signer for the duration of this
|
||||
// Round-1 commit:
|
||||
//
|
||||
// AttCtx := H( "pulsar-att-v1" ||
|
||||
// left_encode(8) || sessionID(16) ||
|
||||
// left_encode(4) || attempt(4) ||
|
||||
// left_encode(32) || groupPub.Bytes(32) ||
|
||||
// left_encode(32) || nodeID(32) ||
|
||||
// left_encode(32) || round1.Commit(32) )
|
||||
//
|
||||
// where H is SHAKE-256 truncated to 32 bytes. The host chain's
|
||||
// attestation-policy module consumes (round1, attCtx, quote) and
|
||||
// verifies the TEE's nonce-bound quote matches attCtx via the
|
||||
// existing mpc/cc/attest dispatcher.
|
||||
//
|
||||
// This function deliberately does NOT touch the AlgebraicRound1Message
|
||||
// wire form. The binding is OUTSIDE the threshold protocol; the
|
||||
// consensus envelope carries (commit, quote) and the chain enforces
|
||||
// presence/policy. No backward-compat hazard.
|
||||
func AttestationContext(
|
||||
setup *AlgebraicSetup,
|
||||
msg *AlgebraicRound1Message,
|
||||
) [32]byte
|
||||
```
|
||||
|
||||
Filed as **PULSAR-V12-TEE-BIND** in BLOCKERS.md → Forward-looking
|
||||
(v1.2). Implementation is mechanical (one helper, one fixed
|
||||
domain-separated SHAKE-256), ships alongside a test that asserts the
|
||||
output is stable across runs and changes if any input field changes.
|
||||
|
||||
## 3. GPU acceleration
|
||||
|
||||
**Bottom line.** Pulsar's GPU surface is **partially wired**:
|
||||
`ref/go/pkg/pulsar/gpu/gpu.go` is the canonical opt-in that registers
|
||||
the pure-Go reference's per-ring SubRing against the
|
||||
`luxfi/lattice/v7/gpu` dispatcher. The dispatcher today does not engage
|
||||
on pulsar's production path (32-bit Montgomery [256]uint32 polys do
|
||||
not flow through `ring.SubRing` []uint64). What IS shipped across the
|
||||
five backends in `lux-private/gpu-kernels/ops/lattice/ntt_mldsa` (q =
|
||||
8380417, R = 2^32, byte-equal to PQClean `MLDSA65_CLEAN_ntt`) is **the
|
||||
exact NTT pulsar needs**, but it is not yet routed through Pulsar's
|
||||
sign path.
|
||||
|
||||
`TestPulsar_GPU_ByteEqual` (`threshold_v03_gpu_byte_eq_test.go`) is
|
||||
the load-bearing regression guard: today it passes vacuously (both
|
||||
legs run pure-Go), but it will catch any byte divergence the moment
|
||||
the GPU NTT is wired in.
|
||||
|
||||
### 3.1 What ships today
|
||||
|
||||
```
|
||||
luxfi/lattice/v7/gpu — dispatcher (cgo plumbing)
|
||||
luxfi/pulsar/ref/go/pkg/pulsar/gpu — opt-in glue (UseAccelerator)
|
||||
lux-private/gpu-kernels/ops/lattice/ntt_mldsa — kernels (5 backends)
|
||||
luxfi/accel.MLDSAVerifyBatch — ABI stub returning ErrNotSupported
|
||||
```
|
||||
|
||||
The pulsar/gpu surface tracks corona/gpu exactly so multi-primitive
|
||||
runtimes share one accelerator entry. `UseAccelerator` sets the
|
||||
SubRing-dispatch threshold to 1024 (above pulsar's N=256), keeping
|
||||
single-poly NTT on CPU; the dispatcher is armed for any future
|
||||
batched-NTT lift. Idempotent. Pure-Go-safe (build-tag-free; on a
|
||||
no-cgo build `lattice/v7/gpu.Available()` returns false and the
|
||||
canonical pure-Go path runs).
|
||||
|
||||
### 3.2 lux-private/gpu-kernels coverage
|
||||
|
||||
`ops/lattice/ntt_mldsa/op.yaml` declares:
|
||||
|
||||
```
|
||||
id: lattice.ntt_mldsa
|
||||
standard: NIST FIPS 204 (ML-DSA-65) forward NTT
|
||||
api: lux_lattice_ntt_mldsa_batch
|
||||
support: cpu yes / metal yes / cuda yes / hip yes / vulkan yes / wgsl yes
|
||||
notes: |
|
||||
In-place forward NTT over Z_q[X]/(X^256 + 1) with q = 8380417.
|
||||
Cooley-Tukey butterflies in Montgomery domain. 8 levels.
|
||||
Byte-equal to PQCLEAN_MLDSA65_CLEAN_ntt at the FIPS 204 reference.
|
||||
Same zeta table shared across all kernel languages.
|
||||
```
|
||||
|
||||
Inverse NTT companion lives at `ops/lattice/intt_mldsa/`. Both kernels
|
||||
exist for every backend (CUDA / HIP / Metal / Vulkan / WGSL / CPU).
|
||||
These are exactly the per-poly primitives pulsar's
|
||||
`mldsa_lattice.go:330-365` implement, with byte-identical Montgomery
|
||||
constants (q = 8380417, qinv = 58728449, R = 2^32).
|
||||
|
||||
### 3.3 Where GPU dispatch wins on pulsar
|
||||
|
||||
Per-signer Round-2 (`threshold_v03.go:678-913`) computes per attempt:
|
||||
|
||||
```
|
||||
yHat : L ntt calls (L = 5 for ML-DSA-65)
|
||||
w : K matrix-vector dotHat calls (each K=6 dotHat involves no NTT
|
||||
— A is in NTT domain already)
|
||||
wCoeff: K invNTT calls (K = 6)
|
||||
s1Hat : L ntt calls (per-party share NTT)
|
||||
cs2 : K ntt + K invNTT calls
|
||||
ct0 : K ntt + K invNTT calls
|
||||
```
|
||||
|
||||
Total per attempt per party: `L + 3K = 5 + 18 = 23` forward NTTs and
|
||||
`K + 2K = 18` inverse NTTs = **41 NTT calls per party per attempt**.
|
||||
Each NTT is on a single 256-coefficient poly. For a quorum of t = 4
|
||||
parties with the FIPS 204 expected ~5 attempts before acceptance, the
|
||||
ceremony runs `41 × 4 × 5 = 820` NTT calls.
|
||||
|
||||
Single-poly GPU dispatch at N = 256 is slower than pure-Go on every
|
||||
host (M1 Max measured, NVIDIA Blackwell measured: cf. corona benches);
|
||||
the GPU win lives in batched dispatch. The natural batch is **the L
|
||||
forward NTTs on yHat + the L NTTs on s1Hat + the K NTTs on cs2 + the K
|
||||
NTTs on ct0 = 5 + 5 + 6 + 6 = 22 forward NTTs**, all on independent
|
||||
polys, all in the same Round-2 frame. That fits the
|
||||
`lux_lattice_ntt_mldsa_batch` ABI exactly.
|
||||
|
||||
### 3.4 Forward-looking — PULSAR-V12-GPU-NTT-WIRE (v1.2)
|
||||
|
||||
Wire pulsar's Round-2 NTT into the gpu-kernels batched path via the
|
||||
luxfi/accel substrate. Three discrete pieces (none ABI-breaking):
|
||||
|
||||
**Piece A: accel substrate.** Expose `lux_lattice_ntt_mldsa_batch` in
|
||||
`luxfi/accel/internal/capi/capi.go` as a real call (the current
|
||||
`MLDSAVerifyBatch` stub at `capi.go:512` returns `ErrNotSupported`; the
|
||||
NTT batch is a sibling). The C ABI ships in `lux-accel`; the Go side
|
||||
is a one-function wrapper.
|
||||
|
||||
**Piece B: pulsar batched NTT entry.** Add a package-private
|
||||
`batchNTT(polys []poly)` to `mldsa_lattice.go` that:
|
||||
|
||||
- if `pulsar/gpu.Enabled()` && `accel.Available()` && `len(polys) >=
|
||||
batchThreshold`, packs the polys into the accel ABI buffer, calls
|
||||
`lux_lattice_ntt_mldsa_batch`, and unpacks.
|
||||
- otherwise loops over `polys` calling the existing pure-Go `ntt()`.
|
||||
|
||||
**Piece C: Round-2 batched call site.** In `threshold_v03.go:680-913`,
|
||||
replace the per-poly NTT loops with a single `batchNTT` call per
|
||||
logical batch (yHat, s1Hat, cs2-input, ct0-input). The output stays
|
||||
byte-equal because the underlying transform is the same. Verified by
|
||||
the existing `TestPulsar_GPU_ByteEqual` regression guard.
|
||||
|
||||
**Byte-equality contract.** The accel substrate's NTT is byte-equal to
|
||||
PQClean `MLDSA65_CLEAN_ntt` at the FIPS 204 reference (cited in
|
||||
`ops/lattice/ntt_mldsa/op.yaml` notes). Pulsar's `mldsa_lattice.go:330`
|
||||
NTT is byte-equal to circl's `internal/dilithium` NTT
|
||||
(`mldsa_lattice.go:8-13` calls out the byte-equality contract). PQClean
|
||||
and circl agree on the FIPS 204 reference NTT. The accel-engaged
|
||||
leg and the pure-Go leg therefore emit byte-identical signatures.
|
||||
|
||||
**Build / packaging.** No new cgo in `ref/go/pkg/pulsar` itself —
|
||||
ref stays pure-Go for NIST reviewers. The cgo plumbing lives behind
|
||||
`pulsar/gpu` (no build-tag) and `luxfi/accel` (cgo build-tagged). The
|
||||
NIST submission tarball cut by `scripts/cut-submission.sh` excludes
|
||||
both subpackages by default; reviewers building `go build
|
||||
./ref/go/pkg/pulsar` see only the circl + golang.org/x/crypto + stdlib
|
||||
transitive graph.
|
||||
|
||||
Filed as **PULSAR-V12-GPU-NTT-WIRE** in BLOCKERS.md → Forward-looking
|
||||
(v1.2).
|
||||
|
||||
### 3.5 Engine-layer batch verify
|
||||
|
||||
`/Users/z/work/lux/consensus/engine/gpu_batch_pipeline.go:532-600`
|
||||
ships GPU batch verify for ECDSA / Ed25519 / BLS. MLDSA is **not in
|
||||
the engine batch pipeline**; the engine has the MLDSA single-shot
|
||||
path at `gpu_batch_pipeline.go:919-932` (`verifyMLDSA`) but no batch
|
||||
variant. `luxfi/accel.MLDSAVerifyBatch` is the ABI substrate that
|
||||
needs to ship before the engine pipeline can call into it
|
||||
(`accel/internal/capi/capi.go:503-520`: returns `ErrNotSupported` per
|
||||
current API status doc).
|
||||
|
||||
Pulsar already has the CPU parallel batch entry
|
||||
(`ref/go/pkg/pulsar/verify_batch.go`); it explicitly defers GPU
|
||||
acceleration to the engine layer where the accel session is owned and
|
||||
the native libs are installed:
|
||||
|
||||
> "The GPU fast-path (accel.LatticeOps.DilithiumVerifyBatch) lives at
|
||||
> the engine layer ... Engine consumers should prefer the accel path
|
||||
> for batches >= luxfi/accel.BLSBatchVerifyThreshold; everything below
|
||||
> that threshold goes through this CPU implementation."
|
||||
|
||||
This is the correct decomplecting boundary: pulsar owns the algorithm
|
||||
and the byte-shape contract; the engine pipeline owns the accelerator
|
||||
session and the batch heuristics; the accel substrate owns the C ABI.
|
||||
The forward-looking item is the SUBSTRATE
|
||||
(`lux_mldsa_verify_batch`), not pulsar code.
|
||||
|
||||
## 4. Cross-validation against pulsar-mptc legacy
|
||||
|
||||
**Bottom line.** Clean. No orphan imports.
|
||||
|
||||
`grep -r 'pulsar-mptc\|pulsarmptc\|mptc' --include='*.go' --include='*.mod' --include='*.sum'`
|
||||
returns:
|
||||
|
||||
- `./ref/go/pkg/pulsar/doc.go`: "targeting NIST MPTC Class N1" — refers
|
||||
to the **NIST competition category**, not the legacy module.
|
||||
- `./ref/go/pkg/pulsar/wire.go`, `./ref/go/pkg/pulsar/verify.go`: refs
|
||||
to `spec/nist-mptc-category.tex` — also the competition category.
|
||||
- `./CHANGELOG.md:157, 201`, `./BLOCKERS.md:14`: HISTORY narrative of
|
||||
the v1.0.7 consolidation; intentional.
|
||||
- `./estimator/README.md:19`, `./ct/dudect/README.md:34`,
|
||||
`./spec/blockers.tex`, `./spec/pulsar.tex`, `./spec/system-model.tex`,
|
||||
`./spec/parameters.tex`: all references to "NIST MPTC" the
|
||||
competition.
|
||||
|
||||
No `import "github.com/luxfi/pulsar-mptc"`. No `pulsar-mptc` in
|
||||
`go.mod` / `go.sum`. The v1.0.7 consolidation
|
||||
(commit `7be057b`, BLOCKERS.md line 14) is complete and clean.
|
||||
|
||||
## 5. Build sanity
|
||||
|
||||
```
|
||||
$ go build ./... — clean
|
||||
$ go vet ./... — clean
|
||||
$ go test ./ref/go/pkg/pulsar -run 'TestPulsar_Wire|TestOrchestrateV03|TestPrecompile|TestAlgebraic_FullCycle' -count=1
|
||||
ok github.com/luxfi/pulsar/ref/go/pkg/pulsar 4.275s
|
||||
$ go test ./ref/go/pkg/pulsar -run TestPulsar_GPU_ByteEqual -count=1
|
||||
ok github.com/luxfi/pulsar/ref/go/pkg/pulsar 6.026s
|
||||
```
|
||||
|
||||
Jasmin corpus: 23/23 compile (per commit `04cdc24`). EasyCrypt theory
|
||||
`V04_Sign_Ctx.ec` ships in `proofs/easycrypt/`.
|
||||
|
||||
## 6. Audit verdict
|
||||
|
||||
`BLOCKERS.md` stays at "submission-ready. No open items." for the
|
||||
existing claims.
|
||||
|
||||
Two forward-looking items file under v1.2. Both are **extensions** of
|
||||
the current design (one decomplected hook each), not flaws in existing
|
||||
claims:
|
||||
|
||||
- **PULSAR-V12-TEE-BIND** — in-pulsar canonical TBS-bytes helper that
|
||||
the host chain's attestation-policy module can bind to. No wire
|
||||
change. No ABI change. Implementation is mechanical.
|
||||
- **PULSAR-V12-GPU-NTT-WIRE** — route pulsar's Round-2 NTT batches
|
||||
through `lux_lattice_ntt_mldsa_batch` once the accel C ABI ships.
|
||||
Byte-equal contract enforced by existing `TestPulsar_GPU_ByteEqual`.
|
||||
|
||||
The two are independent of each other and of any existing closed
|
||||
finding. They can ship in either order or in parallel; both target
|
||||
v1.2.
|
||||
+84
@@ -2,6 +2,11 @@
|
||||
|
||||
**Status: submission-ready. No open items.**
|
||||
|
||||
Forward-looking v1.2 extensions are tracked under
|
||||
`## Forward-looking (v1.2)` below; they are EXTENSIONS surfaced by the
|
||||
2026-06 fresh four-dimension audit (`AUDIT-2026-06.md`), not flaws in
|
||||
the v1.1.1 claims.
|
||||
|
||||
This file is the closed-finding registry. New findings open under
|
||||
`## Open`; on fix they move to `## Closed` with commit + tag. IDs
|
||||
are cited from source-code comments and from
|
||||
@@ -19,6 +24,85 @@ and the v1.0.7 sign-off.
|
||||
|
||||
None.
|
||||
|
||||
## Forward-looking (v1.2)
|
||||
|
||||
These are EXTENSIONS surfaced by the 2026-06 fresh four-dimension audit
|
||||
(`AUDIT-2026-06.md`). Both are independent of every closed finding;
|
||||
both ship as decomplected hooks that do NOT touch existing wire form,
|
||||
ABI, or any FIPS 204 byte-equality contract. Neither is a flaw in the
|
||||
current claims — they are deferred-by-design forward-pointing items
|
||||
filed for the next minor.
|
||||
|
||||
### PULSAR-V12-TEE-BIND — TEE-quote binding helper for permissionless quorum gating
|
||||
|
||||
**Status**: SCAFFOLDED at audit time (commit pending). One pure
|
||||
function `AttestationContext(setup, msg)` in
|
||||
`ref/go/pkg/pulsar/attest.go` returning the canonical 32-byte TBS
|
||||
digest a validator's TEE quote should bind to when the host chain
|
||||
gates permissionless-quorum admission on attestation presence. Five
|
||||
unit tests pin stability + nil-input safety + full input-coverage
|
||||
sensitivity + customisation domain-separation
|
||||
(`ref/go/pkg/pulsar/attest_test.go`).
|
||||
|
||||
**Why v1.2 not v1.1.x**: the audit treats the existing v1.1.1 claims
|
||||
(byte-equality, public-BFT safety, FIPS 204 §5.4 ctx-bound) as the
|
||||
single canonical home. The TEE-binding helper extends the surface
|
||||
without altering it; bundling under v1.2 keeps the v1.1.x cut focused
|
||||
on PULSAR-V04-CTX closure.
|
||||
|
||||
**What this is NOT**: it is not a TEE attestation verifier. The
|
||||
chain-validating attestation verifier lives at
|
||||
`github.com/luxfi/mpc/cc/attest`. Pulsar's role is to declare the
|
||||
canonical TBS bytes; the verifier role belongs to the consensus
|
||||
envelope.
|
||||
|
||||
**Custody-mode TEE path**: unchanged.
|
||||
`github.com/luxfi/threshold/protocols/mldsa-tee` already ships the
|
||||
SEV-SNP / TDX / NRAS-attested custody surface that materialises the
|
||||
master seed inside an attested TEE. The permissionless v0.3 path does
|
||||
NOT materialise sk at any party by construction, so attestation there
|
||||
is a policy gate, not a safety primitive.
|
||||
|
||||
### PULSAR-V12-GPU-NTT-WIRE — route Round-2 NTTs through gpu-kernels batched dispatch
|
||||
|
||||
**Status**: SPEC only. The substrate
|
||||
(`lux-private/gpu-kernels/ops/lattice/ntt_mldsa`, q = 8380417, R = 2^32,
|
||||
byte-equal to PQClean `MLDSA65_CLEAN_ntt`) ships across all five
|
||||
backends (CUDA, HIP, Metal, Vulkan, WGSL). The C ABI
|
||||
`lux_lattice_ntt_mldsa_batch` is the natural batched-NTT entry. The
|
||||
existing `pulsar/gpu.UseAccelerator` keeps the SubRing-dispatch
|
||||
threshold above pulsar's N=256 production ring so single-poly GPU
|
||||
dispatch is intentionally off (it loses to pure-Go at N=256 on every
|
||||
host). The batched dispatch (22 forward NTTs per Round-2-party-attempt
|
||||
on independent polys) is where the GPU win lives.
|
||||
|
||||
**Three discrete pieces**:
|
||||
|
||||
- **Substrate**: lift `lux_mldsa_verify_batch` and
|
||||
`lux_lattice_ntt_mldsa_batch` from `ErrNotSupported` stub
|
||||
(`luxfi/accel/internal/capi/capi.go:503-531`) to a real call.
|
||||
- **Pulsar batched NTT entry**: package-private `batchNTT(polys []poly)`
|
||||
in `mldsa_lattice.go` that dispatches via accel iff
|
||||
`pulsar/gpu.Enabled()` and `accel.Available()` and
|
||||
`len(polys) >= batchThreshold`; otherwise falls back to the existing
|
||||
per-poly pure-Go `ntt()`.
|
||||
- **Round-2 batched call sites**: replace per-poly loops in
|
||||
`threshold_v03.go:680-913` with `batchNTT` calls per logical batch
|
||||
(yHat, s1Hat, cs2-input, ct0-input).
|
||||
|
||||
**Byte-equality contract**: enforced by the existing
|
||||
`TestPulsar_GPU_ByteEqual` regression guard
|
||||
(`threshold_v03_gpu_byte_eq_test.go`). Today it passes vacuously (both
|
||||
legs run pure-Go); after PULSAR-V12-GPU-NTT-WIRE lands it pins the
|
||||
accel-engaged leg byte-equal to the pure-Go leg.
|
||||
|
||||
**Engine-layer batch verify**: pulsar's CPU
|
||||
`VerifyBatch` already defers GPU acceleration to the engine layer per
|
||||
`verify_batch.go:11-23`. The substrate (`MLDSAVerifyBatch`) is the
|
||||
forward-looking item, not pulsar code; engine wiring at
|
||||
`/Users/z/work/lux/consensus/engine/gpu_batch_pipeline.go` adds the
|
||||
MLDSA batch path once the substrate ships.
|
||||
|
||||
## Closed
|
||||
|
||||
### PULSAR-V04-CTX — v0.4 ctx-bound threshold sign (FIPS 204 §5.4)
|
||||
|
||||
@@ -0,0 +1,102 @@
|
||||
// Copyright (C) 2025-2026, Lux Industries Inc. All rights reserved.
|
||||
// See the file LICENSE for licensing terms.
|
||||
|
||||
package pulsar
|
||||
|
||||
// attest.go — canonical TBS bytes for binding a validator's TEE
|
||||
// quote to its Round-1 commit in a public-permissionless Pulsar
|
||||
// quorum (PULSAR-V12-TEE-BIND from AUDIT-2026-06.md).
|
||||
//
|
||||
// Pulsar's public-permissionless v0.3 path requires no TEE for
|
||||
// safety: AlgebraicAggregate produces a byte-identical FIPS 204
|
||||
// ML-DSA-65 signature without materialising the master sk at any
|
||||
// quorum party. The CUSTODY-mode TEE path lives outside this
|
||||
// package at github.com/luxfi/threshold/protocols/mldsa-tee.
|
||||
//
|
||||
// Some host chains nonetheless want to GATE participation in the
|
||||
// permissionless quorum on attestation presence — i.e. only validators
|
||||
// running an attested binary in an attested TEE may join the quorum,
|
||||
// even though the protocol itself is safe without it. The host
|
||||
// chain's attestation-policy module needs ONE canonical TBS recipe
|
||||
// that the validator's TEE quote binds to. This file provides that
|
||||
// recipe.
|
||||
//
|
||||
// DECOMPLECTING NOTE
|
||||
//
|
||||
// This file deliberately does NOT touch AlgebraicRound1Message,
|
||||
// AlgebraicRound2Message, or any other wire shape. The attestation
|
||||
// binding lives ALONGSIDE the threshold protocol, not INSIDE it. The
|
||||
// host chain's consensus envelope carries (commit, quote) as a single
|
||||
// validator broadcast; the chain's attestation-policy verifier
|
||||
// consumes (commit, AttestationContext(commit), quote) to decide
|
||||
// whether to admit the commit into the quorum.
|
||||
//
|
||||
// Wire stability invariant: bytes-on-wire for AlgebraicRound1Message,
|
||||
// AlgebraicRound2Message, Signature, GroupKey, and AbortEvidence are
|
||||
// UNCHANGED by this file's existence.
|
||||
|
||||
// AttestationContextTag is the customisation string for the SP 800-185
|
||||
// TupleHash binding the validator's TEE quote to its Round-1 commit.
|
||||
// The tag is unique-per-pulsar to keep the attestation TBS bytes
|
||||
// distinct from any other Pulsar transcript hash (Round-1 commit
|
||||
// transcript, MAC keys, signature challenge), preventing cross-context
|
||||
// collisions.
|
||||
const AttestationContextTag = "pulsar-att-v1"
|
||||
|
||||
// AttestationContext returns the canonical 32-byte TBS digest a
|
||||
// validator's TEE quote should bind to when participating in a
|
||||
// public-permissionless Pulsar quorum that the host chain gates by
|
||||
// attestation presence.
|
||||
//
|
||||
// The digest is computed via the SP 800-185 TupleHash primitive
|
||||
// shared with the rest of the pulsar transcript layer (see
|
||||
// transcript.go: transcriptHash32). The tuple is the ordered
|
||||
// concatenation of:
|
||||
//
|
||||
// tag = "pulsar-att-v1" (customisation string)
|
||||
// parts[0] = sessionID (16 bytes)
|
||||
// parts[1] = attempt (big-endian 4 bytes)
|
||||
// parts[2] = groupPubBytes (FIPS 204 pk-encoding bytes)
|
||||
// parts[3] = nodeID (32 bytes)
|
||||
// parts[4] = commit (32 bytes)
|
||||
//
|
||||
// Each part is framed by left_encode(bit_len) per SP 800-185 §2.3.1.
|
||||
// Any field change shifts the digest unambiguously.
|
||||
//
|
||||
// The 32-byte output is the value the validator's TEE quote nonce
|
||||
// (or report-data field, per vendor convention) MUST be set to. The
|
||||
// host chain's attestation-policy verifier then:
|
||||
//
|
||||
// 1. Calls AttestationContext(setup, msg) to reconstruct the bound
|
||||
// value from the (setup, msg) pair it received with the quote.
|
||||
// 2. Calls the vendor's attestation verifier (mpc/cc/attest.Dispatch)
|
||||
// to verify the quote against the policy-pinned root, asserting
|
||||
// quote.Nonce == AttestationContext.
|
||||
// 3. Admits the commit into the quorum iff both pass.
|
||||
//
|
||||
// Wire impact: NONE. This is a pure function over already-
|
||||
// transmitted Round-1 inputs. The attestation envelope is the host
|
||||
// chain's consensus-envelope concern, not pulsar's.
|
||||
//
|
||||
// Panic safety: returns the zero [32]byte for nil setup or nil msg
|
||||
// rather than panicking, matching the rest of the package's no-panic
|
||||
// boundary (per pulsar.tex §6.1 DD-007).
|
||||
func AttestationContext(setup *AlgebraicSetup, msg *AlgebraicRound1Message) [32]byte {
|
||||
if setup == nil || msg == nil || setup.Pub == nil {
|
||||
var zero [32]byte
|
||||
return zero
|
||||
}
|
||||
var attemptBytes [4]byte
|
||||
attemptBytes[0] = byte(msg.Attempt >> 24)
|
||||
attemptBytes[1] = byte(msg.Attempt >> 16)
|
||||
attemptBytes[2] = byte(msg.Attempt >> 8)
|
||||
attemptBytes[3] = byte(msg.Attempt)
|
||||
return transcriptHash32(
|
||||
AttestationContextTag,
|
||||
msg.SessionID[:],
|
||||
attemptBytes[:],
|
||||
setup.Pub.Bytes,
|
||||
msg.NodeID[:],
|
||||
msg.Commit[:],
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,169 @@
|
||||
// Copyright (C) 2025-2026, Lux Industries Inc. All rights reserved.
|
||||
// See the file LICENSE for licensing terms.
|
||||
|
||||
package pulsar
|
||||
|
||||
// attest_test.go — TBS-digest contract for AttestationContext
|
||||
// (PULSAR-V12-TEE-BIND).
|
||||
//
|
||||
// Two invariants pinned:
|
||||
//
|
||||
// 1. STABLE — the digest is deterministic across runs for the same
|
||||
// (setup, msg) tuple.
|
||||
// 2. SENSITIVE — changing ANY input field (sessionID, attempt,
|
||||
// groupPub, nodeID, commit) changes the digest. This is the
|
||||
// load-bearing property: an attacker that can flip ONE field
|
||||
// while keeping the quote bound to the OLD digest must lose,
|
||||
// so the digest function must be coverage-complete.
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func attestFixtureSetup() *AlgebraicSetup {
|
||||
pub := &PublicKey{Mode: ModeP65, Bytes: make([]byte, 1952)}
|
||||
for i := range pub.Bytes {
|
||||
pub.Bytes[i] = byte(i)
|
||||
}
|
||||
return &AlgebraicSetup{Mode: ModeP65, Pub: pub}
|
||||
}
|
||||
|
||||
func attestFixtureMsg() *AlgebraicRound1Message {
|
||||
m := &AlgebraicRound1Message{
|
||||
SessionID: [16]byte{'s', 'i', 'd', '-', 'a', 't', 't', '-', '0', '0', '0', '1'},
|
||||
Attempt: 0x01020304,
|
||||
}
|
||||
for i := range m.NodeID {
|
||||
m.NodeID[i] = byte(0x10 + i)
|
||||
}
|
||||
for i := range m.Commit {
|
||||
m.Commit[i] = byte(0x80 + i)
|
||||
}
|
||||
return m
|
||||
}
|
||||
|
||||
// TestAttestationContext_Stable — same inputs => same digest.
|
||||
func TestAttestationContext_Stable(t *testing.T) {
|
||||
setup := attestFixtureSetup()
|
||||
msg := attestFixtureMsg()
|
||||
a := AttestationContext(setup, msg)
|
||||
b := AttestationContext(setup, msg)
|
||||
if a != b {
|
||||
t.Fatalf("AttestationContext non-deterministic:\n a = %x\n b = %x", a, b)
|
||||
}
|
||||
var zero [32]byte
|
||||
if a == zero {
|
||||
t.Fatalf("AttestationContext returned zero on valid inputs")
|
||||
}
|
||||
}
|
||||
|
||||
// TestAttestationContext_NilInputs — defensive returns the zero digest.
|
||||
func TestAttestationContext_NilInputs(t *testing.T) {
|
||||
var zero [32]byte
|
||||
if got := AttestationContext(nil, attestFixtureMsg()); got != zero {
|
||||
t.Fatalf("nil setup did not return zero digest: %x", got)
|
||||
}
|
||||
if got := AttestationContext(attestFixtureSetup(), nil); got != zero {
|
||||
t.Fatalf("nil msg did not return zero digest: %x", got)
|
||||
}
|
||||
setupNilPub := &AlgebraicSetup{Mode: ModeP65}
|
||||
if got := AttestationContext(setupNilPub, attestFixtureMsg()); got != zero {
|
||||
t.Fatalf("nil setup.Pub did not return zero digest: %x", got)
|
||||
}
|
||||
}
|
||||
|
||||
// TestAttestationContext_Sensitive — every input field, when mutated,
|
||||
// MUST produce a different digest. This is the coverage-complete
|
||||
// property; an attacker that wants to keep a TEE quote (bound to the
|
||||
// old digest) valid while replaying with ANY altered field must lose.
|
||||
func TestAttestationContext_Sensitive(t *testing.T) {
|
||||
base := AttestationContext(attestFixtureSetup(), attestFixtureMsg())
|
||||
|
||||
t.Run("sessionID", func(t *testing.T) {
|
||||
msg := attestFixtureMsg()
|
||||
msg.SessionID[0] ^= 1
|
||||
mut := AttestationContext(attestFixtureSetup(), msg)
|
||||
if mut == base {
|
||||
t.Fatalf("sessionID mutation did not change digest")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("attempt", func(t *testing.T) {
|
||||
msg := attestFixtureMsg()
|
||||
msg.Attempt ^= 0x01
|
||||
mut := AttestationContext(attestFixtureSetup(), msg)
|
||||
if mut == base {
|
||||
t.Fatalf("attempt mutation did not change digest")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("groupPub", func(t *testing.T) {
|
||||
setup := attestFixtureSetup()
|
||||
setup.Pub.Bytes[0] ^= 1
|
||||
mut := AttestationContext(setup, attestFixtureMsg())
|
||||
if mut == base {
|
||||
t.Fatalf("groupPub mutation did not change digest")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("nodeID", func(t *testing.T) {
|
||||
msg := attestFixtureMsg()
|
||||
msg.NodeID[0] ^= 1
|
||||
mut := AttestationContext(attestFixtureSetup(), msg)
|
||||
if mut == base {
|
||||
t.Fatalf("nodeID mutation did not change digest")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("commit", func(t *testing.T) {
|
||||
msg := attestFixtureMsg()
|
||||
msg.Commit[0] ^= 1
|
||||
mut := AttestationContext(attestFixtureSetup(), msg)
|
||||
if mut == base {
|
||||
t.Fatalf("commit mutation did not change digest")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// TestAttestationContext_DomainSeparation — the customisation tag
|
||||
// must be reflected in the output. A digest computed with a different
|
||||
// customisation string (over the same inputs) must differ. Asserted
|
||||
// via cshake256 directly — we re-derive what the function should
|
||||
// produce and compare.
|
||||
func TestAttestationContext_DomainSeparation(t *testing.T) {
|
||||
setup := attestFixtureSetup()
|
||||
msg := attestFixtureMsg()
|
||||
got := AttestationContext(setup, msg)
|
||||
|
||||
var attemptBytes [4]byte
|
||||
attemptBytes[0] = byte(msg.Attempt >> 24)
|
||||
attemptBytes[1] = byte(msg.Attempt >> 16)
|
||||
attemptBytes[2] = byte(msg.Attempt >> 8)
|
||||
attemptBytes[3] = byte(msg.Attempt)
|
||||
|
||||
want := transcriptHash32(
|
||||
AttestationContextTag,
|
||||
msg.SessionID[:],
|
||||
attemptBytes[:],
|
||||
setup.Pub.Bytes,
|
||||
msg.NodeID[:],
|
||||
msg.Commit[:],
|
||||
)
|
||||
if got != want {
|
||||
t.Fatalf("AttestationContext disagrees with transcriptHash32:\n got = %x\n want = %x", got, want)
|
||||
}
|
||||
|
||||
// Same parts under a different customisation must differ.
|
||||
other := transcriptHash32(
|
||||
"pulsar-att-different",
|
||||
msg.SessionID[:],
|
||||
attemptBytes[:],
|
||||
setup.Pub.Bytes,
|
||||
msg.NodeID[:],
|
||||
msg.Commit[:],
|
||||
)
|
||||
if bytes.Equal(other[:], got[:]) {
|
||||
t.Fatalf("customisation tag did not domain-separate the digest")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user