Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion 04.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ The mint `Bob` responds with a quote that includes some common fields for all me
}
```

Where `quote` is the quote ID, `request` is the payment request for the quote, and `unit` corresponds to the value provided in the request.
Where `quote` is the quote ID, `request` is the payment request for the quote, and `unit` corresponds to the value provided in the request. The `quote` id **MUST** be a [UUIDv7](https://www.rfc-editor.org/rfc/rfc9562).

> [!CAUTION]
>
Expand Down
34 changes: 24 additions & 10 deletions 20.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ The mint `Bob` then responds with a `PostMintQuoteBolt11Response`:
{
"quote": <str>,
"request": <str>,
"unit": <str_enum[UNIT]>,
"state": <str_enum[STATE]>,
"expiry": <int>,
"pubkey": <str|null> // Optional <-- New
Expand Down Expand Up @@ -77,19 +78,32 @@ Response of `Bob`:

### Message aggregation

To provide a signature for a mint request, the owner of the signing public keys must concatenate the quote ID `quote` in `PostMintQuoteBolt11Response` and the `B_` fields of all `BlindedMessages` in the `PostMintBolt11Request` (i.e., the outputs, see [NUT-00][00]) to a single message string in the order they appear in the `PostMintRequest`. This concatenated string is then hashed and signed (see [Signature scheme](#signature-scheme)).

> [!NOTE]
>
> Concatenating the quote ID and the outputs into a single message prevents maliciously replacing the outputs.

If a request has `n` outputs, the message to sign becomes:
To authenticate a mint request, the signer commits to the quote ID and all `BlindedMessages` (the outputs, see [NUT-00][00]) of the `PostMintBolt11Request`, in the order they appear in the request. The message is built over raw bytes:

```
msg_to_sign = quote || B_0 || ... || B_(n-1)
msg_to_sign = "Cashu_MintQuoteSig_v1" // domain-separation tag (ASCII, not length-prefixed)
|| len32(quote) || quote // quote = UTF-8 quote id
|| for each output i (in request order):
len32(amount_i) || amount_i // amount = canonical minimal big-endian
|| len32(B_i) || B_i // B_ = raw compressed point bytes
```

Where `||` denotes concatenation, `quote` is the UTF-8 quote id in `PostMintQuoteBolt11Response`, and each `B_n` is a UTF-8 encoded hex string of the outputs in the `PostMintBolt11Request`.
Where:

- `||` denotes byte concatenation and `len32(x)` is the 32-bit (4-byte) big-endian length of the following data `x`.
- `quote` is the UTF-8 quote id from the `PostMintQuoteBolt11Response`.
- `amount_i` is the output amount as canonical minimal big-endian bytes (e.g. `0` → empty byte array, `1` → `0x01`, `256` → `0x0100`); mints **MUST** reject non-minimal encodings.
- `B_i` is the raw byte representation of the blinded message (e.g. 33-byte compressed secp256k1 or 48-byte BLS12-381 point), decoded from the request's hex string.

Committing to the quote id and to each output's amount and point — domain-separated and length-framed — binds the signature to the exact set, order and value of the outputs, so the outputs cannot be maliciously replaced and the message cannot be reinterpreted across keyset curves.

> [!IMPORTANT]
>
> This replaces the earlier `quote || B_0 || ... || B_(n-1)` concatenation and is a **breaking change**: mints **MUST NOT** accept the legacy message, and wallets **MUST** sign using the format above.

> [!NOTE]
>
> The message deliberately does **not** commit to the keyset `id`: the amount and `B_` carry the value-bearing data, and leaving `id` uncommitted lets a wallet re-target a rotated keyset without a new signature, mirroring `SIG_ALL` in [NUT-11][11].

### Signature scheme

Expand All @@ -115,7 +129,7 @@ The wallet `Alice` includes the following `PostMintBolt11Request` data in its re

with the `quote` being the quote ID from the previous step and `outputs` being `BlindedMessages` as in [NUT-04][04].

`signature` is the signature on the `msg_to_sign` which is the concatenated quote id and the outputs as defined above.
`signature` is the signature on the `msg_to_sign` as defined above.

The mint responds with a `PostMintBolt11Response` as in [NUT-04][04] if all validations are successful.

Expand Down
16 changes: 3 additions & 13 deletions 29.md
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,8 @@ Once all quoted payments are confirmed, the wallet mints the proofs by calling:
POST https://mint.host:3338/v1/mint/{method}/batch
```

The batch endpoint is method-specific: every quote in the batch **MUST** be for the same payment `method` as the `{method}` in the URL, and a batch that mixes payment methods **MUST** be rejected.

The wallet includes the following body in its request:

```json
Expand Down Expand Up @@ -213,19 +215,7 @@ Per [NUT-20][20], quotes can require authentication via signatures. When using b

#### Signature Message

Following the [NUT-20 message aggregation][20-msg-agg] pattern, the signature for `quotes[i]` is computed as:

```
msg_to_sign = quote_id[i] || B_0 || B_1 || ... || B_(n-1)
```

Where:

- `quote_id[i]` is the UTF-8 encoded quote ID at index `i`
- `B_0 ... B_(n-1)` are **all blinded messages** from the `outputs` array (regardless of amount splitting)
- `||` denotes concatenation

The signature is a BIP340 Schnorr signature on the SHA-256 hash of `msg_to_sign`.
Each locked quote is signed independently exactly as in [NUT-20][20]: `signatures[i]` is the NUT-20 signature over `quotes[i]` and the request's `outputs`. The batch `outputs` are a single consolidated set (not partitioned per quote), so each signature is computed over the full `outputs` array.

### Signature Validation Failure

Expand Down
70 changes: 10 additions & 60 deletions tests/20-test.md
Original file line number Diff line number Diff line change
@@ -1,79 +1,29 @@
# NUT-20 Test Vectors

The following is a `PostMintBolt11Request` with a valid signature. Where the `pubkey` in the `PostMintQuoteBolt11Request` is `03d56ce4e446a85bbdaa547b4ec2b073d40ff802831352b8272b7dd7a4de5a7cac`.
The following is a `PostMintBolt11Request` with a valid signature, where the `pubkey` in the `PostMintQuoteBolt11Response` is `0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798` (secret key `0x01`).

```json
{
"quote": "9d745270-1405-46de-b5c5-e2762b4f5e00",
"quote": "0192d3c0-7e8a-7c3d-8e9f-1a2b3c4d5e6f",
"outputs": [
{
"amount": 1,
"id": "00456a94ab4e1c46",
"B_": "0342e5bcc77f5b2a3c2afb40bb591a1e27da83cddc968abdc0ec4904201a201834"
"id": "009a1f293253e41e",
"B_": "036d6caac248af96f6afa7f904f550253a0f3ef3f5aa2fe6838a95b216691468e2"
},
{
"amount": 1,
"id": "00456a94ab4e1c46",
"B_": "032fd3c4dc49a2844a89998d5e9d5b0f0b00dde9310063acb8a92e2fdafa4126d4"
},
{
"amount": 1,
"id": "00456a94ab4e1c46",
"B_": "033b6fde50b6a0dfe61ad148fff167ad9cf8308ded5f6f6b2fe000a036c464c311"
},
{
"amount": 1,
"id": "00456a94ab4e1c46",
"B_": "02be5a55f03e5c0aaea77595d574bce92c6d57a2a0fb2b5955c0b87e4520e06b53"
},
{
"amount": 1,
"id": "00456a94ab4e1c46",
"B_": "02209fc2873f28521cbdde7f7b3bb1521002463f5979686fd156f23fe6a8aa2b79"
"id": "009a1f293253e41e",
"B_": "021f8a566c205633d029094747d2e18f44e05993dda7a5f88f496078205f656e59"
}
],
"signature": "d4b386f21f7aa7172f0994ee6e4dd966539484247ea71c99b81b8e09b1bb2acbc0026a43c221fd773471dc30d6a32b04692e6837ddaccf0830a63128308e4ee0"
"signature": "4881093a332ff7c79f3e598ce5b249d64978b47165a0b19c18adf0ced0246228e61e702f0abaf1bf27b92be4336bdbabacfbe4c914076386b3c66fdcd0b3480e"
}
```

The following is the expected message to sign on the above `PostMintBolt11Request`.
The corresponding `msg_to_sign` (hex) and its SHA-256 hash are:

```
[57, 100, 55, 52, 53, 50, 55, 48, 45, 49, 52, 48, 53, 45, 52, 54, 100, 101, 45, 98, 53, 99, 53, 45, 101, 50, 55, 54, 50, 98, 52, 102, 53, 101, 48, 48, 48, 51, 52, 50, 101, 53, 98, 99, 99, 55, 55, 102, 53, 98, 50, 97, 51, 99, 50, 97, 102, 98, 52, 48, 98, 98, 53, 57, 49, 97, 49, 101, 50, 55, 100, 97, 56, 51, 99, 100, 100, 99, 57, 54, 56, 97, 98, 100, 99, 48, 101, 99, 52, 57, 48, 52, 50, 48, 49, 97, 50, 48, 49, 56, 51, 52, 48, 51, 50, 102, 100, 51, 99, 52, 100, 99, 52, 57, 97, 50, 56, 52, 52, 97, 56, 57, 57, 57, 56, 100, 53, 101, 57, 100, 53, 98, 48, 102, 48, 98, 48, 48, 100, 100, 101, 57, 51, 49, 48, 48, 54, 51, 97, 99, 98, 56, 97, 57, 50, 101, 50, 102, 100, 97, 102, 97, 52, 49, 50, 54, 100, 52, 48, 51, 51, 98, 54, 102, 100, 101, 53, 48, 98, 54, 97, 48, 100, 102, 101, 54, 49, 97, 100, 49, 52, 56, 102, 102, 102, 49, 54, 55, 97, 100, 57, 99, 102, 56, 51, 48, 56, 100, 101, 100, 53, 102, 54, 102, 54, 98, 50, 102, 101, 48, 48, 48, 97, 48, 51, 54, 99, 52, 54, 52, 99, 51, 49, 49, 48, 50, 98, 101, 53, 97, 53, 53, 102, 48, 51, 101, 53, 99, 48, 97, 97, 101, 97, 55, 55, 53, 57, 53, 100, 53, 55, 52, 98, 99, 101, 57, 50, 99, 54, 100, 53, 55, 97, 50, 97, 48, 102, 98, 50, 98, 53, 57, 53, 53, 99, 48, 98, 56, 55, 101, 52, 53, 50, 48, 101, 48, 54, 98, 53, 51, 48, 50, 50, 48, 57, 102, 99, 50, 56, 55, 51, 102, 50, 56, 53, 50, 49, 99, 98, 100, 100, 101, 55, 102, 55, 98, 51, 98, 98, 49, 53, 50, 49, 48, 48, 50, 52, 54, 51, 102, 53, 57, 55, 57, 54, 56, 54, 102, 100, 49, 53, 54, 102, 50, 51, 102, 101, 54, 97, 56, 97, 97, 50, 98, 55, 57]
```

The following is a `PostMintBolt11Request` with an invalid signature. Where the `pubkey` in the `PostMintQuoteBolt11Request` is `03d56ce4e446a85bbdaa547b4ec2b073d40ff802831352b8272b7dd7a4de5a7cac`.

```json
{
"quote": "9d745270-1405-46de-b5c5-e2762b4f5e00",
"outputs": [
{
"amount": 1,
"id": "00456a94ab4e1c46",
"B_": "0342e5bcc77f5b2a3c2afb40bb591a1e27da83cddc968abdc0ec4904201a201834"
},
{
"amount": 1,
"id": "00456a94ab4e1c46",
"B_": "032fd3c4dc49a2844a89998d5e9d5b0f0b00dde9310063acb8a92e2fdafa4126d4"
},
{
"amount": 1,
"id": "00456a94ab4e1c46",
"B_": "033b6fde50b6a0dfe61ad148fff167ad9cf8308ded5f6f6b2fe000a036c464c311"
},
{
"amount": 1,
"id": "00456a94ab4e1c46",
"B_": "02be5a55f03e5c0aaea77595d574bce92c6d57a2a0fb2b5955c0b87e4520e06b53"
},
{
"amount": 1,
"id": "00456a94ab4e1c46",
"B_": "02209fc2873f28521cbdde7f7b3bb1521002463f5979686fd156f23fe6a8aa2b79"
}
],
"signature": "cb2b8e7ea69362dfe2a07093f2bbc319226db33db2ef686c940b5ec976bcbfc78df0cd35b3e998adf437b09ee2c950bd66dfe9eb64abd706e43ebc7c669c36c3"
}
msg_to_sign = 43617368755f4d696e7451756f74655369675f76310000002430313932643363302d376538612d376333642d386539662d316132623363346435653666000000010100000021036d6caac248af96f6afa7f904f550253a0f3ef3f5aa2fe6838a95b216691468e2000000010100000021021f8a566c205633d029094747d2e18f44e05993dda7a5f88f496078205f656e59
sha256(msg_to_sign) = c164fd384879f74ab6ea2e7cf13d90ed42e6df9d5de607eeb5c9cc7d36fb1c21
```
8 changes: 4 additions & 4 deletions tests/29-tests.md
Original file line number Diff line number Diff line change
Expand Up @@ -130,9 +130,9 @@ The following is a valid NUT-20 batch mint request where the signature correctly
```shell
quote: "locked-quote"
pubkey: 0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798
msg_to_sign_bytes: utf8("locked-quote") || B0 || B1
msg_hash: 5ac550d5416e81c613b58e3f1fb095390fb828b55e8991fd9de231ca8e31e859
signature[0]: 9408920d0b94cee5eb6df20f14d2a655e7ce2ce309dc1f1aeb69b219efe76716933b2206eba3a54f9a953c92edaa922ab3e6912e02383dda42a193409567a0dc
msg_to_sign_bytes: 43617368755f4d696e7451756f74655369675f76310000000c6c6f636b65642d71756f7465000000010100000021036d6caac248af96f6afa7f904f550253a0f3ef3f5aa2fe6838a95b216691468e2000000010100000021021f8a566c205633d029094747d2e18f44e05993dda7a5f88f496078205f656e59
msg_hash: 03dc68d6617bba502d8648efd0965bf393841082cf04fd03e5de4bcb5777cdfc
signature[0]: a913e48177027d87e0e38c6f2021763c46997ff4866a4b63ebca800b0776b28519eab37377cf9bc1869e489d7b25747b7a998eaa1c33c2cac7fa168449d8267a
```

```json
Expand All @@ -151,7 +151,7 @@ signature[0]: 9408920d0b94cee5eb6df20f14d2a655e7ce2ce309dc1f1aeb69b219efe7671693
}
],
"signatures": [
"9408920d0b94cee5eb6df20f14d2a655e7ce2ce309dc1f1aeb69b219efe76716933b2206eba3a54f9a953c92edaa922ab3e6912e02383dda42a193409567a0dc"
"a913e48177027d87e0e38c6f2021763c46997ff4866a4b63ebca800b0776b28519eab37377cf9bc1869e489d7b25747b7a998eaa1c33c2cac7fa168449d8267a"
]
}
```
Loading