// 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 )