dnssec #13
8 changed files with 264 additions and 20 deletions
29
bundles/bind/README.md
Normal file
29
bundles/bind/README.md
Normal 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/
|
2
bundles/bind/files/dnssec.key
Normal file
2
bundles/bind/files/dnssec.key
Normal file
|
@ -0,0 +1,2 @@
|
|||
; ${type} ${key_id}
|
||||
${record}
|
13
bundles/bind/files/dnssec.private
Normal file
13
bundles/bind/files/dnssec.private
Normal file
|
@ -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']}
|
|
@ -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,6 +146,49 @@ for view_name, view_conf in master_node.metadata.get('bind/views').items():
|
|||
}
|
||||
|
||||
|
||||
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]
|
||||
|
||||
files[f"/var/cache/bind/keys/K{zone_name}.+008+{data['key_id']}.private"] = {
|
||||
'content_type': 'mako',
|
||||
'source': 'dnssec.private',
|
||||
'context': {
|
||||
'data': data['privkey_file'],
|
||||
},
|
||||
'group': 'bind',
|
||||
'needs': [
|
||||
'pkg_apt:bind9',
|
||||
],
|
||||
'needed_by': [
|
||||
'svc_systemd:bind9',
|
||||
],
|
||||
'triggers': [
|
||||
'svc_systemd:bind9:restart',
|
||||
],
|
||||
}
|
||||
|
||||
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'] = {
|
||||
|
|
|
@ -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',
|
||||
|
|
128
libs/dnssec.py
Executable file
128
libs/dnssec.py
Executable file
|
@ -0,0 +1,128 @@
|
|||
# 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)))
|
||||
)
|
||||
|
||||
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
|
||||
flags = 256 if salt == 'zsk' else 257
|
||||
|
||||
dnskey = ''.join(privkey.public_key().public_bytes(
|
||||
crypto_serialization.Encoding.PEM,
|
||||
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_chunks}',
|
||||
'key_id': _calc_keyid(flags, protocol, algorithm, dnskey),
|
||||
'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, key_id):
|
||||
ds = _calc_ds(zone, flags, protocol, algorithm, dnskey)
|
||||
|
||||
return[
|
||||
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
|
||||
|
||||
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'], ksk_data['key_id'])
|
||||
|
||||
return {
|
||||
'zsk_data': zsk_data,
|
||||
'ksk_data': ksk_data,
|
||||
'ds_records': ds_records,
|
||||
}
|
|
@ -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,
|
||||
)
|
||||
|
|
|
@ -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': {
|
||||
|
|
Loading…
Reference in a new issue