From 9129a69880564bc256e858fa82474b917f3d92a0 Mon Sep 17 00:00:00 2001 From: Komal Sukhani Date: Fri, 23 Aug 2019 18:20:31 +0530 Subject: [PATCH 01/10] Added following new fields in user session 1. RSAEnabled 2. RSACertificateId --- user/session.go | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/user/session.go b/user/session.go index abb2010611e3..228ef764a99e 100644 --- a/user/session.go +++ b/user/session.go @@ -75,13 +75,15 @@ type SessionState struct { JWTData struct { Secret string `json:"secret" msg:"secret"` } `json:"jwt_data" msg:"jwt_data"` - HMACEnabled bool `json:"hmac_enabled" msg:"hmac_enabled"` - HmacSecret string `json:"hmac_string" msg:"hmac_string"` - IsInactive bool `json:"is_inactive" msg:"is_inactive"` - ApplyPolicyID string `json:"apply_policy_id" msg:"apply_policy_id"` - ApplyPolicies []string `json:"apply_policies" msg:"apply_policies"` - DataExpires int64 `json:"data_expires" msg:"data_expires"` - Monitor struct { + HMACEnabled bool `json:"hmac_enabled" msg:"hmac_enabled"` + RSAEnabled bool `json:"rsa_enabled" msg:"rsa_enabled"` + HmacSecret string `json:"hmac_string" msg:"hmac_string"` + RSACertificateId string `json:"rsa_certificate_id" msg:"rsa_certificate_id"` + IsInactive bool `json:"is_inactive" msg:"is_inactive"` + ApplyPolicyID string `json:"apply_policy_id" msg:"apply_policy_id"` + ApplyPolicies []string `json:"apply_policies" msg:"apply_policies"` + DataExpires int64 `json:"data_expires" msg:"data_expires"` + Monitor struct { TriggerLimits []float64 `json:"trigger_limits" msg:"trigger_limits"` } `json:"monitor" msg:"monitor"` EnableDetailedRecording bool `json:"enable_detail_recording" msg:"enable_detail_recording"` From 9e6ba67dcc6ca72971922a1bbf5788aed41d4926 Mon Sep 17 00:00:00 2001 From: Komal Sukhani Date: Fri, 23 Aug 2019 18:22:18 +0530 Subject: [PATCH 02/10] Added new field in Policy 1. RSAEnabled --- gateway/middleware.go | 2 ++ gateway/mw_jwt.go | 1 + user/policy.go | 1 + 3 files changed, 4 insertions(+) diff --git a/gateway/middleware.go b/gateway/middleware.go index 4d6c0b296d9c..25f13e595f36 100644 --- a/gateway/middleware.go +++ b/gateway/middleware.go @@ -418,6 +418,7 @@ func (t BaseMiddleware) ApplyPolicies(session *user.SessionState) error { rights[k] = v } session.HMACEnabled = policy.HMACEnabled + session.RSAEnabled = policy.RSAEnabled } } else { if len(policies) > 1 { @@ -443,6 +444,7 @@ func (t BaseMiddleware) ApplyPolicies(session *user.SessionState) error { // ACL rights = policy.AccessRights session.HMACEnabled = policy.HMACEnabled + session.RSAEnabled = policy.RSAEnabled } // Required for all diff --git a/gateway/mw_jwt.go b/gateway/mw_jwt.go index 7bb9349c1959..0ea6411718ad 100644 --- a/gateway/mw_jwt.go +++ b/gateway/mw_jwt.go @@ -702,6 +702,7 @@ func generateSessionFromPolicy(policyID, orgID string, enforceOrg bool) (user.Se session.AccessRights[apiID] = access } session.HMACEnabled = policy.HMACEnabled + session.RSAEnabled = policy.RSAEnabled session.IsInactive = policy.IsInactive session.Tags = policy.Tags diff --git a/user/policy.go b/user/policy.go index fca8aef53e20..2f957b496f85 100644 --- a/user/policy.go +++ b/user/policy.go @@ -14,6 +14,7 @@ type Policy struct { ThrottleRetryLimit int `bson:"throttle_retry_limit" json:"throttle_retry_limit"` AccessRights map[string]AccessDefinition `bson:"access_rights" json:"access_rights"` HMACEnabled bool `bson:"hmac_enabled" json:"hmac_enabled"` + RSAEnabled bool `bson:"rsa_enabled" json:"rsa_enabled"` Active bool `bson:"active" json:"active"` IsInactive bool `bson:"is_inactive" json:"is_inactive"` Tags []string `bson:"tags" json:"tags"` From 6d7908e75bb5bfadc5b40dd563aaec07523261ee Mon Sep 17 00:00:00 2001 From: Komal Sukhani Date: Fri, 23 Aug 2019 18:23:15 +0530 Subject: [PATCH 03/10] Added a new method in Certificate Manager to fetch publickey --- certs/manager.go | 36 ++++++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/certs/manager.go b/certs/manager.go index 96ce68c2589c..747e4d4a7068 100644 --- a/certs/manager.go +++ b/certs/manager.go @@ -331,6 +331,42 @@ func (c *CertificateManager) ListPublicKeys(keyIDs []string) (out []string) { return out } +// Returns list of fingerprints +func (c *CertificateManager) ListRawPublicKey(keyID string) (out interface{}) { + var rawKey []byte + var err error + + if isSHA256(keyID) { + var val string + val, err = c.storage.GetKey("raw-" + keyID) + if err != nil { + c.logger.Warn("Can't retrieve public key from Redis:", keyID, err) + return nil + } + rawKey = []byte(val) + } else { + rawKey, err = ioutil.ReadFile(keyID) + if err != nil { + c.logger.Error("Error while reading public key from file:", keyID, err) + return nil + } + } + + block, _ := pem.Decode(rawKey) + if block == nil { + c.logger.Error("Can't parse public key:", keyID) + return nil + } + + out, err = x509.ParsePKIXPublicKey(block.Bytes) + if err != nil { + c.logger.Error("Error while parsing public key:", keyID, err) + return nil + } + + return out +} + func (c *CertificateManager) ListAllIds(prefix string) (out []string) { keys := c.storage.GetKeys("raw-" + prefix + "*") From 5343f61b6fddd4977dddc00f0452645cbcd02c3e Mon Sep 17 00:00:00 2001 From: Komal Sukhani Date: Fri, 23 Aug 2019 18:25:21 +0530 Subject: [PATCH 04/10] Added support of RSA in request checking middleware --- gateway/mw_hmac.go | 173 +++++++++++++++++++++++++++++++++++---------- 1 file changed, 135 insertions(+), 38 deletions(-) diff --git a/gateway/mw_hmac.go b/gateway/mw_hmac.go index b7b6ef040a3d..a6a9af6419c0 100644 --- a/gateway/mw_hmac.go +++ b/gateway/mw_hmac.go @@ -1,7 +1,9 @@ package gateway import ( + "crypto" "crypto/hmac" + "crypto/rsa" "crypto/sha1" "crypto/sha256" "crypto/sha512" @@ -61,21 +63,12 @@ func (hm *HMACMiddleware) ProcessRequest(w http.ResponseWriter, r *http.Request, } // Generate a signature string - signatureString, err := generateHMACSignatureStringFromRequest(r, fieldValues.Headers) + signatureString, err := generateSignatureStringFromRequest(r, fieldValues.Headers) if err != nil { logger.WithError(err).WithField("signature_string", signatureString).Error("Signature string generation failed") return hm.authorizationError(r) } - // Get a session for the Key ID - secret, session, err := hm.getSecretAndSessionForKeyID(r, fieldValues.KeyID) - if err != nil { - logger.WithError(err).WithFields(logrus.Fields{ - "keyID": fieldValues.KeyID, - }).Error("No HMAC secret for this key") - return hm.authorizationError(r) - } - if len(hm.Spec.HmacAllowedAlgorithms) > 0 { algorithmAllowed := false for _, alg := range hm.Spec.HmacAllowedAlgorithms { @@ -90,31 +83,95 @@ func (hm *HMACMiddleware) ProcessRequest(w http.ResponseWriter, r *http.Request, } } - // Create a signed string with the secret - encodedSignature := generateEncodedSignature(signatureString, secret, fieldValues.Algorthm) - - // Compare - matchPass := encodedSignature == fieldValues.Signature - - // Check for lower case encoding (.Net issues, again) - if !matchPass { - isLower, lowerList := hm.hasLowerCaseEscaped(fieldValues.Signature) - if isLower { - logger.Debug("--- Detected lower case encoding! ---") - upperedSignature := hm.replaceWithUpperCase(fieldValues.Signature, lowerList) - if encodedSignature == upperedSignature { - matchPass = true - encodedSignature = upperedSignature - } + var secret string + var rsaKey *rsa.PublicKey + var session user.SessionState + + if strings.HasPrefix(fieldValues.Algorthm, "rsa") { + var certificateId string + + certificateId, session, err = hm.getRSACertificateIdAndSessionForKeyID(r, fieldValues.KeyID) + if err != nil { + logger.WithError(err).WithFields(logrus.Fields{ + "keyID": fieldValues.KeyID, + }).Error("Failed to fetch session/public key") + return hm.authorizationError(r) + } + + publicKey := CertificateManager.ListRawPublicKey(certificateId) + if publicKey == nil { + log.Error("Certificate not found") + return errors.New("Certificate not found"), http.StatusInternalServerError + } + var ok bool + rsaKey, ok = publicKey.(*rsa.PublicKey) + if !ok { + log.Error("Certificate doesn't contain RSA Public key") + return errors.New("Certificate doesn't contain RSA Public key"), http.StatusInternalServerError + } + } else { + // Get a session for the Key ID + secret, session, err = hm.getSecretAndSessionForKeyID(r, fieldValues.KeyID) + if err != nil { + logger.WithError(err).WithFields(logrus.Fields{ + "keyID": fieldValues.KeyID, + }).Error("No HMAC secret for this key") + return hm.authorizationError(r) } } + var matchPass bool - if !matchPass { - logger.WithFields(logrus.Fields{ - "expected": encodedSignature, - "got": fieldValues.Signature, - }).Error("Signature string does not match!") - return hm.authorizationError(r) + if strings.HasPrefix(fieldValues.Algorthm, "rsa") { + matchPass, err = validateRSAEncodedSignature(signatureString, rsaKey, fieldValues.Algorthm, fieldValues.Signature) + if err != nil { + logger.WithError(err).Error("Signature validation failed.") + } + + if !matchPass { + isLower, lowerList := hm.hasLowerCaseEscaped(fieldValues.Signature) + if isLower { + logger.Debug("--- Detected lower case encoding! ---") + upperedSignature := hm.replaceWithUpperCase(fieldValues.Signature, lowerList) + matchPass, err = validateRSAEncodedSignature(signatureString, rsaKey, fieldValues.Algorthm, upperedSignature) + if err != nil { + logger.WithError(err).Error("Signature validation failed.") + } + } + } + + if !matchPass { + logger.WithFields(logrus.Fields{ + "got": fieldValues.Signature, + }).Error("Signature string does not match!") + return hm.authorizationError(r) + } + } else { + // Create a signed string with the secret + encodedSignature := generateHMACEncodedSignature(signatureString, secret, fieldValues.Algorthm) + + // Compare + matchPass = encodedSignature == fieldValues.Signature + + // Check for lower case encoding (.Net issues, again) + if !matchPass { + isLower, lowerList := hm.hasLowerCaseEscaped(fieldValues.Signature) + if isLower { + logger.Debug("--- Detected lower case encoding! ---") + upperedSignature := hm.replaceWithUpperCase(fieldValues.Signature, lowerList) + if encodedSignature == upperedSignature { + matchPass = true + encodedSignature = upperedSignature + } + } + } + + if !matchPass { + logger.WithFields(logrus.Fields{ + "expected": encodedSignature, + "got": fieldValues.Signature, + }).Error("Signature string does not match!") + return hm.authorizationError(r) + } } // Check clock skew @@ -233,6 +290,20 @@ func (hm *HMACMiddleware) getSecretAndSessionForKeyID(r *http.Request, keyId str return session.HmacSecret, session, nil } +func (hm *HMACMiddleware) getRSACertificateIdAndSessionForKeyID(r *http.Request, keyId string) (string, user.SessionState, error) { + session, keyExists := hm.CheckSessionAndIdentityForValidKey(keyId, r) + if !keyExists { + return "", session, errors.New("Key ID does not exist") + } + + if session.RSACertificateId == "" || !session.RSAEnabled { + hm.Logger().Info("API Requires RSA signature, session missing RSA Certificate Id or RSA not enabled for key") + return "", session, errors.New("This key ID is invalid") + } + + return session.RSACertificateId, session, nil +} + func getDateHeader(r *http.Request) (string, string) { auxHeaderVal := r.Header.Get(altHeaderSpec) // Prefer aux if present @@ -260,10 +331,7 @@ func getFieldValues(authHeader string) (*HMACFieldValues, error) { set := HMACFieldValues{} for _, element := range strings.Split(authHeader, ",") { - kv := strings.Split(element, "=") - if len(kv) != 2 { - return nil, errors.New("Header field value malformed (need two elements in field)") - } + kv := strings.SplitN(element, "=", 2) key := strings.ToLower(kv[0]) value := strings.Trim(kv[1], `"`) @@ -296,7 +364,7 @@ func getFieldValues(authHeader string) (*HMACFieldValues, error) { // "Signature keyId="9876",algorithm="hmac-sha1",headers="x-test x-test-2",signature="queryEscape(base64(sig))"") -func generateHMACSignatureStringFromRequest(r *http.Request, headers []string) (string, error) { +func generateSignatureStringFromRequest(r *http.Request, headers []string) (string, error) { signatureString := "" for i, header := range headers { loweredHeader := strings.TrimSpace(strings.ToLower(header)) @@ -321,7 +389,7 @@ func generateHMACSignatureStringFromRequest(r *http.Request, headers []string) ( return signatureString, nil } -func generateEncodedSignature(signatureString, secret string, algorithm string) string { +func generateHMACEncodedSignature(signatureString, secret string, algorithm string) string { key := []byte(secret) var hashFunction func() hash.Hash @@ -342,3 +410,32 @@ func generateEncodedSignature(signatureString, secret string, algorithm string) encodedString := base64.StdEncoding.EncodeToString(h.Sum(nil)) return url.QueryEscape(encodedString) } + +func validateRSAEncodedSignature(signatureString string, publicKey *rsa.PublicKey, algorithm string, signature string) (bool, error) { + var hashFunction hash.Hash + var hashType crypto.Hash + + switch algorithm { + case "rsa-sha256": + hashFunction = sha256.New() + hashType = crypto.SHA256 + default: + hashFunction = sha256.New() + hashType = crypto.SHA256 + } + hashFunction.Write([]byte(signatureString)) + hashed := hashFunction.Sum(nil) + + decodedSignature, err := base64.StdEncoding.DecodeString(signature) + if err != nil { + log.Error("Error while base64 decoding signature:", err) + return false, err + } + err = rsa.VerifyPKCS1v15(publicKey, hashType, hashed, decodedSignature) + if err != nil { + log.Error("Signature match failed:", err) + return false, err + } + + return true, nil +} From 002ddba61bdc6bb18d94e58ae7365ed12089f0f9 Mon Sep 17 00:00:00 2001 From: Komal Sukhani Date: Fri, 23 Aug 2019 18:27:07 +0530 Subject: [PATCH 05/10] Add test cases for RSA signature validation --- gateway/mw_hmac_test.go | 171 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 171 insertions(+) diff --git a/gateway/mw_hmac_test.go b/gateway/mw_hmac_test.go index b12cbf416de6..9ebd9ef09960 100644 --- a/gateway/mw_hmac_test.go +++ b/gateway/mw_hmac_test.go @@ -1,10 +1,16 @@ package gateway import ( + "crypto" "crypto/hmac" + "crypto/rand" + "crypto/rsa" "crypto/sha1" + "crypto/sha256" "crypto/sha512" + "crypto/x509" "encoding/base64" + "encoding/pem" "fmt" "hash" "net/http" @@ -57,6 +63,21 @@ func createHMACAuthSession() *user.SessionState { return session } +func createRSAAuthSession(pubCertId string) *user.SessionState { + session := new(user.SessionState) + session.Rate = 8.0 + session.Allowance = session.Rate + session.LastCheck = time.Now().Unix() + session.Per = 1.0 + session.QuotaRenewalRate = 300 // 5 minutes + session.QuotaRenews = time.Now().Unix() + 20 + session.QuotaRemaining = 1 + session.QuotaMax = -1 + session.RSAEnabled = true + session.RSACertificateId = pubCertId + return session +} + func getHMACAuthChain(spec *APISpec) http.Handler { remote, _ := url.Parse(testHttpAny) proxy := TykNewSingleHostReverseProxy(remote, spec) @@ -153,6 +174,59 @@ func testPrepareHMACAuthSessionPass(tb testing.TB, hashFn func() hash.Hash, even return encodedString, spec, req, sessionKey } +func testPrepareRSAAuthSessionPass(tb testing.TB, eventWG *sync.WaitGroup, privateKey *rsa.PrivateKey, pubCertId string, withHeader bool, isBench bool) (string, *APISpec, *http.Request, string) { + spec := CreateSpecTest(tb, hmacAuthDef) + session := createRSAAuthSession(pubCertId) + + // Should not receive an AuthFailure event + cb := func(em config.EventMessage) { + eventWG.Done() + } + spec.EventPaths = map[apidef.TykEvent][]config.TykEventHandler{ + "AuthFailure": {&testAuthFailEventHandler{cb}}, + } + + sessionKey := "" + if isBench { + sessionKey = uuid.New() + } else { + sessionKey = "9876" + } + + spec.SessionManager.UpdateSession(sessionKey, session, 60, false) + + req := TestReq(tb, "GET", "/", nil) + + refDate := "Mon, 02 Jan 2006 15:04:05 MST" + + // Signature needs to be: Authorization: Signature keyId="hmac-key-1",algorithm="hmac-sha1",signature="Base64(HMAC-SHA1(signing string))" + + // Prep the signature string + tim := time.Now().Format(refDate) + req.Header.Set("Date", tim) + signatureString := "" + if withHeader { + req.Header.Set("X-Test-1", "hello") + req.Header.Set("X-Test-2", "world") + signatureString = strings.ToLower("(request-target): ") + "get /\n" + signatureString += strings.ToLower("Date") + ": " + tim + "\n" + signatureString += strings.ToLower("X-Test-1") + ": " + "hello" + "\n" + signatureString += strings.ToLower("X-Test-2") + ": " + "world" + } else { + signatureString = strings.ToLower("Date") + ": " + tim + } + + h := sha256.New() + h.Write([]byte(signatureString)) + hashed := h.Sum(nil) + + signature, _ := rsa.SignPKCS1v15(rand.Reader, privateKey, crypto.SHA256, hashed) + + sigString := base64.StdEncoding.EncodeToString(signature) + + return sigString, spec, req, sessionKey +} + func TestHMACAuthSessionPass(t *testing.T) { // Should not receive an AuthFailure event var eventWG sync.WaitGroup @@ -543,3 +617,100 @@ func TestHMACAuthSessionPassWithHeaderFieldLowerCase(t *testing.T) { t.Error("Request should not have generated an AuthFailure event!: \n") } } + +func TestRSAAuthSessionPass(t *testing.T) { + _, _, _, serverCert := genServerCertificate() + privateKey := serverCert.PrivateKey.(*rsa.PrivateKey) + x509Cert, _ := x509.ParseCertificate(serverCert.Certificate[0]) + pubDer, _ := x509.MarshalPKIXPublicKey(x509Cert.PublicKey) + pubPem := pem.EncodeToMemory(&pem.Block{Type: "PUBLIC KEY", Bytes: pubDer}) + pubID, _ := CertificateManager.Add(pubPem, "") + defer CertificateManager.Delete(pubID) + + // Should not receive an AuthFailure event + var eventWG sync.WaitGroup + eventWG.Add(1) + encodedString, spec, req, sessionKey := testPrepareRSAAuthSessionPass(t, &eventWG, privateKey, pubID, false, false) + + recorder := httptest.NewRecorder() + req.Header.Set("Authorization", fmt.Sprintf("Signature keyId=\"%s\",algorithm=\"rsa-sha256\",signature=\"%s\"", sessionKey, encodedString)) + + chain := getHMACAuthChain(spec) + chain.ServeHTTP(recorder, req) + + if recorder.Code != 200 { + t.Error("Initial request failed with non-200 code, should have gone through!: \n", recorder.Code, recorder.Body.String()) + } + + // Check we did not get our AuthFailure event + if !waitTimeout(&eventWG, 20*time.Millisecond) { + t.Error("Request should not have generated an AuthFailure event!: \n") + } +} + +func BenchmarkRSAAuthSessionPass(b *testing.B) { + b.ReportAllocs() + + _, _, _, serverCert := genServerCertificate() + privateKey := serverCert.PrivateKey.(*rsa.PrivateKey) + x509Cert, _ := x509.ParseCertificate(serverCert.Certificate[0]) + pubDer, _ := x509.MarshalPKIXPublicKey(x509Cert.PublicKey) + pubPem := pem.EncodeToMemory(&pem.Block{Type: "PUBLIC KEY", Bytes: pubDer}) + pubID, _ := CertificateManager.Add(pubPem, "") + defer CertificateManager.Delete(pubID) + + var eventWG sync.WaitGroup + eventWG.Add(b.N) + encodedString, spec, req, sessionKey := testPrepareRSAAuthSessionPass(b, &eventWG, privateKey, pubID, false, true) + + recorder := httptest.NewRecorder() + req.Header.Set("Authorization", fmt.Sprintf("Signature keyId=\"%s\",algorithm=\"rsa-sha256\",signature=\"%s\"", sessionKey, encodedString)) + + chain := getHMACAuthChain(spec) + + for i := 0; i < b.N; i++ { + chain.ServeHTTP(recorder, req) + if recorder.Code != 200 { + b.Error("Initial request failed with non-200 code, should have gone through!: \n", recorder.Code, recorder.Body.String()) + } + } +} + +func TestRSAAuthSessionKeyMissing(t *testing.T) { + _, _, _, serverCert := genServerCertificate() + privateKey := serverCert.PrivateKey.(*rsa.PrivateKey) + x509Cert, _ := x509.ParseCertificate(serverCert.Certificate[0]) + pubDer, _ := x509.MarshalPKIXPublicKey(x509Cert.PublicKey) + pubPem := pem.EncodeToMemory(&pem.Block{Type: "PUBLIC KEY", Bytes: pubDer}) + pubID, _ := CertificateManager.Add(pubPem, "") + defer CertificateManager.Delete(pubID) + + spec := CreateSpecTest(t, hmacAuthDef) + + // Should receive an AuthFailure event + var eventWG sync.WaitGroup + eventWG.Add(1) + cb := func(em config.EventMessage) { + eventWG.Done() + } + spec.EventPaths = map[apidef.TykEvent][]config.TykEventHandler{ + "AuthFailure": {&testAuthFailEventHandler{cb}}, + } + + recorder := httptest.NewRecorder() + encodedString, spec, req, _ := testPrepareRSAAuthSessionPass(t, &eventWG, privateKey, pubID, false, false) + + req.Header.Set("Authorization", fmt.Sprintf("Signature keyId=\"98765\",algorithm=\"rsa-sha256\",signature=\"%s\"", encodedString)) + + chain := getHMACAuthChain(spec) + chain.ServeHTTP(recorder, req) + + if recorder.Code != 400 { + t.Error("Request should have failed with key not found error!: \n", recorder.Code) + } + + // Check we did get our AuthFailure event + if waitTimeout(&eventWG, 20*time.Millisecond) { + t.Error("Request should have generated an AuthFailure event!: \n") + } +} From 1ba6f763b57238c654c61e1664c4eb855f66970f Mon Sep 17 00:00:00 2001 From: Komal Sukhani Date: Fri, 23 Aug 2019 18:27:59 +0530 Subject: [PATCH 06/10] Added new fields in Request Signing middleware 1.HeaderList 2.CertificateId --- apidef/api_definitions.go | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/apidef/api_definitions.go b/apidef/api_definitions.go index 615a1f448628..a9913e1add12 100644 --- a/apidef/api_definitions.go +++ b/apidef/api_definitions.go @@ -498,10 +498,12 @@ type BundleManifest struct { } type RequestSigningMeta struct { - IsEnabled bool `bson:"is_enabled" json:"is_enabled"` - Secret string `bson:"secret" json:"secret"` - KeyId string `bson:"key_id" json:"key_id"` - Algorithm string `bson:"algorithm" json:"algorithm"` + IsEnabled bool `bson:"is_enabled" json:"is_enabled"` + Secret string `bson:"secret" json:"secret"` + KeyId string `bson:"key_id" json:"key_id"` + Algorithm string `bson:"algorithm" json:"algorithm"` + HeaderList []string `bson:"header_list" json:"header_list"` + CertificateId string `bson:"certificate_id" json:"certificate"` } // Clean will URL encode map[string]struct variables for saving From 6cce853e645c4837c82352e1014f012828b1e8ea Mon Sep 17 00:00:00 2001 From: Komal Sukhani Date: Fri, 23 Aug 2019 18:31:23 +0530 Subject: [PATCH 07/10] 1. Added support of RSA in request signing middleware 2. Added a way to specify headers list to used for request signing --- gateway/mw_request_signing.go | 121 +++++++++++++++++++++++++++------- 1 file changed, 98 insertions(+), 23 deletions(-) diff --git a/gateway/mw_request_signing.go b/gateway/mw_request_signing.go index 02b0610d1305..287322ab6c7e 100644 --- a/gateway/mw_request_signing.go +++ b/gateway/mw_request_signing.go @@ -1,7 +1,14 @@ package gateway import ( + "crypto" + "crypto/rand" + "crypto/rsa" + "crypto/sha256" + "encoding/base64" "errors" + "github.com/TykTechnologies/tyk/certs" + "hash" "net/http" "strings" "time" @@ -19,34 +26,56 @@ func (s *RequestSigning) EnabledForSpec() bool { return s.Spec.RequestSigning.IsEnabled } -var supportedAlgorithms = []string{"hmac-sha1", "hmac-sha256", "hmac-sha384", "hmac-sha512"} +var supportedAlgorithms = []string{"hmac-sha1", "hmac-sha256", "hmac-sha384", "hmac-sha512", "rsa-sha256"} -func generateHeaderList(r *http.Request) []string { - headers := make([]string, len(r.Header)+1) +func generateHeaderList(r *http.Request, headerList []string) []string { + var result []string - headers[0] = "(request-target)" - i := 1 + if len(headerList) == 0 { + result = make([]string, len(r.Header)+1) + result[0] = "(request-target)" + i := 1 - for k := range r.Header { - loweredCaseHeader := strings.ToLower(k) - headers[i] = strings.TrimSpace(loweredCaseHeader) - i++ - } + for k := range r.Header { + loweredCaseHeader := strings.ToLower(k) + result[i] = strings.TrimSpace(loweredCaseHeader) + i++ + } - //Date header is must as per Signing HTTP Messages Draft - if r.Header.Get("date") == "" { - refDate := "Mon, 02 Jan 2006 15:04:05 MST" - tim := time.Now().Format(refDate) + // date header is must as per Signing HTTP Messages Draft + if r.Header.Get("date") == "" { + refDate := "Mon, 02 Jan 2006 15:04:05 MST" + tim := time.Now().Format(refDate) + + r.Header.Set("date", tim) + result = append(result, "date") + } + } else { + result = make([]string, 0, len(headerList)) + + for _, v := range headerList { + if r.Header.Get(v) != "" { + result = append(result, v) + } + } - r.Header.Set("date", tim) - headers = append(headers, "date") + if len(result) == 0 { + headers := []string{"(request-target)", "date"} + result = append(result, headers...) + + if r.Header.Get("date") == "" { + refDate := "Mon, 02 Jan 2006 15:04:05 MST" + tim := time.Now().Format(refDate) + r.Header.Set("date", tim) + } + } } - return headers + return result } func (s *RequestSigning) ProcessRequest(w http.ResponseWriter, r *http.Request, _ interface{}) (error, int) { - if s.Spec.RequestSigning.Secret == "" || s.Spec.RequestSigning.KeyId == "" || s.Spec.RequestSigning.Algorithm == "" { + if (s.Spec.RequestSigning.Secret == "" && s.Spec.RequestSigning.CertificateId == "") || s.Spec.RequestSigning.KeyId == "" || s.Spec.RequestSigning.Algorithm == "" { log.Error("Fields required for signing the request are missing") return errors.New("Fields required for signing the request are missing"), http.StatusInternalServerError } @@ -67,18 +96,40 @@ func (s *RequestSigning) ProcessRequest(w http.ResponseWriter, r *http.Request, } if !algorithmAllowed { log.WithField("algorithm", s.Spec.RequestSigning.Algorithm).Error("Algorithm not supported") - return errors.New("Request signing Algorithm is not supported"), http.StatusInternalServerError + return errors.New("Request signing algorithm is not supported"), http.StatusInternalServerError } - headers := generateHeaderList(r) - signatureString, err := generateHMACSignatureStringFromRequest(r, headers) + headers := generateHeaderList(r, s.Spec.RequestSigning.HeaderList) + + signatureString, err := generateSignatureStringFromRequest(r, headers) if err != nil { log.Error(err) return err, http.StatusInternalServerError } - strHeaders := strings.Join(headers, " ") - encodedSignature := generateEncodedSignature(signatureString, s.Spec.RequestSigning.Secret, s.Spec.RequestSigning.Algorithm) + + var encodedSignature string + + if strings.HasPrefix(s.Spec.RequestSigning.Algorithm, "rsa") { + certList := CertificateManager.List([]string{s.Spec.RequestSigning.CertificateId}, certs.CertificatePrivate) + if len(certList) == 0 { + log.Error("Certificate not found") + return errors.New("Certificate not found"), http.StatusInternalServerError + } + cert := certList[0] + rsaKey, ok := cert.PrivateKey.(*rsa.PrivateKey) + if !ok { + log.Error("Certificate does not contain RSA private key") + return errors.New("Certificate does not contain RSA private key"), http.StatusInternalServerError + } + encodedSignature, err = generateRSAEncodedSignature(signatureString, rsaKey, s.Spec.RequestSigning.Algorithm) + if err != nil { + log.Error("Error while generating signature:", err) + return err, http.StatusInternalServerError + } + } else { + encodedSignature = generateHMACEncodedSignature(signatureString, s.Spec.RequestSigning.Secret, s.Spec.RequestSigning.Algorithm) + } //Generate Authorization header authHeader := "Signature " @@ -96,3 +147,27 @@ func (s *RequestSigning) ProcessRequest(w http.ResponseWriter, r *http.Request, return nil, http.StatusOK } + +func generateRSAEncodedSignature(signatureString string, privateKey *rsa.PrivateKey, algorithm string) (string, error) { + var hashFunction hash.Hash + var hashType crypto.Hash + + switch algorithm { + case "rsa-sha256": + hashFunction = sha256.New() + hashType = crypto.SHA256 + default: + hashFunction = sha256.New() + hashType = crypto.SHA256 + } + hashFunction.Write([]byte(signatureString)) + hashed := hashFunction.Sum(nil) + + rawsignature, err := rsa.SignPKCS1v15(rand.Reader, privateKey, hashType, hashed) + if err != nil { + return "", err + } + encodedSignature := base64.StdEncoding.EncodeToString(rawsignature) + + return encodedSignature, nil +} From a87d3873c992f773c0a3f8e8f5217c6ace764466 Mon Sep 17 00:00:00 2001 From: Komal Sukhani Date: Fri, 23 Aug 2019 18:33:40 +0530 Subject: [PATCH 08/10] Added test cases for RSA request signing and headerList --- gateway/mw_request_signing_test.go | 152 +++++++++++++++++++++++++++-- 1 file changed, 142 insertions(+), 10 deletions(-) diff --git a/gateway/mw_request_signing_test.go b/gateway/mw_request_signing_test.go index e669d940cfe1..839aa869fa7a 100644 --- a/gateway/mw_request_signing_test.go +++ b/gateway/mw_request_signing_test.go @@ -1,8 +1,12 @@ package gateway import ( + "crypto/x509" "encoding/json" + "encoding/pem" + "strings" "testing" + "time" "github.com/TykTechnologies/tyk/test" "github.com/TykTechnologies/tyk/user" @@ -10,15 +14,23 @@ import ( var algoList = [4]string{"hmac-sha1", "hmac-sha256", "hmac-sha384", "hmac-sha512"} -func generateSpec(algo string) { +func generateSession(algo, data string) string { sessionKey := CreateSession(func(s *user.SessionState) { - s.HMACEnabled = true - s.HmacSecret = "9879879878787878" + if strings.HasPrefix(algo, "rsa") { + s.RSACertificateId = data + s.RSAEnabled = true + } else { + s.HmacSecret = data + s.HMACEnabled = true + } s.AccessRights = map[string]user.AccessDefinition{"protected": {APIID: "protected", Versions: []string{"v1"}}} - }) + return sessionKey +} + +func generateSpec(algo string, data string, sessionKey string, headerList []string) { BuildAndLoadAPI(func(spec *APISpec) { spec.APIID = "protected" spec.Name = "protected api" @@ -35,10 +47,19 @@ func generateSpec(algo string) { spec.VersionData.Versions["v1"] = version }, func(spec *APISpec) { spec.Proxy.ListenPath = "/test" + spec.UseKeylessAccess = true spec.RequestSigning.IsEnabled = true spec.RequestSigning.KeyId = sessionKey - spec.RequestSigning.Secret = "9879879878787878" + + if strings.HasPrefix(algo, "rsa") { + spec.RequestSigning.CertificateId = data + } else { + spec.RequestSigning.Secret = data + } spec.RequestSigning.Algorithm = algo + if headerList != nil { + spec.RequestSigning.HeaderList = headerList + } version := spec.VersionData.Versions["v1"] json.Unmarshal([]byte(`{ @@ -58,15 +79,16 @@ func generateSpec(algo string) { } -func TestRequestSigning(t *testing.T) { +func TestHMACRequestSigning(t *testing.T) { ts := StartTest() defer ts.Close() + secret := "9879879878787878" for _, algo := range algoList { name := "Test with " + algo t.Run(name, func(t *testing.T) { - - generateSpec(algo) + sessionKey := generateSession(algo, secret) + generateSpec(algo, secret, sessionKey, nil) ts.Run(t, []test.TestCase{ {Path: "/test/by_name", Code: 200}, @@ -74,8 +96,87 @@ func TestRequestSigning(t *testing.T) { }) } + t.Run("Valid Custom headerList", func(t *testing.T) { + algo := "hmac-sha1" + headerList := []string{"foo", "date"} + + sessionKey := generateSession(algo, secret) + generateSpec(algo, secret, sessionKey, headerList) + + refDate := "Mon, 02 Jan 2006 15:04:05 MST" + tim := time.Now().Format(refDate) + + headers := map[string]string{"foo": "bar", "date": tim} + + ts.Run(t, []test.TestCase{ + {Path: "/test/by_name", Code: 200, Headers: headers}, + }...) + }) + + t.Run("Invalid Custom headerList", func(t *testing.T) { + algo := "hmac-sha1" + headerList := []string{"foo"} + + sessionKey := generateSession(algo, secret) + generateSpec(algo, secret, sessionKey, headerList) + + ts.Run(t, []test.TestCase{ + {Path: "/test/by_name", Code: 200}, + }...) + }) + + t.Run("Invalid algorithm", func(t *testing.T) { + algo := "hmac-123" + sessionKey := generateSession(algo, secret) + generateSpec(algo, secret, sessionKey, nil) + + ts.Run(t, []test.TestCase{ + {Path: "/test/by_name", Code: 500}, + }...) + }) + + t.Run("Invalid Date field", func(t *testing.T) { + algo := "hmac-sha1" + sessionKey := generateSession(algo, secret) + generateSpec(algo, secret, sessionKey, nil) + + headers := map[string]string{"date": "Mon, 02 Jan 2006 15:04:05 GMT"} + + ts.Run(t, []test.TestCase{ + {Path: "/test/by_name", Headers: headers, Code: 400}, + }...) + }) +} + +func TestRSARequestSigning(t *testing.T) { + ts := StartTest() + defer ts.Close() + + _, _, combinedPem, cert := genServerCertificate() + privCertId, _ := CertificateManager.Add(combinedPem, "") + defer CertificateManager.Delete(privCertId) + + x509Cert, _ := x509.ParseCertificate(cert.Certificate[0]) + pubDer, _ := x509.MarshalPKIXPublicKey(x509Cert.PublicKey) + pubPem := pem.EncodeToMemory(&pem.Block{Type: "PUBLIC KEY", Bytes: pubDer}) + pubCertId, _ := CertificateManager.Add(pubPem, "") + defer CertificateManager.Delete(pubCertId) + + name := "Test with rsa-sha256" + t.Run(name, func(t *testing.T) { + algo := "rsa-sha256" + sessionKey := generateSession(algo, pubCertId) + generateSpec(algo, privCertId, sessionKey, nil) + + ts.Run(t, []test.TestCase{ + {Path: "/test/by_name", Code: 200}, + }...) + }) + t.Run("Invalid algorithm", func(t *testing.T) { - generateSpec("random") + algo := "rsa-123" + sessionKey := generateSession(algo, pubCertId) + generateSpec(algo, privCertId, sessionKey, nil) ts.Run(t, []test.TestCase{ {Path: "/test/by_name", Code: 500}, @@ -83,7 +184,9 @@ func TestRequestSigning(t *testing.T) { }) t.Run("Invalid Date field", func(t *testing.T) { - generateSpec("hmac-sha1") + algo := "rsa-sha256" + sessionKey := generateSession(algo, pubCertId) + generateSpec(algo, privCertId, sessionKey, nil) headers := map[string]string{"date": "Mon, 02 Jan 2006 15:04:05 GMT"} @@ -91,4 +194,33 @@ func TestRequestSigning(t *testing.T) { {Path: "/test/by_name", Headers: headers, Code: 400}, }...) }) + + t.Run("Custom headerList", func(t *testing.T) { + algo := "rsa-sha256" + headerList := []string{"foo", "date"} + + sessionKey := generateSession(algo, pubCertId) + generateSpec(algo, privCertId, sessionKey, headerList) + + refDate := "Mon, 02 Jan 2006 15:04:05 MST" + tim := time.Now().Format(refDate) + + headers := map[string]string{"foo": "bar", "date": tim} + + ts.Run(t, []test.TestCase{ + {Path: "/test/by_name", Code: 200, Headers: headers}, + }...) + }) + + t.Run("Non-existing Custom headers", func(t *testing.T) { + algo := "rsa-sha256" + headerList := []string{"foo"} + + sessionKey := generateSession(algo, pubCertId) + generateSpec(algo, privCertId, sessionKey, headerList) + + ts.Run(t, []test.TestCase{ + {Path: "/test/by_name", Code: 200}, + }...) + }) } From 87f091259c0356dc0c7eda120da2284f64606715 Mon Sep 17 00:00:00 2001 From: Komal Sukhani Date: Fri, 23 Aug 2019 19:12:04 +0530 Subject: [PATCH 09/10] Renamed HMACMiddleware to SignatureVerficationMiddleware --- gateway/api_loader.go | 2 +- gateway/mw_hmac.go | 26 +++++++++++++------------- 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/gateway/api_loader.go b/gateway/api_loader.go index 2b5bf85dcd8e..9aa720290513 100644 --- a/gateway/api_loader.go +++ b/gateway/api_loader.go @@ -316,7 +316,7 @@ func processSpec(spec *APISpec, apisByListen map[string]int, logger.Info("Checking security policy: Basic") } - if mwAppendEnabled(&authArray, &HMACMiddleware{BaseMiddleware: baseMid}) { + if mwAppendEnabled(&authArray, &SignatureVerficationMiddleware{BaseMiddleware: baseMid}) { logger.Info("Checking security policy: HMAC") } diff --git a/gateway/mw_hmac.go b/gateway/mw_hmac.go index a6a9af6419c0..4d6be9b81ecd 100644 --- a/gateway/mw_hmac.go +++ b/gateway/mw_hmac.go @@ -27,25 +27,25 @@ import ( const dateHeaderSpec = "Date" const altHeaderSpec = "x-aux-date" -// HMACMiddleware will check if the request has a signature, and if the request is allowed through -type HMACMiddleware struct { +// SignatureVerficationMiddleware will check if the request has a signature, and if the request is allowed through +type SignatureVerficationMiddleware struct { BaseMiddleware lowercasePattern *regexp.Regexp } -func (hm *HMACMiddleware) Name() string { +func (hm *SignatureVerficationMiddleware) Name() string { return "HMAC" } -func (k *HMACMiddleware) EnabledForSpec() bool { +func (k *SignatureVerficationMiddleware) EnabledForSpec() bool { return k.Spec.EnableSignatureChecking } -func (hm *HMACMiddleware) Init() { +func (hm *SignatureVerficationMiddleware) Init() { hm.lowercasePattern = regexp.MustCompile(`%[a-f0-9][a-f0-9]`) } -func (hm *HMACMiddleware) ProcessRequest(w http.ResponseWriter, r *http.Request, _ interface{}) (error, int) { +func (hm *SignatureVerficationMiddleware) ProcessRequest(w http.ResponseWriter, r *http.Request, _ interface{}) (error, int) { token := r.Header.Get("Authorization") if token == "" { return hm.authorizationError(r) @@ -198,12 +198,12 @@ func stripSignature(token string) string { return strings.TrimSpace(token) } -func (hm *HMACMiddleware) hasLowerCaseEscaped(signature string) (bool, []string) { +func (hm *SignatureVerficationMiddleware) hasLowerCaseEscaped(signature string) (bool, []string) { foundList := hm.lowercasePattern.FindAllString(signature, -1) return len(foundList) > 0, foundList } -func (hm *HMACMiddleware) replaceWithUpperCase(originalSignature string, lowercaseList []string) string { +func (hm *SignatureVerficationMiddleware) replaceWithUpperCase(originalSignature string, lowercaseList []string) string { newSignature := originalSignature for _, lStr := range lowercaseList { asUpper := strings.ToUpper(lStr) @@ -213,7 +213,7 @@ func (hm *HMACMiddleware) replaceWithUpperCase(originalSignature string, lowerca return newSignature } -func (hm *HMACMiddleware) setContextVars(r *http.Request, token string) { +func (hm *SignatureVerficationMiddleware) setContextVars(r *http.Request, token string) { if !hm.Spec.EnableContextVars { return } @@ -225,7 +225,7 @@ func (hm *HMACMiddleware) setContextVars(r *http.Request, token string) { } } -func (hm *HMACMiddleware) authorizationError(r *http.Request) (error, int) { +func (hm *SignatureVerficationMiddleware) authorizationError(r *http.Request) (error, int) { hm.Logger().Info("Authorization field missing or malformed") AuthFailed(hm, r, r.Header.Get(headers.Authorization)) @@ -233,7 +233,7 @@ func (hm *HMACMiddleware) authorizationError(r *http.Request) (error, int) { return errors.New("Authorization field missing, malformed or invalid"), http.StatusBadRequest } -func (hm HMACMiddleware) checkClockSkew(dateHeaderValue string) bool { +func (hm SignatureVerficationMiddleware) checkClockSkew(dateHeaderValue string) bool { // Reference layout for parsing time: "Mon Jan 2 15:04:05 MST 2006" refDate := "Mon, 02 Jan 2006 15:04:05 MST" // Fall back to a numeric timezone, since some environments don't provide a timezone name code @@ -275,7 +275,7 @@ type HMACFieldValues struct { Signature string } -func (hm *HMACMiddleware) getSecretAndSessionForKeyID(r *http.Request, keyId string) (string, user.SessionState, error) { +func (hm *SignatureVerficationMiddleware) getSecretAndSessionForKeyID(r *http.Request, keyId string) (string, user.SessionState, error) { session, keyExists := hm.CheckSessionAndIdentityForValidKey(keyId, r) if !keyExists { return "", session, errors.New("Key ID does not exist") @@ -290,7 +290,7 @@ func (hm *HMACMiddleware) getSecretAndSessionForKeyID(r *http.Request, keyId str return session.HmacSecret, session, nil } -func (hm *HMACMiddleware) getRSACertificateIdAndSessionForKeyID(r *http.Request, keyId string) (string, user.SessionState, error) { +func (hm *SignatureVerficationMiddleware) getRSACertificateIdAndSessionForKeyID(r *http.Request, keyId string) (string, user.SessionState, error) { session, keyExists := hm.CheckSessionAndIdentityForValidKey(keyId, r) if !keyExists { return "", session, errors.New("Key ID does not exist") From b7998fbf1bfbb9e145510d3e2760179823b53ce5 Mon Sep 17 00:00:00 2001 From: Komal Sukhani Date: Fri, 23 Aug 2019 19:14:21 +0530 Subject: [PATCH 10/10] Renamed files --- gateway/{mw_hmac.go => mw_request_validator.go} | 0 gateway/{mw_hmac_test.go => mw_request_validator_test.go} | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename gateway/{mw_hmac.go => mw_request_validator.go} (100%) rename gateway/{mw_hmac_test.go => mw_request_validator_test.go} (100%) diff --git a/gateway/mw_hmac.go b/gateway/mw_request_validator.go similarity index 100% rename from gateway/mw_hmac.go rename to gateway/mw_request_validator.go diff --git a/gateway/mw_hmac_test.go b/gateway/mw_request_validator_test.go similarity index 100% rename from gateway/mw_hmac_test.go rename to gateway/mw_request_validator_test.go