Absent SSL Server Certificates
Recently I’ve been attempting to gain local control over an IoT gizmo that I own. Unfortunately the manufacturer seems competent and so far I’ve failed to do so. However, one of the dead ends I explored was mildly interesting.
I identified some open source software running on the thing. I was looking through it for programming errors when I came across some code using OpenSSL to connect out to the internet, which was structured like this:
int c = SSL_connect(ssl);
if (c != 1) {
return FAILURE;
}
X509 *x509 = SSL_get_peer_certificate(ssl);
if (x509) {
int v = SSL_get_verify_result(ssl);
if (v != X509_V_OK) {
return FAILURE;
}
} else {
/* no certificate - oh well */
}
/* ... continue to use connection ... */
They didn’t actually write “oh well” but it still made me stop and scratch my head. Apparently if you’re a MITM and you want to skip past the certificate pinning and verification, all you have to do is not provide a certificate. If that’s something you can choose to do, anyway…?
When I look at the documentation for SSL_get_peer_certificate
it’s clear that it could return null. This is logical because it’s the same function you use when getting the client’s certificate. I’m pretty familiar with client certificates and how they’re an optional thing, but what about the server?
Due to the protocol definition, a TLS/SSL server will always send a certificate, if present.
…if present? That’s not really what “always” means. This clearly deserves some investigation.
After doing some research and poking around in the guts of OpenSSL I discovered that there are three modes of operation where the server doesn’t actually need to send a certificate, all of which are a little obscure (to me).
All of these are supported by OpenSSL. openssl s_server
even has a -nocert
option so it’s easy to play with this yourself.
These three varieties of “non-certificate modes” are neatly summarised in OpenSSL’s client state machine, where it expects to get a certificate right after the Server Hello:
case TLS_ST_CR_SRVR_HELLO:
/* ... */
} else if (!(s->s3.tmp.new_cipher->algorithm_auth
& (SSL_aNULL | SSL_aSRP | SSL_aPSK))) {
if (mt == SSL3_MT_CERTIFICATE) {
st->hand_state = TLS_ST_CR_CERT;
return 1;
}
}
Notice the expectation that the server is providing a certificate now, which is what that SSL3_MT_CERTIFICATE
means. If we don’t reach that return
, we fall out of the switch and the connection fails. Unless we hit one of those three special categories, the server must supply a certificate and it will be parsed and a non-null value will be returned from SSL_get_peer_certificate
. I understand now what was going through the documentation author’s mind—it’s super strict about getting a server cert, except for those times when it isn’t.
So as a MITM, can I direct this device to connect to an OpenSSL server that activates one of these three modes? Sadly for me, I don’t think so.
First let’s consider the anonymous ciphersuites. OpenSSL supports lots of different ciphersuites, including some really bad ones, so they don’t want you to use just anything. For that reason ciphersuites are associated with a “security level” and unless you make an effort to turn on some insecure ones then they won’t be offered. According to the internet, the anon ciphersuites were disabled by default since OpenSSL 1.1.0 and you need to use a special syntax to enable them. My target code is of course not tinkering with the ciphersuites so this doesn’t help me very much, unless they were using a very old version of OpenSSL. (They were not.)
Then we have PSK and SRP. These both involve extra authentication steps and to prepare for it you must provide OpenSSL with a function pointers via methods like SSL_CTX_set_psk_client_callback
and SSL_CTX_set_srp_client_pwd_callback
. One of their test comments explains:
/*
* All ciphers in the DEFAULT cipherlist meet the default security level.
* However, default supported ciphers exclude SRP and PSK ciphersuites
* for which no callbacks have been set up.
* ...
This is easy to check. If you run a server like this:
openssl s_server -cert cert.pem -key key.pem -psk 1234 -port 4433 -debug
…and compare running clients like this:
openssl s_client -port 4433 -debug localhost
openssl s_client -port 4433 -debug -psk 1234 localhost
…in the second case, the server will log a much larger list of “Shared Ciphers” with PSK
in them.
That is, for either of these PSK or SRP modes to be available to me, the client code must have registered suitable callbacks in readiness for using them. They of course have not done so, since they never intended to support that. I also reviewed the Client Hello in my packet capture and it only indicates supported ciphersuites that would require me to present a valid certificate.
So the developers got away with it. I do wonder whether they knew all this and decided their verification code was safe, or whether they thought “servers always have a certificate” and got lucky thanks to implementation details of OpenSSL. If I was writing or reviewing that code, I would want to handle the null case explicitly.
For now I have still not hacked my gizmo but it’s nice to learn a few more things about TLS.