diff --git a/Cargo.toml b/Cargo.toml index 088ea55..8e1a870 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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" diff --git a/examples/provision.rs b/examples/provision.rs new file mode 100644 index 0000000..7c71a5b --- /dev/null +++ b/examples/provision.rs @@ -0,0 +1,144 @@ +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(()) +} + +/// SMTP server +#[derive(Parser)] +struct Options { + #[clap(long)] + name: String, +}