use axum::http::StatusCode;
use nameth::NamedEnumValues as _;
use nameth::nameth;
use openssl::error::ErrorStack;
use openssl::nid::Nid;
use openssl::x509::X509Name;
use openssl::x509::X509NameBuilder;
use crate::http_error::IsHttpError;
pub(super) fn make_name(args: CertitficateName) -> Result<X509Name, MakeNameError> {
let mut name = X509NameBuilder::new().map_err(MakeNameError::NewBuilder)?;
let mut set = |nid, value| {
let Some(value) = value else {
return Ok::<_, MakeNameError>(());
};
match name.append_entry_by_nid(nid, value) {
Ok(()) => Ok(()),
Err(error) => {
let nid = nid
.long_name()
.map_err(|error| MakeNameError::InvalidField { error, nid })?
.to_owned();
let value = value.to_owned();
Err(Box::new(InvalidValueError { error, nid, value }).into())
}
}
};
let country = args.country.map(String::from_iter);
set(Nid::COUNTRYNAME, country.as_deref())?;
set(Nid::STATEORPROVINCENAME, args.state_or_province)?;
set(Nid::LOCALITYNAME, args.locality)?;
set(Nid::ORGANIZATIONNAME, args.organization)?;
set(Nid::COMMONNAME, args.common_name)?;
Ok(name.build())
}
#[derive(Clone, Copy, Default, Debug, PartialEq, Eq)]
pub struct CertitficateName<'t> {
pub country: Option<[char; 2]>,
pub state_or_province: Option<&'t str>,
pub locality: Option<&'t str>,
pub organization: Option<&'t str>,
pub common_name: Option<&'t str>,
}
#[nameth]
#[derive(thiserror::Error, Debug)]
pub enum MakeNameError {
#[error("[{n}] Failed to create a new LDAP Name builder: {0}", n = self.name())]
NewBuilder(ErrorStack),
#[error("[{n}] {0}", n = self.name())]
InvalidValue(#[from] Box<InvalidValueError>),
#[error("[{n}] Invalid LDAP field NID={nid}: {error}", n = self.name(), nid = nid.as_raw())]
InvalidField { error: ErrorStack, nid: Nid },
}
#[nameth]
#[derive(thiserror::Error, Debug)]
#[error("Failed to set LDAP field {nid} = '{value}': {error}")]
pub struct InvalidValueError {
error: ErrorStack,
nid: String,
value: String,
}
impl IsHttpError for MakeNameError {
fn status_code(&self) -> StatusCode {
match self {
Self::NewBuilder { .. } => StatusCode::INTERNAL_SERVER_ERROR,
Self::InvalidValue { .. } => StatusCode::BAD_REQUEST,
Self::InvalidField { .. } => StatusCode::INTERNAL_SERVER_ERROR,
}
}
}
#[cfg(test)]
mod tests {
#[test]
fn common_name() {
let name = super::make_name(super::CertitficateName {
common_name: Some("Test cert"),
..Default::default()
})
.unwrap();
let name: String = name.entries().map(|entry| format!("{entry:?}")).collect();
assert_eq!("\"commonName = \\\"Test cert\\\"\"", format!("{name:?}"));
}
#[test]
fn country() {
let name = super::make_name(super::CertitficateName {
common_name: Some("Test cert"),
country: Some(['F', 'R']),
..Default::default()
})
.unwrap();
let name: String = name.entries().map(|entry| format!("{entry:?}")).collect();
assert_eq!(
"\"countryName = \\\"FR\\\"commonName = \\\"Test cert\\\"\"",
format!("{name:?}")
);
}
#[test]
fn error() {
let too_long: String = (0..200).map(|_| 'X').collect();
let Err(error) = super::make_name(super::CertitficateName {
common_name: Some(&too_long),
..Default::default()
}) else {
panic!();
};
let super::MakeNameError::InvalidValue(invalid_value) = &error else {
panic!();
};
assert_eq!(too_long, invalid_value.value);
assert_eq!("commonName", invalid_value.nid);
assert!(error.to_string().starts_with(&format!(
"[InvalidValue] Failed to set LDAP field commonName = '{too_long}': "
),));
}
}