Initial commit

This commit is contained in:
nemunaire 2026-04-23 18:30:01 +07:00
commit 1ade94ce28
16 changed files with 118 additions and 170 deletions

4
.gitignore vendored
View file

@ -1,2 +1,2 @@
checker-openpgpkey
checker-openpgpkey.so
checker-email-keys
checker-email-keys.so

View file

@ -6,9 +6,9 @@ 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 .
RUN CGO_ENABLED=0 go build -ldflags "-X main.Version=${CHECKER_VERSION}" -o /checker-email-keys .
FROM scratch
COPY --from=builder /checker-openpgpkey /checker-openpgpkey
COPY --from=builder /checker-email-keys /checker-email-keys
EXPOSE 8080
ENTRYPOINT ["/checker-openpgpkey"]
ENTRYPOINT ["/checker-email-keys"]

View file

@ -1,4 +1,4 @@
CHECKER_NAME := checker-openpgpkey
CHECKER_NAME := checker-email-keys
CHECKER_IMAGE := happydomain/$(CHECKER_NAME)
CHECKER_VERSION ?= custom-build

2
NOTICE
View file

@ -1,4 +1,4 @@
checker-openpgpkey
checker-email-keys
Copyright (c) 2026 The happyDomain Authors
This product is licensed under the MIT License (see LICENSE).

View file

@ -1,4 +1,4 @@
# checker-openpgpkey
# checker-email-keys
DANE-Email posture checker for happyDomain.
@ -107,7 +107,7 @@ Auto-filled by the host: `domain_name`, `subdomain`, `service`,
make plugin
# Standalone HTTP server
make && ./checker-openpgpkey -listen :8080
make && ./checker-email-keys -listen :8080
```
## HTML report

View file

@ -1,9 +1,3 @@
// This file is part of the happyDomain (R) project.
// Copyright (c) 2026 happyDomain
// Authors: Pierre-Olivier Mercier, et al.
//
// Licensed under the MIT License (see LICENSE).
package checker
import (
@ -32,9 +26,9 @@ import (
// serviceBody is the common envelope for the two services.
type serviceBody struct {
Username string `json:"username,omitempty"`
OpenPGP *dns.OPENPGPKEY `json:"openpgpkey,omitempty"`
SMIMEA *dns.SMIMEA `json:"smimea,omitempty"`
Username string `json:"username,omitempty"`
OpenPGP *dns.OPENPGPKEY `json:"openpgpkey,omitempty"`
SMIMEA *dns.SMIMEA `json:"smimea,omitempty"`
}
// Collect runs the full DANE-email testsuite and returns an *EmailKeyData
@ -123,13 +117,9 @@ func (p *emailKeyProvider) Collect(ctx context.Context, opts sdk.CheckerOptions)
data.RecordCount = len(ans.Records)
if ans.Rcode == dns.RcodeNameError || len(ans.Records) == 0 {
sev := SeverityCrit
if ans.Rcode == dns.RcodeNameError {
sev = SeverityCrit
}
data.Findings = append(data.Findings, Finding{
Code: CodeDNSNoRecord,
Severity: sev,
Severity: SeverityCrit,
Message: fmt.Sprintf("Authoritative DNS returned no %s record at %s.", dns.TypeToString[qtype], data.QueriedOwner),
Fix: "Ensure the record is present in the zone and that the zone has been loaded by the authoritative servers.",
})

View file

@ -1,9 +1,3 @@
// This file is part of the happyDomain (R) project.
// Copyright (c) 2026 happyDomain
// Authors: Pierre-Olivier Mercier, et al.
//
// Licensed under the MIT License (see LICENSE).
package checker
import (
@ -18,9 +12,9 @@ var Version = "built-in"
// Option ids.
const (
OptionResolver = "resolver"
OptionCertExpiryWarnDays = "certExpiryWarnDays"
OptionRequireDNSSEC = "requireDNSSEC"
OptionResolver = "resolver"
OptionCertExpiryWarnDays = "certExpiryWarnDays"
OptionRequireDNSSEC = "requireDNSSEC"
OptionRequireEmailProtection = "requireEmailProtection"
)

View file

@ -1,9 +1,3 @@
// This file is part of the happyDomain (R) project.
// Copyright (c) 2026 happyDomain
// Authors: Pierre-Olivier Mercier, et al.
//
// Licensed under the MIT License (see LICENSE).
package checker
import (

View file

@ -1,9 +1,3 @@
// This file is part of the happyDomain (R) project.
// Copyright (c) 2026 happyDomain
// Authors: Pierre-Olivier Mercier, et al.
//
// Licensed under the MIT License (see LICENSE).
package checker
import (

View file

@ -1,9 +1,3 @@
// This file is part of the happyDomain (R) project.
// Copyright (c) 2026 happyDomain
// Authors: Pierre-Olivier Mercier, et al.
//
// Licensed under the MIT License (see LICENSE).
package checker
import (

View file

@ -1,9 +1,3 @@
// This file is part of the happyDomain (R) project.
// Copyright (c) 2026 happyDomain
// Authors: Pierre-Olivier Mercier, et al.
//
// Licensed under the MIT License (see LICENSE).
package checker
import (
@ -47,16 +41,16 @@ type subkeyRow struct {
// reportData is the template context.
type reportData struct {
Kind string
Headline string
Badge string // "ok" / "warn" / "fail" / "neutral"
QueriedOwner string
Kind string
Headline string
Badge string // "ok" / "warn" / "fail" / "neutral"
QueriedOwner string
ExpectedOwner string
Resolver string
DNSSEC string // "secure" / "insecure" / "unknown"
RecordCount int
Username string
CollectedAt string
Resolver string
DNSSEC string // "secure" / "insecure" / "unknown"
RecordCount int
Username string
CollectedAt string
OpenPGP *openPGPView
SMIMEA *smimeaView
@ -69,41 +63,41 @@ type reportData struct {
}
type openPGPView struct {
Fingerprint string
KeyID string
Algorithm string
Bits int
UIDs []string
Created string
Expires string
Revoked bool
Encrypt bool
Subkeys []subkeyRow
RawSize int
EntityCount int
Fingerprint string
KeyID string
Algorithm string
Bits int
UIDs []string
Created string
Expires string
Revoked bool
Encrypt bool
Subkeys []subkeyRow
RawSize int
EntityCount int
}
type smimeaView struct {
Usage string
Selector string
MatchingType string
HashOnly bool
HashHex string
Subject string
Issuer string
Serial string
NotBefore string
NotAfter string
SignatureAlgo string
KeyAlgo string
Bits int
Emails []string
DNSNames []string
EmailProtection bool
Usage string
Selector string
MatchingType string
HashOnly bool
HashHex string
Subject string
Issuer string
Serial string
NotBefore string
NotAfter string
SignatureAlgo string
KeyAlgo string
Bits int
Emails []string
DNSNames []string
EmailProtection bool
DigitalSignature bool
KeyEncipherment bool
SelfSigned bool
IsCA bool
KeyEncipherment bool
SelfSigned bool
IsCA bool
}
// GetHTMLReport implements sdk.CheckerHTMLReporter.

View file

@ -1,9 +1,3 @@
// This file is part of the happyDomain (R) project.
// Copyright (c) 2026 happyDomain
// Authors: Pierre-Olivier Mercier, et al.
//
// Licensed under the MIT License (see LICENSE).
package checker
import (

View file

@ -1,9 +1,3 @@
// This file is part of the happyDomain (R) project.
// Copyright (c) 2026 happyDomain
// Authors: Pierre-Olivier Mercier, et al.
//
// Licensed under the MIT License (see LICENSE).
// Package checker implements the OPENPGPKEY/SMIMEA DANE checker for
// happyDomain. It runs a comprehensive testsuite on the DNS-published
// OpenPGP key (RFC 7929) or S/MIME certificate (RFC 8162) corresponding
@ -44,42 +38,42 @@ const (
// UI keys remediation templates off them.
const (
// DNS-level.
CodeDNSQueryFailed = "dns_query_failed"
CodeDNSNoRecord = "dns_no_record"
CodeDNSQueryFailed = "dns_query_failed"
CodeDNSNoRecord = "dns_no_record"
CodeDNSRecordMismatch = "dns_record_mismatch"
CodeDNSNotSecure = "dnssec_not_validated"
CodeDNSNotSecure = "dnssec_not_validated"
CodeOwnerHashMismatch = "owner_hash_mismatch"
// OpenPGP.
CodePGPParseError = "pgp_parse_error"
CodePGPNoEntity = "pgp_no_entity"
CodePGPRevoked = "pgp_primary_revoked"
CodePGPExpired = "pgp_primary_expired"
CodePGPExpiringSoon = "pgp_primary_expiring_soon"
CodePGPWeakAlgorithm = "pgp_weak_algorithm"
CodePGPWeakKeySize = "pgp_weak_key_size"
CodePGPNoEncryption = "pgp_no_encryption_subkey"
CodePGPNoIdentity = "pgp_no_identity"
CodePGPUIDMismatch = "pgp_uid_mismatch"
CodePGPParseError = "pgp_parse_error"
CodePGPNoEntity = "pgp_no_entity"
CodePGPRevoked = "pgp_primary_revoked"
CodePGPExpired = "pgp_primary_expired"
CodePGPExpiringSoon = "pgp_primary_expiring_soon"
CodePGPWeakAlgorithm = "pgp_weak_algorithm"
CodePGPWeakKeySize = "pgp_weak_key_size"
CodePGPNoEncryption = "pgp_no_encryption_subkey"
CodePGPNoIdentity = "pgp_no_identity"
CodePGPUIDMismatch = "pgp_uid_mismatch"
CodePGPMultipleEntities = "pgp_multiple_entities"
CodePGPRecordTooLarge = "pgp_record_too_large"
CodePGPRecordTooLarge = "pgp_record_too_large"
// SMIMEA.
CodeSMIMEABadUsage = "smimea_bad_usage"
CodeSMIMEABadSelector = "smimea_bad_selector"
CodeSMIMEABadMatchType = "smimea_bad_match_type"
CodeSMIMEACertParseError = "smimea_cert_parse_error"
CodeSMIMEACertExpired = "smimea_cert_expired"
CodeSMIMEACertExpiringSoon = "smimea_cert_expiring_soon"
CodeSMIMEACertNotYetValid = "smimea_cert_not_yet_valid"
CodeSMIMEABadUsage = "smimea_bad_usage"
CodeSMIMEABadSelector = "smimea_bad_selector"
CodeSMIMEABadMatchType = "smimea_bad_match_type"
CodeSMIMEACertParseError = "smimea_cert_parse_error"
CodeSMIMEACertExpired = "smimea_cert_expired"
CodeSMIMEACertExpiringSoon = "smimea_cert_expiring_soon"
CodeSMIMEACertNotYetValid = "smimea_cert_not_yet_valid"
CodeSMIMEANoEmailProtection = "smimea_no_email_protection_eku"
CodeSMIMEAEmailMismatch = "smimea_email_mismatch"
CodeSMIMEAWeakKeySize = "smimea_weak_key_size"
CodeSMIMEAWeakSignatureAlg = "smimea_weak_signature_algorithm"
CodeSMIMEANoKeyUsage = "smimea_missing_key_usage"
CodeSMIMEAChainUntrusted = "smimea_chain_untrusted"
CodeSMIMEASelfSigned = "smimea_self_signed"
CodeSMIMEAHashOnly = "smimea_hash_only"
CodeSMIMEAEmailMismatch = "smimea_email_mismatch"
CodeSMIMEAWeakKeySize = "smimea_weak_key_size"
CodeSMIMEAWeakSignatureAlg = "smimea_weak_signature_algorithm"
CodeSMIMEANoKeyUsage = "smimea_missing_key_usage"
CodeSMIMEAChainUntrusted = "smimea_chain_untrusted"
CodeSMIMEASelfSigned = "smimea_self_signed"
CodeSMIMEAHashOnly = "smimea_hash_only"
)
// Finding describes a single observation produced while running the
@ -184,14 +178,14 @@ type OpenPGPInfo struct {
// SubkeyInfo summarises one OpenPGP subkey.
type SubkeyInfo struct {
Algorithm string `json:"algorithm"`
Bits int `json:"bits,omitempty"`
CanSign bool `json:"can_sign,omitempty"`
CanEncrypt bool `json:"can_encrypt,omitempty"`
CanAuth bool `json:"can_auth,omitempty"`
CreatedAt time.Time `json:"created_at,omitempty"`
ExpiresAt time.Time `json:"expires_at,omitempty"`
Revoked bool `json:"revoked,omitempty"`
Algorithm string `json:"algorithm"`
Bits int `json:"bits,omitempty"`
CanSign bool `json:"can_sign,omitempty"`
CanEncrypt bool `json:"can_encrypt,omitempty"`
CanAuth bool `json:"can_auth,omitempty"`
CreatedAt time.Time `json:"created_at,omitempty"`
ExpiresAt time.Time `json:"expires_at,omitempty"`
Revoked bool `json:"revoked,omitempty"`
}
// SMIMEAInfo summarises the S/MIME record.
@ -204,7 +198,7 @@ type SMIMEAInfo struct {
// certificate (selector 0, matching type 0). For selector 1 + type 0
// only PublicKey is populated. For matching types 1/2, neither is
// populated; only the digest is transported.
Certificate *CertInfo `json:"certificate,omitempty"`
Certificate *CertInfo `json:"certificate,omitempty"`
PublicKey *PubKeyInfo `json:"public_key,omitempty"`
// HashHex, when set, is the hex digest embedded in the record.
@ -213,21 +207,21 @@ type SMIMEAInfo struct {
// CertInfo summarises an X.509 certificate.
type CertInfo struct {
Subject string `json:"subject,omitempty"`
Issuer string `json:"issuer,omitempty"`
SerialHex string `json:"serial_hex,omitempty"`
NotBefore time.Time `json:"not_before,omitempty"`
NotAfter time.Time `json:"not_after,omitempty"`
SignatureAlgorithm string `json:"signature_algorithm,omitempty"`
PublicKeyAlgorithm string `json:"public_key_algorithm,omitempty"`
PublicKeyBits int `json:"public_key_bits,omitempty"`
EmailAddresses []string `json:"email_addresses,omitempty"`
DNSNames []string `json:"dns_names,omitempty"`
HasEmailProtectionEKU bool `json:"has_email_protection_eku,omitempty"`
HasDigitalSignature bool `json:"has_digital_signature,omitempty"`
HasKeyEncipherment bool `json:"has_key_encipherment,omitempty"`
IsSelfSigned bool `json:"is_self_signed,omitempty"`
IsCA bool `json:"is_ca,omitempty"`
Subject string `json:"subject,omitempty"`
Issuer string `json:"issuer,omitempty"`
SerialHex string `json:"serial_hex,omitempty"`
NotBefore time.Time `json:"not_before,omitempty"`
NotAfter time.Time `json:"not_after,omitempty"`
SignatureAlgorithm string `json:"signature_algorithm,omitempty"`
PublicKeyAlgorithm string `json:"public_key_algorithm,omitempty"`
PublicKeyBits int `json:"public_key_bits,omitempty"`
EmailAddresses []string `json:"email_addresses,omitempty"`
DNSNames []string `json:"dns_names,omitempty"`
HasEmailProtectionEKU bool `json:"has_email_protection_eku,omitempty"`
HasDigitalSignature bool `json:"has_digital_signature,omitempty"`
HasKeyEncipherment bool `json:"has_key_encipherment,omitempty"`
IsSelfSigned bool `json:"is_self_signed,omitempty"`
IsCA bool `json:"is_ca,omitempty"`
}
// PubKeyInfo summarises an SPKI-only SMIMEA record.

2
go.mod
View file

@ -1,4 +1,4 @@
module git.happydns.org/checker-openpgpkey
module git.happydns.org/checker-email-keys
go 1.25.0

10
main.go
View file

@ -4,23 +4,23 @@ import (
"flag"
"log"
openpgpkey "git.happydns.org/checker-openpgpkey/checker"
emailkeys "git.happydns.org/checker-email-keys/checker"
sdk "git.happydns.org/checker-sdk-go/checker"
)
var listenAddr = flag.String("listen", ":8080", "HTTP listen address")
// Version is the standalone binary's version. Override with:
//
// go build -ldflags "-X main.Version=1.2.3" .
var Version = "custom-build"
var listenAddr = flag.String("listen", ":8080", "HTTP listen address")
func main() {
flag.Parse()
openpgpkey.Version = Version
emailkeys.Version = Version
server := sdk.NewServer(openpgpkey.Provider())
server := sdk.NewServer(emailkeys.Provider())
if err := server.ListenAndServe(*listenAddr); err != nil {
log.Fatalf("server error: %v", err)
}

View file

@ -4,18 +4,18 @@
package main
import (
openpgpkey "git.happydns.org/checker-openpgpkey/checker"
emailkeys "git.happydns.org/checker-email-keys/checker"
sdk "git.happydns.org/checker-sdk-go/checker"
)
// Version is the plugin's version, meant to be overridden by CI:
//
// go build -buildmode=plugin -ldflags "-X main.Version=1.2.3" -o checker-openpgpkey.so ./plugin
// go build -buildmode=plugin -ldflags "-X main.Version=1.2.3" -o checker-email-keys.so ./plugin
var Version = "custom-build"
// NewCheckerPlugin is the symbol resolved by happyDomain when loading
// the .so file.
func NewCheckerPlugin() (*sdk.CheckerDefinition, sdk.ObservationProvider, error) {
openpgpkey.Version = Version
return openpgpkey.Definition(), openpgpkey.Provider(), nil
emailkeys.Version = Version
return emailkeys.Definition(), emailkeys.Provider(), nil
}