Skip to content

Commit a07249a

Browse files
committed
ACME: external account binding support.
1 parent 511be2a commit a07249a

File tree

7 files changed

+306
-31
lines changed

7 files changed

+306
-31
lines changed

README.md

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@ The module implements following specifications:
1313

1414
* [RFC8555] (Automatic Certificate Management Environment) with limitations:
1515
* Only HTTP-01 challenge type is supported
16-
* External account binding is not supported
1716

1817
[NGINX]: https://nginx.org/
1918
[RFC8555]: https://www.rfc-editor.org/rfc/rfc8555.html
@@ -179,6 +178,22 @@ regarding account issues.
179178
The `mailto:` scheme will be assumed unless specified
180179
explicitly.
181180

181+
### external_account_key
182+
183+
**Syntax:** external_account_key `kid` `file`
184+
185+
**Default:** -
186+
187+
**Context:** acme_issuer
188+
189+
A key identifier and a file with the MAC key for external account authorization
190+
([RFC8555 § 7.3.4](https://www.rfc-editor.org/rfc/rfc8555.html#section-7.3.4)).
191+
192+
The value `data:key` can be specified instead of the `file` to load the key
193+
directly from the configuration without using intermediate files.
194+
195+
In both cases, the key is expected to be encoded as base64url.
196+
182197
### ssl_trusted_certificate
183198

184199
**Syntax:** ssl_trusted_certificate `file`

src/acme.rs

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -166,9 +166,11 @@ where
166166
&self.key,
167167
self.account.as_deref(),
168168
&url.to_string(),
169-
&nonce,
169+
Some(&nonce),
170170
payload.as_ref(),
171-
)?;
171+
)?
172+
.to_string();
173+
172174
let req = http::Request::builder()
173175
.uri(url)
174176
.method(http::Method::POST)
@@ -227,9 +229,34 @@ where
227229
pub async fn new_account(&mut self) -> Result<types::Account> {
228230
self.directory = self.get_directory().await?;
229231

232+
if self.directory.meta.external_account_required == Some(true)
233+
&& self.issuer.eab_key.is_none()
234+
{
235+
return Err(anyhow!("external account key required"));
236+
}
237+
238+
let external_account_binding = self
239+
.issuer
240+
.eab_key
241+
.as_ref()
242+
.map(|x| -> Result<_> {
243+
let key = crate::jws::ShaWithHmacKey::new(&x.key, 256);
244+
let payload = serde_json::to_vec(&self.key)?;
245+
let message = crate::jws::sign_jws(
246+
&key,
247+
Some(x.kid),
248+
&self.directory.new_account.to_string(),
249+
None,
250+
&payload,
251+
)?;
252+
Ok(message)
253+
})
254+
.transpose()?;
255+
230256
let payload = types::AccountRequest {
231257
terms_of_service_agreed: self.issuer.accept_tos,
232258
contact: &self.issuer.contacts,
259+
external_account_binding,
233260

234261
..Default::default()
235262
};

src/acme/types.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,8 @@ pub struct AccountRequest<'a> {
7373
pub terms_of_service_agreed: Option<bool>,
7474
#[serde(skip_serializing_if = "Option::is_none")]
7575
pub only_return_existing: Option<bool>,
76-
// external_account_binding: Option<JWS>,
76+
#[serde(skip_serializing_if = "Option::is_none")]
77+
pub external_account_binding: Option<crate::jws::SignedMessage>,
7778
}
7879

7980
#[derive(Clone, Debug, Deserialize, Eq, PartialEq)]

src/conf.rs

Lines changed: 69 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,10 @@ use core::ffi::{c_char, c_void, CStr};
77
use core::{mem, ptr};
88

99
use nginx_sys::{
10-
ngx_command_t, ngx_conf_parse, ngx_conf_t, ngx_http_core_srv_conf_t, ngx_str_t, ngx_uint_t,
11-
NGX_CONF_1MORE, NGX_CONF_BLOCK, NGX_CONF_FLAG, NGX_CONF_NOARGS, NGX_CONF_TAKE1,
12-
NGX_HTTP_MAIN_CONF, NGX_HTTP_MAIN_CONF_OFFSET, NGX_HTTP_SRV_CONF, NGX_HTTP_SRV_CONF_OFFSET,
13-
NGX_LOG_EMERG,
10+
ngx_command_t, ngx_conf_parse, ngx_conf_t, ngx_decode_base64url, ngx_http_core_srv_conf_t,
11+
ngx_str_t, ngx_uint_t, NGX_CONF_1MORE, NGX_CONF_BLOCK, NGX_CONF_FLAG, NGX_CONF_NOARGS,
12+
NGX_CONF_TAKE1, NGX_CONF_TAKE2, NGX_HTTP_MAIN_CONF, NGX_HTTP_MAIN_CONF_OFFSET,
13+
NGX_HTTP_SRV_CONF, NGX_HTTP_SRV_CONF_OFFSET, NGX_LOG_EMERG,
1414
};
1515
use ngx::collections::Vec;
1616
use ngx::core::{Pool, Status, NGX_CONF_ERROR, NGX_CONF_OK};
@@ -80,7 +80,7 @@ pub static mut NGX_HTTP_ACME_COMMANDS: [ngx_command_t; 4] = [
8080
ngx_command_t::empty(),
8181
];
8282

83-
static mut NGX_HTTP_ACME_ISSUER_COMMANDS: [ngx_command_t; 8] = [
83+
static mut NGX_HTTP_ACME_ISSUER_COMMANDS: [ngx_command_t; 9] = [
8484
ngx_command_t {
8585
name: ngx_string!("uri"),
8686
type_: NGX_CONF_TAKE1 as ngx_uint_t,
@@ -105,6 +105,14 @@ static mut NGX_HTTP_ACME_ISSUER_COMMANDS: [ngx_command_t; 8] = [
105105
offset: 0,
106106
post: ptr::null_mut(),
107107
},
108+
ngx_command_t {
109+
name: ngx_string!("external_account_key"),
110+
type_: NGX_CONF_TAKE2 as ngx_uint_t,
111+
set: Some(cmd_issuer_set_external_account_key),
112+
conf: 0,
113+
offset: 0,
114+
post: ptr::null_mut(),
115+
},
108116
ngx_command_t {
109117
name: ngx_string!("ssl_trusted_certificate"),
110118
type_: NGX_CONF_TAKE1 as ngx_uint_t,
@@ -391,6 +399,62 @@ extern "C" fn cmd_issuer_set_account_key(
391399
NGX_CONF_OK
392400
}
393401

402+
extern "C" fn cmd_issuer_set_external_account_key(
403+
cf: *mut ngx_conf_t,
404+
_cmd: *mut ngx_command_t,
405+
conf: *mut c_void,
406+
) -> *mut c_char {
407+
let cf = unsafe { cf.as_mut().expect("cf") };
408+
let issuer = unsafe { conf.cast::<Issuer>().as_mut().expect("issuer conf") };
409+
410+
if issuer.eab_key.is_some() {
411+
return NGX_CONF_DUPLICATE;
412+
}
413+
414+
let mut pool = cf.pool();
415+
// NGX_CONF_TAKE2 ensures that args contains 3 elements
416+
let args = cf.args();
417+
418+
if args[1].is_empty() || args[2].is_empty() {
419+
return NGX_CONF_INVALID_VALUE;
420+
}
421+
422+
// SAFETY: the value is not empty, well aligned, and the conversion result is assigned to an
423+
// object in the same pool.
424+
let Ok(kid) = (unsafe { conf_value_to_str(&args[1]) }) else {
425+
return NGX_CONF_INVALID_VALUE;
426+
};
427+
428+
let mut encoded = if let Some(arg) = args[2].strip_prefix(b"data:") {
429+
arg
430+
} else {
431+
match crate::util::read_to_ngx_str(cf, &args[2]) {
432+
Ok(x) => x,
433+
Err(e) => return cf.error(args[0], &e),
434+
}
435+
};
436+
437+
crate::util::ngx_str_trim(&mut encoded);
438+
439+
let len = encoded.len.div_ceil(4) * 3;
440+
let mut key = ngx_str_t {
441+
data: pool.alloc_unaligned(len).cast(),
442+
len,
443+
};
444+
445+
if key.data.is_null() {
446+
return NGX_CONF_ERROR;
447+
}
448+
449+
if !Status(unsafe { ngx_decode_base64url(&mut key, &mut encoded) }).is_ok() {
450+
return c"invalid base64url encoded value".as_ptr().cast_mut();
451+
}
452+
453+
issuer.eab_key = Some(issuer::ExternalAccountKey { kid, key });
454+
455+
NGX_CONF_OK
456+
}
457+
394458
extern "C" fn cmd_issuer_set_uri(
395459
cf: *mut ngx_conf_t,
396460
_cmd: *mut ngx_command_t,

src/conf/issuer.rs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ pub struct Issuer {
4444
pub uri: Uri,
4545
pub account_key: PrivateKey,
4646
pub contacts: Vec<&'static str, Pool>,
47+
pub eab_key: Option<ExternalAccountKey>,
4748
pub resolver: Option<NonNull<ngx_resolver_t>>,
4849
pub resolver_timeout: ngx_msec_t,
4950
pub ssl_trusted_certificate: ngx_str_t,
@@ -58,6 +59,12 @@ pub struct Issuer {
5859
pub data: Option<&'static RwLock<IssuerContext>>,
5960
}
6061

62+
#[derive(Debug)]
63+
pub struct ExternalAccountKey {
64+
pub kid: &'static str,
65+
pub key: ngx_str_t,
66+
}
67+
6168
#[derive(Debug, Error)]
6269
pub enum IssuerError {
6370
#[error("cannot load account key: {0}")]
@@ -88,6 +95,7 @@ impl Issuer {
8895
uri: Default::default(),
8996
account_key: PrivateKey::Unset,
9097
contacts: Vec::new_in(alloc.clone()),
98+
eab_key: None,
9199
resolver: None,
92100
resolver_timeout: NGX_CONF_UNSET_MSEC,
93101
ssl_trusted_certificate: ngx_str_t::empty(),

0 commit comments

Comments
 (0)