Skip to content

Commit 7273db4

Browse files
committed
Add MS-SFU PA-FOR-USER and the S4U2Self client exchange
Phase 6 (first part) of the native Kerberos v5 work: Service-for-User constrained delegation ([MS-SFU]), layered on RFC 4120's TGS exchange. - New sfu package: BuildPAForUser / ParsePAForUser / VerifyPAForUser for the PA-FOR-USER padata (type 129). The keyed checksum over name-type(LE32)||name-strings||realm||auth-package is computed at key usage KERB_NON_KERB_CKSUM_SALT (17). [MS-SFU] 2.2.1 specifies HMAC-MD5, which fits an RC4 TGT session key; for AES session keys the checksum type paired with the key's enctype is used so it interoperates with modern Active Directory. - KerberosClient.S4U2Self(user, realm) requests a service ticket to the client's own principal on behalf of an arbitrary user, using PA-FOR-USER over the service's TGT. - Supporting helpers: crypto.ChecksumTypeForEType (etype -> paired checksum type), messages.ExplicitGeneralString (exported GeneralString-in-context helper for the new package), and iana.PAForUser. sfu is unit-tested: build/parse round-trip, the checksum type follows the session-key etype, and tampering with the impersonated identity or the key is detected. S4U2Proxy is deferred: it needs correct additional-tickets marshaling in the TGS-REQ (the existing KDCReqBody.AdditTickets []Ticket field would not emit APPLICATION[1] tickets correctly), which will be fixed alongside it. stdlib-only.
1 parent e1d1cab commit 7273db4

6 files changed

Lines changed: 354 additions & 0 deletions

File tree

network/kerberos/v5/crypto/checksum.go

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,27 @@ func VerifyChecksum(cksumType int, key []byte, usage int, data, want []byte) boo
4747
return hmac.Equal(got, want)
4848
}
4949

50+
// ChecksumTypeForEType returns the checksum type paired with an encryption type
51+
// (RFC 3961/3962/8009/4757), i.e. the checksum an authenticator or PA-FOR-USER
52+
// should use when keyed with a key of that etype. Reports false for unsupported
53+
// etypes.
54+
func ChecksumTypeForEType(etype int) (int, bool) {
55+
switch etype {
56+
case iana.ETypeRC4HMAC:
57+
return iana.CksumTypeHMACMD5, true
58+
case iana.ETypeAES128CTSHMACSHA196:
59+
return iana.CksumTypeHMACSHA196AES128, true
60+
case iana.ETypeAES256CTSHMACSHA196:
61+
return iana.CksumTypeHMACSHA196AES256, true
62+
case iana.ETypeAES128CTSHMACSHA256:
63+
return iana.CksumTypeHMACSHA256128AES128, true
64+
case iana.ETypeAES256CTSHMACSHA384:
65+
return iana.CksumTypeHMACSHA384192AES256, true
66+
default:
67+
return 0, false
68+
}
69+
}
70+
5071
// aesSHA1Checksum implements the RFC 3961 simplified-profile checksum used by
5172
// the AES-SHA1 enctypes (cksumtype 15/16): Kc = DK(base-key, usage | 0x99),
5273
// then HMAC-SHA1(Kc, data) truncated to 96 bits (12 bytes). keyLen is the AES

network/kerberos/v5/iana/constants.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,7 @@ const (
9696
PAPKASReq = 16 // PA-PK-AS-REQ (PKINIT, MS-PKCA)
9797
PAPKASRep = 17 // PA-PK-AS-REP (PKINIT, MS-PKCA)
9898
PAETypeInfo2 = 19 // PA-ETYPE-INFO2 (replaces PA-ETYPE-INFO)
99+
PAForUser = 129 // PA-FOR-USER (MS-SFU S4U2Self)
99100
PASvrReferralInfo = 20 // PA-SVR-REFERRAL-INFO (RFC 6806)
100101
PAPACRequest = 128 // KERB-PA-PAC-REQUEST (MS-KILE)
101102
PAFXCookie = 133 // PA-FX-COOKIE (RFC 6113 FAST)

network/kerberos/v5/messages/types.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,14 @@ func realmExplicit(tag int, s string) asn1.RawValue {
102102
return asn1.RawValue{Class: asn1.ClassContextSpecific, Tag: tag, IsCompound: true, Bytes: gsBytes}
103103
}
104104

105+
// ExplicitGeneralString returns s encoded as an ASN.1 [tag] EXPLICIT
106+
// { GeneralString } context element. Exported for other packages (e.g. the
107+
// MS-SFU PA-FOR-USER builder) that must emit GeneralString fields the standard
108+
// library would otherwise encode as PrintableString.
109+
func ExplicitGeneralString(tag int, s string) asn1.RawValue {
110+
return realmExplicit(tag, s)
111+
}
112+
105113
// PrincipalNameMarshal is the wire representation of PrincipalName for marshaling.
106114
// It uses []asn1.RawValue (GeneralString) instead of []string, which Go's asn1
107115
// would incorrectly encode as PrintableString.

network/kerberos/v5/s4u.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+
"strings"
6+
"time"
7+
8+
kerbcrypto "github.com/TheManticoreProject/Manticore/network/kerberos/v5/crypto"
9+
"github.com/TheManticoreProject/Manticore/network/kerberos/v5/messages"
10+
"github.com/TheManticoreProject/Manticore/network/kerberos/v5/sfu"
11+
)
12+
13+
// S4U2Self performs the MS-SFU S4U2Self exchange: the service (this client's
14+
// principal), holding its own TGT, requests a service ticket to itself on behalf
15+
// of the user (impersonateUser, impersonateRealm), identified only by name. It
16+
// is the first half of constrained delegation and, on its own, a way to obtain a
17+
// usable service ticket (with the target user's PAC) for any user without their
18+
// secret — subject to the account's delegation configuration.
19+
//
20+
// GetTGT must have succeeded first (the client must hold its service TGT). If
21+
// impersonateRealm is empty the client's realm is used. Returns the service
22+
// ticket, its raw APPLICATION[1] bytes, and the ticket session key.
23+
func (c *KerberosClient) S4U2Self(impersonateUser, impersonateRealm string) (messages.Ticket, []byte, []byte, error) {
24+
if !c.hasTGT {
25+
return messages.Ticket{}, nil, nil, fmt.Errorf("kerberos: no TGT: call GetTGT first")
26+
}
27+
if impersonateUser == "" {
28+
return messages.Ticket{}, nil, nil, fmt.Errorf("kerberos: S4U2Self requires a user to impersonate")
29+
}
30+
if impersonateRealm == "" {
31+
impersonateRealm = c.realm
32+
} else {
33+
impersonateRealm = strings.ToUpper(impersonateRealm)
34+
}
35+
36+
// PA-FOR-USER identifies the impersonated user, protected by a checksum keyed
37+
// with this service's TGT session key.
38+
userName := messages.PrincipalName{
39+
NameType: messages.NameTypePrincipal,
40+
NameString: []string{impersonateUser},
41+
}
42+
paForUser, err := sfu.BuildPAForUser(userName, impersonateRealm, c.sessionKey, c.sessionEType)
43+
if err != nil {
44+
return messages.Ticket{}, nil, nil, fmt.Errorf("kerberos: build PA-FOR-USER: %w", err)
45+
}
46+
47+
// AP-REQ over the service's own TGT.
48+
apReqBytes, err := c.buildAPReq()
49+
if err != nil {
50+
return messages.Ticket{}, nil, nil, fmt.Errorf("kerberos: build AP-REQ: %w", err)
51+
}
52+
53+
// The requested server is the service itself (its own account).
54+
self := messages.PrincipalName{
55+
NameType: messages.NameTypePrincipal,
56+
NameString: []string{c.username},
57+
}
58+
59+
nonce := randomNonce()
60+
tgsReq := &messages.TGSReq{
61+
PVNO: messages.KerberosV5,
62+
MsgType: messages.MsgTypeTGSReq,
63+
PAData: []messages.PAData{
64+
{PADataType: messages.PATGSReq, PADataValue: apReqBytes},
65+
paForUser,
66+
{PADataType: messages.PAPACRequest, PADataValue: []byte{0x30, 0x05, 0xa0, 0x03, 0x01, 0x01, 0xff}},
67+
},
68+
ReqBody: messages.KDCReqBody{
69+
KDCOptions: kdcOptionsForTGSReq(),
70+
Realm: c.realm,
71+
SName: self,
72+
Till: time.Now().UTC().Add(24 * time.Hour),
73+
Nonce: nonce,
74+
EType: []int{
75+
messages.ETypeAES256CTSHMACSHA196,
76+
messages.ETypeAES128CTSHMACSHA196,
77+
messages.ETypeRC4HMAC,
78+
},
79+
},
80+
}
81+
82+
tgsReqBytes, err := tgsReq.Marshal()
83+
if err != nil {
84+
return messages.Ticket{}, nil, nil, fmt.Errorf("kerberos: marshal S4U2Self 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: S4U2Self 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 S4U2Self 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 S4U2Self 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 S4U2Self EncTGSRepPart: %w", err)
108+
}
109+
if encTGSRep.Nonce != nonce {
110+
return messages.Ticket{}, nil, nil, fmt.Errorf("kerberos: S4U2Self 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/sfu/foruser.go

Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
// Package sfu implements the Microsoft Service for User and Constrained
2+
// Delegation extensions ([MS-SFU]) that layer onto RFC 4120's TGS exchange:
3+
// PA-FOR-USER (S4U2Self) and the padata used for S4U2Proxy. Like the rest of
4+
// the MS extensions, these ride the RFC's padata / additional-tickets extension
5+
// points — the TGS-REQ/REP messages are unchanged.
6+
package sfu
7+
8+
import (
9+
"encoding/asn1"
10+
"encoding/binary"
11+
"fmt"
12+
13+
kerbcrypto "github.com/TheManticoreProject/Manticore/network/kerberos/v5/crypto"
14+
"github.com/TheManticoreProject/Manticore/network/kerberos/v5/iana"
15+
"github.com/TheManticoreProject/Manticore/network/kerberos/v5/messages"
16+
)
17+
18+
// authPackageKerberos is the fixed auth-package value in PA-FOR-USER ([MS-SFU]
19+
// 2.2.1): the authentication mechanism name, which MUST be "Kerberos".
20+
const authPackageKerberos = "Kerberos"
21+
22+
// s4uByteArray builds the S4UByteArray checksummed by PA-FOR-USER ([MS-SFU]
23+
// 2.2.1): the userName.name-type as a 4-byte little-endian integer, followed by
24+
// each userName.name-string component, then userRealm, then auth-package — with
25+
// no null terminators.
26+
func s4uByteArray(userName messages.PrincipalName, userRealm, authPackage string) []byte {
27+
buf := make([]byte, 4)
28+
binary.LittleEndian.PutUint32(buf, uint32(userName.NameType))
29+
for _, s := range userName.NameString {
30+
buf = append(buf, []byte(s)...)
31+
}
32+
buf = append(buf, []byte(userRealm)...)
33+
buf = append(buf, []byte(authPackage)...)
34+
return buf
35+
}
36+
37+
// BuildPAForUser builds the PA-FOR-USER pre-authentication data element used in
38+
// an S4U2Self TGS-REQ, on behalf of the user (userName, userRealm). The keyed
39+
// checksum is computed with the requesting service's TGT session key at key
40+
// usage KERB_NON_KERB_CKSUM_SALT (17).
41+
//
42+
// [MS-SFU] 2.2.1 specifies KERB_CHECKSUM_HMAC_MD5, which is correct when the TGT
43+
// session key is RC4. For AES TGT session keys, modern KDCs expect the checksum
44+
// type paired with the session key's enctype; this function selects that type
45+
// from sessionKeyEType so it interoperates with current Active Directory.
46+
func BuildPAForUser(userName messages.PrincipalName, userRealm string, sessionKey []byte, sessionKeyEType int) (messages.PAData, error) {
47+
cksumType, ok := kerbcrypto.ChecksumTypeForEType(sessionKeyEType)
48+
if !ok {
49+
return messages.PAData{}, fmt.Errorf("sfu: unsupported session-key etype %d", sessionKeyEType)
50+
}
51+
buf := s4uByteArray(userName, userRealm, authPackageKerberos)
52+
sum, err := kerbcrypto.GetChecksum(cksumType, sessionKey, iana.KeyUsageKerbNonKerbCksumSalt, buf)
53+
if err != nil {
54+
return messages.PAData{}, fmt.Errorf("sfu: PA-FOR-USER checksum: %w", err)
55+
}
56+
57+
userNameElem, err := asn1.MarshalWithParams(messages.MarshalPrincipalName(userName), "explicit,tag:0")
58+
if err != nil {
59+
return messages.PAData{}, err
60+
}
61+
userRealmElem, err := asn1.Marshal(messages.ExplicitGeneralString(1, userRealm))
62+
if err != nil {
63+
return messages.PAData{}, err
64+
}
65+
cksumElem, err := asn1.MarshalWithParams(messages.Checksum{CKSumType: cksumType, Checksum: sum}, "explicit,tag:2")
66+
if err != nil {
67+
return messages.PAData{}, err
68+
}
69+
authPkgElem, err := asn1.Marshal(messages.ExplicitGeneralString(3, authPackageKerberos))
70+
if err != nil {
71+
return messages.PAData{}, err
72+
}
73+
74+
body := make([]byte, 0, len(userNameElem)+len(userRealmElem)+len(cksumElem)+len(authPkgElem))
75+
body = append(body, userNameElem...)
76+
body = append(body, userRealmElem...)
77+
body = append(body, cksumElem...)
78+
body = append(body, authPkgElem...)
79+
80+
seq, err := asn1.Marshal(asn1.RawValue{Class: asn1.ClassUniversal, Tag: asn1.TagSequence, IsCompound: true, Bytes: body})
81+
if err != nil {
82+
return messages.PAData{}, err
83+
}
84+
return messages.PAData{PADataType: iana.PAForUser, PADataValue: seq}, nil
85+
}
86+
87+
// PAForUser is the decoded content of a PA-FOR-USER element.
88+
type PAForUser struct {
89+
UserName messages.PrincipalName
90+
UserRealm string
91+
Cksum messages.Checksum
92+
AuthPackage string
93+
}
94+
95+
type paForUserInner struct {
96+
UserName messages.PrincipalName `asn1:"explicit,tag:0"`
97+
UserRealm string `asn1:"explicit,tag:1,generalstring"`
98+
Cksum messages.Checksum `asn1:"explicit,tag:2"`
99+
AuthPackage string `asn1:"explicit,tag:3,generalstring"`
100+
}
101+
102+
// ParsePAForUser decodes a PA-FOR-USER padata-value.
103+
func ParsePAForUser(b []byte) (*PAForUser, error) {
104+
var inner paForUserInner
105+
if _, err := asn1.Unmarshal(b, &inner); err != nil {
106+
return nil, fmt.Errorf("sfu: parse PA-FOR-USER: %w", err)
107+
}
108+
return &PAForUser{
109+
UserName: inner.UserName,
110+
UserRealm: inner.UserRealm,
111+
Cksum: inner.Cksum,
112+
AuthPackage: inner.AuthPackage,
113+
}, nil
114+
}
115+
116+
// VerifyPAForUser recomputes the PA-FOR-USER checksum with the given TGT session
117+
// key and reports whether it matches, defending against tampering with the
118+
// impersonated identity.
119+
func VerifyPAForUser(p *PAForUser, sessionKey []byte, sessionKeyEType int) bool {
120+
cksumType, ok := kerbcrypto.ChecksumTypeForEType(sessionKeyEType)
121+
if !ok || cksumType != p.Cksum.CKSumType {
122+
return false
123+
}
124+
buf := s4uByteArray(p.UserName, p.UserRealm, p.AuthPackage)
125+
return kerbcrypto.VerifyChecksum(cksumType, sessionKey, iana.KeyUsageKerbNonKerbCksumSalt, buf, p.Cksum.Checksum)
126+
}
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
package sfu
2+
3+
import (
4+
"bytes"
5+
"testing"
6+
7+
"github.com/TheManticoreProject/Manticore/network/kerberos/v5/iana"
8+
"github.com/TheManticoreProject/Manticore/network/kerberos/v5/messages"
9+
)
10+
11+
func impersonated() messages.PrincipalName {
12+
return messages.PrincipalName{NameType: iana.NameTypePrincipal, NameString: []string{"administrator"}}
13+
}
14+
15+
func TestBuildParsePAForUserRoundtrip(t *testing.T) {
16+
key := bytes.Repeat([]byte{0x42}, 32) // AES256 TGT session key
17+
pa, err := BuildPAForUser(impersonated(), "CORP.LOCAL", key, iana.ETypeAES256CTSHMACSHA196)
18+
if err != nil {
19+
t.Fatalf("BuildPAForUser: %v", err)
20+
}
21+
if pa.PADataType != iana.PAForUser {
22+
t.Errorf("padata type = %d, want %d", pa.PADataType, iana.PAForUser)
23+
}
24+
25+
p, err := ParsePAForUser(pa.PADataValue)
26+
if err != nil {
27+
t.Fatalf("ParsePAForUser: %v", err)
28+
}
29+
if len(p.UserName.NameString) != 1 || p.UserName.NameString[0] != "administrator" {
30+
t.Errorf("userName mismatch: %+v", p.UserName)
31+
}
32+
if p.UserRealm != "CORP.LOCAL" {
33+
t.Errorf("userRealm = %q", p.UserRealm)
34+
}
35+
if p.AuthPackage != "Kerberos" {
36+
t.Errorf("auth-package = %q, want Kerberos", p.AuthPackage)
37+
}
38+
if p.Cksum.CKSumType != iana.CksumTypeHMACSHA196AES256 {
39+
t.Errorf("cksum type = %d, want %d (paired with AES256)", p.Cksum.CKSumType, iana.CksumTypeHMACSHA196AES256)
40+
}
41+
if !VerifyPAForUser(p, key, iana.ETypeAES256CTSHMACSHA196) {
42+
t.Error("VerifyPAForUser rejected a valid element")
43+
}
44+
}
45+
46+
func TestPAForUserChecksumTypeFollowsSessionKey(t *testing.T) {
47+
// RC4 session key -> KERB_CHECKSUM_HMAC_MD5 (the [MS-SFU] literal type).
48+
rc4Key := bytes.Repeat([]byte{0x11}, 16)
49+
pa, err := BuildPAForUser(impersonated(), "CORP.LOCAL", rc4Key, iana.ETypeRC4HMAC)
50+
if err != nil {
51+
t.Fatal(err)
52+
}
53+
p, _ := ParsePAForUser(pa.PADataValue)
54+
if p.Cksum.CKSumType != iana.CksumTypeHMACMD5 {
55+
t.Errorf("RC4 session key should use HMAC-MD5 checksum (-138), got %d", p.Cksum.CKSumType)
56+
}
57+
if !VerifyPAForUser(p, rc4Key, iana.ETypeRC4HMAC) {
58+
t.Error("RC4 PA-FOR-USER did not verify")
59+
}
60+
}
61+
62+
func TestVerifyPAForUserDetectsTamper(t *testing.T) {
63+
key := bytes.Repeat([]byte{0x42}, 32)
64+
pa, _ := BuildPAForUser(impersonated(), "CORP.LOCAL", key, iana.ETypeAES256CTSHMACSHA196)
65+
p, _ := ParsePAForUser(pa.PADataValue)
66+
67+
// Change the impersonated user; the checksum must no longer match.
68+
p.UserName.NameString[0] = "guest"
69+
if VerifyPAForUser(p, key, iana.ETypeAES256CTSHMACSHA196) {
70+
t.Error("VerifyPAForUser accepted a tampered impersonation target")
71+
}
72+
73+
// Wrong session key must also fail.
74+
p2, _ := ParsePAForUser(pa.PADataValue)
75+
if VerifyPAForUser(p2, bytes.Repeat([]byte{0x99}, 32), iana.ETypeAES256CTSHMACSHA196) {
76+
t.Error("VerifyPAForUser accepted the wrong session key")
77+
}
78+
}
79+
80+
func TestBuildPAForUserRejectsBadEType(t *testing.T) {
81+
if _, err := BuildPAForUser(impersonated(), "R", bytes.Repeat([]byte{1}, 16), 999); err == nil {
82+
t.Error("expected error for unsupported session-key etype")
83+
}
84+
}

0 commit comments

Comments
 (0)