diff --git a/Makefile b/Makefile index 436f746..4f97548 100644 --- a/Makefile +++ b/Makefile @@ -79,3 +79,6 @@ generate_keys :; go run scripts/generate_keys/main.go --mnemonic "${mnemonic}" .PHONY: generate_multisig generate_multisig :; go run scripts/generate_multisig/main.go --publickeys "${publickeys}" --threshold ${threshold} + +.PHONY: gcp_kms +gcp_kms :; GCP_CREDS_JSON=${GCP_CREDS_JSON} GCP_KMS_KEY_NAME=${GCP_KMS_KEY_NAME} go run scripts/gcp_kms/main.go diff --git a/go.mod b/go.mod index a6f8ee7..194daf3 100644 --- a/go.mod +++ b/go.mod @@ -3,14 +3,17 @@ module github.com/dan13ram/wpokt-oracle go 1.22.3 require ( + cloud.google.com/go/kms v1.15.8 cloud.google.com/go/secretmanager v1.13.0 cosmossdk.io/math v1.3.0 cosmossdk.io/x/tx v0.13.2 + github.com/btcsuite/btcd/btcec/v2 v2.3.2 github.com/cometbft/cometbft v0.38.6 github.com/cosmos/cosmos-sdk v0.50.6 github.com/cosmos/go-bip39 v1.0.0 github.com/cosmos/gogoproto v1.4.12 github.com/dan13ram/go-ethereum-hdwallet v0.0.1 + github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0 github.com/ethereum/go-ethereum v1.14.3 github.com/googleapis/gax-go/v2 v2.12.3 github.com/joho/godotenv v1.5.1 @@ -18,6 +21,7 @@ require ( github.com/square/mongo-lock v0.0.0-20230808145049-cfcf499f6bf0 github.com/stretchr/testify v1.9.0 go.mongodb.org/mongo-driver v1.15.0 + google.golang.org/api v0.177.0 google.golang.org/grpc v1.63.2 google.golang.org/protobuf v1.34.0 gopkg.in/yaml.v2 v2.4.0 @@ -46,7 +50,6 @@ require ( github.com/bgentry/speakeasy v0.1.1-0.20220910012023-760eaf8b6816 // indirect github.com/bits-and-blooms/bitset v1.10.0 // indirect github.com/btcsuite/btcd v0.24.0 // indirect - github.com/btcsuite/btcd/btcec/v2 v2.3.2 // indirect github.com/btcsuite/btcd/btcutil v1.1.5 // indirect github.com/btcsuite/btcd/chaincfg/chainhash v1.1.0 // indirect github.com/cenkalti/backoff/v4 v4.1.3 // indirect @@ -71,7 +74,6 @@ require ( github.com/danieljoos/wincred v1.1.2 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/deckarep/golang-set/v2 v2.1.0 // indirect - github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0 // indirect github.com/desertbit/timer v0.0.0-20180107155436-c41aec40b27f // indirect github.com/dgraph-io/badger/v2 v2.2007.4 // indirect github.com/dgraph-io/ristretto v0.1.1 // indirect @@ -193,7 +195,6 @@ require ( golang.org/x/text v0.14.0 // indirect golang.org/x/time v0.5.0 // indirect golang.org/x/tools v0.20.0 // indirect - google.golang.org/api v0.177.0 // indirect google.golang.org/genproto v0.0.0-20240401170217-c3f982113cda // indirect google.golang.org/genproto/googleapis/api v0.0.0-20240429193739-8cf5692501f6 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20240429193739-8cf5692501f6 // indirect diff --git a/go.sum b/go.sum index 43eb30e..f514eb5 100644 --- a/go.sum +++ b/go.sum @@ -10,6 +10,8 @@ cloud.google.com/go/compute/metadata v0.3.0 h1:Tz+eQXMEqDIKRsmY3cHTL6FVaynIjX2Qx cloud.google.com/go/compute/metadata v0.3.0/go.mod h1:zFmK7XCadkQkj6TtorcaGlCW1hT1fIilQDwofLpJ20k= cloud.google.com/go/iam v1.1.7 h1:z4VHOhwKLF/+UYXAJDFwGtNF0b6gjsW1Pk9Ml0U/IoM= cloud.google.com/go/iam v1.1.7/go.mod h1:J4PMPg8TtyurAUvSmPj8FF3EDgY1SPRZxcUGrn7WXGA= +cloud.google.com/go/kms v1.15.8 h1:szIeDCowID8th2i8XE4uRev5PMxQFqW+JjwYxL9h6xs= +cloud.google.com/go/kms v1.15.8/go.mod h1:WoUHcDjD9pluCg7pNds131awnH429QGvRM3N/4MyoVs= cloud.google.com/go/secretmanager v1.13.0 h1:nQ/Ca2Gzm/OEP8tr1hiFdHRi5wAnAmsm9qTjwkivyrQ= cloud.google.com/go/secretmanager v1.13.0/go.mod h1:yWdfNmM2sLIiyv6RM6VqWKeBV7CdS0SO3ybxJJRhBEs= cosmossdk.io/api v0.7.4 h1:sPo8wKwCty1lht8kgL3J7YL1voJywP3YWuA5JKkBz30= diff --git a/scripts/gcp_kms/main.go b/scripts/gcp_kms/main.go new file mode 100644 index 0000000..efbab3b --- /dev/null +++ b/scripts/gcp_kms/main.go @@ -0,0 +1,325 @@ +package main + +import ( + "context" + "crypto/sha256" + "crypto/x509/pkix" + "encoding/asn1" + "encoding/pem" + "fmt" + "log" + "math/big" + "os" + + kms "cloud.google.com/go/kms/apiv1" + "cloud.google.com/go/kms/apiv1/kmspb" + btcecdsa "github.com/btcsuite/btcd/btcec/v2/ecdsa" + "github.com/cosmos/cosmos-sdk/crypto/keys/secp256k1" + dcrecSecp256k1 "github.com/decred/dcrd/dcrec/secp256k1/v4" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/crypto" + "google.golang.org/api/option" +) + +var GoogleAppCredsFilePath = os.Getenv("GCP_CREDS_JSON") +var GoogleKeyName = os.Getenv("GCP_KMS_KEY_NAME") + +func main() { + fmt.Println("Google App Creds Path: ", GoogleAppCredsFilePath) + fmt.Println("Google KMS Key Name: ", GoogleKeyName) + + // Initialize the KMS client + client, err := kms.NewKeyManagementClient(context.Background(), option.WithCredentialsFile(GoogleAppCredsFilePath)) + if err != nil { + log.Fatalf("failed to create KMS client: %v", err) + } + defer client.Close() + + // Specify the key name + keyName := GoogleKeyName + + // Get the key version details + keyVersion, err := getKeyVersionDetails(client, keyName) + if err != nil { + log.Fatalf("failed to get key version details: %v", err) + } + + // Print the key algorithm + fmt.Printf("Key Algorithm: %s\n", keyVersion.Algorithm.String()) + + if keyVersion.Algorithm != kmspb.CryptoKeyVersion_EC_SIGN_SECP256K1_SHA256 { + log.Fatalf("key algorithm is not supported: %v", keyVersion.Algorithm) + } + + // Prepare the transaction data (example) + txData := []byte("example transaction data") + + // Hash the transaction data + hash := sha256.New() + hash.Write(txData) + txHash := hash.Sum(nil) + + ethAddress, err := resolveEthAddr(client, keyName) + if err != nil { + log.Fatalf("failed to resolve address: %v", err) + } + + fmt.Printf("Eth Address: %s\n", ethAddress.Hex()) + + { + // Sign the hash using KMS + signature, err := ethSignHash(common.BytesToHash(txHash), client, keyName, ethAddress) + if err != nil { + log.Fatalf("failed to sign: %v", err) + } + + hexSig := fmt.Sprintf("%x", signature) + fmt.Printf("Eth Signature: %s\n", hexSig) + } + + pubKey, err := resolveCosmosPubKey(client, keyName) + if err != nil { + log.Fatalf("failed to resolve public key: %v", err) + } + + fmt.Printf("Cosmos Public Key: %x\n", pubKey.Key) + + { + + // Sign the hash using KMS + signature, err := cosmosSignHash(client, keyName, common.BytesToHash(txHash)) + if err != nil { + log.Fatalf("failed to sign: %v", err) + } + + hexSig := fmt.Sprintf("%x", signature) + fmt.Printf("Cosmos Signature: %s\n", hexSig) + } +} + +func resolveEthAddr(client *kms.KeyManagementClient, keyName string) (common.Address, error) { + resp, err := client.GetPublicKey(context.Background(), &kmspb.GetPublicKeyRequest{Name: keyName}) + if err != nil { + return common.Address{}, fmt.Errorf("Google KMS public key %q lookup: %w", keyName, err) + } + + block, _ := pem.Decode([]byte(resp.Pem)) + if block == nil { + return common.Address{}, fmt.Errorf("Google KMS public key %q PEM empty: %.130q", keyName, resp.Pem) + } + + var info struct { + AlgID pkix.AlgorithmIdentifier + Key asn1.BitString + } + _, err = asn1.Unmarshal(block.Bytes, &info) + if err != nil { + return common.Address{}, fmt.Errorf("Google KMS public key %q PEM block %q: %v", keyName, block.Type, err) + } + + wantAlg := asn1.ObjectIdentifier{1, 2, 840, 10045, 2, 1} + if gotAlg := info.AlgID.Algorithm; !gotAlg.Equal(wantAlg) { + return common.Address{}, fmt.Errorf("Google KMS public key %q ASN.1 algorithm %s intead of %s", keyName, gotAlg, wantAlg) + } + + // length := len(info.Key.Bytes) + // fmt.Printf("ETH Public Key Length: %d\n", length) + + return ethPubKeyAddr(info.Key.Bytes), nil +} + +// PubKeyAddr returns the Ethereum address for (uncompressed-)key bytes. +func ethPubKeyAddr(bytes []byte) common.Address { + digest := crypto.Keccak256(bytes[1:]) + var addr common.Address + copy(addr[:], digest[12:]) + return addr +} + +func ethSignHash(hash common.Hash, client *kms.KeyManagementClient, keyName string, ethAddress common.Address) ([]byte, error) { + // Resolve a signature + req := kmspb.AsymmetricSignRequest{ + Name: keyName, + Digest: &kmspb.Digest{ + Digest: &kmspb.Digest_Sha256{ + Sha256: hash[:], + }, + }, + } + resp, err := client.AsymmetricSign(context.Background(), &req) + if err != nil { + return nil, fmt.Errorf("Google KMS asymmetric sign operation: %w", err) + } + + // Parse signature + var params struct{ R, S *big.Int } + _, err = asn1.Unmarshal(resp.Signature, ¶ms) + if err != nil { + return nil, fmt.Errorf("Google KMS asymmetric signature encoding: %w", err) + } + var rLen, sLen int // byte size + if params.R != nil { + rLen = (params.R.BitLen() + 7) / 8 + } + if params.S != nil { + sLen = (params.S.BitLen() + 7) / 8 + } + if rLen == 0 || rLen > 32 || sLen == 0 || sLen > 32 { + return nil, fmt.Errorf("Google KMS asymmetric signature with %d-byte r and %d-byte s denied on size", rLen, sLen) + } + + // Need uncompressed signature with "recovery ID" at end: + // https://bitcointalk.org/index.php?topic=5249677.0 + // https://ethereum.stackexchange.com/a/53182/39582 + var sig [66]byte // + 1-byte header + 1-byte tailer + params.R.FillBytes(sig[33-rLen : 33]) + params.S.FillBytes(sig[65-sLen : 65]) + + // Brute force try includes KMS verification + var recoverErr error + for recoveryID := byte(0); recoveryID < 2; recoveryID++ { + sig[0] = recoveryID + 27 // BitCoin header + btcsig := sig[:65] // Exclude Ethereum 'v' parameter + pubKey, _, err := btcecdsa.RecoverCompact(btcsig, hash[:]) + if err != nil { + recoverErr = err + continue + } + + if ethPubKeyAddr(pubKey.SerializeUncompressed()) == ethAddress { + // Sign the transaction + sig[65] = recoveryID // Ethereum 'v' parameter + return sig[1:], nil // Exclude BitCoin header + } + } + // RecoverErr can be nil, but that's OK + return nil, fmt.Errorf("Google KMS asymmetric signature address recovery mis: %w", recoverErr) +} + +func cosmosSignHash(client *kms.KeyManagementClient, keyName string, hash [32]byte) ([]byte, error) { + // Sign the hash using KMS + req := &kmspb.AsymmetricSignRequest{ + Name: keyName, + Digest: &kmspb.Digest{ + Digest: &kmspb.Digest_Sha256{ + Sha256: hash[:], + }, + }, + } + + resp, err := client.AsymmetricSign(context.Background(), req) + if err != nil { + return nil, fmt.Errorf("failed to sign: %v", err) + } + + signature := resp.Signature + + // Extract r and s values from the signature + var params struct{ R, S *big.Int } + _, err = asn1.Unmarshal(signature, ¶ms) + if err != nil { + return nil, fmt.Errorf("Google KMS asymmetric signature encoding: %w", err) + } + + // fmt.Printf("R: %s\n", params.R.String()) + // fmt.Printf("S: %s\n", params.S.String()) + + rBytes := params.R.Bytes() + sBytes := params.S.Bytes() + + // Ensure r and s are 32 bytes each + rPadded := make([]byte, 32) + sPadded := make([]byte, 32) + copy(rPadded[32-len(rBytes):], rBytes) + copy(sPadded[32-len(sBytes):], sBytes) + + finalSig := append(rPadded, sPadded...) + + pubKey, err := resolveSecp256k1PubKey(client, keyName) + if err != nil { + return nil, fmt.Errorf("failed to resolve public key: %v", err) + } + + sig, err := signatureFromBytes(finalSig) + if err != nil { + return nil, fmt.Errorf("failed to parse signature: %v", err) + } + + if !sig.Verify(hash[:], pubKey) { + return nil, fmt.Errorf("signature verification failed") + } + + return finalSig, nil +} + +func signatureFromBytes(sigStr []byte) (*btcecdsa.Signature, error) { + var r dcrecSecp256k1.ModNScalar + r.SetByteSlice(sigStr[:32]) + var s dcrecSecp256k1.ModNScalar + s.SetByteSlice(sigStr[32:64]) + if s.IsOverHalfOrder() { + return nil, fmt.Errorf("signature is not in lower-S form") + } + + return btcecdsa.NewSignature(&r, &s), nil +} + +func resolveCosmosPubKey(client *kms.KeyManagementClient, keyName string) (*secp256k1.PubKey, error) { + pubkeyObject, err := resolveSecp256k1PubKey(client, keyName) + if err != nil { + return nil, fmt.Errorf("failed to resolve public key: %v", err) + } + + pk := pubkeyObject.SerializeCompressed() + + return &secp256k1.PubKey{Key: pk}, nil +} + +func resolveSecp256k1PubKey(client *kms.KeyManagementClient, keyName string) (*dcrecSecp256k1.PublicKey, error) { + publicKeyResp, err := client.GetPublicKey(context.Background(), &kmspb.GetPublicKeyRequest{Name: keyName}) + if err != nil { + return nil, fmt.Errorf("failed to get public key: %v", err) + } + + publicKeyPem := publicKeyResp.Pem + + block, _ := pem.Decode([]byte(publicKeyPem)) + if block == nil { + return nil, fmt.Errorf("Google KMS public key %q PEM empty: %.130q", keyName, publicKeyPem) + } + + var info struct { + AlgID pkix.AlgorithmIdentifier + Key asn1.BitString + } + _, err = asn1.Unmarshal(block.Bytes, &info) + if err != nil { + return nil, fmt.Errorf("Google KMS public key %q PEM block %q: %v", keyName, block.Type, err) + } + + wantAlg := asn1.ObjectIdentifier{1, 2, 840, 10045, 2, 1} + if gotAlg := info.AlgID.Algorithm; !gotAlg.Equal(wantAlg) { + return nil, fmt.Errorf("Google KMS public key %q ASN.1 algorithm %s instead of %s", keyName, gotAlg, wantAlg) + } + + pubkeyObject, err := dcrecSecp256k1.ParsePubKey(info.Key.Bytes) + if err != nil { + return nil, fmt.Errorf("failed to parse public key: %v", err) + } + + return pubkeyObject, nil +} + +func getKeyVersionDetails(client *kms.KeyManagementClient, keyName string) (*kmspb.CryptoKeyVersion, error) { + // Request the key version details + req := &kmspb.GetCryptoKeyVersionRequest{ + Name: keyName, + } + + resp, err := client.GetCryptoKeyVersion(context.Background(), req) + if err != nil { + return nil, fmt.Errorf("failed to get key version details: %v", err) + } + + return resp, nil +}