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

mint - NUT20 verify signatures on mint quotes #113

Merged
merged 1 commit into from
Feb 12, 2025
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
7 changes: 5 additions & 2 deletions cashu/cashu.go
Original file line number Diff line number Diff line change
Expand Up @@ -473,11 +473,13 @@ const (
MintQuoteRequestNotPaidErrCode CashuErrCode = 20001
MintQuoteAlreadyIssuedErrCode CashuErrCode = 20002
MintingDisabledErrCode CashuErrCode = 20003
MintQuoteInvalidSigErrCode CashuErrCode = 20008

MeltQuotePendingErrCode CashuErrCode = 20005
MeltQuoteAlreadyPaidErrCode CashuErrCode = 20006
LightningPaymentErrCode CashuErrCode = 20008
MeltQuoteErrCode CashuErrCode = 20009

//LightningPaymentErrCode CashuErrCode = 20008
MeltQuoteErrCode CashuErrCode = 20009
)

var (
Expand All @@ -492,6 +494,7 @@ var (
MintQuoteAlreadyIssued = Error{Detail: "quote already issued", Code: MintQuoteAlreadyIssuedErrCode}
MintingDisabled = Error{Detail: "minting is disabled", Code: MintingDisabledErrCode}
MintAmountExceededErr = Error{Detail: "max amount for minting exceeded", Code: AmountLimitExceeded}
MintQuoteInvalidSigErr = Error{Detail: "Mint quote with pubkey but no valid signature provided.", Code: MintQuoteInvalidSigErrCode}
OutputsOverQuoteAmountErr = Error{Detail: "sum of the output amounts is greater than quote amount", Code: StandardErrCode}
ProofAlreadyUsedErr = Error{Detail: "proof already used", Code: ProofAlreadyUsedErrCode}
ProofPendingErr = Error{Detail: "proof is pending", Code: ProofAlreadyUsedErrCode}
Expand Down
3 changes: 3 additions & 0 deletions cashu/nuts/nut06/nut06.go
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@ type Nuts struct {
Nut14 Supported `json:"14"`
Nut15 *NutSetting `json:"15,omitempty"`
Nut17 nut17.InfoSetting `json:"17"`
Nut20 Supported `json:"20"`
}

// custom unmarshaller because format to signal support for nut-15 changed.
Expand All @@ -109,6 +110,7 @@ func (nuts *Nuts) UnmarshalJSON(data []byte) error {
Nut14 Supported `json:"14"`
Nut15 json.RawMessage `json:"15,omitempty"`
Nut17 nut17.InfoSetting `json:"17"`
Nut20 Supported `json:"20"`
}

if err := json.Unmarshal(data, &tempNuts); err != nil {
Expand All @@ -125,6 +127,7 @@ func (nuts *Nuts) UnmarshalJSON(data []byte) error {
nuts.Nut12 = tempNuts.Nut12
nuts.Nut14 = tempNuts.Nut14
nuts.Nut17 = tempNuts.Nut17
nuts.Nut20 = tempNuts.Nut20

if err := json.Unmarshal(tempNuts.Nut15, &nuts.Nut15); err != nil {
var nut15Methods []MethodSetting
Expand Down
15 changes: 15 additions & 0 deletions cashu/nuts/nut20/nut20.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,3 +26,18 @@ func SignMintQuote(

return sig, nil
}

func VerifyMintQuoteSignature(
signature *schnorr.Signature,
quoteId string,
blindedMessages cashu.BlindedMessages,
publicKey *secp256k1.PublicKey,
) bool {
msg := quoteId
for _, bm := range blindedMessages {
msg += bm.B_
}
hash := sha256.Sum256([]byte(msg))

return signature.Verify(hash[:], publicKey)
}
154 changes: 154 additions & 0 deletions cashu/nuts/nut20/nut20_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
package nut20

import (
"encoding/hex"
"testing"

"github.com/btcsuite/btcd/btcec/v2/schnorr"
"github.com/decred/dcrd/dcrec/secp256k1/v4"
"github.com/elnosh/gonuts/cashu"
)

func TestSignMintQuote(t *testing.T) {
privateKey, _ := secp256k1.GeneratePrivateKey()

tests := []struct {
quoteId string
outputs cashu.BlindedMessages
privateKey *secp256k1.PrivateKey
}{
{
quoteId: "9d745270-1405-46de-b5c5-e2762b4f5e00",
outputs: cashu.BlindedMessages{
cashu.BlindedMessage{
Amount: 1,
Id: "00456a94ab4e1c46",
B_: "0342e5bcc77f5b2a3c2afb40bb591a1e27da83cddc968abdc0ec4904201a201834",
},
cashu.BlindedMessage{
Amount: 1,
Id: "00456a94ab4e1c46",
B_: "032fd3c4dc49a2844a89998d5e9d5b0f0b00dde9310063acb8a92e2fdafa4126d4",
},
cashu.BlindedMessage{
Amount: 1,
Id: "00456a94ab4e1c46",
B_: "033b6fde50b6a0dfe61ad148fff167ad9cf8308ded5f6f6b2fe000a036c464c311",
},
cashu.BlindedMessage{
Amount: 1,
Id: "00456a94ab4e1c46",
B_: "02be5a55f03e5c0aaea77595d574bce92c6d57a2a0fb2b5955c0b87e4520e06b53",
},
cashu.BlindedMessage{
Amount: 1,
Id: "00456a94ab4e1c46",
B_: "02209fc2873f28521cbdde7f7b3bb1521002463f5979686fd156f23fe6a8aa2b79",
},
},
privateKey: privateKey,
},
}

for _, test := range tests {
sig, err := SignMintQuote(test.privateKey, test.quoteId, test.outputs)
if err != nil {
t.Fatalf("got unexpected error signing mint quote: %v", err)
}

if !VerifyMintQuoteSignature(sig, test.quoteId, test.outputs, test.privateKey.PubKey()) {
t.Fatal("generated invalid signature on mint quote")
}
}
}

func TestVerifyMintQuoteSignature(t *testing.T) {
tests := []struct {
quoteId string
outputs cashu.BlindedMessages
pubkey string
signature string
expected bool
}{
{
quoteId: "9d745270-1405-46de-b5c5-e2762b4f5e00",
outputs: cashu.BlindedMessages{
cashu.BlindedMessage{
Amount: 1,
Id: "00456a94ab4e1c46",
B_: "0342e5bcc77f5b2a3c2afb40bb591a1e27da83cddc968abdc0ec4904201a201834",
},
cashu.BlindedMessage{
Amount: 1,
Id: "00456a94ab4e1c46",
B_: "032fd3c4dc49a2844a89998d5e9d5b0f0b00dde9310063acb8a92e2fdafa4126d4",
},
cashu.BlindedMessage{
Amount: 1,
Id: "00456a94ab4e1c46",
B_: "033b6fde50b6a0dfe61ad148fff167ad9cf8308ded5f6f6b2fe000a036c464c311",
},
cashu.BlindedMessage{
Amount: 1,
Id: "00456a94ab4e1c46",
B_: "02be5a55f03e5c0aaea77595d574bce92c6d57a2a0fb2b5955c0b87e4520e06b53",
},
cashu.BlindedMessage{
Amount: 1,
Id: "00456a94ab4e1c46",
B_: "02209fc2873f28521cbdde7f7b3bb1521002463f5979686fd156f23fe6a8aa2b79",
},
},
pubkey: "03d56ce4e446a85bbdaa547b4ec2b073d40ff802831352b8272b7dd7a4de5a7cac",
signature: "d4b386f21f7aa7172f0994ee6e4dd966539484247ea71c99b81b8e09b1bb2acbc0026a43c221fd773471dc30d6a32b04692e6837ddaccf0830a63128308e4ee0",
expected: true,
},
{
quoteId: "9d745270-1405-46de-b5c5-e2762b4f5e00",
outputs: cashu.BlindedMessages{
cashu.BlindedMessage{
Amount: 1,
Id: "00456a94ab4e1c46",
B_: "0342e5bcc77f5b2a3c2afb40bb591a1e27da83cddc968abdc0ec4904201a201834",
},
cashu.BlindedMessage{
Amount: 1,
Id: "00456a94ab4e1c46",
B_: "032fd3c4dc49a2844a89998d5e9d5b0f0b00dde9310063acb8a92e2fdafa4126d4",
},
cashu.BlindedMessage{
Amount: 1,
Id: "00456a94ab4e1c46",
B_: "033b6fde50b6a0dfe61ad148fff167ad9cf8308ded5f6f6b2fe000a036c464c311",
},
cashu.BlindedMessage{
Amount: 1,
Id: "00456a94ab4e1c46",
B_: "02be5a55f03e5c0aaea77595d574bce92c6d57a2a0fb2b5955c0b87e4520e06b53",
},
cashu.BlindedMessage{
Amount: 1,
Id: "00456a94ab4e1c46",
B_: "02209fc2873f28521cbdde7f7b3bb1521002463f5979686fd156f23fe6a8aa2b79",
},
},
pubkey: "03d56ce4e446a85bbdaa547b4ec2b073d40ff802831352b8272b7dd7a4de5a7cac",
signature: "cb2b8e7ea69362dfe2a07093f2bbc319226db33db2ef686c940b5ec976bcbfc78df0cd35b3e998adf437b09ee2c950bd66dfe9eb64abd706e43ebc7c669c36c3",
expected: false,
},
}

for _, test := range tests {
sigBytes, _ := hex.DecodeString(test.signature)
signature, _ := schnorr.ParseSignature(sigBytes)

pubkeyBytes, _ := hex.DecodeString(test.pubkey)
publickey, _ := secp256k1.ParsePubKey(pubkeyBytes)

valid := VerifyMintQuoteSignature(signature, test.quoteId, test.outputs, publickey)
if valid != test.expected {
t.Fatalf("expected '%v' but got '%v'", test.expected, valid)
}
}

}
46 changes: 45 additions & 1 deletion mint/mint.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import (
"time"

"github.com/btcsuite/btcd/btcec/v2"
"github.com/btcsuite/btcd/btcec/v2/schnorr"
"github.com/btcsuite/btcd/btcutil/hdkeychain"
"github.com/btcsuite/btcd/chaincfg"
"github.com/decred/dcrd/dcrec/secp256k1/v4"
Expand All @@ -30,6 +31,7 @@ import (
"github.com/elnosh/gonuts/cashu/nuts/nut11"
"github.com/elnosh/gonuts/cashu/nuts/nut14"
"github.com/elnosh/gonuts/cashu/nuts/nut17"
"github.com/elnosh/gonuts/cashu/nuts/nut20"
"github.com/elnosh/gonuts/crypto"
"github.com/elnosh/gonuts/mint/lightning"
"github.com/elnosh/gonuts/mint/pubsub"
Expand Down Expand Up @@ -269,6 +271,21 @@ func (m *Mint) RequestMintQuote(mintQuoteRequest nut04.PostMintQuoteBolt11Reques
return storage.MintQuote{}, cashu.BuildCashuError(errmsg, cashu.UnitErrCode)
}

var publicKey *secp256k1.PublicKey
if len(mintQuoteRequest.Pubkey) > 0 {
hexPubkey, err := hex.DecodeString(mintQuoteRequest.Pubkey)
if err != nil {
errmsg := fmt.Sprintf("invalid public key '%v'", err)
return storage.MintQuote{}, cashu.BuildCashuError(errmsg, cashu.StandardErrCode)
}

publicKey, err = secp256k1.ParsePubKey(hexPubkey)
if err != nil {
errmsg := fmt.Sprintf("invalid public key '%v'", err)
return storage.MintQuote{}, cashu.BuildCashuError(errmsg, cashu.StandardErrCode)
}
}

// check limits
requestAmount := mintQuoteRequest.Amount
if m.limits.MintingSettings.MaxAmount > 0 {
Expand Down Expand Up @@ -307,6 +324,7 @@ func (m *Mint) RequestMintQuote(mintQuoteRequest nut04.PostMintQuoteBolt11Reques
PaymentHash: invoice.PaymentHash,
State: nut04.Unpaid,
Expiry: uint64(time.Now().Add(time.Second * time.Duration(invoice.Expiry)).Unix()),
Pubkey: publicKey,
}

err = m.db.SaveMintQuote(mintQuote)
Expand Down Expand Up @@ -407,11 +425,36 @@ func (m *Mint) MintTokens(mintTokensRequest nut04.PostMintBolt11Request) (cashu.
errmsg := fmt.Sprintf("error getting blind signatures from db: %v", err)
return cashu.BuildCashuError(errmsg, cashu.DBErrCode)
}

if len(sigs) > 0 {
return cashu.BlindedMessageAlreadySigned
}

// verify signature on mint quote
if mintQuote.Pubkey != nil {
if len(mintTokensRequest.Signature) == 0 {
return cashu.MintQuoteInvalidSigErr
}

sigBytes, err := hex.DecodeString(mintTokensRequest.Signature)
if err != nil {
return cashu.MintQuoteInvalidSigErr
}
signature, err := schnorr.ParseSignature(sigBytes)
if err != nil {
return cashu.MintQuoteInvalidSigErr
}

if !nut20.VerifyMintQuoteSignature(
signature,
mintQuote.Id,
mintTokensRequest.Outputs,
mintQuote.Pubkey,
) {
return cashu.MintQuoteInvalidSigErr
}
m.logDebugf("verified signature on mint quote")
}

blindedSignatures, err = m.signBlindedMessages(blindedMessages)
if err != nil {
return err
Expand Down Expand Up @@ -1495,6 +1538,7 @@ func (m *Mint) SetMintInfo(mintInfo MintInfo) {
},
},
},
Nut20: nut06.Supported{Supported: true},
}

if m.mppEnabled {
Expand Down
Loading