use crate::FaucetError;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
#[derive(Clone, Serialize, Deserialize, JsonSchema, Default, PartialEq, Eq)]
pub struct TlsClientConfig {
pub client_cert: Option<String>,
pub client_key: Option<String>,
pub client_identity_pkcs12: Option<String>,
pub pkcs12_password: Option<String>,
pub min_version: Option<String>,
}
impl std::fmt::Debug for TlsClientConfig {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let secret = |v: &Option<String>| v.as_ref().map(|_| "***");
f.debug_struct("TlsClientConfig")
.field("client_cert", &self.client_cert)
.field("client_key", &secret(&self.client_key))
.field("client_identity_pkcs12", &self.client_identity_pkcs12)
.field("pkcs12_password", &secret(&self.pkcs12_password))
.field("min_version", &self.min_version)
.finish()
}
}
impl TlsClientConfig {
pub fn validate(&self) -> Result<(), FaucetError> {
let has_pem = self.client_cert.is_some() || self.client_key.is_some();
let has_p12 = self.client_identity_pkcs12.is_some();
if has_pem && has_p12 {
return Err(FaucetError::Config(
"tls: specify either a PEM pair (client_cert + client_key) or \
client_identity_pkcs12, not both"
.into(),
));
}
if !has_pem && !has_p12 {
return Err(FaucetError::Config(
"tls: provide a PEM pair (client_cert + client_key) or \
client_identity_pkcs12"
.into(),
));
}
if has_pem && (self.client_cert.is_none() || self.client_key.is_none()) {
return Err(FaucetError::Config(
"tls: client_cert and client_key must be provided together".into(),
));
}
if let Some(v) = &self.min_version
&& !matches!(v.as_str(), "1.2" | "1.3")
{
return Err(FaucetError::Config(format!(
"tls: unsupported min_version {v:?} (expected \"1.2\" or \"1.3\")"
)));
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::TlsClientConfig;
#[test]
fn debug_never_prints_key_material() {
let cfg = TlsClientConfig {
client_cert: Some("CERT".into()),
client_key: Some("PRIVATE-KEY-PEM".into()),
client_identity_pkcs12: None,
pkcs12_password: Some("p12-pass".into()),
min_version: None,
};
let text = format!("{cfg:?}");
assert!(
!text.contains("PRIVATE-KEY-PEM") && !text.contains("p12-pass"),
"{text}"
);
assert!(text.contains("CERT") && text.contains("***"), "{text}");
}
fn pem() -> TlsClientConfig {
TlsClientConfig {
client_cert: Some("cert".into()),
client_key: Some("key".into()),
..Default::default()
}
}
#[test]
fn valid_pem_pair_passes() {
assert!(pem().validate().is_ok());
}
#[test]
fn valid_pkcs12_passes() {
let c = TlsClientConfig {
client_identity_pkcs12: Some("/path/id.p12".into()),
pkcs12_password: Some("pw".into()),
..Default::default()
};
assert!(c.validate().is_ok());
}
#[test]
fn pem_and_pkcs12_together_is_rejected() {
let c = TlsClientConfig {
client_cert: Some("cert".into()),
client_key: Some("key".into()),
client_identity_pkcs12: Some("/path/id.p12".into()),
..Default::default()
};
assert!(c.validate().is_err());
}
#[test]
fn empty_config_is_rejected() {
assert!(TlsClientConfig::default().validate().is_err());
}
#[test]
fn half_pem_pair_is_rejected() {
let cert_only = TlsClientConfig {
client_cert: Some("cert".into()),
..Default::default()
};
assert!(cert_only.validate().is_err());
let key_only = TlsClientConfig {
client_key: Some("key".into()),
..Default::default()
};
assert!(key_only.validate().is_err());
}
#[test]
fn min_version_is_validated() {
let mut c = pem();
c.min_version = Some("1.3".into());
assert!(c.validate().is_ok());
c.min_version = Some("1.2".into());
assert!(c.validate().is_ok());
c.min_version = Some("1.1".into());
assert!(c.validate().is_err());
}
}