Skip to content

Commit

Permalink
Fix #493: Expose ACME structures for user-managed certificate renewal (
Browse files Browse the repository at this point in the history
…#494)

* Expose methods for explicit certificate renewal

* Notes

* Style fixes

* Use struct return from issue cert to prevent mixing up priv and pub keys

* update examples

---------

Co-authored-by: andrew <>
Co-authored-by: Sunli <[email protected]>
Co-authored-by: Sunli <[email protected]>
  • Loading branch information
3 people committed Jun 22, 2023
1 parent a04b7e7 commit 5d8048c
Show file tree
Hide file tree
Showing 9 changed files with 298 additions and 94 deletions.
10 changes: 10 additions & 0 deletions examples/poem/acme-expanded-http-01/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
[package]
name = "example-acme-expanded-http-01"
version = "0.1.0"
edition = "2021"
publish = false

[dependencies]
poem = { path = "../../../poem", features = ["acme"] }
tokio = { version = "1.12.0", features = ["rt-multi-thread", "macros"] }
tracing-subscriber = { version = "0.3.9", features = ["env-filter"] }
92 changes: 92 additions & 0 deletions examples/poem/acme-expanded-http-01/src/main.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
//! If you want to manage certificates yourself (sharing between servers,
//! sending over the network, etc) you can use this expanded ACME
//! certificate generation process which gives you access to the
//! generated certificates.
use std::{sync::Arc, time::Duration};

use poem::{
get, handler,
listener::{
acme::{
issue_cert, seconds_until_expiry, AcmeClient, ChallengeType, Http01Endpoint,
Http01TokensMap, ResolveServerCert, ResolvedCertListener, LETS_ENCRYPT_PRODUCTION,
},
Listener, TcpListener,
},
middleware::Tracing,
web::Path,
EndpointExt, Route, RouteScheme, Server,
};
use tokio::{spawn, time::sleep};

#[handler]
fn hello(Path(name): Path<String>) -> String {
format!("hello: {}", name)
}

#[tokio::main]
async fn main() -> Result<(), std::io::Error> {
if std::env::var_os("RUST_LOG").is_none() {
std::env::set_var("RUST_LOG", "poem=debug");
}
tracing_subscriber::fmt::init();

let mut acme_client =
AcmeClient::try_new(&LETS_ENCRYPT_PRODUCTION.parse().unwrap(), vec![]).await?;
let cert_resolver = Arc::new(ResolveServerCert::default());
let challenge = ChallengeType::Http01;
let keys_for_http_challenge = Http01TokensMap::new();

{
let domains = vec!["poem.rs".to_string()];
let keys_for_http_challenge = keys_for_http_challenge.clone();
let cert_resolver = Arc::downgrade(&cert_resolver);
spawn(async move {
loop {
let sleep_duration;
if let Some(cert_resolver) = cert_resolver.upgrade() {
let cert = match issue_cert(
&mut acme_client,
&cert_resolver,
&domains,
challenge,
Some(&keys_for_http_challenge),
)
.await
{
Ok(result) => result.rustls_key,
Err(err) => {
eprintln!("failed to issue certificate: {}", err);
sleep(Duration::from_secs(60 * 5)).await;
continue;
}
};
sleep_duration = seconds_until_expiry(&cert) - 12 * 60 * 60;
*cert_resolver.cert.write() = Some(cert);
} else {
break;
}
sleep(Duration::from_secs(sleep_duration as u64)).await;
}
});
}

let app = RouteScheme::new()
.https(Route::new().at("/hello/:name", get(hello)))
.http(Http01Endpoint {
keys: keys_for_http_challenge,
})
.with(Tracing);

Server::new(
ResolvedCertListener::new(
TcpListener::bind("0.0.0.0:443"),
cert_resolver,
ChallengeType::Http01,
)
.combine(TcpListener::bind("0.0.0.0:80")),
)
.name("hello-world")
.run(app)
.await
}
8 changes: 2 additions & 6 deletions poem/src/listener/acme/auto_cert.rs
Original file line number Diff line number Diff line change
@@ -1,25 +1,21 @@
use std::{
collections::HashMap,
fmt::{self, Debug, Formatter},
path::PathBuf,
sync::Arc,
};

use http::Uri;
use parking_lot::RwLock;

use crate::listener::acme::{
builder::AutoCertBuilder, endpoint::Http01Endpoint, keypair::KeyPair, ChallengeType,
builder::AutoCertBuilder, endpoint::Http01Endpoint, ChallengeType, Http01TokensMap,
};

/// ACME configuration
pub struct AutoCert {
pub(crate) directory_url: Uri,
pub(crate) domains: Vec<String>,
pub(crate) contacts: Vec<String>,
pub(crate) key_pair: Arc<KeyPair>,
pub(crate) challenge_type: ChallengeType,
pub(crate) keys_for_http01: Option<Arc<RwLock<HashMap<String, String>>>>,
pub(crate) keys_for_http01: Option<Http01TokensMap>,
pub(crate) cache_path: Option<PathBuf>,
pub(crate) cache_cert: Option<Vec<u8>>,
pub(crate) cache_key: Option<Vec<u8>>,
Expand Down
4 changes: 1 addition & 3 deletions poem/src/listener/acme/builder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,9 @@ use std::{
collections::HashSet,
io::{Error as IoError, ErrorKind, Result as IoResult},
path::PathBuf,
sync::Arc,
};

use crate::listener::acme::{keypair::KeyPair, AutoCert, ChallengeType, LETS_ENCRYPT_PRODUCTION};
use crate::listener::acme::{AutoCert, ChallengeType, LETS_ENCRYPT_PRODUCTION};

/// ACME configuration builder
pub struct AutoCertBuilder {
Expand Down Expand Up @@ -109,7 +108,6 @@ impl AutoCertBuilder {
directory_url,
domains: self.domains.into_iter().collect(),
contacts: self.contacts.into_iter().collect(),
key_pair: Arc::new(KeyPair::generate()?),
challenge_type: self.challenge_type,
keys_for_http01: match self.challenge_type {
ChallengeType::Http01 => Some(Default::default()),
Expand Down
22 changes: 12 additions & 10 deletions poem/src/listener/acme/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,20 +21,19 @@ use crate::{
Body,
};

pub(crate) struct AcmeClient {
/// A client for ACME-supporting TLS certificate services.
pub struct AcmeClient {
client: Client<HttpsConnector<HttpConnector>>,
directory: Directory,
key_pair: Arc<KeyPair>,
pub(crate) key_pair: Arc<KeyPair>,
contacts: Vec<String>,
kid: Option<String>,
}

impl AcmeClient {
pub(crate) async fn try_new(
directory_url: &Uri,
key_pair: Arc<KeyPair>,
contacts: Vec<String>,
) -> IoResult<Self> {
/// Create a new client. `directory_url` is the url for the ACME provider. `contacts` is a list
/// of URLS (ex: `mailto:`) the ACME service can use to reach you if there's issues with your certificates.
pub async fn try_new(directory_url: &Uri, contacts: Vec<String>) -> IoResult<Self> {
let client_builder = HttpsConnectorBuilder::new();
#[cfg(feature = "acme-native-roots")]
let client_builder1 = client_builder.with_native_roots();
Expand All @@ -46,13 +45,16 @@ impl AcmeClient {
Ok(Self {
client,
directory,
key_pair,
key_pair: Arc::new(KeyPair::generate()?),
contacts,
kid: None,
})
}

pub(crate) async fn new_order(&mut self, domains: &[String]) -> IoResult<NewOrderResponse> {
pub(crate) async fn new_order<T: AsRef<str>>(
&mut self,
domains: &[T],
) -> IoResult<NewOrderResponse> {
let kid = match &self.kid {
Some(kid) => kid,
None => {
Expand Down Expand Up @@ -83,7 +85,7 @@ impl AcmeClient {
.iter()
.map(|domain| Identifier {
ty: "dns".to_string(),
value: domain.to_string(),
value: domain.as_ref().to_string(),
})
.collect(),
}),
Expand Down
36 changes: 32 additions & 4 deletions poem/src/listener/acme/endpoint.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,38 @@ use parking_lot::RwLock;

use crate::{error::NotFoundError, Endpoint, IntoResponse, Request, Response, Result};

/// A tokens storage for http01 challenge
#[derive(Debug, Clone, Default)]
pub struct Http01TokensMap(Arc<RwLock<HashMap<String, String>>>);

impl Http01TokensMap {
/// Create a new http01 challenge tokens storage for use in challenge endpoint
/// and [`issue_cert`].
#[inline]
pub fn new() -> Self {
Self::default()
}

/// Inserts an entry to the storage
pub fn insert(&self, token: impl Into<String>, authorization: impl Into<String>) {
self.0.write().insert(token.into(), authorization.into());
}

/// Removes an entry from the storage
pub fn remove(&self, token: impl AsRef<str>) {
self.0.write().remove(token.as_ref());
}

/// Gets the authorization by token
pub fn get(&self, token: impl AsRef<str>) -> Option<String> {
self.0.read().get(token.as_ref()).cloned()
}
}

/// An endpoint for `HTTP-01` challenge.
pub struct Http01Endpoint {
pub(crate) keys: Arc<RwLock<HashMap<String, String>>>,
/// Challenge keys for http01 domain verification.
pub keys: Http01TokensMap,
}

#[async_trait::async_trait]
Expand All @@ -19,9 +48,8 @@ impl Endpoint for Http01Endpoint {
.path()
.strip_prefix("/.well-known/acme-challenge/")
{
let keys = self.keys.read();
if let Some(value) = keys.get(token) {
return Ok(value.clone().into_response());
if let Some(value) = self.keys.get(token) {
return Ok(value.into_response());
}
}

Expand Down
Loading

0 comments on commit 5d8048c

Please sign in to comment.