Skip to content

NSEC3 zone signing support #1263

@asannes

Description

@asannes

Motivation

I wanted to test NSEC3 zone signing and found sign_zone in dns/dnssec.py:

def sign_zone(
...
        if nsec3:
            raise NotImplementedError("Signing with NSEC3 not yet implemented")
	else:
            ...
            return _sign_zone_nsec(zone, _txn, _rrset_signer)

And I was thinking I could make a go at implementing it, this being my first
try at a contribution to dnspython I figured I would outline my approach
first before wasting your time on a PR that might be going in the wrong
direction. From what I can see, all the bits need is in the dnspython code base already.

Describe the solution you'd like.

I want to make a functional NSEC3 signing implementation.

From what I can see, it would basically be implementing in the dnssec.py
file (after _sign_zone_nsec):

def _sign_zone_nsec3(
    zone: dns.zone.Zone,
    txn: dns.transaction.Transaction,
    nsec3param: NSEC3PARAM,
    rrset_signer: RRsetSigner | None = None,
) -> None:
    """NSEC3 zone signer"""

Re-using rrset_signer to sign the rrset and creating the NSEC3 name chains
as described in https://datatracker.ietf.org/doc/html/rfc5155#section-7.1:

1.  Select the hash algorithm and the values for salt and iterations.

This is what nsec3param contains (slots = ["algorithm", "flags", "iterations",
"salt"]).

2.  For each unique original owner name in the zone add an NSEC3 RR.
       *  If Opt-Out is being used, owner names of unsigned delegations
          MAY be excluded.

       *  The owner name of the NSEC3 RR is the hash of the original
          owner name, prepended as a single label to the zone name.

       *  The Next Hashed Owner Name field is left blank for the moment.

       *  If Opt-Out is being used, set the Opt-Out bit to one.

       *  For collision detection purposes, optionally keep track of the
          original owner name with the NSEC3 RR.

       *  Additionally, for collision detection purposes, optionally
          create an additional NSEC3 RR corresponding to the original
          owner name with the asterisk label prepended (i.e., as if a
          wildcard existed as a child of this owner name) and keep track
          of this original owner name.  Mark this NSEC3 RR as temporary.

The Opt-Out flag in nsec3param.flags first bit, this enables opt-out for not
secured delegations (ns, but no ds). If we want to support opt-out.

We go through all the names, we filter out not secured delegations, sign the
rdatasets, and create hashed names with nsec3_hash(). To handle collisions we
store the hash to a dict[hash str, list[name str]]. Or maybe we could just
add the types into the list up front?

The last point, is about wildcard collisions, we could defer it to the end
and iterate all the names pre-pending wildcards and seeing if there is any
collisions before creating the NSEC3 records, and raise a Nsec3Collision
exception?

3.  For each RRSet at the original owner name, set the corresponding bit in the Type Bit Maps field.  

In _sign_zone_nsec() this is done later in _txn_add_nsec(), should we try to keep it as
equal as possible?

4.  If the difference in number of labels between the apex and the      
    original owner name is greater than 1, additional NSEC3 RRs need    
    to be added for every empty non-terminal between the apex and the   
    original owner name.  This process may generate NSEC3 RRs with      
    duplicate hashed owner names.  Optionally, for collision            
    detection, track the original owner names of these NSEC3 RRs and    
    create temporary NSEC3 RRs for wildcard collisions in a similar     
    fashion to step 1.   

This should probably be solved while iterating, if there are more than one
label between the zone and the record we generate all hashes. We must then
allow to not find any rdatasets in the zone and just have the mandatory
RRSIG type set in the bitmap for it. If there is added other names for this
with data they should combine just fine.

5. Sort the set of NSEC3 RRs into hash order. 

This should be be possible by using the result of sort(hased_names.keys()) as created the steps
above.

6.  Combine NSEC3 RRs with identical hashed owner names by replacing    
    them with a single NSEC3 RR with the Type Bit Maps field            
    consisting of the union of the types represented by the set of      
    NSEC3 RRs.  If the original owner name was tracked, then            
    collisions may be detected when combining, as all of the matching   
    NSEC3 RRs should have the same original owner name.  Discard any    
    possible temporary NSEC3 RRs.

7.  In each NSEC3 RR, insert the next hashed owner name by using the    
    value of the next NSEC3 RR in hash order.  The next hashed owner    
    name of the last NSEC3 RR in the zone contains the value of the     
    hashed owner name of the first NSEC3 RR in the hash order.

The sorted list of hashed names next should be next_hash = sorted_hashes[(i + 1) % len(sorted_hashes)]. And if we have something similar to _sign_zone_nsec(),
say _sign_zone_nsec3() that takes that as a parameter and can lookup all the
rdatasets for the name to create the bitmap.

8.  Finally, add an NSEC3PARAM RR with the same Hash Algorithm,         
    Iterations, and Salt fields to the zone apex.  

Just add the NSEC3PARAM provided signed.

   If a hash collision is detected, then a new salt has to be chosen,
   and the signing process restarted.

An open question is what to do with illegal collisions between non-existant
wildcards covering real names. In the signing process it says that we should
restart with a new salt, but since nsec3param is given as a parameter, should we
throw an exception or should we randomly generate a salt and try again?

And, as it is not recommended for small and medium zone sizes, should we even support opt out?

Metadata

Metadata

Assignees

No one assigned

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions