Signing NSD DNSSEC Zones with Nitrokey HSM


This article presents a way of utilising the Nitrokey HSM hardware security token for signing DNSSEC zone files for use with the NSD name server daemon.

Introduction

The NSD is a part of the OpenBSD's base system since 2010; it is widely used with other operating systems as well. Reportedly, deployments of the NSD include a number of DNS root servers. NLnet Labs, a non-profit foundation that developed the NSD, also distributes a software library, ldns that is aimed at simplification of DNS programming. ldns includes a number of example programs that many vendors of open operating systems distribute as a separate package, usually named ldnsutils, that is advertised as a set of client programs which is intended for administration of DNSSEC. In particular, ldnsutils contains the utility ldns-signzone(1), a tool for signing DNSSEC zone files. ldns-signzone is mentioned in several tutorials and even in a book dedicated to deployment of DNSSEC—to me it looked as a perfect companion for managing zone files for NSD.

Glancing at the ldns-signzone(1)'s manpage, I immediately took note of an availability of support for OpenSSL engines. Having the great pleasure of using the Nitrokey HSM for more than a year, I finally perceived the option of anchoring DNS security with something tangible and affordable. Unfortunately, application of HSMs is still deemed somewhat more of a corner case among the general public; supporting them in ldns-signzone(1), apparently, was not a task of priority for the developers of ldns. I discovered that—while all the ground work is performed already—the tool still needs more of it to be of use with hardware security tokens.

To put a long story short, it can be done with a certain effort. Please find below my account of achieving that goal, where I explain how to sign DNSSEC zone files on Debian GNU/Linux 9.4 (stretch) or on OpenBSD 6.2 using ldns-signzone(1) and the Nitrokey HSM. It is worth the while: I have two DNSSEC domains (of different topology) signed that way—both validate successfully using the DNSSEC Analyzer from Verisign Labs or the DNSViz.

Dramatis Personnæ

…or what a more technology minded reader would call a ‘software stack’. In the order of appearance, bottom up from the operating system's kernel:

ccid
The generic USB CCID (Chip/Smart Card Interface Devices) driver and ICCD (Integrated Circuit(s) Card Devices).
pcslite
Open implementation of the PC/SC specification.
OpenSC
The standard APIs to smart cards, e.g. PKCS#11 API, Windows' Smart Card Minidriver and macOS Tokend.
libp11
PKCS#11 engine plugin for the OpenSSL library that allows accessing PKCS#11 modules in a semi-transparent way.
OpenSSL
A general-purpose cryptography library.
ldns
A library for simplification of DNS programming.
ldns-signzone(1)
The utility for signing DNSSEC zone files.
As George A. Miller famously enquired—“…what about the magical number seven?”—this is a fairly complex software system in itself. But worse yet, there is the second dimension to this small world: underlying operating systems, each with its own frisky quirks. Fear not, however, my dear reader: put your trust in the source code and—with some patience—we shall prevail.

Conventions

First, we must agree on several items that do not depend on the operating system at hand. Please do not skip this section under any circumstances, otherwise I cannot guarantee the results.

Directory Structure

I wrote the instructions that you will find below so that they prevent any damage to, or even little interference with, the standard software on your machine. However, that is not possible without making certain assumptions about the way of keeping files.

I assume that under your home directory, "${HOME}" you have the directory pkg where you store files that you download from other machines, you have the directory src where you work with source code, and you have the directory tmp where you store temporary files. I further assume that you are going to install the software that is necessary for accomplishing our task into the directory opt, where every software package is stored in a separate subdirecory.

The following creates a shell script called ldns-build-vars.sh that defines the described structure:

cat <<EOF > ldns-build-vars.sh
WD="\${HOME}"
PKG="\${WD}/pkg"
SRC="\${WD}/src"
TMP="\${WD}/tmp"
OPT="\${HOME}/opt"
EOF
You can always import this script (and, hence, have those variables defined) by invocation of the shell's dot command (look carefully, there is a period in front of ldns-build-vars.sh.
. ldns-build-vars.sh

Please feel free to change that script the way you prefer but remember that below I assume that the variables WD, PKG, SRC, TMP, and OPT are set.

Those variables can be cleared simply be leaving (exiting) the running shell.

Checking Out Specific Git Commits

Below I shall instruct you to fetch the source code from several public repositories. Since most of those, if not all of them, are development repositories, they change often. For that reason, immediately following the fetching itself, I ask you to check out a specific commit, i.e. to bring your local copy of that repository to a known point in the lifetime of the project.

Verification of Signatures

Below, wherever you see a hyperlink, for instance, x, immediately followed by a hyperlink sig in parentheses, like this: (sig), it means that I expect you to perform the following:

  1. Save both linked files x and sig into the directory "${PKG}".
  2. Verify the detached PGP signature sig made on the file x as follows:
    gpg2 --keyserver pgp.mit.edu --auto-key-retrieve --verify "${PKG}/x.asc"
    You will have to substitute x in the command above with the name of the actual file that you downloaded. Carefully examine the output of gpg2(1) to ensure that the signing key is correct and the signature is valid.

I distribute only signed patch files. The fingerprint of my public key is D853 E6A9 F0D1 E01C 4833 DF59 75F7 BBFB 009E 3949. At any time, I publish only one PGP key that is not revoked. All signatures are made using the Nitrokey Pro hardware token. Please do not proceed and immediately notify me if there is anything wrong with verification of signatures.

Installation on Debian GNU/Linux 9.4

This version of Debian is almost ready for our task: ccid and pcslite can be used as distributed. Unfortunately, OpenSC and libp11 are quite old: they fail to detect Nitrokey HSM's key identifiers properly (the slot numbers are off by unpredictable amount) which makes them inconvenient, if at all usable.

Therefore, besides patching and installing ldns itself, first you must compile and install newer versions of OpenSC and libp11.

The Initial State

I assume that you performed the minimal installation of the operating system, and then installed and enabled sudo(1).

Ensure that the directory structure exists:

. ldns-build-vars.sh
mkdir -p "${WD}" "${PKG}" "${SRC}" "${TMP}" "${OPT}"

Install Required Debian Packages

sudo apt-get install git pcscd libpcsclite-dev libssl-dev zlib1g-dev make autoconf automake libtool m4 pkg-config wget

Pull the Source Code from Public Git Repositories

cd "${SRC}"
Assuming a certain quality of the Internet connection, the following three shell commands can be performed in parallel:
git clone https://github.com/OpenSC/OpenSC.git \
    && cd OpenSC \
    && git checkout 5d5caecfab968e5da2c137429b7c50b8ee5a5563 \
    && cd -
git clone https://github.com/OpenSC/libp11.git \
    && cd libp11 \
    && git checkout b81fede710b08555601950db23161bec81baacaa \
    && cd -
git clone https://git.nlnetlabs.nl/ldns \
    && cd ldns \
    && git checkout ac80c440fad29b2b4dc40c5ac24f5215e2a1ef73 \
    && cd -

Configure and Install OpenSC

cd "${SRC}/OpenSC"
./bootstrap
./configure --prefix="${OPT}/opensc"
make
make install

Configure and Install libp11

cd "${SRC}/libp11"
./bootstrap
./configure --prefix="${OPT}/libp11" \
    --with-enginesdir="${OPT}/libp11/libexec" \
    --with-pkcs11-module=opensc-pkcs11.so
make
make install

Configure and Install ldns

There are two patches that you must apply:

  1. 0001-ldns-signzone-Complete-engine-support.patch (sig)
    for examples/lds-signzone.c, the source code of the utility ldns-signzone(1).
  2. 0002-ldns-signzone-Amend-manpage-for-engine-options.patch (sig)
    for examples/lds-signzone.1, the manual page of the utility ldns-signzone(1).
cd "${SRC}/ldns"
git apply "${PKG}/0001-ldns-signzone-Complete-engine-support.patch"
git apply "${PKG}/0002-ldns-signzone-Amend-manpage-for-engine-options.patch"
Now proceed with the configuration and installation:
libtoolize -ci
autoreconf -fi
./configure --prefix="${OPT}/ldns" --with-examples --with-drill
make
make install

Signing DNSSEC Zone Files on Debian GNU/Linux 9.4

Create the engine configuration file for OpenSSL:

cd "${TMP}"
cat <<EOF > openssl.cnf
openssl_conf = openssl_init

[openssl_init]
engines=engine_section

[engine_section]
pkcs11 = pkcs11_section

[pkcs11_section]
engine_id = pkcs11
dynamic_path = ${OPT}/libp11/libexec/libpkcs11.so
MODULE_PATH = ${OPT}/opensc/lib/opensc-pkcs11.so
init = 0
EOF
Fetch the sample zone configuration file:
wget -c http://penzin.net/ldns-signzone/nxdomain.net.zone
Prepend the location of utilities from OpenSC and ldns to the search path of the shell:
export PATH="${OPT}/opensc/bin:${OPT}/ldns/bin:${PATH}"
Launch the pcscd(8) daemon:
sudo service pcscd start
Insert the Nitrokey HSM token into an available USB slot and list its contents to find whether you have suitable signing keys for testing:
pkcs15-tool -D
(Hint: if you are trying this using a Virtual Box' VM on Debian GNU/Linux as a host, make sure that, on the host machine, your user account is a member of the vboxusers group, otherwise the token will not be attached to the VM.)

If you happen not to have suitable keys on the token or you prefer not to use them for testing, you can create a Key Signing Key (KSK) on the token as follows (please have the token's User PIN ready):

pkcs11-tool -l -k --key-type rsa:2048 --usage-sign -a 'Testing KSK' --private
and the Zone Signing Key (ZSK) as follows:
pkcs11-tool -l -k --key-type rsa:1024 --usage-sign -a 'Testing ZSK' --private

In both cases, the output shall be similar to the following (this is the case for a 2048-bit key):

Using slot 0 with a present token (0x0)
Logging in to "SmartCard-HSM (UserPIN)".
Please enter User PIN: 
Key pair generated:
Private Key Object; RSA 
  label:      Testing KSK
  ID:         469d905b86fa48982fe08c53366c98a701763af7
  Usage:      sign, unwrap
Public Key Object; RSA 2048 bits
  label:      Testing KSK
  ID:         469d905b86fa48982fe08c53366c98a701763af7
  Usage:      verify, wrap

Look for lines that start with ‘ID:’—these are keypair identifiers. Please note those, you will use them with ldns-signzone(1). Should you like to delete a keypair later, it can be done by deleting its private key using the identifier of the keypair, for instance, the following command will remove the key pair 469d905b86fa48982fe08c53366c98a701763af7 from the token:

pkcs11-tool -l -b --type privkey -d 469d905b86fa48982fe08c53366c98a701763af7

Assuming that, for instance, you have a KSK with the identifier 469d905b86fa48982fe08c53366c98a701763af7 and a ZSK with the identifier 4d3e6d2ab539fb4c2dcf859d21dc192b25400ab1, you are ready to sign a zone file (please have the token's User PIN ready):

OPENSSL_CONF=openssl.cnf \
ldns-signzone -E pkcs11 \
    -K RSASHA256,469d905b86fa48982fe08c53366c98a701763af7 \
    -k RSASHA256,4d3e6d2ab539fb4c2dcf859d21dc192b25400ab1 \
    nxdomain.net.zone
The output shall be similar to
Engine key id: 469d905b86fa48982fe08c53366c98a701763af7, algo 8
Enter PKCS#11 token PIN for UserPIN (SmartCard-HSM):
Engine key id: 4d3e6d2ab539fb4c2dcf859d21dc192b25400ab1, algo 8
If all went well, you shall find a signed zone in the file nxdomain.net.zone.signed. Congratulations!

Installation on OpenBSD 6.2

The situation on OpenBSD is quite peculiar. Since ldns is hardwired for OpenSSL engines that are completely disabled in LibreSSL (for good), this is the first time in four years (since LibreSSL was forked from OpenSSL) when I faced a reason to compile OpenSSL for OpenBSD.

Due to this fact (and nothing else) you will need to rebuild the software stack starting from OpenSC.

The Initial State

I assume that you performed the installation of the operating system and then configured doas(1).

Ensure that the directory structure exists:

. ldns-build-vars.sh
mkdir -p "${WD}" "${PKG}" "${SRC}" "${TMP}" "${OPT}"

Install Required OpenBSD Packages

doas pkg_add git gmake autoconf-2.69p2 automake-1.15.1 libtool m4 ccid pcsc-lite gnupg-2.1.23
cat <<EOF >> ldns-build-vars.sh
export AUTOCONF_VERSION=2.69
export AUTOMAKE_VERSION=1.15
EOF
. ldns-build-vars.sh

Pull the Source Code from Public Git Repositories

cd "${SRC}"
Assuming a certain quality of the Internet connection, the following four shell commands can be performed in parallel:
git clone https://github.com/openssl/openssl.git -b OpenSSL_1_1_0-stable \
    && cd openssl \
    && git checkout b2758a2292aceda93e9f44c219b94fe21bb9a650 \
    && cd -
git clone https://github.com/OpenSC/OpenSC.git \
    && cd OpenSC \
    && git checkout e1d6f0869bdac873878d72044bfc44d6ca5373b4 \
    && cd -
git clone https://github.com/OpenSC/libp11.git \
    && cd libp11 \
    && git checkout b81fede710b08555601950db23161bec81baacaa \
    && cd -
git clone https://git.nlnetlabs.nl/ldns \
    && cd ldns \
    && git checkout ac80c440fad29b2b4dc40c5ac24f5215e2a1ef73 \
    && cd -

Configure and Install OpenSSL

cd "${SRC}/openssl"
./Configure shared --prefix="${OPT}/openssl" BSD-x86_64
make
make install_sw

Configure and Install OpenSC

sh ./bootstrap
PKG_CONFIG_PATH="${OPT}/openssl/lib/pkgconfig" \
./configure --prefix="${OPT}/opensc" --disable-notify
make
make install

Configure and Install libp11

cd "${SRC}/libp11"
sh ./bootstrap
PKG_CONFIG_PATH="${OPT}/openssl/lib/pkgconfig" \
./configure --prefix="${OPT}/libp11" \
    --with-enginesdir="${OPT}/libp11/libexec" \
    --with-pkcs11-module=opensc-pkcs11.so
make
make install

Configure and Install ldns

There are two patches that you must apply:

  1. 0001-ldns-signzone-Complete-engine-support.patch (sig)
    for examples/lds-signzone.c, the source code of the utility ldns-signzone(1).
  2. 0002-ldns-signzone-Amend-manpage-for-engine-options.patch (sig)
    for examples/lds-signzone.1, the manual page of the utility ldns-signzone(1).
cd "${SRC}/ldns"
git apply "${PKG}/0001-ldns-signzone-Complete-engine-support.patch"
git apply "${PKG}/0002-ldns-signzone-Amend-manpage-for-engine-options.patch"
Now proceed with the configuration and installation:
libtoolize -ci
autoreconf -fi
./configure --prefix="${OPT}/ldns" --with-examples --with-drill --with-ssl="${OPT}/openssl"
make
make install

Signing DNSSEC Zone Files on OpenBSD 6.2

The procedure is not different from the described in Signing DNSSEC Zone Files on Debian GNU/Linux 9.4 above, except the following:

  1. You must have the following definition
    LD_LIBRARY_PATH="${OPT}/openssl/lib:${OPT}/opensc/lib"
    in your shell while you use any utility from OpenSC or ldns.

    Please be careful though: while binaries of OpenSSL that you compiled are not in the search path of the shell (there is no chance that they will be executed instead of their counterparts from LibreSSL) and while OpenSSL and LibreSSL have different naming scheme for their libraries, still the situation is not completely healthy and safe.

    I advise you to set both PATH and LD_LIBRARY_PATH only temporarily, while you perform signing operations. In particular, refrain from doing that in any script that is loaded automatically during system boot or during user login.

  2. Use ftp(1) for downloading the sample zone file, not wget.
  3. Launch the pcscd(8) daemon as follows:
    doas rcctl start pcscd
  4. When signing zone files, you may encounter a number of truly frightening and discouraging messages (when these messages appear at all, there are usually two of them; they are produced when OpenSC allocates memory for PIN codes) similar to the following:
    0x19bafd03e118 22:18:00.314 cannot lock memory, sensitive data may be paged to disk
    These messages are harmless in the sense that they do not affect the operation of ldns-signzone(1). They are caused by a failure of OpenSC to lock a region of memory; more precisely by a failure of mlock(2) to prevent paging (that is, writing to a swap file or disk partition) a region of memory. This condition may come into effect under severe memory loads and indeed may pose a security risk unless the swap file is encrypted, which, by default, is the case on OpenBSD since the version 3.8—for more than 13 years. I suspect that this issue is caused by a linuxism, since on Linux the region of memory which is being locked does not have to be aligned to a page boundary (see the Linux version of mlock(2), the description of the EINVAL error in particular) and the source code of the function sc_mem_alloc_secure (see src/libopensc/sc.c under the OpenSC source tree) indeed is devoid of any effort to allocate aligned memory (for instance, using posix_memalign(3) which is a standard, portable interface). OpenSC does not report the exact cause of the run-time failure which leaves room to speculations like this.

Vadim Penzin, March 14th, 2018


I hereby place this article and the accompanying source code into the public domain.
You are welcome to contact me by writing to howto at this domain.
I publish this information in the hope that it will be useful, but without ANY WARRANTY.
You are responsible for any and all consequences that may arise as the result of using this information.