use super::der;
#[derive(Debug, PartialEq, Eq)]
pub enum Decision {
Accept,
Reject(String),
}
pub const MIN_RSA_BITS: usize = 2048;
pub const MIN_EC_BITS: usize = 256;
pub const DENY_LABEL_SUBSTRINGS: &[(&str, &str)] = &[
];
pub fn evaluate(
distrust_after: bool,
label: &str,
server_auth: Option<&str>,
der_bytes: &[u8],
now: u64,
) -> Decision {
match server_auth {
Some("CKT_NSS_TRUSTED_DELEGATOR") => {}
Some(other) => {
return Decision::Reject(format!("not a server-auth trust anchor ({other})"))
}
None => return Decision::Reject("no server-auth trust record".into()),
}
if distrust_after {
return Decision::Reject("NSS server distrust-after is set (CA being phased out)".into());
}
let lc = label.to_ascii_lowercase();
for (needle, reason) in DENY_LABEL_SUBSTRINGS {
if lc.contains(&needle.to_ascii_lowercase()) {
return Decision::Reject(format!("deny list: {reason}"));
}
}
if let Err(reason) = check_cert(der_bytes, now) {
return Decision::Reject(reason);
}
Decision::Accept
}
pub fn check_cert(der_bytes: &[u8], now: u64) -> Result<(), String> {
let tbs = der::tbs_certificate(der_bytes).map_err(|e| format!("malformed certificate: {e}"))?;
let validity = der::tbs_field(tbs, der::TbsField::Validity).map_err(|e| e.to_string())?;
let v = der::read_tlv(validity).map_err(|e| e.to_string())?.0;
let times = der::children(v.content).map_err(|e| e.to_string())?;
let not_after = times
.get(1)
.ok_or_else(|| "validity missing notAfter".to_string())?;
let exp = der::parse_time(not_after.raw).map_err(|e| e.to_string())?;
if exp.0 < now {
return Err(format!("expired (notAfter {})", exp.0));
}
let spki = der::tbs_field(tbs, der::TbsField::Spki).map_err(|e| e.to_string())?;
match der::spki_key_info(spki).map_err(|e| e.to_string())? {
der::KeyInfo::Rsa { bits } if bits >= MIN_RSA_BITS => {}
der::KeyInfo::Rsa { bits } => return Err(format!("RSA key too small ({bits} bits)")),
der::KeyInfo::Ec { curve, bits } if bits >= MIN_EC_BITS => {
let _ = curve;
}
der::KeyInfo::Ec { curve, bits } => {
return Err(format!("EC curve too weak ({curve}, {bits} bits)"))
}
der::KeyInfo::Other { oid } => {
return Err(format!("unsupported key algorithm (OID bytes {oid:02x?})"))
}
}
Ok(())
}
pub fn now_yyyymmddhhmmss() -> u64 {
let secs = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_secs())
.unwrap_or(0);
let (y, mo, d, h, mi, s) = civil_from_unix(secs as i64);
(y as u64) * 10_000_000_000
+ (mo as u64) * 100_000_000
+ (d as u64) * 1_000_000
+ (h as u64) * 10_000
+ (mi as u64) * 100
+ s as u64
}
fn civil_from_unix(secs: i64) -> (i64, u32, u32, u32, u32, u32) {
let days = secs.div_euclid(86_400);
let rem = secs.rem_euclid(86_400);
let (h, mi, s) = (rem / 3600, (rem % 3600) / 60, rem % 60);
let z = days + 719_468;
let era = z.div_euclid(146_097);
let doe = z.rem_euclid(146_097);
let yoe = (doe - doe / 1460 + doe / 36_524 - doe / 146_096) / 365;
let y = yoe + era * 400;
let doy = doe - (365 * yoe + yoe / 4 - yoe / 100);
let mp = (5 * doy + 2) / 153;
let d = doy - (153 * mp + 2) / 5 + 1;
let m = if mp < 10 { mp + 3 } else { mp - 9 };
let y = if m <= 2 { y + 1 } else { y };
(y, m as u32, d as u32, h as u32, mi as u32, s as u32)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn now_is_in_a_sane_range() {
let n = now_yyyymmddhhmmss();
assert!(n > 20_200_000_000_000, "got {n}"); assert!(n < 21_000_000_000_000, "got {n}"); }
#[test]
fn rejects_non_anchor() {
let d = evaluate(false, "X", Some("CKT_NSS_MUST_VERIFY_TRUST"), &[], 0);
assert!(matches!(d, Decision::Reject(_)));
}
}