Hi Mia,
On Fri, May 01, 2026 at 09:06:19PM +0300, Mia Kanashi wrote:
> Subject: [PATCH 3/4] MINOR: acme: implement EAB - external account binding
> This patch implements ACME EAB.
>
> Patch introduces two new functions related to JWS handling:
> jws_b64_hmac_signature in jws.c, and acme_jws_eab_payload in acme.c,
> which are like jws_b64_signature and acme_jws_payload respectively.
> They duplicate basically the same functionality but for the use case
> of HMAC, OpenSSL allows to use EVP_PKEY for HMAC functionality, but
> although isn't deprecated it was removed in BoringSSL, and was removed
> (due to BoringSSL roots) but then readded back in AWS-LC, because of
> "legacy clients" (citing them), for that reason alone I say that having
> a dedicated function for hmac is better, HMAC() macro seems to be widely
> supported unlike other ways of doing same thing.
>
> Also touches and rewrites jws_b64_protected() to work for the use case
> of EAB, in EAB JWS nonce must not be set.
>
> Configuring EAB requires two parts: Key ID and MAC Key.
> Key ID is an ASCII string that specifies the name of the record CA
> should look up. MAC Key is a base64url encoded key that is used
> for the sake of JWS signing, using HS256 or other algorithms.
>
> It is not clear whether providing a config for algorithms other than
> HS256 makes any sense, standard ACME implementation of Go decided not to
> for example. There are really no benefits, HS256 support is mandatory,
> but not for others, so I decided to split that part into a separate
> commit. Keep in mind that HS256 is only used for initial JWS signing
> of initial account binding process, but not for anything else.
>
> A thing about EAB is that it is /required only during account creation/
> so it is unexpectedly complex to think about.
> Also some CAs provide EAB credential pair that is reused between
> multiple account order requests, for example ZeroSSL, but others like
> Google Trusted Services require an unique EAB credential for each new
> account creation request.
>
> There are a lot of ways config could be implemented, I decided to make
> so that Key ID and MAC Key are stored in separate files on disk.
> Other servers that implement ACME, for example stalwart-mail, Caddy,
> allow to specify values directly in the config fields, but then
> they have template directives that allow to expand contents of a file
> into them.
>
> In Caddy you can do one of the following:
>
> acme_eab {
> key_id {file./etc/caddy/eab.key_id}
> mac_key {file./etc/caddy/eab.mac_key}
> }
>
> acme_eab {
> key_id kid-1
> mac_key HjudV5qnbreN-n9WyFSH
> }
>
> acme_eab {
> key_id {env.ACME_EAB_KEY_ID}
> mac_key {env.ACME_EAB_MAC_KEY}
> }
>
> HAProxy allows to expand environment variables, but not expand files
> like that.
>
> So I decided to implement a purely file based approach for now,
> as it provides the most operational flexibility, and in particular works
> well with systemd credentials.
>
> Another way I though to implement it is by combining multiple creds
> in a single file, for example using a format like: <id>:<key>
>
> Or using a map file per acme section with contents as following:
>
> id "kid-1"
> key "whatever"
>
> But the multi-file approach seemed the most compatible with existing
> setups using placeholders. Although, ID and the Key do come in pairs,
> so it makes not much sense to separate them logically speaking,
> I haven't seen anyone else to do it like that in a single file,
> unless it was a config file.
>
> I decided to use options like this in an acme section:
>
> eab-mac-alg HS512
> eab-mac-key pebble.eab.key
> eab-key-id pebble.eab.id
>
> It could have been instead something like this too:
>
> eab alg HS512 mac-key pebble.eab.key key-id pebble.eab.id
>
> The reason I want with the first is just because it was easier to do :)
>
> I also decided to not error out on empty files, but warn instead,
> as credential existance is always checked for.
> Used read_line_to_trash function from tools.c for reading files,
> that is something that could be replaced by a dedicated function too.
>
> There is no need to explicitly configure anything in the config,
> If no eab-* keywords are used, filename is implicitly created based
> on a section name, and then haproxy tries to load it. Whether it is a
> good idea or not... I'm not sure.
>
> There are a lot of decisions here that are slightly tricky,
> even for this simple feature, if there are any ideas on how to change it
> for the better, it should be done. I hope other HAProxy users will
> guide the overall direction here.
>
> No backport needed :)
You should probably split this commit in two, introduce the HMAC in the jws
first, then use it in the ACME code.
> ---
> doc/configuration.txt | 14 ++++
> include/haproxy/acme-t.h | 7 ++
> include/haproxy/jws.h | 1 +
> src/acme.c | 163 +++++++++++++++++++++++++++++++++++++--
> src/jws.c | 80 ++++++++++++++-----
> 5 files changed, 239 insertions(+), 26 deletions(-)
>
> diff --git a/doc/configuration.txt b/doc/configuration.txt
> index 8a673c29a..d2b91eed2 100644
> --- a/doc/configuration.txt
> +++ b/doc/configuration.txt
> @@ -32638,6 +32638,20 @@ Example:
> curves P-384
> map virt@acme
>
> +eab-mac-key <filename>
> +eab-key-id <filename>
> + Configure the path to the EAB MAC key and EAB key id credential pair. You
> + should get credentials from you CA and place them at the specified path
> + before launching HAProxy. If not explicitly specified, HAProxy will try to
> + load a file based on an acme section name similar to "account-key" keyword,
> + "<name>.eab.id" for the key id, "<name>.eab.key" for the mac key, where
> + "<name>" is the name of the current acme section. If the file doesn't
> exist,
> + or is empty, HAProxy will ignore it. Whitespace in the file is NOT ignored.
> +
> + EAB key id file should be a plain ASCII string that CA provides as an id.
> + EAB MAC key file should be a base64url encoded MAC key that CA provides.
> + EAB credentials are used only during the initial ACME account creation, and
> + can be removed afterwards.
>
> 12.9. Healthchecks
> ------------------
> diff --git a/include/haproxy/acme-t.h b/include/haproxy/acme-t.h
> index 24df7c44a..87f3bfffa 100644
> --- a/include/haproxy/acme-t.h
> +++ b/include/haproxy/acme-t.h
> @@ -4,6 +4,7 @@
>
> #include <haproxy/acme_resolvers-t.h>
> #include <haproxy/istbuf.h>
> +#include <haproxy/buf-t.h>
> #include <haproxy/openssl-compat.h>
>
> #if defined(HAVE_ACME)
> @@ -40,6 +41,12 @@ struct acme_cfg {
> int bits; /* bits for RSA */
> int curves; /* NID of curves */
> } key;
> + struct {
> + char *kid_file; /* EAB key id filename */
> + char *mac_key_file; /* base64url encoded EAB hmac key
> filename */
> + char *kid; /* EAB key id */
> + struct buffer mac_key; /* raw EAB hmac key */
> + } eab;
> char *challenge; /* HTTP-01, DNS-01, etc */
> char *profile; /* ACME profile */
> char *vars; /* variables put in the dpapi sink */
> diff --git a/include/haproxy/jws.h b/include/haproxy/jws.h
> index f68147cff..4f5fb8c40 100644
> --- a/include/haproxy/jws.h
> +++ b/include/haproxy/jws.h
> @@ -11,6 +11,7 @@ size_t EVP_PKEY_to_pub_jwk(EVP_PKEY *pkey, char *dst,
> size_t dsize);
> enum jwt_alg EVP_PKEY_to_jws_alg(EVP_PKEY *pkey);
> size_t jws_b64_payload(char *payload, char *dst, size_t dsize);
> size_t jws_b64_protected(enum jwt_alg alg, char *kid, char *jwk, char
> *nonce, char *url, char *dst, size_t dsize);
> +size_t jws_b64_hmac_signature(char *key, size_t key_len, enum jwt_alg alg,
> char *b64protected, char *b64payload, char *dst, size_t dsize);
> size_t jws_b64_signature(EVP_PKEY *pkey, enum jwt_alg alg, char
> *b64protected, char *b64payload, char *dst, size_t dsize);
> size_t jws_flattened(char *protected, char *payload, char *signature, char
> *dst, size_t dsize);
> size_t jws_thumbprint(EVP_PKEY *pkey, char *dst, size_t dsize);
> diff --git a/src/acme.c b/src/acme.c
> index dc038bcab..e7f7d1a77 100644
> --- a/src/acme.c
> +++ b/src/acme.c
> @@ -418,6 +418,38 @@ static int cfg_parse_acme_kws(char **args, int
> section_type, struct proxy *curpx
> ha_alert("parsing [%s:%d]: out of memory.\n", file,
> linenum);
> goto out;
> }
> + } else if (strcmp(args[0], "eab-key-id") == 0) {
> + 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;
> +
> + ha_free(&cur_acme->eab.kid_file);
> + cur_acme->eab.kid_file = strdup(args[1]);
> + if (!cur_acme->eab.kid_file) {
> + err_code |= ERR_ALERT | ERR_FATAL;
> + ha_alert("parsing [%s:%d]: out of memory.\n", file,
> linenum);
> + goto out;
> + }
> + } else if (strcmp(args[0], "eab-mac-key") == 0) {
> + 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;
> +
> + ha_free(&cur_acme->eab.mac_key_file);
> + cur_acme->eab.mac_key_file = strdup(args[1]);
> + if (!cur_acme->eab.mac_key_file) {
> + err_code |= ERR_ALERT | ERR_FATAL;
> + ha_alert("parsing [%s:%d]: out of memory.\n", file,
> linenum);
> + goto out;
> + }
> } else if (strcmp(args[0], "challenge") == 0) {
> if ((!*args[1]) ||
> ((strcasecmp("http-01", args[1]) != 0) &&
> @@ -799,21 +831,87 @@ static int cfg_postsection_acme()
> char *errmsg = NULL;
> char *path;
> char store_path[PATH_MAX]; /* complete path with crt_base */
> + int rv = 0;
> struct stat st;
>
> /* if dns-persist-01 is set, add an extra INITIAL_DNS check */
> if (strcasecmp(cur_acme->challenge, "dns-persist-01") == 0)
> cur_acme->cond_ready |= ACME_RDY_INITIAL_DNS;
>
> - /* if account key filename is unspecified, choose a filename for it */
> - if (!cur_acme->account.file) {
> - if (!memprintf(&cur_acme->account.file, "%s.account.key",
> cur_acme->name)) {
> + /* if either account key, eab kid, or mac key filename is unspecified,
> choose a filename for it */
> + if ((!cur_acme->account.file && !memprintf(&cur_acme->account.file,
> "%s.account.key", cur_acme->name))
> + || (!cur_acme->eab.kid_file && !memprintf(&cur_acme->eab.kid_file,
> "%s.eab.id", cur_acme->name))
> + || (!cur_acme->eab.mac_key_file &&
> !memprintf(&cur_acme->eab.mac_key_file, "%s.eab.key", cur_acme->name))) {
> + err_code |= ERR_ALERT | ERR_FATAL | ERR_ABORT;
> + ha_alert("acme: out of memory.\n");
> + goto out;
> + }
> +
> + if (global_ssl.crt_base && *cur_acme->eab.kid_file != '/')
> + rv = read_line_to_trash("%s/%s", global_ssl.crt_base,
> cur_acme->eab.kid_file);
> + else
> + rv = read_line_to_trash("%s", cur_acme->eab.kid_file);
> +
> + /* if read at least one character successfully */
> + if (rv >= 1) {
> + const char *p;
> + cur_acme->eab.kid = my_strndup(trash.area, trash.data);
> + if (!cur_acme->eab.kid) {
> + ha_alert("acme: out of memory.\n");
> + err_code |= ERR_ALERT | ERR_FATAL | ERR_ABORT;
> + goto out;
> + }
> +
> + /* technically ACME RFC allows any ASCII string here,
> + * but in practice CAs usually provide key id as a base64url
> encoded secret or an UUID */
> + for (p = cur_acme->eab.kid; *p; p++) {
> + if (!isalnum((uchar)*p) && *p != '-' && *p != '_') {
> + ha_warning("acme: section '%s': EAB key id
> contains strange character '%c'.\n", cur_acme->name, *p);
> + break; /* no need to print this warning many
> times */
> + }
> + }
> + } else if (rv == 0) {
> + /* empty files are allowed, but issue a warning */
> + ha_warning("acme: section '%s': EAB key id from '%s' is
> empty.\n", cur_acme->name, cur_acme->eab.kid_file);
> + }
> +
> + if (global_ssl.crt_base && *cur_acme->eab.mac_key_file != '/')
> + rv = read_line_to_trash("%s/%s", global_ssl.crt_base,
> cur_acme->eab.mac_key_file);
> + else
> + rv = read_line_to_trash("%s", cur_acme->eab.mac_key_file);
> +
I don't know if that's a good idea to use crt-base here, if it's not a PEM
certificate we will probably need something else to configure the path. Like an
acme-account-path or something like that.
> + if (rv >= 1) {
> + struct buffer *dec_mac = get_trash_chunk();
> +
> + rv = base64dec(trash.area, trash.data, dec_mac->area,
> dec_mac->size);
> + if (rv < 0) {
> + ha_alert("acme: section '%s': failed to base64url
> decode EAB mac key.\n", cur_acme->name);
> err_code |= ERR_ALERT | ERR_FATAL | ERR_ABORT;
> + goto out;
> + }
> + dec_mac->data = rv;
> +
> + if (rv < 32) {
> + ha_alert("acme: section '%s': EAB mac key from '%s' is
> only %d bytes long, but at least 32 bytes is required for the specified mac
> type.\n",
> + cur_acme->name, cur_acme->eab.kid_file, rv);
> + err_code |= ERR_ALERT | ERR_FATAL | ERR_ABORT;
> + goto out;
> + }
> +
> + if (chunk_dup(&cur_acme->eab.mac_key, dec_mac) == NULL) {
> ha_alert("acme: out of memory.\n");
> + err_code |= ERR_ALERT | ERR_FATAL | ERR_ABORT;
> goto out;
> }
> + } else if (rv == 0) {
> + ha_warning("acme: section '%s': EAB mac key from '%s' is
> empty.\n", cur_acme->name, cur_acme->eab.mac_key_file);
> }
>
> + if ((cur_acme->eab.mac_key.data == 0) != (cur_acme->eab.kid == NULL)) {
> + ha_alert("acme: section '%s': EAB mac key and key id are
> mutually dependent, specify both or neither.\n", cur_acme->name);
> + err_code |= ERR_ALERT | ERR_FATAL | ERR_ABORT;
> + goto out;
> + }
>
> if (global_ssl.crt_base && *cur_acme->account.file != '/') {
> int rv;
> @@ -988,6 +1086,8 @@ void deinit_acme()
> ha_free(&acme_cfgs->challenge);
> ha_free(&acme_cfgs->map);
> ha_free(&acme_cfgs->profile);
> + chunk_destroy(&acme_cfgs->eab.mac_key);
> + ha_free(&acme_cfgs->eab.kid);
>
> free(acme_cfgs);
> acme_cfgs = next;
> @@ -1010,6 +1110,8 @@ static struct cfg_kw_list cfg_kws_acme = {ILH, {
> { CFG_ACME, "challenge-ready", cfg_parse_acme_kws },
> { CFG_ACME, "dns-delay", cfg_parse_acme_kws },
> { CFG_ACME, "dns-timeout", cfg_parse_acme_kws },
> + { CFG_ACME, "eab-key-id", cfg_parse_acme_kws },
> + { CFG_ACME, "eab-mac-key", cfg_parse_acme_kws },
> { CFG_ACME, "acme-vars", cfg_parse_acme_vars_provider },
> { CFG_ACME, "provider-name", cfg_parse_acme_vars_provider },
> { CFG_GLOBAL, "acme.scheduler", cfg_parse_global_acme_sched },
> @@ -1269,6 +1371,46 @@ int acme_jws_payload(struct buffer *req, struct ist
> nonce, struct ist url, EVP_P
> return ret;
> }
>
> +int acme_jws_eab_payload(struct ist url, EVP_PKEY *acc_key, struct buffer
> mac_key, char *kid, 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_HS256;
> + int ret = 1;
> +
> + 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(acc_key, jwk->area, jwk->size);
> +
> + b64payload->data = jws_b64_payload(jwk->area, b64payload->area,
> b64payload->size);
> + b64prot->data = jws_b64_protected(alg, kid, NULL, NULL, url.ptr,
> b64prot->area, b64prot->size);
> + b64sign->data = jws_b64_hmac_signature(mac_key.area, mac_key.data, 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(b64payload);
> + free_trash_chunk(b64prot);
> + free_trash_chunk(jwk);
> + free_trash_chunk(b64sign);
> +
> + return ret;
> +}
> +
> /*
> * Update every certificate instances for the new store
> *
> @@ -2211,20 +2353,28 @@ int acme_req_account(struct task *task, struct
> acme_ctx *ctx, int newaccount, ch
> {
> struct buffer *req_in = NULL;
> struct buffer *req_out = NULL;
> + struct buffer *eab_req_out = NULL;
> const struct http_hdr hdrs[] = {
> { IST("Content-Type"), IST("application/jose+json") },
> { IST_NULL, IST_NULL }
> };
> int ret = 1;
>
> - if ((req_in = alloc_trash_chunk()) == NULL)
> + if ((req_in = alloc_trash_chunk()) == NULL)
> goto error;
> - if ((req_out = alloc_trash_chunk()) == NULL)
> + if ((req_out = alloc_trash_chunk()) == NULL)
> + goto error;
> + if ((eab_req_out = alloc_trash_chunk()) == NULL)
> goto error;
>
> if (newaccount) {
> chunk_appendf(req_in, "{");
> - if (ctx->cfg->account.contact != NULL)
> + if (ctx->cfg->eab.mac_key.data > 0 && ctx->cfg->eab.kid !=
> NULL) {
> + if (acme_jws_eab_payload(ctx->resources.newAccount,
> ctx->cfg->account.pkey, ctx->cfg->eab.mac_key, ctx->cfg->eab.kid,
> eab_req_out, errmsg) != 0)
> + goto out;
> + chunk_appendf(req_in, "\"externalAccountBinding\":
> %.*s,", (int)eab_req_out->data, eab_req_out->area);
> + }
> + if (ctx->cfg->account.contact)
> chunk_appendf(req_in, "\"contact\": [ \"mailto:%s\"
> ],", ctx->cfg->account.contact);
> chunk_appendf(req_in, "\"termsOfServiceAgreed\": true");
> chunk_appendf(req_in, "}");
> @@ -2246,6 +2396,7 @@ int acme_req_account(struct task *task, struct acme_ctx
> *ctx, int newaccount, ch
> out:
> free_trash_chunk(req_in);
> free_trash_chunk(req_out);
> + free_trash_chunk(eab_req_out);
>
> return ret;
> }
Split this part.
> diff --git a/src/jws.c b/src/jws.c
> index e4ea30de6..f8fb4738f 100644
> --- a/src/jws.c
> +++ b/src/jws.c
> @@ -219,6 +219,7 @@ size_t EVP_PKEY_to_pub_jwk(EVP_PKEY *pkey, char *dst,
> size_t dsize)
> /*
> * Generate the JWS payload and converts it to base64url.
> * Use either <kid> or <jwk>, but won't use both
> + * <nonce> is optional.
> *
> * Return the size of the data or 0
> */
> @@ -226,13 +227,14 @@ size_t EVP_PKEY_to_pub_jwk(EVP_PKEY *pkey, char *dst,
> size_t dsize)
> size_t jws_b64_protected(enum jwt_alg alg, char *kid, char *jwk, char
> *nonce, char *url,
> char *dst, size_t dsize)
> {
> - char *acc;
> - char *acctype;
> int ret = 0;
> struct buffer *json = NULL;
> const char *algstr;
>
> switch (alg) {
> + case JWS_ALG_HS256: algstr = "HS256"; break;
> + case JWS_ALG_HS384: algstr = "HS384"; break;
> + case JWS_ALG_HS512: algstr = "HS512"; break;
> case JWS_ALG_RS256: algstr = "RS256"; break;
> case JWS_ALG_RS384: algstr = "RS384"; break;
> case JWS_ALG_RS512: algstr = "RS512"; break;
> @@ -246,24 +248,16 @@ size_t jws_b64_protected(enum jwt_alg alg, char *kid,
> char *jwk, char *nonce, ch
> if ((json = alloc_trash_chunk()) == NULL)
> goto out;
>
> - /* kid or jwk ? */
> - acc = kid ? kid : jwk;
> - acctype = kid ? "kid" : "jwk";
> -
> - ret = snprintf(json->area, json->size, "{\n"
> - " \"alg\": \"%s\",\n"
> - " \"%s\": %s%s%s,\n"
> - " \"nonce\": \"%s\",\n"
> - " \"url\": \"%s\"\n"
> - "}\n",
> - algstr, acctype, kid ? "\"" : "", acc, kid ? "\"" : "",
> nonce, url);
> - if (ret >= json->size) {
> - ret = 0;
> - goto out;
> - }
> -
> -
> - json->data = ret;
> + chunk_appendf(json, "{");
> + if (kid)
> + chunk_appendf(json, "\"kid\": \"%s\",", kid);
> + else
> + chunk_appendf(json, "\"jwk\": %s,", jwk);
> + if (nonce)
> + chunk_appendf(json, "\"nonce\": \"%s\",", nonce);
> + chunk_appendf(json, "\"alg\": \"%s\",", algstr);
> + chunk_appendf(json, "\"url\": \"%s\"", url);
> + chunk_appendf(json, "}");
>
> ret = a2base64url(json->area, json->data, dst, dsize);
> out:
> @@ -458,6 +452,52 @@ size_t jws_b64_signature(EVP_PKEY *pkey, enum jwt_alg
> alg, char *b64protected, c
> return 0;
> }
>
> +
> +/*
> + * Generate a JWS HMAC signature using the base64url protected buffer and
> the base64url payload buffer
> + *
> + * Return the size of the data or 0
> + */
> +size_t jws_b64_hmac_signature(char *key, size_t key_len, enum jwt_alg alg,
> char *b64protected, char *b64payload, char *dst, size_t dsize)
> +{
> + const EVP_MD *evp_alg = NULL;
> + int ret = 0;
> + unsigned char mac[EVP_MAX_MD_SIZE] = {};
> + unsigned int mac_len = 0;
> + struct buffer *sig_data = NULL;
> +
> + if ((sig_data = alloc_trash_chunk()) == NULL)
> + goto out;
> +
> + switch (alg) {
> + case JWS_ALG_HS256: evp_alg = EVP_sha256(); break;
> + case JWS_ALG_HS384: evp_alg = EVP_sha384(); break;
> + case JWS_ALG_HS512: evp_alg = EVP_sha512(); break;
> + default:
> + goto out;
> + }
> +
> + if (!chunk_memcat(sig_data, b64protected, strlen(b64protected)) ||
> + !chunk_memcat(sig_data, ".", 1) ||
> + !chunk_memcat(sig_data, b64payload, strlen(b64payload)))
> + goto out;
> +
> + if (HMAC(evp_alg, key, (int)key_len,
> + (unsigned char*)sig_data->area, sig_data->data,
> + mac, &mac_len) == NULL)
> + goto out;
> +
> + ret = a2base64url((const char *)mac, mac_len, dst, dsize);
> +
> +out:
> + free_trash_chunk(sig_data);
> +
> + if (ret > 0)
> + return ret;
> + return 0;
> +}
> +
> +
> /*
> * Fill a <dst> buffer of <dsize> size with a jwk thumbprint from a pkey
> *
> --
> 2.53.0
>
>
--
William Lallemand