feature: signer using aws kms (#358)
* feature: signer using aws kms * docs: add basic docstrings to all methods * lint: cargo fmt
This commit is contained in:
parent
9fc142ca61
commit
5382b5bdfb
|
@ -66,6 +66,15 @@ dependencies = [
|
|||
"backtrace",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ansi_term"
|
||||
version = "0.12.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d52a9bb7ec0cf484c551830a7ce27bd20d67eac647e1befb56b0be4ee39a55d2"
|
||||
dependencies = [
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anyhow"
|
||||
version = "1.0.41"
|
||||
|
@ -498,6 +507,15 @@ dependencies = [
|
|||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crc32fast"
|
||||
version = "1.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "81156fece84ab6a9f2afdb109ce3ae577e42b1228441eded99bd77f627953b1a"
|
||||
dependencies = [
|
||||
"cfg-if 1.0.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crunchy"
|
||||
version = "0.2.2"
|
||||
|
@ -595,6 +613,27 @@ dependencies = [
|
|||
"generic-array 0.14.4",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "dirs-next"
|
||||
version = "2.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b98cf8ebf19c3d1b223e151f99a4f9f0690dca41414773390fc824184ac833e1"
|
||||
dependencies = [
|
||||
"cfg-if 1.0.0",
|
||||
"dirs-sys-next",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "dirs-sys-next"
|
||||
version = "0.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4ebda144c4fe02d1f7ea1a7d9641b6fc6b580adcfa024ae48797ecdeb6825b4d"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"redox_users",
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ecdsa"
|
||||
version = "0.12.3"
|
||||
|
@ -888,10 +927,16 @@ dependencies = [
|
|||
"futures-util",
|
||||
"hex",
|
||||
"rand 0.8.4",
|
||||
"rusoto_core",
|
||||
"rusoto_kms",
|
||||
"sha2 0.9.5",
|
||||
"spki",
|
||||
"tempfile",
|
||||
"thiserror",
|
||||
"tokio",
|
||||
"tracing",
|
||||
"tracing-futures",
|
||||
"tracing-subscriber",
|
||||
"yubihsm",
|
||||
]
|
||||
|
||||
|
@ -1417,12 +1462,32 @@ dependencies = [
|
|||
"cfg-if 1.0.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "matchers"
|
||||
version = "0.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f099785f7595cc4b4553a174ce30dd7589ef93391ff414dbb67f62392b9e0ce1"
|
||||
dependencies = [
|
||||
"regex-automata",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "matches"
|
||||
version = "0.1.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7ffc5c5338469d4d3ea17d269fa8ea3512ad247247c30bd2df69e68309ed0a08"
|
||||
|
||||
[[package]]
|
||||
name = "md-5"
|
||||
version = "0.9.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7b5a279bb9607f9f53c22d496eade00d138d1bdcccd07d74650387cf94942a15"
|
||||
dependencies = [
|
||||
"block-buffer 0.9.0",
|
||||
"digest 0.9.0",
|
||||
"opaque-debug 0.3.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "memchr"
|
||||
version = "2.4.0"
|
||||
|
@ -1906,6 +1971,16 @@ dependencies = [
|
|||
"bitflags",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "redox_users"
|
||||
version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "528532f3d801c87aec9def2add9ca802fe569e44a544afe633765267840abe64"
|
||||
dependencies = [
|
||||
"getrandom 0.2.3",
|
||||
"redox_syscall",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "regex"
|
||||
version = "1.5.4"
|
||||
|
@ -1917,6 +1992,15 @@ dependencies = [
|
|||
"regex-syntax",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "regex-automata"
|
||||
version = "0.1.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132"
|
||||
dependencies = [
|
||||
"regex-syntax",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "regex-syntax"
|
||||
version = "0.6.25"
|
||||
|
@ -2017,6 +2101,89 @@ dependencies = [
|
|||
"libusb1-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rusoto_core"
|
||||
version = "0.47.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5b4f000e8934c1b4f70adde180056812e7ea6b1a247952db8ee98c94cd3116cc"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"base64 0.13.0",
|
||||
"bytes",
|
||||
"crc32fast",
|
||||
"futures",
|
||||
"http",
|
||||
"hyper",
|
||||
"hyper-tls",
|
||||
"lazy_static",
|
||||
"log",
|
||||
"rusoto_credential",
|
||||
"rusoto_signature",
|
||||
"rustc_version",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"tokio",
|
||||
"xml-rs",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rusoto_credential"
|
||||
version = "0.47.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6a46b67db7bb66f5541e44db22b0a02fed59c9603e146db3a9e633272d3bac2f"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"chrono",
|
||||
"dirs-next",
|
||||
"futures",
|
||||
"hyper",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"shlex",
|
||||
"tokio",
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rusoto_kms"
|
||||
version = "0.47.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d7892cd2cca7644d33bd6fafdb2236efd3659162fd7b73ca68d3877f0528399c"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"bytes",
|
||||
"futures",
|
||||
"rusoto_core",
|
||||
"serde",
|
||||
"serde_json",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rusoto_signature"
|
||||
version = "0.47.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6264e93384b90a747758bcc82079711eacf2e755c3a8b5091687b5349d870bcc"
|
||||
dependencies = [
|
||||
"base64 0.13.0",
|
||||
"bytes",
|
||||
"chrono",
|
||||
"digest 0.9.0",
|
||||
"futures",
|
||||
"hex",
|
||||
"hmac",
|
||||
"http",
|
||||
"hyper",
|
||||
"log",
|
||||
"md-5",
|
||||
"percent-encoding",
|
||||
"pin-project-lite",
|
||||
"rusoto_credential",
|
||||
"rustc_version",
|
||||
"serde",
|
||||
"sha2 0.9.5",
|
||||
"tokio",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustc-demangle"
|
||||
version = "0.1.20"
|
||||
|
@ -2029,6 +2196,15 @@ version = "2.1.0"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3e75f6a532d0fd9f7f13144f392b6ad56a32696bfcd9c78f797f16bbb6f072d6"
|
||||
|
||||
[[package]]
|
||||
name = "rustc_version"
|
||||
version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bfa0f585226d2e68097d4f95d113b15b83a82e819ab25717ec0590d9584ef366"
|
||||
dependencies = [
|
||||
"semver",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustls"
|
||||
version = "0.19.1"
|
||||
|
@ -2126,6 +2302,12 @@ dependencies = [
|
|||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "semver"
|
||||
version = "1.0.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "568a8e6258aa33c13358f81fd834adb854c6f7c9468520910a9b1e8fac068012"
|
||||
|
||||
[[package]]
|
||||
name = "serde"
|
||||
version = "1.0.126"
|
||||
|
@ -2229,6 +2411,30 @@ dependencies = [
|
|||
"opaque-debug 0.3.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sharded-slab"
|
||||
version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "79c719719ee05df97490f80a45acfc99e5a30ce98a1e4fb67aee422745ae14e3"
|
||||
dependencies = [
|
||||
"lazy_static",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "shlex"
|
||||
version = "1.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "42a568c8f2cd051a4d283bd6eb0343ac214c1b0f1ac19f93e1175b2dee38c73d"
|
||||
|
||||
[[package]]
|
||||
name = "signal-hook-registry"
|
||||
version = "1.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e51e73328dc4ac0c7ccbda3a494dfa03df1de2f46018127f60c693f2648455b0"
|
||||
dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "signature"
|
||||
version = "1.3.1"
|
||||
|
@ -2258,6 +2464,12 @@ version = "0.4.3"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f173ac3d1a7e3b28003f40de0b5ce7fe2710f9b9dc3fc38664cebee46b3b6527"
|
||||
|
||||
[[package]]
|
||||
name = "smallvec"
|
||||
version = "1.6.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fe0f37c9e8f3c5a4a66ad655a93c74daac4ad00c441533bf5c6e7990bb42604e"
|
||||
|
||||
[[package]]
|
||||
name = "socket2"
|
||||
version = "0.4.0"
|
||||
|
@ -2358,6 +2570,15 @@ dependencies = [
|
|||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "thread_local"
|
||||
version = "1.1.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8018d24e04c95ac8790716a5987d0fec4f8b27249ffa0f7d33f1369bdfb88cbd"
|
||||
dependencies = [
|
||||
"once_cell",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "time"
|
||||
version = "0.1.43"
|
||||
|
@ -2404,7 +2625,9 @@ dependencies = [
|
|||
"memchr",
|
||||
"mio",
|
||||
"num_cpus",
|
||||
"once_cell",
|
||||
"pin-project-lite",
|
||||
"signal-hook-registry",
|
||||
"tokio-macros",
|
||||
"winapi",
|
||||
]
|
||||
|
@ -2497,9 +2720,21 @@ checksum = "09adeb8c97449311ccd28a427f96fb563e7fd31aabf994189879d9da2394b89d"
|
|||
dependencies = [
|
||||
"cfg-if 1.0.0",
|
||||
"pin-project-lite",
|
||||
"tracing-attributes",
|
||||
"tracing-core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tracing-attributes"
|
||||
version = "0.1.15"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c42e6fa53307c8a17e4ccd4dc81cf5ec38db9209f59b222210375b54ee40d1e2"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tracing-core"
|
||||
version = "0.1.18"
|
||||
|
@ -2519,6 +2754,49 @@ dependencies = [
|
|||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tracing-log"
|
||||
version = "0.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a6923477a48e41c1951f1999ef8bb5a3023eb723ceadafe78ffb65dc366761e3"
|
||||
dependencies = [
|
||||
"lazy_static",
|
||||
"log",
|
||||
"tracing-core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tracing-serde"
|
||||
version = "0.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fb65ea441fbb84f9f6748fd496cf7f63ec9af5bca94dd86456978d055e8eb28b"
|
||||
dependencies = [
|
||||
"serde",
|
||||
"tracing-core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tracing-subscriber"
|
||||
version = "0.2.19"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ab69019741fca4d98be3c62d2b75254528b5432233fd8a4d2739fec20278de48"
|
||||
dependencies = [
|
||||
"ansi_term",
|
||||
"chrono",
|
||||
"lazy_static",
|
||||
"matchers",
|
||||
"regex",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"sharded-slab",
|
||||
"smallvec",
|
||||
"thread_local",
|
||||
"tracing",
|
||||
"tracing-core",
|
||||
"tracing-log",
|
||||
"tracing-serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "try-lock"
|
||||
version = "0.2.3"
|
||||
|
@ -2798,6 +3076,12 @@ version = "0.2.0"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "85e60b0d1b5f99db2556934e21937020776a5d31520bf169e851ac44e6420214"
|
||||
|
||||
[[package]]
|
||||
name = "xml-rs"
|
||||
version = "0.8.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d2d7d3948613f75c98fd9328cfdcc45acc4d360655289d0a7d4ec931392200a3"
|
||||
|
||||
[[package]]
|
||||
name = "yubihsm"
|
||||
version = "0.39.0"
|
||||
|
|
|
@ -29,6 +29,13 @@ yubihsm = { version = "0.39.0", features = ["secp256k1", "http", "usb"], optiona
|
|||
futures-util = "0.3.16"
|
||||
futures-executor = "0.3.16"
|
||||
|
||||
# aws
|
||||
rusoto_core = { version = "0.47.0", optional = true }
|
||||
rusoto_kms = { version = "0.47.0", optional = true }
|
||||
tracing = { version = "0.1.26", optional = true }
|
||||
tracing-futures = { version = "0.2.5", optional = true }
|
||||
spki = { version = "0.4.0", optional = true }
|
||||
|
||||
[dev-dependencies]
|
||||
ethers = { version = "0.4.0", path = "../ethers" }
|
||||
yubihsm = { version = "0.39.0", features = ["secp256k1", "usb", "mockhsm"] }
|
||||
|
@ -36,7 +43,10 @@ yubihsm = { version = "0.39.0", features = ["secp256k1", "usb", "mockhsm"] }
|
|||
tempfile = "3.2.0"
|
||||
tokio = { version = "1.5", default-features = false, features = ["macros"] }
|
||||
|
||||
tracing-subscriber = "0.2.19"
|
||||
|
||||
[features]
|
||||
celo = ["ethers-core/celo"]
|
||||
ledger = ["coins-ledger"]
|
||||
yubi = ["yubihsm"]
|
||||
aws = ["rusoto_core", "rusoto_kms", "tracing", "tracing-futures", "spki"]
|
||||
|
|
|
@ -0,0 +1,314 @@
|
|||
//! AWS KMS-based Signer
|
||||
|
||||
use ethers_core::{
|
||||
k256::ecdsa::{Error as K256Error, Signature as KSig, VerifyingKey},
|
||||
types::{Address, Signature as EthSig, TransactionRequest, 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<SignError>),
|
||||
#[error("{0}")]
|
||||
GetPublicKeyError(#[from] RusotoError<GetPublicKeyError>),
|
||||
#[error("{0}")]
|
||||
K256(#[from] K256Error),
|
||||
#[error("{0}")]
|
||||
Spki(spki::der::Error),
|
||||
#[error("{0}")]
|
||||
Other(String),
|
||||
}
|
||||
|
||||
impl From<String> for AwsSignerError {
|
||||
fn from(s: String) -> Self {
|
||||
Self::Other(s)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<spki::der::Error> for AwsSignerError {
|
||||
fn from(e: spki::der::Error) -> Self {
|
||||
Self::Spki(e)
|
||||
}
|
||||
}
|
||||
|
||||
#[instrument(err, skip(kms, key_id), fields(key_id = %key_id.as_ref()))]
|
||||
async fn request_get_pubkey<T>(
|
||||
kms: &KmsClient,
|
||||
key_id: T,
|
||||
) -> Result<rusoto_kms::GetPublicKeyResponse, RusotoError<GetPublicKeyError>>
|
||||
where
|
||||
T: AsRef<str>,
|
||||
{
|
||||
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<T>(
|
||||
kms: &KmsClient,
|
||||
key_id: T,
|
||||
digest: [u8; 32],
|
||||
) -> Result<SignResponse, RusotoError<SignError>>
|
||||
where
|
||||
T: AsRef<str>,
|
||||
{
|
||||
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<T>(
|
||||
kms: &'a KmsClient,
|
||||
key_id: T,
|
||||
chain_id: u64,
|
||||
) -> Result<AwsSigner<'a>, AwsSignerError>
|
||||
where
|
||||
T: AsRef<str>,
|
||||
{
|
||||
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<T>(&self, key_id: T) -> Result<VerifyingKey, AwsSignerError>
|
||||
where
|
||||
T: AsRef<str>,
|
||||
{
|
||||
Ok(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<VerifyingKey, AwsSignerError> {
|
||||
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<T>(
|
||||
&self,
|
||||
key_id: T,
|
||||
digest: [u8; 32],
|
||||
) -> Result<KSig, AwsSignerError>
|
||||
where
|
||||
T: AsRef<str>,
|
||||
{
|
||||
Ok(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<KSig, AwsSignerError> {
|
||||
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<EthSig, AwsSignerError> {
|
||||
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<S: Send + Sync + AsRef<[u8]>>(
|
||||
&self,
|
||||
message: S,
|
||||
) -> Result<EthSig, Self::Error> {
|
||||
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: &TransactionRequest) -> Result<EthSig, Self::Error> {
|
||||
let sighash = tx.sighash(self.chain_id);
|
||||
self.sign_digest_with_eip155(sighash).await
|
||||
}
|
||||
|
||||
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<T: Into<u64>>(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 = std::env::var("AWS_KEY_ID").expect("no key id");
|
||||
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");
|
||||
}
|
||||
}
|
|
@ -0,0 +1,99 @@
|
|||
//! These utils are NOT meant for general usage. They are ONLY meant for use
|
||||
//! 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::{
|
||||
recoverable::{Id, Signature as RSig},
|
||||
Signature as KSig, VerifyingKey,
|
||||
},
|
||||
elliptic_curve::sec1::ToEncodedPoint,
|
||||
FieldBytes,
|
||||
},
|
||||
types::{Address, Signature as EthSig, H256},
|
||||
utils::keccak256,
|
||||
};
|
||||
use rusoto_kms::{GetPublicKeyResponse, SignResponse};
|
||||
|
||||
use crate::aws::AwsSignerError;
|
||||
|
||||
/// Converts a recoverable signature to an ethers signature
|
||||
pub(super) fn rsig_to_ethsig(sig: &RSig) -> EthSig {
|
||||
let v: u8 = sig.recovery_id().into();
|
||||
let v = (v + 27) as u64;
|
||||
let r_bytes: FieldBytes = sig.r().into();
|
||||
let s_bytes: FieldBytes = sig.s().into();
|
||||
let r = H256::from_slice(&r_bytes.as_slice());
|
||||
let s = H256::from_slice(&s_bytes.as_slice());
|
||||
EthSig { r, s, v }
|
||||
}
|
||||
|
||||
/// Makes a trial recovery to check whether an RSig corresponds to a known
|
||||
/// `VerifyingKey`
|
||||
fn check_candidate(sig: &RSig, digest: [u8; 32], vk: &VerifyingKey) -> bool {
|
||||
if let Ok(key) = sig.recover_verify_key_from_digest_bytes(digest.as_ref().into()) {
|
||||
key == *vk
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
/// Recover an rsig from a signature under a known key by trial/error
|
||||
pub(super) fn rsig_from_digest_bytes_trial_recovery(
|
||||
sig: &KSig,
|
||||
digest: [u8; 32],
|
||||
vk: &VerifyingKey,
|
||||
) -> RSig {
|
||||
let sig_0 = RSig::new(sig, Id::new(0).unwrap()).unwrap();
|
||||
let sig_1 = RSig::new(sig, Id::new(1).unwrap()).unwrap();
|
||||
|
||||
if check_candidate(&sig_0, digest, vk) {
|
||||
sig_0
|
||||
} else if check_candidate(&sig_1, digest, vk) {
|
||||
sig_1
|
||||
} else {
|
||||
panic!("bad sig");
|
||||
}
|
||||
}
|
||||
|
||||
/// Modify the v value of a signature to conform to eip155
|
||||
pub(super) fn apply_eip155(sig: &mut EthSig, chain_id: u64) {
|
||||
let v = (chain_id * 2 + 35) + ((sig.v - 1) % 2);
|
||||
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<VerifyingKey, AwsSignerError> {
|
||||
let raw = resp
|
||||
.public_key
|
||||
.ok_or_else(|| AwsSignerError::from("Pubkey not found in response".to_owned()))?;
|
||||
|
||||
let spk = spki::SubjectPublicKeyInfo::try_from(raw.as_ref())?;
|
||||
let key = VerifyingKey::from_sec1_bytes(&spk.subject_public_key)?;
|
||||
|
||||
Ok(key)
|
||||
}
|
||||
|
||||
/// Decode an AWS KMS Signature response
|
||||
pub(super) fn decode_signature(resp: SignResponse) -> Result<KSig, AwsSignerError> {
|
||||
let raw = resp
|
||||
.signature
|
||||
.ok_or_else(|| AwsSignerError::from("Signature not found in response".to_owned()))?;
|
||||
|
||||
let mut sig = KSig::from_der(&raw)?;
|
||||
sig.normalize_s()?;
|
||||
Ok(sig)
|
||||
}
|
|
@ -10,6 +10,7 @@
|
|||
//! - [Private key](crate::LocalWallet)
|
||||
//! - [Ledger](crate::Ledger)
|
||||
//! - [YubiHSM2](crate::YubiWallet)
|
||||
//! - [AWS KMS](crate::AwsSigner)
|
||||
//!
|
||||
//! ```no_run
|
||||
//! # use ethers::{
|
||||
|
@ -62,6 +63,12 @@ pub use ledger::{
|
|||
#[cfg(feature = "yubi")]
|
||||
pub use yubihsm;
|
||||
|
||||
#[cfg(feature = "aws")]
|
||||
mod aws;
|
||||
|
||||
#[cfg(feature = "aws")]
|
||||
pub use aws::AwsSigner;
|
||||
|
||||
use async_trait::async_trait;
|
||||
use ethers_core::types::{Address, Signature, TransactionRequest};
|
||||
use std::error::Error;
|
||||
|
|
Loading…
Reference in New Issue