Skip to content

Commit 3ef6705

Browse files
committed
ACME: alternative chains support.
1 parent 2934b34 commit 3ef6705

File tree

9 files changed

+418
-30
lines changed

9 files changed

+418
-30
lines changed

Cargo.lock

Lines changed: 11 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ http-body = "1.0.1"
1818
http-body-util = "0.1.3"
1919
http-serde = "2.1.1"
2020
hyper = { version = "1.6.0", features = ["client", "http1"] }
21+
iri-string = "0.7"
2122
libc = "0.2.174"
2223
nginx-sys = "0.5.0"
2324
ngx = { version = "0.5.0", features = ["async", "serde", "std"] }

README.md

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -237,6 +237,24 @@ Accepted values:
237237
The generated account keys are preserved across reloads,
238238
but will be lost on restart unless [state_path](#state_path) is configured.
239239

240+
### chain
241+
242+
**Syntax:** **`chain`** `issuer`=_`name`_
243+
244+
**Default:** -
245+
246+
**Context:** acme_issuer
247+
248+
_This directive appeared in version 0.3.0._
249+
250+
Specifies the preferred certificate chain.
251+
252+
If the ACME issuer offers multiple certificate chains,
253+
prefer the chain with the topmost certificate issued from the
254+
Subject Common Name _`name`_.
255+
256+
If no matches, the default chain will be used.
257+
240258
### challenge
241259

242260
**Syntax:** **`challenge`** _`type`_
@@ -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: 69 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -11,25 +11,26 @@ use std::string::{String, ToString};
1111

1212
use bytes::Bytes;
1313
use error::{NewAccountError, NewCertificateError, RequestError};
14-
use http::Uri;
14+
use http::{Response, Uri};
1515
use ngx::allocator::{Allocator, Box};
1616
use ngx::async_::sleep;
1717
use ngx::collections::Vec;
1818
use ngx::ngx_log_debug;
1919
use openssl::pkey::{PKey, PKeyRef, Private};
20-
use openssl::x509::{self, extension as x509_ext, X509Req};
20+
use openssl::x509::{self, extension as x509_ext, X509Req, X509};
2121
use types::{AccountStatus, ProblemCategory};
2222

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

3131
pub mod account_key;
3232
pub mod error;
33+
pub mod headers;
3334
pub mod solvers;
3435
pub mod types;
3536

@@ -49,7 +50,8 @@ pub enum NewAccountOutput<'a> {
4950
}
5051

5152
pub struct NewCertificateOutput {
52-
pub chain: Bytes,
53+
pub bytes: Bytes,
54+
pub x509: std::vec::Vec<X509>,
5355
pub pkey: PKey<Private>,
5456
}
5557

@@ -246,7 +248,7 @@ where
246248
if let Some(val) = res
247249
.headers()
248250
.get(http::header::RETRY_AFTER)
249-
.and_then(parse_retry_after)
251+
.and_then(headers::parse_retry_after)
250252
.filter(|x| x > &MAX_SERVER_RETRY_INTERVAL)
251253
{
252254
return Err(RequestError::RateLimited(val));
@@ -461,11 +463,69 @@ where
461463

462464
let certificate = order
463465
.certificate
464-
.ok_or(NewCertificateError::CertificateUrl)?;
466+
.ok_or(NewCertificateError::MissingCertificate)?;
465467

466-
let chain = self.post(&certificate, b"").await?.into_body();
468+
let res = self.post(&certificate, b"").await?;
467469

468-
Ok(NewCertificateOutput { chain, pkey })
470+
if let Some(ref matcher) = self.issuer.chain {
471+
let (bytes, x509) = self
472+
.find_preferred_chain(&certificate, res, matcher)
473+
.await?;
474+
Ok(NewCertificateOutput { bytes, x509, pkey })
475+
} else {
476+
let bytes = res.into_body();
477+
let x509 =
478+
X509::stack_from_pem(&bytes).map_err(NewCertificateError::InvalidCertificate)?;
479+
if x509.is_empty() {
480+
return Err(NewCertificateError::MissingCertificate);
481+
}
482+
483+
Ok(NewCertificateOutput { bytes, x509, pkey })
484+
}
485+
}
486+
487+
async fn find_preferred_chain(
488+
&self,
489+
base: &Uri,
490+
cert: Response<Bytes>,
491+
matcher: &CertificateChainMatcher,
492+
) -> Result<(Bytes, std::vec::Vec<X509>), NewCertificateError> {
493+
let default =
494+
X509::stack_from_pem(cert.body()).map_err(NewCertificateError::InvalidCertificate)?;
495+
496+
if !matcher.test(&default) {
497+
if let Ok(base) = iri_string::types::UriAbsoluteString::try_from(base.to_string()) {
498+
let alternates = cert
499+
.headers()
500+
.get_all(http::header::LINK)
501+
.into_iter()
502+
.filter_map(headers::parse_link)
503+
.flatten()
504+
.filter(|x| x.is_rel("alternate"));
505+
506+
for link in alternates {
507+
let uri = link.target.resolve_against(&base).to_string();
508+
let Ok(uri) = Uri::try_from(uri) else {
509+
continue;
510+
};
511+
512+
let res = self.post(&uri, b"").await?;
513+
let bytes = res.into_body();
514+
515+
let stack = X509::stack_from_pem(&bytes)
516+
.map_err(NewCertificateError::InvalidCertificate)?;
517+
if matcher.test(&stack) {
518+
return Ok((bytes, stack));
519+
}
520+
}
521+
}
522+
}
523+
524+
if default.is_empty() {
525+
return Err(NewCertificateError::MissingCertificate);
526+
}
527+
528+
Ok((cert.into_body(), default))
469529
}
470530

471531
async fn do_authorization(
@@ -586,7 +646,7 @@ async fn wait_for_retry<B>(
586646
let retry_after = res
587647
.headers()
588648
.get(http::header::RETRY_AFTER)
589-
.and_then(parse_retry_after)
649+
.and_then(headers::parse_retry_after)
590650
.unwrap_or(interval)
591651
.min(MAX_SERVER_RETRY_INTERVAL);
592652

@@ -616,15 +676,3 @@ where
616676
{
617677
serde_json::from_slice(bytes).map_err(RequestError::ResponseFormat)
618678
}
619-
620-
fn parse_retry_after(val: &http::HeaderValue) -> Option<Duration> {
621-
let val = val.to_str().ok()?;
622-
623-
// Retry-After: <http-date>
624-
if let Ok(time) = Time::parse(val) {
625-
return Some(time - Time::now());
626-
}
627-
628-
// Retry-After: <delay-seconds>
629-
val.parse().map(Duration::from_secs).ok()
630-
}

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)