Extend NUT-13 to derive NUT-20 quote signing keys from seed #356

Open
opened 2026-03-29 01:43:09 +00:00 by Forte11Cuba · 1 comment
Forte11Cuba commented 2026-03-29 01:43:09 +00:00 (Migrated from github.com)

Problem

NUT-13 enables deterministic derivation of proof secrets (0x00) and blinding factors (0x01) from a wallet seed, allowing full ecash recovery via NUT-09.

NUT-20 quote signing keys are not covered. All known implementations ( cdk (Rust) and Nutshell (Python) ) generate them randomly:

These keys exist only in the local database. After device loss, the user cannot produce the NUT-20 signature required to claim proofs from a PAID quote even with the correct mnemonic and the quote ID.

What this solves

A user who has:

  • Their mnemonic (seed)
  • A quote ID (from their records or provided by the mint operator)

Can currently recover on mints without NUT-20: just call mint(quote_id). Cannot currently recover on mints with NUT-20: the signing key is lost, the mint rejects with error 20008. With deterministic derivation, the wallet regenerates the same signing key from the seed and produces a valid signature. Recovery works on all mints.

Proposed derivation
Use the same HMAC-SHA256 pattern as NUT-13 with a separate domain string, since quote keys are not tied to a keyset:
message = b"Cashu_KDF_HMAC_SHA256_QUOTE" || quote_counter_bytes
quote_signing_key = HMAC_SHA256(seed, message)

Where:

  • quote_counter_bytes is an unsigned 64-bit integer in big-endian format
  • The 32-byte HMAC digest is used as a secp256k1 secret key
  • The corresponding public key is sent to the mint per NUT-20

The wallet persists a quote_counter (incremented per mint quote) alongside keyset counters.
NUT-20 recommends a unique public key per quote. Deterministic derivation preserves this — each counter value produces a unique, unlinkable key.

This proposal covers key derivation only. With the quote ID, recovery works immediately.
Full automatic recovery (without knowing the quote ID) would require a mint endpoint to look up quotes by public key. That could be defined in a separate NUT.

Problem NUT-13 enables deterministic derivation of proof secrets (0x00) and blinding factors (0x01) from a wallet seed, allowing full ecash recovery via NUT-09. NUT-20 quote signing keys are not covered. All known implementations ( cdk (Rust) and Nutshell (Python) ) generate them randomly: These keys exist only in the local database. After device loss, the user cannot produce the NUT-20 signature required to claim proofs from a PAID quote even with the correct mnemonic and the quote ID. What this solves A user who has: - Their mnemonic (seed) - A quote ID (from their records or provided by the mint operator) Can currently recover on mints without NUT-20: just call mint(quote_id). Cannot currently recover on mints with NUT-20: the signing key is lost, the mint rejects with error 20008. With deterministic derivation, the wallet regenerates the same signing key from the seed and produces a valid signature. Recovery works on all mints. Proposed derivation Use the same HMAC-SHA256 pattern as NUT-13 with a separate domain string, since quote keys are not tied to a keyset: message = b"Cashu_KDF_HMAC_SHA256_QUOTE" || quote_counter_bytes quote_signing_key = HMAC_SHA256(seed, message) Where: - quote_counter_bytes is an unsigned 64-bit integer in big-endian format - The 32-byte HMAC digest is used as a secp256k1 secret key - The corresponding public key is sent to the mint per NUT-20 The wallet persists a quote_counter (incremented per mint quote) alongside keyset counters. NUT-20 recommends a unique public key per quote. Deterministic derivation preserves this — each counter value produces a unique, unlinkable key. This proposal covers key derivation only. With the quote ID, recovery works immediately. Full automatic recovery (without knowing the quote ID) would require a mint endpoint to look up quotes by public key. That could be defined in a separate NUT.
thesimplekid commented 2026-03-29 09:04:28 +00:00 (Migrated from github.com)

Yeah we've talked about this before and with https://github.com/cashubtc/nuts/pull/341 with it makes sense where as now there is no way to get the quote ids back so it's less helpful. I think for this it makes more sense to use bip32 then HMAC would be very similar to https://github.com/cashubtc/nuts/pull/331.

https://github.com/cashubtc/nuts/pull/331#discussion_r2728358700

Yeah we've talked about this before and with https://github.com/cashubtc/nuts/pull/341 with it makes sense where as now there is no way to get the quote ids back so it's less helpful. I think for this it makes more sense to use bip32 then HMAC would be very similar to https://github.com/cashubtc/nuts/pull/331. https://github.com/cashubtc/nuts/pull/331#discussion_r2728358700
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
forgejo-admin/nuts#356
No description provided.