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