Mismatch in Limit CertificateAuthoritiesExtension.authorities to 3 through 2^16-1 encoded octets.
Problem Description
This candidate targeted the requirement to Limit CertificateAuthoritiesExtension.authorities to 3 through 2^16-1 encoded octets.. Rechecking the standard, the source, and a feature-enabled live build confirmed that the mismatch is real when the certificate_authorities code path is compiled in.
This report deduplicates multiple candidate-level records that resolved to the same root cause.
Standard Requirement
- Official standard: RFC 8446
- Section: Appendix B Protocol Data Structures and Constant Values (lines 7285-7287)
struct {
DistinguishedName authorities<3..2^16-1>;
} CertificateAuthoritiesExtension;
Interpretation:
The implementation must Limit CertificateAuthoritiesExtension.authorities to 3 through 2^16-1 encoded octets..
This lower bound is deliberate rather than an editorial accident. In the same RFC section, structures that permit empty vectors are written with a 0 lower bound, e.g. OIDFilterExtension.filters<0..2^16-1> in Section 4.2.5. By contrast, CertificateAuthoritiesExtension.authorities<3..2^16-1> excludes an empty encoded authorities vector.
Relevant Source Code
The certificate_authorities implementation allows an empty authorities vector, so the RFC 8446 lower bound of 3 encoded octets is not enforced end to end.
src/tls.c:7705-7777
static word16 TLSX_CA_Names_GetSize(void* data)
{
WOLFSSL* ssl = (WOLFSSL*)data;
WOLF_STACK_OF(WOLFSSL_X509_NAME)* names;
word32 size = 0;
/* Length of names */
size += OPAQUE16_LEN;
for (names = SSL_PRIORITY_CA_NAMES(ssl); names != NULL; names = names->next) {
byte seq[MAX_SEQ_SZ];
WOLFSSL_X509_NAME* name = names->data.name;
if (name != NULL) {
/* 16-bit length | SEQ | Len | DER of name */
size += (word32)(OPAQUE16_LEN + SetSequence(name->rawLen, seq) +
name->rawLen);
if (size > WOLFSSL_MAX_16BIT) {
return 0;
}
}
}
return (word16)size;
}
static word16 TLSX_CA_Names_Write(void* data, byte* output)
{
WOLFSSL* ssl = (WOLFSSL*)data;
WOLF_STACK_OF(WOLFSSL_X509_NAME)* names;
byte* len;
/* Reserve space for the length value */
len = output;
output += OPAQUE16_LEN;
for (names = SSL_PRIORITY_CA_NAMES(ssl); names != NULL; names = names->next) {
byte seq[MAX_SEQ_SZ];
WOLFSSL_X509_NAME* name = names->data.name;
if (name != NULL) {
c16toa((word16)name->rawLen +
(word16)SetSequence(name->rawLen, seq), output);
output += OPAQUE16_LEN;
output += SetSequence(name->rawLen, output);
XMEMCPY(output, name->raw, name->rawLen);
output += name->rawLen;
}
}
/* Write the total length */
c16toa((word16)(output - len - OPAQUE16_LEN), len);
return (word16)(output - len);
}
static int TLSX_CA_Names_Parse(WOLFSSL *ssl, const byte* input,
word16 length, byte isRequest)
{
word16 extLen;
(void)isRequest;
wolfSSL_sk_X509_NAME_pop_free(ssl->peer_ca_names, NULL);
ssl->peer_ca_names = wolfSSL_sk_X509_NAME_new(NULL);
if (ssl->peer_ca_names == NULL)
return MEMORY_ERROR;
if (length < OPAQUE16_LEN)
return BUFFER_ERROR;
ato16(input, &extLen);
input += OPAQUE16_LEN;
length -= OPAQUE16_LEN;
if (extLen != length)
return BUFFER_ERROR;
while (length) {
certificate_authorities serializes a 16-bit authorities vector and parses each DistinguishedName as a 16-bit length-prefixed DER name.
src/tls.c:16450-16458
#if !defined(NO_CERTS) && !defined(WOLFSSL_NO_CA_NAMES)
if (IsAtLeastTLSv1_3(ssl->version) &&
SSL_PRIORITY_CA_NAMES(ssl) != NULL) {
WOLFSSL_MSG("Adding certificate authorities extension");
if ((ret = TLSX_Push(&ssl->extensions,
TLSX_CERTIFICATE_AUTHORITIES, ssl, ssl->heap)) != 0) {
return ret;
}
}
wolfSSL only adds certificate_authorities when a CA-name list is configured locally.
Because the condition is SSL_PRIORITY_CA_NAMES(ssl) != NULL, this is a pointer-presence check, not a non-empty-list check. A non-NULL stack with zero entries is still emitted.
src/ssl_api_cert.c:1348-1379, src/x509.c:14874-14940, and src/ssl_api_cert.c:1551-1595
The CA-list APIs take ownership of any caller-provided stack without validating that it contains at least one name:
wolfSSL_CTX_set0_CA_list() and wolfSSL_set0_CA_list() store names directly.
wolfSSL_sk_X509_NAME_new(NULL) creates a valid empty stack object with num == 0.
wolfSSL_load_client_CA_file() starts by creating an empty stack and only pushes entries if certificates are actually read, so an empty non-NULL list is a representable API value rather than a synthetic memory corruption case.
tests/api.c:34357-34439 and tests/api/test_tls_ext.c:831-980, 1132-1210
The upstream tests cover:
- successful transmission/parsing of non-empty CA-name lists; and
- rejection of certain malformed CA-name encodings.
They do not cover the boundary case where the authorities vector is syntactically well-formed but empty (00 00 inner vector length). That gap is consistent with the missing lower-bound enforcement in the implementation.
wolfssl/internal.h:1005-1021
The CA-name APIs and certificate_authorities support are compiled only when OPENSSL_EXTRA is enabled; otherwise WOLFSSL_NO_CA_NAMES is defined and the entire path is compiled out.
Runtime Evidence
Earlier wrapper caveat
The previously cited wrapper step for this record is source-backed only. For pos == 1471 it writes a static summary and exits; it does not execute a live handshake.
Actual runtime recheck on 2026-08-03
To verify the behavior on a build where this feature is actually present, I configured a feature-enabled build with -DWOLFSSL_LIGHTY=yes, which enabled OPENSSL_EXTRA, OPENSSL_ALL, and HAVE_LIGHTY. I then ran the reproducer in three modes.
Non-empty control
Command:
certificate_authorities_empty_vector_repro.exe nonempty
Observed stdout:
{
"mode": "nonempty",
"handshake_ret": 0,
"cert_cb_called": true,
"configured_client_ca_list_present": true,
"configured_client_ca_list_count": 1,
"peer_ca_list_present": true,
"peer_ca_list_count": 1
}
This confirms the baseline path: a non-empty configured CA-name list is transmitted and the peer receives one CA name.
Empty-list repro
Command:
certificate_authorities_empty_vector_repro.exe empty
Observed stdout:
{
"mode": "empty",
"handshake_ret": 0,
"cert_cb_called": true,
"configured_client_ca_list_present": true,
"configured_client_ca_list_count": 0,
"peer_ca_list_present": true,
"peer_ca_list_count": 0
}
This shows the defect directly: the configured CA-name list exists but is empty, the handshake still succeeds, and the peer receives a present-but-empty CA-name list instead of rejecting the extension or treating it as absent.
NULL-list control
Command:
certificate_authorities_empty_vector_repro.exe null
Observed stdout:
{
"mode": "null",
"handshake_ret": 0,
"cert_cb_called": true,
"configured_client_ca_list_present": false,
"configured_client_ca_list_count": -1,
"peer_ca_list_present": false,
"peer_ca_list_count": -1
}
This confirms the intended contrast: when the configured list is NULL, the handshake still reaches the certificate callback, but no certificate_authorities list is observed on the peer at all.
Inconsistency Reason
- In a build where CA-name support is enabled, wolfSSL serializes an empty
certificate_authorities vector as length 0 and the peer parses it into an empty CA-name stack instead of rejecting the <3..2^16-1> lower-bound violation.
- The corresponding
NULL configuration path behaves differently and correctly suppresses the extension entirely, so the defect is specifically that an empty-but-present list is treated as sendable and acceptable.
Decision Reason
- The RFC 8446 structure requires
authorities<3..2^16-1>. Runtime controls show the implementation distinguishes NULL from non-NULL: NULL suppresses the extension entirely, while an empty configured CA-name stack is serialized as a zero-length authorities vector and accepted by the peer as an empty list. That is a real syntax-handling defect in the compiled certificate_authorities implementation, though it is feature-gated rather than present in builds where OPENSSL_EXTRA is disabled.
Mismatch in Limit CertificateAuthoritiesExtension.authorities to 3 through 2^16-1 encoded octets.
Problem Description
This candidate targeted the requirement to Limit CertificateAuthoritiesExtension.authorities to 3 through 2^16-1 encoded octets.. Rechecking the standard, the source, and a feature-enabled live build confirmed that the mismatch is real when the
certificate_authoritiescode path is compiled in.This report deduplicates multiple candidate-level records that resolved to the same root cause.
Standard Requirement
Interpretation:
The implementation must Limit CertificateAuthoritiesExtension.authorities to 3 through 2^16-1 encoded octets..
This lower bound is deliberate rather than an editorial accident. In the same RFC section, structures that permit empty vectors are written with a
0lower bound, e.g.OIDFilterExtension.filters<0..2^16-1>in Section 4.2.5. By contrast,CertificateAuthoritiesExtension.authorities<3..2^16-1>excludes an empty encoded authorities vector.Relevant Source Code
The certificate_authorities implementation allows an empty authorities vector, so the RFC 8446 lower bound of 3 encoded octets is not enforced end to end.
src/tls.c:7705-7777certificate_authorities serializes a 16-bit authorities vector and parses each DistinguishedName as a 16-bit length-prefixed DER name.
src/tls.c:16450-16458wolfSSL only adds certificate_authorities when a CA-name list is configured locally.
Because the condition is
SSL_PRIORITY_CA_NAMES(ssl) != NULL, this is a pointer-presence check, not a non-empty-list check. A non-NULL stack with zero entries is still emitted.src/ssl_api_cert.c:1348-1379,src/x509.c:14874-14940, andsrc/ssl_api_cert.c:1551-1595The CA-list APIs take ownership of any caller-provided stack without validating that it contains at least one name:
wolfSSL_CTX_set0_CA_list()andwolfSSL_set0_CA_list()storenamesdirectly.wolfSSL_sk_X509_NAME_new(NULL)creates a valid empty stack object withnum == 0.wolfSSL_load_client_CA_file()starts by creating an empty stack and only pushes entries if certificates are actually read, so an empty non-NULL list is a representable API value rather than a synthetic memory corruption case.tests/api.c:34357-34439andtests/api/test_tls_ext.c:831-980,1132-1210The upstream tests cover:
They do not cover the boundary case where the authorities vector is syntactically well-formed but empty (
00 00inner vector length). That gap is consistent with the missing lower-bound enforcement in the implementation.wolfssl/internal.h:1005-1021The CA-name APIs and
certificate_authoritiessupport are compiled only whenOPENSSL_EXTRAis enabled; otherwiseWOLFSSL_NO_CA_NAMESis defined and the entire path is compiled out.Runtime Evidence
Earlier wrapper caveat
The previously cited wrapper step for this record is source-backed only. For
pos == 1471it writes a static summary and exits; it does not execute a live handshake.Actual runtime recheck on 2026-08-03
To verify the behavior on a build where this feature is actually present, I configured a feature-enabled build with
-DWOLFSSL_LIGHTY=yes, which enabledOPENSSL_EXTRA,OPENSSL_ALL, andHAVE_LIGHTY. I then ran the reproducer in three modes.Non-empty control
Command:
certificate_authorities_empty_vector_repro.exe nonemptyObserved stdout:
{ "mode": "nonempty", "handshake_ret": 0, "cert_cb_called": true, "configured_client_ca_list_present": true, "configured_client_ca_list_count": 1, "peer_ca_list_present": true, "peer_ca_list_count": 1 }This confirms the baseline path: a non-empty configured CA-name list is transmitted and the peer receives one CA name.
Empty-list repro
Command:
certificate_authorities_empty_vector_repro.exe emptyObserved stdout:
{ "mode": "empty", "handshake_ret": 0, "cert_cb_called": true, "configured_client_ca_list_present": true, "configured_client_ca_list_count": 0, "peer_ca_list_present": true, "peer_ca_list_count": 0 }This shows the defect directly: the configured CA-name list exists but is empty, the handshake still succeeds, and the peer receives a present-but-empty CA-name list instead of rejecting the extension or treating it as absent.
NULL-list control
Command:
certificate_authorities_empty_vector_repro.exe nullObserved stdout:
{ "mode": "null", "handshake_ret": 0, "cert_cb_called": true, "configured_client_ca_list_present": false, "configured_client_ca_list_count": -1, "peer_ca_list_present": false, "peer_ca_list_count": -1 }This confirms the intended contrast: when the configured list is
NULL, the handshake still reaches the certificate callback, but nocertificate_authoritieslist is observed on the peer at all.Inconsistency Reason
certificate_authoritiesvector as length0and the peer parses it into an empty CA-name stack instead of rejecting the<3..2^16-1>lower-bound violation.NULLconfiguration path behaves differently and correctly suppresses the extension entirely, so the defect is specifically that an empty-but-present list is treated as sendable and acceptable.Decision Reason
authorities<3..2^16-1>. Runtime controls show the implementation distinguishesNULLfrom non-NULL:NULLsuppresses the extension entirely, while an empty configured CA-name stack is serialized as a zero-length authorities vector and accepted by the peer as an empty list. That is a real syntax-handling defect in the compiledcertificate_authoritiesimplementation, though it is feature-gated rather than present in builds whereOPENSSL_EXTRAis disabled.