use std::borrow::Cow;
use bon::bon;
use google_cloud_kms_v1::{
client::KeyManagementService, model::crypto_key_version::CryptoKeyVersionAlgorithm,
};
use huskarl_core::crypto::signer::{JwsSigningKey, SigningKeyMetadata};
use p256::ecdsa::signature;
use snafu::prelude::*;
#[derive(Debug, Snafu)]
#[non_exhaustive]
pub enum SetupError {
GetCryptoKey {
source: google_cloud_kms_v1::Error,
},
ListCryptoKeyVersions {
source: google_cloud_kms_v1::Error,
},
NoEnabledCryptoKeyVersions,
UnsupportedAlgorithm {
algorithm: CryptoKeyVersionAlgorithm,
},
InvalidKeyVersionName,
}
#[derive(Debug, Snafu)]
#[non_exhaustive]
pub enum SigningError {
AsymmetricSign {
source: google_cloud_kms_v1::Error,
},
SignatureConversion {
source: signature::Error,
},
MismatchedAlgorithmInfo,
}
impl huskarl_core::Error for SigningError {
fn is_retryable(&self) -> bool {
match self {
SigningError::AsymmetricSign { source } => source.is_timeout() || source.is_exhausted(),
SigningError::SignatureConversion { .. } | SigningError::MismatchedAlgorithmInfo => {
false
}
}
}
}
#[derive(Debug, Clone)]
pub struct AsymmetricJwsKey {
kms_client: KeyManagementService,
resource_name: String,
key_metadata: SigningKeyMetadata,
}
#[bon]
impl AsymmetricJwsKey {
async fn resolve_resource_name(
key_name: &str,
key_version: Option<String>,
kms_client: &KeyManagementService,
) -> Result<String, SetupError> {
if let Some(supplied_version) = key_version {
Ok(supplied_version)
} else {
Ok(kms_client
.list_crypto_key_versions()
.set_parent(key_name)
.set_page_size(1)
.set_filter("state=ENABLED")
.set_order_by("name desc")
.send()
.await
.context(ListCryptoKeyVersionsSnafu)?
.crypto_key_versions
.into_iter()
.next()
.ok_or(NoEnabledCryptoKeyVersionsSnafu.build())?
.name
.rsplit('/')
.next()
.ok_or(InvalidKeyVersionNameSnafu.build())?
.to_string())
}
}
#[builder(finish_fn = build)]
#[allow(clippy::type_complexity)]
pub async fn builder(
#[builder(into)]
key_name: String,
#[builder(into)]
key_version: Option<String>,
kms_client: KeyManagementService,
#[builder(with = |f: impl Fn(&str) -> String + 'static| Box::new(f))]
with_kid_from_key_version: Option<Box<dyn FnOnce(&str) -> String>>,
) -> Result<Self, SetupError> {
let resolved_key_version =
Self::resolve_resource_name(&key_name, key_version, &kms_client).await?;
let resolved_key_version_name =
format!("{key_name}/cryptoKeyVersions/{resolved_key_version}");
let kid = with_kid_from_key_version.map(|f| f(&resolved_key_version));
let key_metadata = get_signing_key_metadata_for_resource(
&kms_client,
&resolved_key_version_name,
kid.as_deref(),
)
.await?;
Ok(Self {
kms_client,
resource_name: resolved_key_version_name,
key_metadata,
})
}
}
impl JwsSigningKey for AsymmetricJwsKey {
type Error = SigningError;
fn key_metadata(&self) -> Cow<'_, SigningKeyMetadata> {
Cow::Borrowed(&self.key_metadata)
}
async fn sign_unchecked(&self, input: &[u8]) -> Result<Vec<u8>, Self::Error> {
let response = self
.kms_client
.asymmetric_sign()
.set_name(&self.resource_name)
.set_data(input.to_vec())
.send()
.await
.context(AsymmetricSignSnafu)?;
ensure!(
response.name == self.resource_name,
MismatchedAlgorithmInfoSnafu
);
let signature = response.signature.to_vec();
match self.key_metadata.jws_algorithm.as_ref() {
"ES256" => convert_ecdsa_der_to_fixed(&signature, EcDsaVariant::P256)
.context(SignatureConversionSnafu),
"ES384" => convert_ecdsa_der_to_fixed(&signature, EcDsaVariant::P384)
.context(SignatureConversionSnafu),
_ => Ok(signature),
}
}
}
async fn get_signing_key_metadata_for_resource(
kms_client: &KeyManagementService,
resource_name: &str,
kid: Option<&str>,
) -> Result<SigningKeyMetadata, SetupError> {
let key_version = kms_client
.get_crypto_key_version()
.set_name(resource_name)
.send()
.await
.context(GetCryptoKeySnafu)?;
let jws_algorithm =
get_jws_algorithm(&key_version.algorithm).with_context(|| UnsupportedAlgorithmSnafu {
algorithm: key_version.algorithm,
})?;
Ok(SigningKeyMetadata::builder()
.jws_algorithm(jws_algorithm)
.maybe_key_id(kid)
.build())
}
fn get_jws_algorithm(algorithm: &CryptoKeyVersionAlgorithm) -> Option<&'static str> {
use CryptoKeyVersionAlgorithm::{
EcSignEd25519, EcSignP256Sha256, EcSignP384Sha384, RsaSignPkcs12048Sha256,
RsaSignPkcs13072Sha256, RsaSignPkcs14096Sha256, RsaSignPkcs14096Sha512,
RsaSignPss2048Sha256, RsaSignPss3072Sha256, RsaSignPss4096Sha256, RsaSignPss4096Sha512,
};
match algorithm {
RsaSignPss2048Sha256 | RsaSignPss3072Sha256 | RsaSignPss4096Sha256 => Some("PS256"),
RsaSignPss4096Sha512 => Some("PS512"),
RsaSignPkcs12048Sha256 | RsaSignPkcs13072Sha256 | RsaSignPkcs14096Sha256 => Some("RS256"),
RsaSignPkcs14096Sha512 => Some("RS512"),
EcSignP256Sha256 => Some("ES256"),
EcSignP384Sha384 => Some("ES384"),
EcSignEd25519 => Some("Ed25519"),
_ => None,
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
enum EcDsaVariant {
P256,
P384,
}
fn convert_ecdsa_der_to_fixed(
der_sig: &[u8],
variant: EcDsaVariant,
) -> Result<Vec<u8>, signature::Error> {
match variant {
EcDsaVariant::P256 => {
let sig = p256::ecdsa::Signature::from_der(der_sig)?;
Ok(sig.to_bytes().to_vec())
}
EcDsaVariant::P384 => {
let sig = p384::ecdsa::Signature::from_der(der_sig)?;
Ok(sig.to_bytes().to_vec())
}
}
}