DNSSEC key tag (keyid) and DS signature calculation in python

This one took me a considerable amount of hours to figure out so here it is.

While trying to automate DNS zone generation I had to calculate some of the values programmatically. Two of the auto-generated values had to do with DNSSEC entries: The key tag (or keyid) and the DS record’s signatures.

The required details on how these are calculated are found in the following places:

For the calculations you need to provide the following:

  • For the key tag: flags, protocol, algorithm, public key
  • For the DS signatures: owner (the domain name), flags, protocol, algorithm, public key

Code

I used python for this but the approach is the same for other languages since the algorithms are the same.

So here it is:

import struct
import hashlib
import base64

def calc_keyid(flags, protocol, algorithm, st):
  """
  @param owner        The corresponding domain
  @param flags        The flags of the entry (256 or 257)
  @param protocol     Should always be 3
  @param algorithm    Should always be 5
  @param st           The public key as listed in the DNSKEY record.
                      Spaces are removed.
  @return The key tag
  """
  # Remove spaces and create the wire format
  st0=st.replace(' ', '')
  st2=struct.pack('!HBB', int(flags), int(protocol), int(algorithm))
  st2+=base64.b64decode(st0)

  # Calculate the tag
  cnt=0
  for idx in xrange(len(st2)):
    s=struct.unpack('B', st2[idx])[0]
    if (idx % 2) == 0:
      cnt+=s<<8
    else:
      cnt+=s

  ret=((cnt & 0xFFFF) + (cnt>>16)) & 0xFFFF

  return(ret)

def calc_ds(owner, flags, protocol, algorithm, st):
  """
  @param flags        Usually it is 257 or something that indicates a KSK.
                      It can be 256 though.
  @param protocol     Should always be 3
  @param algorithm    Should always be 5
  @param st           The public key as listed in the DNSKEY record.
                      Spaces are removed.
  @return A dictionary of hashes where the key is the hashing algorithm.
  """

  # Remove spaces and create the wire format
  st0=st.replace(' ', '')
  st2=struct.pack('!HBB', int(flags), int(protocol), int(algorithm))
  st2+=base64.b64decode(st0)

  # Ensure a trailing dot
  if owner[-1]=='.':
    owner2=owner
  else:
    owner2=owner+'.'

  # Create the name wire format
  owner3=''
  for i in owner2.split('.'):
    owner3+=struct.pack('B', len(i))+i

  # Calculate the hashes
  st3=owner3+st2
  ret={
    'sha1':    hashlib.sha1(st3).hexdigest().upper(),
    'sha256':  hashlib.sha256(st3).hexdigest().upper(),
  }

  return(ret)

Data

The following were created by bind’s dnssec tools:

$ cat Ktest.hell.gr.+005+33630.key
; This is a zone-signing key, keyid 33630, for test.hell.gr.
; Created: 20101007114826 (Thu Oct  7 14:48:26 2010)
; Publish: 20101007114826 (Thu Oct  7 14:48:26 2010)
; Activate: 20101007114826 (Thu Oct  7 14:48:26 2010)
test.hell.gr. IN DNSKEY 256 3 5 AwEAAb+lTDjZCfq7D5N9cNd1ug30wLrbCXB9mVJJQGlQQHpiHHlMaLGG
  sV2/j5+eojHp+WQUzNpOzrULF6msbEvUuV2gSEnpbueRV4twO8muGE+x
  eUuseSoHh/aTpA8Z9SPubb01mduqqaUEN5Juz2Q4hF0dSUSJYlJPKhp6
  NrOgoeyj

$ cat dsset-test.hell.gr.
test.hell.gr.        IN DS 33630 5 1 A2AD2648B353365631EBC9C70EDA1E0C04563FCC
test.hell.gr.        IN DS 33630 5 2 4177EAEC09A37178357871EBE3FB361CABB2861F12A1D51DDE18CBA2 439BB5C1

Result

>>> domain='test.hell.gr'
>>> flags=256
>>> protocol=3
>>> algorithm=5
>>> key='AwEAAb+...goeyj' # Truncated
>>> calc_keyid(flags, protocol, algorithm, key)
33630
>>> r=calc_ds(domain, flags, protocol, algorithm, key)
>>> r['sha1']
'A2AD2648B353365631EBC9C70EDA1E0C04563FCC'
>>> r['sha256']
'4177EAEC09A37178357871EBE3FB361CABB2861F12A1D51DDE18CBA2439BB5C1'

Legal

You can use the above under the MIT license. If it doesn’t fit your needs let me know. My intention is to make this usable by anyone for any kind of use with no obligation.

Leave a Reply

Your email address will not be published. Required fields are marked *