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 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 .", 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 @.", 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, }, }