From aefd3d54c187640049e9fa05b2d12276db3f2099 Mon Sep 17 00:00:00 2001 From: Eric Blankenhorn Date: Thu, 10 Sep 2026 13:35:51 -0500 Subject: [PATCH 1/4] Fix non-minimal DER encoding of caller-supplied certificate serials --- doc/dox_comments/header_files/asn_public.h | 14 ++ tests/api/test_asn.c | 218 +++++++++++++++++++++ tests/api/test_asn.h | 2 + tests/unit-mcdc/test_asn_cert_whitebox.c | 28 +++ wolfcrypt/src/asn.c | 34 +++- 5 files changed, 294 insertions(+), 2 deletions(-) diff --git a/doc/dox_comments/header_files/asn_public.h b/doc/dox_comments/header_files/asn_public.h index 2592e5cd005..87bcea22452 100644 --- a/doc/dox_comments/header_files/asn_public.h +++ b/doc/dox_comments/header_files/asn_public.h @@ -115,12 +115,22 @@ void wc_CertFree(Cert* cert); either an rsaKey or an eccKey to generate the certificate. The certificate must be initialized with wc_InitCert before this method is called. + A serial number left at the wc_InitCert default (cert->serialSz of 0) is + randomly generated. A caller-supplied serial is taken as a big-endian + magnitude of at most CTC_SERIAL_SIZE bytes and is encoded as a minimal DER + INTEGER, so any leading zero bytes of a fixed-width serial are dropped and + the sign pad is added when needed. Do not prepend a sign pad by hand. + \return Success On successfully making an x509 certificate from the specified input cert, returns the size of the cert generated. \return MEMORY_E Returned if there is an error allocating memory with XMALLOC \return BUFFER_E Returned if the provided derBuffer is too small to store the generated certificate + \return BAD_FUNC_ARG Returned if cert->serialSz is negative or larger + than CTC_SERIAL_SIZE, or if the serial number is zero. RFC 5280 4.1.2.2 + requires a positive serial; define WOLFSSL_ASN_ALLOW_0_SERIAL to permit + a zero serial. \return Others Additional error messages may be returned if the cert generation is not successful. @@ -156,9 +166,13 @@ int wc_MakeCert(Cert* cert, byte* derBuffer, word32 derSz, RsaKey* rsaKey, \ingroup ASN \brief Makes certificate with generic key type support. + The serial number contract is the same as wc_MakeCert(). + \return Size of certificate on success \return MEMORY_E if memory allocation fails \return BUFFER_E if buffer too small + \return BAD_FUNC_ARG if the serial number is zero, negative in size, or + longer than CTC_SERIAL_SIZE \return Other error codes on failure \param cert Initialized cert structure diff --git a/tests/api/test_asn.c b/tests/api/test_asn.c index 9905c380bf5..5e8d5f98f0c 100644 --- a/tests/api/test_asn.c +++ b/tests/api/test_asn.c @@ -4330,6 +4330,224 @@ int test_wc_MakeCert_generalizedTimeValidity(void) return EXPECT_RESULT(); } +#if defined(TEST_SIGN_CERT_BOUNDS_RSA) || defined(TEST_SIGN_CERT_BOUNDS_ECC) +#define TEST_MAKECERT_SERIAL +#endif + +#ifdef TEST_MAKECERT_SERIAL +/* Longest serialNumber TLV the cases below expect. */ +#define TEST_SERIAL_TLV_MAX 8 + +/* Find the serialNumber TLV in a certificate body. wc_MakeCert() writes a + * TBSCertificate, so the [0] EXPLICIT version precedes the serial. */ +static const byte* test_cert_serial_tlv(const byte* body, int bodySz) +{ + int i; + + for (i = 0; ((i + 5 + TEST_SERIAL_TLV_MAX) <= bodySz) && (i < 16); i++) { + if ((body[i] == 0xA0) && (body[i + 1] == 0x03) && + (body[i + 2] == ASN_INTEGER) && (body[i + 3] == 0x01) && + (body[i + 5] == ASN_INTEGER)) { + return &body[i + 5]; + } + } + + return NULL; +} + +/* Build a certificate body carrying the given serial. */ +static int test_cert_make_serial(Cert* cert, WC_RNG* rng, void* key, + byte* out, word32 outSz, const byte* serial, int serialSz) +{ + int ret; + int copySz = serialSz; + + ret = wc_InitCert(cert); + if (ret != 0) + return ret; + + /* Keep the copy inside cert->serial so an over-long size can still be + * handed to the generator to exercise its bound check. */ + if (copySz > CTC_SERIAL_SIZE) + copySz = CTC_SERIAL_SIZE; + if (copySz < 0) + copySz = 0; + + cert->isCA = 0; + XMEMCPY(cert->serial, serial, (size_t)copySz); + cert->serialSz = serialSz; + XSTRNCPY(cert->subject.country, "US", CTC_NAME_SIZE); + XSTRNCPY(cert->subject.org, "wolfSSL", CTC_NAME_SIZE); + XSTRNCPY(cert->subject.commonName, "serial-encoding", CTC_NAME_SIZE); + +#ifdef TEST_SIGN_CERT_BOUNDS_RSA + cert->sigType = CTC_SHA256wRSA; + return wc_MakeCert(cert, out, outSz, (RsaKey*)key, NULL, rng); +#else + cert->sigType = CTC_SHA256wECDSA; + return wc_MakeCert(cert, out, outSz, NULL, (ecc_key*)key, rng); +#endif +} +#endif /* TEST_MAKECERT_SERIAL */ + +/* + * A caller-supplied serial number must be encoded as a minimal DER INTEGER. + * + * A fixed-width device serial carries leading zero bytes that DER does not + * allow, and the encoder only ever adds the sign pad, so those bytes used to + * reach the certificate untouched. The result failed to parse, here and in + * every other strict decoder. + */ +int test_wc_MakeCert_serial_encoding(void) +{ + EXPECT_DECLS; +#ifdef TEST_MAKECERT_SERIAL + static const struct { + byte serial[CTC_SERIAL_SIZE]; + int serialSz; + byte expect[TEST_SERIAL_TLV_MAX]; + int expectSz; + } cases[] = { + /* Redundant leading zeros are dropped. */ + { { 0x00, 0x00, 0x00, 0x01 }, 4, { 0x02, 0x01, 0x01 }, 3 }, + { { 0x00, 0x00, 0x12, 0x34 }, 4, { 0x02, 0x02, 0x12, 0x34 }, 4 }, + { { 0x00, 0x12, 0x34, 0x56 }, 4, { 0x02, 0x03, 0x12, 0x34, 0x56 }, 5 }, + /* Already minimal, so left alone. */ + { { 0x12, 0x34, 0x56, 0x78 }, 4, + { 0x02, 0x04, 0x12, 0x34, 0x56, 0x78 }, 6 }, + /* A set MSB still gets the sign pad it needs. */ + { { 0x80, 0x00, 0x00, 0x01 }, 4, + { 0x02, 0x05, 0x00, 0x80, 0x00, 0x00, 0x01 }, 7 }, + /* A sign pad the caller already supplied is not doubled. */ + { { 0x00, 0x80, 0x01 }, 3, { 0x02, 0x03, 0x00, 0x80, 0x01 }, 5 } + }; + static const byte zeroSerial[4] = { 0x00, 0x00, 0x00, 0x00 }; + static const byte zeroTlv[3] = { 0x02, 0x01, 0x00 }; + byte maxSerial[CTC_SERIAL_SIZE]; + WC_RNG rng; + Cert cert; + DecodedCert decoded; + byte* body = NULL; + const byte* tlv = NULL; + int rngInit = 0; + int bodySz = 0; + int signedSz = 0; + int i; +#ifdef TEST_SIGN_CERT_BOUNDS_RSA + RsaKey key; + word32 idx = 0; +#else + ecc_key key; + word32 idx = 0; +#endif + int keyInit = 0; + + XMEMSET(&rng, 0, sizeof(rng)); + XMEMSET(&cert, 0, sizeof(cert)); + XMEMSET(&key, 0, sizeof(key)); + + ExpectIntEQ(wc_InitRng(&rng), 0); + if (EXPECT_SUCCESS()) rngInit = 1; + + ExpectNotNull(body = (byte*)XMALLOC(SIGN_CERT_SCRATCH_SZ, HEAP_HINT, + DYNAMIC_TYPE_TMP_BUFFER)); + +#ifdef TEST_SIGN_CERT_BOUNDS_RSA + 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); +#else + ExpectIntEQ(wc_ecc_init_ex(&key, HEAP_HINT, testDevId), 0); + if (EXPECT_SUCCESS()) keyInit = 1; + ExpectIntEQ(wc_EccPrivateKeyDecode(ecc_key_der_256, &idx, &key, + sizeof_ecc_key_der_256), 0); +#endif + + for (i = 0; i < (int)(sizeof(cases) / sizeof(cases[0])); i++) { + ExpectIntGT(bodySz = test_cert_make_serial(&cert, &rng, &key, body, + SIGN_CERT_SCRATCH_SZ, cases[i].serial, cases[i].serialSz), 0); + ExpectNotNull(tlv = test_cert_serial_tlv(body, bodySz)); + if (EXPECT_SUCCESS() && (tlv != NULL)) { + ExpectIntEQ(tlv[1] + 2, cases[i].expectSz); + ExpectIntEQ(XMEMCMP(tlv, cases[i].expect, + (size_t)cases[i].expectSz), 0); + } + + /* What wolfSSL emits, wolfSSL has to be able to parse back. */ + ExpectIntGT(signedSz = wc_SignCert(bodySz, cert.sigType, body, + SIGN_CERT_SCRATCH_SZ, +#ifdef TEST_SIGN_CERT_BOUNDS_RSA + &key, NULL, +#else + NULL, &key, +#endif + &rng), 0); + if (EXPECT_SUCCESS()) { + wc_InitDecodedCert(&decoded, body, (word32)signedSz, HEAP_HINT); + ExpectIntEQ(wc_ParseCert(&decoded, CERT_TYPE, NO_VERIFY, NULL), 0); + wc_FreeDecodedCert(&decoded); + } + } + +#if !defined(WOLFSSL_NO_ASN_STRICT) && !defined(WOLFSSL_PYTHON) && \ + !defined(WOLFSSL_ASN_ALLOW_0_SERIAL) + /* RFC 5280 4.1.2.2 needs a positive serial, so blank silicon has to fail + * at generation rather than mint a certificate nothing will accept. */ + ExpectIntEQ(test_cert_make_serial(&cert, &rng, &key, body, + SIGN_CERT_SCRATCH_SZ, zeroSerial, (int)sizeof(zeroSerial)), + WC_NO_ERR_TRACE(BAD_FUNC_ARG)); + (void)zeroTlv; +#elif defined(WOLFSSL_ASN_TEMPLATE) + /* The permissive build still has to emit the canonical zero encoding. + * The original back end rejects zero regardless of this macro. */ + ExpectIntGT(bodySz = test_cert_make_serial(&cert, &rng, &key, body, + SIGN_CERT_SCRATCH_SZ, zeroSerial, (int)sizeof(zeroSerial)), 0); + ExpectNotNull(tlv = test_cert_serial_tlv(body, bodySz)); + if (EXPECT_SUCCESS() && (tlv != NULL)) { + ExpectIntEQ(XMEMCMP(tlv, zeroTlv, sizeof(zeroTlv)), 0); + } +#else + (void)zeroSerial; + (void)zeroTlv; +#endif + + /* Exactly CTC_SERIAL_SIZE is the largest serial that must be accepted. */ + XMEMSET(maxSerial, 0x11, sizeof(maxSerial)); + ExpectIntGT(bodySz = test_cert_make_serial(&cert, &rng, &key, body, + SIGN_CERT_SCRATCH_SZ, maxSerial, CTC_SERIAL_SIZE), 0); + ExpectNotNull(tlv = test_cert_serial_tlv(body, bodySz)); + if (EXPECT_SUCCESS() && (tlv != NULL)) { + ExpectIntEQ(tlv[1], CTC_SERIAL_SIZE); + } + + /* A negative size is rejected by both ASN back ends. */ + ExpectIntEQ(test_cert_make_serial(&cert, &rng, &key, body, + SIGN_CERT_SCRATCH_SZ, cases[0].serial, -1), + WC_NO_ERR_TRACE(BAD_FUNC_ARG)); + +#ifdef WOLFSSL_ASN_TEMPLATE + /* Over the 20 octet ceiling RFC 5280 4.1.2.2 sets. The original ASN + * back end truncates through SetSerialNumber() instead of erroring. */ + ExpectIntEQ(test_cert_make_serial(&cert, &rng, &key, body, + SIGN_CERT_SCRATCH_SZ, cases[0].serial, CTC_SERIAL_SIZE + 1), + WC_NO_ERR_TRACE(BAD_FUNC_ARG)); +#endif + + XFREE(body, HEAP_HINT, DYNAMIC_TYPE_TMP_BUFFER); + if (keyInit) { +#ifdef TEST_SIGN_CERT_BOUNDS_RSA + wc_FreeRsaKey(&key); +#else + wc_ecc_free(&key); +#endif + } + if (rngInit) + wc_FreeRng(&rng); +#endif /* TEST_MAKECERT_SERIAL */ + return EXPECT_RESULT(); +} + /* * MC/DC wave 2 - decision-targeted negative paths for PKCS#8 wrap/parse * and RSA key decode. Targets argument-check, short-buffer, and diff --git a/tests/api/test_asn.h b/tests/api/test_asn.h index 7d77583124a..03638911d1a 100644 --- a/tests/api/test_asn.h +++ b/tests/api/test_asn.h @@ -50,6 +50,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_MakeCert_generalizedTimeValidity(void); +int test_wc_MakeCert_serial_encoding(void); int test_wc_DecodeKeyUsage_decipherOnly(void); int test_wc_DecodeExtKeyUsage_ssh(void); int test_wc_DecodeExtKeyUsage_ssh_oid_collision(void); @@ -83,6 +84,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_MakeCert_generalizedTimeValidity), \ + TEST_DECL_GROUP("asn", test_wc_MakeCert_serial_encoding), \ TEST_DECL_GROUP("asn", test_wc_DecodeKeyUsage_decipherOnly), \ TEST_DECL_GROUP("asn", test_wc_DecodeExtKeyUsage_ssh), \ TEST_DECL_GROUP("asn", test_wc_DecodeExtKeyUsage_ssh_oid_collision), \ diff --git a/tests/unit-mcdc/test_asn_cert_whitebox.c b/tests/unit-mcdc/test_asn_cert_whitebox.c index 9c52f9e783e..a4b47b3d399 100644 --- a/tests/unit-mcdc/test_asn_cert_whitebox.c +++ b/tests/unit-mcdc/test_asn_cert_whitebox.c @@ -2158,6 +2158,29 @@ static void wb_fill_name(CertName* name, const char* cn) /* Build one fixture. Returns 0 on success. Never asserts on the *content* * of the result beyond "the generator accepted it": the whole point of some * of these shapes is that a strict parser will later reject them. */ +/* Rewrite a freshly built body's serial number to zero. + * + * wc_MakeCert() refuses to emit a zero serial (RFC 5280 4.1.2.2), so a + * zero-serial fixture is built with 0x01 and the single content octet is + * patched here before signing. The encoded length does not change, so the + * signature still covers the whole body. */ +static int wb_patch_zero_serial(byte* der, int derSz) +{ + int i; + + for (i = 0; ((i + 8) <= derSz) && (i < 16); i++) { + if ((der[i] == 0xA0) && (der[i + 1] == 0x03) && + (der[i + 2] == ASN_INTEGER) && (der[i + 3] == 0x01) && + (der[i + 5] == ASN_INTEGER) && (der[i + 6] == 0x01) && + (der[i + 7] == 0x01)) { + der[i + 7] = 0x00; + return 0; + } + } + + return -1; +} + static int wb_make_fixture(WbFix* out, const WbSpec* spec) { Cert* cert; @@ -2220,7 +2243,9 @@ static int wb_make_fixture(WbFix* out, const WbSpec* spec) } if (spec->zeroSerial) { + /* Patched down to 0 after the body is built. */ XMEMSET(cert->serial, 0, sizeof(cert->serial)); + cert->serial[0] = 0x01; cert->serialSz = 1; } else { @@ -2274,6 +2299,9 @@ static int wb_make_fixture(WbFix* out, const WbSpec* spec) ret = 0; } } + if (ret == 0 && spec->zeroSerial) { + ret = wb_patch_zero_serial(out->der, cert->bodySz); + } if (ret == 0) { bodySz = cert->bodySz; ret = wc_SignCert(bodySz, cert->sigType, out->der, WB_FIX_DER_SZ, diff --git a/wolfcrypt/src/asn.c b/wolfcrypt/src/asn.c index ba98f4b7871..826c0d59767 100644 --- a/wolfcrypt/src/asn.c +++ b/wolfcrypt/src/asn.c @@ -31050,6 +31050,8 @@ static int MakeAnyCert(Cert* cert, byte* derBuffer, word32 derSz, int ret = 0; word32 issRawLen = 0; word32 sbjRawLen = 0; + const byte* serialPtr = NULL; + word32 serialLen = 0; byte localBefore[MAX_DATE_SIZE]; byte localAfter[MAX_DATE_SIZE]; @@ -31150,6 +31152,34 @@ static int MakeAnyCert(Cert* cert, byte* derBuffer, word32 derSz, cert->serialSz = CTC_GEN_SERIAL_SZ; ret = GenerateInteger(rng, cert->serial, CTC_GEN_SERIAL_SZ); } + /* Serial has to fit cert->serial, which is the RFC 5280 4.1.2.2 cap. */ + if ((ret == 0) && ((cert->serialSz < 0) || + (cert->serialSz > CTC_SERIAL_SIZE))) { + WOLFSSL_MSG("Serial number size out of range"); + WOLFSSL_ERROR_VERBOSE(BAD_FUNC_ARG); + ret = BAD_FUNC_ARG; + } + if (ret == 0) { + serialPtr = cert->serial; + serialLen = (word32)cert->serialSz; + /* DER requires the minimum number of octets, so drop the redundant + * leading zeros a caller-supplied fixed-width serial carries. Parsed + * serials are already minimal unless WOLFSSL_ASN_INT_LEAD_0_ANY. */ + while ((serialLen > 1) && (serialPtr[0] == 0)) { + serialLen--; + serialPtr++; + } + } +#if !defined(WOLFSSL_NO_ASN_STRICT) && !defined(WOLFSSL_PYTHON) && \ + !defined(WOLFSSL_ASN_ALLOW_0_SERIAL) + /* RFC 5280 4.1.2.2 requires a positive serial number. Reject zero rather + * than emit a certificate wolfSSL itself will not parse. */ + if ((ret == 0) && (serialLen == 1) && (serialPtr[0] == 0)) { + WOLFSSL_MSG("Error serial number of 0 for generated certificate"); + WOLFSSL_ERROR_VERBOSE(BAD_FUNC_ARG); + ret = BAD_FUNC_ARG; + } +#endif if (ret == 0) { /* Determine issuer name size. */ #if defined(WOLFSSL_CERT_EXT) || defined(OPENSSL_EXTRA) || \ @@ -31203,8 +31233,8 @@ static int MakeAnyCert(Cert* cert, byte* derBuffer, word32 derSz, /* Set version, serial number and signature OID */ SetASN_Int8Bit(&dataASN[X509CERTASN_IDX_TBS_VER_INT], (byte)cert->version); - SetASN_Buffer(&dataASN[X509CERTASN_IDX_TBS_SERIAL], cert->serial, - (word32)cert->serialSz); + SetASN_Buffer(&dataASN[X509CERTASN_IDX_TBS_SERIAL], serialPtr, + serialLen); #ifdef WOLFSSL_DUAL_ALG_CERTS if (cert->sigType == 0) { /* sigOID being 0 indicates preTBS. Do not encode signature. */ From 2efbed81c4463e8d0f5609af37df47b2a312911a Mon Sep 17 00:00:00 2001 From: Eric Blankenhorn Date: Thu, 10 Sep 2026 15:28:25 -0500 Subject: [PATCH 2/4] Fix from review --- doc/dox_comments/header_files/asn_public.h | 17 ++++++++++------- tests/api/test_asn.c | 10 ++++++++++ tests/unit-mcdc/test_asn_cert_whitebox.c | 2 ++ wolfcrypt/src/asn.c | 14 +++++++++++++- 4 files changed, 35 insertions(+), 8 deletions(-) diff --git a/doc/dox_comments/header_files/asn_public.h b/doc/dox_comments/header_files/asn_public.h index 87bcea22452..4e248c5d915 100644 --- a/doc/dox_comments/header_files/asn_public.h +++ b/doc/dox_comments/header_files/asn_public.h @@ -117,9 +117,12 @@ void wc_CertFree(Cert* cert); A serial number left at the wc_InitCert default (cert->serialSz of 0) is randomly generated. A caller-supplied serial is taken as a big-endian - magnitude of at most CTC_SERIAL_SIZE bytes and is encoded as a minimal DER - INTEGER, so any leading zero bytes of a fixed-width serial are dropped and - the sign pad is added when needed. Do not prepend a sign pad by hand. + value of at most CTC_SERIAL_SIZE bytes and is normalized to a minimal DER + INTEGER: redundant leading zero bytes are stripped and the sign pad is + added back when the high bit is set. Supplying the magnitude alone is + enough; a sign pad the caller adds is accepted and is not duplicated. + The encoded value, including any sign pad, must not exceed + CTC_SERIAL_SIZE octets. \return Success On successfully making an x509 certificate from the specified input cert, returns the size of the cert generated. @@ -127,10 +130,10 @@ void wc_CertFree(Cert* cert); with XMALLOC \return BUFFER_E Returned if the provided derBuffer is too small to store the generated certificate - \return BAD_FUNC_ARG Returned if cert->serialSz is negative or larger - than CTC_SERIAL_SIZE, or if the serial number is zero. RFC 5280 4.1.2.2 - requires a positive serial; define WOLFSSL_ASN_ALLOW_0_SERIAL to permit - a zero serial. + \return BAD_FUNC_ARG Returned if cert->serialSz is negative, if the + encoded serial would exceed CTC_SERIAL_SIZE octets, or if the serial + number is zero. RFC 5280 4.1.2.2 requires a positive serial of at most + 20 octets; define WOLFSSL_ASN_ALLOW_0_SERIAL to permit a zero serial. \return Others Additional error messages may be returned if the cert generation is not successful. diff --git a/tests/api/test_asn.c b/tests/api/test_asn.c index 5e8d5f98f0c..566eb6f6357 100644 --- a/tests/api/test_asn.c +++ b/tests/api/test_asn.c @@ -4521,6 +4521,16 @@ int test_wc_MakeCert_serial_encoding(void) ExpectIntEQ(tlv[1], CTC_SERIAL_SIZE); } +#ifdef WOLFSSL_ASN_TEMPLATE + /* A 20 byte serial with the high bit set needs a sign pad, which would + * push the encoded value to 21 octets. */ + XMEMSET(maxSerial, 0x11, sizeof(maxSerial)); + maxSerial[0] = 0x80; + ExpectIntEQ(test_cert_make_serial(&cert, &rng, &key, body, + SIGN_CERT_SCRATCH_SZ, maxSerial, CTC_SERIAL_SIZE), + WC_NO_ERR_TRACE(BAD_FUNC_ARG)); +#endif + /* A negative size is rejected by both ASN back ends. */ ExpectIntEQ(test_cert_make_serial(&cert, &rng, &key, body, SIGN_CERT_SCRATCH_SZ, cases[0].serial, -1), diff --git a/tests/unit-mcdc/test_asn_cert_whitebox.c b/tests/unit-mcdc/test_asn_cert_whitebox.c index a4b47b3d399..329dbb1870e 100644 --- a/tests/unit-mcdc/test_asn_cert_whitebox.c +++ b/tests/unit-mcdc/test_asn_cert_whitebox.c @@ -2169,8 +2169,10 @@ static int wb_patch_zero_serial(byte* der, int derSz) int i; for (i = 0; ((i + 8) <= derSz) && (i < 16); i++) { + /* [0] EXPLICIT { INTEGER 2 } then the serial INTEGER holding 1. */ if ((der[i] == 0xA0) && (der[i + 1] == 0x03) && (der[i + 2] == ASN_INTEGER) && (der[i + 3] == 0x01) && + (der[i + 4] == 0x02) && (der[i + 5] == ASN_INTEGER) && (der[i + 6] == 0x01) && (der[i + 7] == 0x01)) { der[i + 7] = 0x00; diff --git a/wolfcrypt/src/asn.c b/wolfcrypt/src/asn.c index 826c0d59767..08446051d18 100644 --- a/wolfcrypt/src/asn.c +++ b/wolfcrypt/src/asn.c @@ -31052,6 +31052,7 @@ static int MakeAnyCert(Cert* cert, byte* derBuffer, word32 derSz, word32 sbjRawLen = 0; const byte* serialPtr = NULL; word32 serialLen = 0; + word32 encodedLen = 0; byte localBefore[MAX_DATE_SIZE]; byte localAfter[MAX_DATE_SIZE]; @@ -31169,13 +31170,24 @@ static int MakeAnyCert(Cert* cert, byte* derBuffer, word32 derSz, serialLen--; serialPtr++; } + /* The sign pad added for a set high bit counts towards the RFC 5280 + * 4.1.2.2 limit of 20 octets. */ + encodedLen = serialLen; + if ((serialPtr[0] & 0x80) != 0) { + encodedLen++; + } + if (encodedLen > CTC_SERIAL_SIZE) { + WOLFSSL_MSG("Encoded serial number longer than 20 octets"); + WOLFSSL_ERROR_VERBOSE(BAD_FUNC_ARG); + ret = BAD_FUNC_ARG; + } } #if !defined(WOLFSSL_NO_ASN_STRICT) && !defined(WOLFSSL_PYTHON) && \ !defined(WOLFSSL_ASN_ALLOW_0_SERIAL) /* RFC 5280 4.1.2.2 requires a positive serial number. Reject zero rather * than emit a certificate wolfSSL itself will not parse. */ if ((ret == 0) && (serialLen == 1) && (serialPtr[0] == 0)) { - WOLFSSL_MSG("Error serial number of 0 for generated certificate"); + WOLFSSL_MSG("Serial number must be positive (non-zero)"); WOLFSSL_ERROR_VERBOSE(BAD_FUNC_ARG); ret = BAD_FUNC_ARG; } From 5c9de0f9793915727b2015d7de7541b08d7be5d8 Mon Sep 17 00:00:00 2001 From: Eric Blankenhorn Date: Tue, 15 Sep 2026 10:12:54 -0500 Subject: [PATCH 3/4] Fix from review --- tests/api/test_asn.c | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/tests/api/test_asn.c b/tests/api/test_asn.c index 566eb6f6357..e002e2828f6 100644 --- a/tests/api/test_asn.c +++ b/tests/api/test_asn.c @@ -4475,14 +4475,13 @@ int test_wc_MakeCert_serial_encoding(void) } /* What wolfSSL emits, wolfSSL has to be able to parse back. */ - ExpectIntGT(signedSz = wc_SignCert(bodySz, cert.sigType, body, - SIGN_CERT_SCRATCH_SZ, #ifdef TEST_SIGN_CERT_BOUNDS_RSA - &key, NULL, + ExpectIntGT(signedSz = wc_SignCert(bodySz, cert.sigType, body, + SIGN_CERT_SCRATCH_SZ, &key, NULL, &rng), 0); #else - NULL, &key, + ExpectIntGT(signedSz = wc_SignCert(bodySz, cert.sigType, body, + SIGN_CERT_SCRATCH_SZ, NULL, &key, &rng), 0); #endif - &rng), 0); if (EXPECT_SUCCESS()) { wc_InitDecodedCert(&decoded, body, (word32)signedSz, HEAP_HINT); ExpectIntEQ(wc_ParseCert(&decoded, CERT_TYPE, NO_VERIFY, NULL), 0); From 25f31a557d860210d1f386895b4969b004e554ca Mon Sep 17 00:00:00 2001 From: Eric Blankenhorn Date: Tue, 15 Sep 2026 11:42:19 -0500 Subject: [PATCH 4/4] Fix from review --- tests/api/test_asn.c | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/tests/api/test_asn.c b/tests/api/test_asn.c index e002e2828f6..e6e8b361306 100644 --- a/tests/api/test_asn.c +++ b/tests/api/test_asn.c @@ -4362,6 +4362,10 @@ static int test_cert_make_serial(Cert* cert, WC_RNG* rng, void* key, int ret; int copySz = serialSz; + /* The caller scans this buffer once the call returns, so leave it in a + * known state on the paths that bail out before the generator writes. */ + XMEMSET(out, 0, outSz); + ret = wc_InitCert(cert); if (ret != 0) return ret;