diff --git a/examples/provision.rs b/examples/provision.rs index 04df707..cf4f00c 100644 --- a/examples/provision.rs +++ b/examples/provision.rs @@ -130,8 +130,15 @@ async fn main() -> anyhow::Result<()> { // Finalize the order and print certificate chain, private key and account credentials. - let cert_chain_pem = order.finalize(&csr).await.unwrap(); - info!("certficate chain:\n\n{}", cert_chain_pem,); + order.finalize(&csr).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{}", cert.serialize_private_key_pem()); info!( "account credentials:\n\n{}", diff --git a/src/lib.rs b/src/lib.rs index 92b5fd4..53393aa 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -75,9 +75,9 @@ impl Order { /// Request a certificate from the given Certificate Signing Request (CSR) /// /// Creating a CSR is outside of the scope of instant-acme. Make sure you pass in a - /// DER representation of the CSR in `csr_der`. The resulting `String` will contain the - /// PEM-encoded certificate chain. - pub async fn finalize(&mut self, csr_der: &[u8]) -> Result { + /// DER representation of the CSR in `csr_der`. Call `certificate()` to retrieve the + /// certificate chain once the order is in the appropriate state. + pub async fn finalize(&mut self, csr_der: &[u8]) -> Result<(), Error> { let rsp = self .account .post( @@ -88,24 +88,51 @@ impl Order { .await?; self.nonce = nonce_from_response(&rsp); - let state = Problem::check::(rsp).await?; + self.state = Problem::check::(rsp).await?; + Ok(()) + } - let cert_url = match state.certificate { - Some(url) => url, - None => return Err(Error::Str("no certificate URL")), + /// Get the certificate for this order + /// + /// If the cached order state is in `ready` or `processing` state, this will poll the server + /// for the latest state. If the order is still in `processing` state after that, this will + /// return `Ok(None)`. If the order is in `valid` state, this will attempt to retrieve + /// the certificate from the server and return it as a `String`. If the order contains + /// an error or ends up in any state other than `valid` or `processing`, return an error. + pub async fn certificate(&mut self) -> Result, Error> { + if matches!(self.state.status, OrderStatus::Processing) { + let rsp = self + .account + .post(None::<&Empty>, self.nonce.take(), &self.url) + .await?; + self.nonce = nonce_from_response(&rsp); + self.state = Problem::check::(rsp).await?; + } + + if let Some(error) = &self.state.error { + return Err(Error::Api(error.clone())); + } else if self.state.status == OrderStatus::Processing { + return Ok(None); + } else if self.state.status != OrderStatus::Valid { + return Err(Error::Str("invalid order state")); + } + + let cert_url = match &self.state.certificate { + Some(cert_url) => cert_url, + None => return Err(Error::Str("no certificate URL found")), }; let rsp = self .account - .post(None::<&Empty>, self.nonce.take(), &cert_url) + .post(None::<&Empty>, self.nonce.take(), cert_url) .await?; self.nonce = nonce_from_response(&rsp); let body = hyper::body::to_bytes(Problem::from_response(rsp).await?).await?; - Ok( + Ok(Some( String::from_utf8(body.to_vec()) .map_err(|_| "unable to decode certificate as UTF-8")?, - ) + )) } /// Notify the server that the given challenge is ready to be completed diff --git a/src/types.rs b/src/types.rs index 807d18a..43fe7ad 100644 --- a/src/types.rs +++ b/src/types.rs @@ -60,7 +60,7 @@ pub struct AccountCredentials<'a> { } /// An RFC 7807 problem document as returned by the ACME server -#[derive(Debug, Deserialize)] +#[derive(Clone, Debug, Deserialize)] #[serde(rename_all = "camelCase")] pub struct Problem { /// One of an enumerated list of problem types