//! AWS KMS-based Signer use ethers_core::{ k256::ecdsa::{Error as K256Error, Signature as KSig, VerifyingKey}, types::{ transaction::{eip2718::TypedTransaction, eip712::Eip712}, Address, Signature as EthSig, H256, }, utils::hash_message, }; use rusoto_core::RusotoError; use rusoto_kms::{ GetPublicKeyError, GetPublicKeyRequest, Kms, KmsClient, SignError, SignRequest, SignResponse, }; use tracing::{debug, instrument, trace}; mod utils; use utils::{apply_eip155, rsig_to_ethsig, verifying_key_to_address}; /// An ethers Signer that uses keys held in Amazon AWS KMS. /// /// The AWS Signer passes signing requests to the cloud service. AWS KMS keys /// are identified by a UUID, the `key_id`. /// /// Because the public key is unknwon, we retrieve it on instantiation of the /// 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}; /// /// user ethers_signers::Signer; /// /// 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(kms_client, key_id, chain_id).await?; /// let sig = signer.sign_message(H256::zero()).await?; /// ``` #[derive(Clone)] pub struct AwsSigner<'a> { kms: &'a rusoto_kms::KmsClient, chain_id: u64, key_id: String, pubkey: VerifyingKey, address: Address, } impl<'a> std::fmt::Debug for AwsSigner<'a> { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.debug_struct("AwsSigner") .field("key_id", &self.key_id) .field("chain_id", &self.chain_id) .field("pubkey", &hex::encode(self.pubkey.to_bytes())) .field("address", &self.address) .finish() } } impl<'a> std::fmt::Display for AwsSigner<'a> { 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 ) } } /// Errors produced by the AwsSigner #[derive(thiserror::Error, Debug)] pub enum AwsSignerError { #[error("{0}")] SignError(#[from] RusotoError), #[error("{0}")] GetPublicKeyError(#[from] RusotoError), #[error("{0}")] K256(#[from] K256Error), #[error("{0}")] Spki(spki::Error), #[error("{0}")] Other(String), #[error(transparent)] /// Error when converting from a hex string HexError(#[from] hex::FromHexError), /// Error type from Eip712Error message #[error("error encoding eip712 struct: {0:?}")] Eip712Error(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) } } #[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<'a> AwsSigner<'a> { /// Instantiate a new signer from an existing `KmsClient` 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: &'a KmsClient, key_id: T, chain_id: u64, ) -> Result, AwsSignerError> where T: AsRef, { let pubkey = request_get_pubkey(kms, &key_id).await.map(utils::decode_pubkey)??; let address = verifying_key_to_address(&pubkey); debug!( "Instantiated AWS signer with pubkey 0x{} and address 0x{}", hex::encode(&pubkey.to_bytes()), hex::encode(&address) ); Ok(Self { kms, chain_id, key_id: key_id.as_ref().to_owned(), pubkey, address }) } /// 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)? } /// 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( &self, key_id: T, digest: [u8; 32], ) -> Result where T: AsRef, { request_sign_digest(self.kms, key_id, digest).await.map(utils::decode_signature)? } /// Sign a digest with this signer's key pub async fn sign_digest(&self, digest: [u8; 32]) -> Result { self.sign_digest_with_key(self.key_id.clone(), digest).await } /// Sign a digest with this signer's key and add the eip155 `v` value /// corresponding to this signer's chain_id #[instrument(err, skip(digest), fields(digest = %hex::encode(&digest)))] async fn sign_digest_with_eip155(&self, digest: H256) -> Result { let sig = self.sign_digest(digest.into()).await?; let sig = utils::rsig_from_digest_bytes_trial_recovery(&sig, digest.into(), &self.pubkey); let mut sig = rsig_to_ethsig(&sig); apply_eip155(&mut sig, self.chain_id); Ok(sig) } } #[async_trait::async_trait] impl<'a> super::Signer for AwsSigner<'a> { type Error = AwsSignerError; #[instrument(err, skip(message))] async fn sign_message>( &self, message: S, ) -> Result { let message = message.as_ref(); let message_hash = hash_message(message); trace!("{:?}", message_hash); trace!("{:?}", message); self.sign_digest_with_eip155(message_hash).await } #[instrument(err)] async fn sign_transaction(&self, tx: &TypedTransaction) -> Result { let sighash = tx.sighash(); self.sign_digest_with_eip155(sighash).await } async fn sign_typed_data( &self, payload: &T, ) -> Result { let hash = payload.encode_eip712().map_err(|e| Self::Error::Eip712Error(e.to_string()))?; let digest = self.sign_digest_with_eip155(hash.into()).await?; Ok(digest) } fn address(&self) -> Address { 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 } } #[cfg(test)] mod tests { use rusoto_core::{ credential::{EnvironmentProvider, StaticProvider}, Client, HttpClient, Region, }; use tracing::metadata::LevelFilter; use super::*; use crate::Signer; #[allow(dead_code)] fn setup_tracing() { tracing_subscriber::fmt().with_max_level(LevelFilter::DEBUG).try_init().unwrap(); } #[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) } #[tokio::test] async fn it_signs_messages() { let chain_id = 1; let key_id = match std::env::var("AWS_KEY_ID") { Ok(id) => id, _ => return, }; setup_tracing(); 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"); } }