From 418f0c0bbe1a6663f116fce93d4b3f00d5380a78 Mon Sep 17 00:00:00 2001 From: Mia Kanashi Date: Mon, 23 Feb 2026 01:04:46 +0200 Subject: [PATCH] BUG/MEDIUM: acme: skip doing challenge if it is already valid If server returns an auth with status valid it seems that client needs to always skip it, CA can recycle authorizations, without this change haproxy fails to obtain certificates in that case. It is also something that is explicitly allowed and stated in the dns-persist-01 draft RFC. Note that it would be better to change how haproxy does status polling, and implements the state machine, but that will take some thought and time, this patch is a quick fix of the problem. See: https://github.com/letsencrypt/boulder/issues/2125 https://github.com/letsencrypt/pebble/issues/133 This must be backported to 3.2 and later. --- include/haproxy/acme-t.h | 1 + src/acme.c | 30 ++++++++++++++++++++++++++++++ 2 files changed, 31 insertions(+) diff --git a/include/haproxy/acme-t.h b/include/haproxy/acme-t.h index b26682dd0..e93b617c7 100644 --- a/include/haproxy/acme-t.h +++ b/include/haproxy/acme-t.h @@ -58,6 +58,7 @@ struct acme_auth { struct ist auth; /* auth URI */ struct ist chall; /* challenge URI */ struct ist token; /* token */ + int validated; /* already validated */ int ready; /* is the challenge ready ? */ void *next; }; diff --git a/src/acme.c b/src/acme.c index 406c3cfae..44b0d7995 100644 --- a/src/acme.c +++ b/src/acme.c @@ -1654,6 +1654,19 @@ int acme_res_auth(struct task *task, struct acme_ctx *ctx, struct acme_auth *aut auth->dns = istdup(ist2(t2->area, t2->data)); + ret = mjson_get_string(hc->res.buf.area, hc->res.buf.data, "$.status", trash.area, trash.size); + if (ret == -1) { + memprintf(errmsg, "couldn't get a \"status\" from Authorization URL \"%s\"", auth->auth.ptr); + goto error; + } + trash.data = ret; + + /* if auth is already valid we need to skip solving challenges */ + if (strncasecmp("valid", trash.area, trash.data) == 0) { + auth->validated = 1; + goto out; + } + /* get the multiple challenges and select the one from the configuration */ for (i = 0; ; i++) { int ret; @@ -1761,6 +1774,7 @@ int acme_res_auth(struct task *task, struct acme_ctx *ctx, struct acme_auth *aut break; } +out: ret = 0; error: @@ -2263,6 +2277,14 @@ re: break; case ACME_CHALLENGE: if (http_st == ACME_HTTP_REQ) { + /* if challenge is already validated we skip this stage */ + if (ctx->next_auth->validated) { + if ((ctx->next_auth = ctx->next_auth->next) == NULL) { + st = ACME_CHKCHALLENGE; + ctx->next_auth = ctx->auths; + } + goto nextreq; + } /* if the challenge is not ready, wait to be wakeup */ if (!ctx->next_auth->ready) @@ -2292,6 +2314,14 @@ re: break; case ACME_CHKCHALLENGE: if (http_st == ACME_HTTP_REQ) { + /* if challenge is already validated we skip this stage */ + if (ctx->next_auth->validated) { + if ((ctx->next_auth = ctx->next_auth->next) == NULL) + st = ACME_FINALIZE; + + goto nextreq; + } + if (acme_post_as_get(task, ctx, ctx->next_auth->chall, &errmsg) != 0) goto retry; } -- 2.47.3