231 lines
7.2 KiB
Go
231 lines
7.2 KiB
Go
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:<user>@<domain>`.",
|
|
}}
|
|
}
|
|
|
|
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
|
|
}
|