feat(crypto): migrate BDHKE to BLS12-381 (v3 keysets)#999
Draft
a1denvalu3 wants to merge 30 commits into
Draft
Conversation
…h BLS12-381 cryptography
… verification - Fixed keyset v3 derivation logic and ID generation - Replaced additive blinding logic with BLS multiplicative blinding - Handled backwards compatibility for DLEQ skipping and verifying - Implemented batch BLS pairing verification for unblinded signatures - Removed redundant dummy DLEQ generation in BLS operations - Fixed failing unit tests and resolved type-checking union issues
❌ 46 Tests Failed:
View the top 3 failed test(s) by shortest run time
To view more test analytics, go to the Test Analytics Dashboard |
a8f8486 to
0c63c63
Compare
fix v3 Proof.Y hash-to-curve and BlindedSignature dleq=None
6 tasks
robwoodgate
added a commit
to robwoodgate/nutshell
that referenced
this pull request
May 13, 2026
Four coupled fixes that surface together when a fresh Nutshell 0.21.0
auth mint runs against any OIDC provider (e.g. Keycloak 25+) and a v3
(BLS) keyset is generated on first start. Each fix is small; bundled
because they share one architectural root: AuthLedger inherits the
mint CRUD (LedgerCrudSqlite) and the global `mint_input_fee_ppk`
setting, while the auth migrations chain / response models / user-id
contract never kept up.
1. Force `input_fee_ppk=0` on auth keyset generation.
Auth proofs are NUT-22 amount-1 bearer tokens — never swapped or
melted. `AuthLedger.verify_blind_auth` already explicitly skips
fee calculation ("We do not calculate fees for auth keysets").
But `Ledger.activate_keyset` reads `settings.mint_input_fee_ppk`
unconditionally, so any mint with a non-zero global fee bakes
that value into the auth keyset id — semantically wrong, and
breaks wallet-side id re-derivation (auth router publishes
`input_fee_ppk=null`, wallet derives without the suffix → id
mismatch → keyset rejected as inauthentic). Matches CDK's
behaviour (crates/cdk/src/mint/builder.rs forces fee=0 for the
Auth unit).
Implementation: `LedgerKeysets` exposes a per-instance
`keyset_input_fee_ppk: Optional[int] = None` defaulting to
`settings.mint_input_fee_ppk`; `AuthLedger` overrides to `0`.
No behaviour change for non-auth ledgers.
2. m003: add `final_expiry` column to auth `keysets` table.
LedgerCrudSqlite.store_keyset INSERTs `final_expiry` (added on
the mint side in m031 for keysets v2). Auth migrations stopped
at m002, so v3 keyset generation crashes with
`no column named final_expiry`. Mirrors mint m031.
3. m004: align auth `promises` table with the mint-side schema.
The mint side evolved `promises` to add mint_quote / swap_id
(m023) and melt_quote / signed_at + drop the `c_ NOT NULL`
constraint (m032-ish). LedgerCrudSqlite.store_promise INSERTs
the full column set, so auth-side blind minting (first
exercised by v3 BAT issuance at 0.21+) trips first
`no column named mint_quote` then
`NOT NULL constraint failed: promises.c_`. Auth never populates
any of these new columns, but the schema must accept the
INSERT. SQLite path rebuilds the table (matching mint m032
shape); Postgres path uses ALTER chain.
4. Tolerate missing `sub` claim in clear-auth tokens.
`_get_user` hard-coded `decoded_token["sub"]`, which raises
KeyError when the IdP omits `sub` from access tokens. Keycloak
25+ does this by default for public clients (the
`oidc-subject-mapper` declared in the cashu-realm.json gets
silently dropped on import). CDK's `verify_cat` doesn't read
`sub` at all and works against the same realm. Fall back to
`preferred_username` then `azp` so single-user-per-realm
rate-limit tracking still works on those setups without
changing happy-path semantics for IdPs that do ship `sub`.
Cross-IdP, not Keycloak-specific.
Verified end-to-end against a freshly-built local container and
cashu-ts (v3 BAT path) — wallet OIDC password grant → 3 BATs minted →
auth keyset id verifies (`02 + sha256("1:<G2-pubkey>|unit:auth")` →
`027cbc55...`) → BLS pairing accepts the BATs → mint/swap/receive all
green.
Out of scope: the underlying smell is `AuthLedger` using
`LedgerCrudSqlite` instead of the (existing-but-unused)
`AuthLedgerCrudSqlite`, whose leaner `store_keyset` / `store_promise`
already match the auth m001 schema and would obviate (1)–(3).
Switching requires adding several missing methods to `AuthLedgerCrud`
(`store_blinded_message`, `update_keyset`, `bump_keyset_*`, balance
logs) — too wide for this PR. Worth a follow-up issue.
Refs cashubtc#999.
robwoodgate
added a commit
to robwoodgate/nutshell
that referenced
this pull request
May 13, 2026
Five coupled fixes that surface together when a fresh Nutshell 0.21.0
auth mint runs against any OIDC provider (e.g. Keycloak 25+) and a v3
(BLS) keyset is generated on first start. Each fix is small; bundled
because they share one architectural root: AuthLedger inherits the
mint CRUD (LedgerCrudSqlite) and the global `mint_input_fee_ppk`
setting, while the auth migrations chain / response models / user-id
contract / auth-side CRUD never kept up.
1. Force `input_fee_ppk=0` on auth keyset generation.
Auth proofs are NUT-22 amount-1 bearer tokens — never swapped or
melted. `AuthLedger.verify_blind_auth` already explicitly skips
fee calculation ("We do not calculate fees for auth keysets").
But `Ledger.activate_keyset` reads `settings.mint_input_fee_ppk`
unconditionally, so any mint with a non-zero global fee bakes
that value into the auth keyset id — semantically wrong, and
breaks wallet-side id re-derivation (auth router publishes
`input_fee_ppk=null`, wallet derives without the suffix → id
mismatch → keyset rejected as inauthentic). Matches CDK's
behaviour (crates/cdk/src/mint/builder.rs forces fee=0 for the
Auth unit).
Implementation: `LedgerKeysets` exposes a per-instance
`keyset_input_fee_ppk: Optional[int] = None` defaulting to
`settings.mint_input_fee_ppk`; `AuthLedger` overrides to `0`.
No behaviour change for non-auth ledgers.
2. m003: add `final_expiry` column to auth `keysets` table.
LedgerCrudSqlite.store_keyset INSERTs `final_expiry` (added on
the mint side in m031 for keysets v2). Auth migrations stopped
at m002, so v3 keyset generation crashes with
`no column named final_expiry`. Mirrors mint m031.
3. m004: align auth `promises` table with the mint-side schema.
The mint side evolved `promises` to add mint_quote / swap_id
(m023) and melt_quote / signed_at + drop the `c_ NOT NULL`
constraint (m032-ish). LedgerCrudSqlite.store_promise INSERTs
the full column set, so auth-side blind minting (first
exercised by v3 BAT issuance at 0.21+) trips first
`no column named mint_quote` then
`NOT NULL constraint failed: promises.c_`. Auth never populates
any of these new columns, but the schema must accept the
INSERT. SQLite path rebuilds the table (matching mint m032
shape); Postgres path uses ALTER chain.
4. Tolerate missing `sub` claim in clear-auth tokens.
`_get_user` hard-coded `decoded_token["sub"]`, which raises
KeyError when the IdP omits `sub` from access tokens. Keycloak
25+ does this by default for public clients (the
`oidc-subject-mapper` declared in the cashu-realm.json gets
silently dropped on import). CDK's `verify_cat` doesn't read
`sub` at all and works against the same realm. Fall back to
`preferred_username` then `azp` so single-user-per-realm
rate-limit tracking still works on those setups without
changing happy-path semantics for IdPs that do ship `sub`.
Cross-IdP, not Keycloak-specific.
5. AuthLedgerCrudSqlite.get_keyset: use MintKeyset.from_row.
`MintKeyset(**row)` passes `amounts` as the raw stringified-JSON
stored in SQLite (e.g. `"[1]"`) directly into the constructor.
`MintKeyset.from_row` does `json.loads(row["amounts"])` first.
Iteration over `self.amounts` would then walk characters instead
of elements — producing junk key material. Latent in the current
architecture because AuthLedger uses LedgerCrudSqlite (whose
get_keyset is correct) and self.auth_crud is only invoked for
user CRUD, but a real trap for the eventual switch-to-
AuthLedgerCrudSqlite cleanup. Spotted by the security-scan bot
on this PR.
Verified end-to-end against a freshly-built local container and
cashu-ts (v3 BAT path) — wallet OIDC password grant → 3 BATs minted →
auth keyset id verifies (`02 + sha256("1:<G2-pubkey>|unit:auth")` →
`027cbc55...`) → BLS pairing accepts the BATs → mint/swap/receive all
green.
Out of scope: the underlying smell is `AuthLedger` using
`LedgerCrudSqlite` instead of the (existing-but-unused)
`AuthLedgerCrudSqlite`, whose leaner `store_keyset` / `store_promise`
already match the auth m001 schema and would obviate (1)–(3).
Switching requires adding several missing methods to `AuthLedgerCrud`
(`store_blinded_message`, `update_keyset`, `bump_keyset_*`, balance
logs) — too wide for this PR. Worth a follow-up issue.
Refs cashubtc#999.
robwoodgate
added a commit
to robwoodgate/nutshell
that referenced
this pull request
May 13, 2026
Five coupled fixes that surface together when a fresh Nutshell 0.21.0
auth mint runs against any OIDC provider (e.g. Keycloak 25+) and a v3
(BLS) keyset is generated on first start. Each fix is small; bundled
because they share one architectural root: AuthLedger inherits the
mint CRUD (LedgerCrudSqlite) and the global `mint_input_fee_ppk`
setting, while the auth migrations chain / response models / user-id
contract / auth-side CRUD never kept up.
1. Force `input_fee_ppk=0` on auth keyset generation.
Auth proofs are NUT-22 amount-1 bearer tokens — never swapped or
melted. `AuthLedger.verify_blind_auth` already explicitly skips
fee calculation ("We do not calculate fees for auth keysets").
But `Ledger.activate_keyset` reads `settings.mint_input_fee_ppk`
unconditionally, so any mint with a non-zero global fee bakes
that value into the auth keyset id — semantically wrong, and
breaks wallet-side id re-derivation (auth router publishes
`input_fee_ppk=null`, wallet derives without the suffix → id
mismatch → keyset rejected as inauthentic). Matches CDK's
behaviour (crates/cdk/src/mint/builder.rs forces fee=0 for the
Auth unit).
Implementation: `LedgerKeysets` exposes a per-instance
`keyset_input_fee_ppk: Optional[int] = None` defaulting to
`settings.mint_input_fee_ppk`; `AuthLedger` overrides to `0`.
No behaviour change for non-auth ledgers.
2. m003: add `final_expiry` column to auth `keysets` table.
LedgerCrudSqlite.store_keyset INSERTs `final_expiry` (added on
the mint side in m031 for keysets v2). Auth migrations stopped
at m002, so v3 keyset generation crashes with
`no column named final_expiry`. Mirrors mint m031.
3. m004: align auth `promises` table with the mint-side schema.
The mint side evolved `promises` to add mint_quote / swap_id
(m023) and melt_quote / signed_at + drop the `c_ NOT NULL`
constraint (m032-ish). LedgerCrudSqlite.store_promise INSERTs
the full column set, so auth-side blind minting (first
exercised by v3 BAT issuance at 0.21+) trips first
`no column named mint_quote` then
`NOT NULL constraint failed: promises.c_`. Auth never populates
any of these new columns, but the schema must accept the
INSERT. SQLite path rebuilds the table (matching mint m032
shape); Postgres path uses ALTER chain.
4. Tolerate missing `sub` claim in clear-auth tokens.
`_get_user` hard-coded `decoded_token["sub"]`, which raises
KeyError when the IdP omits `sub` from access tokens. Keycloak
25+ does this by default for public clients (the
`oidc-subject-mapper` declared in the cashu-realm.json gets
silently dropped on import). CDK's `verify_cat` doesn't read
`sub` at all and works against the same realm. Fall back to
`preferred_username` then `azp` so single-user-per-realm
rate-limit tracking still works on those setups without
changing happy-path semantics for IdPs that do ship `sub`.
Cross-IdP, not Keycloak-specific.
5. AuthLedgerCrudSqlite.get_keyset: use MintKeyset.from_row.
`MintKeyset(**row)` passes `amounts` as the raw stringified-JSON
stored in SQLite (e.g. `"[1]"`) directly into the constructor.
`MintKeyset.from_row` does `json.loads(row["amounts"])` first.
Iteration over `self.amounts` would then walk characters instead
of elements — producing junk key material. Latent in the current
architecture because AuthLedger uses LedgerCrudSqlite (whose
get_keyset is correct) and self.auth_crud is only invoked for
user CRUD, but a real trap for the eventual switch-to-
AuthLedgerCrudSqlite cleanup.
Verified end-to-end against a freshly-built local container and
cashu-ts (v3 BAT path) — wallet OIDC password grant → 3 BATs minted →
auth keyset id verifies (`02 + sha256("1:<G2-pubkey>|unit:auth")` →
`027cbc55...`) → BLS pairing accepts the BATs → mint/swap/receive all
green.
Out of scope: the underlying smell is `AuthLedger` using
`LedgerCrudSqlite` instead of the (existing-but-unused)
`AuthLedgerCrudSqlite`, whose leaner `store_keyset` / `store_promise`
already match the auth m001 schema and would obviate (1)–(3).
Switching requires adding several missing methods to `AuthLedgerCrud`
(`store_blinded_message`, `update_keyset`, `bump_keyset_*`, balance
logs) — too wide for this PR. Worth a follow-up issue.
Refs cashubtc#999.
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.
Summary
This pull request introduces BLS12-381 cryptography into the Cashu protocol, enabling smaller proofs and paving the way for multi-signature schemes and batch verification.
Core Changes
v3keysets using the BLS12-381 curve.Y * r) to replace legacy additive blinding (Y + r*G).e(C, G2) == e(Y, K2)).v1/v2(secp256k1) keysets.02prefix for BLS keysets.Testing
secp256k1andBLS12-381logic.tests/test_crypto_bls.pytest suite specifically for deterministic hash-to-curve testing, verification of individual BLS protocol steps, and batched BLS pairing checks.