Skip to content

Server certificate callback can ignore signature_algorithms_cert and send an unacceptable certificate chain #11072

Description

@LiD0209

Server certificate callback can ignore signature_algorithms_cert and send an unacceptable certificate chain

Executive Summary

wolfSSL parses the client's signature_algorithms_cert extension, but the supported server certificate setup callback path does not expose that certificate-signature restriction through the public selection API. As a result, a callback can choose a certificate chain by looking only at the broader signature_algorithms list and can send a chain whose certificate signatures are outside the client's advertised certificate-signature allow-list.

A focused TLS 1.3 runtime reproducer confirms this behavior. The client advertised signature_algorithms_cert=RSA+SHA256, the server had both an RSA-signed and an ECDSA-signed certificate available for the same TLS identity, and the callback-selected chain that was actually sent to the client was the ECDSA-signed one. The handshake still completed successfully.

Standard Requirement

  • RFC: RFC 8446
  • Section: 4.4.2.2 Server Certificate Selection
  • Relevant lines: 3720-3724

Standard basis:

  • signature_algorithms_cert applies to signatures that appear in certificates, while signature_algorithms applies to CertificateVerify signatures. See RFC 8446 Section 4.2.3.
  • When the server is able to provide a conforming chain, every certificate it provides must be signed by a client-advertised certificate-signature algorithm. See RFC 8446 Section 4.4.2.2.

Interpretation:

  • If the client narrows signature_algorithms_cert, that narrower list constrains certificate signatures, not just the handshake signature.
  • If the server has multiple usable certificates and at least one chain satisfies the client's certificate-signature constraints, it must choose such a chain.
  • It is not enough that CertificateVerify uses an acceptable algorithm. The chain itself must also satisfy the certificate-signature rule.

Relevant Source Code

The relevant implementation gap is in the server-side certificate selection path, not in general TLS 1.3 parsing.

1. wolfSSL parses signature_algorithms_cert

src/tls.c:8075 parses the extension and stores it in ssl->certHashSigAlgo / ssl->certHashSigAlgoSz.

static int TLSX_SignatureAlgorithmsCert_Parse(WOLFSSL *ssl, const byte* input,
                                              word16 length, byte isRequest)
{
    ...
    ssl->certHashSigAlgoSz = len;
    ...
    XMEMCPY(ssl->certHashSigAlgo, input, ssl->certHashSigAlgoSz);
    return 0;
}

This matters because the issue is not "wolfSSL never learned the client's certificate-signature constraints". It did.

2. The public certificate-selection helper exposes only the broader signature list

src/ssl.c:4302 exposes ssl->clSuites->hashSigAlgo through wolfSSL_get_client_suites_sigalgs().

int wolfSSL_get_client_suites_sigalgs(const WOLFSSL* ssl,
        const byte** suites, word16* suiteSz,
        const byte** hashSigAlgo, word16* hashSigAlgoSz)
{
    ...
    if (hashSigAlgo != NULL && hashSigAlgoSz != NULL) {
        *hashSigAlgo = ssl->clSuites->hashSigAlgo;
        *hashSigAlgoSz = ssl->clSuites->hashSigAlgoSz;
    }
    ...
}

That helper does not return ssl->certHashSigAlgo. So a callback that uses the public helper sees the CertificateVerify signature list, not the certificate-signature list.

3. The server callback runs before certificate sending

src/ssl_api_cert.c:2674 defines the internal callback wrapper:

int CertSetupCbWrapper(WOLFSSL* ssl)
{
    int ret = 0;
    if (ssl->ctx->certSetupCb != NULL) {
        ret = ssl->ctx->certSetupCb(ssl, ssl->ctx->certSetupCbArg);
        ...
    }
    return ret;
}

The TLS 1.3 server path invokes that callback during ClientHello processing at src/tls13.c:7975:

#ifdef WOLFSSL_CERT_SETUP_CB
    if ((ret = CertSetupCbWrapper(ssl)) != 0)
        goto exit_dch;
#endif

So the callback is a first-class server certificate selection hook.

4. The TLS 1.3 send path transmits the currently loaded chain

src/tls13.c:9739 sends the certificate currently loaded into ssl->buffers.certificate and ssl->buffers.certChain.

if (!ssl->buffers.certificate || !ssl->buffers.certificate->buffer) {
    WOLFSSL_MSG("Send Cert missing certificate buffer");
    return NO_CERT_ERROR;
}
...
certSz = ssl->buffers.certificate->length;
...
if (certSz > 0 && ssl->buffers.certChainCnt > 0) {
    p = ssl->buffers.certChain->buffer;
    certChainSz = ssl->buffers.certChain->length;
    length += certChainSz;
    listSz += certChainSz;
}

There is no server-side filter here that re-checks the loaded chain's certificate signatures against ssl->certHashSigAlgo before sending.

Runtime Reproduction

Reproduction goal

Prove a clean server-side selection mismatch where:

  1. the client sends a narrow signature_algorithms_cert list,
  2. the server has a conforming alternative certificate available,
  3. the callback still chooses a non-conforming certificate because it sees only the broader public signature list, and
  4. the client actually receives the non-conforming certificate.

Reproduction setup

The reproducer built a native TLS 1.3 client and server around wolfSSL's in-memory I/O path. It configured two server certificates for the same TLS identity, narrowed the client certificate-signature allow-list, invoked the server certificate callback during ClientHello processing, completed the handshake, and inspected the leaf certificate received by the client.

Server certificate pair:

  • certs/server-ecc-rsa.pem: leaf certificate signature algorithm sha256WithRSAEncryption
  • certs/server-ecc.pem: leaf certificate signature algorithm ecdsa-with-SHA256

These certificates are intentionally suitable for this test because both use the same TLS identity key material while their certificate signatures differ. The certificate-generation definitions are at certs/renewcerts.sh:707 and certs/renewcerts.sh:747.

Client behavior in the reproducer:

  • signature_algorithms_cert is narrowed to RSA+SHA256
  • the ordinary signature_algorithms list still includes ECDSA

Server callback behavior in the reproducer:

  • observes the public signature list from ssl->clSuites->hashSigAlgo
  • also records ssl->certHashSigAlgo for debugging
  • chooses server-ecc.pem whenever ECDSA appears in the public list

Observed output

The reproduced run on August 3, 2026 produced:

handshake_ok=true
sigalgs_cert_present=true
callback_invoked=true
callback_ok=true
public_sigalgs=ECDSA+SHA512:ECDSA+SHA384:ECDSA+SHA256:RSA-PSS+SHA512:0x080b:RSA-PSS+SHA384:0x080a:RSA-PSS+SHA256:0x0809:RSA+SHA512:RSA+SHA384:RSA+SHA256:0x0301
cert_sigalgs=RSA+SHA256
chosen_chain=server-ecc.pem
peer_cert_sig_alg=ecdsa-with-SHA256
peer_cert_is_rsa_sha256=false
peer_cert_is_ecdsa_sha256=true

What this proves:

  • The client really sent signature_algorithms_cert; sigalgs_cert_present=true.
  • wolfSSL parsed that narrower certificate-signature list; inside the callback it appeared as cert_sigalgs=RSA+SHA256.
  • The callback still chose the ECDSA-signed certificate because the public helper-facing view still contained ECDSA.
  • The client actually received an ECDSA-signed leaf certificate, not just a locally recorded choice.
  • The server had an RSA-signed alternative available but did not use it.

Why This Is a Real Issue

This is not just a documentation mismatch or a theoretical API nit.

The runtime result demonstrates all of the facts needed to establish a standards mismatch:

  • The client narrowed certificate-signature acceptance to RSA-only.
  • The server had a conforming RSA-signed certificate chain available.
  • The server instead selected and sent an ECDSA-signed certificate chain.
  • The reason it did so is that the supported callback-driven selection interface exposes only the broader handshake-signature list through the public helper.

That behavior conflicts with the RFC 8446 server certificate selection rule when a conforming chain is available.

Decision Reason

  • signature_algorithms_cert and signature_algorithms have different meanings under RFC 8446.
  • wolfSSL parses both, but its callback-oriented server certificate selection interface does not faithfully surface the certificate-signature constraint.
  • The focused TLS 1.3 runtime reproducer shows that this gap is observable on the wire: the client receives an ECDSA-signed certificate even though it advertised signature_algorithms_cert=RSA+SHA256 and an RSA-signed alternative certificate existed.

Therefore 0001 is a real issue for the supported callback-driven server certificate selection path.

Scope and Remaining Uncertainty

Confirmed scope:

  • This report is runtime-confirmed for the TLS 1.3 server certificate setup callback path.
  • It demonstrates a real mismatch between the RFC requirement and the behavior exposed by the supported callback-driven selection interface.

Not claimed here:

  • This report does not prove that every non-callback, preloaded, or internally selected multi-chain configuration in wolfSSL behaves the same way.
  • The confirmed issue here is specifically that the supported callback-based selection path can choose and send a non-conforming chain even when a conforming alternative exists.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions