diff options
Diffstat (limited to 'client')
-rw-r--r-- | client/client.go | 91 | ||||
-rw-r--r-- | client/messages.go | 69 | ||||
-rw-r--r-- | client/network.go | 92 |
3 files changed, 245 insertions, 7 deletions
diff --git a/client/client.go b/client/client.go index b7f5eee..1bfad5f 100644 --- a/client/client.go +++ b/client/client.go @@ -1,19 +1,96 @@ package client import ( + "context" + "fmt" + "sync" + "git.tardisproject.uk/tcmal/vault-plugin-kerberos-secrets/config" + krbClient "github.com/jcmturner/gokrb5/v8/client" + krbConfig "github.com/jcmturner/gokrb5/v8/config" + "github.com/jcmturner/gokrb5/v8/iana/nametype" + // "github.com/jcmturner/gokrb5/v8/kadmin" + krbMessages "github.com/jcmturner/gokrb5/v8/messages" + krbTypes "github.com/jcmturner/gokrb5/v8/types" ) -type client struct{} +type client struct { + *sync.Mutex -func ClientFromConfig(config *config.Config) (client, error) { - return client{}, nil + kCfg *krbConfig.Config + kClient *krbClient.Client } -func (c client) SetPassword(username string, password string) error { - return nil // TODO +func ClientFromConfig(config *config.Config) (client, error) { + kCfg := krbConfig.New() + kCfg.Realms = []krbConfig.Realm{ + { + Realm: config.Realm, + DefaultDomain: config.Realm, + KDC: config.KDC, + KPasswdServer: config.KPasswdServer, + AdminServer: []string{}, + MasterKDC: config.KDC, + }, + } + + kClient := krbClient.NewWithPassword(config.Username, config.Realm, config.Password, kCfg) + + return client{ + &sync.Mutex{}, + kCfg, + kClient, + }, nil } -func (c client) SetPasswordWithOld(username string, oldPassword, newPassword string) error { - return nil // TODO +func (c client) SetPassword(ctx context.Context, username string, password string) error { + c.Lock() + defer c.Unlock() + + if err := c.kClient.AffirmLogin(); err != nil { + return fmt.Errorf("error logging in as admin principal: %e", err) + } + + // Get a ticket for using kadmin/admin + cl := c.kClient + ASReq, err := krbMessages.NewASReqForChgPasswd(cl.Credentials.Domain(), cl.Config, cl.Credentials.CName()) + if err != nil { + return fmt.Errorf("error creating ticket request for kadmin: %s", err) + } + ASRep, err := cl.ASExchange(cl.Credentials.Domain(), ASReq, 0) + if err != nil { + return fmt.Errorf("error exchanging request for kadmin ticket: %s", err) + } + + // Construct the change passwd msg + msg, key, err := ChangePasswdMsg( + krbTypes.NewPrincipalName(nametype.KRB_NT_PRINCIPAL, username), + cl.Credentials.CName(), + cl.Credentials.Domain(), + password, + ASRep.Ticket, + ASRep.DecryptedEncPart.Key, + ) + + if err != nil { + return fmt.Errorf("error creating change passwd msg: %s", err) + } + + // Send it to kpasswd + r, err := sendToKAdmin(cl, msg) + if err != nil { + return fmt.Errorf("error communicating with kpasswd: %s", err) + } + + // Decrypt the result + if r.ResultCode != 0 { + return fmt.Errorf("error response from kadmin: code: %d; result: %s; krberror: %v", r.ResultCode, r.Result, r.KRBError) + } + + err = r.Decrypt(key) + if err != nil { + return fmt.Errorf("error decrypting result: %s", err) + } + + return nil } diff --git a/client/messages.go b/client/messages.go new file mode 100644 index 0000000..b3d2c4f --- /dev/null +++ b/client/messages.go @@ -0,0 +1,69 @@ +package client + +import ( + "github.com/jcmturner/gokrb5/v8/crypto" + "github.com/jcmturner/gokrb5/v8/kadmin" + "github.com/jcmturner/gokrb5/v8/krberror" + "github.com/jcmturner/gokrb5/v8/messages" + "github.com/jcmturner/gokrb5/v8/types" +) + +// ChangePasswdMsg generate a change password request and also return the key needed to decrypt the reply. +func ChangePasswdMsg(targetName types.PrincipalName, cname types.PrincipalName, realm, password string, tkt messages.Ticket, sessionKey types.EncryptionKey) (r kadmin.Request, k types.EncryptionKey, err error) { + // Create change password data struct and marshal to bytes + chgpasswd := kadmin.ChangePasswdData{ + NewPasswd: []byte(password), + TargName: targetName, + TargRealm: realm, + } + chpwdb, err := chgpasswd.Marshal() + if err != nil { + err = krberror.Errorf(err, krberror.KRBMsgError, "error marshaling change passwd data") + return + } + + // Generate authenticator + auth, err := types.NewAuthenticator(realm, cname) + if err != nil { + err = krberror.Errorf(err, krberror.KRBMsgError, "error generating new authenticator") + return + } + etype, err := crypto.GetEtype(sessionKey.KeyType) + if err != nil { + err = krberror.Errorf(err, krberror.KRBMsgError, "error generating subkey etype") + return + } + err = auth.GenerateSeqNumberAndSubKey(etype.GetETypeID(), etype.GetKeyByteSize()) + if err != nil { + err = krberror.Errorf(err, krberror.KRBMsgError, "error generating subkey") + return + } + k = auth.SubKey + + // Generate AP_REQ + APreq, err := messages.NewAPReq(tkt, sessionKey, auth) + if err != nil { + return + } + + // Form the KRBPriv encpart data + kp := messages.EncKrbPrivPart{ + UserData: chpwdb, + Timestamp: auth.CTime, + Usec: auth.Cusec, + SequenceNumber: auth.SeqNumber, + } + kpriv := messages.NewKRBPriv(kp) + + err = kpriv.EncryptEncPart(k) + if err != nil { + err = krberror.Errorf(err, krberror.EncryptingError, "error encrypting change passwd data") + return + } + + r = kadmin.Request{ + APREQ: APreq, + KRBPriv: kpriv, + } + return +} diff --git a/client/network.go b/client/network.go new file mode 100644 index 0000000..b29928f --- /dev/null +++ b/client/network.go @@ -0,0 +1,92 @@ +package client + +import ( + "encoding/binary" + "fmt" + "io" + "net" + "strings" + "time" + + krbClient "github.com/jcmturner/gokrb5/v8/client" + "github.com/jcmturner/gokrb5/v8/kadmin" +) + +// From here: https://github.com/jcmturner/gokrb5/blob/v8.4.4/v8/client/passwd.go#L51C1-L75C2 +// It would be really nice if this was public, but it isn't :( +func sendToKAdmin(cl *krbClient.Client, msg kadmin.Request) (r kadmin.Reply, err error) { + _, kps, err := cl.Config.GetKpasswdServers(cl.Credentials.Domain(), true) + if err != nil { + return + } + b, err := msg.Marshal() + if err != nil { + return + } + var rb []byte + rb, err = dialSendTCP(kps, b) + if err != nil { + return + } + err = r.Unmarshal(rb) + + return +} + +// Below are from here: https://github.com/jcmturner/gokrb5/blob/master/v8/client/network.go +// Likewise, it sucks that the change password API is so limited, and there is zero low-level exposure without copy-pasting code. +// dialSendTCP establishes a TCP connection to a KDC. +func dialSendTCP(kdcs map[int]string, b []byte) ([]byte, error) { + var errs []string + for i := 1; i <= len(kdcs); i++ { + conn, err := net.DialTimeout("tcp", kdcs[i], 5*time.Second) + if err != nil { + errs = append(errs, fmt.Sprintf("error establishing connection to %s: %v", kdcs[i], err)) + continue + } + if err := conn.SetDeadline(time.Now().Add(5 * time.Second)); err != nil { + errs = append(errs, fmt.Sprintf("error setting deadline on connection to %s: %v", kdcs[i], err)) + continue + } + // conn is guaranteed to be a TCPConn + rb, err := sendTCP(conn.(*net.TCPConn), b) + if err != nil { + errs = append(errs, fmt.Sprintf("error sending to %s: %v", kdcs[i], err)) + continue + } + return rb, nil + } + return nil, fmt.Errorf("error sending: %s", strings.Join(errs, "; ")) +} + +// sendTCP sends bytes to connection over TCP. +func sendTCP(conn *net.TCPConn, b []byte) ([]byte, error) { + defer conn.Close() + var r []byte + // RFC 4120 7.2.2 specifies the first 4 bytes indicate the length of the message in big endian order. + hb := make([]byte, 4, 4) + binary.BigEndian.PutUint32(hb, uint32(len(b))) + b = append(hb, b...) + + _, err := conn.Write(b) + if err != nil { + return r, fmt.Errorf("error sending to %s: %v", conn.RemoteAddr().String(), err) + } + + sh := make([]byte, 4, 4) + _, err = conn.Read(sh) + if err != nil { + return r, fmt.Errorf("error reading response size header: %v", err) + } + s := binary.BigEndian.Uint32(sh) + + rb := make([]byte, s, s) + _, err = io.ReadFull(conn, rb) + if err != nil { + return r, fmt.Errorf("error reading response: %v", err) + } + if len(rb) < 1 { + return r, fmt.Errorf("no response data from %s", conn.RemoteAddr().String()) + } + return rb, nil +} |