1package spnego
2
3import (
4	"context"
5	"errors"
6	"fmt"
7
8	"github.com/jcmturner/gofork/encoding/asn1"
9	"github.com/jcmturner/gokrb5/v8/client"
10	"github.com/jcmturner/gokrb5/v8/gssapi"
11	"github.com/jcmturner/gokrb5/v8/messages"
12	"github.com/jcmturner/gokrb5/v8/service"
13	"github.com/jcmturner/gokrb5/v8/types"
14)
15
16// https://msdn.microsoft.com/en-us/library/ms995330.aspx
17
18// Negotiation state values.
19const (
20	NegStateAcceptCompleted  NegState = 0
21	NegStateAcceptIncomplete NegState = 1
22	NegStateReject           NegState = 2
23	NegStateRequestMIC       NegState = 3
24)
25
26// NegState is a type to indicate the SPNEGO negotiation state.
27type NegState int
28
29// NegTokenInit implements Negotiation Token of type Init.
30type NegTokenInit struct {
31	MechTypes      []asn1.ObjectIdentifier
32	ReqFlags       asn1.BitString
33	MechTokenBytes []byte
34	MechListMIC    []byte
35	mechToken      gssapi.ContextToken
36	settings       *service.Settings
37}
38
39type marshalNegTokenInit struct {
40	MechTypes      []asn1.ObjectIdentifier `asn1:"explicit,tag:0"`
41	ReqFlags       asn1.BitString          `asn1:"explicit,optional,tag:1"`
42	MechTokenBytes []byte                  `asn1:"explicit,optional,omitempty,tag:2"`
43	MechListMIC    []byte                  `asn1:"explicit,optional,omitempty,tag:3"` // This field is not used when negotiating Kerberos tokens
44}
45
46// NegTokenResp implements Negotiation Token of type Resp/Targ
47type NegTokenResp struct {
48	NegState      asn1.Enumerated
49	SupportedMech asn1.ObjectIdentifier
50	ResponseToken []byte
51	MechListMIC   []byte
52	mechToken     gssapi.ContextToken
53	settings      *service.Settings
54}
55
56type marshalNegTokenResp struct {
57	NegState      asn1.Enumerated       `asn1:"explicit,tag:0"`
58	SupportedMech asn1.ObjectIdentifier `asn1:"explicit,optional,tag:1"`
59	ResponseToken []byte                `asn1:"explicit,optional,omitempty,tag:2"`
60	MechListMIC   []byte                `asn1:"explicit,optional,omitempty,tag:3"` // This field is not used when negotiating Kerberos tokens
61}
62
63// NegTokenTarg implements Negotiation Token of type Resp/Targ
64type NegTokenTarg NegTokenResp
65
66// Marshal an Init negotiation token
67func (n *NegTokenInit) Marshal() ([]byte, error) {
68	m := marshalNegTokenInit{
69		MechTypes:      n.MechTypes,
70		ReqFlags:       n.ReqFlags,
71		MechTokenBytes: n.MechTokenBytes,
72		MechListMIC:    n.MechListMIC,
73	}
74	b, err := asn1.Marshal(m)
75	if err != nil {
76		return nil, err
77	}
78	nt := asn1.RawValue{
79		Tag:        0,
80		Class:      2,
81		IsCompound: true,
82		Bytes:      b,
83	}
84	nb, err := asn1.Marshal(nt)
85	if err != nil {
86		return nil, err
87	}
88	return nb, nil
89}
90
91// Unmarshal an Init negotiation token
92func (n *NegTokenInit) Unmarshal(b []byte) error {
93	init, nt, err := UnmarshalNegToken(b)
94	if err != nil {
95		return err
96	}
97	if !init {
98		return errors.New("bytes were not that of a NegTokenInit")
99	}
100	nInit := nt.(NegTokenInit)
101	n.MechTokenBytes = nInit.MechTokenBytes
102	n.MechListMIC = nInit.MechListMIC
103	n.MechTypes = nInit.MechTypes
104	n.ReqFlags = nInit.ReqFlags
105	return nil
106}
107
108// Verify an Init negotiation token
109func (n *NegTokenInit) Verify() (bool, gssapi.Status) {
110	// Check if supported mechanisms are in the MechTypeList
111	var mtSupported bool
112	for _, m := range n.MechTypes {
113		if m.Equal(gssapi.OIDKRB5.OID()) || m.Equal(gssapi.OIDMSLegacyKRB5.OID()) {
114			if n.mechToken == nil && n.MechTokenBytes == nil {
115				return false, gssapi.Status{Code: gssapi.StatusContinueNeeded}
116			}
117			mtSupported = true
118			break
119		}
120	}
121	if !mtSupported {
122		return false, gssapi.Status{Code: gssapi.StatusBadMech, Message: "no supported mechanism specified in negotiation"}
123	}
124	// There should be some mechtoken bytes for a KRB5Token (other mech types are not supported)
125	mt := new(KRB5Token)
126	mt.settings = n.settings
127	if n.mechToken == nil {
128		err := mt.Unmarshal(n.MechTokenBytes)
129		if err != nil {
130			return false, gssapi.Status{Code: gssapi.StatusDefectiveToken, Message: err.Error()}
131		}
132		n.mechToken = mt
133	} else {
134		var ok bool
135		mt, ok = n.mechToken.(*KRB5Token)
136		if !ok {
137			return false, gssapi.Status{Code: gssapi.StatusDefectiveToken, Message: "MechToken is not a KRB5 token as expected"}
138		}
139	}
140	// Verify the mechtoken
141	return n.mechToken.Verify()
142}
143
144// Context returns the SPNEGO context which will contain any verify user identity information.
145func (n *NegTokenInit) Context() context.Context {
146	if n.mechToken != nil {
147		mt, ok := n.mechToken.(*KRB5Token)
148		if !ok {
149			return nil
150		}
151		return mt.Context()
152	}
153	return nil
154}
155
156// Marshal a Resp/Targ negotiation token
157func (n *NegTokenResp) Marshal() ([]byte, error) {
158	m := marshalNegTokenResp{
159		NegState:      n.NegState,
160		SupportedMech: n.SupportedMech,
161		ResponseToken: n.ResponseToken,
162		MechListMIC:   n.MechListMIC,
163	}
164	b, err := asn1.Marshal(m)
165	if err != nil {
166		return nil, err
167	}
168	nt := asn1.RawValue{
169		Tag:        1,
170		Class:      2,
171		IsCompound: true,
172		Bytes:      b,
173	}
174	nb, err := asn1.Marshal(nt)
175	if err != nil {
176		return nil, err
177	}
178	return nb, nil
179}
180
181// Unmarshal a Resp/Targ negotiation token
182func (n *NegTokenResp) Unmarshal(b []byte) error {
183	init, nt, err := UnmarshalNegToken(b)
184	if err != nil {
185		return err
186	}
187	if init {
188		return errors.New("bytes were not that of a NegTokenResp")
189	}
190	nResp := nt.(NegTokenResp)
191	n.MechListMIC = nResp.MechListMIC
192	n.NegState = nResp.NegState
193	n.ResponseToken = nResp.ResponseToken
194	n.SupportedMech = nResp.SupportedMech
195	return nil
196}
197
198// Verify a Resp/Targ negotiation token
199func (n *NegTokenResp) Verify() (bool, gssapi.Status) {
200	if n.SupportedMech.Equal(gssapi.OIDKRB5.OID()) || n.SupportedMech.Equal(gssapi.OIDMSLegacyKRB5.OID()) {
201		if n.mechToken == nil && n.ResponseToken == nil {
202			return false, gssapi.Status{Code: gssapi.StatusContinueNeeded}
203		}
204		mt := new(KRB5Token)
205		mt.settings = n.settings
206		if n.mechToken == nil {
207			err := mt.Unmarshal(n.ResponseToken)
208			if err != nil {
209				return false, gssapi.Status{Code: gssapi.StatusDefectiveToken, Message: err.Error()}
210			}
211			n.mechToken = mt
212		} else {
213			var ok bool
214			mt, ok = n.mechToken.(*KRB5Token)
215			if !ok {
216				return false, gssapi.Status{Code: gssapi.StatusDefectiveToken, Message: "MechToken is not a KRB5 token as expected"}
217			}
218		}
219		if mt == nil {
220			return false, gssapi.Status{Code: gssapi.StatusContinueNeeded}
221		}
222		// Verify the mechtoken
223		return mt.Verify()
224	}
225	return false, gssapi.Status{Code: gssapi.StatusBadMech, Message: "no supported mechanism specified in negotiation"}
226}
227
228// State returns the negotiation state of the negotiation response.
229func (n *NegTokenResp) State() NegState {
230	return NegState(n.NegState)
231}
232
233// Context returns the SPNEGO context which will contain any verify user identity information.
234func (n *NegTokenResp) Context() context.Context {
235	if n.mechToken != nil {
236		mt, ok := n.mechToken.(*KRB5Token)
237		if !ok {
238			return nil
239		}
240		return mt.Context()
241	}
242	return nil
243}
244
245// UnmarshalNegToken umarshals and returns either a NegTokenInit or a NegTokenResp.
246//
247// The boolean indicates if the response is a NegTokenInit.
248// If error is nil and the boolean is false the response is a NegTokenResp.
249func UnmarshalNegToken(b []byte) (bool, interface{}, error) {
250	var a asn1.RawValue
251	_, err := asn1.Unmarshal(b, &a)
252	if err != nil {
253		return false, nil, fmt.Errorf("error unmarshalling NegotiationToken: %v", err)
254	}
255	switch a.Tag {
256	case 0:
257		var n marshalNegTokenInit
258		_, err = asn1.Unmarshal(a.Bytes, &n)
259		if err != nil {
260			return false, nil, fmt.Errorf("error unmarshalling NegotiationToken type %d (Init): %v", a.Tag, err)
261		}
262		nt := NegTokenInit{
263			MechTypes:      n.MechTypes,
264			ReqFlags:       n.ReqFlags,
265			MechTokenBytes: n.MechTokenBytes,
266			MechListMIC:    n.MechListMIC,
267		}
268		return true, nt, nil
269	case 1:
270		var n marshalNegTokenResp
271		_, err = asn1.Unmarshal(a.Bytes, &n)
272		if err != nil {
273			return false, nil, fmt.Errorf("error unmarshalling NegotiationToken type %d (Resp/Targ): %v", a.Tag, err)
274		}
275		nt := NegTokenResp{
276			NegState:      n.NegState,
277			SupportedMech: n.SupportedMech,
278			ResponseToken: n.ResponseToken,
279			MechListMIC:   n.MechListMIC,
280		}
281		return false, nt, nil
282	default:
283		return false, nil, errors.New("unknown choice type for NegotiationToken")
284	}
285
286}
287
288// NewNegTokenInitKRB5 creates new Init negotiation token for Kerberos 5
289func NewNegTokenInitKRB5(cl *client.Client, tkt messages.Ticket, sessionKey types.EncryptionKey) (NegTokenInit, error) {
290	mt, err := NewKRB5TokenAPREQ(cl, tkt, sessionKey, []int{gssapi.ContextFlagInteg, gssapi.ContextFlagConf}, []int{})
291	if err != nil {
292		return NegTokenInit{}, fmt.Errorf("error getting KRB5 token; %v", err)
293	}
294	mtb, err := mt.Marshal()
295	if err != nil {
296		return NegTokenInit{}, fmt.Errorf("error marshalling KRB5 token; %v", err)
297	}
298	return NegTokenInit{
299		MechTypes:      []asn1.ObjectIdentifier{gssapi.OIDKRB5.OID()},
300		MechTokenBytes: mtb,
301	}, nil
302}
303