233 lines
6.2 KiB
Go
233 lines
6.2 KiB
Go
package checker
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"strings"
|
|
"time"
|
|
|
|
sdk "git.happydns.org/checker-sdk-go/checker"
|
|
)
|
|
|
|
// peerCertificateRule flags successful handshakes in which the server sent
|
|
// no certificate. This is distinct from chain validity: if no cert was sent,
|
|
// hostname/chain/expiry cannot be evaluated.
|
|
type peerCertificateRule struct{}
|
|
|
|
func (r *peerCertificateRule) Name() string { return "tls.peer_certificate_present" }
|
|
func (r *peerCertificateRule) Description() string {
|
|
return "Verifies the server presented a certificate during the TLS handshake."
|
|
}
|
|
|
|
func (r *peerCertificateRule) Evaluate(ctx context.Context, obs sdk.ObservationGetter, _ sdk.CheckerOptions) []sdk.CheckState {
|
|
data, errSt := loadData(ctx, obs)
|
|
if errSt != nil {
|
|
return []sdk.CheckState{*errSt}
|
|
}
|
|
if len(data.Probes) == 0 {
|
|
return []sdk.CheckState{emptyCaseState("tls.peer_certificate_present.no_endpoints")}
|
|
}
|
|
|
|
var out []sdk.CheckState
|
|
anyHandshake := false
|
|
for _, ref := range sortedRefs(data) {
|
|
p := data.Probes[ref]
|
|
if !p.TLSHandshakeOK {
|
|
continue
|
|
}
|
|
anyHandshake = true
|
|
if !p.NoPeerCert {
|
|
continue
|
|
}
|
|
out = append(out, sdk.CheckState{
|
|
Status: sdk.StatusCrit,
|
|
Code: "tls.peer_certificate_present.missing",
|
|
Subject: subjectOf(p),
|
|
Message: fmt.Sprintf("Server on %s completed the handshake but presented no certificate.", p.Endpoint),
|
|
Meta: metaOf(p),
|
|
})
|
|
}
|
|
if !anyHandshake {
|
|
return []sdk.CheckState{unknownState(
|
|
"tls.peer_certificate_present.skipped",
|
|
"No endpoint completed a TLS handshake.",
|
|
)}
|
|
}
|
|
if len(out) == 0 {
|
|
return []sdk.CheckState{passState(
|
|
"tls.peer_certificate_present.ok",
|
|
"Every endpoint presented a certificate.",
|
|
)}
|
|
}
|
|
return out
|
|
}
|
|
|
|
// chainValidityRule flags invalid certificate chains.
|
|
type chainValidityRule struct{}
|
|
|
|
func (r *chainValidityRule) Name() string { return "tls.chain_validity" }
|
|
func (r *chainValidityRule) Description() string {
|
|
return "Verifies the presented certificate chain validates against the system trust store."
|
|
}
|
|
|
|
func (r *chainValidityRule) Evaluate(ctx context.Context, obs sdk.ObservationGetter, _ sdk.CheckerOptions) []sdk.CheckState {
|
|
data, errSt := loadData(ctx, obs)
|
|
if errSt != nil {
|
|
return []sdk.CheckState{*errSt}
|
|
}
|
|
if len(data.Probes) == 0 {
|
|
return []sdk.CheckState{emptyCaseState("tls.chain_validity.no_endpoints")}
|
|
}
|
|
|
|
var out []sdk.CheckState
|
|
any := false
|
|
for _, ref := range sortedRefs(data) {
|
|
p := data.Probes[ref]
|
|
if p.ChainValid == nil {
|
|
continue
|
|
}
|
|
any = true
|
|
if *p.ChainValid {
|
|
continue
|
|
}
|
|
msg := "Invalid certificate chain"
|
|
if p.ChainVerifyErr != "" {
|
|
msg = "Invalid certificate chain: " + p.ChainVerifyErr
|
|
}
|
|
out = append(out, sdk.CheckState{
|
|
Status: sdk.StatusCrit,
|
|
Code: "tls.chain_validity.invalid",
|
|
Subject: subjectOf(p),
|
|
Message: msg,
|
|
Meta: metaOf(p),
|
|
})
|
|
}
|
|
if !any {
|
|
return []sdk.CheckState{unknownState(
|
|
"tls.chain_validity.skipped",
|
|
"No endpoint yielded a certificate chain to verify.",
|
|
)}
|
|
}
|
|
if len(out) == 0 {
|
|
return []sdk.CheckState{passState(
|
|
"tls.chain_validity.ok",
|
|
"Every presented chain validates against the system trust store.",
|
|
)}
|
|
}
|
|
return out
|
|
}
|
|
|
|
// hostnameMatchRule flags endpoints whose leaf cert does not cover the SNI
|
|
// the probe used.
|
|
type hostnameMatchRule struct{}
|
|
|
|
func (r *hostnameMatchRule) Name() string { return "tls.hostname_match" }
|
|
func (r *hostnameMatchRule) Description() string {
|
|
return "Verifies the leaf certificate covers the probed hostname (SNI)."
|
|
}
|
|
|
|
func (r *hostnameMatchRule) Evaluate(ctx context.Context, obs sdk.ObservationGetter, _ sdk.CheckerOptions) []sdk.CheckState {
|
|
data, errSt := loadData(ctx, obs)
|
|
if errSt != nil {
|
|
return []sdk.CheckState{*errSt}
|
|
}
|
|
if len(data.Probes) == 0 {
|
|
return []sdk.CheckState{emptyCaseState("tls.hostname_match.no_endpoints")}
|
|
}
|
|
|
|
var out []sdk.CheckState
|
|
any := false
|
|
for _, ref := range sortedRefs(data) {
|
|
p := data.Probes[ref]
|
|
if p.HostnameMatch == nil {
|
|
continue
|
|
}
|
|
any = true
|
|
if *p.HostnameMatch {
|
|
continue
|
|
}
|
|
out = append(out, sdk.CheckState{
|
|
Status: sdk.StatusCrit,
|
|
Code: "tls.hostname_match.mismatch",
|
|
Subject: subjectOf(p),
|
|
Message: fmt.Sprintf("Certificate does not cover %q (SANs: %s)", p.SNI, strings.Join(p.DNSNames, ", ")),
|
|
Meta: metaOf(p),
|
|
})
|
|
}
|
|
if !any {
|
|
return []sdk.CheckState{unknownState(
|
|
"tls.hostname_match.skipped",
|
|
"No endpoint yielded a certificate to hostname-match.",
|
|
)}
|
|
}
|
|
if len(out) == 0 {
|
|
return []sdk.CheckState{passState(
|
|
"tls.hostname_match.ok",
|
|
"Every certificate covers its probed SNI.",
|
|
)}
|
|
}
|
|
return out
|
|
}
|
|
|
|
// expiryRule flags expired or near-expiry certificates.
|
|
type expiryRule struct{}
|
|
|
|
func (r *expiryRule) Name() string { return "tls.expiry" }
|
|
func (r *expiryRule) Description() string {
|
|
return "Flags expired or soon-to-expire leaf certificates."
|
|
}
|
|
|
|
func (r *expiryRule) Evaluate(ctx context.Context, obs sdk.ObservationGetter, _ sdk.CheckerOptions) []sdk.CheckState {
|
|
data, errSt := loadData(ctx, obs)
|
|
if errSt != nil {
|
|
return []sdk.CheckState{*errSt}
|
|
}
|
|
if len(data.Probes) == 0 {
|
|
return []sdk.CheckState{emptyCaseState("tls.expiry.no_endpoints")}
|
|
}
|
|
|
|
now := time.Now()
|
|
var out []sdk.CheckState
|
|
any := false
|
|
for _, ref := range sortedRefs(data) {
|
|
p := data.Probes[ref]
|
|
if p.NotAfter.IsZero() {
|
|
continue
|
|
}
|
|
any = true
|
|
meta := metaOf(p)
|
|
meta["not_after"] = p.NotAfter
|
|
if p.NotAfter.Before(now) {
|
|
out = append(out, sdk.CheckState{
|
|
Status: sdk.StatusCrit,
|
|
Code: "tls.expiry.expired",
|
|
Subject: subjectOf(p),
|
|
Message: "Certificate expired on " + p.NotAfter.Format(time.RFC3339),
|
|
Meta: meta,
|
|
})
|
|
continue
|
|
}
|
|
if p.NotAfter.Sub(now) < ExpiringSoonThreshold {
|
|
out = append(out, sdk.CheckState{
|
|
Status: sdk.StatusWarn,
|
|
Code: "tls.expiry.expiring_soon",
|
|
Subject: subjectOf(p),
|
|
Message: "Certificate expires in less than 14 days (" + p.NotAfter.Format(time.RFC3339) + ")",
|
|
Meta: meta,
|
|
})
|
|
}
|
|
}
|
|
if !any {
|
|
return []sdk.CheckState{unknownState(
|
|
"tls.expiry.skipped",
|
|
"No endpoint yielded a certificate with an expiry to check.",
|
|
)}
|
|
}
|
|
if len(out) == 0 {
|
|
return []sdk.CheckState{passState(
|
|
"tls.expiry.ok",
|
|
"Every leaf certificate is valid for more than 14 days.",
|
|
)}
|
|
}
|
|
return out
|
|
}
|