Loading flake.nix +4 −0 Original line number Diff line number Diff line Loading @@ -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; }; } wkd-domain-checker/default.nix 0 → 100644 +22 −0 Original line number Diff line number Diff line { 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; }; } wkd-domain-checker/requirements.txt +13 −12 Original line number Diff line number Diff line 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 wkd-domain-checker/setup.py 0 → 100644 +12 −0 Original line number Diff line number Diff line #!/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"], ) wkd-domain-checker/wkd-domain-checker.py +33 −19 Original line number Diff line number Diff line #!/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. Loading @@ -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 Loading @@ -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 <tt>support at keys dot openpgp dot org</tt>\n', 400 req = requests.get( Loading @@ -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}') Loading @@ -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() Loading Loading
flake.nix +4 −0 Original line number Diff line number Diff line Loading @@ -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; }; }
wkd-domain-checker/default.nix 0 → 100644 +22 −0 Original line number Diff line number Diff line { 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; }; }
wkd-domain-checker/requirements.txt +13 −12 Original line number Diff line number Diff line 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
wkd-domain-checker/setup.py 0 → 100644 +12 −0 Original line number Diff line number Diff line #!/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"], )
wkd-domain-checker/wkd-domain-checker.py +33 −19 Original line number Diff line number Diff line #!/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. Loading @@ -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 Loading @@ -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 <tt>support at keys dot openpgp dot org</tt>\n', 400 req = requests.get( Loading @@ -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}') Loading @@ -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() Loading