Skip to content
Merged
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
4 changes: 2 additions & 2 deletions .github/workflows/linux.yml
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ permissions: {}
env:
MARGS: "-j5"
CFLAGS: "-g"
pebble-version: v2.7.0
pebble-version: v2.8.0

jobs:
linux:
Expand Down Expand Up @@ -98,4 +98,4 @@ jobs:
run: |
export PATH=$PATH:$HOME/go/bin
[ -x "$HOME/venv/bin/activate" ] && source $HOME/venv/bin/activate
pytest -v
pytest -v
50 changes: 50 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ into your Apache server log where `mod_md` logs its version at startup.
* [Have a failover ACME CA](#acme-failover)
* [Revocations](#revocations)
* [Use ACME Profiles](#profiles)
* [Use ACME ARI Extension](#acme-ari)
- Stapling
* [Staple all my certificates](#how-to-staple-all-my-certificates)
* [Staple some of my certificates](#how-to-staple-some-of-my-certificates)
Expand Down Expand Up @@ -861,6 +862,47 @@ MDProfileMandatory on

and cert renewal will fail of the profile is not supported by the CA.

# ACME ARI

ACME ARI is an extension to the ACME protocol described in [rfc9773](https://datatracker.ietf.org/doc/rfc9773/). It allows
a client to query the CA for the recommended time window to renew a certificate. This serves two purposes:

1. Steer clients to renewal times that are best suited for the CA. This allows a CA to mitigate peak traffic hours and planned downtime.
2. Inform clients that they need to renew a certificate because it has/will soon become invalid. This may be due to a a certificate having been revoked or to trouble in the certificates trust chain (this has happened in the past).

By default, `mod_md` uses ARI when offered by a CA (Note: it needs at least OpenSSL v1.1.0 for that or the feature
is not available). This happens *in addition* to the "normal" renewal handling. A renewal will be done when ARI says so *or* when the renewal window has been reached. Whichever is earlier. This means things will continue to happen as before, **except** when an abnormal, early renewal is signalled by the CA. Then `mod_md` will do that.

If you want to rely mainly on ARI, configure a smaller `MDRenewWindow`. If you do **not**, for whatever reasons - you know best,
use ARI in any way, configure `MDRenewViaARI off`.

### ARI Renewal Explanations

When a CA advises about renewals via ARI, it *may* accompany it with an "explanationURL" field. At that link,
a human operator should find information about *why* the renewal is advised. Regular renewals should *not*
carry such a URL, indicating normal, unexciting operations.

`mod_md` will log an "explanationURL", when causing a renew, into the server log at level `NOTICE` with the text
`md(<name>): CA advises renew via ARI now, for explanation see <URL>`.

### ARI Manual Checks

There are several examples to be found on the web how you can check the ARI status of your certificate yourself.
Should you have the `md-status` handler configured in your server, you can retrieve the JSON describing a MDomain
there. For each certificate listed, you will see the field `ari-cert-id`. This has the form of `<base64-of-issuer-auth-id>.<base64-of-cert-serial>`.

You can use `curl` to get the ARI of a certificate yourself. For Let's Encrypt:


```sh
> curl https://acme-v02.api.letsencrypt.org/directory
...
"renewalInfo": "https://acme-v02.api.letsencrypt.org/acme/renewal-info",
...
> curl https://acme-v02.api.letsencrypt.org/acme/renewal-info/<ari-cert-id>
...ARI JSON...
```

# Just the Stapling, Mam!

If you just want to use the new OCSP Stapling feature of the module, load it into your apache and configure
Expand Down Expand Up @@ -1805,6 +1847,7 @@ checks by mod_md in v1.1.x which are now eliminated. If you have many domains, t
* [MDProfile](#mdprofile)
* [MDProfileMandatory](#mdprofilemandatory)
* [MDHttpProxy](#mdhttpproxy)
* [MDRenewViaARI](#mdrenewviaari)
* [MDRenewWindow](#mdrenewwindow--when-to-renew)
* [MDRequireHttps](#mdrequirehttps)
* [MDRetryFailover](#mdretryfailover)
Expand Down Expand Up @@ -2478,6 +2521,13 @@ Default: off
Select if a certificate renewal should make a configured profile mandatory, e.g. fail renewal if
the CA does not support it.

## MDRenewViaARI
`MDRenewViaARI on|off`
Default: on

En-/Disable certificate renewals triggered via the ACME ARI extension (rfc9773). These renewals
happen *in addition* to the mechanism controlled by [MDRenewWindow](#mdrenewwindow--when-to-renew).

# Test Suite

The repository comes with test suites. There are some unit tests using `libcheck` and a large overall test
Expand Down
3 changes: 3 additions & 0 deletions src/md.h
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,7 @@ struct md_t {
const char *ca_eab_hmac; /* optional HMAC for external account binding */
const char *profile; /* optional cert profile to order */
int profile_mandatory; /* if profile, when given, is mandatory */
int ari_renewals; /* if ACME ARI (RFC 9773) can trigger renewals */

const char *state_descr; /* description of state of NULL */

Expand All @@ -119,6 +120,8 @@ struct md_t {
#define MD_KEY_ACTIVATION_DELAY "activation-delay"
#define MD_KEY_ACTIVITY "activity"
#define MD_KEY_AGREEMENT "agreement"
#define MD_KEY_ARI_CERT_ID "ari-cert-id"
#define MD_KEY_ARI_RENEWALS "ari-renewals"
#define MD_KEY_AUTHORIZATIONS "authorizations"
#define MD_KEY_BITS "bits"
#define MD_KEY_CA "ca"
Expand Down
18 changes: 10 additions & 8 deletions src/md_acme.c
Original file line number Diff line number Diff line change
Expand Up @@ -332,7 +332,7 @@ static apr_status_t acmev2_GET_as_POST_init(md_acme_req_t *req, void *baton)
return md_acme_req_body_init(req, NULL);
}

static apr_status_t md_acme_req_send(md_acme_req_t *req)
static apr_status_t md_acme_req_send(md_acme_req_t *req, int get_as_post)
{
apr_status_t rv;
md_acme_t *acme = req->acme;
Expand All @@ -352,7 +352,7 @@ static apr_status_t md_acme_req_send(md_acme_req_t *req)
if (APR_SUCCESS != rv) goto leave;
}

if (!strcmp("GET", req->method) && !req->on_init && !req->req_json) {
if (get_as_post && !strcmp("GET", req->method) && !req->on_init && !req->req_json) {
/* See <https://ietf-wg-acme.github.io/acme/draft-ietf-acme-acme.html#rfc.section.6.3>
* and <https://mailarchive.ietf.org/arch/msg/acme/sotffSQ0OWV-qQJodLwWYWcEVKI>
* and <https://community.letsencrypt.org/t/acme-v2-scheduled-deprecation-of-unauthenticated-resource-gets/74380>
Expand Down Expand Up @@ -420,7 +420,7 @@ static apr_status_t md_acme_req_send(md_acme_req_t *req)

if (APR_EAGAIN == rv && req->max_retries > 0) {
--req->max_retries;
rv = md_acme_req_send(req);
rv = md_acme_req_send(req, 1);
}
req = NULL;

Expand Down Expand Up @@ -449,14 +449,15 @@ apr_status_t md_acme_POST(md_acme_t *acme, const char *url,
req->on_err = on_err;
req->baton = baton;

return md_acme_req_send(req);
return md_acme_req_send(req, 1);
}

apr_status_t md_acme_GET(md_acme_t *acme, const char *url,
md_acme_req_init_cb *on_init,
md_acme_req_json_cb *on_json,
md_acme_req_res_cb *on_res,
md_acme_req_err_cb *on_err,
md_acme_req_err_cb *on_err,
int get_as_post,
void *baton)
{
md_acme_req_t *req;
Expand All @@ -472,7 +473,7 @@ apr_status_t md_acme_GET(md_acme_t *acme, const char *url,
req->on_err = on_err;
req->baton = baton;

return md_acme_req_send(req);
return md_acme_req_send(req, get_as_post);
}

void md_acme_report_result(md_acme_t *acme, apr_status_t rv, struct md_result_t *result)
Expand Down Expand Up @@ -507,15 +508,15 @@ static apr_status_t on_got_json(md_acme_t *acme, apr_pool_t *p, const apr_table_
}

apr_status_t md_acme_get_json(struct md_json_t **pjson, md_acme_t *acme,
const char *url, apr_pool_t *p)
const char *url, int get_as_post, apr_pool_t *p)
{
apr_status_t rv;
json_ctx ctx;

ctx.pool = p;
ctx.json = NULL;

rv = md_acme_GET(acme, url, NULL, on_got_json, NULL, NULL, &ctx);
rv = md_acme_GET(acme, url, NULL, on_got_json, NULL, NULL, get_as_post, &ctx);
*pjson = (APR_SUCCESS == rv)? ctx.json : NULL;
return rv;
}
Expand Down Expand Up @@ -720,6 +721,7 @@ static apr_status_t update_directory(const md_http_response_t *res, void *data)
acme->api.v2.revoke_cert = md_json_dups(acme->p, json, "revokeCert", NULL);
acme->api.v2.key_change = md_json_dups(acme->p, json, "keyChange", NULL);
acme->api.v2.new_nonce = md_json_dups(acme->p, json, "newNonce", NULL);
acme->api.v2.renewal_info = md_json_dups(acme->p, json, "renewalInfo", NULL);
/* RFC 8555 only requires "directory" and "newNonce" resources.
* mod_md uses "newAccount" and "newOrder" so check for them.
* But mod_md does not use the "revokeCert" or "keyChange"
Expand Down
4 changes: 3 additions & 1 deletion src/md_acme.h
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,7 @@ struct md_acme_t {
const char *key_change;
const char *revoke_cert;
const char *new_nonce;
const char *renewal_info;
struct apr_array_header_t *profiles;
} v2;
} api;
Expand Down Expand Up @@ -275,6 +276,7 @@ apr_status_t md_acme_GET(md_acme_t *acme, const char *url,
md_acme_req_json_cb *on_json,
md_acme_req_res_cb *on_res,
md_acme_req_err_cb *on_err,
int get_as_post,
void *baton);
/**
* Perform a POST against the ACME url. If a on_json callback is given and
Expand All @@ -301,7 +303,7 @@ apr_status_t md_acme_POST(md_acme_t *acme, const char *url,
* Retrieve a JSON resource from the ACME server
*/
apr_status_t md_acme_get_json(struct md_json_t **pjson, md_acme_t *acme,
const char *url, apr_pool_t *p);
const char *url, int get_as_post, apr_pool_t *p);


apr_status_t md_acme_req_body_init(md_acme_req_t *req, struct md_json_t *jpayload);
Expand Down
2 changes: 1 addition & 1 deletion src/md_acme_authz.c
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,7 @@ apr_status_t md_acme_authz_update(md_acme_authz_t *authz, md_acme_t *acme, apr_p
err = "unable to parse response";
log_level = MD_LOG_ERR;

if (APR_SUCCESS == (rv = md_acme_get_json(&json, acme, authz->url, p))
if (APR_SUCCESS == (rv = md_acme_get_json(&json, acme, authz->url, 1, p))
&& (s = md_json_gets(json, MD_KEY_STATUS, NULL))) {

authz->domain = md_json_gets(json, MD_KEY_IDENTIFIER, MD_KEY_VALUE, NULL);
Expand Down
Loading
Loading