Skip to content

Commit 75fd119

Browse files
committed
ACME: restore previously issued certs from "state_path".
Improves time to readiness after a full restart of the server.
1 parent 03029f7 commit 75fd119

File tree

8 files changed

+234
-5
lines changed

8 files changed

+234
-5
lines changed

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ crate-type = ["cdylib"]
1111

1212
[dependencies]
1313
http = "1.3.1"
14+
libc = "0.2.174"
1415
openssl = { version = "0.10.73", features = ["bindgen"] }
1516
openssl-foreign-types = { package = "foreign-types", version = "0.3" }
1617
openssl-sys = { version = "0.9.109", features = ["bindgen"] }

build.rs

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ use std::env;
88
/// [1]: https://github.com/rust-lang/cargo/issues/3544
99
fn main() {
1010
detect_nginx_features();
11+
detect_libssl_features();
1112

1213
// Generate required compiler flags
1314
if cfg!(target_os = "macos") {
@@ -57,3 +58,33 @@ fn detect_nginx_features() {
5758
}
5859
}
5960
}
61+
62+
/// Detects libssl implementation and version.
63+
fn detect_libssl_features() {
64+
// OpenSSL
65+
let openssl_features = ["awslc", "boringssl", "libressl", "openssl", "openssl111"];
66+
let openssl_version = env::var("DEP_OPENSSL_VERSION_NUMBER").unwrap_or_default();
67+
let openssl_version = u64::from_str_radix(&openssl_version, 16).unwrap_or(0);
68+
69+
println!(
70+
"cargo::rustc-check-cfg=cfg(openssl, values(\"{}\"))",
71+
openssl_features.join("\",\"")
72+
);
73+
74+
#[allow(clippy::unusual_byte_groupings)]
75+
let openssl = if env::var("DEP_OPENSSL_AWSLC").is_ok() {
76+
"awslc"
77+
} else if env::var("DEP_OPENSSL_BORINGSSL").is_ok() {
78+
"boringssl"
79+
} else if env::var("DEP_OPENSSL_LIBRESSL").is_ok() {
80+
"libressl"
81+
} else {
82+
if openssl_version >= 0x01_01_01_00_0 {
83+
println!("cargo::rustc-cfg=openssl=\"openssl111\"");
84+
}
85+
86+
"openssl"
87+
};
88+
89+
println!("cargo::rustc-cfg=openssl=\"{openssl}\"");
90+
}

src/conf/issuer.rs

Lines changed: 91 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,12 +17,14 @@ use ngx::sync::RwLock;
1717
use openssl::pkey::{PKey, Private};
1818
use thiserror::Error;
1919

20+
use super::ext::NgxConfExt;
2021
use super::order::CertificateOrder;
2122
use super::pkey::PrivateKey;
2223
use super::ssl::NgxSsl;
2324
use super::AcmeMainConfig;
24-
use crate::state::certificate::CertificateContext;
25+
use crate::state::certificate::{CertificateContext, CertificateContextInner};
2526
use crate::state::issuer::IssuerContext;
27+
use crate::time::{Time, TimeRange};
2628

2729
const ACCOUNT_KEY_FILE: &str = "account.key";
2830
const NGX_ACME_DEFAULT_RESOLVER_TIMEOUT: ngx_msec_t = 30000;
@@ -163,7 +165,32 @@ impl Issuer {
163165
self.name
164166
);
165167

166-
let cert = CertificateContext::Empty;
168+
let mut cert = CertificateContext::Empty;
169+
170+
if let Some(state_dir) = unsafe { StateDir::from_ptr(self.state_path) } {
171+
match state_dir.load_certificate(cf, order) {
172+
Ok(x) => {
173+
ngx_log_debug!(
174+
cf.log,
175+
"acme: found cached certificate {}/{}, next update in {:?}",
176+
self.name,
177+
order.cache_key(),
178+
(x.next - Time::now()),
179+
);
180+
cert = CertificateContext::Local(x);
181+
}
182+
Err(CachedCertificateError::NotFound) => (),
183+
Err(err) => {
184+
ngx_log_debug!(
185+
cf.log,
186+
"acme: cannot load certificate {}/{} from state path: {}",
187+
self.name,
188+
order.cache_key(),
189+
err
190+
);
191+
}
192+
}
193+
}
167194

168195
if self.orders.try_insert(order.clone(), cert).is_err() {
169196
return Err(Status::NGX_ERROR);
@@ -239,6 +266,20 @@ impl Issuer {
239266
}
240267
}
241268

269+
#[derive(Debug, thiserror::Error)]
270+
enum CachedCertificateError {
271+
#[error(transparent)]
272+
Alloc(#[from] AllocError),
273+
#[error("X509_check_private_key() failed: {0}")]
274+
Mismatch(openssl::error::ErrorStack),
275+
#[error("file not found")]
276+
NotFound,
277+
#[error(transparent)]
278+
Ssl(#[from] openssl::error::ErrorStack),
279+
#[error("failed to load file: {0}")]
280+
CertificateFetch(#[from] super::ssl::CertificateFetchError),
281+
}
282+
242283
/// The StateDir helper encapsulates operations with a persistent state in the state directory.
243284
#[repr(transparent)]
244285
struct StateDir(ngx_path_t);
@@ -264,4 +305,52 @@ impl StateDir {
264305
pub fn write(&self, path: &std::path::Path, data: &[u8]) -> Result<(), std::io::Error> {
265306
std::fs::write(path, data)
266307
}
308+
309+
pub fn load_certificate(
310+
&self,
311+
cf: &mut ngx_conf_t,
312+
order: &CertificateOrder<ngx_str_t, Pool>,
313+
) -> Result<CertificateContextInner<Pool>, CachedCertificateError> {
314+
use openssl_foreign_types::ForeignType;
315+
#[cfg(ngx_ssl_cache)]
316+
use openssl_foreign_types::ForeignTypeRef;
317+
318+
let name = order.cache_key();
319+
320+
let cert = std::format!("{}/{}.crt", self.0.name, name);
321+
if matches!(std::fs::exists(&cert), Ok(false)) {
322+
return Err(CachedCertificateError::NotFound);
323+
}
324+
325+
let key = std::format!("{}/{}.key", self.0.name, name);
326+
if matches!(std::fs::exists(&key), Ok(false)) {
327+
return Err(CachedCertificateError::NotFound);
328+
}
329+
330+
let stack = super::ssl::conf_read_certificate(cf, &cert)?;
331+
#[allow(clippy::get_first)] // ^ can return Stack or Vec, depending on the NGINX version
332+
let cert = stack
333+
.get(0)
334+
.ok_or(super::ssl::CertificateFetchError::Fetch(c"no certificates"))?;
335+
let pkey = super::ssl::conf_read_private_key(cf, &key)?;
336+
337+
if unsafe { openssl_sys::X509_check_private_key(cert.as_ptr(), pkey.as_ptr()) } != 1 {
338+
return Err(CachedCertificateError::Mismatch(
339+
openssl::error::ErrorStack::get(),
340+
));
341+
}
342+
343+
let valid = TimeRange::from_x509(cert).unwrap_or_default();
344+
let temp_alloc = unsafe { Pool::from_ngx_pool(cf.temp_pool) };
345+
346+
let mut chain: Vec<u8, Pool> = Vec::new_in(temp_alloc.clone());
347+
for x509 in stack.into_iter() {
348+
chain.extend(x509.to_pem()?.into_iter());
349+
}
350+
351+
let mut cert = CertificateContextInner::new_in(cf.pool());
352+
cert.set(&chain, &pkey.private_key_to_pem_pkcs8()?, valid)?;
353+
354+
Ok(cert)
355+
}
267356
}

src/conf/ssl.rs

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ use nginx_sys::{
99
use ngx::allocator::AllocError;
1010
use ngx::core::Status;
1111
use openssl::pkey::{PKey, Private};
12+
use openssl::x509::X509;
1213
use openssl_sys::SSL_CTX_set_default_verify_paths;
1314
use thiserror::Error;
1415

@@ -21,6 +22,30 @@ pub enum CertificateFetchError {
2122
#[error("{0:?} {1}")]
2223
Ssl(&'static CStr, openssl::error::ErrorStack),
2324
}
25+
26+
#[cfg(ngx_ssl_cache)]
27+
pub fn conf_read_certificate(
28+
cf: &mut ngx_conf_t,
29+
name: &str,
30+
) -> Result<openssl::stack::Stack<X509>, CertificateFetchError> {
31+
conf_ssl_cache_fetch(cf, nginx_sys::NGX_SSL_CACHE_CERT as _, name)
32+
}
33+
34+
#[cfg(not(ngx_ssl_cache))]
35+
pub fn conf_read_certificate(
36+
_cf: &mut ngx_conf_t,
37+
name: &str,
38+
) -> Result<std::vec::Vec<X509>, CertificateFetchError> {
39+
let Ok(buf) = std::fs::read_to_string(name) else {
40+
return Err(CertificateFetchError::Fetch(c"cannot load certificate"));
41+
};
42+
43+
match X509::stack_from_pem(buf.as_bytes()) {
44+
Ok(x) => Ok(x),
45+
Err(err) => Err(CertificateFetchError::Ssl(c"cannot load key", err)),
46+
}
47+
}
48+
2449
#[cfg(ngx_ssl_cache)]
2550
pub fn conf_read_private_key(
2651
cf: &mut ngx_conf_t,

src/state/certificate.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
use ngx::allocator::{AllocError, Allocator, TryCloneIn};
22
use ngx::collections::Vec;
3-
use ngx::core::SlabPool;
3+
use ngx::core::{Pool, SlabPool};
44
use ngx::sync::RwLock;
55

66
use crate::time::{jitter, Time, TimeRange};
@@ -11,6 +11,8 @@ pub type SharedCertificateContext = RwLock<CertificateContextInner<SlabPool>>;
1111
pub enum CertificateContext {
1212
#[default]
1313
Empty,
14+
// Previously issued certificate, restored from the state directory.
15+
Local(CertificateContextInner<Pool>),
1416
// Ready to use certificate in shared memory.
1517
Shared(&'static SharedCertificateContext),
1618
}

src/state/issuer.rs

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
use core::ptr;
22

3-
use ngx::allocator::AllocError;
3+
use ngx::allocator::{AllocError, TryCloneIn};
44
use ngx::collections::Queue;
55
use ngx::core::SlabPool;
66
use ngx::sync::RwLock;
@@ -21,7 +21,12 @@ impl IssuerContext {
2121
let mut certificates = Queue::try_new_in(alloc.clone())?;
2222

2323
for (_, value) in issuer.orders.iter_mut() {
24-
let ctx = CertificateContextInner::new_in(alloc.clone());
24+
let ctx = if let CertificateContext::Local(value) = value {
25+
value.try_clone_in(alloc.clone())?
26+
} else {
27+
CertificateContextInner::new_in(alloc.clone())
28+
};
29+
2530
let ctx = certificates.push_back(RwLock::new(ctx))?;
2631
*value = CertificateContext::Shared(unsafe { &*ptr::from_ref(ctx) });
2732
}

src/time.rs

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@ use core::ops;
22
use core::time::Duration;
33

44
use nginx_sys::{ngx_random, ngx_time, time_t};
5+
use openssl::asn1::Asn1TimeRef;
6+
use openssl::x509::X509Ref;
7+
use openssl_foreign_types::ForeignTypeRef;
58
use thiserror::Error;
69

710
#[derive(Debug, Error)]
@@ -16,6 +19,66 @@ pub struct InvalidTime;
1619
#[repr(transparent)]
1720
pub struct Time(time_t);
1821

22+
impl TryFrom<&Asn1TimeRef> for Time {
23+
type Error = InvalidTime;
24+
25+
#[cfg(openssl = "openssl111")]
26+
fn try_from(asn1time: &Asn1TimeRef) -> Result<Self, Self::Error> {
27+
let val = unsafe {
28+
let mut tm: libc::tm = core::mem::zeroed();
29+
if openssl_sys::ASN1_TIME_to_tm(asn1time.as_ptr(), &mut tm) != 1 {
30+
return Err(InvalidTime);
31+
}
32+
libc::timegm(&mut tm) as _
33+
};
34+
35+
Ok(Time(val))
36+
}
37+
38+
#[cfg(any(openssl = "awslc", openssl = "boringssl"))]
39+
fn try_from(asn1time: &Asn1TimeRef) -> Result<Self, Self::Error> {
40+
let mut val: time_t = 0;
41+
if unsafe { openssl_sys::ASN1_TIME_to_time_t(asn1time.as_ptr(), &mut val) } != 1 {
42+
return Err(InvalidTime);
43+
}
44+
Ok(Time(val))
45+
}
46+
47+
#[cfg(not(any(openssl = "openssl111", openssl = "awslc", openssl = "boringssl")))]
48+
fn try_from(asn1time: &Asn1TimeRef) -> Result<Self, Self::Error> {
49+
pub const NGX_INVALID_TIME: time_t = nginx_sys::NGX_ERROR as _;
50+
51+
use openssl_sys::{
52+
ASN1_TIME_print, BIO_free, BIO_get_mem_data, BIO_new, BIO_s_mem, BIO_write,
53+
};
54+
55+
let val = unsafe {
56+
let bio = BIO_new(BIO_s_mem());
57+
if bio.is_null() {
58+
openssl::error::ErrorStack::get(); // clear errors
59+
return Err(InvalidTime);
60+
}
61+
62+
let mut value: *mut core::ffi::c_char = core::ptr::null_mut();
63+
/* fake weekday prepended to match C asctime() format */
64+
let prefix = c"Tue ";
65+
BIO_write(bio, prefix.as_ptr().cast(), prefix.count_bytes() as _);
66+
ASN1_TIME_print(bio, asn1time.as_ptr());
67+
let len = BIO_get_mem_data(bio, &mut value);
68+
let val = ngx_parse_http_time(value.cast(), len as _);
69+
70+
BIO_free(bio);
71+
val
72+
};
73+
74+
if val == NGX_INVALID_TIME {
75+
return Err(InvalidTime);
76+
}
77+
78+
Ok(Time(val))
79+
}
80+
}
81+
1982
impl Time {
2083
// time_t can be signed, but is not supposed to be negative
2184
pub const MIN: Self = Self(0);
@@ -33,6 +96,18 @@ pub struct TimeRange {
3396
}
3497

3598
impl TimeRange {
99+
pub fn new(start: Time, end: Time) -> Self {
100+
// ensure that end >= start
101+
let end = end.max(start);
102+
Self { start, end }
103+
}
104+
105+
pub fn from_x509(x509: &X509Ref) -> Option<Self> {
106+
let start = Time::try_from(x509.not_before()).ok()?;
107+
let end = Time::try_from(x509.not_after()).ok()?;
108+
Some(Self::new(start, end))
109+
}
110+
36111
/// Checks if the argument is contained within the interval.
37112
#[inline]
38113
pub fn contains(&self, x: Time) -> bool {

0 commit comments

Comments
 (0)