Rotors of M-209 cipher machine.

Nginx, OpenSSL, and Quantum-Safe Cryptography

Secure Web Service with Nginx, OpenSSL, and Quantum-Safe Cryptography

Step-by-step notes for setting up a web server running the latest protocols and cryptography for the best performance and security.

We will use Nginx for the HTTP/HTTPS service, OpenSSL for the cryptographic infrastructure, and the Open Quantum Safe or OQS liboqs library to "future-proof" your server against quantum computing breakthroughs.

A current operating system should provide most of what you need. If you need to build OQS from source, I'll show you how to do that.

Like many of my technical pages, this serves as an aide-mémoire for me. I figured all this out once, and this helps when I need to return to it. You will see that I have simply included blocks of my heavily commented nginx.conf file with some added explanations.

I find this useful, and I hope that you will, too.

You Will Need Certificates

You might want to start by getting your certificates lined up. See my pages for how to get, and automatically renew, certificates from Let's Encrypt.

I wouldn't advise anyone to start a project like this on GoDaddy, but if you're already locked in to that platform, at least there's a way to automate certificate renewal.

OpenSSL

OpenSSL provides the cryptographic framework. It includes the openssl and s_client commands (along with many others), plus a large shared library of cryptographic functions that other programs can use.

Package management commands

You need OpenSSL 3.* to support Open Quantum Safe. Version 3.0.0 released on 7 September 2021, so your operating system should include it. If not, check your package management system to see if it includes extra packages with newer versions of some utilities, or if it can can reference an added package repository as with the EPEL collection for Red Hat, Oracle Linux, etc.

Wait, "Post-Quantum"? "Quantum-Safe"?

We usually use hybrid cryptography to protect information — symmetric cryptography to protect the data itself, and asymmetric cryptography to protect the authentication and the session keys.

When establishing a connection, the end points use asymmetric cryptography, sometimes called public-key cryptography, first to authenticate, and then to establish a shared session key.

The shared session key is for the symmetric cipher that will protect the data. It will be an ephemeral session key, meaning that it will be randomly generated and then used only for this one connection session. To re-connect and send the same data, a new unique ephemeral key would be generated.

The symmetric cipher will typically be AES, or possibly the ChaCha20/Poly1305 stream cipher which can be used with TLS.

An asymmetric cipher is based on a "trapdoor" function. That's a problem that is enormously, impractically more difficult to do in one direction than in the other. We can safely conclude that there is no practical solution, but it would be quite easy to test whether or not a proposed solution is correct.

How RSA Works

The factoring problem has been a traditional trapdoor function used for session key agreement. The security of the RSA algorithm is based on the difficulty of factoring the product of two large prime numbers. How large? For a 4096-bit RSA key, two prime numbers of about 640 digits each, multiplied together to form a product of 1280 digits. The 4096-bit product is the public key, and the two roughly 2048-bit prime numbers are the private key.

How Elliptic-Curve
Cryptography Works

The classic Diffie-Hellman scheme from the 1970s uses the discrete logarithm problem as its trapdoor. For elliptic-curve cryptography or ECC, the trapdoor function is the related elliptic-curve discrete logarithm problem.

Unfortunately, the factoring and discrete logarithm problems could be quickly solved by a quantum computer with enough stable qubits running Shor's algorithm. That would break either of those trapdoor functions, exposing the session keys and allowing an attacker to decrypt the ciphertext.

Richard Feynman, "Simulating Physics with Computers", Intl. J. of Theoretical Physics, 21:467-488, 1981

Quantum computing had been suggested in 1981, and when Peter Shor published his algorithm in 1994, the cryptographic world realized that quantum computing was be a threat.

Researchers began working on what has been variously called post-quantum, quantum-safe, and quantum-resistant cryptography. The PQCrypto conference series began running in 2006. The goal is the development of asymmetric algorithms unbreakable with Shor's algorithm. The Open Quantum Safe project (GitHub page here) provides open-source software implementing the quantum-resistant algorithms we have so far. The U.S. National Institute of Standards and Technology has led research projects, workshops, conferences, and competitions for PQC, selecting algorithms to become standards.

Grover's algorithm was published just two years after Shor's. It is useful in an attack against a symmetric cipher. But it is not nearly the threat that Shor's algorithm poses to our traditional asymmetric ciphers. 256-bit AES would be reduced to 128-bit strength against a large, stable quantum computer running Grover's algorithm. That's not as strong as the pre-quantum case, but it's still pretty strong.

liboqs — Open Quantum Safe

liboqs is the Open Quantum Safe shared library. You need this library. On FreeBSD, which this server runs, liboqs is available as a package. That package includes the shared library file /usr/local/lib/liboqs.so.

If you need to compile your own liboqs library, see the project's GitHub page for full details.

oqsprovider — Open Quantum Safe

With major version 3, OpenSSL supports "providers", modules for adding capabilities like FIPS compliance and post-quantum algorithms. The shared library file oqsprovider.so adds post-quantum algorithms to OpenSSL. However, you don't need oqsprovider to support the quantum-resistant algorithms in Nginx. It accomplishes that with the liboqs.so shared library.

I have some notes on building oqsprovider on FreeBSD. For Linux, see the Open Quantum Safe oqsprovider page.

Nginx and HTTP

HTTP/1 was finalized in 1996, and updated as 1.1 in 1997. HTTP/2 came out in 2015, and HTTP/3 in 2022. Of course we prefer the latest and greatest as they have better performance and capabilities. But we also want to support the older versions still used by web crawlers. And, unfortunately, also used by bots looking for vulnerable servers...

To do this, the nginx.conf file has the following overall structure. My server is hosting cromwell-intl.com, toilet-guru.com, and two other sites. The following works on Nginx 1.25.0 and later, that was the first release of Nginx to support QUIC and HTTP/3. The listen 443 directives would have to be changed for 1.24.0 and earlier. See the following sections for content and discussion of the server { ... } blocks.

# Let the server start one worker process per CPU core,
# allow many connections per worker.
worker_processes  auto;
events {
    worker_connections  1024;
}

http {
    # Define everything that can be shared by all sites.

    server {
	listen 80;
	server_name  cromwell-intl.com www.cromwell-intl.com;
	# Where and what to log,
	# Redirect to HTTPS on port 443
    }

    server {
	listen 80;
	server_name  toilet-guru.com www.toilet-guru.com;
	# Where and what to log,
	# Redirect to HTTPS on port 443
    }

    # Two other server { listen 80 } blocks...

    server {
	listen 443 quic;
	listen 443 ssl;
	http2 on;
	server_name	_;
	# This is an intentionally bogus definition.  Modern browsers
	# will ignore it because "_" doesn't match a hostname.  See
	# the below explanation.
    }

    server {
	listen 443 quic;
	listen 443 ssl;
	http2 on;
	server_name  cromwell-intl.com www.cromwell-intl.com
	# HTTPS details for that site...
    }

    server {
	listen 443 quic;
	listen 443 ssl;
	http2 on;
	server_name  toilet-guru.com toilet-guru-intl.com
	# HTTPS details for that site...
    }

    # Two other server HTTPS blocks...

}

Why the Bogus "_" Block?

A web server can host multiple web sites, each with a unique "root" location. All of the DNS names for the various sites can resolve to the same IP address. That's happening on this server.

A modern browser supporting the SNI or Server Name Indication extension to TLS specifies the server name in its request, and the server uses the corresponding server { ... } block.

However, requests from extremely old browsers without SNI (IE on Windows XP, for example) would simply use the first server block, getting the first certificate listed there. If the request is for any site other than the first one defined, that would imply information about relationships between sites and organizations.

Those ancient browser clients couldn't make a TLS connection because they require outdated SSL/TLS versions that this server doesn't support. However, tools like the the Qualys scanner simulate non-SNI browsers. So, it also retrieves the first server block. Its certificate will be indicated with "No SNI" and and no details shown by default.

With or without this block, the Qualys report will show "This site works only in browsers with SNI support" in the summary at the top. That's good, as browsers without SNI support are horribly outdated.

The server block for the bogus "_" site looks like the following. The RSA private key and certificate are for example.com, generated and self-signed on my server because the whole point of this is to be syntactically valid but entirely meaningless.

[... earlier blocks deleted ...]
    server {
	listen 443 quic;
	listen 443 ssl;
	http2 on;
	server_name	_;
	ssl_certificate     /usr/local/etc/letsencrypt/rsa-live/example.com/FakeSite_RSA.crt;
	ssl_certificate_key /usr/local/etc/letsencrypt/rsa-live/example.com/FakeSite_RSA.key;
	# Return a status code indicating that the server will not respond
	# to an unwanted request from a non-SNI browser.
	# https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/418
	return 418;
    }
[... following blocks deleted ...]

The "Shared by all sites" Section

As I mentioned at the beginning, I am simply copying in that block of the nginx.conf file with all the detailed comments that contains. The result is that the bulk of this is explanatory comments. I have made the non-comment content bold to make this a little easier to interpret.

This section includes the cryptography. To establish the connection, ssl_ecdh_curve for the Key Exchange or KEX as it's called with ECDHE, and the Key Encapsulation Mechanism or KEM as it's called with quantum-safe algorithms. And then ssl_ciphers for the symmetric cipher suite protecting the data.

As for which quantum-safe KEMs, you would normally select them based on a combination of strength and browser support. But there really isn't any browser support yet. As for strength, my understanding is that the algorithms with 512, 768, and 1024 in their names provide 128-bit, 192-bit, and 256-bit security, as those terms are applied to symmetric cryptography. So, I leaned toward them for my interim configuration.

It also limits the TLS protocol versions with ssl_protocols. My server runs TLS 1.3 and 1.2 only, preferring 1.3. Analysis of my server logs back in 2021 showed that almost no clients ran TLSv1.1, and while some ran TLSv1.0, they were mostly hostile automated bots looking for WordPress and other attack targets.

[...9 lines deleted...]
http {
    ###########################################################################
    # Start by defining everything that can be shared by all sites.
    # Look for "Context: http" for that directive in:
    #   https://nginx.org/en/docs/dirindex.html
    ###########################################################################
    include  mime.types;
    default_type  application/octet-stream;
    index  Index.html index.html;
    error_page 404 /ssi/404page.html;

    # For general log variables see:
    #   http://nginx.org/en/docs/http/ngx_http_log_module.html#log_format
    # For SSL-specific log variables see:
    #   http://nginx.org/en/docs/http/ngx_http_ssl_module.html#variables
    # This is basically the default plus the last three, which I added
    # to capture statistics on client use of protocols and crypto.
    log_format main '$remote_addr - $remote_user [$time_local] "$request" '
			'$status $body_bytes_sent "$http_referer" '
			'$ssl_protocol $ssl_curve $ssl_cipher';

    ###########################################################################
    ## Cryptography and TLS
    ###########################################################################

    # TLS versions 1.2 and 1.3 only, preferring 1.3.
    ssl_protocols TLSv1.3 TLSv1.2;
    # ssl_early_data is susceptible to replay attacks, although I'm not
    # doing anything on this server for which that could matter.
    ssl_early_data on;

    # Set aside 1 MB of storage to cache TLS connection details so a
    # subsequent page load can start immediately.  That's enough for
    # about 4,000 sessions.
    ssl_session_cache    shared:SSL:1m;
    # SSL session cache timeout defaults to 5 minutes, 1 minute should
    # be plenty.  This is abused by advertisers like Google and Facebook,
    # long timeouts like theirs will look suspicious.  See, for example:
    # https://www.zdnet.com/article/advertisers-can-track-users-across-the-internet-via-tls-session-resumption/
    ssl_session_timeout  1m;

    # Specify the ciphers.  Guidance is available here:
    #   https://mozilla.github.io/server-side-tls/ssl-config-generator/
    #   https://wiki.mozilla.org/Security/Server_Side_TLS
    #
    # Using 4.5 months of log data in 2021, the following supports all but
    # 157 out of 1,955,087 clients, or all but 0.00803%.  These are the
    # ciphers used by wikipedia.org as of May 2021, plus two more ChaCha20
    # ciphers used by just 0.0087% and 0.0049% of clients in 2021.
    #
    # See "man ciphers" for a mapping between OpenSSL cipher names and the
    # IETF names.  Or run:
    #   $ openssl -v 'ALL:eNULL' | less
    #
    # My preference:
    #   ECDHE-ECDSA first, then
    #   ECDHE-RSA, then
    #   DHE-RSA
    # and 256-bit before 128-bit in each block.
    #
    # HOWEVER:  OpenSSL's API does not let you specify the TLS 1.3 ciphers
    # in the same way as the earlier ones.  TLS 1.3 ciphers are set by:
    #    SSL_CTX_set_ciphersuites() and SSL_set_ciphersuites()
    # while TLS 1.2 and earlier are set by
    #    SSL_CTX_set_ciphers() and SSL_set_ciphers()
    # So, at least with OpenSSL 1.1.1, Nginx (and Apache) teams weren't
    # initially sure if the API would be stable over the long run.
    # They don't use the *ciphersuites() functions, so you can't change
    # cipher preference order for TLS 1.3.  You get the default:
    #   TLS_AES_256_GCM_SHA384
    #   TLS_CHACHA20_POLY1305_SHA256
    #   TLS_AES_128_GCM_SHA256
    # See https://wiki.openssl.org/index.php/TLS1.3 for background.
    # Also see:
    #   https://github.com/ssllabs/ssllabs-scan/issues/636
    #   https://trac.nginx.org/nginx/ticket/1529
    #
    # My resulting list:
    #   Cipher:                          Used by:
    #   ECDHE-ECDSA-AES256-GCM-SHA384    TLSv1.3
    #   ECDHE-ECDSA-CHACHA20-POLY1305    TLSv1.3
    #   ECDHE-ECDSA-AES128-GCM-SHA256    TLSv1.3
    #   ECDHE-RSA-AES256-GCM-SHA384      TLSv1.2
    #   ECDHE-RSA-CHACHA20-POLY1305      TLSv1.2
    #   ECDHE-RSA-AES128-GCM-SHA256      TLSv1.2
    #   DHE-RSA-AES256-GCM-SHA384        TLSv1.2
    #   DHE-RSA-CHACHA20-POLY1305        TLSv1.2
    #
    #	Unfortunately, unlike Apache, the ssl_ciphers list has to be
    #	one enormously long line.
    #
    ssl_ciphers ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-CHACHA20-POLY1305:ECDHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384:DHE-RSA-CHACHA20-POLY1305;

    # Have the server prefer them in that order:
    ssl_prefer_server_ciphers on;

    # However, only some clients have processors with the AES-NI extension.
    # AES is faster for clients with those processors, but ChaCha20 is
    # 3 times faster for processors without the special instructions.
    # Tell the server to prioritize ChaCha20, which means that if the
    # client reports ChaCha20 as its most-preferred cipher, use it;
    # otherwise the server preference wins.
    ssl_conf_command Options PrioritizeChaCha;

    # Elliptic curves for traditional ECDHE: prefer x448 and Ed25519 curves
    # first, NIST curves later.  Speculation remains about NSA backdoors in
    # NIST curves.  Chrome and derivatives dropped secp521r1 when it wasn't
    # listed in NSA's Suite B list.  Indexing bots *.search.msn.com will be
    # about the only clients using secp384r1.  Few clients support X448.
    #
    # As for the quantum-safe algorithms in the Open Quantum Safe suite,
    # these are just a few of them because nginx has a limit on the parameter
    # length.  These are also supported in the BoringSSL variant, important to
    # enable Chromium support.  Those starting "x25519_", "x448_", "p256_",
    # etc., are hybrid, meaning that a traditional key exchange algorithm
    # is also used along with a post-quantum ones  So, if the new quantum
    # algorithm is broken, we're still reasonably safe.
    #
    # We can test this with, e.g.:
    #   $ curl https://cromwell-intl.com --curves kyber1024 | grep Crypto
    # A list is available here:
    #   https://github.com/open-quantum-safe/oqs-provider/blob/main/ALGORITHMS.md
    # However, it lists, e.g., x25519_kyber512 and NOT x25519_kyber90s512, no
    # "90s".  As of late November and nginx 1.24.0_12,3 on FreeBSD 14.0,
    # these names and their default identifiers are not yet standardized.
    # To see what liboqs and oqsprovider offer:
    #   $ openssl list -key-exchange-algorithms
    #   $ openssl list -kem-algorithms
    #
    # As of late November 2023 and nginx 1.24.0_12,3, these worked for me
    # as an interim solution:
    #      name              TLS identifier
    #   kyber1024              0x023d
    #   p521_kyber1024         0x2f3d
    #   x448_kyber768          0x2f90
    #   x25519_kyber90s512     0x2fa9
    #   x25519_frodo640aes     0x2f80
    #   x25519_frodo640shake   0x2f81
    #   p256_kyber90s512       0x2f3e
    #   p256_frodo640aes       0x2f00
    #   p256_frodo640shake     0x2f01
    # Plus, of course, the non-PQC curves: X448:X25519:secp521r1:secp384r1
    #
    # Note that case matters!  "x488_kyber768" but "X448".
    #
    # https://en.wikipedia.org/wiki/NIST_Post-Quantum_Cryptography_Standardization
    # lists CRYSTALS-Kyber, NTRU, SABER, and Classic McEliece as finalists for
    # PKE/KEM, with FrodoKEM, NTRU Prime, BIKE, HQC, and SIKE as alternates.
    #
    # You can find the hex TLS identifier within the Client Hello packet,
    # within "Extension: supported groups", within "Supported Groups",
    # although Wireshark won't know what the identifier means, nor will
    # most servers.
    ssl_ecdh_curve kyber1024:p521_kyber1024:x448_kyber768:x25519_kyber90s512:p256_kyber90s512:X448:X25519:secp521r1:secp384r1;

    # This file contains the predefined DH group ffdhe4096 recommended
    # by IETF in RFC 7919.  Those have been audited, and may be more
    # resistant to attacks than randomly generated ones.  See:
    #   https://wiki.mozilla.org/Security/Server_Side_TLS
    # As opposed to generating my own with:
    #   openssl dhparam 4096 -out /etc/ssl/dhparam.pem
    ssl_dhparam /usr/local/nginx/ssl_dhparam;

    ###########################################################################
    # Compression suggestions from:
    # https://www.digitalocean.com/community/tutorials/how-to-increase-pagespeed-score-by-changing-your-nginx-configuration-on-ubuntu-16-04
    # - Level 5 (of 1-9) is almost as good as 9, and is much less work.
    # - Compressing very small things may make them larger.
    # - Compress even for clients connecting via proxies like Cloudfront.
    # - If client says it can handle compression, but it asks
    #   for uncompressed, send it the compressed version.
    # - Nginx compresses HTML by default.  Tell it about others data types
    #   that could benefit.
    ###########################################################################
    gzip on;
    gzip_comp_level 5;
    gzip_min_length 256;
    gzip_proxied any;
    gzip_vary on;
    gzip_types text/plain text/css application/x-font-ttf image/x-icon
	application/javascript application/x-javascript text/javascript;

    ###########################################################################
    # TCP Tuning
    #
    # Nagle's algorithm (potentially) adds a 0.2 second delay to every
    # TCP connection.  It made sense in the days of remote keyboard
    # interaction, but it gets in the way of transferring many files.
    # Turn on tcp_nodelay to disable Nagle's algorithm.
    #
    # FreeBSD man page for tcp(4) says:
    #    TCP_NODELAY   Under most circumstances, TCP sends data when it is
    #                  presented; when outstanding data has not yet been
    #                  acknowledged, it gathers small amounts of output to
    #                  be sent in a single packet once an acknowledgement
    #                  is received.  For a small number of clients, such
    #                  as window systems that send a stream of mouse
    #                  events which receive no replies, this packetization
    #                  may cause significant delays.  The boolean option
    #                  TCP_NODELAY defeats this algorithm.
    #
    # It's on by default, but why not make it explicit:
    ###########################################################################
    tcp_nodelay on;

    ###########################################################################
    # tcp_nopush blocks data until either it's done or the packet reaches the
    # MSS, so you more efficiently stream data in larger segments.  You can
    # send a response header and the beginning of a file in one packet, and
    # generally send a file with full packets.
    #
    # FreeBSD man page for tcp(4) says:
    #    TCP_NOPUSH    By convention, the sender-TCP will set the "push"
    #                  bit, and begin transmission immediately (if
    #                  permitted) at the end of every user call to
    #                  write(2) or writev(2).  When this option is set to
    #                  a non-zero value, TCP will delay sending any data
    #                  at all until either the socket is closed, or the
    #                  internal send buffer is filled.
    #
    # This is like the TCP_CORK socket option on Linux.  It's only
    # effective when sendfile is used.
    ###########################################################################
    tcp_nopush on;
    sendfile on;

    ###########################################################################
    # Limit how long a keep-alive client connection will stay open at the
    # server end.
    ###########################################################################
    keepalive_timeout 65;

    ###########################################################################
    # Enable the use of underscores in client request header fields.
    ###########################################################################
    underscores_in_headers on;

[... server { ... } blocks deleted ...]

How Strict Regarding TLS Versions and Ciphers?

My server runs TLS 1.3 because it's the state of the art. It also runs TLS 1.2 because a significant number of clients still run that. For the three-month period of August through October, 2023, I saw that about 7% of the clients ran TLS 1.2.

$ awk '{print $12}' /var/www/log/cromwell-intl-access.log | sort | uniq -c | sort -nr
2743473 TLSv1.3
 208603 TLSv1.2

I wouldn't want to give up 7% of ad revenue, so I continued to prefer TLSv1.3, tolerate TLSv1.2, and accept nothing else. Make your own decision carefully, based on the clients you want, and need, to support. I have a story that illustrates that.

In 2016 I went to Nashville to teach an introductory cybersecurity class to Tennessee state government employees. It was a very introductory course, one in which the more qualified students were often frustrated by the lack of meaningful hands-on exercises. So I always added what I could. One thing I always did was tell them about the Qualys scanner at ssllabs.com and have them run a scan of some system that they thought needed to be secure. "For example, your bank or some other place that you do financial transactions."

As usual, I strolled to the back of the room and chatted with some of the people sitting back there while we waited for people to get to the correct site and then run a scan that can take two or three minutes.

Looking toward the front of the room, I had never seen this before — bright red F scores were appearing on everyone's screens.

Usually almost everyone scans their bank, and things aren't perfect but they aren't too bad overall. This week, however, everyone had scanned a public-facing state government server in the department where they worked. And, the state-wide web server template had serious problems.

Most of the room looked terrified and asked me what they should do. I told them that they should tell someone about this. I had no idea as to exactly who they should inform, but someone needs to know about this.

I put my scan of my server up on the projector. We walked through the major differences, we me telling them that they could follow the links in the Qualys scan result for more details.

I went back to Nashville two months later. The classes ran in a large state office building which had a cafeteria on the ground floor. I would get breakfast there each day, grits and sweet tea.

A guy who had been in the earlier class saw me the first morning and came over to talk. He was complementing me on the class, he had really enjoyed it and gotten a lot out of it. I remembered that he had particularly enjoyed the Qualys scan and been interested in discussing the results. So, I told him that I planned to have them do it again, and hopefully things would work out better. He said that the state IT department had made changes since my last visit. "And, um, let me tell you what else."

The next Monday morning after the course, he set out get to the servers he ran to an A+ score. Within a couple of day, he had. And a few days after that, the state IT department jumped on him.

Their automated scans had detected that his department's servers were no longer adequately backward compatible with old browsers. A state government is obligated to provide services to all residents, at least within reason. But it can't require that all residents immediately upgrade their operating systems to something within the past year or two.

Both of us commented on the absurdity of a state IT department that automatically detected systems that were too secure, but ignored those with many obvious problems for which we've had solutions for several years. And so, later that week we did the Qualys scan and everyone found an A- score. Things had improved! I could tell that group about how much things had changed for the better in two months. And, warn them to not get too enthusiastic about locking down their department's servers.

Here are the other statistics I saw in late 2023.

HTTP version (I have no idea what's going on with the bots claiming to run HTTP/1.0)

$ awk '{print $8}' /var/www/log/cromwell-intl-access.log | sort | uniq -c | sort -nr
2710679 HTTP/1.1
 223717 HTTP/2.0
  12901 HTTP/3.0
   4783 HTTP/1.0 

Key Exchange

$ awk '{print $13}' /var/www/log/cromwell-intl-access.log | sort | uniq -c | sort -nr
2744018 X25519
 155687 secp521r1
  43443 secp384r1
   8910 X448

Cipher

$ awk '{print $14}' /var/www/log/cromwell-intl-access.log | sort | uniq -c | sort -nr
2739818 TLS_AES_256_GCM_SHA384
 191856 ECDHE-ECDSA-AES256-GCM-SHA384
  11676 ECDHE-ECDSA-CHACHA20-POLY1305
   4877 ECDHE-ECDSA-AES128-GCM-SHA256
   3640 TLS_CHACHA20_POLY1305_SHA256
    184 ECDHE-RSA-AES256-GCM-SHA384
      4 ECDHE-RSA-CHACHA20-POLY1305
      3 ECDHE-RSA-AES128-GCM-SHA256
      3 DHE-RSA-AES256-GCM-SHA384

Where Are The Quantum-Safe Clients?

So far there's curl and, well, that's pretty much it.

You can go to chrome://flags in Chrome and search for the experimental Kyber support and enable it. It will list 0x6399 as a support group for key exchange, indicating a hybrid of x25519 and Kyber1024. But that will only be recognized by some of the quantum-safe libraries.

Chrome describing a connection made with hybrid x25519-Kyber1024.

To test with curl do something like the following. Every page on my site includes a small block at the bottom listing the HTTP and TLS protocols plus the cryptography used. See 0x023d in the output, the TLS identifier used by liboqs for Kyber1024.

$ curl https://cromwell-intl.com --curves kyber1024 | grep Crypto
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
1<a href="/open-source/google-freebsd-tls/apache-http2-php.html">Protocols</a>: HTTP/2.0 / TLSv1.3<br><a href="/open-source/nginx-openssl-quantum-safe/">Crypto</a>: 0x023d / TLS_AES_256_GCM_SHA384</p>
00 26580    0 26580    0     0   102k      0 --:--:-- --:--:-- --:--:--  102k

At the time of writing this (December 2023), quantum-safe cryptography across web sites is just getting started. US NIST had published draft federal standards for post-quantum cryptography, and the public comment period had closed near the end of November.

Different implementations were using different hexadecimal identifiers, there was no common naming scheme for the PQC algorithms. Chrome's experimental hybrid x25519-Kyber768Draft00 support describes it in the TLS Client Hello message as 0x6399. Cloudflare's PQC test server recognizes that and runs that specific hybrid and draft algorithm. Mine doesn't.

Chrome offering hybrid x25519-Kyber1024 support as seen but not understood by Wireshark.

It will be a while before we get a really final standard, including a standard set of identifiers, and liboqs and other shared libraries support the algorithm and identifier suite, and those libraries start to be used by browsers.

Meanwhile people are enthusiastically suggesting that you simply recompile your own version of Chrome from source, linking it with all the appropriate shared libraries. Um, no. That would probably take a full day of compiling for each round of trying to figure out how to specify all the include files, shared libraries, and more.

Nginx and liboqs lets my server be ahead of the trend, but numerous course corrections will be needed before all this becomes common.

Two server { ... } Blocks for a Site

Now for the things that must be defined for each site, one server block for HTTP, mostly to log and redirect the client to HTTPS, then a server block for HTTPs.

In the second block, for HTTPS, you will see a line http2 on for HTTP/2, along with two lines about listening on port 443:

listen 443 quic;
listen 443 ssl;
https on; 

HTTP/3 runs on top of QUIC, "Quick UDP Internet Connection". That uses UDP to roughly simulate TCP, with much faster connection setup and a solution to the TCP "head-of-line blocking" problem.

QUIC runs on UDP port 443, so you will need to also open that on a firewall between your server and the rest of the Internet.

You also need an HTTPS type DNS record.

$ dig cromwell-intl.com HTTPS

; <<>> DiG 9.18.20 <<>> cromwell-intl.com HTTPS
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 58569
;; flags: qr rd ra; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 1

;; OPT PSEUDOSECTION:
; EDNS: version: 0, flags:; udp: 512
;; QUESTION SECTION:
;cromwell-intl.com.             IN      HTTPS

;; ANSWER SECTION:
cromwell-intl.com.      3600    IN      HTTPS   1 . alpn="h3,h2" ipv4hint=35.203.182.32

;; Query time: 55 msec
;; SERVER: 169.254.169.254#53(169.254.169.254) (UDP)
;; WHEN: Sat Dec 02 20:47:22 UTC 2023
;; MSG SIZE  rcvd: 79 

If the client assumed that the hostname started "www.", it gets back both the CNAME record correcting that assumption and the HTTPS record info it was looking for.

$ dig www.cromwell-intl.com HTTPS

; <<>> DiG 9.18.20 <<>> www.cromwell-intl.com https
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 20171
;; flags: qr rd ra; QUERY: 1, ANSWER: 2, AUTHORITY: 0, ADDITIONAL: 1

;; OPT PSEUDOSECTION:
; EDNS: version: 0, flags:; udp: 512
;; QUESTION SECTION:
;www.cromwell-intl.com.         IN      HTTPS

;; ANSWER SECTION:
www.cromwell-intl.com.  3600    IN      CNAME   cromwell-intl.com.
cromwell-intl.com.      3600    IN      HTTPS   1 . alpn="h3,h2" ipv4hint=35.203.182.32

;; Query time: 77 msec
;; SERVER: 169.254.169.254#53(169.254.169.254) (UDP)
;; WHEN: Sat Dec 02 20:56:48 UTC 2023
;; MSG SIZE  rcvd: 97 

A potential QUIC client can request an HTTPS DNS record for the server and find that it runs HTTP/2 and HTTP/3 or QUIC, along with a "hint" of the IPv4 address so it doesn't have to do a second DNS lookup. Yes, you can also include an IPv6 hint, but to my surprise, Google Cloud platform still doesn't support IPv6 (unless you're paying for a load-balancer).

You also need to add an Alt-Svc header to the response, see the details below.

You can test your HTTP/3 / QUIC service at http3check.net.

As for experimenting with QUIC and HTTP/3 from the client end, on Chrome go to chrome://flags/ and search for QUIC. Change the setting to Enabled and restart Chrome.

SmashingMagazine.com on HTTP/3:
Part 1: Core Concepts Part 2: Performance Improvements Part 3: Practical Deployment Options

You will see that each site has dual certificates, one for a 384-bit elliptic curve key pair, and the other for a 4096-bit RSA key pair, both signed by Let's Encrypt.

The HTTPS server blocks also set HTTPS security headers. See the comments for further details. You can check yours at:
cspvalidator.org report-uri.io securityheaders.io

Here are the HTTP and HTTPS server blocks for cromwell-intl.com, again with non-comment content in bold.

[... early blocks deleted ...]
    ###########################################################################
    # HTTP Server: Log and redirect to HTTPS.
    ###########################################################################
    server {
	listen       80;
	server_name  cromwell-intl.com www.cromwell-intl.com;

	access_log  /var/www/logs/cromwell-intl-access.log  main;
	error_log   /var/www/logs/cromwell-intl-error.log;
	# Don't log bulky data, even HTTP requests that will be redirected.
	location ~* \.(jpg|jpeg|png|gif|ico|css|js|ttf)$ {
		access_log off;
	}

	root /usr/local/nginx/html;

	# Redirect to HTTPS with same hostname, don't cause a
	# DNS lookup *and* a new protocol at the same time.
	return 301 https://$http_host$request_uri;
    }


    ###########################################################################
    # Now the first of the real HTTPS servers.
    ###########################################################################
    server {
	# FreeBSD 14.0 comes with:
	#   nginx 1.24.0_12,3
	#   nginx-devel 1.25.2_7
	# As stated here:
	#   https://nginx.org/en/docs/quic.html
	# "Support for QUIC and HTTP/3 protocols is available since 1.25.0."
	listen 443 quic;
	listen 443 ssl;
	http2 on;
	server_name  cromwell-intl.com www.cromwell-intl.com;

	# Redirect if hostname starts "www.".
	if ($http_host = www.cromwell-intl.com) {
		return 301 https://cromwell-intl.com$request_uri;
	}

	root /usr/local/nginx/html;

	#######################################################################
	## Logging and cache control for images, CSS, JavaScript, fonts
	#######################################################################
	access_log  /var/www/logs/cromwell-intl-access.log  main;
	error_log   /var/www/logs/cromwell-intl-error.log;
	location ~* \.(jpg|jpeg|png|gif|ico|css|js|ttf)$ {

		###############################################################
		# Logging
		###############################################################
		# Don't log bulky data.
		# HOWEVER, it seems we get a log entry no matter what
		# if the request was referred in by something else.  For
		# example, fetching an image because of Google search.
		access_log off;

		###############################################################
		# Caching
		###############################################################
		# Tell client to cache bulky data for 7 days, which is
		# the Google Pagespeed recommendation / requirement.
		expires 7d;
		# "Pragma public" = Now rather outdated, skip it.
		# "public" = Cache in browser and any intermediate caches.
		# "no-transform" = Caches may not modify my data formats.
		add_header Cache-Control "public, no-transform";
		# Tell client and intermediate caches to understand that
		# compressed and uncompressed versions are equivalent.
		# Goes with gzip_vary below.  More details here:
		# https://blog.stackpath.com/accept-encoding-vary-important
		add_header Vary "Accept-Encoding";

		###############################################################
		# Block image hotlinking.  I changed "rewrite" to "return"
		# in the description provided here:
		# http://nodotcom.org/nginx-image-hotlink-rewrite.html
		# Also see:
		# http://nginx.org/en/docs/http/ngx_http_referer_module.html
		###############################################################
		valid_referers none blocked ~\.google\. ~\.printfriendly\. ~\.bing\. ~\.yahoo\. ~\.baidu.com server_names ~($host);
		if ($invalid_referer) {
			return 301 https://$host/pictures/denied.png;
		}
	}
	# Needed because of above hotlink redirection.
	location = /pictures/denied.png { }

	#######################################################################
	# Certificates and private keys.
	# Send both ECC and RSA certificates.
	# Generating and renewing Let's Encrypt certificates described here:
	# https://cromwell-intl.com/open-source/google-freebsd-tls/tls-certificate.html
	#######################################################################
	# ECC
	ssl_certificate     /usr/local/etc/letsencrypt/ecc-live/cromwell-intl.com/fullchain.pem;
	ssl_certificate_key /usr/local/etc/letsencrypt/ecc-live/cromwell-intl.com/privkey.pem;
	# RSA
	ssl_certificate     /usr/local/etc/letsencrypt/rsa-live/cromwell-intl.com/fullchain.pem;
	ssl_certificate_key /usr/local/etc/letsencrypt/rsa-live/cromwell-intl.com/privkey.pem;

	#######################################################################
	# Security headers
	#######################################################################

	# HTTP/3 over QUIC
	# Should need just the first one, eventually:  h3=":443";ma=86400
	# However, some testing sites like https://domsignal.com/ and thus
	# probably lots of browsers need h3-29 or h3-Q050.
	add_header Alt-Svc 'h3=":443";ma=86400, h3-25=":443";ma=86400, h3-29=":443";ma=86400, h3-Q050=":443";ma-86400, h3-Q046=":443";ma=86400, quic=":443";ma=86400';

	# HSTS
	# The HSTS Preloading mechanism uses a list compiled by Google,
	# and is used by major browsers such as Chrome, Firefox, Opera,
	# Safari, and Edge.
	add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload";
	# OCSP Stapling
	# Support for OCSP Must-Staple is done in the CSR when the certificate
	# is generated.
	ssl_stapling on;
	ssl_stapling_verify on;
	# X-Frame-Options
	add_header X-Frame-Options "SAMEORIGIN";
	# Turn on XSS / Cross-Site Scripting protection in browsers.
	# "1" = on,
	# "mode=block" = block an attack, don't try to sanitize it
	add_header X-Xss-Protection "1; mode=block";
	# Tell the browser (Chrome and Explorer, anyway) not to "sniff" the
	# content and try to figure it out, but simply use the MIME type
	# reported by the server.
	# This means that all files named "*.jpg" must be JPEG, and so on!
	add_header X-Content-Type-Options "nosniff";
	# I set Referrer-Policy liberally.  I think referrer info
	# can be helpful without being absolutely trustworthy,
	# and I don't have any scandalous or sensitive URLs.
	# See https://scotthelme.co.uk/a-new-security-header-referrer-policy/
	add_header Referrer-Policy "no-referrer-when-downgrade";
	# Content Security Policy
	# *If* a client could add a stylesheet, then they could drastically
	# change visibility and appearance.  Also, there is a way to steal
	# sensitive data from within a page.  See:
	#   https://www.mike-gualtieri.com/posts/stealing-data-with-css-attack-and-defense
	# However, none of my pages have forms or handle sensitive data.
	# So, I feel safe using 'unsafe-inline' below.
	#
	# NOTE that if I didn't do that, I would have to convert every single
	# 'style="..."' string to a CSS class instead.  The below limits CSS
	# to coming from my site and *.googleapis.com while allowing inline
	# 'style="..."', which seems plenty safe for me.  Also see the above
	# comment block about setting $noncestring, and why my attempt to
	# replace 'unsafe-inline' with 'nonce-$noncestring' failed.
	#
	# NOTE that adding a similar parameter for style-src breaks both
	# Google AdSense and Infolinks ads, even with 'unsafe-inline'.
	add_header Content-Security-Policy "upgrade-insecure-requests; style-src https://cromwell-intl.com https://*.googleapis.com 'unsafe-inline'; frame-ancestors 'self';";
	# Permissions Policy, see:
	# https://scotthelme.co.uk/goodbye-feature-policy-and-hello-permissions-policy/
	add_header Permissions-Policy "fullscreen=(self)";

	# As recommended by https://frog.tips/
	add_header X-Frog-Unsafe "0";

	# Check security headers at:
	#   https://cspvalidator.org/
	#   https://report-uri.io/home/analyse
	#   https://securityheaders.io/

	# Include the TLS protocol version and negotiated cipher
	# in the HTTP headers, so we can capture them in the logs.
	add_header X-HTTPS-Protocol $ssl_protocol;
	add_header X-HTTPS-Cipher $ssl_cipher;
	add_header X-HTTPS-Curve $ssl_curve;

	#######################################################################
	# Process all *.html files as PHP.  The php-fam service must be
	# running, listening on TCP/9000 on localhost only.
	# The try_files line serves the 404 error page if they ask for
	# a non-existent file.  Without that, you get a cryptic error AND
	# these is a security hole, see:
	#   http://forum.nginx.org/read.php?2,88845,page=3
	#######################################################################
	location ~ \.html$ {
	    try_files     $uri =404;
	    include       fastcgi_params;
	    fastcgi_index Index.html;
	    fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
	    fastcgi_param TLS_PROTOCOL	  $ssl_protocol;
	    fastcgi_param TLS_CIPHER	  $ssl_cipher;
	    fastcgi_param TLS_CURVE	  $ssl_curve;
	    fastcgi_param QUERY_STRING	  $query_string;
	    fastcgi_param REQUEST_METHOD  $request_method;
	    fastcgi_param CONTENT_TYPE	  $content_type;
	    fastcgi_param CONTENT_LENGTH  $content_length;
	    fastcgi_param SCRIPT_NAME	  $fastcgi_script_name;
	    fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
	    fastcgi_param REQUEST_URI	  $request_uri;
	    fastcgi_param DOCUMENT_URI	  $document_uri;
	    fastcgi_param DOCUMENT_ROOT   $document_root;
	    fastcgi_param SERVER_PROTOCOL $server_protocol;
	    fastcgi_param REMOTE_ADDR	  $remote_addr;
	    fastcgi_param REMOTE_PORT	  $remote_port;
	    fastcgi_param SERVER_ADDR	  $server_addr;
	    fastcgi_param SERVER_PORT	  $server_port;
	    fastcgi_param SERVER_NAME	  $server_name;
	    fastcgi_hide_header X-Powered-By;
	    fastcgi_pass 127.0.0.1:9000;
	}

	#######################################################################
	# Now the URL rewriting / redirection rules.
	# A note from:
	# https://www.nginx.com/resources/wiki/start/topics/tutorials/config_pitfalls/
	# BAD:
	#   rewrite ^/(.*)$ http://example.com/$1 permanent;
	# GOOD:
	#   rewrite ^ http://example.com$request_uri? permanent;
	# BETTER:
	#   return 301 http://example.com$request_uri;
	#
	# For rewrites:
	#   rewrite OLD_REGEX NEW permanent;
	# "permanent" = HTTP 301, permanent redirect
	# "redirect" =  HTTP 302, temporary redirect
	#######################################################################

	[... many lines of rewriting and redirection rules for this site...]

    }

[... HTTP and HTTPS server { ... } blocks for the other sites ...]

}

Results — A+ from Qualys / ssllabs.com

Here's the score, you can check it yourself.

A+ result from Qualys / ssllabs.com.

Further Notes

What's the Point of Asymmetric Encryption? How Does Asymmetric Cryptography Work? Quantum Computing and Quantum-Safe Cryptography

US NIST Post-QUantum Cryptography Project Cloudflare discussion of PQC (April 2022) Cloudflare Research page to test your browser The SSL Store blog on Google Chrome PQC support (August 2023) Open Quantum Safe Project Open Quantum Safe at Github open-quantum-safe / liboqs open-quantum-safe / oqs-provider oqs-provider algorithm names and IDs Lan Tian's Blog

To the main Security Page