pkg/ssh/gssapi.go
package ssh
import (
"encoding/binary"
"fmt"
"strings"
"github.com/jcmturner/gokrb5/v8/client"
"github.com/jcmturner/gokrb5/v8/config"
"github.com/jcmturner/gokrb5/v8/credentials"
"github.com/jcmturner/gokrb5/v8/crypto"
"github.com/jcmturner/gokrb5/v8/gssapi"
"github.com/jcmturner/gokrb5/v8/iana/chksumtype"
"github.com/jcmturner/gokrb5/v8/iana/flags"
"github.com/jcmturner/gokrb5/v8/keytab"
"github.com/jcmturner/gokrb5/v8/messages"
"github.com/jcmturner/gokrb5/v8/spnego"
"github.com/jcmturner/gokrb5/v8/types"
)
type Krb5ClientState int
const (
ContextFlagREADY = 128
/* initiator states */
InitiatorStart Krb5ClientState = iota
InitiatorRestart
InitiatorWaitForMutal
InitiatorReady
)
func NewKrb5InitiatorClientWithPassword(username, password, krb5Conf string) (kcl Krb5InitiatorClient, err error) {
c, err := config.Load(krb5Conf)
if err != nil {
return
}
// Set to lookup KDCs in DNS
c.LibDefaults.DNSLookupKDC = true
c.LibDefaults.DNSLookupRealm = true
// Blank out the KDCs to ensure they are not being used
c.Realms = []config.Realm{}
defaultRealm := c.LibDefaults.DefaultRealm
cl := client.NewWithPassword(username, defaultRealm, password, c)
err = cl.Login()
if err != nil {
return
}
err = cl.AffirmLogin()
if err != nil {
return
}
return Krb5InitiatorClient{
client: cl,
state: InitiatorStart,
}, nil
}
func NewKrb5InitiatorClientWithKeytab(username string, krb5Conf, keytabConf string) (kcl Krb5InitiatorClient, err error) {
c, err := config.Load(krb5Conf)
if err != nil {
return
}
// Set to lookup KDCs in DNS
c.LibDefaults.DNSLookupKDC = true
c.LibDefaults.DNSLookupRealm = true
// Blank out the KDCs to ensure they are not being used
c.Realms = []config.Realm{}
// Init keytab from conf
cache, err := keytab.Load(keytabConf)
if err != nil {
return kcl, fmt.Errorf("unmarshal keytabConf failed: %w", err)
}
defaultRealm := c.LibDefaults.DefaultRealm
cl := client.NewWithKeytab(username, defaultRealm, cache, c)
if err != nil {
return
}
err = cl.Login()
if err != nil {
return
}
err = cl.AffirmLogin()
if err != nil {
return
}
return Krb5InitiatorClient{
client: cl,
state: InitiatorStart,
}, nil
}
func NewKrb5InitiatorClientWithCache(krb5Conf, cacheFile string) (kcl Krb5InitiatorClient, err error) {
c, err := config.Load(krb5Conf)
if err != nil {
return
}
// Set to lookup KDCs in DNS
c.LibDefaults.DNSLookupKDC = true
c.LibDefaults.DNSLookupRealm = true
// Blank out the KDCs to ensure they are not being used
c.Realms = []config.Realm{}
// Init krb5 client and login
cache, err := credentials.LoadCCache(cacheFile)
// https://stackoverflow.com/questions/58653482/what-is-the-default-kerberos-credential-cache-on-osx
if err != nil {
return
}
cl, err := client.NewFromCCache(cache, c)
if err != nil {
return
}
err = cl.Login()
if err != nil {
return
}
err = cl.AffirmLogin()
if err != nil {
return
}
return Krb5InitiatorClient{
client: cl,
state: InitiatorStart,
}, nil
}
type Krb5InitiatorClient struct {
state Krb5ClientState
client *client.Client
subkey types.EncryptionKey
}
// Create new authenticator checksum for kerberos MechToken
func (k *Krb5InitiatorClient) newAuthenticatorChksum(flags []int) []byte {
a := make([]byte, 24)
binary.LittleEndian.PutUint32(a[:4], 16)
for _, i := range flags {
if i == gssapi.ContextFlagDeleg {
x := make([]byte, 28-len(a))
a = append(a, x...)
}
f := binary.LittleEndian.Uint32(a[20:24])
f |= uint32(i)
binary.LittleEndian.PutUint32(a[20:24], f)
}
return a
}
func (k *Krb5InitiatorClient) InitSecContext(target string, token []byte, isGSSDelegCreds bool) ([]byte, bool, error) {
GSSAPIFlags := []int{
ContextFlagREADY,
gssapi.ContextFlagInteg,
gssapi.ContextFlagMutual,
}
if isGSSDelegCreds {
GSSAPIFlags = append(GSSAPIFlags, gssapi.ContextFlagDeleg)
}
APOptions := []int{flags.APOptionMutualRequired}
switch k.state {
case InitiatorStart, InitiatorRestart:
newTarget := strings.ReplaceAll(target, "@", "/")
tkt, sessionKey, err := k.client.GetServiceTicket(newTarget)
if err != nil {
return []byte{}, false, err
}
krb5Token, err := spnego.NewKRB5TokenAPREQ(k.client, tkt, sessionKey, GSSAPIFlags, APOptions)
if err != nil {
return nil, false, fmt.Errorf("error generating new kerberos 5 token: %w", err)
}
creds := k.client.Credentials
auth, err := types.NewAuthenticator(creds.Domain(), creds.CName())
if err != nil {
return nil, false, fmt.Errorf("error generating new authenticator: %w", err)
}
auth.Cksum = types.Checksum{
CksumType: chksumtype.GSSAPI,
Checksum: k.newAuthenticatorChksum(GSSAPIFlags),
}
etype, _ := crypto.GetEtype(sessionKey.KeyType)
if err := auth.GenerateSeqNumberAndSubKey(sessionKey.KeyType, etype.GetKeyByteSize()); err != nil {
return nil, false, err
}
k.subkey = auth.SubKey
APReq, err := messages.NewAPReq(
tkt,
sessionKey,
auth,
)
if err != nil {
return nil, false, fmt.Errorf("error generating NewAPReq: %w", err)
}
for _, o := range APOptions {
types.SetFlag(&APReq.APOptions, o)
}
krb5Token.APReq = APReq
outToken, err := krb5Token.Marshal()
if err != nil {
fmt.Println(err)
return []byte{}, false, err
}
k.state = InitiatorWaitForMutal
return outToken, true, nil
case InitiatorWaitForMutal:
var krb5Token spnego.KRB5Token
if err := krb5Token.Unmarshal(token); err != nil {
err := fmt.Errorf("unmarshal APRep token failed: %w", err)
return []byte{}, false, err
}
//var enc messages.EncAPRepPart
//err2 := enc.Unmarshal(krb5Token.APRep.EncPart.Cipher)
//fmt.Printf("err2: %#v, enc: %#v\n", err2, enc)
k.state = InitiatorReady
return []byte{}, false, nil
case InitiatorReady:
return nil, false, fmt.Errorf("called one time too many, client has already been %d", k.state)
default:
return nil, false, fmt.Errorf("invalid state %d", k.state)
}
}
func (k *Krb5InitiatorClient) GetMIC(micFiled []byte) ([]byte, error) {
micToken, err := gssapi.NewInitiatorMICToken(micFiled, k.subkey)
if err != nil {
return nil, err
}
token, err := micToken.Marshal()
if err != nil {
return nil, err
}
return token, nil
}
func (k *Krb5InitiatorClient) DeleteSecContext() error {
k.client.Destroy()
return nil
}