From f6b1f3cc73b501daa70d61648622e3de38bbabc6 Mon Sep 17 00:00:00 2001 From: Vincent Breitmoser Date: Wed, 26 Feb 2025 16:33:57 +0100 Subject: [PATCH] wkd: update a bit, and add to flake --- flake.nix | 4 ++ wkd-domain-checker/default.nix | 22 ++++++++++ wkd-domain-checker/requirements.txt | 25 ++++++------ wkd-domain-checker/setup.py | 12 ++++++ wkd-domain-checker/wkd-domain-checker.py | 52 +++++++++++++++--------- 5 files changed, 84 insertions(+), 31 deletions(-) create mode 100644 wkd-domain-checker/default.nix create mode 100644 wkd-domain-checker/setup.py diff --git a/flake.nix b/flake.nix index 396cbb9..f41890b 100644 --- a/flake.nix +++ b/flake.nix @@ -8,9 +8,13 @@ pkgs = nixpkgs.legacyPackages."${system}"; in rec { packages.hagrid = pkgs.callPackage ./. { }; + packages.wkdDomainChecker = pkgs.callPackage ./wkd-domain-checker/. { }; + packages.default = packages.hagrid; }) // { overlays.hagrid = (final: prev: { hagrid = self.packages."${final.system}".hagrid; }); + overlays.wkdDomainChecker = (final: prev: { wkdDomainChecker = self.packages."${final.system}".wkdDomainChecker; }); + overlays.default = self.overlays.hagrid; }; } diff --git a/wkd-domain-checker/default.nix b/wkd-domain-checker/default.nix new file mode 100644 index 0000000..842605b --- /dev/null +++ b/wkd-domain-checker/default.nix @@ -0,0 +1,22 @@ +{ lib, python3Packages }: + +python3Packages.buildPythonApplication { + pname = "wkd-domain-checker"; + version = "1.0"; + + propagatedBuildInputs = with python3Packages; [ + flask + publicsuffix2 + requests + ]; + + src = ./.; + + meta = with lib; { + description = "WKD domain checker for hagrid wkd gateway"; + homepage = "https://gitlab.com/keys.openpgp.org/hagrid"; + license = with licenses; [ gpl3 ]; + maintainers = with maintainers; [ valodim ]; + platforms = platforms.all; + }; +} diff --git a/wkd-domain-checker/requirements.txt b/wkd-domain-checker/requirements.txt index 20d2947..942907e 100644 --- a/wkd-domain-checker/requirements.txt +++ b/wkd-domain-checker/requirements.txt @@ -1,13 +1,14 @@ -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 +# just for reference, this is canonically built using default.nix +blinker==1.9.0 +certifi==2025.1.31 +charset-normalizer==3.4.1 +click==8.1.8 +Flask==3.1.0 +idna==3.10 +itsdangerous==2.2.0 +Jinja2==3.1.5 +MarkupSafe==3.0.2 publicsuffix2==2.20191221 -requests==2.22.0 -urllib3==1.25.8 -Werkzeug==0.16.1 +requests==2.32.3 +urllib3==2.3.0 +Werkzeug==3.1.3 diff --git a/wkd-domain-checker/setup.py b/wkd-domain-checker/setup.py new file mode 100644 index 0000000..9b980e4 --- /dev/null +++ b/wkd-domain-checker/setup.py @@ -0,0 +1,12 @@ +#!/usr/bin/env python + +from setuptools import setup, find_packages + +setup( + name='wkd-domain-checker', + version='1.0', + # Modules to import from other scripts: + packages=find_packages(), + # Executables + scripts=["wkd-domain-checker.py"], +) diff --git a/wkd-domain-checker/wkd-domain-checker.py b/wkd-domain-checker/wkd-domain-checker.py index 97c2587..387e96e 100644 --- a/wkd-domain-checker/wkd-domain-checker.py +++ b/wkd-domain-checker/wkd-domain-checker.py @@ -1,3 +1,5 @@ +#!/usr/bin/env python + # 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. @@ -10,25 +12,35 @@ # - 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. +# +# Configuration via environment variables: +# +# - FLASK_GATEWAY_DOMAIN must be set to gateway domain (e.g. wkd.keys.openpgp.org) +# - FLASK_ALLOWLIST_FILE may point to a file that contains an explicit allowlist of domains, one per line import requests -from publicsuffix2 import get_sld -from flask import Flask, request, abort, escape -app = Flask(__name__) +import publicsuffix2 +import flask +import markupsafe +import sys -GATEWAY_DOMAIN = 'wkd.keys.openpgp.org' +app = flask.Flask('wkd-domain-checker') +app.config.from_prefixed_env() +if 'GATEWAY_DOMAIN' not in app.config: + app.logger.error('missing config: FLASK_GATEWAY_DOMAIN') + sys.exit(1) -# a manual whitelist of domains. we don't allow arbitrary subdomains for abuse -# reasons, but other entries are generally possible. just ask. -WHITELIST = [ - 'openpgpkey.keys.openpgp.org', - 'openpgpkey.my.amazin.horse' -] +app.config.from_envvar('ALLOWLIST_FILE', silent=True) +if 'ALLOWLIST_FILE' in app.config: + with open(app.config['ALLOWLIST_FILE'], 'r') as f: + app.config['EXPLICIT_ALLOWED_DOMAINS'] = f.read().splitlines() +else: + app.config['EXPLICIT_ALLOWED_DOMAINS'] = [] @app.route('/status/') @app.route('/') def check(): - domain = request.args.get('domain') + domain = flask.request.args.get('domain') if not domain: return 'missing parameter: domain\n', 400 @@ -37,13 +49,15 @@ def check(): return result def check_domain(domain): - if domain in WHITELIST: - return 'ok: domain is whitelisted\n' + # check allowlist of domains. we don't allow arbitrary subdomains for abuse + # reasons, but other entries are generally possible. just ask. + if domain in app.config['EXPLICIT_ALLOWED_DOMAINS']: + return 'ok: domain is allowlisted\n' if not domain.startswith('openpgpkey.'): return 'domain must have "openpgpkey" prefix\n', 400 - if domain != ("openpgpkey." + get_sld(domain)): + if domain != ("openpgpkey." + publicsuffix2.get_sld(domain)): return 'subdomains can only be used upon request. send an email to support at keys dot openpgp dot org\n', 400 req = requests.get( @@ -60,7 +74,7 @@ def check_domain(domain): 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})') + flask.abort(400, f'CNAME lookup failed (http {req.status_code})') response = req.json() app.logger.debug(f'response json: {response}') @@ -76,10 +90,10 @@ def check_domain(domain): 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' + return f'CNAME lookup failed: unexpected response (domain response was for {markupsafe.escape(domain)})\n', 400 + if not answer['data'].startswith(app.config['GATEWAY_DOMAIN']): + return f'CNAME lookup failed: {markupsafe.escape(domain)} resolves to {markupsafe.escape(answer["data"])} (expected {GATEWAY_DOMAIN})\n', 400 + return f'CNAME lookup ok: {markupsafe.escape(domain)} resolves to {GATEWAY_DOMAIN}\n' if __name__ == '__main__': app.run()