From 369a439df4e5b8b3c85a705fcf3e885052667204 Mon Sep 17 00:00:00 2001 From: Reda Chouk Date: Wed, 19 Aug 2026 16:05:58 +0200 Subject: [PATCH] asn: verify KeyPurposeId OID bytes before mapping the sum to an extended key usage bit --- tests/api/test_asn.c | 100 ++++++++++++++++++++++++ tests/api/test_asn.h | 2 + tests/unit-mcdc/test_asn_ext_whitebox.c | 26 ++++++ wolfcrypt/src/asn.c | 19 ++++- 4 files changed, 146 insertions(+), 1 deletion(-) diff --git a/tests/api/test_asn.c b/tests/api/test_asn.c index 1d6f1e571a..bd1611ccd7 100644 --- a/tests/api/test_asn.c +++ b/tests/api/test_asn.c @@ -2711,6 +2711,106 @@ int test_wc_DecodeKeyUsage_decipherOnly(void) return EXPECT_RESULT(); } +#if !defined(NO_ASN) && !defined(NO_RSA) && !defined(NO_CERTS) && \ + defined(WOLFSSL_CERT_GEN) && defined(WOLFSSL_CERT_EXT) && \ + defined(WOLFSSL_EKU_OID) && !defined(NO_SHA256) && \ + defined(USE_CERT_BUFFERS_2048) && !defined(NO_ASN_TIME) && \ + !defined(WC_NO_RNG) && !defined(NO_ASN_CRYPT) && !defined(NO_VERIFY_OID) + /* NO_VERIFY_OID compiles out the OID check this covers. */ + #define TEST_EKU_OID_SUM_COLLISION +#endif + +/* A KeyPurposeId whose wc_oid_sum() collides with id-kp-serverAuth must not + * authorize server authentication, while the real OID still does. */ +int test_wc_DecodeExtKeyUsage_oidSumCollision(void) +{ + EXPECT_DECLS; +#ifdef TEST_EKU_OID_SUM_COLLISION + static const struct { + const char* oid; /* custom EKU OID, NULL to use a named purpose */ + const char* name; /* named purpose, used when oid is NULL */ + byte expected; + } ekuCases[] = { + /* id-kp-serverAuth. */ + { NULL, "serverAuth", EXTKEYUSE_SERVER_AUTH }, +#ifdef WOLFSSL_OLD_OID_SUM + /* 1.5.6.1.5.5.3.1, the same byte sum (71) as id-kp-serverAuth. */ + { "1.5.6.1.5.5.3.1", NULL, 0 }, +#else + /* 1.19.6.1.5.21.7.3.1, the same XOR-shift sum (0x0402012e) as + * id-kp-serverAuth. */ + { "1.19.6.1.5.21.7.3.1", NULL, 0 }, +#endif + }; + WC_RNG rng; + RsaKey key; + byte* der = NULL; + word32 idx = 0; + int rngInit = 0; + int keyInit = 0; + size_t c; + + XMEMSET(&rng, 0, sizeof(rng)); + XMEMSET(&key, 0, sizeof(key)); + + ExpectIntEQ(wc_InitRng(&rng), 0); + if (EXPECT_SUCCESS()) rngInit = 1; + + ExpectNotNull(der = (byte*)XMALLOC(FOURK_BUF, HEAP_HINT, + DYNAMIC_TYPE_TMP_BUFFER)); + + ExpectIntEQ(wc_InitRsaKey_ex(&key, HEAP_HINT, testDevId), 0); + if (EXPECT_SUCCESS()) keyInit = 1; + ExpectIntEQ(wc_RsaPrivateKeyDecode(server_key_der_2048, &idx, &key, + sizeof_server_key_der_2048), 0); + + for (c = 0; c < XELEM_CNT(ekuCases); c++) { + Cert cert; + DecodedCert dCert; + int dCertInit = 0; + int derSz = 0; + + if (!EXPECT_SUCCESS()) break; + + XMEMSET(&cert, 0, sizeof(cert)); + ExpectIntEQ(wc_InitCert(&cert), 0); + if (EXPECT_SUCCESS()) { + cert.sigType = CTC_SHA256wRSA; + cert.isCA = 0; + XSTRNCPY(cert.subject.country, "US", CTC_NAME_SIZE); + XSTRNCPY(cert.subject.org, "wolfSSL", CTC_NAME_SIZE); + XSTRNCPY(cert.subject.commonName, "extKeyUsage", CTC_NAME_SIZE); + } + if (ekuCases[c].oid != NULL) { + ExpectIntEQ(wc_SetExtKeyUsageOID(&cert, ekuCases[c].oid, + (word32)XSTRLEN(ekuCases[c].oid), 0, HEAP_HINT), 0); + } + else { + ExpectIntEQ(wc_SetExtKeyUsage(&cert, ekuCases[c].name), 0); + } + ExpectIntGT(derSz = wc_MakeSelfCert(&cert, der, FOURK_BUF, &key, &rng), + 0); + + if (EXPECT_SUCCESS() && (der != NULL)) { + wc_InitDecodedCert(&dCert, der, (word32)derSz, HEAP_HINT); + dCertInit = 1; + ExpectIntEQ(wc_ParseCert(&dCert, CERT_TYPE, NO_VERIFY, NULL), 0); + /* The extension must be seen in both cases - an unrecognized + * KeyPurposeId is skipped, not an error - but only the real OID + * may authorize a purpose. */ + ExpectIntNE(dCert.extExtKeyUsageSet, 0); + ExpectIntEQ(dCert.extExtKeyUsage, ekuCases[c].expected); + } + if (dCertInit) wc_FreeDecodedCert(&dCert); + } + + if (keyInit) wc_FreeRsaKey(&key); + if (rngInit) wc_FreeRng(&rng); + XFREE(der, HEAP_HINT, DYNAMIC_TYPE_TMP_BUFFER); +#endif /* TEST_EKU_OID_SUM_COLLISION */ + return EXPECT_RESULT(); +} + int test_wc_SignCert_buffer_bounds(void) { EXPECT_DECLS; diff --git a/tests/api/test_asn.h b/tests/api/test_asn.h index 4ac0cf3404..22d38d880d 100644 --- a/tests/api/test_asn.h +++ b/tests/api/test_asn.h @@ -45,6 +45,7 @@ int test_ToTraditional_ex_negative(void); int test_ToTraditional_ex_mldsa_bad_params(void); int test_wc_SignCert_buffer_bounds(void); int test_wc_DecodeKeyUsage_decipherOnly(void); +int test_wc_DecodeExtKeyUsage_oidSumCollision(void); int test_wc_AsnDecisionCoverage(void); int test_wc_AsnFeatureCoverage(void); @@ -70,6 +71,7 @@ int test_wc_AsnFeatureCoverage(void); TEST_DECL_GROUP("asn", test_ToTraditional_ex_mldsa_bad_params), \ TEST_DECL_GROUP("asn", test_wc_SignCert_buffer_bounds), \ TEST_DECL_GROUP("asn", test_wc_DecodeKeyUsage_decipherOnly), \ + TEST_DECL_GROUP("asn", test_wc_DecodeExtKeyUsage_oidSumCollision), \ TEST_DECL_GROUP("asn", test_wc_AsnDecisionCoverage), \ TEST_DECL_GROUP("asn", test_wc_AsnFeatureCoverage) diff --git a/tests/unit-mcdc/test_asn_ext_whitebox.c b/tests/unit-mcdc/test_asn_ext_whitebox.c index 947977058f..9b90e0e73d 100644 --- a/tests/unit-mcdc/test_asn_ext_whitebox.c +++ b/tests/unit-mcdc/test_asn_ext_whitebox.c @@ -904,6 +904,32 @@ static void wb_decode_ext_key_usage(void) &ssh, NULL); WB_CHECK(ret == 0, "extExtKeyUsageOidCnt==NULL out-ptr (2nd operand false)"); +#ifndef NO_VERIFY_OID + /* An OID that is not id-kp-serverAuth but whose wc_oid_sum() collides with + * it must not be treated as serverAuth. */ + { + #ifdef WOLFSSL_OLD_OID_SUM + /* 1.5.6.1.5.5.3.1, the same byte sum (71) as serverAuth. */ + static const byte ekuCollide[] = { + 0x30,0x0A, + 0x06,0x08, 0x2D,0x06,0x01,0x05,0x05,0x05,0x03,0x01 + }; + #else + /* 1.19.6.1.5.21.7.3.1, the same XOR-shift sum (0x0402012e) as + * serverAuth. */ + static const byte ekuCollide[] = { + 0x30,0x0A, + 0x06,0x08, 0x3B,0x06,0x01,0x05,0x15,0x07,0x03,0x01 + }; + #endif + src = NULL; srcSz = 0; count = 0; usage = 0; ssh = 0; oidCnt = 0; + ret = DecodeExtKeyUsage(ekuCollide, sizeof(ekuCollide), &src, &srcSz, + &count, &usage, &ssh, &oidCnt); + WB_CHECK(ret == 0 && usage == 0 && oidCnt == 1, + "OID colliding with serverAuth checksum not set as serverAuth"); + } +#endif /* !NO_VERIFY_OID */ + /* Empty SEQUENCE OF -> loop condition false immediately. */ { static const byte ekuEmpty[] = { 0x30, 0x00 }; diff --git a/wolfcrypt/src/asn.c b/wolfcrypt/src/asn.c index c639a62484..b33e8ad478 100644 --- a/wolfcrypt/src/asn.c +++ b/wolfcrypt/src/asn.c @@ -20967,8 +20967,25 @@ int DecodeExtKeyUsage(const byte* input, word32 sz, ret = 0; } else if (ret == 0) { + word32 oidSum = dataASN[KEYPURPOSEIDASN_IDX_OID].data.oid.sum; + #ifndef NO_VERIFY_OID + const byte* knownOid; + word32 knownOidSz = 0; + + /* The sum is only a checksum, so confirm the bytes match the OID + * it maps to - oidIgnoreType above skipped that check. */ + knownOid = OidFromId(oidSum, oidCertKeyUseType, &knownOidSz); + if ((knownOid == NULL) || + (knownOidSz != + dataASN[KEYPURPOSEIDASN_IDX_OID].data.oid.length) || + (XMEMCMP(dataASN[KEYPURPOSEIDASN_IDX_OID].data.oid.data, + knownOid, knownOidSz) != 0)) { + /* Not the OID this sum stands for - treat as unknown. */ + oidSum = 0; + } + #endif /* Store the bit for the OID. */ - switch (dataASN[KEYPURPOSEIDASN_IDX_OID].data.oid.sum) { + switch (oidSum) { case EKU_ANY_OID: *extExtKeyUsage |= EXTKEYUSE_ANY; break;