From: William Lallemand Date: Mon, 20 Apr 2026 15:33:41 +0000 (+0200) Subject: MINOR: acme: implement draft-ietf-acme-profiles X-Git-Url: http://git.ipfire.org/gitweb/?a=commitdiff_plain;h=0d14bb74733d4746cc30cf8af6fbfaa70be70866;p=thirdparty%2Fhaproxy.git MINOR: acme: implement draft-ietf-acme-profiles The ACME Profiles extension (draft-ietf-acme-profiles) allows a client to request a specific certificate profile by including a "profile" field in the newOrder request. This lets the CA select the appropriate certificate issuance policy (e.g. "classic", "shortlived") for a given order. A new "profile" keyword is added to the acme section. When set, its value is included in the newOrder JSON payload sent to the CA. --- diff --git a/doc/configuration.txt b/doc/configuration.txt index 67f4337af..4e6cdcbea 100644 --- a/doc/configuration.txt +++ b/doc/configuration.txt @@ -32538,6 +32538,22 @@ map account used. The acme task will add entries before validating the challenge and will remove the entries at the end of the task. +profile + Request a specific certificate profile from the CA by including a "profile" + field in the newOrder request. This implements draft-ietf-acme-profiles. + + Profile names are CA-specific short identifiers (e.g. "classic", + "shortlived"). When set, the profile name is sent as-is in the newOrder + JSON payload. The CA is free to ignore the request or return an error if + the profile is not supported. When not set, no profile field is included + and the CA uses its default issuance policy. + + See https://letsencrypt.org/docs/profiles/ for letsencrypt profiles. + + Example: + # Request short-lived certificates + profile shortlived + reuse-key { on | off } If set to "on", HAProxy won't generate a new private key and will keep the previous one. Rotating private keys is recommended, when enabling this option diff --git a/include/haproxy/acme-t.h b/include/haproxy/acme-t.h index 091dcd708..088a2ef46 100644 --- a/include/haproxy/acme-t.h +++ b/include/haproxy/acme-t.h @@ -41,6 +41,7 @@ struct acme_cfg { int curves; /* NID of curves */ } key; char *challenge; /* HTTP-01, DNS-01, etc */ + char *profile; /* ACME profile */ char *vars; /* variables put in the dpapi sink */ char *provider; /* DNS provider put in the dpapi sink */ struct acme_cfg *next; diff --git a/src/acme.c b/src/acme.c index cb419d467..8b4268b3f 100644 --- a/src/acme.c +++ b/src/acme.c @@ -4,6 +4,7 @@ * Implements the ACMEv2 RFC 8555 protocol * Implements the following extensions to the protocol: * draft-ietf-acme-dns-persist - DNS-PERSIST-01 challenge + * draft-ietf-acme-profiles - Profiles Extension */ #include "haproxy/ticks.h" @@ -454,6 +455,35 @@ static int cfg_parse_acme_kws(char **args, int section_type, struct proxy *curpx goto out; } + } else if (strcmp(args[0], "profile") == 0) { + /* save the profile name */ + const char *p; + + if (!*args[1]) { + ha_alert("parsing [%s:%d]: keyword '%s' in '%s' section requires an argument\n", file, linenum, args[0], cursection); + err_code |= ERR_ALERT | ERR_FATAL; + goto out; + } + if (alertif_too_many_args(1, file, linenum, args, &err_code)) + goto out; + + /* profile names are used verbatim in a JSON string; only allow + * alphanumeric characters, hyphens and underscores */ + for (p = args[1]; *p; p++) { + if (!isalnum((uchar)*p) && *p != '-' && *p != '_') { + ha_alert("parsing [%s:%d]: keyword '%s' in '%s' section contains unauthorized character '%c'\n", file, linenum, args[0], cursection, *p); + err_code |= ERR_ALERT | ERR_FATAL; + goto out; + } + } + + ha_free(&cur_acme->profile); + cur_acme->profile = strdup(args[1]); + if (!cur_acme->profile) { + err_code |= ERR_ALERT | ERR_FATAL; + ha_alert("parsing [%s:%d]: out of memory.\n", file, linenum); + goto out; + } } else if (strcmp(args[0], "map") == 0) { /* save the map name for thumbprint + token storage */ if (!*args[1]) { @@ -957,6 +987,7 @@ void deinit_acme() ha_free(&acme_cfgs->provider); ha_free(&acme_cfgs->challenge); ha_free(&acme_cfgs->map); + ha_free(&acme_cfgs->profile); free(acme_cfgs); acme_cfgs = next; @@ -974,6 +1005,7 @@ static struct cfg_kw_list cfg_kws_acme = {ILH, { { CFG_ACME, "bits", cfg_parse_acme_cfg_key }, { CFG_ACME, "curves", cfg_parse_acme_cfg_key }, { CFG_ACME, "map", cfg_parse_acme_kws }, + { CFG_ACME, "profile", cfg_parse_acme_kws }, { CFG_ACME, "reuse-key", cfg_parse_acme_kws }, { CFG_ACME, "challenge-ready", cfg_parse_acme_kws }, { CFG_ACME, "dns-delay", cfg_parse_acme_kws }, @@ -2014,7 +2046,10 @@ int acme_req_neworder(struct task *task, struct acme_ctx *ctx, char **errmsg) chunk_appendf(req_in, "%s{ \"type\": \"dns\", \"value\": \"%s\" }", (*san == *ctx->store->conf.acme.domains) ? "" : ",", *san); } - chunk_appendf(req_in, " ] }"); + chunk_appendf(req_in, " ]"); + if (ctx->cfg->profile) + chunk_appendf(req_in, ", \"profile\": \"%s\"", ctx->cfg->profile); + chunk_appendf(req_in, " }"); TRACE_DATA("NewOrder Decode", ACME_EV_REQ, ctx, &ctx->resources.newOrder, req_in);