use chrono::{NaiveDate, NaiveDateTime, TimeZone, Utc};
use openssl::asn1::Asn1Time;
use std::fs;
use std::fs::{OpenOptions, create_dir_all};
use std::io::Write;
#[cfg(unix)]
use std::os::unix::fs::OpenOptionsExt;
use std::path::Path;
pub trait X509Parts {
fn get_pem(&self) -> Result<Vec<u8>, Box<dyn std::error::Error>>;
fn get_private_key(&self) -> Result<Vec<u8>, Box<dyn std::error::Error>>;
fn pem_extension(&self) -> &'static str;
}
pub trait X509Common {
fn save<P: AsRef<Path>, F: AsRef<Path>>(
&self,
path: P,
filename: F,
) -> Result<(), Box<dyn std::error::Error>>;
}
impl<T: X509Parts> X509Common for T {
fn save<P: AsRef<Path>, F: AsRef<Path>>(
&self,
path: P,
filename: F,
) -> Result<(), Box<dyn std::error::Error>> {
create_dir_all(&path)?;
let os_file = filename
.as_ref()
.file_name()
.ok_or("Failed to extract file name")?;
let write_file =
|suffix: &str, content: &[u8], mode: u32| -> Result<(), Box<dyn std::error::Error>> {
let mut new_name = os_file.to_os_string();
new_name.push(suffix);
let full_path = path.as_ref().join(new_name);
let mut opts = OpenOptions::new();
opts.write(true).create(true).truncate(true);
#[cfg(unix)]
opts.mode(mode);
#[cfg(not(unix))]
let _ = mode;
if full_path.exists() {
fs::remove_file(&full_path)?;
}
let mut file = opts.open(full_path)?;
file.write_all(content)?;
Ok(())
};
if let Ok(ref key) = self.get_private_key() {
write_file("_pkey.pem", key, 0o600)?;
}
write_file(self.pem_extension(), &self.get_pem()?, 0o644)?;
Ok(())
}
}
pub(crate) fn create_asn1_time_from_date(
date_str: &str,
) -> Result<Asn1Time, Box<dyn std::error::Error>> {
let date = NaiveDate::parse_from_str(date_str, "%Y-%m-%d")?;
let datetime = NaiveDateTime::new(date, chrono::NaiveTime::from_hms_opt(0, 0, 0).unwrap());
Ok(Asn1Time::from_unix(
Utc.from_utc_datetime(&datetime).timestamp(),
)?)
}
#[cfg(test)]
mod asn1_time_encoding {
use crate::certificate::{CertBuilder, CsrBuilder, CsrOptions, UseesBuilderFields};
use crate::test_der::{TAG_GENERALIZEDTIME, TAG_UTCTIME, certificate_validity};
fn cert_der(valid_from: &str, valid_to: &str) -> Vec<u8> {
CertBuilder::new()
.common_name("asn1 time test")
.valid_from(valid_from)
.valid_to(valid_to)
.build_and_self_sign()
.unwrap()
.x509
.to_der()
.unwrap()
}
#[test]
fn explicit_date_before_2050_is_utctime() {
let (not_before, not_after) = certificate_validity(&cert_der("2026-08-07", "2026-11-05"));
assert_eq!(not_before.tag, TAG_UTCTIME, "notBefore: {not_before:?}");
assert_eq!(not_after.tag, TAG_UTCTIME, "notAfter: {not_after:?}");
assert_eq!(not_before.value, "260807000000Z");
assert_eq!(not_after.value, "261105000000Z");
}
#[test]
fn last_utctime_year_2049_is_utctime() {
let (not_before, not_after) = certificate_validity(&cert_der("2049-12-30", "2049-12-31"));
assert_eq!(not_before.tag, TAG_UTCTIME, "notBefore: {not_before:?}");
assert_eq!(not_after.tag, TAG_UTCTIME, "notAfter: {not_after:?}");
assert_eq!(not_after.value, "491231000000Z");
}
#[test]
fn first_generalizedtime_year_2050_is_generalizedtime() {
let (not_before, not_after) = certificate_validity(&cert_der("2050-01-01", "2050-01-02"));
assert_eq!(
not_before.tag, TAG_GENERALIZEDTIME,
"notBefore: {not_before:?}"
);
assert_eq!(
not_after.tag, TAG_GENERALIZEDTIME,
"notAfter: {not_after:?}"
);
assert_eq!(not_before.value, "20500101000000Z");
}
#[test]
fn the_two_encodings_may_be_mixed_across_the_boundary() {
let (not_before, not_after) = certificate_validity(&cert_der("2049-12-31", "2050-01-01"));
assert_eq!(not_before.tag, TAG_UTCTIME, "notBefore: {not_before:?}");
assert_eq!(
not_after.tag, TAG_GENERALIZEDTIME,
"notAfter: {not_after:?}"
);
}
#[test]
fn a_far_future_date_keeps_its_century() {
let (_, not_after) = certificate_validity(&cert_der("2026-01-01", "2075-06-01"));
assert_eq!(
not_after.tag, TAG_GENERALIZEDTIME,
"notAfter: {not_after:?}"
);
assert_eq!(not_after.value, "20750601000000Z");
}
#[test]
fn builder_default_dates_are_utctime() {
let der = CertBuilder::new()
.common_name("asn1 default test")
.build_and_self_sign()
.unwrap()
.x509
.to_der()
.unwrap();
let (not_before, not_after) = certificate_validity(&der);
assert_eq!(not_before.tag, TAG_UTCTIME, "notBefore: {not_before:?}");
assert_eq!(not_after.tag, TAG_UTCTIME, "notAfter: {not_after:?}");
}
#[test]
fn a_certificate_signed_from_a_csr_is_utctime() {
let ca = CertBuilder::new()
.common_name("csr test ca")
.is_ca(true)
.build_and_self_sign()
.unwrap();
let csr = CsrBuilder::new()
.common_name("csr subject")
.certificate_signing_request()
.unwrap();
let signed = csr
.build_signed_certificate(
&ca,
CsrOptions::new()
.valid_from("2026-08-07")
.valid_to("2026-11-05"),
)
.unwrap();
let (not_before, not_after) = certificate_validity(&signed.x509.to_der().unwrap());
assert_eq!(not_before.tag, TAG_UTCTIME, "notBefore: {not_before:?}");
assert_eq!(not_after.tag, TAG_UTCTIME, "notAfter: {not_after:?}");
assert_eq!(not_before.value, "260807000000Z");
}
}
#[cfg(all(test, unix))]
mod tests {
use super::*;
use crate::certificate::{CertBuilder, UseesBuilderFields};
use std::os::unix::fs::PermissionsExt;
use tempfile::tempdir;
fn mode_of(path: &Path) -> u32 {
fs::metadata(path).unwrap().permissions().mode() & 0o777
}
fn a_certificate() -> impl X509Common {
CertBuilder::new()
.common_name("perm test")
.build_and_self_sign()
.unwrap()
}
#[test]
fn private_key_is_not_readable_by_group_or_other() {
let dir = tempdir().unwrap();
a_certificate().save(dir.path(), "mytest").unwrap();
let key = dir.path().join("mytest_pkey.pem");
let mode = mode_of(&key);
assert_eq!(
mode & 0o077,
0,
"private key must not be group/world accessible, got {mode:o}"
);
assert_eq!(
mode, 0o600,
"private key should be exactly 0600, got {mode:o}"
);
}
#[test]
fn certificate_itself_is_left_readable() {
let dir = tempdir().unwrap();
a_certificate().save(dir.path(), "mytest").unwrap();
let cert = dir.path().join("mytest_cert.pem");
let mode = mode_of(&cert);
assert_ne!(mode & 0o400, 0, "certificate should be owner-readable");
}
#[test]
fn resaving_tightens_permissions_on_a_pre_existing_key_file() {
let dir = tempdir().unwrap();
let key = dir.path().join("mytest_pkey.pem");
fs::write(&key, b"stale").unwrap();
fs::set_permissions(&key, fs::Permissions::from_mode(0o644)).unwrap();
assert_eq!(mode_of(&key), 0o644);
a_certificate().save(dir.path(), "mytest").unwrap();
assert_eq!(
mode_of(&key),
0o600,
"re-saving must not inherit the old world-readable mode"
);
}
}