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 }