kubenetworks/kubevpn

View on GitHub
pkg/ssh/gssapi.go

Summary

Maintainability
A
3 hrs
Test Coverage
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
}