diff --git a/.drone-manifest.yml b/.drone-manifest.yml deleted file mode 100644 index ba70baf..0000000 --- a/.drone-manifest.yml +++ /dev/null @@ -1,22 +0,0 @@ -image: happydomain/checker-email-keys:{{#if build.tag}}{{trimPrefix "v" build.tag}}{{else}}latest{{/if}} -{{#if build.tags}} -tags: -{{#each build.tags}} - - {{this}} -{{/each}} -{{/if}} -manifests: - - image: happydomain/checker-email-keys:{{#if build.tag}}{{trimPrefix "v" build.tag}}-{{/if}}linux-amd64 - platform: - architecture: amd64 - os: linux - - image: happydomain/checker-email-keys:{{#if build.tag}}{{trimPrefix "v" build.tag}}-{{/if}}linux-arm64 - platform: - architecture: arm64 - os: linux - variant: v8 - - image: happydomain/checker-email-keys:{{#if build.tag}}{{trimPrefix "v" build.tag}}-{{/if}}linux-arm - platform: - architecture: arm - os: linux - variant: v7 diff --git a/.drone.yml b/.drone.yml deleted file mode 100644 index 8f432d9..0000000 --- a/.drone.yml +++ /dev/null @@ -1,187 +0,0 @@ ---- -kind: pipeline -type: docker -name: build-amd64 - -platform: - os: linux - arch: amd64 - -steps: - - name: checker build - image: golang:1-alpine - commands: - - apk add --no-cache git make - - make - environment: - CHECKER_VERSION: "${DRONE_BRANCH}-${DRONE_COMMIT}" - CGO_ENABLED: 0 - when: - event: - exclude: - - tag - - - name: checker build tag - image: golang:1-alpine - commands: - - apk add --no-cache git make - - make - environment: - CHECKER_VERSION: "${DRONE_SEMVER}" - CGO_ENABLED: 0 - when: - event: - - tag - - - name: publish on Docker Hub - image: plugins/docker - settings: - repo: happydomain/checker-email-keys - auto_tag: true - auto_tag_suffix: ${DRONE_STAGE_OS}-${DRONE_STAGE_ARCH} - dockerfile: Dockerfile - build_args: - - CHECKER_VERSION=${DRONE_BRANCH}-${DRONE_COMMIT} - username: - from_secret: docker_username - password: - from_secret: docker_password - when: - event: - exclude: - - tag - - - name: publish on Docker Hub (tag) - image: plugins/docker - settings: - repo: happydomain/checker-email-keys - auto_tag: true - auto_tag_suffix: ${DRONE_STAGE_OS}-${DRONE_STAGE_ARCH} - dockerfile: Dockerfile - build_args: - - CHECKER_VERSION=${DRONE_SEMVER} - username: - from_secret: docker_username - password: - from_secret: docker_password - when: - event: - - tag - -trigger: - branch: - exclude: - - renovate/* - event: - - cron - - push - - tag - ---- -kind: pipeline -type: docker -name: build-arm64 - -platform: - os: linux - arch: arm64 - -steps: - - name: checker build - image: golang:1-alpine - commands: - - apk add --no-cache git make - - make - environment: - CHECKER_VERSION: "${DRONE_BRANCH}-${DRONE_COMMIT}" - CGO_ENABLED: 0 - when: - event: - exclude: - - tag - - - name: checker build tag - image: golang:1-alpine - commands: - - apk add --no-cache git make - - make - environment: - CHECKER_VERSION: "${DRONE_SEMVER}" - CGO_ENABLED: 0 - when: - event: - - tag - - - name: publish on Docker Hub - image: plugins/docker - settings: - repo: happydomain/checker-email-keys - auto_tag: true - auto_tag_suffix: ${DRONE_STAGE_OS}-${DRONE_STAGE_ARCH} - dockerfile: Dockerfile - build_args: - - CHECKER_VERSION=${DRONE_BRANCH}-${DRONE_COMMIT} - username: - from_secret: docker_username - password: - from_secret: docker_password - when: - event: - exclude: - - tag - - - name: publish on Docker Hub (tag) - image: plugins/docker - settings: - repo: happydomain/checker-email-keys - auto_tag: true - auto_tag_suffix: ${DRONE_STAGE_OS}-${DRONE_STAGE_ARCH} - dockerfile: Dockerfile - build_args: - - CHECKER_VERSION=${DRONE_SEMVER} - username: - from_secret: docker_username - password: - from_secret: docker_password - when: - event: - - tag - -trigger: - event: - - cron - - push - - tag - ---- -kind: pipeline -name: docker-manifest - -platform: - os: linux - arch: arm64 - -steps: - - name: publish on Docker Hub - image: plugins/manifest - settings: - auto_tag: true - ignore_missing: true - spec: .drone-manifest.yml - username: - from_secret: docker_username - password: - from_secret: docker_password - -trigger: - branch: - exclude: - - renovate/* - event: - - cron - - push - - tag - -depends_on: - - build-amd64 - - build-arm64 diff --git a/README.md b/README.md index 5ccd72c..f87d3c2 100644 --- a/README.md +++ b/README.md @@ -36,40 +36,53 @@ keys it finds. It does **not** cryptographically verify them: Treat a green report as "the record is well-formed and DNSSEC-signed", not as "the key is trustworthy". -## Rules +## Tests run -| Code | Description | Severity | -|-----------------------------------|---------------------------------------------------------------------------------------------------|---------------------| -| `dns_query_failed` | Verifies that the DNS lookup for the OPENPGPKEY/SMIMEA record succeeds. | CRITICAL | -| `dns_no_record` | Verifies that an OPENPGPKEY/SMIMEA record is published at the expected owner name. | CRITICAL | -| `dns_record_mismatch` | Verifies that the record returned by DNS matches the service-declared record. | WARNING | -| `dnssec_not_validated` | Verifies that the record is authenticated by DNSSEC (AD flag set). | CRITICAL | -| `owner_hash_mismatch` | Verifies that the owner-name first label equals hex(sha256(username))[:28]. | CRITICAL | -| `pgp_parse_error` | Verifies that the OPENPGPKEY record decodes as a valid OpenPGP key. | CRITICAL | -| `pgp_primary_revoked` | Verifies that the OpenPGP primary key carries no revocation signature. | CRITICAL | -| `pgp_primary_expired` | Verifies that the OpenPGP primary key has not passed its self-signature expiry. | CRITICAL | -| `pgp_primary_expiring_soon` | Warns when the OpenPGP primary key expires within the configured window. | WARNING | -| `pgp_weak_algorithm` | Verifies that OpenPGP keys do not use legacy algorithms (DSA/ElGamal). | WARNING | -| `pgp_weak_key_size` | Verifies that OpenPGP RSA keys meet the minimum 2048-bit size (3072+ preferred). | CRITICAL | -| `pgp_no_encryption_subkey` | Verifies that at least one active OpenPGP key advertises encryption capability. | CRITICAL | -| `pgp_no_identity` | Verifies that the OpenPGP key carries at least one self-signed User ID. | WARNING | -| `pgp_uid_mismatch` | Checks that at least one OpenPGP UID references . | INFO | -| `pgp_multiple_entities` | Verifies that the record carries a single OpenPGP entity (RFC 7929). | WARNING | -| `pgp_record_too_large` | Verifies that the OPENPGPKEY record stays below 4 KiB to fit typical UDP answers. | WARNING | -| `smimea_bad_usage` | Verifies that the SMIMEA usage field is 0, 1, 2, or 3. | CRITICAL | -| `smimea_bad_selector` | Verifies that the SMIMEA selector field is 0 (Cert) or 1 (SPKI). | CRITICAL | -| `smimea_bad_match_type` | Verifies that the SMIMEA matching type is 0 (Full), 1 (SHA-256), or 2 (SHA-512). | CRITICAL | -| `smimea_cert_parse_error` | Verifies that the SMIMEA record decodes as a valid X.509 certificate or SPKI. | CRITICAL | -| `smimea_cert_not_yet_valid` | Verifies that the S/MIME certificate's NotBefore is in the past. | CRITICAL | -| `smimea_cert_expired` | Verifies that the S/MIME certificate's NotAfter is in the future. | CRITICAL | -| `smimea_cert_expiring_soon` | Warns when the S/MIME certificate expires within the configured window. | WARNING | -| `smimea_no_email_protection_eku` | Verifies that the S/MIME certificate advertises the emailProtection EKU. | CRITICAL | -| `smimea_missing_key_usage` | Verifies that the certificate carries digitalSignature and/or keyEncipherment key usage. | WARNING | -| `smimea_weak_signature_algorithm` | Verifies that the certificate is not signed with a deprecated algorithm (MD2/MD5/SHA-1). | CRITICAL | -| `smimea_weak_key_size` | Verifies that SMIMEA RSA keys meet the minimum 2048-bit size (3072+ preferred). | CRITICAL | -| `smimea_self_signed` | Flags self-signed certificates paired with PKIX-EE (usage 1). | INFO | -| `smimea_email_mismatch` | Checks that at least one email SAN on the certificate begins with @. | INFO | -| `smimea_hash_only` | Notes that SMIMEA matching types 1/2 transport only a digest, preventing certificate inspection. | INFO | +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. | ## Options diff --git a/checker/collect.go b/checker/collect.go index cc30469..2b715fe 100644 --- a/checker/collect.go +++ b/checker/collect.go @@ -193,13 +193,11 @@ func computeOwner(body serviceBody, prefix, parent string) (expected, recorded s // Normalise: no double dots. expected = strings.Replace(expected, "..", ".", -1) } - // happyDomain encodes service-embedded record owners relative to the - // parent zone, so we must join with parent before treating as FQDN. switch { case body.OpenPGP != nil && body.OpenPGP.Hdr.Name != "": - recorded = dns.Fqdn(sdk.JoinRelative(body.OpenPGP.Hdr.Name, parent)) + recorded = dns.Fqdn(body.OpenPGP.Hdr.Name) case body.SMIMEA != nil && body.SMIMEA.Hdr.Name != "": - recorded = dns.Fqdn(sdk.JoinRelative(body.SMIMEA.Hdr.Name, parent)) + recorded = dns.Fqdn(body.SMIMEA.Hdr.Name) } return } diff --git a/go.mod b/go.mod index 536ca56..d6f2629 100644 --- a/go.mod +++ b/go.mod @@ -3,17 +3,17 @@ module git.happydns.org/checker-email-keys go 1.25.0 require ( - git.happydns.org/checker-sdk-go v1.7.0 - github.com/ProtonMail/go-crypto v1.4.1 + git.happydns.org/checker-sdk-go v1.5.0 + github.com/ProtonMail/go-crypto v1.1.0-alpha.0 github.com/miekg/dns v1.1.72 ) require ( - github.com/cloudflare/circl v1.6.3 // indirect - golang.org/x/crypto v0.51.0 // indirect - golang.org/x/mod v0.36.0 // indirect - golang.org/x/net v0.54.0 // indirect - golang.org/x/sync v0.20.0 // indirect - golang.org/x/sys v0.44.0 // indirect - golang.org/x/tools v0.45.0 // indirect + github.com/cloudflare/circl v1.3.7 // indirect + golang.org/x/crypto v0.46.0 // indirect + golang.org/x/mod v0.31.0 // indirect + golang.org/x/net v0.48.0 // indirect + golang.org/x/sync v0.19.0 // indirect + golang.org/x/sys v0.39.0 // indirect + golang.org/x/tools v0.40.0 // indirect ) diff --git a/go.sum b/go.sum index 33cca37..ee33948 100644 --- a/go.sum +++ b/go.sum @@ -1,22 +1,22 @@ -git.happydns.org/checker-sdk-go v1.7.0 h1:dSgo2js5mfXluLc6x0WWZ0MQULd9XV2GI9z0Usk+Qgw= -git.happydns.org/checker-sdk-go v1.7.0/go.mod h1:aNAcfYFfbhvH9kJhE0Njp5GX0dQbxdRB0rJ0KvSC5nI= -github.com/ProtonMail/go-crypto v1.4.1 h1:9RfcZHqEQUvP8RzecWEUafnZVtEvrBVL9BiF67IQOfM= -github.com/ProtonMail/go-crypto v1.4.1/go.mod h1:e1OaTyu5SYVrO9gKOEhTc+5UcXtTUa+P3uLudwcgPqo= -github.com/cloudflare/circl v1.6.3 h1:9GPOhQGF9MCYUeXyMYlqTR6a5gTrgR/fBLXvUgtVcg8= -github.com/cloudflare/circl v1.6.3/go.mod h1:2eXP6Qfat4O/Yhh8BznvKnJ+uzEoTQ6jVKJRn81BiS4= +git.happydns.org/checker-sdk-go v1.5.0 h1:5uD5Cm6xJ+lwnhbJ09iCXGHbYS9zRh+Yh0NeBHkAPBY= +git.happydns.org/checker-sdk-go v1.5.0/go.mod h1:aNAcfYFfbhvH9kJhE0Njp5GX0dQbxdRB0rJ0KvSC5nI= +github.com/ProtonMail/go-crypto v1.1.0-alpha.0 h1:nHGfwXmFvJrSR9xu8qL7BkO4DqTHXE9N5vPhgY2I+j0= +github.com/ProtonMail/go-crypto v1.1.0-alpha.0/go.mod h1:rA3QumHc/FZ8pAHreoekgiAbzpNsfQAosU5td4SnOrE= +github.com/cloudflare/circl v1.3.7 h1:qlCDlTPz2n9fu58M0Nh1J/JzcFpfgkFHHX3O35r5vcU= +github.com/cloudflare/circl v1.3.7/go.mod h1:sRTcRWXGLrKw6yIGJ+l7amYJFfAXbZG0kBSc8r4zxgA= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/miekg/dns v1.1.72 h1:vhmr+TF2A3tuoGNkLDFK9zi36F2LS+hKTRW0Uf8kbzI= github.com/miekg/dns v1.1.72/go.mod h1:+EuEPhdHOsfk6Wk5TT2CzssZdqkmFhf8r+aVyDEToIs= -golang.org/x/crypto v0.51.0 h1:IBPXwPfKxY7cWQZ38ZCIRPI50YLeevDLlLnyC5wRGTI= -golang.org/x/crypto v0.51.0/go.mod h1:8AdwkbraGNABw2kOX6YFPs3WM22XqI4EXEd8g+x7Oc8= -golang.org/x/mod v0.36.0 h1:JJjpVx6myfUsUdAzZuOSTTmRE0PfZeNWzzvKrP7amb4= -golang.org/x/mod v0.36.0/go.mod h1:moc6ELqsWcOw5Ef3xVprK5ul/MvtVvkIXLziUOICjUQ= -golang.org/x/net v0.54.0 h1:2zJIZAxAHV/OHCDTCOHAYehQzLfSXuf/5SoL/Dv6w/w= -golang.org/x/net v0.54.0/go.mod h1:Sj4oj8jK6XmHpBZU/zWHw3BV3abl4Kvi+Ut7cQcY+cQ= -golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4= -golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= -golang.org/x/sys v0.44.0 h1:ildZl3J4uzeKP07r2F++Op7E9B29JRUy+a27EibtBTQ= -golang.org/x/sys v0.44.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= -golang.org/x/tools v0.45.0 h1:18qN3FAooORvApf5XjCXgsuayZOEtXf6JK18I3+ONa8= -golang.org/x/tools v0.45.0/go.mod h1:LuUGqqaXcXMEFEruIVJVm5mgDD8vww/z/SR1gQ4uE/0= +golang.org/x/crypto v0.46.0 h1:cKRW/pmt1pKAfetfu+RCEvjvZkA9RimPbh7bhFjGVBU= +golang.org/x/crypto v0.46.0/go.mod h1:Evb/oLKmMraqjZ2iQTwDwvCtJkczlDuTmdJXoZVzqU0= +golang.org/x/mod v0.31.0 h1:HaW9xtz0+kOcWKwli0ZXy79Ix+UW/vOfmWI5QVd2tgI= +golang.org/x/mod v0.31.0/go.mod h1:43JraMp9cGx1Rx3AqioxrbrhNsLl2l/iNAvuBkrezpg= +golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU= +golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY= +golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= +golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= +golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk= +golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/tools v0.40.0 h1:yLkxfA+Qnul4cs9QA3KnlFu0lVmd8JJfoq+E41uSutA= +golang.org/x/tools v0.40.0/go.mod h1:Ik/tzLRlbscWpqqMRjyWYDisX8bG13FrdXp3o4Sr9lc=