From 8b2c490811639f7c2fd0514e31ad6e4df2373a49 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Sur=C3=BD?= Date: Tue, 19 May 2026 17:52:22 +0200 Subject: [PATCH] Enforce strict RSA DNSKEY shape during DNSSEC validation A resolver that validated DNSSEC accepted RSA DNSKEYs of any modulus size up to OpenSSL's compile-time ceiling, and accepted any public exponent the wire format could carry. RSA verification cost grows sharply with the modulus length, so an authoritative server could publish an oversized DNSKEY to make each signature check on the resolver many times more expensive than for a normally sized key. The intended verify-time cap had no effect because the helper it called returned the public-exponent bit length rather than the modulus bit length, so the test was always satisfied. Replace it with an honest modulus-range check and a stricter exponent check that accepts only odd exponents in the closed range [3, 2^32 + 1] (covering every Fermat prime up to F5 and the odd intermediate values seen in deployed keys), reject anything outside those bounds at every RSA key load path so an invalid key never reaches the verifier, and keep the same checks at the verifier as a backstop against future load paths. --- lib/dns/dst_api.c | 2 + lib/dns/dst_internal.h | 2 + lib/dns/openssl_shim.h | 7 --- lib/dns/opensslrsa_link.c | 72 +++++++++++++++++++++++--- lib/isc/include/isc/ossl_wrap.h | 17 ++++++- lib/isc/ossl_wrap/ossl1_1.c | 44 ++++++++++++---- lib/isc/ossl_wrap/ossl3.c | 37 +++++++++++--- tests/dns/rsa_test.c | 90 ++++++++++++++++++++++++++++++++- 8 files changed, 239 insertions(+), 32 deletions(-) diff --git a/lib/dns/dst_api.c b/lib/dns/dst_api.c index 561e12a1a4..6b57beb525 100644 --- a/lib/dns/dst_api.c +++ b/lib/dns/dst_api.c @@ -237,6 +237,8 @@ dst__lib_initialize(void) { void dst__lib_shutdown(void) { + dst__opensslrsa_shutdown(); + isc_mem_detach(&dst__mctx); } diff --git a/lib/dns/dst_internal.h b/lib/dns/dst_internal.h index bc48c9fec4..550951e984 100644 --- a/lib/dns/dst_internal.h +++ b/lib/dns/dst_internal.h @@ -192,6 +192,8 @@ dst__hmacsha512_init(struct dst_func **funcp); void dst__opensslrsa_init(struct dst_func **funcp, unsigned short algorithm); void +dst__opensslrsa_shutdown(void); +void dst__opensslecdsa_init(struct dst_func **funcp); void dst__openssleddsa_init(struct dst_func **funcp, unsigned char algorithm); diff --git a/lib/dns/openssl_shim.h b/lib/dns/openssl_shim.h index 215cd363a8..3a50685f8f 100644 --- a/lib/dns/openssl_shim.h +++ b/lib/dns/openssl_shim.h @@ -21,13 +21,6 @@ #include #include -/* - * Limit the size of public exponents. - */ -#ifndef RSA_MAX_PUBEXP_BITS -#define RSA_MAX_PUBEXP_BITS 35 -#endif /* ifndef RSA_MAX_PUBEXP_BITS */ - #if !HAVE_EVP_PKEY_EQ #define EVP_PKEY_eq EVP_PKEY_cmp #endif diff --git a/lib/dns/opensslrsa_link.c b/lib/dns/opensslrsa_link.c index b0d0d950ed..1b6dcb4034 100644 --- a/lib/dns/opensslrsa_link.c +++ b/lib/dns/opensslrsa_link.c @@ -36,6 +36,30 @@ #define OPENSSLRSA_MAX_MODULUS_BITS 4096 #define OPENSSLRSA_MIN_MODULUS_BITS 512 +static BIGNUM *rsa_exponent_min = NULL; +static BIGNUM *rsa_exponent_max = NULL; + +/* + * Accept odd public exponents in [3, 2^32 + 1]. That covers every Fermat + * prime up to F5 and the odd intermediate values seen on the wire. + */ +static bool +rsa_exponent_in_range(const BIGNUM *e) { + if (!BN_is_odd(e)) { + return false; + } + + if (BN_cmp(e, rsa_exponent_min) < 0) { + return false; + } + + if (BN_cmp(e, rsa_exponent_max) > 0) { + return false; + } + + return true; +} + /* length byte + 1.2.840.113549.1.1.11 BER encoded RFC 4055 */ static unsigned char oid_rsasha256[] = { 0x0b, 0x06, 0x09, 0x2a, 0x86, 0x48, 0x86, 0xf7, 0x0d, 0x01, 0x01, 0x0b }; @@ -233,7 +257,10 @@ opensslrsa_verify(dst_context_t *dctx, const isc_region_t *sig) { evp_md_ctx = dctx->ctxdata.evp_md_ctx; pkey = key->keydata.pkeypair.pub; - if (!isc_ossl_wrap_rsa_key_bits_leq(pkey, OPENSSLRSA_MAX_MODULUS_BITS)) + if (!isc_ossl_wrap_rsa_modulus_bits_in_range( + pkey, OPENSSLRSA_MIN_MODULUS_BITS, + OPENSSLRSA_MAX_MODULUS_BITS) || + !isc_ossl_wrap_rsa_exponent_is_allowed(pkey)) { return DST_R_VERIFYFAILURE; } @@ -463,10 +490,12 @@ opensslrsa_fromdns(dst_key_t *key, isc_buffer_t *data) { if (c.e == NULL || c.n == NULL) { CLEANUP(ISC_R_NOMEMORY); } - if (BN_num_bits(c.e) > RSA_MAX_PUBEXP_BITS) { + if (BN_num_bits(c.n) < OPENSSLRSA_MIN_MODULUS_BITS || + BN_num_bits(c.n) > OPENSSLRSA_MAX_MODULUS_BITS) + { CLEANUP(ISC_R_RANGE); } - if (BN_num_bits(c.n) < OPENSSLRSA_MIN_MODULUS_BITS) { + if (!rsa_exponent_in_range(c.e)) { CLEANUP(ISC_R_RANGE); } isc_buffer_forward(data, length); @@ -703,10 +732,12 @@ opensslrsa_parse(dst_key_t *key, isc_lex_t *lexer, dst_key_t *pub) { if (c.n == NULL || c.e == NULL) { CLEANUP(DST_R_INVALIDPRIVATEKEY); } - if (BN_num_bits(c.n) < OPENSSLRSA_MIN_MODULUS_BITS) { + if (BN_num_bits(c.n) < OPENSSLRSA_MIN_MODULUS_BITS || + BN_num_bits(c.n) > OPENSSLRSA_MAX_MODULUS_BITS) + { CLEANUP(ISC_R_RANGE); } - if (BN_num_bits(c.e) > RSA_MAX_PUBEXP_BITS) { + if (!rsa_exponent_in_range(c.e)) { CLEANUP(ISC_R_RANGE); } @@ -744,10 +775,13 @@ opensslrsa_fromlabel(dst_key_t *key, const char *label, const char *pin) { CHECK(dst__openssl_fromlabel(EVP_PKEY_RSA, label, pin, &pubpkey, &privpkey)); - if (!isc_ossl_wrap_rsa_key_bits_leq(pubpkey, RSA_MAX_PUBEXP_BITS)) { + if (!isc_ossl_wrap_rsa_exponent_is_allowed(pubpkey)) { CLEANUP(ISC_R_RANGE); } - if (EVP_PKEY_bits(pubpkey) < OPENSSLRSA_MIN_MODULUS_BITS) { + if (!isc_ossl_wrap_rsa_modulus_bits_in_range( + pubpkey, OPENSSLRSA_MIN_MODULUS_BITS, + OPENSSLRSA_MAX_MODULUS_BITS)) + { CLEANUP(ISC_R_RANGE); } @@ -926,4 +960,28 @@ dst__opensslrsa_init(dst_func_t **funcp, unsigned short algorithm) { *funcp = &opensslrsa_functions; } } + + if (rsa_exponent_min == NULL) { + rsa_exponent_min = BN_new(); + INSIST(rsa_exponent_min != NULL); + + RUNTIME_CHECK(BN_set_word(rsa_exponent_min, 3) == 1); + } + + if (rsa_exponent_max == NULL) { + rsa_exponent_max = BN_new(); + INSIST(rsa_exponent_max != NULL); + + RUNTIME_CHECK(BN_set_bit(rsa_exponent_max, 0) == 1); + RUNTIME_CHECK(BN_set_bit(rsa_exponent_max, 32) == 1); + } +} + +void +dst__opensslrsa_shutdown(void) { + REQUIRE(rsa_exponent_min != NULL); + REQUIRE(rsa_exponent_max != NULL); + + BN_free(rsa_exponent_min); + BN_free(rsa_exponent_max); } diff --git a/lib/isc/include/isc/ossl_wrap.h b/lib/isc/include/isc/ossl_wrap.h index cfb1cf2faf..fe5efe5ac1 100644 --- a/lib/isc/include/isc/ossl_wrap.h +++ b/lib/isc/include/isc/ossl_wrap.h @@ -251,7 +251,22 @@ isc_ossl_wrap_generate_pkcs11_rsa_key(char *uri, size_t bit_size, */ bool -isc_ossl_wrap_rsa_key_bits_leq(EVP_PKEY *pkey, size_t limit); +isc_ossl_wrap_rsa_exponent_is_allowed(EVP_PKEY *pkey); +/*% + * Returns true if the RSA public exponent of `pkey` is odd and lies + * within the closed range [3, 2^32 + 1]. This covers every Fermat + * prime up to F5 plus all odd intermediate values seen in deployed + * DNSSEC keys. Returns false if the exponent cannot be retrieved or + * falls outside that range. + */ + +bool +isc_ossl_wrap_rsa_modulus_bits_in_range(EVP_PKEY *pkey, size_t min, size_t max); +/*% + * Returns true if the RSA modulus bit length of `pkey` is between `min` + * and `max` inclusive. Returns false if the modulus bit length cannot + * be determined. + */ isc_result_t isc_ossl_wrap_rsa_public_components(EVP_PKEY *pkey, diff --git a/lib/isc/ossl_wrap/ossl1_1.c b/lib/isc/ossl_wrap/ossl1_1.c index 6a5740a3b8..03ff2f6599 100644 --- a/lib/isc/ossl_wrap/ossl1_1.c +++ b/lib/isc/ossl_wrap/ossl1_1.c @@ -440,24 +440,48 @@ isc_ossl_wrap_generate_pkcs11_ed448_key(char *uri, EVP_PKEY **pkeyp) { } bool -isc_ossl_wrap_rsa_key_bits_leq(EVP_PKEY *pkey, size_t limit) { +isc_ossl_wrap_rsa_exponent_is_allowed(EVP_PKEY *pkey) { const RSA *rsa; const BIGNUM *ce; + BIGNUM *emin = NULL; + BIGNUM *emax = NULL; + bool ok = false; REQUIRE(pkey != NULL); rsa = EVP_PKEY_get0_RSA(pkey); - if (rsa != NULL) { - ce = NULL; - RSA_get0_key(rsa, NULL, &ce, NULL); - if (ce != NULL) { - int bits = BN_num_bits(ce); - - return bits > 0 && (size_t)bits <= limit; - } + if (rsa == NULL) { + return false; + } + ce = NULL; + RSA_get0_key(rsa, NULL, &ce, NULL); + if (ce == NULL) { + return false; } - return false; + emin = BN_new(); + if (emin == NULL || !BN_set_word(emin, 3)) { + goto cleanup; + } + if (BN_hex2bn(&emax, "100000001") == 0) { + goto cleanup; + } + + ok = BN_is_odd(ce) && BN_cmp(ce, emin) >= 0 && BN_cmp(ce, emax) <= 0; + +cleanup: + BN_free(emin); + BN_free(emax); + return ok; +} + +bool +isc_ossl_wrap_rsa_modulus_bits_in_range(EVP_PKEY *pkey, size_t min, + size_t max) { + REQUIRE(pkey != NULL); + + int bits = EVP_PKEY_bits(pkey); + return bits > 0 && (size_t)bits >= min && (size_t)bits <= max; } isc_result_t diff --git a/lib/isc/ossl_wrap/ossl3.c b/lib/isc/ossl_wrap/ossl3.c index 7021ff5f5d..f486b5c70d 100644 --- a/lib/isc/ossl_wrap/ossl3.c +++ b/lib/isc/ossl_wrap/ossl3.c @@ -628,15 +628,40 @@ cleanup: } bool -isc_ossl_wrap_rsa_key_bits_leq(EVP_PKEY *pkey, size_t limit) { +isc_ossl_wrap_rsa_exponent_is_allowed(EVP_PKEY *pkey) { BIGNUM *e = NULL; - if (EVP_PKEY_get_bn_param(pkey, OSSL_PKEY_PARAM_RSA_E, &e) == 1) { - int bits = BN_num_bits(e); - BN_free(e); + BIGNUM *emin = NULL; + BIGNUM *emax = NULL; + bool ok = false; - return bits > 0 && (size_t)bits <= limit; + if (EVP_PKEY_get_bn_param(pkey, OSSL_PKEY_PARAM_RSA_E, &e) != 1) { + goto cleanup; } - return false; + + emin = BN_new(); + if (emin == NULL || !BN_set_word(emin, 3)) { + goto cleanup; + } + if (BN_hex2bn(&emax, "100000001") == 0) { + goto cleanup; + } + + ok = BN_is_odd(e) && BN_cmp(e, emin) >= 0 && BN_cmp(e, emax) <= 0; + +cleanup: + BN_free(e); + BN_free(emin); + BN_free(emax); + return ok; +} + +bool +isc_ossl_wrap_rsa_modulus_bits_in_range(EVP_PKEY *pkey, size_t min, + size_t max) { + REQUIRE(pkey != NULL); + + int bits = EVP_PKEY_bits(pkey); + return bits > 0 && (size_t)bits >= min && (size_t)bits <= max; } isc_result_t diff --git a/tests/dns/rsa_test.c b/tests/dns/rsa_test.c index c00bda6c9f..954c10576b 100644 --- a/tests/dns/rsa_test.c +++ b/tests/dns/rsa_test.c @@ -228,7 +228,7 @@ ISC_RUN_TEST_IMPL(isc_rsa_fromdns_oversized_exponent) { rdata[i++] = 0x03; /* protocol */ rdata[i++] = DST_ALG_RSASHA256; /* RSA wire key: e_bytes + e + n. Use a 6-byte (48-bit) e - * with a non-zero leading byte so it exceeds the 35-bit cap. */ + * with a non-zero leading byte; outside the allowlist. */ rdata[i++] = 6; rdata[i++] = 0x01; rdata[i++] = 0x02; @@ -250,6 +250,92 @@ ISC_RUN_TEST_IMPL(isc_rsa_fromdns_oversized_exponent) { assert_null(key); } +/* + * dst_key_fromdns rejects RSA DNSKEYs whose public exponent is in the + * accepted numeric range but even (and therefore not a valid RSA exponent). + */ +ISC_RUN_TEST_IMPL(isc_rsa_fromdns_even_exponent) { + isc_result_t result; + dns_fixedname_t fname; + dns_name_t *name; + dst_key_t *key = NULL; + isc_buffer_t buf; + unsigned char rdata[300] = { 0 }; + size_t i = 0; + + UNUSED(state); + + name = dns_fixedname_initname(&fname); + isc_buffer_constinit(&buf, "rsa.", 4); + isc_buffer_add(&buf, 4); + result = dns_name_fromtext(name, &buf, NULL, 0); + assert_int_equal(result, ISC_R_SUCCESS); + + /* DNSKEY rdata: flags(2) + proto(1) + alg(1) + RSA wire pubkey. */ + rdata[i++] = 0x01; /* flags hi (KSK) */ + rdata[i++] = 0x00; /* flags lo */ + rdata[i++] = 0x03; /* protocol */ + rdata[i++] = DST_ALG_RSASHA256; + /* e_bytes=1, exponent=0x04 (even, mathematically invalid). */ + rdata[i++] = 1; + rdata[i++] = 0x04; + /* 256 bytes of arbitrary modulus (2048-bit). */ + for (size_t j = 0; j < 256; j++) { + rdata[i++] = 0xAB; + } + + isc_buffer_init(&buf, rdata, i); + isc_buffer_add(&buf, i); + + result = dst_key_fromdns(name, dns_rdataclass_in, &buf, isc_g_mctx, + &key); + assert_int_equal(result, ISC_R_RANGE); + assert_null(key); +} + +/* dst_key_fromdns rejects RSA DNSKEYs whose modulus exceeds the cap */ +ISC_RUN_TEST_IMPL(isc_rsa_fromdns_oversized_modulus) { + isc_result_t result; + dns_fixedname_t fname; + dns_name_t *name; + dst_key_t *key = NULL; + isc_buffer_t buf; + unsigned char rdata[1100] = { 0 }; + size_t i = 0; + + UNUSED(state); + + name = dns_fixedname_initname(&fname); + isc_buffer_constinit(&buf, "rsa.", 4); + isc_buffer_add(&buf, 4); + result = dns_name_fromtext(name, &buf, NULL, 0); + assert_int_equal(result, ISC_R_SUCCESS); + + /* DNSKEY rdata: flags(2) + proto(1) + alg(1) + RSA wire pubkey. */ + rdata[i++] = 0x01; /* flags hi (KSK) */ + rdata[i++] = 0x00; /* flags lo */ + rdata[i++] = 0x03; /* protocol */ + rdata[i++] = DST_ALG_RSASHA256; + /* + * RSA wire key: e_bytes + e + n. 1-byte exponent (0x03) and a + * 1024-byte modulus (8192 bits, leading byte 0xAB so the high bit + * is set) — twice the 4096-bit maximum. + */ + rdata[i++] = 1; + rdata[i++] = 0x03; + for (size_t j = 0; j < 1024; j++) { + rdata[i++] = 0xAB; + } + + isc_buffer_init(&buf, rdata, i); + isc_buffer_add(&buf, i); + + result = dst_key_fromdns(name, dns_rdataclass_in, &buf, isc_g_mctx, + &key); + assert_int_equal(result, ISC_R_RANGE); + assert_null(key); +} + /* dst_key_fromdns rejects RSA DNSKEYs with a degenerate modulus */ ISC_RUN_TEST_IMPL(isc_rsa_fromdns_short_modulus) { isc_result_t result; @@ -291,6 +377,8 @@ ISC_RUN_TEST_IMPL(isc_rsa_fromdns_short_modulus) { ISC_TEST_LIST_START ISC_TEST_ENTRY(isc_rsa_verify) ISC_TEST_ENTRY(isc_rsa_fromdns_oversized_exponent) +ISC_TEST_ENTRY(isc_rsa_fromdns_even_exponent) +ISC_TEST_ENTRY(isc_rsa_fromdns_oversized_modulus) ISC_TEST_ENTRY(isc_rsa_fromdns_short_modulus) ISC_TEST_LIST_END