Skip to content

Commit e8867da

Browse files
authored
Http-01 Challenge support (#72)
1 parent ffe3292 commit e8867da

9 files changed

+233
-31
lines changed

Cargo.toml

+10-4
Original file line numberDiff line numberDiff line change
@@ -33,11 +33,12 @@ axum-server = { version = "0.7", features = ["tls-rustls-no-provider"], optional
3333
async-web-client = { version = "0.6.2", default-features = false }
3434
http = "1"
3535
blocking = "1.4.1"
36+
tower-service = { version = "0.3.3", optional=true }
3637

3738
[dev-dependencies]
3839
simple_logger = "4.3.3"
3940
clap = { version = "3.1.18", features = ["derive"] }
40-
axum = "0.7"
41+
axum = "0.8"
4142
tokio = { version="1.35.1", features = ["full"] }
4243
tokio-stream = { version="0.1.14", features = ["net"] }
4344
tokio-util = { version="0.7.10", features = ["compat"] }
@@ -59,13 +60,18 @@ rustdoc-args = ["--cfg", "doc_auto_cfg"]
5960
default = ["aws-lc-rs", "tls12"]
6061
ring = ["dep:ring", "async-web-client/ring", "rcgen/ring"]
6162
aws-lc-rs = ["dep:aws-lc-rs", "async-web-client/aws-lc-rs", "rcgen/aws_lc_rs"]
62-
axum = ["dep:axum-server", "tokio"]
63+
axum = ["dep:axum-server", "tower"]
64+
tower = ["dep:tower-service", "tokio"]
6365
tokio = ["dep:tokio", "dep:tokio-util"]
6466
tls12 = ["async-web-client/tls12"]
6567

6668
[[example]]
67-
name="low_level_axum"
68-
required-features=["axum"]
69+
name = "low_level_axum"
70+
required-features = ["axum"]
71+
72+
[[example]]
73+
name = "low_level_axum_http"
74+
required-features = ["axum", "tower"]
6975

7076
[[example]]
7177
name="high_level_warp"

examples/low_level_axum_http.rs

+73
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
use axum::extract::{Path, State};
2+
use axum::response::{IntoResponse, Response};
3+
use axum::{routing::get, Router};
4+
use axum_server::bind;
5+
use clap::Parser;
6+
use http::{header, HeaderValue, StatusCode};
7+
use rustls_acme::caches::DirCache;
8+
use rustls_acme::tower::TowerHttp01ChallengeService;
9+
use rustls_acme::UseChallenge::Http01;
10+
use rustls_acme::{AcmeConfig, ResolvesServerCertAcme};
11+
use std::net::{Ipv6Addr, SocketAddr};
12+
use std::os::macos::raw::stat;
13+
use std::path::PathBuf;
14+
use std::sync::Arc;
15+
use tokio_stream::StreamExt;
16+
17+
#[derive(Parser, Debug)]
18+
struct Args {
19+
/// Domains
20+
#[clap(short, required = true)]
21+
domains: Vec<String>,
22+
23+
/// Contact info
24+
#[clap(short)]
25+
email: Vec<String>,
26+
27+
/// Cache directory
28+
#[clap(short, parse(from_os_str))]
29+
cache: Option<PathBuf>,
30+
31+
/// Use Let's Encrypt production environment
32+
/// (see https://letsencrypt.org/docs/staging-environment/)
33+
#[clap(long)]
34+
prod: bool,
35+
36+
#[clap(short, long, default_value = "443")]
37+
port: u16,
38+
}
39+
40+
#[tokio::main]
41+
async fn main() {
42+
simple_logger::init_with_level(log::Level::Info).unwrap();
43+
let args = Args::parse();
44+
45+
let mut state = AcmeConfig::new(args.domains)
46+
.contact(args.email.iter().map(|e| format!("mailto:{}", e)))
47+
.cache_option(args.cache.clone().map(DirCache::new))
48+
.directory_lets_encrypt(args.prod)
49+
.challenge_type(Http01)
50+
.state();
51+
let acceptor = state.axum_acceptor(state.default_rustls_config());
52+
let tower_service: TowerHttp01ChallengeService = state.http01_challenge_tower_service();
53+
let http_challenge_app = Router::new().route_service("/.well-known/acme-challenge/{challenge_token}", tower_service);
54+
tokio::spawn(challenge_http_app(http_challenge_app));
55+
56+
tokio::spawn(async move {
57+
loop {
58+
match state.next().await.unwrap() {
59+
Ok(ok) => log::info!("event: {:?}", ok),
60+
Err(err) => log::error!("error: {:?}", err),
61+
}
62+
}
63+
});
64+
65+
let app = Router::new().route("/", get(|| async { "Hello Tls!" }));
66+
let addr = SocketAddr::from((Ipv6Addr::UNSPECIFIED, args.port));
67+
bind(addr).acceptor(acceptor).serve(app.into_make_service()).await.unwrap();
68+
}
69+
70+
async fn challenge_http_app(http_challenge_app: Router) {
71+
let listener = tokio::net::TcpListener::bind((Ipv6Addr::UNSPECIFIED, 80)).await.unwrap();
72+
axum::serve(listener, http_challenge_app.into_make_service()).await.unwrap();
73+
}

src/acme.rs

+13-3
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,9 @@
1-
use std::sync::Arc;
2-
31
use crate::any_ecdsa_type;
42
use crate::crypto::error::{KeyRejected, Unspecified};
53
use crate::crypto::rand::SystemRandom;
64
use crate::crypto::signature::{EcdsaKeyPair, EcdsaSigningAlgorithm, ECDSA_P256_SHA256_FIXED_SIGNING};
75
use crate::https_helper::{https, HttpsRequestError};
8-
use crate::jose::{key_authorization_sha256, sign, JoseError};
6+
use crate::jose::{key_authorization, key_authorization_sha256, sign, JoseError};
97
use base64::prelude::*;
108
use futures_rustls::pki_types::{PrivateKeyDer, PrivatePkcs8KeyDer};
119
use futures_rustls::rustls::{sign::CertifiedKey, ClientConfig};
@@ -14,6 +12,7 @@ use http::{Method, Response};
1412
use rcgen::{CustomExtension, KeyPair, PKCS_ECDSA_P256_SHA256};
1513
use serde::{Deserialize, Serialize};
1614
use serde_json::json;
15+
use std::sync::Arc;
1716
use thiserror::Error;
1817

1918
pub const LETS_ENCRYPT_STAGING_DIRECTORY: &str = "https://acme-staging-v02.api.letsencrypt.org/directory";
@@ -130,6 +129,15 @@ impl Account {
130129
let certified_key = CertifiedKey::new(vec![cert.der().clone()], sk);
131130
Ok((challenge, certified_key))
132131
}
132+
pub fn http_01<'a>(&self, challenges: &'a [Challenge]) -> Result<(&'a Challenge, String), AcmeError> {
133+
let challenge = challenges.iter().find(|c| c.typ == ChallengeType::Http01);
134+
let challenge = match challenge {
135+
Some(challenge) => challenge,
136+
None => return Err(AcmeError::NoHttp01Challenge),
137+
};
138+
let key_auth = key_authorization(&self.key_pair, &challenge.token)?;
139+
Ok((challenge, key_auth))
140+
}
133141
}
134142

135143
#[derive(Debug, Clone, Deserialize)]
@@ -245,6 +253,8 @@ pub enum AcmeError {
245253
MissingHeader(&'static str),
246254
#[error("no tls-alpn-01 challenge found")]
247255
NoTlsAlpn01Challenge,
256+
#[error("no http-01 challenge found")]
257+
NoHttp01Challenge,
248258
}
249259

250260
impl From<http::Error> for AcmeError {

src/config.rs

+13
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
use crate::acme::{LETS_ENCRYPT_PRODUCTION_DIRECTORY, LETS_ENCRYPT_STAGING_DIRECTORY};
22
use crate::caches::{BoxedErrCache, CompositeCache, NoCache};
3+
use crate::UseChallenge::TlsAlpn01;
34
use crate::{crypto_provider, AccountCache, Cache, CertCache};
45
use crate::{AcmeState, Incoming};
56
use core::fmt;
@@ -21,6 +22,12 @@ pub struct AcmeConfig<EC: Debug, EA: Debug = EC> {
2122
pub(crate) domains: Vec<String>,
2223
pub(crate) contact: Vec<String>,
2324
pub(crate) cache: Box<dyn Cache<EC = EC, EA = EA>>,
25+
pub(crate) challenge_type: UseChallenge,
26+
}
27+
28+
pub enum UseChallenge {
29+
Http01,
30+
TlsAlpn01,
2431
}
2532

2633
impl AcmeConfig<Infallible, Infallible> {
@@ -78,6 +85,7 @@ impl AcmeConfig<Infallible, Infallible> {
7885
domains: domains.into_iter().map(|s| s.as_ref().into()).collect(),
7986
contact: vec![],
8087
cache: Box::new(NoCache::default()),
88+
challenge_type: TlsAlpn01,
8189
}
8290
}
8391
}
@@ -132,6 +140,7 @@ impl<EC: 'static + Debug, EA: 'static + Debug> AcmeConfig<EC, EA> {
132140
domains: self.domains,
133141
contact: self.contact,
134142
cache: Box::new(cache),
143+
challenge_type: self.challenge_type,
135144
}
136145
}
137146
pub fn cache_compose<CC: 'static + CertCache, CA: 'static + AccountCache>(self, cert_cache: CC, account_cache: CA) -> AcmeConfig<CC::EC, CA::EA> {
@@ -146,6 +155,10 @@ impl<EC: 'static + Debug, EA: 'static + Debug> AcmeConfig<EC, EA> {
146155
None => self.cache(NoCache::<C::EC, C::EA>::default()),
147156
}
148157
}
158+
pub fn challenge_type(mut self, challenge_type: UseChallenge) -> Self {
159+
self.challenge_type = challenge_type;
160+
self
161+
}
149162
pub fn state(self) -> AcmeState<EC, EA> {
150163
AcmeState::new(self)
151164
}

src/jose.rs

+6-2
Original file line numberDiff line numberDiff line change
@@ -23,9 +23,13 @@ pub(crate) fn sign(key: &EcdsaKeyPair, kid: Option<&str>, nonce: String, url: &s
2323
Ok(serde_json::to_string(&body)?)
2424
}
2525

26-
pub(crate) fn key_authorization_sha256(key: &EcdsaKeyPair, token: &str) -> Result<Digest, JoseError> {
26+
pub(crate) fn key_authorization(key: &EcdsaKeyPair, token: &str) -> Result<String, JoseError> {
2727
let jwk = Jwk::new(key);
28-
let key_authorization = format!("{}.{}", token, jwk.thumb_sha256_base64()?);
28+
Ok(format!("{}.{}", token, jwk.thumb_sha256_base64()?))
29+
}
30+
31+
pub(crate) fn key_authorization_sha256(key: &EcdsaKeyPair, token: &str) -> Result<Digest, JoseError> {
32+
let key_authorization = key_authorization(key, token)?;
2933
Ok(digest(&SHA256, key_authorization.as_bytes()))
3034
}
3135

src/lib.rs

+2
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,8 @@ mod resolver;
122122
mod state;
123123
#[cfg(feature = "tokio")]
124124
pub mod tokio;
125+
#[cfg(feature = "tower")]
126+
pub mod tower;
125127

126128
pub use futures_rustls;
127129

src/resolver.rs

+40-12
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,12 @@
1+
use crate::is_tls_alpn_challenge;
12
use futures_rustls::rustls::{
23
server::{ClientHello, ResolvesServerCert},
34
sign::CertifiedKey,
45
};
5-
use std::collections::BTreeMap;
6+
use std::fmt::Debug;
67
use std::sync::Arc;
78
use std::sync::Mutex;
89

9-
use crate::is_tls_alpn_challenge;
10-
1110
#[derive(Debug)]
1211
pub struct ResolvesServerCertAcme {
1312
inner: Mutex<Inner>,
@@ -16,23 +15,47 @@ pub struct ResolvesServerCertAcme {
1615
#[derive(Debug)]
1716
struct Inner {
1817
cert: Option<Arc<CertifiedKey>>,
19-
auth_keys: BTreeMap<String, Arc<CertifiedKey>>,
18+
challenge_data: Option<ChallengeData>,
19+
}
20+
21+
#[derive(Debug)]
22+
enum ChallengeData {
23+
TlsAlpn01 { sni: String, cert: Arc<CertifiedKey> },
24+
Http01 { token: String, key_auth: String },
2025
}
2126

2227
impl ResolvesServerCertAcme {
2328
pub(crate) fn new() -> Arc<Self> {
2429
Arc::new(Self {
2530
inner: Mutex::new(Inner {
2631
cert: None,
27-
auth_keys: Default::default(),
32+
challenge_data: None,
2833
}),
2934
})
3035
}
3136
pub(crate) fn set_cert(&self, cert: Arc<CertifiedKey>) {
3237
self.inner.lock().unwrap().cert = Some(cert);
3338
}
34-
pub(crate) fn set_auth_key(&self, domain: String, cert: Arc<CertifiedKey>) {
35-
self.inner.lock().unwrap().auth_keys.insert(domain, cert);
39+
pub(crate) fn set_tls_alpn_01_challenge_data(&self, domain: String, cert: Arc<CertifiedKey>) {
40+
self.inner.lock().unwrap().challenge_data = Some(ChallengeData::TlsAlpn01 { sni: domain, cert });
41+
}
42+
pub(crate) fn set_http_01_challenge_data(&self, token: String, key_auth: String) {
43+
self.inner.lock().unwrap().challenge_data = Some(ChallengeData::Http01 { token, key_auth })
44+
}
45+
pub(crate) fn clear_challenge_data(&self) {
46+
self.inner.lock().unwrap().challenge_data = None;
47+
}
48+
pub fn get_http_01_key_auth(&self, challenge_token: &str) -> Option<String> {
49+
match &self.inner.lock().unwrap().challenge_data {
50+
Some(ChallengeData::Http01 { token, key_auth }) => {
51+
if token == challenge_token {
52+
Some(key_auth.clone())
53+
} else {
54+
None
55+
}
56+
}
57+
_ => None,
58+
}
3659
}
3760
}
3861

@@ -45,11 +68,16 @@ impl ResolvesServerCert for ResolvesServerCertAcme {
4568
log::debug!("client did not supply SNI");
4669
None
4770
}
48-
Some(domain) => {
49-
let domain = domain.to_owned();
50-
let domain: String = AsRef::<str>::as_ref(&domain).into();
51-
self.inner.lock().unwrap().auth_keys.get(&domain).cloned()
52-
}
71+
Some(domain) => match &self.inner.lock().unwrap().challenge_data {
72+
Some(ChallengeData::TlsAlpn01 { sni, cert }) => {
73+
if sni == domain {
74+
Some(cert.clone())
75+
} else {
76+
None
77+
}
78+
}
79+
_ => None,
80+
},
5381
}
5482
} else {
5583
self.inner.lock().unwrap().cert.clone()

0 commit comments

Comments
 (0)