From 1ab0f94943e02c49d88a358351914f9032a5bca3 Mon Sep 17 00:00:00 2001 From: Michael Forney Date: Fri, 23 Apr 2021 23:14:16 -0700 Subject: [PATCH] acme-client: Port to BearSSL --- usr.sbin/acme-client/acctproc.c | 298 +++++++++------------------ usr.sbin/acme-client/certproc.c | 5 - usr.sbin/acme-client/key.c | 329 ++++++++++++++++++++++++------ usr.sbin/acme-client/key.h | 22 +- usr.sbin/acme-client/keyproc.c | 198 ++++++------------ usr.sbin/acme-client/revokeproc.c | 237 ++++++++++----------- 6 files changed, 564 insertions(+), 525 deletions(-) diff --git a/usr.sbin/acme-client/acctproc.c b/usr.sbin/acme-client/acctproc.c index 9e97a8bb760..8d66dac49d9 100644 --- a/usr.sbin/acme-client/acctproc.c +++ b/usr.sbin/acme-client/acctproc.c @@ -19,73 +19,29 @@ #include #include -#include #include #include #include #include -#include -#include -#include -#include -#include +#include #include "extern.h" #include "key.h" -/* - * Converts a BIGNUM to the form used in JWK. - * This is essentially a base64-encoded big-endian binary string - * representation of the number. - */ -static char * -bn2string(const BIGNUM *bn) -{ - int len; - unsigned char *buf; - char *bbuf; - - /* Extract big-endian representation of BIGNUM. */ - - len = BN_num_bytes(bn); - if ((buf = malloc(len)) == NULL) { - warn("malloc"); - return NULL; - } else if (len != BN_bn2bin(bn, buf)) { - warnx("BN_bn2bin"); - free(buf); - return NULL; - } - - /* Convert to base64url. */ - - if ((bbuf = base64buf_url(buf, len)) == NULL) { - warnx("base64buf_url"); - free(buf); - return NULL; - } - - free(buf); - return bbuf; -} - /* * Extract the relevant RSA components from the key and create the JSON * thumbprint from them. */ static char * -op_thumb_rsa(EVP_PKEY *pkey) +op_thumb_rsa(struct key *key) { char *exp = NULL, *mod = NULL, *json = NULL; - RSA *r; - - if ((r = EVP_PKEY_get0_RSA(pkey)) == NULL) - warnx("EVP_PKEY_get0_RSA"); - else if ((mod = bn2string(RSA_get0_n(r))) == NULL) - warnx("bn2string"); - else if ((exp = bn2string(RSA_get0_e(r))) == NULL) - warnx("bn2string"); + + if ((mod = base64buf_url(key->rsa.pk.n, key->rsa.pk.nlen)) == NULL) + warnx("base64buf_url"); + else if ((exp = base64buf_url(key->rsa.pk.e, key->rsa.pk.elen)) == NULL) + warnx("base64buf_url"); else if ((json = json_fmt_thumb_rsa(exp, mod)) == NULL) warnx("json_fmt_thumb_rsa"); @@ -99,31 +55,23 @@ op_thumb_rsa(EVP_PKEY *pkey) * thumbprint from them. */ static char * -op_thumb_ec(EVP_PKEY *pkey) +op_thumb_ec(struct key *key) { - BIGNUM *X = NULL, *Y = NULL; - EC_KEY *ec = NULL; + size_t len; char *x = NULL, *y = NULL; char *json = NULL; - if ((ec = EVP_PKEY_get0_EC_KEY(pkey)) == NULL) - warnx("EVP_PKEY_get0_EC_KEY"); - else if ((X = BN_new()) == NULL) - warnx("BN_new"); - else if ((Y = BN_new()) == NULL) - warnx("BN_new"); - else if (!EC_POINT_get_affine_coordinates(EC_KEY_get0_group(ec), - EC_KEY_get0_public_key(ec), X, Y, NULL)) - warnx("EC_POINT_get_affine_coordinates"); - else if ((x = bn2string(X)) == NULL) - warnx("bn2string"); - else if ((y = bn2string(Y)) == NULL) - warnx("bn2string"); + /* Points are stored in uncompressed format. */ + len = key->ec.pk.qlen / 2; + if (key->ec.pk.qlen % 2 != 1 || key->ec.pk.q[0] != 0x04) + warnx("invalid EC public key"); + else if ((x = base64buf_url(key->ec.pk.q + 1, len)) == NULL) + warnx("base64buf_url"); + else if ((y = base64buf_url(key->ec.pk.q + 1 + len, len)) == NULL) + warnx("base64buf_url"); else if ((json = json_fmt_thumb_ec(x, y)) == NULL) warnx("json_fmt_thumb_ec"); - BN_free(X); - BN_free(Y); free(x); free(y); return json; @@ -133,26 +81,26 @@ op_thumb_ec(EVP_PKEY *pkey) * The thumbprint operation is used for the challenge sequence. */ static int -op_thumbprint(int fd, EVP_PKEY *pkey) +op_thumbprint(int fd, struct key *pkey) { - char *thumb = NULL, *dig64 = NULL; - unsigned char dig[EVP_MAX_MD_SIZE]; - unsigned int digsz; - int rc = 0; + char *thumb = NULL, *dig64 = NULL; + br_sha256_context ctx; + unsigned char dig[br_sha256_SIZE]; + int rc = 0; /* Construct the thumbprint input itself. */ - switch (EVP_PKEY_base_id(pkey)) { - case EVP_PKEY_RSA: + switch (pkey->type) { + case BR_KEYTYPE_RSA: if ((thumb = op_thumb_rsa(pkey)) != NULL) break; goto out; - case EVP_PKEY_EC: + case BR_KEYTYPE_EC: if ((thumb = op_thumb_ec(pkey)) != NULL) break; goto out; default: - warnx("EVP_PKEY_base_id: unknown key type"); + warnx("unknown key type"); goto out; } @@ -163,12 +111,10 @@ op_thumbprint(int fd, EVP_PKEY *pkey) * it up in the read loop). */ - if (!EVP_Digest(thumb, strlen(thumb), dig, &digsz, EVP_sha256(), - NULL)) { - warnx("EVP_Digest"); - goto out; - } - if ((dig64 = base64buf_url(dig, digsz)) == NULL) { + br_sha256_init(&ctx); + br_sha256_update(&ctx, thumb, strlen(thumb)); + br_sha256_out(&ctx, dig); + if ((dig64 = base64buf_url(dig, sizeof(dig))) == NULL) { warnx("base64buf_url"); goto out; } @@ -183,11 +129,10 @@ out: } static int -op_sign_rsa(char **prot, EVP_PKEY *pkey, const char *nonce, const char *url) +op_sign_rsa(char **prot, struct key *key, const char *nonce, const char *url) { char *exp = NULL, *mod = NULL; int rc = 0; - RSA *r; *prot = NULL; @@ -196,12 +141,10 @@ op_sign_rsa(char **prot, EVP_PKEY *pkey, const char *nonce, const char *url) * Finally, format the header combined with the nonce. */ - if ((r = EVP_PKEY_get0_RSA(pkey)) == NULL) - warnx("EVP_PKEY_get0_RSA"); - else if ((mod = bn2string(RSA_get0_n(r))) == NULL) - warnx("bn2string"); - else if ((exp = bn2string(RSA_get0_e(r))) == NULL) - warnx("bn2string"); + if ((mod = base64buf_url(key->rsa.pk.n, key->rsa.pk.nlen)) == NULL) + warnx("base64buf_url"); + else if ((exp = base64buf_url(key->rsa.pk.e, key->rsa.pk.elen)) == NULL) + warnx("base64buf_url"); else if ((*prot = json_fmt_protected_rsa(exp, mod, nonce, url)) == NULL) warnx("json_fmt_protected_rsa"); else @@ -213,35 +156,27 @@ op_sign_rsa(char **prot, EVP_PKEY *pkey, const char *nonce, const char *url) } static int -op_sign_ec(char **prot, EVP_PKEY *pkey, const char *nonce, const char *url) +op_sign_ec(char **prot, struct key *key, const char *nonce, const char *url) { - BIGNUM *X = NULL, *Y = NULL; - EC_KEY *ec = NULL; + size_t len; char *x = NULL, *y = NULL; int rc = 0; *prot = NULL; - if ((ec = EVP_PKEY_get0_EC_KEY(pkey)) == NULL) - warnx("EVP_PKEY_get0_EC_KEY"); - else if ((X = BN_new()) == NULL) - warnx("BN_new"); - else if ((Y = BN_new()) == NULL) - warnx("BN_new"); - else if (!EC_POINT_get_affine_coordinates(EC_KEY_get0_group(ec), - EC_KEY_get0_public_key(ec), X, Y, NULL)) - warnx("EC_POINT_get_affine_coordinates"); - else if ((x = bn2string(X)) == NULL) - warnx("bn2string"); - else if ((y = bn2string(Y)) == NULL) - warnx("bn2string"); + /* Points are stored in uncompressed format. */ + len = key->ec.pk.qlen / 2; + if (key->ec.pk.qlen % 2 != 1 || key->ec.pk.q[0] != 0x04) + warnx("invalid EC public key"); + else if ((x = base64buf_url(key->ec.pk.q + 1, len)) == NULL) + warnx("base64buf_url"); + else if ((y = base64buf_url(key->ec.pk.q + 1 + len, len)) == NULL) + warnx("base64buf_url"); else if ((*prot = json_fmt_protected_ec(x, y, nonce, url)) == NULL) warnx("json_fmt_protected_ec"); else rc = 1; - BN_free(X); - BN_free(Y); free(x); free(y); return rc; @@ -252,20 +187,18 @@ op_sign_ec(char **prot, EVP_PKEY *pkey, const char *nonce, const char *url) * This requires the sender ("fd") to provide the payload and a nonce. */ static int -op_sign(int fd, EVP_PKEY *pkey, enum acctop op) +op_sign(int fd, struct key *key, enum acctop op) { - EVP_MD_CTX *ctx = NULL; - const EVP_MD *evp_md = NULL; - ECDSA_SIG *ec_sig = NULL; - const BIGNUM *ec_sig_r = NULL, *ec_sig_s = NULL; - int bn_len, sign_len, rc = 0; + br_hash_compat_context ctx; + int sign_len, rc = 0; + unsigned int digsz, sigsz; char *nonce = NULL, *pay = NULL, *pay64 = NULL; char *prot = NULL, *prot64 = NULL; - char *sign = NULL, *dig64 = NULL, *fin = NULL; + char *sign = NULL, *sig64 = NULL, *fin = NULL; char *url = NULL, *kid = NULL, *alg = NULL; - const unsigned char *digp; - unsigned char *dig = NULL, *buf = NULL; - size_t digsz; + unsigned char dig[64]; + unsigned char *sig = NULL; + const unsigned char *oid = NULL; /* Read our payload and nonce from the requestor. */ @@ -282,19 +215,22 @@ op_sign(int fd, EVP_PKEY *pkey, enum acctop op) /* Base64-encode the payload. */ - if ((pay64 = base64buf_url((unsigned char *)pay, strlen(pay))) == NULL) { + if ((pay64 = base64buf_url(pay, strlen(pay))) == NULL) { warnx("base64buf_url"); goto out; } - switch (EVP_PKEY_base_id(pkey)) { - case EVP_PKEY_RSA: + switch (key->type) { + case BR_KEYTYPE_RSA: alg = "RS256"; - evp_md = EVP_sha256(); + ctx.vtable = &br_sha256_vtable; + oid = BR_HASH_OID_SHA256; + sigsz = (key->rsa.sk.n_bitlen + 7) / 8; break; - case EVP_PKEY_EC: + case BR_KEYTYPE_EC: alg = "ES384"; - evp_md = EVP_sha384(); + ctx.vtable = &br_sha384_vtable; + sigsz = 96; break; default: warnx("unknown account key type"); @@ -308,17 +244,17 @@ op_sign(int fd, EVP_PKEY *pkey, enum acctop op) goto out; } } else { - switch (EVP_PKEY_base_id(pkey)) { - case EVP_PKEY_RSA: - if (!op_sign_rsa(&prot, pkey, nonce, url)) + switch (key->type) { + case BR_KEYTYPE_RSA: + if (!op_sign_rsa(&prot, key, nonce, url)) goto out; break; - case EVP_PKEY_EC: - if (!op_sign_ec(&prot, pkey, nonce, url)) + case BR_KEYTYPE_EC: + if (!op_sign_ec(&prot, key, nonce, url)) goto out; break; default: - warnx("EVP_PKEY_base_id"); + warnx("unknown key type"); goto out; } } @@ -341,76 +277,34 @@ op_sign(int fd, EVP_PKEY *pkey, enum acctop op) /* Sign the message. */ - if ((ctx = EVP_MD_CTX_new()) == NULL) { - warnx("EVP_MD_CTX_new"); - goto out; - } - if (!EVP_DigestSignInit(ctx, NULL, evp_md, NULL, pkey)) { - warnx("EVP_DigestSignInit"); - goto out; - } - if (!EVP_DigestSign(ctx, NULL, &digsz, sign, sign_len)) { - warnx("EVP_DigestSign"); - goto out; - } - if ((dig = malloc(digsz)) == NULL) { + ctx.vtable->init(&ctx.vtable); + ctx.vtable->update(&ctx.vtable, sign, sign_len); + ctx.vtable->out(&ctx.vtable, dig); + digsz = ctx.vtable->desc >> BR_HASHDESC_OUT_OFF & BR_HASHDESC_OUT_MASK; + + if ((sig = malloc(sigsz)) == NULL) { warn("malloc"); goto out; } - if (!EVP_DigestSign(ctx, dig, &digsz, sign, sign_len)) { - warnx("EVP_DigestSign"); - goto out; - } - switch (EVP_PKEY_base_id(pkey)) { - case EVP_PKEY_RSA: - if ((dig64 = base64buf_url(dig, digsz)) == NULL) { - warnx("base64buf_url"); + switch (key->type) { + case BR_KEYTYPE_RSA: + if (!br_rsa_pkcs1_sign_get_default()(oid, dig, digsz, + &key->rsa.sk, sig)) { + warnx("br_rsa_pkcs1_sign"); goto out; } break; - case EVP_PKEY_EC: - if (digsz > LONG_MAX) { - warnx("EC signature too long"); - goto out; - } - - digp = dig; - if ((ec_sig = d2i_ECDSA_SIG(NULL, &digp, digsz)) == NULL) { - warnx("d2i_ECDSA_SIG"); + case BR_KEYTYPE_EC: + sigsz = br_ecdsa_sign_raw_get_default()(br_ec_get_default(), + ctx.vtable, dig, &key->ec.sk, sig); + if (sigsz == 0 || sigsz % 2 != 0) { + warnx("br_ecdsa_sign_raw"); goto out; } - - if ((ec_sig_r = ECDSA_SIG_get0_r(ec_sig)) == NULL || - (ec_sig_s = ECDSA_SIG_get0_s(ec_sig)) == NULL) { - warnx("ECDSA_SIG_get0"); - goto out; - } - - if ((bn_len = (EVP_PKEY_bits(pkey) + 7) / 8) <= 0) { - warnx("EVP_PKEY_bits"); - goto out; - } - - if ((buf = calloc(2, bn_len)) == NULL) { - warnx("calloc"); - goto out; - } - - if (BN_bn2binpad(ec_sig_r, buf, bn_len) != bn_len || - BN_bn2binpad(ec_sig_s, buf + bn_len, bn_len) != bn_len) { - warnx("BN_bn2binpad"); - goto out; - } - - if ((dig64 = base64buf_url(buf, 2 * bn_len)) == NULL) { - warnx("base64buf_url"); - goto out; - } - break; default: - warnx("EVP_PKEY_base_id"); + warnx("unknown key type"); goto out; } @@ -420,7 +314,11 @@ op_sign(int fd, EVP_PKEY *pkey, enum acctop op) * when we next enter the read loop). */ - if ((fin = json_fmt_signed(prot64, pay64, dig64)) == NULL) { + if ((sig64 = base64buf_url(sig, sigsz)) == NULL) { + warnx("base64buf_url"); + goto out; + } + if ((fin = json_fmt_signed(prot64, pay64, sig64)) == NULL) { warnx("json_fmt_signed"); goto out; } else if (writestr(fd, COMM_REQ, fin) < 0) @@ -428,8 +326,6 @@ op_sign(int fd, EVP_PKEY *pkey, enum acctop op) rc = 1; out: - ECDSA_SIG_free(ec_sig); - EVP_MD_CTX_free(ctx); free(pay); free(sign); free(pay64); @@ -438,10 +334,9 @@ out: free(kid); free(prot); free(prot64); - free(dig); - free(dig64); + free(sig); + free(sig64); free(fin); - free(buf); return rc; } @@ -449,7 +344,7 @@ int acctproc(int netsock, const char *acctkey, enum keytype keytype) { FILE *f = NULL; - EVP_PKEY *pkey = NULL; + struct key *pkey = NULL; long lval; enum acctop op; int rc = 0, cc, newacct = 0; @@ -475,8 +370,6 @@ acctproc(int netsock, const char *acctkey, enum keytype keytype) /* File-system, user, and sandbox jailing. */ - ERR_load_crypto_strings(); - if (pledge("stdio", NULL) == -1) { warn("pledge"); goto out; @@ -554,8 +447,7 @@ out: close(netsock); if (f != NULL) fclose(f); - EVP_PKEY_free(pkey); - ERR_print_errors_fp(stderr); - ERR_free_strings(); + if (pkey != NULL) + freezero(pkey, sizeof(*pkey) + pkey->datasz); return rc; } diff --git a/usr.sbin/acme-client/certproc.c b/usr.sbin/acme-client/certproc.c index f443d573675..85c3897a4b8 100644 --- a/usr.sbin/acme-client/certproc.c +++ b/usr.sbin/acme-client/certproc.c @@ -21,11 +21,6 @@ #include #include -#include -#include -#include -#include - #include "extern.h" #define BEGIN_MARKER "-----BEGIN CERTIFICATE-----" diff --git a/usr.sbin/acme-client/key.c b/usr.sbin/acme-client/key.c index 9ece3059d4e..9599a7fdbd5 100644 --- a/usr.sbin/acme-client/key.c +++ b/usr.sbin/acme-client/key.c @@ -17,14 +17,11 @@ */ #include +#include #include #include -#include -#include -#include -#include -#include +#include #include "key.h" @@ -33,102 +30,320 @@ */ #define KBITS 4096 +static void +prng_init(const br_prng_class **ctx, const void *params, const void *seed, size_t len) +{ +} + +static void +prng_generate(const br_prng_class **ctx, void *out, size_t len) +{ + arc4random_buf(out, len); +} + +static void +prng_update(const br_prng_class **ctx, const void *seed, size_t len) +{ +} + +static const br_prng_class prng_class = { + 0, prng_init, prng_generate, prng_update +}, *prng = &prng_class; + /* * Create an RSA key with the default KBITS number of bits. */ -EVP_PKEY * +struct key * rsa_key_create(FILE *f, const char *fname) { - EVP_PKEY_CTX *ctx = NULL; - EVP_PKEY *pkey = NULL; + struct key *key = NULL; + size_t slen, plen; + unsigned char *sbuf, *pbuf; + unsigned char d[KBITS / 8]; + unsigned char *der = NULL, *pem = NULL; + size_t derlen, pemlen; - if ((ctx = EVP_PKEY_CTX_new_id(EVP_PKEY_RSA, NULL)) == NULL) { - warnx("EVP_PKEY_CTX_new_id"); - goto err; - } - if (EVP_PKEY_keygen_init(ctx) <= 0) { - warnx("EVP_PKEY_keygen_init"); + slen = BR_RSA_KBUF_PRIV_SIZE(KBITS); + plen = BR_RSA_KBUF_PUB_SIZE(KBITS); + if ((key = malloc(sizeof(*key) + slen + plen)) == NULL) { + warnx("malloc"); goto err; } - if (EVP_PKEY_CTX_set_rsa_keygen_bits(ctx, KBITS) <= 0) { - warnx("EVP_PKEY_set_rsa_keygen_bits"); + key->type = BR_KEYTYPE_RSA; + key->datasz = slen + plen; + sbuf = key->data; + pbuf = key->data + slen; + if (!br_rsa_keygen_get_default()(&prng, &key->rsa.sk, sbuf, + &key->rsa.pk, pbuf, KBITS, 0x10001)) { + warnx("br_rsa_keygen"); goto err; } - if (EVP_PKEY_keygen(ctx, &pkey) <= 0) { - warnx("EVP_PKEY_keygen"); + + /* Compute the private exponent. */ + + if (!br_rsa_compute_privexp_get_default()(d, &key->rsa.sk, 0x10001)) { + warnx("br_rsa_compute_modulus"); goto err; } - /* Serialise the key to the disc. */ + /* Serialise the key to the disk. */ - if (!PEM_write_PrivateKey(f, pkey, NULL, NULL, 0, NULL, NULL)) { - warnx("%s: PEM_write_PrivateKey", fname); + derlen = br_encode_rsa_raw_der(NULL, &key->rsa.sk, &key->rsa.pk, + d, sizeof(d)); + if ((der = malloc(derlen)) == NULL) { + warn("malloc"); + goto err; + } + br_encode_rsa_raw_der(der, &key->rsa.sk, &key->rsa.pk, d, sizeof(d)); + pemlen = br_pem_encode(NULL, der, derlen, BR_ENCODE_PEM_RSA_RAW, 0); + if ((pem = malloc(pemlen + 1)) == NULL) { + warn("malloc"); + goto err; + } + br_pem_encode(pem, der, derlen, BR_ENCODE_PEM_RSA_RAW, 0); + if (fwrite(pem, 1, pemlen, f) != pemlen) { + warn("write private key"); goto err; } - EVP_PKEY_CTX_free(ctx); - return pkey; + free(der); + free(pem); + return key; err: - EVP_PKEY_free(pkey); - EVP_PKEY_CTX_free(ctx); + free(der); + free(pem); + free(key); return NULL; } -EVP_PKEY * +struct key * ec_key_create(FILE *f, const char *fname) { - EVP_PKEY_CTX *ctx = NULL; - EVP_PKEY *pkey = NULL; + struct key *key = NULL; + const br_ec_impl *ec; + size_t slen, plen; + unsigned char *sbuf, *pbuf; + unsigned char *der = NULL, *pem = NULL; + size_t derlen, pemlen; - if ((ctx = EVP_PKEY_CTX_new_id(EVP_PKEY_EC, NULL)) == NULL) { - warnx("EVP_PKEY_CTX_new_id"); - goto err; - } - if (EVP_PKEY_keygen_init(ctx) <= 0) { - warnx("EVP_PKEY_keygen_init"); + slen = BR_EC_KBUF_PRIV_MAX_SIZE; + plen = BR_EC_KBUF_PUB_MAX_SIZE; + if ((key = malloc(sizeof(*key) + slen + plen)) == NULL) { + warn("malloc"); goto err; } - if (EVP_PKEY_CTX_set_ec_paramgen_curve_nid(ctx, NID_secp384r1) <= 0) { - warnx("EVP_PKEY_CTX_set_ec_paramgen_curve_nid"); + key->type = BR_KEYTYPE_EC; + key->datasz = slen + plen; + sbuf = key->data; + pbuf = key->data + slen; + + ec = br_ec_get_default(); + if (br_ec_keygen(&prng, ec, &key->ec.sk, sbuf, BR_EC_secp384r1) == 0) { + warnx("br_ec_keygen"); goto err; } - if (EVP_PKEY_keygen(ctx, &pkey) <= 0) { - warnx("EVP_PKEY_keygen"); + if (br_ec_compute_pub(ec, &key->ec.pk, pbuf, &key->ec.sk) == 0) { + warnx("br_ec_compute_pub"); goto err; } - /* Serialise the key to the disc. */ + /* Serialise the key to the disk in EC format */ - if (!PEM_write_PrivateKey(f, pkey, NULL, NULL, 0, NULL, NULL)) { - warnx("%s: PEM_write_PrivateKey", fname); + if ((derlen = br_encode_ec_raw_der(NULL, &key->ec.sk, + &key->ec.pk)) == 0) { + warnx("br_encode_ec_raw_der"); + goto err; + } + if ((der = malloc(derlen)) == NULL) { + warn("malloc"); + goto err; + } + br_encode_ec_raw_der(der, &key->ec.sk, &key->ec.pk); + pemlen = br_pem_encode(NULL, der, derlen, BR_ENCODE_PEM_EC_RAW, 0); + if ((pem = malloc(pemlen + 1)) == NULL) { + warn("malloc"); + goto err; + } + br_pem_encode(pem, der, derlen, BR_ENCODE_PEM_EC_RAW, 0); + if (fwrite(pem, 1, pemlen, f) != pemlen) { + warn("write private key"); goto err; } - EVP_PKEY_CTX_free(ctx); - return pkey; + free(der); + free(pem); + return key; err: - EVP_PKEY_free(pkey); - EVP_PKEY_CTX_free(ctx); + free(der); + free(pem); + free(key); return NULL; } -EVP_PKEY * +static void +append_skey(void *ctx, const void *src, size_t len) +{ + br_skey_decoder_push(ctx, src, len); +} + +struct key * key_load(FILE *f, const char *fname) { - EVP_PKEY *pkey; + struct key *key = NULL; + size_t datasz, len = 0, n; + int type = 0, err; + unsigned char buf[8192], *pos; + br_pem_decoder_context pemctx; + br_skey_decoder_context keyctx; + br_rsa_compute_modulus compute_modulus; + br_rsa_compute_pubexp compute_pubexp; + const br_ec_impl *ecimpl; + const br_rsa_private_key *rsa; + const br_ec_private_key *ec; + const char *name = NULL; + uint32_t pubexp; - pkey = PEM_read_PrivateKey(f, NULL, NULL, NULL); - if (pkey == NULL) { - warnx("%s: PEM_read_PrivateKey", fname); - return NULL; + br_pem_decoder_init(&pemctx); + br_skey_decoder_init(&keyctx); + while (type == 0) { + if (len == 0) { + if (feof(f)) { + warnx("%s: missing private key", fname); + break; + } + len = fread(buf, 1, sizeof(buf), f); + if (ferror(f)) { + warn("%s: read", fname); + goto err; + } + pos = buf; + } + n = br_pem_decoder_push(&pemctx, pos, len); + pos += n; + len -= n; + switch (br_pem_decoder_event(&pemctx)) { + case BR_PEM_BEGIN_OBJ: + name = br_pem_decoder_name(&pemctx); + if (strcmp(name, BR_ENCODE_PEM_PKCS8) != 0 && + strcmp(name, BR_ENCODE_PEM_RSA_RAW) != 0 && + strcmp(name, BR_ENCODE_PEM_EC_RAW) != 0) { + name = NULL; + break; + } + br_pem_decoder_setdest(&pemctx, append_skey, &keyctx); + break; + case BR_PEM_END_OBJ: + if (name == NULL) + break; + if ((err = br_skey_decoder_last_error(&keyctx)) != 0) { + warnx("%s: br_skey_decoder: %d", fname, err); + goto err; + } + type = br_skey_decoder_key_type(&keyctx); + break; + case 0: + break; + default: + warnx("%s: PEM decoding failed", fname); + goto err; + } } - if (EVP_PKEY_base_id(pkey) == EVP_PKEY_RSA || - EVP_PKEY_base_id(pkey) == EVP_PKEY_EC) - return pkey; - warnx("%s: unsupported key type", fname); - EVP_PKEY_free(pkey); - return NULL; + switch (type) { + case BR_KEYTYPE_RSA: + rsa = br_skey_decoder_get_rsa(&keyctx); + compute_modulus = br_rsa_compute_modulus_get_default(); + compute_pubexp = br_rsa_compute_pubexp_get_default(); + + /* Compute public modulus size. This will fail if + * p or q is not 3 mod 4. */ + if ((datasz = compute_modulus(NULL, rsa)) == 0) { + warnx("%s: br_rsa_compute_modulus", fname); + goto err; + } + datasz += 4 + rsa->plen + rsa->qlen + rsa->dplen + rsa->dqlen + + rsa->iqlen; + + if ((key = malloc(sizeof(*key) + datasz)) == NULL) { + warn("malloc"); + goto err; + } + key->type = BR_KEYTYPE_RSA; + key->datasz = datasz; + + if ((pubexp = compute_pubexp(rsa)) == 0) { + warnx("%s: br_rsa_compute_pubexp", fname); + goto err; + } + + /* Copy private key. */ + key->rsa.sk.n_bitlen = rsa->n_bitlen; + key->rsa.sk.p = key->data; + key->rsa.sk.plen = rsa->plen; + key->rsa.sk.q = key->rsa.sk.p + rsa->plen; + key->rsa.sk.qlen = rsa->qlen; + key->rsa.sk.dp = key->rsa.sk.q + rsa->qlen; + key->rsa.sk.dplen = rsa->dplen; + key->rsa.sk.dq = key->rsa.sk.dp + rsa->dplen; + key->rsa.sk.dqlen = rsa->dqlen; + key->rsa.sk.iq = key->rsa.sk.dq + rsa->dqlen; + key->rsa.sk.iqlen = rsa->iqlen; + memcpy(key->rsa.sk.p, rsa->p, rsa->plen); + memcpy(key->rsa.sk.q, rsa->q, rsa->qlen); + memcpy(key->rsa.sk.dp, rsa->dp, rsa->dplen); + memcpy(key->rsa.sk.dq, rsa->dq, rsa->dqlen); + memcpy(key->rsa.sk.iq, rsa->iq, rsa->iqlen); + + /* Compute public modulus and encode public exponent. */ + key->rsa.pk.n = key->rsa.sk.iq + rsa->iqlen; + key->rsa.pk.nlen = compute_modulus(key->rsa.pk.n, rsa); + key->rsa.pk.elen = 4; + key->rsa.pk.e = key->rsa.pk.n + key->rsa.pk.nlen; + key->rsa.pk.e[0] = pubexp >> 24; + key->rsa.pk.e[1] = pubexp >> 16; + key->rsa.pk.e[2] = pubexp >> 8; + key->rsa.pk.e[3] = pubexp; + + /* Trim leading zeros. */ + while (key->rsa.pk.elen > 0 && key->rsa.pk.e[0] == 0) { + --key->rsa.pk.elen; + ++key->rsa.pk.e; + } + goto out; + case BR_KEYTYPE_EC: + ec = br_skey_decoder_get_ec(&keyctx); + ecimpl = br_ec_get_default(); + if ((datasz = br_ec_compute_pub(ecimpl, NULL, NULL, ec)) == 0) { + warnx("%s: br_ec_compute_pub", fname); + goto err; + } + datasz += ec->xlen; + + if ((key = malloc(sizeof(*key) + datasz)) == NULL) { + warn("malloc"); + goto err; + } + key->type = BR_KEYTYPE_EC; + key->datasz = datasz; + + key->ec.sk.curve = ec->curve; + key->ec.sk.x = key->data; + key->ec.sk.xlen = ec->xlen; + memcpy(key->ec.sk.x, ec->x, ec->xlen); + br_ec_compute_pub(ecimpl, &key->ec.pk, + key->ec.sk.x + key->ec.sk.xlen, &key->ec.sk); + goto out; + } + + warnx("%s: missing private key", fname); + +err: + free(key); + key = NULL; +out: + explicit_bzero(&pemctx, sizeof(pemctx)); + explicit_bzero(&keyctx, sizeof(keyctx)); + return key; } diff --git a/usr.sbin/acme-client/key.h b/usr.sbin/acme-client/key.h index 272d36eb09a..12abdec813c 100644 --- a/usr.sbin/acme-client/key.h +++ b/usr.sbin/acme-client/key.h @@ -18,8 +18,24 @@ #ifndef KEY_H #define KEY_H -EVP_PKEY *rsa_key_create(FILE *, const char *); -EVP_PKEY *ec_key_create(FILE *, const char *); -EVP_PKEY *key_load(FILE *, const char *); +struct key { + int type; + union { + struct { + br_rsa_public_key pk; + br_rsa_private_key sk; + } rsa; + struct { + br_ec_public_key pk; + br_ec_private_key sk; + } ec; + }; + size_t datasz; + unsigned char data[]; +}; + +struct key *rsa_key_create(FILE *, const char *); +struct key *ec_key_create(FILE *, const char *); +struct key *key_load(FILE *, const char *); #endif /* ! KEY_H */ diff --git a/usr.sbin/acme-client/keyproc.c b/usr.sbin/acme-client/keyproc.c index f0df9f292d4..fc7de74b616 100644 --- a/usr.sbin/acme-client/keyproc.c +++ b/usr.sbin/acme-client/keyproc.c @@ -18,55 +18,18 @@ #include #include +#include #include #include #include #include -#include -#include -#include -#include -#include +#include +#include #include "extern.h" #include "key.h" -/* - * This was lifted more or less directly from demos/x509/mkreq.c of the - * OpenSSL source code. - */ -static int -add_ext(STACK_OF(X509_EXTENSION) *sk, int nid, const char *value) -{ - X509_EXTENSION *ex; - char *cp; - - /* - * XXX: I don't like this at all. - * There's no documentation for X509V3_EXT_conf_nid, so I'm not - * sure if the "value" parameter is ever written to, touched, - * etc. - * The 'official' examples suggest not (they use a string - * literal as the input), but to be safe, I'm doing an - * allocation here and just letting it go. - * This leaks memory, but bounded to the number of SANs. - */ - - if ((cp = strdup(value)) == NULL) { - warn("strdup"); - return (0); - } - ex = X509V3_EXT_conf_nid(NULL, NULL, nid, cp); - if (ex == NULL) { - warnx("X509V3_EXT_conf_nid"); - free(cp); - return (0); - } - sk_X509_EXTENSION_push(sk, ex); - return (1); -} - /* * Create an X509 certificate from the private key we have on file. * To do this, we first open the key file, then jail ourselves. @@ -77,18 +40,20 @@ int keyproc(int netsock, const char *keyfile, const char **alts, size_t altsz, enum keytype keytype) { - char *der64 = NULL; - unsigned char *der = NULL, *dercp; - char *sans = NULL, *san = NULL; - FILE *f; - size_t i, sansz; - void *pp; - EVP_PKEY *pkey = NULL; - X509_REQ *x = NULL; - X509_NAME *name = NULL; - int len, rc = 0, cc, nid, newkey = 0; - mode_t prev; - STACK_OF(X509_EXTENSION) *exts = NULL; + char *der64 = NULL; + unsigned char *der = NULL; + FILE *f; + size_t i; + struct key *pkey = NULL; + struct x509cert_req req; + struct x509cert_skey skey; + struct x509cert_dn dn; + struct x509cert_rdn rdn; + struct x509cert_item item; + int len, rc = 0, newkey = 0; + mode_t prev; + + req.alts = NULL; /* * First, open our private key file read-only or write-only if @@ -110,8 +75,6 @@ keyproc(int netsock, const char *keyfile, const char **alts, size_t altsz, /* File-system, user, and sandbox jail. */ - ERR_load_crypto_strings(); - if (pledge("stdio", NULL) == -1) { warn("pledge"); goto out; @@ -145,102 +108,61 @@ keyproc(int netsock, const char *keyfile, const char **alts, size_t altsz, * Then set it as the X509 requester's key. */ - if ((x = X509_REQ_new()) == NULL) { - warnx("X509_REQ_new"); - goto out; - } else if (!X509_REQ_set_version(x, 0)) { - warnx("X509_REQ_set_version"); - goto out; - } else if (!X509_REQ_set_pubkey(x, pkey)) { - warnx("X509_REQ_set_pubkey"); - goto out; + req.pkey.key_type = pkey->type; + skey.type = pkey->type; + switch (pkey->type) { + case BR_KEYTYPE_RSA: + req.pkey.key.rsa = pkey->rsa.pk; + skey.u.rsa = &pkey->rsa.sk; + break; + case BR_KEYTYPE_EC: + req.pkey.key.ec = pkey->ec.pk; + skey.u.ec = &pkey->ec.sk; + break; } /* Now specify the common name that we'll request. */ - if ((name = X509_NAME_new()) == NULL) { - warnx("X509_NAME_new"); - goto out; - } else if (!X509_NAME_add_entry_by_txt(name, "CN", - MBSTRING_ASC, (u_char *)alts[0], -1, -1, 0)) { - warnx("X509_NAME_add_entry_by_txt: CN=%s", alts[0]); - goto out; - } else if (!X509_REQ_set_subject_name(x, name)) { - warnx("X509_req_set_issuer_name"); - goto out; - } + rdn.oid = x509cert_oid_CN; + rdn.val.tag = X509CERT_ASN1_UTF8STRING; + rdn.val.val = alts[0]; + rdn.val.len = strlen(alts[0]); + rdn.val.enc = NULL; + dn.rdn = &rdn; + dn.rdn_len = 1; + req.subject.enc = x509cert_dn_encoder; + req.subject.val = &dn; - /* - * Now add the SAN extensions. - * This was lifted more or less directly from demos/x509/mkreq.c - * of the OpenSSL source code. - * (The zeroth altname is the domain name.) - * TODO: is this the best way of doing this? - */ + /* Now add the SAN extension. */ - nid = NID_subject_alt_name; - if ((exts = sk_X509_EXTENSION_new_null()) == NULL) { - warnx("sk_X509_EXTENSION_new_null"); + req.alts_len = altsz; + req.alts = calloc(altsz, sizeof(req.alts[0])); + if (req.alts == NULL) { + warn("calloc"); goto out; } - /* Initialise to empty string. */ - if ((sans = strdup("")) == NULL) { - warn("strdup"); - goto out; - } - sansz = strlen(sans) + 1; - /* - * For each SAN entry, append it to the string. - * We need a single SAN entry for all of the SAN - * domains: NOT an entry per domain! - */ + /* Add a dNSName SAN entry for each alternate name. */ for (i = 0; i < altsz; i++) { - cc = asprintf(&san, "%sDNS:%s", - i ? "," : "", alts[i]); - if (cc == -1) { - warn("asprintf"); - goto out; - } - pp = recallocarray(sans, sansz, sansz + strlen(san), 1); - if (pp == NULL) { - warn("recallocarray"); - goto out; - } - sans = pp; - sansz += strlen(san); - strlcat(sans, san, sansz); - free(san); - san = NULL; - } - - if (!add_ext(exts, nid, sans)) { - warnx("add_ext"); - goto out; - } else if (!X509_REQ_add_extensions(x, exts)) { - warnx("X509_REQ_add_extensions"); - goto out; - } - sk_X509_EXTENSION_pop_free(exts, X509_EXTENSION_free); - - /* Sign the X509 request using SHA256. */ - - if (!X509_REQ_sign(x, pkey, EVP_sha256())) { - warnx("X509_sign"); - goto out; + req.alts[i].tag = X509CERT_SAN_DNSNAME; + req.alts[i].val = alts[i]; + req.alts[i].len = strlen(alts[i]); } - /* Now, serialise to DER, then base64. */ + /* Sign the X.509 request using SHA256, and serialise to + * DER then base64. */ - if ((len = i2d_X509_REQ(x, NULL)) < 0) { - warnx("i2d_X509_REQ"); + item.enc = x509cert_req_encoder; + item.val = &req; + if ((len = x509cert_sign(&item, &skey, &br_sha256_vtable, NULL)) == 0) { + warnx("x509cert_sign"); goto out; - } else if ((der = dercp = malloc(len)) == NULL) { + } else if ((der = malloc(len)) == NULL) { warn("malloc"); goto out; - } else if (len != i2d_X509_REQ(x, &dercp)) { - warnx("i2d_X509_REQ"); + } else if ((len = x509cert_sign(&item, &skey, &br_sha256_vtable, der)) == 0) { + warnx("x509cert_sign"); goto out; } else if ((der64 = base64buf_url(der, len)) == NULL) { warnx("base64buf_url"); @@ -265,12 +187,8 @@ out: fclose(f); free(der); free(der64); - free(sans); - free(san); - X509_REQ_free(x); - X509_NAME_free(name); - EVP_PKEY_free(pkey); - ERR_print_errors_fp(stderr); - ERR_free_strings(); + free(req.alts); + if (pkey != NULL) + freezero(pkey, pkey->datasz); return rc; } diff --git a/usr.sbin/acme-client/revokeproc.c b/usr.sbin/acme-client/revokeproc.c index 58e81233f1a..378de35f662 100644 --- a/usr.sbin/acme-client/revokeproc.c +++ b/usr.sbin/acme-client/revokeproc.c @@ -22,58 +22,54 @@ #include #include #include +#include #include #include -#include -#include -#include -#include +#include #include "extern.h" #define RENEW_ALLOW (30 * 24 * 60 * 60) -/* - * Convert the X509's expiration time into a time_t value. - */ -static time_t -X509expires(X509 *x) +static void +append_cert(void *ctx, const void *buf, size_t len) { - ASN1_TIME *atim; - struct tm t; - - if ((atim = X509_getm_notAfter(x)) == NULL) { - warnx("missing notAfter"); - return -1; - } - - memset(&t, 0, sizeof(t)); - - if (!ASN1_TIME_to_tm(atim, &t)) { - warnx("invalid ASN1_TIME"); - return -1; + br_x509_certificate *cert = ctx; + size_t newlen; + unsigned char *newdata; + + if (cert->data_len == -1) + return; + newlen = cert->data_len + len; + if ((newdata = realloc(cert->data, newlen)) != NULL) { + memcpy(newdata + cert->data_len, buf, len); + cert->data = newdata; + cert->data_len = newlen; + } else { + warn("realloc"); + cert->data_len = -1; } - - return timegm(&t); } int revokeproc(int fd, const char *certfile, int force, int revocate, const char *const *alts, size_t altsz) { - GENERAL_NAMES *sans = NULL; - unsigned char *der = NULL, *dercp; - char *der64 = NULL; - char *san = NULL, *str, *tok; - int rc = 0, cc, i, len; - size_t *found = NULL; + static const unsigned char dnsname[] = {0, 2}; + char buf[8192], *pos, *sans = NULL, *der64 = NULL; + int rc = 0, cc, state, err; + size_t i, j, n, len = 0, altlen, altmax, eltsz; FILE *f = NULL; - X509 *x = NULL; + br_pem_decoder_context pc; + br_x509_decoder_context xd; + br_x509_minimal_context xc; + br_x509_certificate cert = {0}; + br_name_element *elts = NULL; + uint32_t days, secs; long lval; enum revokeop op, rop; time_t t; - size_t j; /* * First try to open the certificate before we drop privileges @@ -88,8 +84,6 @@ revokeproc(int fd, const char *certfile, int force, /* File-system and sandbox jailing. */ - ERR_load_crypto_strings(); - if (pledge("stdio", NULL) == -1) { warn("pledge"); goto out; @@ -113,39 +107,86 @@ revokeproc(int fd, const char *certfile, int force, goto out; } - if ((x = PEM_read_X509(f, NULL, NULL, NULL)) == NULL) { - warnx("PEM_read_X509"); - goto out; + br_pem_decoder_init(&pc); + for (state = 0; state != 2;) { + if (len == 0) { + if (feof(f)) { + warnx("%s: truncated certificate", certfile); + goto out; + } + len = fread(buf, 1, sizeof(buf), f); + if (ferror(f)) { + warn("fread"); + goto out; + } + pos = buf; + } + n = br_pem_decoder_push(&pc, pos, len); + pos += n; + len -= n; + switch (br_pem_decoder_event(&pc)) { + case BR_PEM_BEGIN_OBJ: + if (strcmp(br_pem_decoder_name(&pc), "CERTIFICATE") == 0) { + br_pem_decoder_setdest(&pc, append_cert, &cert); + state = 1; + } + break; + case BR_PEM_END_OBJ: + if (state == 1) + state = 2; + break; + case 0: + break; + default: + warnx("%s: PEM decoding error", certfile); + goto out; + } } - - /* Cache and sanity check X509v3 extensions. */ - - if (X509_check_purpose(x, -1, -1) <= 0) { - warnx("%s: invalid X509v3 extensions", certfile); + if (cert.data_len == -1) goto out; - } /* Read out the expiration date. */ - if ((t = X509expires(x)) == -1) { - warnx("X509expires"); + br_x509_decoder_init(&xd, NULL, NULL); + br_x509_decoder_push(&xd, cert.data, cert.data_len); + if ((err = br_x509_decoder_last_error(&xd)) != 0) { + warnx("%s: X.509 decoding error %d", certfile, err); goto out; } + br_x509_decoder_get_notafter(&xd, &days, &secs); + t = 86400ll * (days - 719528) + 86400; - /* Extract list of SAN entries from the certificate. */ - - sans = X509_get_ext_d2i(x, NID_subject_alt_name, NULL, NULL); - if (sans == NULL) { - warnx("%s: does not have a SAN entry", certfile); - if (revocate) - goto out; - force = 2; + for (i = 0, altmax = 0; i < altsz; ++i) { + altlen = strlen(alts[i]) + 1; + if (altlen > altmax) + altmax = altlen; + } + eltsz = altsz + 1; + if ((elts = calloc(eltsz, sizeof(elts[0]))) == NULL || + (sans = calloc(eltsz, altmax)) == NULL) { + warn("calloc"); + goto out; + } + for (i = 0; i < eltsz; ++i) { + elts[i].oid = dnsname; + elts[i].buf = sans + i * altmax; + elts[i].len = altmax; } - /* An array of buckets: the number of entries found. */ + /* Extract list of SAN entries from the certificate. */ - if ((found = calloc(altsz, sizeof(size_t))) == NULL) { - warn("calloc"); + br_x509_minimal_init(&xc, &br_sha256_vtable, NULL, 0); + br_x509_minimal_set_hash(&xc, br_sha256_ID, &br_sha256_vtable); + br_x509_minimal_set_hash(&xc, br_sha384_ID, &br_sha384_vtable); + br_x509_minimal_set_hash(&xc, br_sha512_ID, &br_sha512_vtable); + br_x509_minimal_set_name_elements(&xc, elts, eltsz); + xc.vtable->start_chain(&xc.vtable, NULL); + xc.vtable->start_cert(&xc.vtable, cert.data_len); + xc.vtable->append(&xc.vtable, cert.data, cert.data_len); + xc.vtable->end_cert(&xc.vtable); + err = xc.vtable->end_chain(&xc.vtable); + if (err != BR_ERR_X509_NOT_TRUSTED && err != BR_ERR_X509_EXPIRED) { + warnx("%s: X.509 engine error %d", certfile, err); goto out; } @@ -154,63 +195,37 @@ revokeproc(int fd, const char *certfile, int force, * configuration file and that all domains are represented only once. */ - for (i = 0; i < sk_GENERAL_NAME_num(sans); i++) { - GENERAL_NAME *gen_name; - const ASN1_IA5STRING *name; - const unsigned char *name_buf; - int name_len; - int name_type; - - gen_name = sk_GENERAL_NAME_value(sans, i); - assert(gen_name != NULL); - - name = GENERAL_NAME_get0_value(gen_name, &name_type); - if (name_type != GEN_DNS) - continue; - - /* name_buf isn't a C string and could contain embedded NULs. */ - name_buf = ASN1_STRING_get0_data(name); - name_len = ASN1_STRING_length(name); - - for (j = 0; j < altsz; j++) { - if ((size_t)name_len != strlen(alts[j])) - continue; - if (memcmp(name_buf, alts[j], name_len) == 0) + for (i = 0; i < altsz; i++) { + for (j = 0; j < eltsz; j++) { + if (elts[j].status == 1 && + strcmp(alts[i], elts[j].buf) == 0) { + elts[j].status = 0; break; - } - if (j == altsz) { - if (revocate) { - char *visbuf; - - visbuf = calloc(4, name_len + 1); - if (visbuf == NULL) { - warn("%s: unexpected SAN", certfile); - goto out; - } - strvisx(visbuf, name_buf, name_len, VIS_SAFE); - warnx("%s: unexpected SAN entry: %s", - certfile, visbuf); - free(visbuf); - goto out; } - force = 2; - continue; } - if (found[j]++) { + if (j == eltsz) { if (revocate) { - warnx("%s: duplicate SAN entry: %.*s", - certfile, name_len, name_buf); + warnx("%s: domain not listed: %s", certfile, alts[i]); goto out; } force = 2; } } - for (j = 0; j < altsz; j++) { - if (found[j]) + for (i = 0; i < eltsz; i++) { + if (elts[i].status == 0) continue; if (revocate) { - warnx("%s: domain not listed: %s", certfile, alts[j]); + char *visbuf; + + if (elts[i].status != 1 || + stravis(&visbuf, elts[i].buf, VIS_SAFE) < 0) { + warnx("%s: unexpected SAN", certfile); + goto out; + } + warnx("%s: unexpected SAN entry: %s", + certfile, visbuf); + free(visbuf); goto out; } force = 2; @@ -236,16 +251,7 @@ revokeproc(int fd, const char *certfile, int force, if (cc <= 0) goto out; - if ((len = i2d_X509(x, NULL)) < 0) { - warnx("i2d_X509"); - goto out; - } else if ((der = dercp = malloc(len)) == NULL) { - warn("malloc"); - goto out; - } else if (len != i2d_X509(x, &dercp)) { - warnx("i2d_X509"); - goto out; - } else if ((der64 = base64buf_url(der, len)) == NULL) { + if ((der64 = base64buf_url(cert.data, cert.data_len)) == NULL) { warnx("base64buf_url"); goto out; } else if (writestr(fd, COMM_CSR, der64) >= 0) @@ -298,12 +304,9 @@ out: close(fd); if (f != NULL) fclose(f); - X509_free(x); - GENERAL_NAMES_free(sans); - free(der); - free(found); + free(cert.data); + free(sans); + free(elts); free(der64); - ERR_print_errors_fp(stderr); - ERR_free_strings(); return rc; } -- 2.49.0