Skip to content

Commit

Permalink
feat(authority-claimer): implement AWS KMS sign
Browse files Browse the repository at this point in the history
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
mpolitzer committed Oct 7, 2024
1 parent 19bd442 commit cdc7874
Show file tree
Hide file tree
Showing 3 changed files with 300 additions and 0 deletions.
47 changes: 47 additions & 0 deletions internal/anvil/account.go
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,
})
}
}
164 changes: 164 additions & 0 deletions internal/aws-kms-signtx/signtx.go
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
}
89 changes: 89 additions & 0 deletions internal/aws-kms-signtx/signtx_test.go
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)
}

0 comments on commit cdc7874

Please sign in to comment.