NUT-20: signature on mint request #188

Merged
thesimplekid merged 1 commit from sign_mint_quote into main 2024-12-25 20:19:57 +00:00
thesimplekid commented 2024-11-10 17:32:40 +00:00 (Migrated from github.com)

This NUT defines a protocol extension that enables signature-based authentication for mint quote redemption. When requesting a mint quote, clients can provide a public key. The mint will then require a valid signature from the corresponding secret key before processing the mint.

This is defined as an optional extension to NUT-04 and the intention is to reuse it and make it mandatory for bolt12 and onchain.

This NUT defines a protocol extension that enables signature-based authentication for mint quote redemption. When requesting a mint quote, clients can provide a public key. The mint will then require a valid signature from the corresponding secret key before processing the mint. This is defined as an optional extension to NUT-04 and the intention is to reuse it and make it mandatory for bolt12 and onchain. - [x] CDK https://github.com/cashubtc/cdk/pull/446 - [x] Nutshell https://github.com/cashubtc/nutshell/pull/670 - [ ] Nutmix - [ ] goNuts - [ ] cashu-ts
davidcaseria (Migrated from github.com) reviewed 2024-11-10 17:32:40 +00:00
a1denvalu3 (Migrated from github.com) reviewed 2024-11-10 17:32:40 +00:00
a1denvalu3 (Migrated from github.com) reviewed 2024-11-12 20:17:09 +00:00
a1denvalu3 (Migrated from github.com) commented 2024-11-12 20:17:01 +00:00
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.md)) which in this case reads
```suggestion 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.md)) which in this case reads ```
prusnak (Migrated from github.com) reviewed 2024-11-12 20:29:42 +00:00
prusnak (Migrated from github.com) commented 2024-11-12 20:29:42 +00:00

Another way how to fix the issue is to add the following at the end of the file:


[06]: 06.md

This seems to be preferred way used in other documents.

Another way how to fix the issue is to add the following at the end of the file: ``` markdown [06]: 06.md ``` This seems to be preferred way used in other documents.
a1denvalu3 commented 2024-11-12 21:23:51 +00:00 (Migrated from github.com)

@thesimplekid Should we make it clear that wallets should use an ephemeral (one time) key?

@thesimplekid Should we make it clear that wallets should use an ephemeral (one time) key?
a1denvalu3 (Migrated from github.com) reviewed 2024-11-12 23:57:37 +00:00
@ -16,6 +16,8 @@
| 20005 | Quote is pending | [NUT-04][04], [NUT-05][05] |
a1denvalu3 (Migrated from github.com) commented 2024-11-12 23:57:36 +00:00

We need an error for the invalid witness as well.

We need an error for the invalid witness as well.
thesimplekid commented 2024-11-14 11:58:27 +00:00 (Migrated from github.com)

@thesimplekid Should we make it clear that wallets should use an ephemeral (one time) key?

Added note on this

> @thesimplekid Should we make it clear that wallets should use an ephemeral (one time) key? Added note on this
davidcaseria (Migrated from github.com) reviewed 2024-11-14 13:05:00 +00:00
davidcaseria (Migrated from github.com) commented 2024-11-14 13:05:00 +00:00

nit: I think it's best to stick to the RFC 2219 keywords and definitions (i.e., CAN -> MAY or in this case SHOULD?).

We use CAN (or even can) many times elsewhere so this is by no means a blocking comment, but it's something I noticed in the spec that we SHOULD ( 😉 ) cleanup.

nit: I think it's best to stick to the [RFC 2219](https://datatracker.ietf.org/doc/html/rfc2119) keywords and definitions (i.e., **CAN** -> **MAY** or in this case **SHOULD**?). We use **CAN** (or even can) many times elsewhere so this is by no means a blocking comment, but it's something I noticed in the spec that we **SHOULD** ( :wink: ) cleanup.
thesimplekid (Migrated from github.com) reviewed 2024-11-14 13:37:49 +00:00
thesimplekid (Migrated from github.com) commented 2024-11-14 13:37:49 +00:00

Good catch I thought CAN was a keyword.

Good catch I thought CAN was a keyword.
a1denvalu3 (Migrated from github.com) reviewed 2024-11-14 15:14:32 +00:00
a1denvalu3 (Migrated from github.com) commented 2024-11-14 15:14:32 +00:00
To mint a quote where a public key was provided, the minter needs to include signatures in the `PostMintBolt11Request`. We use a [BIP340](https://github.com/bitcoin/bips/blob/master/bip-0340.mediawiki) signature on the SHA-256 hash of the message to sign as defined above.

Also why hash it again if everything that goes into the schnorr signature is hashed anyway?

```suggestion To mint a quote where a public key was provided, the minter needs to include signatures in the `PostMintBolt11Request`. We use a [BIP340](https://github.com/bitcoin/bips/blob/master/bip-0340.mediawiki) signature on the SHA-256 hash of the message to sign as defined above. ``` Also why hash it again if everything that goes into the schnorr signature is hashed anyway?
a1denvalu3 (Migrated from github.com) reviewed 2024-11-14 15:17:57 +00:00
a1denvalu3 (Migrated from github.com) commented 2024-11-14 15:17:57 +00:00

calle and I were discussing whether it should be better to just JSON-serialize the entire payload and then use the utf-8 encoding of the quote_id and the entire serialized payload.

I'm fine with either approach what do you think?

calle and I were discussing whether it should be better to just JSON-serialize the entire payload and then use the utf-8 encoding of the `quote_id` and the entire serialized payload. I'm fine with either approach what do you think?
a1denvalu3 (Migrated from github.com) reviewed 2024-11-14 15:20:46 +00:00
a1denvalu3 (Migrated from github.com) commented 2024-11-14 15:20:46 +00:00

For reference how I am doing it right now in my Nutshell branch (before having seen this):

serialized_outputs = b"".join([o.json().encode("utf-8") for o in outputs])
msgbytes = quote.quote.encode("utf-8") + serialized_outputs
For reference how I am doing it right now in my Nutshell branch (before having seen this): ```python serialized_outputs = b"".join([o.json().encode("utf-8") for o in outputs]) msgbytes = quote.quote.encode("utf-8") + serialized_outputs ```
thesimplekid (Migrated from github.com) reviewed 2024-11-14 15:36:30 +00:00
thesimplekid (Migrated from github.com) commented 2024-11-14 15:36:30 +00:00

Funny, my first thought was to do this until I looked at the sig all draft. But I think its better not to do the json as then we rely on the json libs the different implementation use to produce the same string for example escape chars and white space could be handled differently. The current implantation avoids this and is simple to implement

    /// Constructs the message to be signed according to NUT-19 specification.
    ///
    /// The message is constructed by concatenating:
    /// 1. The quote ID
    /// 2. All blinded secrets (B_0 through B_n)
    ///
    /// Format: `quote_id || B_0 || B_1 || ... || B_n`
    pub fn msg_to_sign(&self) -> String {
        // Pre-calculate capacity to avoid reallocations
        let capacity = self.quote.len() + (self.outputs.len() * 66);
        let mut msg = String::with_capacity(capacity);

        msg.push_str(&self.quote);
        for output in &self.outputs {
            msg.push_str(&output.blinded_secret.to_hex());
        }
        msg
    }
Funny, my first thought was to do this until I looked at the sig all draft. But I think its better not to do the json as then we rely on the json libs the different implementation use to produce the same string for example escape chars and white space could be handled differently. The current implantation avoids this and is simple to implement ```rust /// Constructs the message to be signed according to NUT-19 specification. /// /// The message is constructed by concatenating: /// 1. The quote ID /// 2. All blinded secrets (B_0 through B_n) /// /// Format: `quote_id || B_0 || B_1 || ... || B_n` pub fn msg_to_sign(&self) -> String { // Pre-calculate capacity to avoid reallocations let capacity = self.quote.len() + (self.outputs.len() * 66); let mut msg = String::with_capacity(capacity); msg.push_str(&self.quote); for output in &self.outputs { msg.push_str(&output.blinded_secret.to_hex()); } msg } ```
thesimplekid (Migrated from github.com) reviewed 2024-11-14 15:39:01 +00:00
thesimplekid (Migrated from github.com) commented 2024-11-14 15:39:00 +00:00

Also why hash it again if everything that goes into the schnorr signature is hashed anyway?

I used the wording from NUT-11. Its only hashed once maybe that wording is confusing and should be removed?

> Also why hash it again if everything that goes into the schnorr signature is hashed anyway? I used the wording from NUT-11. Its only hashed once maybe that wording is confusing and should be removed?
a1denvalu3 (Migrated from github.com) reviewed 2024-11-14 15:49:16 +00:00
a1denvalu3 (Migrated from github.com) commented 2024-11-14 15:49:16 +00:00

So yea I've also seen this other times in the NUTs. I personally think it's not needed but for the sake of consistency let's leave it like this. (Also we cannot change NUT-11 without breaking a lot of implementations)

So yea I've also seen this other times in the NUTs. I personally think it's not needed but for the sake of consistency let's leave it like this. (Also we cannot change NUT-11 without breaking a lot of implementations)
thesimplekid (Migrated from github.com) reviewed 2024-11-14 16:06:12 +00:00
thesimplekid (Migrated from github.com) commented 2024-11-14 16:06:12 +00:00

Yeah I think its best this and NUT-11 match.

Yeah I think its best this and NUT-11 match.
callebtc (Migrated from github.com) reviewed 2024-11-15 01:21:30 +00:00
callebtc (Migrated from github.com) commented 2024-11-15 01:21:30 +00:00

We should probably return the pubkey here as well (so that it's clear that the quote is locked)

We should probably return the `pubkey` here as well (so that it's clear that the quote is locked)
callebtc (Migrated from github.com) reviewed 2024-11-15 01:21:56 +00:00
callebtc (Migrated from github.com) commented 2024-11-15 01:21:56 +00:00

We would also return pubkey here if we did above.

We would also return `pubkey` here if we did above.
callebtc (Migrated from github.com) reviewed 2024-11-15 01:44:01 +00:00
callebtc (Migrated from github.com) commented 2024-11-15 01:31:36 +00:00

calle and I were discussing whether it should be better to just JSON-serialize the entire payload and then use the utf-8 encoding of the quote_id and the entire serialized payload.

I think that was a misunderstanding. I was wondering if we should serialize the outputs as hex to bytes or as utf-8. I believe it would be more coherent to use utf-8 here so we can use the same encoding as for the quote ID. That's what I thought we refer to when we said "serialize as json".

i.e.

serialized_outputs = b"".join([o.B_.encode("utf-8") for o in outputs])
msgbytes = quote.quote.encode("utf-8") + serialized_outputs
> calle and I were discussing whether it should be better to just JSON-serialize the entire payload and then use the utf-8 encoding of the `quote_id` and the entire serialized payload. I think that was a misunderstanding. I was wondering if we should serialize the outputs as hex to bytes or as utf-8. I believe it would be more coherent to use utf-8 here so we can use the same encoding as for the quote ID. That's what I thought we refer to when we said "serialize as json". i.e. ```python serialized_outputs = b"".join([o.B_.encode("utf-8") for o in outputs]) msgbytes = quote.quote.encode("utf-8") + serialized_outputs ```
@ -16,6 +16,8 @@
| 20005 | Quote is pending | [NUT-04][04], [NUT-05][05] |
callebtc (Migrated from github.com) commented 2024-11-15 01:40:25 +00:00

If we want to make a required flag for the mint info, we would need an error code for the Quote without pubkey case

If we want to make a `required` flag for the mint info, we would need an error code for the `Quote without pubkey` case
callebtc (Migrated from github.com) commented 2024-11-15 01:43:55 +00:00

Maybe "invalid witness" and "no witness provided" can be the same error.

Maybe "invalid witness" and "no witness provided" can be the same error.
thesimplekid (Migrated from github.com) reviewed 2024-11-15 21:38:20 +00:00
thesimplekid (Migrated from github.com) commented 2024-11-15 21:38:20 +00:00

Is o.B_ here the hex encoding of the pubkey?

Is `o.B_` here the hex encoding of the pubkey?
thesimplekid (Migrated from github.com) reviewed 2024-11-16 09:06:19 +00:00
thesimplekid (Migrated from github.com) commented 2024-11-16 09:06:18 +00:00
    /// Constructs the message to be signed according to NUT-19 specification.
    ///
    /// The message is constructed by concatenating (as UTF-8 encoded bytes):
    /// 1. The quote ID (as UTF-8)
    /// 2. All blinded secrets (B_0 through B_n) converted to hex strings (as UTF-8)
    ///
    /// Format: `quote_id || B_0 || B_1 || ... || B_n`
    /// where each component is encoded as UTF-8 bytes
    pub fn msg_to_sign(&self) -> Vec<u8> {
        // Pre-calculate capacity to avoid reallocations
        let capacity = self.quote.len() + (self.outputs.len() * 66);
        let mut msg = Vec::with_capacity(capacity);
        msg.append(&mut self.quote.clone().into_bytes()); // String.into_bytes() produces UTF-8
        for output in &self.outputs {
            // to_hex() creates a hex string, into_bytes() converts it to UTF-8 bytes
            msg.append(&mut output.blinded_secret.to_hex().into_bytes());
        }
        msg
    }
```rust /// Constructs the message to be signed according to NUT-19 specification. /// /// The message is constructed by concatenating (as UTF-8 encoded bytes): /// 1. The quote ID (as UTF-8) /// 2. All blinded secrets (B_0 through B_n) converted to hex strings (as UTF-8) /// /// Format: `quote_id || B_0 || B_1 || ... || B_n` /// where each component is encoded as UTF-8 bytes pub fn msg_to_sign(&self) -> Vec<u8> { // Pre-calculate capacity to avoid reallocations let capacity = self.quote.len() + (self.outputs.len() * 66); let mut msg = Vec::with_capacity(capacity); msg.append(&mut self.quote.clone().into_bytes()); // String.into_bytes() produces UTF-8 for output in &self.outputs { // to_hex() creates a hex string, into_bytes() converts it to UTF-8 bytes msg.append(&mut output.blinded_secret.to_hex().into_bytes()); } msg } ```
thesimplekid (Migrated from github.com) reviewed 2024-11-16 09:14:38 +00:00
thesimplekid (Migrated from github.com) commented 2024-11-16 09:14:37 +00:00

f866a2511c160870c0e250df176c595001b6d481

f866a2511c160870c0e250df176c595001b6d481
elnosh (Migrated from github.com) reviewed 2024-11-25 16:21:06 +00:00
elnosh (Migrated from github.com) commented 2024-11-25 16:21:06 +00:00

I wonder if the required flag should be set by specific NUT. For BOLT12 and Onchain NUTs it will be a MUST as it is already specified in the PRs for those but I don't think this should break wallets that don't implement it for NUT-04.

I wonder if the `required` flag should be set by specific NUT. For BOLT12 and Onchain NUTs it will be a `MUST` as it is already specified in the PRs for those but I don't think this should break wallets that don't implement it for NUT-04.
thesimplekid (Migrated from github.com) reviewed 2024-12-03 13:21:48 +00:00
thesimplekid (Migrated from github.com) commented 2024-12-03 13:21:48 +00:00

I think we can remove the required flag here. Leaving it up to wallets to choose to use it or not for NUT04 and future nuts that want to require it (eg bolt12) can specify that there.

I think we can remove the required flag here. Leaving it up to wallets to choose to use it or not for NUT04 and future nuts that want to require it (eg bolt12) can specify that there.
callebtc (Migrated from github.com) approved these changes 2024-12-04 15:50:30 +00:00
callebtc (Migrated from github.com) left a comment

LGTM

LGTM
thesimplekid commented 2024-12-04 15:51:19 +00:00 (Migrated from github.com)

ACK cccbb1dd068791557b4a1ccc6e5091a901056a09

ACK cccbb1dd068791557b4a1ccc6e5091a901056a09
thesimplekid (Migrated from github.com) reviewed 2024-12-04 15:53:11 +00:00
thesimplekid (Migrated from github.com) commented 2024-12-04 15:52:57 +00:00
| 20008 | Signature on mint request not provided or invalid | [NUT-20][20]                             |
```suggestion | 20008 | Signature on mint request not provided or invalid | [NUT-20][20] | ```
@ -30,3 +32,4 @@
[10]: 10.md
thesimplekid (Migrated from github.com) commented 2024-12-04 15:53:07 +00:00
[20]: 20.md
```suggestion [20]: 20.md ```
thesimplekid (Migrated from github.com) reviewed 2024-12-04 15:55:35 +00:00
thesimplekid (Migrated from github.com) commented 2024-12-04 15:55:35 +00:00
| 20008 | Signature on mint request not provided or invalid | [NUT-20][20]                             |
| 20009 | Pubkey required on mint quote                             | [NUT-20][20]                              |
```suggestion | 20008 | Signature on mint request not provided or invalid | [NUT-20][20] | | 20009 | Pubkey required on mint quote | [NUT-20][20] | ```
a1denvalu3 (Migrated from github.com) reviewed 2024-12-07 14:45:19 +00:00
@ -17,2 +17,4 @@
| 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] |
a1denvalu3 (Migrated from github.com) commented 2024-12-07 14:45:19 +00:00

Is this error still necessary?

Is this error still necessary?
thesimplekid (Migrated from github.com) reviewed 2024-12-09 09:47:11 +00:00
@ -17,2 +17,4 @@
| 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] |
thesimplekid (Migrated from github.com) commented 2024-12-09 09:47:11 +00:00

Will be used for BOLT12 and onchain

Will be used for BOLT12 and onchain
callebtc (Migrated from github.com) reviewed 2024-12-11 11:51:26 +00:00
callebtc (Migrated from github.com) commented 2024-12-11 11:49:37 +00:00
For future reference: https://json-schema.org/understanding-json-schema/reference/object#required
Sign in to join this conversation.
No description provided.