Skip to content

Commit 2071d0f

Browse files
committed
Add user-to-user (U2U) TGS exchange
Adds KerberosClient.GetTGSU2U: a user-to-user service-ticket request (RFC 4120 ENC-TKT-IN-SKEY) that presents the target user's TGT in additional-tickets and sets the ENC-TKT-IN-SKEY KDC option, so the KDC issues a ticket encrypted under the target's TGT session key rather than a long-term key. Builds on the additional-tickets encoding fixed for S4U2Proxy. The request construction is factored into buildU2UTGSReq and unit-tested without a KDC: the test confirms ENC-TKT-IN-SKEY (bit 28) is set and the target TGT is carried verbatim as an APPLICATION[1] ticket in the [11] additional-tickets field. stdlib-only; kerberos tests, go vet, and go build ./... are green.
1 parent 0ec37f6 commit 2071d0f

3 files changed

Lines changed: 218 additions & 0 deletions

File tree

network/kerberos/v5/client.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -498,6 +498,7 @@ const (
498498
kdcOptionCanonicalize = 15 // byte 1, 0x01 (RFC 6806)
499499
kdcOptionCNameInAddlTkt = 14 // byte 1, 0x02 (MS-SFU S4U2Proxy)
500500
kdcOptionRenewableOK = 27 // byte 3, 0x10
501+
kdcOptionEncTktInSKey = 28 // byte 3, 0x08 (user-to-user)
501502
)
502503

503504
// encodeKDCOptions packs a list of bit positions into a 32-bit BitString

network/kerberos/v5/u2u.go

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
package kerberos
2+
3+
import (
4+
"fmt"
5+
"time"
6+
7+
kerbcrypto "github.com/TheManticoreProject/Manticore/network/kerberos/v5/crypto"
8+
"github.com/TheManticoreProject/Manticore/network/kerberos/v5/messages"
9+
)
10+
11+
// buildU2UTGSReq constructs the TGS-REQ for a user-to-user exchange: a normal
12+
// PA-TGS-REQ (AP-REQ over the client's TGT), the ENC-TKT-IN-SKEY KDC option, the
13+
// target user's TGT in additional-tickets, and the target user as sname. It is
14+
// separated from GetTGSU2U so the request shape can be tested without a KDC.
15+
func (c *KerberosClient) buildU2UTGSReq(targetUser, targetRealm string, targetTGTRaw []byte, nonce int) (*messages.TGSReq, error) {
16+
apReqBytes, err := c.buildAPReq()
17+
if err != nil {
18+
return nil, fmt.Errorf("kerberos: build AP-REQ: %w", err)
19+
}
20+
sname := messages.PrincipalName{
21+
NameType: messages.NameTypePrincipal,
22+
NameString: []string{targetUser},
23+
}
24+
realm := c.realm
25+
if targetRealm != "" {
26+
realm = targetRealm
27+
}
28+
return &messages.TGSReq{
29+
PVNO: messages.KerberosV5,
30+
MsgType: messages.MsgTypeTGSReq,
31+
PAData: []messages.PAData{
32+
{PADataType: messages.PATGSReq, PADataValue: apReqBytes},
33+
},
34+
ReqBody: messages.KDCReqBody{
35+
KDCOptions: encodeKDCOptions(
36+
kdcOptionForwardable,
37+
kdcOptionRenewable,
38+
kdcOptionCanonicalize,
39+
kdcOptionEncTktInSKey,
40+
),
41+
Realm: realm,
42+
SName: sname,
43+
Till: time.Now().UTC().Add(24 * time.Hour),
44+
Nonce: nonce,
45+
EType: []int{
46+
messages.ETypeAES256CTSHMACSHA196,
47+
messages.ETypeAES128CTSHMACSHA196,
48+
messages.ETypeRC4HMAC,
49+
},
50+
AdditTicketsRaw: [][]byte{targetTGTRaw},
51+
},
52+
}, nil
53+
}
54+
55+
// GetTGSU2U performs a user-to-user (U2U) TGS exchange: it requests a service
56+
// ticket to the target user (targetUser in targetRealm, or the client's realm
57+
// if empty), presenting the target user's TGT (targetTGTRaw, raw APPLICATION[1]
58+
// bytes) in additional-tickets with the ENC-TKT-IN-SKEY option. The KDC issues a
59+
// ticket whose enc-part is encrypted with the session key of the target's TGT
60+
// (not a long-term key), which the target verifies via an AP-REQ carrying the
61+
// USE-SESSION-KEY option.
62+
//
63+
// GetTGT must have succeeded first (for the client's own TGT). Returns the
64+
// service ticket, its raw bytes, and the ticket session key.
65+
func (c *KerberosClient) GetTGSU2U(targetUser, targetRealm string, targetTGTRaw []byte) (messages.Ticket, []byte, []byte, error) {
66+
if !c.hasTGT {
67+
return messages.Ticket{}, nil, nil, fmt.Errorf("kerberos: no TGT: call GetTGT first")
68+
}
69+
if targetUser == "" {
70+
return messages.Ticket{}, nil, nil, fmt.Errorf("kerberos: U2U requires a target user")
71+
}
72+
if len(targetTGTRaw) == 0 {
73+
return messages.Ticket{}, nil, nil, fmt.Errorf("kerberos: U2U requires the target user's TGT")
74+
}
75+
76+
nonce := randomNonce()
77+
tgsReq, err := c.buildU2UTGSReq(targetUser, targetRealm, targetTGTRaw, nonce)
78+
if err != nil {
79+
return messages.Ticket{}, nil, nil, err
80+
}
81+
82+
tgsReqBytes, err := tgsReq.Marshal()
83+
if err != nil {
84+
return messages.Ticket{}, nil, nil, fmt.Errorf("kerberos: marshal U2U TGS-REQ: %w", err)
85+
}
86+
resp, err := kdcSend(c.kdcHost, defaultKDCPort, tgsReqBytes)
87+
if err != nil {
88+
return messages.Ticket{}, nil, nil, err
89+
}
90+
91+
var krbErr messages.KRBError
92+
if _, parseErr := krbErr.Unmarshal(resp); parseErr == nil {
93+
return messages.Ticket{}, nil, nil, fmt.Errorf("kerberos: U2U error %d: %s", krbErr.ErrorCode, krbErr.EText)
94+
}
95+
96+
var tgsRep messages.TGSRep
97+
if _, err := tgsRep.Unmarshal(resp); err != nil {
98+
return messages.Ticket{}, nil, nil, fmt.Errorf("kerberos: parse U2U TGS-REP: %w", err)
99+
}
100+
101+
encPlain, err := kerbcrypto.Decrypt(c.sessionEType, c.sessionKey, kerbcrypto.KeyUsageTGSRepEncSessionKey, tgsRep.EncPart.Cipher)
102+
if err != nil {
103+
return messages.Ticket{}, nil, nil, fmt.Errorf("kerberos: decrypt U2U TGS-REP enc-part: %w", err)
104+
}
105+
var encTGSRep messages.EncTGSRepPart
106+
if _, err := encTGSRep.Unmarshal(encPlain); err != nil {
107+
return messages.Ticket{}, nil, nil, fmt.Errorf("kerberos: parse U2U EncTGSRepPart: %w", err)
108+
}
109+
if encTGSRep.Nonce != nonce {
110+
return messages.Ticket{}, nil, nil, fmt.Errorf("kerberos: U2U nonce mismatch: got %d, want %d", encTGSRep.Nonce, nonce)
111+
}
112+
113+
return tgsRep.Ticket, tgsRep.TicketRaw, encTGSRep.Key.KeyValue, nil
114+
}

network/kerberos/v5/u2u_test.go

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
package kerberos
2+
3+
import (
4+
"bytes"
5+
"encoding/asn1"
6+
"testing"
7+
8+
"github.com/TheManticoreProject/Manticore/network/kerberos/v5/messages"
9+
)
10+
11+
// seqChild parses a SEQUENCE TLV and returns the first element with the given
12+
// class/tag.
13+
func seqChild(t *testing.T, seqTLV []byte, class, tag int) asn1.RawValue {
14+
t.Helper()
15+
var seq asn1.RawValue
16+
if _, err := asn1.Unmarshal(seqTLV, &seq); err != nil {
17+
t.Fatalf("parse SEQUENCE: %v", err)
18+
}
19+
rest := seq.Bytes
20+
for len(rest) > 0 {
21+
var e asn1.RawValue
22+
r, err := asn1.Unmarshal(rest, &e)
23+
if err != nil {
24+
t.Fatalf("iterate: %v", err)
25+
}
26+
if e.Class == class && e.Tag == tag {
27+
return e
28+
}
29+
rest = r
30+
}
31+
t.Fatalf("no element class=%d tag=%d", class, tag)
32+
return asn1.RawValue{}
33+
}
34+
35+
func TestU2URequestShape(t *testing.T) {
36+
c := fakeTGTClient(t) // from export_test.go: a client with a populated TGT
37+
38+
// The target user's TGT (any APPLICATION[1] ticket for this structural test).
39+
targetTkt := messages.Ticket{
40+
TktVno: messages.KerberosV5,
41+
Realm: "CORP.LOCAL",
42+
SName: messages.PrincipalName{NameType: messages.NameTypeSRVInst, NameString: []string{"krbtgt", "CORP.LOCAL"}},
43+
EncPart: messages.EncryptedData{EType: messages.ETypeAES256CTSHMACSHA196, Cipher: bytes.Repeat([]byte{0x7a}, 16)},
44+
}
45+
targetTGTRaw, err := targetTkt.Marshal()
46+
if err != nil {
47+
t.Fatal(err)
48+
}
49+
50+
req, err := c.buildU2UTGSReq("victim", "", targetTGTRaw, 4242)
51+
if err != nil {
52+
t.Fatalf("buildU2UTGSReq: %v", err)
53+
}
54+
wire, err := req.Marshal()
55+
if err != nil {
56+
t.Fatalf("marshal: %v", err)
57+
}
58+
59+
// APPLICATION[12] -> inner SEQUENCE -> [4] body.
60+
var app asn1.RawValue
61+
if _, err := asn1.Unmarshal(wire, &app); err != nil {
62+
t.Fatal(err)
63+
}
64+
if app.Class != asn1.ClassApplication || app.Tag != messages.MsgTypeTGSReq {
65+
t.Fatalf("outer tag: class=%d tag=%d", app.Class, app.Tag)
66+
}
67+
bodyElem := seqChild(t, app.Bytes, asn1.ClassContextSpecific, 4)
68+
69+
// KDCOptions [0]: BIT STRING; ENC-TKT-IN-SKEY is bit 28 -> byte 3, 0x08.
70+
optElem := seqChild(t, bodyElem.Bytes, asn1.ClassContextSpecific, 0)
71+
var flags asn1.BitString
72+
if _, err := asn1.Unmarshal(optElem.Bytes, &flags); err != nil {
73+
t.Fatalf("parse KDCOptions: %v", err)
74+
}
75+
if flags.At(kdcOptionEncTktInSKey) != 1 {
76+
t.Errorf("ENC-TKT-IN-SKEY (bit 28) not set; bytes=% X", flags.Bytes)
77+
}
78+
if flags.At(kdcOptionForwardable) != 1 {
79+
t.Errorf("forwardable (bit 1) not set")
80+
}
81+
82+
// additional-tickets [11] -> SEQUENCE OF -> first is APPLICATION[1], and it
83+
// must be the target TGT verbatim.
84+
addl := seqChild(t, bodyElem.Bytes, asn1.ClassContextSpecific, 11)
85+
tkt := seqChild(t, addl.Bytes, asn1.ClassApplication, 1)
86+
if !bytes.Equal(tkt.FullBytes, targetTGTRaw) {
87+
t.Errorf("additional ticket is not the target TGT verbatim")
88+
}
89+
}
90+
91+
func TestGetTGSU2UValidatesInputs(t *testing.T) {
92+
c := NewClient("svc", "corp.local", "10.0.0.1").WithPassword("x")
93+
if _, _, _, err := c.GetTGSU2U("victim", "", []byte{1}); err == nil {
94+
t.Error("expected error without a TGT")
95+
}
96+
c2 := fakeTGTClient(t)
97+
if _, _, _, err := c2.GetTGSU2U("", "", []byte{1}); err == nil {
98+
t.Error("expected error with empty target user")
99+
}
100+
if _, _, _, err := c2.GetTGSU2U("victim", "", nil); err == nil {
101+
t.Error("expected error with no target TGT")
102+
}
103+
}

0 commit comments

Comments
 (0)