Skip to main content

acme_proxy_protocol/acme/
rules.rs

1//! What a request may ask for, as checks over the values themselves.
2//!
3//! No database, no HTTP: an identifier's shape, a contact's, a CSR's agreement
4//! with its order, a datetime's format. The `acme::*` services call these
5//! before anything is written, and the job handlers call the same functions on
6//! the same values, so one rule has one implementation.
7//!
8//! **The CSR checks are the security boundary for `custom` and `relay`.**
9//! Neither backend can check a CSR against the order — one hands it to an
10//! operator's script, the other to an upstream CA that never saw the
11//! authorizations — so whatever passes here is what those backends sign. Read
12//! [`check_csr_matches_order`] before changing any of it, and keep it reading
13//! the DER rather than a re-serialisation of it.
14
15use rcgen::{CertificateSigningRequestParams, DnType, DnValue, SanType};
16use rustls_pki_types::CertificateSigningRequestDer;
17use time::OffsetDateTime;
18use time::format_description::well_known::Rfc3339;
19use tracing::warn;
20
21use acme_proxy_core::error::Problem;
22use acme_proxy_core::identifier::Identifier;
23
24/// Parses a PKCS#10 CSR, which also verifies its own self-signature.
25///
26/// Separated from [`csr_identifiers`] because `OrderService::finalize` performs two checks
27/// on the same CSR: parsing it once avoids reparsing it.
28pub(crate) fn parse_csr(csr_der: &[u8]) -> Result<CertificateSigningRequestParams, Problem> {
29    let der = CertificateSigningRequestDer::from(csr_der.to_vec());
30    CertificateSigningRequestParams::from_der(&der).map_err(|error| {
31        warn!(event = "csr_parse_failed", outcome = "failure", error = %error);
32        Problem::bad_csr("CSR is unparsable")
33    })
34}
35
36/// RFC 8555 §7.4: "The CSR MUST indicate the exact same set of requested
37/// identifiers as the initial newOrder request."
38///
39/// This check lives **here**, in the handler, and not in a backend.
40/// `LocalCa::issue` has always done it, but it is the only backend doing so:
41/// `custom` passes the CSR to an operator script which is not told to verify it,
42/// and `relay` relays it to an upstream CA which only sees *this* server's
43/// account and doesn't know which names the local client has proven.
44/// With either of them, an account authorized for one name could get a certificate
45/// for any other name. The backend check remains as defense in depth — a backend
46/// is a unit others build tests against, and must be safe when asked to sign
47/// directly — but this check makes the guarantee independent of the backend.
48///
49/// Raw comparison, without renormalizing the CSR side: the order identifiers
50/// have already been normalized by `OrderService::new_order`, and normalizing here
51/// would allow signing a leaf bearing `EXAMPLE.COM.` when the check compared
52/// `example.com`. This is also what keeps this check and the one in `LocalCa::issue`
53/// in agreement byte for byte.
54pub(crate) fn check_csr_matches_order(
55    csr: &CertificateSigningRequestParams,
56    csr_der: &[u8],
57    identifiers: &[Identifier],
58) -> Result<(), Problem> {
59    // A SAN that is not a DNS name would not be seen by the set comparison
60    // below, and would therefore travel uninspected all the way into the
61    // signed leaf.
62    if let Some(other) = csr
63        .params
64        .subject_alt_names
65        .iter()
66        .find(|san| !matches!(san, SanType::DnsName(_)))
67    {
68        warn!(event = "csr_non_dns_san", outcome = "failure", san = ?other);
69        return Err(Problem::bad_csr(
70            "CSR carries a subject alternative name that is not a DNS name",
71        ));
72    }
73
74    let csr_dns: std::collections::BTreeSet<&str> = csr
75        .params
76        .subject_alt_names
77        .iter()
78        .filter_map(|san| match san {
79            SanType::DnsName(name) => Some(name.as_str()),
80            _ => None,
81        })
82        .collect();
83    let want_dns: std::collections::BTreeSet<&str> = identifiers
84        .iter()
85        .filter(|id| id.typ == "dns")
86        .map(|id| id.value.as_str())
87        .collect();
88
89    if csr_dns != want_dns {
90        warn!(event = "csr_identifier_mismatch", outcome = "failure", csr = ?csr_dns, order = ?want_dns);
91        return Err(Problem::bad_csr(
92            "CSR does not request the order's identifiers",
93        ));
94    }
95
96    // `CertificateSigningRequestParams::from_der` copies the entire distinguished name
97    // from the CSR into `params`, and `signed_by` writes it into the leaf: a CN
98    // naming a domain the order never authorized ends up asserted by
99    // a certificate that this CA signed. Verifiers ignore the CN since
100    // RFC 2818 was replaced, but "ignored by most" is not "unasserted".
101    //
102    // Only CNs *shaped like a DNS name* are checked. A CN is also, very
103    // ordinarily, a human label — rcgen puts "rcgen self signed
104    // cert" by default — and this is exactly why `filter::identifiers`
105    // already excludes `cn` from its `allow` rules (`SUBJECT_ONLY_TYPES`) while
106    // leaving it reachable by `deny`. Refusing any label would break legitimate
107    // clients without protecting anything: what is dangerous is a CN
108    // that an old verifier could read as a hostname.
109    //
110    // `LocalCa::issue` goes further and empties the whole distinguished name; this check
111    // is what covers backends which transmit the CSR as-is
112    // (`custom`, `relay`).
113    //
114    // Read from the **DER**, not from `csr.params`: rcgen's distinguished name
115    // is a map keyed by type, so of two `CommonName` attributes it keeps only
116    // the last, and it exposes a BMPString or UniversalString value as neither
117    // text nor bytes. Either shape is a CN this check would never see, while
118    // the raw CSR — the thing a `custom` script or an upstream CA is handed —
119    // still carries it.
120    // A CN with no dot is prose here ("ACME client"), unless the order itself
121    // deals in single-label names — which this server allows — in which case a
122    // CN shaped like one is a name being asserted like any other.
123    let single_label_order = want_dns.iter().any(|name| !name.contains('.'));
124    for common_name in subject_common_names(csr_der)? {
125        let candidate = normalize_dns_name(&common_name);
126        let asserted = looks_like_dns_name(&candidate)
127            || (single_label_order
128                && well_formed_name(&candidate)
129                && !candidate.chars().any(|c| c.is_ascii_whitespace()));
130        if asserted && !want_dns.contains(candidate.as_str()) {
131            warn!(event = "csr_common_name_mismatch", outcome = "failure", common_name = %candidate);
132            return Err(Problem::bad_csr(
133                "CSR common name is a domain the order does not cover",
134            ));
135        }
136    }
137
138    Ok(())
139}
140
141/// Every `CommonName` of the CSR's subject, in the order the DER carries them.
142///
143/// Fails closed: a CN whose string encoding this cannot read is refused rather
144/// than skipped, since the check above is the only thing standing between a
145/// `custom` script or an upstream CA and a CN naming a domain the order never
146/// authorized. Reading the DER twice — rcgen has already parsed it — costs one
147/// parse of a few hundred bytes on the finalize path.
148fn subject_common_names(csr_der: &[u8]) -> Result<Vec<String>, Problem> {
149    use x509_parser::prelude::FromDer;
150
151    let (_, request) = x509_parser::certification_request::X509CertificationRequest::from_der(
152        csr_der,
153    )
154    .map_err(|error| {
155        warn!(event = "csr_parse_failed", outcome = "failure", error = %error);
156        Problem::bad_csr("CSR is unparsable")
157    })?;
158
159    request
160        .certification_request_info
161        .subject
162        .iter_common_name()
163        .map(|attribute| {
164            attribute.as_str().map(str::to_string).map_err(|_| {
165                warn!(event = "csr_common_name_unreadable", outcome = "failure");
166                Problem::bad_csr("CSR common name is not a readable string")
167            })
168        })
169        .collect()
170}
171
172/// Whether a subject `CommonName` reads as a host name rather than a human
173/// label — the distinction [`check_csr_matches_order`] uses to decide whether a
174/// CN is a name being asserted or just descriptive text.
175fn looks_like_dns_name(value: &str) -> bool {
176    !value.is_empty()
177        && value.contains('.')
178        && !value.chars().any(|c| c.is_ascii_whitespace())
179        && well_formed_name(value)
180}
181
182/// Projects everything a CSR asks to have certified into the shared
183/// [`Identifier`] shape, so one policy list covers all of it.
184pub(crate) fn csr_identifiers(csr: &CertificateSigningRequestParams) -> Vec<Identifier> {
185    let mut identifiers: Vec<Identifier> = csr
186        .params
187        .subject_alt_names
188        .iter()
189        .map(|san| match san {
190            SanType::DnsName(name) => Identifier::dns(normalize_dns_name(name.as_str())),
191            SanType::IpAddress(ip) => Identifier::new("ip", ip.to_canonical().to_string()),
192            SanType::Rfc822Name(name) => Identifier::new("email", name.as_str().to_string()),
193            SanType::URI(uri) => Identifier::new("uri", uri.as_str().to_string()),
194            other => Identifier::new("other", format!("{other:?}")),
195        })
196        .collect();
197
198    if let Some(common_name) = csr.params.distinguished_name.get(&DnType::CommonName) {
199        identifiers.push(match dn_text(common_name) {
200            Some(value) => Identifier::new("cn", normalize_dns_name(&value)),
201            None => Identifier::new("other", format!("{common_name:?}")),
202        });
203    }
204
205    identifiers
206}
207
208/// Canonicalizes a DNS name for comparison: lowercased, with one trailing dot
209/// removed.
210#[must_use]
211pub fn normalize_dns_name(value: &str) -> String {
212    let trimmed = value.strip_suffix('.').unwrap_or(value);
213    trimmed.to_ascii_lowercase()
214}
215
216/// Whether an identifier names every host under a domain (RFC 8555 §7.1.3).
217#[must_use]
218pub fn is_wildcard(value: &str) -> bool {
219    value.starts_with("*.")
220}
221
222/// The longest a DNS name may be, in presentation form (RFC 1035 §2.3.4's
223/// 255-octet wire limit less the root label and the length octet of the first).
224const MAX_DNS_NAME: usize = 253;
225
226/// The longest a single DNS label may be (RFC 1035 §2.3.4).
227const MAX_DNS_LABEL: usize = 63;
228
229/// Whether a `dns` identifier is a shape this server can act on.
230///
231/// Two rules, and the second is load-bearing beyond mere tidiness.
232///
233/// The wildcard rule (§7.1.3): a `*` is legal only as a single leading `*.`.
234///
235/// The syntax rule: what is left has to actually be a DNS name — bounded
236/// length, non-empty labels drawn from letters, digits, `-` and `_`, with no
237/// label starting or ending in `-`. Without it an identifier may carry `,`,
238/// ` `, `@`, `/`, `#` or a control character, and two subsystems read the
239/// resulting string as something other than one opaque name:
240///
241/// - `filter::custom` joins the identifiers with `,` into
242///   `ACME_FILTER_IDENTIFIERS`, so a name *containing* a comma reads to an
243///   operator script as two names.
244/// - `challenge::http_01` builds `http://{name}/.well-known/…` and hands it to
245///   `Url::parse`, which resolves `internal.corp/` to the host `internal.corp`
246///   and `a@internal.corp` to userinfo plus that host — neither of which the
247///   anchored `deny` regex `internal\.corp` matches. With `challenge.bypass`
248///   on, where `[filter]` is the only access control there is, that is a
249///   deny-list bypass.
250///
251/// **This covers the *order*'s identifiers and only those.** It is applied by
252/// `OrderService::new_order`, so it reaches everything derived from an order — including
253/// the `dns` entries [`csr_identifiers`] projects, which
254/// [`check_csr_matches_order`] has already required to equal them. It does *not*
255/// reach the `cn` and `other` entries that projection adds, which come from the
256/// CSR's subject and are arbitrary text by nature; the delimiter half of this
257/// rule is restated for them by `filter::custom::delimiter_free`, at the one
258/// sink that cares. The `http_01` half needs no such twin — a challenge is
259/// validated against an order identifier, never against a CSR subject.
260///
261/// `_` is deliberately allowed: this server exists to serve internal networks,
262/// where underscore labels are ordinary. The point is to reject delimiters and
263/// control characters, not to enforce a public CA's hostname policy.
264#[must_use]
265pub fn well_formed_name(value: &str) -> bool {
266    let name = match value.strip_prefix("*.") {
267        Some(rest) => rest,
268        None => value,
269    };
270    // Any remaining `*` is a wildcard somewhere it is not allowed.
271    !name.contains('*') && is_dns_name(name)
272}
273
274/// Whether `name` is a syntactically valid DNS name in presentation form.
275fn is_dns_name(name: &str) -> bool {
276    if name.is_empty() || name.len() > MAX_DNS_NAME {
277        return false;
278    }
279    // One trailing dot is the root label and is normalized away before this is
280    // ever compared; anything else empty is a malformed name.
281    let name = name.strip_suffix('.').unwrap_or(name);
282    if name.is_empty() {
283        return false;
284    }
285    name.split('.').all(is_dns_label)
286}
287
288/// Whether a `dns` identifier is really an IP address in disguise.
289///
290/// [`well_formed_name`] accepts `10.0.0.5`: every label is digits, and digits
291/// are a legal label. But `challenge::http_01` hands the name to `Url::parse`,
292/// whose WHATWG host parser reads `10.0.0.5` — and `2130706433`, `0x7f.1`,
293/// `127.1` — as an IPv4 address. A name-shaped `filter.identifiers` regex then
294/// judges one string while the validator connects to another address, and the
295/// certificate would carry an address in a dNSName SAN. RFC 1123 §2.1 already
296/// says a host name's last label is never all-numeric.
297///
298/// The question is answered by the **same** parser the validator uses, so the
299/// two can never disagree about what counts as an address. Kept apart from
300/// [`well_formed_name`] on purpose: that function also decides whether a CSR
301/// common name is a name being asserted, and `10.0.0.5` must stay one there.
302#[must_use]
303pub fn names_an_ip_address(value: &str) -> bool {
304    let name = value.strip_prefix("*.").unwrap_or(value);
305    // A name the parser refuses outright (bad punycode, say) is no address:
306    // `Url::parse` refuses it the same way, so the validator never connects.
307    matches!(
308        url::Host::parse(name),
309        Ok(url::Host::Ipv4(_) | url::Host::Ipv6(_))
310    )
311}
312
313/// Whether one dot-separated component is a valid label.
314fn is_dns_label(label: &str) -> bool {
315    !label.is_empty()
316        && label.len() <= MAX_DNS_LABEL
317        && !label.starts_with('-')
318        && !label.ends_with('-')
319        && label
320            .bytes()
321            .all(|b| b.is_ascii_alphanumeric() || b == b'-' || b == b'_')
322}
323
324/// Validates an account's `contact` URLs (RFC 8555 §7.3).
325///
326/// §7.3: "The server SHOULD validate that the contact URLs in the `contact`
327/// field are valid and supported by the server. If the server validates contact
328/// URLs, it MUST support the `mailto` scheme." This server supports `mailto:`
329/// and nothing else — there is no other scheme it could act on — so anything
330/// else is `unsupportedContact`.
331///
332/// Within `mailto:`, §7.3 names two shapes to reject: "Clients MUST NOT provide
333/// a `mailto` URL in the `contact` field that contains `hfields` [RFC6068] or
334/// more than one `addr-spec` in the `to` component. If a server encounters a
335/// `mailto` contact URL that does not meet these criteria, then it SHOULD
336/// reject it as invalid." Both are `invalidContact`; the distinction from
337/// `unsupportedContact` is deliberate, since only one of the two tells the
338/// client to try a different scheme.
339///
340/// Deliberately *not* a deliverability check: whether mail reaches the address
341/// is not something a syntax check can answer, and refusing a valid-but-unusual
342/// local-part would lock an operator out of their own account.
343pub(crate) fn validate_contacts(contacts: &[String]) -> Result<(), Problem> {
344    match contact_shape_error(contacts) {
345        None => Ok(()),
346        Some(rejection) if rejection.unsupported => {
347            Err(Problem::unsupported_contact(rejection.detail))
348        }
349        Some(rejection) => Err(Problem::invalid_contact(rejection.detail)),
350    }
351}
352
353/// A contact RFC 8555 §7.3 refuses, and which of its two refusals applies.
354pub struct ContactRejection {
355    /// `true` maps to `unsupportedContact`, `false` to `invalidContact`. The
356    /// distinction is deliberate: only one of the two tells the client to try
357    /// a different scheme.
358    pub unsupported: bool,
359    pub detail: String,
360}
361
362/// The shape check behind [`validate_contacts`], returning the reason rather
363/// than a [`Problem`].
364///
365/// Split out because `Problem`'s fields are private, so the web admin — which
366/// answers in its own error shape and not in `application/problem+json` —
367/// could not read the detail back out of one. Sharing the check rather than
368/// writing a second one is what keeps `PATCH /api/accounts/{id}` from
369/// accepting a contact `newAccount` would have refused.
370pub fn contact_shape_error(contacts: &[String]) -> Option<ContactRejection> {
371    /// Most `contact` entries an account may carry.
372    ///
373    /// `order.max_identifiers`' reasoning on the account side: the list is
374    /// unauthenticated client input bounded only by `server.max_body_bytes`,
375    /// it is stored as one JSON column and re-rendered on every account read,
376    /// and `notify` walks it per message. Well past any real address book —
377    /// the point is that there is a ceiling.
378    const MAX_CONTACTS: usize = 32;
379
380    fn unsupported(detail: String) -> Option<ContactRejection> {
381        Some(ContactRejection {
382            unsupported: true,
383            detail,
384        })
385    }
386    fn invalid(detail: String) -> Option<ContactRejection> {
387        Some(ContactRejection {
388            unsupported: false,
389            detail,
390        })
391    }
392
393    if contacts.len() > MAX_CONTACTS {
394        warn!(
395            event = "contact_list_too_long",
396            outcome = "failure",
397            contacts_count = contacts.len()
398        );
399        return invalid(format!(
400            "An account may carry at most {MAX_CONTACTS} contacts; this one carries {}",
401            contacts.len()
402        ));
403    }
404
405    for contact in contacts {
406        let Some(rest) = contact.strip_prefix("mailto:") else {
407            let scheme = contact.split_once(':').map_or("(none)", |(s, _)| s);
408            warn!(event = "contact_scheme_unsupported", outcome = "failure", scheme = %scheme);
409            return unsupported(format!(
410                "Contact {contact} uses an unsupported scheme; only mailto: is supported"
411            ));
412        };
413
414        // A control character is never part of an address, and this value does
415        // not stay inside a JSON document: it is rendered into the `notify`
416        // subsystem's templates, which are `.j2` precisely so auto-escaping is
417        // *off*. A `mailto:a@b.test\nBcc: …` would otherwise be accepted,
418        // stored, and echoed into a message body verbatim.
419        if rest.chars().any(|c| c.is_control()) {
420            warn!(
421                event = "contact_has_control_characters",
422                outcome = "failure"
423            );
424            return invalid(format!(
425                "Contact {contact:?} carries a control character, which is not part of an address"
426            ));
427        }
428
429        // RFC 6068 §2: `hfields` is everything after a `?`.
430        if rest.contains('?') {
431            warn!(event = "contact_has_hfields", outcome = "failure");
432            return invalid(format!(
433                "Contact {contact} carries hfields, which RFC 8555 §7.3 forbids"
434            ));
435        }
436
437        // RFC 6068 §2: multiple addresses in the `to` component are
438        // comma-separated.
439        if rest.contains(',') {
440            warn!(
441                event = "contact_has_multiple_addresses",
442                outcome = "failure"
443            );
444            return invalid(format!(
445                "Contact {contact} names more than one address; RFC 8555 §7.3 allows one"
446            ));
447        }
448
449        // A `mailto:` with nothing to mail, or no domain to mail it to, is not
450        // an address anyone could reach.
451        let Some((local, domain)) = rest.rsplit_once('@') else {
452            warn!(event = "contact_not_an_address", outcome = "failure");
453            return invalid(format!("Contact {contact} is not an email address"));
454        };
455        if local.is_empty() || domain.is_empty() || !domain.contains('.') {
456            warn!(event = "contact_address_incomplete", outcome = "failure");
457            return invalid(format!("Contact {contact} is not a complete email address"));
458        }
459    }
460
461    None
462}
463
464/// The text of a distinguished-name value, when rcgen exposes it as such.
465pub(crate) fn dn_text(value: &DnValue) -> Option<String> {
466    match value {
467        DnValue::Utf8String(text) => Some(text.clone()),
468        DnValue::Ia5String(text) => Some(text.as_str().to_string()),
469        DnValue::PrintableString(text) => Some(text.as_str().to_string()),
470        DnValue::TeletexString(text) => Some(text.as_str().to_string()),
471        _ => None,
472    }
473}
474
475/// Parses an RFC3339 datetime string into epoch seconds.
476pub(crate) fn parse_rfc3339(field: &str, value: &str) -> Result<i64, Problem> {
477    OffsetDateTime::parse(value, &Rfc3339)
478        .map(time::OffsetDateTime::unix_timestamp)
479        .map_err(|_| {
480            warn!(event = "order_datetime_invalid", outcome = "failure", field = %field, value = %value);
481            Problem::malformed("Invalid notBefore/notAfter datetime")
482        })
483}
484
485#[cfg(test)]
486mod tests {
487    use super::*;
488
489    #[test]
490    fn wildcard_shapes_are_recognised_and_the_rest_refused() {
491        assert!(is_wildcard("*.example.com"));
492        assert!(well_formed_name("*.example.com"));
493
494        assert!(!is_wildcard("example.com"));
495        assert!(well_formed_name("example.com"));
496
497        for bad in [
498            "*example.com",
499            "*.*.example.com",
500            "a.*.example.com",
501            "*",
502            "*.",
503        ] {
504            assert!(!well_formed_name(bad), "{bad} must not be well formed");
505        }
506        assert!(!is_wildcard("*example.com"));
507    }
508
509    /// The rule that keeps a `dns` identifier one opaque name.
510    ///
511    /// Each rejected value below is not merely untidy: `filter::custom` joins
512    /// identifiers with `,` and `challenge::http_01` feeds the name to
513    /// `Url::parse`, so a delimiter here means one subsystem reads a different
514    /// name than the one being certified.
515    #[test]
516    fn a_dns_identifier_that_is_not_a_dns_name_is_refused() {
517        for good in [
518            "example.com",
519            "a.example.com",
520            "EXAMPLE.com",
521            "host-1.example.com",
522            // Underscore labels are ordinary on the internal networks this
523            // server exists to serve, and are deliberately allowed.
524            "_acme.example.com",
525            "single-label",
526            // Syntactically a name; `names_an_ip_address` is what refuses it.
527            "1.2.3.4",
528            "*.sub.example.com",
529        ] {
530            assert!(well_formed_name(good), "{good} must be well formed");
531        }
532
533        for bad in [
534            // The delimiter cases, each with a subsystem that misreads it.
535            "a.example.com,b.example.com",
536            "internal.corp/",
537            "user@internal.corp",
538            "example.com#frag",
539            "example.com?q=1",
540            "example .com",
541            "example.com:8080",
542            // Control characters: a log-injection and template vector.
543            "example.com\n",
544            "example.com\r\nX",
545            "example\t.com",
546            // Malformed label shapes.
547            "",
548            ".",
549            "..",
550            "a..b",
551            ".example.com",
552            "-example.com",
553            "example-.com",
554            "a.-b.com",
555        ] {
556            assert!(!well_formed_name(bad), "{bad:?} must not be well formed");
557        }
558    }
559
560    /// Every spelling the WHATWG host parser reads as an address, and none it
561    /// reads as a name.
562    #[test]
563    fn an_address_spelled_as_a_dns_name_is_recognised() {
564        for address in [
565            "10.0.0.5",
566            "127.0.0.1",
567            "169.254.169.254",
568            "2130706433",
569            "0x7f.1",
570            "127.1",
571            "0177.0.0.1",
572            "1.2.3.4.",
573            "*.10.0.0.5",
574        ] {
575            assert!(names_an_ip_address(address), "{address} is an address");
576        }
577        for name in [
578            "example.com",
579            "1.2.3.4.example.com",
580            "10-0-0-5.internal",
581            "single-label",
582            "_acme.example.com",
583            "*.example.com",
584            "0x7f.example",
585        ] {
586            assert!(!names_an_ip_address(name), "{name} is a name");
587        }
588    }
589
590    #[test]
591    fn a_dns_identifier_longer_than_the_protocol_allows_is_refused() {
592        let label = "a".repeat(MAX_DNS_LABEL);
593        assert!(well_formed_name(&format!("{label}.example.com")));
594
595        let too_long_label = "a".repeat(MAX_DNS_LABEL + 1);
596        assert!(!well_formed_name(&format!("{too_long_label}.example.com")));
597
598        // 253 characters exactly, then one more.
599        let name = std::iter::repeat_n(label.as_str(), 4)
600            .collect::<Vec<_>>()
601            .join(".");
602        assert_eq!(name.len(), 255);
603        assert!(!well_formed_name(&name));
604
605        let fits = format!("{}.com", &name[..MAX_DNS_NAME - 4]);
606        assert_eq!(fits.len(), MAX_DNS_NAME);
607        assert!(well_formed_name(&fits));
608    }
609
610    /// A trailing dot is the root label; `normalize_dns_name` strips it, and
611    /// this must not reject a name that arrives before that happens.
612    #[test]
613    fn a_trailing_root_label_is_accepted_but_a_bare_dot_is_not() {
614        assert!(well_formed_name("example.com."));
615        assert!(!well_formed_name("example.com.."));
616        assert!(!well_formed_name("."));
617    }
618
619    /// A control character in a contact reaches the `notify` templates, which
620    /// are `.j2` precisely so auto-escaping is off.
621    #[test]
622    fn a_contact_carrying_a_control_character_is_refused() {
623        for bad in [
624            "mailto:alice@example.com\nBcc: attacker@evil.test",
625            "mailto:alice@example.com\r\n",
626            "mailto:al\tice@example.com",
627            "mailto:alice@example.com\u{0}",
628        ] {
629            let rejection = contact_shape_error(&[bad.to_string()])
630                .unwrap_or_else(|| panic!("{bad:?} must be refused"));
631            // `invalidContact`, not `unsupportedContact`: the scheme is right,
632            // the address is not.
633            assert!(!rejection.unsupported, "{bad:?}");
634        }
635
636        assert!(contact_shape_error(&["mailto:alice@example.com".to_string()]).is_none());
637    }
638
639    fn csr_with(sans: Vec<SanType>, common_name: Option<&str>) -> Vec<u8> {
640        let key_pair = rcgen::KeyPair::generate().unwrap();
641        let mut params = rcgen::CertificateParams::default();
642        params.subject_alt_names = sans;
643        params.distinguished_name = rcgen::DistinguishedName::new();
644        if let Some(name) = common_name {
645            params.distinguished_name.push(DnType::CommonName, name);
646        }
647        params.serialize_request(&key_pair).unwrap().der().to_vec()
648    }
649
650    fn find<'a>(identifiers: &'a [Identifier], typ: &str) -> Vec<&'a str> {
651        identifiers
652            .iter()
653            .filter(|id| id.typ == typ)
654            .map(|id| id.value.as_str())
655            .collect()
656    }
657
658    #[test]
659    fn csr_identifiers_projects_every_san_type() {
660        let der = csr_with(
661            vec![
662                SanType::DnsName("host.example.com".try_into().unwrap()),
663                SanType::IpAddress("10.0.0.1".parse().unwrap()),
664                SanType::Rfc822Name("someone@example.com".try_into().unwrap()),
665                SanType::URI("https://example.com/x".try_into().unwrap()),
666            ],
667            None,
668        );
669
670        let identifiers = csr_identifiers(&parse_csr(&der).unwrap());
671        assert_eq!(find(&identifiers, "dns"), vec!["host.example.com"]);
672        assert_eq!(find(&identifiers, "ip"), vec!["10.0.0.1"]);
673        assert_eq!(find(&identifiers, "email"), vec!["someone@example.com"]);
674        assert_eq!(find(&identifiers, "uri"), vec!["https://example.com/x"]);
675    }
676
677    #[test]
678    fn csr_identifiers_renders_ipv6_addresses() {
679        let der = csr_with(
680            vec![SanType::IpAddress("2001:db8::1".parse().unwrap())],
681            None,
682        );
683        assert_eq!(
684            find(&csr_identifiers(&parse_csr(&der).unwrap()), "ip"),
685            vec!["2001:db8::1"]
686        );
687    }
688
689    #[test]
690    fn csr_identifiers_includes_the_common_name() {
691        let der = csr_with(
692            vec![SanType::DnsName("ok.example.com".try_into().unwrap())],
693            Some("secret.internal.example.com"),
694        );
695
696        let identifiers = csr_identifiers(&parse_csr(&der).unwrap());
697        assert_eq!(find(&identifiers, "dns"), vec!["ok.example.com"]);
698        assert_eq!(
699            find(&identifiers, "cn"),
700            vec!["secret.internal.example.com"]
701        );
702    }
703
704    #[test]
705    fn csr_identifiers_omits_an_absent_common_name() {
706        let der = csr_with(
707            vec![SanType::DnsName("ok.example.com".try_into().unwrap())],
708            None,
709        );
710        assert!(find(&csr_identifiers(&parse_csr(&der).unwrap()), "cn").is_empty());
711    }
712
713    #[test]
714    fn parse_csr_rejects_garbage() {
715        assert!(parse_csr(&[0xde, 0xad, 0xbe, 0xef]).is_err());
716    }
717
718    use acme_proxy_store::testutil::dns_identifiers as dns;
719
720    #[test]
721    fn a_csr_matching_the_order_exactly_is_accepted() {
722        let der = csr_with(
723            vec![
724                SanType::DnsName("a.example.com".try_into().unwrap()),
725                SanType::DnsName("b.example.com".try_into().unwrap()),
726            ],
727            None,
728        );
729        // The order of SANs must not matter: the comparison is on
730        // sets, not on lists.
731        let identifiers = dns(&["b.example.com", "a.example.com"]);
732        assert!(check_csr_matches_order(&parse_csr(&der).unwrap(), &der, &identifiers).is_ok());
733    }
734
735    #[test]
736    fn a_csr_naming_another_domain_is_refused() {
737        let der = csr_with(
738            vec![SanType::DnsName("victim.example".try_into().unwrap())],
739            None,
740        );
741        let value =
742            check_csr_matches_order(&parse_csr(&der).unwrap(), &der, &dns(&["a.example.com"]))
743                .unwrap_err()
744                .to_value();
745        assert_eq!(value["type"], "urn:ietf:params:acme:error:badCSR");
746        assert_eq!(value["status"], 400);
747    }
748
749    #[test]
750    fn a_csr_naming_more_than_the_order_is_refused() {
751        // RFC 8555 §7.4 asks for "the exact same set": a superset is a
752        // refusal, not an acceptable intersection.
753        let der = csr_with(
754            vec![
755                SanType::DnsName("a.example.com".try_into().unwrap()),
756                SanType::DnsName("extra.example.com".try_into().unwrap()),
757            ],
758            None,
759        );
760        assert!(
761            check_csr_matches_order(&parse_csr(&der).unwrap(), &der, &dns(&["a.example.com"]))
762                .is_err()
763        );
764    }
765
766    #[test]
767    fn a_csr_naming_less_than_the_order_is_refused() {
768        let der = csr_with(
769            vec![SanType::DnsName("a.example.com".try_into().unwrap())],
770            None,
771        );
772        assert!(
773            check_csr_matches_order(
774                &parse_csr(&der).unwrap(),
775                &der,
776                &dns(&["a.example.com", "b.example.com"]),
777            )
778            .is_err()
779        );
780    }
781
782    #[test]
783    fn a_csr_smuggling_a_non_dns_san_is_refused() {
784        // The DNS name is the one that was ordered; it is the IP address
785        // smuggled in beside it that nothing else would ever look at.
786        let der = csr_with(
787            vec![
788                SanType::DnsName("a.example.com".try_into().unwrap()),
789                SanType::IpAddress("10.0.0.1".parse().unwrap()),
790            ],
791            None,
792        );
793        assert!(
794            check_csr_matches_order(&parse_csr(&der).unwrap(), &der, &dns(&["a.example.com"]))
795                .is_err()
796        );
797    }
798
799    #[test]
800    fn a_csr_whose_common_name_is_not_an_order_identifier_is_refused() {
801        let der = csr_with(
802            vec![SanType::DnsName("a.example.com".try_into().unwrap())],
803            Some("victim.example"),
804        );
805        assert!(
806            check_csr_matches_order(&parse_csr(&der).unwrap(), &der, &dns(&["a.example.com"]))
807                .is_err()
808        );
809    }
810
811    #[test]
812    fn a_csr_whose_common_name_is_an_order_identifier_is_accepted() {
813        let der = csr_with(
814            vec![SanType::DnsName("a.example.com".try_into().unwrap())],
815            Some("a.example.com"),
816        );
817        assert!(
818            check_csr_matches_order(&parse_csr(&der).unwrap(), &der, &dns(&["a.example.com"]))
819                .is_ok()
820        );
821    }
822
823    #[test]
824    fn a_common_name_that_is_a_human_label_is_left_alone() {
825        // rcgen sets this one by default, and `filter::identifiers` excludes
826        // `cn` from its `allow` rules for exactly this reason: it is not a name
827        // the certificate covers, it is prose.
828        for label in ["rcgen self signed cert", "ACME client", "no-dot-label"] {
829            let der = csr_with(
830                vec![SanType::DnsName("a.example.com".try_into().unwrap())],
831                Some(label),
832            );
833            assert!(
834                check_csr_matches_order(&parse_csr(&der).unwrap(), &der, &dns(&["a.example.com"]))
835                    .is_ok(),
836                "{label} should not be read as a host name"
837            );
838        }
839    }
840
841    /// A CN rcgen's own distinguished name cannot render as text — and which
842    /// `dn_text` therefore skipped — still reaches a `custom` script or an
843    /// upstream CA in the DER. Unreadable is refused, not waved through.
844    #[test]
845    fn a_common_name_in_an_unreadable_encoding_is_refused() {
846        let key_pair = rcgen::KeyPair::generate().unwrap();
847        let mut params = rcgen::CertificateParams::default();
848        params.subject_alt_names = vec![SanType::DnsName("a.example.com".try_into().unwrap())];
849        params.distinguished_name = rcgen::DistinguishedName::new();
850        params.distinguished_name.push(
851            DnType::CommonName,
852            DnValue::BmpString("victim.example".try_into().unwrap()),
853        );
854        let der = params.serialize_request(&key_pair).unwrap().der().to_vec();
855
856        let value =
857            check_csr_matches_order(&parse_csr(&der).unwrap(), &der, &dns(&["a.example.com"]))
858                .unwrap_err()
859                .to_value();
860        assert_eq!(value["type"], "urn:ietf:params:acme:error:badCSR");
861    }
862
863    /// Two `CommonName` attributes in one subject: rcgen's map keeps the last,
864    /// so a check reading `csr.params` would inspect the harmless one and let
865    /// the other through. Hand-built, because rcgen cannot emit this shape —
866    /// and unsigned, because `subject_common_names` is what is under test.
867    #[test]
868    fn every_common_name_of_the_subject_is_read() {
869        use rcgen::PublicKeyData;
870
871        fn tlv(tag: u8, body: &[u8]) -> Vec<u8> {
872            let mut out = vec![tag];
873            match body.len() {
874                len if len < 0x80 => out.push(len as u8),
875                len if len < 0x100 => out.extend([0x81, len as u8]),
876                len => out.extend([0x82, (len >> 8) as u8, (len & 0xff) as u8]),
877            }
878            out.extend(body);
879            out
880        }
881        fn common_name(value: &str) -> Vec<u8> {
882            // SET { SEQUENCE { OID 2.5.4.3, UTF8String value } }
883            let oid = [0x06, 0x03, 0x55, 0x04, 0x03];
884            let mut attribute = oid.to_vec();
885            attribute.extend(tlv(0x0c, value.as_bytes()));
886            tlv(0x31, &tlv(0x30, &attribute))
887        }
888
889        let key_pair = rcgen::KeyPair::generate().unwrap();
890        let mut subject = common_name("a.example.com");
891        subject.extend(common_name("victim.example"));
892        let mut info = tlv(0x02, &[0x00]);
893        info.extend(tlv(0x30, &subject));
894        info.extend(key_pair.subject_public_key_info());
895        info.extend(tlv(0xa0, &[]));
896        let mut request = tlv(0x30, &info);
897        // ecdsa-with-SHA256, and an empty signature: nothing here verifies one.
898        request.extend(tlv(
899            0x30,
900            &[0x06, 0x08, 0x2a, 0x86, 0x48, 0xce, 0x3d, 0x04, 0x03, 0x02],
901        ));
902        request.extend(tlv(0x03, &[0x00]));
903        let der = tlv(0x30, &request);
904
905        assert_eq!(
906            subject_common_names(&der).unwrap(),
907            vec!["a.example.com".to_string(), "victim.example".to_string()]
908        );
909    }
910
911    /// An order for a single-label name makes a single-label CN a name like
912    /// any other — on the networks that shape exists for, `fileserver`
913    /// resolves.
914    #[test]
915    fn a_single_label_common_name_is_checked_against_a_single_label_order() {
916        let der = csr_with(
917            vec![SanType::DnsName("fileserver".try_into().unwrap())],
918            Some("mailserver"),
919        );
920        assert!(
921            check_csr_matches_order(&parse_csr(&der).unwrap(), &der, &dns(&["fileserver"]))
922                .is_err()
923        );
924
925        let der = csr_with(
926            vec![SanType::DnsName("fileserver".try_into().unwrap())],
927            Some("fileserver"),
928        );
929        assert!(
930            check_csr_matches_order(&parse_csr(&der).unwrap(), &der, &dns(&["fileserver"])).is_ok()
931        );
932    }
933
934    #[test]
935    fn common_names_are_recognised_as_host_names_or_not() {
936        assert!(looks_like_dns_name("a.example.com"));
937        assert!(looks_like_dns_name("*.example.com"));
938
939        assert!(!looks_like_dns_name(""));
940        assert!(!looks_like_dns_name("localhost"));
941        assert!(!looks_like_dns_name("rcgen self signed cert"));
942        assert!(!looks_like_dns_name("a.*.example.com"));
943    }
944
945    #[test]
946    fn a_wildcard_csr_matching_its_order_is_accepted() {
947        // `new_order` has already refused the wildcard identifier if
948        // `dns-01` is not enabled; here set equality is all that pins the CSR
949        // to the order.
950        let der = csr_with(
951            vec![SanType::DnsName("*.example.com".try_into().unwrap())],
952            None,
953        );
954        assert!(
955            check_csr_matches_order(&parse_csr(&der).unwrap(), &der, &dns(&["*.example.com"]))
956                .is_ok()
957        );
958    }
959
960    #[test]
961    fn a_csr_differing_only_in_case_is_refused() {
962        // A raw comparison: re-normalising here would let a leaf be signed
963        // carrying the un-normalised form, which was never the form compared.
964        let der = csr_with(
965            vec![SanType::DnsName("A.Example.COM".try_into().unwrap())],
966            None,
967        );
968        assert!(
969            check_csr_matches_order(&parse_csr(&der).unwrap(), &der, &dns(&["a.example.com"]))
970                .is_err()
971        );
972    }
973
974    #[test]
975    fn dn_text_reads_the_string_encodings() {
976        assert_eq!(
977            dn_text(&DnValue::Utf8String("a.example.com".to_string())).as_deref(),
978            Some("a.example.com")
979        );
980        assert_eq!(
981            dn_text(&DnValue::Ia5String("b.example.com".try_into().unwrap())).as_deref(),
982            Some("b.example.com")
983        );
984        assert_eq!(
985            dn_text(&DnValue::PrintableString(
986                "c.example.com".try_into().unwrap()
987            ))
988            .as_deref(),
989            Some("c.example.com")
990        );
991        assert_eq!(
992            dn_text(&DnValue::TeletexString("d.example.com".try_into().unwrap())).as_deref(),
993            Some("d.example.com")
994        );
995    }
996
997    #[test]
998    fn an_unreadable_common_name_becomes_an_other_identifier() {
999        let value = DnValue::BmpString("e.example.com".try_into().unwrap());
1000        assert!(dn_text(&value).is_none());
1001    }
1002}