Skip to content

Commit 9b972f1

Browse files
committed
Wire credentials into the Kerberos client and add TGT export
Phase 3 (part 3c, capability wiring) of the native Kerberos v5 work. - The client now holds a credentials.Credential instead of a bare password string. WithPassword builds one; WithCredential, WithNTHash (overpass-the-hash), and WithAESKey (pass-the-key) are new. Key derivation and the AS-REQ etype list are driven by the credential's SupportedETypes/Key, and the PREAUTH_REQUIRED etype selection is constrained to what the credential can actually satisfy (so an NT-hash client requests RC4 rather than an unusable AES etype). - processASRep now retains the decrypted AS-REP enc-part (times, flags, sname). - ExportTGTKirbi and ExportTGTCCache serialize the acquired TGT to a .kirbi (DER KRB-CRED) and an MIT ccache v4, enabling pass-the-ticket / interchange with Rubeus, Impacket, and Linux Kerberos tooling. - Remove the obsolete WithCCache stub (unused repo-wide). Export is covered by white-box tests that populate the TGT state and verify the .kirbi and ccache round-trip. The structural split of the AS/TGS path into kdc/ + transport/ packages is deferred to live-KDC validation. Full repo builds; kerberos tests, go vet, and go build ./... are green. stdlib-only.
1 parent e928ab0 commit 9b972f1

3 files changed

Lines changed: 265 additions & 41 deletions

File tree

network/kerberos/v5/client.go

Lines changed: 60 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import (
88
"strings"
99
"time"
1010

11+
"github.com/TheManticoreProject/Manticore/network/kerberos/v5/credentials"
1112
kerbcrypto "github.com/TheManticoreProject/Manticore/network/kerberos/v5/crypto"
1213
"github.com/TheManticoreProject/Manticore/network/kerberos/v5/messages"
1314
)
@@ -29,13 +30,14 @@ type KerberosClient struct {
2930
realm string
3031
kdcHost string
3132

32-
password string
33+
cred *credentials.Credential
3334

3435
// Populated after a successful GetTGT call.
3536
tgtTicket messages.Ticket
3637
tgtTicketRaw []byte // raw APPLICATION[1] bytes as received from KDC
3738
sessionKey []byte
3839
sessionEType int
40+
tgtEnc messages.EncASRepPart // decrypted AS-REP enc-part: times, flags, sname
3941
hasTGT bool
4042
}
4143

@@ -50,17 +52,40 @@ func NewClient(username, realm, kdcHost string) *KerberosClient {
5052
}
5153
}
5254

53-
// WithPassword stores the password for use in GetTGT.
55+
// WithPassword configures a password credential for GetTGT.
5456
// Returns the client to allow fluent chaining.
5557
func (c *KerberosClient) WithPassword(password string) *KerberosClient {
56-
c.password = password
58+
c.cred = credentials.NewWithPassword(c.username, c.realm, password)
5759
return c
5860
}
5961

60-
// WithCCache is not yet supported in the native implementation.
61-
// Use the gokrb5-backed KerberosInit helper for ccache-based LDAP binds.
62-
func (c *KerberosClient) WithCCache(_ string) error {
63-
return fmt.Errorf("kerberos: ccache not supported in native implementation; use gokrb5 KerberosInit for LDAP GSSAPI binds")
62+
// WithCredential configures an arbitrary credential (password, NT hash, or AES
63+
// key). Returns the client to allow fluent chaining.
64+
func (c *KerberosClient) WithCredential(cred *credentials.Credential) *KerberosClient {
65+
c.cred = cred
66+
return c
67+
}
68+
69+
// WithNTHash configures an NT-hash (overpass-the-hash) credential from a hex
70+
// string (accepts an "LM:NT" pair). GetTGT will request an RC4-HMAC TGT.
71+
func (c *KerberosClient) WithNTHash(hexHash string) error {
72+
cred, err := credentials.NewWithHexNTHash(c.username, c.realm, hexHash)
73+
if err != nil {
74+
return err
75+
}
76+
c.cred = cred
77+
return nil
78+
}
79+
80+
// WithAESKey configures a pass-the-key credential from a hex-encoded AES key
81+
// (16 bytes -> AES128, 32 bytes -> AES256).
82+
func (c *KerberosClient) WithAESKey(hexKey string) error {
83+
cred, err := credentials.NewWithHexAESKey(c.username, c.realm, hexKey)
84+
if err != nil {
85+
return err
86+
}
87+
c.cred = cred
88+
return nil
6489
}
6590

6691
// GetTGT requests a Ticket Granting Ticket from the KDC using the password
@@ -71,14 +96,14 @@ func (c *KerberosClient) WithCCache(_ string) error {
7196
// (realm+username). If the KDC returns PREAUTH_REQUIRED with different
7297
// etype/salt info, we retry once with the corrected values.
7398
func (c *KerberosClient) GetTGT() error {
74-
if c.password == "" {
75-
return fmt.Errorf("kerberos: no credentials configured: call WithPassword first")
99+
if c.cred == nil {
100+
return fmt.Errorf("kerberos: no credentials configured: call WithPassword/WithNTHash/WithAESKey/WithCredential first")
76101
}
77102

78-
// Attempt with default AD salt. The KDC may respond with PREAUTH_REQUIRED
79-
// if a different salt or etype is required.
80-
etype := messages.ETypeAES256CTSHMACSHA196
81-
salt := c.realm + c.username
103+
// Start with the strongest etype the credential can satisfy and the AD
104+
// default salt. The KDC corrects both via PREAUTH_REQUIRED if needed.
105+
etype := c.cred.SupportedETypes()[0]
106+
salt := c.cred.DefaultSalt()
82107
resp, nonce, err := c.sendASReqWithPreauth(etype, salt, nil)
83108
if err != nil {
84109
return err
@@ -218,7 +243,9 @@ func (c *KerberosClient) Destroy() {
218243
c.sessionKey[i] = 0
219244
}
220245
c.sessionKey = nil
221-
c.password = ""
246+
if c.cred != nil {
247+
c.cred.Destroy()
248+
}
222249
c.hasTGT = false
223250
}
224251

@@ -256,11 +283,7 @@ func (c *KerberosClient) sendASReq(pa_data []messages.PAData) ([]byte, int, erro
256283
},
257284
Till: time.Now().UTC().Add(24 * time.Hour),
258285
Nonce: nonce,
259-
EType: []int{
260-
messages.ETypeAES256CTSHMACSHA196,
261-
messages.ETypeAES128CTSHMACSHA196,
262-
messages.ETypeRC4HMAC,
263-
},
286+
EType: c.cred.SupportedETypes(),
264287
},
265288
}
266289

@@ -279,8 +302,9 @@ func (c *KerberosClient) sendASReq(pa_data []messages.PAData) ([]byte, int, erro
279302
// PA-ETYPE-INFO2 structure embedded in a KRBError's EData.
280303
// Falls back to AES-256 with the default AD salt if no EData is present.
281304
func (c *KerberosClient) pickETypeFromError(krb_err messages.KRBError) (int, string, []byte) {
282-
default_salt := c.realm + c.username
283-
default_etype := messages.ETypeAES256CTSHMACSHA196
305+
preferred := c.cred.SupportedETypes()
306+
default_salt := c.cred.DefaultSalt()
307+
default_etype := preferred[0]
284308

285309
if len(krb_err.EData) == 0 {
286310
return default_etype, default_salt, nil
@@ -294,7 +318,7 @@ func (c *KerberosClient) pickETypeFromError(krb_err messages.KRBError) (int, str
294318
if pa.PADataType == messages.PAETypeInfo2 {
295319
var info messages.ETypeInfo2
296320
if _, err := info.Unmarshal(pa.PADataValue); err == nil && len(info) > 0 {
297-
return pickBestEType(info, default_salt)
321+
return pickBestEType(info, preferred, default_salt)
298322
}
299323
}
300324
}
@@ -303,20 +327,17 @@ func (c *KerberosClient) pickETypeFromError(krb_err messages.KRBError) (int, str
303327
// Try to parse EData directly as ETYPE-INFO2.
304328
var info messages.ETypeInfo2
305329
if _, err := info.Unmarshal(krb_err.EData); err == nil && len(info) > 0 {
306-
return pickBestEType(info, default_salt)
330+
return pickBestEType(info, preferred, default_salt)
307331
}
308332

309333
return default_etype, default_salt, nil
310334
}
311335

312-
// pickBestEType selects the strongest supported etype from an ETypeInfo2 list.
313-
func pickBestEType(info messages.ETypeInfo2, default_salt string) (int, string, []byte) {
314-
// Preference order: AES256 > AES128 > RC4.
315-
preferred := []int{
316-
messages.ETypeAES256CTSHMACSHA196,
317-
messages.ETypeAES128CTSHMACSHA196,
318-
messages.ETypeRC4HMAC,
319-
}
336+
// pickBestEType selects, from an ETYPE-INFO2 list, the strongest etype the
337+
// credential can actually satisfy (preferred is the credential's supported set
338+
// in strength order). This prevents choosing, say, AES when only an NT hash is
339+
// held.
340+
func pickBestEType(info messages.ETypeInfo2, preferred []int, default_salt string) (int, string, []byte) {
320341
for _, want := range preferred {
321342
for _, entry := range info {
322343
if entry.EType == want {
@@ -328,20 +349,17 @@ func pickBestEType(info messages.ETypeInfo2, default_salt string) (int, string,
328349
}
329350
}
330351
}
331-
// Fallback to first entry.
332-
e := info[0]
333-
if e.Salt == "" {
334-
e.Salt = default_salt
335-
}
336-
return e.EType, e.Salt, e.S2KParams
352+
// No advertised etype is supported by the credential; fall back to the
353+
// credential's strongest etype with the default salt.
354+
return preferred[0], default_salt, nil
337355
}
338356

339357
// sendASReqWithPreauth builds and sends an AS-REQ with PA-ENC-TIMESTAMP.
340358
// Returns the raw KDC response bytes and the nonce placed in the request.
341359
func (c *KerberosClient) sendASReqWithPreauth(etype int, salt string, s2k_params []byte) ([]byte, int, error) {
342-
key, err := kerbcrypto.StringToKey(etype, c.password, salt, s2k_params)
360+
key, err := c.cred.Key(etype, salt, s2k_params)
343361
if err != nil {
344-
return nil, 0, fmt.Errorf("kerberos: StringToKey: %w", err)
362+
return nil, 0, fmt.Errorf("kerberos: derive key: %w", err)
345363
}
346364

347365
now := time.Now().UTC()
@@ -396,9 +414,9 @@ func (c *KerberosClient) processASRep(resp []byte, etype int, salt string, s2k_p
396414
return fmt.Errorf("kerberos: parse AS-REP: %w", err)
397415
}
398416

399-
key, err := kerbcrypto.StringToKey(etype, c.password, salt, s2k_params)
417+
key, err := c.cred.Key(etype, salt, s2k_params)
400418
if err != nil {
401-
return fmt.Errorf("kerberos: StringToKey for AS-REP decrypt: %w", err)
419+
return fmt.Errorf("kerberos: derive key for AS-REP decrypt: %w", err)
402420
}
403421

404422
enc_plain, err := kerbcrypto.Decrypt(etype, key, kerbcrypto.KeyUsageASRepEncPart, as_rep.EncPart.Cipher)
@@ -422,6 +440,7 @@ func (c *KerberosClient) processASRep(resp []byte, etype int, salt string, s2k_p
422440
c.tgtTicketRaw = as_rep.TicketRaw
423441
c.sessionKey = enc_as_rep.Key.KeyValue
424442
c.sessionEType = enc_as_rep.Key.KeyType
443+
c.tgtEnc = enc_as_rep
425444
c.hasTGT = true
426445
return nil
427446
}

network/kerberos/v5/export.go

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
package kerberos
2+
3+
import (
4+
"encoding/asn1"
5+
"encoding/binary"
6+
"fmt"
7+
"time"
8+
9+
"github.com/TheManticoreProject/Manticore/network/kerberos/v5/credcache/ccache"
10+
"github.com/TheManticoreProject/Manticore/network/kerberos/v5/credcache/kirbi"
11+
"github.com/TheManticoreProject/Manticore/network/kerberos/v5/messages"
12+
)
13+
14+
// tgtCredInfo builds the KrbCredInfo describing the currently held TGT from the
15+
// stored AS-REP enc-part (session key, flags, times, service name).
16+
func (c *KerberosClient) tgtCredInfo() messages.KrbCredInfo {
17+
return messages.KrbCredInfo{
18+
Key: messages.EncryptionKey{KeyType: c.sessionEType, KeyValue: c.sessionKey},
19+
PRealm: c.realm,
20+
PName: messages.PrincipalName{NameType: messages.NameTypePrincipal, NameString: []string{c.username}},
21+
Flags: c.tgtEnc.Flags,
22+
AuthTime: c.tgtEnc.AuthTime,
23+
StartTime: c.tgtEnc.StartTime,
24+
EndTime: c.tgtEnc.EndTime,
25+
RenewTill: c.tgtEnc.RenewTill,
26+
SRealm: c.tgtEnc.SRealm,
27+
SName: c.tgtEnc.SName,
28+
}
29+
}
30+
31+
// ExportTGTKirbi returns the current TGT as .kirbi (DER KRB-CRED) bytes, suitable
32+
// for pass-the-ticket with Rubeus/Impacket. GetTGT must have succeeded first.
33+
func (c *KerberosClient) ExportTGTKirbi() ([]byte, error) {
34+
if !c.hasTGT {
35+
return nil, fmt.Errorf("kerberos: no TGT: call GetTGT first")
36+
}
37+
cred, err := kirbi.New(c.tgtTicketRaw, c.tgtCredInfo())
38+
if err != nil {
39+
return nil, err
40+
}
41+
return kirbi.Bytes(cred)
42+
}
43+
44+
// ExportTGTCCache returns the current TGT as an MIT ccache (v4) holding a single
45+
// credential. GetTGT must have succeeded first.
46+
func (c *KerberosClient) ExportTGTCCache() (*ccache.CCache, error) {
47+
if !c.hasTGT {
48+
return nil, fmt.Errorf("kerberos: no TGT: call GetTGT first")
49+
}
50+
client := ccache.Principal{
51+
NameType: uint32(messages.NameTypePrincipal),
52+
Realm: c.realm,
53+
Components: []string{c.username},
54+
}
55+
server := ccache.Principal{
56+
NameType: uint32(c.tgtEnc.SName.NameType),
57+
Realm: c.tgtEnc.SRealm,
58+
Components: append([]string(nil), c.tgtEnc.SName.NameString...),
59+
}
60+
start := c.tgtEnc.StartTime
61+
if start.IsZero() {
62+
start = c.tgtEnc.AuthTime
63+
}
64+
return &ccache.CCache{
65+
DefaultPrincipal: client,
66+
Credentials: []ccache.Credential{{
67+
Client: client,
68+
Server: server,
69+
Key: ccache.Keyblock{EType: uint16(c.sessionEType), KeyValue: c.sessionKey},
70+
AuthTime: unixU32(c.tgtEnc.AuthTime),
71+
StartTime: unixU32(start),
72+
EndTime: unixU32(c.tgtEnc.EndTime),
73+
RenewTill: unixU32(c.tgtEnc.RenewTill),
74+
TicketFlags: flagsToUint32(c.tgtEnc.Flags),
75+
Ticket: c.tgtTicketRaw,
76+
}},
77+
}, nil
78+
}
79+
80+
// unixU32 returns t as Unix seconds in a uint32, or 0 for the zero time (rather
81+
// than the wrapped negative value uint32(-6795...) would give).
82+
func unixU32(t time.Time) uint32 {
83+
if t.IsZero() {
84+
return 0
85+
}
86+
return uint32(t.Unix())
87+
}
88+
89+
// flagsToUint32 converts a KerberosFlags BIT STRING (bit 0 = MSB of the first
90+
// octet) to the big-endian uint32 that the ccache ticket_flags field uses. A
91+
// short or over-long byte slice is padded/truncated to exactly 4 bytes.
92+
func flagsToUint32(flags asn1.BitString) uint32 {
93+
var b [4]byte
94+
copy(b[:], flags.Bytes)
95+
return binary.BigEndian.Uint32(b[:])
96+
}

0 commit comments

Comments
 (0)