Skip to content

NUT-20: Signed Mint Quotes for Output Protection #31

@variablefate

Description

@variablefate

Summary

Implement NUT-20 signature verification for mint quotes to prevent output replacement attacks. This ensures the ecash outputs we receive are the ones we requested, protecting against MITM attacks.

Problem

Currently, when we mint tokens:

  1. We request a quote with blinded outputs
  2. We receive a quote ID
  3. We send the outputs with the quote to mint

If an attacker intercepts step 3, they could replace our outputs with their own and steal the minted tokens.

Solution: NUT-20 Signed Mint Quotes

How It Works

  1. Request quote with pubkey: Include a signing pubkey in mint quote request
  2. Sign outputs: Before minting, sign the quote ID + all B_ values with our key
  3. Mint verifies: Mint checks signature before signing outputs

This ensures only the holder of the private key can claim the quote.

Message Format

Mint Quote Request:

  • Add pubkey field (33-byte compressed secp256k1)
  • Mint echoes it back in response

Mint Request:

  • Add signature field (BIP-340 Schnorr)
  • Message: quote_id || B_0 || B_1 || ... || B_n

Signature Creation

We already have BIP-340 Schnorr signing via ACINQ secp256k1 (used for P2PK HTLC claims). Same pattern:

  1. Concatenate: UTF-8(quote_id) + hex(B_0) + hex(B_1) + ...
  2. SHA-256 hash the concatenation
  3. Sign with BIP-340 Schnorr

Implementation Plan

Phase 1: Quote Request Enhancement

Modify CashuBackend.getMintQuote():

  • Generate ephemeral keypair for quote
  • Include pubkey in request
  • Store keypair with quote for later signing

Phase 2: Mint Request Signing

Modify CashuBackend mint operations:

  • Retrieve keypair for quote
  • Build message: quote || B_0 || ... || B_n
  • Sign with BIP-340 Schnorr
  • Include signature in mint request

Phase 3: Storage

Add to WalletStorage:

  • Store quote keypairs (encrypted)
  • Clean up after successful mint
  • Handle quote expiry

Files to Modify

File Changes
CashuBackend.kt Add pubkey to quote request, sign mint request
WalletStorage.kt Quote keypair storage
WalletKeyManager.kt Ephemeral keypair generation (or reuse existing)

Code Pattern

Quote Request:

val ephemeralKey = generateEphemeralKey()
val request = JSONObject().apply {
    put("amount", amountSats)
    put("unit", "sat")
    put("pubkey", ephemeralKey.publicKeyHex)  // NEW
}
// Store keypair for later
walletStorage.saveQuoteKey(quoteId, ephemeralKey)

Mint Request:

val keypair = walletStorage.getQuoteKey(quoteId)
val message = buildSignatureMessage(quoteId, outputs)
val signature = keypair?.signSchnorr(sha256(message))

val request = JSONObject().apply {
    put("quote", quoteId)
    put("outputs", outputsArray)
    put("signature", signature)  // NEW
}

Privacy Consideration

Per NUT-20: Generate a unique keypair for each quote to prevent linkability.

Discovery

Check NUT-06 /v1/info for support:

  • If supported, use signed quotes
  • If not supported, fall back to unsigned (but log warning)

Benefits

  1. MITM protection: Attackers cannot steal minted tokens
  2. Output integrity: Guaranteed to receive requested denominations
  3. Accountability: Mint cannot claim output mismatch

References

  • NUT-20 Specification
  • We already use BIP-340 Schnorr for HTLC claims (WalletKeyManager.signSchnorr)

Acceptance Criteria

  • getMintQuote includes ephemeral pubkey when mint supports NUT-20
  • Mint request includes valid BIP-340 signature
  • Quote keypairs stored securely and cleaned up
  • Graceful fallback for mints without NUT-20 support
  • Unit tests with test vectors

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions