diff --git a/e2e/Cargo.toml b/e2e/Cargo.toml index cecc1d7f9..1013d1415 100644 --- a/e2e/Cargo.toml +++ b/e2e/Cargo.toml @@ -33,6 +33,7 @@ fuel-core-chain-config = { workspace = true, features = [ "std", "test-helpers", ] } +fuels-accounts = { workspace = true, features = ["test-helpers"] } fuel-core-types = { workspace = true, features = [ "da-compression", @@ -46,10 +47,12 @@ fuel-core-client = { workspace = true } + [build-dependencies] anyhow = { workspace = true, features = ["std"] } flate2 = { workspace = true, features = ["zlib"] } fuels-accounts = { workspace = true, features = ["std"] } + reqwest = { workspace = true, features = ["blocking", "default-tls"] } semver = { workspace = true } tar = { workspace = true } diff --git a/e2e/src/client.rs b/e2e/src/client.rs index 6771eec54..7cf86eef9 100644 --- a/e2e/src/client.rs +++ b/e2e/src/client.rs @@ -1,10 +1,6 @@ use url::Url; -use fuel_core_client::client::types::CoinType; -use fuel_core_client::client::{types::Block, FuelClient}; -use fuel_core_types::fuel_tx::Transaction; -use fuel_core_types::fuel_types::{Address, AssetId}; -use fuels::types::coin::Coin; +use fuel_core_client::client::FuelClient; use fuels::types::errors::Error; use fuels::types::errors::Result; #[derive(Clone)] @@ -28,30 +24,6 @@ impl HttpClient { Ok(()) } - pub async fn send_tx(&self, tx: &Transaction) -> Result<()> { - self.client - .submit_and_await_commit(tx) - .await - .map_err(|e| Error::Other(e.to_string()))?; - - Ok(()) - } - - pub async fn get_coin(&self, address: Address, asset_id: AssetId) -> Result { - let coin_type = self - .client - .coins_to_spend(&address, vec![(asset_id, 1, None)], None) - .await - .map_err(|e| Error::Other(e.to_string()))?[0][0]; - - let coin = match coin_type { - CoinType::Coin(c) => Ok(c), - _ => Err(Error::Other("Couldn't get coin".to_string())), - }?; - - Ok(Coin::from(coin)) - } - pub async fn health(&self) -> Result { match self.client.health().await { Ok(healthy) => Ok(healthy), diff --git a/e2e/src/e2e_helpers.rs b/e2e/src/e2e_helpers.rs new file mode 100644 index 000000000..c5cda7005 --- /dev/null +++ b/e2e/src/e2e_helpers.rs @@ -0,0 +1,22 @@ +use crate::{ + fuel_node::{FuelNode, FuelNodeProcess}, + kms::{Kms, KmsKey, KmsProcess}, +}; + +pub async fn start_kms(logs: bool) -> anyhow::Result { + Kms::default().with_show_logs(logs).start().await +} +pub async fn create_and_fund_kms_keys( + kms: &KmsProcess, + fuel_node: &FuelNodeProcess, +) -> anyhow::Result { + let amount = 5_000_000_000; + let key = kms.create_key().await?; + let address = key.kms_data.address.clone(); + fuel_node.fund(address, amount).await?; + + Ok(key) +} +pub async fn start_fuel_node(logs: bool) -> anyhow::Result { + FuelNode::default().with_show_logs(logs).start().await +} diff --git a/e2e/src/fuel_node.rs b/e2e/src/fuel_node.rs index 327ea0b93..e816e145e 100644 --- a/e2e/src/fuel_node.rs +++ b/e2e/src/fuel_node.rs @@ -1,23 +1,13 @@ use crate::client::HttpClient; -use fuel_core_chain_config::{ - ChainConfig, CoinConfig, ConsensusConfig, SnapshotWriter, StateConfig, -}; +use anyhow::Context; use fuel_core_types::{ - fuel_crypto::SecretKey as FuelSecretKey, - fuel_tx::{AssetId, Finalizable, Input, Output, TransactionBuilder, TxPointer}, - fuel_types::Address, + fuel_tx::AssetId, }; -use fuels::crypto::{PublicKey, SecretKey}; +use fuels::accounts::Account; +use fuels::crypto::SecretKey; use fuels::prelude::{Bech32Address, Provider, TxPolicies, WalletUnlocked}; -use itertools::Itertools; -use rand::Rng; -use std::path::PathBuf; use std::str::FromStr; -use anyhow::Context; use url::Url; -use fuels::accounts::Account; -use fuels::accounts::aws_signer::AwsWallet; -use fuels::types::U256; #[derive(Default, Debug)] pub struct FuelNode { @@ -30,52 +20,6 @@ pub struct FuelNodeProcess { } impl FuelNode { - fn create_state_config( - path: impl Into, - consensus_key: &PublicKey, - num_wallets: usize, - ) -> anyhow::Result> { - let chain_config = ChainConfig { - consensus: ConsensusConfig::PoA { - signing_key: Input::owner(consensus_key), - }, - ..ChainConfig::local_testnet() - }; - - let mut rng = &mut rand::thread_rng(); - let keys = std::iter::repeat_with(|| FuelSecretKey::random(&mut rng)) - .take(num_wallets) - .collect_vec(); - - let coins = keys - .iter() - .flat_map(|key| { - std::iter::repeat_with(|| CoinConfig { - owner: Input::owner(&key.public_key()), - amount: u64::MAX, - asset_id: AssetId::zeroed(), - tx_id: rng.gen(), - output_index: rng.gen(), - ..Default::default() - }) - .take(10) - .collect_vec() - }) - .collect_vec(); - - let state_config = StateConfig { - coins, - ..StateConfig::local_testnet() - }; - - let snapshot = SnapshotWriter::json(path); - snapshot - .write_state_config(state_config, &chain_config) - .map_err(|_| anyhow::anyhow!("Failed to write state config"))?; - - Ok(keys) - } - pub async fn start(&self) -> anyhow::Result { let unused_port = portpicker::pick_unused_port() .ok_or_else(|| anyhow::anyhow!("No free port to start fuel-core"))?; @@ -120,45 +64,6 @@ impl FuelNodeProcess { HttpClient::new(&self.url) } - async fn send_transfer_tx(client: HttpClient, key: FuelSecretKey) -> anyhow::Result<()> { - let mut tx = TransactionBuilder::script(vec![], vec![]); - - tx.script_gas_limit(1_000_000); - - let secret_key = key; - let address = Input::owner(&secret_key.public_key()); - - let base_asset = AssetId::zeroed(); - let coin = client.get_coin(address, base_asset).await?; - - tx.add_unsigned_coin_input( - secret_key, - coin.utxo_id, - coin.amount, - coin.asset_id, - TxPointer::default(), - ); - - const AMOUNT: u64 = 1; - let to = Address::default(); - tx.add_output(Output::Coin { - to, - amount: AMOUNT, - asset_id: base_asset, - }); - tx.add_output(Output::Change { - to: address, - amount: 0, - asset_id: base_asset, - }); - - let tx = tx.finalize(); - - client.send_tx(&tx.into()).await?; - - Ok(()) - } - async fn wait_until_healthy(&self) { loop { if let Ok(true) = self.client().health().await { @@ -171,30 +76,27 @@ impl FuelNodeProcess { &self.url } - pub async fn fund( - &self, - address: Bech32Address, - amount: u64 - ) -> anyhow::Result<()> { + pub async fn fund(&self, address: Bech32Address, amount: u64) -> anyhow::Result<()> { let fuels_provider = Provider::connect(self.url()).await?; + // Create a wallet with the private key of the default account let mut default_wallet = WalletUnlocked::new_from_private_key( SecretKey::from_str( "0xde97d8624a438121b86a1956544bd72ed68cd69f2c99555b08b1e8c51ffd511c", - ) - ?, + )?, None, ); default_wallet.set_provider(fuels_provider.clone()); + // Transfer ETH funds to the AWS wallet from the default wallet + let asset_id = + AssetId::from_str("f8f8b6283d7fa5b672b530cbb84fcccb4ff8dc40f8176ef4544ddb1f1952ad07") + .expect("AssetId to be well formed"); + default_wallet - .transfer( - &address, - amount, // Amount to transfer - AssetId::from_str("f8f8b6283d7fa5b672b530cbb84fcccb4ff8dc40f8176ef4544ddb1f1952ad07").expect("AssetId to be well formed"), - TxPolicies::default(), - ) - .await.context("Failed to transfer funds")?; + .transfer(&address, amount, asset_id, TxPolicies::default()) + .await + .context("Failed to transfer funds")?; self.client().produce_blocks(1).await?; diff --git a/e2e/src/kms.rs b/e2e/src/kms.rs index 49a5bb890..895e816dd 100644 --- a/e2e/src/kms.rs +++ b/e2e/src/kms.rs @@ -1,10 +1,8 @@ use anyhow::Context; -use aws_config::Region; -use aws_sdk_kms::{config::Credentials, Client as AWSClient}; -use aws_sdk_kms::config::BehaviorVersion; use testcontainers::{core::ContainerPort, runners::AsyncRunner}; use tokio::io::AsyncBufReadExt; -use fuels::accounts::aws_signer::KmsData; +use fuels::accounts::aws::{AwsClient, AwsConfig, KmsData}; + #[derive(Default)] pub struct Kms { show_logs: bool, @@ -49,28 +47,8 @@ impl Kms { let port = container.get_host_port_ipv4(4566).await?; let url = format!("http://localhost:{}", port); - // Configure AWS SDK - // let config = aws_config::from_env() - // .endpoint_url(url.clone()) - // .region("us-east-1") - // .credentials_provider(Credentials::new("test", "test", None, None, "test")) - // .load() - // .await; - - let config = aws_config::defaults(BehaviorVersion::latest()) - .credentials_provider(Credentials::new( - "test", - "test", - None, - None, - "Static Credentials", - )) - .endpoint_url(url.clone()) - .region(Region::new("us-east-1")) // placeholder region for test - .load() - .await; - - let client = AWSClient::new(&config); + let config = AwsConfig::for_testing(url.clone()).await; + let client = AwsClient::new(config); Ok(KmsProcess { _container: container, @@ -121,14 +99,15 @@ fn spawn_log_printer(container: &testcontainers::ContainerAsync) { pub struct KmsProcess { _container: testcontainers::ContainerAsync, - client: AWSClient, + client: AwsClient, url: String, } impl KmsProcess { pub async fn create_key(&self) -> anyhow::Result { let response = self - .client + .client. + inner() .create_key() .key_usage(aws_sdk_kms::types::KeyUsageType::SignVerify) .key_spec(aws_sdk_kms::types::KeySpec::EccSecgP256K1) @@ -145,13 +124,12 @@ impl KmsProcess { Ok(KmsKey { id, - kms_data,// todo fix this + kms_data, url: self.url.clone(), }) } } - #[derive(Debug, Clone)] pub struct KmsKey { pub id: String, diff --git a/e2e/src/lib.rs b/e2e/src/lib.rs index 9f13caf15..d10c41f3e 100644 --- a/e2e/src/lib.rs +++ b/e2e/src/lib.rs @@ -4,24 +4,84 @@ mod client; mod fuel_node; #[cfg(test)] mod kms; +#[cfg(test)] +mod e2e_helpers; + #[cfg(test)] mod tests { - use crate::fuel_node::FuelNode; use anyhow::Result; - use crate::kms::Kms; + use fuels::prelude::{AssetId, Provider}; + use std::str::FromStr; + use fuels_accounts::aws::AwsWallet; + use fuels_accounts::ViewOnlyAccount; + use crate::e2e_helpers::{create_and_fund_kms_keys, start_fuel_node, start_kms}; - #[tokio::test(flavor = "multi_thread")] - async fn aws_wallet() -> Result<()> { + #[tokio::test] + async fn fund_aws_wallet() -> Result<()> { + let kms = start_kms(false).await?; + let fuel_node = start_fuel_node(false).await?; + let kms_key = create_and_fund_kms_keys(&kms, &fuel_node).await?; - let kms = Kms::default().with_show_logs(false).start().await?; - let key = kms.create_key().await?; - let fuel_node_process = FuelNode::default().with_show_logs(false).start().await?; - - fuel_node_process.fund(key.kms_data.address, 5_000_000_000).await?; + std::env::set_var("AWS_ACCESS_KEY_ID", "test"); + std::env::set_var("AWS_SECRET_ACCESS_KEY", "test"); + std::env::set_var("AWS_REGION", "us-east-1"); + std::env::set_var("AWS_ENDPOINT_URL", &kms_key.url); + let asset_id = + AssetId::from_str("f8f8b6283d7fa5b672b530cbb84fcccb4ff8dc40f8176ef4544ddb1f1952ad07") + .expect("AssetId to be well formed"); + let provider = Provider::connect(fuel_node.url()).await?; + let wallet = AwsWallet::from_kms_key_id(kms_key.id, Some(provider)).await?; + let founded_coins = wallet.get_coins(asset_id).await?.first().expect("No coins found").amount; + assert_eq!(founded_coins, 5000000000); Ok(()) } + + #[tokio::test] + async fn deploy_contract() -> anyhow::Result<()> { + use fuels::prelude::*; + + let kms = start_kms(false).await?; + let fuel_node = start_fuel_node(false).await?; + let kms_key = create_and_fund_kms_keys(&kms, &fuel_node).await?; + + std::env::set_var("AWS_ACCESS_KEY_ID", "test"); + std::env::set_var("AWS_SECRET_ACCESS_KEY", "test"); + std::env::set_var("AWS_REGION", "us-east-1"); + std::env::set_var("AWS_ENDPOINT_URL", &kms_key.url); + + let asset_id = + AssetId::from_str("f8f8b6283d7fa5b672b530cbb84fcccb4ff8dc40f8176ef4544ddb1f1952ad07") + .expect("AssetId to be well formed"); + + let provider = Provider::connect(fuel_node.url()).await?; + let wallet = AwsWallet::from_kms_key_id(kms_key.id, Some(provider)).await?; + + let founded_coins = wallet.get_coins(asset_id).await?.first().expect("No coins found").amount; + assert_eq!(founded_coins, 5000000000); + + let contract_id = Contract::load_from( + "../e2e/sway/contracts/contract_test/out/release/contract_test.bin", + LoadConfiguration::default(), + )? + .deploy(&wallet, TxPolicies::default()) + .await?; + + println!("Contract deployed @ {contract_id}"); + + let founded_coins = wallet.get_coins(asset_id).await?.first().expect("No coins found").amount; + assert_eq!(founded_coins, 4999983198); + + Ok(()) + } + + + + + + + } diff --git a/packages/fuels-accounts/src/aws.rs b/packages/fuels-accounts/src/aws.rs new file mode 100644 index 000000000..c69c7182d --- /dev/null +++ b/packages/fuels-accounts/src/aws.rs @@ -0,0 +1,5 @@ +mod aws_client; +mod aws_signer; + +pub use aws_client::*; +pub use aws_signer::*; \ No newline at end of file diff --git a/packages/fuels-accounts/src/aws/aws_client.rs b/packages/fuels-accounts/src/aws/aws_client.rs new file mode 100644 index 000000000..189b78dac --- /dev/null +++ b/packages/fuels-accounts/src/aws/aws_client.rs @@ -0,0 +1,63 @@ +use aws_config::{default_provider::credentials::DefaultCredentialsChain, Region, SdkConfig}; +#[cfg(feature = "test-helpers")] +use aws_sdk_kms::config::Credentials; +use aws_sdk_kms::{config::BehaviorVersion, Client}; + +#[derive(Debug, Clone)] +pub struct AwsConfig { + sdk_config: SdkConfig, +} + +impl AwsConfig { + pub async fn from_env() -> Self { + let loader = aws_config::defaults(BehaviorVersion::latest()) + .credentials_provider(DefaultCredentialsChain::builder().build().await); + + Self { + sdk_config: loader.load().await, + } + } + + #[cfg(feature = "test-helpers")] + pub async fn for_testing(url: String) -> Self { + let sdk_config = aws_config::defaults(BehaviorVersion::latest()) + .credentials_provider(Credentials::new( + "test", + "test", + None, + None, + "Static Credentials", + )) + .endpoint_url(url) + .region(Region::new("us-east-1")) // placeholder region for test + .load() + .await; + + Self { sdk_config } + } + + pub fn url(&self) -> Option<&str> { + self.sdk_config.endpoint_url() + } + + pub fn region(&self) -> Option<&Region> { + self.sdk_config.region() + } +} + +#[derive(Clone, Debug)] +pub struct AwsClient { + client: Client, +} + +impl AwsClient { + pub fn new(config: AwsConfig) -> Self { + let config = config.sdk_config; + let client = Client::new(&config); + + Self { client } + } + pub fn inner(&self) -> &Client { + &self.client + } +} diff --git a/packages/fuels-accounts/src/aws/aws_signer.rs b/packages/fuels-accounts/src/aws/aws_signer.rs new file mode 100644 index 000000000..6af803137 --- /dev/null +++ b/packages/fuels-accounts/src/aws/aws_signer.rs @@ -0,0 +1,250 @@ +use aws_sdk_kms::{ + primitives::Blob, + types::{KeySpec, MessageType, SigningAlgorithmSpec}, + // Client as AwsClient, +}; +use fuel_crypto::{Message, PublicKey, Signature}; +use fuel_types::AssetId; +use fuels_core::{ + traits::Signer, + types::{ + bech32::{Bech32Address, FUEL_BECH32_HRP}, + coin_type_id::CoinTypeId, + errors::{Error, Result}, + input::Input, + transaction_builders::TransactionBuilder, + }, +}; +use k256::{ + ecdsa::{RecoveryId, Signature as K256Signature, VerifyingKey}, + pkcs8::DecodePublicKey, + PublicKey as K256PublicKey, +}; + +use crate::{provider::Provider, wallet::Wallet, Account, ViewOnlyAccount}; +use crate::aws::{AwsClient, AwsConfig}; + +const AWS_KMS_ERROR_PREFIX: &str = "AWS KMS Error"; + +#[derive(Clone, Debug)] +pub struct AwsWallet { + wallet: Wallet, + kms_data: KmsData, +} + +#[derive(Clone, Debug)] +pub struct KmsData { + id: String, + client: AwsClient, + pub public_key: Vec, + pub address: Bech32Address, +} + +impl KmsData { + pub async fn new(id: String, client: AwsClient) -> anyhow::Result { + Self::validate_key_type(&client, &id).await?; + let public_key = Self::fetch_public_key(&client, &id).await?; + let address = Self::create_bech32_address(&public_key)?; + + Ok(Self { + id, + client, + public_key, + address, + }) + } + + async fn validate_key_type(client: &AwsClient, key_id: &str) -> anyhow::Result<()> { + let key_spec = client + .inner() + .get_public_key() + .key_id(key_id) + .send() + .await? + .key_spec; + + match key_spec { + Some(KeySpec::EccSecgP256K1) => Ok(()), + other => anyhow::bail!( + "{}: Invalid key type, expected EccSecgP256K1, got {:?}", + AWS_KMS_ERROR_PREFIX, + other + ), + } + } + + async fn fetch_public_key(client: &AwsClient, key_id: &str) -> anyhow::Result> { + let response = client.inner().get_public_key().key_id(key_id).send().await?; + + let public_key = response + .public_key() + .ok_or_else(|| anyhow::anyhow!("{}: No public key returned", AWS_KMS_ERROR_PREFIX))?; + + Ok(public_key.clone().into_inner()) + } + + fn create_bech32_address(public_key_bytes: &[u8]) -> anyhow::Result { + let k256_public_key = K256PublicKey::from_public_key_der(public_key_bytes) + .map_err(|_| anyhow::anyhow!("{}: Invalid DER public key", AWS_KMS_ERROR_PREFIX))?; + + let public_key = PublicKey::from(k256_public_key); + let fuel_address = public_key.hash(); + + Ok(Bech32Address::new(FUEL_BECH32_HRP, fuel_address)) + } + + async fn sign_message(&self, message: Message) -> Result { + let signature_der = self.request_signature(message).await?; + let (signature, recovery_id) = self.process_signature(&signature_der, message)?; + + Ok(self.create_fuel_signature(signature, recovery_id)) + } + + async fn request_signature(&self, message: Message) -> Result> { + let reply = self + .client + .inner() + .sign() + .key_id(&self.id) + .signing_algorithm(SigningAlgorithmSpec::EcdsaSha256) + .message_type(MessageType::Digest) + .message(Blob::new(*message)) + .send() + .await + .map_err(|e| { + Error::Other(format!("{}: Failed to sign: {:?}", AWS_KMS_ERROR_PREFIX, e)) + })?; + + reply + .signature + .map(|sig| sig.into_inner()) + .ok_or_else(|| Error::Other(format!("{}: No signature returned", AWS_KMS_ERROR_PREFIX))) + } + + fn process_signature( + &self, + signature_der: &[u8], + message: Message, + ) -> Result<(K256Signature, RecoveryId)> { + let sig = K256Signature::from_der(signature_der).map_err(|_| { + Error::Other(format!("{}: Invalid DER signature", AWS_KMS_ERROR_PREFIX)) + })?; + let sig = sig.normalize_s().unwrap_or(sig); + + let recovery_id = self.determine_recovery_id(&sig, message)?; + + Ok((sig, recovery_id)) + } + + fn determine_recovery_id(&self, sig: &K256Signature, message: Message) -> Result { + let recid1 = RecoveryId::new(false, false); + let recid2 = RecoveryId::new(true, false); + + let correct_public_key = K256PublicKey::from_public_key_der(&self.public_key) + .map_err(|_| { + Error::Other(format!( + "{}: Invalid cached public key", + AWS_KMS_ERROR_PREFIX + )) + })? + .into(); + + let rec1 = VerifyingKey::recover_from_prehash(&*message, sig, recid1); + let rec2 = VerifyingKey::recover_from_prehash(&*message, sig, recid2); + + if rec1.map(|r| r == correct_public_key).unwrap_or(false) { + Ok(recid1) + } else if rec2.map(|r| r == correct_public_key).unwrap_or(false) { + Ok(recid2) + } else { + Err(Error::Other(format!( + "{}: Invalid signature (reduced-x form coordinate)", + AWS_KMS_ERROR_PREFIX + ))) + } + } + + fn create_fuel_signature( + &self, + signature: K256Signature, + recovery_id: RecoveryId, + ) -> Signature { + debug_assert!( + !recovery_id.is_x_reduced(), + "reduced-x form coordinates should be caught earlier" + ); + + let v = recovery_id.is_y_odd() as u8; + let mut signature_bytes = <[u8; 64]>::from(signature.to_bytes()); + signature_bytes[32] = (v << 7) | (signature_bytes[32] & 0x7f); + + Signature::from_bytes(signature_bytes) + } +} + +impl AwsWallet { + pub async fn from_kms_key_id( + kms_key_id: String, + provider: Option, + ) -> anyhow::Result { + let config = AwsConfig::from_env().await; + let client = AwsClient::new(config); + let kms_data = KmsData::new(kms_key_id, client).await?; + + Ok(Self { + wallet: Wallet::from_address(kms_data.address.clone(), provider), + kms_data, + }) + } + + pub fn address(&self) -> &Bech32Address { + self.wallet.address() + } + + pub fn provider(&self) -> Option<&Provider> { + self.wallet.provider() + } +} + +#[async_trait::async_trait] +impl Signer for AwsWallet { + async fn sign(&self, message: Message) -> Result { + self.kms_data.sign_message(message).await + } + + fn address(&self) -> &Bech32Address { + &self.kms_data.address + } +} + +#[cfg_attr(not(target_arch = "wasm32"), async_trait::async_trait)] +impl ViewOnlyAccount for AwsWallet { + fn address(&self) -> &Bech32Address { + self.wallet.address() + } + + fn try_provider(&self) -> Result<&Provider> { + self.wallet.provider().ok_or_else(|| { + Error::Other("No provider available. Make sure to use `set_provider`".to_owned()) + }) + } + + async fn get_asset_inputs_for_amount( + &self, + asset_id: AssetId, + amount: u64, + excluded_coins: Option>, + ) -> Result> { + self.wallet + .get_asset_inputs_for_amount(asset_id, amount, excluded_coins) + .await + } +} + +#[async_trait::async_trait] +impl Account for AwsWallet { + fn add_witnesses(&self, tb: &mut Tb) -> Result<()> { + tb.add_signer(self.clone())?; + Ok(()) + } +} \ No newline at end of file diff --git a/packages/fuels-accounts/src/aws_signer.rs b/packages/fuels-accounts/src/aws_signer.rs deleted file mode 100644 index 6e5791215..000000000 --- a/packages/fuels-accounts/src/aws_signer.rs +++ /dev/null @@ -1,539 +0,0 @@ -use aws_sdk_kms::{ - primitives::Blob, - types::{KeySpec, MessageType, SigningAlgorithmSpec}, - Client as AwsClient, -}; -use fuel_crypto::{Message, PublicKey, Signature}; -use fuel_types::AssetId; -use fuels_core::{ - traits::Signer, - types::{ - bech32::{Bech32Address, FUEL_BECH32_HRP}, - coin_type_id::CoinTypeId, - errors::{Error, Result}, - input::Input, - transaction_builders::TransactionBuilder, - }, -}; -use k256::{ - ecdsa::{RecoveryId, Signature as K256Signature, VerifyingKey}, - pkcs8::DecodePublicKey, - PublicKey as K256PublicKey, -}; - -use crate::{provider::Provider, wallet::Wallet, Account, ViewOnlyAccount}; - -const AWS_KMS_ERROR_PREFIX: &str = "AWS KMS Error"; - -#[derive(Clone, Debug)] -pub struct AwsWallet { - wallet: Wallet, - kms_data: KmsData, -} - -#[derive(Clone, Debug)] -pub struct KmsData { - id: String, - client: AwsClient, - public_key: Vec, - pub address: Bech32Address, -} - -impl KmsData { - pub async fn new(id: String, client: AwsClient) -> anyhow::Result { - Self::validate_key_type(&client, &id).await?; - let public_key = Self::fetch_public_key(&client, &id).await?; - let address = Self::create_bech32_address(&public_key)?; - - Ok(Self { - id, - client, - public_key, - address, - }) - } - - async fn validate_key_type(client: &AwsClient, key_id: &str) -> anyhow::Result<()> { - let key_spec = client - .get_public_key() - .key_id(key_id) - .send() - .await? - .key_spec; - - match key_spec { - Some(KeySpec::EccSecgP256K1) => Ok(()), - other => anyhow::bail!( - "{}: Invalid key type, expected EccSecgP256K1, got {:?}", - AWS_KMS_ERROR_PREFIX, - other - ), - } - } - - async fn fetch_public_key(client: &AwsClient, key_id: &str) -> anyhow::Result> { - let response = client.get_public_key().key_id(key_id).send().await?; - - let public_key = response - .public_key() - .ok_or_else(|| anyhow::anyhow!("{}: No public key returned", AWS_KMS_ERROR_PREFIX))?; - - Ok(public_key.clone().into_inner()) - } - - fn create_bech32_address(public_key_bytes: &[u8]) -> anyhow::Result { - let k256_public_key = K256PublicKey::from_public_key_der(public_key_bytes) - .map_err(|_| anyhow::anyhow!("{}: Invalid DER public key", AWS_KMS_ERROR_PREFIX))?; - - let public_key = PublicKey::from(k256_public_key); - let fuel_address = public_key.hash(); - - Ok(Bech32Address::new(FUEL_BECH32_HRP, fuel_address)) - } - - async fn sign_message(&self, message: Message) -> Result { - let signature_der = self.request_signature(message).await?; - let (signature, recovery_id) = self.process_signature(&signature_der, message)?; - - Ok(self.create_fuel_signature(signature, recovery_id)) - } - - async fn request_signature(&self, message: Message) -> Result> { - let reply = self - .client - .sign() - .key_id(&self.id) - .signing_algorithm(SigningAlgorithmSpec::EcdsaSha256) - .message_type(MessageType::Digest) - .message(Blob::new(*message)) - .send() - .await - .map_err(|e| { - Error::Other(format!("{}: Failed to sign: {:?}", AWS_KMS_ERROR_PREFIX, e)) - })?; - - reply - .signature - .map(|sig| sig.into_inner()) - .ok_or_else(|| Error::Other(format!("{}: No signature returned", AWS_KMS_ERROR_PREFIX))) - } - - fn process_signature( - &self, - signature_der: &[u8], - message: Message, - ) -> Result<(K256Signature, RecoveryId)> { - let sig = K256Signature::from_der(signature_der).map_err(|_| { - Error::Other(format!("{}: Invalid DER signature", AWS_KMS_ERROR_PREFIX)) - })?; - let sig = sig.normalize_s().unwrap_or(sig); - - let recovery_id = self.determine_recovery_id(&sig, message)?; - - Ok((sig, recovery_id)) - } - - fn determine_recovery_id(&self, sig: &K256Signature, message: Message) -> Result { - let recid1 = RecoveryId::new(false, false); - let recid2 = RecoveryId::new(true, false); - - let correct_public_key = K256PublicKey::from_public_key_der(&self.public_key) - .map_err(|_| { - Error::Other(format!( - "{}: Invalid cached public key", - AWS_KMS_ERROR_PREFIX - )) - })? - .into(); - - let rec1 = VerifyingKey::recover_from_prehash(&*message, sig, recid1); - let rec2 = VerifyingKey::recover_from_prehash(&*message, sig, recid2); - - if rec1.map(|r| r == correct_public_key).unwrap_or(false) { - Ok(recid1) - } else if rec2.map(|r| r == correct_public_key).unwrap_or(false) { - Ok(recid2) - } else { - Err(Error::Other(format!( - "{}: Invalid signature (reduced-x form coordinate)", - AWS_KMS_ERROR_PREFIX - ))) - } - } - - fn create_fuel_signature( - &self, - signature: K256Signature, - recovery_id: RecoveryId, - ) -> Signature { - debug_assert!( - !recovery_id.is_x_reduced(), - "reduced-x form coordinates should be caught earlier" - ); - - let v = recovery_id.is_y_odd() as u8; - let mut signature_bytes = <[u8; 64]>::from(signature.to_bytes()); - signature_bytes[32] = (v << 7) | (signature_bytes[32] & 0x7f); - - Signature::from_bytes(signature_bytes) - } -} - -impl AwsWallet { - pub async fn from_kms_key_id( - kms_key_id: String, - provider: Option, - ) -> anyhow::Result { - let config = aws_config::load_from_env().await; - let client = AwsClient::new(&config); - - let kms_data = KmsData::new(kms_key_id, client).await?; - println!("Fuel address: {}", kms_data.address); - - Ok(Self { - wallet: Wallet::from_address(kms_data.address.clone(), provider), - kms_data, - }) - } - - pub fn address(&self) -> &Bech32Address { - self.wallet.address() - } - - pub fn provider(&self) -> Option<&Provider> { - self.wallet.provider() - } -} - -#[async_trait::async_trait] -impl Signer for AwsWallet { - async fn sign(&self, message: Message) -> Result { - self.kms_data.sign_message(message).await - } - - fn address(&self) -> &Bech32Address { - &self.kms_data.address - } -} - -#[cfg_attr(not(target_arch = "wasm32"), async_trait::async_trait)] -impl ViewOnlyAccount for AwsWallet { - fn address(&self) -> &Bech32Address { - self.wallet.address() - } - - fn try_provider(&self) -> Result<&Provider> { - self.wallet.provider().ok_or_else(|| { - Error::Other("No provider available. Make sure to use `set_provider`".to_owned()) - }) - } - - async fn get_asset_inputs_for_amount( - &self, - asset_id: AssetId, - amount: u64, - excluded_coins: Option>, - ) -> Result> { - self.wallet - .get_asset_inputs_for_amount(asset_id, amount, excluded_coins) - .await - } -} - -#[async_trait::async_trait] -impl Account for AwsWallet { - fn add_witnesses(&self, tb: &mut Tb) -> Result<()> { - tb.add_signer(self.clone())?; - Ok(()) - } -} - -// -// // Integration tests using the LocalStack KMS -// // #[cfg(test)] -// // mod tests { -// // use super::*; -// // use fuel_crypto::Message; -// // use std::env; -// // -// // #[tokio::test] -// // async fn test_kms_wallet_with_localstack() -> anyhow::Result<()> { -// // // Start LocalStack -// // let kms = KmsTestContainer::default().with_show_logs(true); -// // let kms_process = kms.start().await?; -// // -// // // Create a new KMS key -// // let test_key = kms_process.create_key().await?; -// // -// // // Set required environment variables -// // env::set_var("AWS_ACCESS_KEY_ID", "test"); -// // env::set_var("AWS_SECRET_ACCESS_KEY", "test"); -// // env::set_var("AWS_REGION", "us-east-1"); -// // env::set_var("AWS_ENDPOINT_URL", test_key.url); -// // -// // // Create KMS wallet -// // let wallet = KMSWallet::from_kms_key_id(test_key.id, None).await?; -// // -// // // Test signing -// // let message = Message::new([1u8; 32]); -// // let signature = wallet.sign(message).await?; -// // -// // // Verify the signature -// // let public_key = wallet.address().hash(); -// // // assert!(signature.verify(&message, &public_key)); -// // -// // Ok(()) -// // } -// // -// // #[tokio::test] -// // async fn test_multiple_signatures() -> anyhow::Result<()> { -// // let kms = KmsTestContainer::default().with_show_logs(false); -// // let kms_process = kms.start().await?; -// // let test_key = kms_process.create_key().await?; -// // -// // env::set_var("AWS_ACCESS_KEY_ID", "test"); -// // env::set_var("AWS_SECRET_ACCESS_KEY", "test"); -// // env::set_var("AWS_REGION", "us-east-1"); -// // env::set_var("AWS_ENDPOINT_URL", test_key.url); -// // -// // let wallet = KMSWallet::from_kms_key_id(test_key.id, None).await?; -// // -// // // Sign multiple messages -// // for i in 0..5 { -// // let message = Message::new([i as u8; 32]); -// // let signature = wallet.sign(message).await?; -// // // assert!(signature.verify(&message, &wallet.address().hash())); -// // } -// // -// // Ok(()) -// // } -// // -// // #[tokio::test] -// // async fn test_error_handling() -> anyhow::Result<()> { -// // // Start LocalStack -// // let kms = KmsTestContainer::default().with_show_logs(false); -// // let kms_process = kms.start().await?; -// // -// // // Set required environment variables first -// // std::env::set_var("AWS_ACCESS_KEY_ID", "test"); -// // std::env::set_var("AWS_SECRET_ACCESS_KEY", "test"); -// // std::env::set_var("AWS_REGION", "us-east-1"); -// // std::env::set_var("AWS_ENDPOINT_URL", &kms_process.url); -// // -// // // Test 1: Invalid key ID -// // { -// // let result = KMSWallet::from_kms_key_id("invalid-key-id".to_string(), None).await; -// // dbg!(&result); -// // assert!(result.is_err()); -// // let err = result.unwrap_err().to_string(); -// // println!("Invalid key ID error: {}", err); -// // assert!(err.contains("AWS KMS Error")); // Check for our error prefix -// // } -// // -// // // Test 2: Wrong key spec -// // { -// // let response = kms_process -// // .client -// // .create_key() -// // .key_usage(aws_sdk_kms::types::KeyUsageType::SignVerify) -// // .key_spec(aws_sdk_kms::types::KeySpec::Rsa2048) // Wrong key spec -// // .send() -// // .await?; -// // -// // let key_id = response -// // .key_metadata -// // .and_then(|metadata| metadata.arn) -// // .ok_or_else(|| anyhow::anyhow!("Key ARN missing from response"))?; -// // -// // let result = KMSWallet::from_kms_key_id(key_id, None).await; -// // println!("Wrong key spec error: {:?}", result); -// // assert!(result.is_err()); -// // let err = result.unwrap_err().to_string(); -// // assert!(err.contains("Invalid key type") || err.contains("key_spec")); -// // } -// // -// // // Test 3: Invalid endpoint -// // { -// // // Set invalid endpoint -// // std::env::set_var("AWS_ENDPOINT_URL", "http://invalid-endpoint:4566"); -// // -// // let result = KMSWallet::from_kms_key_id("any-key-id".to_string(), None).await; -// // assert!(result.is_err()); -// // let err = result.unwrap_err().to_string(); -// // println!("Invalid endpoint error: {}", err); -// // assert!(err.contains("AWS KMS Error") || err.contains("endpoint")); -// // } -// // -// // Ok(()) -// // } -// // -// // // Helper function to print environment variables (useful for debugging) -// // fn print_aws_env() { -// // println!("AWS Environment Variables:"); -// // println!("AWS_ACCESS_KEY_ID: {:?}", env::var("AWS_ACCESS_KEY_ID")); -// // println!("AWS_SECRET_ACCESS_KEY: {:?}", env::var("AWS_SECRET_ACCESS_KEY")); -// // println!("AWS_REGION: {:?}", env::var("AWS_REGION")); -// // println!("AWS_ENDPOINT_URL: {:?}", env::var("AWS_ENDPOINT_URL")); -// // } -// // } -// -// -// #[cfg(test)] -// mod tests { -// use super::*; -// use fuel_crypto::{Message, PublicKey}; -// use std::env; -// use std::str::FromStr; -// -// // Helper function to set up test environment -// async fn setup_test_environment() -> anyhow::Result<(KmsTestProcess, KmsTestKey)> { -// let kms = KmsTestContainer::default().with_show_logs(false); -// let kms_process = kms.start().await?; -// let test_key = kms_process.create_key().await?; -// -// // Set required environment variables -// env::set_var("AWS_ACCESS_KEY_ID", "test"); -// env::set_var("AWS_SECRET_ACCESS_KEY", "test"); -// env::set_var("AWS_REGION", "us-east-1"); -// env::set_var("AWS_ENDPOINT_URL", &test_key.url); -// -// Ok((kms_process, test_key)) -// } -// -// #[tokio::test] -// async fn test_wallet_creation_and_basic_operations() -> anyhow::Result<()> { -// let (kms_process,test_key) = setup_test_environment().await?; -// -// // Test wallet creation -// let wallet = AWSWallet::from_kms_key_id(test_key.id, None).await?; -// -// // Verify address format -// assert!(wallet.address().to_string().starts_with("fuel")); -// -// // Verify provider behavior -// assert!(wallet.provider().is_none()); -// assert!(wallet.try_provider().is_err()); -// -// Ok(()) -// } -// -// #[tokio::test] -// async fn test_message_signing_and_verification() -> anyhow::Result<()> { -// let (kms_process, test_key) = setup_test_environment().await?; -// let wallet = AWSWallet::from_kms_key_id(test_key.id, None).await?; -// -// // Test signing with different message types -// let test_cases = vec![ -// [0u8; 32], // Zero message -// [1u8; 32], // All ones -// [255u8; 32], // All max values -// [123u8; 32], // Random values -// ]; -// -// for msg_bytes in test_cases { -// let message = Message::new(msg_bytes); -// let signature = wallet.sign(message).await?; -// -// // Verify the signature using the wallet's public key -// let public_key = PublicKey::from_str(&wallet.address().hash().to_string())?; -// -// // assert!(signature.verify(&message, &public_key)); -// } -// -// Ok(()) -// } -// -// // #[tokio::test] -// // async fn test_concurrent_signing() -> anyhow::Result<()> { -// // let (_, test_key) = setup_test_environment().await?; -// // let wallet = KMSWallet::from_kms_key_id(test_key.id, None).await?; -// // -// // // Create multiple signing tasks -// // let mut handles = vec![]; -// // for i in 0..5 { -// // let wallet_clone = wallet.clone(); -// // let handle = tokio::spawn(async move { -// // let message = Message::new([i as u8; 32]); -// // wallet_clone.sign(message).await -// // }); -// // handles.push(handle); -// // } -// // -// // // Wait for all signatures and verify them -// // for handle in handles { -// // let signature = handle.await??; -// // assert!(signature.verify( -// // &Message::new([0u8; 32]), -// // &PublicKey::from_bytes(wallet.address().hash().as_ref())? -// // )); -// // } -// // -// // Ok(()) -// // } -// // -// // #[tokio::test] -// // async fn test_error_cases() -> anyhow::Result<()> { -// // let (kms_process, _) = setup_test_environment().await?; -// // -// // // Test 1: Invalid key ID -// // let result = KMSWallet::from_kms_key_id("invalid-key-id".to_string(), None).await; -// // assert!(result.is_err()); -// // assert!(result.unwrap_err().to_string().contains("AWS KMS Error")); -// // -// // // Test 2: Wrong key spec -// // let response = kms_process -// // .client -// // .create_key() -// // .key_usage(aws_sdk_kms::types::KeyUsageType::SignVerify) -// // .key_spec(aws_sdk_kms::types::KeySpec::Rsa2048) -// // .send() -// // .await?; -// // -// // let key_id = response -// // .key_metadata -// // .and_then(|metadata| metadata.arn) -// // .ok_or_else(|| anyhow::anyhow!("Key ARN missing from response"))?; -// // -// // let result = KMSWallet::from_kms_key_id(key_id, None).await; -// // assert!(result.is_err()); -// // assert!(result.unwrap_err().to_string().contains("Invalid key type")); -// // -// // // Test 3: Missing environment variables -// // env::remove_var("AWS_REGION"); -// // let result = KMSWallet::from_kms_key_id("any-key-id".to_string(), None).await; -// // assert!(result.is_err()); -// // -// // Ok(()) -// // } -// // -// // #[tokio::test] -// // async fn test_address_consistency() -> anyhow::Result<()> { -// // let (_, test_key) = setup_test_environment().await?; -// // let wallet = KMSWallet::from_kms_key_id(test_key.id.clone(), None).await?; -// // -// // // Create another wallet instance with the same key -// // let wallet2 = KMSWallet::from_kms_key_id(test_key.id, None).await?; -// // -// // // Verify addresses match -// // assert_eq!(wallet.address(), wallet2.address()); -// // // assert_eq!(wallet.address(), wallet.kms_data.cached_address); -// // -// // Ok(()) -// // } -// // -// // -// // -// // #[tokio::test] -// // async fn test_asset_inputs() -> anyhow::Result<()> { -// // let (_, test_key) = setup_test_environment().await?; -// // let wallet = KMSWallet::from_kms_key_id(test_key.id, None).await?; -// // -// // // Test getting asset inputs (should fail without provider) -// // let result = wallet -// // .get_asset_inputs_for_amount(AssetId::default(), 100, None) -// // .await; -// // assert!(result.is_err()); -// // -// // Ok(()) -// // } -// } diff --git a/packages/fuels-accounts/src/lib.rs b/packages/fuels-accounts/src/lib.rs index 9864d2481..31b8b9402 100644 --- a/packages/fuels-accounts/src/lib.rs +++ b/packages/fuels-accounts/src/lib.rs @@ -11,15 +11,15 @@ pub mod wallet; #[cfg(feature = "std")] pub use account::*; - #[cfg(feature = "std")] -pub mod aws_signer; +pub mod aws; #[cfg(feature = "coin-cache")] mod coin_cache; pub mod predicate; + #[cfg(test)] mod test { #[test]