Skip to content

Add native API for TLS 1.3 certificate_authorities extension - #11089

Open
julek-wolfssl wants to merge 5 commits into
wolfSSL:masterfrom
julek-wolfssl:tls13-certificate-authorities-api
Open

Add native API for TLS 1.3 certificate_authorities extension#11089
julek-wolfssl wants to merge 5 commits into
wolfSSL:masterfrom
julek-wolfssl:tls13-certificate-authorities-api

Conversation

@julek-wolfssl

Copy link
Copy Markdown
Member

RFC 8446 4.2.4 certificate_authorities (extension type 47) was previously only accessible behind OPENSSL_EXTRA via the client_ca_names / ca_names / peer_ca_names WOLF_STACK_OF(WOLFSSL_X509_NAME) stacks, leaving users without the OpenSSL compat layer no way to send or inspect the extension. This adds a native API gated on WOLFSSL_TLS13.

  • New CertificateAuthority node type: a singly-linked list with a flexible-array DN buffer holding the inner DER-encoded Name content (no SEQUENCE header). TLSX_CertificateAuthorities_Add/FreeAll manage the list, gated on WOLFSSL_TLS13 && !NO_CERTS && !WOLFSSL_NO_CA_NAMES.

  • Storage added on WOLFSSL_CTX (ws_ca_names) and WOLFSSL (ws_ca_names + ws_peer_ca_names); SSL shadows CTX via the WS_CA_NAMES helper.

  • New public API mirroring the UseSNI style:

    • wolfSSL_UseCertificateAuthority / CTX variant
    • wolfSSL_ClearCertificateAuthorities / CTX variant
    • wolfSSL_GetPeerCertificateAuthorityCount
    • wolfSSL_GetPeerCertificateAuthority (index-based, copy-out)

    The library prepends the DER SEQUENCE header on the wire and strips it on parse, so callers pass the raw subject content straight from wc_GetDecodedCertSubjectRaw (new accessor on DecodedCert).

  • TLSX_CA_Names_Write is a single emitter matching the PHA_GET_SIZE / PHA_WRITE style (int return, word16* pSz accumulator); macros pass NULL when sizing and the output buffer when serializing. It walks the compat stack first (when OPENSSL_EXTRA is on) then the native list, enforcing the RFC 8446 per-DN cap and capping the whole extension payload (outer length included) at WOLFSSL_MAX_16BIT so the word16 size accumulator stays exact, returning BUFFER_ERROR on overflow.

  • TLSX_CA_Names_Parse always populates ws_peer_ca_names and, when OPENSSL_EXTRA is compiled in, additionally populates peer_ca_names through the existing InitDecodedCert/GetName/CopyDecodedName path. Length validation enforces the RFC 8446 DistinguishedName<1..2^16-1> and authorities<3..2^16-1> bounds.

  • TLSX_GetSize now propagates a non-zero ret by breaking out of the walk, matching TLSX_Write.

  • Teardown paths in SSL_CtxResourceFree and wolfSSL_ResourceFree free the native lists on the owning heap.

Tests added in tests/api/test_tls_ext.c cover argument validation, size limits, send/receive counts, handshake round-trips on SSL and CTX, a cert_cb scenario feeding DecodedCert subjects to the API (with a params loop for TLS 1.3 and DTLS 1.3), the existing OPENSSL_EXTRA cases, and a bad-extension regression. Documentation for all new API functions is added under doc/dox_comments/header_files/.

Copilot AI lite review requested due to automatic review settings August 6, 2026 11:30
@julek-wolfssl julek-wolfssl self-assigned this Aug 6, 2026

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR adds a wolfSSL-native API (gated on WOLFSSL_TLS13) for sending and inspecting the TLS 1.3 certificate_authorities extension (RFC 8446 §4.2.4), making the feature available without requiring the OpenSSL compatibility layer.

Changes:

  • Introduces a native CertificateAuthority linked-list representation, stored on WOLFSSL_CTX / WOLFSSL, with emit/parse support integrated into TLS extensions.
  • Adds public APIs to configure advertised CA DNs and to query peer-provided CA DNs, plus a new wc_GetDecodedCertSubjectRaw() accessor to supply correctly-formatted DN content.
  • Adds API tests covering argument validation, size limits, handshake round-trips (SSL + CTX), and a cert_cb scenario.

Reviewed changes

Copilot reviewed 13 out of 13 changed files in this pull request and generated 5 comments.

Show a summary per file
File Description
wolfssl/wolfcrypt/asn_public.h Declares new public accessor for raw subject DN content (wc_GetDecodedCertSubjectRaw).
wolfcrypt/src/asn.c Implements wc_GetDecodedCertSubjectRaw() returning the inner subject Name SEQUENCE content pointer/length.
wolfssl/ssl.h Adds the new public native certificate_authorities API declarations and header-level documentation.
wolfssl/internal.h Adds native CA list node type and storage fields in WOLFSSL_CTX/WOLFSSL, plus helper macro for CTX fallback.
src/tls.c Implements native CA list management and unified extension sizing/writing/parsing for compat + native sources.
src/ssl_api_ext.c Implements the new public native APIs (Use/Clear/GetPeerCount/GetPeerByIndex).
src/ssl_api_cert.c Tightens OpenSSL-compat CA-names code compilation guards to OPENSSL_EXTRA.
src/internal.c Adds teardown of native lists and adjusts CA-names-related guards in resource free paths.
tests/api/test_tls_ext.h Adds prototypes for new native API tests.
tests/api/test_tls_ext.c Adds comprehensive native API tests, including handshake and cert-callback coverage.
tests/api.c Registers new tests in the main API test list.
doc/dox_comments/header_files/ssl.h Adds doxygen documentation for the new SSL/CTX APIs and peer getters.
doc/dox_comments/header_files/asn_public.h Adds doxygen documentation for wc_GetDecodedCertSubjectRaw().

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/tls.c Outdated
Comment thread wolfssl/internal.h
Comment thread doc/dox_comments/header_files/ssl.h
Comment thread doc/dox_comments/header_files/ssl.h Outdated
Comment thread doc/dox_comments/header_files/ssl.h Outdated

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 13 out of 13 changed files in this pull request and generated no new comments.

Suppressed comments (3)

doc/dox_comments/header_files/asn_public.h:4417

  • The lifetime/ownership statement is incorrect: subjectRaw points into the source DER buffer passed to wc_InitDecodedCert, which DecodedCert does not own. A caller can keep the DecodedCert alive but free/reuse that source buffer and then dereference a dangling pointer. Document that the source DER must remain alive while the returned pointer is used.
    The returned pointer and size reference the inner content of the subject
    Name SEQUENCE (i.e. the bytes after the SEQUENCE tag and length). The
    pointer aliases memory inside the DecodedCert and must not be freed by
    the caller. The data remains valid until the DecodedCert is freed.

wolfcrypt/src/asn.c:23312

  • cert->subjectRaw aliases cert->source, and InitDecodedCert explicitly does not own that source buffer. This comment currently gives the opposite ownership impression and could lead future callers to rely only on the DecodedCert lifetime. State that the input DER buffer controls this pointer's lifetime.
    src/tls.c:7864
  • This still accepts an empty authorities vector (00 00). RFC 8446 defines authorities<3..2^16-1>, so an extension that is present must contain at least three vector bytes; accepting zero bypasses the stated malformed-extension validation. Reject every post-prefix length below 3.
    if (length > 0 && length < 3)

@julek-wolfssl
julek-wolfssl force-pushed the tls13-certificate-authorities-api branch from 516fd37 to f704ea9 Compare August 13, 2026 15:02
@julek-wolfssl
julek-wolfssl marked this pull request as ready for review August 13, 2026 16:04
@github-actions

Copy link
Copy Markdown

retest this please

@github-actions

github-actions Bot commented Aug 13, 2026

Copy link
Copy Markdown

MemBrowse Memory Report

gcc-arm-cortex-m0plus

  • FLASH: .text +1,912 B (+2.9%, 66,903 B / 262,144 B, total: 26% used)

gcc-arm-cortex-m3

  • FLASH: .text +1,880 B (+1.5%, 125,691 B / 262,144 B, total: 48% used)

gcc-arm-cortex-m4

  • FLASH: .text +2,240 B (+1.1%, 204,335 B / 262,144 B, total: 78% used)

gcc-arm-cortex-m4-baremetal

  • FLASH: .text +1,856 B (+2.7%, 69,411 B / 262,144 B, total: 26% used)

gcc-arm-cortex-m4-crypto-only

  • FLASH: .text +1,920 B (+1.1%, 178,026 B / 262,144 B, total: 68% used)

gcc-arm-cortex-m4-dtls13

  • FLASH: .text +3,072 B (+1.7%, 186,748 B / 1,048,576 B, total: 18% used)

gcc-arm-cortex-m4-min-ecc

  • FLASH: .text +1,728 B (+2.8%, 64,261 B / 262,144 B, total: 25% used)

gcc-arm-cortex-m4-openssl-compat

  • FLASH: .rodata +56 B, .text +3,264 B (+0.4%, 779,924 B / 1,048,576 B, total: 74% used)

gcc-arm-cortex-m4-pkcs7

  • FLASH: .text +2,112 B (+1.0%, 217,070 B / 262,144 B, total: 83% used)

gcc-arm-cortex-m4-pq

  • FLASH: .text +3,392 B (+1.1%, 301,276 B / 1,048,576 B, total: 29% used)

gcc-arm-cortex-m4-rsa-only

  • FLASH: .text +3,456 B (+1.1%, 331,824 B / 1,048,576 B, total: 32% used)

gcc-arm-cortex-m4-sp-math

  • FLASH: .text +1,728 B (+2.8%, 64,261 B / 262,144 B, total: 25% used)

gcc-arm-cortex-m4-tls12

  • FLASH: .text +1,856 B (+1.5%, 126,451 B / 262,144 B, total: 48% used)

gcc-arm-cortex-m4-tls13

  • FLASH: .text +3,392 B (+1.4%, 242,081 B / 262,144 B, total: 92% used)

gcc-arm-cortex-m7

  • FLASH: .text +2,240 B (+1.1%, 204,335 B / 262,144 B, total: 78% used)

gcc-arm-cortex-m7-pq

  • FLASH: .text +3,456 B (+1.2%, 302,236 B / 1,048,576 B, total: 29% used)

gcc-arm-cortex-m7-tls13

  • FLASH: .text +3,392 B (+1.4%, 242,145 B / 262,144 B, total: 92% used)

linuxkm-pie

  • Data: __patchable_function_entries +112 B (+0.4%, 26,696 B)

linuxkm-standard

  • Data: __patchable_function_entries +120 B (+0.2%, 49,576 B)

stm32-sim-stm32h753

  • FLASH: .text +1,856 B (+1.0%, 188,208 B / 2,097,152 B, total: 9% used)

@julek-wolfssl

Copy link
Copy Markdown
Member Author

retest this please

@philljj philljj left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The PRB hung because of the draft conversion. Please rebase + force-push so CI will unblock.

@philljj philljj assigned julek-wolfssl and unassigned wolfSSL-Bot Aug 17, 2026
RFC 8446 4.2.4 certificate_authorities (extension type 47) was only
wired up behind OPENSSL_EXTRA via the client_ca_names / ca_names /
peer_ca_names WOLF_STACK_OF(WOLFSSL_X509_NAME) stacks. Users without
the OpenSSL compat layer had no way to send or inspect the extension.

This commit introduces a native API gated on WOLFSSL_TLS13:

- CertificateAuthority node: singly-linked list with a flexible-array
  DN buffer holding the inner DER-encoded Name content (no SEQUENCE
  header). TLSX_CertificateAuthorities_Add/FreeAll manage the list.
  Everything is gated on WOLFSSL_TLS13 && !NO_CERTS &&
  !WOLFSSL_NO_CA_NAMES.
- Storage on WOLFSSL_CTX (ws_ca_names) and WOLFSSL (ws_ca_names +
  ws_peer_ca_names); SSL shadows CTX via the WS_CA_NAMES helper.
- Public API mirroring UseSNI style:
    wolfSSL_UseCertificateAuthority / CTX variant
    wolfSSL_ClearCertificateAuthorities / CTX variant
    wolfSSL_GetPeerCertificateAuthorityCount
    wolfSSL_GetPeerCertificateAuthority  (index-based, copy-out)
  The library prepends the DER SEQUENCE header on the wire and strips
  it on parse, so callers pass the raw subject content straight from
  wc_GetDecodedCertSubjectRaw (new accessor on DecodedCert).
- TLSX_CA_Names_Write is a single emitter matching the PHA_GET_SIZE /
  PHA_WRITE style (int return, word16* pSz accumulator); macros pass
  NULL when sizing and the output buffer when serializing. It walks
  the compat stack first (when OPENSSL_EXTRA is on) then the native
  list, enforcing the RFC 8446 per-DN cap and capping the whole
  extension payload (outer length included) at WOLFSSL_MAX_16BIT so
  the word16 size accumulator stays exact, returning BUFFER_ERROR on
  overflow.
- TLSX_CA_Names_Parse always populates ws_peer_ca_names and, when
  OPENSSL_EXTRA is compiled in, additionally populates peer_ca_names
  through the existing InitDecodedCert/GetName/CopyDecodedName path.
  Length validation enforces the RFC 8446 DistinguishedName<1..2^16-1>
  and authorities<3..2^16-1> bounds.
- TLSX_GetSize now propagates non-zero ret by breaking out of the
  walk, matching TLSX_Write.
- Teardown paths in SSL_CtxResourceFree and wolfSSL_ResourceFree free
  the native lists on the owning heap.

Tests in tests/api/test_tls_ext.c cover argument validation, size
limits, send/receive counts, handshake round-trips on SSL and CTX, a
cert_cb scenario that feeds DecodedCert subjects to the API (with
params loop for TLS 1.3 and DTLS 1.3), the existing OPENSSL_EXTRA
cases, and a bad-extension regression. Documentation for all new API
functions is in doc/dox_comments/header_files/.
- Only advertise the extension when the emitter would produce at least
  one DN. HasAnyCANames() asks TLSX_CA_Names_Write() in size-only mode
  rather than restating its filtering, so the two cannot drift; a
  compat stack holding only NULL/empty names no longer emits an empty
  authorities vector, which RFC 8446 4.2.4 forbids. It reports true on
  error so an oversized list still fails at write time instead of
  silently dropping the extension. TLSX_CA_Names_Write() takes a const
  WOLFSSL* now that it is called from a predicate.
- TLSX_CertificateAuthorities_Add() appends instead of prepending, so
  wire order matches call order as the docs state. The handshake test
  asserts that order rather than ignoring it.
- Scope the MSVC C4200 suppression with warning(push)/(pop) so it does
  not leak into the rest of the translation unit.
- Document MEMORY_ERROR, not MEMORY_E, as the allocation failure
  return; that is what the implementation returns.
Dropping the automatic WOLFSSL_NO_CA_NAMES define for builds without
OPENSSL_EXTRA left test_wolfSSL_CA_list_add/get compiled in there, where
the OpenSSL CA-list API they call is not declared. Smoke, SBOM and bomsh
all failed on the resulting implicit declarations.

WOLFSSL_NO_CA_NAMES is now only ever tested, never defined in tree, so
register it in .wolfssl_known_macro_extras for check-source-text.
@julek-wolfssl
julek-wolfssl force-pushed the tls13-certificate-authorities-api branch from f704ea9 to 926a1dc Compare August 18, 2026 16:10
@philljj
philljj requested review from philljj and wolfSSL-Fenrir-bot and a balanced review from Copilot August 19, 2026 20:49

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 15 out of 15 changed files in this pull request and generated no new comments.

Suppressed comments (6)

src/tls.c:7859

  • The parser accepts an empty authorities vector because the lower-bound check is conditional on length > 0. Since this function is called only when the extension is present, RFC 8446's authorities<3..2^16-1> requires a zero-length vector to be rejected as well.
    if (length > 0 && length < 3)
        return BUFFER_ERROR;

src/tls.c:7840

  • This reset happens only when another certificate_authorities extension is parsed. wolfSSL_clear() reuses a WOLFSSL object without freeing ws_peer_ca_names, so if the next peer omits the extension, the public getters return authorities from the previous connection even though their contract says the count is zero when no extension was received. Clear this connection-derived list when resetting the SSL object.
    /* Reset the wolfSSL native peer list. */
    TLSX_CertificateAuthorities_FreeAll(ssl->ws_peer_ca_names, ssl->heap);
    ssl->ws_peer_ca_names = NULL;

src/tls.c:7722

  • Appending by walking from the head makes repeated additions quadratic. The same helper is called once per peer-controlled entry during parsing, and a maximum-size extension can contain thousands of short DNs, causing millions of pointer traversals during a handshake. Track a tail pointer (or otherwise make insertion O(1)) while preserving wire order.
    /* Append so wire order matches the order of the Add calls. */
    while (*head != NULL)
        head = &(*head)->next;
    *head = node;

src/tls.c:7702

  • This comment says the node is pushed onto the head, but the implementation appends it to the tail.
/* Push a copy of dn/dnSz onto the list head. */

wolfcrypt/src/asn.c:23491

  • The lifetime statement is incorrect: subjectRaw points into cert->source, and wc_InitDecodedCert() explicitly does not own that source buffer. Keeping DecodedCert alive does not prevent the caller's DER buffer from being freed or modified, which would leave this pointer dangling or corrupted.
    doc/dox_comments/header_files/asn_public.h:4417
  • This public lifetime guarantee is incorrect. The returned bytes alias the DER source buffer supplied to wc_InitDecodedCert(), not memory owned by DecodedCert; freeing or modifying that source invalidates the pointer even if the DecodedCert is still alive.
    pointer aliases memory inside the DecodedCert and must not be freed by
    the caller. The data remains valid until the DecodedCert is freed.

… list

RFC 8446 4.2.4 declares authorities as DistinguishedName<3..2^16-1>, so a
present extension carrying an empty vector is a framing error.

wolfSSL_clear() left ws_peer_ca_names in place, so the getters reported
the previous peer's authorities when the next one sent no extension.

Parsing appended each entry by walking from the head, which is quadratic
in the number of entries a peer can send.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants