Tutorial 3: Signing a zone file

The full source code can be found in examples/ldns-signzone.c

Of course, we start by the usual includes. Since we need a bit more here, we'll add those right away.

#include <stdio.h>
#include "config.h"
#ifdef HAVE_SSL
#include <stdlib.h>
#include <unistd.h>
#include <errno.h>
#include <time.h>
#include <ldns/ldns.h>
#include <ldns/keys.h>
#include <openssl/conf.h>
#if defined(HAVE_OPENSSL_ENGINE_H) && !defined(OPENSSL_NO_ENGINE)
config.h
keys.h
Addendum to dnssec.h, this module contains key and algorithm definitions and functions.
ldns.h
Including this file will include all ldns files, and define some lookup tables.

Let's skip the boring usage() and sanity check functions, and dive right into main().

main(int argc, char *argv[])
main
int main(void)
Definition linktest.c:6
{

We'll be reading another zone file, so let's prepare some variables for that.

const char *zonefile_name;
FILE *zonefile = NULL;
int line_nr = 0;
int c;
int argi;

We will create a separate zone structure for the signed zone, so let's have a clear name for the original one.

ldns_zone *orig_zone;
ldns_zone
struct ldns_struct_zone ldns_zone
Definition zone.h:50
ldns_rr_list *orig_rrs = NULL;
ldns_rr *orig_soa = NULL;
ldns_dnssec_zone *signed_zone;
ldns_dnssec_zone
struct ldns_struct_dnssec_zone ldns_dnssec_zone
Definition dnssec_zone.h:104
ldns_rr
struct ldns_struct_rr ldns_rr
Definition rr.h:337
ldns_rr_list
struct ldns_struct_rr_list ldns_rr_list
Definition rr.h:351

To sign a zone, we need keys, so we need some variables to read and store it;

char *keyfile_name_base;
char *keyfile_name = NULL;
FILE *keyfile = NULL;
ldns_key *key = NULL;
#ifndef OPENSSL_NO_ENGINE
ldns_key *eng_ksk = NULL; /* KSK specified with -K */
ldns_key *eng_zsk = NULL; /* ZSK specified with -k */
#endif
ldns_key_list *keys;
ldns_status s;
ldns_status
enum ldns_enum_status ldns_status
Definition error.h:148
ldns_key
struct ldns_struct_key ldns_key
Definition keys.h:167
ldns_key_list
struct ldns_struct_key_list ldns_key_list
Definition keys.h:177

The ldns_key structure holds (private) keys. These can be of any supported algorithm type; you can put an RSA key in it, an DSA key, or an HMAC key. Public keys can simply be put in an ldns_rr structure with type LDNS_RR_TYPE_DNSKEY.

The ldns_key_list type is much like the ldns_rr_list, only, you guessed it, for `ldns_key` entries.

The signed zone will be stored in a new file.

char *outputfile_name = NULL;
FILE *outputfile;

And we have some command line options for the output zone.

struct tm tm;
uint32_t inception;
uint32_t expiration;
ldns_rdf *origin = NULL;
uint32_t ttl = LDNS_DEFAULT_TTL;
ldns_rr_class class = LDNS_RR_CLASS_IN;
LDNS_DEFAULT_TTL
#define LDNS_DEFAULT_TTL
Definition ldns.h:136
ldns_rdf
struct ldns_struct_rdf ldns_rdf
Definition rdata.h:205
ldns_rr_class
enum ldns_enum_rr_class ldns_rr_class
Definition rr.h:61
LDNS_RR_CLASS_IN
@ LDNS_RR_CLASS_IN
the Internet
Definition rr.h:47

`origin` is a domain name, so it can be stored in an ldns_rdf variable with type LDNS_RDF_TYPE_DNAME.

The next part is option parsing, which is pretty straightforward using `getopt()`, so we'll skip this too. U can always look to the source of the file to check it out.

Okay that's it for the variables, let's get to work!

First we'll try to read in the zone that is to be signed.

zonefile = fopen(zonefile_name, "r");
if (!zonefile) {
fprintf(stderr,
"Error: unable to read %s (%s)\n",
zonefile_name,
strerror(errno));
exit(EXIT_FAILURE);
} else {

If the file exists and can be read, we'll let ldns mold it into a zone structure:

s = ldns_zone_new_frm_fp_l(&orig_zone,
ldns_zone_new_frm_fp_l
ldns_status ldns_zone_new_frm_fp_l(ldns_zone **z, FILE *fp, const ldns_rdf *origin, uint32_t ttl, ldns_rr_class c, int *line_nr)
Create a new zone from a file, keep track of the line numbering.

This creates a new (`new`) zone from (`frm`) a filepointer (`fp`), while remembering the current line (`l`) in the input file (for error messages).

A pointer to the zone structure to be filled is passed as the first argument, like in most `new_frm` functions.

Like a lot of ldns functions, this one returns a `ldns_status` indicating success or the type of failure, so let us check that.

if (s != LDNS_STATUS_OK) {
LDNS_STATUS_OK
@ LDNS_STATUS_OK
Definition error.h:26
fprintf(stderr, "Zone not read, error: %s at %s line %d\n",
ldns_get_errorstr_by_id(s),
zonefile_name, line_nr);
exit(EXIT_FAILURE);
} else {
ldns_get_errorstr_by_id
const char * ldns_get_errorstr_by_id(ldns_status err)
look up a descriptive text by each error.
Definition error.c:196

If everything is ok so far, we check if the zone has a SOA record and contains actual data.

orig_soa = ldns_zone_soa(orig_zone);
ldns_zone_soa
ldns_rr * ldns_zone_soa(const ldns_zone *z)
Return the soa record of a zone.
Definition zone.c:17
if (!orig_soa) {
fprintf(stderr,
"Error reading zonefile: missing SOA record\n");
exit(EXIT_FAILURE);
}
orig_rrs = ldns_zone_rrs(orig_zone);
if (!orig_rrs) {
fprintf(stderr,
"Error reading zonefile: no resource records\n");
exit(EXIT_FAILURE);
}
ldns_zone_rrs
ldns_rr_list * ldns_zone_rrs(const ldns_zone *z)
Get a list of a zone's content.
Definition zone.c:35
}

Now that we have the complete zone in our memory, we won't be needing the file anymore.

fclose(zonefile);
}

If there was no origin given, we'll use the one derived from the original zone file.

* relevant to DNS (origin, TTL, etc), and because that
* information becomes known only after the command line
* and the zone file are parsed completely, the program
* needs to post-process these keys before they become usable.
*/
/* The engine's KSK. */
post_process_engine_key ( keys,
eng_ksk,
orig_zone,
add_keys,
ttl,
inception,
expiration );
/* The engine's ZSK. */
post_process_engine_key ( keys,
eng_zsk,
orig_zone,
add_keys,
ttl,
inception,
expiration );
#endif
if (ldns_key_list_key_count(keys) < 1
&& !(signflags & LDNS_SIGN_NO_KEYS_NO_NSECS)) {
fprintf(stderr, "Error: no keys to sign with. Aborting.\n\n");
usage(stderr, prog);
exit(EXIT_FAILURE);
}
LDNS_SIGN_NO_KEYS_NO_NSECS
#define LDNS_SIGN_NO_KEYS_NO_NSECS
Definition dnssec_sign.h:17
ldns_key_list_key_count
size_t ldns_key_list_key_count(const ldns_key_list *key_list)
returns the number of keys in the key list
Definition keys.c:1447
prog
char * prog
Definition ldns-signzone.c:36

No signing party can be complete without keys to sign with, let's fetch those.

Multiple key files can be specified on the command line, by using the base names of the .key/.private file pairs.

keys,
ldns_dnssec_default_replace_signatures,
NULL,
nsec3_algorithm,
nsec3_flags,
nsec3_iterations,
nsec3_salt_length,
nsec3_salt,
signflags,
&fmt_st.hashmap);
} else {
result = ldns_dnssec_zone_sign_flg(signed_zone,
added_rrs,
keys,
ldns_dnssec_default_replace_signatures,
NULL,
signflags);
}
if (result != LDNS_STATUS_OK) {
fprintf(stderr, "Error signing zone: %s\n",
ldns_get_errorstr_by_id(result));
}
if (!outputfile_name) {
outputfile_name = LDNS_XMALLOC(char, MAX_FILENAME_LEN);
snprintf(outputfile_name, MAX_FILENAME_LEN, "%s.signed", zonefile_name);
}
if (signed_zone) {
if (strncmp(outputfile_name, "-", 2) == 0) {
ldns_dnssec_zone_print(stdout, signed_zone);
} else {
outputfile = fopen(outputfile_name, "w");
ldns_dnssec_default_replace_signatures
int ldns_dnssec_default_replace_signatures(ldns_rr *sig, void *n)
Default callback function to always leave present signatures, and add new ones.
ldns_dnssec_zone_sign_flg
ldns_status ldns_dnssec_zone_sign_flg(ldns_dnssec_zone *zone, ldns_rr_list *new_rrs, ldns_key_list *key_list, int(*func)(ldns_rr *, void *), void *arg, int flags)
signs the given zone with the given keys
Definition dnssec_sign.c:1376
ldns_dnssec_zone_print
void ldns_dnssec_zone_print(FILE *out, const ldns_dnssec_zone *zone)
Prints the complete zone to the given file descriptor.
Definition dnssec_zone.c:1109
MAX_FILENAME_LEN
#define MAX_FILENAME_LEN
Definition ldns-signzone.c:34
LDNS_XMALLOC
#define LDNS_XMALLOC(type, count)
Definition util.h:51

As you can see, we append ".private" to the name, which should result in the complete file name of the private key. Later we'll also form the ".key" file name, which will be directly included in the signed zone.

If the file exists, we'll read it and create a `ldns_key` from its contents, much like the way we read the zone earlier.

If this went ok, we need to set the inception and expiration times, which are set in the keys, but will eventually end up in the RRSIGs generated by those keys.

And now that we have read the private keys, we read the public keys and add them to the zone.

Reading them from the files works roughly the same as reading private keys, but public keys are normal Resource Records, and they can be stored in general `ldns_rr` structures.

With `push()` we add them to our key list and our zone. This function clones the data, so we can safely free it after that.

And if we're done, we free the allocated memory for the file name.

If the reading did not work, we print an error. Finally, we move on to the next key in the argument list.

Just to be sure, we add a little check to see if we actually have any keys now.

So, we have our zone, we have our keys, let's do some signing!

Yes. That's it. We now have a completely signed zone, `ldns_zone_sign` checks the keys, and uses the zone signing keys to sign the data resource records. NSEC and RRSIG resource records are generated and added to the new zone.

So now that we have a signed zone, all that is left is to store it somewhere.

If no explicit output file name was given, we'll just append ".signed" to the original zone file name.

`ldns_zone_sign` returns NULL if the signing did not work, so we must check that.

Writing to a file is no different than normal printing, so we'll print to the file and close it.

And of course, give an error if the signing failed.

Just to be nice, let's free the rest of the data we allocated, and exit with the right return value.