144 lines
5.8 KiB
Go
144 lines
5.8 KiB
Go
// Package checker implements a TLS checker for happyDomain. See README for
|
||
// the payload shape and consumer contract.
|
||
package checker
|
||
|
||
import "time"
|
||
|
||
// ObservationKeyTLSProbes is the observation key this checker writes.
|
||
const ObservationKeyTLSProbes = "tls_probes"
|
||
|
||
// Option ids on CheckerOptions.
|
||
const (
|
||
OptionEndpoints = "endpoints"
|
||
OptionProbeTimeoutMs = "probeTimeoutMs"
|
||
)
|
||
|
||
// Defaults shared between the definition's Default field and the runtime
|
||
// fallback when probeTimeoutMs is unset or invalid.
|
||
const (
|
||
DefaultProbeTimeoutMs = 10000
|
||
// MaxConcurrentProbes caps parallel probes per collect run to avoid
|
||
// exhausting file descriptors on domains with many endpoints.
|
||
MaxConcurrentProbes = 32
|
||
)
|
||
|
||
// TLSData is the full collected payload written under ObservationKeyTLSProbes.
|
||
type TLSData struct {
|
||
Probes map[string]TLSProbe `json:"probes"`
|
||
CollectedAt time.Time `json:"collected_at"`
|
||
}
|
||
|
||
// TLSProbe captures the outcome of probing a single endpoint.
|
||
//
|
||
// Only raw observation fields live here. Judgement (severity, pass/fail,
|
||
// human-facing messages) is derived from these fields by CheckRules.
|
||
type TLSProbe struct {
|
||
Host string `json:"host"`
|
||
Port uint16 `json:"port"`
|
||
Endpoint string `json:"endpoint"`
|
||
Type string `json:"type"`
|
||
SNI string `json:"sni,omitempty"`
|
||
|
||
// RequireSTARTTLS is copied from the discovery entry so rules can tell
|
||
// whether a missing STARTTLS advertisement is a hard or soft failure.
|
||
RequireSTARTTLS bool `json:"require_starttls,omitempty"`
|
||
|
||
// STARTTLSDialect mirrors contract.TLSEndpoint.STARTTLS verbatim. An
|
||
// empty value means direct TLS.
|
||
STARTTLSDialect string `json:"starttls_dialect,omitempty"`
|
||
|
||
// Raw error strings. Exactly one of TCPError or HandshakeError is set
|
||
// when the probe failed before gathering handshake data.
|
||
TCPError string `json:"tcp_error,omitempty"`
|
||
HandshakeError string `json:"handshake_error,omitempty"`
|
||
|
||
// STARTTLSNotOffered is true when HandshakeError was produced because
|
||
// the server did not advertise STARTTLS (errStartTLSNotOffered).
|
||
STARTTLSNotOffered bool `json:"starttls_not_offered,omitempty"`
|
||
|
||
// STARTTLSUnsupportedProto is true when the STARTTLS dialect is not
|
||
// implemented by this checker.
|
||
STARTTLSUnsupportedProto bool `json:"starttls_unsupported_proto,omitempty"`
|
||
|
||
// TLSHandshakeOK is true when a TLS handshake completed. It is
|
||
// independent from chain validity.
|
||
TLSHandshakeOK bool `json:"tls_handshake_ok,omitempty"`
|
||
|
||
// TLSVersionNum is the numeric TLS version negotiated (uint16 from
|
||
// crypto/tls). Zero means no handshake occurred. Kept as an unsigned
|
||
// integer so rules can compare against tls.VersionTLS12 without
|
||
// re-parsing a string.
|
||
TLSVersionNum uint16 `json:"tls_version_num,omitempty"`
|
||
|
||
TLSVersion string `json:"tls_version,omitempty"`
|
||
CipherSuite string `json:"cipher_suite,omitempty"`
|
||
CipherSuiteID uint16 `json:"cipher_suite_id,omitempty"`
|
||
|
||
// NoPeerCert is true when the handshake succeeded but the server sent
|
||
// no certificate.
|
||
NoPeerCert bool `json:"no_peer_cert,omitempty"`
|
||
|
||
HostnameMatch *bool `json:"hostname_match,omitempty"`
|
||
ChainValid *bool `json:"chain_valid,omitempty"`
|
||
ChainVerifyErr string `json:"chain_verify_err,omitempty"`
|
||
NotAfter time.Time `json:"not_after,omitempty"`
|
||
Issuer string `json:"issuer,omitempty"`
|
||
// IssuerDN is the leaf's issuer as an RFC 2253 DN string, suitable for
|
||
// matching the CCADB CAA Identifiers CSV "Subject" column when the AKI
|
||
// lookup misses.
|
||
IssuerDN string `json:"issuer_dn,omitempty"`
|
||
// IssuerAKI is the uppercase hex of the leaf's Authority Key Identifier
|
||
// extension (i.e. the issuer cert's SKI). This is the primary lookup key
|
||
// into the CCADB CAA Identifiers CSV ("Subject Key Identifier (Hex)").
|
||
IssuerAKI string `json:"issuer_aki,omitempty"`
|
||
Subject string `json:"subject,omitempty"`
|
||
DNSNames []string `json:"dns_names,omitempty"`
|
||
// Chain carries one entry per certificate presented by the server
|
||
// (leaf first, then intermediates in order). Each entry precomputes
|
||
// the four TLSA selector×matching_type hashes plus the raw DER so
|
||
// DANE consumers can match without re-handshaking or re-parsing.
|
||
Chain []CertInfo `json:"chain,omitempty"`
|
||
ElapsedMS int64 `json:"elapsed_ms,omitempty"`
|
||
|
||
// Error is a compatibility summary of whichever raw error applies.
|
||
// Left for any external consumer still inspecting it; rules should
|
||
// look at TCPError / HandshakeError instead.
|
||
Error string `json:"error,omitempty"`
|
||
}
|
||
|
||
// CertInfo describes one certificate in the presented chain together with
|
||
// pre-hashed forms suitable for DANE/TLSA matching (RFC 6698 §2.1).
|
||
//
|
||
// Hex fields are lowercase, matching the representation emitted by
|
||
// miekg/dns for TLSA RR Certificate fields.
|
||
type CertInfo struct {
|
||
// DERBase64 is the standard base64 encoding of the certificate's DER
|
||
// form. Carried so consumers can do matching-type 0 (Full) without
|
||
// requiring a precomputed "full" hash and for fallback inspection.
|
||
DERBase64 string `json:"der_base64,omitempty"`
|
||
|
||
// Subject / Issuer are short human-readable DNs for the HTML report.
|
||
Subject string `json:"subject,omitempty"`
|
||
Issuer string `json:"issuer,omitempty"`
|
||
|
||
// NotAfter is the certificate's expiry. Carried so editors can show
|
||
// "expires on …" without re-parsing the DER.
|
||
NotAfter time.Time `json:"not_after,omitempty"`
|
||
|
||
// Selector 0 = full certificate.
|
||
CertSHA256 string `json:"cert_sha256,omitempty"`
|
||
CertSHA512 string `json:"cert_sha512,omitempty"`
|
||
|
||
// Selector 1 = SubjectPublicKeyInfo.
|
||
SPKISHA256 string `json:"spki_sha256,omitempty"`
|
||
SPKISHA512 string `json:"spki_sha512,omitempty"`
|
||
|
||
// SPKIDERBase64 lets consumers handle (selector=1, matching=0) without
|
||
// re-parsing the certificate.
|
||
SPKIDERBase64 string `json:"spki_der_base64,omitempty"`
|
||
}
|
||
|
||
// Expiry thresholds shared by rules.
|
||
const (
|
||
ExpiringSoonThreshold = 14 * 24 * time.Hour
|
||
)
|