fix(crypto): align BLS v3 with NUT-00/13 spec hardening#1012
Closed
robwoodgate wants to merge 1 commit into
Closed
fix(crypto): align BLS v3 with NUT-00/13 spec hardening#1012robwoodgate wants to merge 1 commit into
robwoodgate wants to merge 1 commit into
Conversation
Three spec-alignment fixes informed by a recent NUTs review pass:
NUT-00 Point Validation. `PublicKey.__init__` now enforces (a) explicit
identity rejection on parsed bytes (blst's `uncompress` accepts the
infinity encoding); (b) prime-order subgroup membership for both G1 and
G2 inputs. pyblst does not expose blst's fast endomorphism-based
`in_g1`/`in_g2`/`KeyValidate` predicates, so the check falls back to
`P * q == identity` via `scalar_mul` (one full subgroup-order scalar
multiplication per parse). This closes the small-subgroup mint-key-
grinding class: attacker submits crafted `B_` with a small-order
torsion component, mint returns `a * B_t` revealing `a mod t`; chain
across several t's via CRT to recover a. Canonicality (x < p, c0 < p,
c1 < p, valid flag combinations) is already enforced by blst's
`uncompress` (BLST_BAD_ENCODING for non-canonical inputs).
NUT-00 batch verification. `batch_pairing_verification` switched from
non-deterministic `os.urandom(32)` weights to the spec's
Fiat-Shamir transcript with rejection sampling: build a length-prefixed
transcript binding (C_i, K_i, secret_i), collapse to a 32-byte
SHA-256 challenge, derive per-proof weights via
`OS2IP(SHA256(challenge || u32_BE(i) || u32_BE(ctr)))` with
0 < x < BLS_FR_ORDER. Deterministic weights are required for the
security argument: with random weights an attacker holding one
aggregated signature `C' = a * (Y_1 + Y_2)` can fix C_1 arbitrarily
and let C_2 = C' - C_1 to forge two proofs that pass a sum check.
Modular reduction would bias ~7.5% because BLS_FR_ORDER ~ 0.45 * 2^256.
NUT-13 V3 deterministic blinding factor. Split the HMAC-SHA256
derivation into V2 and V3 paths. V2 (secp256k1) keeps the existing
single-HMAC path (bias ~2^-128, negligible). V3 (BLS12-381) uses
rejection sampling with a u32_BE attempt counter appended to the
HMAC input after the 0x01 derivation-type byte:
base = b"Cashu_KDF_HMAC_SHA256" || keyset_id_bytes || counter_bytes
secret = HMAC_SHA256(seed, base || 0x00)
for attempt = 0, 1, ...:
digest = HMAC_SHA256(seed, base || 0x01 || u32_BE(attempt))
x = OS2IP(digest)
if x == 0 or x >= BLS_FR_ORDER: continue
r = digest; break
Test vectors updated to the new spec values
(nuts/tests/{02,13}-tests.md):
- NUT-02 v3 keyset vectors regenerated with distinct G2 keys per
amount (per the new "distinct keys MUST"); placeholder shared-key
vectors removed.
- NUT-13 v3 vector exercises retry: (seed, keyset_id, counter) chosen
so attempt=0 is rejected and attempt=1 is accepted.
V2 wallet keysets test updated to call the renamed
`_derive_secret_hmac_sha256_v2` helper.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Stacks on top of #999 to bring the BLS12-381 v3 implementation in line with the spec changes from the recent NUT-00 / NUT-01 / NUT-02 / NUT-13 review pass (cashubtc/nuts#bls-protocol).
Summary
Three spec-alignment fixes; none are exploitable in current Cashu flows but all violate strict spec MUSTs and weaken interoperability with stricter mints (e.g. blst-backed ones).
NUT-00 Point Validation (`cashu/core/crypto/bls.py`)
`PublicKey.init` now enforces, on parsed bytes:
This closes the small-subgroup mint-key-grinding class: attacker submits crafted `B_` with a small-order torsion component, mint returns `a · B_t` revealing `a mod t`, chain across several t's via CRT to recover `a`.
Canonicality (`x < p`, `c0 < p`, `c1 < p`, valid flag combinations) is already enforced by blst's `uncompress` (`BLST_BAD_ENCODING` for non-canonical inputs), so no extra check is needed.
NUT-00 Batch Verification (`cashu/core/crypto/bls_dhke.py`)
`batch_pairing_verification` previously used `os.urandom(32)` for the per-proof weights. The spec requires a deterministic Fiat-Shamir transcript:
Deterministic weights are required for the security argument: with random weights an attacker holding one aggregated signature `C' = a · (Y_1 + Y_2)` can pick any `C_1` and present `(C_1, C' − C_1)` as two proofs that pass a sum check. The transcript binds each `r_i` to the inputs before the attacker sees the weights.
Rejection sampling (not mod reduction) because `BLS_FR_ORDER ≈ 0.45 · 2^256` would bias mod reduction by ~7.5%.
NUT-13 V3 Deterministic Blinding Factor (`cashu/wallet/secrets.py`)
Split `_derive_secret_hmac_sha256` into V2 and V3 paths:
```
base = b"Cashu_KDF_HMAC_SHA256" || keyset_id_bytes || counter_bytes
secret = HMAC_SHA256(seed, base || 0x00)
for attempt = 0, 1, ...:
digest = HMAC_SHA256(seed, base || 0x01 || u32_BE(attempt))
x = OS2IP(digest)
if x == 0 or x >= BLS_FR_ORDER: continue
r = digest; break
```
Expected ~2.2 iterations per derivation.
Test vectors updated
Test plan
Notes