Skip to content

Commit 4dadf2c

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 afd40f0 commit 4dadf2c

File tree

5 files changed

+165
-3
lines changed

5 files changed

+165
-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: 18 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,20 @@ where
299301
})
300302
.transpose()?;
301303

304+
self.profile = match self.issuer.profile {
305+
Profile::Required(x) => Some(x),
306+
Profile::Preferred(x) if self.directory.meta.profiles.contains_key(x) => Some(x),
307+
Profile::Preferred(x) => {
308+
ngx::ngx_log_error!(
309+
nginx_sys::NGX_LOG_NOTICE,
310+
self.log.as_ptr(),
311+
"acme profile \"{x}\" is not supported by the server"
312+
);
313+
None
314+
}
315+
_ => None,
316+
};
317+
302318
let payload = types::AccountRequest {
303319
terms_of_service_agreed: self.issuer.accept_tos,
304320
contact: &self.issuer.contacts,
@@ -350,6 +366,7 @@ where
350366
identifiers: &identifiers,
351367
not_before: None,
352368
not_after: None,
369+
profile: self.profile,
353370
};
354371

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

src/acme/types.rs

Lines changed: 76 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,8 @@ pub struct DirectoryMetadata {
2222
pub website: Option<Uri>,
2323
pub caa_identities: Option<Vec<String>>,
2424
pub external_account_required: Option<bool>,
25+
#[serde(deserialize_with = "deserialize_null_as_default")]
26+
pub profiles: std::collections::BTreeMap<String, IgnoredAny>,
2527
}
2628

2729
/// RFC8555 Section 7.1.1 Directory
@@ -118,6 +120,8 @@ pub struct OrderRequest<'a> {
118120
pub not_before: Option<String>,
119121
#[serde(skip_serializing_if = "Option::is_none")]
120122
pub not_after: Option<String>,
123+
#[serde(skip_serializing_if = "Option::is_none")]
124+
pub profile: Option<&'a str>,
121125
}
122126

123127
#[derive(Clone, Debug, Deserialize, Eq, PartialEq)]
@@ -200,6 +204,7 @@ pub enum ErrorKind {
200204
ExternalAccountRequired,
201205
IncorrectResponse,
202206
InvalidContact,
207+
InvalidProfile,
203208
Malformed,
204209
OrderNotReady,
205210
RateLimited,
@@ -232,6 +237,7 @@ const ERROR_KIND: &[(&str, ErrorKind)] = &[
232237
),
233238
("incorrectResponse", ErrorKind::IncorrectResponse),
234239
("invalidContact", ErrorKind::InvalidContact),
240+
("invalidProfile", ErrorKind::InvalidProfile),
235241
("malformed", ErrorKind::Malformed),
236242
("orderNotReady", ErrorKind::OrderNotReady),
237243
("rateLimited", ErrorKind::RateLimited),
@@ -336,6 +342,7 @@ impl Problem {
336342

337343
ErrorKind::BadCsr
338344
| ErrorKind::Caa
345+
| ErrorKind::InvalidProfile
339346
| ErrorKind::RejectedIdentifier
340347
| ErrorKind::UnsupportedIdentifier => ProblemCategory::Order,
341348

@@ -346,6 +353,18 @@ impl Problem {
346353
}
347354
}
348355

356+
/// Deserializes value of type T, while handling explicit `null` as a Default.
357+
///
358+
/// This helper complements `#[serde(default)]`, which only works on omitted fields.
359+
fn deserialize_null_as_default<'de, D, T>(deserializer: D) -> Result<T, D::Error>
360+
where
361+
D: serde::de::Deserializer<'de>,
362+
T: serde::de::Deserialize<'de> + Default,
363+
{
364+
let val = Option::<T>::deserialize(deserializer)?;
365+
Ok(val.unwrap_or_default())
366+
}
367+
349368
fn deserialize_vec_of_uri<'de, D>(deserializer: D) -> Result<Vec<Uri>, D::Error>
350369
where
351370
D: serde::Deserializer<'de>,
@@ -385,6 +404,62 @@ where
385404
mod tests {
386405
use super::*;
387406

407+
#[test]
408+
fn directory() {
409+
// complete example
410+
let _: Directory = serde_json::from_str(
411+
r#"{
412+
"newNonce": "https://example.com/acme/new-nonce",
413+
"newAccount": "https://example.com/acme/new-account",
414+
"newOrder": "https://example.com/acme/new-order",
415+
"newAuthz": "https://example.com/acme/new-authz",
416+
"revokeCert": "https://example.com/acme/revoke-cert",
417+
"keyChange": "https://example.com/acme/key-change",
418+
"meta": {
419+
"termsOfService": "https://example.com/acme/terms/2017-5-30",
420+
"website": "https://www.example.com/",
421+
"caaIdentities": ["example.com"],
422+
"externalAccountRequired": false,
423+
"profiles": {
424+
"profile1": "https://example.com/acme/docs/profiles#profile1",
425+
"profile2": "https://example.com/acme/docs/profiles#profile2"
426+
}
427+
}
428+
}"#,
429+
)
430+
.unwrap();
431+
432+
// minimal
433+
let _: Directory = serde_json::from_str(
434+
r#"{
435+
"newNonce": "https://example.com/acme/new-nonce",
436+
"newAccount": "https://example.com/acme/new-account",
437+
"newOrder": "https://example.com/acme/new-order"
438+
}"#,
439+
)
440+
.unwrap();
441+
442+
// null
443+
let _: Directory = serde_json::from_str(
444+
r#"{
445+
"newNonce": "https://example.com/acme/new-nonce",
446+
"newAccount": "https://example.com/acme/new-account",
447+
"newOrder": "https://example.com/acme/new-order",
448+
"newAuthz": null,
449+
"revokeCert": null,
450+
"keyChange": null,
451+
"meta": {
452+
"termsOfService": null,
453+
"website": null,
454+
"caaIdentities": null,
455+
"externalAccountRequired": null,
456+
"profiles": null
457+
}
458+
}"#,
459+
)
460+
.unwrap();
461+
}
462+
388463
#[test]
389464
fn order() {
390465
let _order: Order = serde_json::from_str(

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)