bundlewrap/bin/dnssec
2023-08-01 17:13:41 +02:00

195 lines
5.7 KiB
Python
Executable file

#!/usr/bin/env python3
# https://medium.com/iocscan/how-dnssec-works-9c652257be0
# https://de.wikipedia.org/wiki/RRSIG_Resource_Record
# https://metebalci.com/blog/a-minimum-complete-tutorial-of-dnssec/
# https://bind9.readthedocs.io/en/latest/dnssec-guide.html
from sys import argv
from os.path import realpath, dirname
from bundlewrap.repo import Repository
from base64 import b64decode, urlsafe_b64encode
from cryptography.utils import int_to_bytes
from cryptography.hazmat.primitives import serialization as crypto_serialization
from struct import pack, unpack
from hashlib import sha1, sha256
from json import dumps
from cache_to_disk import cache_to_disk
def long_to_base64(n):
return urlsafe_b64encode(int_to_bytes(n, None)).decode()
zone = argv[1]
repo = Repository(dirname(dirname(realpath(__file__))))
flags = 256
protocol = 3
algorithm = 8
algorithm_name = 'RSASHA256'
# ZSK/KSK DNSKEY
#
# https://cryptography.io/en/latest/hazmat/primitives/asymmetric/rsa/#cryptography.hazmat.primitives.asymmetric.rsa.RSAPrivateNumbers
# https://crypto.stackexchange.com/a/21104
def generate_signing_key_pair(zone, salt):
privkey = repo.libs.rsa.generate_deterministic_rsa_private_key(
b64decode(str(repo.vault.random_bytes_as_base64_for(f'dnssec {salt} ' + zone)))
)
public_exponent = privkey.private_numbers().public_numbers.e
modulo = privkey.private_numbers().public_numbers.n
private_exponent = privkey.private_numbers().d
prime1 = privkey.private_numbers().p
prime2 = privkey.private_numbers().q
exponent1 = privkey.private_numbers().dmp1
exponent2 = privkey.private_numbers().dmq1
coefficient = privkey.private_numbers().iqmp
dnskey = ''.join(privkey.public_key().public_bytes(
crypto_serialization.Encoding.PEM,
crypto_serialization.PublicFormat.SubjectPublicKeyInfo
).decode().split('\n')[1:-2])
return {
'dnskey': dnskey,
'dnskey_record': f'{zone}. IN DNSKEY {flags} {protocol} {algorithm} {dnskey}',
'privkey': privkey,
'privkey_file': {
'Private-key-format': 'v1.3',
'Algorithm': f'{algorithm} ({algorithm_name})',
'Modulus': long_to_base64(modulo),
'PublicExponent': long_to_base64(public_exponent),
'PrivateExponent': long_to_base64(private_exponent),
'Prime1': long_to_base64(prime1),
'Prime2': long_to_base64(prime2),
'Exponent1': long_to_base64(exponent1),
'Exponent2': long_to_base64(exponent2),
'Coefficient': long_to_base64(coefficient),
'Created': 20230428110109,
'Publish': 20230428110109,
'Activate': 20230428110109,
},
}
# DS
#
# https://gist.github.com/wido/4c6288b2f5ba6d16fce37dca3fc2cb4a#file-dnskey_to_dsrecord-py-L40
def _calc_ds(zone, flags, protocol, algorithm, dnskey):
if zone.endswith('.') is False:
zone += '.'
signature = bytes()
for i in zone.split('.'):
signature += pack('B', len(i)) + i.encode()
signature += pack('!HBB', int(flags), int(protocol), int(algorithm))
signature += b64decode(dnskey)
return {
'sha1': sha1(signature).hexdigest().upper(),
'sha256': sha256(signature).hexdigest().upper(),
}
def _calc_keyid(flags, protocol, algorithm, dnskey):
st = pack('!HBB', int(flags), int(protocol), int(algorithm))
st += b64decode(dnskey)
cnt = 0
for idx in range(len(st)):
s = unpack('B', st[idx:idx+1])[0]
if (idx % 2) == 0:
cnt += s << 8
else:
cnt += s
return ((cnt & 0xFFFF) + (cnt >> 16)) & 0xFFFF
def dnskey_to_ds(zone, flags, protocol, algorithm, dnskey):
keyid = _calc_keyid(flags, protocol, algorithm, dnskey)
ds = _calc_ds(zone, flags, protocol, algorithm, dnskey)
return[
f"{zone}. IN DS {str(keyid)} {str(algorithm)} 1 {ds['sha1'].lower()}",
f"{zone}. IN DS {str(keyid)} {str(algorithm)} 2 {ds['sha256'].lower()}",
]
# Result
#@cache_to_disk(30)
def generate_dnssec_for_zone(zone):
zsk_data = generate_signing_key_pair(zone, salt='zsk')
ksk_data = generate_signing_key_pair(zone, salt='ksk')
ds_records = dnskey_to_ds(zone, flags, protocol, algorithm, ksk_data['dnskey'])
return {
'zsk_data': zsk_data,
'ksk_data': ksk_data,
'ds_records': ds_records,
}
print(
generate_dnssec_for_zone(zone),
)
# #########################
# from dns import rrset, rdatatype, rdata
# from dns.rdataclass import IN
# from dns.dnssec import sign, make_dnskey
# from dns.name import Name
# from dns.rdtypes.IN.A import A
# data = generate_dnssec_for_zone(zone)
# zone_name = Name(f'{zone}.'.split('.'))
# assert zone_name.is_absolute()
# # rrset = rrset.from_text_list(
# # name=Name(['test']).derelativize(zone_name),
# # origin=zone_name,
# # relativize=False,
# # ttl=60,
# # rdclass=IN,
# # rdtype=rdatatype.from_text('A'),
# # text_rdatas=[
# # '100.2.3.4',
# # '10.0.0.55',
# # ],
# # )
# rrset = rrset.from_rdata_list(
# name=Name(['test']).derelativize(zone_name),
# ttl=60,
# rdatas=[
# rdata.from_text(
# rdclass=IN,
# rdtype=rdatatype.from_text('A'),
# origin=zone_name,
# tok='1.2.3.4',
# relativize=False,
# ),
# A(IN, rdatatype.from_text('A'), '10.20.30.40')
# ],
# )
# # for e in rrset:
# # print(e.is_absolute())
# dnskey = make_dnskey(
# public_key=data['zsk_data']['privkey'].public_key(),
# algorithm=algorithm,
# flags=flags,
# protocol=protocol,
# )
# sign(
# rrset=rrset,
# private_key=data['zsk_data']['privkey'],
# signer=Name(f'{zone}.'),
# dnskey=dnskey,
# lifetime=99999,
# )