Hash keys used in cached token authenticator

It is possible to configure the token cache to cache failures.  We
allow 1 MB of headers per request, meaning a malicious actor could
cause the cache to use a large amount of memory by filling it with
large invalid tokens.  This change hashes the token before using it
as a key.  Measures have been taken to prevent precomputation
attacks.  SHA 256 is used as the hash to prevent collisions.

Signed-off-by: Monis Khan <mkhan@redhat.com>

Kubernetes-commit: 9a547bca8e6e15273bfafd3496aa6524fd7d35bd
This commit is contained in:
Monis Khan 2019-10-11 15:21:34 -04:00 committed by Kubernetes Publisher
parent 0d1aa698ce
commit c2289feb1e
2 changed files with 116 additions and 4 deletions

View File

@ -18,8 +18,15 @@ package cache
import (
"context"
"fmt"
"crypto/hmac"
"crypto/rand"
"crypto/sha256"
"encoding/binary"
"hash"
"io"
"sync"
"time"
"unsafe"
utilclock "k8s.io/apimachinery/pkg/util/clock"
"k8s.io/apiserver/pkg/authentication/authenticator"
@ -40,6 +47,11 @@ type cachedTokenAuthenticator struct {
failureTTL time.Duration
cache cache
// hashPool is a per authenticator pool of hash.Hash (to avoid allocations from building the Hash)
// HMAC with SHA-256 and a random key is used to prevent precomputation and length extension attacks
// It also mitigates hash map DOS attacks via collisions (the inputs are supplied by untrusted users)
hashPool *sync.Pool
}
type cache interface {
@ -57,6 +69,11 @@ func New(authenticator authenticator.Token, cacheErrs bool, successTTL, failureT
}
func newWithClock(authenticator authenticator.Token, cacheErrs bool, successTTL, failureTTL time.Duration, clock utilclock.Clock) authenticator.Token {
randomCacheKey := make([]byte, 32)
if _, err := rand.Read(randomCacheKey); err != nil {
panic(err) // rand should never fail
}
return &cachedTokenAuthenticator{
authenticator: authenticator,
cacheErrs: cacheErrs,
@ -70,6 +87,12 @@ func newWithClock(authenticator authenticator.Token, cacheErrs bool, successTTL,
// namespaces; a 32k entry cache is therefore a 2x safety
// margin.
cache: newStripedCache(32, fnvHashFunc, func() cache { return newSimpleCache(1024, clock) }),
hashPool: &sync.Pool{
New: func() interface{} {
return hmac.New(sha256.New, randomCacheKey)
},
},
}
}
@ -77,7 +100,7 @@ func newWithClock(authenticator authenticator.Token, cacheErrs bool, successTTL,
func (a *cachedTokenAuthenticator) AuthenticateToken(ctx context.Context, token string) (*authenticator.Response, bool, error) {
auds, _ := authenticator.AudiencesFrom(ctx)
key := keyFunc(auds, token)
key := keyFunc(a.hashPool, auds, token)
if record, ok := a.cache.get(key); ok {
return record.resp, record.ok, record.err
}
@ -97,6 +120,55 @@ func (a *cachedTokenAuthenticator) AuthenticateToken(ctx context.Context, token
return resp, ok, err
}
func keyFunc(auds []string, token string) string {
return fmt.Sprintf("%#v|%v", auds, token)
// keyFunc generates a string key by hashing the inputs.
// This lowers the memory requirement of the cache and keeps tokens out of memory.
func keyFunc(hashPool *sync.Pool, auds []string, token string) string {
h := hashPool.Get().(hash.Hash)
h.Reset()
// try to force stack allocation
var a [4]byte
b := a[:]
writeLengthPrefixedString(h, b, token)
// encode the length of audiences to avoid ambiguities
writeLength(h, b, len(auds))
for _, aud := range auds {
writeLengthPrefixedString(h, b, aud)
}
key := toString(h.Sum(nil)) // skip base64 encoding to save an allocation
hashPool.Put(h)
return key
}
// writeLengthPrefixedString writes s with a length prefix to prevent ambiguities, i.e. "xy" + "z" == "x" + "yz"
// the length of b is assumed to be 4 (b is mutated by this function to store the length of s)
func writeLengthPrefixedString(w io.Writer, b []byte, s string) {
writeLength(w, b, len(s))
if _, err := w.Write(toBytes(s)); err != nil {
panic(err) // Write() on hash never fails
}
}
// writeLength encodes length into b and then writes it via the given writer
// the length of b is assumed to be 4
func writeLength(w io.Writer, b []byte, length int) {
binary.BigEndian.PutUint32(b, uint32(length))
if _, err := w.Write(b); err != nil {
panic(err) // Write() on hash never fails
}
}
// toBytes performs unholy acts to avoid allocations
func toBytes(s string) []byte {
return *(*[]byte)(unsafe.Pointer(&s))
}
// toString performs unholy acts to avoid allocations
func toString(b []byte) string {
return *(*string)(unsafe.Pointer(&b))
}

View File

@ -18,7 +18,11 @@ package cache
import (
"context"
"crypto/hmac"
"crypto/rand"
"crypto/sha256"
"reflect"
"sync"
"testing"
"time"
@ -125,3 +129,39 @@ func TestCachedTokenAuthenticatorWithAudiences(t *testing.T) {
t.Errorf("Expected user1-different")
}
}
var bKey string
// use a realistic token for benchmarking
const jwtToken = `eyJhbGciOiJSUzI1NiIsImtpZCI6IiJ9.eyJpc3MiOiJrdWJlcm5ldGVzL3NlcnZpY2VhY2NvdW50Iiwia3ViZXJuZXRlcy5pby9zZXJ2aWNlYWNjb3VudC9uYW1lc3BhY2UiOiJvcGVuc2hpZnQtc2RuIiwia3ViZXJuZXRlcy5pby9zZXJ2aWNlYWNjb3VudC9zZWNyZXQubmFtZSI6InNkbi10b2tlbi1nNndtYyIsImt1YmVybmV0ZXMuaW8vc2VydmljZWFjY291bnQvc2VydmljZS1hY2NvdW50Lm5hbWUiOiJzZG4iLCJrdWJlcm5ldGVzLmlvL3NlcnZpY2VhY2NvdW50L3NlcnZpY2UtYWNjb3VudC51aWQiOiIzYzM5YzNhYS1kM2Q5LTExZTktYTVkMC0wMmI3YjllODg1OWUiLCJzdWIiOiJzeXN0ZW06c2VydmljZWFjY291bnQ6b3BlbnNoaWZ0LXNkbjpzZG4ifQ.PIs0rsUTekj5AX8yJeLDyW4vQB17YS4IOgO026yjEvsCY7Wv_2TD0lwyZWqyQh639q3jPh2_3LTQq2Cp0cReBP1PYOIGgprNm3C-3OFZRnkls-GH09kvPYE8J_-a1YwjxucOwytzJvEM5QTC9iXfEJNSTBfLge-HMYT1y0AGKs8DWTSC4rtd_2PedK3OYiAyDg_xHA8qNpG9pRNM8vfjV9VsmqJtlbnTVlTngqC0t5vyMaWrmLNRxN0rTbN2W9L3diXRnYqI8BUfgPQb7uhYcPuXGeypaFrN4d3yNN4NbgVxnkgdd2IXQ8elSJuQn6ynrvLgG0JPMmThOHnwvsZDeA`
func BenchmarkKeyFunc(b *testing.B) {
randomCacheKey := make([]byte, 32)
if _, err := rand.Read(randomCacheKey); err != nil {
b.Fatal(err) // rand should never fail
}
hashPool := &sync.Pool{
New: func() interface{} {
return hmac.New(sha256.New, randomCacheKey)
},
}
// use realistic audiences for benchmarking
auds := []string{"7daf30b7-a85c-429b-8b21-e666aecbb235", "c22aa267-bdde-4acb-8505-998be7818400", "44f9b4f3-7125-4333-b04c-1446a16c6113"}
b.Run("has audiences", func(b *testing.B) {
var key string
for n := 0; n < b.N; n++ {
key = keyFunc(hashPool, auds, jwtToken)
}
bKey = key
})
b.Run("nil audiences", func(b *testing.B) {
var key string
for n := 0; n < b.N; n++ {
key = keyFunc(hashPool, nil, jwtToken)
}
bKey = key
})
}