Skip to content

Commit d4067ff

Browse files
committed
Add simplified Order::finalize() method
1 parent 0c7e35a commit d4067ff

File tree

5 files changed

+93
-45
lines changed

5 files changed

+93
-45
lines changed

Cargo.toml

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ default = ["aws-lc-rs", "hyper-rustls"]
1616
aws-lc-rs = ["dep:aws-lc-rs", "hyper-rustls?/aws-lc-rs", "rcgen/aws_lc_rs"]
1717
fips = ["aws-lc-rs", "aws-lc-rs?/fips"]
1818
hyper-rustls = ["dep:hyper", "dep:hyper-rustls", "dep:hyper-util"]
19+
rcgen = ["dep:rcgen"]
1920
ring = ["dep:ring", "hyper-rustls?/ring", "rcgen/ring"]
2021

2122
[dependencies]
@@ -29,6 +30,7 @@ http-body-util = "0.1.2"
2930
hyper = { version = "1.3.1", features = ["client", "http1", "http2"], optional = true }
3031
hyper-rustls = { version = "0.27", default-features = false, features = ["http1", "http2", "native-tokio", "tls12", "rustls-native-certs"], optional = true }
3132
hyper-util = { version = "0.1.5", features = ["client", "client-legacy", "http1", "http2", "tokio"], optional = true }
33+
rcgen = { version = "0.13", default-features = false, features = ["pem"], optional = true }
3234
ring = { version = "0.17", features = ["std"], optional = true }
3335
rustls-pki-types = "1.1.0"
3436
serde = { version = "1.0.104", features = ["derive"] }
@@ -41,7 +43,6 @@ x509-parser = { version = "0.17", default-features = false, optional = true }
4143
[dev-dependencies]
4244
anyhow = "1.0.66"
4345
clap = { version = "4.0.29", features = ["derive"] }
44-
rcgen = { version = "0.13", default-features = false, features = ["pem"] }
4546
rustls = { version = "0.23", default-features = false }
4647
tempfile = "3"
4748
tokio = { version = "1.22.0", features = ["macros", "rt", "rt-multi-thread", "time"] }
@@ -50,7 +51,7 @@ tracing-subscriber = { version = "0.3.16", features = ["env-filter"] }
5051

5152
[[example]]
5253
name = "provision"
53-
required-features = ["hyper-rustls"]
54+
required-features = ["hyper-rustls", "rcgen"]
5455

5556
# Pebble integration test.
5657
# Ignored by default because it requires pebble & pebble-challtestsrv.

examples/provision.rs

Lines changed: 2 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ use std::io;
22
use std::time::Duration;
33

44
use clap::Parser;
5-
use rcgen::{CertificateParams, DistinguishedName, KeyPair};
65
use tokio::time::sleep;
76
use tracing::info;
87

@@ -87,16 +86,9 @@ async fn main() -> anyhow::Result<()> {
8786
return Err(anyhow::anyhow!("unexpected order status: {status:?}"));
8887
}
8988

90-
// If the order is ready, we can provision the certificate.
91-
// Use the rcgen library to create a Certificate Signing Request.
92-
let mut params = CertificateParams::new(opts.names)?;
93-
params.distinguished_name = DistinguishedName::new();
94-
let private_key = KeyPair::generate()?;
95-
let csr = params.serialize_request(&private_key)?;
96-
9789
// Finalize the order and print certificate chain, private key and account credentials.
9890

99-
order.finalize(csr.der()).await?;
91+
let private_key_pem = order.finalize().await?;
10092
let cert_chain_pem = loop {
10193
match order.certificate().await? {
10294
Some(cert_chain_pem) => break cert_chain_pem,
@@ -105,7 +97,7 @@ async fn main() -> anyhow::Result<()> {
10597
};
10698

10799
info!("certificate chain:\n\n{cert_chain_pem}");
108-
info!("private key:\n\n{}", private_key.serialize_pem());
100+
info!("private key:\n\n{private_key_pem}");
109101
Ok(())
110102
}
111103

src/lib.rs

Lines changed: 70 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@ use hyper_util::client::legacy::Client as HyperClient;
2424
use hyper_util::client::legacy::connect::{Connect, HttpConnector};
2525
#[cfg(feature = "hyper-rustls")]
2626
use hyper_util::rt::TokioExecutor;
27+
#[cfg(feature = "rcgen")]
28+
use rcgen::{CertificateParams, DistinguishedName, KeyPair};
2729
use serde::Serialize;
2830
use serde::de::DeserializeOwned;
2931
use tokio::time::sleep;
@@ -76,12 +78,41 @@ impl Order {
7678
}
7779
}
7880

81+
/// Generate a Certificate Signing Request for the order's identifiers and request finalization
82+
///
83+
/// Uses the rcgen crate to generate a Certificate Signing Request (CSR) converting the order's
84+
/// identifiers to Subject Alternative Names, then calls [`Order::finalize_csr()`] with it.
85+
/// Returns the generated private key, serialized as PEM.
86+
///
87+
/// After this succeeds, call [`Order::certificate()`] to retrieve the certificate chain once
88+
/// the order is in the appropriate state.
89+
#[cfg(feature = "rcgen")]
90+
pub async fn finalize(&mut self) -> Result<String, Error> {
91+
let mut names = Vec::with_capacity(self.state.authorizations.len());
92+
let mut identifiers = self.identifiers();
93+
while let Some(result) = identifiers.next().await {
94+
names.push(result?.to_string());
95+
}
96+
97+
let mut params = CertificateParams::new(names).map_err(Error::from_rcgen)?;
98+
params.distinguished_name = DistinguishedName::new();
99+
let private_key = KeyPair::generate().map_err(Error::from_rcgen)?;
100+
let csr = params
101+
.serialize_request(&private_key)
102+
.map_err(Error::from_rcgen)?;
103+
104+
self.finalize_csr(csr.der()).await?;
105+
Ok(private_key.serialize_pem())
106+
}
107+
79108
/// Request a certificate from the given Certificate Signing Request (CSR)
80109
///
81-
/// Creating a CSR is outside of the scope of instant-acme. Make sure you pass in a
82-
/// DER representation of the CSR in `csr_der`. Call `certificate()` to retrieve the
83-
/// certificate chain once the order is in the appropriate state.
84-
pub async fn finalize(&mut self, csr_der: &[u8]) -> Result<(), Error> {
110+
/// `csr_der` contains the CSR representation serialized in DER encoding. If you don't need
111+
/// custom certificate parameters, [`Order::finalize()`] can generate the CSR for you.
112+
///
113+
/// After this succeeds, call [`Order::certificate()`] to retrieve the certificate chain once
114+
/// the order is in the appropriate state.
115+
pub async fn finalize_csr(&mut self, csr_der: &[u8]) -> Result<(), Error> {
85116
let rsp = self
86117
.account
87118
.post(
@@ -139,6 +170,21 @@ impl Order {
139170
))
140171
}
141172

173+
/// Retrieve the identifiers for this order
174+
///
175+
/// This method will retrieve the identifiers attached to the authorizations for this order
176+
/// if they have not been retrieved yet. If you have already consumed the stream generated
177+
/// by [`Order::authorizations()`], this will not involve any network activity.
178+
pub fn identifiers(&mut self) -> Identifiers<'_> {
179+
Identifiers {
180+
inner: AuthStream {
181+
iter: self.state.authorizations.iter_mut(),
182+
nonce: &mut self.nonce,
183+
account: &self.account,
184+
},
185+
}
186+
}
187+
142188
/// Poll the order with exponential backoff until in a final state
143189
///
144190
/// Refresh the order state from the server for `tries` times, waiting `delay` before the
@@ -219,6 +265,26 @@ impl Authorizations<'_> {
219265
}
220266
}
221267

268+
/// An stream-like interface that yields an [`Order`]'s identifiers
269+
///
270+
/// Call [`next()`] to get the next authorization in the order. If the order state
271+
/// does not yet contain the state of the authorization, it will be fetched from the server.
272+
///
273+
/// [`next()`]: Identifiers::next()
274+
pub struct Identifiers<'a> {
275+
inner: AuthStream<'a>,
276+
}
277+
278+
impl<'a> Identifiers<'a> {
279+
/// Yield the next [`Identifier`], fetching the authorization's state if we don't have it yet
280+
pub async fn next(&mut self) -> Option<Result<AuthorizedIdentifier<'a>, Error>> {
281+
Some(match self.inner.next().await? {
282+
Ok((_, state)) => Ok(state.identifier()),
283+
Err(err) => Err(err),
284+
})
285+
}
286+
}
287+
222288
struct AuthStream<'a> {
223289
iter: slice::IterMut<'a, Authorization>,
224290
nonce: &'a mut Option<String>,

src/types.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,13 @@ pub enum Error {
6161
Str(&'static str),
6262
}
6363

64+
impl Error {
65+
#[cfg(feature = "rcgen")]
66+
pub(crate) fn from_rcgen(err: rcgen::Error) -> Self {
67+
Self::Other(Box::new(err))
68+
}
69+
}
70+
6471
impl From<&'static str> for Error {
6572
fn from(s: &'static str) -> Self {
6673
Error::Str(s)

tests/pebble.rs

Lines changed: 11 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,6 @@ use instant_acme::{
2727
};
2828
#[cfg(all(feature = "time", feature = "x509-parser"))]
2929
use instant_acme::{CertificateIdentifier, RevocationRequest};
30-
use rcgen::{CertificateParams, DistinguishedName, KeyPair};
3130
use rustls::RootCertStore;
3231
use rustls::client::{verify_server_cert_signed_by_trust_anchor, verify_server_name};
3332
use rustls::crypto::CryptoProvider;
@@ -418,8 +417,7 @@ impl Environment {
418417
&mut self,
419418
new_order: &NewOrder<'_>,
420419
) -> Result<CertificateDer<'static>, Box<dyn StdError + 'static>> {
421-
let identifiers = new_order.identifiers();
422-
debug!(?identifiers, "creating order");
420+
debug!(identifiers = ?new_order.identifiers(), "creating order");
423421
let mut order = self.account.new_order(new_order).await?;
424422
info!(order_url = order.url(), "created order");
425423

@@ -451,7 +449,7 @@ impl Environment {
451449
}
452450

453451
// Issue a certificate for the names, returning the certificate chain.
454-
let cert_chain = self.certificate(&mut order, identifiers).await?;
452+
let cert_chain = self.certificate(&mut order).await?;
455453

456454
// Split off and parse the EE cert, save the intermediates that follow.
457455
let (ee_cert_der, intermediates) = cert_chain.split_first().unwrap();
@@ -469,17 +467,16 @@ impl Environment {
469467
.unwrap();
470468

471469
// Verify the EE cert is valid for each of the identifiers.
472-
for ident in identifiers {
473-
let domain = match ident {
474-
Identifier::Dns(domain) => domain,
475-
_ => unreachable!("unsupported identifier {ident:?}"),
476-
};
470+
let mut identifiers = order.identifiers();
471+
while let Some(result) = identifiers.next().await {
472+
let ident = result?;
477473

478474
// When verifying a wildcard identifier, use a fixed label under the wildcard.
479475
// The wildcard identifier isn't a valid ServerName itself.
480-
let server_name = match domain.strip_prefix("*.") {
481-
Some(rest) => format!("foo.{rest}"),
482-
None => domain.to_owned(),
476+
let server_name = match (ident.identifier, ident.wildcard) {
477+
(Identifier::Dns(domain), true) => format!("foo.{domain}"),
478+
(Identifier::Dns(_), false) => ident.to_string(),
479+
_ => unreachable!("unsupported identifier {ident:?}"),
483480
};
484481

485482
verify_server_name(&ee_cert, &ServerName::try_from(server_name)?)?;
@@ -494,27 +491,12 @@ impl Environment {
494491
async fn certificate(
495492
&self,
496493
order: &mut Order,
497-
identifiers: &[Identifier],
498494
) -> Result<Vec<CertificateDer<'static>>, Box<dyn StdError>> {
499-
info!(?identifiers, order_url = order.url(), "issuing certificate");
500-
501-
// Create a CSR for the identifiers corresponding to the order.
502-
let mut params = CertificateParams::new(
503-
identifiers
504-
.iter()
505-
.map(|ident| match ident {
506-
Identifier::Dns(domain) => domain.to_owned(),
507-
_ => unreachable!("unsupported identifier {ident:?}"),
508-
})
509-
.collect::<Vec<_>>(),
510-
)?;
511-
params.distinguished_name = DistinguishedName::new();
512-
let private_key = KeyPair::generate()?;
513-
let csr = params.serialize_request(&private_key)?;
495+
info!(order_url = order.url(), "issuing certificate");
514496

515497
// Finalize the order and fetch the issued certificate chain.
516498
debug!(order_url = order.url(), "finalizing order");
517-
order.finalize(csr.der()).await.unwrap();
499+
order.finalize().await?;
518500
debug!(order_url = order.url(), "fetching order certificate chain");
519501
let cert_chain_pem = loop {
520502
match order.certificate().await.unwrap() {

0 commit comments

Comments
 (0)