Proof of nothing: TLS 1.3 authentication bypasses in wolfSSL
- Identifier
- CVE-2022-25640
- Software
- wolfSSL
- Affected
- wolfSSL before 5.2.0
- Fixed in
- wolfSSL 5.2.0
- Reported by
- Aina Toky Rasoamanana and Olivier Levillain, Télécom SudParis
- Disclosed
- 15 June 2022
There is a family of testing tools that work by being systematically rude to a server. Open a connection, send a message it wasn't expecting, write down what comes back, hang up. Do it again with a different sequence. Repeat a few thousand times, feed the transcripts to a learning algorithm, and out comes a diagram. It shows the state machine the server is actually implementing, as opposed to the one in the RFC.
Aina Toky Rasoamanana, Olivier Levillain and Hervé Debar of Télécom SudParis did this at scale. Their ESORICS 2022 paper is Towards a Systematic and Automatic Use of State Machine Inference to Uncover Security Flaws and Fingerprint TLS Stacks. It covers over 400 different versions of server and client implementations in various scenarios. They used the L\* algorithm to compute a state machine for each one from stimuli and observations. Then they went looking for edges that had no business being there.
The paper reproduces a stack of known problems: denial of service, Bleichenbacher padding oracles, and the authentication bypasses CVE-2014-0224 and CVE-2015-0204. It also turns up fresh ones in wolfSSL, Erlang, matrixSSL, fizz and NSS, across TLS 1.0 through 1.3.
Two of the wolfSSL findings are what this post is about. wolfSSL's 5.2.0 release notes credit Rasoamanana and Levillain for both, and rate both High. Debar is the paper's third author, and the vendor credit line names the other two.
What CertificateVerify proves#
A TLS handshake is a fixed sequence of messages with fixed meanings. Being precise about which message does the work is worth the trouble, because that is where both bugs live.
When a party authenticates itself in TLS 1.3, it sends two things. First a Certificate message, which is the certificate chain. Then a CertificateVerify message. That one carries a signature over a transcript hash of everything said so far, made with the private key belonging to the certificate at the front of that chain.
The Certificate message on its own proves nothing whatsoever. Certificates are public. You can get anybody's by connecting to their server, and then send it to somebody else and claim it's yours. The entire weight of authentication rests on CertificateVerify. That is the only part an impostor can't produce without the private key.
Which means a TLS implementation has two jobs, not one. It has to check the signature in CertificateVerify properly. And it has to notice when no CertificateVerify arrived at all.
Skipping CertificateVerify (CVE-2022-25640)#
Here is the first one, quoted from the release notes:
[High] A TLS v1.3 server who requires mutual authentication can be bypassed. If a malicious client does not send the certificate_verify message a client can connect without presenting a certificate even if the server requires one. Thank you to Aina Toky Rasoamanana and Olivier Levillain of Télécom SudParis. CVE-2022-25640
Mutual authentication is the mode where the server wants a certificate from the client too, not only the other way round. It is how a lot of internal service-to-service traffic, VPN infrastructure and IoT fleet management works. The server's access control is "did this connection present a certificate my CA signed". If that check can be skipped, the answer to "who are you" becomes "whoever you like".
A malicious client doesn't need a certificate, or a forged signature, or a padding oracle. It skips a message. Server sends CertificateRequest, client ignores it, sends Finished, and the handshake completes.
The reason this class of bug keeps happening is that checking for absence is structurally different from checking for presence. Verifying a signature is something you do when a message arrives. There is an obvious place to put the code. Get it wrong and the test suite notices, because a handshake that should succeed fails instead.
Verifying that a message arrived at all has no natural home. Nothing triggers it. You need a deliberate checkpoint at the end of the handshake, one that walks the list of everything this configuration required and confirms each one happened. If that checkpoint doesn't exist, nothing in your test suite will ever ask for it, because well-behaved clients always send the message.
Mismatched sig_algo (CVE-2022-25638)#
The second one is subtler:
[High] A TLS v1.3 client attempting to authenticate a TLS v1.3 server can have its certificate check bypassed. If the sig_algo in the certificate_verify message is different than the certificate message checking may be bypassed. Thank you to Aina Toky Rasoamanana and Olivier Levillain of Télécom SudParis. CVE-2022-25638
This is the other direction. An ordinary client connects to a hostile server and fails to authenticate it, which is TLS not doing the one thing everybody installs it for.
CertificateVerify carries two fields. The signature itself, and a SignatureScheme identifier saying how it was made: rsa_pss_rsae_sha256, ecdsa_secp256r1_sha256, and so on. The receiver reads that identifier to work out which verification routine to run, because you can't verify an ECDSA signature with RSA code.
There's an obvious constraint that nothing in the message itself enforces. The announced algorithm has to be compatible with the public key in the Certificate message that arrived a moment earlier. A chain ending in an ECDSA P-256 key cannot produce an RSA-PSS signature. If a server announces one algorithm and presents a key for another, it is lying, and the handshake should end there. In the transcript below it presents no certificate at all.
wolfSSL's note is short, and the paper gives the transcript behind it. The client is sent an empty Certificate message, and then a CertificateVerify message containing an unknown signature algorithm and an arbitrary payload. No key, no signature worth the name. A wolfSSL client up to 5.1.0 goes on to complete the handshake anyway.
That transcript is the third route the authors found to the same destination in the same code. CVE-2020-24613, from 2020, was a wolfSSL TLS 1.3 client that accepted a handshake with CertificateVerify skipped altogether. Their inference run reproduced it. Then they widened the vocabulary of messages the tool was allowed to send, which turned up an empty Certificate followed by a CertificateVerify signed with an arbitrary RSA key. That one became CVE-2021-3336. Widening it again produced the unknown-algorithm variant, CVE-2022-25638. Each time the fix closed the edge that had been demonstrated, and each time there was another edge into the same accepting state.
The fix in 5.2.0#
wolfSSL 5.2.0 was released on 21 February 2022 and fixes both. Its list of new features carries this line: "For TLS 1.3, improved checks on order of received messages." That is the same finding stated as an improvement rather than a CVE. Somebody hands you a machine-inferred diagram of your own protocol implementation and points at two edges. What you generally discover is that the whole ordering layer was doing less than you thought, so you go and fix the layer rather than the two edges.
Neither bug is a memory-safety problem, so a fuzzer pointed at message contents would run forever without finding either. Both show up immediately in a picture of the reachable states with the accepting ones marked. A human looking at that picture sees a path from "the peer hasn't authenticated" to "handshake complete", and doesn't need to be told it's wrong.
CVE-2022-25640 and CVE-2022-25638 were both fixed in wolfSSL 5.2.0. If you want the method rather than the instances, read Rasoamanana, Levillain and Debar's paper, where the two wolfSSL bugs are a few lines in a much larger table.