diff --git a/go.mod b/go.mod index a5e8c69..bd313a4 100644 --- a/go.mod +++ b/go.mod @@ -1,7 +1,10 @@ module github.com/bodgit/tsig +go 1.18 + require ( github.com/alexbrainman/sspi v0.0.0-20180613141037-e580b900e9f5 + github.com/bodgit/gssapi v0.0.1 github.com/enceve/crypto v0.0.0-20160707101852-34d48bb93815 github.com/go-logr/logr v1.2.4 github.com/hashicorp/go-multierror v1.1.1 @@ -12,4 +15,20 @@ require ( github.com/stretchr/testify v1.8.4 ) -go 1.14 +require ( + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/hashicorp/errwrap v1.0.0 // indirect + github.com/hashicorp/go-uuid v1.0.3 // indirect + github.com/jcmturner/aescts/v2 v2.0.0 // indirect + github.com/jcmturner/dnsutils/v2 v2.0.0 // indirect + github.com/jcmturner/gofork v1.7.6 // indirect + github.com/jcmturner/goidentity/v6 v6.0.1 // indirect + github.com/jcmturner/rpc/v2 v2.0.3 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + golang.org/x/crypto v0.13.0 // indirect + golang.org/x/mod v0.12.0 // indirect + golang.org/x/net v0.15.0 // indirect + golang.org/x/sys v0.12.0 // indirect + golang.org/x/tools v0.13.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) diff --git a/go.sum b/go.sum index 4629fcc..1afde14 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,7 @@ github.com/alexbrainman/sspi v0.0.0-20180613141037-e580b900e9f5 h1:P5U+E4x5OkVEKQDklVPmzs71WM56RTTRqV4OrDC//Y4= github.com/alexbrainman/sspi v0.0.0-20180613141037-e580b900e9f5/go.mod h1:976q2ETgjT2snVCf2ZaBnyBbVoPERGjUz+0sofzEfro= +github.com/bodgit/gssapi v0.0.1 h1:NmCbEzfCvP4oVSepPdaWFy27iuW3etly/MqEL2NdU20= +github.com/bodgit/gssapi v0.0.1/go.mod h1:19RkwsW43zAtV8PSfvHEXwMEvVgiQtXciO82iyU/mnw= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -54,7 +56,6 @@ golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58 golang.org/x/crypto v0.13.0 h1:mvySKfSWJ+UKUii46M40LOvyWfN0s2U+46/jDd0e6Ck= golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= -golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.12.0 h1:rmsUpXtvNzj340zd98LZ4KntptpfRHwpFOHG188oHXc= golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= @@ -63,38 +64,29 @@ golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= -golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= golang.org/x/net v0.15.0 h1:ugBLEUaxABaB5AJqW9enI0ACdci2RUd4eP51NTBvuJ8= golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.3.0 h1:ftCYgMx6zT/asHUrPw8BLLscYtGznsLAnjq5RH9P66E= -golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.12.0 h1:CM0HF96J0hcLAwsHPJZjfdNzs0gftsLfgKt57wWHJ0o= golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= -golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= -golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= -golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= -golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= -golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= golang.org/x/tools v0.13.0 h1:Iey4qkscZuv0VvIt8E0neZjtPVQFSc870HQ448QgEmQ= golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= diff --git a/gss/gokrb5.go b/gss/gokrb5.go index 321504e..1529cc9 100644 --- a/gss/gokrb5.go +++ b/gss/gokrb5.go @@ -6,121 +6,25 @@ package gss import ( "encoding/hex" "errors" - "fmt" - "math" "net" - "os" - "os/user" - "strings" "sync" "time" + wrapper "github.com/bodgit/gssapi" "github.com/bodgit/tsig" "github.com/bodgit/tsig/internal/util" "github.com/go-logr/logr" - multierror "github.com/hashicorp/go-multierror" - "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/flags" - "github.com/jcmturner/gokrb5/v8/iana/keyusage" - "github.com/jcmturner/gokrb5/v8/keytab" - "github.com/jcmturner/gokrb5/v8/messages" - "github.com/jcmturner/gokrb5/v8/spnego" - "github.com/jcmturner/gokrb5/v8/types" "github.com/miekg/dns" ) -var ( - errDuplicateToken = errors.New("duplicate per-message token detected") - errOldToken = errors.New("timed-out per-message token detected") - errUnseqToken = errors.New("reordered (early) per-message token detected") - errGapToken = errors.New("skipped predecessor token(s) detected") -) - -type sequenceState struct { - m sync.Mutex - doReplay bool - doSequence bool - base uint64 - next uint64 - receiveMask uint64 - sequenceMask uint64 -} - -func newSequenceState(sequenceNumber uint64, doReplay, doSequence, wide bool) *sequenceState { - ss := &sequenceState{ - doReplay: doReplay, - doSequence: doSequence, - base: sequenceNumber, - } - if wide { - ss.sequenceMask = math.MaxUint64 - } else { - ss.sequenceMask = math.MaxUint32 - } - return ss -} - -func (ss *sequenceState) check(sequenceNumber uint64) error { - if !ss.doReplay && !ss.doSequence { - return nil - } - - ss.m.Lock() - defer ss.m.Unlock() - - relativeSequenceNumber := (sequenceNumber - ss.base) & ss.sequenceMask - - if relativeSequenceNumber >= ss.next { - offset := relativeSequenceNumber - ss.next - ss.receiveMask = ss.receiveMask<<(offset+1) | 1 - ss.next = (relativeSequenceNumber + 1) & ss.sequenceMask - - if offset > 0 && ss.doSequence { - return errGapToken - } - - return nil - } - - offset := ss.next - relativeSequenceNumber - - if offset > 64 { - if ss.doSequence { - return errUnseqToken - } - return errOldToken - } - - bit := uint64(1) << (offset - 1) - if ss.doReplay && ss.receiveMask&bit != 0 { - return errDuplicateToken - } - ss.receiveMask |= bit - if ss.doSequence { - return errUnseqToken - } - - return nil -} - -type context struct { - client *client.Client - key types.EncryptionKey - seq uint64 - ss *sequenceState -} - // Client maps the TKEY name to the context that negotiated it as // well as any other internal state. type Client struct { m sync.RWMutex client *dns.Client config string - ctx map[string]context + ctx map[string]*wrapper.Initiator logger logr.Logger } @@ -146,7 +50,7 @@ func NewClient(dnsClient *dns.Client, options ...func(*Client) error) (*Client, c := &Client{ client: client, - ctx: make(map[string]context), + ctx: make(map[string]*wrapper.Initiator), logger: logr.Discard(), } @@ -183,24 +87,7 @@ func (c *Client) Generate(msg []byte, t *dns.TSIG) ([]byte, error) { return nil, dns.ErrSecret } - token := gssapi.MICToken{ - Flags: gssapi.MICTokenFlagAcceptorSubkey, - SndSeqNum: ctx.seq, - Payload: msg, - } - - if err := token.SetChecksum(ctx.key, keyusage.GSSAPI_INITIATOR_SIGN); err != nil { - return nil, err - } - - b, err := token.Marshal() - if err != nil { - return nil, err - } - - ctx.seq++ - - return b, nil + return ctx.MakeSignature(msg) } // Verify verifies the TSIG MAC based on the established context. @@ -227,87 +114,56 @@ func (c *Client) Verify(stripped []byte, t *dns.TSIG) error { return err } - var token gssapi.MICToken - if err = token.Unmarshal(mac, true); err != nil { - return err - } - token.Payload = stripped - - if err = ctx.ss.check(token.SndSeqNum); err != nil { - return err - } - - // This is the actual verification bit - if _, err = token.Verify(ctx.key, keyusage.GSSAPI_ACCEPTOR_SIGN); err != nil { - return err - } - - return nil + return ctx.VerifySignature(stripped, mac) } -func (c *Client) negotiateContext(host string, cl *client.Client) (string, time.Time, error) { +func (c *Client) negotiateContext(host string, options []wrapper.Option[wrapper.Initiator]) (string, time.Time, error) { + options = append(options, wrapper.WithConfig(c.config), wrapper.WithLogger[wrapper.Initiator](c.logger)) - hostname, _, err := net.SplitHostPort(host) + ctx, err := wrapper.NewInitiator(options...) if err != nil { return "", time.Time{}, err } - keyname := generateTKEYName(hostname) - - tkt, key, err := cl.GetServiceTicket(generateSPN(hostname)) + hostname, _, err := net.SplitHostPort(host) if err != nil { return "", time.Time{}, err } - apreq, err := spnego.NewKRB5TokenAPREQ(cl, tkt, key, []int{gssapi.ContextFlagMutual, gssapi.ContextFlagReplay, gssapi.ContextFlagInteg}, []int{flags.APOptionMutualRequired}) - if err != nil { - return "", time.Time{}, err - } + keyname := generateTKEYName(hostname) - if err = apreq.APReq.DecryptAuthenticator(key); err != nil { - return "", time.Time{}, err - } + spn := generateSPN(hostname) - b, err := apreq.Marshal() - if err != nil { - return "", time.Time{}, err - } + flags := gssapi.ContextFlagMutual | gssapi.ContextFlagReplay | gssapi.ContextFlagInteg - // We don't care about non-TKEY answers, no additional RR's to send, and no signing - tkey, _, err := util.ExchangeTKEY(c.client, host, keyname, tsig.GSS, util.TkeyModeGSS, 3600, b, nil, "", "") + output, cont, err := ctx.Initiate(spn, flags, nil) if err != nil { return "", time.Time{}, err } - if tkey.Header().Name != keyname { - return "", time.Time{}, errors.New("TKEY name does not match") - } + var tkey *dns.TKEY - if b, err = hex.DecodeString(tkey.Key); err != nil { - return "", time.Time{}, err - } - - var aprep spnego.KRB5Token - if err = aprep.Unmarshal(b); err != nil { - return "", time.Time{}, err - } + for cont { + // We don't care about non-TKEY answers, no additional RR's to send, and no signing + tkey, _, err = util.ExchangeTKEY(c.client, host, keyname, tsig.GSS, util.TkeyModeGSS, 3600, output, nil, "", "") + if err != nil { + return "", time.Time{}, err + } - if aprep.IsKRBError() { - return "", time.Time{}, errors.New("received Kerberos error") - } + if tkey.Header().Name != keyname { + return "", time.Time{}, errors.New("TKEY name does not match") + } - if !aprep.IsAPRep() { - return "", time.Time{}, errors.New("didn't receive an AP_REP") - } + var input []byte - if b, err = crypto.DecryptEncPart(aprep.APRep.EncPart, key, keyusage.AP_REP_ENCPART); err != nil { - return "", time.Time{}, err - } + if input, err = hex.DecodeString(tkey.Key); err != nil { + return "", time.Time{}, err + } - var payload messages.EncAPRepPart - payload.Subkey = key // Use current key as fallback if Subkey is not sent - if err = payload.Unmarshal(b); err != nil { - return "", time.Time{}, err + output, cont, err = ctx.Initiate(spn, flags, input) + if err != nil { + return "", time.Time{}, err + } } expiry := time.Unix(int64(tkey.Expiration), 0) @@ -315,103 +171,17 @@ func (c *Client) negotiateContext(host string, cl *client.Client) (string, time. c.m.Lock() defer c.m.Unlock() - c.ctx[keyname] = context{ - client: cl, - key: payload.Subkey, - seq: uint64(apreq.APReq.Authenticator.SeqNumber), - ss: newSequenceState(uint64(payload.SequenceNumber), true, false, true), - } + c.ctx[keyname] = ctx return keyname, expiry, nil } -func loadCache() (*credentials.CCache, error) { - - u, err := user.Current() - if err != nil { - return nil, err - } - - path := "/tmp/krb5cc_" + u.Uid - - env := os.Getenv("KRB5CCNAME") - if strings.HasPrefix(env, "FILE:") { - path = strings.SplitN(env, ":", 2)[1] - } - - cache, err := credentials.LoadCCache(path) - if err != nil { - return nil, err - } - - return cache, nil -} - -func findFile(env string, try []string) (string, error) { - path, ok := os.LookupEnv(env) - if ok { - if _, err := os.Stat(path); err != nil { - return "", fmt.Errorf("%s: %w", env, err) - } - - return path, nil - } - - errs := fmt.Errorf("%s: not found", env) - - for _, t := range try { - _, err := os.Stat(t) - if err != nil { - errs = multierror.Append(errs, err) - - if os.IsNotExist(err) { - continue - } - - return "", errs - } - - return t, nil - } - - return "", errs -} - -func (c *Client) loadConfig() (*config.Config, error) { - if c.config != "" { - return config.NewFromString(c.config) - } - - path, err := findFile("KRB5_CONFIG", []string{"/etc/krb5.conf"}) - if err != nil { - return nil, err - } - - return config.Load(path) -} - // NegotiateContext exchanges RFC 2930 TKEY records with the indicated DNS // server to establish a security context using the current user. // It returns the negotiated TKEY name, expiration time, and any error that // occurred. func (c *Client) NegotiateContext(host string) (string, time.Time, error) { - - cache, err := loadCache() - if err != nil { - return "", time.Time{}, err - } - - cfg, err := c.loadConfig() - if err != nil { - return "", time.Time{}, err - } - - cl, err := client.NewFromCCache(cache, cfg, client.DisablePAFXFAST(true)) - if err != nil { - return "", time.Time{}, err - } - - return c.negotiateContext(host, cl) + return c.negotiateContext(host, nil) } // NegotiateContextWithCredentials exchanges RFC 2930 TKEY records with the @@ -420,21 +190,13 @@ func (c *Client) NegotiateContext(host string) (string, time.Time, error) { // It returns the negotiated TKEY name, expiration time, and any error that // occurred. func (c *Client) NegotiateContextWithCredentials(host, domain, username, password string) (string, time.Time, error) { - - // Should I still initialise the credential cache? - - cfg, err := c.loadConfig() - if err != nil { - return "", time.Time{}, err + options := []wrapper.Option[wrapper.Initiator]{ + wrapper.WithDomain(domain), + wrapper.WithUsername(username), + wrapper.WithPassword(password), } - cl := client.NewWithPassword(username, domain, password, cfg, client.DisablePAFXFAST(true)) - - if err = cl.Login(); err != nil { - return "", time.Time{}, err - } - - return c.negotiateContext(host, cl) + return c.negotiateContext(host, options) } // NegotiateContextWithKeytab exchanges RFC 2930 TKEY records with the @@ -443,26 +205,13 @@ func (c *Client) NegotiateContextWithCredentials(host, domain, username, passwor // It returns the negotiated TKEY name, expiration time, and any error that // occurred. func (c *Client) NegotiateContextWithKeytab(host, domain, username, path string) (string, time.Time, error) { - - // Should I still initialise the credential cache? - - kt, err := keytab.Load(path) - if err != nil { - return "", time.Time{}, err - } - - cfg, err := c.loadConfig() - if err != nil { - return "", time.Time{}, err - } - - cl := client.NewWithKeytab(username, domain, kt, cfg, client.DisablePAFXFAST(true)) - - if err = cl.Login(); err != nil { - return "", time.Time{}, err + options := []wrapper.Option[wrapper.Initiator]{ + wrapper.WithDomain(domain), + wrapper.WithUsername(username), + wrapper.WithKeytab[wrapper.Initiator](path), } - return c.negotiateContext(host, cl) + return c.negotiateContext(host, options) } // DeleteContext deletes the active security context associated with the given @@ -478,7 +227,7 @@ func (c *Client) DeleteContext(keyname string) error { return errors.New("No such context") } - ctx.client.Destroy() + ctx.Close() delete(c.ctx, keyname)