Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Http-01 Challenge Framework support #72

Open
wants to merge 5 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 2 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
13 changes: 12 additions & 1 deletion src/acme.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
use std::sync::Arc;

use aws_lc_rs::digest::Digest;
use crate::any_ecdsa_type;
use crate::crypto::error::{KeyRejected, Unspecified};
use crate::crypto::rand::SystemRandom;
Expand Down Expand Up @@ -130,6 +130,15 @@ impl Account {
let certified_key = CertifiedKey::new(vec![cert.der().clone()], sk);
Ok((challenge, certified_key))
}
pub fn http_01<'a>(&self, challenges: &'a [Challenge]) -> Result<(&'a Challenge, Digest), AcmeError> {
let challenge = challenges.iter().find(|c| c.typ == ChallengeType::Http01);
let challenge = match challenge {
Some(challenge) => challenge,
None => return Err(AcmeError::NoHttp01Challenge),
};
let key_auth = key_authorization_sha256(&self.key_pair, &challenge.token)?;
Ok((challenge, key_auth))
}
}

#[derive(Debug, Clone, Deserialize)]
Expand Down Expand Up @@ -245,6 +254,8 @@ pub enum AcmeError {
MissingHeader(&'static str),
#[error("no tls-alpn-01 challenge found")]
NoTlsAlpn01Challenge,
#[error("no http-01 challenge found")]
NoHttp01Challenge,
}

impl From<http::Error> for AcmeError {
Expand Down
10 changes: 10 additions & 0 deletions src/config.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
use crate::acme::{LETS_ENCRYPT_PRODUCTION_DIRECTORY, LETS_ENCRYPT_STAGING_DIRECTORY};
use crate::caches::{BoxedErrCache, CompositeCache, NoCache};
use crate::UseChallenge::TlsAlpn01;
use crate::{crypto_provider, AccountCache, Cache, CertCache};
use crate::{AcmeState, Incoming};
use core::fmt;
Expand All @@ -21,8 +22,11 @@ pub struct AcmeConfig<EC: Debug, EA: Debug = EC> {
pub(crate) domains: Vec<String>,
pub(crate) contact: Vec<String>,
pub(crate) cache: Box<dyn Cache<EC = EC, EA = EA>>,
pub(crate) challenge_type: UseChallenge
}

pub enum UseChallenge { Http01, TlsAlpn01 }

impl AcmeConfig<Infallible, Infallible> {
/// Creates a new [AcmeConfig] instance.
///
Expand Down Expand Up @@ -78,6 +82,7 @@ impl AcmeConfig<Infallible, Infallible> {
domains: domains.into_iter().map(|s| s.as_ref().into()).collect(),
contact: vec![],
cache: Box::new(NoCache::default()),
challenge_type: TlsAlpn01
}
}
}
Expand Down Expand Up @@ -132,6 +137,7 @@ impl<EC: 'static + Debug, EA: 'static + Debug> AcmeConfig<EC, EA> {
domains: self.domains,
contact: self.contact,
cache: Box::new(cache),
challenge_type: self.challenge_type,
}
}
pub fn cache_compose<CC: 'static + CertCache, CA: 'static + AccountCache>(self, cert_cache: CC, account_cache: CA) -> AcmeConfig<CC::EC, CA::EA> {
Expand All @@ -146,6 +152,10 @@ impl<EC: 'static + Debug, EA: 'static + Debug> AcmeConfig<EC, EA> {
None => self.cache(NoCache::<C::EC, C::EA>::default()),
}
}
pub fn challenge_type(mut self, challenge_type: UseChallenge) -> Self {
self.challenge_type = challenge_type;
self
}
pub fn state(self) -> AcmeState<EC, EA> {
AcmeState::new(self)
}
Expand Down
14 changes: 13 additions & 1 deletion src/resolver.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@ use futures_rustls::rustls::{
sign::CertifiedKey,
};
use std::collections::BTreeMap;
use std::fmt::Debug;
use std::sync::Arc;
use std::sync::Mutex;

use crate::is_tls_alpn_challenge;

#[derive(Debug)]
Expand All @@ -17,6 +17,7 @@ pub struct ResolvesServerCertAcme {
struct Inner {
cert: Option<Arc<CertifiedKey>>,
auth_keys: BTreeMap<String, Arc<CertifiedKey>>,
key_auths: BTreeMap<String, Arc<Vec<u8>>>,
Copy link
Owner

@FlorianUekermann FlorianUekermann Feb 2, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As you suspect. Per RFC we only need to do one challenge (and that's what we do atm). So I think we can drop the b-trees in favor of something like:

enum ChallengeData {
  TlsAlpn01 {
    sni: String,
    cert: Arc<CertifiedKey>,
  },
  Http01 { ... }
}

struct Inner {
  cert: Option<Arc<CertifiedKey>>,
  challenge_data: Option<ChallengeData>
}

Copy link
Author

@jklamer jklamer Feb 3, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Absolutely can do this style.

Is it possible that there are many autherizes going on at once (one for each domain). Will the try_join_all below execute the authorize code out of order? Which might mean each one overrides and deletes the others?

let auth_futures = order.authorizations.iter().map(|url| Self::authorize(&config, &resolver, &account, url));
try_join_all(auth_futures).await?;

I think the code I have now to clear the challenge data would be incorrect in this case.
Looking at the code I think for a number of authorizations less than 30 it will be unordered. I think if we force in order execution to allow the single challenge data implementation to work. I'll implement this and see what you think
Screenshot 2025-02-02 at 7 13 50 PM

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see, I mistook lack of order concurrency for a lack of authorization concurrency. I think your solution is good though, this isn't very time critical and this simplifies cleanup (not that keeping these around would become an issue in practice, but doing this properly is nice).

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah it's interesting. For the Tls apn, the data is bounded by number of domains so doesn't really need to be cleaned up in practice just overwritten. For HTTP-01 the token can become different every authorize, so after enough of them or bunch of failed attempts and a lot of time the accumulation would likely become noticeable

}

impl ResolvesServerCertAcme {
Expand All @@ -25,6 +26,8 @@ impl ResolvesServerCertAcme {
inner: Mutex::new(Inner {
cert: None,
auth_keys: Default::default(),
// Reasonably high key auth cache defaults. Avoid Infinite accumulation
key_auths: Default::default(),
}),
})
}
Expand All @@ -34,6 +37,15 @@ impl ResolvesServerCertAcme {
pub(crate) fn set_auth_key(&self, domain: String, cert: Arc<CertifiedKey>) {
self.inner.lock().unwrap().auth_keys.insert(domain, cert);
}
pub(crate) fn set_key_auth(&self, token: String, key_auth: Arc<Vec<u8>>) {
self.inner.lock().unwrap().key_auths.insert(token, key_auth);
}
pub (crate) fn clear_key_auth(&self, token: &String) {
self.inner.lock().unwrap().key_auths.remove(token);
}
pub fn get_key_auth(&self, token: &String) -> Option<Arc<Vec<u8>>> {
self.inner.lock().unwrap().key_auths.get(token).cloned()
}
}

impl ResolvesServerCert for ResolvesServerCertAcme {
Expand Down
44 changes: 35 additions & 9 deletions src/state.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
use crate::acceptor::AcmeAcceptor;
use crate::acme::{Account, AcmeError, Auth, AuthStatus, Directory, Identifier, Order, OrderStatus, ACME_TLS_ALPN_NAME};
use crate::{any_ecdsa_type, crypto_provider, AcmeConfig, Incoming, ResolvesServerCertAcme};
use crate::{any_ecdsa_type, crypto_provider, AcmeConfig, Incoming, ResolvesServerCertAcme, UseChallenge};
use async_io::Timer;
use chrono::{DateTime, TimeZone, Utc};
use core::fmt;
Expand Down Expand Up @@ -292,17 +292,35 @@ impl<EC: 'static + Debug, EA: 'static + Debug> AcmeState<EC, EA> {
}
async fn authorize(config: &AcmeConfig<EC, EA>, resolver: &ResolvesServerCertAcme, account: &Account, url: &String) -> Result<(), OrderError> {
let auth = account.auth(&config.client_config, url).await?;
let (domain, challenge_url) = match auth.status {
let (domain, challenge_url, token) = match auth.status {
AuthStatus::Pending => {
let Identifier::Dns(domain) = auth.identifier;
log::info!("trigger challenge for {}", &domain);
let (challenge, auth_key) = account.tls_alpn_01(&auth.challenges, domain.clone())?;
resolver.set_auth_key(domain.clone(), Arc::new(auth_key));
let challenge = match config.challenge_type {
UseChallenge::Http01 => {
let (challenge, key_auth) = account.http_01(&auth.challenges)?;
resolver.set_key_auth(challenge.token.clone(), Arc::new(Vec::from(key_auth.as_ref())));
challenge
}
UseChallenge::TlsAlpn01 => {
let (challenge, auth_key) = account.tls_alpn_01(&auth.challenges, domain.clone())?;
resolver.set_auth_key(domain.clone(), Arc::new(auth_key));
challenge
}
};
account.challenge(&config.client_config, &challenge.url).await?;
(domain, challenge.url.clone())
(domain, challenge.url.clone(), challenge.token.clone())
}
AuthStatus::Valid => {
jklamer marked this conversation as resolved.
Show resolved Hide resolved
//clear all tokens from challenges when auth is valid
auth.challenges.iter().map(|c| &c.token).for_each(|t| resolver.clear_key_auth(t));
return Ok(());
}
AuthStatus::Valid => return Ok(()),
_ => return Err(OrderError::BadAuth(auth)),
_ => {
// clear all tokens from challenges when auth is invalid
auth.challenges.iter().map(|c| &c.token).for_each(|t| resolver.clear_key_auth(t));
return Err(OrderError::BadAuth(auth))
},
};
for i in 0u64..5 {
Timer::after(Duration::from_secs(1u64 << i)).await;
Expand All @@ -312,8 +330,16 @@ impl<EC: 'static + Debug, EA: 'static + Debug> AcmeState<EC, EA> {
log::info!("authorization for {} still pending", &domain);
account.challenge(&config.client_config, &challenge_url).await?
}
AuthStatus::Valid => return Ok(()),
_ => return Err(OrderError::BadAuth(auth)),
AuthStatus::Valid => {
// clear token from chosen challenge when auth validated
resolver.clear_key_auth(&token);
return Ok(())
},
_ => {
// clear token from chosen challenge when auth invalidated
resolver.clear_key_auth(&token);
return Err(OrderError::BadAuth(auth))
},
}
}
Err(OrderError::TooManyAttemptsAuth(domain))
Expand Down
Loading