From 13878c343a34175cfc37f4b73d128459d743b18d Mon Sep 17 00:00:00 2001
From: DaniPopes <57450786+DaniPopes@users.noreply.github.com>
Date: Wed, 22 Nov 2023 22:35:42 +0100
Subject: [PATCH] feat(signers): replace `rusoto` with `aws-sdk`
---
ethers-signers/Cargo.toml | 6 +-
ethers-signers/src/aws/mod.rs | 269 +++++++++++++++-----------------
ethers-signers/src/aws/utils.rs | 47 +-----
3 files changed, 129 insertions(+), 193 deletions(-)
diff --git a/ethers-signers/Cargo.toml b/ethers-signers/Cargo.toml
index e074c7169..753ad908a 100644
--- a/ethers-signers/Cargo.toml
+++ b/ethers-signers/Cargo.toml
@@ -43,8 +43,8 @@ futures-util = { workspace = true, optional = true }
futures-executor = { workspace = true, optional = true }
# aws
-rusoto_core = { version = "0.48.0", default-features = false, optional = true }
-rusoto_kms = { version = "0.48.0", default-features = false, optional = true }
+aws-config = { version = "1.0", default-features = false, optional = true }
+aws-sdk-kms = { version = "0.39", default-features = false, optional = true }
spki = { workspace = true, optional = true }
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
@@ -82,5 +82,5 @@ optimism = ["ethers-core/optimism"]
ledger = ["coins-ledger", "futures", "semver"]
trezor = ["trezor-client", "futures", "semver", "home", "protobuf"]
-aws = ["rusoto_core/rustls", "rusoto_kms/rustls", "spki"]
+aws = ["aws-config", "aws-sdk-kms", "spki"]
yubi = ["yubihsm"]
diff --git a/ethers-signers/src/aws/mod.rs b/ethers-signers/src/aws/mod.rs
index fdbca554b..1f70c9b58 100644
--- a/ethers-signers/src/aws/mod.rs
+++ b/ethers-signers/src/aws/mod.rs
@@ -1,5 +1,16 @@
-//! AWS KMS-based Signer
-
+//! AWS KMS-based signer.
+
+use super::Signer;
+use aws_sdk_kms::{
+ error::SdkError,
+ operation::{
+ get_public_key::{GetPublicKeyError, GetPublicKeyOutput},
+ sign::{SignError, SignOutput},
+ },
+ primitives::Blob,
+ types::{MessageType, SigningAlgorithmSpec},
+ Client,
+};
use ethers_core::{
k256::ecdsa::{Error as K256Error, Signature as KSig, VerifyingKey},
types::{
@@ -8,16 +19,12 @@ use ethers_core::{
},
utils::hash_message,
};
-use rusoto_core::RusotoError;
-use rusoto_kms::{
- GetPublicKeyError, GetPublicKeyRequest, Kms, KmsClient, SignError, SignRequest, SignResponse,
-};
+use std::fmt;
use tracing::{debug, instrument, trace};
mod utils;
-use utils::{apply_eip155, verifying_key_to_address};
-/// An ethers Signer that uses keys held in Amazon AWS KMS.
+/// An Ethers signer that uses keys held in Amazon Web Services Key Management Service (AWS KMS).
///
/// The AWS Signer passes signing requests to the cloud service. AWS KMS keys
/// are identified by a UUID, the `key_id`.
@@ -26,34 +33,35 @@ use utils::{apply_eip155, verifying_key_to_address};
/// signer. This means that the new function is `async` and must be called
/// within some runtime.
///
-/// ```compile_fail
-/// use rusoto_core::Client;
-/// use rusoto_kms::{Kms, KmsClient};
+/// ```no_run
+/// # async fn test() {
+/// use aws_config::BehaviorVersion;
+/// use ethers_signers::{AwsSigner, Signer};
///
-/// user ethers_signers::Signer;
+/// let config = aws_config::load_defaults(BehaviorVersion::latest()).await;
+/// let client = aws_sdk_kms::Client::new(&config);
///
-/// let client = Client::new_with(
-/// EnvironmentProvider::default(),
-/// HttpClient::new().unwrap()
-/// );
-/// let kms_client = KmsClient::new_with_client(client, Region::UsWest1);
/// let key_id = "...";
/// let chain_id = 1;
+/// let signer = AwsSigner::new(client, key_id, chain_id).await.unwrap();
+///
+/// let message = vec![0, 1, 2, 3];
///
-/// let signer = AwsSigner::new(kms_client, key_id, chain_id).await?;
-/// let sig = signer.sign_message(H256::zero()).await?;
+/// let sig = signer.sign_message(&message).await.unwrap();
+/// sig.verify(message, signer.address()).expect("valid sig");
+/// # }
/// ```
#[derive(Clone)]
pub struct AwsSigner {
- kms: KmsClient,
+ kms: Client,
chain_id: u64,
key_id: String,
pubkey: VerifyingKey,
address: Address,
}
-impl std::fmt::Debug for AwsSigner {
- fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+impl fmt::Debug for AwsSigner {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("AwsSigner")
.field("key_id", &self.key_id)
.field("chain_id", &self.chain_id)
@@ -63,138 +71,83 @@ impl std::fmt::Debug for AwsSigner {
}
}
-impl std::fmt::Display for AwsSigner {
- fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
- write!(
- f,
- "AwsSigner {{ address: {}, chain_id: {}, key_id: {} }}",
- self.address, self.chain_id, self.key_id
- )
+impl fmt::Display for AwsSigner {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ fmt::Debug::fmt(self, f)
}
}
-/// Errors produced by the AwsSigner
+/// Errors thrown by [`AwsSigner`].
#[derive(thiserror::Error, Debug)]
pub enum AwsSignerError {
- #[error("{0}")]
- SignError(#[from] RusotoError),
- #[error("{0}")]
- GetPublicKeyError(#[from] RusotoError),
- #[error("{0}")]
+ #[error(transparent)]
+ SignError(#[from] SdkError),
+ #[error(transparent)]
+ GetPublicKeyError(#[from] SdkError),
+ #[error(transparent)]
K256(#[from] K256Error),
- #[error("{0}")]
- Spki(spki::Error),
- #[error("{0}")]
- Other(String),
#[error(transparent)]
+ Spki(#[from] spki::Error),
/// Error when converting from a hex string
+ #[error(transparent)]
HexError(#[from] hex::FromHexError),
/// Error type from Eip712Error message
- #[error("error encoding eip712 struct: {0:?}")]
+ #[error("failed encoding eip712 struct: {0:?}")]
Eip712Error(String),
+ #[error("{0}")]
+ Other(String),
}
impl From for AwsSignerError {
- fn from(s: String) -> Self {
- Self::Other(s)
- }
-}
-
-impl From for AwsSignerError {
- fn from(e: spki::Error) -> Self {
- Self::Spki(e)
+ fn from(value: String) -> Self {
+ Self::Other(value)
}
}
-#[instrument(err, skip(kms, key_id), fields(key_id = %key_id.as_ref()))]
-async fn request_get_pubkey(
- kms: &KmsClient,
- key_id: T,
-) -> Result>
-where
- T: AsRef,
-{
- debug!("Dispatching get_public_key");
-
- let req = GetPublicKeyRequest { grant_tokens: None, key_id: key_id.as_ref().to_owned() };
- trace!("{:?}", &req);
- let resp = kms.get_public_key(req).await;
- trace!("{:?}", &resp);
- resp
-}
-
-#[instrument(err, skip(kms, digest, key_id), fields(digest = %hex::encode(digest), key_id = %key_id.as_ref()))]
-async fn request_sign_digest(
- kms: &KmsClient,
- key_id: T,
- digest: [u8; 32],
-) -> Result>
-where
- T: AsRef,
-{
- debug!("Dispatching sign");
- let req = SignRequest {
- grant_tokens: None,
- key_id: key_id.as_ref().to_owned(),
- message: digest.to_vec().into(),
- message_type: Some("DIGEST".to_owned()),
- signing_algorithm: "ECDSA_SHA_256".to_owned(),
- };
- trace!("{:?}", &req);
- let resp = kms.sign(req).await;
- trace!("{:?}", &resp);
- resp
-}
-
impl AwsSigner {
- /// Instantiate a new signer from an existing `KmsClient` and Key ID.
+ /// Instantiate a new signer from an existing `Client` and key ID.
///
/// This function retrieves the public key from AWS and calculates the
/// Etheruem address. It is therefore `async`.
- #[instrument(err, skip(kms, key_id, chain_id), fields(key_id = %key_id.as_ref()))]
- pub async fn new(
- kms: KmsClient,
+ #[instrument(err, skip_all, fields(key_id = %key_id.as_ref()))]
+ pub async fn new>(
+ kms: Client,
key_id: T,
chain_id: u64,
- ) -> Result
- where
- T: AsRef,
- {
- let pubkey = request_get_pubkey(&kms, &key_id).await.map(utils::decode_pubkey)??;
- let address = verifying_key_to_address(&pubkey);
+ ) -> Result {
+ let key_id = key_id.as_ref();
+ let resp = request_get_pubkey(&kms, key_id).await?;
+ let pubkey = decode_pubkey(resp)?;
+ let address = ethers_core::utils::public_key_to_address(&pubkey);
debug!(
- "Instantiated AWS signer with pubkey 0x{} and address 0x{}",
+ "Instantiated AWS signer with pubkey 0x{} and address {address:?}",
hex::encode(pubkey.to_sec1_bytes()),
- hex::encode(address)
);
- Ok(Self { kms, chain_id, key_id: key_id.as_ref().to_owned(), pubkey, address })
+ Ok(Self { kms, chain_id, key_id: key_id.into(), pubkey, address })
}
- /// Fetch the pubkey associated with a key id
+ /// Fetch the pubkey associated with a key ID.
pub async fn get_pubkey_for_key(&self, key_id: T) -> Result
where
T: AsRef,
{
- request_get_pubkey(&self.kms, key_id).await.map(utils::decode_pubkey)?
+ request_get_pubkey(&self.kms, key_id.as_ref()).await.and_then(decode_pubkey)
}
- /// Fetch the pubkey associated with this signer's key ID
+ /// Fetch the pubkey associated with this signer's key ID.
pub async fn get_pubkey(&self) -> Result {
self.get_pubkey_for_key(&self.key_id).await
}
- /// Sign a digest with the key associated with a key id
- pub async fn sign_digest_with_key(
+ /// Sign a digest with the key associated with a key ID.
+ pub async fn sign_digest_with_key>(
&self,
key_id: T,
digest: [u8; 32],
- ) -> Result
- where
- T: AsRef,
- {
- request_sign_digest(&self.kms, key_id, digest).await.map(utils::decode_signature)?
+ ) -> Result {
+ request_sign_digest(&self.kms, key_id.as_ref(), digest).await.and_then(decode_signature)
}
/// Sign a digest with this signer's key
@@ -213,13 +166,13 @@ impl AwsSigner {
let sig = self.sign_digest(digest.into()).await?;
let mut sig =
utils::sig_from_digest_bytes_trial_recovery(&sig, digest.into(), &self.pubkey);
- apply_eip155(&mut sig, chain_id);
+ utils::apply_eip155(&mut sig, chain_id);
Ok(sig)
}
}
#[async_trait::async_trait]
-impl super::Signer for AwsSigner {
+impl Signer for AwsSigner {
type Error = AwsSignerError;
#[instrument(err, skip(message))]
@@ -229,8 +182,7 @@ impl super::Signer for AwsSigner {
) -> Result {
let message = message.as_ref();
let message_hash = hash_message(message);
- trace!("{:?}", message_hash);
- trace!("{:?}", message);
+ trace!(?message_hash, ?message);
self.sign_digest_with_eip155(message_hash, self.chain_id).await
}
@@ -262,58 +214,81 @@ impl super::Signer for AwsSigner {
self.address
}
- /// Returns the signer's chain id
fn chain_id(&self) -> u64 {
self.chain_id
}
- /// Sets the signer's chain id
fn with_chain_id>(mut self, chain_id: T) -> Self {
self.chain_id = chain_id.into();
self
}
}
+#[instrument(err, skip(kms))]
+async fn request_get_pubkey(
+ kms: &Client,
+ key_id: &str,
+) -> Result {
+ kms.get_public_key().key_id(key_id).send().await.map_err(Into::into)
+}
+
+#[instrument(err, skip(kms, digest), fields(digest = %hex::encode(digest)))]
+async fn request_sign_digest(
+ kms: &Client,
+ key_id: &str,
+ digest: [u8; 32],
+) -> Result {
+ kms.sign()
+ .key_id(key_id)
+ .message(Blob::new(digest))
+ .message_type(MessageType::Digest)
+ .signing_algorithm(SigningAlgorithmSpec::EcdsaSha256)
+ .send()
+ .await
+ .map_err(Into::into)
+}
+
+/// Decode an AWS KMS Pubkey response.
+fn decode_pubkey(resp: GetPublicKeyOutput) -> Result {
+ let raw = resp
+ .public_key
+ .as_ref()
+ .ok_or_else(|| AwsSignerError::from("Pubkey not found in response".to_owned()))?;
+
+ let spki = spki::SubjectPublicKeyInfoRef::try_from(raw.as_ref())?;
+ let key = VerifyingKey::from_sec1_bytes(spki.subject_public_key.raw_bytes())?;
+
+ Ok(key)
+}
+
+/// Decode an AWS KMS Signature response.
+fn decode_signature(resp: SignOutput) -> Result {
+ let raw = resp
+ .signature
+ .as_ref()
+ .ok_or_else(|| AwsSignerError::from("Signature not found in response".to_owned()))?;
+
+ let sig = KSig::from_der(raw.as_ref())?;
+ Ok(sig.normalize_s().unwrap_or(sig))
+}
+
#[cfg(test)]
mod tests {
use super::*;
- use crate::Signer;
- use rusoto_core::{
- credential::{EnvironmentProvider, StaticProvider},
- Client, HttpClient, Region,
- };
-
- #[allow(dead_code)]
- fn static_client() -> KmsClient {
- let access_key = "".to_owned();
- let secret_access_key = "".to_owned();
-
- let client = Client::new_with(
- StaticProvider::new(access_key, secret_access_key, None, None),
- HttpClient::new().unwrap(),
- );
- KmsClient::new_with_client(client, Region::UsWest1)
- }
-
- #[allow(dead_code)]
- fn env_client() -> KmsClient {
- let client = Client::new_with(EnvironmentProvider::default(), HttpClient::new().unwrap());
- KmsClient::new_with_client(client, Region::UsWest1)
- }
+ use aws_config::BehaviorVersion;
#[tokio::test]
- async fn it_signs_messages() {
+ async fn sign_message() {
+ let Ok(key_id) = std::env::var("AWS_KEY_ID") else { return };
+ let config = aws_config::load_defaults(BehaviorVersion::latest()).await;
+ let client = aws_sdk_kms::Client::new(&config);
+
let chain_id = 1;
- let key_id = match std::env::var("AWS_KEY_ID") {
- Ok(id) => id,
- _ => return,
- };
- let client = env_client();
let signer = AwsSigner::new(client, key_id, chain_id).await.unwrap();
let message = vec![0, 1, 2, 3];
let sig = signer.sign_message(&message).await.unwrap();
- sig.verify(message, signer.address).expect("valid sig");
+ sig.verify(message, signer.address()).expect("valid sig");
}
}
diff --git a/ethers-signers/src/aws/utils.rs b/ethers-signers/src/aws/utils.rs
index e15f8f7c7..b649ab811 100644
--- a/ethers-signers/src/aws/utils.rs
+++ b/ethers-signers/src/aws/utils.rs
@@ -2,22 +2,15 @@
//! within this module. They DO NOT perform basic safety checks and may panic
//! if used incorrectly.
-use std::convert::TryFrom;
-
use ethers_core::{
k256::{
ecdsa::{RecoveryId, Signature as RSig, Signature as KSig, VerifyingKey},
FieldBytes,
},
- types::{Address, Signature as EthSig, U256},
- utils::keccak256,
+ types::{Signature as EthSig, U256},
};
-use rusoto_kms::{GetPublicKeyResponse, SignResponse};
-
-use crate::aws::AwsSignerError;
-/// Makes a trial recovery to check whether an RSig corresponds to a known
-/// `VerifyingKey`
+/// Makes a trial recovery to check whether an RSig corresponds to a known `VerifyingKey`.
fn check_candidate(
sig: &RSig,
recovery_id: RecoveryId,
@@ -29,7 +22,7 @@ fn check_candidate(
.unwrap_or(false)
}
-/// Recover an rsig from a signature under a known key by trial/error
+/// Recover an rsig from a signature under a known key by trial/error.
pub(super) fn sig_from_digest_bytes_trial_recovery(
sig: &KSig,
digest: [u8; 32],
@@ -49,40 +42,8 @@ pub(super) fn sig_from_digest_bytes_trial_recovery(
}
}
-/// Modify the v value of a signature to conform to eip155
+/// Modify the `v` value of a signature to conform to EIP-155.
pub(super) fn apply_eip155(sig: &mut EthSig, chain_id: u64) {
let v = (chain_id * 2 + 35) + sig.v;
sig.v = v;
}
-
-/// Convert a verifying key to an ethereum address
-pub(super) fn verifying_key_to_address(key: &VerifyingKey) -> Address {
- // false for uncompressed
- let uncompressed_pub_key = key.to_encoded_point(false);
- let public_key = uncompressed_pub_key.to_bytes();
- debug_assert_eq!(public_key[0], 0x04);
- let hash = keccak256(&public_key[1..]);
- Address::from_slice(&hash[12..])
-}
-
-/// Decode an AWS KMS Pubkey response
-pub(super) fn decode_pubkey(resp: GetPublicKeyResponse) -> Result {
- let raw = resp
- .public_key
- .ok_or_else(|| AwsSignerError::from("Pubkey not found in response".to_owned()))?;
-
- let spki = spki::SubjectPublicKeyInfoRef::try_from(raw.as_ref())?;
- let key = VerifyingKey::from_sec1_bytes(spki.subject_public_key.raw_bytes())?;
-
- Ok(key)
-}
-
-/// Decode an AWS KMS Signature response
-pub(super) fn decode_signature(resp: SignResponse) -> Result {
- let raw = resp
- .signature
- .ok_or_else(|| AwsSignerError::from("Signature not found in response".to_owned()))?;
-
- let sig = KSig::from_der(&raw)?;
- Ok(sig.normalize_s().unwrap_or(sig))
-}