Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion examples/provision.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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())?;

Expand Down
7 changes: 4 additions & 3 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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")]
Expand Down
18 changes: 12 additions & 6 deletions src/order.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<ChallengeHandle<'a>> {
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,
Expand Down Expand Up @@ -471,7 +475,7 @@ impl ChallengeHandle<'_> {
&mut self,
payload: &DeviceAttestation<'_>,
) -> Result<ChallengeStatus, Error> {
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"));
}

Expand Down Expand Up @@ -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<KeyAuthorization> {
self.challenge
.token()
.map(|token| KeyAuthorization::new(token, &self.account.key))
}

/// The identifier for this challenge's authorization
Expand All @@ -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
Expand Down
141 changes: 105 additions & 36 deletions src/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -165,7 +165,9 @@ pub struct Problem {

impl Problem {
pub(crate) async fn check<T: DeserializeOwned>(rsp: BytesResponse) -> Result<T, Error> {
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<Bytes, Error> {
Expand Down Expand Up @@ -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 <https://www.rfc-editor.org/rfc/rfc8555#section-6.7.1>
#[derive(Clone, Debug, Deserialize)]
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct Subproblem {
/// The identifier associated with this problem
pub identifier: Option<Identifier>,
Expand Down Expand Up @@ -323,23 +325,6 @@ struct JwkThumb<'a> {
y: &'a str,
}

/// An ACME challenge as described in RFC 8555 (section 7.1.5)
///
/// <https://datatracker.ietf.org/doc/html/rfc8555#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<Problem>,
}

/// 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).
Expand Down Expand Up @@ -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)]
Expand Down Expand Up @@ -713,6 +685,72 @@ impl fmt::Display for AuthorizedIdentifier<'_> {
}
}

/// An ACME challenge as described in RFC 8555 (section 7.1.5)
///
/// <https://datatracker.ietf.org/doc/html/rfc8555#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<Problem>,
}

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<String>,
},
#[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]
Expand All @@ -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
Expand All @@ -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,
Expand All @@ -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)]
Expand Down Expand Up @@ -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",
Expand All @@ -1048,10 +1113,14 @@ mod tests {
}"#;

let obj = serde_json::from_str::<Challenge>(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
Expand Down
16 changes: 12 additions & 4 deletions tests/pebble.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand Down Expand Up @@ -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::<A>(&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());
Expand Down Expand Up @@ -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",
);
Expand All @@ -799,7 +807,7 @@ impl AuthorizationMethod for Http01 {
}

AddHttp01Request {
token: &challenge.token,
token: challenge.token().unwrap(),
content: key_auth.as_str(),
}
}
Expand Down
Loading