Initial commit
This commit is contained in:
commit
6424f920dd
25 changed files with 3737 additions and 0 deletions
231
checker/rules_smimea.go
Normal file
231
checker/rules_smimea.go
Normal file
|
|
@ -0,0 +1,231 @@
|
|||
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
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue