Add Subject Alt Name (SAN) attribute for certificate assertions.
Co-authored-by: Rajiv Aaron Manglani <rajiv@alum.mit.edu>
This commit is contained in:
parent
27d529b6c2
commit
c6e73e0892
|
|
@ -780,6 +780,8 @@ certificate "Subject" == "CN=example.org"
|
|||
certificate "Issuer" == "C=US, O=Let's Encrypt, CN=R3"
|
||||
certificate "Expire-Date" daysAfterNow > 15
|
||||
certificate "Serial-Number" matches /[\da-f]+/
|
||||
certificate "Subject-Alt-Name" contains "DNS:example.org"
|
||||
certificate "Subject-Alt-Name" split "," count == 2
|
||||
```
|
||||
|
||||
[Doc](https://hurl.dev/docs/asserting-response.html#ssl-certificate-assert)
|
||||
|
|
|
|||
|
|
@ -960,7 +960,7 @@ duration < 1000 # Check that response time is less than one second
|
|||
Check the SSL certificate properties. Certificate assert consists of the keyword `certificate`, followed by the
|
||||
certificate attribute value.
|
||||
|
||||
The following attributes are supported: `Subject`, `Issuer`, `Start-Date`, `Expire-Date` and `Serial-Number`.
|
||||
The following attributes are supported: `Subject`, `Issuer`, `Start-Date`, `Expire-Date`, `Serial-Number`, and `Subject-Alt-Name`.
|
||||
|
||||
```hurl
|
||||
GET https://example.org
|
||||
|
|
@ -970,6 +970,8 @@ certificate "Subject" == "CN=example.org"
|
|||
certificate "Issuer" == "C=US, O=Let's Encrypt, CN=R3"
|
||||
certificate "Expire-Date" daysAfterNow > 15
|
||||
certificate "Serial-Number" matches "[0-9af]+"
|
||||
certificate "Subject-Alt-Name" contains "DNS:example.org"
|
||||
certificate "Subject-Alt-Name" split "," count == 2
|
||||
```
|
||||
|
||||
[predicates]: #predicates
|
||||
|
|
@ -1000,4 +1002,4 @@ certificate "Serial-Number" matches "[0-9af]+"
|
|||
[`Content-Type` header]: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Type
|
||||
[`body` assert]: #body-assert
|
||||
[`location` filter]: /docs/filters.md#location
|
||||
[UUID v4]: https://en.wikipedia.org/wiki/Universally_unique_identifier
|
||||
[UUID v4]: https://en.wikipedia.org/wiki/Universally_unique_identifier
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
.TH hurl 1 "20 Nov 2025" "hurl 7.2.0-SNAPSHOT" " Hurl Manual"
|
||||
.TH hurl 1 "07 Dec 2025" "hurl 7.2.0-SNAPSHOT" " Hurl Manual"
|
||||
.SH NAME
|
||||
|
||||
hurl - run and test HTTP requests.
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
.TH hurl 1 "20 Nov 2025" "hurl 7.2.0-SNAPSHOT" " Hurl Manual"
|
||||
.TH hurl 1 "07 Dec 2025" "hurl 7.2.0-SNAPSHOT" " Hurl Manual"
|
||||
.SH NAME
|
||||
|
||||
hurlfmt - format Hurl files
|
||||
|
|
|
|||
|
|
@ -505,6 +505,8 @@ certificate "Subject" == "CN=example.org"
|
|||
certificate "Issuer" == "C=US, O=Let's Encrypt, CN=R3"
|
||||
certificate "Expire-Date" daysAfterNow > 15
|
||||
certificate "Serial-Number" matches /[\da-f]+/
|
||||
certificate "Subject-Alt-Name" contains "DNS:example.org"
|
||||
certificate "Subject-Alt-Name" split "," count == 2
|
||||
```
|
||||
|
||||
[Doc](/docs/asserting-response.md#ssl-certificate-assert)
|
||||
|
|
|
|||
|
|
@ -780,6 +780,8 @@ certificate "Subject" == "CN=example.org"
|
|||
certificate "Issuer" == "C=US, O=Let's Encrypt, CN=R3"
|
||||
certificate "Expire-Date" daysAfterNow > 15
|
||||
certificate "Serial-Number" matches /[\da-f]+/
|
||||
certificate "Subject-Alt-Name" contains "DNS:example.org"
|
||||
certificate "Subject-Alt-Name" split "," count == 2
|
||||
```
|
||||
|
||||
[Doc](https://hurl.dev/docs/asserting-response.html#ssl-certificate-assert)
|
||||
|
|
|
|||
|
|
@ -28,6 +28,7 @@ pub struct Certificate {
|
|||
pub start_date: DateTime<Utc>,
|
||||
pub expire_date: DateTime<Utc>,
|
||||
pub serial_number: String,
|
||||
pub subject_alt_name: Option<String>,
|
||||
}
|
||||
|
||||
impl TryFrom<CertInfo> for Certificate {
|
||||
|
|
@ -44,12 +45,14 @@ impl TryFrom<CertInfo> for Certificate {
|
|||
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();
|
||||
Ok(Certificate {
|
||||
subject,
|
||||
issuer,
|
||||
start_date,
|
||||
expire_date,
|
||||
serial_number,
|
||||
subject_alt_name,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -133,6 +136,15 @@ fn parse_serial_number(attributes: &HashMap<String, String>) -> Result<String, S
|
|||
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> {
|
||||
let mut map = HashMap::new();
|
||||
for s in data {
|
||||
|
|
@ -182,7 +194,7 @@ mod tests {
|
|||
parse_start_date(&attributes).unwrap(),
|
||||
chrono::DateTime::parse_from_rfc2822("Tue, 10 Jan 2023 08:29:52 GMT")
|
||||
.unwrap()
|
||||
.with_timezone(&chrono::Utc)
|
||||
.with_timezone(&Utc)
|
||||
);
|
||||
|
||||
let mut attributes = HashMap::new();
|
||||
|
|
@ -194,7 +206,7 @@ mod tests {
|
|||
parse_start_date(&attributes).unwrap(),
|
||||
chrono::DateTime::parse_from_rfc2822("Tue, 10 Jan 2023 08:29:52 GMT")
|
||||
.unwrap()
|
||||
.with_timezone(&chrono::Utc)
|
||||
.with_timezone(&Utc)
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -221,6 +233,19 @@ mod tests {
|
|||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
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 adddress:0:0:0:0:0:0:0:1".to_string(),
|
||||
);
|
||||
assert_eq!(
|
||||
parse_subject_alt_name(&attributes).unwrap(),
|
||||
"DNS:localhost, IP address:127.0.0.1, IP adddress:0:0:0:0:0:0:0:1".to_string()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_try_from() {
|
||||
assert_eq!(
|
||||
|
|
@ -233,6 +258,8 @@ mod tests {
|
|||
"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 adddress:0:0:0:0:0:0:0:1"
|
||||
.to_string(),
|
||||
]
|
||||
})
|
||||
.unwrap(),
|
||||
|
|
@ -242,12 +269,13 @@ mod tests {
|
|||
issuer: "C = US, ST = Denial, L = Springfield, O = Dis, CN = localhost".to_string(),
|
||||
start_date: chrono::DateTime::parse_from_rfc2822("Tue, 10 Jan 2023 08:29:52 GMT")
|
||||
.unwrap()
|
||||
.with_timezone(&chrono::Utc),
|
||||
.with_timezone(&Utc),
|
||||
expire_date: chrono::DateTime::parse_from_rfc2822("Thu, 30 Oct 2025 08:29:52 GMT")
|
||||
.unwrap()
|
||||
.with_timezone(&chrono::Utc),
|
||||
.with_timezone(&Utc),
|
||||
serial_number: "1e:e8:b1:7f:1b:64:d8:d6:b3:de:87:01:03:d2:a4:f5:33:53:5a:b0"
|
||||
.to_string()
|
||||
.to_string(),
|
||||
subject_alt_name: Some("DNS:localhost, IP address:127.0.0.1, IP adddress:0:0:0:0:0:0:0:1".to_string())
|
||||
}
|
||||
);
|
||||
assert_eq!(
|
||||
|
|
|
|||
|
|
@ -194,6 +194,8 @@ struct CertificateJson {
|
|||
start_date: String,
|
||||
expire_date: String,
|
||||
serial_number: String,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
subject_alt_name: Option<String>,
|
||||
}
|
||||
|
||||
impl HurlResultJson {
|
||||
|
|
@ -448,6 +450,7 @@ impl CertificateJson {
|
|||
start_date: c.start_date.to_string(),
|
||||
expire_date: c.expire_date.to_string(),
|
||||
serial_number: c.serial_number.to_string(),
|
||||
subject_alt_name: c.subject_alt_name.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -133,13 +133,17 @@ fn get_call_html(
|
|||
if let Some(certificate) = &call.response.certificate {
|
||||
let start_date = certificate.start_date.to_string();
|
||||
let end_date = certificate.expire_date.to_string();
|
||||
let values = vec![
|
||||
let mut values = vec![
|
||||
("Subject", certificate.subject.as_str()),
|
||||
("Issuer", certificate.issuer.as_str()),
|
||||
("Start Date", start_date.as_str()),
|
||||
("Expire Date", end_date.as_str()),
|
||||
("Serial Number", certificate.serial_number.as_str()),
|
||||
];
|
||||
if let Some(subject_alt_name) = certificate.subject_alt_name.as_ref() {
|
||||
values.push(("Subject Alt Name", subject_alt_name.as_str()));
|
||||
}
|
||||
|
||||
let table = new_table("Certificate", &values);
|
||||
text.push_str(&table);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -391,6 +391,12 @@ fn eval_query_certificate(
|
|||
CertificateAttributeName::SerialNumber => {
|
||||
Value::String(certificate.serial_number.clone())
|
||||
}
|
||||
CertificateAttributeName::SubjectAltName => {
|
||||
match certificate.subject_alt_name.as_ref() {
|
||||
Some(s) => Value::String(s.clone()),
|
||||
None => return Ok(None),
|
||||
}
|
||||
}
|
||||
};
|
||||
Ok(Some(value))
|
||||
} else {
|
||||
|
|
@ -1484,7 +1490,8 @@ pub mod tests {
|
|||
issuer: String::new(),
|
||||
start_date: Default::default(),
|
||||
expire_date: Default::default(),
|
||||
serial_number: String::new()
|
||||
serial_number: String::new(),
|
||||
subject_alt_name: Some(String::new())
|
||||
}),
|
||||
..default_response()
|
||||
},
|
||||
|
|
|
|||
|
|
@ -296,6 +296,7 @@ pub enum CertificateAttributeName {
|
|||
StartDate,
|
||||
ExpireDate,
|
||||
SerialNumber,
|
||||
SubjectAltName,
|
||||
}
|
||||
|
||||
impl CertificateAttributeName {
|
||||
|
|
@ -307,6 +308,7 @@ impl CertificateAttributeName {
|
|||
CertificateAttributeName::StartDate => "Start-Date",
|
||||
CertificateAttributeName::ExpireDate => "Expire-Date",
|
||||
CertificateAttributeName::SerialNumber => "Serial-Number",
|
||||
CertificateAttributeName::SubjectAltName => "Subject-Alt-Name",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -219,9 +219,11 @@ fn certificate_field(reader: &mut Reader) -> ParseResult<CertificateAttributeNam
|
|||
Ok(CertificateAttributeName::ExpireDate)
|
||||
} else if try_literal(r#"Serial-Number""#, reader).is_ok() {
|
||||
Ok(CertificateAttributeName::SerialNumber)
|
||||
} else if try_literal(r#"Subject-Alt-Name""#, reader).is_ok() {
|
||||
Ok(CertificateAttributeName::SubjectAltName)
|
||||
} else {
|
||||
let value =
|
||||
"Field <Subject>, <Issuer>, <Start-Date>, <Expire-Date> or <Serial-Number>".to_string();
|
||||
"Field <Subject>, <Issuer>, <Start-Date>, <Expire-Date>, <Serial-Number>, or <Subject-Alt-Name>".to_string();
|
||||
let kind = ParseErrorKind::Expecting { value };
|
||||
let cur = reader.cursor();
|
||||
Err(ParseError::new(cur.pos, false, kind))
|
||||
|
|
|
|||
Loading…
Reference in New Issue