diff --git a/src/internal.c b/src/internal.c index ba1ff99467f..864911b2684 100644 --- a/src/internal.c +++ b/src/internal.c @@ -29406,6 +29406,93 @@ int GetCipherSuiteFromName(const char* name, byte* cipherSuite0, return ret; } +#if defined(OPENSSL_EXTRA) || defined(OPENSSL_ALL) +/* Classify a cipher suite for OpenSSL-style "!aNULL"/"!eNULL" exclusion. + * anon != 0: match suites that perform NO peer authentication. + * enull != 0: match suites that perform NO encryption. + * Classification is by suite name: the RFC/IANA name when compiled in + * (anonymous suites contain "_anon_", named NULL-cipher suites contain "_NULL" + * - e.g. TLS_DH_anon_*, TLS_ECDH_anon_*, TLS_*_WITH_NULL_*) and the OpenSSL + * short name (ADH/AECDH/...NULL...), which is the only name available under + * NO_ERROR_STRINGS. The TLS 1.3 integrity-only suites (RFC 9150) are + * NULL-encryption too but carry no "NULL" token in their names, so they are + * matched by suite id below. */ +static int CipherSuiteExcluded(byte first, byte second, int anon, int enull) +{ + int i; + const int sz = GetCipherNamesSize(); + +#ifdef HAVE_NULL_CIPHER + /* TLS 1.3 integrity-only suites perform no encryption; wolfSSL keys them + * off haveNull in InitSuites(), so honor "!eNULL" for them here too. */ + if (enull && (first == ECC_BYTE) && + ((second == TLS_SHA256_SHA256) || (second == TLS_SHA384_SHA384))) { + return 1; + } +#endif + + for (i = 0; i < sz; i++) { + if ((cipher_names[i].cipherSuite0 != first) || + (cipher_names[i].cipherSuite != second)) { + continue; + } + #ifndef NO_CIPHER_SUITE_ALIASES + if (cipher_names[i].flags & WOLFSSL_CIPHER_SUITE_FLAG_NAMEALIAS) { + continue; + } + #endif + #ifndef NO_ERROR_STRINGS + if (cipher_names[i].name_iana != NULL) { + if (anon && + (XSTRSTR(cipher_names[i].name_iana, "_anon_") != NULL)) { + return 1; + } + if (enull && + (XSTRSTR(cipher_names[i].name_iana, "_NULL") != NULL)) { + return 1; + } + } + #endif + if (cipher_names[i].name != NULL) { + if (anon && ((XSTRSTR(cipher_names[i].name, "ADH") != NULL) || + (XSTRSTR(cipher_names[i].name, "AECDH") != NULL))) { + return 1; + } + if (enull && (XSTRSTR(cipher_names[i].name, "NULL") != NULL)) { + return 1; + } + } + break; /* found the suite; not excluded */ + } + return 0; +} + +/* Drop suites matching an exclusion from the explicit user-suite array + * suites[0..*idx). Used to honor an OpenSSL-style exclusion ("!aNULL", + * "!eNULL"/"!NULL", and the "!aNULL:!eNULL" embedded by DEFAULT): the keyword + * only adjusts the InitSuites mask, so without this an explicitly listed + * anonymous or NULL-cipher suite would survive (anonymous = no peer + * authentication; NULL = no confidentiality). Compacts in place; *idx is a + * byte count of 2-byte suite pairs. */ +static void RemoveExcludedSuites(byte* suites, int* idx, int anon, int enull) +{ + int i; + int out = 0; + + for (i = 0; (i + 1) < *idx; i += 2) { + if (CipherSuiteExcluded(suites[i], suites[i + 1], anon, enull)) { + continue; + } + if (out != i) { + suites[out + 0] = suites[i + 0]; + suites[out + 1] = suites[i + 1]; + } + out += 2; + } + *idx = out; +} +#endif /* OPENSSL_EXTRA || OPENSSL_ALL */ + /** Set the enabled cipher suites. @@ -29438,6 +29525,8 @@ static int ParseCipherList(Suites* suites, word16 haveAES128 = 1; /* allowed by default if compiled in */ word16 haveSHA1 = 1; /* allowed by default if compiled in */ word16 haveRC4 = 1; /* allowed by default if compiled in */ + int excludeAnon = 0; /* "!aNULL"/DEFAULT seen (applied at end) */ + int excludeNull = 0; /* "!eNULL"/"!NULL"/DEFAULT seen */ #endif int tls1_3 = 0; const int suiteSz = GetCipherNamesSize(); @@ -29542,10 +29631,19 @@ static int ParseCipherList(Suites* suites, } if (XSTRCMP(name, "DEFAULT") == 0 || XSTRCMP(name, "ALL") == 0) { - if (XSTRCMP(name, "ALL") == 0) + if (XSTRCMP(name, "ALL") == 0) { + /* OpenSSL "ALL" includes anonymous suites and is "all but + * eNULL" (haveNull=0 below stops eNULL generation), but it is + * not a delete directive: unlike DEFAULT it must not set + * excludeAnon/excludeNull, nor clear a prior "!aNULL"/"!eNULL". */ haveSig |= SIG_ANON; - else + } + else { + /* OpenSSL "DEFAULT" embeds "!aNULL:!eNULL" (delete both). */ haveSig &= ~SIG_ANON; + excludeAnon = 1; + excludeNull = 1; + } haveRSA = 1; haveDH = 1; haveECC = 1; @@ -29587,6 +29685,11 @@ static int ParseCipherList(Suites* suites, haveSig |= SIG_ANON; else haveSig &= ~SIG_ANON; + /* Track exclusion (sticky) so an explicit ADH suite is dropped at + * the end regardless of where "!aNULL" sits in the list; a later + * allowing "aNULL" does not undo it (OpenSSL "!" is permanent). */ + if (!allowing) + excludeAnon = 1; if (allowing) { /* Allow RSA by default. */ if (!haveECC) @@ -29601,6 +29704,11 @@ static int ParseCipherList(Suites* suites, if (XSTRCMP(name, "eNULL") == 0 || XSTRCMP(name, "NULL") == 0) { haveNull = allowing; + /* Track exclusion (sticky) so an explicit NULL-cipher suite is + * dropped at the end regardless of "!eNULL"/"!NULL" position; a + * later allowing "eNULL" does not undo it. */ + if (!allowing) + excludeNull = 1; if (allowing) { /* Allow RSA by default. */ if (!haveECC) @@ -29843,6 +29951,38 @@ static int ParseCipherList(Suites* suites, } } while (next); +#if defined(OPENSSL_EXTRA) || defined(OPENSSL_ALL) + /* Apply OpenSSL-style category exclusions to the explicitly-listed suites. + * Done here, after the whole list is parsed, so list order does not matter: + * the "!aNULL"/"!eNULL" keywords above only adjust the InitSuites mask + * (which governs generated defaults), never the suites the user named + * explicitly. The excludeAnon/excludeNull flags are set-only / sticky: like + * OpenSSL's permanent "!", once "!aNULL"/"!eNULL" has excluded a category a + * later allowing keyword does not re-enable it - fail closed. */ + if (excludeAnon) { + haveSig &= ~SIG_ANON; + RemoveExcludedSuites(suites->suites, &idx, 1, 0); + } + if (excludeNull) { + haveNull = 0; + RemoveExcludedSuites(suites->suites, &idx, 0, 1); + } + /* If the user named suites but every one was excluded, fail like OpenSSL + * (empty list) rather than silently returning an unusable suite set. + * callInitSuites means defaults were also requested, so an empty explicit + * part is fine there. */ + if (ret && !callInitSuites && idx == 0) { + /* Commit a fully-consistent empty state: suiteSz 0 with setSuites set + * makes InitSuites() leave it empty (it early-outs on setSuites), so + * this fails closed - no suites offered, no default regeneration. */ + suites->suiteSz = 0; + suites->hashSigAlgoSz = 0; + suites->setSuites = 1; + WOLFSSL_MSG("Cipher list empty after !aNULL/!eNULL exclusions"); + return 0; + } +#endif + if (ret) { int keySz = 0; #ifndef NO_CERTS diff --git a/tests/api.c b/tests/api.c index d34a5afef86..25ba83ffd79 100644 --- a/tests/api.c +++ b/tests/api.c @@ -2314,6 +2314,208 @@ static int test_wolfSSL_set_cipher_list_tls13_with_version(void) return EXPECT_RESULT(); } +/* Build gating for the cipher-list exclusion test. Each sub-test is gated only + * on the BUILD_* macro of the suite it needs, so it cleanly skips (rather than + * failing at runtime) in any build missing it - and the two are independent + * (BUILD_TLS_DH_anon_* already implies HAVE_ANON; + * BUILD_TLS_ECDHE_ECDSA_WITH_NULL_SHA already implies HAVE_NULL_CIPHER). The + * "survivor" suite used only by the mixed (scrub-is-surgical) sub-cases is + * gated separately, so the core exclusion cases still run without it. */ +#if defined(OPENSSL_EXTRA) && !defined(NO_WOLFSSL_CLIENT) && \ + !defined(WOLFSSL_NO_TLS12) + #if defined(BUILD_TLS_DH_anon_WITH_AES_128_CBC_SHA) + #define TEST_CIPHER_EXCLUDE_ANON + #endif + #if defined(BUILD_TLS_ECDHE_ECDSA_WITH_NULL_SHA) + #define TEST_CIPHER_EXCLUDE_NULL + #endif +#endif + +#if defined(TEST_CIPHER_EXCLUDE_ANON) || defined(TEST_CIPHER_EXCLUDE_NULL) +/* Does the parsed suite list on ssl contain the given suite bytes? */ +static int test_suites_contains(WOLFSSL* ssl, byte s0, byte s1) +{ + int i; + if (ssl == NULL || ssl->suites == NULL) + return 0; + for (i = 0; (i + 1) < ssl->suites->suiteSz; i += 2) { + if ((ssl->suites->suites[i] == s0) && + (ssl->suites->suites[i + 1] == s1)) + return 1; + } + return 0; +} +#endif + +/* OpenSSL-style "!aNULL"/"!eNULL" must remove an explicitly-listed anonymous + * (ADH) or NULL-cipher suite from the parsed list, not merely clear the + * InitSuites mask - otherwise a list like "ADH-AES128-SHA:!aNULL" still + * negotiates an unauthenticated (anonymous) suite, defeating the operator's + * intent. Each sub-case uses a fresh WOLFSSL so state cannot bleed across + * calls. */ +static int test_wolfSSL_set_cipher_list_exclusions(void) +{ + EXPECT_DECLS; +#if defined(TEST_CIPHER_EXCLUDE_ANON) || defined(TEST_CIPHER_EXCLUDE_NULL) + WOLFSSL_CTX* ctx = NULL; + WOLFSSL* ssl = NULL; + + ExpectNotNull(ctx = wolfSSL_CTX_new(wolfSSLv23_client_method())); + +#ifdef TEST_CIPHER_EXCLUDE_ANON + /* Control: the explicit ADH suite IS recognized and added on its own (so + * the later "absent" checks cannot pass trivially). */ + ExpectNotNull(ssl = wolfSSL_new(ctx)); + ExpectIntEQ(wolfSSL_set_cipher_list(ssl, "ADH-AES128-SHA"), + WOLFSSL_SUCCESS); + ExpectIntEQ(test_suites_contains(ssl, CIPHER_BYTE, + TLS_DH_anon_WITH_AES_128_CBC_SHA), 1); + wolfSSL_free(ssl); + ssl = NULL; + + /* Reported case: "!aNULL" after the only (anon) suite empties the list, + * which fails like OpenSSL - and leaves no anonymous suite behind. */ + ExpectNotNull(ssl = wolfSSL_new(ctx)); + ExpectIntEQ(wolfSSL_set_cipher_list(ssl, "ADH-AES128-SHA:!aNULL"), + WOLFSSL_FAILURE); + ExpectIntEQ(test_suites_contains(ssl, CIPHER_BYTE, + TLS_DH_anon_WITH_AES_128_CBC_SHA), 0); + wolfSSL_free(ssl); + ssl = NULL; + + /* Reverse order: "!aNULL" before the suite must also block it. */ + ExpectNotNull(ssl = wolfSSL_new(ctx)); + ExpectIntEQ(wolfSSL_set_cipher_list(ssl, "!aNULL:ADH-AES128-SHA"), + WOLFSSL_FAILURE); + ExpectIntEQ(test_suites_contains(ssl, CIPHER_BYTE, + TLS_DH_anon_WITH_AES_128_CBC_SHA), 0); + wolfSSL_free(ssl); + ssl = NULL; + + /* Report PoC #3: the suite named by its IANA name must also be dropped + * (classification is by suite bytes, independent of the input name form). */ + ExpectNotNull(ssl = wolfSSL_new(ctx)); + ExpectIntEQ(wolfSSL_set_cipher_list(ssl, + "TLS_DH_anon_WITH_AES_128_CBC_SHA:!aNULL"), WOLFSSL_FAILURE); + ExpectIntEQ(test_suites_contains(ssl, CIPHER_BYTE, + TLS_DH_anon_WITH_AES_128_CBC_SHA), 0); + wolfSSL_free(ssl); + ssl = NULL; + +#ifdef BUILD_TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256 + /* Mixed list: the authenticated suite survives, only ADH is removed. */ + ExpectNotNull(ssl = wolfSSL_new(ctx)); + ExpectIntEQ(wolfSSL_set_cipher_list(ssl, + "ECDHE-RSA-AES128-GCM-SHA256:ADH-AES128-SHA:!aNULL"), + WOLFSSL_SUCCESS); + ExpectIntEQ(test_suites_contains(ssl, CIPHER_BYTE, + TLS_DH_anon_WITH_AES_128_CBC_SHA), 0); + ExpectIntEQ(test_suites_contains(ssl, ECC_BYTE, + TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256), 1); + wolfSSL_free(ssl); + ssl = NULL; +#endif /* BUILD_TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256 */ + + /* "DEFAULT" embeds "!aNULL", so an explicit ADH before it is removed. */ + ExpectNotNull(ssl = wolfSSL_new(ctx)); + ExpectIntEQ(wolfSSL_set_cipher_list(ssl, "ADH-AES128-SHA:DEFAULT"), + WOLFSSL_SUCCESS); + ExpectIntEQ(test_suites_contains(ssl, CIPHER_BYTE, + TLS_DH_anon_WITH_AES_128_CBC_SHA), 0); + wolfSSL_free(ssl); + ssl = NULL; + + /* Sticky/fail-closed: once "!aNULL" excludes anon, a later allowing form + * ("ALL" includes anon defaults) must NOT bring it back, matching + * OpenSSL's permanent "!". The generated default list must contain no ADH. */ + ExpectNotNull(ssl = wolfSSL_new(ctx)); + ExpectIntEQ(wolfSSL_set_cipher_list(ssl, "!aNULL:ALL"), WOLFSSL_SUCCESS); + ExpectIntEQ(test_suites_contains(ssl, CIPHER_BYTE, + TLS_DH_anon_WITH_AES_128_CBC_SHA), 0); + wolfSSL_free(ssl); + ssl = NULL; +#endif /* TEST_CIPHER_EXCLUDE_ANON */ + +#ifdef TEST_CIPHER_EXCLUDE_NULL + /* eNULL: a NULL-cipher suite must likewise be removed by "!eNULL". This + * runs independently of HAVE_ANON, in any NULL-cipher-enabled build. */ + ExpectNotNull(ssl = wolfSSL_new(ctx)); + ExpectIntEQ(wolfSSL_set_cipher_list(ssl, "ECDHE-ECDSA-NULL-SHA"), + WOLFSSL_SUCCESS); + ExpectIntEQ(test_suites_contains(ssl, ECC_BYTE, + TLS_ECDHE_ECDSA_WITH_NULL_SHA), 1); /* control: recognized */ + wolfSSL_free(ssl); + ssl = NULL; + + /* "!eNULL" after the only (NULL-cipher) suite empties the list and fails, + * leaving no NULL-cipher suite behind (no survivor suite needed). */ + ExpectNotNull(ssl = wolfSSL_new(ctx)); + ExpectIntEQ(wolfSSL_set_cipher_list(ssl, "ECDHE-ECDSA-NULL-SHA:!eNULL"), + WOLFSSL_FAILURE); + ExpectIntEQ(test_suites_contains(ssl, ECC_BYTE, + TLS_ECDHE_ECDSA_WITH_NULL_SHA), 0); + wolfSSL_free(ssl); + ssl = NULL; + +#ifdef BUILD_TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256 + /* Mixed list: the survivor stays, only the NULL-cipher suite is removed. */ + ExpectNotNull(ssl = wolfSSL_new(ctx)); + ExpectIntEQ(wolfSSL_set_cipher_list(ssl, + "ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-NULL-SHA:!eNULL"), + WOLFSSL_SUCCESS); + ExpectIntEQ(test_suites_contains(ssl, ECC_BYTE, + TLS_ECDHE_ECDSA_WITH_NULL_SHA), 0); /* NULL cipher dropped */ + ExpectIntEQ(test_suites_contains(ssl, ECC_BYTE, + TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256), 1); /* survivor kept */ + wolfSSL_free(ssl); + ssl = NULL; + +#ifdef BUILD_TLS_SHA256_SHA256 + /* TLS 1.3 integrity-only suite (RFC 9150) has no "NULL" in its name but is + * NULL-encryption; "!eNULL" must still drop it (matched by suite id). */ + ExpectNotNull(ssl = wolfSSL_new(ctx)); + ExpectIntEQ(wolfSSL_set_cipher_list(ssl, + "ECDHE-RSA-AES128-GCM-SHA256:TLS13-SHA256-SHA256:!eNULL"), + WOLFSSL_SUCCESS); + ExpectIntEQ(test_suites_contains(ssl, ECC_BYTE, + TLS_SHA256_SHA256), 0); /* integrity-only suite dropped */ + ExpectIntEQ(test_suites_contains(ssl, ECC_BYTE, + TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256), 1); /* survivor kept */ + wolfSSL_free(ssl); + ssl = NULL; + + /* OpenSSL compat: "ALL" is "all but eNULL" - it does not generate NULL + * suites, but it is not a delete directive, so a following "eNULL" + * re-enables them. (The earlier sticky excludeNull-for-ALL wrongly kept + * them disabled; only "!eNULL"/"DEFAULT" should.) */ + ExpectNotNull(ssl = wolfSSL_new(ctx)); + ExpectIntEQ(wolfSSL_set_cipher_list(ssl, "ALL"), WOLFSSL_SUCCESS); + ExpectIntEQ(test_suites_contains(ssl, ECC_BYTE, + TLS_SHA256_SHA256), 0); /* ALL alone: no NULL generated */ + wolfSSL_free(ssl); + ssl = NULL; + + ExpectNotNull(ssl = wolfSSL_new(ctx)); + ExpectIntEQ(wolfSSL_set_cipher_list(ssl, "ALL:eNULL"), WOLFSSL_SUCCESS); + ExpectIntEQ(test_suites_contains(ssl, ECC_BYTE, + TLS_SHA256_SHA256), 1); /* "eNULL" after "ALL" re-enables NULL */ + wolfSSL_free(ssl); + ssl = NULL; +#endif /* BUILD_TLS_SHA256_SHA256 */ +#endif /* BUILD_TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256 */ +#endif /* TEST_CIPHER_EXCLUDE_NULL */ + + wolfSSL_CTX_free(ctx); +#endif /* TEST_CIPHER_EXCLUDE_ANON || TEST_CIPHER_EXCLUDE_NULL */ + return EXPECT_RESULT(); +} +#ifdef TEST_CIPHER_EXCLUDE_ANON + #undef TEST_CIPHER_EXCLUDE_ANON +#endif +#ifdef TEST_CIPHER_EXCLUDE_NULL + #undef TEST_CIPHER_EXCLUDE_NULL +#endif + static int test_wolfSSL_set_alpn_protos_default_fails(void) { EXPECT_DECLS; @@ -16966,6 +17168,32 @@ static int test_wolfSSL_PKCS8_d2i(void) EVP_PKEY_free(pkey); pkey = NULL; #endif + /* A negative length must be rejected before the signed->word32 cast + * (CWE-190 -> CWE-125): length = -1 becomes (word32)0xFFFFFFFF, so the + * decoder believes ~4 GiB are available and reads past the input. The + * same key buffers are decoded with their correct positive length above, + * so these calls fail only because of the negative length. All must + * return NULL; with the guard removed the over-read is reported by ASan + * (and, with a full key, faults outright). */ + { + const unsigned char malformed[2] = { 0x30, 0x10 }; + const unsigned char* mp; +#ifndef NO_RSA + #ifdef USE_CERT_BUFFERS_1024 + const unsigned char* rp = (const unsigned char*)server_key_der_1024; + #else + const unsigned char* rp = (const unsigned char*)server_key_der_2048; + #endif + const unsigned char* rp0 = rp; + ExpectNull(d2i_AutoPrivateKey(NULL, &rp, -1)); + rp = rp0; + ExpectNull(d2i_PrivateKey(EVP_PKEY_RSA, NULL, &rp, -1)); +#endif + /* The reported PoC: a 2-byte header claiming 16 bytes of absent + * content drives the over-read in the key-type detection loop. */ + mp = malformed; + ExpectNull(d2i_AutoPrivateKey(NULL, &mp, -1)); + } #endif /* OPENSSL_ALL */ #ifndef NO_FILESYSTEM @@ -35084,6 +35312,7 @@ TEST_CASE testCases[] = { TEST_DECL(test_wolfSSL_set_cipher_list_tls13_keeps_tls12), TEST_DECL(test_wolfSSL_set_cipher_list_tls12_with_version), TEST_DECL(test_wolfSSL_set_cipher_list_tls13_with_version), + TEST_DECL(test_wolfSSL_set_cipher_list_exclusions), TEST_DECL(test_wolfSSL_set_alpn_protos_default_fails), TEST_DECL(test_wolfSSL_CTX_use_certificate), TEST_DECL(test_wolfSSL_CTX_use_certificate_file), diff --git a/tests/api/test_asn.c b/tests/api/test_asn.c index 63cbfd365d7..bfa95626368 100644 --- a/tests/api/test_asn.c +++ b/tests/api/test_asn.c @@ -1495,6 +1495,46 @@ int test_DecodeAltNames_length_underflow(void) return EXPECT_RESULT(); } +/* A certificate must not carry two certificatePolicies extensions + * (non-repeatable per RFC 5280 4.2). DecodeCertExtensions calls + * DecodeExtensionType once per extension; with strict ASN.1 (the default) a + * second certificatePolicies extension must be rejected (ASN_OBJECT_ID_E) + * rather than silently overwriting the first - which happened in + * WOLFSSL_CERT_EXT builds without WOLFSSL_SEP before the duplicate guard was + * extended to cover them. */ +int test_DecodeCertExtensions_dup_certpol(void) +{ + EXPECT_DECLS; +#if (defined(WOLFSSL_SEP) || defined(WOLFSSL_CERT_EXT)) && \ + !defined(WOLFSSL_NO_ASN_STRICT) && !defined(NO_CERTS) && !defined(NO_ASN) + /* Minimal certificatePolicies extnValue: SEQUENCE OF PolicyInformation + * with one policyIdentifier OID 1.2.3.4 (encoded 2A 03 04). */ + static const byte policy[] = { + 0x30, 0x07, /* certificatePolicies SEQUENCE */ + 0x30, 0x05, /* PolicyInformation SEQUENCE */ + 0x06, 0x03, 0x2A, 0x03, 0x04 /* policyIdentifier OID 1.2.3.4 */ + }; + DecodedCert cert; + int isUnknown = 0; + + /* DecodeExtensionType only needs an initialized DecodedCert for its + * bit-fields and policy storage; the source buffer is never parsed here, + * so any non-NULL pointer/size suffices. */ + wc_InitDecodedCert(&cert, policy, (word32)sizeof(policy), NULL); + + /* First certificatePolicies extension: accepted. */ + ExpectIntEQ(DecodeExtensionType(policy, (word32)sizeof(policy), + CERT_POLICY_OID, 0, &cert, &isUnknown), 0); + /* Duplicate certificatePolicies extension: rejected as non-repeatable. */ + ExpectIntEQ(DecodeExtensionType(policy, (word32)sizeof(policy), + CERT_POLICY_OID, 0, &cert, &isUnknown), + WC_NO_ERR_TRACE(ASN_OBJECT_ID_E)); + + wc_FreeDecodedCert(&cert); +#endif + return EXPECT_RESULT(); +} + int test_ParseCert_SM3wSM2_short_pubkey(void) { EXPECT_DECLS; diff --git a/tests/api/test_asn.h b/tests/api/test_asn.h index a0c72b2517a..d48aac7a6f5 100644 --- a/tests/api/test_asn.h +++ b/tests/api/test_asn.h @@ -35,6 +35,7 @@ int test_wolfssl_local_MatchUriNameConstraint(void); int test_wc_DecodeRsaPssParams(void); int test_SerialNumber0_RootCA(void); int test_DecodeAltNames_length_underflow(void); +int test_DecodeCertExtensions_dup_certpol(void); int test_ParseCert_SM3wSM2_short_pubkey(void); int test_wc_DecodeObjectId(void); int test_ToTraditional_ex_handcrafted(void); @@ -54,6 +55,7 @@ int test_ToTraditional_ex_mldsa_bad_params(void); TEST_DECL_GROUP("asn", test_wc_DecodeRsaPssParams), \ TEST_DECL_GROUP("asn", test_SerialNumber0_RootCA), \ TEST_DECL_GROUP("asn", test_DecodeAltNames_length_underflow), \ + TEST_DECL_GROUP("asn", test_DecodeCertExtensions_dup_certpol), \ TEST_DECL_GROUP("asn", test_ParseCert_SM3wSM2_short_pubkey), \ TEST_DECL_GROUP("asn", test_wc_DecodeObjectId), \ TEST_DECL_GROUP("asn", test_ToTraditional_ex_handcrafted), \ diff --git a/tests/api/test_ossl_asn1.c b/tests/api/test_ossl_asn1.c index 46500bd0e03..a88b3eb89bf 100644 --- a/tests/api/test_ossl_asn1.c +++ b/tests/api/test_ossl_asn1.c @@ -2816,6 +2816,28 @@ int test_ASN1_strings(void) der = NULL; } + /* Negative length must be rejected before the signed->word32 cast + * (CWE-190 -> CWE-125). The 2-byte header claims 16 bytes of content + * that are not present in the buffer; passing len = -1 would, without + * the guard, cast to ~4 GiB so GetLength accepts the claimed length and + * d2i_ASN1_STRING over-reads 16 bytes past the buffer. Every wrapper + * must instead return NULL without reading the content. */ + { + unsigned char hdr[2]; + const unsigned char* p; + + hdr[1] = 0x10; /* claim 16 bytes of (absent) content */ + + p = hdr; hdr[0] = ASN_OCTET_STRING; + ExpectNull(d2i_ASN1_OCTET_STRING(NULL, &p, -1)); + p = hdr; hdr[0] = ASN_GENERALSTRING; + ExpectNull(d2i_ASN1_GENERALSTRING(NULL, &p, -1)); + p = hdr; hdr[0] = ASN_UTF8STRING; + ExpectNull(d2i_ASN1_UTF8STRING(NULL, &p, -1)); + p = hdr; hdr[0] = ASN_BIT_STRING; + ExpectNull(d2i_ASN1_BIT_STRING(NULL, &p, -1)); + } + #endif return EXPECT_RESULT(); } diff --git a/wolfcrypt/src/asn.c b/wolfcrypt/src/asn.c index e84d252931d..7e855986f67 100644 --- a/wolfcrypt/src/asn.c +++ b/wolfcrypt/src/asn.c @@ -21036,8 +21036,9 @@ static int DecodeAltSigVal(const byte* input, int sz, DecodedCert* cert) * @return MEMORY_E on dynamic memory allocation failure. * @return Other negative values on error. */ -int DecodeExtensionType(const byte* input, word32 length, word32 oid, - byte critical, DecodedCert* cert, int *isUnknownExt) +WOLFSSL_TEST_VIS int DecodeExtensionType(const byte* input, word32 length, + word32 oid, byte critical, + DecodedCert* cert, int *isUnknownExt) { int ret = 0; word32 idx = 0; @@ -21137,11 +21138,17 @@ int DecodeExtensionType(const byte* input, word32 length, word32 oid, /* Certificate policies. */ case CERT_POLICY_OID: - #ifdef WOLFSSL_SEP + #if defined(WOLFSSL_SEP) || defined(WOLFSSL_CERT_EXT) + /* certificatePolicies is non-repeatable (RFC 5280 4.2). In strict + * mode (the default; VERIFY_AND_SET_OID is a no-op under + * WOLFSSL_NO_ASN_STRICT, like every other extension) reject a + * duplicate regardless of WOLFSSL_SEP - otherwise the second one + * silently overwrites the first (DecodeCertPolicy resets + * extCertPoliciesNb), a policy-authorization confusion. */ VERIFY_AND_SET_OID(cert->extCertPolicySet); + #ifdef WOLFSSL_SEP cert->extCertPolicyCrit = critical ? 1 : 0; #endif - #if defined(WOLFSSL_SEP) || defined(WOLFSSL_CERT_EXT) if (DecodeCertPolicy(input, length, cert) < 0) { ret = ASN_PARSE_E; } diff --git a/wolfssl/wolfcrypt/asn.h b/wolfssl/wolfcrypt/asn.h index 8c82294d12a..45993f0638a 100644 --- a/wolfssl/wolfcrypt/asn.h +++ b/wolfssl/wolfcrypt/asn.h @@ -2089,7 +2089,7 @@ struct DecodedCert { WC_BITFIELD extSubjAltNameSet:1; WC_BITFIELD inhibitAnyOidSet:1; WC_BITFIELD selfSigned:1; /* Indicates subject and issuer are same */ -#ifdef WOLFSSL_SEP +#if defined(WOLFSSL_SEP) || defined(WOLFSSL_CERT_EXT) WC_BITFIELD extCertPolicySet:1; #endif WC_BITFIELD extCRLdistCrit:1; @@ -2369,6 +2369,7 @@ typedef enum MimeStatus #define FreeSigner wc_FreeSigner #define AllocDer wc_AllocDer #define FreeDer wc_FreeDer + #define DecodeExtensionType wc_DecodeExtensionType #endif /* WOLFSSL_API_PREFIX_MAP */ WOLFSSL_LOCAL int HashIdAlg(word32 oidSum); @@ -2412,9 +2413,9 @@ WOLFSSL_LOCAL int DecodePolicyOID(char *out, word32 outSz, const byte *in, word32 inSz); WOLFSSL_LOCAL int EncodePolicyOID(byte *out, word32 *outSz, const char *in, void* heap); -WOLFSSL_LOCAL int DecodeExtensionType(const byte* input, word32 length, - word32 oid, byte critical, - DecodedCert* cert, int *isUnknownExt); +WOLFSSL_TEST_VIS int DecodeExtensionType(const byte* input, word32 length, + word32 oid, byte critical, + DecodedCert* cert, int *isUnknownExt); WOLFSSL_LOCAL int CheckCertSignaturePubKey(const byte* cert, word32 certSz, void* heap, const byte* pubKey, word32 pubKeySz, int pubKeyOID); #if defined(OPENSSL_EXTRA) || defined(WOLFSSL_SMALL_CERT_VERIFY)