Skip to content

Commit

Permalink
[TT-1542] Add fallback hash key functions to catch function changes (T…
Browse files Browse the repository at this point in the history
…ykTechnologies#3505)

Added new `hash_key_function_fallback` array option to specify list of hash function which Gateway needs to check if key with default hash key function not found. 

This functionality needed only if you change default hashing algorithm, but your token information does not hold information about hashing function like custom keys, certificates, or basic auth tokens.
  • Loading branch information
furkansenharputlu authored Mar 30, 2021
1 parent 1f876ba commit d2f9f9a
Show file tree
Hide file tree
Showing 8 changed files with 169 additions and 28 deletions.
16 changes: 16 additions & 0 deletions cli/linter/schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -433,6 +433,22 @@
"sha256"
]
},
"hash_key_function_fallback": {
"type": [
"array",
"null"
],
"items": {
"type": "string",
"enum": [
"",
"murmur32",
"murmur64",
"murmur128",
"sha256"
]
}
},
"health_check": {
"type": [
"object",
Expand Down
1 change: 1 addition & 0 deletions config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -331,6 +331,7 @@ type Config struct {
// Gateway Security Policies
HashKeys bool `json:"hash_keys"`
HashKeyFunction string `json:"hash_key_function"`
HashKeyFunctionFallback []string `json:"hash_key_function_fallback"`
EnableHashedKeysListing bool `json:"enable_hashed_keys_listing"`
MinTokenLength int `json:"min_token_length"`
EnableAPISegregation bool `json:"enable_api_segregation"`
Expand Down
21 changes: 13 additions & 8 deletions gateway/auth_manager.go
Original file line number Diff line number Diff line change
Expand Up @@ -139,13 +139,13 @@ func (b *DefaultSessionManager) SessionDetail(orgID string, keyName string, hash
} else {
if storage.TokenOrg(keyName) != orgID {
// try to get legacy and new format key at once
toSearchList := []string{generateToken(orgID, keyName), keyName}
for _, fallback := range config.Global().HashKeyFunctionFallback {
toSearchList = append(toSearchList, generateToken(orgID, keyName, fallback))
}

var jsonKeyValList []string
jsonKeyValList, err = b.store.GetMultiKey(
[]string{
generateToken(orgID, keyName),
keyName,
},
)
jsonKeyValList, err = b.store.GetMultiKey(toSearchList)

// pick the 1st non empty from the returned list
for _, val := range jsonKeyValList {
Expand Down Expand Up @@ -186,10 +186,15 @@ func (b *DefaultSessionManager) Sessions(filter string) []string {

type DefaultKeyGenerator struct{}

func generateToken(orgID, keyID string) string {
func generateToken(orgID, keyID string, customHashKeyFunction ...string) string {
keyID = strings.TrimPrefix(keyID, orgID)
token, err := storage.GenerateToken(orgID, keyID, config.Global().HashKeyFunction)
hashKeyFunction := config.Global().HashKeyFunction

if len(customHashKeyFunction) > 0 {
hashKeyFunction = customHashKeyFunction[0]
}

token, err := storage.GenerateToken(orgID, keyID, hashKeyFunction)
if err != nil {
log.WithFields(logrus.Fields{
"prefix": "auth-mgr",
Expand Down
109 changes: 109 additions & 0 deletions gateway/auth_manager_test.go
Original file line number Diff line number Diff line change
@@ -1,9 +1,14 @@
package gateway

import (
"crypto/x509"
"net/http"
"testing"

"github.com/TykTechnologies/tyk/apidef"
"github.com/TykTechnologies/tyk/certs"
"github.com/TykTechnologies/tyk/headers"

"github.com/TykTechnologies/tyk/storage"

"github.com/TykTechnologies/tyk/config"
Expand Down Expand Up @@ -102,3 +107,107 @@ func TestAuthenticationAfterUpdateKey(t *testing.T) {
assert(true)
})
}

func TestHashKeyFunctionChanged(t *testing.T) {
_, _, combinedPEM, _ := genServerCertificate()
serverCertID, _ := CertificateManager.Add(combinedPEM, "")
defer CertificateManager.Delete(serverCertID, "")

_, _, _, clientCert := genCertificate(&x509.Certificate{})
clientCertID := certs.HexSHA256(clientCert.Certificate[0])
client := GetTLSClient(nil, nil)

globalConf := config.Global()
globalConf.HttpServerOptions.UseSSL = true
globalConf.HttpServerOptions.SSLCertificates = []string{serverCertID}
globalConf.HashKeys = true
globalConf.HashKeyFunction = "murmur64"
globalConf.LocalSessionCache.DisableCacheSessionState = true
config.SetGlobal(globalConf)

defer ResetTestConfig()

g := StartTest()
defer g.Close()

api := BuildAndLoadAPI(func(spec *APISpec) {
spec.Proxy.ListenPath = "/"
spec.UseKeylessAccess = false
spec.AuthConfigs = map[string]apidef.AuthConfig{
authTokenType: {UseCertificate: true},
}
})[0]

globalConf = config.Global()

testChangeHashFunc := func(t *testing.T, authHeader map[string]string, client *http.Client, failCode int) {
_, _ = g.Run(t, test.TestCase{Headers: authHeader, Client: client, Code: http.StatusOK})

globalConf.HashKeyFunction = "sha256"
config.SetGlobal(globalConf)

_, _ = g.Run(t, test.TestCase{Headers: authHeader, Client: client, Code: failCode})

globalConf.HashKeyFunctionFallback = []string{"murmur64"}
config.SetGlobal(globalConf)

_, _ = g.Run(t, test.TestCase{Headers: authHeader, Client: client, Code: http.StatusOK})

// Reset
globalConf.HashKeyFunction = "murmur64"
globalConf.HashKeyFunctionFallback = nil
config.SetGlobal(globalConf)
}

t.Run("custom key", func(t *testing.T) {
const customKey = "custom-key"

session := CreateStandardSession()
session.AccessRights = map[string]user.AccessDefinition{"test": {
APIID: "test", Versions: []string{"v1"},
}}

_, _ = g.Run(t, test.TestCase{AdminAuth: true, Method: http.MethodPost, Path: "/tyk/keys/" + customKey,
Data: session, Client: client, Code: http.StatusOK})

testChangeHashFunc(t, map[string]string{headers.Authorization: customKey}, client, http.StatusForbidden)
})

t.Run("basic auth key", func(t *testing.T) {
api.UseBasicAuth = true
LoadAPI(api)
globalConf = config.Global()

session := CreateStandardSession()
session.BasicAuthData.Password = "password"
session.AccessRights = map[string]user.AccessDefinition{"test": {
APIID: "test", Versions: []string{"v1"},
}}

_, _ = g.Run(t, test.TestCase{AdminAuth: true, Method: http.MethodPost, Path: "/tyk/keys/user",
Data: session, Client: client, Code: http.StatusOK})

authHeader := map[string]string{"Authorization": genAuthHeader("user", "password")}

testChangeHashFunc(t, authHeader, client, http.StatusUnauthorized)

api.UseBasicAuth = false
LoadAPI(api)
globalConf = config.Global()
})

t.Run("client certificate", func(t *testing.T) {
session := CreateStandardSession()
session.Certificate = clientCertID
session.BasicAuthData.Password = "password"
session.AccessRights = map[string]user.AccessDefinition{"test": {
APIID: "test", Versions: []string{"v1"},
}}

_, _ = g.Run(t, test.TestCase{AdminAuth: true, Method: http.MethodPost, Path: "/tyk/keys/create",
Data: session, Client: client, Code: http.StatusOK})

client = GetTLSClient(&clientCert, nil)
testChangeHashFunc(t, nil, client, http.StatusForbidden)
})
}
10 changes: 6 additions & 4 deletions gateway/middleware.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import (
"github.com/justinas/alice"
newrelic "github.com/newrelic/go-agent"
"github.com/paulbellamy/ratecounter"
cache "github.com/pmylund/go-cache"
"github.com/pmylund/go-cache"
"github.com/sirupsen/logrus"
"golang.org/x/sync/singleflight"

Expand Down Expand Up @@ -652,9 +652,11 @@ func (t BaseMiddleware) CheckSessionAndIdentityForValidKey(originalKey *string,

// Try and get the session from the session store
t.Logger().Debug("Querying local cache")
keyHash := key
cacheKey := key
if t.Spec.GlobalConfig.HashKeys {
cacheKey = storage.HashStr(key)
keyHash = storage.HashStr(key)
cacheKey = storage.HashStr(key, storage.HashMurmur64) // always hash cache keys with murmur64 to prevent collisions
}

// Check in-memory cache
Expand All @@ -675,7 +677,7 @@ func (t BaseMiddleware) CheckSessionAndIdentityForValidKey(originalKey *string,
t.Logger().Debug("Querying keystore")
session, found := GlobalSessionManager.SessionDetail(t.Spec.OrgID, key, false)
if found {
session.SetKeyHash(cacheKey)
session.SetKeyHash(keyHash)
// If exists, assume it has been authorized and pass on
// cache it
clone := session.Clone()
Expand Down Expand Up @@ -704,7 +706,7 @@ func (t BaseMiddleware) CheckSessionAndIdentityForValidKey(originalKey *string,
// update value of originalKey, as for custom-keys it might get updated (the key is generated again using alias)
*originalKey = key

session.SetKeyHash(cacheKey)
session.SetKeyHash(keyHash)
// If not in Session, and got it from AuthHandler, create a session with a new TTL
t.Logger().Info("Recreating session for key: ", obfuscateKey(key))

Expand Down
27 changes: 14 additions & 13 deletions gateway/mw_auth_key.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,11 @@ import (
"net/http"
"strings"

"github.com/TykTechnologies/tyk/apidef"
"github.com/TykTechnologies/tyk/certs"

"github.com/TykTechnologies/tyk/user"

"github.com/TykTechnologies/tyk/apidef"
"github.com/TykTechnologies/tyk/config"
"github.com/TykTechnologies/tyk/request"
"github.com/TykTechnologies/tyk/signature_validator"
Expand Down Expand Up @@ -66,25 +69,23 @@ func (k *AuthKey) ProcessRequest(w http.ResponseWriter, r *http.Request, _ inter
return nil, http.StatusOK
}

key, config := k.getAuthToken(k.getAuthType(), r)
key, authConfig := k.getAuthToken(k.getAuthType(), r)

// If key not provided in header or cookie and client certificate is provided, try to find certificate based key
if config.UseCertificate && key == "" && r.TLS != nil && len(r.TLS.PeerCertificates) > 0 {
key = generateToken(k.Spec.OrgID, certs.HexSHA256(r.TLS.PeerCertificates[0].Raw))
}
keyExists := false
var session user.SessionState
if key != "" {
key = stripBearer(key)
} else if authConfig.UseCertificate && key == "" && r.TLS != nil && len(r.TLS.PeerCertificates) > 0 {
log.Debug("Trying to find key by client certificate")

if key == "" {
// No header value, fail
key = certs.HexSHA256(r.TLS.PeerCertificates[0].Raw)
} else {
k.Logger().Info("Attempted access with malformed header, no auth header found.")

return errorAndStatusCode(ErrAuthAuthorizationFieldMissing)
}

// Ignore Bearer prefix on token if it exists
key = stripBearer(key)

// Check if API key valid
session, keyExists := k.CheckSessionAndIdentityForValidKey(&key, r)
session, keyExists = k.CheckSessionAndIdentityForValidKey(&key, r)
if !keyExists {
k.Logger().WithField("key", obfuscateKey(key)).Info("Attempted access with non-existent key.")

Expand Down
2 changes: 1 addition & 1 deletion gateway/mw_basic_auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -178,7 +178,7 @@ func (k *BasicAuthKeyIsValid) ProcessRequest(w http.ResponseWriter, r *http.Requ
}

// Check if API key valid
keyName := generateToken(k.Spec.OrgID, username)
keyName := username
logger := k.Logger().WithField("key", obfuscateKey(keyName))
session, keyExists := k.CheckSessionAndIdentityForValidKey(&keyName, r)
if !keyExists {
Expand Down
11 changes: 9 additions & 2 deletions storage/storage.go
Original file line number Diff line number Diff line change
Expand Up @@ -144,8 +144,15 @@ func hashFunction(algorithm string) (hash.Hash, error) {
}
}

func HashStr(in string) string {
h, _ := hashFunction(TokenHashAlgo(in))
func HashStr(in string, withAlg ...string) string {
var algo string
if len(withAlg) > 0 && withAlg[0] != "" {
algo = withAlg[0]
} else {
algo = TokenHashAlgo(in)
}

h, _ := hashFunction(algo)
h.Write([]byte(in))
return hex.EncodeToString(h.Sum(nil))
}
Expand Down

0 comments on commit d2f9f9a

Please sign in to comment.