Skip to content

Commit f4bebce

Browse files
committed
ACME: define and allocate shared data structures.
1 parent 2b672bd commit f4bebce

File tree

9 files changed

+490
-10
lines changed

9 files changed

+490
-10
lines changed

src/conf.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ use self::issuer::Issuer;
1616
use self::order::CertificateOrder;
1717
use self::pkey::PrivateKey;
1818
use self::shared_zone::{SharedZone, ACME_ZONE_NAME, ACME_ZONE_SIZE};
19+
use crate::state::AcmeSharedData;
1920

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

@@ -500,7 +502,10 @@ impl AcmeMainConfig {
500502
self.shm_zone = SharedZone::Configured(ACME_ZONE_NAME, ACME_ZONE_SIZE);
501503
}
502504

505+
let amcfp = ptr::from_mut(self).cast();
503506
let shm_zone = self.shm_zone.request(cf)?;
507+
shm_zone.init = Some(crate::state::ngx_acme_shared_zone_init);
508+
shm_zone.data = amcfp;
504509
shm_zone.noreuse = 1;
505510

506511
Ok(())

src/conf/issuer.rs

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,13 +13,16 @@ use ngx::collections::{RbTreeMap, Vec};
1313
use ngx::core::{Pool, Status};
1414
use ngx::http::{HttpModuleLocationConf, NgxHttpCoreModule};
1515
use ngx::ngx_log_debug;
16+
use ngx::sync::RwLock;
1617
use openssl::pkey::{PKey, Private};
1718
use thiserror::Error;
1819

1920
use super::order::CertificateOrder;
2021
use super::pkey::PrivateKey;
2122
use super::ssl::NgxSsl;
2223
use super::AcmeMainConfig;
24+
use crate::state::certificate::CertificateContext;
25+
use crate::state::issuer::IssuerContext;
2326

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

4852
#[derive(Debug, Error)]
@@ -83,6 +87,7 @@ impl Issuer {
8387
ssl,
8488
pkey: None,
8589
orders: RbTreeMap::try_new_in(alloc)?,
90+
data: None,
8691
})
8792
}
8893

@@ -158,7 +163,9 @@ impl Issuer {
158163
self.name
159164
);
160165

161-
if self.orders.try_insert(order.clone(), ()).is_err() {
166+
let cert = CertificateContext::Empty;
167+
168+
if self.orders.try_insert(order.clone(), cert).is_err() {
162169
return Err(Status::NGX_ERROR);
163170
}
164171
} else {

src/conf/shared_zone.rs

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ use core::ffi::c_void;
22
use core::ptr::{self, NonNull};
33

44
use nginx_sys::{ngx_conf_t, ngx_int_t, ngx_shm_zone_t, ngx_str_t, NGX_ERROR};
5-
use ngx::core::Status;
5+
use ngx::core::{SlabPool, Status};
66
use ngx::http::HttpModule;
77
use ngx::log::ngx_cycle_log;
88
use ngx::{ngx_log_debug, ngx_string};
@@ -32,6 +32,13 @@ pub enum SharedZoneError {
3232
}
3333

3434
impl SharedZone {
35+
pub fn allocator(&self) -> Option<SlabPool> {
36+
match self {
37+
Self::Ready(zone) => unsafe { SlabPool::from_shm_zone(zone.as_ref()) },
38+
_ => None,
39+
}
40+
}
41+
3542
pub fn is_configured(&self) -> bool {
3643
!matches!(self, Self::Unset)
3744
}

src/lib.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ use crate::conf::{AcmeMainConfig, AcmeServerConfig, NGX_HTTP_ACME_COMMANDS};
1414
use crate::variables::NGX_HTTP_ACME_VARS;
1515

1616
mod conf;
17+
mod state;
18+
mod time;
1719
mod variables;
1820

1921
#[derive(Debug)]

src/state.rs

Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
//! Shared runtime state of the module.
2+
use core::ffi::c_void;
3+
use core::ptr;
4+
5+
use nginx_sys::{ngx_int_t, ngx_shm_zone_t, NGX_LOG_EMERG};
6+
use ngx::allocator::{AllocError, Allocator, Box, TryCloneIn};
7+
use ngx::collections::Queue;
8+
use ngx::core::{SlabPool, Status};
9+
use ngx::log::ngx_cycle_log;
10+
use ngx::sync::RwLock;
11+
use ngx::{ngx_log_debug, ngx_log_error};
12+
13+
use crate::conf::shared_zone::SharedZone;
14+
use crate::conf::AcmeMainConfig;
15+
16+
pub use self::certificate::CertificateContext;
17+
pub use self::issuer::IssuerContext;
18+
19+
pub mod certificate;
20+
pub mod issuer;
21+
22+
#[derive(Debug)]
23+
pub struct AcmeSharedData<A = SlabPool>
24+
where
25+
A: Allocator + Clone,
26+
{
27+
pub issuers: Queue<RwLock<IssuerContext>, A>,
28+
}
29+
30+
impl<A> AcmeSharedData<A>
31+
where
32+
A: Allocator + Clone,
33+
{
34+
pub fn try_new_in(alloc: A) -> Result<Self, AllocError> {
35+
Ok(Self {
36+
issuers: Queue::try_new_in(alloc)?,
37+
})
38+
}
39+
}
40+
41+
pub extern "C" fn ngx_acme_shared_zone_init(
42+
shm_zone: *mut ngx_shm_zone_t,
43+
data: *mut c_void,
44+
) -> ngx_int_t {
45+
// SAFETY: shm_zone is always valid in this callback
46+
let shm_zone = unsafe { &mut *shm_zone };
47+
let log = ngx_cycle_log().as_ptr();
48+
49+
ngx_log_debug!(
50+
log,
51+
"acme: init shared zone \"{}:{}\"",
52+
shm_zone.shm.name,
53+
shm_zone.shm.size,
54+
);
55+
56+
let oamcf = unsafe { data.cast::<AcmeMainConfig>().as_ref() };
57+
let amcf = unsafe { shm_zone.data.cast::<AcmeMainConfig>().as_mut().unwrap() };
58+
let zone = SharedZone::Ready(shm_zone.into());
59+
60+
let mut alloc = zone.allocator().expect("shared zone allocator");
61+
62+
// Our shared zone is `noreuse`, meaning that we get an empty zone every time unless we are
63+
// running on Windows.
64+
65+
let Ok(mut data) =
66+
AcmeSharedData::try_new_in(alloc.clone()).and_then(|x| Box::try_new_in(x, alloc.clone()))
67+
else {
68+
ngx_log_error!(NGX_LOG_EMERG, log, "cannot allocate acme shared data");
69+
return Status::NGX_ERROR.into();
70+
};
71+
72+
for issuer in &mut amcf.issuers[..] {
73+
// Create new shared data.
74+
let Ok(ctx) = IssuerContext::try_new_in(issuer, alloc.clone()) else {
75+
ngx_log_error!(
76+
NGX_LOG_EMERG,
77+
log,
78+
"cannot allocate acme issuer \"{}\"",
79+
issuer.name,
80+
);
81+
return Status::NGX_ERROR.into();
82+
};
83+
84+
// Copy data from the previous cycle.
85+
if let Some(oissuer) = oamcf.and_then(|x| x.issuer(&issuer.name)) {
86+
ngx_log_debug!(log, "acme: copy old data for issuer \"{}\"", issuer.name);
87+
88+
for (order, ctx) in issuer.orders.iter_mut() {
89+
// Should not fail as we just allocated all the certificate contexts.
90+
let CertificateContext::Shared(ctx) = ctx else {
91+
continue;
92+
};
93+
94+
let Some(CertificateContext::Shared(octx)) = oissuer.orders.get(order) else {
95+
continue;
96+
};
97+
98+
// The old shared zone is going away as soon as we're done, so we have to copy the
99+
// data to the new slab pool.
100+
let Ok(cloned) = octx.read().try_clone_in(alloc.clone()) else {
101+
return Status::NGX_ERROR.into();
102+
};
103+
104+
*ctx.write() = cloned;
105+
}
106+
}
107+
108+
if let Ok(ctx) = data.issuers.push_back(RwLock::new(ctx)) {
109+
// SAFETY: we ensured that the chosen data structure will not move the IssuerContext,
110+
// thus the pointer will remain valid beyond this scope.
111+
//
112+
// The assigned lifetime is a bit misleading though; shared zone will be unmapped
113+
// while the main config is still present, right before calling the cycle pool cleanup.
114+
// A proper ownership-tracking pointer could attempt to unref the data from the config
115+
// destructor _after_ the zone is unmapped and thus trip on an invalid address.
116+
//
117+
// Of all the ways to handle that, we are picking the most obviously unsafe to make
118+
// sure this detail is not missed while reading.
119+
issuer.data = Some(unsafe { &*ptr::from_ref(ctx) });
120+
} else {
121+
ngx_log_error!(
122+
NGX_LOG_EMERG,
123+
log,
124+
"cannot allocate acme issuer \"{}\"",
125+
issuer.name,
126+
);
127+
return Status::NGX_ERROR.into();
128+
}
129+
}
130+
131+
// Will be freed when the zone is unmapped.
132+
let data = Box::leak(data);
133+
134+
alloc.as_mut().data = ptr::from_mut(data).cast();
135+
136+
amcf.data = Some(data);
137+
amcf.shm_zone = zone;
138+
139+
Status::NGX_OK.into()
140+
}

src/state/certificate.rs

Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
1+
use ngx::allocator::{AllocError, Allocator, TryCloneIn};
2+
use ngx::collections::Vec;
3+
use ngx::core::SlabPool;
4+
use ngx::sync::RwLock;
5+
6+
use crate::time::{jitter, Time, TimeRange};
7+
8+
pub type SharedCertificateContext = RwLock<CertificateContextInner<SlabPool>>;
9+
10+
#[derive(Debug, Default)]
11+
pub enum CertificateContext {
12+
#[default]
13+
Empty,
14+
// Ready to use certificate in shared memory.
15+
Shared(&'static SharedCertificateContext),
16+
}
17+
18+
impl CertificateContext {
19+
pub fn as_ref(&self) -> Option<&'static SharedCertificateContext> {
20+
if let CertificateContext::Shared(data) = self {
21+
Some(data)
22+
} else {
23+
None
24+
}
25+
}
26+
}
27+
28+
#[derive(Debug, Default, PartialEq, Eq)]
29+
pub enum CertificateState {
30+
#[default]
31+
Pending,
32+
Ready,
33+
}
34+
35+
#[derive(Debug)]
36+
pub struct CertificateContextInner<A>
37+
where
38+
A: Allocator + Clone,
39+
{
40+
pub state: CertificateState,
41+
pub chain: Vec<u8, A>,
42+
pub pkey: Vec<u8, A>,
43+
pub valid: TimeRange,
44+
pub next: Time,
45+
}
46+
47+
impl<OA> TryCloneIn for CertificateContextInner<OA>
48+
where
49+
OA: Allocator + Clone,
50+
{
51+
type Target<A: Allocator + Clone> = CertificateContextInner<A>;
52+
53+
fn try_clone_in<A: Allocator + Clone>(&self, alloc: A) -> Result<Self::Target<A>, AllocError> {
54+
let mut chain = Vec::new_in(alloc.clone());
55+
chain
56+
.try_reserve_exact(self.chain.len())
57+
.map_err(|_| AllocError)?;
58+
chain.extend(self.chain.iter());
59+
60+
let mut pkey = Vec::new_in(alloc);
61+
pkey.try_reserve_exact(self.pkey.len())
62+
.map_err(|_| AllocError)?;
63+
pkey.extend(self.pkey.iter());
64+
65+
Ok(Self::Target {
66+
state: CertificateState::Ready,
67+
chain,
68+
pkey,
69+
valid: self.valid.clone(),
70+
next: self.next,
71+
})
72+
}
73+
}
74+
75+
impl<A> CertificateContextInner<A>
76+
where
77+
A: Allocator + Clone,
78+
{
79+
pub fn new_in(alloc: A) -> Self {
80+
Self {
81+
state: CertificateState::Pending,
82+
chain: Vec::new_in(alloc.clone()),
83+
pkey: Vec::new_in(alloc.clone()),
84+
valid: Default::default(),
85+
next: Default::default(),
86+
}
87+
}
88+
89+
pub fn set(&mut self, chain: &[u8], pkey: &[u8], valid: TimeRange) -> Result<Time, AllocError> {
90+
const PREFIX: &[u8] = b"data:";
91+
92+
// reallocate the storage only if the current capacity is insufficient
93+
94+
fn needs_realloc<A: Allocator>(buf: &Vec<u8, A>, new_size: usize) -> bool {
95+
buf.capacity() < PREFIX.len() + new_size
96+
}
97+
98+
if needs_realloc(&self.chain, chain.len()) || needs_realloc(&self.pkey, pkey.len()) {
99+
let alloc = self.chain.allocator();
100+
101+
let mut new_chain: Vec<u8, A> = Vec::new_in(alloc.clone());
102+
new_chain
103+
.try_reserve_exact(PREFIX.len() + chain.len())
104+
.map_err(|_| AllocError)?;
105+
106+
let mut new_pkey: Vec<u8, A> = Vec::new_in(alloc.clone());
107+
new_pkey
108+
.try_reserve_exact(PREFIX.len() + pkey.len())
109+
.map_err(|_| AllocError)?;
110+
111+
self.chain = new_chain;
112+
self.pkey = new_pkey;
113+
}
114+
115+
// update the stored data in-place
116+
117+
self.chain.clear();
118+
self.chain.extend(PREFIX);
119+
self.chain.extend(chain);
120+
121+
self.pkey.clear();
122+
self.pkey.extend(PREFIX);
123+
self.pkey.extend(pkey);
124+
125+
// Schedule the next update at around 2/3 of the cert lifetime,
126+
// as recommended in Let's Encrypt integration guide
127+
self.next = valid.start + jitter(valid.duration() * 2 / 3, 2);
128+
self.valid = valid;
129+
130+
self.state = CertificateState::Ready;
131+
132+
Ok(self.next)
133+
}
134+
135+
pub fn chain(&self) -> Option<&[u8]> {
136+
if matches!(self.state, CertificateState::Ready) {
137+
return Some(&self.chain);
138+
}
139+
140+
None
141+
}
142+
143+
pub fn pkey(&self) -> Option<&[u8]> {
144+
if matches!(self.state, CertificateState::Ready) {
145+
return Some(&self.pkey);
146+
}
147+
148+
None
149+
}
150+
}

0 commit comments

Comments
 (0)