diff --git a/04.md b/04.md index 966a0dae..3fa76250 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, `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 c6a6b2b4..80ec7c91 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 ``` diff --git a/tests/29-tests.md b/tests/29-tests.md index 269f6f2d..9ba123c5 100644 --- a/tests/29-tests.md +++ b/tests/29-tests.md @@ -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 @@ -151,7 +151,7 @@ signature[0]: 9408920d0b94cee5eb6df20f14d2a655e7ce2ce309dc1f1aeb69b219efe7671693 } ], "signatures": [ - "9408920d0b94cee5eb6df20f14d2a655e7ce2ce309dc1f1aeb69b219efe76716933b2206eba3a54f9a953c92edaa922ab3e6912e02383dda42a193409567a0dc" + "a913e48177027d87e0e38c6f2021763c46997ff4866a4b63ebca800b0776b28519eab37377cf9bc1869e489d7b25747b7a998eaa1c33c2cac7fa168449d8267a" ] } ```