Skip to content

Commit 8ffa19a

Browse files
committed
mod_md: update to v2.4.26
- Using OCSP stapling information to trigger certificate renewals. Proposed by @frasertweedale. - Added directive `MDCheckInterval` to control how often the server checks for detected revocations. Added proposals for configurations in the README.md chapter "Revocations". - OCSP stapling: accept OCSP responses without a `nextUpdate` entry which is allowed in RFC 6960. Treat those as having an update interval of 12 hours. Added by @frasertweedale. - Adapt OpenSSL usage to changes in their API. By Yann Ylavic. Test Updates - workarounds for using Pebble v2.5 - disable EAB tests for Pebble since v2.5 no longer supports HS256 FWT for EAB keys - some stability improvemnets in error/warning checks git-svn-id: https://svn.apache.org/repos/asf/httpd/httpd/trunk@1916861 13f79535-47bb-0310-9956-ffa450edef68
1 parent d3c4bf9 commit 8ffa19a

17 files changed

+177
-283
lines changed

changes-entries/md_2.4.26.txt

+10
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
* mod_md:
2+
- Using OCSP stapling information to trigger certificate renewals. Proposed
3+
by @frasertweedale.
4+
- Added directive `MDCheckInterval` to control how often the server checks
5+
for detected revocations. Added proposals for configurations in the
6+
README.md chapter "Revocations".
7+
- OCSP stapling: accept OCSP responses without a `nextUpdate` entry which is
8+
allowed in RFC 6960. Treat those as having an update interval of 12 hours.
9+
Added by @frasertweedale.
10+
- Adapt OpenSSL usage to changes in their API. By Yann Ylavic.

docs/manual/mod/mod_md.xml

+25
Original file line numberDiff line numberDiff line change
@@ -1512,4 +1512,29 @@ MDMessageCmd /etc/apache/md-message
15121512
</usage>
15131513
</directivesynopsis>
15141514

1515+
<directivesynopsis>
1516+
<name>MDCheckInterval</name>
1517+
<description>Determines how often certificates are checked</description>
1518+
<syntax>MDCheckInterval <var>duration</var></syntax>
1519+
<default>MDCheckInterval 12h</default>
1520+
<contextlist>
1521+
<context>server config</context>
1522+
</contextlist>
1523+
<compatibility>Available in version 2.4.60 and later</compatibility>
1524+
<usage>
1525+
<p>
1526+
The time between certificate checks. By default, the validity
1527+
and need for renewals is checked twice a day. This interval is
1528+
not followed precisely. Instead the module randomly applies
1529+
a +/-50% jitter to it. With the default of 12 hours, this
1530+
means the actual time between runs varies between 6 and 18
1531+
hours, jittered anew every run. This helps to mitigate
1532+
traffic peaks at ACME servers.
1533+
</p><p>
1534+
The minimum duration you may configure is 1 second. It is
1535+
not recommended to use such short times in production.
1536+
</p>
1537+
</usage>
1538+
</directivesynopsis>
1539+
15151540
</modulesynopsis>

modules/md/md_ocsp.c

+8-7
Original file line numberDiff line numberDiff line change
@@ -678,12 +678,6 @@ static apr_status_t ostat_on_resp(const md_http_response_t *resp, void *baton)
678678
md_result_log(update->result, MD_LOG_DEBUG);
679679
goto cleanup;
680680
}
681-
if (!bnextup) {
682-
rv = APR_EINVAL;
683-
md_result_set(update->result, rv, "OCSP basicresponse reports not valid dates");
684-
md_result_log(update->result, MD_LOG_DEBUG);
685-
goto cleanup;
686-
}
687681

688682
/* Coming here, we have a response for our certid and it is either GOOD
689683
* or REVOKED. Both cases we want to remember and use in stapling. */
@@ -698,7 +692,14 @@ static apr_status_t ostat_on_resp(const md_http_response_t *resp, void *baton)
698692
new_der.free_data = md_openssl_free;
699693
nstat = (bstatus == V_OCSP_CERTSTATUS_GOOD)? MD_OCSP_CERT_ST_GOOD : MD_OCSP_CERT_ST_REVOKED;
700694
valid.start = bup? md_asn1_generalized_time_get(bup) : apr_time_now();
701-
valid.end = md_asn1_generalized_time_get(bnextup);
695+
if (bnextup) {
696+
valid.end = md_asn1_generalized_time_get(bnextup);
697+
}
698+
else {
699+
/* nextUpdate not set; default to 12 hours.
700+
* Refresh attempts will be started some time earlier. */
701+
valid.end = valid.start + apr_time_from_sec(MD_SECS_PER_DAY / 2);
702+
}
702703

703704
/* First, update the instance with a copy */
704705
apr_thread_mutex_lock(ostat->reg->mutex);

modules/md/md_reg.c

+36
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
#include "md_json.h"
3232
#include "md_result.h"
3333
#include "md_reg.h"
34+
#include "md_ocsp.h"
3435
#include "md_store.h"
3536
#include "md_status.h"
3637
#include "md_tailscale.h"
@@ -1321,3 +1322,38 @@ md_job_t *md_reg_job_make(md_reg_t *reg, const char *mdomain, apr_pool_t *p)
13211322
{
13221323
return md_job_make(p, reg->store, MD_SG_STAGING, mdomain, reg->min_delay);
13231324
}
1325+
1326+
static int get_cert_count(const md_t *md)
1327+
{
1328+
if (md->cert_files && md->cert_files->nelts) {
1329+
return md->cert_files->nelts;
1330+
}
1331+
return md_pkeys_spec_count(md->pks);
1332+
}
1333+
1334+
int md_reg_has_revoked_certs(md_reg_t *reg, struct md_ocsp_reg_t *ocsp,
1335+
const md_t *md, apr_pool_t *p)
1336+
{
1337+
const md_pubcert_t *pubcert;
1338+
const md_cert_t *cert;
1339+
md_timeperiod_t ocsp_valid;
1340+
md_ocsp_cert_stat_t cert_stat;
1341+
apr_status_t rv = APR_SUCCESS;
1342+
int i;
1343+
1344+
if (!md->stapling || !ocsp)
1345+
return 0;
1346+
1347+
for (i = 0; i < get_cert_count(md); ++i) {
1348+
if (APR_SUCCESS != md_reg_get_pubcert(&pubcert, reg, md, i, p))
1349+
continue;
1350+
cert = APR_ARRAY_IDX(pubcert->certs, 0, const md_cert_t*);
1351+
if(!cert)
1352+
continue;
1353+
rv = md_ocsp_get_meta(&cert_stat, &ocsp_valid, ocsp, cert, p, md);
1354+
if (APR_SUCCESS == rv && cert_stat == MD_OCSP_CERT_ST_REVOKED) {
1355+
return 1;
1356+
}
1357+
}
1358+
return 0;
1359+
}

modules/md/md_reg.h

+7
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ struct md_pkey_t;
2323
struct md_cert_t;
2424
struct md_result_t;
2525
struct md_pkey_spec_t;
26+
struct md_ocsp_reg_t;
2627

2728
#include "md_store.h"
2829

@@ -310,4 +311,10 @@ apr_status_t md_reg_lock_global(md_reg_t *reg, apr_pool_t *p);
310311
*/
311312
void md_reg_unlock_global(md_reg_t *reg, apr_pool_t *p);
312313

314+
/**
315+
* @return != 0 iff `md` has any certificates known to be REVOKED.
316+
*/
317+
int md_reg_has_revoked_certs(md_reg_t *reg, struct md_ocsp_reg_t *ocsp,
318+
const md_t *md, apr_pool_t *p);
319+
313320
#endif /* mod_md_md_reg_h */

modules/md/md_version.h

+2-2
Original file line numberDiff line numberDiff line change
@@ -27,15 +27,15 @@
2727
* @macro
2828
* Version number of the md module as c string
2929
*/
30-
#define MOD_MD_VERSION "2.4.25"
30+
#define MOD_MD_VERSION "2.4.26"
3131

3232
/**
3333
* @macro
3434
* Numerical representation of the version number of the md module
3535
* release. This is a 24 bit number with 8 bits for major number, 8 bits
3636
* for minor and 8 bits for patch. Version 1.2.3 becomes 0x010203.
3737
*/
38-
#define MOD_MD_VERSION_NUM 0x020419
38+
#define MOD_MD_VERSION_NUM 0x02041a
3939

4040
#define MD_ACME_DEF_URL "https://acme-v02.api.letsencrypt.org/directory"
4141
#define MD_TAILSCALE_DEF_URL "file://localhost/var/run/tailscale/tailscaled.sock"

modules/md/mod_md_config.c

+21-1
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,7 @@ static md_mod_conf_t defmc = {
8484
"crt.sh", /* default cert checker site name */
8585
"https://crt.sh?q=", /* default cert checker site url */
8686
NULL, /* CA cert file to use */
87+
apr_time_from_sec(MD_SECS_PER_DAY/2), /* default time between cert checks */
8788
apr_time_from_sec(5), /* minimum delay for retries */
8889
13, /* retry_failover after 14 errors, with 5s delay ~ half a day */
8990
0, /* store locks, disabled by default */
@@ -624,6 +625,24 @@ static const char *md_config_set_base_server(cmd_parms *cmd, void *dc, const cha
624625
return set_on_off(&config->mc->manage_base_server, value, cmd->pool);
625626
}
626627

628+
static const char *md_config_set_check_interval(cmd_parms *cmd, void *dc, const char *value)
629+
{
630+
md_srv_conf_t *config = md_config_get(cmd->server);
631+
const char *err = md_conf_check_location(cmd, MD_LOC_NOT_MD);
632+
apr_time_t interval;
633+
634+
(void)dc;
635+
if (err) return err;
636+
if (md_duration_parse(&interval, value, "s") != APR_SUCCESS) {
637+
return "unrecognized duration format";
638+
}
639+
if (interval < apr_time_from_sec(1)) {
640+
return "check interval cannot be less than one second";
641+
}
642+
config->mc->check_interval = interval;
643+
return NULL;
644+
}
645+
627646
static const char *md_config_set_min_delay(cmd_parms *cmd, void *dc, const char *value)
628647
{
629648
md_srv_conf_t *config = md_config_get(cmd->server);
@@ -1304,7 +1323,8 @@ const command_rec md_cmds[] = {
13041323
"Configure locking of store for updates."),
13051324
AP_INIT_TAKE1("MDMatchNames", md_config_set_match_mode, NULL, RSRC_CONF,
13061325
"Determines how DNS names are matched to vhosts."),
1307-
1326+
AP_INIT_TAKE1("MDCheckInterval", md_config_set_check_interval, NULL, RSRC_CONF,
1327+
"Time between certificate checks."),
13081328
AP_INIT_TAKE1(NULL, NULL, NULL, RSRC_CONF, NULL)
13091329
};
13101330

modules/md/mod_md_config.h

+1
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,7 @@ struct md_mod_conf_t {
7575
const char *cert_check_name; /* name of the linked certificate check site */
7676
const char *cert_check_url; /* url "template for" checking a certificate */
7777
const char *ca_certs; /* root certificates to use for connections */
78+
apr_time_t check_interval; /* duration between cert renewal checks */
7879
apr_time_t min_delay; /* minimum delay for retries */
7980
int retry_failover; /* number of errors to trigger CA failover */
8081
int use_store_locks; /* use locks when updating store */

modules/md/mod_md_drive.c

+14-6
Original file line numberDiff line numberDiff line change
@@ -100,15 +100,20 @@ static void process_drive_job(md_renew_ctx_t *dctx, md_job_t *job, apr_pool_t *p
100100
}
101101

102102
if (md_will_renew_cert(md)) {
103-
/* Renew the MDs credentials in a STAGING area. Might be invoked repeatedly
103+
/* Renew the MDs credentials in a STAGING area. Might be invoked repeatedly
104104
* without discarding previous/intermediate results.
105105
* Only returns SUCCESS when the renewal is complete, e.g. STAGING has a
106106
* complete set of new credentials.
107107
*/
108108
ap_log_error( APLOG_MARK, APLOG_DEBUG, 0, dctx->s, APLOGNO(10052)
109109
"md(%s): state=%d, driving", job->mdomain, md->state);
110110

111-
if (!md_reg_should_renew(dctx->mc->reg, md, dctx->p)) {
111+
if (md->stapling && dctx->mc->ocsp &&
112+
md_reg_has_revoked_certs(dctx->mc->reg, dctx->mc->ocsp, md, dctx->p)) {
113+
ap_log_error( APLOG_MARK, APLOG_DEBUG, 0, dctx->s, APLOGNO()
114+
"md(%s): has revoked certificates", job->mdomain);
115+
}
116+
else if (!md_reg_should_renew(dctx->mc->reg, md, dctx->p)) {
112117
ap_log_error( APLOG_MARK, APLOG_DEBUG, 0, dctx->s, APLOGNO(10053)
113118
"md(%s): no need to renew", job->mdomain);
114119
goto expiry;
@@ -180,10 +185,13 @@ int md_will_renew_cert(const md_t *md)
180185
return 1;
181186
}
182187

183-
static apr_time_t next_run_default(void)
188+
static apr_time_t next_run_default(md_renew_ctx_t *dctx)
184189
{
185-
/* we'd like to run at least twice a day by default */
186-
return apr_time_now() + apr_time_from_sec(MD_SECS_PER_DAY / 2);
190+
unsigned char c;
191+
apr_time_t delay = dctx->mc->check_interval;
192+
193+
md_rand_bytes(&c, sizeof(c), dctx->p);
194+
return apr_time_now() + delay + (delay * (c - 128) / 256);
187195
}
188196

189197
static apr_status_t run_watchdog(int state, void *baton, apr_pool_t *ptemp)
@@ -211,7 +219,7 @@ static apr_status_t run_watchdog(int state, void *baton, apr_pool_t *ptemp)
211219
* and we schedule ourself at the earliest of all. A job may specify 0
212220
* as next_run to indicate that it wants to participate in the normal
213221
* regular runs. */
214-
next_run = next_run_default();
222+
next_run = next_run_default(dctx);
215223
for (i = 0; i < dctx->jobs->nelts; ++i) {
216224
job = APR_ARRAY_IDX(dctx->jobs, i, md_job_t *);
217225

test/modules/md/conftest.py

+2-1
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,8 @@ def env(pytestconfig) -> MDTestEnv:
3232
env.setup_httpd()
3333
env.apache_access_log_clear()
3434
env.httpd_error_log.clear_log()
35-
return env
35+
yield env
36+
env.apache_stop()
3637

3738

3839
@pytest.fixture(autouse=True, scope="package")

test/modules/md/md_cert_util.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -166,10 +166,10 @@ def get_key_length(self):
166166

167167
def get_san_list(self):
168168
text = OpenSSL.crypto.dump_certificate(OpenSSL.crypto.FILETYPE_TEXT, self.cert).decode("utf-8")
169-
m = re.search(r"X509v3 Subject Alternative Name:\s*(.*)", text)
169+
m = re.search(r"X509v3 Subject Alternative Name:(\s+critical)?\s*(.*)", text)
170170
sans_list = []
171171
if m:
172-
sans_list = m.group(1).split(",")
172+
sans_list = m.group(2).split(",")
173173

174174
def _strip_prefix(s):
175175
return s.split(":")[1] if s.strip().startswith("DNS:") else s.strip()

test/modules/md/md_env.py

+10-5
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,11 @@ def has_acme_server(cls):
7373

7474
@classmethod
7575
def has_acme_eab(cls):
76-
return cls.get_acme_server() == 'pebble'
76+
return False
77+
# Pebble, since v2.5.0 no longer supports HS256 for EAB, which
78+
# is the only thing mod_md supports. Issue opened at pebble:
79+
# https://github.com/letsencrypt/pebble/issues/455
80+
# return cls.get_acme_server() == 'pebble'
7781

7882
@classmethod
7983
def is_pebble(cls) -> bool:
@@ -356,13 +360,14 @@ def check_md_credentials(self, domain):
356360
MDCertUtil.validate_privkey(self.store_domain_file(domain, 'privkey.pem'))
357361
cert = MDCertUtil(self.store_domain_file(domain, 'pubcert.pem'))
358362
cert.validate_cert_matches_priv_key(self.store_domain_file(domain, 'privkey.pem'))
359-
# check SANs and CN
360-
assert cert.get_cn() == domain
363+
# No longer check CN, it may not be set or is not trusted anyway
364+
# assert cert.get_cn() == domain, f'CN: expected "{domain}", got {cert.get_cn()}'
365+
# check SANs
361366
# compare lists twice in opposite directions: SAN may not respect ordering
362367
san_list = list(cert.get_san_list())
363368
assert len(san_list) == len(domains)
364-
assert set(san_list).issubset(domains)
365-
assert set(domains).issubset(san_list)
369+
assert set(san_list).issubset(domains), f'{san_list} not subset of {domains}'
370+
assert set(domains).issubset(san_list), f'{domains} not subset of {san_list}'
366371
# check valid dates interval
367372
not_before = cert.get_not_before()
368373
not_after = cert.get_not_after()

test/modules/md/test_300_conf_validate.py

+22-9
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,8 @@
1515
class TestConf:
1616

1717
@pytest.fixture(autouse=True, scope='class')
18-
def _class_scope(self, env):
18+
def _class_scope(self, env, acme):
19+
acme.start(config='default')
1920
env.clear_store()
2021

2122
# test case: just one MDomain definition
@@ -413,7 +414,7 @@ def test_md_300_025(self, env, ca, url):
413414
def test_md_300_026(self, env):
414415
assert env.apache_stop() == 0
415416
conf = MDConf(env)
416-
domain = f"t300_026.{env.http_tld}"
417+
domain = f"t300-026.{env.http_tld}"
417418
conf.add(f"""
418419
MDomain {domain}
419420
""")
@@ -460,11 +461,12 @@ def test_md_300_027(self, env, cas, should_work):
460461
def test_md_300_028(self, env):
461462
assert env.apache_stop() == 0
462463
conf = MDConf(env)
463-
domaina = f"t300_028a.{env.http_tld}"
464-
domainb = f"t300_028b.{env.http_tld}"
465-
dalias = f"t300_028alias.{env.http_tld}"
464+
domaina = f"t300-028a.{env.http_tld}"
465+
domainb = f"t300-028b.{env.http_tld}"
466+
dalias = f"t300-028alias.{env.http_tld}"
466467
conf.add_vhost(port=env.http_port, domains=[domaina, domainb, dalias], with_ssl=False)
467468
conf.add(f"""
469+
MDMembers manual
468470
MDomain {domaina}
469471
MDomain {domainb} {dalias}
470472
""")
@@ -481,23 +483,28 @@ def test_md_300_028(self, env):
481483
</VirtualHost>
482484
""")
483485
conf.install()
484-
# This does not work as we have both MDs match domaina's vhost
486+
# This does not work as we have both MDs match domain's vhost
485487
assert env.apache_fail() == 0
486488
env.httpd_error_log.ignore_recent(
487-
lognos = [
488-
"AH10238" # 2 MDs match the same vhost
489+
lognos=[
490+
"AH10238", # 2 MDs match the same vhost
489491
]
490492
)
491493
# It works, if we only match on ServerNames
492494
conf.add("MDMatchNames servernames")
493495
conf.install()
494496
assert env.apache_restart() == 0
497+
env.httpd_error_log.ignore_recent(
498+
lognos=[
499+
"AH10040", # ServerAlias not covered
500+
]
501+
)
495502

496503
# wildcard and specfic MD overlaps
497504
def test_md_300_029(self, env):
498505
assert env.apache_stop() == 0
499506
conf = MDConf(env)
500-
domain = f"t300_029.{env.http_tld}"
507+
domain = f"t300-029.{env.http_tld}"
501508
subdomain = f"sub.{domain}"
502509
conf.add_vhost(port=env.http_port, domains=[domain, subdomain], with_ssl=False)
503510
conf.add(f"""
@@ -531,4 +538,10 @@ def test_md_300_029(self, env):
531538
conf.add("MDMatchNames servernames")
532539
conf.install()
533540
assert env.apache_restart() == 0
541+
time.sleep(2)
542+
assert env.apache_stop() == 0
543+
# we need dns-01 challenge for the wildcard, which is not configured
544+
env.httpd_error_log.ignore_recent(matches=[
545+
r'.*None of offered challenge types.*are supported.*'
546+
])
534547

0 commit comments

Comments
 (0)