summaryrefslogtreecommitdiff
path: root/client
diff options
context:
space:
mode:
Diffstat (limited to 'client')
-rw-r--r--client/client.go77
-rw-r--r--client/cmd.go131
-rw-r--r--client/messages.go69
-rw-r--r--client/network.go92
4 files changed, 143 insertions, 226 deletions
diff --git a/client/client.go b/client/client.go
index 1bfad5f..d6a3487 100644
--- a/client/client.go
+++ b/client/client.go
@@ -6,40 +6,18 @@ import (
"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 {
*sync.Mutex
- kCfg *krbConfig.Config
- kClient *krbClient.Client
+ config *config.Config
}
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,
+ config,
}, nil
}
@@ -47,50 +25,19 @@ func (c client) SetPassword(ctx context.Context, username string, password strin
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())
+ // check if the principal exists
+ exists, err := c.princExists(ctx, username)
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)
+ return fmt.Errorf("error checking principal exists: %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)
+ if !exists {
+ // if not, create it
+ err = c.doCreatePrinc(ctx, username, password)
+ } else {
+ // otherwise, just set the password
+ err = c.doChangePassword(ctx, username, password)
}
- return nil
+ return err
}
diff --git a/client/cmd.go b/client/cmd.go
new file mode 100644
index 0000000..b69880c
--- /dev/null
+++ b/client/cmd.go
@@ -0,0 +1,131 @@
+package client
+
+import (
+ "context"
+ "fmt"
+ "io"
+ "os/exec"
+ "strings"
+)
+
+func (c client) princExists(ctx context.Context, name string) (exists bool, err error) {
+ out, err := c.execKrbAdmin(ctx, fmt.Sprintf("getprinc \"%s\"", name), func(writer io.WriteCloser) error { return nil })
+ if err != nil {
+ err = fmt.Errorf("%s (output: %s)", err, out)
+ return
+ }
+
+ if strings.Contains(out, "Principal does not exist") {
+ exists = false
+ } else if strings.Contains(out, "Expiration date: ") {
+ exists = true
+ } else {
+ err = fmt.Errorf("unrecognised output format: %s", out)
+ }
+
+ return
+}
+
+func (c client) doCreatePrinc(ctx context.Context, name string, password string) (err error) {
+ out, err := c.execKrbAdmin(ctx, fmt.Sprintf("addprinc \"%s\"", name), func(writer io.WriteCloser) error {
+ toWrite := append([]byte(password), '\n')
+ for i := 0; i < 2; i++ {
+ n, err := writer.Write(toWrite)
+ if err != nil || n != len(toWrite) {
+ return fmt.Errorf("error writing to stdin: %s", err)
+ }
+ }
+ return nil
+ })
+
+ if err != nil {
+ return
+ }
+
+ if !strings.Contains(out, "created") {
+ err = fmt.Errorf("unrecognised output format: %s", out)
+ }
+
+ return
+}
+
+func (c client) doChangePassword(ctx context.Context, name string, password string) (err error) {
+ out, err := c.execKrbAdmin(ctx, fmt.Sprintf("cpw \"%s\"", name), func(writer io.WriteCloser) error {
+ toWrite := append([]byte(password), '\n')
+ for i := 0; i < 2; i++ {
+ n, err := writer.Write(toWrite)
+ if err != nil || n != len(toWrite) {
+ return fmt.Errorf("error writing to stdin: %s", err)
+ }
+ }
+ return nil
+ })
+
+ if err != nil {
+ return
+ }
+
+ if !strings.Contains(out, "changed") {
+ err = fmt.Errorf("unrecognised output format: %s", out)
+ }
+
+ return
+}
+
+// execKrbAdmin starts krbadmin with the appropriate commands, and tries to authenticate as the admin principal
+func (c client) execKrbAdmin(ctx context.Context, query string, writeFunc func(writer io.WriteCloser) error) (string, error) {
+ kadm, err := exec.LookPath("kadmin")
+ if err != nil {
+ return "", fmt.Errorf("error finding kadmin executable: %s", err)
+ }
+ cmd := exec.CommandContext(
+ ctx,
+ kadm,
+ "-p",
+ c.config.Username,
+ "-r",
+ c.config.Realm,
+ "-s",
+ c.config.KAdminServer,
+ "-q",
+ query,
+ )
+
+ stdin, err := cmd.StdinPipe()
+ if err != nil {
+ return "", fmt.Errorf("error getting stdin pipe: %s", err)
+ }
+
+ errChan := make(chan error)
+
+ go func() {
+ defer stdin.Close()
+ toWrite := append([]byte(c.config.Password), '\n')
+ n, err := stdin.Write(toWrite)
+
+ if err != nil || n != len(toWrite) {
+ errChan <- fmt.Errorf("error writing to stdin: %s", err)
+ return
+ }
+
+ err = writeFunc(stdin)
+ if err != nil || n != len(toWrite) {
+ errChan <- fmt.Errorf("error writing to stdin: %s", err)
+ return
+ }
+ }()
+
+ rawOut, err := cmd.CombinedOutput()
+ if err != nil {
+ return "", err
+ }
+ out := string(rawOut)
+
+ select {
+ case err = <-errChan:
+ return "", err
+ default:
+ }
+
+ return out, nil
+}
diff --git a/client/messages.go b/client/messages.go
deleted file mode 100644
index b3d2c4f..0000000
--- a/client/messages.go
+++ /dev/null
@@ -1,69 +0,0 @@
-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
deleted file mode 100644
index b29928f..0000000
--- a/client/network.go
+++ /dev/null
@@ -1,92 +0,0 @@
-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
-}