Reject zero-length serial number in mbedtls_x509write_crt - #10826
Open
94xhn wants to merge 1 commit into
Open
Conversation
mbedtls_x509write_crt_set_serial_raw() only checked the upper bound
on serial_len (rejecting anything longer than
MBEDTLS_X509_RFC5280_MAX_SERIAL_LEN) but silently accepted
serial_len == 0. That's also the state a freshly-initialized
mbedtls_x509write_cert context is left in if the setter is never
called at all, since mbedtls_x509write_crt_init() zeroes the whole
struct.
mbedtls_x509write_crt_der() then encodes ctx->serial/ctx->serial_len
directly as an ASN.1 INTEGER:
MBEDTLS_ASN1_CHK_ADD(len, mbedtls_asn1_write_raw_buffer(&c, buf,
ctx->serial, ctx->serial_len));
if (*c & 0x80) {
...
} else {
MBEDTLS_ASN1_CHK_ADD(len, mbedtls_asn1_write_len(&c, buf,
ctx->serial_len));
}
MBEDTLS_ASN1_CHK_ADD(len, mbedtls_asn1_write_tag(&c, buf,
MBEDTLS_ASN1_INTEGER));
With serial_len == 0, mbedtls_asn1_write_raw_buffer(..., 0) is a
no-op: it decrements nothing and returns 0, leaving the cursor `c`
untouched. The following `*c & 0x80` test then reads whatever byte
was written immediately before the serial field - in practice the
tag byte of the just-written Signature AlgorithmIdentifier SEQUENCE
(0x30, high bit clear) - so the `else` branch always runs, writing a
length octet of 0x00 followed by the INTEGER tag 0x02. The
CertificateSerialNumber field is thus encoded as the two bytes
`02 00`.
Per X.690 8.3.1, an INTEGER's contents "shall consist of one or more
octets"; `02 00` has zero content octets and is not a valid DER
encoding at all. Confirmed independently with
`openssl asn1parse -inform DER` on the standalone 2 bytes `02 00`,
which reports "BAD INTEGER". mbedtls's own parser
(mbedtls_x509_get_serial() in library/x509.c) has no length check
either and round-trips it silently, but other, stricter X.509/ASN.1
parsers reject certificates built this way.
Reject serial_len == 0 in mbedtls_x509write_crt_set_serial_raw(),
and add the same check directly in mbedtls_x509write_crt_der() as a
defense-in-depth measure for the case where the setter was never
called at all.
Verified with a standalone host-C program that copies the exact
mbedtls_asn1_write_len()/mbedtls_asn1_write_tag()/
mbedtls_asn1_write_raw_buffer() bodies and the vulnerable snippet
from mbedtls_x509write_crt_der() verbatim: with serial_len == 0 it
deterministically produces `02 00` (confirmed BAD by openssl
asn1parse), while serial_len == 1 with content byte 0x00 correctly
produces the valid `02 01 00`, and a normal multi-byte serial is
unaffected.
Signed-off-by: yi chen <94xhn1@gmail.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
mbedtls_x509write_crt_set_serial_raw()only checks the upper bound onserial_len(rejecting anything longer thanMBEDTLS_X509_RFC5280_MAX_SERIAL_LEN), but silently acceptsserial_len == 0:serial_len == 0is also exactly the state a freshly-initializedmbedtls_x509write_certis left in if the setter is never called at all,since
mbedtls_x509write_crt_init()justmemsets the whole struct to 0.The bug
mbedtls_x509write_crt_der()encodesctx->serial/ctx->serial_lendirectly as an ASN.1 INTEGER:
mbedtls_asn1_write_raw_buffer(&c, buf, ctx->serial, 0)is a no-op for azero-length input (
tf-psa-crypto/utilities/asn1write.c):So with
serial_len == 0, the cursorcnever moves, and the following*c & 0x80test reads whatever byte happens to already sit at thatbuffer position — in practice the tag byte of the Signature
AlgorithmIdentifierSEQUENCEthatmbedtls_x509write_crt_der()always writes immediately before the serial field
(
mbedtls_asn1_write_algorithm_identifier_ext()always ends by writingtag
0x30, whose top bit is clear). Theelsebranch therefore alwaysruns deterministically: it writes a length octet of
0x00, then theINTEGERtag0x02. TheCertificateSerialNumberfield ends upencoded as the two bytes
02 00.Per X.690 §8.3.1, an
INTEGER's contents "shall consist of one or moreoctets" —
02 00has zero content octets and is not a valid DER (oreven BER) encoding. I confirmed this independently:
mbedtls's own parser side,
mbedtls_x509_get_serial()inlibrary/x509.c, has no length check either and will happily re-parsethis malformed field, so the round-trip inside mbedtls is silent —
but a certificate generated this way can be rejected by other, stricter
X.509/ASN.1 parsers (as demonstrated above with OpenSSL's own decoder).
Fix
serial_len == 0inmbedtls_x509write_crt_set_serial_raw(),alongside the existing upper-bound check.
ctx->serial_len == 0check directly inmbedtls_x509write_crt_der(), as defense-in-depth for the case wherethe setter is never called at all (the struct's zeroed-by-
init()default state).
mbedtls_x509write_crt_set_serial_raw().x509_set_serial_checkintests/suites/test_suite_x509write.functioncovering thezero-length-serial rejection (the existing test there only covered the
too-long case).
ChangeLog.dentry.Testing / verification
I don't have the test suite building locally for this repo right now —
tf-psa-cryptoandframeworkare git submodules that aren'tinitialized in a plain checkout, and initializing + configuring the full
CMake build was out of scope for verifying this specific encoding bug.
Instead, I extracted the exact vulnerable logic into a standalone,
dependency-free host-C reproduction: the three real ASN.1-writing
primitives (
mbedtls_asn1_write_len,mbedtls_asn1_write_tag,mbedtls_asn1_write_raw_buffer, copied verbatim fromtf-psa-crypto/utilities/asn1write.c) plus a line-for-line copy of thevulnerable serial-writing snippet from
x509write_crt.c(including thepreceding
0x30tag write that stands in for the real, always-presentAlgorithmIdentifierSEQUENCEtag byte).Results (
gcc -Wall -Wextra, compiles clean, run on Windows/MinGW):This confirms: (a) the bug is 100% deterministic given the real call
sequence in
mbedtls_x509write_crt_der(), not a rare edge case; (b) acorrect zero-valued serial (
serial_len == 1, content byte0x00) isencoded correctly and is unaffected by rejecting
serial_len == 0specifically; (c) ordinary multi-byte serials are unaffected. I also
manually traced
mbedtls_x509_get_serial()(library/x509.c) to confirmit has no corresponding length check, so the round-trip-within-mbedtls
silence is real and not masked elsewhere.
Happy to add a build-verified
test_suite_x509writerun if someone canconfirm the preferred way to stand up the
tf-psa-crypto/frameworksubmodules in CI for a one-off check, or if a maintainer can run it
against this branch.
Generative AI
I used generative AI tools (Claude) when researching and drafting this
PR, but a human has reviewed the code and reasoning and is responsible
for the content of this PR.