Use string slice instaed of owned string in SSL certificate attributes.

This commit is contained in:
Jean-Christophe Amiel 2025-12-14 16:42:04 +01:00
parent d7d7fbf917
commit 8894fcabc0
No known key found for this signature in database
GPG Key ID: 07FF11CFD55356CC
1 changed files with 89 additions and 82 deletions

View File

@ -23,8 +23,10 @@ use super::easy_ext::CertInfo;
/// Represents an SSL/TLS certificate.
///
/// Each attribute `subject`, `issuer` etc... is optional, so we can test invalid certificate,
/// (i.e. a certificate without serial number).
/// Each attribute `subject`, `issuer` etc... is optional, so we can test invalid certificates,
/// (i.e. a certificate without serial number). For the moment, we parse attributes values coming
/// from libcurl, whose format depends on the SSL/TLS backend and is very weak.
/// TODO: parse the X.509 certificate value ourselves to have string guarantee on the format.
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Certificate {
subject: Option<String>,
@ -55,26 +57,32 @@ impl Certificate {
}
}
/// Returns the subject attribute.
pub fn subject(&self) -> Option<&String> {
self.subject.as_ref()
}
/// Returns the issuer attribute.
pub fn issuer(&self) -> Option<&String> {
self.issuer.as_ref()
}
/// Returns the start date attribute.
pub fn start_date(&self) -> Option<DateTime<Utc>> {
self.start_date
}
/// Returns the expire date attribute.
pub fn expire_date(&self) -> Option<DateTime<Utc>> {
self.expire_date
}
/// Returns the serial number attribute.
pub fn serial_number(&self) -> Option<&String> {
self.serial_number.as_ref()
}
/// Returns the subject alternative name attribute.
pub fn subject_alt_name(&self) -> Option<&String> {
self.subject_alt_name.as_ref()
}
@ -83,18 +91,19 @@ impl Certificate {
impl TryFrom<CertInfo> for Certificate {
type Error = String;
/// parse `cert_info`
/// support different "formats" in cert info
/// Parses `cert_info`.
///
/// Support different "formats" in cert info
/// - attribute name: "Start date" vs "Start Date"
/// - date format: "Jan 10 08:29:52 2023 GMT" vs "2023-01-10 08:29:52 GMT"
fn try_from(cert_info: CertInfo) -> Result<Self, Self::Error> {
let attributes = parse_attributes(&cert_info.data);
let subject = parse_subject(&attributes).ok();
let issuer = parse_issuer(&attributes).ok();
let start_date = parse_start_date(&attributes).ok();
let expire_date = parse_expire_date(&attributes).ok();
let serial_number = parse_serial_number(&attributes).ok();
let subject_alt_name = parse_subject_alt_name(&attributes).ok();
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);
Ok(Certificate {
subject,
issuer,
@ -106,6 +115,21 @@ impl TryFrom<CertInfo> for Certificate {
}
}
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,
];
/// Parses certificate's subject attribute.
///
/// TODO: we're exposing the subject and issuer directly from libcurl. In the certificate, these
@ -122,34 +146,26 @@ impl TryFrom<CertInfo> for Certificate {
///
/// See:
/// - <integration/hurl/ssl/cacert_to_json.out.pattern>
/// - https://curl.se/mail/lib-2024-06/0013.html
fn parse_subject(attributes: &HashMap<String, String>) -> Result<String, String> {
match attributes.get("subject") {
None => Err(format!("missing Subject attribute in {attributes:?}")),
Some(value) => Ok(value.clone()),
}
/// - <https://curl.se/mail/lib-2024-06/0013.html>
fn parse_subject(attributes: &HashMap<&str, &str>) -> Option<String> {
attributes.get(SUBJECT_ATTRIBUTE).map(|s| s.to_string())
}
/// Parses certificate's issuer attribute.
fn parse_issuer(attributes: &HashMap<String, String>) -> Result<String, String> {
match attributes.get("issuer") {
None => Err(format!("missing Issuer attribute in {attributes:?}")),
Some(value) => Ok(value.clone()),
}
fn parse_issuer(attributes: &HashMap<&str, &str>) -> Option<String> {
attributes.get(ISSUER_ATTRIBUTE).map(|s| s.to_string())
}
fn parse_start_date(attributes: &HashMap<String, String>) -> Result<DateTime<Utc>, String> {
match attributes.get("start date") {
None => Err(format!("missing start date attribute in {attributes:?}")),
Some(value) => Ok(parse_date(value)?),
}
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<String, String>) -> Result<DateTime<Utc>, String> {
match attributes.get("expire date") {
None => Err("missing expire date attribute".to_string()),
Some(value) => Ok(parse_date(value)?),
}
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> {
@ -161,53 +177,53 @@ fn parse_date(value: &str) -> Result<DateTime<Utc>, String> {
Ok(naive_date_time.and_local_timezone(Utc).unwrap())
}
fn parse_serial_number(attributes: &HashMap<String, String>) -> Result<String, String> {
let value = attributes
.get("serial number")
.cloned()
.ok_or(format!("Missing serial number attribute in {attributes:?}"))?;
let normalized_value = if value.contains(':') {
value
.split(':')
.filter(|e| !e.is_empty())
.collect::<Vec<&str>>()
.join(":")
} else {
value
.chars()
.collect::<Vec<char>>()
.chunks(2)
.map(|c| c.iter().collect::<String>())
.collect::<Vec<String>>()
.join(":")
};
Ok(normalized_value)
fn parse_serial_number(attributes: &HashMap<&str, &str>) -> Option<String> {
attributes.get(SERIAL_NUMBER_ATTRIBUTE).map(|value| {
// Serial numbers can come through libcurl in various format.
// Either `AA:BB:CC` or `AABBCC`.
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<String, String>) -> Result<String, String> {
match attributes.get("x509v3 subject alternative name") {
None => Err(format!(
"missing x509v3 subject alternative name attribute in {attributes:?}"
)),
Some(value) => Ok(value.clone()),
}
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<String, 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) {
map.insert(name.to_lowercase(), value);
// We're only interested in attributes declared in `ATTRIBUTES`.
// We work with indices to use a `HashMap<&str, &str>` instead of `HashMap<String, &str>`
ATTRIBUTES
.iter()
.position(|&att| att == name.to_lowercase())
.map(|index| map.insert(ATTRIBUTES[index], value));
}
}
map
}
fn parse_attribute(s: &str) -> Option<(String, String)> {
fn parse_attribute(s: &str) -> Option<(&str, &str)> {
if let Some(index) = s.find(':') {
let (name, value) = s.split_at(index);
Some((name.to_string(), value[1..].to_string()))
Some((name, &value[1..]))
} else {
None
}
@ -223,8 +239,8 @@ mod tests {
fn test_parse_subject() {
let mut attributes = HashMap::new();
attributes.insert(
"subject".to_string(),
"C=US, ST=Denial, L=Springfield, O=Dis, CN=localhost".to_string(),
"subject",
"C=US, ST=Denial, L=Springfield, O=Dis, CN=localhost",
);
assert_eq!(
parse_subject(&attributes).unwrap(),
@ -235,10 +251,7 @@ mod tests {
#[test]
fn test_parse_start_date() {
let mut attributes = HashMap::new();
attributes.insert(
"start date".to_string(),
"Jan 10 08:29:52 2023 GMT".to_string(),
);
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")
@ -247,10 +260,7 @@ mod tests {
);
let mut attributes = HashMap::new();
attributes.insert(
"start date".to_string(),
"2023-01-10 08:29:52 GMT".to_string(),
);
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")
@ -263,8 +273,8 @@ mod tests {
fn test_parse_serial_number() {
let mut attributes = HashMap::new();
attributes.insert(
"serial number".to_string(),
"1e:e8:b1:7f:1b:64:d8:d6:b3:de:87:01:03:d2:a4:f5:33:53:5a:b0:".to_string(),
"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(),
@ -272,10 +282,7 @@ mod tests {
);
let mut attributes = HashMap::new();
attributes.insert(
"serial number".to_string(),
"1ee8b17f1b64d8d6b3de870103d2a4f533535ab0".to_string(),
);
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()
@ -286,8 +293,8 @@ mod tests {
fn test_parse_subject_alt_name() {
let mut attributes = HashMap::new();
attributes.insert(
"x509v3 subject alternative name".to_string(),
"DNS:localhost, IP address:127.0.0.1, IP address:0:0:0:0:0:0:0:1".to_string(),
"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(),