Skip to content

ACME: external account binding support. #22

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 4 commits into
base: main
Choose a base branch
from
Draft
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
17 changes: 16 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ The module implements following specifications:

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

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

### external_account_key

**Syntax:** external_account_key `kid` `file`

**Default:** -

**Context:** acme_issuer

A key identifier and a file with the MAC key for external account authorization
([RFC8555 § 7.3.4](https://www.rfc-editor.org/rfc/rfc8555.html#section-7.3.4)).

The value `data:key` can be specified instead of the `file` to load the key
directly from the configuration without using intermediate files.

In both cases, the key is expected to be encoded as base64url.

### ssl_trusted_certificate

**Syntax:** ssl_trusted_certificate `file`
Expand Down
29 changes: 27 additions & 2 deletions src/acme.rs
Original file line number Diff line number Diff line change
Expand Up @@ -166,9 +166,11 @@ where
&self.key,
self.account.as_deref(),
&url.to_string(),
&nonce,
Some(&nonce),
payload.as_ref(),
)?;
)?
.to_string();

let req = http::Request::builder()
.uri(url)
.method(http::Method::POST)
Expand Down Expand Up @@ -227,6 +229,12 @@ where
pub async fn new_account(&mut self) -> Result<types::Account> {
self.directory = self.get_directory().await?;

if self.directory.meta.external_account_required == Some(true)
&& self.issuer.eab_key.is_none()
{
return Err(anyhow!("external account key required"));
}

// We validate that the strings are valid UTF-8 at configuration time.
let contact: Vec<&str> = self
.issuer
Expand All @@ -235,9 +243,26 @@ where
.map(|x| x.to_str())
.collect::<Result<_, _>>()?;

let external_account_binding = self.issuer.eab_key.as_ref().and_then(|x| {
let key = crate::jws::ShaWithHmacKey::new(&x.key, 256);
let kid = x.kid.to_str().ok()?;

let payload = serde_json::to_vec(&self.key).ok()?;

crate::jws::sign_jws(
&key,
Some(kid),
&self.directory.new_account.to_string(),
None,
&payload,
)
.ok()
});

let payload = types::AccountRequest {
terms_of_service_agreed: self.issuer.accept_tos,
contact,
external_account_binding,

..Default::default()
};
Expand Down
3 changes: 2 additions & 1 deletion src/acme/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,8 @@ pub struct AccountRequest<'a> {
pub terms_of_service_agreed: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub only_return_existing: Option<bool>,
// external_account_binding: Option<JWS>,
#[serde(skip_serializing_if = "Option::is_none")]
pub external_account_binding: Option<crate::jws::SignedMessage>,
}

#[derive(Clone, Debug, Deserialize, Eq, PartialEq)]
Expand Down
64 changes: 59 additions & 5 deletions src/conf.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,10 @@ use core::ffi::{c_char, c_void, CStr};
use core::{mem, ptr};

use nginx_sys::{
ngx_command_t, ngx_conf_parse, ngx_conf_t, ngx_http_core_srv_conf_t, ngx_str_t, ngx_uint_t,
NGX_CONF_1MORE, NGX_CONF_BLOCK, NGX_CONF_FLAG, NGX_CONF_NOARGS, NGX_CONF_TAKE1,
NGX_HTTP_MAIN_CONF, NGX_HTTP_MAIN_CONF_OFFSET, NGX_HTTP_SRV_CONF, NGX_HTTP_SRV_CONF_OFFSET,
NGX_LOG_EMERG,
ngx_command_t, ngx_conf_parse, ngx_conf_t, ngx_decode_base64url, ngx_http_core_srv_conf_t,
ngx_str_t, ngx_uint_t, NGX_CONF_1MORE, NGX_CONF_BLOCK, NGX_CONF_FLAG, NGX_CONF_NOARGS,
NGX_CONF_TAKE1, NGX_CONF_TAKE2, NGX_HTTP_MAIN_CONF, NGX_HTTP_MAIN_CONF_OFFSET,
NGX_HTTP_SRV_CONF, NGX_HTTP_SRV_CONF_OFFSET, NGX_LOG_EMERG,
};
use ngx::collections::Vec;
use ngx::core::{Pool, Status, NGX_CONF_ERROR, NGX_CONF_OK};
Expand Down Expand Up @@ -79,7 +79,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; 8] = [
static mut NGX_HTTP_ACME_ISSUER_COMMANDS: [ngx_command_t; 9] = [
ngx_command_t {
name: ngx_string!("uri"),
type_: NGX_CONF_TAKE1 as ngx_uint_t,
Expand All @@ -104,6 +104,14 @@ static mut NGX_HTTP_ACME_ISSUER_COMMANDS: [ngx_command_t; 8] = [
offset: 0,
post: ptr::null_mut(),
},
ngx_command_t {
name: ngx_string!("external_account_key"),
type_: NGX_CONF_TAKE2 as ngx_uint_t,
set: Some(cmd_issuer_set_external_account_key),
conf: 0,
offset: 0,
post: ptr::null_mut(),
},
ngx_command_t {
name: ngx_string!("ssl_trusted_certificate"),
type_: NGX_CONF_TAKE1 as ngx_uint_t,
Expand Down Expand Up @@ -383,6 +391,52 @@ extern "C" fn cmd_issuer_set_account_key(
NGX_CONF_OK
}

extern "C" fn cmd_issuer_set_external_account_key(
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") };

if issuer.eab_key.is_some() {
return NGX_CONF_DUPLICATE;
}

let mut pool = cf.pool();
// NGX_CONF_TAKE2 ensures that args contains 3 elements
let args = cf.args();

let mut encoded = if let Some(arg) = args[2].strip_prefix(b"data:") {
arg
} else {
match crate::util::read_to_ngx_str(cf, &args[2]) {
Ok(x) => x,
Err(e) => return cf.error(args[0], &e),
}
};

crate::util::ngx_str_trim(&mut encoded);

let len = encoded.len.div_ceil(4) * 3;
let mut key = ngx_str_t {
data: pool.alloc_unaligned(len).cast(),
len,
};

if key.data.is_null() {
return NGX_CONF_ERROR;
}

if !Status(unsafe { ngx_decode_base64url(&mut key, &mut encoded) }).is_ok() {
return c"invalid base64 encoded value".as_ptr().cast_mut();
}

issuer.eab_key = Some(issuer::ExternalAccountKey { kid: args[1], key });

NGX_CONF_OK
}

extern "C" fn cmd_issuer_set_uri(
cf: *mut ngx_conf_t,
_cmd: *mut ngx_command_t,
Expand Down
8 changes: 8 additions & 0 deletions src/conf/issuer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ pub struct Issuer {
pub uri: Uri,
pub account_key: PrivateKey,
pub contacts: Vec<ngx_str_t, Pool>,
pub eab_key: Option<ExternalAccountKey>,
pub resolver: Option<NonNull<ngx_resolver_t>>,
pub resolver_timeout: ngx_msec_t,
pub ssl_trusted_certificate: ngx_str_t,
Expand All @@ -58,6 +59,12 @@ pub struct Issuer {
pub data: Option<&'static RwLock<IssuerContext>>,
}

#[derive(Debug)]
pub struct ExternalAccountKey {
pub kid: ngx_str_t,
pub key: ngx_str_t,
}

#[derive(Debug, Error)]
pub enum IssuerError {
#[error("cannot load account key: {0}")]
Expand Down Expand Up @@ -88,6 +95,7 @@ impl Issuer {
uri: Default::default(),
account_key: PrivateKey::Unset,
contacts: Vec::new_in(alloc.clone()),
eab_key: None,
resolver: None,
resolver_timeout: NGX_CONF_UNSET_MSEC,
ssl_trusted_certificate: ngx_str_t::empty(),
Expand Down
Loading
Loading