Testing HTTPS clients using openssl to simulate a server

This article describes how to test your HTTPS client or browser using openssl. To test your HTTPS client, you need an HTTPS server, or a web server, such as IIS, apache, nginx, or openssl. You also need some test cases. There are three common failure modes in SSL/TLS:

  1. The client makes the connection when it should not,
  2. The connection fails when it should succeed, and
  3. The connection is properly made, but the data is corrupted in transmission.
  4. There is a 4th failure mode: the data might not be securely transmitted. That failure mode is out of scope of this article.

To make sure that any problems uncovered in testing are due to issues in your HTTPS client, we want to use a “known good” HTTPS server. We also want a server that is “pedantic” or “unforgiving”. openssl fits these requirements precisely.

In this article, I am going to describe how use the openssl s_server command to be an HTTPS server. There are many configuration items that have to be just right, so I am not only going to show you how to do it right, but I am also going to share with you what went wrong, and how I went about diagnosing them and fixing them.

DID YOU KNOW?
A “client” is a computer or a computer program that initiates a connection to a “server”. A “server” is a computer program that waits for a connection to arrive from a “client”. For HTTP and HTTPS, there are “browsers” and “clients”. Browsers are designed for interaction with humans and usually have graphical user interfaces. All browsers are HTTP/HTTPS clients.

However, there are HTTP/HTTPS clients which are not browsers. These clients are designed for use as automated systems. The wise server designer will ensure that their system can be used effectively with HTTPS clients that are browsers and HTTPS clients that are not browsers.

In this tutorial you will learn:

  • How to select a good HTTPS client or browser
  • How to use openssl as an HTTPS server
  • How to use an HTTPS server to test an HTTPS client

Testing HTTPS client using openssl to simulate a server
Testing HTTPS client using openssl to simulate a server

Software Requirements and Conventions Used

Software Requirements and Linux Command Line Conventions
Category Requirements, Conventions or Software Version Used
System Any Linux system
Software OpenSSL or any HTTPS server such IIS, Apache Nginx
Other Privileged access to your Linux system as root or via the sudo command.
Conventions # – requires given linux commands to be executed with root privileges either directly as a root user or by use of sudo command
$ – requires given linux commands to be executed as a regular non-privileged user

How to test your HTTPS client step by step instructions

I will use the adjectives “righteous” to indicate that a test did something properly, and “errorneous” to indicate that a test did something wrong. If a test fails when it should, then that’s a righteous failure. If a test passes when it should not, then that’s a erroneous pass.

I wanted to use an HTTPS client that I could break and repair at will, and I found it: the http command (it’s in github as httpie). If I use the -verify=no option, then the client is broken: it will erroneously pass tests. I was unable to create an erroneous fail, and that’s A Good Thing as it means that if the client fails, then something is wrong.

At the heart of the SSL/TLS protocol (they changed the name and little else) are two files, a “certificate” (or “cert” for short) and a secret “key”. All over the protocol, one end of the connection will ask the other end for a certificate. The first end will use some of the information in the cert to create a mathematical puzzle that only something that has the secret key can answer. The secret key never leaves its machine: solving the problem means that the near end knows that the far end has the key, but not what the key is.

SSL TLS Certificate authentication handshake
SSL TLS Certificate authentication handshake

The openssl command is essentially a command line interface to libssl. It contains a crude server invoked with the s_server subcommand. openssl will need a public cert/private key pair. In my case, I already had them for my production web server. I got them from let’s encrypt, for free.

As a proof-of-concept that the server is working properly, I copied the cert and key to my development machine and started the openssl HTTPS server.

On the server side:

$ openssl s_server -status_verbose -HTTP -cert fullchain.pem -key privkey.pem
Using default temp DH parameters
ACCEPT

My first attempt FAILED!

$ http --verify=yes jeffs-desktop:4433/index.html

http: error: ConnectionError: (Connection aborted., RemoteDisconnected(Remote end closed connection without response)) while doing GET request to URL: http://jeffs-desktop:4433/index.html

First hypothesis: the key and the cert don’t match. I checked that:

$ openssl x509 -noout -modulus -in fullchain.pemls  | openssl md5
(stdin)= b9dbd040d9a0c3b5d3d50af46bc87784
$ openssl rsa -noout -modulus -in privkey.pem | openssl md5
(stdin)= b9dbd040d9a0c3b5d3d50af46bc87784

They match. So why is this failing? Because my certificate is for linuxconfig.dns.net but I am using jeffs-desktop as my host name.

jeffs@jeffs-desktop:~/documents$ openssl x509 -text -noout -in fullchain.pem | fgrep CN
        Issuer: C = US, O = Let’s Encrypt, CN = R3
        Subject: CN = linuxconfig.ddns.net

This is a righteous failure: the server was misconfigured and my client detected it. Had I used the
-verify=no option, then I would have a broken client and it would have not detected the problem. Note that any data transmitted would still be secure against an eavesdropper. I can fix this problem by modifying my /etc/hosts file with my own IPv4 and IPv6 addresses.

192.168.1.149   linuxconfig.ddns.net
2601:602:8500:b65:155a:7b81:65c:21fa    linuxconfig.ddns.net

(incidentally, the ease by which you can fake an IP address is one of the motivations of SSL/TLS in the first place).
Try again. On the server side:

$ openssl s_server -status_verbose -HTTP -cert fullchain.pem -key privkey.pem
Using default temp DH parameters
ACCEPT

On the client side:

http --verify=yes https://linuxconfig.ddns.net:4433/index.html
On the server side, I get the error message:

140101997737280:error:14094418:SSL routines:ssl3_read_bytes:tlsv1 alert unknown ca:../ssl/record/rec_layer_s3.c:1543:SSL alert number 48
On the client side, I get the error message:
http: error: SSLError: HTTPSConnectionPool(host='linuxconfig.ddns.net', port=4433): Max retries exceeded with url: / (Caused by SSLError(SSLCertVerificationError(1, '[SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed: unable to get local issuer certificate (_ssl.c:1131)'))) while doing GET request to URL: https://linuxconfig.ddns.net:4433/

That error message, CERTIFICATE_VERIFY_FAILED, is an important clue: it means that the certificate’s Certificate Authority (CA) could not be verified. Since the client could not verify the certificate, if failed to make the connection. This is another righteous failure.

The certificate itself could be forged – and the client has no way to know. However, the certificate references a certificate authority (CA), and the CA either knows that the certificate is valid or else it rejects verification. How do we know that the CA is trustworthy?

The CA itself has a certificate, an intermediate certificate, and that certificate references another CA. Eventually, this chain of certificates reaches a root certificate. A root certificate signs itself, and therefore is, by definition, trustworthy. In this case, something has gone wrong with this chain of certificates, this chain of trust.

$ openssl s_client -showcerts -connect linuxconfig.ddns.net:4433
CONNECTED(00000003)
depth=0 CN = linuxconfigan.ddns.net
verify error:num=20:unable to get local issuer certificate
verify return:1
depth=0 CN = linuxconfigan.ddns.net
verify error:num=21:unable to verify the first certificate
verify return:1
---
Certificate chain
 0 s:CN = linuxconfigan.ddns.net
   i:C = US, O = Let's Encrypt, CN = R3
-----BEGIN CERTIFICATE-----

I know my production server is working properly. This is what the chain is supposed to look like (note the port number 443, not 4433):

$ openssl s_client -showcerts -connect linuxconfig.ddns.net:443
CONNECTED(00000003)
depth=2 C = US, O = Internet Security Research Group, CN = ISRG Root X1
verify return:1
depth=1 C = US, O = Let's Encrypt, CN = R3
verify return:1
depth=0 CN = linuxconfig.ddns.net
verify return:1
---
Certificate chain
 0 s:CN = linuxconfig.ddns.net
   i:C = US, O = Let's Encrypt, CN = R3
-----BEGIN CERTIFICATE-----
MIIFYjCCBEqgAwIBAgISA0MTOSmISSsIyRls8O/2XpAaMA0GCSqGSIb3DQEBCwUA
...
-----END CERTIFICATE-----
 1 s:C = US, O = Let's Encrypt, CN = R3
   i:C = US, O = Internet Security Research Group, CN = ISRG Root X1
-----BEGIN CERTIFICATE----
...
-----END CERTIFICATE-----
 2 s:C = US, O = Internet Security Research Group, CN = ISRG Root X1
   i:O = Digital Signature Trust Co., CN = DST Root CA X3
-----BEGIN CERTIFICATE-----
…

There are two ways to proceed from here: I can turn off certificate verification or I can add Let’s Encrypt’s certificate to the list of known CAs. Turning off verification is fast and safe. Adding the CA to the list of known CAs is more arcane. Let’s do both. On the server side, I haven’t touched a thing. On the client side, I turn off verification and I get:

$ http –verify=no  https://linuxconfig.ddns.net:4433/index.html
http: error: ConnectionError: ('Connection aborted.', BadStatusLine('\n')) while doing GET request to URL: https://linuxconfig.ddns.net:4433/index.html
$ echo $?
1

This error message tells me that there’s been a violation of the HTTP (not HTTPS) protocol. The server served the first line of the file, index.html, when it should have returned an HTTP return header block. This is a server side flaw, and it would break all HTTP clients. A careful look at the documentation tells me to use the -WWW (not -www) option with openssl, instead of the -HTTP option. I do that:

openssl s_server -status_verbose -WWW -cert fullchain.pem -key privkey.pem and it works properly, with the caveat that I haven’t gotten certificate validation to work yet.

$ http -verify=no https://linuxconfig.ddns.net:4433/helloworld.c
HTTP/1.0 200 ok
Content-type: text/plain

#include 
int main (int argc, char *argv[]) {
        printf("Hello, world\n\n");
}

Since I used -verify=no, this is actually an erroneous pass.

To verify that my certificate chain is valid, I can use the openssl verify command:

$ openssl verify -purpose sslserver fullchain.pem
CN = linuxconfig.ddns.net
error 20 at 0 depth lookup: unable to get local issuer certificate
error cert.pem: verification failed

The quick solution was to try the openssl s_server command on my production web server, using production configuration files. This is (reasonably) safe to do because the openssl server will run on port 4433 while my production server is running on port 443.

# openssl s_server -status_verbose -WWW \
-cert /etc/letsencrypt/live/linuxconfig.ddns.net/fullchain.pem  \
-key /etc/letsencrypt/live/linuxconfig.ddns.net/privkey.pem -accept 4433

Hmm. Nginx is working like a champ. openssl is not. This is why openssl makes for a better test bed than nginx: if nginx’s configuration is wrong, it will try to muddle through. If openssl’s configuration is wrong, it will call you on it. openssl’s configuration is stored in /etc/ssl/openssl.cnf.

It says that the CA certs are in /etc/ssl/certs . The Internet Services Research Group (ISRG)’s root certificate is there. But let’s encrypt’s intermediate cert is not. That makes sense in a way: Let’s encrypt has a wonderful certbot that knew all about nginx when I ran it, but I didn’t run certbot with openssl, so the let’s encrypt’s cert wasn’t in /etc/ssl/certs/ . I got let’s encrypt’s certificate with the:

$ wget https://letsencrypt.org/certs/lets-encrypt-r3.pem

The above command, copied the file lets_encrypt_r3.pem into /etc/ssl/certs/ , ran the the c_rehash program, and voila:

# openssl verify -CApath /etc/ssl/certs/ \ 	/etc/letsencrypt/live/linuxconfig.ddns.net/fullchain.pem
/etc/letsencrypt/live/linuxconfig.ddns.net/fullchain.pem: OK

That’s nice, but the test is, can I see helloworld.c?

$ http --verify=yes https://linuxconfig.ddns.net:4433/helloworld.c
HTTP/1.0 200 ok
Content-type: text/plain

#include 
int main (int argc, char *argv[]) {
	printf("Hello, world\n\n");
}

Yes. I have now verified that my working HTTPS client will righteously pass and righteously fail, at least for the test cases I worked with. There are some other things that go wrong with SSL/TLS such as Certificate Revocation Lists (CRLs), but I hope you get a good idea.

Next, I want to verify that files sent between the openssl HTTPS server and my HTTPS client will not be corrupted, not even one bit. I can’t verify that every file will be transmitted without error, but what I can do is transmit a large binary file, verify that it was transmitted correctly, and then infer that large files won’t be corrupted.

I used the ls -lorS command to find a large file, calculated its SHA256 sum, transmitted it using openssl as the server, saved the received file, and calculate the SHA256 sum on that file. The SHA 256 sums should match.

On the server side:

$ ls -lorS | tail -1
-rw-rw-r-- 1 jeffs 121329853 May 23  2020 CybersecurityEssentials.pdf
$ sha256sum CybersecurityEssentials.pdf
49a49c8e525a3d6830fce1c1ee0bfce2d3dd4b000eeff5925b074802e62024e0 CybersecurityEssentials.pdf

On the client side:

$ http --verify=no https://linuxconfig.ddns.net:4433/CybersecurityEssentials.pdf -o /tmp/CybersecurityEssentials.pdf 
$ sha256sum /tmp/CybersecurityEssentials.pdf 
49a49c8e525a3d6830fce1c1ee0bfce2d3dd4b000eeff5925b074802e62024e0  /tmp/CybersecurityEssentials.pdf

That PDF file is 121MB, big enough for my purposes. The SHA256 sums match, so the file was transmitted properly.

Conclusion

In this article, I described the common failure modes the HTTPS protocol. I used some criteria for selecting an HTTPS server to use for testing an HTTPS client, and selected openssl. I selected an easy to use HTTPS client. I showed some common failure modes, and observed that the client detected those failures.

The hard part was configuring openssl properly, so I showed what can go wrong and how to fix it. Finally, I demonstrated that, using openssl as a server and my HTTPS client, I could transmit a file without data corruption.



Comments and Discussions
Linux Forum