From 2256f8b3846366a58d6e8adb9685d455d6e1ed96 Mon Sep 17 00:00:00 2001 From: cronekorkn <git@ckn.li> Date: Tue, 1 Aug 2023 18:08:54 +0200 Subject: [PATCH 1/3] wip --- bundles/bind/README.md | 29 +++++++++ bundles/bind/items.py | 34 +++++++++++ bundles/bind/metadata.py | 18 +++++- libs/dnssec.py | 129 +++++++++++++++++++++++++++++++++++++++ libs/rsa.py | 9 ++- nodes/netcup.mails.py | 28 ++++----- 6 files changed, 227 insertions(+), 20 deletions(-) create mode 100644 bundles/bind/README.md create mode 100755 libs/dnssec.py diff --git a/bundles/bind/README.md b/bundles/bind/README.md new file mode 100644 index 0000000..bd78bcc --- /dev/null +++ b/bundles/bind/README.md @@ -0,0 +1,29 @@ +## DNSSEC + +https://wiki.debian.org/DNSSEC%20Howto%20for%20BIND%209.9+#The_signing_part +https://blog.apnic.net/2021/11/02/dnssec-provisioning-automation-with-cds-cdnskey-in-the-real-world/ +https://gist.github.com/wido/4c6288b2f5ba6d16fce37dca3fc2cb4a + +```python +import dns.dnssec +algorithm = dns.dnssec.RSASHA256 +``` + +```python +import cryptography +pk = cryptography.hazmat.primitives.asymmetric.rsa.generate_private_key(key_size=2048, public_exponent=65537) +``` + +## Nomenclature + +### parent + +DNSKEY: + the public key + +DS + +### sub + +ZSK/KSK: + https://www.cloudflare.com/de-de/dns/dnssec/how-dnssec-works/ diff --git a/bundles/bind/items.py b/bundles/bind/items.py index b85c92e..8853747 100644 --- a/bundles/bind/items.py +++ b/bundles/bind/items.py @@ -132,6 +132,40 @@ for view_name, view_conf in master_node.metadata.get('bind/views').items(): } +for zone, conf in master_node.metadata.get('bind/zones').items(): + directories[f"/var/lib/bind/{view_name}"] = { + 'owner': 'bind', + 'group': 'bind', + 'purge': True, + 'needed_by': [ + 'svc_systemd:bind9', + ], + 'triggers': [ + 'svc_systemd:bind9:restart', + ], + } + + for zone_name, zone_conf in view_conf['zones'].items(): + files[f"/var/lib/bind/{view_name}/{zone_name}"] = { + 'source': 'db', + 'content_type': 'mako', + 'unless': f"test -f /var/lib/bind/{view_name}/{zone_name}" if zone_conf.get('allow_update', False) else 'false', + 'context': { + 'serial': datetime.now().strftime('%Y%m%d%H'), + 'records': zone_conf['records'], + 'hostname': node.metadata.get('bind/hostname'), + 'type': node.metadata.get('bind/type'), + }, + 'owner': 'bind', + 'group': 'bind', + 'needed_by': [ + 'svc_systemd:bind9', + ], + 'triggers': [ + 'svc_systemd:bind9:restart', + ], + } + svc_systemd['bind9'] = {} actions['named-checkconf'] = { diff --git a/bundles/bind/metadata.py b/bundles/bind/metadata.py index 56155fc..89f9d15 100644 --- a/bundles/bind/metadata.py +++ b/bundles/bind/metadata.py @@ -39,7 +39,7 @@ defaults = { 'zones': {}, }, }, - 'zones': set(), + 'zones': {}, }, 'nftables': { 'input': { @@ -61,6 +61,22 @@ defaults = { } +@metadata_reactor.provides( + 'bind/zones', +) +def dnssec(metadata): + return { + 'bind': { + 'zones': { + zone: { + 'dnssec': repo.libs.dnssec.generate_dnssec_for_zone(zone, node), + } + for zone in metadata.get('bind/zones') + }, + }, + } + + @metadata_reactor.provides( 'bind/type', 'bind/master_ip', diff --git a/libs/dnssec.py b/libs/dnssec.py new file mode 100755 index 0000000..5cc4553 --- /dev/null +++ b/libs/dnssec.py @@ -0,0 +1,129 @@ +# 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 + + +def long_to_base64(n): + return urlsafe_b64encode(int_to_bytes(n, None)).decode() + +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, repo): + privkey = repo.libs.rsa.generate_deterministic_rsa_private_key( + b64decode(str(repo.vault.random_bytes_as_base64_for(f'dnssec {salt} ' + zone))) + ) + privkey_pem = privkey.private_bytes( + crypto_serialization.Encoding.PEM, + crypto_serialization.PrivateFormat.PKCS8, + crypto_serialization.NoEncryption() + ).decode() + + 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_pem, + '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 + +def generate_dnssec_for_zone(zone, node): + zsk_data = generate_signing_key_pair(zone, salt='zsk', repo=node.repo) + ksk_data = generate_signing_key_pair(zone, salt='ksk', repo=node.repo) + 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, + } diff --git a/libs/rsa.py b/libs/rsa.py index e2666fb..a16e065 100644 --- a/libs/rsa.py +++ b/libs/rsa.py @@ -1,7 +1,6 @@ # https://stackoverflow.com/a/18266970 from Crypto.PublicKey import RSA -from Crypto.Hash import HMAC from struct import pack from hashlib import sha3_512 from cryptography.hazmat.primitives.serialization import load_der_private_key @@ -23,12 +22,12 @@ class PRNG(object): @cache_to_disk(30) -def _generate_deterministic_rsa_private_key(secret_bytes): - return RSA.generate(2048, randfunc=PRNG(secret_bytes)).export_key('DER') +def _generate_deterministic_rsa_private_key(secret_bytes, key_size): + return RSA.generate(key_size, randfunc=PRNG(secret_bytes)).export_key('DER') @cache -def generate_deterministic_rsa_private_key(secret_bytes): +def generate_deterministic_rsa_private_key(secret_bytes, key_size=2048): return load_der_private_key( - _generate_deterministic_rsa_private_key(secret_bytes), + _generate_deterministic_rsa_private_key(secret_bytes, key_size), password=None, ) diff --git a/nodes/netcup.mails.py b/nodes/netcup.mails.py index 70c5351..acfa3b5 100644 --- a/nodes/netcup.mails.py +++ b/nodes/netcup.mails.py @@ -39,20 +39,20 @@ 'hostname': 'resolver.name', 'acme_zone': 'acme.sublimity.de', 'zones': { - 'sublimity.de', - 'freibrief.net', - 'nadenau.net', - 'naeder.net', - 'wettengl.net', - 'wingl.de', - 'woodpipe.de', - 'ckn.li', - 'islamicstate.eu', - 'hausamsilberberg.de', - 'wiegand.tel', - 'lonercrew.io', - 'left4.me', - 'elimu-kwanza.de', + 'sublimity.de': {}, + 'freibrief.net': {}, + 'nadenau.net': {}, + 'naeder.net': {}, + 'wettengl.net': {}, + 'wingl.de': {}, + 'woodpipe.de': {}, + 'ckn.li': {}, + 'islamicstate.eu': {}, + 'hausamsilberberg.de': {}, + 'wiegand.tel': {}, + 'lonercrew.io': {}, + 'left4.me': {}, + 'elimu-kwanza.de': {}, }, }, 'dns': { -- 2.39.5 From 5b0b2e77ce4c3c19f2a5a0541dbe050e5cbfbce0 Mon Sep 17 00:00:00 2001 From: cronekorkn <git@ckn.li> Date: Tue, 1 Aug 2023 18:58:37 +0200 Subject: [PATCH 2/3] wip --- bundles/bind/files/dnssec.key | 2 + bundles/bind/files/dnssec.private | 13 +++++++ bundles/bind/items.py | 65 +++++++++++++++++++++---------- libs/dnssec.py | 17 +++----- 4 files changed, 65 insertions(+), 32 deletions(-) create mode 100644 bundles/bind/files/dnssec.key create mode 100644 bundles/bind/files/dnssec.private diff --git a/bundles/bind/files/dnssec.key b/bundles/bind/files/dnssec.key new file mode 100644 index 0000000..82841e9 --- /dev/null +++ b/bundles/bind/files/dnssec.key @@ -0,0 +1,2 @@ +; ${type} ${key_id} +${record} diff --git a/bundles/bind/files/dnssec.private b/bundles/bind/files/dnssec.private new file mode 100644 index 0000000..3a46b3f --- /dev/null +++ b/bundles/bind/files/dnssec.private @@ -0,0 +1,13 @@ +Private-key-format: ${data['Private-key-format']} +Algorithm: ${data['Algorithm']} +Modulus: ${data['Modulus']} +PublicExponent: ${data['PublicExponent']} +PrivateExponent: ${data['PrivateExponent']} +Prime1: ${data['Prime1']} +Prime2: ${data['Prime2']} +Exponent1: ${data['Exponent1']} +Exponent2: ${data['Exponent2']} +Coefficient: ${data['Coefficient']} +Created: ${data['Created']} +Publish: ${data['Publish']} +Activate: ${data['Activate']} diff --git a/bundles/bind/items.py b/bundles/bind/items.py index 8853747..a006bf9 100644 --- a/bundles/bind/items.py +++ b/bundles/bind/items.py @@ -23,6 +23,20 @@ directories[f'/var/lib/bind'] = { ], } +directories[f'/var/cache/bind/keys'] = { + 'group': 'bind', + 'purge': True, + 'needs': [ + 'pkg_apt:bind9', + ], + 'needed_by': [ + 'svc_systemd:bind9', + ], + 'triggers': [ + 'svc_systemd:bind9:restart', + ], +} + files['/etc/default/bind9'] = { 'source': 'defaults', 'needed_by': [ @@ -132,32 +146,20 @@ for view_name, view_conf in master_node.metadata.get('bind/views').items(): } -for zone, conf in master_node.metadata.get('bind/zones').items(): - directories[f"/var/lib/bind/{view_name}"] = { - 'owner': 'bind', - 'group': 'bind', - 'purge': True, - 'needed_by': [ - 'svc_systemd:bind9', - ], - 'triggers': [ - 'svc_systemd:bind9:restart', - ], - } +for zone_name, zone_conf in master_node.metadata.get('bind/zones').items(): + for sk in ('zsk_data', 'ksk_data'): + data = zone_conf['dnssec'][sk] - for zone_name, zone_conf in view_conf['zones'].items(): - files[f"/var/lib/bind/{view_name}/{zone_name}"] = { - 'source': 'db', + files[f"/var/cache/bind/keys/K{zone_name}.+008+{data['key_id']}.private"] = { 'content_type': 'mako', - 'unless': f"test -f /var/lib/bind/{view_name}/{zone_name}" if zone_conf.get('allow_update', False) else 'false', + 'source': 'dnssec.private', 'context': { - 'serial': datetime.now().strftime('%Y%m%d%H'), - 'records': zone_conf['records'], - 'hostname': node.metadata.get('bind/hostname'), - 'type': node.metadata.get('bind/type'), + 'data': data['privkey_file'], }, - 'owner': 'bind', 'group': 'bind', + 'needs': [ + 'pkg_apt:bind9', + ], 'needed_by': [ 'svc_systemd:bind9', ], @@ -166,6 +168,27 @@ for zone, conf in master_node.metadata.get('bind/zones').items(): ], } + files[f"/var/cache/bind/keys/K{zone_name}.+008+{data['key_id']}.key"] = { + 'content_type': 'mako', + 'source': 'dnssec.key', + 'context': { + 'type': sk, + 'key_id': data['key_id'], + 'record': data['dnskey_record'], + }, + 'group': 'bind', + 'needs': [ + 'pkg_apt:bind9', + ], + 'needed_by': [ + 'svc_systemd:bind9', + ], + 'triggers': [ + 'svc_systemd:bind9:restart', + ], + } + + svc_systemd['bind9'] = {} actions['named-checkconf'] = { diff --git a/libs/dnssec.py b/libs/dnssec.py index 5cc4553..1e43136 100755 --- a/libs/dnssec.py +++ b/libs/dnssec.py @@ -30,11 +30,6 @@ def generate_signing_key_pair(zone, salt, repo): privkey = repo.libs.rsa.generate_deterministic_rsa_private_key( b64decode(str(repo.vault.random_bytes_as_base64_for(f'dnssec {salt} ' + zone))) ) - privkey_pem = privkey.private_bytes( - crypto_serialization.Encoding.PEM, - crypto_serialization.PrivateFormat.PKCS8, - crypto_serialization.NoEncryption() - ).decode() public_exponent = privkey.private_numbers().public_numbers.e modulo = privkey.private_numbers().public_numbers.n @@ -44,6 +39,7 @@ def generate_signing_key_pair(zone, salt, repo): exponent1 = privkey.private_numbers().dmp1 exponent2 = privkey.private_numbers().dmq1 coefficient = privkey.private_numbers().iqmp + flags = 256 if salt == 'zsk' else 257 dnskey = ''.join(privkey.public_key().public_bytes( crypto_serialization.Encoding.PEM, @@ -53,7 +49,7 @@ def generate_signing_key_pair(zone, salt, repo): return { 'dnskey': dnskey, 'dnskey_record': f'{zone}. IN DNSKEY {flags} {protocol} {algorithm} {dnskey}', - 'privkey': privkey_pem, + 'key_id': _calc_keyid(flags, protocol, algorithm, dnskey), 'privkey_file': { 'Private-key-format': 'v1.3', 'Algorithm': f'{algorithm} ({algorithm_name})', @@ -106,13 +102,12 @@ def _calc_keyid(flags, protocol, algorithm, dnskey): return ((cnt & 0xFFFF) + (cnt >> 16)) & 0xFFFF -def dnskey_to_ds(zone, flags, protocol, algorithm, dnskey): - keyid = _calc_keyid(flags, protocol, algorithm, dnskey) +def dnskey_to_ds(zone, flags, protocol, algorithm, dnskey, key_id): 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()}", + f"{zone}. IN DS {str(key_id)} {str(algorithm)} 1 {ds['sha1'].lower()}", + f"{zone}. IN DS {str(key_id)} {str(algorithm)} 2 {ds['sha256'].lower()}", ] # Result @@ -120,7 +115,7 @@ def dnskey_to_ds(zone, flags, protocol, algorithm, dnskey): def generate_dnssec_for_zone(zone, node): zsk_data = generate_signing_key_pair(zone, salt='zsk', repo=node.repo) ksk_data = generate_signing_key_pair(zone, salt='ksk', repo=node.repo) - ds_records = dnskey_to_ds(zone, flags, protocol, algorithm, ksk_data['dnskey']) + ds_records = dnskey_to_ds(zone, flags, protocol, algorithm, ksk_data['dnskey'], ksk_data['key_id']) return { 'zsk_data': zsk_data, -- 2.39.5 From 4567b66c606678fcad7c8682c5e93e8d17c56b0c Mon Sep 17 00:00:00 2001 From: cronekorkn <git@ckn.li> Date: Tue, 1 Aug 2023 19:03:08 +0200 Subject: [PATCH 3/3] wip --- libs/dnssec.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/libs/dnssec.py b/libs/dnssec.py index 1e43136..990e66b 100755 --- a/libs/dnssec.py +++ b/libs/dnssec.py @@ -46,9 +46,13 @@ def generate_signing_key_pair(zone, salt, repo): crypto_serialization.PublicFormat.SubjectPublicKeyInfo ).decode().split('\n')[1:-2]) + dnskey_chunks = ' '.join( + dnskey[i:i+56] for i in range(0, len(dnskey), 56) + ) + return { 'dnskey': dnskey, - 'dnskey_record': f'{zone}. IN DNSKEY {flags} {protocol} {algorithm} {dnskey}', + 'dnskey_record': f'{zone}. IN DNSKEY {flags} {protocol} {algorithm} {dnskey_chunks}', 'key_id': _calc_keyid(flags, protocol, algorithm, dnskey), 'privkey_file': { 'Private-key-format': 'v1.3', -- 2.39.5