-
Notifications
You must be signed in to change notification settings - Fork 690
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
node: add amazon kms and benchmark signers #4148
base: main
Are you sure you want to change the base?
Changes from 2 commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,167 @@ | ||
package guardiansigner | ||
|
||
import ( | ||
"bytes" | ||
"context" | ||
"crypto/ecdsa" | ||
"encoding/asn1" | ||
"errors" | ||
"fmt" | ||
"math/big" | ||
|
||
"github.com/aws/aws-sdk-go-v2/config" | ||
"github.com/aws/aws-sdk-go-v2/service/kms" | ||
kms_types "github.com/aws/aws-sdk-go-v2/service/kms/types" | ||
"github.com/aws/aws-sdk-go/aws" | ||
ethcrypto "github.com/ethereum/go-ethereum/crypto" | ||
) | ||
|
||
var ( | ||
secp256k1N = ethcrypto.S256().Params().N | ||
secp256k1HalfN = new(big.Int).Div(secp256k1N, big.NewInt(2)) | ||
) | ||
|
||
type asn1EcSig struct { | ||
R asn1.RawValue | ||
S asn1.RawValue | ||
} | ||
|
||
type asn1EcPublicKey struct { | ||
EcPublicKeyInfo asn1EcPublicKeyInfo | ||
PublicKey asn1.BitString | ||
} | ||
|
||
type asn1EcPublicKeyInfo struct { | ||
Algorithm asn1.ObjectIdentifier | ||
Parameters asn1.ObjectIdentifier | ||
} | ||
|
||
type AmazonKms struct { | ||
KeyId string | ||
Region string | ||
svc *kms.Client | ||
} | ||
|
||
func NewAmazonKmsSigner(unsafeDevMode bool, keyPath string) (*AmazonKms, error) { | ||
amazonKmsSigner := AmazonKms{ | ||
KeyId: keyPath, | ||
} | ||
|
||
cfg, err := config.LoadDefaultConfig(context.TODO(), config.WithRegion("eu-north-1")) | ||
if err != nil { | ||
return nil, errors.New("failed to load default config") | ||
} | ||
|
||
amazonKmsSigner.svc = kms.NewFromConfig(cfg) | ||
|
||
return &amazonKmsSigner, nil | ||
} | ||
|
||
func (a *AmazonKms) Sign(hash []byte) (signature []byte, err error) { | ||
|
||
// request signing | ||
res, err := a.svc.Sign(context.TODO(), &kms.SignInput{ | ||
KeyId: aws.String(a.KeyId), | ||
Message: hash, | ||
SigningAlgorithm: kms_types.SigningAlgorithmSpecEcdsaSha256, | ||
MessageType: kms_types.MessageTypeDigest, | ||
}) | ||
|
||
if err != nil { | ||
return nil, fmt.Errorf("Signing failed: %w", err) | ||
} | ||
|
||
// decode r and s values | ||
r, s := derSignatureToRS(res.Signature) | ||
|
||
// if s is greater than secp256k1HalfN, we need to substract secp256k1N from it | ||
sBigInt := new(big.Int).SetBytes(s) | ||
if sBigInt.Cmp(secp256k1HalfN) > 0 { | ||
s = new(big.Int).Sub(secp256k1N, sBigInt).Bytes() | ||
} | ||
|
||
// r and s need to be 32 bytes in size | ||
r = adjustBufferSize(r) | ||
s = adjustBufferSize(s) | ||
|
||
// AWS KMS does not provide the recovery id. But that doesn't matter too much, since we can | ||
// attempt recovery id's 0 and 1, and in the process ensure that the signature is valid. | ||
expectedPublicKey := a.PublicKey() | ||
signature = append(r, s...) | ||
|
||
// try recovery id 0 | ||
ecSigWithRecid := append(signature, []byte{0}...) | ||
pubkey, err := ethcrypto.SigToPub(hash[:], ecSigWithRecid) | ||
pleasew8t marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
if bytes.Equal(ethcrypto.CompressPubkey(pubkey), ethcrypto.CompressPubkey(&expectedPublicKey)) { | ||
return ecSigWithRecid, nil | ||
} | ||
|
||
ecSigWithRecid = append(signature, []byte{1}...) | ||
pubkey, err = ethcrypto.SigToPub(hash[:], ecSigWithRecid) | ||
pleasew8t marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
// try recovery id 1 | ||
if bytes.Equal(ethcrypto.CompressPubkey(pubkey), ethcrypto.CompressPubkey(&expectedPublicKey)) { | ||
return ecSigWithRecid, nil | ||
} | ||
|
||
return nil, fmt.Errorf("Failed to generate signature") | ||
} | ||
|
||
func (a *AmazonKms) PublicKey() ecdsa.PublicKey { | ||
pubKeyOutput, _ := a.svc.GetPublicKey(context.TODO(), &kms.GetPublicKeyInput{ | ||
KeyId: aws.String(a.KeyId), | ||
}) | ||
|
||
var asn1Pubkey asn1EcPublicKey | ||
_, _ = asn1.Unmarshal(pubKeyOutput.PublicKey, &asn1Pubkey) | ||
|
||
ecdsaPubkey := ecdsa.PublicKey{ | ||
X: new(big.Int).SetBytes(asn1Pubkey.PublicKey.Bytes[1 : 1+32]), | ||
Y: new(big.Int).SetBytes(asn1Pubkey.PublicKey.Bytes[1+32:]), | ||
} | ||
|
||
return ecdsaPubkey | ||
} | ||
|
||
func (a *AmazonKms) Verify(sig []byte, hash []byte) (bool, error) { | ||
return true, nil | ||
} | ||
|
||
// https://bitcoin.stackexchange.com/questions/92680/what-are-the-der-signature-and-sec-format | ||
// 1. 0x30 byte: header byte to indicate compound structure | ||
// 2. one byte to encode the length of the following data | ||
// 3. 0x02: header byte indicating an integer | ||
// 4. one byte to encode the length of the following r value | ||
// 5. the r value as a big-endian integer | ||
// 6. 0x02: header byte indicating an integer | ||
// 7. one byte to encode the length of the following s value | ||
// 8. the s value as a big-endian integer | ||
func derSignatureToRS(signature []byte) (rBytes []byte, sBytes []byte) { | ||
var sigAsn1 asn1EcSig | ||
_, err := asn1.Unmarshal(signature, &sigAsn1) | ||
|
||
if err != nil { | ||
panic(err) | ||
pleasew8t marked this conversation as resolved.
Show resolved
Hide resolved
|
||
} | ||
|
||
return sigAsn1.R.Bytes, sigAsn1.S.Bytes | ||
// return rBytes, sBytes | ||
} | ||
|
||
func adjustBufferSize(b []byte) []byte { | ||
pleasew8t marked this conversation as resolved.
Show resolved
Hide resolved
|
||
length := len(b) | ||
|
||
if length == 32 { | ||
return b | ||
} | ||
|
||
if length > 32 { | ||
return b[length-32:] | ||
} | ||
|
||
tmp := make([]byte, 32) | ||
copy(tmp[32-length:], b) | ||
|
||
return tmp | ||
} |
Original file line number | Diff line number | Diff line change | ||||
---|---|---|---|---|---|---|
@@ -0,0 +1,59 @@ | ||||||
package guardiansigner | ||||||
|
||||||
import ( | ||||||
"crypto/ecdsa" | ||||||
"fmt" | ||||||
"time" | ||||||
) | ||||||
|
||||||
type BenchmarkSigner struct { | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Please, document this. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. IIUC this means we can benchmark AWS KMS signer by passing its configuration to a signer of type |
||||||
innerSigner GuardianSigner | ||||||
} | ||||||
|
||||||
func NewBenchmarkSigner(unsafeDevMode bool, signerKeyPath string) (*BenchmarkSigner, error) { | ||||||
innerSigner, err := NewGuardianSignerFromUri(signerKeyPath, unsafeDevMode) | ||||||
|
||||||
if err != nil { | ||||||
return nil, fmt.Errorf("failed to create benchmark signer: %w", err) | ||||||
} | ||||||
|
||||||
return &BenchmarkSigner{ | ||||||
innerSigner: innerSigner, | ||||||
}, nil | ||||||
} | ||||||
|
||||||
func (b *BenchmarkSigner) Sign(hash []byte) ([]byte, error) { | ||||||
|
||||||
start := time.Now() | ||||||
|
||||||
sig, err := b.innerSigner.Sign(hash) | ||||||
|
||||||
duration := time.Since(start) | ||||||
fmt.Printf("Signing execution time: %v\n", duration) | ||||||
|
||||||
return sig, err | ||||||
} | ||||||
|
||||||
func (b *BenchmarkSigner) PublicKey() ecdsa.PublicKey { | ||||||
|
||||||
start := time.Now() | ||||||
|
||||||
pubKey := b.innerSigner.PublicKey() | ||||||
|
||||||
duration := time.Since(start) | ||||||
fmt.Printf("Public key retrieval time: %v\n", duration) | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This could be exposed through a Prometheus histogram. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Here's an example of the definition of an histogram wormhole/node/pkg/processor/processor.go Line 169 in 02f468f
wormhole/node/pkg/processor/processor.go Line 323 in 02f468f
|
||||||
|
||||||
return pubKey | ||||||
} | ||||||
|
||||||
func (b *BenchmarkSigner) Verify(sig []byte, hash []byte) (bool, error) { | ||||||
|
||||||
start := time.Now() | ||||||
|
||||||
valid, err := b.innerSigner.Verify(sig, hash) | ||||||
|
||||||
duration := time.Since(start) | ||||||
fmt.Printf("Signature verification time: %v\n", duration) | ||||||
|
||||||
return valid, err | ||||||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm sorry if I failed to flag this before, but i think it would be ideal if both
Sign
andVerify
were to have acontext.Context
parameter.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do you think I can just pass in the contexts that are available in the calling scope, and create a new one in scopes that don't have an existing context available?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's better than having no Context so I'm good with it.