Skip to content

Commit a8248be

Browse files
committed
added gcp kms example
1 parent 71c821d commit a8248be

File tree

4 files changed

+334
-3
lines changed

4 files changed

+334
-3
lines changed

Makefile

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,3 +79,6 @@ generate_keys :; go run scripts/generate_keys/main.go --mnemonic "${mnemonic}"
7979

8080
.PHONY: generate_multisig
8181
generate_multisig :; go run scripts/generate_multisig/main.go --publickeys "${publickeys}" --threshold ${threshold}
82+
83+
.PHONY: gcp_kms
84+
gcp_kms :; GCP_CREDS_JSON=${GCP_CREDS_JSON} GCP_KMS_KEY_NAME=${GCP_KMS_KEY_NAME} go run scripts/gcp_kms/main.go

go.mod

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,21 +3,25 @@ module github.com/dan13ram/wpokt-oracle
33
go 1.22.3
44

55
require (
6+
cloud.google.com/go/kms v1.15.8
67
cloud.google.com/go/secretmanager v1.13.0
78
cosmossdk.io/math v1.3.0
89
cosmossdk.io/x/tx v0.13.2
10+
github.com/btcsuite/btcd/btcec/v2 v2.3.2
911
github.com/cometbft/cometbft v0.38.6
1012
github.com/cosmos/cosmos-sdk v0.50.6
1113
github.com/cosmos/go-bip39 v1.0.0
1214
github.com/cosmos/gogoproto v1.4.12
1315
github.com/dan13ram/go-ethereum-hdwallet v0.0.1
16+
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0
1417
github.com/ethereum/go-ethereum v1.14.3
1518
github.com/googleapis/gax-go/v2 v2.12.3
1619
github.com/joho/godotenv v1.5.1
1720
github.com/sirupsen/logrus v1.9.3
1821
github.com/square/mongo-lock v0.0.0-20230808145049-cfcf499f6bf0
1922
github.com/stretchr/testify v1.9.0
2023
go.mongodb.org/mongo-driver v1.15.0
24+
google.golang.org/api v0.177.0
2125
google.golang.org/grpc v1.63.2
2226
google.golang.org/protobuf v1.34.0
2327
gopkg.in/yaml.v2 v2.4.0
@@ -46,7 +50,6 @@ require (
4650
github.com/bgentry/speakeasy v0.1.1-0.20220910012023-760eaf8b6816 // indirect
4751
github.com/bits-and-blooms/bitset v1.10.0 // indirect
4852
github.com/btcsuite/btcd v0.24.0 // indirect
49-
github.com/btcsuite/btcd/btcec/v2 v2.3.2 // indirect
5053
github.com/btcsuite/btcd/btcutil v1.1.5 // indirect
5154
github.com/btcsuite/btcd/chaincfg/chainhash v1.1.0 // indirect
5255
github.com/cenkalti/backoff/v4 v4.1.3 // indirect
@@ -71,7 +74,6 @@ require (
7174
github.com/danieljoos/wincred v1.1.2 // indirect
7275
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
7376
github.com/deckarep/golang-set/v2 v2.1.0 // indirect
74-
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0 // indirect
7577
github.com/desertbit/timer v0.0.0-20180107155436-c41aec40b27f // indirect
7678
github.com/dgraph-io/badger/v2 v2.2007.4 // indirect
7779
github.com/dgraph-io/ristretto v0.1.1 // indirect
@@ -193,7 +195,6 @@ require (
193195
golang.org/x/text v0.14.0 // indirect
194196
golang.org/x/time v0.5.0 // indirect
195197
golang.org/x/tools v0.20.0 // indirect
196-
google.golang.org/api v0.177.0 // indirect
197198
google.golang.org/genproto v0.0.0-20240401170217-c3f982113cda // indirect
198199
google.golang.org/genproto/googleapis/api v0.0.0-20240429193739-8cf5692501f6 // indirect
199200
google.golang.org/genproto/googleapis/rpc v0.0.0-20240429193739-8cf5692501f6 // indirect

go.sum

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ cloud.google.com/go/compute/metadata v0.3.0 h1:Tz+eQXMEqDIKRsmY3cHTL6FVaynIjX2Qx
1010
cloud.google.com/go/compute/metadata v0.3.0/go.mod h1:zFmK7XCadkQkj6TtorcaGlCW1hT1fIilQDwofLpJ20k=
1111
cloud.google.com/go/iam v1.1.7 h1:z4VHOhwKLF/+UYXAJDFwGtNF0b6gjsW1Pk9Ml0U/IoM=
1212
cloud.google.com/go/iam v1.1.7/go.mod h1:J4PMPg8TtyurAUvSmPj8FF3EDgY1SPRZxcUGrn7WXGA=
13+
cloud.google.com/go/kms v1.15.8 h1:szIeDCowID8th2i8XE4uRev5PMxQFqW+JjwYxL9h6xs=
14+
cloud.google.com/go/kms v1.15.8/go.mod h1:WoUHcDjD9pluCg7pNds131awnH429QGvRM3N/4MyoVs=
1315
cloud.google.com/go/secretmanager v1.13.0 h1:nQ/Ca2Gzm/OEP8tr1hiFdHRi5wAnAmsm9qTjwkivyrQ=
1416
cloud.google.com/go/secretmanager v1.13.0/go.mod h1:yWdfNmM2sLIiyv6RM6VqWKeBV7CdS0SO3ybxJJRhBEs=
1517
cosmossdk.io/api v0.7.4 h1:sPo8wKwCty1lht8kgL3J7YL1voJywP3YWuA5JKkBz30=

scripts/gcp_kms/main.go

Lines changed: 325 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,325 @@
1+
package main
2+
3+
import (
4+
"context"
5+
"crypto/sha256"
6+
"crypto/x509/pkix"
7+
"encoding/asn1"
8+
"encoding/pem"
9+
"fmt"
10+
"log"
11+
"math/big"
12+
"os"
13+
14+
kms "cloud.google.com/go/kms/apiv1"
15+
"cloud.google.com/go/kms/apiv1/kmspb"
16+
btcecdsa "github.com/btcsuite/btcd/btcec/v2/ecdsa"
17+
"github.com/cosmos/cosmos-sdk/crypto/keys/secp256k1"
18+
dcrecSecp256k1 "github.com/decred/dcrd/dcrec/secp256k1/v4"
19+
"github.com/ethereum/go-ethereum/common"
20+
"github.com/ethereum/go-ethereum/crypto"
21+
"google.golang.org/api/option"
22+
)
23+
24+
var GoogleAppCredsFilePath = os.Getenv("GCP_CREDS_JSON")
25+
var GoogleKeyName = os.Getenv("GCP_KMS_KEY_NAME")
26+
27+
func main() {
28+
fmt.Println("Google App Creds Path: ", GoogleAppCredsFilePath)
29+
fmt.Println("Google KMS Key Name: ", GoogleKeyName)
30+
31+
// Initialize the KMS client
32+
client, err := kms.NewKeyManagementClient(context.Background(), option.WithCredentialsFile(GoogleAppCredsFilePath))
33+
if err != nil {
34+
log.Fatalf("failed to create KMS client: %v", err)
35+
}
36+
defer client.Close()
37+
38+
// Specify the key name
39+
keyName := GoogleKeyName
40+
41+
// Get the key version details
42+
keyVersion, err := getKeyVersionDetails(client, keyName)
43+
if err != nil {
44+
log.Fatalf("failed to get key version details: %v", err)
45+
}
46+
47+
// Print the key algorithm
48+
fmt.Printf("Key Algorithm: %s\n", keyVersion.Algorithm.String())
49+
50+
if keyVersion.Algorithm != kmspb.CryptoKeyVersion_EC_SIGN_SECP256K1_SHA256 {
51+
log.Fatalf("key algorithm is not supported: %v", keyVersion.Algorithm)
52+
}
53+
54+
// Prepare the transaction data (example)
55+
txData := []byte("example transaction data")
56+
57+
// Hash the transaction data
58+
hash := sha256.New()
59+
hash.Write(txData)
60+
txHash := hash.Sum(nil)
61+
62+
ethAddress, err := resolveEthAddr(client, keyName)
63+
if err != nil {
64+
log.Fatalf("failed to resolve address: %v", err)
65+
}
66+
67+
fmt.Printf("Eth Address: %s\n", ethAddress.Hex())
68+
69+
{
70+
// Sign the hash using KMS
71+
signature, err := ethSignHash(common.BytesToHash(txHash), client, keyName, ethAddress)
72+
if err != nil {
73+
log.Fatalf("failed to sign: %v", err)
74+
}
75+
76+
hexSig := fmt.Sprintf("%x", signature)
77+
fmt.Printf("Eth Signature: %s\n", hexSig)
78+
}
79+
80+
pubKey, err := resolveCosmosPubKey(client, keyName)
81+
if err != nil {
82+
log.Fatalf("failed to resolve public key: %v", err)
83+
}
84+
85+
fmt.Printf("Cosmos Public Key: %x\n", pubKey.Key)
86+
87+
{
88+
89+
// Sign the hash using KMS
90+
signature, err := cosmosSignHash(client, keyName, common.BytesToHash(txHash))
91+
if err != nil {
92+
log.Fatalf("failed to sign: %v", err)
93+
}
94+
95+
hexSig := fmt.Sprintf("%x", signature)
96+
fmt.Printf("Cosmos Signature: %s\n", hexSig)
97+
}
98+
}
99+
100+
func resolveEthAddr(client *kms.KeyManagementClient, keyName string) (common.Address, error) {
101+
resp, err := client.GetPublicKey(context.Background(), &kmspb.GetPublicKeyRequest{Name: keyName})
102+
if err != nil {
103+
return common.Address{}, fmt.Errorf("Google KMS public key %q lookup: %w", keyName, err)
104+
}
105+
106+
block, _ := pem.Decode([]byte(resp.Pem))
107+
if block == nil {
108+
return common.Address{}, fmt.Errorf("Google KMS public key %q PEM empty: %.130q", keyName, resp.Pem)
109+
}
110+
111+
var info struct {
112+
AlgID pkix.AlgorithmIdentifier
113+
Key asn1.BitString
114+
}
115+
_, err = asn1.Unmarshal(block.Bytes, &info)
116+
if err != nil {
117+
return common.Address{}, fmt.Errorf("Google KMS public key %q PEM block %q: %v", keyName, block.Type, err)
118+
}
119+
120+
wantAlg := asn1.ObjectIdentifier{1, 2, 840, 10045, 2, 1}
121+
if gotAlg := info.AlgID.Algorithm; !gotAlg.Equal(wantAlg) {
122+
return common.Address{}, fmt.Errorf("Google KMS public key %q ASN.1 algorithm %s intead of %s", keyName, gotAlg, wantAlg)
123+
}
124+
125+
// length := len(info.Key.Bytes)
126+
// fmt.Printf("ETH Public Key Length: %d\n", length)
127+
128+
return ethPubKeyAddr(info.Key.Bytes), nil
129+
}
130+
131+
// PubKeyAddr returns the Ethereum address for (uncompressed-)key bytes.
132+
func ethPubKeyAddr(bytes []byte) common.Address {
133+
digest := crypto.Keccak256(bytes[1:])
134+
var addr common.Address
135+
copy(addr[:], digest[12:])
136+
return addr
137+
}
138+
139+
func ethSignHash(hash common.Hash, client *kms.KeyManagementClient, keyName string, ethAddress common.Address) ([]byte, error) {
140+
// Resolve a signature
141+
req := kmspb.AsymmetricSignRequest{
142+
Name: keyName,
143+
Digest: &kmspb.Digest{
144+
Digest: &kmspb.Digest_Sha256{
145+
Sha256: hash[:],
146+
},
147+
},
148+
}
149+
resp, err := client.AsymmetricSign(context.Background(), &req)
150+
if err != nil {
151+
return nil, fmt.Errorf("Google KMS asymmetric sign operation: %w", err)
152+
}
153+
154+
// Parse signature
155+
var params struct{ R, S *big.Int }
156+
_, err = asn1.Unmarshal(resp.Signature, &params)
157+
if err != nil {
158+
return nil, fmt.Errorf("Google KMS asymmetric signature encoding: %w", err)
159+
}
160+
var rLen, sLen int // byte size
161+
if params.R != nil {
162+
rLen = (params.R.BitLen() + 7) / 8
163+
}
164+
if params.S != nil {
165+
sLen = (params.S.BitLen() + 7) / 8
166+
}
167+
if rLen == 0 || rLen > 32 || sLen == 0 || sLen > 32 {
168+
return nil, fmt.Errorf("Google KMS asymmetric signature with %d-byte r and %d-byte s denied on size", rLen, sLen)
169+
}
170+
171+
// Need uncompressed signature with "recovery ID" at end:
172+
// https://bitcointalk.org/index.php?topic=5249677.0
173+
// https://ethereum.stackexchange.com/a/53182/39582
174+
var sig [66]byte // + 1-byte header + 1-byte tailer
175+
params.R.FillBytes(sig[33-rLen : 33])
176+
params.S.FillBytes(sig[65-sLen : 65])
177+
178+
// Brute force try includes KMS verification
179+
var recoverErr error
180+
for recoveryID := byte(0); recoveryID < 2; recoveryID++ {
181+
sig[0] = recoveryID + 27 // BitCoin header
182+
btcsig := sig[:65] // Exclude Ethereum 'v' parameter
183+
pubKey, _, err := btcecdsa.RecoverCompact(btcsig, hash[:])
184+
if err != nil {
185+
recoverErr = err
186+
continue
187+
}
188+
189+
if ethPubKeyAddr(pubKey.SerializeUncompressed()) == ethAddress {
190+
// Sign the transaction
191+
sig[65] = recoveryID // Ethereum 'v' parameter
192+
return sig[1:], nil // Exclude BitCoin header
193+
}
194+
}
195+
// RecoverErr can be nil, but that's OK
196+
return nil, fmt.Errorf("Google KMS asymmetric signature address recovery mis: %w", recoverErr)
197+
}
198+
199+
func cosmosSignHash(client *kms.KeyManagementClient, keyName string, hash [32]byte) ([]byte, error) {
200+
// Sign the hash using KMS
201+
req := &kmspb.AsymmetricSignRequest{
202+
Name: keyName,
203+
Digest: &kmspb.Digest{
204+
Digest: &kmspb.Digest_Sha256{
205+
Sha256: hash[:],
206+
},
207+
},
208+
}
209+
210+
resp, err := client.AsymmetricSign(context.Background(), req)
211+
if err != nil {
212+
return nil, fmt.Errorf("failed to sign: %v", err)
213+
}
214+
215+
signature := resp.Signature
216+
217+
// Extract r and s values from the signature
218+
var params struct{ R, S *big.Int }
219+
_, err = asn1.Unmarshal(signature, &params)
220+
if err != nil {
221+
return nil, fmt.Errorf("Google KMS asymmetric signature encoding: %w", err)
222+
}
223+
224+
// fmt.Printf("R: %s\n", params.R.String())
225+
// fmt.Printf("S: %s\n", params.S.String())
226+
227+
rBytes := params.R.Bytes()
228+
sBytes := params.S.Bytes()
229+
230+
// Ensure r and s are 32 bytes each
231+
rPadded := make([]byte, 32)
232+
sPadded := make([]byte, 32)
233+
copy(rPadded[32-len(rBytes):], rBytes)
234+
copy(sPadded[32-len(sBytes):], sBytes)
235+
236+
finalSig := append(rPadded, sPadded...)
237+
238+
pubKey, err := resolveSecp256k1PubKey(client, keyName)
239+
if err != nil {
240+
return nil, fmt.Errorf("failed to resolve public key: %v", err)
241+
}
242+
243+
sig, err := signatureFromBytes(finalSig)
244+
if err != nil {
245+
return nil, fmt.Errorf("failed to parse signature: %v", err)
246+
}
247+
248+
if !sig.Verify(hash[:], pubKey) {
249+
return nil, fmt.Errorf("signature verification failed")
250+
}
251+
252+
return finalSig, nil
253+
}
254+
255+
func signatureFromBytes(sigStr []byte) (*btcecdsa.Signature, error) {
256+
var r dcrecSecp256k1.ModNScalar
257+
r.SetByteSlice(sigStr[:32])
258+
var s dcrecSecp256k1.ModNScalar
259+
s.SetByteSlice(sigStr[32:64])
260+
if s.IsOverHalfOrder() {
261+
return nil, fmt.Errorf("signature is not in lower-S form")
262+
}
263+
264+
return btcecdsa.NewSignature(&r, &s), nil
265+
}
266+
267+
func resolveCosmosPubKey(client *kms.KeyManagementClient, keyName string) (*secp256k1.PubKey, error) {
268+
pubkeyObject, err := resolveSecp256k1PubKey(client, keyName)
269+
if err != nil {
270+
return nil, fmt.Errorf("failed to resolve public key: %v", err)
271+
}
272+
273+
pk := pubkeyObject.SerializeCompressed()
274+
275+
return &secp256k1.PubKey{Key: pk}, nil
276+
}
277+
278+
func resolveSecp256k1PubKey(client *kms.KeyManagementClient, keyName string) (*dcrecSecp256k1.PublicKey, error) {
279+
publicKeyResp, err := client.GetPublicKey(context.Background(), &kmspb.GetPublicKeyRequest{Name: keyName})
280+
if err != nil {
281+
return nil, fmt.Errorf("failed to get public key: %v", err)
282+
}
283+
284+
publicKeyPem := publicKeyResp.Pem
285+
286+
block, _ := pem.Decode([]byte(publicKeyPem))
287+
if block == nil {
288+
return nil, fmt.Errorf("Google KMS public key %q PEM empty: %.130q", keyName, publicKeyPem)
289+
}
290+
291+
var info struct {
292+
AlgID pkix.AlgorithmIdentifier
293+
Key asn1.BitString
294+
}
295+
_, err = asn1.Unmarshal(block.Bytes, &info)
296+
if err != nil {
297+
return nil, fmt.Errorf("Google KMS public key %q PEM block %q: %v", keyName, block.Type, err)
298+
}
299+
300+
wantAlg := asn1.ObjectIdentifier{1, 2, 840, 10045, 2, 1}
301+
if gotAlg := info.AlgID.Algorithm; !gotAlg.Equal(wantAlg) {
302+
return nil, fmt.Errorf("Google KMS public key %q ASN.1 algorithm %s instead of %s", keyName, gotAlg, wantAlg)
303+
}
304+
305+
pubkeyObject, err := dcrecSecp256k1.ParsePubKey(info.Key.Bytes)
306+
if err != nil {
307+
return nil, fmt.Errorf("failed to parse public key: %v", err)
308+
}
309+
310+
return pubkeyObject, nil
311+
}
312+
313+
func getKeyVersionDetails(client *kms.KeyManagementClient, keyName string) (*kmspb.CryptoKeyVersion, error) {
314+
// Request the key version details
315+
req := &kmspb.GetCryptoKeyVersionRequest{
316+
Name: keyName,
317+
}
318+
319+
resp, err := client.GetCryptoKeyVersion(context.Background(), req)
320+
if err != nil {
321+
return nil, fmt.Errorf("failed to get key version details: %v", err)
322+
}
323+
324+
return resp, nil
325+
}

0 commit comments

Comments
 (0)