From: William Lallemand Date: Wed, 9 Apr 2025 18:31:52 +0000 (+0200) Subject: MINOR: acme: check if the account exist X-Git-Tag: v3.2-dev11~95 X-Git-Url: http://git.ipfire.org/?a=commitdiff_plain;h=7f9bf4d5f73f6d28fe2c2bb27410ae38fe0f08e3;p=thirdparty%2Fhaproxy.git MINOR: acme: check if the account exist This patch implements the retrival of the KID (account identifier) using the pkey. A request is sent to the newAccount URL using the onlyReturnExisting option, which allow to get the kid of an existing account. acme_jws_payload() implement a way to generate a JWS payload using the nonce, pkey and provided URI. --- diff --git a/include/haproxy/acme-t.h b/include/haproxy/acme-t.h index 4a0940086..0e319d381 100644 --- a/include/haproxy/acme-t.h +++ b/include/haproxy/acme-t.h @@ -32,6 +32,7 @@ struct acme_cfg { enum acme_st { ACME_RESSOURCES = 0, ACME_NEWNONCE, + ACME_CHKACCOUNT, ACME_END }; @@ -54,5 +55,6 @@ struct acme_ctx { struct ist newOrder; } ressources; struct ist nonce; + struct ist kid; }; #endif diff --git a/src/acme.c b/src/acme.c index 639932ef2..829a99829 100644 --- a/src/acme.c +++ b/src/acme.c @@ -502,7 +502,7 @@ static void acme_httpclient_end(struct httpclient *hc) } -int acme_http_req(struct task *task, struct acme_ctx *ctx, struct ist url, enum http_meth_t meth) +int acme_http_req(struct task *task, struct acme_ctx *ctx, struct ist url, enum http_meth_t meth, const struct http_hdr *hdrs, struct ist payload) { struct httpclient *hc; @@ -510,7 +510,7 @@ int acme_http_req(struct task *task, struct acme_ctx *ctx, struct ist url, enum if (!hc) goto error; - if (httpclient_req_gen(hc, hc->req.url, hc->req.meth, NULL, IST_NULL) != ERR_NONE) + if (httpclient_req_gen(hc, hc->req.url, hc->req.meth, hdrs, payload) != ERR_NONE) goto error; hc->ops.res_end = acme_httpclient_end; @@ -529,6 +529,164 @@ error: } +int acme_jws_payload(struct buffer *req, struct ist nonce, struct ist url, EVP_PKEY *pkey, struct buffer *output, char **errmsg) +{ + struct buffer *b64payload = NULL; + struct buffer *b64prot = NULL; + struct buffer *b64sign = NULL; + struct buffer *jwk = NULL; + enum jwt_alg alg = JWS_ALG_NONE; + int ret = 1; + + + if (req->data == 0) { + memprintf(errmsg, "no input data"); + goto error; + } + + b64payload = alloc_trash_chunk(); + b64prot = alloc_trash_chunk(); + jwk = alloc_trash_chunk(); + b64sign = alloc_trash_chunk(); + + if (!b64payload || !b64prot || !jwk || !b64sign || !output) { + memprintf(errmsg, "out of memory"); + goto error; + } + + jwk->data = EVP_PKEY_to_pub_jwk(pkey, jwk->area, jwk->size); + alg = EVP_PKEY_to_jws_alg(pkey); + + if (alg == JWS_ALG_NONE) { + memprintf(errmsg, "couldn't chose a JWK algorithm"); + goto error; + } + + b64payload->data = jws_b64_payload(req->area, b64payload->area, b64payload->size); + b64prot->data = jws_b64_protected(alg, NULL, jwk->area, nonce.ptr, url.ptr, b64prot->area, b64prot->size); + b64sign->data = jws_b64_signature(pkey, alg, b64prot->area, b64payload->area, b64sign->area, b64sign->size); + output->data = jws_flattened(b64prot->area, b64payload->area, b64sign->area, output->area, output->size); + + if (output->data == 0) + goto error; + + ret = 0; + +error: + free_trash_chunk(b64sign); + free_trash_chunk(jwk); + free_trash_chunk(b64prot); + free_trash_chunk(b64payload); + + + return ret; +} + +int acme_req_account(struct task *task, struct acme_ctx *ctx, int newaccount, char **errmsg) +{ + struct buffer *req_in = NULL; + struct buffer *req_out = NULL; + const struct http_hdr hdrs[] = { + { IST("Content-Type"), IST("application/jose+json") }, + { IST_NULL, IST_NULL } + }; + char *accountreq = "{\n" + " \"termsOfServiceAgreed\": true,\n" + " \"onlyReturnExisting\": true\n" + "}\n"; + char *newaccountreq = "{\n" + " \"termsOfServiceAgreed\": true,\n" + " \"contact\": [\n" + " \"mailto:%s\"\n" + " ]\n" + "}\n"; + int ret = 1; + + if ((req_in = alloc_trash_chunk()) == NULL) + goto error; + if ((req_out = alloc_trash_chunk()) == NULL) + goto error; + + if (newaccount) + chunk_printf(req_in, newaccountreq, ctx->cfg->account.contact); + else + chunk_printf(req_in, "%s", accountreq); + + if (acme_jws_payload(req_in, ctx->nonce, ctx->ressources.newAccount, ctx->cfg->account.pkey, req_out, errmsg) != 0) + goto error; + + if (acme_http_req(task, ctx, ctx->ressources.newAccount, HTTP_METH_POST, hdrs, ist2(req_out->area, req_out->data))) + goto error; + + ret = 0; +error: + memprintf(errmsg, "couldn't generate the newAccount request"); + + free_trash_chunk(req_in); + free_trash_chunk(req_out); + + return ret; +} + +int acme_res_account(struct task *task, struct acme_ctx *ctx, char **errmsg) +{ + struct httpclient *hc; + struct http_hdr *hdrs, *hdr; + struct buffer *t1 = NULL, *t2 = NULL; + int ret = 1; + + hc = ctx->hc; + if (!hc) + goto error; + + if ((t1 = alloc_trash_chunk()) == NULL) + goto error; + if ((t2 = alloc_trash_chunk()) == NULL) + goto error; + + hdrs = hc->res.hdrs; + + for (hdr = hdrs; isttest(hdr->v); hdr++) { + if (isteqi(hdr->n, ist("Location"))) { + istfree(&ctx->kid); + ctx->kid = istdup(hdr->v); + } + if (isteqi(hdr->n, ist("Replay-Nonce"))) { + istfree(&ctx->nonce); + ctx->nonce = istdup(hdr->v); + } + } + + if (hc->res.status < 200 || hc->res.status >= 300) { + if ((ret = mjson_get_string(hc->res.buf.area, hc->res.buf.data, "$.detail", t1->area, t1->size)) > -1) + t1->data = ret; + if ((ret = mjson_get_string(hc->res.buf.area, hc->res.buf.data, "$.type", t2->area, t2->size)) > -1) + t2->data = ret; + + /* not an error, we only need to create a new account */ + if (strcmp("urn:ietf:params:acme:error:accountDoesNotExist", t2->area) == 0) + goto out; + + if (t2->data && t1->data) + memprintf(errmsg, "invalid HTTP status code %d when getting Account URL: \"%.*s\" (%.*s)", hc->res.status, (int)t1->data, t1->area, (int)t2->data, t2->area); + else + memprintf(errmsg, "invalid HTTP status code %d when getting Account URL", hc->res.status); + goto error; + } +out: + ret = 0; + +error: + free_trash_chunk(t1); + free_trash_chunk(t2); + httpclient_destroy(hc); + ctx->hc = NULL; + + return ret; +} + + + int acme_nonce(struct task *task, struct acme_ctx *ctx, char **errmsg) { struct httpclient *hc; @@ -645,7 +803,7 @@ struct task *acme_process(struct task *task, void *context, unsigned int state) switch (st) { case ACME_RESSOURCES: if (http_st == ACME_HTTP_REQ) { - if (acme_http_req(task, ctx, ist(ctx->cfg->uri), HTTP_METH_GET) != 0) + if (acme_http_req(task, ctx, ist(ctx->cfg->uri), HTTP_METH_GET, NULL, IST_NULL) != 0) goto retry; } @@ -661,7 +819,7 @@ struct task *acme_process(struct task *task, void *context, unsigned int state) break; case ACME_NEWNONCE: if (http_st == ACME_HTTP_REQ) { - if (acme_http_req(task, ctx, ctx->ressources.newNonce, HTTP_METH_HEAD) != 0) + if (acme_http_req(task, ctx, ctx->ressources.newNonce, HTTP_METH_HEAD, NULL, IST_NULL) != 0) goto retry; } if (http_st == ACME_HTTP_RES) { @@ -669,6 +827,22 @@ struct task *acme_process(struct task *task, void *context, unsigned int state) http_st = ACME_HTTP_REQ; goto retry; } + st = ACME_CHKACCOUNT; + http_st = ACME_HTTP_REQ; + task_wakeup(task, TASK_WOKEN_MSG); + } + + break; + case ACME_CHKACCOUNT: + if (http_st == ACME_HTTP_REQ) { + if (acme_req_account(task, ctx, 0, &errmsg) != 0) + goto retry; + } + if (http_st == ACME_HTTP_RES) { + if (acme_res_account(task, ctx, &errmsg) != 0) { + http_st = ACME_HTTP_REQ; + goto retry; + } st = ACME_END; }