6 Commits
Author SHA1 Message Date
zeekayandHanzo Dev 7a762eb446 Strengthen the relabel arm to isolate the nonce ledger
The relabel bypass tests reused one joint nonce under a new NonceID but
left the clearance QC stale, so the QC guard could have been the thing
refusing the second partial. Re-attest the relabeled cert with a QC over
its new payload: the QC guard passes, and the per-share single-use nonce
ledger is the only guard left to refuse the reused nonce. That refusal —
which keeps the (c_A-c_B)*s1 key-recovery system from ever being
assembled — is now proven in isolation on both the explicit and the bare
default ledger paths.

Co-authored-by: Hanzo Dev <[email protected]>
2026-08-21 10:57:31 -07:00
zeekay 9b175ae9b8 pulsar: the single-use guard keys on the secret share, not the public label
The nonce ledger keyed dedup on the packed w1, a public label. A swapped cert
carries a different w1, so it reserved a fresh entry and the reuse guard never
fired — the ledger recorded two first-uses of two labels while one secret was
spent twice. Two fresh signers sharing the same y-share then released two
z-partials under two challenges, and z_i^A − z_i^B = (c_A − c_B)·λ_i·s1_i
recovered the share, the master key, and a forged FIPS 204 signature on a message
no signer saw. A guard on the label cannot protect the secret the label refers to.

The reservation now keys on y_i, the secret nonce share the response actually
spends. It is constant across the two swapped certs, because they share the
share, and distinct across legitimately different nonces — so the second spend of
one share is refused where the w1 key could not see it. The committee-dedup
property the w1 key protected is preserved: the ledger is already
per-validator-share, so the same joint nonce reused across two committees that
share the victim still collapses to one key.

Reverting the reservation to the w1 key reopens the recovery and fails the test.
2026-08-21 08:41:15 -07:00
zeekay 8b1ea4ef7d pulsar: one share, one binding — and the cert is checked where it is spent
Round1 rebound. The y-share is set once and Round1 never touches it, so a second
cert replaced the challenge derived over that share while the secret stayed put.
Round2 then released a second z-partial against it, and z_A - z_B =
(c_A - c_B)·λ·s1_i hands out the share — from there the master key, and a stock
FIPS 204 signature on a message no signer saw.

The single-use ledger could not catch it. It keys on the packed w1, so a swapped
cert carries a different w1, reserves a FRESH entry, and the reuse guard never
fires: the ledger faithfully records two first uses of two labels while one
secret is spent twice. A guard on the public label cannot protect the secret the
label refers to. This one sits where a cert meets the share it will be spent
against.

The cert is also now checked on that path. VerifyNonceCert is exported,
complete, and called by nothing, so every clearance QC reached the signer
unexamined. What is self-contained is settled here — a missing QC, and a QC that
does not cover the cert's own payload. The weight and the bitmap are not: both
need the validator set size, a signer knows only its signing quorum, and
inventing that number would refuse honest certs. The caller holding the set must
run VerifyNonceCert before Round1.
2026-08-21 01:01:10 -07:00
zeekayandHanzo Dev 22731aba99 spec: cite Ringtail by its title, and state Corona's licence
references.bib gave ePrint 2024/1113 the title "Corona: Practical Two-Round
Threshold Signatures from Learning with Errors". The paper is "Ringtail:
Practical Two-Round Threshold Signatures from Learning with Errors", by the
six authors the entry already named correctly. A title is the authors' words.
The file's own header asks for exactly this — "verified against
https://eprint.iacr.org... Do not paraphrase or guess".

Ringtail is their scheme. Corona is Lux's implementation over Ring-LWE;
Pulsar retargets the same two-round structure to Module-LWE for FIPS 204.
The prose and the citation key now say so, and this spec is the NIST MPTC
submission, so it is the last place to get it wrong.

patent-notes.tex stated "Corona's open-source repo is Apache-2.0 with no
asserted patent claims". Corona is under the Lux Ecosystem License v1.2,
which reserves commercial use — no Apache grant and no Apache §3 patent
grant. The surrounding paragraph had been flattened by a rename into
"Pulsar ... is derivative of Pulsar" and "Pulsar (and therefore Pulsar) is a
fork"; it now names Corona and Ringtail where each is meant.

Dropped the residual Pulsar-M spelling, including the naming rule that
forbade writing "Pulsar" and required writing "Pulsar".

Co-authored-by: Hanzo Dev <[email protected]>
2026-07-29 11:12:14 -07:00
zeekay 160df36c07 fix(pulsar): hard-cap PartyID before signer-bitmap allocation (DoS)
Item7(a) library-side defense-in-depth, on SHIPPED pulsar v1.9.0 (the tag
cloud's consensus v1.35.30 pins). Non-breaking: CanonicalSignerSet's
2-arg signature is UNCHANGED so the existing consensus consumer compiles
and runs unmodified.

CanonicalSignerSet sized the signer bitmap at make([]byte, maxID/8+1)
with maxID taken straight from an attacker-controlled uint32 Partial.
PartyID and no upper bound — a single Partial naming PartyID near
math.MaxUint32 drives a ~512MiB allocation (memory-exhaustion DoS).

Added MaxCanonicalPartyID = 1<<20 (bitmap <= 128 KiB) and a typed
ErrPartyIDOutOfRange; CanonicalSignerSet now rejects any partial in
`valid` whose PartyID >= the cap BEFORE the sort or the allocation. The
whole slice is checked, not just the chosen subset, so a hostile PartyID
cannot hide behind the threshold cut. The cap sits far above any real
validator set (Lux runs hundreds to low thousands) so legitimate inputs
are never rejected.

This is orthogonal to — not a replacement for — the consumer's own
exact-ValidatorSetSize bound (consensus PulsarRoundSigner.Finalize, its
companion commit): the library needs no external context and stays
signature-stable, so this hardening protects any future caller lacking
that upstream guard.

New test: TestCanonicalSignerSet_RejectsOversizedPartyID (MaxUint32,
excluded-from-chosen, at-cap boundary, and the just-below-cap happy path
with a bounded bitmap). No go.mod/go.sum change.
2026-07-08 07:34:25 -07:00
zeekayandHanzo Dev fd0b427d46 refactor(layout): ref/go/pkg -> pkg, ref/go/cmd -> cmd (canonical Go layout); home pulsard daemon at pkg/pulsard
Moves the Go reference implementation to the idiomatic repo-root pkg/ (library)
+ cmd/ (genkat tool) layout, dropping the ref/go/ nesting. Imports become
github.com/luxfi/pulsar/pkg/pulsar. The pulsar DAEMON (formerly the unhomed
standalone github.com/luxfi/pulsard module) now lives canonically at
github.com/luxfi/pulsar/pkg/pulsard. No replace directives.

BREAKING: consumers must update github.com/luxfi/pulsar/ref/go/pkg/... ->
github.com/luxfi/pulsar/pkg/... and bump to v1.9.0.

Co-authored-by: Hanzo Dev <[email protected]>
2026-06-30 15:59:28 -07:00