TLS/SSL certificates for developers

Dominik Rüttiger

U.S. passport in flight

Source: Photo by Global Residence Index on Unsplash

TLS/SSL certificates are the fundamental source of trust on the web. Everyone uses them every day, most without even knowing they exist. With modern zero trust architecture, developers have to deal with them even more.

I feel like many developers only learn the very basics to get along, which can lead to dangerous mistakes. Cryptographic Failures is #2 on the OWASP Top 10 for a reason.

Developers do not have to be experts on TLS/SSL certificates, but know how to handle them safely. Let’s go!

How can I trust someone on the web?

We visit a ridiculous number of websites every day, and we want to be sure that the people and companies behind them are who they say they are.

In real life, we can ask strangers to show us their passports. We have learned to judge the validity of a passport by checking features such as watermarks that presumably could only be produced by a government’s passport office. So as long as we trust the passport office, we can verify the identity.

On the web, certificates are the equivalent of passports. Every subject can get a digital certificate from a certificate authority (CA), which takes the role of the passport office. There is a list of trusted CAs built into our browsers and operating systems by their developers, that everyone is expected to trust.

Show me your passport!

Let’s check out the certificate of this blog. We are using commands provided by OpenSSL:

Terminal window
openssl s_client -connect drsys.de:443 \
-servername www.drsys.de \
-showcerts \
< /dev/null \
| openssl x509 -text
# Pro tip: This shows the first certificate in the chain.

The hostname and DNS name can be set independently, which is handy for debugging locally on a webserver (-connect localhost:443).

The format is X.509 and includes:

Certificate:
Data:
...
Issuer: C=US, O=Let's Encrypt, CN=R11
Validity
Not Before: Nov 14 06:27:32 2024 GMT
Not After : Feb 12 06:27:31 2025 GMT
Subject: CN=drsys.de
...
X509v3 extensions:
...
X509v3 Subject Alternative Name:
DNS:drsys.de, DNS:www.drsys.de
...
...

Usually, this certificate is Base64 encoded and looks like this:

-----BEGIN CERTIFICATE-----
MIIF7DCCBNSgAwIBAgISAzj+tEN7hA6oWpq8txzOoC1FMA0GCSqGSIb3DQEBCwUA
MDMxCzAJBgNVBAYTAlVTMRYwFAYDVQQKEw1MZXQncyBFbmNyeXB0MQwwCgYDVQQD
...
b+z7p3X+Q8JBlR66Jc8rKHX4uSXmkxHa7iKpSRGeuNvvEC+9QiQpE7Q65f0pJ/pv
enqWJtMWhIf9TtQGbN94wbzQT1PUl1i4sCvdd9n3DOs=
-----END CERTIFICATE-----

Common filename extensions are .pem, .cer and .crt.

Verify a passport

Let’s find out if we are dealing with a valid certificate:

Terminal window
openssl s_client -connect drsys.de:443 \
-servername www.drsys.de \
-showcerts \
-verify_hostname www.drsys.de \
< /dev/null
# Pro tip: This shows the whole certiciate chain.

If we see Verification: OK and Verified peername: www.drsys.de, we are good. You could also set -connect localhost:443 for debugging locally on a webserver. See the official docs for more verification options.

Behind the scenes

The fascinating part is, that your browser is actually able to verify the certificates locally. This is possible using public-key cryptography with a digital signature.

First, let’s see how a certificate is requested from a certificate authority (CA):

  1. Create a key pair with a private and public key
  2. Send the public key together with a domain name to the CA
    • this is called a Certificate Signing Request (CSR)
  3. The CA verifies your identity (e.g. that you control the DNS entries)
  4. The CA creates a certificate which includes the information from the CSR and additional information like validity
  5. The CA signs it (calculate a hash, encrypt it with it’s private key and add it to the certificate)
    • encrypting with the private key is specific for RSA and can be different for other algorithms

The Verification of the certificate then goes like:

  1. Check the validity and domain name
  2. Check if we trust the issuer
    • Is the public key included in our local authorities list?
  3. Hash the certificate, decrypt the signature with the CA’s public key and compare the two

These steps usually have to be repeated multiple times, because it is common to have hierarchies of CAs:

-----BEGIN CERTIFICATE-----
# Server certificate
-----END CERTIFICATE-----
-----BEGIN CERTIFICATE-----
# Intermediate CA certificate
-----END CERTIFICATE-----
-----BEGIN CERTIFICATE-----
# Root CA certificate
-----END CERTIFICATE-----

We will do all these steps ourselves in the bonus section.

Get your own passport

Traditionally you would buy a TLS/SSL certificate from a company like DigiCert. But since 2015, the non-profit organisation Let’s Encrypt issues certificates for free through an easy, fully automated process. Some webservers/reverse proxys like Caddy and Traefik even obtain and renew certificates automatically.

Another option is to create your own certificate authority and make your users’ browsers trust it. This is common for internal applications in enterprises.

Use your passport

Getting a TLS/SSL certificate from Let’s Encrypt is very easy if you have a server with a public ip address.

  1. Set DNS A/AAAA records to point to the server

  2. Create a Caddy configuration file:

    Caddyfile
    demo.example.com # Change to your domain
    respond "Hi there!"
  3. Create a Docker Compose file:

    compose.yaml
    services:
    caddy:
    image: caddy:latest
    restart: unless-stopped
    ports:
    - '80:80'
    - '443:443'
    volumes:
    - ./Caddyfile:/etc/caddy/Caddyfile
    - caddy_data:/data
    - caddy_config:/config
    volumes:
    caddy_data:
    caddy_config:
  4. Start Caddy:

    Terminal window
    docker compose up -d
  5. Open your domain (e.g. https://demo.example.com) in the browser and check the certificate

For more use cases, like using Caddy as a reverse proxy, see the official Caddy docs.

Bonus: Make your own passport

It’s totally possible to create your own CA, sign certificates and use them with your server. We will also see how we can trust our CA and validate the result.

  1. Run these commands:

    Terminal window
    # Create self-signed certificate for root CA
    openssl req -out myRootCA.crt \
    -newkey rsa:4096 \
    -noenc \
    -keyout myRootCA.key \
    -x509 \
    -days 365 \
    -subj "/CN=myRootCA"
    # Create private key and CSR for localhost
    openssl req -out localhost.csr \
    -new \
    -newkey rsa:4096 \
    -noenc \
    -keyout localhost.key \
    -addext "subjectAltName = DNS:localhost" \
    -subj "/CN=localhost"
    # Sign the CSR
    openssl x509 -in localhost.csr \
    -req \
    -copy_extensions copy \
    -out localhost.crt \
    -days 365 \
    -CA myRootCA.crt \
    -CAkey myRootCA.key
    # Build certificate chain
    cat localhost.crt myRootCA.crt > localhost-chain.crt
  2. Check the files:

    Terminal window
    # View certificate
    openssl x509 -in localhost.crt -text
    # Verify certificate by explicitly setting our root CA
    # certificate as trusted
    openssl verify -CAfile myRootCA.crt localhost.crt
    # Check if certificate and private key match
    openssl x509 -in localhost.crt -noout -modulus | openssl md5
    openssl rsa -in localhost.key -noout -modulus | openssl md5
    # Check private key
    openssl rsa -in localhost.key -noout -check
  3. Create a Caddy configuration file:

    Caddyfile
    localhost
    tls /etc/caddy/cert.pem /etc/caddy/key.pem
    respond "Hi there!"
  4. Create a Docker Compose file:

    compose.yaml
    services:
    caddy:
    image: caddy:latest
    restart: unless-stopped
    ports:
    - '443:443'
    volumes:
    - ./Caddyfile:/etc/caddy/Caddyfile
    - ./localhost-chain.crt:/etc/caddy/cert.pem
    - ./localhost.key:/etc/caddy/key.pem
    - caddy_data:/data
    - caddy_config:/config
    volumes:
    caddy_data:
    caddy_config:
  5. Start Caddy:

    Terminal window
    docker compose up -d
  6. Verify the served certificate by explicitly setting our root CA certificate as trusted:

    Terminal window
    openssl s_client -connect localhost:443 \
    -servername localhost \
    -showcerts \
    -CAfile myRootCA.crt \
    -verify_hostname localhost \
    < /dev/null
    # Check: Verification: OK / Verified peername: localhost

FAQ

What do TLS and SSL stand for?
Transport Layer Security and Secure Sockets Layer.

How is SSL related to TLS?
The now-deprecated SSL specification is the predecessor of the TLS protocol. The term SSL is still widely used though, but usually just refers to TLS.

How can I import TLS/SSL certificates to my operating system?
For Debian/Ubuntu Linux, save the files in the /usr/local/share/ca-certificates/ folder with suffix .crt and import them with /usr/sbin/update-ca-certificates.

What happens if someone gets control of one of the CAs built into our browsers and operating systems?
This would be catastrophic because it would allow them to fake any identity on the web.

How does Let’s Encrypt’s CA hierarchy look like?
Let’s Encrypt has two root CAs ISRG Root X1 and ISRG Root X2 and four intermediate CAs. For more details see the official “Chains of Trust” documentation. The root certificates are already available on our systems. On Linux, you can analyse them with:

Terminal window
openssl x509 -in /etc/ssl/certs/ISRG_Root_X1.pem \
-text

How can I see/verify SMTP server certificates?
The default port for secure connection is 587. If the SMTP server is still using STARTTLS, use the -starttls smtp option.

Tags

#bash #dev #devops #linux #security

Share

Hacker News LinkedIn Reddit Twitter/X