Skip to content

Commit 4c4a168

Browse files
committed
ACME: allow specifying preferred or required profile.
This change implements [draft-ietf-acme-profiles] version 00. [draft-ietf-acme-profiles]: https://datatracker.ietf.org/doc/draft-ietf-acme-profiles/ Fixes: #4.
1 parent 013f8f6 commit 4c4a168

File tree

6 files changed

+104
-3
lines changed

6 files changed

+104
-3
lines changed

README.md

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,12 @@ The module implements following specifications:
1515
- Only HTTP-01 challenge type is supported
1616
- [RFC8737] (ACME TLS Application-Layer Protocol Negotiation (ALPN) Challenge
1717
Extension)
18+
- [draft-ietf-acme-profiles] (ACME Profiles Extension, version 00)
1819

1920
[NGINX]: https://nginx.org/
2021
[RFC8555]: https://datatracker.ietf.org/doc/html/rfc8555
2122
[RFC8737]: https://datatracker.ietf.org/doc/html/rfc8737
23+
[draft-ietf-acme-profiles]: https://datatracker.ietf.org/doc/draft-ietf-acme-profiles/
2224

2325
## Getting Started
2426

@@ -283,6 +285,21 @@ In both cases, the key is expected to be encoded in
283285

284286
[RFC8555#eab]: https://datatracker.ietf.org/doc/html/rfc8555#section-7.3.4
285287

288+
### profile
289+
290+
**Syntax:** **`profile`** _`name`_ \[`require`]
291+
292+
**Default:** -
293+
294+
**Context:** acme_issuer
295+
296+
Requests the supported [certificate profile][draft-ietf-acme-profiles]
297+
_`name`_ from the ACME server.
298+
299+
The `require` parameter will cause the account registration and certificate
300+
renewals to fail if the ACME server does not advertise support for the
301+
specified profile.
302+
286303
### ssl_trusted_certificate
287304

288305
**Syntax:** **`ssl_trusted_certificate`** _`file`_

src/acme.rs

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ use types::{AccountStatus, ProblemCategory};
2323
use self::account_key::{AccountKey, AccountKeyError};
2424
use self::types::{AuthorizationStatus, ChallengeKind, ChallengeStatus, OrderStatus};
2525
use crate::conf::identifier::Identifier;
26-
use crate::conf::issuer::Issuer;
26+
use crate::conf::issuer::{Issuer, Profile};
2727
use crate::conf::order::CertificateOrder;
2828
use crate::net::http::HttpClient;
2929
use crate::time::Time;
@@ -72,6 +72,7 @@ where
7272
log: NonNull<nginx_sys::ngx_log_t>,
7373
key: AccountKey,
7474
account: Option<String>,
75+
profile: Option<&'a str>,
7576
nonce: NoncePool,
7677
directory: types::Directory,
7778
solvers: Vec<Box<dyn solvers::ChallengeSolver + Send + 'a>>,
@@ -131,6 +132,7 @@ where
131132
log,
132133
key,
133134
account: None,
135+
profile: None,
134136
nonce: Default::default(),
135137
directory: Default::default(),
136138
solvers: Vec::new(),
@@ -299,6 +301,24 @@ where
299301
})
300302
.transpose()?;
301303

304+
self.profile = match self.issuer.profile {
305+
Profile::Required(x) | Profile::Preferred(x)
306+
if self.directory.meta.profiles.contains_key(x) =>
307+
{
308+
Some(x)
309+
}
310+
Profile::Preferred(x) => {
311+
ngx::ngx_log_error!(
312+
nginx_sys::NGX_LOG_NOTICE,
313+
self.log.as_ptr(),
314+
"acme profile \"{x}\" is not supported by the server"
315+
);
316+
None
317+
}
318+
Profile::Required(_) => return Err(NewAccountError::Profile),
319+
_ => None,
320+
};
321+
302322
let payload = types::AccountRequest {
303323
terms_of_service_agreed: self.issuer.accept_tos,
304324
contact: &self.issuer.contacts,
@@ -350,6 +370,7 @@ where
350370
identifiers: &identifiers,
351371
not_before: None,
352372
not_after: None,
373+
profile: self.profile,
353374
};
354375

355376
let payload = serde_json::to_string(&payload).map_err(RequestError::RequestFormat)?;

src/acme/error.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,9 @@ pub enum NewAccountError {
2121
#[error("external account key required")]
2222
ExternalAccount,
2323

24+
#[error("profile is not supported")]
25+
Profile,
26+
2427
#[error(transparent)]
2528
Protocol(#[from] Problem),
2629

@@ -47,6 +50,7 @@ impl NewAccountError {
4750
pub fn is_invalid(&self) -> bool {
4851
match self {
4952
Self::ExternalAccount => true,
53+
Self::Profile => true,
5054
Self::Protocol(err) => matches!(
5155
err.category(),
5256
ProblemCategory::Account | ProblemCategory::Malformed

src/acme/types.rs

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ use std::string::{String, ToString};
1010

1111
use http::Uri;
1212
use ngx::collections::Vec;
13-
use serde::{Deserialize, Serialize};
13+
use serde::{de::IgnoredAny, Deserialize, Serialize};
1414

1515
use crate::conf::identifier::Identifier;
1616

@@ -22,6 +22,7 @@ pub struct DirectoryMetadata {
2222
pub website: Option<Uri>,
2323
pub caa_identities: Vec<String>,
2424
pub external_account_required: Option<bool>,
25+
pub profiles: std::collections::BTreeMap<String, IgnoredAny>,
2526
}
2627

2728
/// RFC8555 Section 7.1.1 Directory
@@ -118,6 +119,8 @@ pub struct OrderRequest<'a> {
118119
pub not_before: Option<String>,
119120
#[serde(skip_serializing_if = "Option::is_none")]
120121
pub not_after: Option<String>,
122+
#[serde(skip_serializing_if = "Option::is_none")]
123+
pub profile: Option<&'a str>,
121124
}
122125

123126
#[derive(Clone, Debug, Deserialize, Eq, PartialEq)]
@@ -200,6 +203,7 @@ pub enum ErrorKind {
200203
ExternalAccountRequired,
201204
IncorrectResponse,
202205
InvalidContact,
206+
InvalidProfile,
203207
Malformed,
204208
OrderNotReady,
205209
RateLimited,
@@ -232,6 +236,7 @@ const ERROR_KIND: &[(&str, ErrorKind)] = &[
232236
),
233237
("incorrectResponse", ErrorKind::IncorrectResponse),
234238
("invalidContact", ErrorKind::InvalidContact),
239+
("invalidProfile", ErrorKind::InvalidProfile),
235240
("malformed", ErrorKind::Malformed),
236241
("orderNotReady", ErrorKind::OrderNotReady),
237242
("rateLimited", ErrorKind::RateLimited),
@@ -331,6 +336,7 @@ impl Problem {
331336
| ErrorKind::BadSignatureAlgorithm
332337
| ErrorKind::ExternalAccountRequired
333338
| ErrorKind::InvalidContact
339+
| ErrorKind::InvalidProfile
334340
| ErrorKind::UnsupportedContact
335341
| ErrorKind::UserActionRequired => ProblemCategory::Account,
336342

src/conf.rs

Lines changed: 45 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,7 @@ pub static mut NGX_HTTP_ACME_COMMANDS: [ngx_command_t; 4] = [
8484
ngx_command_t::empty(),
8585
];
8686

87-
static mut NGX_HTTP_ACME_ISSUER_COMMANDS: [ngx_command_t; 10] = [
87+
static mut NGX_HTTP_ACME_ISSUER_COMMANDS: [ngx_command_t; 11] = [
8888
ngx_command_t {
8989
name: ngx_string!("uri"),
9090
type_: NGX_CONF_TAKE1 as ngx_uint_t,
@@ -125,6 +125,14 @@ static mut NGX_HTTP_ACME_ISSUER_COMMANDS: [ngx_command_t; 10] = [
125125
offset: 0,
126126
post: ptr::null_mut(),
127127
},
128+
ngx_command_t {
129+
name: ngx_string!("profile"),
130+
type_: nginx_sys::NGX_CONF_TAKE12 as ngx_uint_t,
131+
set: Some(cmd_issuer_set_profile),
132+
conf: 0,
133+
offset: 0,
134+
post: ptr::null_mut(),
135+
},
128136
ngx_command_t {
129137
name: ngx_string!("ssl_trusted_certificate"),
130138
type_: NGX_CONF_TAKE1 as ngx_uint_t,
@@ -496,6 +504,42 @@ extern "C" fn cmd_issuer_set_external_account_key(
496504
NGX_CONF_OK
497505
}
498506

507+
extern "C" fn cmd_issuer_set_profile(
508+
cf: *mut ngx_conf_t,
509+
_cmd: *mut ngx_command_t,
510+
conf: *mut c_void,
511+
) -> *mut c_char {
512+
let cf = unsafe { cf.as_mut().expect("cf") };
513+
let issuer = unsafe { conf.cast::<Issuer>().as_mut().expect("issuer conf") };
514+
515+
if !matches!(issuer.profile, issuer::Profile::Unset) {
516+
return NGX_CONF_DUPLICATE;
517+
}
518+
519+
// NGX_CONF_TAKE12 ensures that args contains either 2 or 3 elements
520+
let args = cf.args();
521+
522+
// SAFETY: the value is not empty, well aligned, and the conversion result is assigned to an
523+
// object in the same pool.
524+
let Ok(profile) = (unsafe { conf_value_to_str(&args[1]) }) else {
525+
return NGX_CONF_INVALID_VALUE;
526+
};
527+
528+
let require = match args.get(2) {
529+
Some(x) if x.as_ref() == b"require" => true,
530+
Some(_) => return NGX_CONF_INVALID_VALUE,
531+
None => false,
532+
};
533+
534+
issuer.profile = if require {
535+
issuer::Profile::Required(profile)
536+
} else {
537+
issuer::Profile::Preferred(profile)
538+
};
539+
540+
NGX_CONF_OK
541+
}
542+
499543
extern "C" fn cmd_issuer_set_uri(
500544
cf: *mut ngx_conf_t,
501545
_cmd: *mut ngx_command_t,

src/conf/issuer.rs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ pub struct Issuer {
5050
pub challenge: Option<ChallengeKind>,
5151
pub contacts: Vec<&'static str, Pool>,
5252
pub eab_key: Option<ExternalAccountKey>,
53+
pub profile: Profile,
5354
pub resolver: Option<NonNull<ngx_resolver_t>>,
5455
pub resolver_timeout: ngx_msec_t,
5556
pub ssl_trusted_certificate: ngx_str_t,
@@ -70,6 +71,13 @@ pub struct ExternalAccountKey {
7071
pub key: ngx_str_t,
7172
}
7273

74+
#[derive(Debug)]
75+
pub enum Profile {
76+
Preferred(&'static str),
77+
Required(&'static str),
78+
Unset,
79+
}
80+
7381
#[derive(Debug, Error)]
7482
pub enum IssuerError {
7583
#[error("cannot load account key: {0}")]
@@ -102,6 +110,7 @@ impl Issuer {
102110
challenge: None,
103111
contacts: Vec::new_in(alloc.clone()),
104112
eab_key: None,
113+
profile: Profile::Unset,
105114
resolver: None,
106115
resolver_timeout: NGX_CONF_UNSET_MSEC,
107116
ssl_trusted_certificate: ngx_str_t::empty(),

0 commit comments

Comments
 (0)