The level of complexity caused by KSK/ZSK split in DNSSEC isn’t necessary for most purposes; you can in fact just have one key for a zone and sign everything with it such as co.uk. does. I make frequent use of these Combined (or Single) Signing Keys as shown here.

However, the KSK/ZSK split has one advantage: it allows us to keep the KSK offline and only bring it out occasionally to sign the DNSKEY RRset. This key can be kept stable (i.e. it doesn’t need to be changed frequently or even at all) and thus the interaction with the parent (DS submission) be kept at a minimum.

Jaromir Talir held a presentation about offline KSK with Knot DNS, and from the terminology he used in 2019 I made this diagram a few years ago to help me better understand the flow of keys.

key flow in offline KSK signing

The idea is that the KSK (depicted on the left) is managed completely offline, e.g. on a laptop which is otherwise kept in a safe place. Only public key material is carried around – on floppy disks or USB memory keys, say, and as such there is an extremely low risk of the KSK being compromised.

The ZSK and the zone it signs are maintained on the right, online and/or on a hidden primary. Since the ZSK can easily be replaced there is little risk of compromise, and the zone data is public anyway.

Once created, the ZSK (or ZSKs – plural) are copied to the KSK machine for signing. The KSK signer uses the KSK to sign the DNSKEY RRset and the resulting RRset and its signatures (RRSIG) are transported back to the zone signer on the right. This data is now public: the DNSKEY RRset contains the public keys for the zone.

The zone is now signed (on the right) with the ZSK only, so this signs all of the RRsets in the zone, and as a last step we enrich the newly-signed zone with the public DNSKEY of the KSK and its signatures, and this zone (named example.com.final in the diagram) we then load and serve.

The diagram shows “key requests” being passed left and right. These could be signed (PGP?) containers carrying public key material, but for our purposes simple files containing the public DNSKEY records suffice.

In the example which follows, the offline KSK will be managed with tools from the ldns toolchain, and the zone signer with tools from BIND and optionally from ldns. Why? Just to introduce diversity.

creating the keys

We begin by preparing our environment. Everything in the directory left contains files which we will have on the offline KSK machine; conversely, the directory right contains files on our actual zone signer. Obviously these directories will be on distinct machines.

% mkdir left right
% touch left/KSK.data                       # this file will contain public data

% cd right/
right% dnssec-keygen -a13 example.com       # generate ZSK
Generating key pair.
Kexample.com.+013+54147

right% ln -s Kexample.com.+013+54147.key ZSK.key

We copy the public ZSK.key to the left. In the following script, I have to replace $INCLUDE.*$ by the actual public ZSK DNSKEY RR because ldns-read-zone and friends don’t understand $INCLUDE.

% cd left/
left% ldns-keygen -a13 -k example.com       # generate KSK
Kexample.com.+013+11238

left% cat example.com
$TTL 3600
@ SOA  unused.invalid. jp.invalid.  1 3H 1H 1W 1H
  NS   unused.invalid.

; $INCLUDE "../zone/ZSK.key"

; This is a ZONE-SIGNING key, keyid 54147, for example.com.
example.com. IN DNSKEY 256 3 13 ah+p9L1ydOMAWsHTW9sLokRDrA1by6TV6ZPLKPDhkowEMAN7o36Vs1Bt db+obk6h22D548XWTWV6cWEkbNYHPQ==

left% cat Kexample.com.+013+11238.ds
example.com.	IN	DS	11238 13 2 2d0d834fe6aea4c80ec3bcbf5af753653c9d9ea6dc997ea86e90c2615edef19f

Note the DS record which we will later submit to the parent. This is public data, so we can safely copy it to the right for further submission.

On the left, the content of the zone doesn’t matter, and I’ve used invalid to emphasize the fact. We will now sign that zone with the KSK, and I set the signature validity to a date I’ll recognize in the final zone. As there’s no portable method of getting date(1) to add an offset to epoch, I’ve created eitime for this. In future: -e $(eitime -z 60) should do the trick.

The only records from this signed zone we are interested in are the DNSKEY and RRSIG over DNSKEY.

left% ldns-signzone -e 20221224 -o example.com example.com Kexample.com.+013+11238

# now grab the KSK DNSKEY record and its RRSIG
left% ldns-read-zone example.com.signed |
	awk '($4 == "DNSKEY" && $5 == 257) || ($4 == "RRSIG" && $5 == "DNSKEY") { print }' | tee KSK.data
example.com.	3600	IN	DNSKEY	257 3 13 cMW3t5/jWcESqrzNgNjksnfUZdS4TtqO1gOae0cBRxgopoT/FNIa5GATNQAg64TJw3tgECVOq1beXpbJGSjG7Q== ;{id = 11238 (ksk), size = 256b}
example.com.	3600	IN	RRSIG	DNSKEY 13 2 3600 20221224000000 20220922073938 11238 example.com. 35J+toX0FyIyXRTNpamM6UVyivLFV1deUSWrJl+3DCVH2feolbDP45i/myLDnnywAS4NzQIZqjdkZQMTyVSxfg==

left% wc -l KSK.data
       2 KSK.data

The file KSK.data contains the public key and signature which we transfer (floppy disk, remember?) back to the right.

signing the zone

Back on the right we have the zone, the ZSK, and the public KSK in the file KSK.data, so we can now sign this:

right% ./bsig.sh
*** sign the zone; set SOA serial to epoch
example.com.signed
*** remove dsset file as it contains ZSK only
*** replace RRSIG over DNSKEY created by ZSK with that created by KSK
*** verify signed zone using ldns-verify-zone
Zone is verified and complete
*** verify signed zone using dnssec-verify
Loading zone 'example.com' from file 'example.com.signed'
Verifying the zone using the following algorithms: ECDSAP256SHA256.
Zone fully signed:
Algorithm: ECDSAP256SHA256: KSKs: 1 active, 0 stand-by, 0 revoked
                            ZSKs: 1 active, 0 stand-by, 0 revoked
*** verify signed zone using validns
records found:       17
skipped dups:        0
record sets found:   10
unique names found:  3
delegations found:   0
    nsec3 records:   0
not authoritative names, not counting delegation points:
                     0
validation errors:   0
signatures verified: 8
time taken:          0.003s

The b in bsig.sh is for signing with the BIND utilities, and there’s an lsig.sh which uses ldns for signing.

Both scripts work in a similar way: they first sign the zone using only our ZSK (BIND’s smart signing finds the key by itself, with ldns I have to specify it). Then they remove the RRSIG over DNSKEY which was created by the ZSK (we don’t need that; our DNSKEY RRset is signed by the KSK), and add the public DNSKEY of the KSK and its RRSIG, both obtained from left. Finally, we use three distinct tools to verify the validity of the zone.

The signed zone file is the one we (re-)load on our (hidden) primary server.

refreshing

Signing the zone and therewith refreshing signatures on the (hidden) primary shouldn’t be too much of a problem and can easily be scheduled; cron(8) comes to mind.

A bit more involved is refreshing the signatures over the DNSKEY RRset with the KSK on left. Depending on the chosen method of public DNSKEY and RRSIG transport (the file KSK.data) from left to right this can actually mean a manual task (“floppy disk”) which must be done before signatures expire.

finally

As the diagram above suggests, there’s no reason why you couldn’t add complexity security by adding a bunch of HSMs here and there.

And if you don’t like using floppy disks to transport the public keys from right to left and back, but want to make life difficult for intruders, a serial cable and the likes of UUCP or Kermit might fit the bill. :-)

Further reading

dnssec and dns :: 22 Sep 2022 :: e-mail