summaryrefslogtreecommitdiff
path: root/client
diff options
context:
space:
mode:
Diffstat (limited to 'client')
-rw-r--r--client/client.go91
-rw-r--r--client/messages.go69
-rw-r--r--client/network.go92
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
+}