Skip to content

Commit 93e51d0

Browse files
committed
ACME: external account binding support.
1 parent 064714b commit 93e51d0

File tree

7 files changed

+294
-31
lines changed

7 files changed

+294
-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: 27 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,6 +229,12 @@ 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+
230238
// We validate that the strings are valid UTF-8 at configuration time.
231239
let contact: Vec<&str> = self
232240
.issuer
@@ -235,9 +243,26 @@ where
235243
.map(|x| x.to_str())
236244
.collect::<Result<_, _>>()?;
237245

246+
let external_account_binding = self.issuer.eab_key.as_ref().and_then(|x| {
247+
let key = crate::jws::ShaWithHmacKey::new(&x.key, 256);
248+
let kid = x.kid.to_str().ok()?;
249+
250+
let payload = serde_json::to_vec(&self.key).ok()?;
251+
252+
crate::jws::sign_jws(
253+
&key,
254+
Some(kid),
255+
&self.directory.new_account.to_string(),
256+
None,
257+
&payload,
258+
)
259+
.ok()
260+
});
261+
238262
let payload = types::AccountRequest {
239263
terms_of_service_agreed: self.issuer.accept_tos,
240264
contact,
265+
external_account_binding,
241266

242267
..Default::default()
243268
};

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: 59 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};
@@ -79,7 +79,7 @@ pub static mut NGX_HTTP_ACME_COMMANDS: [ngx_command_t; 4] = [
7979
ngx_command_t::empty(),
8080
];
8181

82-
static mut NGX_HTTP_ACME_ISSUER_COMMANDS: [ngx_command_t; 8] = [
82+
static mut NGX_HTTP_ACME_ISSUER_COMMANDS: [ngx_command_t; 9] = [
8383
ngx_command_t {
8484
name: ngx_string!("uri"),
8585
type_: NGX_CONF_TAKE1 as ngx_uint_t,
@@ -104,6 +104,14 @@ static mut NGX_HTTP_ACME_ISSUER_COMMANDS: [ngx_command_t; 8] = [
104104
offset: 0,
105105
post: ptr::null_mut(),
106106
},
107+
ngx_command_t {
108+
name: ngx_string!("external_account_key"),
109+
type_: NGX_CONF_TAKE2 as ngx_uint_t,
110+
set: Some(cmd_issuer_set_external_account_key),
111+
conf: 0,
112+
offset: 0,
113+
post: ptr::null_mut(),
114+
},
107115
ngx_command_t {
108116
name: ngx_string!("ssl_trusted_certificate"),
109117
type_: NGX_CONF_TAKE1 as ngx_uint_t,
@@ -383,6 +391,52 @@ extern "C" fn cmd_issuer_set_account_key(
383391
NGX_CONF_OK
384392
}
385393

394+
extern "C" fn cmd_issuer_set_external_account_key(
395+
cf: *mut ngx_conf_t,
396+
_cmd: *mut ngx_command_t,
397+
conf: *mut c_void,
398+
) -> *mut c_char {
399+
let cf = unsafe { cf.as_mut().expect("cf") };
400+
let issuer = unsafe { conf.cast::<Issuer>().as_mut().expect("issuer conf") };
401+
402+
if issuer.eab_key.is_some() {
403+
return NGX_CONF_DUPLICATE;
404+
}
405+
406+
let mut pool = cf.pool();
407+
// NGX_CONF_TAKE2 ensures that args contains 3 elements
408+
let args = cf.args();
409+
410+
let mut encoded = if let Some(arg) = args[2].strip_prefix(b"data:") {
411+
arg
412+
} else {
413+
match crate::util::read_to_ngx_str(cf, &args[2]) {
414+
Ok(x) => x,
415+
Err(e) => return cf.error(args[0], &e),
416+
}
417+
};
418+
419+
crate::util::ngx_str_trim(&mut encoded);
420+
421+
let len = encoded.len.div_ceil(4) * 3;
422+
let mut key = ngx_str_t {
423+
data: pool.alloc_unaligned(len).cast(),
424+
len,
425+
};
426+
427+
if key.data.is_null() {
428+
return NGX_CONF_ERROR;
429+
}
430+
431+
if !Status(unsafe { ngx_decode_base64url(&mut key, &mut encoded) }).is_ok() {
432+
return c"invalid base64 encoded value".as_ptr().cast_mut();
433+
}
434+
435+
issuer.eab_key = Some(issuer::ExternalAccountKey { kid: args[1], key });
436+
437+
NGX_CONF_OK
438+
}
439+
386440
extern "C" fn cmd_issuer_set_uri(
387441
cf: *mut ngx_conf_t,
388442
_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<ngx_str_t, 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: ngx_str_t,
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)