From b969c4aa3fda246872e80d8cf74ca080240cd914 Mon Sep 17 00:00:00 2001 From: Rob Woodgate Date: Tue, 26 May 2026 16:06:53 +0100 Subject: [PATCH 1/8] NUT-20: harden mint quote signature message (breaking); NUT-04: UUIDv7 quote ids Replace the legacy `quote || B_0 || ... || B_(n-1)` mint-quote signature message with a domain-separated, length-framed, amount-committing `msg_to_sign`. This is a breaking change: mints no longer accept the legacy message. The message does not commit the keyset `id`, so a wallet can re-target a rotated keyset without a new signature. - NUT-04: quote ids MUST be a UUIDv7. - NUT-20: define the hardened `msg_to_sign` (replaces the concatenation). - NUT-29: each locked quote is signed independently per NUT-20 over the consolidated outputs; mixed-method batches are rejected. - tests/20-test.md: hardened-message test vector. --- 04.md | 2 +- 20.md | 34 ++++++++++++++++------- 29.md | 16 +++-------- tests/20-test.md | 70 +++++++----------------------------------------- 4 files changed, 38 insertions(+), 84 deletions(-) diff --git a/04.md b/04.md index 18f1d468..496b1319 100644 --- a/04.md +++ b/04.md @@ -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 string in UUID v7 format, `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] > diff --git a/20.md b/20.md index 4bf1b598..2bbc4095 100644 --- a/20.md +++ b/20.md @@ -45,6 +45,7 @@ The mint `Bob` then responds with a `PostMintQuoteBolt11Response`: { "quote": , "request": , + "unit": , "state": , "expiry": , "pubkey": // Optional <-- New @@ -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 @@ -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. diff --git a/29.md b/29.md index 144a659d..5225d230 100644 --- a/29.md +++ b/29.md @@ -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 @@ -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 diff --git a/tests/20-test.md b/tests/20-test.md index e534f948..e6481581 100644 --- a/tests/20-test.md +++ b/tests/20-test.md @@ -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 ``` From 91abdbb5ed4087c67db23e96c17bc19e4078c726 Mon Sep 17 00:00:00 2001 From: a1denvalu3 Date: Mon, 25 May 2026 12:10:37 +0200 Subject: [PATCH 2/8] Update NUT-29 signature message to length-prefixed bytes --- tests/29-tests.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/tests/29-tests.md b/tests/29-tests.md index b67ee920..a67b86ec 100644 --- a/tests/29-tests.md +++ b/tests/29-tests.md @@ -134,16 +134,16 @@ Expected behavior: The following is a valid NUT-20 batch mint request where the signature correctly covers all outputs in order. The quote has pubkey `0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798` (sk = 1). ```shell -quote: "019e6d5a-2347-7000-80fe-07ae8fa79774" +quote: "locked-quote" pubkey: 0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798 -msg_to_sign_bytes: utf8("019e6d5a-2347-7000-80fe-07ae8fa79774") || B0 || B1 -msg_hash: 5ac550d5416e81c613b58e3f1fb095390fb828b55e8991fd9de231ca8e31e859 -signature[0]: 9408920d0b94cee5eb6df20f14d2a655e7ce2ce309dc1f1aeb69b219efe76716933b2206eba3a54f9a953c92edaa922ab3e6912e02383dda42a193409567a0dc +msg_to_sign_bytes: 43617368755f4d696e7451756f74655369675f76310000000c6c6f636b65642d71756f7465000000010100000021036d6caac248af96f6afa7f904f550253a0f3ef3f5aa2fe6838a95b216691468e2000000010100000021021f8a566c205633d029094747d2e18f44e05993dda7a5f88f496078205f656e59 +msg_hash: 03dc68d6617bba502d8648efd0965bf393841082cf04fd03e5de4bcb5777cdfc +signature[0]: a913e48177027d87e0e38c6f2021763c46997ff4866a4b63ebca800b0776b28519eab37377cf9bc1869e489d7b25747b7a998eaa1c33c2cac7fa168449d8267a ``` ```json { - "quotes": ["019e6d5a-2347-7000-80fe-07ae8fa79774"], + "quotes": ["locked-quote"], "outputs": [ { "amount": 1, @@ -157,7 +157,7 @@ signature[0]: 9408920d0b94cee5eb6df20f14d2a655e7ce2ce309dc1f1aeb69b219efe7671693 } ], "signatures": [ - "9408920d0b94cee5eb6df20f14d2a655e7ce2ce309dc1f1aeb69b219efe76716933b2206eba3a54f9a953c92edaa922ab3e6912e02383dda42a193409567a0dc" + "a913e48177027d87e0e38c6f2021763c46997ff4866a4b63ebca800b0776b28519eab37377cf9bc1869e489d7b25747b7a998eaa1c33c2cac7fa168449d8267a" ] } ``` From 660f59dc90d5fb4952beb927bba1ddb61b498da9 Mon Sep 17 00:00:00 2001 From: a1denvalu3 <43107113+a1denvalu3@users.noreply.github.com> Date: Thu, 18 Jun 2026 11:01:45 +0200 Subject: [PATCH 3/8] Update 20.md Co-authored-by: callebtc <93376500+callebtc@users.noreply.github.com> --- 20.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/20.md b/20.md index 2bbc4095..b4cfd11a 100644 --- a/20.md +++ b/20.md @@ -92,7 +92,7 @@ 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. +- `amount_i` is the output amount as canonical minimal big-endian bytes (e.g. `0` → empty byte array, `1` → `0x01`, `256` → `0x0100`); - `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. From bce71280619006f3b1a29e3cd4cde45d83b5fd1f Mon Sep 17 00:00:00 2001 From: a1denvalu3 <43107113+a1denvalu3@users.noreply.github.com> Date: Thu, 18 Jun 2026 11:07:28 +0200 Subject: [PATCH 4/8] Update 20.md Co-authored-by: callebtc <93376500+callebtc@users.noreply.github.com> --- 20.md | 1 - 1 file changed, 1 deletion(-) diff --git a/20.md b/20.md index b4cfd11a..4e384107 100644 --- a/20.md +++ b/20.md @@ -95,7 +95,6 @@ Where: - `amount_i` is the output amount as canonical minimal big-endian bytes (e.g. `0` → empty byte array, `1` → `0x01`, `256` → `0x0100`); - `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] > From 27101de3ee4fd92c3ee9364d373e7ae3f8557918 Mon Sep 17 00:00:00 2001 From: a1denvalu3 <43107113+a1denvalu3@users.noreply.github.com> Date: Thu, 18 Jun 2026 11:07:59 +0200 Subject: [PATCH 5/8] Update 29.md Co-authored-by: callebtc <93376500+callebtc@users.noreply.github.com> --- 29.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/29.md b/29.md index 5225d230..ab61cdca 100644 --- a/29.md +++ b/29.md @@ -87,7 +87,7 @@ 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 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 by the mint. The wallet includes the following body in its request: From 0cec6b353b82399a2c4582b4dbba97008399cd79 Mon Sep 17 00:00:00 2001 From: a1denvalu3 <43107113+a1denvalu3@users.noreply.github.com> Date: Thu, 18 Jun 2026 11:08:22 +0200 Subject: [PATCH 6/8] Update 20.md Co-authored-by: callebtc <93376500+callebtc@users.noreply.github.com> --- 20.md | 4 ---- 1 file changed, 4 deletions(-) diff --git a/20.md b/20.md index 4e384107..5e614385 100644 --- a/20.md +++ b/20.md @@ -96,10 +96,6 @@ Where: - `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. -> [!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]. From bce07184050e5b3b9d72a90c3a55242ac2b2fa90 Mon Sep 17 00:00:00 2001 From: a1denvalu3 <43107113+a1denvalu3@users.noreply.github.com> Date: Thu, 18 Jun 2026 11:08:36 +0200 Subject: [PATCH 7/8] Update 20.md Co-authored-by: callebtc <93376500+callebtc@users.noreply.github.com> --- 20.md | 3 --- 1 file changed, 3 deletions(-) diff --git a/20.md b/20.md index 5e614385..45cd5a53 100644 --- a/20.md +++ b/20.md @@ -96,9 +96,6 @@ Where: - `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. -> [!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 From a5a7c70c1b37fe41f114b483517896da68e0286c Mon Sep 17 00:00:00 2001 From: a1denvalu3 Date: Thu, 18 Jun 2026 11:14:09 +0200 Subject: [PATCH 8/8] Address review comments on 20.md and 29.md --- 20.md | 6 ++---- 29.md | 16 ++++++++++++++++ 2 files changed, 18 insertions(+), 4 deletions(-) diff --git a/20.md b/20.md index 45cd5a53..2145c694 100644 --- a/20.md +++ b/20.md @@ -90,13 +90,11 @@ msg_to_sign = "Cashu_MintQuoteSig_v1" // domain-separation tag ( Where: -- `||` denotes byte concatenation and `len32(x)` is the 32-bit (4-byte) big-endian length of the following data `x`. +- `||` denotes byte concatenation and `len32(x)` is the 32-bit (4-byte) big-endian length of the byte array `x` in bytes. - `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`); +- `amount_i` is the output amount as canonical minimal big-endian bytes (e.g. `0` → empty byte array, `1` → `0x01`, `256` → `0x0100`); thus `len32(amount_i)` is its length in bytes as a 32-bit integer (e.g. `0` for amount `0`, `1` for amount `1`, `2` for amount `256`). - `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. - - ### Signature scheme To mint a quote where a public key was provided, the wallet includes a signature on `msg_to_sign` in the `PostMintBolt11Request`. We use a [BIP340](https://github.com/bitcoin/bips/blob/master/bip-0340.mediawiki) Schnorr signature on the SHA-256 hash of the message to sign as defined above. diff --git a/29.md b/29.md index ab61cdca..c3efcffd 100644 --- a/29.md +++ b/29.md @@ -217,6 +217,22 @@ Per [NUT-20][20], quotes can require authentication via signatures. When using b 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. +Following the [NUT-20 message aggregation][20-msg-agg] pattern, the signature message for `quotes[i]` is computed as: + +``` +msg_to_sign = "Cashu_MintQuoteSig_v1" // domain-separation tag (ASCII, not length-prefixed) + || len32(quotes[i]) || quotes[i] // quotes[i] = UTF-8 quote id at index i + || for each output j (in request order): + len32(amount_j) || amount_j // amount_j = canonical minimal big-endian + || len32(B_j) || B_j // B_j = raw compressed point bytes +``` + +Where: + +- `quotes[i]` is the UTF-8 encoded quote ID from the request's `quotes` array at index `i` +- `outputs` are **all blinded messages** from the request's `outputs` array (regardless of which quote they correspond to) +- `||` denotes byte concatenation and `len32(x)` is the 32-bit (4-byte) big-endian length of the following data `x`. + ### Signature Validation Failure If **any signature in the batch is invalid**, the mint MUST reject the **entire batch** and return an error. This maintains atomicity: all quotes must be successfully authenticated and minted together, or none at all.