Found an issue where mbedTLS silently drops all X.509 extensions when the [3] EXPLICIT wrapper tag is changed to [1] or [2]. Tested on mbedTLS 2.28.
RFC 5280 §4.1.2.9 says the extensions field must use tag 0xa3 ([3] EXPLICIT). If you take a valid cert and change just that one byte to 0xa1 (issuerUniqueID tag) or 0xa2 (subjectUniqueID tag), mbedTLS accepts the cert but produces a mbedtls_x509_crt with:
ext_types == 0
ca_istrue == 0
key_usage == 0
max_pathlen == 0
All critical constraints just disappear. The cert DER bytes are still there but the parsed structure reports no extensions.
The bug: In library/x509_crt.c::x509_get_crt_ext(), when the tag isn't [3], the function treats it as "extensions absent" and returns success:
if( ( ret = mbedtls_asn1_get_tag( p, end, &len,
MBEDTLS_ASN1_CONTEXT_SPECIFIC | MBEDTLS_ASN1_CONSTRUCTED | 3 ) ) != 0 )
{
if( ret == MBEDTLS_ERR_ASN1_UNEXPECTED_TAG )
return( 0 ); /* ← silently skips on wrong wrapper tag */
...
}
The OPTIONAL-skip on UNEXPECTED_TAG is correct when extensions are genuinely absent, but wrong when a different context-specific tag is present — that's a malformed cert, not an optional field.
12 other implementations reject it:
OpenSSL 3.x, OpenSSL 4.x, LibreSSL, BoringSSL, GnuTLS, wolfSSL, Botan, NSS, ANSSI x509-parser, rusticata x509-parser, rustls-webpki, briansmith webpki — all reject with parse errors. Only mbedTLS and BearSSL accept.
Reproduction:
# Generate a cert with critical extensions
openssl req -x509 -newkey rsa:2048 -keyout /tmp/k.pem -out /tmp/c.pem \
-days 365 -nodes -subj "/CN=Test" -sha256 \
-addext "keyUsage=critical,digitalSignature" \
-addext "basicConstraints=critical,CA:FALSE"
openssl x509 -in /tmp/c.pem -outform DER -out /tmp/base.der
# Mutate the extensions wrapper tag 0xa3 → 0xa2
python3 -c "
data = bytearray(open('/tmp/base.der','rb').read())
for i in range(len(data) - 2):
if data[i] == 0xa3 and data[i+2] == 0x30:
data[i] = 0xa2
break
open('/tmp/poc.der','wb').write(data)
"
# mbedTLS accepts (ext_types=0), everyone else rejects
Fix: Only accept the optional-skip when there's genuinely no more data:
- if( ret == MBEDTLS_ERR_ASN1_UNEXPECTED_TAG )
- return( 0 );
+ if( ret == MBEDTLS_ERR_ASN1_UNEXPECTED_TAG && *p == end )
+ return( 0 ); /* genuinely absent */
+ if( ret == MBEDTLS_ERR_ASN1_UNEXPECTED_TAG )
+ return( MBEDTLS_ERROR_ADD( MBEDTLS_ERR_X509_INVALID_EXTENSIONS, ret ) );
Found through 14-implementation differential fuzzing (PathDiff diff_fuzz_v2). BearSSL has the same bug; I'll file separately with them.
Found an issue where mbedTLS silently drops all X.509 extensions when the
[3] EXPLICITwrapper tag is changed to[1]or[2]. Tested on mbedTLS 2.28.RFC 5280 §4.1.2.9 says the extensions field must use tag
0xa3([3] EXPLICIT). If you take a valid cert and change just that one byte to0xa1(issuerUniqueID tag) or0xa2(subjectUniqueID tag), mbedTLS accepts the cert but produces ambedtls_x509_crtwith:ext_types == 0ca_istrue == 0key_usage == 0max_pathlen == 0All critical constraints just disappear. The cert DER bytes are still there but the parsed structure reports no extensions.
The bug: In
library/x509_crt.c::x509_get_crt_ext(), when the tag isn't[3], the function treats it as "extensions absent" and returns success:The OPTIONAL-skip on UNEXPECTED_TAG is correct when extensions are genuinely absent, but wrong when a different context-specific tag is present — that's a malformed cert, not an optional field.
12 other implementations reject it:
OpenSSL 3.x, OpenSSL 4.x, LibreSSL, BoringSSL, GnuTLS, wolfSSL, Botan, NSS, ANSSI x509-parser, rusticata x509-parser, rustls-webpki, briansmith webpki — all reject with parse errors. Only mbedTLS and BearSSL accept.
Reproduction:
Fix: Only accept the optional-skip when there's genuinely no more data:
Found through 14-implementation differential fuzzing (PathDiff diff_fuzz_v2). BearSSL has the same bug; I'll file separately with them.