diff --git a/examples/provision.rs b/examples/provision.rs index 9f92a43..21989df 100644 --- a/examples/provision.rs +++ b/examples/provision.rs @@ -71,7 +71,7 @@ async fn main() -> anyhow::Result<()> { println!( "_acme-challenge.{} IN TXT {}", challenge.identifier(), - challenge.key_authorization().dns_value() + challenge.key_authorization().unwrap().dns_value() ); io::stdin().read_line(&mut String::new())?; diff --git a/src/lib.rs b/src/lib.rs index fee784d..ff77a80 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -44,9 +44,10 @@ pub use order::{ mod types; pub use types::{ AccountCredentials, Authorization, AuthorizationState, AuthorizationStatus, - AuthorizedIdentifier, CertificateIdentifier, Challenge, ChallengeStatus, ChallengeType, - DeviceAttestation, Error, Identifier, LetsEncrypt, NewAccount, NewOrder, OrderState, - OrderStatus, Problem, ProfileMeta, RevocationReason, RevocationRequest, Subproblem, ZeroSsl, + AuthorizedIdentifier, CertificateIdentifier, Challenge, ChallengeState, ChallengeStatus, + ChallengeType, DeviceAttestation, Error, Identifier, LetsEncrypt, NewAccount, NewOrder, + OrderState, OrderStatus, Problem, ProfileMeta, RevocationReason, RevocationRequest, Subproblem, + ZeroSsl, }; use types::{Directory, JoseJson, Signer}; #[cfg(feature = "time")] diff --git a/src/order.rs b/src/order.rs index 970e2bf..84c67b5 100644 --- a/src/order.rs +++ b/src/order.rs @@ -394,7 +394,11 @@ impl<'a> AuthorizationHandle<'a> { /// /// Yields an object to interact with the challenge for the given type, if available. pub fn challenge(&'a mut self, r#type: ChallengeType) -> Option> { - let challenge = self.state.challenges.iter().find(|c| c.r#type == r#type)?; + let challenge = self + .state + .challenges + .iter() + .find(|c| c.state.r#type() == r#type)?; Some(ChallengeHandle { identifier: self.state.identifier(), challenge, @@ -471,7 +475,7 @@ impl ChallengeHandle<'_> { &mut self, payload: &DeviceAttestation<'_>, ) -> Result { - if self.challenge.r#type != ChallengeType::DeviceAttest01 { + if self.challenge.state.r#type() != ChallengeType::DeviceAttest01 { return Err(Error::Str("challenge type should be device-attest-01")); } @@ -503,8 +507,10 @@ impl ChallengeHandle<'_> { /// Combines a challenge's token with the thumbprint of the account's public key to compute /// the challenge's `KeyAuthorization`. The `KeyAuthorization` must be used to provision the /// expected challenge response based on the challenge type in use. - pub fn key_authorization(&self) -> KeyAuthorization { - KeyAuthorization::new(self.challenge, &self.account.key) + pub fn key_authorization(&self) -> Option { + self.challenge + .token() + .map(|token| KeyAuthorization::new(token, &self.account.key)) } /// The identifier for this challenge's authorization @@ -529,8 +535,8 @@ impl Deref for ChallengeHandle<'_> { pub struct KeyAuthorization(String); impl KeyAuthorization { - fn new(challenge: &Challenge, key: &Key) -> Self { - Self(format!("{}.{}", challenge.token, &key.thumb)) + fn new(token: &str, key: &Key) -> Self { + Self(format!("{token}.{}", &key.thumb)) } /// Get the key authorization value diff --git a/src/types.rs b/src/types.rs index 99f2156..7ab059b 100644 --- a/src/types.rs +++ b/src/types.rs @@ -145,7 +145,7 @@ mod pkcs8_serde { } /// An RFC 7807 problem document as returned by the ACME server -#[derive(Clone, Debug, Deserialize)] +#[derive(Clone, Debug, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] pub struct Problem { /// One of an enumerated list of problem types @@ -165,7 +165,9 @@ pub struct Problem { impl Problem { pub(crate) async fn check(rsp: BytesResponse) -> Result { - Ok(serde_json::from_slice(&Self::from_response(rsp).await?)?) + let body = Self::from_response(rsp).await?; + println!("{}", str::from_utf8(&body).unwrap()); + Ok(serde_json::from_slice(&body)?) } pub(crate) async fn from_response(rsp: BytesResponse) -> Result { @@ -209,7 +211,7 @@ impl std::error::Error for Problem {} /// An RFC 8555 subproblem document contained within a problem returned by the ACME server /// /// See -#[derive(Clone, Debug, Deserialize)] +#[derive(Clone, Debug, Deserialize, Serialize)] pub struct Subproblem { /// The identifier associated with this problem pub identifier: Option, @@ -323,23 +325,6 @@ struct JwkThumb<'a> { y: &'a str, } -/// An ACME challenge as described in RFC 8555 (section 7.1.5) -/// -/// -#[derive(Debug, Deserialize)] -pub struct Challenge { - /// Type of challenge - pub r#type: ChallengeType, - /// Challenge identifier - pub url: String, - /// Token for this challenge - pub token: String, - /// Current status - pub status: ChallengeStatus, - /// Potential error state - pub error: Option, -} - /// Contents of an ACME order as described in RFC 8555 (section 7.1.3) /// /// The order identity will usually be represented by an [Order](crate::Order). @@ -636,19 +621,6 @@ impl AuthorizationState { } } -/// Status for an [`AuthorizationState`] -#[allow(missing_docs)] -#[derive(Clone, Copy, Debug, Serialize, Deserialize, Eq, PartialEq)] -#[serde(rename_all = "camelCase")] -pub enum AuthorizationStatus { - Pending, - Valid, - Invalid, - Revoked, - Expired, - Deactivated, -} - /// Represent an identifier in an ACME [Order](crate::Order) #[allow(missing_docs)] #[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq)] @@ -713,6 +685,72 @@ impl fmt::Display for AuthorizedIdentifier<'_> { } } +/// An ACME challenge as described in RFC 8555 (section 7.1.5) +/// +/// +#[derive(Debug, Deserialize, Serialize)] +pub struct Challenge { + /// Type of challenge + #[serde(flatten)] + pub state: ChallengeState, + /// Challenge identifier + pub url: String, + /// Current status + pub status: ChallengeStatus, + /// Potential error state + pub error: Option, +} + +impl Challenge { + /// Get the token for this challenge, if it has one + pub fn token(&self) -> Option<&str> { + match &self.state { + ChallengeState::Http01 { token } + | ChallengeState::Dns01 { token } + | ChallengeState::TlsAlpn01 { token } => Some(token), + ChallengeState::DeviceAttest01 + | ChallengeState::DnsPersist01 { .. } + | ChallengeState::Unknown(_) => None, + } + } +} + +#[allow(missing_docs)] +#[derive(Debug, Deserialize, Eq, PartialEq, Serialize)] +#[serde(tag = "type")] +pub enum ChallengeState { + #[serde(rename = "http-01")] + Http01 { token: String }, + #[serde(rename = "dns-01")] + Dns01 { token: String }, + #[serde(rename = "dns-persist-01")] + DnsPersist01 { + #[serde(rename = "issuer-domain-names")] + issuer_domain_names: Vec, + }, + #[serde(rename = "tls-alpn-01")] + TlsAlpn01 { token: String }, + /// Note: Device attestation support is experimental + #[serde(rename = "device-attest-01")] + DeviceAttest01, + #[serde(untagged)] + Unknown(String), +} + +impl ChallengeState { + /// Get the type of this challenge + pub fn r#type(&self) -> ChallengeType { + match self { + Self::Http01 { .. } => ChallengeType::Http01, + Self::Dns01 { .. } => ChallengeType::Dns01, + Self::DnsPersist01 { .. } => ChallengeType::DnsPersist01, + Self::TlsAlpn01 { .. } => ChallengeType::TlsAlpn01, + Self::DeviceAttest01 => ChallengeType::DeviceAttest01, + Self::Unknown(s) => ChallengeType::Unknown(s.clone()), + } + } +} + /// The challenge type #[allow(missing_docs)] #[non_exhaustive] @@ -722,6 +760,8 @@ pub enum ChallengeType { Http01, #[serde(rename = "dns-01")] Dns01, + #[serde(rename = "dns-persist-01")] + DnsPersist01, #[serde(rename = "tls-alpn-01")] TlsAlpn01, /// Note: Device attestation support is experimental @@ -733,7 +773,7 @@ pub enum ChallengeType { /// Status of an ACME [Challenge] #[allow(missing_docs)] -#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq)] +#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)] #[serde(rename_all = "camelCase")] pub enum ChallengeStatus { Pending, @@ -742,6 +782,19 @@ pub enum ChallengeStatus { Invalid, } +/// Status for an [`AuthorizationState`] +#[allow(missing_docs)] +#[derive(Clone, Copy, Debug, Serialize, Deserialize, Eq, PartialEq)] +#[serde(rename_all = "camelCase")] +pub enum AuthorizationStatus { + Pending, + Valid, + Invalid, + Revoked, + Expired, + Deactivated, +} + /// Status of an [Order](crate::Order) #[allow(missing_docs)] #[derive(Clone, Copy, Debug, Deserialize, PartialEq, Eq)] @@ -1040,6 +1093,18 @@ mod tests { // https://datatracker.ietf.org/doc/html/rfc8555#section-8.4 #[test] fn challenge() { + dbg!( + serde_json::to_string(&Challenge { + state: ChallengeState::Dns01 { + token: "foo".to_owned() + }, + url: "bar".to_owned(), + status: ChallengeStatus::Pending, + error: None, + }) + .unwrap() + ); + const CHALLENGE: &str = r#"{ "type": "dns-01", "url": "https://example.com/acme/chall/Rg5dV14Gh1Q", @@ -1048,10 +1113,14 @@ mod tests { }"#; let obj = serde_json::from_str::(CHALLENGE).unwrap(); - assert_eq!(obj.r#type, ChallengeType::Dns01); + assert_eq!( + obj.state, + ChallengeState::Dns01 { + token: "evaGxfADs6pSRb2LAv9IZf17Dt3juxGJ-PCt92wr-oA".to_owned() + } + ); assert_eq!(obj.url, "https://example.com/acme/chall/Rg5dV14Gh1Q"); assert_eq!(obj.status, ChallengeStatus::Pending); - assert_eq!(obj.token, "evaGxfADs6pSRb2LAv9IZf17Dt3juxGJ-PCt92wr-oA"); } // https://datatracker.ietf.org/doc/html/rfc8555#section-7.6 diff --git a/tests/pebble.rs b/tests/pebble.rs index 37dbc73..061674f 100644 --- a/tests/pebble.rs +++ b/tests/pebble.rs @@ -565,7 +565,7 @@ impl Environment { Command::new(&challtestsrv_path) .arg("-management") .arg(format!(":{}", config.challtestsrv_port)) - .arg("-dns01") + .arg("-dnsserver") .arg(format!(":{}", config.dns_port)) .arg("-http01") .arg(format!(":{}", config.pebble.http_port)) @@ -637,25 +637,33 @@ impl Environment { // Collect up the relevant challenges, provisioning the expected responses as we go. let mut authorizations = order.authorizations(); while let Some(result) = authorizations.next().await { + dbg!("got authorization"); let mut authz = result?; + dbg!(&authz.challenges); match authz.status { AuthorizationStatus::Pending => {} AuthorizationStatus::Valid => continue, _ => unreachable!("unexpected authz state: {:?}", authz.status), } + dbg!("selecting challenge"); let mut challenge = authz .challenge(A::TYPE) .ok_or_else(|| format!("no {:?} challenge found", A::TYPE))?; - let key_authz = challenge.key_authorization(); + dbg!("get key authorization"); + let key_authz = challenge.key_authorization().unwrap(); + dbg!("request challenge"); self.request_challenge::(&challenge, &key_authz).await?; debug!(challenge_url = challenge.url, "marking challenge ready"); + dbg!("set challenge ready"); challenge.set_ready().await?; + dbg!("challenge ready"); } // Poll until the order is ready. + dbg!("poll_ready()"); let status = order.poll_ready(&RETRY_POLICY).await?; if status != OrderStatus::Ready { return Err(format!("unexpected order status: {status:?}").into()); @@ -787,7 +795,7 @@ impl AuthorizationMethod for Http01 { key_auth: &'a KeyAuthorization, ) -> impl Serialize + 'a { debug!( - token = challenge.token, + token = ?challenge.token(), key_auth = key_auth.as_str(), "provisioning HTTP-01 response", ); @@ -799,7 +807,7 @@ impl AuthorizationMethod for Http01 { } AddHttp01Request { - token: &challenge.token, + token: challenge.token().unwrap(), content: key_auth.as_str(), } }