Skip to content

X.509 extensions wrapper tag mutation silently strips all certificate extensions #10774

Description

@afldl

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No fields configured for Bug.

    Projects

    Status
    No status

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions