#[cfg(all(not(target_arch = "wasm32"), feature = "native-tls"))]
use huskarl_core::secrets::SecretBytes;
#[cfg(all(
not(target_arch = "wasm32"),
any(feature = "rustls-tls", feature = "native-tls")
))]
use huskarl_core::secrets::{Secret, SecretString};
use huskarl_core::{
Error,
platform::{MaybeSendBoxFuture, MaybeSendSync},
};
#[cfg(all(
not(target_arch = "wasm32"),
any(feature = "rustls-tls", feature = "native-tls")
))]
use snafu::ResultExt as _;
#[cfg(all(
not(target_arch = "wasm32"),
any(feature = "rustls-tls", feature = "native-tls")
))]
#[derive(Debug, snafu::Snafu, huskarl_macros::Classify)]
#[non_exhaustive]
pub(crate) enum MtlsError {
#[snafu(display("fetching mTLS {what} secret"))]
FetchingSecret {
what: &'static str,
source: Error,
},
#[snafu(display("parsing mTLS identity"))]
#[classify(no)]
ParsingIdentity {
source: reqwest::Error,
},
}
pub struct ConfiguredBuilder {
pub(crate) builder: reqwest::ClientBuilder,
#[cfg(all(
not(target_arch = "wasm32"),
any(feature = "rustls-tls", feature = "native-tls")
))]
pub(crate) identity: Option<reqwest::Identity>,
}
impl ConfiguredBuilder {
#[must_use]
pub fn new(builder: reqwest::ClientBuilder) -> Self {
Self {
builder,
#[cfg(all(
not(target_arch = "wasm32"),
any(feature = "rustls-tls", feature = "native-tls")
))]
identity: None,
}
}
#[cfg(all(
not(target_arch = "wasm32"),
any(feature = "rustls-tls", feature = "native-tls")
))]
#[must_use]
pub fn with_identity(mut self, identity: reqwest::Identity) -> Self {
self.identity = Some(identity);
self
}
}
pub trait MtlsProvider: MaybeSendSync {
fn apply(
&self,
builder: reqwest::ClientBuilder,
) -> MaybeSendBoxFuture<'_, Result<ConfiguredBuilder, Error>>;
fn uses_mtls(&self) -> bool;
}
#[derive(Debug, Clone, Copy, Default)]
pub struct NoMtls;
impl MtlsProvider for NoMtls {
fn apply(
&self,
builder: reqwest::ClientBuilder,
) -> MaybeSendBoxFuture<'_, Result<ConfiguredBuilder, Error>> {
Box::pin(async move { Ok(ConfiguredBuilder::new(builder)) })
}
fn uses_mtls(&self) -> bool {
false
}
}
#[cfg(all(not(target_arch = "wasm32"), feature = "rustls-tls"))]
pub struct MtlsPem<S: Secret<Output = SecretString>> {
secret: S,
}
#[cfg(all(not(target_arch = "wasm32"), feature = "rustls-tls"))]
impl<S: Secret<Output = SecretString>> MtlsPem<S> {
pub fn new(secret: S) -> Self {
Self { secret }
}
}
#[cfg(all(not(target_arch = "wasm32"), feature = "rustls-tls"))]
impl<S: Secret<Output = SecretString>> MtlsProvider for MtlsPem<S> {
fn apply(
&self,
builder: reqwest::ClientBuilder,
) -> MaybeSendBoxFuture<'_, Result<ConfiguredBuilder, Error>> {
Box::pin(async move {
let secret_output = self
.secret
.get_secret_value()
.await
.context(FetchingSecretSnafu { what: "PEM" })?;
let identity =
reqwest::Identity::from_pem(secret_output.value.expose_secret().as_bytes())
.map_err(parse_identity_error)?;
Ok(ConfiguredBuilder::new(builder.identity(identity.clone())).with_identity(identity))
})
}
fn uses_mtls(&self) -> bool {
true
}
}
#[cfg(all(not(target_arch = "wasm32"), feature = "native-tls"))]
pub struct MtlsPkcs12<D: Secret<Output = SecretBytes>, P: Secret<Output = SecretString>> {
der: D,
password: P,
}
#[cfg(all(not(target_arch = "wasm32"), feature = "native-tls"))]
impl<D: Secret<Output = SecretBytes>, P: Secret<Output = SecretString>> MtlsPkcs12<D, P> {
pub fn new(der: D, password: P) -> Self {
Self { der, password }
}
}
#[cfg(all(not(target_arch = "wasm32"), feature = "native-tls"))]
impl<D: Secret<Output = SecretBytes>, P: Secret<Output = SecretString>> MtlsProvider
for MtlsPkcs12<D, P>
{
fn apply(
&self,
builder: reqwest::ClientBuilder,
) -> MaybeSendBoxFuture<'_, Result<ConfiguredBuilder, Error>> {
Box::pin(async move {
let der = self
.der
.get_secret_value()
.await
.context(FetchingSecretSnafu { what: "DER" })?;
let password = self
.password
.get_secret_value()
.await
.context(FetchingSecretSnafu { what: "password" })?;
let identity = reqwest::Identity::from_pkcs12_der(
der.value.expose_secret(),
password.value.expose_secret(),
)
.map_err(parse_identity_error)?;
Ok(ConfiguredBuilder::new(builder.identity(identity.clone())).with_identity(identity))
})
}
fn uses_mtls(&self) -> bool {
true
}
}
#[cfg(all(not(target_arch = "wasm32"), feature = "native-tls"))]
pub struct MtlsPkcs8Pem<K: Secret<Output = SecretString>> {
cert_chain: String,
key: K,
}
#[cfg(all(not(target_arch = "wasm32"), feature = "native-tls"))]
impl<K: Secret<Output = SecretString>> MtlsPkcs8Pem<K> {
pub fn new(cert_chain: impl Into<String>, key: K) -> Self {
Self {
cert_chain: cert_chain.into(),
key,
}
}
}
#[cfg(all(not(target_arch = "wasm32"), feature = "native-tls"))]
impl<K: Secret<Output = SecretString>> MtlsProvider for MtlsPkcs8Pem<K> {
fn apply(
&self,
builder: reqwest::ClientBuilder,
) -> MaybeSendBoxFuture<'_, Result<ConfiguredBuilder, Error>> {
Box::pin(async move {
let key = self
.key
.get_secret_value()
.await
.context(FetchingSecretSnafu {
what: "private key",
})?;
let identity = reqwest::Identity::from_pkcs8_pem(
self.cert_chain.as_bytes(),
key.value.expose_secret().as_bytes(),
)
.map_err(parse_identity_error)?;
Ok(ConfiguredBuilder::new(builder.identity(identity.clone())).with_identity(identity))
})
}
fn uses_mtls(&self) -> bool {
true
}
}
#[cfg(all(
not(target_arch = "wasm32"),
any(feature = "rustls-tls", feature = "native-tls")
))]
#[track_caller]
fn parse_identity_error(source: reqwest::Error) -> Error {
Error::from(MtlsError::ParsingIdentity { source })
}