This is a private blog by Jens Lechtenbörger.

Jens Lechtenbörger

OpenPGP key: 0xA142FD84
(What is OpenPGP? Learn how to protect your e-mail.)

Creative Commons License
Unless explicitly stated otherwise, my posts on this blog are licensed under a Creative Commons Attribution-ShareAlike 4.0 International License.

Certificate Pinning for GNU/Linux and Android

Previously, I described the dismal state of SSL/TLS security and explained how certificate pinning protects against man-in-the-middle (MITM) attacks; in particular, I recommended GnuTLS with its command line tool gnutls-cli for do-it-yourself certificate pinning based on trust-on-first-use (TOFU). In this post, I explain how I apply those ideas on my Android phone. In a nutshell, I use gnutls-cli in combination with socat and shell scripting to create separate, certificate pinned TLS tunnels for every target server; then, I configure my apps to connect into such a tunnel instead of the target server, which protects my apps against MITM attacks with “trusted” and other certificates. Note that nothing in this post is specific to Android; instead, I installed the app Lil’ Debi, which provides a Debian GNU/Linux system as prerequisite for the following.

Prepare Debian Environment

Lil’ Debi by default uses DNS servers of a US based search engine company, which is configured in /etc/resolv.conf. I don’t want that company to learn when I access my e-mail (and more). Instead, I’d like to use the “normal” DNS servers of the network to which I’m connected, which gets configured automatically via DHCP on Android. However, I don’t know how to inject that information reliably into Debian (automatically, upon connectivity changes). Hence, I’m currently manually running something like dhclient -d wlan0, which updates the network configuration. I’d love to hear about better solutions.

Next, the stable version of gnutls-bin does not support the option --strict-tofu. More recent versions are available among “experimental” packages. To install those, I switched to Debian Testing (replace stable with testing in /etc/apt/sources.list; do apt-get update, apt-get dist-upgrade, apt-get autoremove). Then, I installed gnutls-cli:
apt-get -t experimental install gnutls-bin

Afterwards, I created a non-root user gnutls with directories to be used in shell scripts below:
useradd -s /bin/false -r -d /var/lib/gnutls gnutls
mkdir /var/{lib,log}/gnutls
chown gnutls /var/{lib,log}/gnutls
chmod 755 /var/{lib,log}/gnutls

For network access on android, I also needed to assign gnutls to a special group as follows. (Before that, I got “net.c:142: socket() failed: Permission denied” or “socket: Permission denied” for network commands.)
groupadd -g 3003 aid_inet
usermod -G aid_inet gnutls

Finally, certificates need to be pinned with GnuTLS. I did that on my PC as described previously and copied the resulting file ~/.gnutls/known_hosts to /var/lib/gnutls/.gnutls/known_hosts.

Certificate Pinning via Tunnels/Proxies

I use socat to create (encrypting) proxies (i.e., local servers that relay received data towards the real destinations). In my case, socat relays received data via a shell script into GnuTLS, which establishes the TLS connection to the real destination and performs certificate checking with option --strict-tofu. Thus, the combination of socat, shell script, and gnutls-cli creates a TLS tunnel with certificate pinning against MITM attacks. Clearly, none of this is necessary for apps that pin certificates themselves. (On my phone, ChatSecure, a chat app implementing end-to-end encryption with Off-The-Record (OTR) Messaging, pins certificates, but other apps such as K-9 Mail, CalDAV Sync Adapter. and DAVdroid do not.) For the sake of an example, suppose that I send e-mail via server smtp.example.org at port 25, which I would normally enter in my e-mail app along with the setting to use SSL/TLS for every connection, which leaves me vulnerable to MITM attacks with “trusted” certificates. Let’s see how to replace that setting with a secure tunnel. First, the following command starts a socat proxy that listens on port 1125 for incoming network connections from my phone. For every connection, it executes the script gnutls-tunnel.shand relays all network traffic into that script:
$ socat TCP4-LISTEN:1125,bind=,reuseaddr,fork \
EXEC:"/usr/local/bin/gnutls-tunnel.sh -s -t smtp.example.org 25"

Second, the script is invoked with the options -s -t smtp.example.org 25. Thus, the script invokes gnutls-cli to open an SMTP (e-mail delivery) connection with TLS protection (some details of gnutls-tunnel.sh are explained below, details of gnutls-cli in my previous article). If certificate verification succeeds, this establishes a tunnel from the phone’s local port 1125 to the mail server. (There is nothing special about the number 1125; I prefer numbers ending in “25” for SMTP.)

Third, I configure my e-mail app to use the server named localhost at port 1125 (without encryption). Then, the app sends e-mails into socat, which forwards them into the script, which in turn relays them via a GnuTLS secured connection to the mail server smtp.example.org.

Shell Scripting

To setup my GnuTLS tunnels, I use three scripts, which are contained in this tar archive. (One of those scripts contains the following text: “I don’t like shell scripts. I don’t know much about shell scripts. This is a shell script. Use at your own risk. Read the license.”)

First, instead of the invocation of socat shown above, I’m using the following wrapper script, start-tls.sh, whose first argument needs to be the local port to be used by socat, while the other arguments are passed on. Moreover, the script redirects log messages to a file.
umask 0022
socat TCP4-LISTEN:$LPORT,bind=,reuseaddr,fork EXEC:"/usr/local/bin/gnutls-tunnel.sh $*" >> $LOG 2>&1 &

Second, gnutls-tunnel.sh embeds the invocation of gnutls-cli --strict-tofu, parses its output, and writes log messages. That script is too long to reproduce here, but I’d like to point out that it sends data through a background process as described by Marco Maggi. Moreover, it uses a “delayed encrypted bridge.” Currently, the script knows the following essential options:

  • -t: Use option --starttls for gnutls-cli; this start with a protocol-specific plaintext connection which switches to TLS later on.
  • -h: Try to talk HTTP in case of errors.
  • -i: Try to talk IMAP in case of errors.
  • -s: Try to talk SMTP, possibly before STARTTLS and in case of errors.

Third, I use a script called start-tls-tunnels.sh to start my TLS tunnels, essentially as follows:
run_as () {
su -s $TLSSHELL -c "$1" $TLSUSER
# SMTP (-s) with STARTTLS (-t) if SMTPS is not supported, typically to
# port 25 or 587:
run_as "/usr/local/bin/socat-tls.sh 1125 -t -s smtp.example.org 587"
# Without -t if server supports SMTPS at port 465:
run_as "/usr/local/bin/socat-tls.sh 1225 -s mail.example.org 465"
# IMAPS (-i) at port 993:
run_as "/usr/local/bin/socat-tls.sh 1193 -i imap.example.org 993"
run_as "/usr/local/bin/socat-tls.sh 1293 -i imap2.example.org 993"
# HTTPS (-h) at port 443:
run_as "/usr/local/bin/socat-tls.sh 1143 -h owncloud.example.org 443"

Once the Debian system is running (via Lil’ Debi), I invoke start-tls-tunnels.sh in the Debian shell. (This could be automated in the app’s startup script start-debian.sh.) Then, I configure K-9 Mail to use localhostwith the local ports defined in the script (without encryption).

(You may want to remove the log files under /var/log/gnutls from time to time.)

Certificate Expiry, MITM Attacks

Whenever certificate verification fails because the presented certificate does not match the pinned one, gnutls-tunnel.sh logs an error message and reports an error condition to the invoking app. Clearly, it is up to the app whether and how to inform the user. For example, K-9 Mail fails silently for e-mail retrieval via IMAP (which is an old bug) but triggers a notification when sending e-mail via SMTP. The following screen shot displays notifications of K-9 Mail and CalDAV Sync Adapter.

MITM Notifications of K-9 Mail and CalDAV Sync Adapter

The screenshot shows that in case of certificate failures for HTTPS connections, I’m using error code 418. That number was specified in RFC 2324 (updated a couple of days ago in RFC 7168). If you see error code 418, you know that you are in deep trouble without coffee.

In any case, the user needs to decide whether the server was equipped with a new certificate, which needs to be pinned, or whether a MITM attack takes place.

What’s Next

SSL/TLS is a mess, and the above is far more complicated than I’d like it to be. I hope to see more apps pinning certificates themselves. Clearly, users of such apps need some guidance how to identify the correct certificates that should be pinned.

If you develop apps, please implement certificate pinning. As I wrote previously, I believe these papers to be good starting points:

You may also want to think about the consequences if “trust” in some CA is discontinued as just happened for CAcert for Debian and its derivatives. Recall that CAcert was a “trusted” CA in Debian, which implied that lots of software “trusted” any certificate issued by that CA without needing to ask users any complicated question at all. Now, as that “trust” has been revoked (see this bug report for details), users will see warnings concerning those same, unchanged (!), previously “trusted” certificates; depending on the client software, they may even experience an unrecoverable error, rendering them unable to access the server at all. Clearly, this is far from desirable.

However, if your app supports certificate pinning, then such a revocation of “trust” does not matter at all. The app will simply continue to be usable. It is high time to distinguish trust from “trust.”