MINOR: acme: allow IP SAN in certificate request

Implement IP in both requestOrder and CSR so a certificate with SAN IPs
can be generated.
This commit is contained in:
William Lallemand 2026-04-20 18:06:43 +02:00
parent 0d14bb7473
commit 95c400d08e
4 changed files with 63 additions and 11 deletions

View File

@ -32193,6 +32193,19 @@ domains <string>
load crt "example.com.pem" acme LE domains "bar.example.com,foo.example.com"
ips <string>
Configure the list of IP addresses that will be included as IP SANs in the
ACME certificate. IP addresses are separated by commas in the list.
Generating a certificate with IPs might require the use of the "shortlived"
profile.
See also Section 12.8 ("ACME"), "acme" and "domains" in this section.
Example:
load crt "server.pem" acme LE ips "192.0.2.1,2001:db8::1"
key <filename>
This argument is optional. Load a private key in PEM format. If a private key
was already defined in "crt", it will overwrite it.

View File

@ -72,6 +72,7 @@ struct ckch_conf {
struct {
char *id;
char **domains;
char **ips;
} acme;
struct {
struct {

View File

@ -2029,7 +2029,9 @@ int acme_req_neworder(struct task *task, struct acme_ctx *ctx, char **errmsg)
{ IST_NULL, IST_NULL }
};
int ret = 1;
int first = 1;
char **san = ctx->store->conf.acme.domains;
char **ip = ctx->store->conf.acme.ips;
if ((req_in = alloc_trash_chunk()) == NULL)
goto error;
@ -2038,12 +2040,16 @@ int acme_req_neworder(struct task *task, struct acme_ctx *ctx, char **errmsg)
chunk_printf(req_in, "{ \"identifiers\": [ ");
if (!san)
if (!san && !ip)
goto error;
for (; san && *san; san++) {
// fprintf(stderr, "%s:%d %s\n", __FUNCTION__, __LINE__, *san);
chunk_appendf(req_in, "%s{ \"type\": \"dns\", \"value\": \"%s\" }", (*san == *ctx->store->conf.acme.domains) ? "" : ",", *san);
chunk_appendf(req_in, "%s{ \"type\": \"dns\", \"value\": \"%s\" }", first ? "" : ",", *san);
first = 0;
}
for (; ip && *ip; ip++) {
chunk_appendf(req_in, "%s{ \"type\": \"ip\", \"value\": \"%s\" }", first ? "" : ",", *ip);
first = 0;
}
chunk_appendf(req_in, " ]");
@ -3038,7 +3044,7 @@ end:
/*
* Generate a X509_REQ using a PKEY and a list of SAN finished by a NULL entry
*/
X509_REQ *acme_x509_req(EVP_PKEY *pkey, char **san)
X509_REQ *acme_x509_req(EVP_PKEY *pkey, char **san, char **ips)
{
struct buffer *san_trash = NULL;
X509_REQ *x = NULL;
@ -3060,9 +3066,9 @@ X509_REQ *acme_x509_req(EVP_PKEY *pkey, char **san)
if ((nm = X509_NAME_new()) == NULL)
goto error;
/* common name is the first SAN in the list */
/* common name is the first domain, or the first IP if no domain */
if (!X509_NAME_add_entry_by_txt(nm, "CN", MBSTRING_ASC,
(unsigned char *)san[0], -1, -1, 0))
(unsigned char *)(san ? san[0] : ips[0]), -1, -1, 0))
goto error;
/* assign the CN to the REQ */
if (!X509_REQ_set_subject_name(x, nm))
@ -3072,8 +3078,11 @@ X509_REQ *acme_x509_req(EVP_PKEY *pkey, char **san)
if ((exts = sk_X509_EXTENSION_new_null()) == NULL)
goto error;
for (i = 0; san[i]; i++) {
chunk_appendf(san_trash, "%sDNS:%s", i ? "," : "", san[i]);
for (i = 0; san && san[i]; i++) {
chunk_appendf(san_trash, "%sDNS:%s", san_trash->data ? "," : "", san[i]);
}
for (i = 0; ips && ips[i]; i++) {
chunk_appendf(san_trash, "%sIP:%s", san_trash->data ? "," : "", ips[i]);
}
if ((str_san = my_strndup(san_trash->area, san_trash->data)) == NULL)
goto error;
@ -3159,8 +3168,8 @@ static int acme_start_task(struct ckch_store *store, char **errmsg)
struct ckch_store *newstore = NULL;
EVP_PKEY *pkey = NULL;
if (!store->conf.acme.domains) {
memprintf(errmsg, "No 'domains' were configured for certificate. ");
if (!store->conf.acme.domains && !store->conf.acme.ips) {
memprintf(errmsg, "No 'domains' or 'ips' were configured for certificate. ");
goto err;
}
@ -3211,7 +3220,7 @@ static int acme_start_task(struct ckch_store *store, char **errmsg)
pkey = NULL;
}
ctx->req = acme_x509_req(newstore->data->key, store->conf.acme.domains);
ctx->req = acme_x509_req(newstore->data->key, store->conf.acme.domains, store->conf.acme.ips);
if (!ctx->req) {
memprintf(errmsg, "%sCan't generate a CSR.", *errmsg ? *errmsg : "");
goto err;

View File

@ -1112,6 +1112,26 @@ struct ckch_store *ckchs_dup(const struct ckch_store *src)
dst->conf.acme.domains = r;
}
if (src->conf.acme.ips) {
r = NULL;
n = 0;
/* copy the array of IP strings */
while (src->conf.acme.ips[n]) {
r = realloc(r, sizeof(char *) * (n + 2));
if (!r)
goto error;
r[n] = strdup(src->conf.acme.ips[n]);
if (!r[n])
goto error;
n++;
}
r[n] = 0;
dst->conf.acme.ips = r;
}
return dst;
error:
@ -4904,6 +4924,7 @@ struct ckch_conf_kws ckch_conf_kws[] = {
{ "acme", offsetof(struct ckch_conf, acme.id), PARSE_TYPE_STR, ckch_conf_acme_init, },
#endif
{ "domains", offsetof(struct ckch_conf, acme.domains), PARSE_TYPE_ARRAY_SUBSTR, NULL, },
{ "ips", offsetof(struct ckch_conf, acme.ips), PARSE_TYPE_ARRAY_SUBSTR, NULL, },
{ "generate-dummy", offsetof(struct ckch_conf, gencrt.on), PARSE_TYPE_ONOFF, NULL, },
{ "keytype", offsetof(struct ckch_conf, gencrt.key.type), PARSE_TYPE_STR, NULL, },
{ "bits", offsetof(struct ckch_conf, gencrt.key.bits), PARSE_TYPE_INT, NULL, },
@ -5276,6 +5297,14 @@ void ckch_conf_clean(struct ckch_conf *conf)
}
ha_free(&conf->acme.domains);
r = conf->acme.ips;
while (r && *r) {
char *prev = *r;
r++;
free(prev);
}
ha_free(&conf->acme.ips);
}
static char current_crtstore_name[PATH_MAX] = {};