Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

NUT-20: signature on mint request #188

Merged
merged 1 commit into from
Dec 25, 2024
Merged
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
6 changes: 4 additions & 2 deletions 04.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -24,7 +26,7 @@ The wallet of `Alice` includes the following `PostMintQuoteBolt11Request` data i
{
"amount": <int>,
"unit": <str_enum["sat"]>,
"description": <str|null>
"description": <str> // Optional
Comment on lines -27 to +29
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

}
```

Expand All @@ -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

Expand Down
185 changes: 185 additions & 0 deletions 20.md
Original file line number Diff line number Diff line change
@@ -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": <int>,
"unit": <str_enum["sat"]>,
"description": <str>, // Optional
"pubkey": <str> // 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": <str>,
"request": <str>,
"state": <str_enum[STATE]>,
"expiry": <int>,
"pubkey": <str> // 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": <str>,
"outputs": <Array[BlindedMessage]>,
"signature": <str|null> <-- 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": <bool>,
}
}
```

[00]: 00.md
[04]: 04.md
[06]: 06.md
[errors]: error_codes.md
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:

Expand Down
3 changes: 3 additions & 0 deletions error_codes.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -30,3 +32,4 @@
[10]: 10.md
[11]: 11.md
[12]: 12.md
[20]: 20.md
79 changes: 79 additions & 0 deletions tests/20-test.md
Original file line number Diff line number Diff line change
@@ -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"
}
```
Loading