> OpenSSL Cookbook: Chapter 2. Testing TLS with OpenSSL


Contents
Previous
Next

2 Testing TLS with OpenSSL

Due to the large number of protocol features and implementation quirks, it’s sometimes difficult to determine the exact configuration and features of secure servers. Although many tools exist for this purpose, it’s often difficult to know exactly how they work, and that sometimes makes it difficult to fully trust their results. Even though I spent years testing secure servers and have access to good tools, when I really want to understand what is going on, I resort to using OpenSSL and Wireshark. I am not saying that you should use OpenSSL for everyday testing; on the contrary, you should find an automated tool that you trust. For online testing, I recommend Hardenize;22 for offline work, consider testssl.sh.23 But when you really need to be certain of something, the only way is to get your hands dirty with OpenSSL.

Using OpenSSL for testing purposes has become more difficult recently because, paradoxically, OpenSSL itself got better. In the aftermath of Heartbleed, the OpenSSL developers undertook a great overhaul, one aspect of which was removal of obsolete cryptography. That is great news for everyone, of course, but does make our lives more difficult. To test for a wide variety of conditions, we may need to use two versions: one recent and one old. The recent one is useful to test modern features (e.g., TLS 1.3), but the old one is what you need to test obsolete functionality.

At the time of writing, the new version will most definitely be from the 1.1.1 branch. As for the old, after some research, I settled on OpenSSL 1.0.2g, configured so that the removal of some obsolete features is reverted:

$ ./config \
--prefix=/opt/openssl-1.0.2g \
--openssldir=/opt/openssl-1.0.2g \
no-shared \
enable-ssl2 \
enable-ssl3 \
enable-weak-ssl-ciphers

Throughout this chapter, I will refer to these two versions of OpenSSL as new and old. That’s how you’ll know which version to use for the testing. Refer to the previous chapter for more information on how to configure and install OpenSSL.

OpenSSL comes with a client tool that you can use to connect to a secure server. The tool is similar to telnet or nc in the sense that it handles the encryption aspect but allows you to fully control the layer that comes next.

To connect to a server, you need to supply a hostname and a port. For example:

$ openssl s_client -crlf \
-connect www.feistyduck.com:443 \
-servername www.feistyduck.com

Notice that you had to supply the hostname twice. The -connect switch is used to establish the TCP connection, but -servername is used to specify the hostname sent at the TLS level. Starting with OpenSSL 1.1.1, the s_client tool automatically configures the latter. You’ll still need to use the -servername switch if (1) you’re using an earlier version of OpenSSL, (2) you’re connecting to an IP address, or (3) the TLS host needs to be different. Use the -noservername switch to avoid sending hostname information in the TLS handshake.

Once you type the command, you’re going to see a lot of diagnostic output (more about that in a moment) followed by an opportunity to type whatever you want. Because we’re talking to an HTTP server, the most sensible thing to do is to submit an HTTP request. In the following example, I use a HEAD request because it instructs the server not to send the response body:

HEAD / HTTP/1.0
Host: www.feistyduck.com

HTTP/1.1 200 OK
Date: Mon, 24 Aug 2020 16:38:02 GMT
Server: Apache
Strict-Transport-Security: max-age=31536000; includeSubDomains; preload
Cache-control: no-cache, must-revalidate
Content-Type: text/html;charset=UTF-8
Transfer-Encoding: chunked
Set-Cookie: JSESSIONID=882D48C8842EA82E3F3AFACC4425A695; Path=/; Secure; HttpOnly
Connection: close

read:errno=0

Note

If, when connecting to a remote server in this way, the TLS handshake completes but you’re getting disconnected after the first HTTP request line, check that you’ve specified the -crlf switch on the command line. This switch ensures that the newlines you type are translated to a carriage return plus line feed combo to ensure string HTTP compliance.

Now we know that the TLS communication layer is working: we got through to the HTTP server, submitted a request, and received a response back. Let’s go back to the diagnostic output. The first couple of lines will show the information about the server certificate:

CONNECTED(00000003)
depth=2 C = GB, ST = Greater Manchester, L = Salford, O = COMODO CA Limited, CN = ↩
COMODO RSA Certification Authority
verify return:1
depth=1 C = GB, ST = Greater Manchester, L = Salford, O = COMODO CA Limited, CN = ↩
COMODO RSA Domain Validation Secure Server CA
verify return:1
depth=0 OU = Domain Control Validated, OU = PositiveSSL, CN = www.feistyduck.com
verify return:1

The next section in the output lists all the certificates presented by the server in the order in which they were delivered:

Certificate chain
 0 s:OU = Domain Control Validated, OU = PositiveSSL, CN = www.feistyduck.com
   i:C = GB, ST = Greater Manchester, L = Salford, O = COMODO CA Limited, CN = COMODO ↩
RSA Domain Validation Secure Server CA
 1 s:C = GB, ST = Greater Manchester, L = Salford, O = COMODO CA Limited, CN = COMODO ↩
RSA Domain Validation Secure Server CA
   i:C = GB, ST = Greater Manchester, L = Salford, O = COMODO CA Limited, CN = COMODO ↩
RSA Certification Authority

For each certificate, the first line shows the subject and the second line shows the issuer information.

This part is very useful when you need to see exactly what certificates are sent; browser certificate viewers typically display reconstructed certificate chains that can be almost completely different from the presented ones. To determine if the chain is nominally correct, you might wish to verify that the subjects and issuers match. You start with the leaf (web server) certificate at the top, and then you go down the list, matching the issuer of the current certificate to the subject of the next. The last issuer you see can point to some root certificate that is not in the chain, or—if the self-signed root is included—it can point to itself.

The next item in the output is the server certificate; it’s a lot of text, but I’m going to remove most of it for brevity:

Server certificate
-----BEGIN CERTIFICATE-----
MIIFUzCCBDugAwIBAgIRAPR/CbWZEksfCIRqxNcesPIwDQYJKoZIhvcNAQELBQAw
gZAxCzAJBgNVBAYTAkdCMRswGQYDVQQIExJHcmVhdGVyIE1hbmNoZXN0ZXIxEDAO
BgNVBAcTB1NhbGZvcmQxGjAYBgNVBAoTEUNPTU9ETyBDQSBMaW1pdGVkMTYwNAYD
[...]
L1MPjFiB5pyvf9jDBxv8TmG4Q6TnDDhw2t2Qil6lhsPAMZ9odP22W3uaLE1y7aB6
zbQXjVsc3E1THfFZWRzDPsU4fN/1iGlbrcAWa2sFfhJXrCDfAowFJ8A1n9jMiNEG
WfQfGgA2ar2xUtsqA7Re6XlXOlwBPuQ=
-----END CERTIFICATE-----
subject=OU = Domain Control Validated, OU = PositiveSSL, CN = www.feistyduck.com
issuer=C = GB, ST = Greater Manchester, L = Salford, O = COMODO CA Limited, CN = COMODO ↩
RSA Domain Validation Secure Server CA

Note

By default, the s_client tool shows just the leaf certificate. If you wish to obtain the entire chain, use the -showcerts switch.

If you want to have a better look at the certificate, you’ll first need to copy it from the output and store it in a separate file. I’ll discuss how to do that in the next section.

The following is a lot of information about the TLS connection, most of which is self-explanatory:

---
No client certificate CA names sent
Peer signing digest: SHA512
Peer signature type: RSA
Server Temp Key: ECDH, P-256, 256 bits
---
SSL handshake has read 3624 bytes and written 446 bytes
Verification: OK
---
New, TLSv1.2, Cipher is ECDHE-RSA-AES128-GCM-SHA256
Server public key is 2048 bit
Secure Renegotiation IS supported
Compression: NONE
Expansion: NONE
No ALPN negotiated
SSL-Session:
    Protocol  : TLSv1.2
    Cipher    : ECDHE-RSA-AES128-GCM-SHA256
    Session-ID: 73FC4831AF053C46291C2D8CC90BF7F1D5B12178E488FBB4DC49A302B870E8DE
    Session-ID-ctx: 
    Master-Key: E60DA9C6669C2C7DFFD8A3AD2CD17405CC0B9B69C4184469D779A9BA19A6FD4B3D602A02↩
3BD8B23F8D9A9FF2CBB5DDF7
    PSK identity: None
    PSK identity hint: None
    SRP username: None
    TLS session ticket lifetime hint: 300 (seconds)
    TLS session ticket:
    0000 - 31 95 ff d0 4c 42 dd d0-24 64 03 5c fc 55 1d 17   1...LB..$d.\.U..
    0010 - 05 c4 61 1f b8 ba fd fe-f7 6c 6c e9 ae a2 49 3f   ..a......ll...I?
    0020 - c5 19 d4 e9 69 a5 79 d5-af 13 26 c8 2c e7 f0 01   ....i.y...&.,...
    0030 - 3b 42 d8 c0 29 4c fa 7e-88 aa 8d c8 0b 30 96 ce   ;B..)L.~.....0..
    0040 - 43 40 2c 09 0b aa 2e d5-61 e3 34 7a a3 78 2f 93   C@,.....a.4z.x/.
    0050 - 67 5a b9 96 78 f5 e7 69-b7 b6 2d 8c 00 8f 04 ab   gZ..x..i..-.....
    0060 - 42 1d 26 db 92 ec 2d 2f-ba 1c c6 61 87 64 0e d5   B.&...-/...a.d..
    0070 - f2 ce 20 d0 07 a5 e2 6d-c6 45 50 c2 45 14 a8 ee   .. ....m.EP.E...
    0080 - 59 7c 63 e1 d7 d8 b0 b6-76 21 d2 13 97 eb bd 97   Y|c.....v!......
    0090 - a1 d3 e8 5c 61 da da 2d-85 80 db ae de 56 97 e1   ...\a..-.....V..
    00a0 - e8 7a 25 f9 bf cf b6 18-48 5b b0 03 a5 e6 ec 0a   .z%.....H[......
    00b0 - bf 2f 0d 1a 6b ae 79 10-80 9c cf 4d 66 8f 90 43   ./..k.y....Mf..C
    00c0 - 69 54 32 be 0c 89 57 e8-6d 81 b5 3e 5b cb 5e 8e   iT2...W.m..>[.^.

    Start Time: 1598288068
    Timeout   : 7200 (sec)
    Verify return code: 0 (ok)
    Extended master secret: no

The most important information here is the protocol version (TLS 1.2) and cipher suite used (ECDHE-RSA-AES128-GCM-SHA256). Do note that protocol information appears in two locations, which is potentially confusing when different versions are shown. The first location describes the minimum protocol requirement with the negotiated cipher suite, while the second location points to the actual protocol version currently being negotiated. You will see a difference in protocol versions with some older cipher suites—for example:

New, TLSv1/SSLv3, Cipher is DHE-RSA-AES128-SHA

The selected suite could be used with SSL 3.0, but it’s used with TLS 1.2 on this connection:

    Protocol  : TLSv1.2
    Cipher    : DHE-RSA-AES128-SHAs

You can also determine that the server has issued to you a session ID and a TLS session ticket (a way of resuming sessions without having the server maintain state) and that secure renegotiation is supported.

Note

If you’re connecting to a TLS 1.3 server, the output may be different. Sometimes you will observe less information initially, with additional information arriving later in bursts. This behavior depends on the implementation and reflects the changes in TLS 1.3, which transmits session tickets as separate protocol messages that are sent only after the handshake is complete. Additionally, multiple session tickets are usually sent on the same connection.

Just because you are able to connect to a TLS server, that doesn’t mean that the service is configured correctly, even if the server supports all the right protocols and cipher suites. It is equally important that the configured certificate matches the correct DNS names.

By default, the s_client tool reports but otherwise ignores certificate issues. Further, before you begin to trust its judgment you need to be confident that it can recognize a valid certificate when it sees one. This is especially true when you’re using a custom-compiled binary.

In the example from the previous section, the verification status code (shown on the penultimate line) was 0, which means that the verification has been successful. If you’re connecting to a server that has a valid public certificate but you see status 20 instead, that probably means that trusted roots haven’t been correctly configured:

Verify return code: 20 (unable to get local issuer certificate)

At this point, if you don’t wish to fix your OpenSSL installation, you can instead use the -CApath switch to point to the location where the roots are kept. For example:

$ openssl s_client -connect www.feistyduck.com:443 -CApath /etc/ssl/certs/

If you instead have a single file with the roots in it, use the -CAfile switch:

$ openssl s_client -connect www.feistyduck.com:443 \
-CAfile /etc/ssl/certs/ca-certificates.crt

Even if you get a successful status code at this point, that doesn’t mean that the certificate is correctly configured. That’s because the s_client tool doesn’t check that the certificate is correct for the given hostname; you have to tell it to do that manually and tell it which hostname to use:

$ openssl s_client -connect www.feistyduck.com:443 -verify_hostname www.feistyduck.com

If there is a mismatch, you might see status code 62:

Verify return code: 62 (Hostname mismatch)

Otherwise, you’ll see the familiar status code 0. In the rare instance that you need to verify a certificate that has been issued for an IP address instead of a hostname, you’ll need to use the -verify_ip switch for the verification.

When used with HTTP, TLS wraps the entire plaintext communication channel to form HTTPS. Some other protocols start off as plaintext, but then they upgrade to encryption. If you want to test such a protocol, you’ll have to tell OpenSSL which protocol it is so that it can upgrade on your behalf. Provide the protocol information using the -starttls switch. For example:

$ openssl s_client -connect gmail-smtp-in.l.google.com:25 -starttls smtp

At the time of writing, the supported protocols in recent OpenSSL releases are smtp, pop3, imap, ftp, xmpp, xmpp-server, irc, postgres, mysql, lmtp, nntp, sieve, and ldap. There is less choice with OpenSSL 1.0.2g: smtp, pop3, imap, ftp, and xmpp.

Some protocols require the client to provide their names. For example, for SMTP, OpenSSL will use mail.example.com by default, but you can specify the correct value with the -name switch. If you’re testing XMPP, you may need to specify the correct server name; you can do this with the -xmpphost switch.

When you connect to a remote secure server using s_client, it will dump the server’s PEM-encoded certificate to standard output. If you need the certificate for any reason, you can copy it from the scroll-back buffer. If you know in advance you only want to retrieve the certificate, you can use this command line as a shortcut:

$ echo | openssl s_client -connect www.feistyduck.com:443 2>&1 | sed --quiet '/-BEGIN ↩
CERTIFICATE-/,/-END CERTIFICATE-/p' > feistyduck.crt

The purpose of the echo command at the beginning is to separate your shell from s_client. If you don’t do that, s_client will wait for your input until the server times out (which may potentially take a very long time).

By default, s_client will print only the leaf certificate; if you want to print the entire chain, give it the -showcerts switch. With that switch enabled, the previous command line will place all the certificates in the same file.

$ echo | openssl s_client -showcerts -connect www.feistyduck.com:443 2>&1 | sed --quiet ↩
'/-BEGIN CERTIFICATE-/,/-END CERTIFICATE-/p' > feistyduck.chain

Another useful trick is to pipe the output of s_client directly to the x509 tool. The following command shows detailed server information, along with its SHA256 fingerprint:

$ echo | openssl s_client -connect www.feistyduck.com:443 2>&1 | openssl x509 -noout ↩
-text -fingerprint -sha256

Sometimes you will need to take the certificate fingerprint and use it with other tools. Unfortunately, OpenSSL outputs certificates in a format that shows individual bytes and separates them using colons. This handy command line normalizes certificate fingerprints by removing the colons and converting the hexadecimal characters to lowercase:

$ echo | openssl s_client -connect www.feistyduck.com:443 2>&1 | openssl x509 -noout ↩
-fingerprint -sha256 | sed 's/://g' | tr '[:upper:]' '[:lower:]' | sed 's/sha256 ↩
fingerprint=//g'

Note

Connecting to remote TLS servers and reviewing their certificates is a pretty common operation, but you shouldn’t spend your time remembering and typing these long commands. Instead, invest in writing a couple of shell functions that will package this functionality into easy-to-use commands.

By default, s_client will try to use the best protocol to talk to the remote server and report the negotiated version in output. As mentioned earlier, you will find the protocol version in the output twice, and you want the line that explicitly talks about the protocol:24

    Protocol  : TLSv1.2

If you need to test support for specific protocol versions, you have two options. You can explicitly choose one protocol to test by supplying one of the -ssl2, -ssl3, -tls1, -tls1_1, -tls1_2, or tls1_3 switches. Naturally, each switch requires support for a specific protocol version in the testing tool. If you want to exclude a particular protocol from the testing, there is a family of switches that disable protocols (e.g., -no_tls_1_2 for TLS 1.2). Sometimes that may be the better approach. Starting with OpenSSL 1.1.0, there are two new options, -min_protocol and -max_protocol, which control the minimum and maximum protocol version, respectively.

For example, here’s the output you might get when testing a server that doesn’t support a certain protocol version:

$ openssl s_client -connect www.example.com:443 -tls1_2
CONNECTED(00000003)
140455015261856:error:1408F10B:SSL routines:SSL3_GET_RECORD:wrong version number:s3↩
_pkt.c:340:
---
no peer certificate available
---
No client certificate CA names sent
---
SSL handshake has read 5 bytes and written 7 bytes
---
New, (NONE), Cipher is (NONE)
Secure Renegotiation IS NOT supported
Compression: NONE
Expansion: NONE
SSL-Session:
    Protocol  : TLSv1.2
    Cipher    : 0000
    Session-ID:
    Session-ID-ctx:
    Master-Key:
    Key-Arg   : None
    PSK identity: None
    PSK identity hint: None
    SRP username: None
    Start Time: 1339231204
    Timeout   : 7200 (sec)
    Verify return code: 0 (ok)
---

Understanding if a server supports SSL 2.0 may sometimes require more work, due to the fact that this old and very insecure version of the SSL protocol uses a different handshake from that used from SSL 3.0 onward. Although servers that support only SSL 2.0 should now be very rare, to check this eventuality, you’ll need to submit a separate check using the -ssl2 switch.

Another protocol difference is that SSL 2.0 servers are sometimes seen without any configured cipher suites. In that case, although SSL 2.0 is supported, technically speaking, any handshake attempts will still fail. You should treat this situation as misconfiguration.

It’s not very likely that you will be spending a lot of time testing cipher suite configuration using OpenSSL on the command line. This is because you can effectively test for only one suite at a time; testing for more than 300 cipher suites that are supported by TLS 1.2 and earlier protocol revisions would take a considerable amount of time. This is a perfect opportunity to use those handy tools that automate the process.

Still, there will be times when you will need to probe servers to determine if they support a particular suite or a cryptographic primitive, or if the preference is correctly configured.

The introduction of TLS 1.3 made testing in this area slightly more complicated, but it’s still manageable. Because of the differences between this protocol version and all other revisions, it’s usually best to split your tests into two groups. When testing TLS 1.3, always use the -ciphersuites switch in combination with -tls1_3. The usual approach is to specify only one suite to determine if it’s supported:

$ echo | openssl s_client -connect www.hardenize.com:443 -tls1_3 -ciphersuites TLS_AES↩
_128_GCM_SHA256 2>/dev/null| grep New
New, TLSv1.3, Cipher is TLS_AES_128_GCM_SHA256

The output will naturally be different if you pick a suite that is not supported:

$ echo | openssl s_client -connect www.hardenize.com:443 -tls1_3 -ciphersuites TLS_AES↩
_128_CCM_SHA256 2>/dev/null | grep New
New, (NONE), Cipher is (NONE)

When you’re testing the configuration of TLS 1.2 and earlier protocol versions, use the -cipher switch in combination with -no_tls1_3 (assuming you’re using a version of OpenSSL that supports TLS 1.3):

$ echo | openssl s_client -connect www.hardenize.com:443 -no_tls1_3 -cipher AESGCM ↩
2>/dev/null | grep New
New, TLSv1.2, Cipher is ECDHE-ECDSA-AES128-GCM-SHA256

As you can see in the previous example, when testing TLS 1.2 and earlier you don’t have to specify only one cipher suite, but in that case you will need to observe what has been negotiated. If you want to probe further, you can always tweak the command line to remove the previously negotiated suite:

$ echo | openssl s_client -connect www.hardenize.com:443 -no_tls1_3 -cipher ↩
'AESGCM:!ECDHE-ECDSA-AES128-GCM-SHA256' 2>/dev/null | grep New
New, TLSv1.2, Cipher is ECDHE-ECDSA-AES256-GCM-SHA384

Even though you won’t be testing for a great many suites manually, there is a quick way to determine if a particular server supports any of the many bad cryptographic primitives. To do this, use your old OpenSSL version and list all the bad cipher suite keywords, like this:

$ echo | openssl s_client -connect example.com:443 -cipher '3DES DES RC2 RC4 IDEA SEED ↩
CAMELLIA MD5 aNULL eNULL EXPORT LOW' 2>/dev/null | grep New
New, TLSv1/SSLv3, Cipher is DHE-RSA-CAMELLIA256-SHA

Another good test is to see if a server supports the RSA key exchange that doesn’t support forward secrecy:

$ echo | /opt/openssl-1.0.2g/bin/openssl s_client -connect example.com:443 -cipher k↩
RSA 2>/dev/null | grep New
New, TLSv1/SSLv3, Cipher is AES128-GCM-SHA256

Ideally, you’d get a handshake failure here, but it’s not terrible if you don’t, provided the server uses the RSA key exchange only as a matter of last resort. You can check this by offering suites with forward secrecy as your least preferred option:

$ echo | /opt/openssl-1.0.2g/bin/openssl s_client -connect example.com:443 -cipher 'DHE ↩
ECDHE kRSA +kECDHE +kDHE' 2>/dev/null | grep New
New, TLSv1/SSLv3, Cipher is ECDHE-RSA-AES128-GCM-SHA256

As a general rule, TLS servers should always be configured to enforce their cipher suite preferences, ensuring that they negotiate their preferred cipher suite with every client. This feature is essential with TLS 1.2 and earlier protocol revisions, which support many cipher suites, most of them undesirable. It’s a different story with TLS 1.3: it only has a handful of suites available at this time and all of them are secure, so enforcing server preference doesn’t matter that much.25

Note

Because cipher suite preference doesn’t matter much with TLS 1.3, some stacks don’t even support it with this protocol, even if they do with earlier protocol versions. Thus, for the best results, you will want to test separately for TLS 1.3 and everything else—or separately for every supported protocol. This is another case in which automation is the better choice.

To test for server suite preference, you first need to have some idea of what suites are supported. For example, you could have the complete list of supported suites. Alternatively, you can probe the server with different suite types—for example, those that use ECDHE versus DHE or RSA key exchange.

With two suites in hand, you need to initiate two connections, first offering one of the suites as your first choice, then the other:

$ echo | openssl s_client -connect www.hardenize.com:443 -tls1_3 -ciphersuites 'TLS_AES↩
_128_GCM_SHA256:TLS_AES_256_GCM_SHA384' 2>/dev/null | grep New
New, TLSv1.3, Cipher is TLS_AES_128_GCM_SHA256
zoom:~ ivanr$ echo | openssl s_client -connect www.hardenize.com:443 -tls1_3 ↩
-ciphersuites 'TLS_AES_256_GCM_SHA384:TLS_AES_128_GCM_SHA256' 2>/dev/null | grep New
New, TLSv1.3, Cipher is TLS_AES_256_GCM_SHA384

If you see the same suite negotiated on both connections, that means that the server is configured to actively select negotiated suites. Otherwise, it isn’t. The server in the previous example is one of those TLS 1.3 servers that doesn’t enforce preference. That very same server does have a preference with TLS 1.2; we can see that it always selects a better suite, even when we push it to the end of our list:

$ echo | openssl s_client -connect www.hardenize.com:443 -no_tls1_3 -cipher ↩
'ECDHE+AESGCM RSA' 2>/dev/null | grep New
New, TLSv1.2, Cipher is ECDHE-ECDSA-AES128-GCM-SHA256
$ echo | openssl s_client -connect www.hardenize.com:443 -no_tls1_3 -cipher ↩
'ECDHE+AESGCM RSA +ECDHE' 2>/dev/null | grep New
New, TLSv1.2, Cipher is ECDHE-ECDSA-AES128-GCM-SHA256

When it comes to server suite preference testing, the ChaCha20 suites are best avoided. This is because some servers support another type of preference, where they treat AES-GCM and ChaCha20 suites as equal in terms of security and respect client preference as a special case. The idea is that the client will prefer the faster cipher suite, which is typically ChaCha20 for mobile devices and AES-GCM for desktops.

That said, with servers that support this type of preference, you may want to test if it’s working correctly. To do that, you’ll need to use three supported cipher suites and three tests. The purpose of the first two tests is to establish that the server selects its favorite suite when ChaCha20 is not involved:

$ echo | openssl s_client -connect www.hardenize.com:443 -no_tls1_3 -cipher ↩
'ECDHE-RSA-AES128-GCM-SHA256 ECDHE-ECDSA-AES128-GCM-SHA256 ECDHE-ECDSA-CHACHA20-POLY1305↩
'  2>/dev/null | grep New
New, TLSv1.2, Cipher is ECDHE-ECDSA-AES128-GCM-SHA256
$ echo | openssl s_client -connect www.hardenize.com:443 -no_tls1_3 -cipher ↩
'ECDHE-ECDSA-AES128-GCM-SHA256 ECDHE-RSA-AES128-GCM-SHA256  ECDHE-ECDSA-CHACHA20-POLY130↩
5'  2>/dev/null | grep New
New, TLSv1.2, Cipher is ECDHE-ECDSA-AES128-GCM-SHA256

If you see that the server responds with the same suite in both cases, you can submit another test with a supported ChaCha20 suite first. If you see the server selecting it, you know it’s configured to support the client-preferred suite:

$ echo | openssl s_client -connect www.hardenize.com:443 -no_tls1_3 -cipher ↩
'ECDHE-ECDSA-CHACHA20-POLY1305 ECDHE-RSA-AES128-GCM-SHA256 ECDHE-ECDSA-AES128-GCM-SHA256↩
'  2>/dev/null | grep New
New, TLSv1.2, Cipher is ECDHE-ECDSA-CHACHA20-POLY1305

Named groups are predefined cryptographic parameters that are used for key exchange. In TLS 1.3, named groups include both elliptic curve (EC) and finite field (DH) parameters. TLS 1.2 and earlier generally use only predefined elliptic curves; the server provides DH parameters on every connection.26 In a handshake, the client and server have to agree on a common named group over which the key exchange will take place, and it’s important that the selected group satisfies desired security requirements.

In practice, there is seldom a need to test servers for named groups. Although there’s a fair number of named groups in various RFCs, OpenSSL is probably the only major client to have extensive support. Historically, you could only use NIST’s P-256 and P-384 EC groups because these were the only widely supported curves. Relatively recently, X25519 and X448 groups were added as an alternative. Because all these curves are strong, there is little need to spend time thinking about them.

You may find yourself testing named group configuration usually to understand what your web server is doing. For example, you may care about X25519 and want to ensure it’s available and preferred. To test for this, use the s_client tool and the -curves switch. For example, here’s how to determine if a single named group is supported:

$ echo | openssl s_client -connect hardenize.com:443 -curves X25519 2>/dev/null | grep ↩
"Server Temp Key"
Server Temp Key: X25519, 253 bits

On success, you will see the named group in the output, because that’s the group that was selected for the handshake. On failure, you may see no output, which means that the handshake failed. Alternatively, the server, unable to negotiate an ECDHE suite, may fall back to a DHE suite, indicated by the following output:

Server Temp Key: DH, 2048 bits

If you need to test for named group preference, you need to offer two or more named groups, with your preferred one last. If you see it negotiated, that will mean that the server actively chooses the group it considers most appropriate. Use colons to separate the groups and be aware that the names are case-sensitive.

$ echo | openssl s_client -connect hardenize.com:443 -curves prime256v1:X25519 2>/dev↩
/null | grep "Server Temp Key"
Server Temp Key: X25519, 253 bits

Note

You can get the complete list of elliptic curves supported by OpenSSL using the ecparam tool and the -list_curves switch. To that list, add X25519 and X448. Support for finite field groups is currently not available but should arrive with OpenSSL 3.0.

DNS-based Authentication of Named Entities (DANE) is a set of standards that enables you to endorse the TLS certificates you use via DNS configuration. For this to work, DANE requires DNS itself to be secure, which means that DNSSEC is necessary. Therefore, DANE is essentially a mechanism for pinning; only the certificates you approve will be accepted as valid by DANE-enabled clients. DANE itself is not controversial, but DNSSEC, on which it relies, is a very divisive topic, with the world split between those who love it and those who hate it. As a result, DANE is currently not universally supported. It’s more commonly used to secure SMTP servers; there is no support at the browser level.

Supporting DANE adds some complexity to your TLS deployments because of the way DNS configuration is propagated and cached. Before you use a new certificate you need to ensure that your new DNS configuration (endorsing that certificate) is fully propagated. Thus, you would typically first publish your DNS changes, wait for a period time sufficient for the caches to clear, and only then deploy the certificates.27

The testing itself is straightforward; you use the s_client tool while feeding it DANE data. This is handy because it enables you to test a connection even before making DNS changes. First, let’s see what DANE configuration looks like.

DANE stores configuration in TLSA resource records, using two prefix labels to indicate the protocol and port:

$ host -t TLSA _25._tcp.mail.protonmail.ch
_25._tcp.mail.protonmail.ch has TLSA record 3 1 1 76BB66711DA416433CA890A5B2E5A0533C6006↩
478F7D10A4469A947A CC8399E1
_25._tcp.mail.protonmail.ch has TLSA record 3 1 1 6111A5698D23C89E09C36FF833C1487EDC1B0C↩
841F87C49DAE8F7A09 E11E979E

This output contains two endorsements, one per certificate. Having two endorsements is not unusual. For example, perhaps you might have a service that uses two certificates (e.g., one with an RSA key and another with an ECDSA key), or you have a backup certificate, or you’re simply in a transitional period when you’re switching certificates. The three numbers at the beginning indicate that the endorsement targets the certificate directly (3) via its public key (1) and a SHA256 hash (1). The rest of the data is the hash itself.

To test, you connect to the SMTP service while providing the DANE data using the -dane_tlsa_domain and -dane_tlsa_rrdata switches:

$ openssl s_client -starttls smtp \
-connect mail.protonmail.ch:25 \
-dane_tlsa_domain mail.protonmail.ch \
-dane_tlsa_rrdata "3 1 1 76BB66711DA416433CA890A5B2E5A0533C6006478F7D10A4469A947ACC8399E↩
1"

If the verification is successful, you will see something like this in the output:

---
SSL handshake has read 5209 bytes and written 433 bytes
Verification: OK
Verified peername: *.protonmail.ch
DANE TLSA 3 1 1 ...8f7d10a4469a947acc8399e1 matched EE certificate at depth 0
---

If you’d like to test for validation failure, just break the supplied hash. The result will be similar to the following output:

---
SSL handshake has read 5209 bytes and written 433 bytes
Verification error: No matching DANE TLSA records
---

For the best results, when testing DANE in this way, always provide all known TLSA records (one per -dane_tlsa_rrdata switch). If you do, services that use multiple certificates simultaneously will check out no matter what certificate is negotiated. For TLS 1.2 and earlier, it’s possible to force a particular certificate via a choice of client-supported cipher suites (the -cipher switch). TLS 1.3 suites are different, and for this protocol version you would need to use the -sigalgs switch with a value such as ecdsa_secp256r1_sha256 or rsa_pss_rsae_sha256.

When coupled with the -reconnect switch, the s_client command can be used to test session reuse. In this mode, s_client will connect to the target server six times. It will create a new session on the first connection, then try to reuse the same session in the subsequent five connections:

$ echo | openssl s_client -connect www.feistyduck.com:443 -reconnect

Note

Due to a bug in OpenSSL, at the time of writing session resumption testing doesn’t work in combination with TLS 1.3. Until the bug is resolved,28 the best you can do is test the earlier protocol versions. Use the -no_tls1_3 switch.

The previous command will produce a sea of output, most of which you won’t care about. The key parts are the information about new and reused sessions. There should be only one new session at the beginning, indicated by the following line:

New, TLSv1.2, Cipher is ECDHE-RSA-AES128-GCM-SHA256

This is followed by five session reuses, indicated by lines like this:

Reused, TLSv1.2, Cipher is ECDHE-RSA-AES128-GCM-SHA256

Most of the time, you don’t want to look at all that output and want an answer quickly. You can get it using the following command line:

$ echo | openssl s_client -connect www.feistyduck.com:443 -reconnect 2> /dev/null | ↩
grep 'New\|Reuse'
New, TLSv1.2, Cipher is ECDHE-RSA-AES128-GCM-SHA256
Reused, TLSv1.2, Cipher is ECDHE-RSA-AES128-GCM-SHA256
Reused, TLSv1.2, Cipher is ECDHE-RSA-AES128-GCM-SHA256
Reused, TLSv1.2, Cipher is ECDHE-RSA-AES128-GCM-SHA256
Reused, TLSv1.2, Cipher is ECDHE-RSA-AES128-GCM-SHA256
Reused, TLSv1.2, Cipher is ECDHE-RSA-AES128-GCM-SHA256

Here’s what the command does:

  • The -reconnect switch activates the session reuse mode.

  • The 2> /dev/null part hides stderr output, which you don’t care about.

  • Finally, the piped grep command filters out the rest of the fluff and lets through only the lines that you care about.

Note

If you don’t want to include session tickets in the test—for example, because not all clients support this feature yet—you can disable this method of resumption using the -no_ticket switch. This option doesn’t apply to TLS 1.3.

If you need better control over resumption, the s_client tool provides options to persist the connection state to a file. On your first connection, use the -sess_out switch to record the state:

$ openssl s_client -connect www.feistyduck.com:443 -sess_out sess.pem

To view the recorded state, use the sess_id tool:

$ openssl sess_id -in sess.pem -noout -text
SSL-Session:
    Protocol  : TLSv1.2
    Cipher    : ECDHE-ECDSA-AES128-GCM-SHA256
    Session-ID: F7384C2C4BE621F66045ECE12A89821FEE789C2E75B78C90C428BE37E0FE4599
    Session-ID-ctx: 
    Master-Key: 9D39C582D9AA1618B2F16C7911C4BFFB61D6D1FD578A93B1145FD2B4DBFDE76EB2279BA5↩
0AEFFCD95320BEEBC9489FAF
    PSK identity: None
    PSK identity hint: None
    SRP username: None
    TLS session ticket lifetime hint: 64800 (seconds)
    TLS session ticket:
    0000 - a2 d3 e3 04 03 21 85 6d-1a 4f 9c 82 fc 4e 15 e0   .....!.m.O...N..
    0010 - 9b b8 b1 24 0d 95 a3 0a-b8 24 d4 f5 d2 be b8 56   ...$.....$.....V
    0020 - b2 f0 e9 c5 e5 53 31 b5-24 74 96 ba e4 56 32 68   .....S1.$t...V2h
    0030 - fe bb 7a 7f 28 d7 c4 19-6a c5 ca 22 3a a7 2d 45   ..z.(...j..":.-E
    0040 - 52 91 74 f7 a8 fa 75 40-02 b9 84 9c 84 0d a8 06   R.t...u@........
    0050 - c7 a1 65 af 8b 54 19 74-52 e8 c4 f4 47 1c 3f f0   ..e..T.tR...G.?.
    0060 - 46 35 1a 3c a9 a5 73 30-33 b7 20 bd dc 8a b8 f9   F5.<..s03. .....
    0070 - 79 20 4a de b3 60 83 53-c7 a7 62 e1 a2 9e 55 8c   y J..`.S..b...U.
    0080 - 24 0a f5 4c ab 81 a5 d9-36 ae 52 61 a1 4e b7 99   $..L....6.Ra.N..
    0090 - 20 9e ca 67 49 ea 80 a4-14 ce ac 36 aa 20 0e 53    ..gI......6. .S
    00a0 - d7 9f 14 a6 7c b9 88 4c-6b 69 93 d4 62 fb 02 50   ....|..Lki..b..P

    Start Time: 1602414785
    Timeout   : 300 (sec)
    Verify return code: 20 (unable to get local issuer certificate)
    Extended master secret: no

Finally, to connect again using the same session state, use the -sess_in switch:

$ openssl s_client -connect www.feistyduck.com:443 -sess_in sess.pem

Keeping the state across connections in this way gives you more control and enables you to completely change connection parameters from one connection to another. For example, you could connect to one server on your first attempt, then another server on your second. This may be of use when you need to test if session resumption is correctly implemented on a web server cluster. Manual control of your connections allows you to spread them over time, perhaps testing for session timeouts and ticket key rotation.

If an OCSP responder is malfunctioning, sometimes it’s difficult to understand exactly why. Checking certificate revocation status from the command line is possible, but it’s not quite straightforward. You need to perform the following steps:

  1. Obtain the certificate that you wish to check for revocation.

  2. Obtain the issuing certificate.

  3. Determine the URL of the OCSP responder.

  4. Submit an OCSP request and observe the response.

For the first two steps, connect to the server with the -showcerts switch specified:

$ openssl s_client -connect www.feistyduck.com:443 -showcerts

The first certificate in the output will be the one belonging to the server. If the certificate chain is properly configured, the second certificate will be that of the issuer. To confirm, check that the issuer of the first certificate matches the subject of the second:

Certificate chain
 0 s:OU = Domain Control Validated, OU = PositiveSSL, CN = www.feistyduck.com
   i:C = GB, ST = Greater Manchester, L = Salford, O = COMODO CA Limited, CN = COMODO ↩
RSA Domain Validation Secure Server CA
-----BEGIN CERTIFICATE-----
MIIFUzCCBDugAwIBAgIRAPR/CbWZEksfCIRqxNcesPIwDQYJKoZIhvcNAQELBQAw
[...]
zbQXjVsc3E1THfFZWRzDPsU4fN/1iGlbrcAWa2sFfhJXrCDfAowFJ8A1n9jMiNEG
WfQfGgA2ar2xUtsqA7Re6XlXOlwBPuQ=
-----END CERTIFICATE-----
 1 s:C = GB, ST = Greater Manchester, L = Salford, O = COMODO CA Limited, CN = COMODO ↩
RSA Domain Validation Secure Server CA
   i:C = GB, ST = Greater Manchester, L = Salford, O = COMODO CA Limited, CN = COMODO ↩
RSA Certification Authority
-----BEGIN CERTIFICATE-----
MIIGCDCCA/CgAwIBAgIQKy5u6tl1NmwUim7bo3yMBzANBgkqhkiG9w0BAQwFADCB
hTELMAkGA1UEBhMCR0IxGzAZBgNVBAgTEkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4G
[...]

If the second certificate isn’t the right one, check the rest of the chain; some servers don’t serve the chain in the correct order. If you can’t find the issuer certificate in the chain, you’ll have to find it somewhere else. One way to do that is to look for the Authority Information Access extension in the leaf certificate:

$ openssl x509 -in fd.crt -noout -text
[...]
    Authority Information Access:
        CA Issuers - URI:http://crt.comodoca.com/COMODORSADomainValidationSecureServerCA↩
.crt
        OCSP - URI:http://ocsp.comodoca.com

If the CA Issuers information is present, it should contain the URL of the issuer certificate. If the issuer certificate information isn’t available, you can try to open the site in a browser, let it reconstruct the chain, and download the issuing certificate from its certificate viewer. If all that fails, you can look for the certificate in your trust store or visit the CA’s web site.

If you already have the certificates and just need to know the address of the OCSP responder, use the -ocsp_uri switch with the x509 command as a shortcut:

$ openssl x509 -in fd.crt -noout -ocsp_uri
http://ocsp.comodoca.com

Now you can submit the OCSP request:

$ openssl ocsp -issuer issuer.crt -cert fd.crt -url http://ocsp.comodoca.com -CAfile ↩
issuer.crt
WARNING: no nonce in response
Response verify OK
fd.crt: good
        This Update: Aug 30 22:35:12 2020 GMT
	 Next Update: Sep  6 22:35:12 2020 GMT

You want to look for two things in the response. First, check that the response itself is valid (Response verify OK in the previous example), and second, check what the response said. When you see good as the status, that means that the certificate hasn’t been revoked. The status will be revoked for revoked certificates.

Note

The warning message about the missing nonce is telling you that OpenSSL wanted to use a nonce as a protection against replay attacks, but the server in question did not reply with one. This generally happens because CAs want to improve the performance of their OCSP responders. When they disable the nonce protection (the standard allows it), OCSP responses can be produced (usually in batch), cached, and reused for a period of time.

You may encounter OCSP responders that do not respond successfully to the previous command line. The following suggestions may help in such situations.

Do not request a nonce

Some servers cannot handle nonce requests and respond with errors. OpenSSL will request a nonce by default. To disable nonces, use the -no_nonce command-line switch.

Supply a Host request header

Although most OCSP servers respond to HTTP requests that don’t specify the correct hostname in the Host header, some don’t. If you encounter an error message that includes an HTTP error code (e.g., 404), try adding the hostname to your OCSP request. You can do this with the help of the -header switch.

With the previous two points in mind, the final command to use is the following:

$ openssl ocsp -issuer issuer.crt -cert fd.crt -url http://ocsp.comodoca.com -CAfile ↩
issuer.crt -no_nonce -header Host ocsp.comodoca.com

OCSP stapling is an optional feature that allows a server certificate to be accompanied by an OCSP response that proves its validity. Because the OCSP response is delivered over an already existing connection, the client does not have to fetch it separately.

OCSP stapling is used only if requested by a client, which submits the status_request extension in the handshake request. A server that supports OCSP stapling will respond by including an OCSP response as part of the handshake.

When using the s_client tool, OCSP stapling is requested with the -status switch:

$ echo | openssl s_client -connect www.feistyduck.com:443 -status

The OCSP-related information will be displayed at the very beginning of the connection output. For example, with a server that does not support stapling you will see this line near the top of the output:

CONNECTED(00000003)
OCSP response: no response sent

With a server that does support stapling, you will see the entire OCSP response in the output:

OCSP Response Data:
    OCSP Response Status: successful (0x0)
    Response Type: Basic OCSP Response
    Version: 1 (0x0)
    Responder Id: 90AF6A3A945A0BD890EA125673DF43B43A28DAE7
    Produced At: Aug 30 22:35:12 2020 GMT
    Responses:
    Certificate ID:
      Hash Algorithm: sha1
      Issuer Name Hash: 7AE13EE8A0C42A2CB428CBE7A605461940E2A1E9
      Issuer Key Hash: 90AF6A3A945A0BD890EA125673DF43B43A28DAE7
      Serial Number: F47F09B599124B1F08846AC4D71EB0F2
    Cert Status: good
    This Update: Aug 30 22:35:12 2020 GMT
    Next Update: Sep  6 22:35:12 2020 GMT

    Signature Algorithm: sha256WithRSAEncryption
         1b:9d:be:3e:e6:b2:9a:e6:22:fe:69:cc:55:a9:62:5d:29:79:
         [...]

The certificate status good means that the certificate has not been revoked.

Checking certificate verification with a Certificate Revocation List (CRL) is even more involved than doing the same via OCSP. The process is as follows:

  1. Obtain the certificate you wish to check for revocation.

  2. Obtain the issuing certificate.

  3. Download and verify the CRL.

  4. Look for the certificate serial number in the CRL.

The first steps overlap with OCSP checking; to complete them follow the instructions in the section called “Checking OCSP Revocation”.

The location of the CRL is encoded in the server certificate; look for the “X509v3 CRL Distribution Points” section in the text output. For example:

$ openssl x509 -in fd.crt -noout -text | grep -A 5 CRL
[...]
                  URI:http://crl.comodoca.com/COMODORSADomainValidationSecureServerCA.cr↩
l

Then fetch the CRL from the CA:

$ wget http://crl.comodoca.com/COMODORSADomainValidationSecureServerCA.crl -O comodo.crl

Verify that the CRL is valid (i.e., signed by the issuer certificate):

$ openssl crl -in comodo.crl -inform DER -CAfile issuer.crt -noout
verify OK

Now, determine the serial number of the certificate you wish to check:

$ openssl x509 -in fd.crt -noout -serial
serial=F47F09B599124B1F08846AC4D71EB0F2

At this point, you can convert the CRL into a human-readable format and inspect it manually:

$ openssl crl -in comodo.crl -inform DER -text -noout
Certificate Revocation List (CRL):
        Version 2 (0x1)
        Signature Algorithm: sha256WithRSAEncryption
        Issuer: C = GB, ST = Greater Manchester, L = Salford, O = COMODO CA Limited, CN ↩
= COMODO RSA Domain Validation Secure Server CA
        Last Update: Aug 31 07:52:03 2020 GMT
        Next Update: Sep  7 07:52:03 2020 GMT
        CRL extensions:
            X509v3 Authority Key Identifier: 
                keyid:90:AF:6A:3A:94:5A:0B:D8:90:EA:12:56:73:DF:43:B4:3A:28:DA:E7

            X509v3 CRL Number: 
                2149
            1.3.6.1.4.1.311.21.4: 
200903195203Z   .
Revoked Certificates:
    Serial Number: 70DAB4B3229280F04364BC58DB2AB922
        Revocation Date: May 29 12:18:27 2017 GMT
    Serial Number: 51894D40389CDAB84A7A6F3374E1D893
        Revocation Date: May 30 23:20:55 2017 GMT
    [...]
    Signature Algorithm: sha256WithRSAEncryption
         5a:7c:6e:6e:98:05:c4:24:2b:84:7a:28:6f:45:26:33:6b:88:
         4d:dd:61:22:e4:23:47:76:c7:8a:55:ec:f9:72:29:47:21:73:
         [...]

The CRL starts with some metadata, which is followed by a list of revoked certificates, and it ends with a signature (which we verified in the previous step). If the serial number of the server certificate is on the list, that means it had been revoked.

If you don’t want to look for the serial number visually (some CRLs can be quite long), grep for it, but be careful that your formatting matches that used by the crl tool. For example:

$ openssl crl -in comodo.crl -inform DER -text -noout | grep F47F09B599124B1F08846AC4D71↩
EB0F2

In TLS, renegotiation is a failed feature that was responsible for several protocol weaknesses, some of which are quite easy to exploit. TLS 1.3 no longer supports renegotiation, but there are still older servers out there that support it with earlier protocol revisions.

The s_client tool has a couple of features that can assist you with manual testing of renegotiation. First of all, when you connect, the tool will report if the remote server supports secure renegotiation. This is because a server that supports secure renegotiation indicates its support for it via a special TLS extension that is exchanged during the handshake phase. When support is available, the output may look like this:

New, TLSv1/SSLv3, Cipher is AES256-SHA
Server public key is 2048 bit
Secure Renegotiation IS supported
Compression: NONE
Expansion: NONE
SSL-Session:
    [...]

If secure renegotiation is not supported, the output will be slightly different:

Secure Renegotiation IS NOT supported

Note

Because TLS 1.3 doesn’t support renegotiation, the s_client tool will always give a negative answer if this protocol version is negotiated. To ensure reliable results, use the -no_tls1_3 switch to force negotiation of an earlier protocol version.

Even if the server indicates support for secure renegotiation, you may wish to test whether it also allows clients to initiate renegotiation. Client-initiated renegotiation is a protocol feature that doesn’t serve any purpose in practice (because the server can always initiate renegotiation when it is needed) and makes the server more susceptible to denial of service attacks.

To initiate renegotiation, after the TLS handshake is complete, type an R character on a line by itself. For example, assuming we’re talking to an HTTP server, you can type the first line of a request, initiate renegotiation, and then finish the request. Here’s what that looks like when talking to a web server that supports client-initiated renegotiation:

GET / HTTP/1.0
R
RENEGOTIATING
depth=2 C = US, O = DigiCert Inc, OU = www.digicert.com, CN = DigiCert High Assurance ↩
EV Root CA
verify return:1
depth=1 C = US, O = DigiCert Inc, OU = www.digicert.com, CN = DigiCert SHA2 Extended ↩
Validation Server CA
verify return:1
depth=0 businessCategory = Private Organization, jurisdictionC = US, jurisdictionST = ↩
California, serialNumber = C2543436, C = US, ST = California, L = Mountain View, O = ↩
Mozilla Foundation, OU = Cloud Services, CN = addons.mozilla.org
verify return:1
Host: addons.mozilla.org

HTTP/1.1 301 Moved Permanently
Content-Type: text/plain; charset=utf-8
Date: Mon, 31 Aug 2020 12:40:49 GMT
Location: /en-US/firefox/
Strict-Transport-Security: max-age=31536000
Content-Length: 49
Connection: Close

Moved Permanently. Redirecting to /en-US/firefox/closed

When renegotiation is taking place, the server will send its certificates to the client again. You can see the verification of the certificate chain in the output. The next line after that continues with the Host request header. Seeing the web server’s response is the proof that renegotiation is supported. Because of the various ways the renegotiation issue was addressed in various versions of SSL/TLS libraries, servers that do not support renegotiation may break the connection or may keep it open but refuse to continue to talk over it (which usually results in a timeout).

A server that does not support renegotiation will flatly refuse the second handshake on the connection:

HEAD / HTTP/1.0
R
RENEGOTIATING
140003560109728:error:1409E0E5:SSL routines:SSL3_WRITE_BYTES:ssl handshake failure:s3↩
_pkt.c:592:

At the time of writing, the default behavior for OpenSSL is to connect to servers that don’t support secure renegotiation; it will also accept both secure and insecure renegotiation, opting for whatever the server is able to do. If renegotiation is successful with a server that doesn’t support secure renegotiation, you will know that the server supports insecure client-initiated renegotiation.

Note

The most reliable way to test for insecure renegotiation is to use the method described in this section, but with a version of OpenSSL that was released before the discovery of insecure renegotiation (e.g., 0.9.8k). I mention this because there is a small number of servers that support both secure and insecure renegotiation. This vulnerability is difficult to detect with modern versions of OpenSSL, which always prefer the secure option.

You can test for Heartbleed manually with OpenSSL or by using one of the tools designed for this purpose. There are now many utilities available, because Heartbleed is very easy to exploit. But, as usual with such tools, there is a question of their accuracy. There is evidence that some tools fail to detect vulnerable servers.29 Given the seriousness of Heartbleed, it’s best to either test manually or by using a tool that gives you full visibility of the process. I am going to describe an approach you can use with only a modified version of OpenSSL.

Some parts of the test don’t require modifications to OpenSSL, assuming you have a version that supports the Heartbeat protocol (starting with 1.0.1, but before 1.1.0). For example, to determine if the remote server supports the Heartbeat protocol, use the -tlsextdebug switch to display server extensions when connecting:

$ openssl s_client -connect www.feistyduck.com:443 -tlsextdebug
CONNECTED(00000003)
TLS server extension "renegotiation info" (id=65281), len=1
0001 - <SPACES/NULS>
TLS server extension "EC point formats" (id=11), len=4
0000 - 03 00 01 02                                       ....
TLS server extension "session ticket" (id=35), len=0
TLS server extension "heartbeat" (id=15), len=1
0000 - 01
[...]

A server that does not return the heartbeat extension is not vulnerable to Heartbleed. To test if a server responds to heartbeat requests, use the -msg switch to request that protocol messages are shown, connect to the server, wait until the handshake completes, then type B and press return:

$ openssl s_client -connect www.feistyduck.com:443 -tlsextdebug -msg
[...]
---
B
HEARTBEATING
>>> TLS 1.2  [length 0025], HeartbeatRequest
    01 00 12 00 00 3c 83 1a 9f 1a 5c 84 aa 86 9e 20
    c7 a2 ac d7 6f f0 c9 63 9b d5 85 bf 9a 47 61 27
    d5 22 4c 70 75
<<< TLS 1.2  [length 0025], HeartbeatResponse
    02 00 12 00 00 3c 83 1a 9f 1a 5c 84 aa 86 9e 20
    c7 a2 ac d7 6f 52 4c ee b3 d8 a1 75 9a 6b bd 74
    f8 60 32 99 1c
read R BLOCK

This output shows a complete heartbeat request and response. The second and third bytes in both heartbeat messages specify payload length. We submitted a payload of 18 bytes (12 hexadecimal) and the server responded with a payload of the same size. In both cases there were also additional 16 bytes of padding. The first two bytes in the payload make the sequence number, which OpenSSL uses to match responses to requests. The remaining payload bytes and the padding are just random data.

To detect a vulnerable server, you’ll have to prepare a special version of OpenSSL that sends an incorrect payload length. Vulnerable servers take the declared payload length and respond with that many bytes irrespective of the length of the actual payload provided.

At this point, you have to decide if you want to build an invasive test (which exploits the server by retrieving some data from the process) or a noninvasive test. This will depend on your circumstances. If you have permission for your testing activities, use the invasive test. With it, you’ll be able to see exactly what is returned, and there won’t be room for errors. For example, some versions of GnuTLS support Heartbeat and will respond to requests with incorrect payload length, but they will not actually return server data. A noninvasive test can’t reliably diagnose that situation.

The following patch against OpenSSL 1.0.1h creates a noninvasive version of the test:

--- t1_lib.c.original   2014-07-04 17:29:35.092000000 +0100
+++ t1_lib.c    2014-07-04 17:31:44.528000000 +0100
@@ -2583,6 +2583,7 @@
 #endif

 #ifndef OPENSSL_NO_HEARTBEATS
+#define PAYLOAD_EXTRA 16
 int
 tls1_process_heartbeat(SSL *s)
        {
@@ -2646,7 +2647,7 @@
                 * sequence number */
                n2s(pl, seq);

-               if (payload == 18 && seq == s->tlsext_hb_seq)
+               if ((payload == (18 + PAYLOAD_EXTRA)) && seq == s->tlsext_hb_seq)
                        {
                        s->tlsext_hb_seq++;
                        s->tlsext_hb_pending = 0;
@@ -2705,7 +2706,7 @@
        /* Message Type */
        *p++ = TLS1_HB_REQUEST;
        /* Payload length (18 bytes here) */
-       s2n(payload, p);
+       s2n(payload + PAYLOAD_EXTRA, p);
        /* Sequence number */
        s2n(s->tlsext_hb_seq, p);
        /* 16 random bytes */

To build a noninvasive test, increase payload length by up to 16 bytes, or the length of the padding. When a vulnerable server responds to such a request, it will return the padding but nothing else. To build an invasive test, increase the payload length by, say, 32 bytes. A vulnerable server will respond with a payload of 50 bytes (18 bytes sent by OpenSSL by default, plus your 32 bytes) and send 16 bytes of padding. By increasing the declared length of the payload in this way, a vulnerable server will return up to 64 KB of data. A server not vulnerable to Heartbleed will not respond.

To produce your own Heartbleed testing tool, unpack a fresh copy of OpenSSL source code, edit ssl/t1_lib.c to make the change as in the patch, compile as usual, but don’t install. The resulting openssl binary will be placed in the apps/ subdirectory. Because it is statically compiled, you can rename it to something like openssl-heartbleed and move it to its permanent location.

Here’s an example of the output you’d get with a vulnerable server that returns 16 bytes of server data (in bold):

B
HEARTBEATING
>>> TLS 1.2  [length 0025], HeartbeatRequest
    01 00 32 00 00 7c e8 f5 62 35 03 bb 00 34 19 4d
    57 7e f1 e5 90 6e 71 a9 26 85 96 1c c4 2b eb d5
    93 e2 d7 bb 5f
<<< TLS 1.2  [length 0045], HeartbeatResponse
    02 00 32 00 00 7c e8 f5 62 35 03 bb 00 34 19 4d
    57 7e f1 e5 90 6e 71 a9 26 85 96 1c c4 2b eb d5
    93 e2 d7 bb 5f 6f 81 0f aa dc e0 47 62 3f 7e dc
    60 95 c6 ba df c9 f6 9d 2b c8 66 f8 a5 45 64 0b
    d2 f5 3d a9 ad
read R BLOCK

If you want to see more data retrieved in a single response, increase the payload length, recompile, and test again. Alternatively, to retrieve another batch of the same size, use the B command again.

Starting from OpenSSL 1.0.2, when you connect to a server, the s_client command prints the strength of the ephemeral Diffie-Hellman key if one is used. Thus, to determine the strength of server’s DH parameters, all you need to do is connect to it while offering only suites that use the DH key exchange. For example:

$ openssl s_client -connect www.feistyduck.com:443 -cipher kEDH
[...]
---No client certificate CA names sent
Peer signing digest: SHA512
Peer signature type: RSA
Server Temp Key: DH, 2048 bits
---
[...]

Servers that support export suites might actually offer even weaker DH parameters. To check for that possibility, connect using your old OpenSSL30 while offering only export DHE suites:

$ openssl s_client -connect www.feistyduck.com:443 -cipher kEDH+EXPORT

This command should fail with well-configured servers. Otherwise, you’ll probably see the server offering to negotiate insecure 512-bit DH parameters.

[22] Hardenize (retrieved 31 August 2020)

[23] testssl.sh (retrieved 29 August 2020)

[24] Do note that when connecting to a TLS 1.3 server, the protocol information may not appear immediately on the connection. Instead, it will be printed when the session ticket is received, which may take a couple of seconds with some servers.

[25] Out of five cipher suites that TLS 1.3 supports, OpenSSL enables only three by default. The remaining two are CCM suites, which are intended for use with embedded systems, in which every little bit of performance and battery life matters. These suites are not worth using for other use cases—especially the CCM_8 suite, which reduces the strength of authentication.

[26] RFC 7919, which came out in 2016, redefined the elliptic_curves TLS extension to support finite field groups and changed the extension name to supported_groups. Although this extension applies to TLS 1.2, support for it is not widespread.

[27] New Adventures in DNSSEC and DANE (Jan Schaumann, retrieved 2 October 2020)

[28] s_client -reconnect Option Is Broken with TLSv1.3 (OpenSSL, retrieved 31 August 2020)

[29] Bugs in Heartbleed detection scripts (Shannon Simpson and Adrian Hayter, 14 April 2014)

[30] Support for EXPORT cipher suites was removed in OpenSSL 1.1.0.

Copyright © 2022 Feisty Duck. All rights reserved.