package checker import ( "fmt" "strings" "time" sdk "git.happydns.org/checker-sdk-go/checker" ) // SMIMEA-specific rules: field-value validity (usage/selector/matching // type), certificate parse, validity window, extended key usage, key // usage flags, signature-algorithm and key-size strength, self-signed // handling, email SAN/username pairing, and digest-only guidance. func checkSMIMEABadUsage(d *EmailKeyData, _ sdk.CheckerOptions) []issue { if d.SMIMEA == nil || d.SMIMEA.Usage <= 3 { return nil } return []issue{{ Severity: sdk.StatusCrit, Message: fmt.Sprintf("Unknown SMIMEA usage %d (expected 0 PKIX-TA, 1 PKIX-EE, 2 DANE-TA, 3 DANE-EE).", d.SMIMEA.Usage), Hint: "Use usage 3 (DANE-EE) for self-hosted S/MIME certificates.", }} } func checkSMIMEABadSelector(d *EmailKeyData, _ sdk.CheckerOptions) []issue { if d.SMIMEA == nil || d.SMIMEA.Selector <= 1 { return nil } return []issue{{ Severity: sdk.StatusCrit, Message: fmt.Sprintf("Unknown SMIMEA selector %d (expected 0 Cert or 1 SPKI).", d.SMIMEA.Selector), Hint: "Use selector 0 to publish the full certificate.", }} } func checkSMIMEABadMatchType(d *EmailKeyData, _ sdk.CheckerOptions) []issue { if d.SMIMEA == nil || d.SMIMEA.MatchingType <= 2 { return nil } return []issue{{ Severity: sdk.StatusCrit, Message: fmt.Sprintf("Unknown SMIMEA matching type %d (expected 0 Full, 1 SHA-256, 2 SHA-512).", d.SMIMEA.MatchingType), Hint: "Use matching type 0 so the whole certificate is transported, or type 1 (SHA-256) for a digest.", }} } func checkSMIMEACertParseError(d *EmailKeyData, _ sdk.CheckerOptions) []issue { if d.SMIMEA == nil { return []issue{{ Severity: sdk.StatusCrit, Message: "Service body has no SMIMEA record.", Hint: "Attach a valid SMIMEA record to the service.", }} } if d.SMIMEA.ParseError == "" { return nil } return []issue{{ Severity: sdk.StatusCrit, Message: d.SMIMEA.ParseError, Hint: "Ensure the certificate is DER-encoded (not PEM) before hex-encoding it into SMIMEA RDATA.", }} } func checkSMIMEACertNotYetValid(d *EmailKeyData, _ sdk.CheckerOptions) []issue { ci := smimeaCert(d) if ci == nil || ci.NotBefore.IsZero() { return nil } if !time.Now().Before(ci.NotBefore) { return nil } return []issue{{ Severity: sdk.StatusCrit, Message: fmt.Sprintf("Certificate is not yet valid (NotBefore = %s).", ci.NotBefore.Format(time.RFC3339)), Hint: "Check the system clock on the CA/signer, or wait until the certificate's notBefore date.", }} } func checkSMIMEACertExpired(d *EmailKeyData, _ sdk.CheckerOptions) []issue { ci := smimeaCert(d) if ci == nil || ci.NotAfter.IsZero() { return nil } if !time.Now().After(ci.NotAfter) { return nil } return []issue{{ Severity: sdk.StatusCrit, Message: fmt.Sprintf("Certificate expired on %s.", ci.NotAfter.Format(time.RFC3339)), Hint: "Issue a fresh certificate and republish the SMIMEA record.", }} } func checkSMIMEACertExpiring(d *EmailKeyData, opts sdk.CheckerOptions) []issue { ci := smimeaCert(d) if ci == nil || ci.NotAfter.IsZero() { return nil } warnDays := sdk.GetIntOption(opts, OptionCertExpiryWarnDays, 30) if warnDays <= 0 { return nil } now := time.Now() window := time.Duration(warnDays) * 24 * time.Hour if ci.NotAfter.Before(now) || ci.NotAfter.Sub(now) >= window { return nil } return []issue{{ Severity: sdk.StatusWarn, Message: fmt.Sprintf("Certificate expires on %s.", ci.NotAfter.Format(time.RFC3339)), Hint: "Renew before expiry and update the SMIMEA record with the new certificate.", }} } func checkSMIMEANoEmailProtect(d *EmailKeyData, opts sdk.CheckerOptions) []issue { ci := smimeaCert(d) if ci == nil || ci.HasEmailProtectionEKU { return nil } sev := sdk.StatusWarn if sdk.GetBoolOption(opts, OptionRequireEmailProtection, true) { sev = sdk.StatusCrit } return []issue{{ Severity: sev, Message: "Certificate lacks the emailProtection Extended Key Usage; RFC 8550/8551 agents will refuse it.", Hint: "Re-issue the certificate with `extendedKeyUsage = emailProtection` (OID 1.3.6.1.5.5.7.3.4).", }} } func checkSMIMEAMissingKeyUsage(d *EmailKeyData, _ sdk.CheckerOptions) []issue { ci := smimeaCert(d) if ci == nil || ci.HasDigitalSignature || ci.HasKeyEncipherment { return nil } return []issue{{ Severity: sdk.StatusWarn, Message: "Certificate has neither digitalSignature nor keyEncipherment key usage; S/MIME signing or encryption will be refused.", Hint: "Add `keyUsage = digitalSignature, keyEncipherment` to the certificate profile.", }} } var weakSMIMEASignatureAlgorithms = map[string]bool{ "MD2-RSA": true, "MD5-RSA": true, "SHA1-RSA": true, "DSA-SHA1": true, "ECDSA-SHA1": true, } func checkSMIMEAWeakSigAlgorithm(d *EmailKeyData, _ sdk.CheckerOptions) []issue { ci := smimeaCert(d) if ci == nil || ci.SignatureAlgorithm == "" { return nil } if !weakSMIMEASignatureAlgorithms[ci.SignatureAlgorithm] { return nil } return []issue{{ Severity: sdk.StatusCrit, Message: fmt.Sprintf("Certificate is signed with %s, a deprecated algorithm.", ci.SignatureAlgorithm), Hint: "Re-issue the certificate with SHA-256 (or better) signatures.", }} } func checkSMIMEAWeakKeySize(d *EmailKeyData, _ sdk.CheckerOptions) []issue { if d.SMIMEA == nil { return nil } algo, bits := "", 0 switch { case d.SMIMEA.Certificate != nil: algo, bits = d.SMIMEA.Certificate.PublicKeyAlgorithm, d.SMIMEA.Certificate.PublicKeyBits case d.SMIMEA.PublicKey != nil: algo, bits = d.SMIMEA.PublicKey.Algorithm, d.SMIMEA.PublicKey.Bits default: return nil } if iss := rsaKeySizeIssue(algo, bits, "Certificate"); iss != nil { return []issue{*iss} } return nil } func checkSMIMEASelfSigned(d *EmailKeyData, _ sdk.CheckerOptions) []issue { ci := smimeaCert(d) if ci == nil || !ci.IsSelfSigned { return nil } if d.SMIMEA.Usage != 1 && d.SMIMEA.Usage != 3 { return nil } return []issue{{ Severity: sdk.StatusInfo, Message: "End-entity usage advertises a self-signed certificate; DANE-EE (usage 3) makes this safe, but PKIX-EE (usage 1) consumers will reject it.", Hint: "Switch the record to usage 3 (DANE-EE) if you operate your own CA, or chain the certificate under a public CA for usage 1.", }} } func checkSMIMEAEmailMismatch(d *EmailKeyData, _ sdk.CheckerOptions) []issue { ci := smimeaCert(d) if ci == nil || ci.EmailMatchesUsername == nil || *ci.EmailMatchesUsername { return nil } return []issue{{ Severity: sdk.StatusInfo, Message: fmt.Sprintf("None of the certificate's email SANs (%s) begin with %s@; clients that strictly match SAN to envelope address will reject it.", strings.Join(ci.EmailAddresses, ", "), d.Username), Hint: "Re-issue the certificate with the correct `subjectAltName = email:@`.", }} } func checkSMIMEAHashOnly(d *EmailKeyData, _ sdk.CheckerOptions) []issue { if d.SMIMEA == nil || d.SMIMEA.MatchingType == 0 { return nil } return []issue{{ Severity: sdk.StatusInfo, Message: "Record carries only a digest; the certificate itself cannot be verified by this checker.", Hint: "Switch to matching type 0 (Full) to let verifiers inspect and pin the certificate.", }} } func smimeaCert(d *EmailKeyData) *CertInfo { if d.SMIMEA == nil { return nil } return d.SMIMEA.Certificate }