use std::{io, time::Duration}; use clap::Parser; use rcgen::{CertificateParams, DistinguishedName, KeyPair}; 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, credentials) = Account::create( &NewAccount { contact: &[], terms_of_service_agreed: true, only_return_existing: false, }, LetsEncrypt::Staging.url(), None, ) .await?; info!( "account credentials:\n\n{}", serde_json::to_string_pretty(&credentials).unwrap() ); // 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 = account .new_order(&NewOrder { identifiers: &[identifier], }) .await .unwrap(); let state = order.state(); info!("order state: {:#?}", state); assert!(matches!(state.status, OrderStatus::Pending)); // Pick the desired challenge type and prepare the response. let authorizations = order.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 the Return 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); loop { sleep(delay).await; let state = order.refresh().await.unwrap(); if let OrderStatus::Ready | OrderStatus::Invalid = state.status { info!("order state: {:#?}", state); break; } delay *= 2; tries += 1; match tries < 5 { true => info!(?state, tries, "order is not ready, waiting {delay:?}"), false => { error!(tries, "order is not ready: {state:#?}"); return Err(anyhow::anyhow!("order is not ready")); } } } let state = order.state(); if state.status != OrderStatus::Ready { return Err(anyhow::anyhow!( "unexpected order status: {:?}", state.status )); } 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 private_key = KeyPair::generate()?; let csr = params.serialize_request(&private_key)?; // Finalize the order and print certificate chain, private key and account credentials. order.finalize(csr.der()).await.unwrap(); let cert_chain_pem = loop { match order.certificate().await.unwrap() { Some(cert_chain_pem) => break cert_chain_pem, None => sleep(Duration::from_secs(1)).await, } }; info!("certficate chain:\n\n{}", cert_chain_pem); info!("private key:\n\n{}", private_key.serialize_pem()); Ok(()) } #[derive(Parser)] struct Options { #[clap(long)] name: String, }