Skip to content

Commit

Permalink
mint: nut20 verify signatures on mint quotes
Browse files Browse the repository at this point in the history
  • Loading branch information
elnosh committed Feb 12, 2025
1 parent 0000966 commit 88e3d08
Show file tree
Hide file tree
Showing 11 changed files with 347 additions and 6 deletions.
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

0 comments on commit 88e3d08

Please sign in to comment.