TLSv1.3 HTTP/2 on Apache stack

Samson Sham
10 min readNov 19, 2019

--

How to enable your Apache & php stack with self-signed certificate TLSv1.3 on HTTP/2 for macOS?

tl;dr

Apache with latest OpenSSL built are required, thus Bitnami package is used. dnsmasq is used for domain name resolution for TLSv1.3 with certificate X.509v3

First off. Does anyone know your macOS has Apache & php pre-installed?
Most likely not, but it doesn’t matter, since the Apache that comes with your macOS (latest Catalina 10.15) uses LibreSSL 2.8.3 which doesn’t support TLSv1.3 as of Oct 25, 2019.

You can check by:

$ apachectl -v    # Apache version
$ openssl version # OpenSSL version

Apache Problem

You cannot just install another version of OpenSSL and hope Apache will magically work. It doesn’t work like this, since Apache needs to be compiled with the SSL tool path configured. Compiling with source code seems scary, why not get a pre-built Apache with correct SSL tool version included?

So I have chosen to use my beloved Bitnami packaged MAMP stack, which has been supporting my work for more than 7 years.

By current version of 7.3.10, it has the prerequisites of Apache version (2.4.41) & OpenSSL version (1.1.1d) supporting HTTP/2 and TLSv1.3 with respectively.

Basically MAMP is a package with Apache, MySQL, PHP bundled for Mac, there are packages for Linux(LAMP) or Windows(WAMP). Such that all bundled software(s) are encapsulated from your host environment.

DocumentRoot
represents the file directory location of where your front page is serving, e.g.localhost:8080. For Bitnami, it is located at /Applications/mampstack-[version]/apache2/htdocs/. This is defined in httpd.conf.

httpd.conf
a file containing configurations which controls the behaviours and features of your Apache server, it is located at /Applications/mampstack-[version]/apache2/conf/. The most apparent configuration would be your server TCP port, where Bitnami has default to use port 8080 instead of port 80 for hosting your server.

bitnami.conf
is an extension ofhttpd.conf which contains Bitnami customized configurations for HTTPS or others, located at /Applications/mampstack-[version]/apache2/conf/bitnami/. We will mostly edit these 2 files on our following sections.

Bitnami package comes with a controller panel app, manager-osx.app. Since Bitnami wouldn’t start the server services automatically on startup, though I believe you can configure it, you will need to activate the services on machine startup.

HTTP/2 configuration

Configurations are mostly in these two files

/Applications/mampstack-[version]/apache2/conf/httpd.conf
/Applications/mampstack-[version]/apache2/conf/bitnami/bitnami.conf

Enable mod_http2 for HTTP/2.

LoadModule http2_module modules/mod_http2.so

Then you will need to enforce the protocol

Protocols h2 http/1.1

h2 is assuming you use HTTPS (HTTP/2 over TLS), while h2cis without TLS (HTTP/2 over TCP). But from the trends of the internet, it is very likely that all browser vendors will be enforcing TLS on all both HTTP/1.1, HTTP/2 and HTTP/3 in the coming years.

# (Optional) Since default is ON if http2_module is enabled
# Put inside the VirtualHost
H2Direct On

PHP Problem

It happens that the pre-built Apache by default has configured with MPM prefork for php, where MPM prefork isn’t compatible with HTTP/2 (Reference: https://httpd.apache.org/docs/2.4/howto/http2.html#mpm-config). For that reason, we have to switch to MPM event and use PHP-FPM. Luckily there is a pre-configured module script we can use by disabling the mod_php. Commenting out the MPM prefork module and mod_php module, and enabling MPM event.

# LoadModule mpm_prefork_module modules/mod_mpm_prefork.so
# LoadModule php7_module modules/libphp7.so
LoadModule mpm_event_module modules/mod_mpm_event.so

whereas the following script is in place from the shipped Apache, which helps you configure PHP-FPM automatically when mod_php is disabled.

# This enables using PHP-FPM when mod_php is disabled
<IfModule !php7_module>
Action application/x-httpd-php "/bitnami-error-php-fpm-did-not-handle-the-connection"
Define USE_PHP_FPM
Include "conf/php-fpm-apache.conf"
</IfModule>

After configuring the httpd configurations, as according to the documentation, you will need to rename the disabled control script of php to enable PHP-FPM.

# Do note that this is not in the Apache folder
$ mv /Applications/mampstack-[version]/php/scripts/ctl.sh.disabled \
/Applications/mampstack-[version]/php/scripts/ctl.sh

At this stage, it is better to reboot your host machine, this will save you from a lot of troubles, trust me! After reboot, just start the PHP-FPM& Apache from the manager-osx.app.

Check your connection

At this stage, you should be able to test the connection with HTTP/2 by

$ curl -v --http2 https://www.your-domain-name.com

Your should be able to see something like this:

* ALPN, offering h2
* ALPN, offering http/1.1
* successfully set certificate verify locations:
* CAfile: /etc/ssl/cert.pem
CApath: none
* TLSv1.2 (OUT), TLS handshake, Client hello (1):
* TLSv1.2 (IN), TLS handshake, Server hello (2):
* TLSv1.2 (IN), TLS handshake, Certificate (11):
* TLSv1.2 (IN), TLS handshake, Server key exchange (12):
* TLSv1.2 (IN), TLS handshake, Server finished (14):
* TLSv1.2 (OUT), TLS handshake, Client key exchange (16):
* TLSv1.2 (OUT), TLS change cipher, Change cipher spec (1):
* TLSv1.2 (OUT), TLS handshake, Finished (20):
* TLSv1.2 (IN), TLS change cipher, Change cipher spec (1):
* TLSv1.2 (IN), TLS handshake, Finished (20):
* SSL connection using TLSv1.2 / ECDHE-RSA-AES256-GCM-SHA384
* ALPN, server accepted to use h2

TLSv1.3 configuration

Certificate and key preparation

Before we setup the configuration, we may prepare a X.509 certificate for our SSL. It is outlined in RFC-8446#4.4.2.3 that for TLSv1.3 the certificate MUST be X.509v3. As such, we may prepare a X.509 version 3 configuration like this:

[ req ]
distinguished_name = req_distinguished_name
req_extensions = v3_req
x509_extensions = v3_req

[ req_distinguished_name ]

[ v3_req ]
basicConstraints = critical, CA:true, pathlen:1
keyUsage = nonRepudiation, digitalSignature, keyEncipherment, keyCertSign
subjectAltName = @alt_names

[ alt_names ]
DNS.1 = localhost
DNS.2 = www.your-domain-name.com

We may save this configuration as openssl.cnf. Notice that we are declaring this certificate as a root certificate authority where certificate authority(CA) parameter is true, such that this certificate can be installed individually onto mobile devices without further chain of trust. By specifying both req_extensions and x509_extensions to same namespaced configuration, with this trick, we can skip a step of creating the a certificate signing request(CSR), but I have to clarify this should not be the proper way for going into production. Please rely on a professional public key infrastructure(PKI) service.

  • req_extensions is where you declare the namespace containing a list of extensions to add for CSR, normally should not be a CA.
  • x509_extensions is where you declare the namespace containing a list of extensions to certificate generated when -x509 is requested.
  • subjectAltName, Subject Alternative Name(SAN), is where we declare our domain name(s).

By using the following command, we can generate the certificate and key pair:

$ openssl req -new \
-newkey rsa:4096 \
-keyout server.key \
-sha512 \
-config openssl.cnf \
-out server.crt \
-nodes \
-subj "/O=YourOrganization/CN=www.your-domain-name.com" \
-x509 \
-days 9999
  • -newkey: generating a new private key with 4096 bit
  • -sha512: signing the key with 512 bit length long SHA hash
  • -nodes: don’t encrypt the output key to avoid entering passphrase
  • -subj: details on the certificate, this common name(CN) should match what is specified in configuration file’s SANs
  • -config: request certificate as according to this X.509 version 3 configuration file

Details please refer to OpenSSL documentation.

Apache configurations

As we know that TLSv1.0/1.1 will be end of life in March 2020, and most legacy SSL protocols have been outdated already, so why not just enable TLSv1.2 & TLSv1.3?

# Requires Apache 2.4.36 & OpenSSL 1.1.1
SSLProtocol -all +TLSv1.3 +TLSv1.2
# Specify the SSL Certificate and Key generated on previous section
SSLCertificateFile "/path/to/server.crt"
SSLCertificateKeyFile "/path/to/server.key"

You can find some useful configurations here:

You can also find some useful SSL suggestions here:

Trusting self signed certificate

A TLS handshake wouldn’t be complete if you use a self signed certificate for your server. Clients accessing your server will verify your certificate against known Certificate Authorities. Browsers will warn “Your connection is not private” if clients have not manually trust your self signed certificate on their devices. To trust a self signed certificate, you may first obtain the certificate(*.crt). Your host machine may have the certificate already as you generated from previous section, but you may need to download the certificate(*.crt) for your client devices either by hosting the cert or by email, etc.

For desktop clients,

For iOS clients,

For Android clients,

Cipher Suite

TLSv1.3 has a default set of cypher suites, which are not interchangeable by TLSv1.2, so if you want to support TLSv1.2, you may require to add both cypher suites together.

Also, do note that OpenSSL will no longer support TLSv1.3 cypher suite name containing special characters such as “+”, “!”, “-”, etc.

The standard of TLSv1.3 is published as RFC-8446.

Key exchange mechanism

On TLSv1.3, all public key exchange mechanisms provide forward secrecy (RFC-8446#1.2), so if you want to leave TLSv1.2 open as backward compatibility, you may consider to use elliptic curve Diffie-Hellman ephemeral(ECDHE) or DHE as key exchange algorithm for forward secrecy to align with TLSv1.3, and add the finite field Diffie-Hellman ephemeral(FFDHE) groups as DH parameter as outlined in RFC-7919, by adding SSLOpenSSLConfCmd DHParameters to your Apache configuration:

# Either you may get the FFDHE groups from Mozilla
# (Each request will generate a new groups)
# curl https://ssl-config.mozilla.org/ffdhe2048.txt > /path/to/dhparam.pem# Or you may generate it by yourself by OpenSSL
# openssl dhparam -out dhparam.pem 2048
SSLOpenSSLConfCmd DHParameters "/etc/ssl/dhparam.pem"

which guarantees the keys are on the curve making it more resilient against small subgroup attacks.

Domain name resolution

If it isn’t ngrok not supporting TLSv1.3 and HTTP/2 at the moment (Verified by ngrok founder), I will be using ngrok, I would always like to try ngrok, yet I had no chance.

For your host environment, there are multiple ways you can redirect www.your-domain-name.com traffic to your own server,

  • Register host, edit the registry on /etc/hosts, add the entry
127.0.0.1        www.your-domain-name.com

This will allow the traffic of your host environment to be routed to 127.0.0.1

The following methods will be to add a Name Server resolver, which will require an extra step to setup a DNS server. But in this way, not only the host environment, but we also open up the opportunity to allow other devices to connect to the server as well via LAN or possibly a designated server with a WAN IP.

  • ̶A̶d̶d̶ ̶N̶a̶m̶e̶ ̶S̶e̶r̶v̶e̶r̶ ̶r̶e̶s̶o̶l̶v̶e̶r̶ ̶c̶o̶n̶f̶i̶g̶,̶ ̶e̶d̶i̶t̶ ̶t̶h̶e̶ ̶r̶e̶g̶i̶s̶t̶r̶y̶ ̶o̶n̶/etc/resolv.conf, ̶a̶d̶d̶ ̶t̶h̶e̶ ̶e̶n̶t̶r̶y̶ ̶o̶n̶ ̶t̶o̶p̶ ̶o̶f̶ ̶t̶h̶e̶ ̶l̶i̶s̶t̶
# While this file will always be overwritten
# when connecting to a DHCP server,
# therefore this method is not recommended
nameserver 127.0.0.1
  • Add a domain name specific Name Server resolver. First create directory/etc/resolver, then add a file, whose filename be named as www.your-domain-name.com with file content:
nameserver       127.0.0.1

This way we are declaring to use 127.0.0.1(it can be other IP!) as our DNS resolver, whether it be our first resolver or it be our specific domain resolver with respectively.

With the Name Server directive set up, we still need a DNS server to answer the enquiry. dnsmasq is what we will be using. There are many implementations of dnsmasq on the docker hub, and andyshinn’s one is being used this time due to its lightweight-ness.

$ docker run -it --rm -p 53:53/udp \
--cap-add=NET_ADMIN \
andyshinn/dnsmasq:latest \
--log-queries \
--log-facilities=- \
--address=/www.your-domain-name.com/192.168.1.1

Here we are setting up a DNS server with docker by mapping the host UDP 53 port to our docker container that answer to enquiry of www.your-domain-name.com and to resolve it as 192.168.1.1, pay attention that the IP address is being seen by the external enquiries as well, hence if you’re returning a LAN IP, do make sure the external devices are in that subnet as well, this is quite similar to the usage of ngrok so to speak. You can also specify the resolution IP as a WAN IP or a docker container IP as long as the external devices can reach them.

At this stage, we are done with the host environment. But how about the external devices? In my example, I am sharing the network through my host environment, therefore I am returning a host local IP 192.168.1.1. By sharing the network through host, the connected external devices are automatically configured with their DNS servers pointing to our host. This is the easiest way.

If you want the DNS server be separated from the host environment, you may need a dedicated IP for DNS server itself, then setup your devices with custom DNS server with that IP.

Check your connection

cURL is a good tool for debugging HTTP connections, but before we use it, please check your cURL version is compatible for HTTP/2 and TLSv1.3. Be reminded that the cURL shipped with OS Catalina (10.15) is using LibreSSL, which does not support TLSv1.3 as mentioned at the beginning. Coincidentally, how unfortunate that the cURL that comes with Bitnami MAMP doesn’t have nghttp2 library included, which means it is supporting TLSv1.3 but not HTTP/2 on the other hand.

# With MacOS cURL, you can test the HTTP/2 capability
$ curl -v --http2 https://www.your-domain-name.com
# With Bitnami's cURL, you can test the TLSv1.3 capability
$ /Applications/mampstack-[version]/common/bin/curl -v https://www.your-domain-name.com

On the other hand, with OpenSSL, you can test the TLS handshake connection, this way you are able to get insights of which HTTP and TLS protocols are in used.

$ openssl s_client -connect www.your-domain-name.com:443 -alpn 'h2'
$ openssl s_client -connect www.your-domain-name.com:443 -status

By checking against application-layer protocol negotiation(ALPN) parameter, you will be able to see if HTTP/2 is chosen to be the protocol to proceed with the handshake.

complete architect and brief network flow

More

For TLS exploration, you can refer to this note:

For certificates and PKI (private key infrastructure), you may refer to the explanation here:

The explanation by Mike is very thorough and easy to understand at the same time practical, you really want to spend some time on reading it.

Last bit

Please give your support to Hong Kong citizens standing against the totalitarian states. Search #StandWithHK and you may know some details.

--

--