From 7a16e4f00397a63e0d63d285ab29c70251d3af60 Mon Sep 17 00:00:00 2001 From: shreyav Date: Mon, 25 Mar 2024 16:23:39 -0700 Subject: [PATCH] certs in pubkeyresponse --- uma/protocol/pub_key_response.go | 82 +++++++++++++++++++++++++++++--- uma/test/protocol_test.go | 66 ++++++++++++++++++++++++- uma/test/uma_test.go | 14 ++++-- uma/utils/cert_utils.go | 66 ++++++++++++++++++++----- 4 files changed, 204 insertions(+), 24 deletions(-) diff --git a/uma/protocol/pub_key_response.go b/uma/protocol/pub_key_response.go index ba02f55..7747da5 100644 --- a/uma/protocol/pub_key_response.go +++ b/uma/protocol/pub_key_response.go @@ -1,23 +1,93 @@ package protocol -import "encoding/hex" +import ( + "encoding/hex" + "encoding/json" + "errors" + "github.com/uma-universal-money-address/uma-go-sdk/uma/utils" +) // PubKeyResponse is sent from a VASP to another VASP to provide its public keys. // It is the response to GET requests at `/.well-known/lnurlpubkey`. type PubKeyResponse struct { + // SigningCertChain is the PEM-encoded certificate chain used to verify signatures from a VASP. + SigningCertChain *string + // EncryptionCertChain is the PEM-encoded certificate chain used to encrypt TR info sent to a VASP. + EncryptionCertChain *string // SigningPubKeyHex is used to verify signatures from a VASP. Hex-encoded byte array. - SigningPubKeyHex string `json:"signingPubKey"` + SigningPubKeyHex *string // EncryptionPubKeyHex is used to encrypt TR info sent to a VASP. Hex-encoded byte array. - EncryptionPubKeyHex string `json:"encryptionPubKey"` + EncryptionPubKeyHex *string // ExpirationTimestamp [Optional] Seconds since epoch at which these pub keys must be refreshed. // They can be safely cached until this expiration (or forever if null). - ExpirationTimestamp *int64 `json:"expirationTimestamp"` + ExpirationTimestamp *int64 } func (r *PubKeyResponse) SigningPubKey() ([]byte, error) { - return hex.DecodeString(r.SigningPubKeyHex) + if r.SigningCertChain != nil { + publicKey, err := utils.ExtractPubkeyFromPemCertificateChain(r.SigningCertChain) + if err != nil { + return nil, err + } + return publicKey.SerializeUncompressed(), nil + } else if r.SigningPubKeyHex != nil { + return hex.DecodeString(*r.SigningPubKeyHex) + } else { + return nil, errors.New("signingPubKeyHex is nil") + } } func (r *PubKeyResponse) EncryptionPubKey() ([]byte, error) { - return hex.DecodeString(r.EncryptionPubKeyHex) + if r.EncryptionCertChain != nil { + publicKey, err := utils.ExtractPubkeyFromPemCertificateChain(r.EncryptionCertChain) + if err != nil { + return nil, err + } + return publicKey.SerializeUncompressed(), nil + } else if r.EncryptionPubKeyHex == nil { + return hex.DecodeString(*r.EncryptionPubKeyHex) + } else { + return nil, errors.New("encryptionPubKeyHex is nil") + } +} + +func (r *PubKeyResponse) MarshalJSON() ([]byte, error) { + signingCertChainHexDer, err := utils.ConvertPemCertificateChainToHexEncodedDer(r.SigningCertChain) + if err != nil { + return nil, err + } + encryptionCertChainHexDer, err := utils.ConvertPemCertificateChainToHexEncodedDer(r.EncryptionCertChain) + if err != nil { + return nil, err + } + m := map[string]interface{}{ + "signingCertChain": signingCertChainHexDer, + "encryptionCertChain": encryptionCertChainHexDer, + "signingPubKey": r.SigningPubKeyHex, + "encryptionPubKey": r.EncryptionPubKeyHex, + "expirationTimestamp": r.ExpirationTimestamp, + } + return json.Marshal(m) +} + +func (r *PubKeyResponse) UnmarshalJSON(data []byte) error { + var temp struct { + SigningCertChainHexDer *[]string `json:"signingCertChain"` + EncryptionCertChainHexDer *[]string `json:"encryptionCertChain"` + SigningPubKeyHex string `json:"signingPubKey"` + EncryptionPubKeyHex string `json:"encryptionPubKey"` + ExpirationTimestamp *int64 `json:"expirationTimestamp"` + } + + if err := json.Unmarshal(data, &temp); err != nil { + return err + } + + r.SigningCertChain, _ = utils.ConvertHexEncodedDerToPemCertChain(temp.SigningCertChainHexDer) + r.EncryptionCertChain, _ = utils.ConvertHexEncodedDerToPemCertChain(temp.EncryptionCertChainHexDer) + r.SigningPubKeyHex = &temp.SigningPubKeyHex + r.EncryptionPubKeyHex = &temp.EncryptionPubKeyHex + r.ExpirationTimestamp = temp.ExpirationTimestamp + + return nil } diff --git a/uma/test/protocol_test.go b/uma/test/protocol_test.go index 84ef378..0ecc324 100644 --- a/uma/test/protocol_test.go +++ b/uma/test/protocol_test.go @@ -2,9 +2,11 @@ package uma_test import ( "encoding/json" + "testing" + "github.com/stretchr/testify/require" + "github.com/uma-universal-money-address/uma-go-sdk/uma" umaprotocol "github.com/uma-universal-money-address/uma-go-sdk/uma/protocol" - "testing" ) func TestParseV0Currency(t *testing.T) { @@ -187,3 +189,65 @@ func TestParseV0PayReqResponse(t *testing.T) { require.Equal(t, "https://example.com/utxo", *(compliance.UtxoCallback)) require.Equal(t, 0, payReqResp.UmaMajorVersion) } + +func TestEncodeAndParsePubKeyResponse(t *testing.T) { + pubKeyHex := "04419c5467ea563f0010fd614f85e885ac99c21b8e8d416241175fdd5efd2244fe907e2e6fa3dd6631b1b17cd28798da8d882a34c4776d44cc4090781c7aadea1b" + pemCertChain := `-----BEGIN CERTIFICATE----- +MIIB1zCCAXygAwIBAgIUGN3ihBj1RnKoeTM/auDFnNoThR4wCgYIKoZIzj0EAwIw +QjELMAkGA1UEBhMCVVMxEzARBgNVBAgMCmNhbGlmb3JuaWExDjAMBgNVBAcMBWxv +cyBhMQ4wDAYDVQQKDAVsaWdodDAeFw0yNDAzMDUyMTAzMTJaFw0yNDAzMTkyMTAz +MTJaMEIxCzAJBgNVBAYTAlVTMRMwEQYDVQQIDApjYWxpZm9ybmlhMQ4wDAYDVQQH +DAVsb3MgYTEOMAwGA1UECgwFbGlnaHQwVjAQBgcqhkjOPQIBBgUrgQQACgNCAARB +nFRn6lY/ABD9YU+F6IWsmcIbjo1BYkEXX91e/SJE/pB+Lm+j3WYxsbF80oeY2o2I +KjTEd21EzECQeBx6reobo1MwUTAdBgNVHQ4EFgQUU87LnQdiP6XIE6LoKU1PZnbt +bMwwHwYDVR0jBBgwFoAUU87LnQdiP6XIE6LoKU1PZnbtbMwwDwYDVR0TAQH/BAUw +AwEB/zAKBggqhkjOPQQDAgNJADBGAiEAvsrvoeo3rbgZdTHxEUIgP0ArLyiO34oz +NlwL4gk5GpgCIQCvRx4PAyXNV9T6RRE+3wFlqwluOc/pPOjgdRw/wpoNPQ== +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIICdjCCAV6gAwIBAgIUAekCcU1Qhjo2Y6L2Down9BLdfdUwDQYJKoZIhvcNAQEL +BQAwNDELMAkGA1UEBhMCVVMxCzAJBgNVBAgMAmNhMQwwCgYDVQQHDANsb3MxCjAI +BgNVBAoMAWEwHhcNMjQwMzA4MDEwNTU3WhcNMjUwMzA4MDEwNTU3WjBAMQswCQYD +VQQGEwJVUzELMAkGA1UECAwCY2ExDDAKBgNVBAcMA2xvczEKMAgGA1UECgwBYTEK +MAgGA1UECwwBYTBWMBAGByqGSM49AgEGBSuBBAAKA0IABJ11ZAQKylgIzZmuI5NE ++DyZ9BUDZhxUPSxTxl+s1am+Lxzr9D7wlwOiiqCYHFWpL6lkCmJcCC06P3RyzXIT +KmyjQjBAMB0GA1UdDgQWBBRXgW6xGB3+mTSSUKlhSiu3LS+TKTAfBgNVHSMEGDAW +gBTFmyv7+YDpK0WAOHJYAzjynmWsMDANBgkqhkiG9w0BAQsFAAOCAQEAFVAA3wo+ +Hi/k+OWO/1CFqIRV/0cA8F05sBMiKVA11xB6I1y54aUV4R0jN76fOiN1jnZqTRnM +G8rZUfQgE/LPVbb1ERHQfd8yaeI+TerKdPkMseu/jnvI+dDJfQdsY7iaa7NPO0dm +t8Nz75cYW8kYuDaq0Hb6uGsywf9LGO/VjrDhyiRxmZ1Oq4JxQmLuh5SDcPfqHTR3 +VbMC1b7eVXaA9O2qYS36zv8cCUSUl5sOSwM6moaFN+xLtVNJ6ZhKPNS2Gd8znhzZ +AQZcDDpXBO6ORNbhVk5A3X6eQX4Ek1HBTa3pcSUQomYAA9TIuVzL6DSot5GWS8Ek +usLY8crt6ys3KQ== +-----END CERTIFICATE----- +` + pubKeyResponse, err := uma.GetPubKeyResponse(pemCertChain, pemCertChain, nil) + require.NoError(t, err) + + pubKeyResponseJson, err := pubKeyResponse.MarshalJSON() + require.NoError(t, err) + pubKeyResponseJsonMap := make(map[string]interface{}) + err = json.Unmarshal(pubKeyResponseJson, &pubKeyResponseJsonMap) + require.NoError(t, err) + + reserializedPubKeyResponse := umaprotocol.PubKeyResponse{} + err = json.Unmarshal(pubKeyResponseJson, &reserializedPubKeyResponse) + require.NoError(t, err) + require.Equal(t, *pubKeyResponse, reserializedPubKeyResponse) + + keysOnlyPubKeyResponse := umaprotocol.PubKeyResponse{ + SigningPubKeyHex: &pubKeyHex, + EncryptionPubKeyHex: &pubKeyHex, + } + + pubKeyResponseJson, err = keysOnlyPubKeyResponse.MarshalJSON() + require.NoError(t, err) + pubKeyResponseJsonMap = make(map[string]interface{}) + err = json.Unmarshal(pubKeyResponseJson, &pubKeyResponseJsonMap) + require.NoError(t, err) + + reserializedPubKeyResponse = umaprotocol.PubKeyResponse{} + err = json.Unmarshal(pubKeyResponseJson, &reserializedPubKeyResponse) + require.NoError(t, err) + require.Equal(t, keysOnlyPubKeyResponse, reserializedPubKeyResponse) +} diff --git a/uma/test/uma_test.go b/uma/test/uma_test.go index 0304283..4b51000 100644 --- a/uma/test/uma_test.go +++ b/uma/test/uma_test.go @@ -674,9 +674,8 @@ func TestParseAndEncodePayReqToQueryParams(t *testing.T) { require.Equal(t, payreq, payreqReparsed) } -func TestParseX509CertificateString(t *testing.T) { - pemCert := ` ------BEGIN CERTIFICATE----- +func TestCertificateUtils(t *testing.T) { + pemCert := `-----BEGIN CERTIFICATE----- MIIB1zCCAXygAwIBAgIUGN3ihBj1RnKoeTM/auDFnNoThR4wCgYIKoZIzj0EAwIw QjELMAkGA1UEBhMCVVMxEzARBgNVBAgMCmNhbGlmb3JuaWExDjAMBgNVBAcMBWxv cyBhMQ4wDAYDVQQKDAVsaWdodDAeFw0yNDAzMDUyMTAzMTJaFw0yNDAzMTkyMTAz @@ -705,13 +704,20 @@ AQZcDDpXBO6ORNbhVk5A3X6eQX4Ek1HBTa3pcSUQomYAA9TIuVzL6DSot5GWS8Ek usLY8crt6ys3KQ== -----END CERTIFICATE----- ` - pubkey, err := utils.ExtractPubkeyFromPemCertificateChain(pemCert) + pubkey, err := utils.ExtractPubkeyFromPemCertificateChain(&pemCert) require.NoError(t, err) publicKeyBytes := pubkey.SerializeUncompressed() expectedPublicKey, err := hex.DecodeString("04419c5467ea563f0010fd614f85e885ac99c21b8e8d416241175fdd5efd2244fe907e2e6fa3dd6631b1b17cd28798da8d882a34c4776d44cc4090781c7aadea1b") require.NoError(t, err) require.Equal(t, publicKeyBytes, expectedPublicKey) + + hexDerCerts, err := utils.ConvertPemCertificateChainToHexEncodedDer(&pemCert) + require.NoError(t, err) + require.Len(t, hexDerCerts, 2) + newPemCert, err := utils.ConvertHexEncodedDerToPemCertChain(&hexDerCerts) + require.NoError(t, err) + require.Equal(t, *newPemCert, pemCert) } func createLnurlpRequest(t *testing.T, signingPrivateKey []byte) umaprotocol.LnurlpRequest { diff --git a/uma/utils/cert_utils.go b/uma/utils/cert_utils.go index 5de8822..1311bd5 100644 --- a/uma/utils/cert_utils.go +++ b/uma/utils/cert_utils.go @@ -3,6 +3,7 @@ package utils import ( "crypto/x509/pkix" "encoding/asn1" + "encoding/hex" "encoding/pem" "errors" "github.com/decred/dcrd/dcrec/secp256k1/v4" @@ -10,29 +11,68 @@ import ( "time" ) -func ExtractPubkeyFromPemCertificateChain(certChain string) (*secp256k1.PublicKey, error) { - block, _ := pem.Decode([]byte(certChain)) - if block == nil { - return nil, errors.New("failed to parse certificate chain PEM") +func ConvertPemCertificateChainToHexEncodedDer(certChain *string) ([]string, error) { + if certChain == nil { + return []string{}, nil } - asn1Data := block.Bytes + asn1Certs, err := getAsn1DataFromPemChain(certChain) + if err != nil { + return nil, err + } + var v []string + for _, block := range *asn1Certs { + v = append(v, hex.EncodeToString(block)) + } + return v, nil +} - var v []*certificate - for len(asn1Data) > 0 { - cert := new(certificate) - var err error - asn1Data, err = asn1.Unmarshal(asn1Data, cert) +func ConvertHexEncodedDerToPemCertChain(hexDerCerts *[]string) (*string, error) { + if hexDerCerts == nil || len(*hexDerCerts) == 0 { + return nil, nil + } + var pemCertChain string + for _, hexDerCert := range *hexDerCerts { + derCert, err := hex.DecodeString(hexDerCert) if err != nil { return nil, err } - v = append(v, cert) + block := &pem.Block{ + Type: "CERTIFICATE", + Bytes: derCert, + } + pemCertChain = pemCertChain + string(pem.EncodeToMemory(block)) } + return &pemCertChain, nil +} - if len(v) == 0 { +func ExtractPubkeyFromPemCertificateChain(certChain *string) (*secp256k1.PublicKey, error) { + asn1Certs, err := getAsn1DataFromPemChain(certChain) + if err != nil { + return nil, err + } + if len(*asn1Certs) == 0 { return nil, errors.New("empty certificate chain") } + cert := new(certificate) + _, err = asn1.Unmarshal((*asn1Certs)[0], cert) + if err != nil { + return nil, err + } + return parseToSecp256k1PublicKey(&cert.TBSCertificate.PublicKey) +} - return parseToSecp256k1PublicKey(&v[0].TBSCertificate.PublicKey) +func getAsn1DataFromPemChain(certChain *string) (*[][]byte, error) { + pemData := []byte(*certChain) + var v [][]byte + for len(pemData) > 0 { + var block *pem.Block + block, pemData = pem.Decode(pemData) + if block == nil { + return nil, errors.New("failed to decode PEM block") + } + v = append(v, block.Bytes) + } + return &v, nil } func parseToSecp256k1PublicKey(keyData *publicKeyInfo) (*secp256k1.PublicKey, error) {