diff --git a/contrib/testing/README.md b/contrib/testing/README.md index a817dec..5f94a92 100644 --- a/contrib/testing/README.md +++ b/contrib/testing/README.md @@ -35,6 +35,9 @@ one-off files in `/tmp`: - `mail-dns-live-check.py` logs in through the real Web and IMAPS interfaces and verifies that setup produced both an actionable SPF/DKIM/DMARC task and a forwardable administrator message without private-key material. +- `dkim-material-check.py` compares a real OpenDKIM-generated RSA-2048 key + with Bongo's GnuTLS-derived public value, imports it, checks permissions and + DNS output, then verifies byte-identical reuse. The complete mapping from the original one-off `/tmp` names to maintained scripts is recorded in `tmp-script-map.md`. diff --git a/contrib/testing/dkim-material-check.py b/contrib/testing/dkim-material-check.py new file mode 100755 index 0000000..21cf693 --- /dev/null +++ b/contrib/testing/dkim-material-check.py @@ -0,0 +1,89 @@ +#!/usr/bin/env python3 +"""Exercise real OpenDKIM generation and Bongo's GnuTLS key import path.""" + +from __future__ import annotations + +import os +import re +import shutil +import subprocess +import sys +import tempfile +from pathlib import Path +from types import SimpleNamespace +from unittest import mock + + +source = Path(os.environ.get( + "BONGO_SOURCE_ROOT", Path(__file__).resolve().parents[2])).resolve() +sys.path.insert(0, str(source / "src" / "libs" / "python")) + +from bongo.configuration.model import Scenario # noqa: E402 +from bongo.configuration.security_material import ( # noqa: E402 + _private_key_data, provision_security_material, +) + + +generator = shutil.which("opendkim-genkey") +certtool = shutil.which("certtool") +if not generator or not certtool: + raise SystemExit("opendkim-genkey and GnuTLS certtool are required") + +with tempfile.TemporaryDirectory(prefix="bongo-dkim-import-", dir="/tmp") as name: + root = Path(name) + source_key = root / "source" / "bongo.private" + source_key.parent.mkdir(mode=0o700) + subprocess.run([ + generator, "-b", "2048", "-d", "example.test", + "-D", str(source_key.parent), "-s", "bongo", + ], check=True, stdin=subprocess.DEVNULL, stdout=subprocess.PIPE, + stderr=subprocess.PIPE, timeout=120) + source_key.chmod(0o600) + private_key, public_key = _private_key_data(source_key) + generated_txt = (source_key.parent / "bongo.txt").read_text( + encoding="ascii") + published = "".join(re.findall(r'"([^"]*)"', generated_txt)) + match = re.search(r"(?:^|;)\s*p\s*=\s*([^;\s]+)", published, + re.IGNORECASE) + if match is None or match.group(1) != public_key: + raise SystemExit("GnuTLS public-key derivation differs from OpenDKIM") + + paths = SimpleNamespace( + ssl_dir=root / "etc/bongo/ssl.d", + dkim_dir=root / "etc/bongo/dkim.d", + dmarc_dir=root / "etc/bongo/dmarc.d") + scenario = Scenario( + hostname="mail.example.test", bind_address="192.0.2.5", + domains=["example.test"], admin_password="not-used-by-this-test", + dkim_selector="bongo", dkim_key_directory=str(paths.dkim_dir), + dkim_import_keys={"example.test": str(source_key)}, + mail_dns={"example.test": { + "spf": ["v=spf1 mx -all"], + "dkim": [f"v=DKIM1; k=rsa; p={public_key}"], + "dmarc": ["v=DMARC1; p=reject; " + "rua=mailto:dmarc-reports@example.test"], + "errors": [], + }}) + with mock.patch( + "bongo.configuration.security_material.os.geteuid", + return_value=0), mock.patch( + "bongo.configuration.security_material._bongo_ids", + return_value=(os.getuid(), os.getgid())), mock.patch( + "bongo.configuration.security_material.os.chown"), mock.patch( + "bongo.configuration.security_material.os.fchown"): + result = provision_security_material(paths, scenario) + repeated = provision_security_material(paths, scenario) + + target = paths.dkim_dir / "example.test" / "bongo.private" + if target.read_bytes() != private_key: + raise SystemExit("imported DKIM key differs from its source") + if target.stat().st_mode & 0o777 != 0o640: + raise SystemExit("imported DKIM key does not use mode 0640") + bongo_txt = (target.parent / "bongo.txt").read_text(encoding="ascii") + bongo_published = "".join(re.findall(r'"([^"]*)"', bongo_txt)) + if public_key not in bongo_published: + raise SystemExit("Bongo DKIM DNS hint does not match the imported key") + if result[0].actions or repeated[0].actions: + raise SystemExit("matching published DNS unexpectedly requires an action") + +print("PASS OpenDKIM RSA-2048 generation and GnuTLS DKIM import/reuse")