This commit is contained in:
cronekorkn 2023-08-01 18:08:54 +02:00
parent aede8d21c1
commit 2256f8b384
Signed by: cronekorkn
SSH key fingerprint: SHA256:v0410ZKfuO1QHdgKBsdQNF64xmTxOF8osF1LIqwTcVw
6 changed files with 227 additions and 20 deletions

29
bundles/bind/README.md Normal file
View file

@ -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/

View file

@ -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'] = {

View file

@ -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',

129
libs/dnssec.py Executable file
View file

@ -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,
}

View file

@ -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,
)

View file

@ -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': {