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:
- the client sends a narrow
signature_algorithms_cert list,
- the server has a conforming alternative certificate available,
- the callback still chooses a non-conforming certificate because it sees only the broader public signature list, and
- 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.
Server certificate callback can ignore
signature_algorithms_certand send an unacceptable certificate chainExecutive Summary
wolfSSL parses the client's
signature_algorithms_certextension, 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 broadersignature_algorithmslist 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
4.4.2.2 Server Certificate Selection3720-3724Standard basis:
signature_algorithms_certapplies to signatures that appear in certificates, whilesignature_algorithmsapplies toCertificateVerifysignatures. See RFC 8446 Section 4.2.3.Interpretation:
signature_algorithms_cert, that narrower list constrains certificate signatures, not just the handshake signature.CertificateVerifyuses 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_certsrc/tls.c:8075parses the extension and stores it inssl->certHashSigAlgo/ssl->certHashSigAlgoSz.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:4302exposesssl->clSuites->hashSigAlgothroughwolfSSL_get_client_suites_sigalgs().That helper does not return
ssl->certHashSigAlgo. So a callback that uses the public helper sees theCertificateVerifysignature list, not the certificate-signature list.3. The server callback runs before certificate sending
src/ssl_api_cert.c:2674defines the internal callback wrapper:The TLS 1.3 server path invokes that callback during ClientHello processing at
src/tls13.c:7975: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:9739sends the certificate currently loaded intossl->buffers.certificateandssl->buffers.certChain.There is no server-side filter here that re-checks the loaded chain's certificate signatures against
ssl->certHashSigAlgobefore sending.Runtime Reproduction
Reproduction goal
Prove a clean server-side selection mismatch where:
signature_algorithms_certlist,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 algorithmsha256WithRSAEncryptioncerts/server-ecc.pem: leaf certificate signature algorithmecdsa-with-SHA256These 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:707andcerts/renewcerts.sh:747.Client behavior in the reproducer:
signature_algorithms_certis narrowed toRSA+SHA256signature_algorithmslist still includes ECDSAServer callback behavior in the reproducer:
ssl->clSuites->hashSigAlgossl->certHashSigAlgofor debuggingserver-ecc.pemwhenever ECDSA appears in the public listObserved output
The reproduced run on August 3, 2026 produced:
What this proves:
signature_algorithms_cert;sigalgs_cert_present=true.cert_sigalgs=RSA+SHA256.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:
That behavior conflicts with the RFC 8446 server certificate selection rule when a conforming chain is available.
Decision Reason
signature_algorithms_certandsignature_algorithmshave different meanings under RFC 8446.signature_algorithms_cert=RSA+SHA256and an RSA-signed alternative certificate existed.Therefore
0001is a real issue for the supported callback-driven server certificate selection path.Scope and Remaining Uncertainty
Confirmed scope:
Not claimed here: