Skip to content

Commit af2e525

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 09066e0 commit af2e525

File tree

5 files changed

+162
-3
lines changed

5 files changed

+162
-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: 73 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,15 @@ impl Problem {
346353
}
347354
}
348355

356+
fn deserialize_null_as_default<'de, D, T>(deserializer: D) -> Result<T, D::Error>
357+
where
358+
D: serde::de::Deserializer<'de>,
359+
T: serde::de::Deserialize<'de> + Default,
360+
{
361+
let val = Option::<T>::deserialize(deserializer)?;
362+
Ok(val.unwrap_or_default())
363+
}
364+
349365
fn deserialize_vec_of_uri<'de, D>(deserializer: D) -> Result<Vec<Uri>, D::Error>
350366
where
351367
D: serde::Deserializer<'de>,
@@ -385,6 +401,62 @@ where
385401
mod tests {
386402
use super::*;
387403

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