Search code examples
hashdnsdnssec

Verifying NSEC3 records


I'm fiddling with DNSSEC, and I'd like to try to verify NSEC3 records generated by dnssec-signzone from bind9-utils (which I presume are valid). This is my zone file:

$ORIGIN dnssectest.mvolfik.tk.
$TTL 120
@   SOA dnssectestns.mvolfik.tk. email.example.com. 15 259200 3600 300000 3600
    A   192.168.0.101
s3c A   192.168.0.101

$INCLUDE zsk.key
$INCLUDE ksk.key

ZSK and KSK are generated with dnssec-keygen -a ECDSAP256SHA256 dnssectest.mvolfik.tk. (add -f KSK respectively)

I then signed it using the command dnssec-signzone -3 deadbeef -H 5 -o dnssectest.mvolfik.tk -k ksk.key zonefile zsk.key (use NSEC3 with deadbeef hex salt, 5 iterations)

I got the following NSEC3 records in the zonefile.signed: (omitted RRSIG and DNSKEY as irrelevant; A and SOA didn't change)

            0   NSEC3PARAM 1 0 5 DEADBEEF
F66KKS17FM851AVA4EARFHS55I3TOO85.dnssectest.mvolfik.tk. 3600 IN NSEC3 1 0 5 DEADBEEF (
                    D60TA5J5RS4JD5AQK25B1BCUAHGP4DHC
                    A SOA RRSIG DNSKEY NSEC3PARAM )
D60TA5J5RS4JD5AQK25B1BCUAHGP4DHC.dnssectest.mvolfik.tk. 3600 IN NSEC3 1 0 5 DEADBEEF (
                    F66KKS17FM851AVA4EARFHS55I3TOO85
                    A RRSIG )

Now that I know that the only domains in this zone are s3c.dnssectest.mvolfik.tk. and dnssectest.mvolfik.tk., I assume that the following Python script would get me the same hashes as in the signe zone file above: (from pseudocode in RFC 5155)

import hashlib
def ih(salt, x, k):
    if k == 0:
        return hashlib.sha1(x + salt).digest()
    return hashlib.sha1(ih(salt, x, k-1) + salt).digest()

print(ih(bytes.fromhex("deadbeef"), b"s3c.dnssectest.mvolfik.tk.", 5).hex())

print(ih(bytes.fromhex("deadbeef"), b"dnssectest.mvolfik.tk.", 5).hex())

However, I instead got b58374998347ba833ab33f15332829a589a80d82 and 545e01397a776ee73aa0372aea015408cc384574. What am I doing wrong?


Solution

  • So I looked into dnspython source code, and found the nsec3_hash function. Turns out that the name must be in wire format (means removing dots and instead prefixing labels a length byte - \x03s3c\x10dnssectest\x07mvolfik\x02tk\x00 etc, null byte at the end). And the result is encoded with base32 (0-9A-V), not hex. Probably easier just to use the dnspython library, but here's the full (a bit naive) code:

    import hashlib, base64
    
    b32_trans = str.maketrans(
        "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567", "0123456789ABCDEFGHIJKLMNOPQRSTUV"
    )
    
    
    def ih(salt, x, k):
        if k == 0:
            return hashlib.sha1(x + salt).digest()
        return hashlib.sha1(ih(salt, x, k - 1) + salt).digest()
    
    
    def nsec3(salt, name, k):
        if not name.endswith("."):
            name += "."
        labels = name.split(".")
        name_wire = b"".join(len(l).to_bytes(1, "big") + l.lower().encode() for l in labels)
        digest = ih(bytes.fromhex(salt), name_wire, k)
        return base64.b32encode(digest).decode().translate(b32_trans)
    
    print(nsec3("deadbeef", "dnssectest.mvolfik.tk.", 5))
    print(nsec3("deadbeef", "s3c.dnssectest.mvolfik.tk.", 5))
    

    This gets the correct hashes seen in the NSEC3 records