checker-tls/checker/types.go
Pierre-Olivier Mercier a9f37c79cf Add tlsenum package and add version/cipher enumeration into the checker
tlsenum package probes a remote endpoint with one ClientHello
per (version, cipher) pair via utls, so the checker can report the
exact set the server accepts rather than only the suite Go's stdlib
happens to negotiate. Probe accepts an Upgrader callback so STARTTLS
dialects plug in without tlsenum learning about them; the checker
bridges its existing dialect registry through upgraderFor.
2026-04-29 13:35:29 +07:00

179 lines
7.3 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// 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"
OptionEnumerateCiphers = "enumerateCiphers"
)
// 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"`
// Enum carries the protocol-version and cipher-suite sweep. It is only
// populated when the user enables OptionEnumerateCiphers. Direct TLS and
// supported STARTTLS dialects are both swept; a STARTTLS endpoint with
// an unknown dialect is skipped with a reason recorded in Enum.Skipped.
Enum *TLSEnumeration `json:"enum,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
)
// TLSEnumeration is the result of sweeping a (version × cipher) matrix
// against an endpoint. The exact set the server accepts (rather than just the
// one combination it negotiated under default Go preferences) lets rules flag
// legacy versions and weak cipher suites that would otherwise stay invisible.
type TLSEnumeration struct {
// Versions lists every protocol version for which at least one cipher
// was accepted, with the matching cipher suites.
Versions []EnumVersion `json:"versions,omitempty"`
// Skipped is set when enumeration was not attempted (e.g. STARTTLS
// endpoint, prior handshake failure). Empty when enumeration ran.
Skipped string `json:"skipped,omitempty"`
// DurationMS is the wall-clock time spent enumerating, for ops visibility.
DurationMS int64 `json:"duration_ms,omitempty"`
}
// EnumVersion is one accepted protocol version plus the ciphers it accepted.
type EnumVersion struct {
Version uint16 `json:"version"`
Name string `json:"name"`
Ciphers []EnumCipher `json:"ciphers,omitempty"`
}
// EnumCipher is one accepted cipher suite.
type EnumCipher struct {
ID uint16 `json:"id"`
Name string `json:"name"`
}