Skip to content

Commit

Permalink
Custom signature validation (#2171)
Browse files Browse the repository at this point in the history
#2045

`auth` section now has new `validate_signature` boolean field, and `signature` section for configuring signature flow.

Currently supports mashery signature validation modes `MasherySHA256`
and `MasheryMD5`.

```json
"auth": {
  "validate_signature": true,
  "signature": {
    "algorithm": "MasherySHA256",
    "header": "X-Signature",
    "secret": "secret",
    "allowed_clock_skew": 2
  }
}
```

"secret" field can hold dynamic values from meta or context, for example: "$tyk_meta.signature_secret".

Additionally, you can override error code and message using:
```
  "error_code": 403,
  "error_message": "your signature is invalid"
```

Benchmarks:

```
BenchmarkMasherySha256Sum_Hash-4          500000              2094 ns/op
208 B/op          4 allocs/op
```
  • Loading branch information
buger committed Mar 14, 2019
1 parent b11a7d1 commit 241fd01
Show file tree
Hide file tree
Showing 7 changed files with 343 additions and 6 deletions.
23 changes: 17 additions & 6 deletions apidef/api_definitions.go
Original file line number Diff line number Diff line change
Expand Up @@ -445,12 +445,23 @@ type APIDefinition struct {
}

type Auth struct {
UseParam bool `mapstructure:"use_param" bson:"use_param" json:"use_param"`
ParamName string `mapstructure:"param_name" bson:"param_name" json:"param_name"`
UseCookie bool `mapstructure:"use_cookie" bson:"use_cookie" json:"use_cookie"`
CookieName string `mapstructure:"cookie_name" bson:"cookie_name" json:"cookie_name"`
AuthHeaderName string `mapstructure:"auth_header_name" bson:"auth_header_name" json:"auth_header_name"`
UseCertificate bool `mapstructure:"use_certificate" bson:"use_certificate" json:"use_certificate"`
UseParam bool `mapstructure:"use_param" bson:"use_param" json:"use_param"`
ParamName string `mapstructure:"param_name" bson:"param_name" json:"param_name"`
UseCookie bool `mapstructure:"use_cookie" bson:"use_cookie" json:"use_cookie"`
CookieName string `mapstructure:"cookie_name" bson:"cookie_name" json:"cookie_name"`
AuthHeaderName string `mapstructure:"auth_header_name" bson:"auth_header_name" json:"auth_header_name"`
UseCertificate bool `mapstructure:"use_certificate" bson:"use_certificate" json:"use_certificate"`
ValidateSignature bool `mapstructure:"validate_signature" bson:"validate_signature" json:"validate_signature"`
Signature SignatureConfig `mapstructure:"signature" bson:"signature" json:"signature,omitempty"`
}

type SignatureConfig struct {
Algorithm string `mapstructure:"algorithm" bson:"algorithm" json:"algorithm"`
Header string `mapstructure:"header" bson:"header" json:"header"`
Secret string `mapstructure:"secret" bson:"secret" json:"secret"`
AllowedClockSkew int64 `mapstructure:"allowed_clock_skew" bson:"allowed_clock_skew" json:"allowed_clock_skew"`
ErrorCode int `mapstructure:"error_code" bson:"error_code" json:"error_code"`
ErrorMessage string `mapstructure:"error_message" bson:"error_message" json:"error_message"`
}

type GlobalRateLimit struct {
Expand Down
51 changes: 51 additions & 0 deletions mw_auth_key.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,12 @@ import (
"github.com/TykTechnologies/tyk/apidef"
"github.com/TykTechnologies/tyk/certs"
"github.com/TykTechnologies/tyk/request"
"github.com/TykTechnologies/tyk/signature_validator"
)

const (
defaultSignatureErrorCode = http.StatusUnauthorized
defaultSignatureErrorMessage = "Request signature verification failed"
)

// KeyExists will check if the key being used to access the API is in the request data,
Expand Down Expand Up @@ -104,6 +110,51 @@ func (k *AuthKey) ProcessRequest(w http.ResponseWriter, r *http.Request, _ inter
k.setContextVars(r, key)
}

return k.validateSignature(r, key)
}

func (k *AuthKey) validateSignature(r *http.Request, key string) (error, int) {
config := k.Spec.Auth
logger := k.Logger().WithField("key", obfuscateKey(key))

if !config.ValidateSignature {
return nil, http.StatusOK
}

errorCode := defaultSignatureErrorCode
if config.Signature.ErrorCode != 0 {
errorCode = config.Signature.ErrorCode
}

errorMessage := defaultSignatureErrorMessage
if config.Signature.ErrorMessage != "" {
errorMessage = config.Signature.ErrorMessage
}

validator := signature_validator.SignatureValidator{}
if err := validator.Init(config.Signature.Algorithm); err != nil {
logger.WithError(err).Info("Invalid signature verification algorithm")
return errors.New("internal server error"), http.StatusInternalServerError
}

signature := r.Header.Get(config.Signature.Header)
if signature == "" {
logger.Info("Request signature header not found or empty")
return errors.New(errorMessage), errorCode
}

secret := replaceTykVariables(r, config.Signature.Secret, false)

if secret == "" {
logger.Info("Request signature secret not found or empty")
return errors.New(errorMessage), errorCode
}

if err := validator.Validate(signature, key, secret, config.Signature.AllowedClockSkew); err != nil {
logger.WithError(err).Info("Request signature validation failed")
return errors.New(errorMessage), errorCode
}

return nil, http.StatusOK
}

Expand Down
76 changes: 76 additions & 0 deletions mw_auth_key_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package main

import (
"encoding/hex"
"fmt"
"net/http"
"net/http/httptest"
Expand All @@ -12,6 +13,7 @@ import (
"github.com/lonelycode/go-uuid/uuid"

"github.com/TykTechnologies/tyk/config"
"github.com/TykTechnologies/tyk/signature_validator"
"github.com/TykTechnologies/tyk/test"
"github.com/TykTechnologies/tyk/user"
)
Expand Down Expand Up @@ -101,6 +103,80 @@ func TestMurmur3CharBug(t *testing.T) {
})
}

func TestSignatureValidation(t *testing.T) {
defer resetTestConfig()
ts := newTykTestServer()
defer ts.Close()

api := buildAPI(func(spec *APISpec) {
spec.UseKeylessAccess = false
spec.Proxy.ListenPath = "/"
spec.Auth.ValidateSignature = true
spec.Auth.Signature.Algorithm = "MasheryMD5"
spec.Auth.Signature.Header = "Signature"
spec.Auth.Signature.Secret = "foobar"
spec.Auth.Signature.AllowedClockSkew = 1
})[0]

t.Run("Static signature", func(t *testing.T) {
api.Auth.Signature.Secret = "foobar"
loadAPI(api)

key := createSession()
hasher := signature_validator.MasheryMd5sum{}
validHash := hasher.Hash(key, "foobar", time.Now().Unix())

validSigHeader := map[string]string{
"authorization": key,
"signature": hex.EncodeToString(validHash),
}

invalidSigHeader := map[string]string{
"authorization": key,
"signature": "junk",
}

emptySigHeader := map[string]string{
"authorization": key,
}

ts.Run(t, []test.TestCase{
{Headers: emptySigHeader, Code: 401},
{Headers: invalidSigHeader, Code: 401},
{Headers: validSigHeader, Code: 200},
}...)
})

t.Run("Dynamic signature", func(t *testing.T) {
api.Auth.Signature.Secret = "$tyk_meta.signature_secret"
loadAPI(api)

key := createSession(func(s *user.SessionState) {
s.MetaData = map[string]interface{}{
"signature_secret": "foobar",
}
})

hasher := signature_validator.MasheryMd5sum{}
validHash := hasher.Hash(key, "foobar", time.Now().Unix())

validSigHeader := map[string]string{
"authorization": key,
"signature": hex.EncodeToString(validHash),
}

invalidSigHeader := map[string]string{
"authorization": key,
"signature": "junk",
}

ts.Run(t, []test.TestCase{
{Headers: invalidSigHeader, Code: 401},
{Headers: validSigHeader, Code: 200},
}...)
})
}

func createAuthKeyAuthSession(isBench bool) *user.SessionState {
session := new(user.SessionState)
// essentially non-throttled
Expand Down
36 changes: 36 additions & 0 deletions signature_validator/hash.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package signature_validator

import (
"crypto/md5"
"crypto/sha256"
"strconv"
)

type Hasher interface {
Name() string
Hash(token string, sharedSecret string, timeStamp int64) []byte
}

type MasherySha256Sum struct{}

func (m MasherySha256Sum) Name() string {
return "MasherySHA256"
}

func (m MasherySha256Sum) Hash(token string, sharedSecret string, timeStamp int64) []byte {
signature := sha256.Sum256([]byte(token + sharedSecret + strconv.FormatInt(timeStamp, 10)))

return signature[:]
}

type MasheryMd5sum struct{}

func (m MasheryMd5sum) Name() string {
return "MasheryMD5"
}

func (m MasheryMd5sum) Hash(token string, sharedSecret string, timeStamp int64) []byte {
signature := md5.Sum([]byte(token + sharedSecret + strconv.FormatInt(timeStamp, 10)))

return signature[:]
}
34 changes: 34 additions & 0 deletions signature_validator/hash_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package signature_validator

import (
"encoding/hex"
"testing"
"time"
)

const (
token = "5bcef48a3f03d311ff27d156630baf849e3b438b8a48fec99239d5c9"
sharedSecret = "foobar"
now = 1546259837
)

func TestMasherySha256Sum_Hash(t *testing.T) {
expected := "fce2e80253cd438b666341176f34bde499116b63719e2482dae6965518ffd316"

hasher := MasherySha256Sum{}
hashed := hex.EncodeToString(hasher.Hash(token, sharedSecret, now))

if hashed != expected {
t.Fatalf("expected %s, got %s", expected, hashed)
}
}

func BenchmarkMasherySha256Sum_Hash(b *testing.B) {

b.ReportAllocs()

for n := 0; n < b.N; n++ {
hasher := MasherySha256Sum{}
hasher.Hash(token, sharedSecret, time.Now().Unix())
}
}
54 changes: 54 additions & 0 deletions signature_validator/validate.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
package signature_validator

import (
"bytes"
"encoding/hex"
"errors"
"fmt"
"time"
)

type Validator interface {
Init(hasherName string) error
Validate(attempt, actual string, allowedClockSkew int64) error
}

type SignatureValidator struct {
h Hasher
}

func (v *SignatureValidator) Init(hasherName string) error {
switch hasherName {
case "MasherySHA256":
v.h = MasherySha256Sum{}
case "MasheryMD5":
v.h = MasheryMd5sum{}
default:
return errors.New(fmt.Sprintf("unsupported hasher type (%s)", hasherName))
}

return nil
}

func (v SignatureValidator) Validate(signature, key, secret string, allowedClockSkew int64) error {
signatureBytes, _ := hex.DecodeString(signature)

fmt.Println("Validating:", signature, key, secret)

now := time.Now().Unix()
for i := int64(0); i <= allowedClockSkew; i++ {
if bytes.Equal(v.h.Hash(key, secret, now+i), signatureBytes) {
return nil
}

if i == int64(0) {
continue
}

if bytes.Equal(v.h.Hash(key, secret, now-i), signatureBytes) {
return nil
}
}

return errors.New("signature is not valid")
}
75 changes: 75 additions & 0 deletions signature_validator/validate_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
package signature_validator

import (
"encoding/hex"
"testing"
"time"

"github.com/pkg/errors"
)

func TestValidateSignature_Init(t *testing.T) {

type tt = struct {
In string
Error error
}

suite := []tt{
{"", errors.New("empty string in init")},
{"SomeJunk", errors.New("non existent")},
{"MasherySHA256", nil},
{"MasheryMD5", nil},
}

for _, s := range suite {

validator := SignatureValidator{}
err := validator.Init(s.In)

if err != nil && s.Error == nil {
t.Errorf("expected success, got error %s", err.Error())
t.FailNow()
}

if err == nil && s.Error != nil {
t.Errorf("expected error (%s), got success", s.Error.Error())
t.FailNow()
}
}
}

func TestValidateSignature_Validate(t *testing.T) {
type tt struct {
SignatureAttempt string
Error error
}

allowedClockSkew := int64(100)

suite := []tt{
{SignatureAttempt: "", Error: errors.New("should not pass with missing signature")},
{SignatureAttempt: "abcde", Error: errors.New("should not pass with incorrect signature")},
{SignatureAttempt: hex.EncodeToString(MasherySha256Sum{}.Hash(token, sharedSecret, time.Now().Unix()-101)), Error: errors.New("clock too slow")},
{SignatureAttempt: hex.EncodeToString(MasherySha256Sum{}.Hash(token, sharedSecret, time.Now().Unix()+101)), Error: errors.New("clock too fast")},
{SignatureAttempt: hex.EncodeToString(MasherySha256Sum{}.Hash(token, sharedSecret, time.Now().Unix())), Error: nil},
{SignatureAttempt: hex.EncodeToString(MasherySha256Sum{}.Hash(token, sharedSecret, time.Now().Unix()+99)), Error: nil},
{SignatureAttempt: hex.EncodeToString(MasherySha256Sum{}.Hash(token, sharedSecret, time.Now().Unix()-99)), Error: nil},
}

validator := SignatureValidator{}
_ = validator.Init("MasherySHA256")

for _, s := range suite {
err := validator.Validate(s.SignatureAttempt, token, sharedSecret, allowedClockSkew)
if err != nil && s.Error == nil {
t.Errorf("expected valid, got error %s", err.Error())
t.FailNow()
}

if err == nil && s.Error != nil {
t.Errorf("expected error (%s), got valid", s.Error.Error())
t.FailNow()
}
}
}

0 comments on commit 241fd01

Please sign in to comment.