A DNSSEC Primer

DNSSEC is a hugely complex protocol. The current specification is defined in three RFCs: RFC 4033, 4034 and 4035. This post will attempt to explain the core of the protocol and what is required to sign a DNS zone with DNSSEC. The process of validating DNSSEC records shall be left for a future post.

While there are arguments both for and against DNSSEC, this post will not take a side. It aims to be a strictly technical explanation on how DNSSEC works.

Introduction

The original DNS protocol has no security guarantees. DNS data can be intercepted, modified and spoofed without any means of detection. The goal of DNSSEC is to fix this by providing origin authentication and integrity for DNS data.

The DNS system is organized as a hierarchy of subdomains below the DNS root domain (.). A subdomain and all the subdomains below it that is managed by one administrative authority is referred to as a DNS zone. As an extension to DNS, DNSSEC heavily relies on this design for its security model.

DNSSEC Record Types

DNSSEC introduces four new Resource Record (RR) types to the DNS protocol: DNS Public Key (DNSKEY), Resource Record Signature (RRSIG), Next Secure (NSEC), and Delegation Signer (DS).

The use of each RR type will be explained as we walk through the protocol.

How to sign a zone?

DNSSEC uses a public/private keypair to sign DNS records. The public key portion of this keypair is stored in a DNSKEY record. This record is used by resolvers to validate the signatures covering the DNS records of the zone. The keypair used to sign DNS zone records is referred to as the Zone Signing Key (ZSK). The ZSK is used to sign all records except DNSKEY records. Another public/private keypair, known as the Key Signing Key (KSK) is used to sign DNSKEY records. This split of responsibilities between the ZSK and KSK allows for the ZSK to be rotated frequently without changing the KSK. The benefit of this will become apparent in a later section of this post.

DNS records of the same type are grouped into a RRset which is then signed with the ZSK. This signature is stored in a RRSIG record which is used by resolvers to validate the authenticity and integrity of the records contained in the RRset. In a properly signed DNS zone, there should be a RRSIG record covering every RR type present in the DNS zone. RRSIG records have an expiration date (which is distinct from the TTL of the RRSIG record) and RRsets must be regularly re-signed.

The next piece of the DNSSEC puzzle is NSEC records. NSEC records list the RRsets associated with the DNS name as well as point to the next authoritative name and are used to authenticate the denial of existence of a DNS record. Take the case of a DNS zone beginning at example.com with two subdomains, alpha.example.com and omega.example.com. The DNS names are sorted in canonical order (defined in RFC 4034 Section 6) and we end up with the sorted list [example.com, alpha.example.com, omega.example.com].

example.com contains the following NSEC record:

example.com. 86400 IN NSEC alpha.example.com. (
	A MX RRSIG NSEC )

This indicates that example.com has a A, MX, RRSIG and NSEC RRset associated with it and that the next authoritative name in the zone is alpha.example.com.

alpha.example.com contains the following NSEC record:

alpha.example.com. 86400 IN NSEC omega.example.com. (
	A MX RRSIG NSEC )

This indicates that the next authoritative name in the zone is omega.example.com. We can use this to prove that beta.example.com does not exist since the names are sorted and there are no zones between alpha.example.com and omega.example.com.

Unfortunately, NSEC records makes it trivially easy to enumerate the names in a zone. This is a goldmine of information in a targeted attack since it can reveal sensitive information about an organization such as technologies in use. This is the reason why AXFR queries (also known as DNS zone transfers) is disabled by most DNS servers. NSEC records in DNSSEC have been replaced by NSEC3 records, which is designed to make enumeration a lot more difficult (although it does not completely fix the problem). A look at how NSEC3 records work shall be the topic of a future post.

With all these pieces in place, the integrity of a DNS zone can be verified by resolvers if the resolvers have an out-of-band method to verify the KSK. The DNSSEC RFCs use the term “island of security” to describe such a zone. However, this doesn’t scale as a DNS resolver cannot possibly verify the KSK of every domain out of band. DNSSEC solves this by establishing an authentication chain starting from the root domain (.).

Take the example of a DNS zone beginning at example.com. example.com contains two DNSKEY records, the ZSK and KSK. example.com can establish an authentication chain to the parent domain (com) by publishing a DS record at com containing the hash of the example.com’s KSK. Since the DS record is signed by com, any resolver that can validate com can validate any child zones of com. This process is repeated between com and the root domain (.). With this, any resolver that knows the KSK of the root domain can validate any DNSSEC enabled domain.

Since the DS record in the parent zone contains the hash of the KSK, rotating the KSK requires communication with the parent zone, which belongs to a different administrative authority. This makes the process of rotating the KSK slightly more difficult. The split of responsibilities between the ZSK and KSK allows for a much more frequent rotation of the ZSK (which is the key used to actually sign zone records) since updating the ZSK only requires publishing a new DNSKEY record. The KSK, which is only used to sign the DNSKEY RRset, can be kept in a more secure (and more inaccessible) location and can be rotated less frequently.

Conclusion

For a zone to be considered properly signed, it should contain:

  1. Two DNSKEY records, containing the KSK and ZSK
  2. RRSIG records for each RR type in the zone
  3. NSEC (or NSEC3) records for each authoritative name in the zone
  4. A DS record in the parent zone containing the hash of the KSK

The other side of the DNSSEC protocol is validating the signed zones. That shall be covered in a future post.

Shoutout to @diagprov for reviewing this post!

An interesting crypto vulnerability

I came across an interesting tweet by Juliano Rizzo.

Tweet image

The correct answer is that the statement is true if several (very unlikely to happen in the real world) conditions are met. Let us take a look at why it happens and what conditions have to be met for this to work.

1. HMAC

I quote from RFC 2104 Section 2.

The authentication key K can be of any length up to B, the block length of the hash function. Applications that use keys longer than B bytes will first hash the key using H and then use the resultant L byte string as the actual key to HMAC.

This means that for keys that are longer than the block size of the hash used for the HMAC (>64 bytes in the case of SHA1), HMAC(key) == HMAC(HASH(key)).

>>> import hashlib
>>> import hmac
>>> key1 = b"This is a very long key, a very very long key indeed. This key is absurdly long."
>>> key2 = hashlib.sha1(key1).digest()
>>> hmac.new(key1, b"msg", "sha1").digest)
b'\xac\x87j&\xc6}\xa3\xc4\xf2$z\x06\x19\x87\\e\x81N\xcei'
>>> hmac.new(key2, b"msg", "sha1").digest)
b'\xac\x87j&\xc6}\xa3\xc4\xf2$z\x06\x19\x87\\e\x81N\xcei'

2. PBKDF2 (and Scrypt)

PBKDF2 essentially boils down to applying HMAC a number of times to a key in a loop. Thus, the property of HMAC mentioned in the previous section applies to PBKDF2(as well as Scrypt, which uses PBKDF2 internally).

>>> import hashlib
>>> key1 = b"This is a very long key, a very very long key indeed. This key is absurdly long."
>>> key2 = hashlib.sha1(key1).digest()
>>> hashlib.pbkdf2_hmac("sha1", key1, b"salt", 1)
b'\xf1\x18\xa4J]y\xf6\x85J\x8eq\xef\xea\x16>\x826+\x7f\xc8'
>>> hashlib.pbkdf2_hmac("sha1", key2, b"salt", 1)
b'\xf1\x18\xa4J]y\xf6\x85J\x8eq\xef\xea\x16>\x826+\x7f\xc8'

So, how does this result in a vulnerability?

If Dropbox had decided to switch from SHA1 to PBKDF2_HMAC_SHA1 instead of Bcrypt, any attacker that manage to obtain dumps of the SHA1 hashed password and the PBKDF2_HMAC_SHA1 hashed passwords can authenticate as any users that a. have passwords longer than 64 bytes and b. reused the same password in the switch without cracking the password hash.

So why isn’t this likely to be a problem? There are a number of unlikely conditions that have to be fulfilled first.

  1. The attacker has to have access to both the SHA1 and the PBKDF2_HMAC_SHA1 password dumps. This would require Dropbox to keep the old SHA1 hashed passwords around together with the new PBKDF2_HMAC_SHA1 hashed passwords or that the attacker managed to dump the database both before and after the algorithm switch.

  2. Users have to use passwords longer than 64 bytes. This is very uncommon even for users who use password managers or passphrases.

  3. Users have to reuse the same password before and after the algorithm switch. This is especially unlikely to happen because users who are security-consious enough to use passwords longer than 64 bytes most likely will not reuse passwords.

So there you have it, a very unlikely set of circumstances that if fulfilled can potentially result in a very interesting vulnerability.

A faster PBKDF2 for Python

I came across a blog post titled “PBKDF2: performance matters” where the author discusses how most implementations of PBKDF2 are slower than it otherwise could be.

After reading the blog post, I decided to write some Python bindings to see how much of a performance increase I can obtain over the standard library’s hashlib.pbkdf2_hmac implementation. My goal is a library with an interface that is compatible with hashlib.pbkdf2_hmac.

The results are surprisingly good. With a basic benchmarking script on CPython 3.4.1, my implementation is about 3 times as fast as the standard library.

$ ./bench.sh
Benchmark hashlib...
100 loops, best of 3: 60.2 msec per loop
Benchmark fastpbkdf2...
100 loops, best of 3: 20.3 msec per loop

With PyPy 2.6.0, the results are even better.

$ ./bench.sh
Benchmark hashlib...
100 loops, best of 3: 242 msec per loop
Benchmark fastpbkdf2...
100 loops, best of 3: 19.2 msec per loop

I have since release my library as a PyPI package and the code is available on GitHub.

Simply install the package with pip,

pip install fastpbkdf2

and import the function

from fastpbkdf2 import pbkdf2_hmac

The interface is exactly the same as hashlib.pbkdf2_hmac and should be a drop-in replacement.

Introducing python-aead

Cryptography libraries often have complicated APIs with many different options to tweak. It is a goal PyCA’s cryptography library to provide safe and easy to use APIs for common cryptographic tasks. To that end, the cryptography package has a Fernet recipe for symmetric encryption derived from the original Ruby implementation and specification. However, the Fernet recipe lacks the ability to authentiate (without encrypting) arbitrary data.

To make up for that use case not being covered by Fernet, I have written and released on PyPI a library called aead. It can be installed with pip.

$ pip install aead

The aead library is based on a IETF Internet Draft from David McGrew. It is essentially AES_128_CBC and HMAC_SHA_256 composed with an encrypt-then-mac construction. It relies on the cryptography library for the cryptographic primitives.

It has a simple to use API heavily inspired by the Fernet recipe in the cryptography library.

The module contains a single class that can be imported.

from aead import AEAD

The class takes requires an encryption key to be initialized. The key has to be 32 bytes long and encoded with base64url as specified in RFC 4648. The library provides a classmethod to generate a suitable random key.

cryptor = AEAD(AEAD.generate_key())

After initializing the object, encryption can be done by calling the .encrypt() method. The .encrypt() method takes two paremeters, the first being the data you want to encrypt and the second being associated data that you want to authenticate but not encrypt. The second parameter is optional and can be left out if there isn’t any data to authenticate.

ct = cryptor.encrypt(b"Hello, World!", b"Additional Data")

.encrypt() returns base64url encoded cipher text.

Decrypting any data encrypted with aead is similar. Simply call .decrypt() in place of .encrypt(). The .decrypt() method takes two parameters, the first being the cipher text that needs decrypting and the second being the associated data that was authenticated.

If the cipher text is corrupted or the associated data provided during the decryption process does not match the associated data provided during encryption, a ValueError is raised, otherwise the decrypted plain text is returned.

The repository for aead can be found on GitHub and the README.md file in the repository should be treated as the source of truth if any information there differs from this blog post due to changes over time.

Look before you pip

For Python programmers, downloading Python packages from PyPI, the Python Package Index, is second nature. Tools like pip and conventions like the requirements.txt file that most Python projects follow provides a consistent way of specifying project dependencies.

However, installing random packages from PyPI is actually very dangerous, a fact that not many people are aware of. There are a few factors that contribute to this.

  1. Python packages can execute arbitrary Python code during the installation process.

  2. PyPI packages are not moderated. Unlike the package managers used in Linux distros, anyone can register an account and upload Python packages without going through a review process. While this is one factor contributing to PyPI’s success as a package repository, you will have to trust the maintainer of the package that the package is safe.

As a proof of concept, I have written a setup.py file that connects to a Metasploit listener and downloads a Meterpreter shell during installation. This demonstrates that it is trivial for someone to execute arbitrary code on a machine through the installation of a Python package. You can obtain the code from GitHub.

Run the Metasploit listener.

msf > use exploit/multi/handler
msf exploit(handler) > set payload python/meterpreter/reverse_tcp
msf exploit(handler) > set LHOST 127.0.0.1

Finally, run the setup.py file.

python setup.py install

You should obtain a Meterpreter shell with the same privileges that you ran the setup.py script with.

While my example involves connecting to a Metasploit listener on localhost, the same attack can be extended to install malware from remote systems or do almost anything a Python script can do.

The problematic thing about this attack is that there are valid reasons for Python packages to execute code during installation. This ranges from things like OS version checks to compiling C code for packages that rely on C extensions. Restricting setuptools to a subset of Python during installation isn’t exactly foolproof as demonstrated by the numerous Python sandbox escape techniques. Moderating PyPI isn’t a solution either as that will greatly diminish PyPI’s attractiveness as a package repository.

Here are two recommendations to limit the potential of such attacks.

  1. NEVER install Python packages as root. This limits the privileges an attacker has if the attack succeeds. virtualenv is incredibly useful for this.

  2. If you are in an organization with larger resources, audit the third-party packages you depend on. Mirror trusted packages on an internal devpi server instead of installing packages directly from PyPI.

While I am limiting the details in this post to Python packages as that is the ecosystem I am most familiar with, I believe that this issue also extends to other languages and ecosystems such as Ruby and the gems ecosystem. While there has been an increased focus over the years on paying attention to good security practices when writing code, many forget about third-party code. This worries me because third-party code represents such a large attack surface open to exploits. As we all know, security is only as strong as the weakest link.