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)) -}