PULSAR-V13: contain hint-path leak + land verified BCC/CEF math core

- hard-gate threshold_v03 AlgebraicAggregate (fail-closed, PULSAR-V13-HINT-LEAK)
- threshold_bcc.go: BoundaryClear + findHintToTarget (FIPS UseHint) + ML-DSA-65/87
  param guard; no production code forms c*s2/c*t0/r0/LowBits
- 6 verified tests green (gate, param guard, FindHintToTarget<->UseHint round-trip,
  BoundaryClear=>HighBits-stable+r0-bound, off-by-one edges, yield 9.8%)
- spec/threshold-mldsa-boundary-clearance.tex (BCC0/BCC+, full-w-hidden, ZK req)
- BLOCKERS: V13-HINT-LEAK + V13-W-LEAK + V13-PARTIAL-Z-PROOF + resolution criteria
This commit is contained in:
Antje Worring
2026-06-21 00:54:00 -07:00
parent 4fb0078bc8
commit 26212bf9ae
5 changed files with 706 additions and 9 deletions
+79 -1
View File
@@ -22,7 +22,85 @@ and the v1.0.7 sign-off.
## Open
None.
### PULSAR-V13-HINT-LEAK (CRITICAL)
The v0.3/v0.4 `AlgebraicAggregate*` path broadcasts `CS2 = c·λ_i·s_{2,i}`
and `CT0 = c·λ_i·t_{0,i}` **unmasked** (`ref/go/pkg/pulsar/threshold_v03.go`
round2EmitFull, ~:930-963) and reconstructs `c·s2_joint`, `c·t0_joint` at
the aggregator (~:1131-1168). Both are secret-key-derived: `c`, `λ_i`
public ⇒ `s_{2,i} = (c·λ_i)^{-1}·CS2` (one session when `c` invertible;
else accumulate). Worse, `c·s2_joint = c·s2_master` (Shamir), so revealing
the **aggregate** over varying public `c` leaks the long-term secret-key
components `s2` (and likewise `t0`) via a linear system, plus the secret
relation `A·s1 = (t1·2^d + t0) s2`; whether or not this immediately
recovers short `s1` (a lattice/preimage problem), threshold secrecy and
transcript simulation are already broken. Leaderless ⇒ every
quorum member aggregates ⇒ every corrupt validator learns it each round.
The in-code `PUBLIC-BFT-SAFETY NOTE` claiming `(z_i,cs2_i,ct0_i)` are
`(t1)`-secret is **false** — it only covers the `y_i`-masked `z_i`.
Confirmed by adversarial audit (`AUDIT-2026-06.md` re-audit) + direct code
read. Masking the individual shares is necessary but **not** sufficient —
the aggregate reconstruction still leaks the master key. The fix must
never reconstruct `c·s2`/`c·t0`/`r0`.
**Containment (landed, branch `fix/threshold-mldsa-hint-leak`):**
`Round2Sign` now **fails closed** with `ErrUnsafeThresholdV03HintPath`
unless `AllowUnsafeThresholdV03ForTests` is set (test
`TestThresholdV03DisabledByDefault`). The dangerously-false `(t1)`-secret
note is corrected.
**Replacement design + verified math core:** boundary-cleared nonces +
carry elimination (`spec/threshold-mldsa-boundary-clearance.tex`,
`threshold_bcc.go`). The hint is computed from the **public**
`w' = A·z c·t1·2^d` and `w1 = HighBits(w)` strictly via FIPS `UseHint`
(`findHintToTarget`); boundary clearance (margin `2β`) keeps the small
`c·s2` shift off the boundary; `c·t0` is structurally
`‖c·t0‖∞ ≤ τ·2^{d-1} < γ2` for **ML-DSA-65/87 only** (param-guarded).
Verified (`threshold_bcc_test.go`, all green): `FindHintToTarget↔UseHint`
round-trip, `BoundaryClear ⇒ HighBits-stable + r0-bound`, exact off-by-one
edges, offline yield ≈ **9.8 %**.
**NOT RESOLVED.** The math is verified but the production path is not built.
Resolution criteria (all required for NIST/consensus):
- [x] `AlgebraicAggregate*` disabled in production builds (hard gate).
- [x] Hint derived only via public `FindHintToTarget(w', w1)` (FIPS `UseHint`).
- [x] No production code computes `c·s2`/`c·t0`/`r0`/`LowBits(residual)`.
- [x] Boundary predicate proves the hidden `r0` bound; ML-DSA-65 scope enforced.
- [ ] `CS2`/`CT0` and all hint-secret wire fields **deleted** from production messages (reflection test).
- [ ] **Full `w` never public / reconstructible** (PULSAR-V13-W-LEAK) — needs the ZK clearance proof.
- [ ] Partial-`z` correctness proof (PULSAR-V13-PARTIAL-Z-PROOF).
- [ ] Canonical, non-grindable nonce selection.
- [ ] DKG never reconstructs the master key / `t0`; certifies the `t0` bound.
- [ ] Rejected attempts simulatable, not publicly leaked; coarse abort classes.
- [ ] Tree aggregation (z-sums + bitmaps + proof roots) for ~1000 signers.
- [ ] Two-certificate consensus artifact (ML-DSA sig + signer bitmap/transcript root).
- [ ] Final sigs verify under ≥2 independent FIPS 204 verifiers on the BCC path.
- [ ] External cryptographic review of the no-MPC leaderless instantiation.
### PULSAR-V13-W-LEAK (CRITICAL — replacement-design hazard)
The boundary-cleared nonce certificate must **not** publish the full
commitment `w = A·y`. Once `z` is assembled, `w' = A·z c·t1·2^d` is
public, so anyone with full `w` computes `w' w = c·t0 c·s2 = Δ` — the
same challenge-multiplied long-term-secret residual as PULSAR-V13-HINT-LEAK,
in a different form. Production may publish only `w1 = HighBits(w)`, a
commitment to `w`, and a **zero-knowledge boundary-clearance proof** (that a
hidden `w = A·y` is boundary-clear), never full `w`, `w_i` shares that
reconstruct it, `LowBits(w)`, or `w' w`. The current `BoundaryClear(w)`
Go predicate and `spec` are **debug-oracle/prototype**: production needs the
ZK proof machinery, which does not yet exist ⇒ the BCC/CEF signing path is
**prototype, not production**, even though the arithmetic tests pass.
### PULSAR-V13-PARTIAL-Z-PROOF (HIGH — consensus robustness)
BCC/CEF removes the hint-path leakage, but leaderless consensus also needs
proof-carrying `z`-partials: each signer proves `z_i = λ_i·y_i + c·λ_i·s_{1,i}`
bound to `(session_id, nonce_id, party_id, DKG share commitment, nonce
commitment)` without revealing `y_i`/`s_{1,i}`. Otherwise one bad partial
fails the aggregate with no clean blame path (leaderless DoS). Verify
partials **without** `c·s2_i`/`c·t0_i`/`r0_i`/hint shares (those fields must
not exist).
## Forward-looking (v1.2)
+167
View File
@@ -0,0 +1,167 @@
package pulsar
import "errors"
// Boundary-Cleared / Carry-Elimination threshold ML-DSA (Pulsar-BCC/CEF).
//
// No-MPC / no-TEE hint path replacing the leaking AlgebraicAggregate
// reconstruction (BLOCKERS.md PULSAR-V13-HINT-LEAK,
// spec/threshold-mldsa-boundary-clearance.tex).
//
// CONFORMANCE RULE (central invariant): the final hint h is determined by
// public nonce-region certificates and the public challenge — NEVER by
// reconstructing the secret residual. Nothing here forms c·s2, c·t0, r0,
// or LowBits(w + Δ). The hint is derived from the public reconstructed
// w' = A·z c·t1·2^d and the public target w1 strictly via the FIPS 204
// UseHint primitive (no informal "±1 corrector").
var (
// ErrUnsafeThresholdV03HintPath gates the disabled, leaking
// AlgebraicAggregate path (PULSAR-V13-HINT-LEAK).
ErrUnsafeThresholdV03HintPath = errors.New(
"pulsar: threshold_v03 AlgebraicAggregate path is disabled " +
"(PULSAR-V13-HINT-LEAK: broadcasts c*s2/c*t0 hint-path secret " +
"material); use the BCC/CEF signing path")
// ErrBCCParamSet rejects parameter sets outside the proven scope.
ErrBCCParamSet = errors.New(
"pulsar: Pulsar-BCC/CEF is proven for ML-DSA-65/87 only; this " +
"parameter set violates the ||c*t0||_inf < gamma2 bound")
// ErrNoFIPSHint means no valid FIPS 204 hint reaches the target w1 for
// some coefficient (boundary/region violated, or weight > omega).
ErrNoFIPSHint = errors.New(
"pulsar: no valid FIPS hint reaches target w1 (boundary/region " +
"violated); consume the nonce and retry")
)
// bccD is the ML-DSA Power2Round low-bit drop; t0 ∈ [2^(d1), 2^(d1)].
const bccD = 13
// bccParams returns (gamma2, beta = τ·η, omega) for a mode, and ok=false
// for any FIPS 204 parameter set outside the proven BCC scope. The scope
// condition is ‖c·t0‖∞ ≤ τ·2^(d1) < γ2: it makes the FIPS c·t0 check
// vacuous and bounds the carry to ±1 high-bit. It holds for ML-DSA-65
// (49·4096 < 261888) and ML-DSA-87 (60·4096 < 261888) but NOT ML-DSA-44
// (39·4096 > 95232). Callers MUST gate on ok before signing.
func bccParams(mode Mode) (gamma2, beta, omega uint32, ok bool) {
tau, om, _, g2 := modeTauOmega(mode)
_, _, eta := modeShape(mode)
if uint32(tau)*(1<<(bccD-1)) >= g2 {
return 0, 0, 0, false
}
return g2, uint32(tau) * eta, uint32(om), true
}
// boundaryThreshold is the centered-low-bits magnitude below which a
// coefficient is boundary-clear: |a0_centered(w_j)| < γ2 2β. The 2β
// margin covers BOTH the HighBits boundary (the ±β shift c·s2 cannot
// change HighBits) AND the FIPS r0 rejection edge (‖LowBits(w c·s2)‖∞ <
// γ2 β stays in range) — high-bit stability alone is insufficient.
func boundaryThreshold(gamma2 uint32, beta uint32) uint32 { return gamma2 - 2*beta }
// centeredLowBits returns the FIPS Decompose low part of a, centered into
// (−γ2, γ2]. a must be normalized to [0, q).
func centeredLowBits(a uint32, gamma2 uint32) int32 {
a0plusQ, _ := decompose(a, gamma2)
a0 := int32(a0plusQ)
if a0plusQ > (mldsaQ-1)/2 {
a0 -= mldsaQ
}
return a0
}
// highBitsCoeff returns the FIPS Decompose high part a1 of a.
func highBitsCoeff(a uint32, gamma2 uint32) uint32 {
_, a1 := decompose(a, gamma2)
return a1
}
// boundaryClearCoeff reports whether coefficient a is far enough from
// every HighBits boundary that a hidden ±β shift cannot move its high bits
// and its hidden r0 stays in FIPS range.
func boundaryClearCoeff(a uint32, gamma2 uint32, beta uint32) bool {
a0 := centeredLowBits(a, gamma2)
if a0 < 0 {
a0 = -a0
}
return uint32(a0) < boundaryThreshold(gamma2, beta)
}
// BoundaryClear is the offline, message-independent, fully-public nonce-
// certification predicate: every coefficient of the public commitment w is
// boundary-clear for the c·s2 perturbation. A surviving nonce guarantees
// HighBits(w c·s2) = HighBits(w) = w1 and the FIPS r0-norm check for
// every valid challenge — without ever computing c·s2.
func BoundaryClear(w polyVec, gamma2 uint32, beta uint32) bool {
for i := range w {
for j := 0; j < mldsaN; j++ {
if !boundaryClearCoeff(w[i][j], gamma2, beta) {
return false
}
}
}
return true
}
// highBitsVec returns HighBits(w) coefficient-wise. w must be normalized.
func highBitsVec(w polyVec, gamma2 uint32) polyVec {
out := make(polyVec, len(w))
for i := range w {
for j := 0; j < mldsaN; j++ {
out[i][j] = highBitsCoeff(w[i][j], gamma2)
}
}
return out
}
// useHint applies one FIPS 204 (Algorithm 40) hint bit to coefficient r,
// returning the corrected high part r1. m = (q1)/(2γ2) high-bit buckets.
// r must be normalized to [0, q).
func useHint(hbit, r, gamma2 uint32) uint32 {
m := uint32((mldsaQ - 1) / (2 * gamma2))
r0plusQ, r1 := decompose(r, gamma2)
if hbit == 0 {
return r1
}
r0 := int32(r0plusQ)
if r0plusQ > (mldsaQ-1)/2 {
r0 -= mldsaQ
}
if r0 > 0 {
return (r1 + 1) % m
}
return (r1 + m - 1) % m
}
// findHintToTarget derives the ML-DSA hint from PUBLIC data only — the
// public reconstructed wPrime = A·z c·t1·2^d and the public target w1 =
// HighBits(w). It NEVER forms c·s2, c·t0, or r0. The hint bit is not a
// signed correction: a coefficient takes h_j = 0 when HighBits already
// matches, h_j = 1 iff UseHint(1, ·) reaches the target, else there is no
// valid FIPS hint (boundary/region violated) and signing must consume the
// nonce and retry. Returns the hint and ok; ok=false ⇒ ErrNoFIPSHint.
//
// Theorem: findHintToTarget(wPrime, w1) = (h, true) iff
// UseHint(h, wPrime) = w1 and weight(h) ≤ omega.
func findHintToTarget(wPrime, targetW1 polyVec, gamma2, omega uint32) (polyVec, bool) {
h := make(polyVec, len(wPrime))
var weight uint32
for i := range wPrime {
for j := 0; j < mldsaN; j++ {
switch {
case highBitsCoeff(wPrime[i][j], gamma2) == targetW1[i][j]:
h[i][j] = 0
case useHint(1, wPrime[i][j], gamma2) == targetW1[i][j]:
h[i][j] = 1
weight++
default:
return nil, false // no valid FIPS hint for this coefficient
}
}
}
if weight > omega {
return nil, false
}
return h, true
}
+186
View File
@@ -0,0 +1,186 @@
package pulsar
import (
"math/rand"
"testing"
)
// These are DEBUG-ORACLE tests: they reconstruct the hidden commitment w
// directly to verify the BCC/CEF arithmetic, lemmas, and yield. Production
// code must NOT compute or publish full w (PULSAR-V13-W-LEAK); it publishes
// only w1, a commitment to w, and a ZK boundary-clearance proof.
func randPolyVec(rng *rand.Rand, k int) polyVec {
w := make(polyVec, k)
for i := 0; i < k; i++ {
for j := 0; j < mldsaN; j++ {
w[i][j] = uint32(rng.Int63n(mldsaQ))
}
}
return w
}
func subMod(a uint32, delta int32) uint32 {
v := (int64(a) - int64(delta)) % mldsaQ
if v < 0 {
v += mldsaQ
}
return uint32(v)
}
// The leaking AlgebraicAggregate path must be disabled by default
// (PULSAR-V13-HINT-LEAK), independent of any other signer state.
func TestThresholdV03DisabledByDefault(t *testing.T) {
s := &AlgebraicThresholdSigner{}
if _, _, err := s.Round2Sign(nil, nil); err != ErrUnsafeThresholdV03HintPath {
t.Fatalf("threshold_v03 must fail closed by default; got err=%v", err)
}
}
// BCC/CEF is proven only where ||c·t0||_inf < gamma2 (ML-DSA-65/87, not 44).
func TestBCCParamGuard(t *testing.T) {
for _, m := range []Mode{ModeP65, ModeP87} {
if _, _, _, ok := bccParams(m); !ok {
t.Fatalf("mode %d must be in BCC scope", m)
}
}
if _, _, _, ok := bccParams(ModeP44); ok {
t.Fatalf("ML-DSA-44 must be rejected (tau*2^(d-1) >= gamma2)")
}
// Sanity on the c·t0 bound itself.
for _, m := range []Mode{ModeP65, ModeP87} {
tau, _, _, g2 := modeTauOmega(m)
if uint32(tau)*(1<<(bccD-1)) >= g2 {
t.Fatalf("mode %d: c·t0 bound should be vacuous", m)
}
}
}
// Theorem: findHintToTarget(w', target) = (h, true) implies
// UseHint(h, w') = target coefficient-wise. The hint bit is validated via
// FIPS UseHint, never an informal ±1 corrector.
func TestFindHintToTargetMatchesUseHint(t *testing.T) {
gamma2, _, omega, _ := bccParams(ModeP65)
k, _, _ := modeShape(ModeP65)
rng := rand.New(rand.NewSource(2))
checked := 0
for iter := 0; iter < 3000; iter++ {
wPrime := randPolyVec(rng, k)
// Build a reachable, omega-sparse target like a real ML-DSA hint:
// baseline all h=0 (HighBits), then flip up to omega coefficients to
// the h=1 result.
target := highBitsVec(wPrime, gamma2)
nflip := rng.Intn(int(omega) + 1)
for f := 0; f < nflip; f++ {
i := rng.Intn(k)
j := rng.Intn(mldsaN)
target[i][j] = useHint(1, wPrime[i][j], gamma2)
}
h, ok := findHintToTarget(wPrime, target, gamma2, omega)
if !ok {
continue // weight > omega for random targets; skip
}
checked++
for i := 0; i < k; i++ {
for j := 0; j < mldsaN; j++ {
if useHint(h[i][j], wPrime[i][j], gamma2) != target[i][j] {
t.Fatalf("UseHint(h,w') != target at [%d][%d]", i, j)
}
}
}
}
if checked == 0 {
t.Fatal("no reachable targets exercised")
}
t.Logf("verified UseHint(findHintToTarget(w',t),w')==t on %d vectors", checked)
}
// Core BCC+ lemma (debug oracle): a boundary-clear w keeps HighBits stable
// under every hidden ||c·s2||_inf <= beta shift, and the hidden r0 stays in
// the FIPS rejection range — without the protocol ever forming c·s2 or r0.
func TestBoundaryClearImpliesHighBitsStable(t *testing.T) {
gamma2, beta, _, ok := bccParams(ModeP65)
if !ok {
t.Fatal("P65 params")
}
k, _, _ := modeShape(ModeP65)
rng := rand.New(rand.NewSource(1))
cleared := 0
for iter := 0; iter < 4000; iter++ {
w := randPolyVec(rng, k)
if !BoundaryClear(w, gamma2, beta) {
continue
}
cleared++
hb0 := highBitsVec(w, gamma2)
for i := 0; i < k; i++ {
for j := 0; j < mldsaN; j++ {
delta := int32(rng.Intn(int(2*beta+1))) - int32(beta) // [-beta, beta]
shifted := subMod(w[i][j], delta)
if highBitsCoeff(shifted, gamma2) != hb0[i][j] {
t.Fatalf("HighBits moved under |c·s2|<=beta on boundary-clear w [%d][%d]", i, j)
}
r0 := centeredLowBits(shifted, gamma2)
if r0 < 0 {
r0 = -r0
}
if uint32(r0) >= gamma2-beta {
t.Fatalf("hidden r0 bound violated on boundary-clear w [%d][%d]", i, j)
}
}
}
}
if cleared == 0 {
t.Fatal("no boundary-clear nonces sampled")
}
t.Logf("verified HighBits-stable + r0-bound on %d boundary-clear nonces", cleared)
}
// Exact off-by-one audit of the 2β margin against FIPS Decompose.
func TestBoundaryClearEdgeCases(t *testing.T) {
gamma2, beta, _, _ := bccParams(ModeP65)
thr := int32(boundaryThreshold(gamma2, beta)) // gamma2 - 2*beta
mk := func(a0 int32) uint32 { // coefficient with centered low bits a0 in bucket 1
v := (int64(2*gamma2) + int64(a0)) % mldsaQ
if v < 0 {
v += mldsaQ
}
return uint32(v)
}
cases := []struct {
a0 int32
want bool
}{
{thr - 1, true}, {thr, false}, {thr + 1, false},
{-(thr - 1), true}, {-thr, false}, {-(thr + 1), false},
{0, true},
}
for _, c := range cases {
a := mk(c.a0)
if got := centeredLowBits(a, gamma2); got != c.a0 {
t.Fatalf("centeredLowBits(mk(%d))=%d", c.a0, got)
}
if got := boundaryClearCoeff(a, gamma2, beta); got != c.want {
t.Fatalf("boundaryClearCoeff(a0=%d)=%v want %v (thr=%d)", c.a0, got, c.want, thr)
}
}
}
// Measure the real offline boundary-clearance yield for ML-DSA-65.
func TestBoundaryClearanceYield(t *testing.T) {
gamma2, beta, _, _ := bccParams(ModeP65)
k, _, _ := modeShape(ModeP65)
rng := rand.New(rand.NewSource(3))
const trials = 3000
clear := 0
for i := 0; i < trials; i++ {
if BoundaryClear(randPolyVec(rng, k), gamma2, beta) {
clear++
}
}
yield := float64(clear) / trials
t.Logf("ML-DSA-65 boundary-clear yield (margin 2β=%d, γ2=%d): %.4f", 2*beta, gamma2, yield)
if yield < 0.03 || yield > 0.25 {
t.Fatalf("yield %.4f outside expected ~0.10 band", yield)
}
}
+24 -8
View File
@@ -439,6 +439,12 @@ type AlgebraicThresholdSigner struct {
NodeID NodeID
Share *AlgebraicKeyShare
// AllowUnsafeThresholdV03ForTests opts INTO the disabled, leaking
// AlgebraicAggregate hint path (PULSAR-V13-HINT-LEAK). Default false ⇒
// Round2Sign fails closed with ErrUnsafeThresholdV03HintPath. Set only
// in tests exercising the legacy vector; NEVER in production.
AllowUnsafeThresholdV03ForTests bool
SessionID [16]byte
Attempt uint32
@@ -754,15 +760,25 @@ func (s *AlgebraicThresholdSigner) Round2W(round1 []*AlgebraicRound1Message) (*A
// (This party's own w_i is taken from local state — do not include it
// in peerW.)
//
// PUBLIC-BFT-SAFETY NOTE: this party's contribution (z_i, cs2_i, ct0_i)
// is INFORMATION-THEORETICALLY (t-1)-secret about its share material —
// the per-party arithmetic is z_i = y_i + c·λ_i·s_{1,i} where y_i is
// fresh randomness from this party's RNG and c is a public challenge.
// An adversary that obtains t-1 such contributions cannot recover any
// single party's s_{1,i} because the y_i mask hides it (each y_i is
// drawn from the FIPS 204 (-γ_1', γ_1'] uniform distribution, which
// is sufficient to mask under the M-LWE assumption).
// ⚠️ SECURITY-CRITICAL — PULSAR-V13-HINT-LEAK (see BLOCKERS.md).
// The y_i mask hides s_{1,i} in z_i = y_i + c·λ_i·s_{1,i} (correct). But
// the cs2_i = c·λ_i·s_{2,i} and ct0_i = c·λ_i·t_{0,i} fields emitted by
// round2EmitFull below carry NO mask. Since c and λ_i are public,
// s_{2,i} = (c·λ_i)^{-1}·cs2_i is recoverable; and the aggregate
// Σ cs2_i = c·s2_master (likewise ct0_i → t0) leaks long-term ML-DSA
// secret-key material over varying-c sessions plus the secret relation
// A·s1 = (t1·2^d + t0) s2 — breaking threshold secrecy and transcript
// simulation. The prior claim that (z_i, cs2_i, ct0_i) are
// (t-1)-secret was FALSE — it only held for z_i. This path is being
// replaced by boundary-cleared nonces + carry elimination
// (spec/threshold-mldsa-boundary-clearance.tex), which never forms
// c·s2/c·t0/r0. Do NOT rely on this path for production secrecy.
func (s *AlgebraicThresholdSigner) Round2Sign(round1 []*AlgebraicRound1Message, peerW map[NodeID]polyVec) (*AlgebraicRound2Message, *AbortEvidence, error) {
// PULSAR-V13-HINT-LEAK: this path broadcasts c*s2/c*t0 hint-path secret
// material; fail closed in production. Opt in only for legacy tests.
if !s.AllowUnsafeThresholdV03ForTests {
return nil, nil, ErrUnsafeThresholdV03HintPath
}
return s.round2EmitFull(round1, peerW)
}
+250
View File
@@ -0,0 +1,250 @@
\documentclass[11pt]{article}
\usepackage{amsmath,amssymb,amsthm}
\usepackage[margin=1in]{geometry}
\newtheorem{lemma}{Lemma}
\newtheorem{theorem}{Theorem}
\newtheorem{definition}{Definition}
\theoremstyle{remark}\newtheorem{remark}{Remark}
\title{Pulsar Threshold ML-DSA via Boundary-Cleared Nonces and Carry
Elimination\\(No MPC, No TEE) --- BCC0 / BCC+}
\author{Lux Core Team --- \texttt{github.com/luxfi/pulsar}}
\date{}
\begin{document}
\maketitle
\section{The leak this replaces}
The v0.3/v0.4 \texttt{AlgebraicAggregate} path reconstructs $c\,\mathbf
s_2^{\mathrm{joint}}$ and $c\,\mathbf t_0^{\mathrm{joint}}$ at the
aggregator to form the FIPS hint. But $\mathbf s_2^{\mathrm{joint}}$ is
the master $\mathbf s_2$ (Shamir reconstruction) and $c$ is public and
varies per session, so revealing $c\,\mathbf s_2^{\mathrm{joint}}$ over
sessions is a linear system in the fixed $\mathbf s_2$ --- one invertible
$c$ recovers it; then $A\mathbf s_1 = \mathbf t - \mathbf s_2$ recovers
$\mathbf s_1$, a total key break. In the leaderless model every quorum
member aggregates, so every corrupt validator learns it. \textbf{Masking
the per-party shares does not help: the masks cancel in the aggregate the
protocol must reconstruct.} The cure is never to form the secret residual.
\section{The invariant and the whole game}
With $\mathbf w = A\mathbf y$, $\mathbf z = \mathbf y + c\,\mathbf s_1$,
$\mathbf t = A\mathbf s_1 + \mathbf s_2 = \mathbf t_1 2^d + \mathbf t_0$,
the verifier recomputes
\[
\mathbf w' \;=\; A\mathbf z - c\,\mathbf t_1 2^d
\;=\; \mathbf w + c\,\mathbf t_0 - c\,\mathbf s_2
\;=\; \mathbf w + \Delta, \qquad \Delta := c\,\mathbf t_0 - c\,\mathbf
s_2,
\]
and accepts iff $\mathrm{UseHint}(\mathbf h, \mathbf w') = \mathbf w_1$
where $\mathbf w_1 = \mathrm{HighBits}(\mathbf w)$ fixed $c = H(\mu \| \mathbf
w_1)$.
\begin{theorem}[No-leak target]
Produce a public $\mathbf h$ with $\mathrm{UseHint}(\mathbf h, \mathbf w +
\Delta) = \mathrm{HighBits}(\mathbf w)$ \emph{without} revealing $\Delta$,
$\mathrm{LowBits}(\mathbf w + \Delta)$, $c\,\mathbf s_2$, $c\,\mathbf
t_0$, or $r_0$.
\end{theorem}
\section{Conformance rule (central invariant)}
\label{sec:rule}
\textbf{The final hint $\mathbf h$ must be determined by public
nonce-region certificates and the public challenge --- never by
reconstructing the secret residual.} For BCC0 the certificate guarantees
$\mathbf h = 0$; for BCC+ it guarantees a publicly-determined hint
pattern. \emph{Any implementation that computes $\mathbf h$ by first
forming $c\,\mathbf s_2$, $c\,\mathbf t_0$, $r_0$, or
$\mathrm{LowBits}(\mathbf w + \Delta)$ is non-conformant.} Note $\mathbf
w' = A\mathbf z - c\,\mathbf t_1 2^d$ is itself \emph{public} (computable
by the verifier from $A,\mathbf z,c,\mathbf t_1$); reading $\mathbf h$ off
$(\mathbf w', \mathbf w_1)$ reveals only $\mathbf h$, which is in the
signature anyway --- it is \emph{not} residual reconstruction.
\subsection{Full $\mathbf w$ must be hidden (PULSAR-V13-W-LEAK)}
The full commitment $\mathbf w = A\mathbf y$ must \emph{never} be public or
reconstructible by a $t-1$ coalition. Since $\mathbf w' = A\mathbf z -
c\,\mathbf t_1 2^d$ becomes public once $\mathbf z$ is assembled, anyone
holding full $\mathbf w$ computes $\mathbf w' - \mathbf w = c\,\mathbf t_0
- c\,\mathbf s_2 = \Delta$ --- the same long-term-secret residual as the
original leak. \textbf{Public:} $\mathbf w_1$, a commitment to $\mathbf w$,
a \emph{zero-knowledge} boundary-clearance proof, and the final $(\mathbf
z,\mathbf h,\text{sig})$. \textbf{Forbidden:} full $\mathbf w$, $\mathbf
w_i$ shares reconstructing it, $\mathrm{LowBits}(\mathbf w)$, $\mathbf w' -
\mathbf w$. Hence $\textsc{BoundaryClear}(\mathbf w)$ on \emph{public}
$\mathbf w$ is \textbf{debug-oracle only}; production replaces it with a ZK
proof that a hidden $\mathbf w = A\mathbf y$ satisfies the predicate,
revealing none of $\mathbf w$, $\mathrm{LowBits}(\mathbf w)$, or per-
coefficient distances. \textbf{Until that ZK machinery exists the BCC/CEF
signing path is prototype, not production.}
\section{Profiles}
\subsection{BCC0 --- conservative $\mathbf h = 0$}
Use only nonces whose hidden shift cannot move any coefficient across a
high-bit boundary: $\mathrm{HighBits}(\mathbf w + \Delta) =
\mathrm{HighBits}(\mathbf w)$, so $\mathbf h = 0$.
\begin{lemma}[BCC0 stability]\label{lem:bcc0}
$\textsc{BoundaryClear}(\mathbf w, B_\Delta) \wedge \|\Delta\|_\infty \le
B_\Delta \;\Rightarrow\; \mathrm{HighBits}(\mathbf w + \Delta) =
\mathrm{HighBits}(\mathbf w)$, whence $\mathrm{UseHint}(0, \mathbf w') =
\mathbf w_1$ and unmodified FIPS 204 accepts.
\end{lemma}
\textbf{BCC0 is infeasible for ML-DSA-65} (\S\ref{sec:bounds}): the worst-
case $B_\Delta$ exceeds $\gamma_2$, so no coefficient can clear. Retained
as a definition / fallback profile only, not used.
\subsection{BCC+ --- carry elimination, nonzero $\mathbf h$}
Allow $\mathbf h \neq 0$, but derive it from public data. The shift splits
into a \emph{small} part $-c\,\mathbf s_2$ and a \emph{large} part
$c\,\mathbf t_0$. Boundary-clear \emph{only} the small part; the large
part is resolved by the public hint.
\begin{definition}[Per-coefficient region]
For coefficient $w_j$ and margin $B$ (covering $\|c\,\mathbf
s_2\|_\infty$), classify by FIPS \texttt{Decompose}:
$\textsc{SafeZeroCarry}$ if $\mathrm{dist}(w_j) > B$ and the $c\,\mathbf
t_0$ carry cannot cross; $\textsc{SafeKnownCarry}$ if the carry is
resolvable to a single public bit from $\mathbf w'$; $\textsc{Unsafe}$
otherwise. Accept a nonce iff no coefficient is $\textsc{Unsafe}$.
\end{definition}
\begin{lemma}[BCC+ correctness]\label{lem:bccplus}
If $\textsc{BoundaryClear}(\mathbf w, 2\beta)$ (clears $c\,\mathbf s_2$)
and $\|c\,\mathbf t_0\|_\infty < \alpha$ (so each coefficient's high bits
move by at most $\pm 1$), then (i) $\mathrm{HighBits}(\mathbf w -
c\,\mathbf s_2) = \mathbf w_1$ and $\|\mathrm{LowBits}(\mathbf w -
c\,\mathbf s_2)\|_\infty < \gamma_2 - \beta$ (the FIPS $r_1{=}\mathbf
w_1$, $r_0$-norm conditions); and (ii) the public hint $h_j := [\,
\mathrm{HighBits}(w'_j) \neq w_{1,j}\,]$ equals the FIPS hint
$\mathrm{MakeHint}(-c\,\mathbf t_0, \mathbf w - c\,\mathbf s_2)$ and
satisfies $\mathrm{UseHint}(\mathbf h, \mathbf w') = \mathbf w_1$.
\end{lemma}
\emph{Reconciliation with ``region-derived'':} the $c\,\mathbf t_0$ carry
depends on the secret $\mathbf t_0$ and the online $c$, so it cannot be
fixed fully offline; the \emph{offline} certificate clears $c\,\mathbf
s_2$ and bounds the carry to $\pm 1$, and the \emph{online} step reads the
carry bit off the public $\mathbf w'$. Both inputs ($\mathbf w'$, $\mathbf
w_1$) are public; no residual is formed. This is BCC+ \S2A realised.
\section{Concrete bounds (ML-DSA-65)}
\label{sec:bounds}
$q{=}8380417,\ d{=}13,\ \tau{=}49,\ \eta{=}4,\ \gamma_2{=}261888,\
\alpha{=}2\gamma_2{=}523776,\ \beta{=}\tau\eta{=}196,\ \omega{=}55$.
\begin{itemize}
\item $\|c\,\mathbf s_2\|_\infty \le B_{s_2} = \tau\eta = 196$.
\item $\|c\,\mathbf t_0\|_\infty \le \tau\,2^{d-1} = 49\cdot4096 = 200704
< \gamma_2 < \alpha$: the FIPS $\|c\,\mathbf t_0\|<\gamma_2$ check is
\emph{vacuous} (always holds), and the carry moves $\mathrm{HighBits}$
by at most $\pm1$ per coefficient. No check, no reconstruction.
\item \textbf{BCC0 infeasible:} needs clearance $> B_\Delta = B_{s_2} +
\tau\,2^{d-1} \approx 401555 > \gamma_2 = 261888$ (max possible
clearance). Yield $0$.
\item \textbf{BCC+ feasible:} clear only $c\,\mathbf s_2$ with margin $B =
2\beta = 392$ (covers the $\mathrm{HighBits}$ boundary and the $r_0 <
\gamma_2-\beta$ edge). Per-coefficient clear prob $\approx 1 -
B/\gamma_2 = 0.9985$; all-$256{\cdot}6$ yield $\approx 0.10$ (measure;
raise margin only, never lower). $c\,\mathbf t_0$ contributes the
nonzero, $\omega$-sparse public hint.
\end{itemize}
\section{Proof obligations}
\paragraph{BCC0 chain (Lemma~\ref{lem:bcc0}).} $\mathbf w{=}A\mathbf y$;
$\mathbf z{=}\mathbf y{+}c\mathbf s_1$; $A\mathbf z{-}c\mathbf t_1 2^d
{=}\mathbf w{+}\Delta$; $\|\Delta\|_\infty\le B_\Delta$;
$\textsc{BoundaryClear}(\mathbf w,B_\Delta)$ $\Rightarrow$
$\mathrm{HighBits}(\mathbf w{+}\Delta){=}\mathbf w_1$ $\Rightarrow$
$\mathrm{UseHint}(0,\cdot){=}\mathbf w_1$ $\Rightarrow$ accept.
\paragraph{BCC+ / CEF chain (Lemma~\ref{lem:bccplus}).} as above with the
public hint; $h$ region/public-$\mathbf w'$-derived; for all $\Delta$ with
$\|\Delta\|_\infty\le B_\Delta$, $\mathrm{UseHint}(\mathbf h,\mathbf w{+}
\Delta){=}\mathbf w_1$.
\paragraph{Soundness.} A malicious signer's bad $\mathbf z_i$ yields a
wrong $\mathbf w'$ and a wrong $\mathbf h$ $\Rightarrow$ the mandatory
final unmodified-FIPS verify \emph{fails} (identifiable abort via the
per-party $\mathbf z_i$ proof/MAC). \textbf{Never fall back to forming
$c\,\mathbf s_2$/$c\,\mathbf t_0$/$r_0$.}
\paragraph{Transcript privacy / simulatability.} The transcript holds
$\{\mathbf w,\mathbf w_1,c,\mathbf z,\mathbf h,\text{commitments},
\text{proofs}\}$ and \emph{not} $c\,\mathbf s_{2,i}$, $c\,\mathbf
t_{0,i}$, $c\,\mathbf s_2^{\mathrm{joint}}$, $c\,\mathbf
t_0^{\mathrm{joint}}$, $\Delta$, $r_0$, $\mathrm{LowBits}(\mathbf
w{+}\Delta)$. A simulator reproduces it from $(pk, m, \text{final sig},
\text{signer bitmap}, \text{corrupt shares})$ without honest shares.
\paragraph{Conditioned-nonce security (hardest).} Conditioning the pool on
the public predicate $\textsc{BoundaryClear}(\mathbf w, 2\beta)$ (resp.\
$\textsc{AllRegionsSafe}$) must not weaken EUF-CMA: $\mathbf w = A\mathbf
y$ is independent of the key, and the surviving $\mathbf y$ distribution
must remain a valid rejection-sampled nonce distribution (or the signature
distribution must remain simulatable). \emph{This is a required proof, not
an assumption; if \textsc{2B nonce shaping} is used, the distribution
proof is mandatory before deployment.}
\section{Wire format (deletion audit)}
\textbf{Remove} \texttt{CS2}, \texttt{CT0} (and any \texttt{cs2},
\texttt{ct0}, \texttt{D2}, \texttt{D0}, \texttt{R0Share},
\texttt{LowBitsShare}, \texttt{HintInput}, \texttt{HintShare}) from the
Round-2 message and the $\tau_2$ transcript. Old fields fail
deserialization outside an explicit legacy-test parser.
\begin{verbatim}
type BoundaryNonceCert struct {
NonceID [32]byte; PKEpoch uint64; CommitteeID [32]byte
SignerSetRoot [32]byte; W1 PackedHighBits; WCommitment []byte
ClearanceProof []byte; Margin uint32; CommitRoot [32]byte; Consumed bool }
// NO full W field (PULSAR-V13-W-LEAK); W is debug-oracle only.
type ZPartial struct {
PartyID PartyID; NonceID [32]byte; SessionID [32]byte
ZShare PackedPolyVecL; Proof []byte; MAC []byte }
type PulsarSignature struct { C PackedChallenge; Z PackedPolyVecL; H PackedHint }
\end{verbatim}
No \texttt{CS2/CT0/D2/D0/R0Share/LowBitsShare/HintInput/HintShare}.
\section{Protocol}
\paragraph{Offline (message-independent) nonce pool.} Sample $\mathbf
y_i$; commit. The aggregate $\mathbf w = \sum\lambda_i\mathbf w_i$ is
\emph{hidden}: in production each signer proves in zero knowledge that the
hidden $\mathbf w$ is boundary-clear, publishing only $\mathbf w_1$, a
commitment to $\mathbf w$, and the clearance proof (PULSAR-V13-W-LEAK; the
debug oracle may form $\mathbf w$ to classify regions vs $B$). If
\textsc{AllRegionsSafe}, store the cert ($\mathbf w_1,\text{WCommitment},
\text{ClearanceProof},\text{CommitRoot}$); else discard. Run continuously off
the consensus path.
\paragraph{Online sign.} Pick an unused cert; $\mu = H(pk\|ctx\|m)$, $c =
H(\mu\|\mathbf w_1)$; collect $\mathbf z_i = \lambda_i\mathbf y_i +
c\lambda_i\mathbf s_{1,i}$; $\mathbf z = \sum\mathbf z_i$; reject to a
fresh nonce if $\|\mathbf z\|_\infty \ge \gamma_1-\beta$; compute public
$\mathbf w' = A\mathbf z - c\mathbf t_1 2^d$ and $\mathbf h$ (BCC0:
$\mathbf 0$; BCC+: $h_j=[\mathrm{HighBits}(w'_j)\neq w_{1,j}]$); reject if
$\mathrm{wt}(\mathbf h) > \omega$; emit $\textsc{sigEncode}(\tilde
c,\mathbf z,\mathbf h)$; assert unmodified FIPS 204 verify (failure $=$
bug).
\section{Fallback (Option 3)}
If unmodified-FIPS-204-byte-equality is not mandatory, use the
Raccoon/Ringtail-style threshold-friendly hint scheme --- this is exactly
the \textbf{Corona} route (\texttt{luxfi/corona}, LP-4440): public
threshold signature, leaderless, no MPC/TEE, but \emph{not} FIPS-byte-
equal.
\section{Scale / consensus notes}
For $\sim$1000+ validators: generate boundary-safe nonce certs
continuously offline; online consensus only tree-aggregates $\mathbf z$
shares; the final signature is ordinary ML-DSA; the QC carries the signer
bitmap $+$ transcript root. Do \emph{not} boundary-filter online per
message.
\section{Practical risks}
(1) usable safe-nonce rate; (2) the conditioned-nonce distribution proof;
(3) exact FIPS boundary classification (against \texttt{Decompose}, not
integer rounding); (4) keeping the margin small enough for yield while
covering $c\,\mathbf s_2$. If BCC+ yield is poor, prefer \textsc{2B nonce
shaping} (with its distribution proof) over weakening the margin.
\section{Phased plan + TALUS alignment}
\textbf{Phase 1:} BCC+ as above --- this spec, Lemmas~\ref{lem:bcc0},
\ref{lem:bccplus} machine-checked (\texttt{Crypto/Pulsar/Boundary\-
Clearance.lean}) against the FIPS \texttt{Decompose}/\texttt{HighBits}/
\texttt{MakeHint}/\texttt{UseHint} definitions, Go impl reusing the
existing \texttt{decompose}/\texttt{power2Round} primitives, the
regression / no-residual / distribution / CIRCL$+$pq-crystals interop
tests on the BCC path, and the offline-yield measurement.
\textbf{Phase 2:} \textsc{2B nonce shaping} if yield demands, with the
distribution proof. \textbf{Phase 3:} align terminology and proof
obligations with the TALUS boundary-clearance / carry-elimination
construction.
\end{document}