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:
parent
8a7f9feaf7
commit
a9f37c79cf
18 changed files with 1569 additions and 5 deletions
135
checker/rules_enumeration_test.go
Normal file
135
checker/rules_enumeration_test.go
Normal file
|
|
@ -0,0 +1,135 @@
|
|||
package checker
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"encoding/json"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
sdk "git.happydns.org/checker-sdk-go/checker"
|
||||
)
|
||||
|
||||
// stubObs is a minimal ObservationGetter that serves a pre-built TLSData
|
||||
// payload and ignores related lookups. It is local to this file rather than
|
||||
// promoted to a shared helper to keep the rule tests self-contained.
|
||||
type stubObs struct{ data TLSData }
|
||||
|
||||
func (s stubObs) Get(_ context.Context, key sdk.ObservationKey, dest any) error {
|
||||
if key != ObservationKeyTLSProbes {
|
||||
return nil
|
||||
}
|
||||
raw, _ := json.Marshal(s.data)
|
||||
return json.Unmarshal(raw, dest)
|
||||
}
|
||||
func (s stubObs) GetRelated(_ context.Context, _ sdk.ObservationKey) ([]sdk.RelatedObservation, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func newProbeWithEnum(versions ...EnumVersion) TLSProbe {
|
||||
return TLSProbe{
|
||||
Host: "example.test", Port: 443, Endpoint: "example.test:443", Type: "tls",
|
||||
TLSHandshakeOK: true, TLSVersionNum: tls.VersionTLS13,
|
||||
Enum: &TLSEnumeration{Versions: versions},
|
||||
}
|
||||
}
|
||||
|
||||
func TestVersionEnumerationRule_Skipped_NoEnum(t *testing.T) {
|
||||
obs := stubObs{data: TLSData{
|
||||
Probes: map[string]TLSProbe{"a": {Host: "x", Port: 443, Endpoint: "x:443", Type: "tls", TLSHandshakeOK: true}},
|
||||
CollectedAt: time.Now(),
|
||||
}}
|
||||
got := (&versionEnumerationRule{}).Evaluate(context.Background(), obs, nil)
|
||||
if len(got) != 1 || got[0].Code != "tls.enum.versions.skipped" {
|
||||
t.Fatalf("want a single skipped state, got %+v", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestVersionEnumerationRule_OK_OnlyModern(t *testing.T) {
|
||||
obs := stubObs{data: TLSData{
|
||||
Probes: map[string]TLSProbe{
|
||||
"a": newProbeWithEnum(
|
||||
EnumVersion{Version: tls.VersionTLS12, Name: "TLS 1.2"},
|
||||
EnumVersion{Version: tls.VersionTLS13, Name: "TLS 1.3"},
|
||||
),
|
||||
},
|
||||
}}
|
||||
got := (&versionEnumerationRule{}).Evaluate(context.Background(), obs, nil)
|
||||
if len(got) != 1 || got[0].Status != sdk.StatusOK || got[0].Code != "tls.enum.versions.ok" {
|
||||
t.Fatalf("want a single OK state, got %+v", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestVersionEnumerationRule_LegacyAccepted(t *testing.T) {
|
||||
obs := stubObs{data: TLSData{
|
||||
Probes: map[string]TLSProbe{
|
||||
"a": newProbeWithEnum(
|
||||
EnumVersion{Version: tls.VersionTLS10, Name: "TLS 1.0"},
|
||||
EnumVersion{Version: tls.VersionTLS12, Name: "TLS 1.2"},
|
||||
),
|
||||
},
|
||||
}}
|
||||
got := (&versionEnumerationRule{}).Evaluate(context.Background(), obs, nil)
|
||||
if len(got) != 1 || got[0].Status != sdk.StatusWarn || got[0].Code != "tls.enum.versions.legacy_accepted" {
|
||||
t.Fatalf("want a single warn state, got %+v", got)
|
||||
}
|
||||
if !strings.Contains(got[0].Message, "TLS 1.0") {
|
||||
t.Fatalf("warn message should mention the legacy version, got %q", got[0].Message)
|
||||
}
|
||||
}
|
||||
|
||||
func TestClassifyCipher(t *testing.T) {
|
||||
cases := map[string]string{
|
||||
"TLS_RSA_WITH_NULL_SHA": "NULL",
|
||||
"TLS_DH_anon_WITH_AES_128_CBC_SHA": "anonymous",
|
||||
"TLS_RSA_EXPORT_WITH_RC4_40_MD5": "EXPORT",
|
||||
"TLS_ECDHE_RSA_WITH_RC4_128_SHA": "RC4",
|
||||
"TLS_RSA_WITH_3DES_EDE_CBC_SHA": "3DES/DES",
|
||||
"TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256": "",
|
||||
"TLS_AES_256_GCM_SHA384": "",
|
||||
"TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256": "",
|
||||
}
|
||||
for name, want := range cases {
|
||||
if got := classifyCipher(name); got != want {
|
||||
t.Errorf("classifyCipher(%q) = %q, want %q", name, got, want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestWeakCipherRule_Detects(t *testing.T) {
|
||||
obs := stubObs{data: TLSData{
|
||||
Probes: map[string]TLSProbe{
|
||||
"a": newProbeWithEnum(
|
||||
EnumVersion{Version: tls.VersionTLS12, Name: "TLS 1.2", Ciphers: []EnumCipher{
|
||||
{ID: 0xC02F, Name: "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256"},
|
||||
{ID: 0x000A, Name: "TLS_RSA_WITH_3DES_EDE_CBC_SHA"},
|
||||
{ID: 0x0005, Name: "TLS_RSA_WITH_RC4_128_SHA"},
|
||||
}},
|
||||
),
|
||||
},
|
||||
}}
|
||||
got := (&weakCipherRule{}).Evaluate(context.Background(), obs, nil)
|
||||
if len(got) != 1 || got[0].Status != sdk.StatusWarn || got[0].Code != "tls.enum.ciphers.weak_accepted" {
|
||||
t.Fatalf("want a single weak warn state, got %+v", got)
|
||||
}
|
||||
if !strings.Contains(got[0].Message, "RC4") || !strings.Contains(got[0].Message, "3DES/DES") {
|
||||
t.Fatalf("warn message should list the broken categories, got %q", got[0].Message)
|
||||
}
|
||||
}
|
||||
|
||||
func TestWeakCipherRule_OK_OnlyModern(t *testing.T) {
|
||||
obs := stubObs{data: TLSData{
|
||||
Probes: map[string]TLSProbe{
|
||||
"a": newProbeWithEnum(
|
||||
EnumVersion{Version: tls.VersionTLS13, Name: "TLS 1.3", Ciphers: []EnumCipher{
|
||||
{ID: 0x1301, Name: "TLS_AES_128_GCM_SHA256"},
|
||||
}},
|
||||
),
|
||||
},
|
||||
}}
|
||||
got := (&weakCipherRule{}).Evaluate(context.Background(), obs, nil)
|
||||
if len(got) != 1 || got[0].Status != sdk.StatusOK || got[0].Code != "tls.enum.ciphers.ok" {
|
||||
t.Fatalf("want a single OK state, got %+v", got)
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue