300 lines
11 KiB
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,
|
|
},
|
|
}
|