Use string slice instaed of owned string in SSL certificate attributes.
This commit is contained in:
parent
d7d7fbf917
commit
8894fcabc0
|
|
@ -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(),
|
||||
|
|
|
|||
Loading…
Reference in New Issue