diff --git a/uma/protocol/backing_signature.go b/uma/protocol/backing_signature.go new file mode 100644 index 0000000..a709015 --- /dev/null +++ b/uma/protocol/backing_signature.go @@ -0,0 +1,12 @@ +package protocol + +// BackingSignature is a signature by a backing VASP that can attest to the authenticity of the message, +// along with its associated domain. +type BackingSignature struct { + // Domain is the domain of the VASP that produced the signature. Public keys for this VASP will be fetched from + // the domain at /.well-known/lnurlpubkey and used to verify the signature. + Domain string `json:"domain"` + + // Signature is the signature of the payload. + Signature string `json:"signature"` +} diff --git a/uma/protocol/lnurl_request.go b/uma/protocol/lnurl_request.go index 9758b87..1d7b273 100644 --- a/uma/protocol/lnurl_request.go +++ b/uma/protocol/lnurl_request.go @@ -28,6 +28,8 @@ type LnurlpRequest struct { // UmaVersion is the version of the UMA protocol that VASP1 prefers to use for this transaction. For the version // negotiation flow, see https://static.swimlanes.io/87f5d188e080cb8e0494e46f80f2ae74.png UmaVersion *string + // BackingSignatures is an array of backing VASP signatures. + BackingSignatures *[]BackingSignature } // AsUmaRequest returns the request as an UmaLnurlpRequest if it is a valid UMA request, otherwise it returns nil. @@ -45,6 +47,7 @@ func (q *LnurlpRequest) AsUmaRequest() *UmaLnurlpRequest { VaspDomain: *q.VaspDomain, Timestamp: *q.Timestamp, UmaVersion: *q.UmaVersion, + BackingSignatures: q.BackingSignatures, } } @@ -76,6 +79,13 @@ func (q *LnurlpRequest) EncodeToUrl() (*url.URL, error) { queryParams.Add("isSubjectToTravelRule", strconv.FormatBool(isSubjectToTravelRule)) queryParams.Add("timestamp", strconv.FormatInt(q.Timestamp.Unix(), 10)) queryParams.Add("umaVersion", *q.UmaVersion) + if q.BackingSignatures != nil { + backingSignatures := make([]string, len(*q.BackingSignatures)) + for i, backingSignature := range *q.BackingSignatures { + backingSignatures[i] = fmt.Sprintf("%s:%s", backingSignature.Domain, backingSignature.Signature) + } + queryParams.Add("backingSignatures", strings.Join(backingSignatures, ",")) + } } lnurlpUrl.RawQuery = queryParams.Encode() return &lnurlpUrl, nil @@ -100,6 +110,8 @@ type UmaLnurlpRequest struct { // UmaVersion is the version of the UMA protocol that VASP1 prefers to use for this transaction. For the version // negotiation flow, see https://static.swimlanes.io/87f5d188e080cb8e0494e46f80f2ae74.png UmaVersion string + // BackingSignatures is an array of backing VASP signatures. + BackingSignatures *[]BackingSignature } func (q *LnurlpRequest) SignablePayload() ([]byte, error) { @@ -109,3 +121,26 @@ func (q *LnurlpRequest) SignablePayload() ([]byte, error) { payloadString := strings.Join([]string{q.ReceiverAddress, *q.Nonce, strconv.FormatInt(q.Timestamp.Unix(), 10)}, "|") return []byte(payloadString), nil } + +// Append a backing signature to the LnurlpRequest. +// +// Args: +// +// signingPrivateKey: the private key to use to sign the payload. +// domain: the domain of the VASP that is signing the payload. The associated public key will be fetched from +// /.well-known/lnurlpubkey on this domain to verify the signature. +func (q *LnurlpRequest) AppendBackingSignature(signingPrivateKey []byte, domain string) error { + signablePayload, err := q.SignablePayload() + if err != nil { + return err + } + signature, err := utils.SignPayload(signablePayload, signingPrivateKey) + if err != nil { + return err + } + if q.BackingSignatures == nil { + q.BackingSignatures = &[]BackingSignature{} + } + *q.BackingSignatures = append(*q.BackingSignatures, BackingSignature{Signature: *signature, Domain: domain}) + return nil +} diff --git a/uma/protocol/lnurl_response.go b/uma/protocol/lnurl_response.go index 2ccff3f..69a3121 100644 --- a/uma/protocol/lnurl_response.go +++ b/uma/protocol/lnurl_response.go @@ -1,6 +1,7 @@ package protocol import ( + "github.com/uma-universal-money-address/uma-go-sdk/uma/utils" "strconv" "strings" ) @@ -46,6 +47,8 @@ type LnurlComplianceResponse struct { IsSubjectToTravelRule bool `json:"isSubjectToTravelRule"` // ReceiverIdentifier is the identifier of the receiver at VASP2. ReceiverIdentifier string `json:"receiverIdentifier"` + // BackingSignatures is the list of backing signatures from VASPs that can attest to the authenticity of the message. + BackingSignatures *[]BackingSignature `json:"backingSignatures,omitempty"` } func (r *LnurlpResponse) IsUmaResponse() bool { @@ -99,3 +102,28 @@ func (r *UmaLnurlpResponse) SignablePayload() []byte { }, "|") return []byte(payloadString) } + +// Append a backing signature to the UmaLnurlpResponse. +// +// Args: +// +// signingPrivateKey: the private key to use to sign the payload. +// domain: the domain of the VASP that is signing the payload. The associated public key will be fetched from +// /.well-known/lnurlpubkey on this domain to verify the signature. +func (r *UmaLnurlpResponse) AppendBackingSignature(signingPrivateKey []byte, domain string) error { + signature, err := utils.SignPayload(r.SignablePayload(), signingPrivateKey) + if err != nil { + return err + } + if r.Compliance.BackingSignatures == nil { + r.Compliance.BackingSignatures = &[]BackingSignature{} + } + *r.Compliance.BackingSignatures = append(*r.Compliance.BackingSignatures, BackingSignature{ + Signature: *signature, + Domain: domain, + }) + if r.LnurlpResponse.Compliance != nil { + r.LnurlpResponse.Compliance.BackingSignatures = r.Compliance.BackingSignatures + } + return nil +} diff --git a/uma/protocol/pay_request.go b/uma/protocol/pay_request.go index 3b48ef7..989a539 100644 --- a/uma/protocol/pay_request.go +++ b/uma/protocol/pay_request.go @@ -4,6 +4,7 @@ import ( "encoding/json" "errors" "fmt" + "github.com/uma-universal-money-address/uma-go-sdk/uma/utils" "net/url" "strconv" "strings" @@ -243,6 +244,44 @@ func (p *PayRequest) SignablePayload() ([]byte, error) { return []byte(payloadString), nil } +// Append a backing signature to the PayRequest. +// +// Args: +// +// signingPrivateKey: the private key to use to sign the payload. +// domain: the domain of the VASP that is signing the payload. The associated public key will be fetched from +// /.well-known/lnurlpubkey on this domain to verify the signature. +func (p *PayRequest) AppendBackingSignature(signingPrivateKey []byte, domain string) error { + signablePayload, err := p.SignablePayload() + if err != nil { + return err + } + signature, err := utils.SignPayload(signablePayload, signingPrivateKey) + if err != nil { + return err + } + complianceData, err := p.PayerData.Compliance() + if err != nil { + return err + } + if complianceData == nil { + return errors.New("compliance payer data is missing") + } + if complianceData.BackingSignatures == nil { + complianceData.BackingSignatures = &[]BackingSignature{} + } + *complianceData.BackingSignatures = append(*complianceData.BackingSignatures, BackingSignature{ + Signature: *signature, + Domain: domain, + }) + complianceMap, err := complianceData.AsMap() + if err != nil { + return err + } + (*p.PayerData)["compliance"] = complianceMap + return nil +} + // ParsePayRequestFromQueryParams Parses a pay request from query parameters. // This is useful for parsing a non-UMA pay request from a URL query since raw LNURL uses a GET request for the payreq, // whereas UMA uses a POST request. diff --git a/uma/protocol/payee_data.go b/uma/protocol/payee_data.go index cb2ae45..98f06a0 100644 --- a/uma/protocol/payee_data.go +++ b/uma/protocol/payee_data.go @@ -47,6 +47,8 @@ type CompliancePayeeData struct { // SignatureTimestamp is the unix timestamp (in seconds since epoch) of when the request was sent. Used in the signature. // Note: This field is optional for UMA v0.X backwards-compatibility. It is required for UMA v1.X. SignatureTimestamp *int64 `json:"signatureTimestamp,omitempty"` + // BackingSignatures is the list of backing signatures from VASPs that can attest to the authenticity of the message. + BackingSignatures *[]BackingSignature `json:"backingSignatures,omitempty"` } func (c *CompliancePayeeData) AsMap() (map[string]interface{}, error) { diff --git a/uma/protocol/payer_data.go b/uma/protocol/payer_data.go index a2bfc81..5271c3c 100644 --- a/uma/protocol/payer_data.go +++ b/uma/protocol/payer_data.go @@ -107,6 +107,8 @@ type CompliancePayerData struct { SignatureTimestamp int64 `json:"signatureTimestamp"` // UtxoCallback is the URL that the receiver will call to send UTXOs of the channel that the receiver used to receive the payment once it completes. UtxoCallback string `json:"utxoCallback"` + // BackingSignatures is the list of backing signatures from VASPs that can attest to the authenticity of the message. + BackingSignatures *[]BackingSignature `json:"backingSignatures,omitempty"` } func (c *CompliancePayerData) AsMap() (map[string]interface{}, error) { diff --git a/uma/protocol/payreq_response.go b/uma/protocol/payreq_response.go index 371cfa4..2df7e29 100644 --- a/uma/protocol/payreq_response.go +++ b/uma/protocol/payreq_response.go @@ -3,6 +3,7 @@ package protocol import ( "encoding/json" "errors" + "github.com/uma-universal-money-address/uma-go-sdk/uma/utils" ) // PayReqResponse is the response sent by the receiver to the sender to provide an invoice. @@ -209,3 +210,48 @@ func (p *PayReqResponse) UnmarshalJSON(data []byte) error { p.SuccessAction = v1.SuccessAction return nil } + +// Append a backing signature to the PayReqResponse. +// +// Args: +// +// signingPrivateKey: the private key to use to sign the payload. +// domain: the domain of the VASP that is signing the payload. The associated public key will be fetched from +// /.well-known/lnurlpubkey on this domain to verify the signature. +// payerIdentifier: the identifier of the sender. For example, $alice@vasp1.com +// payeeIdentifier: the identifier of the receiver. For example, $bob@vasp2.com +func (p *PayReqResponse) AppendBackingSignature( + signingPrivateKey []byte, + domain string, + payerIdentifier string, + payeeIdentifier string, +) error { + complianceData, err := p.PayeeData.Compliance() + if err != nil { + return err + } + if complianceData == nil { + return errors.New("compliance payee data is missing") + } + signablePayload, err := complianceData.SignablePayload(payerIdentifier, payeeIdentifier) + if err != nil { + return err + } + signature, err := utils.SignPayload(signablePayload, signingPrivateKey) + if err != nil { + return err + } + if complianceData.BackingSignatures == nil { + complianceData.BackingSignatures = &[]BackingSignature{} + } + *complianceData.BackingSignatures = append(*complianceData.BackingSignatures, BackingSignature{ + Signature: *signature, + Domain: domain, + }) + complianceMap, err := complianceData.AsMap() + if err != nil { + return err + } + (*p.PayeeData)["compliance"] = complianceMap + return nil +} diff --git a/uma/test/uma_test.go b/uma/test/uma_test.go index f42e4e8..0f85921 100644 --- a/uma/test/uma_test.go +++ b/uma/test/uma_test.go @@ -197,6 +197,37 @@ func TestSignAndVerifyLnurlpRequestDuplicateNonce(t *testing.T) { require.Error(t, err) } +func TestSignAndVerifyLnurlpRequestWithBackingSignature(t *testing.T) { + senderSigningPrivateKey, err := secp256k1.GeneratePrivateKey() + require.NoError(t, err) + backingVaspSigningPrivateKey, err := secp256k1.GeneratePrivateKey() + require.NoError(t, err) + queryUrl, err := uma.GetSignedLnurlpRequestUrl(senderSigningPrivateKey.Serialize(), "$bob@vasp2.com", "vasp1.com", true, nil) + require.NoError(t, err) + parsedLnurlpRequest, err := uma.ParseLnurlpRequest(*queryUrl) + require.NoError(t, err) + backingDomain := "backingvasp.com" + err = parsedLnurlpRequest.AppendBackingSignature(backingVaspSigningPrivateKey.Serialize(), backingDomain) + require.NoError(t, err) + queryUrl, err = parsedLnurlpRequest.EncodeToUrl() + require.NoError(t, err) + query, err := uma.ParseLnurlpRequest(*queryUrl) + require.NoError(t, err) + require.NotNil(t, query.BackingSignatures) + require.Equal(t, 1, len(*query.BackingSignatures)) + err = uma.VerifyUmaLnurlpQuerySignature( + *query.AsUmaRequest(), + getPubKeyResponse(senderSigningPrivateKey), + getNonceCache(), + ) + require.NoError(t, err) + publicKeyCache := uma.NewInMemoryPublicKeyCache() + backingVaspPubKeyResponse := getPubKeyResponse(backingVaspSigningPrivateKey) + publicKeyCache.AddPublicKeyForVasp(backingDomain, &backingVaspPubKeyResponse) + err = uma.VerifyUmaLnurlpQueryBackingSignatures(*query.AsUmaRequest(), publicKeyCache) + require.NoError(t, err) +} + func TestSignAndVerifyLnurlpResponse(t *testing.T) { senderSigningPrivateKey, err := secp256k1.GeneratePrivateKey() require.NoError(t, err) @@ -248,6 +279,68 @@ func TestSignAndVerifyLnurlpResponse(t *testing.T) { require.NoError(t, err) } +func TestSignAndVerifyLnurlpResponseWithBackingSignature(t *testing.T) { + senderSigningPrivateKey, err := secp256k1.GeneratePrivateKey() + require.NoError(t, err) + receiverSigningPrivateKey, err := secp256k1.GeneratePrivateKey() + require.NoError(t, err) + backingVaspSigningPrivateKey, err := secp256k1.GeneratePrivateKey() + require.NoError(t, err) + serializedPrivateKey := receiverSigningPrivateKey.Serialize() + request := createLnurlpRequest(t, senderSigningPrivateKey.Serialize()) + metadata, err := createMetadataForBob() + require.NoError(t, err) + isSubjectToTravelRule := true + kycStatus := umaprotocol.KycStatusVerified + response, err := uma.GetLnurlpResponse( + request, + "https://vasp2.com/api/lnurl/payreq/$bob", + metadata, + 1, + 10_000_000, + &serializedPrivateKey, + &isSubjectToTravelRule, + &umaprotocol.CounterPartyDataOptions{ + "name": umaprotocol.CounterPartyDataOption{Mandatory: false}, + "email": umaprotocol.CounterPartyDataOption{Mandatory: false}, + "compliance": umaprotocol.CounterPartyDataOption{Mandatory: true}, + }, + &[]umaprotocol.Currency{ + { + Code: "USD", + Name: "US Dollar", + Symbol: "$", + MillisatoshiPerUnit: 34_150, + Convertible: umaprotocol.ConvertibleCurrency{ + MinSendable: 1, + MaxSendable: 10_000_000, + }, + Decimals: 2, + }, + }, + &kycStatus, + nil, + nil, + ) + require.NoError(t, err) + backingDomain := "backingvasp.com" + err = response.AsUmaResponse().AppendBackingSignature(backingVaspSigningPrivateKey.Serialize(), backingDomain) + require.NoError(t, err) + responseJson, err := json.Marshal(response) + require.NoError(t, err) + response, err = uma.ParseLnurlpResponse(responseJson) + require.NoError(t, err) + require.NotNil(t, response.Compliance.BackingSignatures) + require.Equal(t, 1, len(*response.Compliance.BackingSignatures)) + err = uma.VerifyUmaLnurlpResponseSignature(*response.AsUmaResponse(), getPubKeyResponse(receiverSigningPrivateKey), getNonceCache()) + require.NoError(t, err) + publicKeyCache := uma.NewInMemoryPublicKeyCache() + backingVaspPubKeyResponse := getPubKeyResponse(backingVaspSigningPrivateKey) + publicKeyCache.AddPublicKeyForVasp(backingDomain, &backingVaspPubKeyResponse) + err = uma.VerifyUmaLnurlpResponseBackingSignatures(*response.AsUmaResponse(), publicKeyCache) + require.NoError(t, err) +} + func TestPayReqCreationAndParsing(t *testing.T) { senderSigningPrivateKey, err := secp256k1.GeneratePrivateKey() require.NoError(t, err) @@ -360,6 +453,53 @@ func TestMsatsPayReqCreationAndParsing(t *testing.T) { require.NotNil(t, payreq) } +func TestSignAndVerifyPayReqBackingSignatures(t *testing.T) { + senderSigningPrivateKey, err := secp256k1.GeneratePrivateKey() + require.NoError(t, err) + receiverEncryptionPrivateKey, err := secp256k1.GeneratePrivateKey() + require.NoError(t, err) + backingVaspSigningPrivateKey, err := secp256k1.GeneratePrivateKey() + require.NoError(t, err) + payreq, err := uma.GetUmaPayRequest( + 1000, + receiverEncryptionPrivateKey.PubKey().SerializeUncompressed(), + senderSigningPrivateKey.Serialize(), + "USD", + true, + "$alice@vasp1.com", + 1, + nil, + nil, + nil, + nil, + umaprotocol.KycStatusVerified, + nil, + nil, + "/api/lnurl/utxocallback?txid=1234", + nil, + nil, + ) + require.NoError(t, err) + backingDomain := "backingvasp.com" + err = payreq.AppendBackingSignature(backingVaspSigningPrivateKey.Serialize(), backingDomain) + require.NoError(t, err) + reqJson, err := json.Marshal(payreq) + require.NoError(t, err) + payreq, err = uma.ParsePayRequest(reqJson) + require.NoError(t, err) + complianceData, err := payreq.PayerData.Compliance() + require.NoError(t, err) + require.NotNil(t, complianceData.BackingSignatures) + require.Equal(t, 1, len(*complianceData.BackingSignatures)) + err = uma.VerifyPayReqSignature(payreq, getPubKeyResponse(senderSigningPrivateKey), getNonceCache()) + require.NoError(t, err) + publicKeyCache := uma.NewInMemoryPublicKeyCache() + backingVaspPubKeyResponse := getPubKeyResponse(backingVaspSigningPrivateKey) + publicKeyCache.AddPublicKeyForVasp(backingDomain, &backingVaspPubKeyResponse) + err = uma.VerifyPayReqBackingSignatures(payreq, publicKeyCache) + require.NoError(t, err) +} + type FakeInvoiceCreator struct{} func (f *FakeInvoiceCreator) CreateInvoice(int64, string, *string) (*string, error) { @@ -632,6 +772,86 @@ func TestV0PayReqResponseAndParsing(t *testing.T) { require.Equal(t, payreqResponse, parsedResponse) } +func TestSignAndVerifyPayReqResponseBackingSignatures(t *testing.T) { + senderSigningPrivateKey, err := secp256k1.GeneratePrivateKey() + require.NoError(t, err) + receiverEncryptionPrivateKey, err := secp256k1.GeneratePrivateKey() + require.NoError(t, err) + receiverSigningPrivateKey, err := secp256k1.GeneratePrivateKey() + require.NoError(t, err) + backingVaspSigningPrivateKey, err := secp256k1.GeneratePrivateKey() + require.NoError(t, err) + payreq, err := uma.GetUmaPayRequest( + 1000, + receiverEncryptionPrivateKey.PubKey().SerializeUncompressed(), + senderSigningPrivateKey.Serialize(), + "USD", + true, + "$alice@vasp1.com", + 1, + nil, + nil, + nil, + nil, + umaprotocol.KycStatusVerified, + nil, + nil, + "/api/lnurl/utxocallback?txid=1234", + nil, + nil, + ) + require.NoError(t, err) + client := &FakeInvoiceCreator{} + metadata, err := createMetadataForBob() + require.NoError(t, err) + payeeData := umaprotocol.PayeeData{ + "identifier": "$bob@vasp2.com", + } + receivingCurrencyCode := "USD" + receivingCurrencyDecimals := 2 + fee := int64(100_000) + conversionRate := float64(24_150) + utxoCallback := "/api/lnurl/utxocallback?txid=1234" + payeeIdentifier := "$bob@vasp2.com" + serializedPrivateKey := receiverSigningPrivateKey.Serialize() + payreqResponse, err := uma.GetPayReqResponse( + *payreq, + client, + metadata, + &receivingCurrencyCode, + &receivingCurrencyDecimals, + &conversionRate, + &fee, + &[]string{"abcdef12345"}, + nil, + &utxoCallback, + &payeeData, + &serializedPrivateKey, + &payeeIdentifier, + nil, + nil, + ) + require.NoError(t, err) + backingDomain := "backingvasp.com" + err = payreqResponse.AppendBackingSignature(backingVaspSigningPrivateKey.Serialize(), backingDomain, "$alice@vasp1.com", "$bob@vasp2.com") + require.NoError(t, err) + responseJson, err := json.Marshal(payreqResponse) + require.NoError(t, err) + parsedResponse, err := uma.ParsePayReqResponse(responseJson) + require.NoError(t, err) + complianceData, err := parsedResponse.PayeeData.Compliance() + require.NoError(t, err) + require.NotNil(t, complianceData.BackingSignatures) + require.Equal(t, 1, len(*complianceData.BackingSignatures)) + err = uma.VerifyPayReqResponseSignature(parsedResponse, getPubKeyResponse(receiverSigningPrivateKey), getNonceCache(), "$alice@vasp1.com", "$bob@vasp2.com") + require.NoError(t, err) + publicKeyCache := uma.NewInMemoryPublicKeyCache() + backingVaspPubKeyResponse := getPubKeyResponse(backingVaspSigningPrivateKey) + publicKeyCache.AddPublicKeyForVasp(backingDomain, &backingVaspPubKeyResponse) + err = uma.VerifyPayReqResponseBackingSignatures(parsedResponse, publicKeyCache, "$alice@vasp1.com", "$bob@vasp2.com") + require.NoError(t, err) +} + func TestSignAndVerifyPostTransactionCallback(t *testing.T) { signingPrivateKey, err := secp256k1.GeneratePrivateKey() require.NoError(t, err) diff --git a/uma/uma.go b/uma/uma.go index ccea7e7..cf10e4f 100644 --- a/uma/uma.go +++ b/uma/uma.go @@ -117,31 +117,7 @@ func GenerateNonce() (*string, error) { return &nonce, nil } -func signPayloadToBytes(payload []byte, privateKeyBytes []byte) ([]byte, error) { - privateKey := secp256k1.PrivKeyFromBytes(privateKeyBytes) - hash := crypto.SHA256.New() - _, err := hash.Write(payload) - if err != nil { - return nil, err - } - hashedPayload := hash.Sum(nil) - signature, err := privateKey.ToECDSA().Sign(rand.Reader, hashedPayload, crypto.SHA256) - if err != nil { - return nil, err - } - return signature, nil -} - -func signPayload(payload []byte, privateKeyBytes []byte) (*string, error) { - signature, err := signPayloadToBytes(payload, privateKeyBytes) - if err != nil { - return nil, err - } - signatureString := hex.EncodeToString(signature) - return &signatureString, nil -} - -// VerifyPayReqSignature Verifies the signature on an uma pay request based on the public key of the VASP making the +// VerifyPayReqSignature Verifies the primary signature on an uma pay request based on the public key of the VASP making the // request. // // Args: @@ -171,6 +147,39 @@ func VerifyPayReqSignature(query *protocol.PayRequest, otherVaspPubKeyResponse p return verifySignature(signablePayload, complianceData.Signature, otherVaspPubKeyResponse) } +// VerifyPayReqBackingSignatures Verifies the backing signatures on a PayRequest. You may optionally +// call this function after VerifyPayReqSignature to verify signatures from backing VASPs. +// +// Args: +// +// query: the signed query to verify. +// publicKeyCache: the PublicKeyCache cache to use. You can use the InMemoryPublicKeyCache struct, or +// implement your own persistent cache with any storage type. +func VerifyPayReqBackingSignatures(query *protocol.PayRequest, publicKeyCache PublicKeyCache) error { + complianceData, err := query.PayerData.Compliance() + if err != nil { + return err + } + if complianceData.BackingSignatures == nil { + return nil + } + signablePayload, err := query.SignablePayload() + if err != nil { + return err + } + for _, backingSignature := range *complianceData.BackingSignatures { + backingVaspPubKeyResponse, err := FetchPublicKeyForVasp(backingSignature.Domain, publicKeyCache) + if err != nil { + return err + } + err = verifySignature(signablePayload, backingSignature.Signature, *backingVaspPubKeyResponse) + if err != nil { + return err + } + } + return nil +} + // verifySignature Verifies the signature of the uma request. // // Args: @@ -247,7 +256,7 @@ func GetSignedLnurlpRequestUrl( if err != nil { return nil, err } - signature, err := signPayload(signablePayload, signingPrivateKey) + signature, err := utils.SignPayload(signablePayload, signingPrivateKey) if err != nil { return nil, err } @@ -285,14 +294,15 @@ func ParseLnurlpRequest(url url.URL) (*protocol.LnurlpRequest, error) { // // url: the full URL of the uma request. // receiverDomain: the domain of the receiver UMA of the payment. This is used to override the domain in the URL. -func ParseLnurlpRequestWithReceiverDomain(url url.URL, receiverDomain string) (*protocol.LnurlpRequest, error) { - query := url.Query() +func ParseLnurlpRequestWithReceiverDomain(requestUrl url.URL, receiverDomain string) (*protocol.LnurlpRequest, error) { + query := requestUrl.Query() signature := query.Get("signature") vaspDomain := query.Get("vaspDomain") nonce := query.Get("nonce") isSubjectToTravelRule := strings.ToLower(query.Get("isSubjectToTravelRule")) == "true" umaVersion := query.Get("umaVersion") timestamp := query.Get("timestamp") + backingSignatures := query.Get("backingSignatures") var timestampAsTime *time.Time if timestamp != "" { timestampAsString, dateErr := strconv.ParseInt(timestamp, 10, 64) @@ -310,7 +320,7 @@ func ParseLnurlpRequestWithReceiverDomain(url url.URL, receiverDomain string) (* } } - pathParts := strings.Split(url.Path, "/") + pathParts := strings.Split(requestUrl.Path, "/") if len(pathParts) != 4 || pathParts[1] != ".well-known" || pathParts[2] != "lnurlp" { return nil, errors.New("invalid uma request path") } @@ -328,6 +338,29 @@ func ParseLnurlpRequestWithReceiverDomain(url url.URL, receiverDomain string) (* return &s } + var backingSignaturesParsed *[]protocol.BackingSignature + if backingSignatures != "" { + pairs := strings.Split(backingSignatures, ",") + signatures := make([]protocol.BackingSignature, len(pairs)) + for i, pair := range pairs { + decodedPair, err := url.QueryUnescape(pair) + if err != nil { + return nil, errors.New("invalid backing signature format") + } + + lastColonIndex := strings.LastIndex(decodedPair, ":") + if lastColonIndex == -1 { + return nil, errors.New("invalid backing signature format") + } + + signatures[i] = protocol.BackingSignature{ + Domain: decodedPair[:lastColonIndex], + Signature: decodedPair[lastColonIndex+1:], + } + } + backingSignaturesParsed = &signatures + } + return &protocol.LnurlpRequest{ ReceiverAddress: receiverAddress, VaspDomain: nilIfEmpty(vaspDomain), @@ -336,10 +369,11 @@ func ParseLnurlpRequestWithReceiverDomain(url url.URL, receiverDomain string) (* Nonce: nilIfEmpty(nonce), Timestamp: timestampAsTime, IsSubjectToTravelRule: &isSubjectToTravelRule, + BackingSignatures: backingSignaturesParsed, }, nil } -// VerifyUmaLnurlpQuerySignature Verifies the signature on an uma Lnurlp query based on the public key of the VASP making the request. +// VerifyUmaLnurlpQuerySignature Verifies the primary signature on an uma Lnurlp query based on the public key of the VASP making the request. // // Args: // @@ -358,6 +392,35 @@ func VerifyUmaLnurlpQuerySignature(query protocol.UmaLnurlpRequest, otherVaspPub return verifySignature(signablePayload, query.Signature, otherVaspPubKeyResponse) } +// VerifyUmaLnurlpQueryBackingSignatures Verifies the backing signatures on an UmaLnurlpRequest. You may optionally +// call this function after VerifyUmaLnurlpQuerySignature to verify signatures from backing VASPs. +// +// Args: +// +// query: the signed query to verify. +// publicKeyCache: the PublicKeyCache cache to use. You can use the InMemoryPublicKeyCache struct, or implement +// your own persistent cache with any storage type. +func VerifyUmaLnurlpQueryBackingSignatures(query protocol.UmaLnurlpRequest, publicKeyCache PublicKeyCache) error { + if query.BackingSignatures == nil { + return nil + } + signablePayload, err := query.SignablePayload() + if err != nil { + return err + } + for _, backingSignature := range *query.BackingSignatures { + backingVaspPubKeyResponse, err := FetchPublicKeyForVasp(backingSignature.Domain, publicKeyCache) + if err != nil { + return err + } + err = verifySignature(signablePayload, backingSignature.Signature, *backingVaspPubKeyResponse) + if err != nil { + return err + } + } + return nil +} + func GetLnurlpResponse( request protocol.LnurlpRequest, callback string, @@ -451,7 +514,7 @@ func getSignedLnurlpComplianceResponse( return nil, err } payloadString := strings.Join([]string{query.ReceiverAddress, *nonce, strconv.FormatInt(timestamp, 10)}, "|") - signature, err := signPayload([]byte(payloadString), privateKeyBytes) + signature, err := utils.SignPayload([]byte(payloadString), privateKeyBytes) if err != nil { return nil, err } @@ -465,7 +528,7 @@ func getSignedLnurlpComplianceResponse( }, nil } -// VerifyUmaLnurlpResponseSignature Verifies the signature on an uma Lnurlp response based on the public key of the VASP making the request. +// VerifyUmaLnurlpResponseSignature Verifies the primary signature on an uma Lnurlp response based on the public key of the VASP making the request. // // Args: // @@ -480,6 +543,32 @@ func VerifyUmaLnurlpResponseSignature(response protocol.UmaLnurlpResponse, other return verifySignature(response.SignablePayload(), response.Compliance.Signature, otherVaspPubKeyResponse) } +// VerifyUmaLnurlpResponseBackingSignatures Verifies the backing signatures on an UmaLnurlpResponse. You may optionally +// call this function after VerifyUmaLnurlpResponseSignature to verify signatures from backing VASPs. +// +// Args: +// +// response: the signed response to verify. +// publicKeyCache: the PublicKeyCache cache to use. You can use the InMemoryPublicKeyCache struct, or implement your +// own persistent cache with any storage type. +func VerifyUmaLnurlpResponseBackingSignatures(response protocol.UmaLnurlpResponse, publicKeyCache PublicKeyCache) error { + if response.Compliance.BackingSignatures == nil { + return nil + } + signablePayload := response.SignablePayload() + for _, backingSignature := range *response.Compliance.BackingSignatures { + backingVaspPubKeyResponse, err := FetchPublicKeyForVasp(backingSignature.Domain, publicKeyCache) + if err != nil { + return err + } + err = verifySignature(signablePayload, backingSignature.Signature, *backingVaspPubKeyResponse) + if err != nil { + return err + } + } + return nil +} + func ParseLnurlpResponse(bytes []byte) (*protocol.LnurlpResponse, error) { var response protocol.LnurlpResponse err := json.Unmarshal(bytes, &response) @@ -690,7 +779,7 @@ func getSignedCompliancePayerData( } } payloadString := strings.Join([]string{payerIdentifier, *nonce, strconv.FormatInt(timestamp, 10)}, "|") - signature, err := signPayload([]byte(payloadString), sendingVaspPrivateKeyBytes) + signature, err := utils.SignPayload([]byte(payloadString), sendingVaspPrivateKeyBytes) if err != nil { return nil, err } @@ -992,7 +1081,7 @@ func getSignedCompliancePayeeData( if err != nil { return nil, err } - signature, err := signPayload([]byte(payloadString), receivingVaspPrivateKeyBytes) + signature, err := utils.SignPayload([]byte(payloadString), receivingVaspPrivateKeyBytes) if err != nil { return nil, err } @@ -1051,6 +1140,46 @@ func VerifyPayReqResponseSignature( return verifySignature(signablePayload, *complianceData.Signature, otherVaspPubKeyResponse) } +// VerifyPayReqResponseBackingSignatures Verifies the backing signatures on a PayReqResponse. You may optionally +// call this function after VerifyPayReqResponseSignature to verify signatures from backing VASPs. +// +// Args: +// +// query: the signed query to verify. +// publicKeyCache: the PublicKeyCache cache to use. You can use the InMemoryPublicKeyCache struct, or +// implement your own persistent cache with any storage type. +// payerIdentifier: the identifier of the sender. For example, $alice@vasp1.com +// payeeIdentifier: the identifier of the receiver. For example, $bob@vasp2.com +func VerifyPayReqResponseBackingSignatures( + response *protocol.PayReqResponse, + publicKeyCache PublicKeyCache, + payerIdentifier string, + payeeIdentifier string, +) error { + complianceData, err := response.PayeeData.Compliance() + if err != nil { + return err + } + if complianceData.BackingSignatures == nil { + return nil + } + signablePayload, err := complianceData.SignablePayload(payerIdentifier, payeeIdentifier) + if err != nil { + return err + } + for _, backingSignature := range *complianceData.BackingSignatures { + backingVaspPubKeyResponse, err := FetchPublicKeyForVasp(backingSignature.Domain, publicKeyCache) + if err != nil { + return err + } + err = verifySignature(signablePayload, backingSignature.Signature, *backingVaspPubKeyResponse) + if err != nil { + return err + } + } + return nil +} + // GetPostTransactionCallback Creates a signed post transaction callback. // // Args: @@ -1078,7 +1207,7 @@ func GetPostTransactionCallback( if err != nil { return nil, err } - signature, err := signPayload(*signablePayload, signingPrivateKey) + signature, err := utils.SignPayload(*signablePayload, signingPrivateKey) if err != nil { return nil, err } @@ -1160,7 +1289,7 @@ func CreateUmaInvoice( if err != nil { return nil, err } - signature, err := signPayloadToBytes(signablePayload, signingPrivateKey) + signature, err := utils.SignPayloadToBytes(signablePayload, signingPrivateKey) if err != nil { return nil, err } diff --git a/uma/utils/signing_utils.go b/uma/utils/signing_utils.go new file mode 100644 index 0000000..af02b96 --- /dev/null +++ b/uma/utils/signing_utils.go @@ -0,0 +1,33 @@ +package utils + +import ( + "crypto" + "crypto/rand" + "encoding/hex" + + "github.com/decred/dcrd/dcrec/secp256k1/v4" +) + +func SignPayloadToBytes(payload []byte, privateKeyBytes []byte) ([]byte, error) { + privateKey := secp256k1.PrivKeyFromBytes(privateKeyBytes) + hash := crypto.SHA256.New() + _, err := hash.Write(payload) + if err != nil { + return nil, err + } + hashedPayload := hash.Sum(nil) + signature, err := privateKey.ToECDSA().Sign(rand.Reader, hashedPayload, crypto.SHA256) + if err != nil { + return nil, err + } + return signature, nil +} + +func SignPayload(payload []byte, privateKeyBytes []byte) (*string, error) { + signature, err := SignPayloadToBytes(payload, privateKeyBytes) + if err != nil { + return nil, err + } + signatureString := hex.EncodeToString(signature) + return &signatureString, nil +}