Skip to content

ACME: define and allocate shared data structures. #12

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

Merged
merged 4 commits into from
Jul 30, 2025
Merged
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
8 changes: 8 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,13 @@ crate-type = ["cdylib"]

[dependencies]
http = "1.3.1"
libc = "0.2.174"
openssl = { version = "0.10.73", features = ["bindgen"] }
openssl-foreign-types = { package = "foreign-types", version = "0.3" }
openssl-sys = { version = "0.9.109", features = ["bindgen"] }
siphasher = { version = "1.0.1", default-features = false }
thiserror = { version = "2.0.12", default-features = false }
zeroize = "1.8.1"

[dependencies.nginx-sys]
git = "https://github.com/nginx/ngx-rust"
Expand Down
11 changes: 3 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -218,7 +218,7 @@ challenge data for all the configured certificate issuers.

### acme_certificate

**Syntax:** acme_certificate `issuer` [`identifier` ...] [ `key` = `alg[:size]` | `file` ]
**Syntax:** acme_certificate `issuer` [`identifier` ...] [ `key` = `alg[:size]` ]

**Default:** -

Expand All @@ -234,17 +234,12 @@ regular expressions and wildcards are not supported.

[server_name]: https://nginx.org/en/docs/http/ngx_http_core_module.html#server_name

The `key` parameter sets the type of generated private key or a
path to an existing file. Supported key algorithms and sizes:
The `key` parameter sets the type of a generated private key. Supported key
algorithms and sizes:
`ecdsa:256` (default), `ecdsa:384`,
`ecdsa:521`,
`rsa:2048` .. `rsa:4096`.

> Since 1.27.2, the `key` parameter supports the additional schemes implemented in the
> [ssl_certificate_key](https://nginx.org/en/docs/http/ngx_http_ssl_module.html#ssl_certificate_key)
> directive: `data:` , `engine:` and more recently `store:` ,
> with a caveat that password-protected keys are not supported.

## Embedded Variables

The `ngx_http_acme_module` module defines following embedded
Expand Down
31 changes: 31 additions & 0 deletions build.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ use std::env;
/// [1]: https://github.com/rust-lang/cargo/issues/3544
fn main() {
detect_nginx_features();
detect_libssl_features();

// Generate required compiler flags
if cfg!(target_os = "macos") {
Expand Down Expand Up @@ -57,3 +58,33 @@ fn detect_nginx_features() {
}
}
}

/// Detects libssl implementation and version.
fn detect_libssl_features() {
// OpenSSL
let openssl_features = ["awslc", "boringssl", "libressl", "openssl", "openssl111"];
let openssl_version = env::var("DEP_OPENSSL_VERSION_NUMBER").unwrap_or_default();
let openssl_version = u64::from_str_radix(&openssl_version, 16).unwrap_or(0);

println!(
"cargo::rustc-check-cfg=cfg(openssl, values(\"{}\"))",
openssl_features.join("\",\"")
);

#[allow(clippy::unusual_byte_groupings)]
let openssl = if env::var("DEP_OPENSSL_AWSLC").is_ok() {
"awslc"
} else if env::var("DEP_OPENSSL_BORINGSSL").is_ok() {
"boringssl"
} else if env::var("DEP_OPENSSL_LIBRESSL").is_ok() {
"libressl"
} else {
if openssl_version >= 0x01_01_01_00_0 {
println!("cargo::rustc-cfg=openssl=\"openssl111\"");
}

"openssl"
};

println!("cargo::rustc-cfg=openssl=\"{openssl}\"");
}
6 changes: 6 additions & 0 deletions src/conf.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ use self::issuer::Issuer;
use self::order::CertificateOrder;
use self::pkey::PrivateKey;
use self::shared_zone::{SharedZone, ACME_ZONE_NAME, ACME_ZONE_SIZE};
use crate::state::AcmeSharedData;

pub mod ext;
pub mod identifier;
Expand All @@ -31,6 +32,7 @@ const NGX_CONF_DUPLICATE: *mut c_char = c"is duplicate".as_ptr().cast_mut();
#[derive(Debug, Default)]
pub struct AcmeMainConfig {
pub issuers: Vec<Issuer>,
pub data: Option<&'static AcmeSharedData>,
pub shm_zone: shared_zone::SharedZone,
}

Expand Down Expand Up @@ -284,6 +286,7 @@ extern "C" fn cmd_add_certificate(
for value in &args[2..] {
if let Some(key) = value.strip_prefix(b"key=") {
order.key = match PrivateKey::try_from(key) {
Ok(PrivateKey::File(_)) => return c"invalid \"key\" value".as_ptr().cast_mut(),
Ok(val) => val,
Err(err) => return cf.error(args[0], &err),
};
Expand Down Expand Up @@ -499,7 +502,10 @@ impl AcmeMainConfig {
self.shm_zone = SharedZone::Configured(ACME_ZONE_NAME, ACME_ZONE_SIZE);
}

let amcfp = ptr::from_mut(self).cast();
let shm_zone = self.shm_zone.request(cf)?;
shm_zone.init = Some(crate::state::ngx_acme_shared_zone_init);
shm_zone.data = amcfp;
shm_zone.noreuse = 1;

Ok(())
Expand Down
104 changes: 101 additions & 3 deletions src/conf/issuer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,19 @@ use ngx::collections::{RbTreeMap, Vec};
use ngx::core::{Pool, Status};
use ngx::http::{HttpModuleLocationConf, NgxHttpCoreModule};
use ngx::ngx_log_debug;
use ngx::sync::RwLock;
use openssl::pkey::{PKey, Private};
use thiserror::Error;
use zeroize::Zeroizing;

use super::ext::NgxConfExt;
use super::order::CertificateOrder;
use super::pkey::PrivateKey;
use super::ssl::NgxSsl;
use super::AcmeMainConfig;
use crate::state::certificate::{CertificateContext, CertificateContextInner};
use crate::state::issuer::IssuerContext;
use crate::time::{Time, TimeRange};

const ACCOUNT_KEY_FILE: &str = "account.key";
const NGX_ACME_DEFAULT_RESOLVER_TIMEOUT: ngx_msec_t = 30000;
Expand All @@ -41,8 +47,9 @@ pub struct Issuer {
// Generated fields
// ngx_ssl_t stores a pointer to itself in SSL_CTX ex_data.
pub ssl: Box<NgxSsl, Pool>,
pub orders: RbTreeMap<CertificateOrder<ngx_str_t, Pool>, (), Pool>,
pub orders: RbTreeMap<CertificateOrder<ngx_str_t, Pool>, CertificateContext, Pool>,
pub pkey: Option<PKey<Private>>,
pub data: Option<&'static RwLock<IssuerContext>>,
}

#[derive(Debug, Error)]
Expand Down Expand Up @@ -83,6 +90,7 @@ impl Issuer {
ssl,
pkey: None,
orders: RbTreeMap::try_new_in(alloc)?,
data: None,
})
}

Expand Down Expand Up @@ -158,7 +166,34 @@ impl Issuer {
self.name
);

if self.orders.try_insert(order.clone(), ()).is_err() {
let mut cert = CertificateContext::Empty;

if let Some(state_dir) = unsafe { StateDir::from_ptr(self.state_path) } {
match state_dir.load_certificate(cf, order) {
Ok(x) => {
ngx_log_debug!(
cf.log,
"acme: found cached certificate {}/{}, next update in {:?}",
self.name,
order.cache_key(),
(x.next - Time::now()),
);
cert = CertificateContext::Local(x);
}
Err(CachedCertificateError::NotFound) => (),
Err(err) => {
ngx_log_debug!(
cf.log,
"acme: cannot load certificate {}/{} from state path: {}",
self.name,
order.cache_key(),
err
);
}
}
}

if self.orders.try_insert(order.clone(), cert).is_err() {
return Err(Status::NGX_ERROR);
}
} else {
Expand Down Expand Up @@ -222,7 +257,7 @@ impl Issuer {
}
}

if let Ok(buf) = pkey.private_key_to_pem_pkcs8() {
if let Ok(buf) = pkey.private_key_to_pem_pkcs8().map(Zeroizing::new) {
// Ignore write errors.
let _ = state_dir.write(&path, &buf);
}
Expand All @@ -232,6 +267,20 @@ impl Issuer {
}
}

#[derive(Debug, thiserror::Error)]
enum CachedCertificateError {
#[error(transparent)]
Alloc(#[from] AllocError),
#[error("X509_check_private_key() failed: {0}")]
Mismatch(openssl::error::ErrorStack),
#[error("file not found")]
NotFound,
#[error(transparent)]
Ssl(#[from] openssl::error::ErrorStack),
#[error("failed to load file: {0}")]
CertificateFetch(#[from] super::ssl::CertificateFetchError),
}

/// The StateDir helper encapsulates operations with a persistent state in the state directory.
#[repr(transparent)]
struct StateDir(ngx_path_t);
Expand All @@ -257,4 +306,53 @@ impl StateDir {
pub fn write(&self, path: &std::path::Path, data: &[u8]) -> Result<(), std::io::Error> {
std::fs::write(path, data)
}

pub fn load_certificate(
&self,
cf: &mut ngx_conf_t,
order: &CertificateOrder<ngx_str_t, Pool>,
) -> Result<CertificateContextInner<Pool>, CachedCertificateError> {
use openssl_foreign_types::ForeignType;
#[cfg(ngx_ssl_cache)]
use openssl_foreign_types::ForeignTypeRef;

let name = order.cache_key();

let cert = std::format!("{}/{}.crt", self.0.name, name);
if matches!(std::fs::exists(&cert), Ok(false)) {
return Err(CachedCertificateError::NotFound);
}

let key = std::format!("{}/{}.key", self.0.name, name);
if matches!(std::fs::exists(&key), Ok(false)) {
return Err(CachedCertificateError::NotFound);
}

let stack = super::ssl::conf_read_certificate(cf, &cert)?;
#[allow(clippy::get_first)] // ^ can return Stack or Vec, depending on the NGINX version
let cert = stack
.get(0)
.ok_or(super::ssl::CertificateFetchError::Fetch(c"no certificates"))?;
let pkey = super::ssl::conf_read_private_key(cf, &key)?;

if unsafe { openssl_sys::X509_check_private_key(cert.as_ptr(), pkey.as_ptr()) } != 1 {
return Err(CachedCertificateError::Mismatch(
openssl::error::ErrorStack::get(),
));
}

let valid = TimeRange::from_x509(cert).unwrap_or_default();
let temp_alloc = unsafe { Pool::from_ngx_pool(cf.temp_pool) };

let mut chain: Vec<u8, Pool> = Vec::new_in(temp_alloc.clone());
for x509 in stack.into_iter() {
chain.extend(x509.to_pem()?.into_iter());
}

let mut cert = CertificateContextInner::new_in(cf.pool());
let pkey = Zeroizing::new(pkey.private_key_to_pem_pkcs8()?);
cert.set(&chain, &pkey, valid)?;

Ok(cert)
}
}
9 changes: 8 additions & 1 deletion src/conf/shared_zone.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ use core::ffi::c_void;
use core::ptr::{self, NonNull};

use nginx_sys::{ngx_conf_t, ngx_int_t, ngx_shm_zone_t, ngx_str_t, NGX_ERROR};
use ngx::core::Status;
use ngx::core::{SlabPool, Status};
use ngx::http::HttpModule;
use ngx::log::ngx_cycle_log;
use ngx::{ngx_log_debug, ngx_string};
Expand Down Expand Up @@ -32,6 +32,13 @@ pub enum SharedZoneError {
}

impl SharedZone {
pub fn allocator(&self) -> Option<SlabPool> {
match self {
Self::Ready(zone) => unsafe { SlabPool::from_shm_zone(zone.as_ref()) },
_ => None,
}
}

pub fn is_configured(&self) -> bool {
!matches!(self, Self::Unset)
}
Expand Down
27 changes: 26 additions & 1 deletion src/conf/ssl.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ use nginx_sys::{
use ngx::allocator::AllocError;
use ngx::core::Status;
use openssl::pkey::{PKey, Private};
use openssl::x509::X509;
use openssl_sys::SSL_CTX_set_default_verify_paths;
use thiserror::Error;

Expand All @@ -21,6 +22,30 @@ pub enum CertificateFetchError {
#[error("{0:?} {1}")]
Ssl(&'static CStr, openssl::error::ErrorStack),
}

#[cfg(ngx_ssl_cache)]
pub fn conf_read_certificate(
cf: &mut ngx_conf_t,
name: &str,
) -> Result<openssl::stack::Stack<X509>, CertificateFetchError> {
conf_ssl_cache_fetch(cf, nginx_sys::NGX_SSL_CACHE_CERT as _, name)
}

#[cfg(not(ngx_ssl_cache))]
pub fn conf_read_certificate(
_cf: &mut ngx_conf_t,
name: &str,
) -> Result<std::vec::Vec<X509>, CertificateFetchError> {
let Ok(buf) = std::fs::read_to_string(name) else {
return Err(CertificateFetchError::Fetch(c"cannot load certificate"));
};

match X509::stack_from_pem(buf.as_bytes()) {
Ok(x) => Ok(x),
Err(err) => Err(CertificateFetchError::Ssl(c"cannot load key", err)),
}
}

#[cfg(ngx_ssl_cache)]
pub fn conf_read_private_key(
cf: &mut ngx_conf_t,
Expand All @@ -34,7 +59,7 @@ pub fn conf_read_private_key(
_cf: &mut ngx_conf_t,
name: &str,
) -> Result<PKey<Private>, CertificateFetchError> {
let Ok(buf) = std::fs::read_to_string(name) else {
let Ok(buf) = std::fs::read_to_string(name).map(zeroize::Zeroizing::new) else {
return Err(CertificateFetchError::Fetch(c"cannot load key"));
};

Expand Down
2 changes: 2 additions & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ use crate::conf::{AcmeMainConfig, AcmeServerConfig, NGX_HTTP_ACME_COMMANDS};
use crate::variables::NGX_HTTP_ACME_VARS;

mod conf;
mod state;
mod time;
mod variables;

#[derive(Debug)]
Expand Down
Loading
Loading