Add certificate provisioning example
This commit is contained in:
parent
9a041d5569
commit
485b771ca3
|
@ -18,3 +18,11 @@ ring = { version = "0.16.20", features = ["std"] }
|
|||
serde = { version = "1.0.104", features = ["derive"] }
|
||||
serde_json = "1.0.78"
|
||||
thiserror = "1.0.30"
|
||||
|
||||
[dev-dependencies]
|
||||
anyhow = "1.0.66"
|
||||
clap = { version = "4.0.29", features = ["derive"] }
|
||||
rcgen = "0.10.0"
|
||||
tokio = { version = "1.22.0", features = ["rt", "macros", "rt-multi-thread"] }
|
||||
tracing = "0.1.37"
|
||||
tracing-subscriber = "0.3.16"
|
||||
|
|
|
@ -0,0 +1,143 @@
|
|||
use std::{io, time::Duration};
|
||||
|
||||
use clap::Parser;
|
||||
use rcgen::{Certificate, CertificateParams, DistinguishedName};
|
||||
use tokio::time::sleep;
|
||||
use tracing::{error, info};
|
||||
|
||||
use instant_acme::{
|
||||
Account, AuthorizationStatus, ChallengeType, Identifier, LetsEncrypt, NewAccount, NewOrder,
|
||||
OrderStatus,
|
||||
};
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> anyhow::Result<()> {
|
||||
tracing_subscriber::fmt::init();
|
||||
let opts = Options::parse();
|
||||
|
||||
// Create a new account. This will generate a fresh ECDSA key for you.
|
||||
// Alternatively, restore an account from serialized credentials by
|
||||
// using `Account::from_credentials()`.
|
||||
|
||||
let account = Account::create(
|
||||
&NewAccount {
|
||||
contact: &[],
|
||||
terms_of_service_agreed: true,
|
||||
only_return_existing: false,
|
||||
},
|
||||
LetsEncrypt::Staging.url(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
// Create the ACME order based on the given domain names.
|
||||
// Note that this only needs an `&Account`, so the library will let you
|
||||
// process multiple orders in parallel for a single account.
|
||||
|
||||
let identifier = Identifier::Dns(opts.name);
|
||||
let (mut order, state) = account
|
||||
.new_order(&NewOrder {
|
||||
identifiers: &[identifier],
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
info!("order state: {:#?}", state);
|
||||
assert!(matches!(state.status, OrderStatus::Pending));
|
||||
|
||||
// Pick the desired challenge type and prepare the response.
|
||||
|
||||
let authorizations = order.authorizations(&state.authorizations).await.unwrap();
|
||||
let mut challenges = Vec::with_capacity(authorizations.len());
|
||||
for authz in &authorizations {
|
||||
match authz.status {
|
||||
AuthorizationStatus::Pending => {}
|
||||
AuthorizationStatus::Valid => continue,
|
||||
_ => todo!(),
|
||||
}
|
||||
|
||||
// We'll use the DNS challenges for this example, but you could
|
||||
// pick something else to use here.
|
||||
|
||||
let challenge = authz
|
||||
.challenges
|
||||
.iter()
|
||||
.find(|c| c.r#type == ChallengeType::Dns01)
|
||||
.ok_or_else(|| anyhow::anyhow!("no dns01 challenge found"))?;
|
||||
|
||||
let Identifier::Dns(identifier) = &authz.identifier;
|
||||
|
||||
println!("Please set the following DNS record then press any key:");
|
||||
println!(
|
||||
"_acme-challenge.{} IN TXT {}",
|
||||
identifier,
|
||||
order.key_authorization(challenge).dns_value()
|
||||
);
|
||||
io::stdin().read_line(&mut String::new()).unwrap();
|
||||
|
||||
challenges.push((identifier, &challenge.url));
|
||||
}
|
||||
|
||||
// Let the server know we're ready to accept the challenges.
|
||||
|
||||
for (_, url) in &challenges {
|
||||
order.set_challenge_ready(url).await.unwrap();
|
||||
}
|
||||
|
||||
// Exponentially back off until the order becomes ready or invalid.
|
||||
|
||||
let mut tries = 1u8;
|
||||
let mut delay = Duration::from_millis(250);
|
||||
let state = loop {
|
||||
sleep(delay).await;
|
||||
let state = order.state().await.unwrap();
|
||||
if let OrderStatus::Ready | OrderStatus::Invalid = state.status {
|
||||
info!("order state: {:#?}", state);
|
||||
break state;
|
||||
}
|
||||
|
||||
delay *= 2;
|
||||
tries += 1;
|
||||
match tries < 5 {
|
||||
true => info!(?state, tries, "order is not ready, waiting {delay:?}"),
|
||||
false => {
|
||||
error!(?state, tries, "order is not ready");
|
||||
return Err(anyhow::anyhow!("order is not ready"));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if state.status == OrderStatus::Invalid {
|
||||
return Err(anyhow::anyhow!("order is invalid"));
|
||||
}
|
||||
|
||||
let mut names = Vec::with_capacity(challenges.len());
|
||||
for (identifier, _) in challenges {
|
||||
names.push(identifier.to_owned());
|
||||
}
|
||||
|
||||
// If the order is ready, we can provision the certificate.
|
||||
// Use the rcgen library to create a Certificate Signing Request.
|
||||
|
||||
let mut params = CertificateParams::new(names.clone());
|
||||
params.distinguished_name = DistinguishedName::new();
|
||||
let cert = Certificate::from_params(params).unwrap();
|
||||
let csr = cert.serialize_request_der()?;
|
||||
|
||||
// Finalize the order and print certificate chain, private key and account credentials.
|
||||
|
||||
let cert_chain_pem = order.finalize(&csr, &state.finalize).await.unwrap();
|
||||
info!("certficate chain:\n\n{}", cert_chain_pem,);
|
||||
info!("private key:\n\n{}", cert.serialize_private_key_pem());
|
||||
info!(
|
||||
"account credentials:\n\n{}",
|
||||
serde_json::to_string_pretty(&account.credentials()).unwrap()
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[derive(Parser)]
|
||||
struct Options {
|
||||
#[clap(long)]
|
||||
name: String,
|
||||
}
|
Loading…
Reference in New Issue