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 "Issuer" == "C=US, O=Let's Encrypt, CN=R3"
|
||||||
certificate "Expire-Date" daysAfterNow > 15
|
certificate "Expire-Date" daysAfterNow > 15
|
||||||
certificate "Serial-Number" matches /[\da-f]+/
|
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)
|
[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
|
Check the SSL certificate properties. Certificate assert consists of the keyword `certificate`, followed by the
|
||||||
certificate attribute value.
|
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
|
```hurl
|
||||||
GET https://example.org
|
GET https://example.org
|
||||||
|
|
@ -970,6 +970,8 @@ certificate "Subject" == "CN=example.org"
|
||||||
certificate "Issuer" == "C=US, O=Let's Encrypt, CN=R3"
|
certificate "Issuer" == "C=US, O=Let's Encrypt, CN=R3"
|
||||||
certificate "Expire-Date" daysAfterNow > 15
|
certificate "Expire-Date" daysAfterNow > 15
|
||||||
certificate "Serial-Number" matches "[0-9af]+"
|
certificate "Serial-Number" matches "[0-9af]+"
|
||||||
|
certificate "Subject-Alt-Name" contains "DNS:example.org"
|
||||||
|
certificate "Subject-Alt-Name" split "," count == 2
|
||||||
```
|
```
|
||||||
|
|
||||||
[predicates]: #predicates
|
[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
|
[`Content-Type` header]: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Type
|
||||||
[`body` assert]: #body-assert
|
[`body` assert]: #body-assert
|
||||||
[`location` filter]: /docs/filters.md#location
|
[`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
|
.SH NAME
|
||||||
|
|
||||||
hurl - run and test HTTP requests.
|
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
|
.SH NAME
|
||||||
|
|
||||||
hurlfmt - format Hurl files
|
hurlfmt - format Hurl files
|
||||||
|
|
|
||||||
|
|
@ -505,6 +505,8 @@ certificate "Subject" == "CN=example.org"
|
||||||
certificate "Issuer" == "C=US, O=Let's Encrypt, CN=R3"
|
certificate "Issuer" == "C=US, O=Let's Encrypt, CN=R3"
|
||||||
certificate "Expire-Date" daysAfterNow > 15
|
certificate "Expire-Date" daysAfterNow > 15
|
||||||
certificate "Serial-Number" matches /[\da-f]+/
|
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)
|
[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 "Issuer" == "C=US, O=Let's Encrypt, CN=R3"
|
||||||
certificate "Expire-Date" daysAfterNow > 15
|
certificate "Expire-Date" daysAfterNow > 15
|
||||||
certificate "Serial-Number" matches /[\da-f]+/
|
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)
|
[Doc](https://hurl.dev/docs/asserting-response.html#ssl-certificate-assert)
|
||||||
|
|
|
||||||
|
|
@ -28,6 +28,7 @@ pub struct Certificate {
|
||||||
pub start_date: DateTime<Utc>,
|
pub start_date: DateTime<Utc>,
|
||||||
pub expire_date: DateTime<Utc>,
|
pub expire_date: DateTime<Utc>,
|
||||||
pub serial_number: String,
|
pub serial_number: String,
|
||||||
|
pub subject_alt_name: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl TryFrom<CertInfo> for Certificate {
|
impl TryFrom<CertInfo> for Certificate {
|
||||||
|
|
@ -44,12 +45,14 @@ impl TryFrom<CertInfo> for Certificate {
|
||||||
let start_date = parse_start_date(&attributes)?;
|
let start_date = parse_start_date(&attributes)?;
|
||||||
let expire_date = parse_expire_date(&attributes)?;
|
let expire_date = parse_expire_date(&attributes)?;
|
||||||
let serial_number = parse_serial_number(&attributes)?;
|
let serial_number = parse_serial_number(&attributes)?;
|
||||||
|
let subject_alt_name = parse_subject_alt_name(&attributes).ok();
|
||||||
Ok(Certificate {
|
Ok(Certificate {
|
||||||
subject,
|
subject,
|
||||||
issuer,
|
issuer,
|
||||||
start_date,
|
start_date,
|
||||||
expire_date,
|
expire_date,
|
||||||
serial_number,
|
serial_number,
|
||||||
|
subject_alt_name,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -133,6 +136,15 @@ fn parse_serial_number(attributes: &HashMap<String, String>) -> Result<String, S
|
||||||
Ok(normalized_value)
|
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_attributes(data: &Vec<String>) -> HashMap<String, String> {
|
||||||
let mut map = HashMap::new();
|
let mut map = HashMap::new();
|
||||||
for s in data {
|
for s in data {
|
||||||
|
|
@ -182,7 +194,7 @@ mod tests {
|
||||||
parse_start_date(&attributes).unwrap(),
|
parse_start_date(&attributes).unwrap(),
|
||||||
chrono::DateTime::parse_from_rfc2822("Tue, 10 Jan 2023 08:29:52 GMT")
|
chrono::DateTime::parse_from_rfc2822("Tue, 10 Jan 2023 08:29:52 GMT")
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.with_timezone(&chrono::Utc)
|
.with_timezone(&Utc)
|
||||||
);
|
);
|
||||||
|
|
||||||
let mut attributes = HashMap::new();
|
let mut attributes = HashMap::new();
|
||||||
|
|
@ -194,7 +206,7 @@ mod tests {
|
||||||
parse_start_date(&attributes).unwrap(),
|
parse_start_date(&attributes).unwrap(),
|
||||||
chrono::DateTime::parse_from_rfc2822("Tue, 10 Jan 2023 08:29:52 GMT")
|
chrono::DateTime::parse_from_rfc2822("Tue, 10 Jan 2023 08:29:52 GMT")
|
||||||
.unwrap()
|
.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]
|
#[test]
|
||||||
fn test_try_from() {
|
fn test_try_from() {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
|
|
@ -233,6 +258,8 @@ mod tests {
|
||||||
"Serial Number:1ee8b17f1b64d8d6b3de870103d2a4f533535ab0".to_string(),
|
"Serial Number:1ee8b17f1b64d8d6b3de870103d2a4f533535ab0".to_string(),
|
||||||
"Start date:Jan 10 08:29:52 2023 GMT".to_string(),
|
"Start date:Jan 10 08:29:52 2023 GMT".to_string(),
|
||||||
"Expire date:Oct 30 08:29:52 2025 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(),
|
.unwrap(),
|
||||||
|
|
@ -242,12 +269,13 @@ mod tests {
|
||||||
issuer: "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(),
|
||||||
start_date: chrono::DateTime::parse_from_rfc2822("Tue, 10 Jan 2023 08:29:52 GMT")
|
start_date: chrono::DateTime::parse_from_rfc2822("Tue, 10 Jan 2023 08:29:52 GMT")
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.with_timezone(&chrono::Utc),
|
.with_timezone(&Utc),
|
||||||
expire_date: chrono::DateTime::parse_from_rfc2822("Thu, 30 Oct 2025 08:29:52 GMT")
|
expire_date: chrono::DateTime::parse_from_rfc2822("Thu, 30 Oct 2025 08:29:52 GMT")
|
||||||
.unwrap()
|
.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"
|
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!(
|
assert_eq!(
|
||||||
|
|
|
||||||
|
|
@ -194,6 +194,8 @@ struct CertificateJson {
|
||||||
start_date: String,
|
start_date: String,
|
||||||
expire_date: String,
|
expire_date: String,
|
||||||
serial_number: String,
|
serial_number: String,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
subject_alt_name: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl HurlResultJson {
|
impl HurlResultJson {
|
||||||
|
|
@ -448,6 +450,7 @@ impl CertificateJson {
|
||||||
start_date: c.start_date.to_string(),
|
start_date: c.start_date.to_string(),
|
||||||
expire_date: c.expire_date.to_string(),
|
expire_date: c.expire_date.to_string(),
|
||||||
serial_number: c.serial_number.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 {
|
if let Some(certificate) = &call.response.certificate {
|
||||||
let start_date = certificate.start_date.to_string();
|
let start_date = certificate.start_date.to_string();
|
||||||
let end_date = certificate.expire_date.to_string();
|
let end_date = certificate.expire_date.to_string();
|
||||||
let values = vec![
|
let mut values = vec![
|
||||||
("Subject", certificate.subject.as_str()),
|
("Subject", certificate.subject.as_str()),
|
||||||
("Issuer", certificate.issuer.as_str()),
|
("Issuer", certificate.issuer.as_str()),
|
||||||
("Start Date", start_date.as_str()),
|
("Start Date", start_date.as_str()),
|
||||||
("Expire Date", end_date.as_str()),
|
("Expire Date", end_date.as_str()),
|
||||||
("Serial Number", certificate.serial_number.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);
|
let table = new_table("Certificate", &values);
|
||||||
text.push_str(&table);
|
text.push_str(&table);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -391,6 +391,12 @@ fn eval_query_certificate(
|
||||||
CertificateAttributeName::SerialNumber => {
|
CertificateAttributeName::SerialNumber => {
|
||||||
Value::String(certificate.serial_number.clone())
|
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))
|
Ok(Some(value))
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -1484,7 +1490,8 @@ pub mod tests {
|
||||||
issuer: String::new(),
|
issuer: String::new(),
|
||||||
start_date: Default::default(),
|
start_date: Default::default(),
|
||||||
expire_date: Default::default(),
|
expire_date: Default::default(),
|
||||||
serial_number: String::new()
|
serial_number: String::new(),
|
||||||
|
subject_alt_name: Some(String::new())
|
||||||
}),
|
}),
|
||||||
..default_response()
|
..default_response()
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -296,6 +296,7 @@ pub enum CertificateAttributeName {
|
||||||
StartDate,
|
StartDate,
|
||||||
ExpireDate,
|
ExpireDate,
|
||||||
SerialNumber,
|
SerialNumber,
|
||||||
|
SubjectAltName,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl CertificateAttributeName {
|
impl CertificateAttributeName {
|
||||||
|
|
@ -307,6 +308,7 @@ impl CertificateAttributeName {
|
||||||
CertificateAttributeName::StartDate => "Start-Date",
|
CertificateAttributeName::StartDate => "Start-Date",
|
||||||
CertificateAttributeName::ExpireDate => "Expire-Date",
|
CertificateAttributeName::ExpireDate => "Expire-Date",
|
||||||
CertificateAttributeName::SerialNumber => "Serial-Number",
|
CertificateAttributeName::SerialNumber => "Serial-Number",
|
||||||
|
CertificateAttributeName::SubjectAltName => "Subject-Alt-Name",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -219,9 +219,11 @@ fn certificate_field(reader: &mut Reader) -> ParseResult<CertificateAttributeNam
|
||||||
Ok(CertificateAttributeName::ExpireDate)
|
Ok(CertificateAttributeName::ExpireDate)
|
||||||
} else if try_literal(r#"Serial-Number""#, reader).is_ok() {
|
} else if try_literal(r#"Serial-Number""#, reader).is_ok() {
|
||||||
Ok(CertificateAttributeName::SerialNumber)
|
Ok(CertificateAttributeName::SerialNumber)
|
||||||
|
} else if try_literal(r#"Subject-Alt-Name""#, reader).is_ok() {
|
||||||
|
Ok(CertificateAttributeName::SubjectAltName)
|
||||||
} else {
|
} else {
|
||||||
let value =
|
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 kind = ParseErrorKind::Expecting { value };
|
||||||
let cur = reader.cursor();
|
let cur = reader.cursor();
|
||||||
Err(ParseError::new(cur.pos, false, kind))
|
Err(ParseError::new(cur.pos, false, kind))
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue