Skip to content

Commit

Permalink
added gcp kms example
Browse files Browse the repository at this point in the history
  • Loading branch information
dan13ram committed Jul 2, 2024
1 parent 71c821d commit a8248be
Show file tree
Hide file tree
Showing 4 changed files with 334 additions and 3 deletions.
3 changes: 3 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -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
7 changes: 4 additions & 3 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -3,21 +3,25 @@ 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
github.com/sirupsen/logrus v1.9.3
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
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -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=
Expand Down
325 changes: 325 additions & 0 deletions scripts/gcp_kms/main.go
Original file line number Diff line number Diff line change
@@ -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, &params)
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, &params)
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
}

0 comments on commit a8248be

Please sign in to comment.