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.
This commit is contained in:
nemunaire 2026-04-29 13:34:27 +07:00
commit a9f37c79cf
18 changed files with 1569 additions and 5 deletions

View file

@ -9,8 +9,9 @@ const ObservationKeyTLSProbes = "tls_probes"
// Option ids on CheckerOptions.
const (
OptionEndpoints = "endpoints"
OptionProbeTimeoutMs = "probeTimeoutMs"
OptionEndpoints = "endpoints"
OptionProbeTimeoutMs = "probeTimeoutMs"
OptionEnumerateCiphers = "enumerateCiphers"
)
// Defaults shared between the definition's Default field and the runtime
@ -100,6 +101,12 @@ type TLSProbe struct {
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.
@ -142,3 +149,31 @@ type CertInfo struct {
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"`
}