-
Notifications
You must be signed in to change notification settings - Fork 723
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
pleasew8t
committed
Nov 22, 2024
1 parent
753386a
commit 44d29e7
Showing
3 changed files
with
385 additions
and
9 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,250 @@ | ||
package guardiansigner | ||
|
||
import ( | ||
"bytes" | ||
"context" | ||
"crypto/ecdsa" | ||
"encoding/asn1" | ||
"errors" | ||
"fmt" | ||
"math/big" | ||
"strings" | ||
"time" | ||
|
||
"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)) | ||
|
||
// The timeout for KMS operations. This is necessary to avoid situations where | ||
// the signing or verification is blocked indefinitely. | ||
KMS_TIMEOUT = time.Second * 15 | ||
) | ||
|
||
// The ASN.1 structure for an ECDSA signature produced by AWS KMS. | ||
type asn1EcSig struct { | ||
R asn1.RawValue | ||
S asn1.RawValue | ||
} | ||
|
||
// The ASN.1 structure for an ECDSA public key produced by AWS KMS. | ||
type asn1EcPublicKey struct { | ||
EcPublicKeyInfo asn1EcPublicKeyInfo | ||
PublicKey asn1.BitString | ||
} | ||
|
||
// The ASN.1 structure for the public key info in an ECDSA public key produced by AWS KMS. | ||
type asn1EcPublicKeyInfo struct { | ||
Algorithm asn1.ObjectIdentifier | ||
Parameters asn1.ObjectIdentifier | ||
} | ||
|
||
// getRegionFromArn extracts the region from an ARN. The region is at index 3 in the ARN. | ||
func getRegionFromArn(arn string) string { | ||
// Information in ARNs are colon-separated | ||
arn_parts := strings.Split(arn, ":") | ||
|
||
// https://docs.aws.amazon.com/IAM/latest/UserGuide/reference-arns.html#arns-syntax | ||
// The format of an ARN is arn:partition:service:region:account-id:resource-info, so | ||
// the region is at index 3. | ||
if len(arn) < 4 { | ||
return "" | ||
} | ||
|
||
return arn_parts[3] | ||
} | ||
|
||
// AmazonKms is a signer that uses AWS KMS to sign messages. The URI is expected to be | ||
// in the format amazonkms://<key-arn>. | ||
type AmazonKms struct { | ||
keyId string | ||
region string | ||
publicKey ecdsa.PublicKey | ||
client *kms.Client | ||
} | ||
|
||
// NewAmazonKmsSigner creates a new AmazonKms signer. The keyPath is expected to be an ARN, | ||
// identifying the key in AWS KMS. The region is extracted from the ARN, and the AWS KMS | ||
// client is created with the region. | ||
// NOTE: The public key is retrieved during signer creation, and stored as a property of the | ||
// signer. This is because the public key is not expected to change during runtime. | ||
func NewAmazonKmsSigner(ctx context.Context, unsafeDevMode bool, keyPath string) (*AmazonKms, error) { | ||
timeoutCtx, cancel := context.WithTimeout(ctx, KMS_TIMEOUT) | ||
defer cancel() | ||
|
||
// Extract the region from the key path. The region is required to create a new KMS client. | ||
// If the region is not present in the key path, the ARN is considered invalid. | ||
region := getRegionFromArn(keyPath) | ||
|
||
if region == "" { | ||
return nil, errors.New("Invalid KMS ARN") | ||
} | ||
|
||
amazonKmsSigner := AmazonKms{ | ||
keyId: keyPath, | ||
region: getRegionFromArn(keyPath), | ||
} | ||
|
||
// Create a configuration object to create a new KMS client from. The region passed to | ||
// `config.WithDefaultRegion()` must match the region in the actual ARN, otherwise the SDK throws | ||
// an error. This is why the region is first extracted from the keyPath. | ||
cfg, err := config.LoadDefaultConfig(timeoutCtx, config.WithDefaultRegion(amazonKmsSigner.region)) | ||
if err != nil { | ||
return nil, errors.New("Failed to load default config") | ||
} | ||
|
||
amazonKmsSigner.client = kms.NewFromConfig(cfg) | ||
|
||
// Get the public key here, and store it as a property. The public key shouldn't change during | ||
// runtime, so it's safe to fetch once and store it as a property. | ||
pubKeyOutput, err := amazonKmsSigner.client.GetPublicKey(timeoutCtx, &kms.GetPublicKeyInput{ | ||
KeyId: aws.String(amazonKmsSigner.keyId), | ||
}) | ||
|
||
if err != nil { | ||
return nil, fmt.Errorf("KMS signer creation failed: %w", err) | ||
} | ||
|
||
var asn1Pubkey asn1EcPublicKey | ||
_, err = asn1.Unmarshal(pubKeyOutput.PublicKey, &asn1Pubkey) | ||
|
||
if err != nil { | ||
return nil, fmt.Errorf("Failed to unmarshal KMS public key: %w", err) | ||
} | ||
|
||
ecdsaPubkey := ecdsa.PublicKey{ | ||
X: new(big.Int).SetBytes(asn1Pubkey.PublicKey.Bytes[1 : 1+32]), | ||
Y: new(big.Int).SetBytes(asn1Pubkey.PublicKey.Bytes[1+32:]), | ||
} | ||
|
||
amazonKmsSigner.publicKey = ecdsaPubkey | ||
|
||
return &amazonKmsSigner, nil | ||
} | ||
|
||
func (a *AmazonKms) Sign(ctx context.Context, hash []byte) (signature []byte, err error) { | ||
timeoutCtx, cancel := context.WithTimeout(ctx, KMS_TIMEOUT) | ||
defer cancel() | ||
|
||
// Call the AWS KMS service to sign the input hash. | ||
res, err := a.client.Sign(timeoutCtx, &kms.SignInput{ | ||
KeyId: aws.String(a.keyId), | ||
Message: hash, | ||
SigningAlgorithm: kms_types.SigningAlgorithmSpecEcdsaSha256, | ||
MessageType: kms_types.MessageTypeDigest, | ||
}) | ||
|
||
if err != nil { | ||
return nil, fmt.Errorf("KMS Signing failed: %w", err) | ||
} | ||
|
||
// Decode r and s values | ||
r, s, err := derSignatureToRS(res.Signature) | ||
|
||
if err != nil { | ||
return nil, fmt.Errorf("Failed to decode signature: %w", err) | ||
} | ||
|
||
// 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(ctx) | ||
signature = append(r, s...) | ||
|
||
// try recovery id 0 | ||
ecSigWithRecid := append(signature, []byte{0}...) | ||
pubkey, _ := ethcrypto.SigToPub(hash[:], ecSigWithRecid) | ||
|
||
if bytes.Equal(ethcrypto.CompressPubkey(pubkey), ethcrypto.CompressPubkey(&expectedPublicKey)) { | ||
return ecSigWithRecid, nil | ||
} | ||
|
||
ecSigWithRecid = append(signature, []byte{1}...) | ||
pubkey, _ = ethcrypto.SigToPub(hash[:], ecSigWithRecid) | ||
|
||
// try recovery id 1 | ||
if bytes.Equal(ethcrypto.CompressPubkey(pubkey), ethcrypto.CompressPubkey(&expectedPublicKey)) { | ||
return ecSigWithRecid, nil | ||
} | ||
|
||
// Reaching this return implies that it wasn't possible to generate a valid signature. This shouldn't | ||
// happen, unless there is something seriously wrong with the KMS service. | ||
return nil, fmt.Errorf("Failed to generate valid signature") | ||
} | ||
|
||
func (a *AmazonKms) PublicKey(ctx context.Context) ecdsa.PublicKey { | ||
return a.publicKey | ||
} | ||
|
||
func (a *AmazonKms) Verify(ctx context.Context, sig []byte, hash []byte) (bool, error) { | ||
timeoutCtx, cancel := context.WithTimeout(ctx, time.Second*15) | ||
defer cancel() | ||
|
||
// Use ethcrypto to recover the public key | ||
recoveredPubKey, err := ethcrypto.SigToPub(hash, sig) | ||
|
||
if err != nil { | ||
return false, err | ||
} | ||
|
||
// Load the KMS signer's public key | ||
kmsPublicKey := a.PublicKey(timeoutCtx) | ||
|
||
return recoveredPubKey.Equal(kmsPublicKey), 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) ([]byte, []byte, error) { | ||
var sigAsn1 asn1EcSig | ||
_, err := asn1.Unmarshal(signature, &sigAsn1) | ||
|
||
if err != nil { | ||
return nil, nil, err | ||
} | ||
|
||
return sigAsn1.R.Bytes, sigAsn1.S.Bytes, nil | ||
} | ||
|
||
// adjustBufferSize takes an input buffer and | ||
// a) trims it down to 32 bytes, if the input length is greater than 32, or | ||
// b) returns the input as-is, if the input length is equal to 32, or | ||
// c) left-pads it to 32 bytes, if the input length is less than 32. | ||
func adjustBufferSize(b []byte) []byte { | ||
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 | ||
} |
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,78 @@ | ||
package guardiansigner | ||
|
||
/* | ||
The Benchmark signer is a type of signer that wraps other signers, | ||
recording the latency of signing and signature verification into | ||
histograms. As additional signers are implemented, relying on 3rd | ||
party services, benchmarking signers is useful to ensure observation | ||
signing happens at an acceptable rate. | ||
*/ | ||
|
||
import ( | ||
"context" | ||
"crypto/ecdsa" | ||
"time" | ||
|
||
"github.com/prometheus/client_golang/prometheus" | ||
"github.com/prometheus/client_golang/prometheus/promauto" | ||
) | ||
|
||
// The BenchmarkSigner is a signer that wraps other signers, recording the latency of | ||
// signing and signature verification through prometheus histograms. | ||
type BenchmarkSigner struct { | ||
innerSigner GuardianSigner | ||
} | ||
|
||
var ( | ||
guardianSignerSigningLatency = promauto.NewHistogram( | ||
prometheus.HistogramOpts{ | ||
Name: "wormhole_guardian_signer_signing_latency_us", | ||
Help: "Latency histogram for Guardian signing requests", | ||
Buckets: []float64{10.0, 20.0, 50.0, 100.0, 1000.0, 5000.0, 10000.0, 100_000.0, 1_000_000.0, 10_000_000.0, 100_000_000.0, 1_000_000_000.0}, | ||
}) | ||
|
||
guardianSignerVerifyLatency = promauto.NewHistogram( | ||
prometheus.HistogramOpts{ | ||
Name: "wormhole_guardian_signer_sig_verify_latency_us", | ||
Help: "Latency histogram for Guardian signature verification requests", | ||
Buckets: []float64{10.0, 20.0, 50.0, 100.0, 1000.0, 5000.0, 10000.0, 100_000.0, 1_000_000.0, 10_000_000.0, 100_000_000.0, 1_000_000_000.0}, | ||
}) | ||
) | ||
|
||
func BenchmarkWrappedSigner(innerSigner GuardianSigner) *BenchmarkSigner { | ||
if innerSigner == nil { | ||
return nil | ||
} | ||
|
||
return &BenchmarkSigner{ | ||
innerSigner: innerSigner, | ||
} | ||
} | ||
|
||
func (b *BenchmarkSigner) Sign(ctx context.Context, hash []byte) ([]byte, error) { | ||
start := time.Now() | ||
sig, err := b.innerSigner.Sign(ctx, hash) | ||
duration := time.Since(start) | ||
|
||
// Add Observation to histogram | ||
guardianSignerSigningLatency.Observe(float64(duration.Microseconds())) | ||
|
||
return sig, err | ||
} | ||
|
||
func (b *BenchmarkSigner) PublicKey(ctx context.Context) ecdsa.PublicKey { | ||
pubKey := b.innerSigner.PublicKey(ctx) | ||
return pubKey | ||
} | ||
|
||
func (b *BenchmarkSigner) Verify(ctx context.Context, sig []byte, hash []byte) (bool, error) { | ||
|
||
start := time.Now() | ||
valid, err := b.innerSigner.Verify(ctx, sig, hash) | ||
duration := time.Since(start) | ||
|
||
// Add observation to histogram | ||
guardianSignerVerifyLatency.Observe(float64(duration.Microseconds())) | ||
|
||
return valid, err | ||
} |
Oops, something went wrong.