Skip to content

Commit 1b01dee

Browse files
committed
ACME: preferred certificate chain support.
The new directive, "preferred_chain", allows selecting one of the alternative certificate chains (RFC8555 § 7.4.2) by a Subject Common Name that issued the topmost certificate in the chain. If no suitable chains are found, the default one will be used. To fetch the alternative chains, we need a rather complicated parser for RFC8288 Link header. After considering the correctness, maintenance status and dependency weight of the available implementations, I decided to add our own instead, simplified as much as possible.
1 parent dbb34d8 commit 1b01dee

File tree

7 files changed

+433
-29
lines changed

7 files changed

+433
-29
lines changed

README.md

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -290,6 +290,24 @@ In both cases, the key is expected to be encoded in
290290

291291
[RFC8555#eab]: https://datatracker.ietf.org/doc/html/rfc8555#section-7.3.4
292292

293+
### preferred_chain
294+
295+
**Syntax:** **`preferred_chain`** _`issuer name`_
296+
297+
**Default:** -
298+
299+
**Context:** acme_issuer
300+
301+
_This directive appeared in version 0.3.0._
302+
303+
Specifies the preferred certificate chain.
304+
305+
If the ACME issuer offers multiple certificate chains,
306+
prefer the chain with the topmost certificate issued from the
307+
Subject Common Name _`issuer name`_.
308+
309+
If no matches, the default chain will be used.
310+
293311
### profile
294312

295313
**Syntax:** **`profile`** _`name`_ \[`require`]
@@ -298,6 +316,8 @@ In both cases, the key is expected to be encoded in
298316

299317
**Context:** acme_issuer
300318

319+
_This directive appeared in version 0.3.0._
320+
301321
Requests the supported [certificate profile][draft-ietf-acme-profiles]
302322
_`name`_ from the ACME server.
303323

src/acme.rs

Lines changed: 68 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -18,19 +18,20 @@ use ngx::async_::sleep;
1818
use ngx::collections::Vec;
1919
use ngx::ngx_log_debug;
2020
use openssl::pkey::{PKey, PKeyRef, Private};
21-
use openssl::x509::{self, extension as x509_ext, X509Req};
21+
use openssl::x509::{self, extension as x509_ext, X509Req, X509};
2222
use types::{AccountStatus, ProblemCategory};
2323

2424
use self::account_key::{AccountKey, AccountKeyError};
2525
use self::types::{AuthorizationStatus, ChallengeKind, ChallengeStatus, OrderStatus};
2626
use crate::conf::identifier::Identifier;
27-
use crate::conf::issuer::{Issuer, Profile};
27+
use crate::conf::issuer::{CertificateChainMatcher, Issuer, Profile};
2828
use crate::conf::order::CertificateOrder;
2929
use crate::net::http::HttpClient;
3030
use crate::time::Time;
3131

3232
pub mod account_key;
3333
pub mod error;
34+
pub mod headers;
3435
pub mod solvers;
3536
pub mod types;
3637

@@ -53,7 +54,8 @@ pub enum NewAccountOutput<'a> {
5354
}
5455

5556
pub struct NewCertificateOutput {
56-
pub chain: Bytes,
57+
pub bytes: Bytes,
58+
pub x509: std::vec::Vec<X509>,
5759
pub pkey: PKey<Private>,
5860
}
5961

@@ -272,7 +274,7 @@ where
272274
if let Some(val) = res
273275
.headers()
274276
.get(http::header::RETRY_AFTER)
275-
.and_then(parse_retry_after)
277+
.and_then(headers::parse_retry_after)
276278
.filter(|x| x > &MAX_SERVER_RETRY_INTERVAL)
277279
{
278280
return Err(RequestError::RateLimited(val));
@@ -487,11 +489,69 @@ where
487489

488490
let certificate = order
489491
.certificate
490-
.ok_or(NewCertificateError::CertificateUrl)?;
492+
.ok_or(NewCertificateError::MissingCertificate)?;
491493

492-
let chain = self.post(&certificate, b"").await?.into_body();
494+
let res = self.post(&certificate, b"").await?;
493495

494-
Ok(NewCertificateOutput { chain, pkey })
496+
if let Some(ref matcher) = self.issuer.chain {
497+
let (bytes, x509) = self
498+
.find_preferred_chain(&certificate, res, matcher)
499+
.await?;
500+
Ok(NewCertificateOutput { bytes, x509, pkey })
501+
} else {
502+
let bytes = res.into_body();
503+
let x509 =
504+
X509::stack_from_pem(&bytes).map_err(NewCertificateError::InvalidCertificate)?;
505+
if x509.is_empty() {
506+
return Err(NewCertificateError::MissingCertificate);
507+
}
508+
509+
Ok(NewCertificateOutput { bytes, x509, pkey })
510+
}
511+
}
512+
513+
async fn find_preferred_chain(
514+
&self,
515+
base: &Uri,
516+
cert: http::Response<Bytes>,
517+
matcher: &CertificateChainMatcher,
518+
) -> Result<(Bytes, std::vec::Vec<X509>), NewCertificateError> {
519+
let default =
520+
X509::stack_from_pem(cert.body()).map_err(NewCertificateError::InvalidCertificate)?;
521+
522+
if !matcher.test(&default) {
523+
if let Ok(base) = iri_string::types::UriAbsoluteString::try_from(base.to_string()) {
524+
let alternates = cert
525+
.headers()
526+
.get_all(http::header::LINK)
527+
.into_iter()
528+
.filter_map(headers::parse_link)
529+
.flatten()
530+
.filter(|x| x.is_rel("alternate"));
531+
532+
for link in alternates {
533+
let uri = link.target.resolve_against(&base).to_string();
534+
let Ok(uri) = Uri::try_from(uri) else {
535+
continue;
536+
};
537+
538+
let res = self.post(&uri, b"").await?;
539+
let bytes = res.into_body();
540+
541+
let stack = X509::stack_from_pem(&bytes)
542+
.map_err(NewCertificateError::InvalidCertificate)?;
543+
if matcher.test(&stack) {
544+
return Ok((bytes, stack));
545+
}
546+
}
547+
}
548+
}
549+
550+
if default.is_empty() {
551+
return Err(NewCertificateError::MissingCertificate);
552+
}
553+
554+
Ok((cert.into_body(), default))
495555
}
496556

497557
async fn do_authorization(
@@ -612,7 +672,7 @@ async fn wait_for_retry<B>(
612672
let retry_after = res
613673
.headers()
614674
.get(http::header::RETRY_AFTER)
615-
.and_then(parse_retry_after)
675+
.and_then(headers::parse_retry_after)
616676
.unwrap_or(interval)
617677
.min(MAX_SERVER_RETRY_INTERVAL);
618678

@@ -642,15 +702,3 @@ where
642702
{
643703
serde_json::from_slice(bytes).map_err(RequestError::ResponseFormat)
644704
}
645-
646-
fn parse_retry_after(val: &http::HeaderValue) -> Option<Duration> {
647-
let val = val.to_str().ok()?;
648-
649-
// Retry-After: <http-date>
650-
if let Ok(time) = Time::parse(val) {
651-
return Some(time - Time::now());
652-
}
653-
654-
// Retry-After: <delay-seconds>
655-
val.parse().map(Duration::from_secs).ok()
656-
}

src/acme/error.rs

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -62,15 +62,18 @@ pub enum NewCertificateError {
6262
#[error("unexpected authorization status {0:?}")]
6363
AuthorizationStatus(super::types::AuthorizationStatus),
6464

65-
#[error("no certificate in the validated order")]
66-
CertificateUrl,
67-
6865
#[error("unexpected challenge status {0:?}")]
6966
ChallengeStatus(super::types::ChallengeStatus),
7067

7168
#[error("csr generation failed ({0})")]
7269
Csr(openssl::error::ErrorStack),
7370

71+
#[error("PEM_read_bio_X509() failed: {0}")]
72+
InvalidCertificate(openssl::error::ErrorStack),
73+
74+
#[error("no certificate in the completed order")]
75+
MissingCertificate,
76+
7477
#[error("no supported challenges")]
7578
NoSupportedChallenges,
7679

0 commit comments

Comments
 (0)