use async_trait::async_trait;
use aws_config::BehaviorVersion;
use aws_sdk_kms::{
primitives::Blob,
types::{KeySpec, MessageType, SigningAlgorithmSpec},
Client as KmsClient,
};
use crate::signer::{PlatformSigner, SignerError, SigningAlgorithm};
pub const ENV_KEY_ID: &str = "MOCKFORGE_PLATFORM_SIGNING_KMS_KEY_ID";
#[derive(Debug, Clone)]
pub struct AwsKmsSigner {
client: KmsClient,
key_id: String,
algorithm: SigningAlgorithm,
}
impl AwsKmsSigner {
pub async fn from_env() -> Result<Self, SignerError> {
let key_id = std::env::var(ENV_KEY_ID).map_err(|_| SignerError::MissingEnv(ENV_KEY_ID))?;
if key_id.trim().is_empty() {
return Err(SignerError::InvalidKeyId(format!("{ENV_KEY_ID} is empty")));
}
Self::from_key_id(key_id).await
}
pub async fn from_key_id(key_id: impl Into<String>) -> Result<Self, SignerError> {
let key_id = key_id.into();
let aws_config = aws_config::defaults(BehaviorVersion::latest()).load().await;
let client = KmsClient::new(&aws_config);
let algorithm = probe_algorithm(&client, &key_id).await?;
tracing::info!(
key_id = %key_id,
algorithm = ?algorithm,
"AwsKmsSigner ready"
);
Ok(Self {
client,
key_id,
algorithm,
})
}
pub fn with_client(
client: KmsClient,
key_id: impl Into<String>,
algorithm: SigningAlgorithm,
) -> Self {
Self {
client,
key_id: key_id.into(),
algorithm,
}
}
}
#[async_trait]
impl PlatformSigner for AwsKmsSigner {
fn key_id(&self) -> &str {
&self.key_id
}
fn algorithm(&self) -> SigningAlgorithm {
self.algorithm
}
async fn public_key_der(&self) -> Result<Vec<u8>, SignerError> {
let out = self
.client
.get_public_key()
.key_id(&self.key_id)
.send()
.await
.map_err(|e| SignerError::Backend(format!("kms GetPublicKey: {e}")))?;
let bytes = out
.public_key()
.ok_or_else(|| SignerError::UnexpectedPublicKey("response had no public key".into()))?;
Ok(bytes.as_ref().to_vec())
}
async fn sign(&self, message: &[u8]) -> Result<Vec<u8>, SignerError> {
let signing_alg = match self.algorithm {
SigningAlgorithm::EcdsaSha256P256 => SigningAlgorithmSpec::EcdsaSha256,
SigningAlgorithm::EcdsaSha384P384 => SigningAlgorithmSpec::EcdsaSha384,
};
let out = self
.client
.sign()
.key_id(&self.key_id)
.message(Blob::new(message.to_vec()))
.message_type(MessageType::Raw)
.signing_algorithm(signing_alg)
.send()
.await
.map_err(|e| SignerError::Backend(format!("kms Sign: {e}")))?;
let sig = out
.signature()
.ok_or_else(|| SignerError::Backend("kms Sign returned no signature".into()))?;
Ok(sig.as_ref().to_vec())
}
}
async fn probe_algorithm(
client: &KmsClient,
key_id: &str,
) -> Result<SigningAlgorithm, SignerError> {
let out = client
.get_public_key()
.key_id(key_id)
.send()
.await
.map_err(|e| SignerError::Backend(format!("kms GetPublicKey (probe): {e}")))?;
let spec = out
.key_spec()
.ok_or_else(|| SignerError::Backend("kms GetPublicKey returned no KeySpec".into()))?;
match spec {
KeySpec::EccNistP256 => Ok(SigningAlgorithm::EcdsaSha256P256),
KeySpec::EccNistP384 => Ok(SigningAlgorithm::EcdsaSha384P384),
other => Err(SignerError::Backend(format!(
"unsupported KMS KeySpec {other:?}; expected ECC_NIST_P256 or ECC_NIST_P384"
))),
}
}