Initial commit
This commit is contained in:
commit
21e66f1d0b
20 changed files with 2673 additions and 0 deletions
332
checker/collect_test.go
Normal file
332
checker/collect_test.go
Normal file
|
|
@ -0,0 +1,332 @@
|
|||
package checker
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
asn1 "github.com/jcmturner/gofork/encoding/asn1"
|
||||
"github.com/jcmturner/gokrb5/v8/iana/errorcode"
|
||||
"github.com/jcmturner/gokrb5/v8/iana/etypeID"
|
||||
"github.com/jcmturner/gokrb5/v8/iana/nametype"
|
||||
"github.com/jcmturner/gokrb5/v8/iana/patype"
|
||||
"github.com/jcmturner/gokrb5/v8/messages"
|
||||
"github.com/jcmturner/gokrb5/v8/types"
|
||||
)
|
||||
|
||||
// buildKRBError constructs a marshaled KRB-ERROR with the given code and
|
||||
// optional EData payload.
|
||||
func buildKRBError(t *testing.T, realm string, code int32, edata []byte) []byte {
|
||||
t.Helper()
|
||||
sname := types.NewPrincipalName(nametype.KRB_NT_SRV_INST, "krbtgt/"+realm)
|
||||
k := messages.NewKRBError(sname, realm, code, "")
|
||||
k.STime = time.Date(2025, 1, 2, 3, 4, 5, 0, time.UTC)
|
||||
k.Susec = 0
|
||||
k.EData = edata
|
||||
raw, err := k.Marshal()
|
||||
if err != nil {
|
||||
t.Fatalf("marshal KRBError: %v", err)
|
||||
}
|
||||
return raw
|
||||
}
|
||||
|
||||
// buildETypeInfo2EData marshals a PADataSequence containing one
|
||||
// PA_ETYPE_INFO2 entry per supplied (etype, salt) pair.
|
||||
func buildETypeInfo2EData(t *testing.T, entries []types.ETypeInfo2Entry) []byte {
|
||||
t.Helper()
|
||||
value, err := asn1.Marshal(types.ETypeInfo2(entries))
|
||||
if err != nil {
|
||||
t.Fatalf("marshal ETypeInfo2: %v", err)
|
||||
}
|
||||
pas := types.PADataSequence{
|
||||
{PADataType: patype.PA_ETYPE_INFO2, PADataValue: value},
|
||||
{PADataType: patype.PA_PK_AS_REQ, PADataValue: []byte{0x00}},
|
||||
}
|
||||
raw, err := asn1.Marshal(pas)
|
||||
if err != nil {
|
||||
t.Fatalf("marshal PADataSequence: %v", err)
|
||||
}
|
||||
return raw
|
||||
}
|
||||
|
||||
func TestParseASResponse_KRBErrorPreauthRequiredWithEData(t *testing.T) {
|
||||
edata := buildETypeInfo2EData(t, []types.ETypeInfo2Entry{
|
||||
{EType: etypeID.AES256_CTS_HMAC_SHA1_96, Salt: "EXAMPLE.COMuser"},
|
||||
{EType: etypeID.RC4_HMAC, Salt: ""},
|
||||
})
|
||||
raw := buildKRBError(t, "EXAMPLE.COM", errorcode.KDC_ERR_PREAUTH_REQUIRED, edata)
|
||||
|
||||
var out ASProbeResult
|
||||
parseASResponse(raw, &out)
|
||||
|
||||
if out.Error != "" {
|
||||
t.Fatalf("unexpected parse error: %s", out.Error)
|
||||
}
|
||||
if out.ErrorCode != errorcode.KDC_ERR_PREAUTH_REQUIRED {
|
||||
t.Errorf("ErrorCode = %d, want KDC_ERR_PREAUTH_REQUIRED", out.ErrorCode)
|
||||
}
|
||||
if !out.PreauthReq {
|
||||
t.Error("PreauthReq should be true for KDC_ERR_PREAUTH_REQUIRED")
|
||||
}
|
||||
if out.ServerRealm != "EXAMPLE.COM" {
|
||||
t.Errorf("ServerRealm = %q, want EXAMPLE.COM", out.ServerRealm)
|
||||
}
|
||||
if out.ServerTime.IsZero() {
|
||||
t.Error("ServerTime should be populated from STime")
|
||||
}
|
||||
if !out.PKINITOffered {
|
||||
t.Error("PKINITOffered should be true (PA_PK_AS_REQ present)")
|
||||
}
|
||||
if len(out.Enctypes) != 2 {
|
||||
t.Fatalf("Enctypes len = %d, want 2", len(out.Enctypes))
|
||||
}
|
||||
|
||||
var sawAES, sawRC4 bool
|
||||
for _, e := range out.Enctypes {
|
||||
switch e.ID {
|
||||
case etypeID.AES256_CTS_HMAC_SHA1_96:
|
||||
sawAES = true
|
||||
if e.Weak {
|
||||
t.Error("AES256 should not be flagged weak")
|
||||
}
|
||||
if e.Source != "etype-info2" {
|
||||
t.Errorf("AES256 Source = %q, want etype-info2", e.Source)
|
||||
}
|
||||
case etypeID.RC4_HMAC:
|
||||
sawRC4 = true
|
||||
if !e.Weak {
|
||||
t.Error("RC4_HMAC should be flagged weak")
|
||||
}
|
||||
}
|
||||
}
|
||||
if !sawAES || !sawRC4 {
|
||||
t.Errorf("missing expected enctypes (sawAES=%v sawRC4=%v)", sawAES, sawRC4)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseASResponse_KRBErrorPrincipalUnknownNoEData(t *testing.T) {
|
||||
raw := buildKRBError(t, "EXAMPLE.COM", errorcode.KDC_ERR_C_PRINCIPAL_UNKNOWN, nil)
|
||||
|
||||
var out ASProbeResult
|
||||
parseASResponse(raw, &out)
|
||||
|
||||
if out.Error != "" {
|
||||
t.Fatalf("unexpected parse error: %s", out.Error)
|
||||
}
|
||||
if out.ErrorCode != errorcode.KDC_ERR_C_PRINCIPAL_UNKNOWN {
|
||||
t.Errorf("ErrorCode = %d, want KDC_ERR_C_PRINCIPAL_UNKNOWN", out.ErrorCode)
|
||||
}
|
||||
if out.PreauthReq {
|
||||
t.Error("PreauthReq should be false")
|
||||
}
|
||||
if len(out.Enctypes) != 0 {
|
||||
t.Errorf("Enctypes should be empty, got %d", len(out.Enctypes))
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseASResponse_GarbageBytes(t *testing.T) {
|
||||
var out ASProbeResult
|
||||
parseASResponse([]byte{0xde, 0xad, 0xbe, 0xef, 0xca, 0xfe}, &out)
|
||||
if out.Error == "" {
|
||||
t.Fatal("expected an Error string for unparsable bytes")
|
||||
}
|
||||
if !strings.Contains(out.Error, "deadbeefcafe") {
|
||||
t.Errorf("Error should include hex prefix of payload, got %q", out.Error)
|
||||
}
|
||||
}
|
||||
|
||||
func TestExtractEData_ETypeInfoFallback(t *testing.T) {
|
||||
// PA_ETYPE_INFO (legacy) only. Salt is octet-string here.
|
||||
value, err := asn1.Marshal(types.ETypeInfo{
|
||||
{EType: etypeID.AES128_CTS_HMAC_SHA1_96, Salt: []byte("salty")},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("marshal ETypeInfo: %v", err)
|
||||
}
|
||||
edata, err := asn1.Marshal(types.PADataSequence{
|
||||
{PADataType: patype.PA_ETYPE_INFO, PADataValue: value},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("marshal PADataSequence: %v", err)
|
||||
}
|
||||
|
||||
enctypes, pkinit := extractEData(edata)
|
||||
if pkinit {
|
||||
t.Error("PKINIT should not be reported when no PA_PK_AS_REQ is present")
|
||||
}
|
||||
if len(enctypes) != 1 {
|
||||
t.Fatalf("got %d enctypes, want 1", len(enctypes))
|
||||
}
|
||||
if enctypes[0].Source != "etype-info" {
|
||||
t.Errorf("Source = %q, want etype-info", enctypes[0].Source)
|
||||
}
|
||||
if enctypes[0].Salt != "salty" {
|
||||
t.Errorf("Salt = %q, want salty", enctypes[0].Salt)
|
||||
}
|
||||
}
|
||||
|
||||
func TestExtractEData_ETypeInfo2WinsOverInfo(t *testing.T) {
|
||||
// Both PA_ETYPE_INFO2 and PA_ETYPE_INFO advertise the same enctype.
|
||||
// The legacy info should be skipped (de-duplicated).
|
||||
v2, _ := asn1.Marshal(types.ETypeInfo2{
|
||||
{EType: etypeID.AES256_CTS_HMAC_SHA1_96, Salt: "fromInfo2"},
|
||||
})
|
||||
v1, _ := asn1.Marshal(types.ETypeInfo{
|
||||
{EType: etypeID.AES256_CTS_HMAC_SHA1_96, Salt: []byte("fromInfo")},
|
||||
})
|
||||
edata, _ := asn1.Marshal(types.PADataSequence{
|
||||
{PADataType: patype.PA_ETYPE_INFO2, PADataValue: v2},
|
||||
{PADataType: patype.PA_ETYPE_INFO, PADataValue: v1},
|
||||
})
|
||||
got, _ := extractEData(edata)
|
||||
if len(got) != 1 {
|
||||
t.Fatalf("got %d entries, want 1 (de-duplicated)", len(got))
|
||||
}
|
||||
if got[0].Salt != "fromInfo2" {
|
||||
t.Errorf("salt = %q, want fromInfo2 (etype-info2 must take precedence)", got[0].Salt)
|
||||
}
|
||||
}
|
||||
|
||||
func TestExtractEData_BadASN1(t *testing.T) {
|
||||
enctypes, pkinit := extractEData([]byte{0xff, 0x00})
|
||||
if enctypes != nil || pkinit {
|
||||
t.Errorf("expected nil/false on garbage, got %v / %v", enctypes, pkinit)
|
||||
}
|
||||
}
|
||||
|
||||
func TestEtypeName(t *testing.T) {
|
||||
if got := etypeName(etypeID.AES256_CTS_HMAC_SHA1_96); !strings.Contains(strings.ToLower(got), "aes256") {
|
||||
t.Errorf("AES256 name = %q, want it to mention aes256", got)
|
||||
}
|
||||
if got := etypeName(99999); got != "etype-99999" {
|
||||
t.Errorf("unknown etype = %q, want etype-99999", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestErrorcodeNameAndKRBErrorInfo(t *testing.T) {
|
||||
name := errorcodeName(errorcode.KDC_ERR_PREAUTH_REQUIRED)
|
||||
if !strings.Contains(name, "PREAUTH") {
|
||||
t.Errorf("errorcodeName = %q, want it to contain PREAUTH", name)
|
||||
}
|
||||
|
||||
// Typed KRBError: errors.As path.
|
||||
sname := types.NewPrincipalName(nametype.KRB_NT_SRV_INST, "krbtgt/EXAMPLE.COM")
|
||||
krb := messages.NewKRBError(sname, "EXAMPLE.COM", errorcode.KDC_ERR_PREAUTH_REQUIRED, "")
|
||||
code, n, ok := krbErrorInfo(krb)
|
||||
if !ok || code != errorcode.KDC_ERR_PREAUTH_REQUIRED || !strings.Contains(n, "PREAUTH") {
|
||||
t.Errorf("krbErrorInfo(typed) = %d %q %v", code, n, ok)
|
||||
}
|
||||
|
||||
// String fallback: gokrb5 sometimes wraps the code only inside the message.
|
||||
wrapped := errors.New("login failed: KRB Error: (24) KDC_ERR_PREAUTH_FAILED - bla")
|
||||
code, n, ok = krbErrorInfo(wrapped)
|
||||
if !ok || code != 24 {
|
||||
t.Errorf("krbErrorInfo(string) code=%d ok=%v", code, ok)
|
||||
}
|
||||
if !strings.Contains(n, "PREAUTH_FAILED") {
|
||||
t.Errorf("krbErrorInfo(string) name = %q", n)
|
||||
}
|
||||
|
||||
if _, _, ok := krbErrorInfo(nil); ok {
|
||||
t.Error("krbErrorInfo(nil) should return ok=false")
|
||||
}
|
||||
if _, _, ok := krbErrorInfo(errors.New("plain old error")); ok {
|
||||
t.Error("krbErrorInfo on a non-KRB error should return ok=false")
|
||||
}
|
||||
}
|
||||
|
||||
func TestRoleForPrefix(t *testing.T) {
|
||||
cases := map[string]string{
|
||||
"_kerberos._tcp.": "kdc",
|
||||
"_kerberos._udp.": "kdc",
|
||||
"_kerberos-master._tcp.": "master",
|
||||
"_kerberos-adm._tcp.": "kadmin",
|
||||
"_kpasswd._tcp.": "kpasswd",
|
||||
"_kpasswd._udp.": "kpasswd",
|
||||
}
|
||||
for in, want := range cases {
|
||||
if got := roleForPrefix(in); got != want {
|
||||
t.Errorf("roleForPrefix(%q) = %q, want %q", in, got, want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestOptFloat(t *testing.T) {
|
||||
cases := []struct {
|
||||
in any
|
||||
want float64
|
||||
}{
|
||||
{float64(2.5), 2.5},
|
||||
{float32(1.5), 1.5},
|
||||
{int(7), 7},
|
||||
{int64(8), 8},
|
||||
{"3.14", 3.14},
|
||||
{"nope", 42}, // falls back to default
|
||||
{nil, 42}, // missing key path is exercised below
|
||||
}
|
||||
for _, c := range cases {
|
||||
opts := map[string]any{"k": c.in}
|
||||
got := optFloat(opts, "k", 42)
|
||||
if got != c.want {
|
||||
t.Errorf("optFloat(%v) = %v, want %v", c.in, got, c.want)
|
||||
}
|
||||
}
|
||||
if got := optFloat(map[string]any{}, "missing", 99); got != 99 {
|
||||
t.Errorf("optFloat(missing) = %v, want 99", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestOptBool(t *testing.T) {
|
||||
cases := []struct {
|
||||
in any
|
||||
def bool
|
||||
want bool
|
||||
}{
|
||||
{true, false, true},
|
||||
{false, true, false},
|
||||
{"true", false, true},
|
||||
{"1", false, true},
|
||||
{"false", true, false}, // unrecognized string -> default
|
||||
{nil, true, true},
|
||||
{42, false, false}, // unsupported type -> default
|
||||
}
|
||||
for _, c := range cases {
|
||||
opts := map[string]any{}
|
||||
if c.in != nil {
|
||||
opts["k"] = c.in
|
||||
}
|
||||
got := optBool(opts, "k", c.def)
|
||||
if got != c.want {
|
||||
t.Errorf("optBool(%v, def=%v) = %v, want %v", c.in, c.def, got, c.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestSmallHelpers(t *testing.T) {
|
||||
if got := abs(-3 * time.Second); got != 3*time.Second {
|
||||
t.Errorf("abs negative = %v", got)
|
||||
}
|
||||
if got := abs(2 * time.Second); got != 2*time.Second {
|
||||
t.Errorf("abs positive = %v", got)
|
||||
}
|
||||
if got := firstNonEmpty("", "", "x", "y"); got != "x" {
|
||||
t.Errorf("firstNonEmpty = %q", got)
|
||||
}
|
||||
if got := firstNonEmpty("", ""); got != "" {
|
||||
t.Errorf("firstNonEmpty(all empty) = %q", got)
|
||||
}
|
||||
if got := first([]byte{1, 2, 3}, 16); len(got) != 3 {
|
||||
t.Errorf("first(short) len = %d, want 3", len(got))
|
||||
}
|
||||
if got := first([]byte{1, 2, 3, 4, 5}, 2); len(got) != 2 || got[0] != 1 || got[1] != 2 {
|
||||
t.Errorf("first(long) = %v", got)
|
||||
}
|
||||
list := []EnctypeEntry{{ID: 18}, {ID: 17}}
|
||||
if !hasEnctype(list, 17) {
|
||||
t.Error("hasEnctype should find 17")
|
||||
}
|
||||
if hasEnctype(list, 23) {
|
||||
t.Error("hasEnctype should not find 23")
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue