]> git.ipfire.org Git - thirdparty/bind9.git/commitdiff
Enforce strict RSA DNSKEY shape during DNSSEC validation
authorOndřej Surý <ondrej@isc.org>
Tue, 19 May 2026 15:52:22 +0000 (17:52 +0200)
committerOndřej Surý <ondrej@isc.org>
Mon, 8 Jun 2026 16:00:27 +0000 (18:00 +0200)
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
lib/dns/dst_internal.h
lib/dns/openssl_shim.h
lib/dns/opensslrsa_link.c
lib/isc/include/isc/ossl_wrap.h
lib/isc/ossl_wrap/ossl1_1.c
lib/isc/ossl_wrap/ossl3.c
tests/dns/rsa_test.c

index 561e12a1a4dc6b1637cbf0bcfe82389e1b307560..6b57beb525bb17ee4823460a5b6e5df6b9642d6a 100644 (file)
@@ -237,6 +237,8 @@ dst__lib_initialize(void) {
 
 void
 dst__lib_shutdown(void) {
+       dst__opensslrsa_shutdown();
+
        isc_mem_detach(&dst__mctx);
 }
 
index bc48c9fec41ddec051fcbe937e522446a4c7970f..550951e984d0ae230592de0e1fe974196bddfaff 100644 (file)
@@ -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);
index 215cd363a8a8972bfe5438ef718f14b7674dfeec..3a50685f8fc1cc96184402777578ad5cc176a95b 100644 (file)
 #include <openssl/opensslv.h>
 #include <openssl/rsa.h>
 
-/*
- * 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
index b0d0d950ed69684ca257815564e701b6de3f8cf5..1b6dcb40343591355c6c73671b0d8d0f13db4c3e 100644 (file)
 #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);
 }
index cfb1cf2faf4129a261a32add6cfbb18f1c954821..fe5efe5ac1b10191105d41b9db4d562f9c12d5c9 100644 (file)
@@ -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,
index 6a5740a3b8e9961db04758eb90f287c2ee4d9472..03ff2f6599b7d4bd8eaf81d976f6e45b8f82b134 100644 (file)
@@ -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);
+       if (rsa == NULL) {
+               return false;
+       }
+       ce = NULL;
+       RSA_get0_key(rsa, NULL, &ce, NULL);
+       if (ce == NULL) {
+               return false;
+       }
 
-                       return bits > 0 && (size_t)bits <= limit;
-               }
+       emin = BN_new();
+       if (emin == NULL || !BN_set_word(emin, 3)) {
+               goto cleanup;
+       }
+       if (BN_hex2bn(&emax, "100000001") == 0) {
+               goto cleanup;
        }
 
-       return false;
+       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
index 7021ff5f5d9a878cd00f68e25b6fe162efb73eb0..f486b5c70d095e5654fca88fe0e16a0fd50c3dcf 100644 (file)
@@ -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
index c00bda6c9f1fbd8a91bd7ed8a273bd1cdef7c0fe..954c10576b025eea9a7b6cf462bffb8c906f8945 100644 (file)
@@ -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