From dec11550cfae3a99ba21dbe83798e938bcd1fc33 Mon Sep 17 00:00:00 2001 From: thesimplekid Date: Sun, 10 Nov 2024 17:30:39 +0000 Subject: [PATCH] feat: nut19 signature on mint request --- 04.md | 6 +- 20.md | 185 +++++++++++++++++++++++++++++++++++++++++++++++ README.md | 1 + error_codes.md | 3 + tests/20-test.md | 79 ++++++++++++++++++++ 5 files changed, 272 insertions(+), 2 deletions(-) create mode 100644 20.md create mode 100644 tests/20-test.md diff --git a/04.md b/04.md index e3f7a28b..dae26011 100644 --- a/04.md +++ b/04.md @@ -2,6 +2,8 @@ `mandatory` +`used in: NUT-20` + --- Minting tokens is a two-step process: requesting a mint quote and minting new tokens. Here, we describe both steps. @@ -24,7 +26,7 @@ The wallet of `Alice` includes the following `PostMintQuoteBolt11Request` data i { "amount": , "unit": , - "description": + "description": // Optional } ``` @@ -49,7 +51,7 @@ Where `quote` is the quote ID and `request` is the payment request to fulfill. ` - `"PAID"` means that the request has been paid. - `"ISSUED"` means that the quote has already been issued. -Note: `quote` is a **unique and random** id generated by the mint to internally look up the payment state. `quote` **MUST** remain a secret between user and mint and **MUST NOT** be derivable from the payment request. A third party who knows the `quote` ID can front-run and steal the tokens that this operation mints. +> [!CAUTION] > `quote` is a **unique and random** id generated by the mint to internally look up the payment state. `quote` **MUST** remain a secret between user and mint and **MUST NOT** be derivable from the payment request. A third party who knows the `quote` ID can front-run and steal the tokens that this operation mints. ## Example diff --git a/20.md b/20.md new file mode 100644 index 00000000..02bd5027 --- /dev/null +++ b/20.md @@ -0,0 +1,185 @@ +# NUT-20: Signature on Mint Quote + +`optional` + +`depends on: NUT-04` + +--- + +This NUT defines signature-based authentication for mint quote redemption. When requesting a mint quote, clients provide a public key. The mint will then require a valid signature from the corresponding secret key to process the mint operation. + +> [!CAUTION] > [NUT-04][04] mint quotes without a public key can be minted by anyone who knows the mint quote id without providing a signature. + +## Mint quote + +To request a mint quote, the wallet of `Alice` makes a `POST /v1/mint/quote/{method}` request where `method` is the payment method requested. We present an example with the `method` being `bolt11` here. + +```http +POST https://mint.host:3338/v1/mint/quote/bolt11 +``` + +The wallet of `Alice` includes the following `PostMintQuoteBolt11Request` data in its request: + +```json +{ + "amount": , + "unit": , + "description": , // Optional + "pubkey": // Optional <-- New +} +``` + +with the requested `amount`,`unit`, and `description` according to [NUT-04][04]. + +`pubkey` is the public key that will be required for signature verification during the minting operation. The mint will only mint ecash after receiving a valid signature from the corresponding private key in the subsequent `PostMintRequest`. + +> [!IMPORTANT] > **Privacy:** To prevent the mint from being able to link multiple mint quotes, wallets **SHOULD** generate a unique public key for each mint quote request. + +The mint `Bob` then responds with a `PostMintQuoteBolt11Response`: + +```json +{ + "quote": , + "request": , + "state": , + "expiry": , + "pubkey": // Optional <-- New +} +``` + +The response is the same as in [NUT-04][04] except for `pubkey` which has been provided by the wallet in the previous request. + +## Example + +Request of `Alice` with curl: + +```bash +curl -X POST http://localhost:3338/v1/mint/quote/bolt11 -d '{"amount": 10, "unit": "sat", "pubkey": "03d56ce4e446a85bbdaa547b4ec2b073d40ff802831352b8272b7dd7a4de5a7cac"}' -H "Content-Type: application/json" +``` + +Response of `Bob`: + +```json +{ + "quote": "9d745270-1405-46de-b5c5-e2762b4f5e00", + "request": "lnbc100n1pj4apw9...", + "state": "UNPAID", + "expiry": 1701704757, + "pubkey": "03d56ce4e446a85bbdaa547b4ec2b073d40ff802831352b8272b7dd7a4de5a7cac" +} +``` + +## Signing the mint request + +### 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: + +``` +msg_to_sign = quote || B_0 || ... || B_(n-1) +``` + +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`. + +### 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. + +## Minting tokens + +After requesting a mint quote and paying the request, the wallet proceeds with minting new tokens by calling the `POST /v1/mint/{method}` endpoint where `method` is the payment method requested (here `bolt11`). + +```http +POST https://mint.host:3338/v1/mint/bolt11 +``` + +The wallet `Alice` includes the following `PostMintBolt11Request` data in its request + +```json +{ + "quote": , + "outputs": , + "signature": <-- New +} +``` + +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. + +The mint responds with a `PostMintBolt11Response` as in [NUT-04][04] if all validations are successful. + +## Example + +Request of `Alice` with curl: + +```bash +curl -X POST https://mint.host:3338/v1/mint/bolt11 -H "Content-Type: application/json" -d \ +'{ + "quote": "9d745270-1405-46de-b5c5-e2762b4f5e00", + "outputs": [ + { + "amount": 8, + "id": "009a1f293253e41e", + "B_": "035015e6d7ade60ba8426cefaf1832bbd27257636e44a76b922d78e79b47cb689d" + }, + { + "amount": 2, + "id": "009a1f293253e41e", + "B_": "0288d7649652d0a83fc9c966c969fb217f15904431e61a44b14999fabc1b5d9ac6" + } + ], + "signature": "d9be080b33179387e504bb6991ea41ae0dd715e28b01ce9f63d57198a095bccc776874914288e6989e97ac9d255ac667c205fa8d90a211184b417b4ffdd24092" + +}' +``` + +Response of `Bob`: + +```json +{ + "signatures": [ + { + "id": "009a1f293253e41e", + "amount": 2, + "C_": "0224f1c4c564230ad3d96c5033efdc425582397a5a7691d600202732edc6d4b1ec" + }, + { + "id": "009a1f293253e41e", + "amount": 8, + "C_": "0277d1de806ed177007e5b94a8139343b6382e472c752a74e99949d511f7194f6c" + } + ] +} +``` + +## Errors + +If the wallet user `Alice` does not include a signature on the `PostMintBolt11Request` but did include a `pubkey` in the `PostMintBolt11QuoteRequest` then `Bob` **MUST** respond with an error. `Alice` **CAN** repeat the request with a valid signature. + +See [Error Codes][errors]: + +- `20008`: Mint quote with `pubkey` but no valid `signature` provided for mint request. +- `20009`: Mint quote requires `pubkey` but none given or invalid `pubkey`. + +## Settings + +The settings for this NUT indicate the support for requiring a signature before minting. They are part of the info response of the mint ([NUT-06][06]) which in this case reads + +```json +{ + "20": { + "supported": , + } +} +``` + +[00]: 00.md +[04]: 04.md +[06]: 06.md +[errors]: error_codes.md diff --git a/README.md b/README.md index ac0a5bc4..7ca750b4 100644 --- a/README.md +++ b/README.md @@ -35,6 +35,7 @@ Wallets and mints `MUST` implement all mandatory specs and `CAN` implement optio | [17][17] | WebSocket subscriptions | [Nutshell][py] | [Nutshell][py] | | [18][18] | Payment requests | [Cashu.me][cashume], [Boardwalk][bwc], [cdk-cli] | - | | [19][19] | Cached Responses | - | [Nutshell][py], [cdk-mintd] | +| [20][20] | Signature on Mint Quote | [cdk-cli] | [cdk-mintd] | #### Wallets: diff --git a/error_codes.md b/error_codes.md index f3403309..892428f4 100644 --- a/error_codes.md +++ b/error_codes.md @@ -16,6 +16,8 @@ | 20005 | Quote is pending | [NUT-04][04], [NUT-05][05] | | 20006 | Invoice already paid | [NUT-05][05] | | 20007 | Quote is expired | [NUT-04][04], [NUT-05][05] | +| 20008 | Signature for mint request invalid | [NUT-20][20] | +| 20009 | Pubkey required for mint quote | [NUT-20][20] | [00]: 00.md [01]: 01.md @@ -30,3 +32,4 @@ [10]: 10.md [11]: 11.md [12]: 12.md +[20]: 20.md diff --git a/tests/20-test.md b/tests/20-test.md new file mode 100644 index 00000000..e534f948 --- /dev/null +++ b/tests/20-test.md @@ -0,0 +1,79 @@ +# NUT-20 Test Vectors + +The following is a `PostMintBolt11Request` with a valid 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": "d4b386f21f7aa7172f0994ee6e4dd966539484247ea71c99b81b8e09b1bb2acbc0026a43c221fd773471dc30d6a32b04692e6837ddaccf0830a63128308e4ee0" +} +``` + +The following is the expected message to sign on the above `PostMintBolt11Request`. + +``` +[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" +} +```