use super::*;
use crate::sqlite::status::OrderStatus;
async fn spawn_responder(tokens: Arc<dyn http01::TokenStore>) -> String {
let app = axum::Router::new()
.route(
"/.well-known/acme-challenge/{token}",
axum::routing::get(crate::handlers::get_challenge_file),
)
.with_state(crate::handlers::Http01Stores(Arc::new(vec![tokens])));
let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
let base = format!("http://127.0.0.1:{}", listener.local_addr().unwrap().port());
tokio::spawn(async move { axum::serve(listener, app).await.unwrap() });
base
}
#[tokio::test(flavor = "multi_thread")]
async fn http01_serves_the_key_authorization_triggers_and_retracts() {
let tokens = Arc::new(StubTokens::default());
let responder = spawn_responder(tokens.clone()).await;
let upstream = testsrv::start(Script {
chain: real_chain().await,
pose_challenge: true,
offer_http01: true,
http01_responder: Some(responder),
..Script::default()
})
.await;
let dir = TempDir::new("upstream");
let db = database().await;
let queue = test_queue(db.clone());
let signer = with_tokens(
RelaySigner::from_config(
&config(&upstream, &dir),
&relay_parts(db.clone(), no_notifiers(), queue.clone()),
&crate::signer::CarriedState::new(),
)
.unwrap(),
tokens.clone(),
);
let _runner = TestRunner::start(queue, &signer);
let order = ready_order(db.clone()).await;
signer
.issue(
order.id.to_string().as_str(),
&csr_der(),
&identifiers(),
RequestedValidity::default(),
)
.await
.unwrap();
await_status(db, order.id.to_string().as_str(), OrderStatus::Valid).await;
assert_eq!(upstream.challenge_triggered(), 1);
let thumbprint = crate::extractors::acme::jwk_thumbprint(signer.0.account.spki_der()).unwrap();
assert_eq!(
upstream.http01_body(),
Some(format!("{}.{thumbprint}", testsrv::CHALLENGE_TOKEN)),
"the upstream must have read the exact key authorization off the responder"
);
let published = tokens.published.lock().unwrap().clone();
assert_eq!(
published,
vec![(
testsrv::CHALLENGE_TOKEN.to_string(),
format!("{}.{thumbprint}", testsrv::CHALLENGE_TOKEN),
)]
);
assert_eq!(
tokens.retracted.lock().unwrap().clone(),
vec![testsrv::CHALLENGE_TOKEN.to_string()],
"the token must not stay fetchable after the attempt"
);
}
#[tokio::test(flavor = "multi_thread")]
async fn http01_retracts_after_a_rejected_challenge() {
let tokens = Arc::new(StubTokens::default());
let responder = spawn_responder(tokens.clone()).await;
let upstream = testsrv::start(Script {
pose_challenge: true,
offer_http01: true,
fail_challenge: true,
http01_responder: Some(responder),
..Script::default()
})
.await;
let dir = TempDir::new("upstream");
let db = database().await;
let queue = test_queue(db.clone());
let signer = with_tokens(
RelaySigner::from_config(
&config(&upstream, &dir),
&relay_parts(db.clone(), no_notifiers(), queue.clone()),
&crate::signer::CarriedState::new(),
)
.unwrap(),
tokens.clone(),
);
let _runner = TestRunner::start(queue, &signer);
let order = ready_order(db.clone()).await;
signer
.issue(
order.id.to_string().as_str(),
&csr_der(),
&identifiers(),
RequestedValidity::default(),
)
.await
.unwrap();
await_status(db, order.id.to_string().as_str(), OrderStatus::Invalid).await;
assert_eq!(
tokens.retracted.lock().unwrap().len(),
1,
"a failed attempt must still retract its token"
);
assert!(
tokens.lookup(testsrv::CHALLENGE_TOKEN).is_none(),
"nothing must remain fetchable"
);
}
#[tokio::test(flavor = "multi_thread")]
async fn http01_refuses_an_upstream_offering_only_dns01() {
let upstream = testsrv::start(Script {
pose_challenge: true,
..Script::default()
})
.await;
let dir = TempDir::new("upstream");
let db = database().await;
let queue = test_queue(db.clone());
let signer = with_tokens(
RelaySigner::from_config(
&config(&upstream, &dir),
&relay_parts(db.clone(), no_notifiers(), queue.clone()),
&crate::signer::CarriedState::new(),
)
.unwrap(),
Arc::new(StubTokens::default()),
);
let _runner = TestRunner::start(queue, &signer);
let order = ready_order(db.clone()).await;
signer
.issue(
order.id.to_string().as_str(),
&csr_der(),
&identifiers(),
RequestedValidity::default(),
)
.await
.unwrap();
await_status(
db.clone(),
order.id.to_string().as_str(),
OrderStatus::Invalid,
)
.await;
let mapping = UpstreamOrder::find_by_order_id(order.id.to_string().as_str(), &db)
.await
.unwrap()
.unwrap();
assert!(
mapping.error.unwrap().contains("no http-01"),
"the reason must name what was missing"
);
assert_eq!(upstream.challenge_triggered(), 0);
}
#[tokio::test(flavor = "multi_thread")]
async fn http01_refuses_a_wildcard_authorization() {
let upstream = testsrv::start(Script {
pose_challenge: true,
offer_http01: true,
wildcard_identifier: true,
..Script::default()
})
.await;
let dir = TempDir::new("upstream");
let db = database().await;
let queue = test_queue(db.clone());
let signer = with_tokens(
RelaySigner::from_config(
&config(&upstream, &dir),
&relay_parts(db.clone(), no_notifiers(), queue.clone()),
&crate::signer::CarriedState::new(),
)
.unwrap(),
Arc::new(StubTokens::default()),
);
let _runner = TestRunner::start(queue, &signer);
let order = ready_order(db.clone()).await;
signer
.issue(
order.id.to_string().as_str(),
&csr_der(),
&identifiers(),
RequestedValidity::default(),
)
.await
.unwrap();
await_status(
db.clone(),
order.id.to_string().as_str(),
OrderStatus::Invalid,
)
.await;
let mapping = UpstreamOrder::find_by_order_id(order.id.to_string().as_str(), &db)
.await
.unwrap()
.unwrap();
let error = mapping.error.unwrap();
assert!(
error.contains("wildcard") && error.contains("dns01"),
"the reason must name the wildcard and point at the strategy that can: {error}"
);
assert_eq!(upstream.challenge_triggered(), 0);
}
#[tokio::test(flavor = "multi_thread")]
async fn an_unknown_dns_provider_is_a_startup_error() {
let upstream = testsrv::start(Script::default()).await;
let dir = TempDir::new("upstream");
let mut cfg = config(&upstream, &dir);
cfg.challenge_strategy = "dns01".to_string();
cfg.dns01.provider = "route53".to_string();
let error = startup_error(RelaySigner::from_config(
&cfg,
&relay_parts(
database().await,
no_notifiers(),
test_queue(database().await),
),
&crate::signer::CarriedState::new(),
));
assert!(error.contains("route53"), "{error}");
}
#[tokio::test(flavor = "multi_thread")]
async fn a_reload_carries_the_published_key_authorizations_across() {
let upstream = testsrv::start(Script::default()).await;
let dir = TempDir::new("upstream");
let mut cfg = config(&upstream, &dir);
cfg.challenge_strategy = "http01".to_string();
let parts = relay_parts(
database().await,
no_notifiers(),
test_queue(database().await),
);
let build = |cfg: &RelayConfig, carried: &crate::signer::CarriedState| {
RelaySigner::from_config(cfg, &parts, carried).unwrap()
};
let running = build(&cfg, &crate::signer::CarriedState::new());
running
.http01_tokens()
.expect("the http01 strategy has a store")
.publish("tok", "tok.thumbprint");
let mut edited = cfg;
edited.poll_interval_ms += 1;
let reloaded = build(&edited, &running.carried_state());
assert_eq!(
reloaded
.http01_tokens()
.expect("still the http01 strategy")
.lookup("tok"),
Some("tok.thumbprint".to_string()),
"a fetch already in flight must still be answerable after the reload",
);
let mut bypassing = edited.clone();
bypassing.challenge_strategy = "bypass".to_string();
let intermediate = build(&bypassing, &reloaded.carried_state());
assert!(intermediate.http01_tokens().is_none());
let back = build(&edited, &intermediate.carried_state());
assert!(
back.http01_tokens()
.expect("http01 again")
.lookup("tok")
.is_none(),
"nothing was live to carry: the intermediate backend served no tokens",
);
}
#[tokio::test(flavor = "multi_thread")]
async fn http01_answers_past_a_challenge_type_carrying_no_token() {
let tokens = Arc::new(StubTokens::default());
let responder = spawn_responder(tokens.clone()).await;
let upstream = testsrv::start(Script {
chain: real_chain().await,
pose_challenge: true,
offer_http01: true,
offer_tokenless_challenge: true,
http01_responder: Some(responder),
..Script::default()
})
.await;
let dir = TempDir::new("upstream");
let db = database().await;
let queue = test_queue(db.clone());
let signer = with_tokens(
RelaySigner::from_config(
&config(&upstream, &dir),
&relay_parts(db.clone(), no_notifiers(), queue.clone()),
&crate::signer::CarriedState::new(),
)
.unwrap(),
tokens.clone(),
);
let _runner = TestRunner::start(queue, &signer);
let order = ready_order(db.clone()).await;
signer
.issue(
order.id.to_string().as_str(),
&csr_der(),
&identifiers(),
RequestedValidity::default(),
)
.await
.unwrap();
await_status(db, order.id.to_string().as_str(), OrderStatus::Valid).await;
let thumbprint = crate::extractors::acme::jwk_thumbprint(signer.0.account.spki_der()).unwrap();
assert_eq!(
upstream.http01_body(),
Some(format!("{}.{thumbprint}", testsrv::CHALLENGE_TOKEN)),
"the body served must come from the http-01 token, not the entry beside it"
);
assert_eq!(upstream.tokenless_triggered(), 0);
}