Jonathan Turner 9 лет назад
Родитель
Сommit
a3580bba79
6 измененных файлов с 207 добавлено и 20 удалено
  1. 2 1
      messages/KDCRep.go
  2. 15 0
      messages/KRBError.go
  3. 2 1
      messages/Ticket.go
  4. 59 18
      service/http.go
  5. 111 0
      service/replayCache.go
  6. 18 0
      types/PrincipalName.go

+ 2 - 1
messages/KDCRep.go

@@ -208,7 +208,8 @@ func (k *ASRep) IsValid(cfg *config.Config, asReq ASReq) (bool, error) {
 	if len(asReq.ReqBody.Addresses) > 0 {
 		//TODO compare if address list is the same
 	}
-	if time.Since(k.DecryptedEncPart.AuthTime) > cfg.LibDefaults.Clockskew || k.DecryptedEncPart.AuthTime.Sub(time.Now().UTC()) > cfg.LibDefaults.Clockskew {
+	t := time.Now().UTC()
+	if t.Sub(k.DecryptedEncPart.AuthTime) > cfg.LibDefaults.Clockskew || k.DecryptedEncPart.AuthTime.Sub(t) > cfg.LibDefaults.Clockskew {
 		return false, fmt.Errorf("Clock skew with KDC too large. Greater than %v seconds", cfg.LibDefaults.Clockskew.Seconds())
 	}
 	if asReq.PAData.Contains(patype.PA_REQ_ENC_PA_REP) {

+ 15 - 0
messages/KRBError.go

@@ -4,6 +4,7 @@ package messages
 import (
 	"fmt"
 	"github.com/jcmturner/asn1"
+	"github.com/jcmturner/gokrb5/iana"
 	"github.com/jcmturner/gokrb5/iana/asnAppTag"
 	"github.com/jcmturner/gokrb5/iana/errorcode"
 	"github.com/jcmturner/gokrb5/iana/msgtype"
@@ -28,6 +29,20 @@ type KRBError struct {
 	EData     []byte              `asn1:"optional,explicit,tag:12"`
 }
 
+func NewKRBError(sname types.PrincipalName, realm string, code int, etext string) KRBError {
+	t := time.Now().UTC()
+	return KRBError{
+		PVNO:      iana.PVNO,
+		MsgType:   msgtype.KRB_ERROR,
+		STime:     t,
+		Susec:     int((t.UnixNano() / int64(time.Microsecond)) - (t.Unix() * 1e6)),
+		ErrorCode: code,
+		SName:     sname,
+		Realm:     realm,
+		EText:     etext,
+	}
+}
+
 // Unmarshal bytes b into the KRBError struct.
 func (k *KRBError) Unmarshal(b []byte) error {
 	_, err := asn1.UnmarshalWithParams(b, k, fmt.Sprintf("application,explicit,tag:%v", asnAppTag.KRBError))

+ 2 - 1
messages/Ticket.go

@@ -6,6 +6,7 @@ import (
 	"github.com/jcmturner/gokrb5/asn1tools"
 	"github.com/jcmturner/gokrb5/crypto"
 	"github.com/jcmturner/gokrb5/iana/asnAppTag"
+	"github.com/jcmturner/gokrb5/iana/errorcode"
 	"github.com/jcmturner/gokrb5/iana/keyusage"
 	"github.com/jcmturner/gokrb5/keytab"
 	"github.com/jcmturner/gokrb5/types"
@@ -125,7 +126,7 @@ func MarshalTicketSequence(tkts []Ticket) (asn1.RawValue, error) {
 func (t *Ticket) DecryptEncPart(keytab keytab.Keytab) error {
 	key, err := keytab.GetEncryptionKey(t.SName.NameString, t.Realm, t.EncPart.KVNO, t.EncPart.EType)
 	if err != nil {
-		return fmt.Errorf("Could not get key from keytab: %v", err)
+		return NewKRBError(t.SName, t.Realm, errorcode.KRB_AP_ERR_NOKEY, fmt.Sprintf("Could not get key from keytab: %v", err))
 	}
 	b, err := crypto.DecryptEncPart(t.EncPart, key, keyusage.KDC_REP_TICKET)
 	if err != nil {

+ 59 - 18
service/http.go

@@ -6,58 +6,99 @@ import (
 	"fmt"
 	"github.com/jcmturner/gokrb5/GSSAPI"
 	"github.com/jcmturner/gokrb5/crypto"
+	"github.com/jcmturner/gokrb5/iana/errorcode"
 	"github.com/jcmturner/gokrb5/iana/keyusage"
 	"github.com/jcmturner/gokrb5/keytab"
+	"github.com/jcmturner/gokrb5/messages"
 	"github.com/jcmturner/gokrb5/types"
 	"net/http"
 	"strings"
+	"time"
 )
 
-func SPNEGOHandler(w http.ResponseWriter, r *http.Request) {
-
-}
-
-func SPNEGOKRB5Authenticate(w http.ResponseWriter, r *http.Request, ktab keytab.Keytab) (bool, error) {
+// Authenticate the request. Returns:
+//
+// boolean: indicates if authenticate succeeded
+//
+// string: client principal name
+//
+// string: client realm
+//
+// error: reason for any authentication failure
+func SPNEGOKRB5Authenticate(w http.ResponseWriter, r *http.Request, ktab keytab.Keytab) (bool, string, string, error) {
 	s := strings.SplitN(r.Header.Get("Authorization"), " ", 2)
 	if len(s) != 2 || s[0] != "Negotiate" {
 		// TODO set the NegTokenResp Negotiate header here on the w
-		return false, errors.New("No Authorization header with Negotiate content found")
+		return false, nil, nil, errors.New("No Authorization header with Negotiate content found")
 	}
 	b, err := base64.StdEncoding.DecodeString(s[1])
 	if err != nil {
-		return false, fmt.Errorf("Authorization header Negotiate content could not be base64 decoded: %v", err)
+		return false, nil, nil, fmt.Errorf("Authorization header Negotiate content could not be base64 decoded: %v", err)
 	}
 	isInit, nt, err := GSSAPI.UnmarshalNegToken(b)
 	if err != nil || !isInit {
-		return false, fmt.Errorf("SPNEGO negotiation token is not a NegTokenInit: %v", err)
+		return false, nil, nil, fmt.Errorf("SPNEGO negotiation token is not a NegTokenInit: %v", err)
 	}
 	nInit := nt.(GSSAPI.NegTokenInit)
 	if nInit.MechTypes != GSSAPI.MechTypeOID_Krb5 {
-		return false, errors.New("OID of MechToken is not of type KRB5")
+		return false, nil, nil, errors.New("OID of MechToken is not of type KRB5")
 	}
 	var mt GSSAPI.MechToken
 	err = mt.Unmarshal(nInit.MechToken)
 	if err != nil {
-		return false, fmt.Errorf("Error unmarshalling MechToken: %v", err)
+		return false, nil, nil, fmt.Errorf("Error unmarshalling MechToken: %v", err)
 	}
 	if !mt.IsAPReq() {
-		return false, errors.New("MechToken does not contain an AP_REQ")
+		return false, nil, nil, errors.New("MechToken does not contain an AP_REQ - KRB_AP_ERR_MSG_TYPE")
 	}
 	err = mt.APReq.Ticket.DecryptEncPart(ktab)
 	if err != nil {
-		return false, fmt.Errorf("Error decrypting the service ticket provided: %v", err)
+		return false, nil, nil, fmt.Errorf("Error decrypting the service ticket provided: %v", err)
 	}
-	sessionKey := mt.APReq.Ticket.DecryptedEncPart.Key
-	ab, err := crypto.DecryptEncPart(mt.APReq.Authenticator, sessionKey, keyusage.AP_REQ_AUTHENTICATOR)
+	ab, err := crypto.DecryptEncPart(mt.APReq.Authenticator, mt.APReq.Ticket.DecryptedEncPart.Key, keyusage.AP_REQ_AUTHENTICATOR)
 	if err != nil {
-		return false, fmt.Errorf("Error decrypting the authenticator provided: %v", err)
+		return false, nil, nil, fmt.Errorf("Error decrypting the authenticator provided: %v", err)
 	}
 	var a types.Authenticator
 	err = a.Unmarshal(ab)
 	if err != nil {
-		return false, fmt.Errorf("Error unmarshalling the authenticator: %v", err)
+		return false, nil, nil, fmt.Errorf("Error unmarshalling the authenticator: %v", err)
+	}
+	// VALIDATIONS
+	// Check CName in Authenticator is the same as that in the ticket
+	if !a.CName.Equal(mt.APReq.Ticket.DecryptedEncPart.CName) {
+		return false, nil, nil, messages.NewKRBError(mt.APReq.Ticket.SName, mt.APReq.Ticket.Realm, errorcode.KRB_AP_ERR_BADMATCH, "CName in Authenticator does not match that in service ticket")
+	}
+	// TODO client address check
+	//The addresses in the ticket (if any) are then
+	//searched for an address matching the operating-system reported
+	//address of the client.  If no match is found or the server insists on
+	//ticket addresses but none are present in the ticket, the
+	//KRB_AP_ERR_BADADDR error is returned.
+
+	// Check the clock skew between the client and the service server
+	ct := a.CTime.Add(time.Duration(a.Cusec) * time.Microsecond)
+	t := time.Now().UTC()
+	// Hardcode 5 min max skew. May want to make this configurable
+	d := time.Duration(5) * time.Minute
+	if t.Sub(ct) > d || ct.Sub(t) > d {
+		return false, nil, nil, messages.NewKRBError(mt.APReq.Ticket.SName, mt.APReq.Ticket.Realm, errorcode.KRB_AP_ERR_SKEW, fmt.Sprintf("Clock skew with client too large. Greater than %v seconds", d))
 	}
-	// TODO check timestamp within skew etc...
 
-	return true, nil
+	// Check for replay
+	rc := GetReplayCache(d)
+	if rc.IsReplay(d, mt.APReq.Ticket.SName, a) {
+		return false, nil, nil, messages.NewKRBError(mt.APReq.Ticket.SName, mt.APReq.Ticket.Realm, errorcode.KRB_AP_ERR_REPEAT, "Replay detected")
+	}
+
+	// Check for future tickets or invalid tickets
+	if mt.APReq.Ticket.DecryptedEncPart.StartTime.Sub(t) > d || types.IsFlagSet(mt.APReq.Ticket.DecryptedEncPart.Flags, types.Invalid) {
+		return false, nil, nil, messages.NewKRBError(mt.APReq.Ticket.SName, mt.APReq.Ticket.Realm, errorcode.KRB_AP_ERR_TKT_NYV, "Service ticket provided is not yet valid")
+	}
+
+	// Check for expired ticket
+	if t.Sub(mt.APReq.Ticket.DecryptedEncPart.EndTime) > d {
+		return false, nil, nil, messages.NewKRBError(mt.APReq.Ticket.SName, mt.APReq.Ticket.Realm, errorcode.KRB_AP_ERR_TKT_EXPIRED, "Service ticket provided has expired")
+	}
+	return true, a.CName.GetPrincipalNameString(), a.CRealm, nil
 }

+ 111 - 0
service/replayCache.go

@@ -0,0 +1,111 @@
+package service
+
+import (
+	"github.com/jcmturner/gokrb5/types"
+	"sync"
+	"time"
+)
+
+/*The server MUST utilize a replay cache to remember any authenticator
+presented within the allowable clock skew.
+The replay cache will store at least the server name, along with the
+client name, time, and microsecond fields from the recently-seen
+authenticators, and if a matching tuple is found, the
+KRB_AP_ERR_REPEAT error is returned.  Note that the rejection here is
+restricted to authenticators from the same principal to the same
+server.  Other client principals communicating with the same server
+principal should not have their authenticators rejected if the time
+and microsecond fields happen to match some other client's
+authenticator.
+
+If a server loses track of authenticators presented within the
+allowable clock skew, it MUST reject all requests until the clock
+skew interval has passed, providing assurance that any lost or
+replayed authenticators will fall outside the allowable clock skew
+and can no longer be successfully replayed.  If this were not done,
+an attacker could subvert the authentication by recording the ticket
+and authenticator sent over the network to a server and replaying
+them following an event that caused the server to lose track of
+recently seen authenticators.*/
+
+type ServiceCache map[string]ClientEntries
+
+type ClientEntries struct {
+	ReplayMap map[time.Time]ReplayCacheEntry
+	SeqNumber int
+	SubKey    types.EncryptionKey
+}
+
+type ReplayCacheEntry struct {
+	PresentedTime time.Time
+	SName         types.PrincipalName
+	CTime         time.Time // This combines the ticket's CTime and Cusec
+}
+
+var replayCache ServiceCache
+
+func GetReplayCache(d time.Duration) *ServiceCache {
+	// Create a singleton of the ReplayCache and start a background thread to regularly clean out old entries
+	var once sync.Once
+	once.Do(func() {
+		replayCache = make(ServiceCache)
+		go func() {
+			for {
+				time.Sleep(d)
+				replayCache.ClearOldEntries(d)
+			}
+		}()
+	})
+	return &replayCache
+}
+
+func (c *ServiceCache) AddEntry(sname types.PrincipalName, a types.Authenticator) {
+	ct := a.CTime.Add(time.Duration(a.Cusec) * time.Microsecond)
+	if ce, ok := c[a.CName.GetPrincipalNameString()]; ok {
+		ce.ReplayMap[ct] = ReplayCacheEntry{
+			PresentedTime: time.Now().UTC(),
+			SName:         sname,
+			CTime:         ct,
+		}
+		ce.SeqNumber = a.SeqNumber
+		ce.SubKey = a.SubKey
+	} else {
+		c[a.CName.GetPrincipalNameString()] = ClientEntries{
+			ReplayMap: map[time.Time]ReplayCacheEntry{
+				ct: {
+					PresentedTime: time.Now().UTC(),
+					SName:         sname,
+					CTime:         ct,
+				},
+			},
+			SeqNumber: a.SeqNumber,
+			SubKey:    a.SubKey,
+		}
+	}
+}
+
+func (c *ServiceCache) ClearOldEntries(d time.Duration) {
+	for ck := range *c {
+		for ct, e := range c[ck].ReplayMap {
+			if time.Now().UTC().Sub(e.PresentedTime) > d {
+				delete(c[ck].ReplayMap, ct)
+			}
+		}
+		if len(c[ck]) == 0 {
+			delete(c, ck)
+		}
+	}
+}
+
+func (c *ServiceCache) IsReplay(d time.Duration, sname types.PrincipalName, a types.Authenticator) bool {
+	if ck, ok := c[a.CName.GetPrincipalNameString()]; ok {
+		ct := a.CTime.Add(time.Duration(a.Cusec) * time.Microsecond)
+		if e, ok := ck.ReplayMap[ct]; ok {
+			if e.SName == sname {
+				return true
+			}
+		}
+	}
+	c.AddEntry(sname, a)
+	return false
+}

+ 18 - 0
types/PrincipalName.go

@@ -1,5 +1,7 @@
 package types
 
+import "strings"
+
 // Reference: https://www.ietf.org/rfc/rfc4120.txt
 // Section: 5.2.2
 
@@ -16,3 +18,19 @@ func (pn *PrincipalName) GetSalt(realm string) string {
 	}
 	return string(sb)
 }
+
+func (pn *PrincipalName) Equal(n PrincipalName) bool {
+	if n.NameType != pn.NameType {
+		return false
+	}
+	for i, s := range pn.NameString {
+		if n.NameString[i] != s {
+			return false
+		}
+	}
+	return true
+}
+
+func (pn *PrincipalName) GetPrincipalNameString() string {
+	return strings.Join(pn.NameString, "/")
+}