diff --git a/wkd-domain-checker/.gitignore b/wkd-domain-checker/.gitignore new file mode 100644 index 0000000..5ceb386 --- /dev/null +++ b/wkd-domain-checker/.gitignore @@ -0,0 +1 @@ +venv diff --git a/wkd-domain-checker/requirements.txt b/wkd-domain-checker/requirements.txt new file mode 100644 index 0000000..20d2947 --- /dev/null +++ b/wkd-domain-checker/requirements.txt @@ -0,0 +1,13 @@ +certifi==2019.11.28 +chardet==3.0.4 +Click==7.0 +Flask==1.1.1 +gunicorn==20.0.4 +idna==2.8 +itsdangerous==1.1.0 +Jinja2==2.11.0 +MarkupSafe==1.1.1 +publicsuffix2==2.20191221 +requests==2.22.0 +urllib3==1.25.8 +Werkzeug==0.16.1 diff --git a/wkd-domain-checker/wkd-domain-checker.py b/wkd-domain-checker/wkd-domain-checker.py new file mode 100644 index 0000000..b249174 --- /dev/null +++ b/wkd-domain-checker/wkd-domain-checker.py @@ -0,0 +1,89 @@ +# Simple flask server that checks whether a domain is allowed as a WKD target. +# Most importantly, this determines whether we attempt to request a certificate +# from letsencrypt for it. +# +# The fqdn must fulfill the following criteria: +# - it must start with "openpgpkey.", since only those are relevant for the WKD +# advanced method +# - it must be directly below a public suffix. this makes it hard for anyone to +# generate arbitrary numbers of subdomains. +# - it must be a CNAME that points to wkd.keys.openpgp.org. We do a simple DoH +# request to cloudflare to make sure it looks correct from someone else's +# perspective. + +import requests +from publicsuffix2 import get_sld +from flask import Flask, request, abort, escape +app = Flask(__name__) + +GATEWAY_DOMAIN = 'wkd.keys.openpgp.org' + +# a manual whitelist of domains. we don't allow arbitrary subdomains for abuse +# reasons, but other entries are generally possible. just ask. +WHITELIST = [ + 'openpgpkey.my.amazin.horse' +] + +@app.route('/status/') +@app.route('/') +def check(): + domain = request.args.get('domain') + if not domain: + return 'missing parameter: domain\n', 400 + + result = check_domain(domain) + app.logger.info(f'{domain}: {result}') + return result + +def check_domain(domain): + if domain in WHITELIST: + return 'ok: domain is whitelisted\n' + + if not domain.startswith('openpgpkey.'): + return 'domain must have "openpgpkey" prefix\n', 400 + + if domain != ("openpgpkey." + get_sld(domain)): + return 'domain must not be a subdomain!\n', 400 + + req = requests.get( + 'https://cloudflare-dns.com/dns-query', + params={ + 'name': domain, + 'type': 'CNAME' + }, + headers={ + 'accept': 'application/dns-json' + } + ) + app.logger.debug(f'lookup url: {req.url}') + + if req.status_code != 200: + app.logger.debug(f'dns error: {req.status_code} {req.text})') + abort(400, f'CNAME lookup failed (http {req.status_code})') + response = req.json() + app.logger.debug(f'response json: {response}') + + if 'Status' not in response: + return 'CNAME lookup failed (no status)\n', 400 + if response['Status'] != 0: + return 'CNAME lookup failed (invalid domain?)\n', 400 + if 'Answer' not in response: + return 'CNAME lookup failed: no CNAME record set\n', 400 + if len(response['Answer']) != 1: + return 'CNAME lookup failed: ambiguous answer section\n', 400 + answer = response['Answer'][0] + if answer['type'] != 5: + return 'CNAME lookup failed: unexpected response (record type)\n', 400 + if answer['name'] != domain and answer['name'] != f'{domain}.': + return f'CNAME lookup failed: unexpected response (domain response was for {escape(domain)})\n', 400 + if not answer['data'].startswith(GATEWAY_DOMAIN): + return f'CNAME lookup failed: {escape(domain)} resolves to {escape(answer["data"])} (expected {GATEWAY_DOMAIN})\n', 400 + return f'CNAME lookup ok: {escape(domain)} resolves to {GATEWAY_DOMAIN}\n' + +if __name__ == '__main__': + app.run() +else: + import logging + gunicorn_logger = logging.getLogger('gunicorn.error') + app.logger.handlers = gunicorn_logger.handlers + app.logger.setLevel(gunicorn_logger.level) diff --git a/wkd-domain-checker/wkd-domain-checker.service b/wkd-domain-checker/wkd-domain-checker.service new file mode 100644 index 0000000..00dcc0e --- /dev/null +++ b/wkd-domain-checker/wkd-domain-checker.service @@ -0,0 +1,16 @@ +[Unit] +Description=wkd-domain-checker +After=network.target + +[Service] +Type=simple +WorkingDirectory=/root/wkd-domain-checker +ExecStart=/root/wkd-domain-checker/venv/bin/gunicorn --log-level=info wkd-domain-checker:app +ExecReload=/bin/kill -s HUP $MAINPID +KillMode=mixed +TimeoutStopSec=5 +User=nobody +PrivateTmp=true + +[Install] +WantedBy=default.target