use bon::Builder;
use const_oid::ObjectIdentifier;
use const_oid::db::rfc4519;
use log::warn;
use time::Duration;
use time::OffsetDateTime;
use crate::error::CertKitError;
use x509_cert::attr::AttributeTypeAndValue;
use x509_cert::name::{RdnSequence, RelativeDistinguishedName};
use super::extensions::ToAndFromX509Extension;
pub use crate::cert::extensions::ExtendedKeyUsage;
pub use crate::cert::extensions::ExtendedKeyUsageOption;
use crate::key::PublicKey;
#[derive(Clone, Debug, Builder)]
pub struct CertificateParams {
pub subject: DistinguishedName,
pub subject_public_key: PublicKey,
#[builder(default)]
pub usages: Vec<ExtendedKeyUsageOption>,
#[builder(default)]
pub is_ca: bool,
pub max_path_length: Option<u8>,
#[builder(default)]
pub extensions: Vec<ExtensionParam>,
}
#[derive(Clone, Debug, Builder, Default)]
pub struct DistinguishedName {
pub common_name: String,
pub country: Option<String>,
pub state: Option<String>,
pub locality: Option<String>,
pub organization: Option<String>,
pub organization_unit: Option<String>,
}
fn dn_rdn(oid: ObjectIdentifier, value: &str) -> Result<RelativeDistinguishedName, CertKitError> {
let any = der::Any::new(der::Tag::Utf8String, value.as_bytes())
.map_err(|e| CertKitError::EncodingError(format!("DN attribute value: {e}")))?;
let atv = AttributeTypeAndValue { oid, value: any };
let set = der::asn1::SetOfVec::try_from(vec![atv])
.map_err(|e| CertKitError::EncodingError(format!("DN attribute set: {e}")))?;
Ok(RelativeDistinguishedName(set))
}
impl DistinguishedName {
pub fn as_x509_name(&self) -> Result<x509_cert::name::DistinguishedName, CertKitError> {
let mut rdns: Vec<RelativeDistinguishedName> = Vec::new();
let optional_attrs = [
(rfc4519::C, &self.country),
(rfc4519::ST, &self.state),
(rfc4519::L, &self.locality),
(rfc4519::O, &self.organization),
(rfc4519::OU, &self.organization_unit),
];
for (oid, value) in optional_attrs {
if let Some(value) = value {
if !value.is_empty() {
rdns.push(dn_rdn(oid, value)?);
}
}
}
rdns.push(dn_rdn(rfc4519::CN, &self.common_name)?);
Ok(RdnSequence(rdns))
}
pub fn from_x509_name(
x509dn: &x509_cert::name::DistinguishedName,
) -> Result<Self, CertKitError> {
let mut common_name = String::new();
let mut organization_unit = None;
let mut organization = None;
let mut locality = None;
let mut state = None;
let mut country = None;
for rdn in x509dn.0.iter() {
for attr in rdn.0.iter() {
let value = attr
.value
.decode_as::<der::asn1::Utf8StringRef<'_>>()
.map(|s| s.as_str().to_owned())
.or_else(|_| {
attr.value
.decode_as::<der::asn1::PrintableStringRef<'_>>()
.map(|s| s.as_str().to_owned())
})
.or_else(|_| {
attr.value
.decode_as::<der::asn1::Ia5StringRef<'_>>()
.map(|s| s.as_str().to_owned())
})
.map_err(|_| {
CertKitError::DecodingError(format!(
"DN attribute {} value cannot be decoded as a string",
attr.oid
))
})?;
match attr.oid {
oid if oid == rfc4519::CN => common_name = value,
oid if oid == rfc4519::OU => organization_unit = Some(value),
oid if oid == rfc4519::O => organization = Some(value),
oid if oid == rfc4519::L => locality = Some(value),
oid if oid == rfc4519::ST => state = Some(value),
oid if oid == rfc4519::C => country = Some(value),
_ => {
warn!("Unknown DN attribute {} value {}", attr.oid, value);
}
}
}
}
Ok(DistinguishedName {
common_name,
organization_unit,
organization,
locality,
state,
country,
})
}
}
#[derive(Copy, Clone, Debug)]
pub struct Validity {
not_before: x509_cert::time::Time,
not_after: x509_cert::time::Time,
}
fn encode_x509_time(dt: OffsetDateTime) -> Result<x509_cert::time::Time, CertKitError> {
let sys_time: std::time::SystemTime = dt.into();
match der::asn1::UtcTime::from_system_time(sys_time) {
Ok(ut) => Ok(x509_cert::time::Time::UtcTime(ut)),
Err(_) => {
let gt = der::asn1::GeneralizedTime::from_system_time(sys_time).map_err(|e| {
CertKitError::EncodingError(format!("timestamp out of GeneralizedTime range: {e}"))
})?;
Ok(x509_cert::time::Time::GeneralTime(gt))
}
}
}
impl Validity {
pub fn new(
not_before: OffsetDateTime,
not_after: OffsetDateTime,
) -> Result<Self, CertKitError> {
Ok(Self {
not_before: encode_x509_time(not_before)?,
not_after: encode_x509_time(not_after)?,
})
}
pub fn for_days(days: i64) -> Result<Self, CertKitError> {
let now = OffsetDateTime::now_utc();
Self::new(now, now + Duration::days(days))
}
pub fn not_before(&self) -> x509_cert::time::Time {
self.not_before
}
pub fn not_after(&self) -> x509_cert::time::Time {
self.not_after
}
pub fn duration(&self) -> std::time::Duration {
let nb: std::time::SystemTime = self.not_before.to_system_time();
let na: std::time::SystemTime = self.not_after.to_system_time();
na.duration_since(nb).unwrap_or_default()
}
pub fn remaining(&self) -> Option<std::time::Duration> {
let na: std::time::SystemTime = self.not_after.to_system_time();
na.duration_since(std::time::SystemTime::now()).ok()
}
}
#[derive(Clone, Debug)]
pub struct ExtensionParam {
pub oid: ObjectIdentifier,
pub critical: bool,
pub value: Vec<u8>,
}
impl ExtensionParam {
pub fn from_extension<E: ToAndFromX509Extension>(
extension: E,
critical: bool,
) -> Result<Self, CertKitError> {
let value = extension.to_x509_extension_value()?;
Ok(Self {
oid: E::OID,
critical,
value,
})
}
pub fn to_extension<E: ToAndFromX509Extension>(&self) -> Result<E, CertKitError> {
E::from_x509_extension_value(&self.value)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn common_name_only_produces_single_rdn() {
let dn = DistinguishedName {
common_name: "leaf.example.com".to_string(),
..Default::default()
};
let x509_name = dn.as_x509_name().unwrap();
assert_eq!(x509_name.0.len(), 1, "expected a single RDN");
let attrs: Vec<_> = x509_name.0.iter().flat_map(|rdn| rdn.0.iter()).collect();
assert_eq!(attrs.len(), 1, "expected a single attribute");
assert_eq!(attrs[0].oid, rfc4519::CN);
assert_eq!(x509_name.to_string(), "CN=leaf.example.com");
let round_tripped = DistinguishedName::from_x509_name(&x509_name).unwrap();
assert_eq!(round_tripped.common_name, "leaf.example.com");
assert!(round_tripped.organization_unit.is_none());
assert!(round_tripped.organization.is_none());
assert!(round_tripped.locality.is_none());
assert!(round_tripped.state.is_none());
assert!(round_tripped.country.is_none());
}
#[test]
fn only_populated_attributes_are_emitted_in_order() {
let dn = DistinguishedName {
common_name: "leaf.example.com".to_string(),
organization: Some("Example Corp".to_string()),
country: Some("US".to_string()),
..Default::default()
};
let x509_name = dn.as_x509_name().unwrap();
assert_eq!(
x509_name.to_string(),
"CN=leaf.example.com,O=Example Corp,C=US"
);
}
#[test]
fn empty_string_attributes_are_skipped() {
let dn = DistinguishedName {
common_name: "leaf.example.com".to_string(),
organization_unit: Some(String::new()),
country: Some("US".to_string()),
..Default::default()
};
let x509_name = dn.as_x509_name().unwrap();
assert_eq!(x509_name.to_string(), "CN=leaf.example.com,C=US");
}
#[test]
fn metacharacters_in_values_do_not_panic_or_inject() {
let dn = DistinguishedName {
common_name: "Acme, Inc.+O=Evil".to_string(),
..Default::default()
};
let x509_name = dn.as_x509_name().unwrap();
assert_eq!(x509_name.0.len(), 1, "expected a single RDN");
let attrs: Vec<_> = x509_name.0.iter().flat_map(|rdn| rdn.0.iter()).collect();
assert_eq!(attrs.len(), 1, "expected a single attribute");
assert_eq!(attrs[0].oid, rfc4519::CN);
let round_tripped = DistinguishedName::from_x509_name(&x509_name).unwrap();
assert_eq!(round_tripped.common_name, "Acme, Inc.+O=Evil");
assert!(round_tripped.organization.is_none());
}
}