DNSSEC validation in Go for fun and profit
Doing cryptography is hard, luckily there are enough libraries out there that help you with it. OpenSSL is probably one of the best (known). Go has its own crypto library, which is written in pure Go.
Now with these aids crypto becomes doable for mere mortals, but all these libraries work with buffers which hold the data, the signature and sometimes the key also. Off-by-one errors in composing these buffers leads to a “Bogus signature” error (in DNSSEC). The problem here is that you don’t get any other clue on what went wrong. For me as a programmer an error such as “Shift buffer A one byte to the left and you’re OK”, would be much better. But due to the nature of crypto these kind of errors are not possible, nor desirable.
So you are then faced with the problem of getting 3 buffers filled with the right content and rightly aligned and the only feedback you get is OK or not OK. Clearly this is a nasty problem.
Filling buffers⌗
In RFC 4035, section “5.3.2. Reconstructing the Signed Data” says the following:
signed_data = RRSIG_RDATA | RR(1) | RR(2)... where
"|" denotes concatenation
RRSIG_RDATA is the wire format of the RRSIG RDATA fields
with the Signature field excluded and the Signer's Name
in canonical form.
RR(i) = name | type | class | OrigTTL | RDATA length | RDATA
name is calculated according to the function below
class is the RRset's class
type is the RRset type and all RRs in the class
OrigTTL is the value from the RRSIG Original TTL field
All names in the RDATA field are in canonical form
Which is pretty strait forward, but it needs a lot of conversions from my local representation to the wire format and concatenating buffers. Needless to say, this can go wrong in many places.
Going forward So how do figure out if you’re getting close to successfully validating a signature in DNSSEC? I used the following procedure:
- Debug the hell out of your program;
- Find some other piece of software that successfully! performs the same function;
- Debug the hell out of this other piece of software;
- Hopefully you’ll get some insights on where your program is failing.
Actual steps taken The other software I used is ldns as I’m very familiar with its source code.
Next I wrote I little C program (ldns-verify
), that does signature validation, and
also prints the RRs (as an extra control measure).
/* ... */
ldns_rr_new_frm_str(&dnskey, "miek.nl. IN DNSKEY 256 3 8
AwEAAcNEU67LJI5GEgF9QLNqLO1SMq1EdoQ6E9f85ha0k0ewQGCblyW2836GiVsm6k8Kr5EC
IoMJ6fZWf3CQSQ9ycWfTyOHfmI3eQ/1Covhb2y4bAmL/07PhrL7ozWBW3wBfM335Ft9xjtXH
Py7ztCbV9qZ4TVDTW/Iyg0PiwgoXVesz", 14400, NULL, NULL);
ldns_rr_new_frm_str(&soa, "miek.nl. IN SOA open.nlnetlabs.nl.
miekg.atoom.net. 1293945905 14400 3600 604800 86400", 14400,
NULL, NULL);
ldns_rr_new_frm_str(&rrsig, "miek.nl. IN RRSIG SOA 8 2 14400
20110201042505 20110102042505 12051 miek.nl.
oMCbslaAVIp/8kVtLSms3tDABpcPRUgHLrOR48OOplkYo+8TeEGWwkSwaz/MRo2fB4FxW0qj
/hTlIjUGuACSd+b1wKdH5GvzRJc2pFmxtCbm55ygAh4EUL0F6U5cKtGJGSXxxg6UFCQ0doJC
miGFa78LolaUOXImJrk6AFrGa0M=", 14400, NULL, NULL);
ldns_rr_print(stdout, rrsig);
ldns_rr_list_push_rr(soalist, soa);
ldns_rr_list_push_rr(keylist, dnskey);
ldns_rr_list_print(stdout, soalist);
ldns_rr_list_print(stdout, keylist);
s = 0;
s = ldns_verify_rrsig_keylist_notime(soalist, rrsig, keylist, goodkeys);
printf("WTF: %s\n", ldns_get_errorstr_by_id(s));
With this I found my first mistake. Which was that I didn’t take the timezone (UTC) into account when setting the signature inception and expiration times in my test program.
Next I put the ldns library itself full of debugging statements, for instance in
the file dnssec_verify.c
in the function ldns_verify_rrsig_evp_raw
,
that print the raw content of the buffers in decimal:
fprintf(stderr, "RRSETBUF\n");
size_t i;
for(i = 0; i < ldns_buffer_position(rrset); i++) {
fprintf(stderr, "%d ", ldns_buffer_read_u8_at(rrset, i));
}
fprintf(stderr, "\n");
fprintf(stderr, "SIGBUF\n");
for(i = 0; i < siglen; i++) {
fprintf(stderr, "%d ", sig[i]);
}
fprintf(stderr, "\n");
Then I put a bunch of fmt.Printf
s in my Go code, which also prints
the buffers just before the signature validation.
fmt.Printf("SIGBUF %v", sigbuf)
Next run ldns-verify
with the patched library:
LD_LIBRARY_PATH=/tmp/ldns-1.4.6/.libs ./ldns-verify
And after watching, counting and checking buffers like this all
afternoon (from ldns-verify
):
RRSETBUF
0 6 8 2 0 0 56 64 77 71 139 33 77 31 254 33 47 19 4 109 105 101 107 2
110 108 0 4 109 105 101 107 2 110 108 0 0 6 0 1 0 0 56 64 0 56 4 111
112 101 110 9 110 108 110 101 116 108 97 98 115 2 110 108 0 5 109
105 101 107 103 5 97 116 111 111 109 3 110 101 116 0 77 32 12
49 0 0 56 64 0 0 14 16 0 9 58 128 0 1 81 128
And this from my Go test prog:
SIGNEDDATA BUF
[0 6 8 2 0 0 56 64 77 71 139 33 77 31 254 33 47 19 4 109 105 101 107 2
110 108 0 4 109 105 101 107 2 110 108 0 0 6 0 1 0 0 56 64 0 56 4 111
112 101 110 9 110 108 110 101 116 108 97 98 115 2 110 108 0 5 109
105 101 107 103 5 97 116 111 111 109 3 110 101 116 0 77 32 12
49 0 0 56 64 0 0 14 16 0 9 58 128 0 1 81 128]
I fixed the bugs, fixed the buffers and got the RSASHA256
validation
working! Now only RSASHA1
and RSAMD5
and I’m done :-)
Go code The actual validation code looks something like this (still needs to be cleaned up):
case AlgRSASHA256:
// RFC 3110, section 2. RSA Public KEY Resource Records
// Assume length is in the first byte!
// keybuf[1]
_E := int(keybuf[3]) <<16
_E += int(keybuf[2]) <<8
_E += int(keybuf[1])
println("_E", _E)
pubkey := new(rsa.PublicKey)
pubkey.E = _E
pubkey.N = big.NewInt(0)
pubkey.N.SetBytes(keybuf[4:])
fmt.Fprintf(os.Stderr, "keybug len %d", len(keybuf[4:]))
fmt.Fprintf(os.Stderr, "PubKey %s\n", pubkey.N)
// Hash the signeddata
s := sha256.New()
io.WriteString(s, string(signeddata))
sighash := s.Sum()
println("sig hash", len(sighash))
err := rsa.VerifyPKCS1v15(pubkey, rsa.HashSHA256, sighash, sigbuf)
if err == nil {
fmt.Fprintf(os.Stderr, "NO SHIT Sherlock!!\n")
} else {
fmt.Fprintf(os.Stderr, "*********** %v\n", err)
}