129 lines
3.4 KiB
Go
129 lines
3.4 KiB
Go
// Package service provides server side integrations for Kerberos authentication.
|
|
package service
|
|
|
|
import (
|
|
"github.com/jcmturner/gokrb5/v8/types"
|
|
"sync"
|
|
"time"
|
|
)
|
|
|
|
// Replay cache is required as specified in RFC 4120 section 3.2.3
|
|
|
|
// Cache for tickets received from clients keyed by fully qualified client name. Used to track replay of tickets.
|
|
type Cache struct {
|
|
entries map[string]clientEntries
|
|
mux sync.RWMutex
|
|
}
|
|
|
|
// clientEntries holds entries of client details sent to the service.
|
|
type clientEntries struct {
|
|
replayMap map[time.Time]replayCacheEntry
|
|
seqNumber int64
|
|
subKey types.EncryptionKey
|
|
}
|
|
|
|
// Cache entry tracking client time values of tickets sent to the service.
|
|
type replayCacheEntry struct {
|
|
presentedTime time.Time
|
|
sName types.PrincipalName
|
|
cTime time.Time // This combines the ticket's CTime and Cusec
|
|
}
|
|
|
|
func (c *Cache) getClientEntries(cname types.PrincipalName) (clientEntries, bool) {
|
|
c.mux.RLock()
|
|
defer c.mux.RUnlock()
|
|
ce, ok := c.entries[cname.PrincipalNameString()]
|
|
return ce, ok
|
|
}
|
|
|
|
func (c *Cache) getClientEntry(cname types.PrincipalName, t time.Time) (replayCacheEntry, bool) {
|
|
if ce, ok := c.getClientEntries(cname); ok {
|
|
c.mux.RLock()
|
|
defer c.mux.RUnlock()
|
|
if e, ok := ce.replayMap[t]; ok {
|
|
return e, true
|
|
}
|
|
}
|
|
return replayCacheEntry{}, false
|
|
}
|
|
|
|
// Instance of the ServiceCache. This needs to be a singleton.
|
|
var replayCache Cache
|
|
var once sync.Once
|
|
|
|
// GetReplayCache returns a pointer to the Cache singleton.
|
|
func GetReplayCache(d time.Duration) *Cache {
|
|
// Create a singleton of the ReplayCache and start a background thread to regularly clean out old entries
|
|
once.Do(func() {
|
|
replayCache = Cache{
|
|
entries: make(map[string]clientEntries),
|
|
}
|
|
go func() {
|
|
for {
|
|
// TODO consider using a context here.
|
|
time.Sleep(d)
|
|
replayCache.ClearOldEntries(d)
|
|
}
|
|
}()
|
|
})
|
|
return &replayCache
|
|
}
|
|
|
|
// AddEntry adds an entry to the Cache.
|
|
func (c *Cache) AddEntry(sname types.PrincipalName, a types.Authenticator) {
|
|
ct := a.CTime.Add(time.Duration(a.Cusec) * time.Microsecond)
|
|
if ce, ok := c.getClientEntries(a.CName); ok {
|
|
c.mux.Lock()
|
|
defer c.mux.Unlock()
|
|
ce.replayMap[ct] = replayCacheEntry{
|
|
presentedTime: time.Now().UTC(),
|
|
sName: sname,
|
|
cTime: ct,
|
|
}
|
|
ce.seqNumber = a.SeqNumber
|
|
ce.subKey = a.SubKey
|
|
} else {
|
|
c.mux.Lock()
|
|
defer c.mux.Unlock()
|
|
c.entries[a.CName.PrincipalNameString()] = clientEntries{
|
|
replayMap: map[time.Time]replayCacheEntry{
|
|
ct: {
|
|
presentedTime: time.Now().UTC(),
|
|
sName: sname,
|
|
cTime: ct,
|
|
},
|
|
},
|
|
seqNumber: a.SeqNumber,
|
|
subKey: a.SubKey,
|
|
}
|
|
}
|
|
}
|
|
|
|
// ClearOldEntries clears entries from the Cache that are older than the duration provided.
|
|
func (c *Cache) ClearOldEntries(d time.Duration) {
|
|
c.mux.Lock()
|
|
defer c.mux.Unlock()
|
|
for ke, ce := range c.entries {
|
|
for k, e := range ce.replayMap {
|
|
if time.Now().UTC().Sub(e.presentedTime) > d {
|
|
delete(ce.replayMap, k)
|
|
}
|
|
}
|
|
if len(ce.replayMap) == 0 {
|
|
delete(c.entries, ke)
|
|
}
|
|
}
|
|
}
|
|
|
|
// IsReplay tests if the Authenticator provided is a replay within the duration defined. If this is not a replay add the entry to the cache for tracking.
|
|
func (c *Cache) IsReplay(sname types.PrincipalName, a types.Authenticator) bool {
|
|
ct := a.CTime.Add(time.Duration(a.Cusec) * time.Microsecond)
|
|
if e, ok := c.getClientEntry(a.CName, ct); ok {
|
|
if e.sName.Equal(sname) {
|
|
return true
|
|
}
|
|
}
|
|
c.AddEntry(sname, a)
|
|
return false
|
|
}
|