diff --git a/CHANGELOG.md b/CHANGELOG.md index 876083f..7480b77 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,18 @@ # EasyWKS Changelog + +## Version 0.4.2 + +Minor feature release + +### Changes + + +- Add option to provide DNS NOTIFY to DANE zone replicas + + + + ## Version 0.4.1 diff --git a/README.md b/README.md index e6dd73e..bfcdcc4 100644 --- a/README.md +++ b/README.md @@ -288,7 +288,8 @@ WantedBy=multi-user.target If you're using EasyWKS' DANE feature, it is highly recommended to configure the SOA and NS records for each domain you're serving. Generally you want to add NS records for all nameservers that will be serving your zone, and at least -set the MNAME and RNAME components of the SOA record: +set the MNAME and RNAME components of the SOA record. You can also configure EasyWKS to provide zone update +notifications whenever a key is modified: ```yaml domains: @@ -298,6 +299,8 @@ domains: - ns2.example.org. - ns1.example.com. - ns2.example.com. + notify: + - "2001:db8::53@10053" soa: mname: ns1.example.org. rname: dnsadmin.example.org. @@ -312,9 +315,16 @@ domains: Knot is an authoritative nameserver that supports signing a replicated zone by the secondary (replicating) nameserver. To configure Knot to transfer the zone from EasyWKS, set up an EasyWKS remote and use it as the replication master for -DANE zones. DNSSEC signing must be enabled as well: +DANE zones. DNSSEC signing must be enabled as well. If you want Knot to be notified of zone changes, set up a notify +ACL too: ```yaml + +acl: + - id: acl-easywks.example.org + address: [::1] + action: notify + remote: - id: remote-easywks.example.org address: [::1]@10053 @@ -322,6 +332,7 @@ remote: zone: - domain: _openpgpkey.example.org master: remote-easywks.example.org + acl: acl-easywks.example.org dnssec-signing: on dnssec-policy: ... ``` diff --git a/easywks/__init__.py b/easywks/__init__.py index 489c7e1..4cedeb6 100644 --- a/easywks/__init__.py +++ b/easywks/__init__.py @@ -1,2 +1,2 @@ -__version__ = '0.4.1' +__version__ = '0.4.2' diff --git a/easywks/config.py b/easywks/config.py index 15fcdb7..fe5131a 100644 --- a/easywks/config.py +++ b/easywks/config.py @@ -144,6 +144,15 @@ def _validate_dane(value): return f'ns items must be strings, got {type(k)}' else: value['ns'] = ['localhost.'] + if 'notify' in value: + notify = value['notify'] + if not isinstance(notify, list): + return f'notify must map to a list, got {type(notify)}' + for k in notify: + if not isinstance(k, str): + return f'notify items must be strings, got {type(k)}' + else: + value['notify'] = [] def _validate_responses(value): diff --git a/easywks/dnsd.py b/easywks/dnsd.py index 259962c..2224b00 100644 --- a/easywks/dnsd.py +++ b/easywks/dnsd.py @@ -1,6 +1,5 @@ from datetime import datetime -import dns from twisted.internet import reactor, defer from twisted.names import dns, server, common, error from twisted.python import util as tputil, failure diff --git a/easywks/files.py b/easywks/files.py index e2c49a7..cebf11a 100644 --- a/easywks/files.py +++ b/easywks/files.py @@ -8,7 +8,7 @@ from pgpy import PGPKey from .config import Config from .crypto import create_pgp_key, privkey_to_pubkey -from .util import hash_user_id, armor_keys, split_revoked, dane_digest +from .util import hash_user_id, armor_keys, split_revoked, dane_digest, dane_notify def _locked_read(file: str, binary: bool = False): @@ -67,6 +67,7 @@ def init_working_directory(): _locked_write(os.path.join(wdir, domain, 'hu', uid), bytes(key), binary=True) digest = dane_digest(Config[domain].submission_address) _locked_write(os.path.join(wdir, domain, 'dane', digest), bytes(key), binary=True) + dane_notify(domain) def read_public_key(domain, user): @@ -99,6 +100,7 @@ def write_public_key(domain, user, key, revoked): joined = bytes(key) + b''.join([bytes(k) for k in revoked]) _locked_write(keyfile, joined, binary=True) _locked_write(danefile, joined, binary=True) + dane_notify(domain) def read_pending_key(domain, nonce): diff --git a/easywks/util.py b/easywks/util.py index a0322a5..a212f3d 100644 --- a/easywks/util.py +++ b/easywks/util.py @@ -6,10 +6,15 @@ import hashlib import secrets import string import textwrap +import logging +from twisted.names import dns +from twisted.internet import reactor, defer from pgpy import PGPKey from pgpy.constants import SignatureType +from .config import Config + def _zrtp_base32(sha1: bytes) -> str: # https://datatracker.ietf.org/doc/html/rfc6189#section-5.1.6 @@ -83,3 +88,28 @@ def split_revoked(keys: Iterable[PGPKey]) -> Tuple[List[PGPKey], List[PGPKey]]: break key = [k for k in keys if k not in revoked_keys] return key, list(revoked_keys) + + +def dane_notify(domain: str): + secondaries = Config[domain].dane.get('notify', []) + if len(secondaries) == 0: + return + origin = dns.domainString(f'_openpgpkey.{domain}') + # this is ugly, but has to do for now + for host in secondaries: + try: + if '@' in host: + addr, port = host.split('@', 1) + port = int(port) + else: + addr = host + port = 53 + # Bind a v4 or v6 UDP client socket + proto = dns.DNSDatagramProtocol(controller=None) + reactor.listenUDP(0, proto, interface='::' if ':' in addr else '0.0.0.0') + # Assemble and send NOTIFY message + m = dns.Message(proto.pickID(), opCode=dns.OP_NOTIFY, auth=1) + m.queries = [dns.Query(origin, dns.SOA, dns.IN)] + proto.writeMessage(m, (addr, port)) + except Exception: + logging.exception(f'An error occurred while attempting to notify {host}')