diff --git a/ChangeLog b/ChangeLog index 9674c05d..de90bdff 100644 --- a/ChangeLog +++ b/ChangeLog @@ -1,3 +1,7 @@ + * New directive "MDStaplingKeepResponse" for controlling how long OCSP responses are + kept in the store and older ones get removed at start up. + * Errors reports by an ACME CA may include "subproblems", where several causes may be reported. + These are now part of the md-status reporting and also logged. Test cases added. * OCSP status and validity now part of md-status resource. * The number of log entries for a single job is now limited to 128. New entries will cause the oldest ones to be removed. diff --git a/README.md b/README.md index aed62f93..10393079 100644 --- a/README.md +++ b/README.md @@ -1213,8 +1213,12 @@ checks by mod_md in v1.1.x which are now eliminated. If you have many domains, t * [MDRenewWindow](#mdrenewwindow--when-to-renew) * [MDWarnWindow](#mdwarnwindow) * [MDServerStatus](#mdserverstatus) +* [MDStapling](#mdstapling) +* [MDStapleOthers](#mdstapleothers) +* [MDStaplingKeepResponse](#mdstaplingkeepresponse) * [MDStoreDir](#mdstoredir) + ## MDomain ***Define list of domain names that belong to one group***
@@ -1525,6 +1529,46 @@ Default: `on` Controls if Managed Domains respond to public requests for `/.httpd/certificate-status` or not. +## MDStapling + +***Enable stapling for all or a particular MDomain.***
+`MDStapling on|off`
+Default: `off` + +`mod_md` has its own implementation for providing OCSP stapling information. This is an +alternative to the one provided by `mod_ssl`. For backward compatiblity reasons, this is +disabled by default. + +The new stapling can be switched on for all certificates on the server or for an individual MDomain. This +will replace any stapling configurtion in `mod_ssl` for these hosts. When disabled, the `mod_ssl` +stapling (if configured) will do the work. This allows for a gradual shift over from one +implementation to the other. + +The stapling of `mod_md` will also work for domains where the certificates are not managed +by this module (see MDStapleOthers for how to control this). This allows use of the new stapling +without using any ACME certificate management. + +## MDStapleOthers + +***Enable stapling for certificates not managed by mod_md.***
+`MDStapling on|off`
+Default: `on` + +This setting only takes effect when `MDStapling` is enabled. It controls if `mod_md` should +also provide stapling information for certificates that are not directly controlled by it, e.g. +renewed via an ACME CA. + +## MDStaplingKeepResponse + +***Controls when responses are considered old and will be removed.***
+`MDStaplingKeepResponse duration`
+Default: 7d + +This time window specifies when OCSP response data used in stapling shall be removed +from the store again on start up. Response information older than 7 days (default) is +deleted. This keeps the store from growing when certificates are renewed/reconfigured +frequently. + ## ServerAdmin / Contact Information diff --git a/mod_md.xcodeproj/project.pbxproj b/mod_md.xcodeproj/project.pbxproj index 7ddc6269..2956b1ae 100644 --- a/mod_md.xcodeproj/project.pbxproj +++ b/mod_md.xcodeproj/project.pbxproj @@ -23,6 +23,7 @@ B2123B8D1F2DD7A200267CAF /* test_md_json.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; path = test_md_json.c; sourceTree = ""; }; B2123B8E1F2DD7A200267CAF /* test_common.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = test_common.h; sourceTree = ""; }; B2307ED520E2562100487AD7 /* test.ini */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = test.ini; sourceTree = ""; }; + B23C47E722F1A45900B8F494 /* test_0740_acme_errors.py */ = {isa = PBXFileReference; lastKnownFileType = text.script.python; path = test_0740_acme_errors.py; sourceTree = ""; }; B24052151EF9145000E36701 /* md_core.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; path = md_core.c; sourceTree = ""; }; B24052161EF9145000E36701 /* md_crypt.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; path = md_crypt.c; sourceTree = ""; }; B24052171EF9145000E36701 /* md_crypt.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = md_crypt.h; sourceTree = ""; }; @@ -315,6 +316,7 @@ B2C1BE0621F5CB38002EF7B4 /* test_0710_migration.py */, B2C1BE082201AC02002EF7B4 /* test_0720_wildcard.py */, B2830C2422B27C3F005DBFBF /* test_0730_static.py */, + B23C47E722F1A45900B8F494 /* test_0740_acme_errors.py */, B2FEEDDF1FFB910D0029E19C /* test_0800_must_staple.py */, B2A3E34D22CA34C2009D35AE /* test_0801_stapling.py */, B2FFC9E920C5776A004A8F58 /* test_0900_notify.py */, diff --git a/src/md.h b/src/md.h index aba2f489..b8c09108 100644 --- a/src/md.h +++ b/src/md.h @@ -41,6 +41,7 @@ struct md_pkey_spec_t; #define MD_TIME_LIFE_NORM (apr_time_from_sec(100 * MD_SECS_PER_DAY)) #define MD_TIME_RENEW_WINDOW_DEF (apr_time_from_sec(33 * MD_SECS_PER_DAY)) #define MD_TIME_WARN_WINDOW_DEF (apr_time_from_sec(10 * MD_SECS_PER_DAY)) +#define MD_TIME_OCSP_KEEP_NORM (apr_time_from_sec(7 * MD_SECS_PER_DAY)) #define MD_OTHER "other" @@ -193,6 +194,7 @@ struct md_t { #define MD_KEY_STATE "state" #define MD_KEY_STATUS "status" #define MD_KEY_STORE "store" +#define MD_KEY_SUBPROBLEMS "subproblems" #define MD_KEY_TEMPORARY "temporary" #define MD_KEY_TOKEN "token" #define MD_KEY_TOTAL "total" diff --git a/src/md_acme.c b/src/md_acme.c index 02c2b833..2f6d66aa 100644 --- a/src/md_acme.c +++ b/src/md_acme.c @@ -175,7 +175,10 @@ static apr_status_t inspect_problem(md_acme_req_t *req, const md_http_response_t ptype = md_json_gets(problem, MD_KEY_TYPE, NULL); pdetail = md_json_gets(problem, MD_KEY_DETAIL, NULL); req->rv = problem_status_get(ptype); - md_result_problem_set(req->result, req->rv, ptype, pdetail); + md_result_problem_set(req->result, req->rv, ptype, pdetail, + md_json_getj(problem, MD_KEY_SUBPROBLEMS, NULL)); + + if (APR_STATUS_IS_EAGAIN(req->rv)) { md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, req->rv, req->p, @@ -494,7 +497,8 @@ void md_acme_report_result(md_acme_t *acme, apr_status_t rv, struct md_result_t md_result_set(result, rv, NULL); } else { - md_result_problem_set(result, acme->last->status, acme->last->problem, acme->last->detail); + md_result_problem_set(result, acme->last->status, acme->last->problem, + acme->last->detail, acme->last->subproblems); } } diff --git a/src/md_acme_authz.c b/src/md_acme_authz.c index 4a82c35f..45062e2b 100644 --- a/src/md_acme_authz.c +++ b/src/md_acme_authz.c @@ -157,6 +157,7 @@ static int copy_challenge_error(void *baton, size_t index, md_json_t *json) if (md_json_has_key(json, MD_KEY_ERROR, NULL)) { ctx->authz->error_type = md_json_dups(ctx->p, json, MD_KEY_ERROR, MD_KEY_TYPE, NULL); ctx->authz->error_detail = md_json_dups(ctx->p, json, MD_KEY_ERROR, MD_KEY_DETAIL, NULL); + ctx->authz->error_subproblems = md_json_dupj(ctx->p, json, MD_KEY_ERROR, MD_KEY_SUBPROBLEMS, NULL); } return 1; } @@ -177,6 +178,7 @@ apr_status_t md_acme_authz_update(md_acme_authz_t *authz, md_acme_t *acme, apr_p authz->state = MD_ACME_AUTHZ_S_UNKNOWN; json = NULL; authz->error_type = authz->error_detail = NULL; + authz->error_subproblems = NULL; err = "unable to parse response"; log_level = MD_LOG_ERR; diff --git a/src/md_acme_authz.h b/src/md_acme_authz.h index 3a0cfe37..fe1abe55 100644 --- a/src/md_acme_authz.h +++ b/src/md_acme_authz.h @@ -51,6 +51,7 @@ struct md_acme_authz_t { apr_time_t expires; const char *error_type; const char *error_detail; + const struct md_json_t *error_subproblems; struct md_json_t *resource; }; diff --git a/src/md_acme_order.c b/src/md_acme_order.c index 24b5f8fe..b70c527f 100644 --- a/src/md_acme_order.c +++ b/src/md_acme_order.c @@ -468,7 +468,7 @@ apr_status_t md_acme_order_start_challenges(md_acme_order_t *order, md_acme_t *a case MD_ACME_AUTHZ_S_INVALID: rv = APR_EINVAL; if (authz->error_type) { - md_result_problem_set(result, rv, authz->error_type, authz->error_detail); + md_result_problem_set(result, rv, authz->error_type, authz->error_detail, NULL); goto leave; } /* fall through */ diff --git a/src/md_json.c b/src/md_json.c index 3777f866..4391cf64 100644 --- a/src/md_json.c +++ b/src/md_json.c @@ -107,12 +107,12 @@ void md_json_destroy(md_json_t *json) } } -md_json_t *md_json_copy(apr_pool_t *pool, md_json_t *json) +md_json_t *md_json_copy(apr_pool_t *pool, const md_json_t *json) { return json_create(pool, json_copy(json->j)); } -md_json_t *md_json_clone(apr_pool_t *pool, md_json_t *json) +md_json_t *md_json_clone(apr_pool_t *pool, const md_json_t *json) { return json_create(pool, json_deep_copy(json->j)); } @@ -314,7 +314,7 @@ int md_json_is(const md_json_type_t jtype, md_json_t *json, ...) return 0; } -static const char *md_json_type_name(md_json_t *json) +static const char *md_json_type_name(const md_json_t *json) { json_t *j = json->j; if (json_is_object(j)) return "object"; @@ -463,6 +463,22 @@ md_json_t *md_json_getj(md_json_t *json, ...) return NULL; } +md_json_t *md_json_dupj(apr_pool_t *p, const md_json_t *json, ...) +{ + json_t *j; + va_list ap; + + va_start(ap, json); + j = jselect(json, ap); + va_end(ap); + + if (j) { + json_incref(j); + return json_create(p, j); + } + return NULL; +} + const md_json_t *md_json_getcj(const md_json_t *json, ...) { json_t *j; @@ -482,7 +498,7 @@ const md_json_t *md_json_getcj(const md_json_t *json, ...) return NULL; } -apr_status_t md_json_setj(md_json_t *value, md_json_t *json, ...) +apr_status_t md_json_setj(const md_json_t *value, md_json_t *json, ...) { va_list ap; apr_status_t rv; @@ -510,7 +526,7 @@ apr_status_t md_json_setj(md_json_t *value, md_json_t *json, ...) return rv; } -apr_status_t md_json_addj(md_json_t *value, md_json_t *json, ...) +apr_status_t md_json_addj(const md_json_t *value, md_json_t *json, ...) { va_list ap; apr_status_t rv; @@ -675,7 +691,7 @@ apr_status_t md_json_clone_to(void *value, md_json_t *json, apr_pool_t *p, void return md_json_setj(md_json_clone(p, value), json, NULL); } -apr_status_t md_json_clone_from(void **pvalue, md_json_t *json, apr_pool_t *p, void *baton) +apr_status_t md_json_clone_from(void **pvalue, const md_json_t *json, apr_pool_t *p, void *baton) { (void)baton; *pvalue = md_json_clone(p, json); @@ -876,7 +892,7 @@ apr_status_t md_json_setsa(apr_array_header_t *a, md_json_t *json, ...) /* formatting, parsing */ typedef struct { - md_json_t *json; + const md_json_t *json; md_json_fmt_t fmt; const char *fname; apr_file_t *f; @@ -901,7 +917,7 @@ static int dump_cb(const char *buffer, size_t len, void *baton) return (rv == APR_SUCCESS)? 0 : -1; } -apr_status_t md_json_writeb(md_json_t *json, md_json_fmt_t fmt, apr_bucket_brigade *bb) +apr_status_t md_json_writeb(const md_json_t *json, md_json_fmt_t fmt, apr_bucket_brigade *bb) { int rv = json_dump_callback(json->j, dump_cb, bb, fmt_to_flags(fmt)); return rv? APR_EGENERAL : APR_SUCCESS; @@ -921,7 +937,7 @@ static int chunk_cb(const char *buffer, size_t len, void *baton) return 0; } -const char *md_json_writep(md_json_t *json, apr_pool_t *p, md_json_fmt_t fmt) +const char *md_json_writep(const md_json_t *json, apr_pool_t *p, md_json_fmt_t fmt) { apr_array_header_t *chunks; int rv; @@ -944,7 +960,7 @@ const char *md_json_writep(md_json_t *json, apr_pool_t *p, md_json_fmt_t fmt) } } -apr_status_t md_json_writef(md_json_t *json, apr_pool_t *p, md_json_fmt_t fmt, apr_file_t *f) +apr_status_t md_json_writef(const md_json_t *json, apr_pool_t *p, md_json_fmt_t fmt, apr_file_t *f) { apr_status_t rv; const char *s; @@ -963,7 +979,7 @@ apr_status_t md_json_writef(md_json_t *json, apr_pool_t *p, md_json_fmt_t fmt, a return rv; } -apr_status_t md_json_fcreatex(md_json_t *json, apr_pool_t *p, md_json_fmt_t fmt, +apr_status_t md_json_fcreatex(const md_json_t *json, apr_pool_t *p, md_json_fmt_t fmt, const char *fpath, apr_fileperms_t perms) { apr_status_t rv; @@ -987,7 +1003,7 @@ static apr_status_t write_json(void *baton, apr_file_t *f, apr_pool_t *p) return rv; } -apr_status_t md_json_freplace(md_json_t *json, apr_pool_t *p, md_json_fmt_t fmt, +apr_status_t md_json_freplace(const md_json_t *json, apr_pool_t *p, md_json_fmt_t fmt, const char *fpath, apr_fileperms_t perms) { j_write_ctx ctx; @@ -1171,7 +1187,7 @@ apr_status_t md_json_copy_to(md_json_t *dest, const md_json_t *src, ...) return rv; } -const char *md_json_dump_state(md_json_t *json, apr_pool_t *p) +const char *md_json_dump_state(const md_json_t *json, apr_pool_t *p) { if (!json) return "NULL"; return apr_psprintf(p, "%s, refc=%ld", md_json_type_name(json), (long)json->j->refcount); diff --git a/src/md_json.h b/src/md_json.h index ab736bb1..dc612834 100644 --- a/src/md_json.h +++ b/src/md_json.h @@ -47,8 +47,8 @@ typedef enum { md_json_t *md_json_create(apr_pool_t *pool); void md_json_destroy(md_json_t *json); -md_json_t *md_json_copy(apr_pool_t *pool, md_json_t *json); -md_json_t *md_json_clone(apr_pool_t *pool, md_json_t *json); +md_json_t *md_json_copy(apr_pool_t *pool, const md_json_t *json); +md_json_t *md_json_clone(apr_pool_t *pool, const md_json_t *json); int md_json_has_key(const md_json_t *json, ...); @@ -74,9 +74,10 @@ apr_status_t md_json_sets(const char *s, md_json_t *json, ...); /* json manipulation */ md_json_t *md_json_getj(md_json_t *json, ...); +md_json_t *md_json_dupj(apr_pool_t *p, const md_json_t *json, ...); const md_json_t *md_json_getcj(const md_json_t *json, ...); -apr_status_t md_json_setj(md_json_t *value, md_json_t *json, ...); -apr_status_t md_json_addj(md_json_t *value, md_json_t *json, ...); +apr_status_t md_json_setj(const md_json_t *value, md_json_t *json, ...); +apr_status_t md_json_addj(const md_json_t *value, md_json_t *json, ...); apr_status_t md_json_insertj(md_json_t *value, size_t index, md_json_t *json, ...); /* Array/Object manipulation */ @@ -96,7 +97,7 @@ apr_status_t md_json_pass_from(void **pvalue, md_json_t *json, apr_pool_t *p, vo /* conversions from json to json in specified pool */ apr_status_t md_json_clone_to(void *value, md_json_t *json, apr_pool_t *p, void *baton); -apr_status_t md_json_clone_from(void **pvalue, md_json_t *json, apr_pool_t *p, void *baton); +apr_status_t md_json_clone_from(void **pvalue, const md_json_t *json, apr_pool_t *p, void *baton); /* Manipulating/Iteration on generic Arrays */ apr_status_t md_json_geta(apr_array_header_t *a, md_json_from_cb *cb, @@ -118,13 +119,13 @@ apr_status_t md_json_dupsa(apr_array_header_t *a, apr_pool_t *p, md_json_t *json apr_status_t md_json_setsa(apr_array_header_t *a, md_json_t *json, ...); /* serialization & parsing */ -apr_status_t md_json_writeb(md_json_t *json, md_json_fmt_t fmt, struct apr_bucket_brigade *bb); -const char *md_json_writep(md_json_t *json, apr_pool_t *p, md_json_fmt_t fmt); -apr_status_t md_json_writef(md_json_t *json, apr_pool_t *p, +apr_status_t md_json_writeb(const md_json_t *json, md_json_fmt_t fmt, struct apr_bucket_brigade *bb); +const char *md_json_writep(const md_json_t *json, apr_pool_t *p, md_json_fmt_t fmt); +apr_status_t md_json_writef(const md_json_t *json, apr_pool_t *p, md_json_fmt_t fmt, struct apr_file_t *f); -apr_status_t md_json_fcreatex(md_json_t *json, apr_pool_t *p, md_json_fmt_t fmt, +apr_status_t md_json_fcreatex(const md_json_t *json, apr_pool_t *p, md_json_fmt_t fmt, const char *fpath, apr_fileperms_t perms); -apr_status_t md_json_freplace(md_json_t *json, apr_pool_t *p, md_json_fmt_t fmt, +apr_status_t md_json_freplace(const md_json_t *json, apr_pool_t *p, md_json_fmt_t fmt, const char *fpath, apr_fileperms_t perms); apr_status_t md_json_readb(md_json_t **pjson, apr_pool_t *pool, struct apr_bucket_brigade *bb); @@ -140,7 +141,7 @@ apr_status_t md_json_read_http(md_json_t **pjson, apr_pool_t *pool, apr_status_t md_json_copy_to(md_json_t *dest, const md_json_t *src, ...); -const char *md_json_dump_state(md_json_t *json, apr_pool_t *p); +const char *md_json_dump_state(const md_json_t *json, apr_pool_t *p); apr_status_t md_json_timeperiod_set(struct md_timeperiod_t *tp, md_json_t *json, ...); diff --git a/src/md_ocsp.c b/src/md_ocsp.c index d444ae7d..d153e14c 100644 --- a/src/md_ocsp.c +++ b/src/md_ocsp.c @@ -242,7 +242,7 @@ static apr_status_t ocsp_status_refresh(md_ocsp_status_t *ostat, apr_pool_t *pte md_data_t resp_der; md_timeperiod_t resp_valid; md_ocsp_cert_stat_t resp_stat; - + /* Check if the store holds a newer response than the one we have */ mtime = md_store_get_modified(store, MD_SG_OCSP, ostat->md_name, ostat->file_name, ptemp); if (mtime <= ostat->resp_mtime) goto leave; rv = md_store_load_json(store, MD_SG_OCSP, ostat->md_name, ostat->file_name, &jprops, ptemp); @@ -402,8 +402,7 @@ apr_status_t md_ocsp_get_status(unsigned char **pder, int *pderlen, *pder = NULL; *pderlen = 0; if (ostat->resp_der.len <= 0) { - /* No resonse known, check the store if out watchdog retrieved one - * in the meantime. */ + /* No response known, check store for new response. */ ocsp_status_refresh(ostat, p); if (ostat->resp_der.len <= 0) { md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, 0, reg->p, diff --git a/src/md_result.c b/src/md_result.c index c51308fd..589b7a39 100644 --- a/src/md_result.c +++ b/src/md_result.c @@ -75,6 +75,7 @@ void md_result_activity_setn(md_result_t *result, const char *activity) { result->activity = activity; result->problem = result->detail = NULL; + result->subproblems = NULL; on_change(result); } @@ -92,15 +93,18 @@ void md_result_set(md_result_t *result, apr_status_t status, const char *detail) result->status = status; result->problem = NULL; result->detail = detail? apr_pstrdup(result->p, detail) : NULL; + result->subproblems = NULL; on_change(result); } void md_result_problem_set(md_result_t *result, apr_status_t status, - const char *problem, const char *detail) + const char *problem, const char *detail, + const md_json_t *subproblems) { result->status = status; result->problem = dup_trim(result->p, problem); result->detail = apr_pstrdup(result->p, detail); + result->subproblems = subproblems? md_json_clone(result->p, subproblems) : NULL; on_change(result); } @@ -115,6 +119,7 @@ void md_result_problem_printf(md_result_t *result, apr_status_t status, va_start(ap, fmt); result->detail = apr_pvsprintf(result->p, fmt, ap); va_end(ap); + result->subproblems = NULL; on_change(result); } @@ -126,6 +131,7 @@ void md_result_printf(md_result_t *result, apr_status_t status, const char *fmt, va_start(ap, fmt); result->detail = apr_pvsprintf(result->p, fmt, ap); va_end(ap); + result->subproblems = NULL; on_change(result); } @@ -147,7 +153,7 @@ md_result_t*md_result_from_json(const struct md_json_t *json, apr_pool_t *p) result->activity = md_json_dups(p, json, MD_KEY_ACTIVITY, NULL); s = md_json_dups(p, json, MD_KEY_VALID_FROM, NULL); if (s && *s) result->ready_at = apr_date_parse_rfc(s); - + result->subproblems = md_json_dupj(p, json, MD_KEY_SUBPROBLEMS, NULL); return result; } @@ -170,6 +176,9 @@ struct md_json_t *md_result_to_json(const md_result_t *result, apr_pool_t *p) apr_rfc822_date(ts, result->ready_at); md_json_sets(ts, json, MD_KEY_VALID_FROM, NULL); } + if (result->subproblems) { + md_json_setj(result->subproblems, json, MD_KEY_SUBPROBLEMS, NULL); + } return json; } @@ -201,6 +210,7 @@ void md_result_assign(md_result_t *dest, const md_result_t *src) dest->detail = src->detail; dest->activity = src->activity; dest->ready_at = src->ready_at; + dest->subproblems = src->subproblems; } void md_result_dup(md_result_t *dest, const md_result_t *src) @@ -210,6 +220,7 @@ void md_result_dup(md_result_t *dest, const md_result_t *src) dest->detail = src->detail? apr_pstrdup(dest->p, src->detail) : NULL; dest->activity = src->activity? apr_pstrdup(dest->p, src->activity) : NULL; dest->ready_at = src->ready_at; + dest->subproblems = src->subproblems? md_json_clone(dest->p, src->subproblems) : NULL; on_change(dest); } @@ -235,6 +246,11 @@ void md_result_log(md_result_t *result, int level) msg = apr_psprintf(result->p, "%s%sdetail[%s]", msg, sep, result->detail); sep = " "; } + if (result->subproblems) { + msg = apr_psprintf(result->p, "%s%ssubproblems[%s]", msg, sep, + md_json_writep(result->subproblems, result->p, MD_JSON_FMT_COMPACT)); + sep = " "; + } md_log_perror(MD_LOG_MARK, (md_log_level_t)level, result->status, result->p, "%s", msg); } } diff --git a/src/md_result.h b/src/md_result.h index 21823735..f4fb19a7 100644 --- a/src/md_result.h +++ b/src/md_result.h @@ -30,6 +30,7 @@ struct md_result_t { apr_status_t status; const char *problem; const char *detail; + const struct md_json_t *subproblems; const char *activity; apr_time_t ready_at; md_result_change_cb *on_change; @@ -46,7 +47,8 @@ void md_result_activity_printf(md_result_t *result, const char *fmt, ...); void md_result_set(md_result_t *result, apr_status_t status, const char *detail); void md_result_problem_set(md_result_t *result, apr_status_t status, - const char *problem, const char *detail); + const char *problem, const char *detail, + const struct md_json_t *subproblems); void md_result_problem_printf(md_result_t *result, apr_status_t status, const char *problem, const char *fmt, ...); diff --git a/src/mod_md_config.c b/src/mod_md_config.c index e711efd9..7819b8fb 100644 --- a/src/mod_md_config.c +++ b/src/mod_md_config.c @@ -41,6 +41,11 @@ #define MD_DEFAULT_BASE_DIR "md" #endif +static md_timeslice_t def_ocsp_keep_window = { + 0, + MD_TIME_OCSP_KEEP_NORM, +}; + /* Default settings for the global conf */ static md_mod_conf_t defmc = { NULL, /* list of mds */ @@ -67,6 +72,7 @@ static md_mod_conf_t defmc = { 0, /* dry_run flag */ 1, /* server_status_enabled */ 1, /* certificate_status_enabled */ + &def_ocsp_keep_window, /* default time to keep ocsp responses */ }; static md_timeslice_t def_renew_window = { @@ -852,6 +858,19 @@ static const char *md_config_set_certificate_status(cmd_parms *cmd, void *dc, co return set_on_off(&sc->mc->certificate_status_enabled, value, cmd->pool); } +static const char *md_config_set_ocsp_keep_window(cmd_parms *cmd, void *dc, const char *value) +{ + md_srv_conf_t *sc = md_config_get(cmd->server); + const char *err; + + (void)dc; + if (!inside_md_section(cmd) && (err = ap_check_cmd_context(cmd, GLOBAL_ONLY))) { + return err; + } + err = md_timeslice_parse(&sc->mc->ocsp_keep_window, cmd->pool, value, MD_TIME_OCSP_KEEP_NORM); + if (err) return apr_psprintf(cmd->pool, "MDStaplingKeepResponse %s", err); + return NULL; +} const command_rec md_cmds[] = { AP_INIT_TAKE1("MDCertificateAuthority", md_config_set_ca, NULL, RSRC_CONF, @@ -917,6 +936,8 @@ const command_rec md_cmds[] = { "Enable/Disable OCSP Stapling for this/all Managed Domain(s)."), AP_INIT_TAKE1("MDStapleOthers", md_config_set_staple_others, NULL, RSRC_CONF, "Enable/Disable OCSP Stapling for certificates not in Managed Domains."), + AP_INIT_TAKE1("MDStaplingKeepResponse", md_config_set_ocsp_keep_window, NULL, RSRC_CONF, + "The amount of time to keep an OCSP response in the store."), AP_INIT_TAKE1(NULL, NULL, NULL, RSRC_CONF, NULL) }; diff --git a/src/mod_md_config.h b/src/mod_md_config.h index a6962ba1..573ebf50 100644 --- a/src/mod_md_config.h +++ b/src/mod_md_config.h @@ -67,6 +67,7 @@ struct md_mod_conf_t { int dry_run; /* != 0 iff config dry run */ int server_status_enabled; /* if module should add to server-status handler */ int certificate_status_enabled; /* if module should expose /.httpd/certificate-status */ + md_timeslice_t *ocsp_keep_window; /* time that we keep ocsp responses around */ }; typedef struct md_srv_conf_t { diff --git a/src/mod_md_ocsp.c b/src/mod_md_ocsp.c index 23b7f5de..15b46907 100644 --- a/src/mod_md_ocsp.c +++ b/src/mod_md_ocsp.c @@ -161,7 +161,20 @@ static apr_status_t run_watchdog(int state, void *baton, apr_pool_t *ptemp) return APR_SUCCESS; } -apr_status_t md_ocsp_start_watching(struct md_mod_conf_t *mc, server_rec *s, apr_pool_t *p) +static apr_status_t ocsp_remove_old_responses(md_mod_conf_t *mc, apr_pool_t *p) +{ + md_timeperiod_t keep_norm, keep; + + keep_norm.end = apr_time_now(); + keep_norm.start = keep.end - MD_TIME_OCSP_KEEP_NORM; + keep = md_timeperiod_slice_before_end(&keep_norm, mc->ocsp_keep_window); + /* remove any ocsp response older than keep.start */ + /* TODO */ + (void)p; + return APR_ENOTIMPL; +} + +apr_status_t md_ocsp_start_watching(md_mod_conf_t *mc, server_rec *s, apr_pool_t *p) { apr_allocator_t *allocator; md_ocsp_ctx_t *octx; @@ -196,9 +209,24 @@ apr_status_t md_ocsp_start_watching(struct md_mod_conf_t *mc, server_rec *s, apr octx->s = s; octx->mc = mc; - /* TODO: make sure that store is prepped, has correct permissions and - * perform house-keeping, such as removing OCSP responses no longer used - * and older than 24 hours. */ + /* Time for some house keeping, before the server goes live (again): + * - we store OCSP responses for each certificate individually by its SHA-1 id + * - this means, as long as certificate do not change, the number of response + * files remains stable. + * - But when a certificate changes (is replaced), the response is obsolete + * - we do not get notified when a certificate is no longer used. An admin + * might just reconfigure or change the content of a file (backup/restore etc.) + * - also, certificates might be added by some openssl config commands or other + * modules that we do not immediately see right at startup. We cannot assume + * that any OCSP response we cannot relate to a certificate RIGHT NOW, is no + * longer needed. + * - since the response files are relatively small, we have no problem with + * keeping them around for a while. We just do not want an ever growing store. + * - The simplest and effective way seems to be to just remove files older + * a certain amount of time. Take a 7 day default and let the admin configure + * it for very special setups. + */ + ocsp_remove_old_responses(mc, octx->p); rv = wd_get_instance(&octx->watchdog, MD_OCSP_WATCHDOG_NAME, 0, 1, octx->p); if (APR_SUCCESS != rv) { diff --git a/test/test_0740_acme_errors.py b/test/test_0740_acme_errors.py index e69de29b..839c3a4d 100644 --- a/test/test_0740_acme_errors.py +++ b/test/test_0740_acme_errors.py @@ -0,0 +1,79 @@ +# test ACME error responses and their processing + +import json +import os +import pytest +import re +import socket +import ssl +import sys +import time + +from datetime import datetime +from httplib import HTTPSConnection +from test_base import TestEnv +from test_base import HttpdConf +from test_base import CertUtil + + +def setup_module(module): + print("setup_module module:%s" % module.__name__) + TestEnv.initv2() + TestEnv.APACHE_CONF_SRC = "data/test_auto" + TestEnv.check_acme() + TestEnv.clear_store() + TestEnv.install_test_conf(); + assert TestEnv.apache_start() == 0 + + +def teardown_module(module): + print("teardown_module module:%s" % module.__name__) + assert TestEnv.apache_stop() == 0 + + +class TestAcmeErrors: + + def setup_method(self, method): + print("setup_method: %s" % method.__name__) + self.test_domain = TestEnv.get_method_domain(method) + + def teardown_method(self, method): + print("teardown_method: %s" % method.__name__) + + #----------------------------------------------------------------------------------------------- + # test case: MD with 2 names, one invalid + # + def test_740_000(self): + domain = self.test_domain + domains = [ domain, "invalid!." + domain ] + conf = HttpdConf() + conf.add_admin( "admin@not-forbidden.org" ) + conf.add_md( domains ) + conf.add_vhost(domains) + conf.install() + assert TestEnv.apache_restart() == 0 + md = TestEnv.await_error(domain) + assert md + assert md['renewal']['errors'] > 0 + assert md['renewal']['last']['problem'] == 'urn:ietf:params:acme:error:rejectedIdentifier' + assert md['renewal']['last']['detail'] == ("Error creating new order :: Cannot issue for \"%s\": Invalid character in DNS name" % (domains[1])) + + # test case: MD with 3 names, 2 invalid + # + def test_740_001(self): + domain = self.test_domain + domains = [ domain, "invalid1!." + domain, "invalid2!." + domain ] + conf = HttpdConf() + conf.add_admin( "admin@not-forbidden.org" ) + conf.add_md( domains ) + conf.add_vhost(domains) + conf.install() + assert TestEnv.apache_restart() == 0 + md = TestEnv.await_error(domain) + assert md + assert md['renewal']['errors'] > 0 + assert md['renewal']['last']['problem'] == 'urn:ietf:params:acme:error:rejectedIdentifier' + assert md['renewal']['last']['detail'] == ("Error creating new order :: Cannot issue for \"%s\": Invalid character in DNS name (and 1 more problems. Refer to sub-problems for more information.)" % (domains[1])) + assert md['renewal']['last']['subproblems'] + assert len(md['renewal']['last']['subproblems']) == 2 +