From b97f30faf476f5221ee5e87049728314079a63b5 Mon Sep 17 00:00:00 2001 From: Pierre-Olivier Mercier Date: Thu, 23 Apr 2026 12:13:33 +0700 Subject: [PATCH] Initial commit --- .gitignore | 2 + Dockerfile | 14 + LICENSE | 21 + Makefile | 25 ++ NOTICE | 31 ++ README.md | 128 ++++++ checker/collect.go | 858 +++++++++++++++++++++++++++++++++++++++++ checker/definition.go | 107 +++++ checker/dns.go | 127 ++++++ checker/interactive.go | 177 +++++++++ checker/provider.go | 29 ++ checker/report.go | 680 ++++++++++++++++++++++++++++++++ checker/rule.go | 120 ++++++ checker/types.go | 245 ++++++++++++ go.mod | 19 + go.sum | 22 ++ main.go | 27 ++ plugin/plugin.go | 21 + 18 files changed, 2653 insertions(+) create mode 100644 .gitignore create mode 100644 Dockerfile create mode 100644 LICENSE create mode 100644 Makefile create mode 100644 NOTICE create mode 100644 README.md create mode 100644 checker/collect.go create mode 100644 checker/definition.go create mode 100644 checker/dns.go create mode 100644 checker/interactive.go create mode 100644 checker/provider.go create mode 100644 checker/report.go create mode 100644 checker/rule.go create mode 100644 checker/types.go create mode 100644 go.mod create mode 100644 go.sum create mode 100644 main.go create mode 100644 plugin/plugin.go diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..d297e8c --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +checker-openpgpkey +checker-openpgpkey.so diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..b346539 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,14 @@ +FROM golang:1.25-alpine AS builder + +ARG CHECKER_VERSION=custom-build + +WORKDIR /src +COPY go.mod go.sum ./ +RUN go mod download +COPY . . +RUN CGO_ENABLED=0 go build -ldflags "-X main.Version=${CHECKER_VERSION}" -o /checker-openpgpkey . + +FROM scratch +COPY --from=builder /checker-openpgpkey /checker-openpgpkey +EXPOSE 8080 +ENTRYPOINT ["/checker-openpgpkey"] diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..07d44d8 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 The happyDomain Authors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the “Software”), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..ddf32ee --- /dev/null +++ b/Makefile @@ -0,0 +1,25 @@ +CHECKER_NAME := checker-openpgpkey +CHECKER_IMAGE := happydomain/$(CHECKER_NAME) +CHECKER_VERSION ?= custom-build + +CHECKER_SOURCES := main.go $(wildcard checker/*.go) + +GO_LDFLAGS := -X main.Version=$(CHECKER_VERSION) + +.PHONY: all plugin docker clean + +all: $(CHECKER_NAME) + +$(CHECKER_NAME): $(CHECKER_SOURCES) + go build -ldflags "$(GO_LDFLAGS)" -o $@ . + +plugin: $(CHECKER_NAME).so + +$(CHECKER_NAME).so: $(CHECKER_SOURCES) $(wildcard plugin/*.go) + go build -buildmode=plugin -ldflags "$(GO_LDFLAGS)" -o $@ ./plugin/ + +docker: + docker build --build-arg CHECKER_VERSION=$(CHECKER_VERSION) -t $(CHECKER_IMAGE) . + +clean: + rm -f $(CHECKER_NAME) $(CHECKER_NAME).so diff --git a/NOTICE b/NOTICE new file mode 100644 index 0000000..c463df1 --- /dev/null +++ b/NOTICE @@ -0,0 +1,31 @@ +checker-openpgpkey +Copyright (c) 2026 The happyDomain Authors + +This product is licensed under the MIT License (see LICENSE). + +------------------------------------------------------------------------------- +Third-party notices +------------------------------------------------------------------------------- + +This product includes software developed as part of the checker-sdk-go +project (https://git.happydns.org/happyDomain/checker-sdk-go), licensed +under the Apache License, Version 2.0: + + checker-sdk-go + Copyright 2020-2026 The happyDomain Authors + +You may obtain a copy of the Apache License 2.0 at: + http://www.apache.org/licenses/LICENSE-2.0 + +------------------------------------------------------------------------------- +OpenPGP parsing +------------------------------------------------------------------------------- + +This product uses github.com/ProtonMail/go-crypto/openpgp (fork of the +deprecated golang.org/x/crypto/openpgp), BSD-style license. + +------------------------------------------------------------------------------- +DNS +------------------------------------------------------------------------------- + +This product uses github.com/miekg/dns, BSD-style license. diff --git a/README.md b/README.md new file mode 100644 index 0000000..1efd879 --- /dev/null +++ b/README.md @@ -0,0 +1,128 @@ +# checker-openpgpkey + +DANE-Email posture checker for happyDomain. + +Runs a comprehensive testsuite on a domain's DNS-published OpenPGP key +(`OPENPGPKEY`, [RFC 7929][rfc7929]) or S/MIME certificate (`SMIMEA`, +[RFC 8162][rfc8162]) and renders an actionable HTML report whose top +block nudges the user toward the fix for the most common failure +scenarios. + +This checker binds to the happyDomain services: + +- `abstract.OpenPGP`: individual user's PGP key, owner-hashed below + `._openpgpkey.`. +- `abstract.SMimeCert`: user's S/MIME certificate, owner-hashed below + `._smimecert.`. + +[rfc7929]: https://www.rfc-editor.org/rfc/rfc7929 +[rfc8162]: https://www.rfc-editor.org/rfc/rfc8162 + +## Tests run + +All findings are tagged by severity (`info` / `warn` / `crit`) so the +rule engine can fold them into a single `CheckState`. + +### DNS (both record types) + +| Code | Severity | What it catches | +| --- | --- | --- | +| `dns_query_failed` | crit | The resolver returned an error or did not answer. | +| `dns_no_record` | crit | The authoritative answer has no record at the expected owner. | +| `dnssec_not_validated` | crit / warn | The validating resolver did not set `AD`. RFC 7929/8162 mandate DNSSEC; the severity is configurable via `requireDNSSEC`. | +| `dns_record_mismatch` | warn | The record returned by DNS differs from the one declared in the service (typically a stale zone on the authoritative servers). | +| `owner_hash_mismatch` | crit | Record owner-name first label is not `sha256(localpart)[:28]`; mail clients will never find it. | + +### OpenPGP-specific (RFC 7929) + +| Code | Severity | What it catches | +| --- | --- | --- | +| `pgp_parse_error` | crit | Malformed base64 or OpenPGP packet stream. | +| `pgp_no_entity` | crit | Record decoded but carries no valid entity. | +| `pgp_primary_revoked` | crit | Primary key has a revocation signature. | +| `pgp_primary_expired` | crit | Self-signature expired; clients will refuse to encrypt. | +| `pgp_primary_expiring_soon` | warn | Expires within the `certExpiryWarnDays` window (default 30). | +| `pgp_weak_algorithm` | warn | Uses DSA / ElGamal (phase-out). | +| `pgp_weak_key_size` | crit / warn | RSA below 2048 bits is critical, 2048-3071 is a warn. | +| `pgp_no_encryption_subkey` | crit | No active key in the entity advertises encryption capability. | +| `pgp_no_identity` | warn | No self-signed User ID. | +| `pgp_uid_mismatch` | info | None of the UIDs reference ``. | +| `pgp_multiple_entities` | warn | Record carries more than one entity (RFC 7929 recommends one). | +| `pgp_record_too_large` | warn | Raw key > 4 KiB; forces UDP→TCP fallback on every lookup. | + +### SMIMEA-specific (RFC 8162) + +| Code | Severity | What it catches | +| --- | --- | --- | +| `smimea_bad_usage` / `_selector` / `_match_type` | crit | Field outside the allowed range. | +| `smimea_cert_parse_error` | crit | Hex-encoded blob is not a valid X.509 certificate / SPKI. | +| `smimea_cert_expired` / `_not_yet_valid` | crit | `notBefore` / `notAfter` gate the current time out. | +| `smimea_cert_expiring_soon` | warn | Within the `certExpiryWarnDays` window. | +| `smimea_no_email_protection_eku` | crit / warn | Missing `emailProtection` EKU (RFC 8550/8551 agents will reject). | +| `smimea_missing_key_usage` | warn | Neither `digitalSignature` nor `keyEncipherment` key-usage is set. | +| `smimea_email_mismatch` | info | No email SAN starts with `@`. | +| `smimea_weak_signature_algorithm` | crit | MD5 / SHA-1 based signature. | +| `smimea_weak_key_size` | crit / warn | RSA < 2048 / 3072 bits. | +| `smimea_self_signed` | info | Self-signed certificate paired with PKIX-EE usage. | +| `smimea_hash_only` | info | Matching-type 1/2 only carries a digest; certificate can't be inspected. | + +## Why a bespoke checker instead of a third-party testsuite? + +There is no canonical "OPENPGPKEY / SMIMEA testsuite" in Go or as a +self-hostable online service: + +- `ldns-dane` (NLnet Labs) validates DANE-TLSA and handles SMIMEA only + shallowly (it parses the record without deep certificate checks). +- `hokey` (Paul Wouters) queries OPENPGPKEY but does not validate the + key material. +- Online DANE validators (e.g. `dane.sys4.de`, `has-tls-rpt.com`) focus + on SMTP DANE-TLSA, not email-identity records. + +The heavy lifting here is standard Go parsing: + +- `github.com/ProtonMail/go-crypto/openpgp` (maintained fork of the + deprecated `golang.org/x/crypto/openpgp`) for OpenPGP packet parsing, + UIDs, subkeys, revocations, key-lifetime self-signatures. +- `crypto/x509` for SMIMEA certificate parsing, validity window, EKU, + key-usage, signature-algorithm and key-size checks. +- `github.com/miekg/dns` for the DNS+EDNS0+DO query and the `AD` flag + read-back used as the DNSSEC-validation signal. + +## Options + +| Id | Type | Default | Description | +| --- | --- | --- | --- | +| `resolver` | string | *(system)* | Validating resolver to query; comma-separated list accepted. | +| `certExpiryWarnDays` | number | 30 | Raise an `expiring_soon` warning within this window. | +| `requireDNSSEC` | bool | true | When false, missing AD is a warn instead of crit. | +| `requireEmailProtection` | bool | true | When false, missing `emailProtection` EKU is a warn instead of crit. | + +Auto-filled by the host: `domain_name`, `subdomain`, `service`, +`service_type`. + +## Running + +```bash +# Plugin (loaded by happyDomain at startup) +make plugin + +# Standalone HTTP server +make && ./checker-openpgpkey -listen :8080 +``` + +## HTML report + +The report renders as a self-contained HTML document intended for +embedding in an `