use std::borrow::Cow;
use std::sync::Arc;
use bon::bon;
use google_cloud_kms_v1::{
client::KeyManagementService, model::crypto_key_version::CryptoKeyVersionAlgorithm,
};
use huskarl_core::BoxedError;
use huskarl_core::crypto::KeyMatchStrength;
use huskarl_core::crypto::signer::{JwsSigner, JwsSignerSelector};
use huskarl_core::crypto::verifier::{
BoxedJwsVerifier, JwsVerifier, KeyMatch, MultiKeyVerifier, VerifyError,
};
use snafu::prelude::*;
use super::super::version::{self, VersionStrategy};
use super::setup;
use super::{
GetCryptoKeyVersionSnafu, ListCryptoKeyVersionsSnafu, NoEnabledCryptoKeyVersionsSnafu,
ResolveVersionSnafu, UnsupportedAlgorithmSnafu,
};
pub use super::{KeyError, SetupError};
type KidMapper = Arc<dyn Fn(&str) -> String + Send + Sync>;
#[derive(Debug, Snafu)]
#[non_exhaustive]
pub enum SigningError {
MacSign {
source: google_cloud_kms_v1::Error,
},
MismatchedKeyInfo,
}
#[derive(Debug, Snafu)]
#[non_exhaustive]
pub enum VerificationError {
MacVerify {
source: google_cloud_kms_v1::Error,
},
}
impl huskarl_core::Error for VerificationError {
fn is_retryable(&self) -> bool {
match self {
VerificationError::MacVerify { source } => source.is_timeout() || source.is_exhausted(),
}
}
}
impl huskarl_core::Error for SigningError {
fn is_retryable(&self) -> bool {
match self {
SigningError::MacSign { source } => source.is_timeout() || source.is_exhausted(),
SigningError::MismatchedKeyInfo => false,
}
}
}
#[derive(Debug, Clone)]
pub struct KeyVersion {
kms_client: KeyManagementService,
resource_name: String,
jws_algorithm: String,
key_id: Option<String>,
}
#[bon]
impl KeyVersion {
#[builder(finish_fn = build)]
pub async fn builder(
#[builder(into)]
resource_name: String,
kms_client: KeyManagementService,
#[builder(with = |f: impl Fn(&str) -> String + Send + Sync + 'static| Arc::new(f))]
with_kid_from_key_version: Option<KidMapper>,
) -> Result<Self, SetupError> {
build_key_version(resource_name, kms_client, with_kid_from_key_version).await
}
}
impl JwsSignerSelector for KeyVersion {
type Signer = Self;
fn select_signer(&self) -> Self::Signer {
self.clone()
}
}
impl JwsSigner for KeyVersion {
type Error = SigningError;
fn jws_algorithm(&self) -> Cow<'_, str> {
Cow::Borrowed(&self.jws_algorithm)
}
fn key_id(&self) -> Option<Cow<'_, str>> {
self.key_id.as_deref().map(Cow::Borrowed)
}
async fn sign(&self, input: &[u8]) -> Result<Vec<u8>, Self::Error> {
let response = self
.kms_client
.mac_sign()
.set_name(&self.resource_name)
.set_data(input.to_vec())
.send()
.await
.context(MacSignSnafu)?;
ensure!(response.name == self.resource_name, MismatchedKeyInfoSnafu);
Ok(response.mac.to_vec())
}
}
impl JwsVerifier for KeyVersion {
type Error = VerificationError;
fn key_match(&self, key_match: &KeyMatch<'_>) -> Option<KeyMatchStrength> {
if key_match.alg != self.jws_algorithm {
return None;
}
match (key_match.kid, self.key_id.as_deref()) {
(Some(jwt_kid), Some(my_kid)) if jwt_kid != my_kid => None,
(Some(_), Some(_)) => Some(KeyMatchStrength::ByKeyId),
_ => Some(KeyMatchStrength::ByAlgorithm),
}
}
async fn verify(
&self,
input: &[u8],
signature: &[u8],
_key_match: &KeyMatch<'_>,
) -> Result<(), VerifyError<Self::Error>> {
let response = self
.kms_client
.mac_verify()
.set_name(&self.resource_name)
.set_data(input.to_vec())
.set_mac(signature.to_vec())
.send()
.await
.context(MacVerifySnafu)
.map_err(|source| VerifyError::Other { source })?;
if response.success {
Ok(())
} else {
Err(VerifyError::SignatureMismatch)
}
}
}
#[derive(Debug, Clone)]
pub struct SigningKey {
key_version: KeyVersion,
}
#[bon]
impl SigningKey {
#[builder(finish_fn = build)]
pub async fn builder(
#[builder(into)]
key_name: String,
kms_client: KeyManagementService,
#[builder(default)]
strategy: VersionStrategy,
#[builder(with = |f: impl Fn(&str) -> String + Send + Sync + 'static| Arc::new(f))]
with_kid_from_key_version: Option<KidMapper>,
) -> Result<Self, KeyError> {
let version_id = version::resolve_version(&key_name, &strategy, &kms_client)
.await
.context(ResolveVersionSnafu)?;
let resource_name = format!("{key_name}/cryptoKeyVersions/{version_id}");
let kv_response = kms_client
.get_crypto_key_version()
.set_name(&resource_name)
.send()
.await
.context(GetCryptoKeyVersionSnafu)?;
let resolved_name = if kv_response.name.is_empty() {
resource_name
} else {
kv_response.name
};
let vid = version::version_id_from_resource_name(&resolved_name);
let key_id = with_kid_from_key_version.as_ref().map(|f| f(vid));
let jws_algorithm = get_jws_algorithm(&kv_response.algorithm).ok_or_else(|| {
UnsupportedAlgorithmSnafu {
algorithm: kv_response.algorithm,
}
.build()
})?;
Ok(Self {
key_version: KeyVersion {
kms_client,
resource_name: resolved_name,
jws_algorithm: jws_algorithm.to_string(),
key_id,
},
})
}
}
impl JwsSignerSelector for SigningKey {
type Signer = KeyVersion;
fn select_signer(&self) -> KeyVersion {
self.key_version.clone()
}
}
impl JwsSigner for SigningKey {
type Error = SigningError;
fn jws_algorithm(&self) -> Cow<'_, str> {
self.key_version.jws_algorithm()
}
fn key_id(&self) -> Option<Cow<'_, str>> {
self.key_version.key_id()
}
async fn sign(&self, input: &[u8]) -> Result<Vec<u8>, Self::Error> {
self.key_version.sign(input).await
}
}
#[derive(Debug, Clone)]
pub struct VerifyingKey {
verifier: Arc<MultiKeyVerifier>,
}
#[bon]
impl VerifyingKey {
#[builder(finish_fn = build)]
pub async fn builder(
#[builder(into)]
key_name: String,
kms_client: KeyManagementService,
#[builder(with = |f: impl Fn(&str) -> String + Send + Sync + 'static| Arc::new(f))]
with_kid_from_key_version: Option<KidMapper>,
max_versions: Option<usize>,
) -> Result<Self, KeyError> {
let raw = version::list_enabled_kms_versions(
&kms_client,
&key_name,
max_versions,
Some("name desc"),
)
.await
.context(ListCryptoKeyVersionsSnafu)?;
ensure!(!raw.is_empty(), NoEnabledCryptoKeyVersionsSnafu);
let versions: Vec<KeyVersion> = raw
.iter()
.filter_map(|v| {
let jws_algorithm = get_jws_algorithm(&v.algorithm)?;
let vid = version::version_id_from_resource_name(&v.name);
let key_id = with_kid_from_key_version.as_ref().map(|f| f(vid));
Some(KeyVersion {
kms_client: kms_client.clone(),
resource_name: v.name.clone(),
jws_algorithm: jws_algorithm.to_string(),
key_id,
})
})
.collect();
let verifier = Arc::new(
MultiKeyVerifier::new(versions.into_iter().map(BoxedJwsVerifier::new).collect())
.try_all_on_ambiguous_match(true),
);
Ok(Self { verifier })
}
}
impl JwsVerifier for VerifyingKey {
type Error = BoxedError;
fn key_match(&self, key_match: &KeyMatch<'_>) -> Option<KeyMatchStrength> {
self.verifier.key_match(key_match)
}
async fn verify(
&self,
input: &[u8],
signature: &[u8],
key_match: &KeyMatch<'_>,
) -> Result<(), VerifyError<Self::Error>> {
self.verifier.verify(input, signature, key_match).await
}
}
async fn build_key_version(
resource_name: String,
kms_client: KeyManagementService,
with_kid_from_key_version: Option<KidMapper>,
) -> Result<KeyVersion, SetupError> {
let kv_response = kms_client
.get_crypto_key_version()
.set_name(&resource_name)
.send()
.await
.context(setup::GetCryptoKeyVersionSnafu)?;
let resolved_name = if kv_response.name.is_empty() {
resource_name
} else {
kv_response.name
};
let version_id = version::version_id_from_resource_name(&resolved_name);
let key_id = with_kid_from_key_version.map(|f| f(version_id));
let jws_algorithm =
get_jws_algorithm(&kv_response.algorithm).context(setup::UnsupportedAlgorithmSnafu {
algorithm: kv_response.algorithm,
})?;
Ok(KeyVersion {
kms_client,
resource_name: resolved_name,
jws_algorithm: jws_algorithm.to_string(),
key_id,
})
}
fn get_jws_algorithm(algorithm: &CryptoKeyVersionAlgorithm) -> Option<&'static str> {
use CryptoKeyVersionAlgorithm::{HmacSha256, HmacSha384, HmacSha512};
match algorithm {
HmacSha256 => Some("HS256"),
HmacSha384 => Some("HS384"),
HmacSha512 => Some("HS512"),
_ => None,
}
}