-
Notifications
You must be signed in to change notification settings - Fork 64
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(authority-claimer): implement AWS KMS sign
The test requires: - Setup a KMS key: Key Type: "Asymmetric" Key Spec: ECC_SECG_P256K1 Key Usage: Sign and verify Signing Algorithms: ECDSA_SHA_256 - Setup the ARN variable in signtx_test.go of said key - Configure the AWS credentials on the machine running the test
- Loading branch information
Showing
3 changed files
with
300 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,47 @@ | ||
/* Anvil provides 10 dev accounts by default. Make them easy to use in tests */ | ||
package anvil | ||
|
||
import ( | ||
"crypto/ecdsa" | ||
"github.com/ethereum/go-ethereum/crypto" | ||
"github.com/ethereum/go-ethereum/common" | ||
) | ||
|
||
/* Bundled PrivateKey, PublicKey and Address */ | ||
type Account = struct{ | ||
PrivateKey *ecdsa.PrivateKey; | ||
PublicKey *ecdsa.PublicKey; | ||
Address common.Address; | ||
} | ||
|
||
/* Default anvil dev accounts */ | ||
var DevAccounts = []Account{} | ||
|
||
func init() { | ||
anvilPrivateKeys := []string{ | ||
"0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80", | ||
"0x59c6995e998f97a5a0044966f0945389dc9e86dae88c7a8412f4603b6b78690d", | ||
"0x5de4111afa1a4b94908f83103eb1f1706367c2e68ca870fc3fb9a804cdab365a", | ||
"0x7c852118294e51e653712a81e05800f419141751be58f605c371e15141b007a6", | ||
"0x47e179ec197488593b187f80a00eb0da91f1b9d0b13f8733639f19c30a34926a", | ||
"0x8b3a350cf5c34c9194ca85829a2df0ec3153be0318b5e2d3348e872092edffba", | ||
"0x92db14e403b83dfe3df233f83dfa3a0d7096f21ca9b0d6d6b8d88b2b4ec1564e", | ||
"0x4bbbf85ce3377467afe5d46f804f221813b2bb87f24d81f60f1fcdbf7cbf4356", | ||
"0xdbda1821b80551c9d65939329250298aa3472ba22feea921c0cf5d620ea67b97", | ||
"0x2a871d0798f97d79848a013d4936a73bf4cc922c825d33c1cf7073dff6d409c6", | ||
} | ||
for _, key := range(anvilPrivateKeys) { | ||
privateKey, err := crypto.HexToECDSA(key[2:]) | ||
if err != nil { | ||
panic(err) | ||
} | ||
publicKey := privateKey.Public().(*ecdsa.PublicKey) | ||
address := crypto.PubkeyToAddress(*publicKey) | ||
|
||
DevAccounts = append(DevAccounts, Account{ | ||
PrivateKey: privateKey, | ||
PublicKey: publicKey, | ||
Address: address, | ||
}) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,164 @@ | ||
/* Package signtx provides utilities for dealing with private keys stored | ||
* remotely in a Amazon KMS secure server. | ||
* | ||
* Without local access to the private key, signing a message involves creating | ||
* a digest for it, sending it to KMS server, signing it there, then retrieving | ||
* the signature. */ | ||
package signtx | ||
|
||
import ( | ||
"context" | ||
"crypto/ecdsa" | ||
"encoding/asn1" | ||
"errors" | ||
"math/big" | ||
"reflect" | ||
|
||
"github.com/aws/aws-sdk-go-v2/service/kms" | ||
"github.com/ethereum/go-ethereum/common" | ||
"github.com/ethereum/go-ethereum/core/types" | ||
"github.com/ethereum/go-ethereum/crypto" | ||
) | ||
|
||
/* This is the signature for a function that takes a transaction, passes it | ||
* through a signer to obtain a message digest, creates a signature and embeds | ||
* it into the transaction itself. */ | ||
type SignTxFn = func(tx *types.Transaction, s types.Signer) (*types.Transaction, error) | ||
|
||
/* AWS sometimes reply with a `r` larger than 32bytes padded on the left with | ||
* zeros. Trim it down to a total of 32bytes */ | ||
func normalizeR(R []byte) ([]byte, error) { | ||
if len(R) <= 32 { | ||
return R, nil | ||
} | ||
for i := 0; i < len(R)-32; i++ { | ||
if R[i] != 0 { // must be padding | ||
return nil, errors.New("malformed `r` component") | ||
} | ||
} | ||
return R[len(R)-32:], nil | ||
} | ||
|
||
/* normalize `s` to the lower half of N according to EIP-2 | ||
* ref. https://eips.ethereum.org/EIPS/eip-2 */ | ||
func normalizeS(S []byte) []byte { | ||
N := crypto.S256().Params().N | ||
halfN := new(big.Int).Div(N, big.NewInt(2)) //nolint:mnd | ||
SBI := new(big.Int).SetBytes(S) | ||
|
||
if SBI.Cmp(halfN) > 0 { | ||
S = new(big.Int).Sub(N, SBI).Bytes() | ||
} | ||
return S | ||
} | ||
|
||
/* Compute the final component `v` of the ethereum signature, one KMS doesn't | ||
* provide, and assemble `r`, `s`, and `v` into the signature byte array. | ||
* Format is: [ R || S || V ], check signature_cgo.go for details. | ||
* | ||
* This `v` field is an ethereum extension to the ECDSA `r` and `s` signature | ||
* values used to facilitate the retrieval of the public key from the hash + | ||
* signature pair. We use this property to compute `v` by trial and error. One | ||
* of the values of `v` will hold ecrecover(hash, sig) == publicKey, and that | ||
* is the one ethereum wants. */ | ||
func assembleSignature(r []byte, s []byte, hash []byte, key []byte) ([]byte, error) { | ||
sig := make([]byte, 65) | ||
|
||
// align `s` and `r` in case they have less then 32bytes in size | ||
copy(sig[32-len(r):], r) | ||
copy(sig[64-len(s):], s) | ||
|
||
for i := byte(0); i < 2; i++ { | ||
sig[64] = i | ||
pub, err := crypto.Ecrecover(hash, sig[:]) | ||
if err != nil { | ||
return nil, err | ||
} | ||
if reflect.DeepEqual(pub, key) { | ||
return sig, nil | ||
} | ||
} | ||
return sig, errors.New("failed to compute signature") | ||
} | ||
|
||
/* Create a SignTxFn that uses the KMS infrastructure from AWS for signing. | ||
* The function wraps a context ctx, KMS client and arn to access and identify | ||
* the signing key. */ | ||
func CreateAWSSignTxFn( | ||
ctx context.Context, | ||
client *kms.Client, | ||
arn *string, | ||
) (SignTxFn, *ecdsa.PublicKey, common.Address, error) { | ||
publicKeyBytes, err := GetPublicKeyBytes(ctx, client, arn) | ||
if err != nil { | ||
return nil, nil, common.Address{}, err | ||
} | ||
publicKey, err := crypto.UnmarshalPubkey(publicKeyBytes) | ||
if err != nil { | ||
return nil, nil, common.Address{}, err | ||
} | ||
return func(tx *types.Transaction, signer types.Signer) (*types.Transaction, error) { | ||
hash := signer.Hash(tx).Bytes() | ||
signOutput, err := client.Sign(ctx, &kms.SignInput{ | ||
KeyId: arn, | ||
Message: hash, | ||
SigningAlgorithm: "ECDSA_SHA_256", | ||
MessageType: "DIGEST", | ||
}) | ||
if err != nil { | ||
return nil, err | ||
} | ||
|
||
/* AWS returns the signature wrapped in a DER-encoded object. | ||
* Try to unwrap it before continuing. */ | ||
type ecdsaSigValue struct { | ||
R asn1.RawValue | ||
S asn1.RawValue | ||
} | ||
var asn1sig ecdsaSigValue | ||
_, err = asn1.Unmarshal(signOutput.Signature, &asn1sig) | ||
if err != nil { | ||
return nil, err | ||
} | ||
|
||
S := normalizeS(asn1sig.S.Bytes) | ||
R, err := normalizeR(asn1sig.R.Bytes) | ||
if err != nil { | ||
return nil, err | ||
} | ||
signature, err := assembleSignature(R, S, hash, publicKeyBytes) | ||
if err != nil { | ||
return nil, err | ||
} | ||
return tx.WithSignature(signer, signature[:]) | ||
}, publicKey, crypto.PubkeyToAddress(*publicKey), nil | ||
} | ||
|
||
func GetPublicKeyBytes(ctx context.Context, client *kms.Client, Arn *string) ([]byte, error) { | ||
publicKeyOutput, err := client.GetPublicKey(ctx, &kms.GetPublicKeyInput{ | ||
KeyId: Arn, | ||
}) | ||
if err != nil { | ||
return nil, err | ||
} | ||
|
||
/* AWS returns the public key wrapped in a DER-encoded object. Unwrap | ||
* it before returning | ||
* ref: | ||
* https://pkg.go.dev/github.com/aws/aws-sdk-go-v2/service/kms#GetPublicKeyOutput | ||
* https://datatracker.ietf.org/doc/html/rfc5280 (p.16-17) */ | ||
type algorithmIdentifier struct { | ||
Algorithm asn1.ObjectIdentifier | ||
Parameters asn1.ObjectIdentifier // Optional by the spec | ||
} | ||
type subjectPublicKeyInfo struct { | ||
Algorithm algorithmIdentifier | ||
SubjectPublicKey asn1.BitString | ||
} | ||
var asn1key subjectPublicKeyInfo | ||
_, err = asn1.Unmarshal(publicKeyOutput.PublicKey, &asn1key) | ||
if err != nil { | ||
return nil, err | ||
} | ||
return asn1key.SubjectPublicKey.Bytes, nil | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,89 @@ | ||
package signtx | ||
|
||
import ( | ||
"context" | ||
"crypto/ecdsa" | ||
"math/big" | ||
"testing" | ||
|
||
"github.com/cartesi/rollups-node/internal/anvil" | ||
|
||
"github.com/ethereum/go-ethereum/common" | ||
"github.com/ethereum/go-ethereum/core/types" | ||
"github.com/ethereum/go-ethereum/ethclient" | ||
|
||
awscfg "github.com/aws/aws-sdk-go-v2/config" | ||
awskms "github.com/aws/aws-sdk-go-v2/service/kms" | ||
) | ||
|
||
var ARN = "" | ||
|
||
/* Create a SignTxFn from a private key. Useful for testing */ | ||
func CreateSignTxFnFromPrivateKey(privateKey *ecdsa.PrivateKey) SignTxFn { | ||
return func(tx *types.Transaction, s types.Signer) (*types.Transaction, error) { | ||
return types.SignTx(tx, s, privateKey) | ||
} | ||
} | ||
|
||
func sendFunds( | ||
value *big.Int, | ||
SignTx SignTxFn, | ||
ctx context.Context, | ||
sender common.Address, | ||
recipient common.Address, | ||
) { | ||
client, err := ethclient.Dial("http://127.0.0.1:8545") // anvil | ||
if err != nil { | ||
panic(err) | ||
} | ||
|
||
nonce, err := client.PendingNonceAt(context.Background(), sender) | ||
if err != nil { | ||
panic(err) | ||
} | ||
gasLimit := uint64(21000) | ||
gasPrice, err := client.SuggestGasPrice(ctx) | ||
if err != nil { | ||
panic(err) | ||
} | ||
var data []byte | ||
tx := types.NewTransaction(nonce, recipient, value, gasLimit, gasPrice, data) | ||
chainID, err := client.NetworkID(context.Background()) | ||
if err != nil { | ||
panic(err) | ||
} | ||
signedTx, err := SignTx(tx, types.NewEIP155Signer(chainID)) | ||
if err != nil { | ||
panic(err) | ||
} | ||
err = client.SendTransaction(context.Background(), signedTx) | ||
if err != nil { | ||
panic(err) | ||
} | ||
} | ||
|
||
func TestSignTx(t *testing.T) { | ||
if len(ARN) == 0 { | ||
t.Skip("Skipping test, ARN for KMS key is unset") | ||
} | ||
value20 := big.NewInt(2000000000000000000) // in wei (2 eth) | ||
value10 := big.NewInt(1000000000000000000) // in wei (1 eth) | ||
|
||
AnvilPrivateKey := anvil.DevAccounts[0].PrivateKey | ||
AnvilAddress := anvil.DevAccounts[0].Address | ||
|
||
config, err := awscfg.LoadDefaultConfig(context.Background()) | ||
if err != nil { | ||
panic(err) | ||
} | ||
kms := awskms.NewFromConfig(config) | ||
SignTx, _, KMSAddress, err := CreateAWSSignTxFn(context.Background(), kms, &ARN) | ||
if err != nil { | ||
panic(err) | ||
} | ||
|
||
sendFunds(value20, CreateSignTxFnFromPrivateKey(AnvilPrivateKey), | ||
context.Background(), AnvilAddress, KMSAddress) | ||
sendFunds(value10, SignTx, | ||
context.Background(), KMSAddress, AnvilAddress) | ||
} |