use std::net::IpAddr;
use std::sync::Arc;
use rcgen::{CertificateSigningRequestParams, DnType, DnValue, SanType};
use rustls_pki_types::CertificateSigningRequestDer;
use time::OffsetDateTime;
use time::format_description::well_known::Rfc3339;
use tracing::{error, instrument, warn};
use crate::challenge::ChallengeError;
use crate::error::Problem;
use crate::filter::{EabIdentity, FilterPolicy, IdentifierContext, IdentifierStage, Outcome};
use crate::sqlite::{
account::Account,
authz::{Authorization, Challenge},
db::Database,
nonce::now_secs,
order::{Identifier, Order},
status::OrderStatus,
};
#[instrument(name = "check_identifiers", skip_all)]
pub(crate) async fn check_identifiers(
filter: &FilterPolicy,
client_ip: Option<IpAddr>,
account_id: &str,
profile: &str,
stage: IdentifierStage,
identifiers: &[Identifier],
database: &Database,
) -> Result<(), Problem> {
let eab = if filter.needs_eab() {
resolve_eab(account_id, profile, database).await?
} else {
None
};
let context = IdentifierContext {
client_ip,
account_id,
stage,
identifiers,
eab,
};
match filter.check_identifiers(&context).await {
Outcome::Allow => Ok(()),
Outcome::Deny(detail) => Err(match stage {
IdentifierStage::NewOrder => Problem::rejected_identifier(detail),
IdentifierStage::Csr => Problem::bad_csr(detail),
}),
Outcome::Undecided(_) => Err(Problem::server_internal("Request filtering failed")),
}
}
async fn resolve_eab(
account_id: &str,
profile: &str,
database: &Database,
) -> Result<Option<EabIdentity>, Problem> {
let account = Account::find_by_id(profile, account_id, database)
.await
.map_err(|error| {
error!(event = "account_lookup_failed", outcome = "failure", account_id, error = %error);
Problem::server_internal("Database error")
})?;
let Some(kid) = account.and_then(|account| account.eab_kid) else {
return Ok(None);
};
let key = crate::sqlite::eab::Eab::find_any_by_kid(&kid, database)
.await
.map_err(|error| {
error!(event = "eab_lookup_failed", outcome = "failure", kid = %kid, error = %error);
Problem::server_internal("Database error")
})?;
Ok(key.map(|key| EabIdentity {
kid: key.kid,
label: key.label,
active: key.status == "active",
}))
}
pub(crate) fn challenge_problem(error: &ChallengeError) -> Problem {
match error {
ChallengeError::Connection(detail) => Problem::connection(detail.clone()),
ChallengeError::Dns(detail) => Problem::dns(detail.clone()),
ChallengeError::IncorrectResponse(detail) => Problem::incorrect_response(detail.clone()),
ChallengeError::Tls(detail) => Problem::tls(detail.clone()),
ChallengeError::Unauthorized(detail) => Problem::access_denied(detail.clone()),
ChallengeError::Internal(_) => Problem::server_internal("Challenge validation failed"),
}
}
pub(crate) fn parse_csr(csr_der: &[u8]) -> Result<CertificateSigningRequestParams, Problem> {
let der = CertificateSigningRequestDer::from(csr_der.to_vec());
CertificateSigningRequestParams::from_der(&der).map_err(|error| {
warn!(event = "csr_parse_failed", outcome = "failure", error = %error);
Problem::bad_csr("CSR is unparsable")
})
}
pub(crate) fn check_csr_matches_order(
csr: &CertificateSigningRequestParams,
identifiers: &[Identifier],
) -> Result<(), Problem> {
if let Some(other) = csr
.params
.subject_alt_names
.iter()
.find(|san| !matches!(san, SanType::DnsName(_)))
{
warn!(event = "csr_non_dns_san", outcome = "failure", san = ?other);
return Err(Problem::bad_csr(
"CSR carries a subject alternative name that is not a DNS name",
));
}
let csr_dns: std::collections::BTreeSet<&str> = csr
.params
.subject_alt_names
.iter()
.filter_map(|san| match san {
SanType::DnsName(name) => Some(name.as_str()),
_ => None,
})
.collect();
let want_dns: std::collections::BTreeSet<&str> = identifiers
.iter()
.filter(|id| id.typ == "dns")
.map(|id| id.value.as_str())
.collect();
if csr_dns != want_dns {
warn!(event = "csr_identifier_mismatch", outcome = "failure", csr = ?csr_dns, order = ?want_dns);
return Err(Problem::bad_csr(
"CSR does not request the order's identifiers",
));
}
if let Some(common_name) = csr.params.distinguished_name.get(&DnType::CommonName)
&& let Some(text) = dn_text(common_name)
{
let candidate = normalize_dns_name(&text);
if looks_like_dns_name(&candidate) && !want_dns.contains(candidate.as_str()) {
warn!(event = "csr_common_name_mismatch", outcome = "failure", common_name = %candidate);
return Err(Problem::bad_csr(
"CSR common name is a domain the order does not cover",
));
}
}
Ok(())
}
fn looks_like_dns_name(value: &str) -> bool {
!value.is_empty()
&& value.contains('.')
&& !value.chars().any(|c| c.is_ascii_whitespace())
&& well_formed_name(value)
}
pub(crate) fn csr_identifiers(csr: &CertificateSigningRequestParams) -> Vec<Identifier> {
let mut identifiers: Vec<Identifier> = csr
.params
.subject_alt_names
.iter()
.map(|san| match san {
SanType::DnsName(name) => Identifier::dns(normalize_dns_name(name.as_str())),
SanType::IpAddress(ip) => Identifier::new("ip", ip.to_canonical().to_string()),
SanType::Rfc822Name(name) => Identifier::new("email", name.as_str().to_string()),
SanType::URI(uri) => Identifier::new("uri", uri.as_str().to_string()),
other => Identifier::new("other", format!("{other:?}")),
})
.collect();
if let Some(common_name) = csr.params.distinguished_name.get(&DnType::CommonName) {
identifiers.push(match dn_text(common_name) {
Some(value) => Identifier::new("cn", normalize_dns_name(&value)),
None => Identifier::new("other", format!("{common_name:?}")),
});
}
identifiers
}
#[must_use]
pub fn normalize_dns_name(value: &str) -> String {
let trimmed = value.strip_suffix('.').unwrap_or(value);
trimmed.to_ascii_lowercase()
}
#[must_use]
pub fn is_wildcard(value: &str) -> bool {
value.starts_with("*.")
}
const MAX_DNS_NAME: usize = 253;
const MAX_DNS_LABEL: usize = 63;
#[must_use]
pub fn well_formed_name(value: &str) -> bool {
let name = match value.strip_prefix("*.") {
Some(rest) => rest,
None => value,
};
!name.contains('*') && is_dns_name(name)
}
fn is_dns_name(name: &str) -> bool {
if name.is_empty() || name.len() > MAX_DNS_NAME {
return false;
}
let name = name.strip_suffix('.').unwrap_or(name);
if name.is_empty() {
return false;
}
name.split('.').all(is_dns_label)
}
fn is_dns_label(label: &str) -> bool {
!label.is_empty()
&& label.len() <= MAX_DNS_LABEL
&& !label.starts_with('-')
&& !label.ends_with('-')
&& label
.bytes()
.all(|b| b.is_ascii_alphanumeric() || b == b'-' || b == b'_')
}
pub(crate) fn validate_contacts(contacts: &[String]) -> Result<(), Problem> {
match contact_shape_error(contacts) {
None => Ok(()),
Some(rejection) if rejection.unsupported => {
Err(Problem::unsupported_contact(rejection.detail))
}
Some(rejection) => Err(Problem::invalid_contact(rejection.detail)),
}
}
pub(crate) struct ContactRejection {
pub unsupported: bool,
pub detail: String,
}
pub(crate) fn contact_shape_error(contacts: &[String]) -> Option<ContactRejection> {
const MAX_CONTACTS: usize = 32;
fn unsupported(detail: String) -> Option<ContactRejection> {
Some(ContactRejection {
unsupported: true,
detail,
})
}
fn invalid(detail: String) -> Option<ContactRejection> {
Some(ContactRejection {
unsupported: false,
detail,
})
}
if contacts.len() > MAX_CONTACTS {
warn!(
event = "contact_list_too_long",
outcome = "failure",
contacts_count = contacts.len()
);
return invalid(format!(
"An account may carry at most {MAX_CONTACTS} contacts; this one carries {}",
contacts.len()
));
}
for contact in contacts {
let Some(rest) = contact.strip_prefix("mailto:") else {
let scheme = contact.split_once(':').map_or("(none)", |(s, _)| s);
warn!(event = "contact_scheme_unsupported", outcome = "failure", scheme = %scheme);
return unsupported(format!(
"Contact {contact} uses an unsupported scheme; only mailto: is supported"
));
};
if rest.chars().any(|c| c.is_control()) {
warn!(
event = "contact_has_control_characters",
outcome = "failure"
);
return invalid(format!(
"Contact {contact:?} carries a control character, which is not part of an address"
));
}
if rest.contains('?') {
warn!(event = "contact_has_hfields", outcome = "failure");
return invalid(format!(
"Contact {contact} carries hfields, which RFC 8555 §7.3 forbids"
));
}
if rest.contains(',') {
warn!(
event = "contact_has_multiple_addresses",
outcome = "failure"
);
return invalid(format!(
"Contact {contact} names more than one address; RFC 8555 §7.3 allows one"
));
}
let Some((local, domain)) = rest.rsplit_once('@') else {
warn!(event = "contact_not_an_address", outcome = "failure");
return invalid(format!("Contact {contact} is not an email address"));
};
if local.is_empty() || domain.is_empty() || !domain.contains('.') {
warn!(event = "contact_address_incomplete", outcome = "failure");
return invalid(format!("Contact {contact} is not a complete email address"));
}
}
None
}
pub(crate) fn dn_text(value: &DnValue) -> Option<String> {
match value {
DnValue::Utf8String(text) => Some(text.clone()),
DnValue::Ia5String(text) => Some(text.as_str().to_string()),
DnValue::PrintableString(text) => Some(text.as_str().to_string()),
DnValue::TeletexString(text) => Some(text.as_str().to_string()),
_ => None,
}
}
#[instrument(name = "signer_account", skip_all)]
pub(crate) async fn signer_account(
cached: Option<Account>,
profile: &str,
pubkey: &[u8],
database: &Arc<Database>,
) -> Result<Account, Problem> {
let account = match cached {
Some(account) => account,
None => Account::find_by_pubkey(profile, pubkey, database)
.await
.map_err(|error| {
error!(event = "account_lookup_failed", outcome = "failure", error = %error);
Problem::server_internal("Account lookup failed")
})?
.ok_or_else(|| Problem::account_does_not_exist("Unknown account"))?,
};
if account.status == "deactivated" {
warn!(event = "account_deactivated_request_refused", outcome = "failure", account_id = %account.id);
return Err(Problem::unauthorized("Account is deactivated"));
}
Ok(account)
}
#[instrument(name = "load_owned_order", skip_all, fields(order_id = %id, account_id = %account.id))]
pub(crate) async fn load_owned_order(
id: &str,
account: &Account,
database: &Arc<Database>,
) -> Result<Order, Problem> {
let order = Order::find_by_id(id, database)
.await
.map_err(|error| {
error!(event = "order_lookup_failed", outcome = "failure", order_id = %id, error = %error);
Problem::server_internal("Order lookup failed")
})?
.ok_or_else(|| Problem::malformed("Unknown order"))?;
if order.profile != account.profile {
warn!(
event = "order_profile_mismatch",
outcome = "failure",
order_id = %id,
order_profile = %order.profile,
request_profile = %account.profile
);
return Err(Problem::malformed("Unknown order"));
}
if order.account_id != account.id {
warn!(event = "order_ownership_mismatch", outcome = "failure", order_id = %id, account_id = %account.id);
return Err(Problem::unauthorized(
"Order belongs to a different account",
));
}
if order.status != OrderStatus::Valid && order.expires <= now_secs() {
warn!(event = "order_expired", outcome = "failure", order_id = %id, expires = order.expires);
return Err(Problem::malformed("Order has expired"));
}
Ok(order)
}
#[instrument(name = "load_owned_authz", skip_all, fields(authz_id = %id))]
pub(crate) async fn load_owned_authz(
id: &str,
account: &Account,
database: &Arc<Database>,
) -> Result<(Authorization, Order), Problem> {
let authz = Authorization::find_by_id(id, database)
.await
.map_err(|error| {
error!(event = "authz_lookup_failed", outcome = "failure", authz_id = %id, error = %error);
Problem::server_internal("Authorization lookup failed")
})?
.ok_or_else(|| Problem::malformed("Unknown authorization"))?;
let order = load_owned_order(&authz.order_id, account, database).await?;
Ok((authz, order))
}
#[instrument(name = "load_owned_challenge", skip_all, fields(challenge_id = %id))]
pub(crate) async fn load_owned_challenge(
id: &str,
account: &Account,
database: &Arc<Database>,
) -> Result<(Challenge, Authorization, Order), Problem> {
let challenge = Challenge::find_by_id(id, database)
.await
.map_err(|error| {
error!(event = "challenge_lookup_failed", outcome = "failure", challenge_id = %id, error = %error);
Problem::server_internal("Challenge lookup failed")
})?
.ok_or_else(|| Problem::malformed("Unknown challenge"))?;
let (authz, order) = load_owned_authz(&challenge.authz_id, account, database).await?;
Ok((challenge, authz, order))
}
#[instrument(name = "order_authz_ids", skip_all, fields(order_id = %order_id))]
pub(crate) async fn order_authz_ids(
order_id: &str,
database: &Arc<Database>,
) -> Result<Vec<String>, Problem> {
Ok(Authorization::find_by_order(order_id, database)
.await
.map_err(|error| {
error!(event = "authz_list_failed", outcome = "failure", order_id = %order_id, error = %error);
Problem::server_internal("Authorization lookup failed")
})?
.into_iter()
.map(|authz| authz.id)
.collect())
}
pub(crate) fn parse_rfc3339(field: &str, value: &str) -> Result<i64, Problem> {
OffsetDateTime::parse(value, &Rfc3339)
.map(time::OffsetDateTime::unix_timestamp)
.map_err(|_| {
warn!(event = "order_datetime_invalid", outcome = "failure", field = %field, value = %value);
Problem::malformed("Invalid notBefore/notAfter datetime")
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn wildcard_shapes_are_recognised_and_the_rest_refused() {
assert!(is_wildcard("*.example.com"));
assert!(well_formed_name("*.example.com"));
assert!(!is_wildcard("example.com"));
assert!(well_formed_name("example.com"));
for bad in [
"*example.com",
"*.*.example.com",
"a.*.example.com",
"*",
"*.",
] {
assert!(!well_formed_name(bad), "{bad} must not be well formed");
}
assert!(!is_wildcard("*example.com"));
}
#[test]
fn a_dns_identifier_that_is_not_a_dns_name_is_refused() {
for good in [
"example.com",
"a.example.com",
"EXAMPLE.com",
"host-1.example.com",
"_acme.example.com",
"single-label",
"1.2.3.4",
"*.sub.example.com",
] {
assert!(well_formed_name(good), "{good} must be well formed");
}
for bad in [
"a.example.com,b.example.com",
"internal.corp/",
"user@internal.corp",
"example.com#frag",
"example.com?q=1",
"example .com",
"example.com:8080",
"example.com\n",
"example.com\r\nX",
"example\t.com",
"",
".",
"..",
"a..b",
".example.com",
"-example.com",
"example-.com",
"a.-b.com",
] {
assert!(!well_formed_name(bad), "{bad:?} must not be well formed");
}
}
#[test]
fn a_dns_identifier_longer_than_the_protocol_allows_is_refused() {
let label = "a".repeat(MAX_DNS_LABEL);
assert!(well_formed_name(&format!("{label}.example.com")));
let too_long_label = "a".repeat(MAX_DNS_LABEL + 1);
assert!(!well_formed_name(&format!("{too_long_label}.example.com")));
let name = std::iter::repeat_n(label.as_str(), 4)
.collect::<Vec<_>>()
.join(".");
assert_eq!(name.len(), 255);
assert!(!well_formed_name(&name));
let fits = format!("{}.com", &name[..MAX_DNS_NAME - 4]);
assert_eq!(fits.len(), MAX_DNS_NAME);
assert!(well_formed_name(&fits));
}
#[test]
fn a_trailing_root_label_is_accepted_but_a_bare_dot_is_not() {
assert!(well_formed_name("example.com."));
assert!(!well_formed_name("example.com.."));
assert!(!well_formed_name("."));
}
#[test]
fn a_contact_carrying_a_control_character_is_refused() {
for bad in [
"mailto:alice@example.com\nBcc: attacker@evil.test",
"mailto:alice@example.com\r\n",
"mailto:al\tice@example.com",
"mailto:alice@example.com\u{0}",
] {
let rejection = contact_shape_error(&[bad.to_string()])
.unwrap_or_else(|| panic!("{bad:?} must be refused"));
assert!(!rejection.unsupported, "{bad:?}");
}
assert!(contact_shape_error(&["mailto:alice@example.com".to_string()]).is_none());
}
#[test]
fn challenge_errors_map_to_their_acme_problem_types() {
let cases = [
(
ChallengeError::Connection("refused".into()),
"urn:ietf:params:acme:error:connection",
400,
),
(
ChallengeError::Dns("no record".into()),
"urn:ietf:params:acme:error:dns",
400,
),
(
ChallengeError::IncorrectResponse("wrong value".into()),
"urn:ietf:params:acme:error:incorrectResponse",
403,
),
(
ChallengeError::Tls("no alpn".into()),
"urn:ietf:params:acme:error:tls",
400,
),
(
ChallengeError::Unauthorized("wrong body".into()),
"urn:ietf:params:acme:error:unauthorized",
403,
),
];
for (error, typ, status) in cases {
let value = challenge_problem(&error).to_value();
assert_eq!(value["type"], typ);
assert_eq!(value["status"], status);
assert_eq!(value["detail"], error.detail());
}
let internal = challenge_problem(&ChallengeError::Internal("no validator".into()));
let value = internal.to_value();
assert_eq!(value["type"], "urn:ietf:params:acme:error:serverInternal");
assert_eq!(value["status"], 500);
assert!(!value["detail"].as_str().unwrap().contains("no validator"));
}
fn csr_with(sans: Vec<SanType>, common_name: Option<&str>) -> Vec<u8> {
let key_pair = rcgen::KeyPair::generate().unwrap();
let mut params = rcgen::CertificateParams::default();
params.subject_alt_names = sans;
params.distinguished_name = rcgen::DistinguishedName::new();
if let Some(name) = common_name {
params.distinguished_name.push(DnType::CommonName, name);
}
params.serialize_request(&key_pair).unwrap().der().to_vec()
}
fn find<'a>(identifiers: &'a [Identifier], typ: &str) -> Vec<&'a str> {
identifiers
.iter()
.filter(|id| id.typ == typ)
.map(|id| id.value.as_str())
.collect()
}
#[test]
fn csr_identifiers_projects_every_san_type() {
let der = csr_with(
vec![
SanType::DnsName("host.example.com".try_into().unwrap()),
SanType::IpAddress("10.0.0.1".parse().unwrap()),
SanType::Rfc822Name("someone@example.com".try_into().unwrap()),
SanType::URI("https://example.com/x".try_into().unwrap()),
],
None,
);
let identifiers = csr_identifiers(&parse_csr(&der).unwrap());
assert_eq!(find(&identifiers, "dns"), vec!["host.example.com"]);
assert_eq!(find(&identifiers, "ip"), vec!["10.0.0.1"]);
assert_eq!(find(&identifiers, "email"), vec!["someone@example.com"]);
assert_eq!(find(&identifiers, "uri"), vec!["https://example.com/x"]);
}
#[test]
fn csr_identifiers_renders_ipv6_addresses() {
let der = csr_with(
vec![SanType::IpAddress("2001:db8::1".parse().unwrap())],
None,
);
assert_eq!(
find(&csr_identifiers(&parse_csr(&der).unwrap()), "ip"),
vec!["2001:db8::1"]
);
}
#[test]
fn csr_identifiers_includes_the_common_name() {
let der = csr_with(
vec![SanType::DnsName("ok.example.com".try_into().unwrap())],
Some("secret.internal.example.com"),
);
let identifiers = csr_identifiers(&parse_csr(&der).unwrap());
assert_eq!(find(&identifiers, "dns"), vec!["ok.example.com"]);
assert_eq!(
find(&identifiers, "cn"),
vec!["secret.internal.example.com"]
);
}
#[test]
fn csr_identifiers_omits_an_absent_common_name() {
let der = csr_with(
vec![SanType::DnsName("ok.example.com".try_into().unwrap())],
None,
);
assert!(find(&csr_identifiers(&parse_csr(&der).unwrap()), "cn").is_empty());
}
#[test]
fn parse_csr_rejects_garbage() {
assert!(parse_csr(&[0xde, 0xad, 0xbe, 0xef]).is_err());
}
use crate::testutil::dns_identifiers as dns;
#[test]
fn a_csr_matching_the_order_exactly_is_accepted() {
let der = csr_with(
vec![
SanType::DnsName("a.example.com".try_into().unwrap()),
SanType::DnsName("b.example.com".try_into().unwrap()),
],
None,
);
let identifiers = dns(&["b.example.com", "a.example.com"]);
assert!(check_csr_matches_order(&parse_csr(&der).unwrap(), &identifiers).is_ok());
}
#[test]
fn a_csr_naming_another_domain_is_refused() {
let der = csr_with(
vec![SanType::DnsName("victim.example".try_into().unwrap())],
None,
);
let value = check_csr_matches_order(&parse_csr(&der).unwrap(), &dns(&["a.example.com"]))
.unwrap_err()
.to_value();
assert_eq!(value["type"], "urn:ietf:params:acme:error:badCSR");
assert_eq!(value["status"], 400);
}
#[test]
fn a_csr_naming_more_than_the_order_is_refused() {
let der = csr_with(
vec![
SanType::DnsName("a.example.com".try_into().unwrap()),
SanType::DnsName("extra.example.com".try_into().unwrap()),
],
None,
);
assert!(
check_csr_matches_order(&parse_csr(&der).unwrap(), &dns(&["a.example.com"])).is_err()
);
}
#[test]
fn a_csr_naming_less_than_the_order_is_refused() {
let der = csr_with(
vec![SanType::DnsName("a.example.com".try_into().unwrap())],
None,
);
assert!(
check_csr_matches_order(
&parse_csr(&der).unwrap(),
&dns(&["a.example.com", "b.example.com"]),
)
.is_err()
);
}
#[test]
fn a_csr_smuggling_a_non_dns_san_is_refused() {
let der = csr_with(
vec![
SanType::DnsName("a.example.com".try_into().unwrap()),
SanType::IpAddress("10.0.0.1".parse().unwrap()),
],
None,
);
assert!(
check_csr_matches_order(&parse_csr(&der).unwrap(), &dns(&["a.example.com"])).is_err()
);
}
#[test]
fn a_csr_whose_common_name_is_not_an_order_identifier_is_refused() {
let der = csr_with(
vec![SanType::DnsName("a.example.com".try_into().unwrap())],
Some("victim.example"),
);
assert!(
check_csr_matches_order(&parse_csr(&der).unwrap(), &dns(&["a.example.com"])).is_err()
);
}
#[test]
fn a_csr_whose_common_name_is_an_order_identifier_is_accepted() {
let der = csr_with(
vec![SanType::DnsName("a.example.com".try_into().unwrap())],
Some("a.example.com"),
);
assert!(
check_csr_matches_order(&parse_csr(&der).unwrap(), &dns(&["a.example.com"])).is_ok()
);
}
#[test]
fn a_common_name_that_is_a_human_label_is_left_alone() {
for label in ["rcgen self signed cert", "ACME client", "no-dot-label"] {
let der = csr_with(
vec![SanType::DnsName("a.example.com".try_into().unwrap())],
Some(label),
);
assert!(
check_csr_matches_order(&parse_csr(&der).unwrap(), &dns(&["a.example.com"]))
.is_ok(),
"{label} should not be read as a host name"
);
}
}
#[test]
fn common_names_are_recognised_as_host_names_or_not() {
assert!(looks_like_dns_name("a.example.com"));
assert!(looks_like_dns_name("*.example.com"));
assert!(!looks_like_dns_name(""));
assert!(!looks_like_dns_name("localhost"));
assert!(!looks_like_dns_name("rcgen self signed cert"));
assert!(!looks_like_dns_name("a.*.example.com"));
}
#[test]
fn a_wildcard_csr_matching_its_order_is_accepted() {
let der = csr_with(
vec![SanType::DnsName("*.example.com".try_into().unwrap())],
None,
);
assert!(
check_csr_matches_order(&parse_csr(&der).unwrap(), &dns(&["*.example.com"])).is_ok()
);
}
#[test]
fn a_csr_differing_only_in_case_is_refused() {
let der = csr_with(
vec![SanType::DnsName("A.Example.COM".try_into().unwrap())],
None,
);
assert!(
check_csr_matches_order(&parse_csr(&der).unwrap(), &dns(&["a.example.com"])).is_err()
);
}
#[test]
fn dn_text_reads_the_string_encodings() {
assert_eq!(
dn_text(&DnValue::Utf8String("a.example.com".to_string())).as_deref(),
Some("a.example.com")
);
assert_eq!(
dn_text(&DnValue::Ia5String("b.example.com".try_into().unwrap())).as_deref(),
Some("b.example.com")
);
assert_eq!(
dn_text(&DnValue::PrintableString(
"c.example.com".try_into().unwrap()
))
.as_deref(),
Some("c.example.com")
);
assert_eq!(
dn_text(&DnValue::TeletexString("d.example.com".try_into().unwrap())).as_deref(),
Some("d.example.com")
);
}
#[test]
fn an_unreadable_common_name_becomes_an_other_identifier() {
let value = DnValue::BmpString("e.example.com".try_into().unwrap());
assert!(dn_text(&value).is_none());
}
}