Skip to content
Open
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
29 changes: 29 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -255,10 +255,16 @@ Accepted values:

- `http-01` (`http`)
- `tls-alpn-01` (`tls-alpn`)
- `none` (`off`) — disables challenges for use with
challengeless ACME (e.g. with [external account binding](#external_account_key))

_ACME challenges are versioned. If an unversioned name is specified,
the module automatically selects the latest implemented version._

When `none` is specified, the module expects the ACME server to
pre-authorize all identifiers (e.g. via external account binding)
and will skip the challenge flow entirely.

### common_name_in_csr

**Syntax:** **`common_name_in_csr`** `on` | `off`
Expand Down Expand Up @@ -395,6 +401,29 @@ Some servers require accepting the terms of service before account registration.
The terms are usually available on the ACME server's website,
and the URL will be printed to the error log if necessary.

### csr_additional_fields

**Syntax:** **`csr_additional_fields`** _`key`_=_`value`_ ...

**Default:** -

**Context:** acme_issuer

Sets additional subject fields in the certificate signing request.
Multiple fields can be specified in a single directive. Supported keys:

- `organization` — Organization (O)
- `organizational_unit` — Organizational Unit (OU)
- `country` — Country (C); should be a two-letter ISO 3166-1 alpha-2 code
- `locality` — Locality (L)
- `state` — State or Province (ST)

Example:

```nginx
csr_additional_fields "organization=My Company" country=US state=WA;
```

### acme_shared_zone

**Syntax:** **`acme_shared_zone`** `zone`=_`name`_:_`size`_
Expand Down
21 changes: 20 additions & 1 deletion src/acme.rs
Original file line number Diff line number Diff line change
Expand Up @@ -192,7 +192,11 @@ where
}

async fn get_nonce(&self) -> Result<String, RequestError> {
let res = self.get(&self.directory.new_nonce).await?;
let req = http::Request::builder()
.uri(&self.directory.new_nonce)
.method(http::Method::HEAD)
.body(String::new())?;
let res = self.http.request(req).await.map_err(RequestError::from)?;
try_get_header(res.headers(), &REPLAY_NONCE).ok_or(RequestError::Nonce).map(String::from)
}

Expand Down Expand Up @@ -685,6 +689,21 @@ pub fn make_certificate_request<A: Allocator>(
x509_name.append_entry_by_text("CN", name)?;
}
}
if let Some(val) = order.csr_subject.organization {
x509_name.append_entry_by_text("O", val)?;
}
if let Some(val) = order.csr_subject.organizational_unit {
x509_name.append_entry_by_text("OU", val)?;
}
if let Some(val) = order.csr_subject.country {
x509_name.append_entry_by_text("C", val)?;
}
if let Some(val) = order.csr_subject.locality {
x509_name.append_entry_by_text("L", val)?;
}
if let Some(val) = order.csr_subject.state {
x509_name.append_entry_by_text("ST", val)?;
}
let x509_name = x509_name.build();
req.set_subject_name(&x509_name)?;

Expand Down
73 changes: 69 additions & 4 deletions src/conf.rs
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ pub static mut NGX_HTTP_ACME_COMMANDS: [ngx_command_t; 4] = [
ngx_command_t::empty(),
];

static mut NGX_HTTP_ACME_ISSUER_COMMANDS: [ngx_command_t; 13] = [
static mut NGX_HTTP_ACME_ISSUER_COMMANDS: [ngx_command_t; 14] = [
ngx_command_t {
name: ngx_string!("uri"),
type_: NGX_CONF_TAKE1 as ngx_uint_t,
Expand Down Expand Up @@ -181,6 +181,14 @@ static mut NGX_HTTP_ACME_ISSUER_COMMANDS: [ngx_command_t; 13] = [
offset: 0,
post: ptr::null_mut(),
},
ngx_command_t {
name: ngx_string!("csr_additional_fields"),
type_: NGX_CONF_1MORE as ngx_uint_t,
set: Some(cmd_issuer_set_csr_additional_fields),
conf: 0,
offset: 0,
post: ptr::null_mut(),
},
ngx_command_t::empty(),
];

Expand Down Expand Up @@ -368,15 +376,16 @@ extern "C" fn cmd_issuer_set_challenge(
let val = cf.args()[1];

let val = match val.as_bytes() {
b"http" | b"http-01" => ChallengeKind::Http01,
b"tls-alpn" | b"tls-alpn-01" => ChallengeKind::TlsAlpn01,
b"http" | b"http-01" => Some(ChallengeKind::Http01),
b"tls-alpn" | b"tls-alpn-01" => Some(ChallengeKind::TlsAlpn01),
b"none" => None,
_ => {
ngx_conf_log_error!(NGX_LOG_EMERG, cf, "unsupported challenge: {val}");
return NGX_CONF_ERROR;
}
};

issuer.challenge = Some(val);
issuer.challenge = val;

NGX_CONF_OK
}
Expand Down Expand Up @@ -644,6 +653,62 @@ extern "C" fn cmd_issuer_set_state_path(
unsafe { nginx_sys::ngx_conf_set_path_slot(cf, cmd, ptr::from_mut(issuer).cast()) }
}

extern "C" fn cmd_issuer_set_csr_additional_fields(
cf: *mut ngx_conf_t,
_cmd: *mut ngx_command_t,
conf: *mut c_void,
) -> *mut c_char {
let cf = unsafe { cf.as_mut().expect("cf") };
let issuer = unsafe { conf.cast::<Issuer>().as_mut().expect("issuer conf") };

// NGX_CONF_1MORE ensures that args contains at least 2 elements
let args = cf.args();

let args = unsafe { core::slice::from_raw_parts(args.as_ptr(), args.len()) };

for arg in &args[1..] {
let bytes = arg.as_bytes();
let Some(eq_pos) = bytes.iter().position(|&b| b == b'=') else {
ngx_conf_log_error!(NGX_LOG_EMERG, cf, "invalid \"csr_additional_fields\" parameter: {}", arg);
return NGX_CONF_ERROR;
};

let key = &bytes[..eq_pos];
let val = ngx_str_t {
data: unsafe { arg.data.add(eq_pos + 1) },
len: arg.len - eq_pos - 1,
};

if val.is_empty() {
return NGX_CONF_INVALID_VALUE;
}

let Ok(val) = (unsafe { conf_value_to_str(&val) }) else {
return NGX_CONF_INVALID_VALUE;
};

let field = match key {
b"organization" => &mut issuer.csr_subject.organization,
b"organizational_unit" => &mut issuer.csr_subject.organizational_unit,
b"country" => &mut issuer.csr_subject.country,
b"locality" => &mut issuer.csr_subject.locality,
b"state" => &mut issuer.csr_subject.state,
_ => {
ngx_conf_log_error!(NGX_LOG_EMERG, cf, "unknown \"csr_additional_fields\" parameter: {}", arg);
return NGX_CONF_ERROR;
}
};

if field.is_some() {
return NGX_CONF_DUPLICATE;
}

*field = Some(val);
}

NGX_CONF_OK
}

extern "C" fn cmd_issuer_set_accept_tos(
_cf: *mut ngx_conf_t,
_cmd: *mut ngx_command_t,
Expand Down
14 changes: 11 additions & 3 deletions src/conf/issuer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ use thiserror::Error;
use zeroize::Zeroizing;

use super::ext::NgxConfExt;
use super::order::CertificateOrder;
use super::order::{CertificateOrder, CsrSubject};
use super::pkey::PrivateKey;
use super::ssl::NgxSsl;
use super::AcmeMainConfig;
Expand All @@ -50,6 +50,7 @@ pub struct Issuer {
pub challenge: Option<ChallengeKind>,
pub common_name_in_csr: ngx_flag_t,
pub contacts: Vec<&'static str, Pool>,
pub csr_subject: CsrSubject,
pub eab_key: Option<ExternalAccountKey>,
pub profile: Profile,
pub resolver: Option<NonNull<ngx_resolver_t>>,
Expand Down Expand Up @@ -115,6 +116,7 @@ impl Issuer {
challenge: None,
common_name_in_csr: NGX_CONF_UNSET_FLAG,
contacts: Vec::new_in(alloc.clone()),
csr_subject: CsrSubject::default(),
eab_key: None,
profile: Profile::Unset,
resolver: None,
Expand Down Expand Up @@ -232,8 +234,10 @@ impl Issuer {
pub fn add_certificate_order(
&mut self,
cf: &mut ngx_conf_t,
order: &CertificateOrder<&'static str, Pool>,
order: &mut CertificateOrder<&'static str, Pool>,
) -> Result<(), Status> {
order.csr_subject = self.csr_subject.clone();

if self.orders.get(order).is_none() {
debug!(cf, "acme: order \"{}\" created in issuer \"{}\"", order.cache_key(), self.name);

Expand Down Expand Up @@ -285,7 +289,11 @@ impl Issuer {
if let Some(state_dir) = state_dir {
let path = state_dir.full_path(path);

state_dir.write(&path, buf).map_err(|_| Status::NGX_ERROR)?;
if let Err(err) = state_dir.write(&path, buf) {
use std::io::Write;
let _ = writeln!(std::io::stderr(), "DBG write_state_file path={path:?} err={err} kind={:?}", err.kind());
return Err(Status::NGX_ERROR);
}
}
Ok(())
}
Expand Down
15 changes: 13 additions & 2 deletions src/conf/order.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,23 @@ use crate::conf::ext::NgxConfExt;
use crate::conf::identifier::Identifier;
use crate::conf::pkey::PrivateKey;

#[derive(Clone, Debug, Default)]
pub struct CsrSubject {
pub organization: Option<&'static str>,
pub organizational_unit: Option<&'static str>,
pub country: Option<&'static str>,
pub locality: Option<&'static str>,
pub state: Option<&'static str>,
}

#[derive(Clone, Debug)]
pub struct CertificateOrder<S, A>
where
A: Allocator,
{
pub identifiers: Vec<Identifier<S>, A>,
pub key: PrivateKey,
pub csr_subject: CsrSubject,
}

impl<S, A> CertificateOrder<S, A>
Expand All @@ -35,7 +45,7 @@ where
where
S: Default,
{
Self { identifiers: Vec::new_in(alloc), key: Default::default() }
Self { identifiers: Vec::new_in(alloc), key: Default::default(), csr_subject: Default::default() }
}

/// Generates a stable unique identifier for this order.
Expand Down Expand Up @@ -110,6 +120,7 @@ where

fn try_clone_in<A: Allocator + Clone>(&self, alloc: A) -> Result<Self::Target<A>, AllocError> {
let key = self.key.clone();
let csr_subject = self.csr_subject.clone();

let mut identifiers: Vec<Identifier<NgxString<A>>, A> = Vec::new_in(alloc.clone());
identifiers.try_reserve_exact(self.identifiers.len()).map_err(|_| AllocError)?;
Expand All @@ -118,7 +129,7 @@ where
identifiers.push(id.try_clone_in(alloc.clone())?);
}

Ok(Self::Target { identifiers, key })
Ok(Self::Target { identifiers, key, csr_subject })
}
}

Expand Down
3 changes: 2 additions & 1 deletion src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -274,7 +274,8 @@ async fn ngx_acme_update_certificates_for_issuer(
let tls_solver = acme::solvers::tls_alpn::TlsAlpn01Solver::new(&amsh.tls_alpn_01_state);
client.add_solver(tls_solver);
}
_ => unreachable!("invalid configuration"),
// Challengeless ACME (e.g. with external account binding): no solver needed.
_ => {}
};

let mut next = Timestamp::MAX;
Expand Down
Loading
Loading