checker-email-keys/checker/rules_check.go

300 lines
11 KiB
Go

package checker
import (
sdk "git.happydns.org/checker-sdk-go/checker"
)
// Rule names. Each name is also the CheckState.Code emitted by the
// corresponding rule. They are kept as exported constants so callers
// (e.g. the report layer's remediation picker) can reference them
// without copying strings.
const (
RuleDNSQueryFailed = "dns_query_failed"
RuleDNSNoRecord = "dns_no_record"
RuleDNSRecordMismatch = "dns_record_mismatch"
RuleDNSSECNotValidated = "dnssec_not_validated"
RuleOwnerHashMismatch = "owner_hash_mismatch"
RulePGPParseError = "pgp_parse_error"
RulePGPPrimaryRevoked = "pgp_primary_revoked"
RulePGPPrimaryExpired = "pgp_primary_expired"
RulePGPPrimaryExpiring = "pgp_primary_expiring_soon"
RulePGPWeakAlgorithm = "pgp_weak_algorithm"
RulePGPWeakKeySize = "pgp_weak_key_size"
RulePGPNoEncryption = "pgp_no_encryption_subkey"
RulePGPNoIdentity = "pgp_no_identity"
RulePGPUIDMismatch = "pgp_uid_mismatch"
RulePGPMultipleEntities = "pgp_multiple_entities"
RulePGPRecordTooLarge = "pgp_record_too_large"
RuleSMIMEABadUsage = "smimea_bad_usage"
RuleSMIMEABadSelector = "smimea_bad_selector"
RuleSMIMEABadMatchType = "smimea_bad_match_type"
RuleSMIMEACertParseError = "smimea_cert_parse_error"
RuleSMIMEACertNotYetValid = "smimea_cert_not_yet_valid"
RuleSMIMEACertExpired = "smimea_cert_expired"
RuleSMIMEACertExpiring = "smimea_cert_expiring_soon"
RuleSMIMEANoEmailProtect = "smimea_no_email_protection_eku"
RuleSMIMEAMissingKeyUsage = "smimea_missing_key_usage"
RuleSMIMEAWeakSigAlgorithm = "smimea_weak_signature_algorithm"
RuleSMIMEAWeakKeySize = "smimea_weak_key_size"
RuleSMIMEASelfSigned = "smimea_self_signed"
RuleSMIMEAEmailMismatch = "smimea_email_mismatch"
RuleSMIMEAHashOnly = "smimea_hash_only"
)
var kindsOpenPGP = []string{KindOpenPGPKey}
var kindsSMIMEA = []string{KindSMIMEA}
// optExpiryWarn is the per-rule option documentation for
// OptionCertExpiryWarnDays. The same option id is shared by the PGP
// expiring-soon rule and the SMIMEA expiring-soon rule.
var optExpiryWarn = sdk.CheckerOptionsDocumentation{
UserOpts: []sdk.CheckerOptionDocumentation{{
Id: OptionCertExpiryWarnDays,
Type: "number",
Label: "Expiry warning threshold (days)",
Description: "Emit a warning when the primary key or S/MIME certificate expires in less than this many days.",
Default: float64(30),
}},
}
var optRequireDNSSEC = sdk.CheckerOptionsDocumentation{
UserOpts: []sdk.CheckerOptionDocumentation{{
Id: OptionRequireDNSSEC,
Type: "bool",
Label: "Require DNSSEC",
Description: "When enabled, a non-DNSSEC-validated lookup is reported as critical (otherwise as warning). RFC 7929 and RFC 8162 mandate DNSSEC.",
Default: true,
}},
}
var optRequireEmailProtection = sdk.CheckerOptionsDocumentation{
UserOpts: []sdk.CheckerOptionDocumentation{{
Id: OptionRequireEmailProtection,
Type: "bool",
Label: "Require emailProtection EKU",
Description: "When enabled, an S/MIME certificate without the emailProtection Extended Key Usage is reported as critical.",
Default: true,
}},
}
// allRules is the canonical list of rules this checker exposes. Each
// entry registers one CheckRule, implemented by the check<Name> funcs
// in rules_dns.go, rules_pgp.go, and rules_smimea.go.
var allRules = []*rule{
// ── DNS / owner (both kinds), rules_dns.go ──
{
name: RuleDNSQueryFailed,
description: "The DNS lookup for the OPENPGPKEY/SMIMEA record must succeed.",
okMessage: "DNS lookup succeeded.",
check: checkDNSQueryFailed,
},
{
name: RuleDNSNoRecord,
description: "An OPENPGPKEY/SMIMEA record must be published at the expected owner name.",
okMessage: "A record is published at the queried owner name.",
check: checkDNSNoRecord,
},
{
name: RuleDNSRecordMismatch,
description: "The record returned by DNS must match the service-declared record.",
okMessage: "DNS matches the service-declared record.",
check: checkDNSRecordMismatch,
},
{
name: RuleDNSSECNotValidated,
description: "The record must be authenticated by DNSSEC; RFC 7929 and RFC 8162 mandate it.",
okMessage: "DNSSEC validated the record (AD flag set).",
options: optRequireDNSSEC,
check: checkDNSSECNotValidated,
},
{
name: RuleOwnerHashMismatch,
description: "The first label of the owner name must equal hex(sha256(username))[:28].",
okMessage: "Owner-name hash matches the username.",
check: checkOwnerHashMismatch,
},
// ── OpenPGP (kind openpgpkey), rules_pgp.go ──
{
name: RulePGPParseError,
description: "The OPENPGPKEY record must decode as a valid OpenPGP key.",
okMessage: "OpenPGP key parsed successfully.",
kinds: kindsOpenPGP,
check: checkPGPParseError,
},
{
name: RulePGPPrimaryRevoked,
description: "The OpenPGP primary key must not carry a revocation signature.",
okMessage: "Primary key is not revoked.",
kinds: kindsOpenPGP,
check: checkPGPPrimaryRevoked,
},
{
name: RulePGPPrimaryExpired,
description: "The OpenPGP primary key must not be past its self-signature expiry.",
okMessage: "Primary key is not expired.",
kinds: kindsOpenPGP,
check: checkPGPPrimaryExpired,
},
{
name: RulePGPPrimaryExpiring,
description: "Warn when the OpenPGP primary key expires within the configured window.",
okMessage: "Primary key is not expiring soon.",
kinds: kindsOpenPGP,
options: optExpiryWarn,
check: checkPGPPrimaryExpiring,
},
{
name: RulePGPWeakAlgorithm,
description: "The OpenPGP keys must not use legacy algorithms (DSA/ElGamal).",
okMessage: "All OpenPGP keys use modern algorithms.",
kinds: kindsOpenPGP,
check: checkPGPWeakAlgorithm,
},
{
name: RulePGPWeakKeySize,
description: "OpenPGP RSA keys must be at least 2048 bits (NIST SP 800-131A); 3072+ preferred.",
okMessage: "All RSA OpenPGP keys meet the minimum key size.",
kinds: kindsOpenPGP,
check: checkPGPWeakKeySize,
},
{
name: RulePGPNoEncryption,
description: "At least one active (non-revoked, non-expired) OpenPGP key must advertise encryption capability.",
okMessage: "The entity has an active encryption-capable key.",
kinds: kindsOpenPGP,
check: checkPGPNoEncryption,
},
{
name: RulePGPNoIdentity,
description: "The OpenPGP key must carry at least one self-signed User ID.",
okMessage: "The OpenPGP key has at least one identity.",
kinds: kindsOpenPGP,
check: checkPGPNoIdentity,
},
{
name: RulePGPUIDMismatch,
description: "At least one OpenPGP UID should reference <username@…>.",
okMessage: "At least one UID matches the username.",
kinds: kindsOpenPGP,
check: checkPGPUIDMismatch,
},
{
name: RulePGPMultipleEntities,
description: "RFC 7929 recommends a single OpenPGP entity per record.",
okMessage: "The record carries a single OpenPGP entity.",
kinds: kindsOpenPGP,
check: checkPGPMultipleEntities,
},
{
name: RulePGPRecordTooLarge,
description: "The OPENPGPKEY record should stay below 4 KiB to fit typical UDP answers.",
okMessage: "Record size is within the recommended limit.",
kinds: kindsOpenPGP,
check: checkPGPRecordTooLarge,
},
// ── SMIMEA (kind smimea), rules_smimea.go ──
{
name: RuleSMIMEABadUsage,
description: "SMIMEA usage must be 0 (PKIX-TA), 1 (PKIX-EE), 2 (DANE-TA), or 3 (DANE-EE).",
okMessage: "SMIMEA usage is valid.",
kinds: kindsSMIMEA,
check: checkSMIMEABadUsage,
},
{
name: RuleSMIMEABadSelector,
description: "SMIMEA selector must be 0 (Cert) or 1 (SPKI).",
okMessage: "SMIMEA selector is valid.",
kinds: kindsSMIMEA,
check: checkSMIMEABadSelector,
},
{
name: RuleSMIMEABadMatchType,
description: "SMIMEA matching type must be 0 (Full), 1 (SHA-256), or 2 (SHA-512).",
okMessage: "SMIMEA matching type is valid.",
kinds: kindsSMIMEA,
check: checkSMIMEABadMatchType,
},
{
name: RuleSMIMEACertParseError,
description: "The SMIMEA record must decode as a valid X.509 certificate (or SPKI, for selector 1).",
okMessage: "Certificate parsed successfully.",
kinds: kindsSMIMEA,
check: checkSMIMEACertParseError,
},
{
name: RuleSMIMEACertNotYetValid,
description: "The S/MIME certificate's NotBefore must be in the past.",
okMessage: "Certificate is within its validity window.",
kinds: kindsSMIMEA,
check: checkSMIMEACertNotYetValid,
},
{
name: RuleSMIMEACertExpired,
description: "The S/MIME certificate's NotAfter must be in the future.",
okMessage: "Certificate is not expired.",
kinds: kindsSMIMEA,
check: checkSMIMEACertExpired,
},
{
name: RuleSMIMEACertExpiring,
description: "Warn when the S/MIME certificate expires within the configured window.",
okMessage: "Certificate is not expiring soon.",
kinds: kindsSMIMEA,
options: optExpiryWarn,
check: checkSMIMEACertExpiring,
},
{
name: RuleSMIMEANoEmailProtect,
description: "The S/MIME certificate must advertise the emailProtection Extended Key Usage (RFC 8550/8551).",
okMessage: "Certificate carries emailProtection EKU.",
kinds: kindsSMIMEA,
options: optRequireEmailProtection,
check: checkSMIMEANoEmailProtect,
},
{
name: RuleSMIMEAMissingKeyUsage,
description: "The S/MIME certificate must carry digitalSignature and/or keyEncipherment key usage.",
okMessage: "Certificate carries the expected key usages.",
kinds: kindsSMIMEA,
check: checkSMIMEAMissingKeyUsage,
},
{
name: RuleSMIMEAWeakSigAlgorithm,
description: "The certificate must not be signed with a deprecated algorithm (MD2/MD5/SHA-1 based).",
okMessage: "Certificate uses a strong signature algorithm.",
kinds: kindsSMIMEA,
check: checkSMIMEAWeakSigAlgorithm,
},
{
name: RuleSMIMEAWeakKeySize,
description: "SMIMEA RSA keys must be at least 2048 bits; 3072+ preferred.",
okMessage: "Certificate key size meets the minimum.",
kinds: kindsSMIMEA,
check: checkSMIMEAWeakKeySize,
},
{
name: RuleSMIMEASelfSigned,
description: "Self-signed certificates with PKIX-EE (usage 1) are rejected by standard clients.",
okMessage: "Certificate chain is appropriate for the declared usage.",
kinds: kindsSMIMEA,
check: checkSMIMEASelfSigned,
},
{
name: RuleSMIMEAEmailMismatch,
description: "At least one email SAN on the certificate should begin with <username>@.",
okMessage: "At least one email SAN matches the username.",
kinds: kindsSMIMEA,
check: checkSMIMEAEmailMismatch,
},
{
name: RuleSMIMEAHashOnly,
description: "SMIMEA matching types 1/2 transport only a digest; the certificate cannot be verified.",
okMessage: "Full certificate is published.",
kinds: kindsSMIMEA,
check: checkSMIMEAHashOnly,
},
}