From 8b0e0b9683f16e4ca4cf1d3ae3dad344bb5ce30f Mon Sep 17 00:00:00 2001 From: Anthony Hu Date: Tue, 21 Jul 2026 15:53:33 -0400 Subject: [PATCH 1/3] New API for CRL unknown extension callback Adds public entry points mirroring the existing X.509 unknown extension callback so callers can register a handler for unrecognized CRL extensions instead of failing with ASN_CRIT_EXT_E. --- src/crl.c | 14 +++ src/ssl_certman.c | 25 +++++ tests/api/test_certman.c | 196 ++++++++++++++++++++++++++++++++++++--- tests/api/test_certman.h | 9 ++ wolfcrypt/src/asn.c | 104 ++++++++++++++++++--- wolfssl/internal.h | 5 + wolfssl/ssl.h | 18 ++++ wolfssl/wolfcrypt/asn.h | 5 + 8 files changed, 352 insertions(+), 24 deletions(-) diff --git a/src/crl.c b/src/crl.c index 6c242eb6ee5..83f35a84e4c 100644 --- a/src/crl.c +++ b/src/crl.c @@ -899,6 +899,13 @@ int BufferLoadCRL(WOLFSSL_CRL* crl, const byte* buff, long sz, int type, } InitDecodedCRL(dcrl, crl->heap); +#ifdef WC_ASN_UNKNOWN_EXT_CB + if (crl->cm != NULL) { + dcrl->unknownExtCallback = crl->cm->crlUnknownExtCallback; + dcrl->unknownExtCallbackEx = crl->cm->crlUnknownExtCallbackEx; + dcrl->unknownExtCallbackExCtx = crl->cm->crlUnknownExtCallbackExCtx; + } +#endif ret = ParseCRL(currentEntry->certs, dcrl, myBuffer, (word32)sz, verify, crl->cm); @@ -1224,6 +1231,13 @@ int GetCRLInfo(WOLFSSL_CRL* crl, CrlInfo* info, const byte* buff, } InitDecodedCRL(dcrl, crl->heap); +#ifdef WC_ASN_UNKNOWN_EXT_CB + if (crl->cm != NULL) { + dcrl->unknownExtCallback = crl->cm->crlUnknownExtCallback; + dcrl->unknownExtCallbackEx = crl->cm->crlUnknownExtCallbackEx; + dcrl->unknownExtCallbackExCtx = crl->cm->crlUnknownExtCallbackExCtx; + } +#endif ret = ParseCRL(crle->certs, dcrl, myBuffer, (word32)sz, 0, crl->cm); if (ret != 0 && !(ret == WC_NO_ERR_TRACE(ASN_CRL_NO_SIGNER_E))) { diff --git a/src/ssl_certman.c b/src/ssl_certman.c index d712e3b4592..f844bc40ca3 100644 --- a/src/ssl_certman.c +++ b/src/ssl_certman.c @@ -748,6 +748,31 @@ void wolfSSL_CertManagerSetUnknownExtCallback(WOLFSSL_CERT_MANAGER* cm, } } + +#ifdef HAVE_CRL +int wolfSSL_CertManagerSetCRLUnknownExtCallback(WOLFSSL_CERT_MANAGER* cm, + wc_UnknownExtCallback cb) +{ + WOLFSSL_ENTER("wolfSSL_CertManagerSetCRLUnknownExtCallback"); + if (cm == NULL) { + return BAD_FUNC_ARG; + } + cm->crlUnknownExtCallback = cb; + return WOLFSSL_SUCCESS; +} + +int wolfSSL_CertManagerSetCRLUnknownExtCallbackEx(WOLFSSL_CERT_MANAGER* cm, + wc_UnknownExtCallbackEx cb, void* ctx) +{ + WOLFSSL_ENTER("wolfSSL_CertManagerSetCRLUnknownExtCallbackEx"); + if (cm == NULL) { + return BAD_FUNC_ARG; + } + cm->crlUnknownExtCallbackEx = cb; + cm->crlUnknownExtCallbackExCtx = ctx; + return WOLFSSL_SUCCESS; +} +#endif /* HAVE_CRL */ #endif /* WC_ASN_UNKNOWN_EXT_CB */ #if (!defined(NO_WOLFSSL_CLIENT) || !defined(WOLFSSL_NO_CLIENT_AUTH)) || \ diff --git a/tests/api/test_certman.c b/tests/api/test_certman.c index 973fd494dc7..4a959598933 100644 --- a/tests/api/test_certman.c +++ b/tests/api/test_certman.c @@ -2846,12 +2846,10 @@ int test_wolfSSL_CRL_critical_idp(void) return EXPECT_RESULT(); } -int test_wolfSSL_CRL_unknown_critical_ext(void) -{ - EXPECT_DECLS; #if !defined(NO_CERTS) && defined(HAVE_CRL) && !defined(NO_RSA) - - static const unsigned char ca_cert[] = { +/* claim-root CA, DER. Shared by the unknown-critical-CRL-extension tests + * below. */ +static const unsigned char crl_unk_ca_der[] = { 0x30, 0x82, 0x03, 0x1b, 0x30, 0x82, 0x02, 0x03, 0xa0, 0x03, 0x02, 0x01, 0x02, 0x02, 0x14, 0x1e, 0x25, 0xc1, 0x5d, 0x6f, 0x02, 0x21, 0xa0, 0xf0, 0x14, 0x15, 0x9c, 0x3b, 0x4d, 0x1d, 0x73, 0x16, 0x00, @@ -2927,10 +2925,10 @@ int test_wolfSSL_CRL_unknown_critical_ext(void) 0xa2, 0xb0, 0x3e, 0x61, 0x16, 0x69, 0x8f }; - /* CRL with critical obsolete extension OID 2.5.29.1, 422 bytes DER. - * OID 2.5.29.1 is the old X.509v2 Authority Key Identifier, permanently - * superseded by 2.5.29.35. No implementation will ever support it. */ - static const unsigned char crl_obsolete_critical[] = { +/* CRL with a critical obsolete CRL-level extension OID 2.5.29.1, 422 bytes + * DER. OID 2.5.29.1 is the old X.509v2 Authority Key Identifier, permanently + * superseded by 2.5.29.35. No implementation will ever support it. */ +static const unsigned char crl_obsolete_critical[] = { 0x30, 0x82, 0x01, 0xa6, 0x30, 0x81, 0x8f, 0x02, 0x01, 0x01, 0x30, 0x0d, 0x06, 0x09, 0x2a, 0x86, 0x48, 0x86, 0xf7, 0x0d, 0x01, 0x01, 0x0b, 0x05, 0x00, 0x30, 0x15, 0x31, 0x13, 0x30, 0x11, 0x06, 0x03, @@ -2970,13 +2968,18 @@ int test_wolfSSL_CRL_unknown_critical_ext(void) 0xff, 0xad, 0x4d, 0x6e, 0x10, 0x73, 0x68, 0xfa, 0x54, 0x9e, 0xdc, 0x34, 0x70, 0xe4, 0x5d, 0x9e, 0x7c, 0xfa, 0x59, 0x97, 0xde, 0x35, 0x17, 0xbb, 0xaf, 0xa0, 0x28, 0x78, 0x13, 0xbf - }; +}; +#endif /* !NO_CERTS && HAVE_CRL && !NO_RSA */ +int test_wolfSSL_CRL_unknown_critical_ext(void) +{ + EXPECT_DECLS; +#if !defined(NO_CERTS) && defined(HAVE_CRL) && !defined(NO_RSA) WOLFSSL_CERT_MANAGER* cm = NULL; ExpectNotNull(cm = wolfSSL_CertManagerNew()); - ExpectIntEQ(wolfSSL_CertManagerLoadCABuffer(cm, ca_cert, - sizeof(ca_cert), WOLFSSL_FILETYPE_ASN1), WOLFSSL_SUCCESS); + ExpectIntEQ(wolfSSL_CertManagerLoadCABuffer(cm, crl_unk_ca_der, + sizeof(crl_unk_ca_der), WOLFSSL_FILETYPE_ASN1), WOLFSSL_SUCCESS); ExpectIntEQ(wolfSSL_CertManagerEnableCRL(cm, WOLFSSL_CRL_CHECKALL), WOLFSSL_SUCCESS); @@ -3014,6 +3017,175 @@ int test_wolfSSL_CRL_unknown_critical_entry_ext(void) return EXPECT_RESULT(); } +#if !defined(NO_CERTS) && defined(HAVE_CRL) && !defined(NO_RSA) && \ + !defined(NO_FILESYSTEM) && defined(WC_ASN_UNKNOWN_EXT_CB) +/* Counter context plus the OID we observed on the unknown extension. */ +typedef struct CRLUnkExtCtx { + int calls; + int sawCritical; + int oidMatched; +} CRLUnkExtCtx; + +/* CRL Reason OID 2.5.29.21 (last component for the entry-ext test) and + * obsolete X.509v2 AKI 2.5.29.1 (last component for the CRL-level test). */ +static int crl_unk_ext_cb_accept(const word16* oid, word32 oidSz, + int crit, const unsigned char* der, word32 derSz, void* ctxIn) +{ + CRLUnkExtCtx* ctx = (CRLUnkExtCtx*)ctxIn; + (void)der; + (void)derSz; + if (ctx != NULL) { + ctx->calls++; + if (crit) + ctx->sawCritical = 1; + /* Expect OID arc starts at {2, 5, 29, ...}. */ + if (oidSz >= 4 && oid[0] == 2 && oid[1] == 5 && oid[2] == 29) + ctx->oidMatched = 1; + } + return 0; /* accept */ +} + +static int crl_unk_ext_cb_reject(const word16* oid, word32 oidSz, + int crit, const unsigned char* der, word32 derSz, void* ctxIn) +{ + (void)oid; (void)oidSz; (void)crit; (void)der; (void)derSz; + if (ctxIn != NULL) + ((CRLUnkExtCtx*)ctxIn)->calls++; + return ASN_PARSE_E; +} + +static int crl_unk_ext_cb_reject_positive(const word16* oid, word32 oidSz, + int crit, const unsigned char* der, word32 derSz, void* ctxIn) +{ + (void)oid; (void)oidSz; (void)crit; (void)der; (void)derSz; + if (ctxIn != NULL) + ((CRLUnkExtCtx*)ctxIn)->calls++; + return 1; +} +#endif + +/* Callback rescues a critical unknown CRL ENTRY extension that would + * otherwise be rejected per RFC 5280 5.3. The fixture + * crl_critical_entry.pem carries OID 2.5.29.1 in a revoked entry. */ +int test_wolfSSL_CRL_unknown_ext_cb_rescues_critical_entry_ext(void) +{ + EXPECT_DECLS; +#if !defined(NO_CERTS) && defined(HAVE_CRL) && !defined(NO_RSA) && \ + !defined(NO_FILESYSTEM) && defined(WC_ASN_UNKNOWN_EXT_CB) + WOLFSSL_CERT_MANAGER* cm = NULL; + CRLUnkExtCtx ctx = { 0, 0, 0 }; + + ExpectNotNull(cm = wolfSSL_CertManagerNew()); + ExpectIntEQ(wolfSSL_CertManagerLoadCA(cm, + "./certs/crl/extra-crls/claim-root.pem", NULL), WOLFSSL_SUCCESS); + ExpectIntEQ(wolfSSL_CertManagerEnableCRL(cm, WOLFSSL_CRL_CHECKALL), + WOLFSSL_SUCCESS); + + /* Register a callback that accepts any unknown extension. */ + ExpectIntEQ(wolfSSL_CertManagerSetCRLUnknownExtCallbackEx(cm, + crl_unk_ext_cb_accept, &ctx), WOLFSSL_SUCCESS); + + /* Without the callback, this load fails (see + * test_wolfSSL_CRL_unknown_critical_entry_ext above). With an + * accepting callback registered, the load must succeed. */ + ExpectIntEQ(wolfSSL_CertManagerLoadCRLFile(cm, + "./certs/crl/extra-crls/crl_critical_entry.pem", WOLFSSL_FILETYPE_PEM), + WOLFSSL_SUCCESS); + + /* Callback must have fired at least once on the unknown critical OID. */ + ExpectIntGT(ctx.calls, 0); + ExpectIntEQ(ctx.sawCritical, 1); + ExpectIntEQ(ctx.oidMatched, 1); + + wolfSSL_CertManagerFree(cm); +#endif + return EXPECT_RESULT(); +} + +/* Callback rescues a critical unknown CRL-LEVEL extension. Drives the + * dispatch in ParseCRL_Extensions (as opposed to the entry-extension test + * above, which drives ParseCRL_EntryExtensions) using the same embedded + * crl_obsolete_critical fixture that test_wolfSSL_CRL_unknown_critical_ext + * proves is rejected without a callback. */ +int test_wolfSSL_CRL_unknown_ext_cb_rescues_critical_crl_ext(void) +{ + EXPECT_DECLS; +#if !defined(NO_CERTS) && defined(HAVE_CRL) && !defined(NO_RSA) && \ + !defined(NO_FILESYSTEM) && defined(WC_ASN_UNKNOWN_EXT_CB) + WOLFSSL_CERT_MANAGER* cm = NULL; + CRLUnkExtCtx ctx = { 0, 0, 0 }; + + /* Accepting callback: the load must succeed and the callback must have + * seen the critical unknown OID. */ + ExpectNotNull(cm = wolfSSL_CertManagerNew()); + ExpectIntEQ(wolfSSL_CertManagerLoadCABuffer(cm, crl_unk_ca_der, + sizeof(crl_unk_ca_der), WOLFSSL_FILETYPE_ASN1), WOLFSSL_SUCCESS); + ExpectIntEQ(wolfSSL_CertManagerEnableCRL(cm, WOLFSSL_CRL_CHECKALL), + WOLFSSL_SUCCESS); + ExpectIntEQ(wolfSSL_CertManagerSetCRLUnknownExtCallbackEx(cm, + crl_unk_ext_cb_accept, &ctx), WOLFSSL_SUCCESS); + ExpectIntEQ(wolfSSL_CertManagerLoadCRLBuffer(cm, crl_obsolete_critical, + sizeof(crl_obsolete_critical), WOLFSSL_FILETYPE_ASN1), + WOLFSSL_SUCCESS); + ExpectIntGT(ctx.calls, 0); + ExpectIntEQ(ctx.sawCritical, 1); + ExpectIntEQ(ctx.oidMatched, 1); + wolfSSL_CertManagerFree(cm); + cm = NULL; + + /* Rejecting callback: the same CRL must fail to load. */ + ctx.calls = 0; + ExpectNotNull(cm = wolfSSL_CertManagerNew()); + ExpectIntEQ(wolfSSL_CertManagerLoadCABuffer(cm, crl_unk_ca_der, + sizeof(crl_unk_ca_der), WOLFSSL_FILETYPE_ASN1), WOLFSSL_SUCCESS); + ExpectIntEQ(wolfSSL_CertManagerEnableCRL(cm, WOLFSSL_CRL_CHECKALL), + WOLFSSL_SUCCESS); + ExpectIntEQ(wolfSSL_CertManagerSetCRLUnknownExtCallbackEx(cm, + crl_unk_ext_cb_reject, &ctx), WOLFSSL_SUCCESS); + ExpectIntNE(wolfSSL_CertManagerLoadCRLBuffer(cm, crl_obsolete_critical, + sizeof(crl_obsolete_critical), WOLFSSL_FILETYPE_ASN1), + WOLFSSL_SUCCESS); + ExpectIntGT(ctx.calls, 0); + + wolfSSL_CertManagerFree(cm); +#endif + return EXPECT_RESULT(); +} + +/* A callback that rejects with a positive value must not make the load + * report success. WOLFSSL_SUCCESS is 1, and BufferLoadCRL converts its + * error code with "ret ? ret : WOLFSSL_SUCCESS", so a positive callback + * return that propagates unsanitized out of ParseCRL is indistinguishable + * from success while the CRL entry has been discarded. */ +int test_wolfSSL_CRL_unknown_ext_cb_positive_return_fails_load(void) +{ + EXPECT_DECLS; +#if !defined(NO_CERTS) && defined(HAVE_CRL) && !defined(NO_RSA) && \ + !defined(NO_FILESYSTEM) && defined(WC_ASN_UNKNOWN_EXT_CB) + WOLFSSL_CERT_MANAGER* cm = NULL; + CRLUnkExtCtx ctx = { 0, 0, 0 }; + int rc; + + ExpectNotNull(cm = wolfSSL_CertManagerNew()); + ExpectIntEQ(wolfSSL_CertManagerLoadCA(cm, + "./certs/crl/extra-crls/claim-root.pem", NULL), WOLFSSL_SUCCESS); + ExpectIntEQ(wolfSSL_CertManagerEnableCRL(cm, WOLFSSL_CRL_CHECKALL), + WOLFSSL_SUCCESS); + + ExpectIntEQ(wolfSSL_CertManagerSetCRLUnknownExtCallbackEx(cm, + crl_unk_ext_cb_reject_positive, &ctx), WOLFSSL_SUCCESS); + + rc = wolfSSL_CertManagerLoadCRLFile(cm, + "./certs/crl/extra-crls/crl_critical_entry.pem", WOLFSSL_FILETYPE_PEM); + ExpectIntNE(rc, WOLFSSL_SUCCESS); + ExpectIntLT(rc, 0); + ExpectIntGT(ctx.calls, 0); + + wolfSSL_CertManagerFree(cm); +#endif + return EXPECT_RESULT(); +} + int test_wolfSSL_CertManagerCheckOCSPResponse(void) { EXPECT_DECLS; diff --git a/tests/api/test_certman.h b/tests/api/test_certman.h index 35117601452..54570d698ff 100644 --- a/tests/api/test_certman.h +++ b/tests/api/test_certman.h @@ -48,6 +48,9 @@ int test_wolfSSL_CRL_duplicate_extensions(void); int test_wolfSSL_CRL_critical_idp(void); int test_wolfSSL_CRL_unknown_critical_ext(void); int test_wolfSSL_CRL_unknown_critical_entry_ext(void); +int test_wolfSSL_CRL_unknown_ext_cb_rescues_critical_entry_ext(void); +int test_wolfSSL_CRL_unknown_ext_cb_rescues_critical_crl_ext(void); +int test_wolfSSL_CRL_unknown_ext_cb_positive_return_fails_load(void); int test_wolfSSL_CertManagerCheckOCSPResponse(void); int test_various_pathlen_chains(void); int test_wolfSSL_CertManagerRejectMD5Cert(void); @@ -82,6 +85,12 @@ int test_wolfSSL_CertManagerNameConstraint_skid_disambiguates(void); TEST_DECL_GROUP("certman", test_wolfSSL_CRL_critical_idp), \ TEST_DECL_GROUP("certman", test_wolfSSL_CRL_unknown_critical_ext), \ TEST_DECL_GROUP("certman", test_wolfSSL_CRL_unknown_critical_entry_ext), \ + TEST_DECL_GROUP("certman", \ + test_wolfSSL_CRL_unknown_ext_cb_rescues_critical_entry_ext), \ + TEST_DECL_GROUP("certman", \ + test_wolfSSL_CRL_unknown_ext_cb_rescues_critical_crl_ext), \ + TEST_DECL_GROUP("certman", \ + test_wolfSSL_CRL_unknown_ext_cb_positive_return_fails_load), \ TEST_DECL_GROUP("certman", test_wolfSSL_CertManagerCheckOCSPResponse), \ TEST_DECL_GROUP("certman", test_various_pathlen_chains), \ TEST_DECL_GROUP("certman", test_wolfSSL_CertManagerRejectMD5Cert), \ diff --git a/wolfcrypt/src/asn.c b/wolfcrypt/src/asn.c index cbe149b91de..981a6a24df0 100644 --- a/wolfcrypt/src/asn.c +++ b/wolfcrypt/src/asn.c @@ -22125,6 +22125,7 @@ int wc_SetUnknownExtCallbackEx(DecodedCert* cert, cert->unknownExtCallbackExCtx = ctx; return 0; } + #endif /* WC_ASN_UNKNOWN_EXT_CB */ /* @@ -36781,10 +36782,15 @@ static const byte crlReasonOid[] = { 0x55, 0x1d, 0x15 }; /* Parse CRL entry extensions. * Extracts the reason code into *reasonCode if the CRL Reason extension * is present. Per RFC 5280 Section 5.3, returns ASN_CRIT_EXT_E if any - * unknown extension is marked critical. Returns 0 on success. */ + * unknown extension is marked critical, unless a registered + * unknownExtCallback on dcrl accepts it. dcrl may be NULL (no callback + * dispatch). Returns 0 on success. */ static int ParseCRL_EntryExtensions(const byte* buff, word32 idx, word32 maxIdx, - int* reasonCode) + int* reasonCode, DecodedCRL* dcrl) { +#ifndef WC_ASN_UNKNOWN_EXT_CB + (void)dcrl; +#endif while (idx < maxIdx) { int len; int oidLen; @@ -36849,11 +36855,46 @@ static int ParseCRL_EntryExtensions(const byte* buff, word32 idx, word32 maxIdx, } } } - else if (critical) { - /* RFC 5280 Section 5.3: reject CRL with unknown critical - * entry extension. */ - WOLFSSL_MSG("Unknown critical CRL entry extension"); - return ASN_CRIT_EXT_E; + else { + int handled = 0; +#ifdef WC_ASN_UNKNOWN_EXT_CB + if (dcrl != NULL && (dcrl->unknownExtCallback != NULL || + dcrl->unknownExtCallbackEx != NULL)) { + word16 decOid[MAX_OID_SZ]; + word32 decOidSz = MAX_OID_SZ; + word32 valIdx = idx; + int valLen = 0; + int cbRet; + + if (GetOctetString(buff, &valIdx, &valLen, end) < 0) { + return ASN_PARSE_E; + } + cbRet = DecodeObjectId(buff + oidContent, (word32)oidLen, + decOid, &decOidSz); + if (cbRet == 0 && dcrl->unknownExtCallback != NULL) { + cbRet = dcrl->unknownExtCallback(decOid, decOidSz, + critical, buff + valIdx, (word32)valLen); + } + if (cbRet == 0 && dcrl->unknownExtCallbackEx != NULL) { + cbRet = dcrl->unknownExtCallbackEx(decOid, decOidSz, + critical, buff + valIdx, (word32)valLen, + dcrl->unknownExtCallbackExCtx); + } + if (cbRet != 0) { + /* Must stay negative: BufferLoadCRL converts its result + * with "ret ? ret : WOLFSSL_SUCCESS", so a positive + * callback return would collide with WOLFSSL_SUCCESS. */ + return (cbRet < 0) ? cbRet : ASN_PARSE_E; + } + handled = 1; + } +#endif + if (!handled && critical) { + /* RFC 5280 Section 5.3: reject CRL with unknown critical + * entry extension. */ + WOLFSSL_MSG("Unknown critical CRL entry extension"); + return ASN_CRIT_EXT_E; + } } idx = end; } @@ -36870,7 +36911,7 @@ WOLFSSL_TEST_VIS int wc_ParseCRLReasonFromExtensions(const byte* ext, return BAD_FUNC_ARG; } - return ParseCRL_EntryExtensions(ext, 0, extSz, reasonCode); + return ParseCRL_EntryExtensions(ext, 0, extSz, reasonCode, NULL); } #endif @@ -36959,7 +37000,7 @@ static int GetRevoked(RevokedCert* rcert, const byte* buff, word32* idx, #endif ret = ParseCRL_EntryExtensions(buff, extOff, extEnd, - &rc->reasonCode); + &rc->reasonCode, dcrl); } } @@ -37279,9 +37320,48 @@ static int ParseCRL_Extensions(DecodedCRL* dcrl, const byte* buf, word32 idx, mp_free(m); FREE_MP_INT_SIZE(m, NULL, DYNAMIC_TYPE_TMP_BUFFER); } - else if (critical) { - WOLFSSL_MSG("Unknown critical CRL extension"); - ret = ASN_CRIT_EXT_E; + else { + /* Unknown extension OID. Give the caller a chance to + * accept it via the registered callback; otherwise the + * historical strict behavior (reject if critical) is + * preserved. */ + int handled = 0; +#ifdef WC_ASN_UNKNOWN_EXT_CB + if (dcrl->unknownExtCallback != NULL || + dcrl->unknownExtCallbackEx != NULL) { + word16 decOid[MAX_OID_SZ]; + word32 decOidSz = MAX_OID_SZ; + ret = DecodeObjectId( + dataASN[CERTEXTASN_IDX_OID].data.oid.data, + dataASN[CERTEXTASN_IDX_OID].data.oid.length, + decOid, &decOidSz); + if (ret == 0 && dcrl->unknownExtCallback != NULL) { + ret = dcrl->unknownExtCallback(decOid, decOidSz, + critical, + dataASN[CERTEXTASN_IDX_VAL].data.buffer.data, + dataASN[CERTEXTASN_IDX_VAL].length); + } + if (ret == 0 && dcrl->unknownExtCallbackEx != NULL) { + ret = dcrl->unknownExtCallbackEx(decOid, decOidSz, + critical, + dataASN[CERTEXTASN_IDX_VAL].data.buffer.data, + dataASN[CERTEXTASN_IDX_VAL].length, + dcrl->unknownExtCallbackExCtx); + } + if (ret > 0) { + /* Must stay negative: BufferLoadCRL converts its + * result with "ret ? ret : WOLFSSL_SUCCESS", so a + * positive callback return would collide with + * WOLFSSL_SUCCESS. */ + ret = ASN_PARSE_E; + } + handled = 1; + } +#endif + if (!handled && critical) { + WOLFSSL_MSG("Unknown critical CRL extension"); + ret = ASN_CRIT_EXT_E; + } } } /* Move index on to next extension. */ diff --git a/wolfssl/internal.h b/wolfssl/internal.h index 38e5015d0d8..1e3b31c0185 100644 --- a/wolfssl/internal.h +++ b/wolfssl/internal.h @@ -2706,6 +2706,11 @@ struct WOLFSSL_CERT_MANAGER { #endif #ifdef WC_ASN_UNKNOWN_EXT_CB wc_UnknownExtCallback unknownExtCallback; +#if defined(HAVE_CRL) + wc_UnknownExtCallback crlUnknownExtCallback; + wc_UnknownExtCallbackEx crlUnknownExtCallbackEx; + void* crlUnknownExtCallbackExCtx; +#endif #endif #ifdef HAVE_CRL_UPDATE_CB CbUpdateCRL cbUpdateCRL; /* notify thru cb that crl has updated */ diff --git a/wolfssl/ssl.h b/wolfssl/ssl.h index b71810d3181..2cf62c4097f 100644 --- a/wolfssl/ssl.h +++ b/wolfssl/ssl.h @@ -4474,6 +4474,24 @@ WOLFSSL_API void wolfSSL_CTX_SetPerformTlsRecordProcessingCb(WOLFSSL_CTX* ctx, WOLFSSL_API void wolfSSL_CertManagerSetUnknownExtCallback( WOLFSSL_CERT_MANAGER* cm, wc_UnknownExtCallback cb); +#if defined(HAVE_CRL) + /* Register a callback invoked for each CRL extension (CRL-level and + * revoked-entry) whose OID the parser does not recognize, critical or + * not. Returning 0 accepts the extension; a non-zero return rejects + * the CRL and fails the load. + * + * The callback runs on pre-signature-verification data: extensions are + * parsed before the CRL signature is checked, so callback input is + * attacker-controlled even for CRLs that ultimately fail verification. + * Callbacks must not trust the bytes they're handed. */ + WOLFSSL_API int wolfSSL_CertManagerSetCRLUnknownExtCallback( + WOLFSSL_CERT_MANAGER* cm, + wc_UnknownExtCallback cb); + WOLFSSL_API int wolfSSL_CertManagerSetCRLUnknownExtCallbackEx( + WOLFSSL_CERT_MANAGER* cm, + wc_UnknownExtCallbackEx cb, + void* ctx); +#endif #endif WOLFSSL_API int wolfSSL_CertManagerLoadCA(WOLFSSL_CERT_MANAGER* cm, diff --git a/wolfssl/wolfcrypt/asn.h b/wolfssl/wolfcrypt/asn.h index bd380e69ba4..8259f0861f9 100644 --- a/wolfssl/wolfcrypt/asn.h +++ b/wolfssl/wolfcrypt/asn.h @@ -3254,6 +3254,11 @@ struct DecodedCRL { WC_BITFIELD extAuthKeyIdSet:1; /* Auth key identifier set indicator */ #endif WC_BITFIELD crlNumberSet:1; /* CRL number set indicator */ +#ifdef WC_ASN_UNKNOWN_EXT_CB + wc_UnknownExtCallback unknownExtCallback; + wc_UnknownExtCallbackEx unknownExtCallbackEx; + void* unknownExtCallbackExCtx; +#endif }; WOLFSSL_LOCAL void InitDecodedCRL(DecodedCRL* dcrl, void* heap); From 7ea5bb6f767516e11ec2960179cdfdaee7578bf1 Mon Sep 17 00:00:00 2001 From: Anthony Hu Date: Tue, 21 Jul 2026 17:08:16 -0400 Subject: [PATCH 2/3] Set to NULL for non-template ASN --- wolfcrypt/src/asn_orig.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/wolfcrypt/src/asn_orig.c b/wolfcrypt/src/asn_orig.c index 6eab2b6041d..c33e6d0e27c 100644 --- a/wolfcrypt/src/asn_orig.c +++ b/wolfcrypt/src/asn_orig.c @@ -9192,7 +9192,7 @@ static int GetRevoked(RevokedCert* rcert, const byte* buff, word32* idx, #endif ret = ParseCRL_EntryExtensions(buff, seqIdx, extEnd, - &rc->reasonCode); + &rc->reasonCode, NULL); if (ret != 0) { #if defined(OPENSSL_EXTRA) XFREE(rc->extensions, dcrl->heap, DYNAMIC_TYPE_REVOKED); From 73f1a7b66537272d652130830a4282b4d6b74548 Mon Sep 17 00:00:00 2001 From: Anthony Hu Date: Fri, 24 Jul 2026 14:34:17 -0400 Subject: [PATCH 3/3] More tests --- tests/api/test_certman.c | 89 ++++++++++++++++++++++++++++++++++++++++ tests/api/test_certman.h | 3 ++ 2 files changed, 92 insertions(+) diff --git a/tests/api/test_certman.c b/tests/api/test_certman.c index 4a959598933..a46a2028ea8 100644 --- a/tests/api/test_certman.c +++ b/tests/api/test_certman.c @@ -3062,6 +3062,36 @@ static int crl_unk_ext_cb_reject_positive(const word16* oid, word32 oidSz, ((CRLUnkExtCtx*)ctxIn)->calls++; return 1; } + +/* The non-Ex wc_UnknownExtCallback carries no context pointer, so the + * callbacks registered through wolfSSL_CertManagerSetCRLUnknownExtCallback() + * report what they saw through these file-static counters. The exercising + * test resets them before each load. */ +static int crl_unk_ext_noctx_calls; +static int crl_unk_ext_noctx_sawCritical; +static int crl_unk_ext_noctx_oidMatched; + +static int crl_unk_ext_cb_noctx_accept(const word16* oid, word32 oidSz, + int crit, const unsigned char* der, word32 derSz) +{ + (void)der; + (void)derSz; + crl_unk_ext_noctx_calls++; + if (crit) + crl_unk_ext_noctx_sawCritical = 1; + /* Expect OID arc starts at {2, 5, 29, ...}. */ + if (oidSz >= 4 && oid[0] == 2 && oid[1] == 5 && oid[2] == 29) + crl_unk_ext_noctx_oidMatched = 1; + return 0; /* accept */ +} + +static int crl_unk_ext_cb_noctx_reject(const word16* oid, word32 oidSz, + int crit, const unsigned char* der, word32 derSz) +{ + (void)oid; (void)oidSz; (void)crit; (void)der; (void)derSz; + crl_unk_ext_noctx_calls++; + return ASN_PARSE_E; +} #endif /* Callback rescues a critical unknown CRL ENTRY extension that would @@ -3186,6 +3216,65 @@ int test_wolfSSL_CRL_unknown_ext_cb_positive_return_fails_load(void) return EXPECT_RESULT(); } +/* Exercises the context-free entry point + * wolfSSL_CertManagerSetCRLUnknownExtCallback() (as opposed to the ...Ex() + * variant covered by the tests above). Confirms NULL-argument rejection, + * that an accepting callback rescues a critical unknown CRL-level extension, + * and that a rejecting callback fails the load. */ +int test_wolfSSL_CRL_unknown_ext_cb_noctx(void) +{ + EXPECT_DECLS; +#if !defined(NO_CERTS) && defined(HAVE_CRL) && !defined(NO_RSA) && \ + !defined(NO_FILESYSTEM) && defined(WC_ASN_UNKNOWN_EXT_CB) + WOLFSSL_CERT_MANAGER* cm = NULL; + + /* A NULL cert manager is rejected by both registration entry points. */ + ExpectIntEQ(wolfSSL_CertManagerSetCRLUnknownExtCallback(NULL, + crl_unk_ext_cb_noctx_accept), BAD_FUNC_ARG); + ExpectIntEQ(wolfSSL_CertManagerSetCRLUnknownExtCallbackEx(NULL, + crl_unk_ext_cb_accept, NULL), BAD_FUNC_ARG); + + /* Accepting callback: the critical unknown CRL-level extension in the + * crl_obsolete_critical fixture is rescued and the load succeeds. The + * callback must have seen the critical unknown OID. */ + crl_unk_ext_noctx_calls = 0; + crl_unk_ext_noctx_sawCritical = 0; + crl_unk_ext_noctx_oidMatched = 0; + ExpectNotNull(cm = wolfSSL_CertManagerNew()); + ExpectIntEQ(wolfSSL_CertManagerLoadCABuffer(cm, crl_unk_ca_der, + sizeof(crl_unk_ca_der), WOLFSSL_FILETYPE_ASN1), WOLFSSL_SUCCESS); + ExpectIntEQ(wolfSSL_CertManagerEnableCRL(cm, WOLFSSL_CRL_CHECKALL), + WOLFSSL_SUCCESS); + ExpectIntEQ(wolfSSL_CertManagerSetCRLUnknownExtCallback(cm, + crl_unk_ext_cb_noctx_accept), WOLFSSL_SUCCESS); + ExpectIntEQ(wolfSSL_CertManagerLoadCRLBuffer(cm, crl_obsolete_critical, + sizeof(crl_obsolete_critical), WOLFSSL_FILETYPE_ASN1), + WOLFSSL_SUCCESS); + ExpectIntGT(crl_unk_ext_noctx_calls, 0); + ExpectIntEQ(crl_unk_ext_noctx_sawCritical, 1); + ExpectIntEQ(crl_unk_ext_noctx_oidMatched, 1); + wolfSSL_CertManagerFree(cm); + cm = NULL; + + /* Rejecting callback: the same CRL must fail to load. */ + crl_unk_ext_noctx_calls = 0; + ExpectNotNull(cm = wolfSSL_CertManagerNew()); + ExpectIntEQ(wolfSSL_CertManagerLoadCABuffer(cm, crl_unk_ca_der, + sizeof(crl_unk_ca_der), WOLFSSL_FILETYPE_ASN1), WOLFSSL_SUCCESS); + ExpectIntEQ(wolfSSL_CertManagerEnableCRL(cm, WOLFSSL_CRL_CHECKALL), + WOLFSSL_SUCCESS); + ExpectIntEQ(wolfSSL_CertManagerSetCRLUnknownExtCallback(cm, + crl_unk_ext_cb_noctx_reject), WOLFSSL_SUCCESS); + ExpectIntNE(wolfSSL_CertManagerLoadCRLBuffer(cm, crl_obsolete_critical, + sizeof(crl_obsolete_critical), WOLFSSL_FILETYPE_ASN1), + WOLFSSL_SUCCESS); + ExpectIntGT(crl_unk_ext_noctx_calls, 0); + + wolfSSL_CertManagerFree(cm); +#endif + return EXPECT_RESULT(); +} + int test_wolfSSL_CertManagerCheckOCSPResponse(void) { EXPECT_DECLS; diff --git a/tests/api/test_certman.h b/tests/api/test_certman.h index 54570d698ff..6575a9e0b0a 100644 --- a/tests/api/test_certman.h +++ b/tests/api/test_certman.h @@ -51,6 +51,7 @@ int test_wolfSSL_CRL_unknown_critical_entry_ext(void); int test_wolfSSL_CRL_unknown_ext_cb_rescues_critical_entry_ext(void); int test_wolfSSL_CRL_unknown_ext_cb_rescues_critical_crl_ext(void); int test_wolfSSL_CRL_unknown_ext_cb_positive_return_fails_load(void); +int test_wolfSSL_CRL_unknown_ext_cb_noctx(void); int test_wolfSSL_CertManagerCheckOCSPResponse(void); int test_various_pathlen_chains(void); int test_wolfSSL_CertManagerRejectMD5Cert(void); @@ -91,6 +92,8 @@ int test_wolfSSL_CertManagerNameConstraint_skid_disambiguates(void); test_wolfSSL_CRL_unknown_ext_cb_rescues_critical_crl_ext), \ TEST_DECL_GROUP("certman", \ test_wolfSSL_CRL_unknown_ext_cb_positive_return_fails_load), \ + TEST_DECL_GROUP("certman", \ + test_wolfSSL_CRL_unknown_ext_cb_noctx), \ TEST_DECL_GROUP("certman", test_wolfSSL_CertManagerCheckOCSPResponse), \ TEST_DECL_GROUP("certman", test_various_pathlen_chains), \ TEST_DECL_GROUP("certman", test_wolfSSL_CertManagerRejectMD5Cert), \