Skip to content

KMS #22

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 7 commits into from
Jul 23, 2025
Merged

KMS #22

Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
727 changes: 629 additions & 98 deletions Cargo.lock

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,6 @@ members = [
resolver = "2"

[workspace.dependencies]
alloy = { version = "1.0.8" }
alloy = { version = "1.0.23" }
vault-types = { version = "0.1.0", git = "ssh://git@github.com/thirdweb-dev/vault.git", branch = "pb/update-alloy" }
vault-sdk = { version = "0.1.0", git = "ssh://git@github.com/thirdweb-dev/vault.git", branch = "pb/update-alloy" }
36 changes: 35 additions & 1 deletion aa-types/src/userop.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
use alloy::{
core::sol_types::SolValue,
primitives::{Address, B256, Bytes, ChainId, U256, keccak256},
primitives::{Address, B256, Bytes, ChainId, U256, address, keccak256},
rpc::types::{PackedUserOperation, UserOperation},
};
use serde::{Deserialize, Serialize};
Expand All @@ -13,6 +13,9 @@ pub enum VersionedUserOp {
V0_7(PackedUserOperation),
}

pub const ENTRYPOINT_ADDRESS_V0_6: Address = address!("0x5FF137D4b0FDCD49DcA30c7CF57E578a026d2789"); // v0.6
pub const ENTRYPOINT_ADDRESS_V0_7: Address = address!("0x0000000071727De22E5E9d8BAf0edAc6f37da032"); // v0.7

/// Error type for UserOp operations
#[derive(
Debug,
Expand Down Expand Up @@ -182,3 +185,34 @@ pub fn compute_user_op_v07_hash(
let final_hash = keccak256(&outer_encoded);
Ok(final_hash)
}

impl VersionedUserOp {
pub fn hash(&self, chain_id: ChainId) -> Result<B256, UserOpError> {
match self {
VersionedUserOp::V0_6(op) => {
compute_user_op_v06_hash(op, ENTRYPOINT_ADDRESS_V0_6, chain_id)
}
VersionedUserOp::V0_7(op) => {
compute_user_op_v07_hash(op, ENTRYPOINT_ADDRESS_V0_7, chain_id)
}
}
}

pub fn hash_with_custom_entrypoint(
&self,
chain_id: ChainId,
entrypoint: Address,
) -> Result<B256, UserOpError> {
match self {
VersionedUserOp::V0_6(op) => compute_user_op_v06_hash(op, entrypoint, chain_id),
VersionedUserOp::V0_7(op) => compute_user_op_v07_hash(op, entrypoint, chain_id),
}
}

pub fn default_entrypoint(&self) -> Address {
match self {
VersionedUserOp::V0_6(_) => ENTRYPOINT_ADDRESS_V0_6,
VersionedUserOp::V0_7(_) => ENTRYPOINT_ADDRESS_V0_7,
}
}
}
4 changes: 4 additions & 0 deletions core/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -19,3 +19,7 @@ thirdweb-core = { version = "0.1.0", path = "../thirdweb-core" }
uuid = { version = "1.17.0", features = ["v4"] }
utoipa = { version = "5.4.0", features = ["preserve_order"] }
serde_with = "3.13.0"
alloy-signer-aws = { version = "1.0.23", features = ["eip712"] }
aws-config = "1.8.2"
aws-sdk-kms = "1.79.0"
aws-credential-types = "1.2.4"
4 changes: 0 additions & 4 deletions core/src/constants.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,3 @@ pub const DEFAULT_FACTORY_ADDRESS_V0_6: Address =

pub const DEFAULT_IMPLEMENTATION_ADDRESS_V0_6: Address =
address!("0xf22175c80c6e074C171811C59C6c0087e2a6a346");

pub const ENTRYPOINT_ADDRESS_V0_6: Address = address!("0x5FF137D4b0FDCD49DcA30c7CF57E578a026d2789"); // v0.6

pub const ENTRYPOINT_ADDRESS_V0_7: Address = address!("0x0000000071727De22E5E9d8BAf0edAc6f37da032"); // v0.7
68 changes: 63 additions & 5 deletions core/src/credentials.rs
Original file line number Diff line number Diff line change
@@ -1,13 +1,71 @@
use alloy::primitives::ChainId;
use alloy::signers::local::PrivateKeySigner;
use alloy_signer_aws::AwsSigner;
use aws_config::{BehaviorVersion, Region};
use aws_credential_types::provider::future::ProvideCredentials as ProvideCredentialsFuture;
use aws_sdk_kms::config::{Credentials, ProvideCredentials};
use serde::{Deserialize, Serialize};
use thirdweb_core::auth::ThirdwebAuth;
use thirdweb_core::iaw::AuthToken;
use vault_types::enclave::auth::Auth;
use vault_types::enclave::auth::Auth as VaultAuth;

use crate::error::EngineError;

impl SigningCredential {
/// Create a random private key credential for testing
pub fn random_local() -> Self {
SigningCredential::PrivateKey(PrivateKeySigner::random())
}
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum SigningCredential {
Vault(Auth),
Iaw {
auth_token: AuthToken,
thirdweb_auth: ThirdwebAuth
Vault(VaultAuth),
Iaw {
auth_token: AuthToken,
thirdweb_auth: ThirdwebAuth,
},
AwsKms(AwsKmsCredential),
/// Private key signer for testing and development
/// Note: This should only be used in test environments
#[serde(skip)]
PrivateKey(PrivateKeySigner),
Comment on lines +31 to +32
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Document security implications of PrivateKey variant.

The #[serde(skip)] attribute prevents serialization, but consider adding runtime validation to ensure this variant is only used in development/test environments.

+    /// Private key signer for testing and development
+    /// Note: This should only be used in test environments
+    #[serde(skip)]
+    #[cfg_attr(not(any(test, feature = "testing")), deprecated = "PrivateKey credentials should only be used in test environments")]
     PrivateKey(PrivateKeySigner),
🤖 Prompt for AI Agents
In core/src/credentials.rs at lines 31 to 32, the PrivateKey variant is marked
with #[serde(skip)] to avoid serialization, but lacks runtime checks. Add
validation logic to ensure this variant is only instantiated or used in
development or test environments by checking the environment at runtime and
returning an error or panic if used in production.

}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AwsKmsCredential {
pub access_key_id: String,
pub secret_access_key: String,
pub key_id: String,
pub region: String,
}
Comment on lines +35 to +41
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Security concern: Storing AWS credentials in memory.

Storing AWS access keys directly in the struct poses security risks. Consider using AWS STS temporary credentials or IAM roles instead of long-lived access keys.

Consider implementing support for:

  1. AWS STS AssumeRole for temporary credentials
  2. IAM instance roles for EC2/ECS/Lambda environments
  3. Credential rotation mechanisms
🤖 Prompt for AI Agents
In core/src/credentials.rs around lines 23 to 29, the AwsKmsCredential struct
currently stores long-lived AWS access keys directly, which is a security risk.
Refactor the code to support AWS STS AssumeRole for obtaining temporary
credentials, integrate IAM roles for EC2/ECS/Lambda environments to avoid
hardcoding credentials, and implement credential rotation mechanisms to refresh
credentials periodically. This will enhance security by avoiding persistent
sensitive data in memory.


impl ProvideCredentials for AwsKmsCredential {
fn provide_credentials<'a>(&'a self) -> ProvideCredentialsFuture<'a>
where
Self: 'a,
{
let credentials = Credentials::new(
self.access_key_id.clone(),
self.secret_access_key.clone(),
None,
None,
"engine-core",
);
ProvideCredentialsFuture::ready(Ok(credentials))
}
}

impl AwsKmsCredential {
pub async fn get_signer(&self, chain_id: Option<ChainId>) -> Result<AwsSigner, EngineError> {
let config = aws_config::defaults(BehaviorVersion::latest())
.credentials_provider(self.clone())
.region(Region::new(self.region.clone()))
.load()
.await;
let client = aws_sdk_kms::Client::new(&config);

let signer = AwsSigner::new(client, self.key_id.clone(), chain_id).await?;
Ok(signer)
}
Comment on lines +60 to +70
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Add timeout configuration for AWS operations.

AWS SDK operations should have timeout configurations to prevent hanging requests.

 pub async fn get_signer(&self, chain_id: Option<ChainId>) -> Result<AwsSigner, EngineError> {
     let config = aws_config::defaults(BehaviorVersion::latest())
         .credentials_provider(self.clone())
         .region(Region::new(self.region.clone()))
+        .timeout_config(
+            aws_config::timeout::TimeoutConfig::builder()
+                .operation_timeout(std::time::Duration::from_secs(30))
+                .build()
+        )
         .load()
         .await;
     let client = aws_sdk_kms::Client::new(&config);

     let signer = AwsSigner::new(client, self.key_id.clone(), chain_id).await?;
     Ok(signer)
 }
🤖 Prompt for AI Agents
In core/src/credentials.rs around lines 48 to 58, the AWS SDK client is created
without any timeout configuration, which can cause requests to hang
indefinitely. Modify the AWS SDK client or config builder to include a timeout
setting for AWS operations, ensuring that requests will fail gracefully if they
exceed the specified duration. Use the appropriate timeout configuration method
provided by the AWS SDK or underlying HTTP client to set a reasonable timeout
value.

}
143 changes: 143 additions & 0 deletions core/src/error.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
use std::fmt::Debug;

use crate::defs::AddressDef;
use alloy::{
primitives::Address,
transports::{
RpcError as AlloyRpcError, TransportErrorKind, http::reqwest::header::InvalidHeaderValue,
},
};
use alloy_signer_aws::AwsSignerError;
use aws_sdk_kms::error::SdkError;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use thirdweb_core::error::ThirdwebError;
Expand Down Expand Up @@ -242,11 +246,150 @@ pub enum EngineError {
#[error("Thirdweb error: {message}")]
ThirdwebError { message: String },

#[schema(title = "AWS KMS Error")]
#[error(transparent)]
#[serde(rename_all = "camelCase")]
AwsKmsSignerError {
#[serde(flatten)]
error: SerialisableAwsSignerError,
},

#[schema(title = "Engine Internal Error")]
#[error("Internal error: {message}")]
InternalError { message: String },
}

#[derive(thiserror::Error, Debug, Serialize, Clone, Deserialize, utoipa::ToSchema)]
#[serde(rename_all = "SCREAMING_SNAKE_CASE", tag = "type")]
pub enum SerialisableAwsSdkError {
/// The request failed during construction. It was not dispatched over the network.
#[error("Construction failure: {message}")]
ConstructionFailure { message: String },

/// The request failed due to a timeout. The request MAY have been sent and received.
#[error("Timeout error: {message}")]
TimeoutError { message: String },

/// The request failed during dispatch. An HTTP response was not received. The request MAY
/// have been sent.
#[error("Dispatch failure: {message}")]
DispatchFailure { message: String },

/// A response was received but it was not parseable according the the protocol (for example
/// the server hung up without sending a complete response)
#[error("Response error: {message}")]
ResponseError { message: String },

/// An error response was received from the service
#[error("Service error: {message}")]
ServiceError { message: String },

#[error("Other error: {message}")]
Other { message: String },
}

#[derive(Error, Debug, Serialize, Clone, Deserialize, utoipa::ToSchema)]
#[serde(rename_all = "SCREAMING_SNAKE_CASE", tag = "type")]
pub enum SerialisableAwsSignerError {
/// Thrown when the AWS KMS API returns a signing error.
#[error(transparent)]
Sign {
aws_sdk_error: SerialisableAwsSdkError,
},

/// Thrown when the AWS KMS API returns an error.
#[error(transparent)]
GetPublicKey {
aws_sdk_error: SerialisableAwsSdkError,
},

/// [`ecdsa`] error.
#[error("ECDSA error: {message}")]
K256 { message: String },

/// [`spki`] error.
#[error("SPKI error: {message}")]
Spki { message: String },

/// [`hex`](mod@hex) error.
#[error("Hex error: {message}")]
Hex { message: String },

/// Thrown when the AWS KMS API returns a response without a signature.
#[error("signature not found in response")]
SignatureNotFound,

/// Thrown when the AWS KMS API returns a response without a public key.
#[error("public key not found in response")]
PublicKeyNotFound,

#[error("Unknown error: {message}")]
Unknown { message: String },
}

impl<T: Debug> From<SdkError<T>> for SerialisableAwsSdkError {
fn from(err: SdkError<T>) -> Self {
match err {
SdkError::ConstructionFailure(err) => SerialisableAwsSdkError::ConstructionFailure {
message: format!("{:?}", err),
},
SdkError::TimeoutError(err) => SerialisableAwsSdkError::TimeoutError {
message: format!("{:?}", err),
},
SdkError::DispatchFailure(err) => SerialisableAwsSdkError::DispatchFailure {
message: format!("{:?}", err),
},
SdkError::ResponseError(err) => SerialisableAwsSdkError::ResponseError {
message: format!("{:?}", err),
},
SdkError::ServiceError(err) => SerialisableAwsSdkError::ServiceError {
message: format!("{:?}", err),
},
_ => SerialisableAwsSdkError::Other {
message: format!("{:?}", err),
},
}
}
}

impl From<AwsSignerError> for EngineError {
fn from(err: AwsSignerError) -> Self {
match err {
AwsSignerError::Sign(err) => EngineError::AwsKmsSignerError {
error: SerialisableAwsSignerError::Sign {
aws_sdk_error: err.into(),
},
},
AwsSignerError::GetPublicKey(err) => EngineError::AwsKmsSignerError {
error: SerialisableAwsSignerError::GetPublicKey {
aws_sdk_error: err.into(),
},
},
AwsSignerError::K256(err) => EngineError::AwsKmsSignerError {
error: SerialisableAwsSignerError::K256 {
message: err.to_string(),
},
},
AwsSignerError::Spki(err) => EngineError::AwsKmsSignerError {
error: SerialisableAwsSignerError::Spki {
message: err.to_string(),
},
},
AwsSignerError::Hex(err) => EngineError::AwsKmsSignerError {
error: SerialisableAwsSignerError::Hex {
message: err.to_string(),
},
},
AwsSignerError::SignatureNotFound => EngineError::AwsKmsSignerError {
error: SerialisableAwsSignerError::SignatureNotFound,
},
AwsSignerError::PublicKeyNotFound => EngineError::AwsKmsSignerError {
error: SerialisableAwsSignerError::PublicKeyNotFound,
},
}
}
}

impl From<vault_sdk::error::VaultError> for EngineError {
fn from(err: vault_sdk::error::VaultError) -> Self {
let message = match &err {
Expand Down
12 changes: 6 additions & 6 deletions core/src/execution_options/aa.rs
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
use crate::{
constants::{DEFAULT_FACTORY_ADDRESS_V0_6, ENTRYPOINT_ADDRESS_V0_6},
defs::AddressDef,
error::EngineError,
use crate::{constants::DEFAULT_FACTORY_ADDRESS_V0_6, defs::AddressDef, error::EngineError};
use alloy::{
hex::FromHex,
primitives::{Address, Bytes},
};
use alloy::{hex::FromHex, primitives::{Address, Bytes}};
use engine_aa_types::{ENTRYPOINT_ADDRESS_V0_6, ENTRYPOINT_ADDRESS_V0_7};
use schemars::JsonSchema;
use serde::{Deserialize, Deserializer, Serialize};

use crate::constants::{DEFAULT_FACTORY_ADDRESS_V0_7, ENTRYPOINT_ADDRESS_V0_7};
use crate::constants::DEFAULT_FACTORY_ADDRESS_V0_7;

#[derive(Deserialize, Serialize, Debug, JsonSchema, Clone, Copy, utoipa::ToSchema)]
pub enum EntrypointVersion {
Expand Down
Loading