use std::collections::HashMap;
use chrono::{DateTime, NaiveDateTime, Utc};
use super::easy_ext::{CertInfo, Pem};
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Certificate {
subject: Option<String>,
issuer: Option<String>,
start_date: Option<DateTime<Utc>>,
expire_date: Option<DateTime<Utc>>,
serial_number: Option<String>,
subject_alt_name: Option<String>,
value: Option<Pem>,
}
impl Certificate {
pub fn new(
subject: Option<String>,
issuer: Option<String>,
start_date: Option<DateTime<Utc>>,
expire_date: Option<DateTime<Utc>>,
serial_number: Option<String>,
subject_alt_name: Option<String>,
value: Option<Pem>,
) -> Self {
Self {
subject,
issuer,
start_date,
expire_date,
serial_number,
subject_alt_name,
value,
}
}
pub fn subject(&self) -> Option<&String> {
self.subject.as_ref()
}
pub fn issuer(&self) -> Option<&String> {
self.issuer.as_ref()
}
pub fn start_date(&self) -> Option<DateTime<Utc>> {
self.start_date
}
pub fn expire_date(&self) -> Option<DateTime<Utc>> {
self.expire_date
}
pub fn serial_number(&self) -> Option<&String> {
self.serial_number.as_ref()
}
pub fn subject_alt_name(&self) -> Option<&String> {
self.subject_alt_name.as_ref()
}
pub fn value(&self) -> Option<&str> {
self.value.as_ref().map(|pem| pem.as_str())
}
}
impl TryFrom<CertInfo> for Certificate {
type Error = String;
fn try_from(cert_info: CertInfo) -> Result<Self, Self::Error> {
let attributes = parse_attributes(&cert_info.data);
let subject = parse_subject(&attributes);
let issuer = parse_issuer(&attributes);
let start_date = parse_start_date(&attributes);
let expire_date = parse_expire_date(&attributes);
let serial_number = parse_serial_number(&attributes);
let subject_alt_name = parse_subject_alt_name(&attributes);
let value = cert_info.value.clone();
Ok(Certificate {
subject,
issuer,
start_date,
expire_date,
serial_number,
subject_alt_name,
value,
})
}
}
const SUBJECT_ATTRIBUTE: &str = "subject";
const ISSUER_ATTRIBUTE: &str = "issuer";
const START_DATE_ATTRIBUTE: &str = "start date";
const EXPIRE_DATE_ATTRIBUTE: &str = "expire date";
const SERIAL_NUMBER_ATTRIBUTE: &str = "serial number";
const SUBJECT_ALT_NAME_ATTRIBUTE: &str = "x509v3 subject alternative name";
const ATTRIBUTES: &[&str] = &[
SUBJECT_ATTRIBUTE,
ISSUER_ATTRIBUTE,
START_DATE_ATTRIBUTE,
EXPIRE_DATE_ATTRIBUTE,
SERIAL_NUMBER_ATTRIBUTE,
SUBJECT_ALT_NAME_ATTRIBUTE,
];
fn parse_subject(attributes: &HashMap<&str, &str>) -> Option<String> {
attributes.get(SUBJECT_ATTRIBUTE).map(|s| s.to_string())
}
fn parse_issuer(attributes: &HashMap<&str, &str>) -> Option<String> {
attributes.get(ISSUER_ATTRIBUTE).map(|s| s.to_string())
}
fn parse_start_date(attributes: &HashMap<&str, &str>) -> Option<DateTime<Utc>> {
attributes
.get(START_DATE_ATTRIBUTE)
.and_then(|date| parse_date(date).ok())
}
fn parse_expire_date(attributes: &HashMap<&str, &str>) -> Option<DateTime<Utc>> {
attributes
.get(EXPIRE_DATE_ATTRIBUTE)
.and_then(|date| parse_date(date).ok())
}
fn parse_date(value: &str) -> Result<DateTime<Utc>, String> {
let naive_date_time = match NaiveDateTime::parse_from_str(value, "%b %d %H:%M:%S %Y GMT") {
Ok(d) => d,
Err(_) => NaiveDateTime::parse_from_str(value, "%Y-%m-%d %H:%M:%S GMT")
.map_err(|_| format!("can not parse date <{value}>"))?,
};
Ok(naive_date_time.and_local_timezone(Utc).unwrap())
}
fn parse_serial_number(attributes: &HashMap<&str, &str>) -> Option<String> {
attributes.get(SERIAL_NUMBER_ATTRIBUTE).map(|value| {
if value.contains(':') {
value
.split(':')
.filter(|s| !s.is_empty())
.collect::<Vec<&str>>()
.join(":")
} else {
value
.chars()
.collect::<Vec<char>>()
.chunks(2)
.map(|c| c.iter().collect::<String>())
.collect::<Vec<String>>()
.join(":")
}
})
}
fn parse_subject_alt_name(attributes: &HashMap<&str, &str>) -> Option<String> {
attributes
.get(SUBJECT_ALT_NAME_ATTRIBUTE)
.map(|it| it.to_string())
}
fn parse_attributes(data: &Vec<String>) -> HashMap<&str, &str> {
let mut map = HashMap::new();
for s in data {
if let Some((name, value)) = parse_attribute(s) {
ATTRIBUTES
.iter()
.position(|&att| att == name.to_lowercase())
.map(|index| map.insert(ATTRIBUTES[index], value));
}
}
map
}
fn parse_attribute(s: &str) -> Option<(&str, &str)> {
if let Some(index) = s.find(':') {
let (name, value) = s.split_at(index);
Some((name, &value[1..]))
} else {
None
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::http::certificate::Certificate;
use crate::http::easy_ext::CertInfo;
#[test]
fn test_parse_subject() {
let mut attributes = HashMap::new();
attributes.insert(
"subject",
"C=US, ST=Denial, L=Springfield, O=Dis, CN=localhost",
);
assert_eq!(
parse_subject(&attributes).unwrap(),
"C=US, ST=Denial, L=Springfield, O=Dis, CN=localhost".to_string()
);
}
#[test]
fn test_parse_start_date() {
let mut attributes = HashMap::new();
attributes.insert("start date", "Jan 10 08:29:52 2023 GMT");
assert_eq!(
parse_start_date(&attributes).unwrap(),
DateTime::parse_from_rfc2822("Tue, 10 Jan 2023 08:29:52 GMT")
.unwrap()
.with_timezone(&Utc)
);
let mut attributes = HashMap::new();
attributes.insert("start date", "2023-01-10 08:29:52 GMT");
assert_eq!(
parse_start_date(&attributes).unwrap(),
DateTime::parse_from_rfc2822("Tue, 10 Jan 2023 08:29:52 GMT")
.unwrap()
.with_timezone(&Utc)
);
}
#[test]
fn test_parse_serial_number() {
let mut attributes = HashMap::new();
attributes.insert(
"serial number",
"1e:e8:b1:7f:1b:64:d8:d6:b3:de:87:01:03:d2:a4:f5:33:53:5a:b0:",
);
assert_eq!(
parse_serial_number(&attributes).unwrap(),
"1e:e8:b1:7f:1b:64:d8:d6:b3:de:87:01:03:d2:a4:f5:33:53:5a:b0".to_string()
);
let mut attributes = HashMap::new();
attributes.insert("serial number", "1ee8b17f1b64d8d6b3de870103d2a4f533535ab0");
assert_eq!(
parse_serial_number(&attributes).unwrap(),
"1e:e8:b1:7f:1b:64:d8:d6:b3:de:87:01:03:d2:a4:f5:33:53:5a:b0".to_string()
);
}
#[test]
fn test_parse_subject_alt_name() {
let mut attributes = HashMap::new();
attributes.insert(
"x509v3 subject alternative name",
"DNS:localhost, IP address:127.0.0.1, IP address:0:0:0:0:0:0:0:1",
);
assert_eq!(
parse_subject_alt_name(&attributes).unwrap(),
"DNS:localhost, IP address:127.0.0.1, IP address:0:0:0:0:0:0:0:1".to_string()
);
}
#[test]
fn test_try_from() {
assert_eq!(
Certificate::try_from(CertInfo {
data: vec![
"Subject:C = US, ST = Denial, L = Springfield, O = Dis, CN = localhost"
.to_string(),
"Issuer:C = US, ST = Denial, L = Springfield, O = Dis, CN = localhost"
.to_string(),
"Serial Number:1ee8b17f1b64d8d6b3de870103d2a4f533535ab0".to_string(),
"Start date:Jan 10 08:29:52 2023 GMT".to_string(),
"Expire date:Oct 30 08:29:52 2025 GMT".to_string(),
"x509v3 subject alternative name:DNS:localhost, IP address:127.0.0.1, IP address:0:0:0:0:0:0:0:1"
.to_string(),
],
value: None,
})
.unwrap(),
Certificate {
subject: Some("C = US, ST = Denial, L = Springfield, O = Dis, CN = localhost"
.to_string()),
issuer: Some("C = US, ST = Denial, L = Springfield, O = Dis, CN = localhost".to_string()),
start_date: Some(DateTime::parse_from_rfc2822("Tue, 10 Jan 2023 08:29:52 GMT")
.unwrap()
.with_timezone(&Utc)),
expire_date: Some(DateTime::parse_from_rfc2822("Thu, 30 Oct 2025 08:29:52 GMT")
.unwrap()
.with_timezone(&Utc)),
serial_number: Some("1e:e8:b1:7f:1b:64:d8:d6:b3:de:87:01:03:d2:a4:f5:33:53:5a:b0"
.to_string()),
subject_alt_name: Some("DNS:localhost, IP address:127.0.0.1, IP address:0:0:0:0:0:0:0:1".to_string()),
value: None,
}
);
}
}