Skip to content

Commit 2d321f5

Browse files
committed
working for fix for dynamic mtls
1 parent a77e768 commit 2d321f5

4 files changed

Lines changed: 166 additions & 24 deletions

File tree

config/config.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -686,6 +686,11 @@ type SecurityConfig struct {
686686
// Specify public keys used for Certificate Pinning on global level.
687687
PinnedPublicKeys map[string]string `json:"pinned_public_keys"`
688688

689+
// AllowUnsafeDynamicMTLSToken controls whether certificate presence is required for
690+
// dynamic mTLS authentication. If set to false (default), requests with a token but
691+
// no certificate will be rejected for APIs using dynamic mTLS.
692+
AllowUnsafeDynamicMTLSToken bool `json:"allow_unsafe_dynamic_mtls_token"`
693+
689694
Certificates CertificatesConfig `json:"certificates"`
690695

691696
// CertificateExpiryMonitor configures the certificate expiry monitoring and notification feature

gateway/handler_error.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ const (
2828

2929
MsgAuthFieldMissing = "Authorization field missing"
3030
MsgApiAccessDisallowed = "Access to this API has been disallowed"
31+
MsgAuthCertRequired = "Client certificate required"
3132
MsgBearerMailformed = "Bearer token malformed"
3233
MsgKeyNotAuthorized = "Key not authorised"
3334
MsgOauthClientRevoked = "Key not authorised. OAuth client access was revoked"

gateway/mw_auth_key.go

Lines changed: 75 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,8 @@ const (
2828
ErrAuthCertNotFound = "auth.cert_not_found"
2929
ErrAuthCertExpired = "auth.cert_expired"
3030
ErrAuthKeyIsInvalid = "auth.key_is_invalid"
31+
ErrAuthCertRequired = "auth.cert_required"
32+
ErrAuthCertMismatch = "auth.cert_mismatch"
3133

3234
MsgNonExistentKey = "Attempted access with non-existent key."
3335
MsgNonExistentCert = "Attempted access with non-existent cert."
@@ -59,6 +61,16 @@ func initAuthKeyErrors() {
5961
Message: MsgCertificateExpired,
6062
Code: http.StatusForbidden,
6163
}
64+
65+
TykErrors[ErrAuthCertRequired] = config.TykError{
66+
Message: MsgAuthCertRequired,
67+
Code: http.StatusUnauthorized,
68+
}
69+
70+
TykErrors[ErrAuthCertMismatch] = config.TykError{
71+
Message: MsgApiAccessDisallowed,
72+
Code: http.StatusUnauthorized,
73+
}
6274
}
6375

6476
// KeyExists will check if the key being used to access the API is in the request data,
@@ -99,41 +111,81 @@ func (k *AuthKey) ProcessRequest(_ http.ResponseWriter, r *http.Request, _ inter
99111
}
100112

101113
key, authConfig := k.getAuthToken(k.getAuthType(), r)
114+
if key == "" {
115+
key = stripBearer(key)
116+
}
117+
var keyExists, updateSession bool
102118
var certHash string
103-
104-
keyExists := false
105119
var session user.SessionState
106-
updateSession := false
107-
if key != "" {
108-
key = stripBearer(key)
109-
} else if authConfig.UseCertificate && key == "" && r.TLS != nil && len(r.TLS.PeerCertificates) > 0 {
110-
log.Debug("Trying to find key by client certificate")
111-
certHash = k.Spec.OrgID + crypto.HexSHA256(r.TLS.PeerCertificates[0].Raw)
112-
if time.Now().After(r.TLS.PeerCertificates[0].NotAfter) {
113-
return errorAndStatusCode(ErrAuthCertExpired)
120+
if authConfig.UseCertificate && r.TLS != nil {
121+
if len(r.TLS.PeerCertificates) > 0 {
122+
if time.Now().After(r.TLS.PeerCertificates[0].NotAfter) {
123+
return errorAndStatusCode(ErrAuthCertExpired)
124+
}
125+
certHash = k.Spec.OrgID + crypto.HexSHA256(r.TLS.PeerCertificates[0].Raw)
114126
}
115127

116-
key = k.Gw.generateToken(k.Spec.OrgID, certHash)
117-
} else {
118-
k.Logger().Info("Attempted access with malformed header, no auth header found.")
119-
return errorAndStatusCode(ErrAuthAuthorizationFieldMissing)
120-
}
121-
122-
session, keyExists = k.CheckSessionAndIdentityForValidKey(key, r)
123-
key = session.KeyID
124-
if !keyExists {
125-
// fallback to search by cert
126-
session, keyExists = k.CheckSessionAndIdentityForValidKey(certHash, r)
127-
if !keyExists {
128-
return k.reportInvalidKey(key, r, MsgNonExistentKey, ErrAuthKeyNotFound)
128+
if !k.Gw.GetConfig().Security.AllowUnsafeDynamicMTLSToken {
129+
if certHash == "" {
130+
return errorAndStatusCode(ErrAuthCertRequired)
131+
}
132+
key = k.Gw.generateToken(k.Spec.OrgID, certHash)
133+
session, keyExists = k.CheckSessionAndIdentityForValidKey(key, r)
134+
} else {
135+
if key != "" {
136+
session, keyExists = k.CheckSessionAndIdentityForValidKey(key, r)
137+
key = session.KeyID
138+
if !keyExists {
139+
session, keyExists = k.CheckSessionAndIdentityForValidKey(certHash, r)
140+
if !keyExists {
141+
return k.reportInvalidKey(key, r, MsgNonExistentKey, ErrAuthKeyNotFound)
142+
}
143+
}
144+
}
129145
}
130146
}
131147

148+
//if key != "" {
149+
// fmt.Println("key: ", key)
150+
// key = stripBearer(key)
151+
//} else if authConfig.UseCertificate && key == "" && r.TLS != nil && len(r.TLS.PeerCertificates) > 0 {
152+
// fmt.Println("Trying to find key by client certificate")
153+
// log.Debug("Trying to find key by client certificate")
154+
// certHash = k.Spec.OrgID + crypto.HexSHA256(r.TLS.PeerCertificates[0].Raw)
155+
// fmt.Println("certHash: ", certHash)
156+
// if time.Now().After(r.TLS.PeerCertificates[0].NotAfter) {
157+
// return errorAndStatusCode(ErrAuthCertExpired)
158+
// }
159+
//
160+
// key = k.Gw.generateToken(k.Spec.OrgID, certHash)
161+
// fmt.Println("key: ", key)
162+
//} else {
163+
// k.Logger().Info("Attempted access with malformed header, no auth header found.")
164+
// return errorAndStatusCode(ErrAuthAuthorizationFieldMissing)
165+
//}
166+
//
167+
//session, keyExists = k.CheckSessionAndIdentityForValidKey(key, r)
168+
//key = session.KeyID
169+
//fmt.Printf("CheckSessionAndIdentityForValidKey: %s %v\n", key, keyExists)
170+
//if !keyExists {
171+
// // fallback to search by cert
172+
// session, keyExists = k.CheckSessionAndIdentityForValidKey(certHash, r)
173+
// fmt.Printf("CheckSessionAndIdentityForValidKey (cert hash): %s %v\n", certHash, keyExists)
174+
// if !keyExists {
175+
// return k.reportInvalidKey(key, r, MsgNonExistentKey, ErrAuthKeyNotFound)
176+
// }
177+
//}
178+
132179
if authConfig.UseCertificate {
133180
certLookup := session.Certificate
134181

135182
if r.TLS != nil && len(r.TLS.PeerCertificates) > 0 {
136183
certLookup = certHash
184+
//if session.Certificate != "" && session.Certificate != certHash {
185+
// // Certificate mismatch - provided certificate doesn't match the one in session
186+
// return errorAndStatusCode(ErrAuthCertMismatch)
187+
//}
188+
137189
if session.Certificate != certHash {
138190
session.Certificate = certHash
139191
updateSession = true

gateway/mw_auth_key_test.go

Lines changed: 85 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"crypto/x509"
55
"encoding/hex"
66
"fmt"
7+
"github.com/TykTechnologies/tyk/internal/crypto"
78
"net/http"
89
"net/http/httptest"
910
"net/url"
@@ -601,6 +602,7 @@ func TestDynamicMTLS(t *testing.T) {
601602

602603
conf := func(globalConf *config.Config) {
603604
globalConf.Security.ControlAPIUseMutualTLS = false
605+
globalConf.Security.AllowUnsafeDynamicMTLSToken = false // Default secure behavior
604606
globalConf.HttpServerOptions.UseSSL = true
605607
globalConf.HttpServerOptions.SSLInsecureSkipVerify = true
606608
globalConf.HttpServerOptions.SSLCertificates = []string{"default" + certID}
@@ -634,8 +636,9 @@ func TestDynamicMTLS(t *testing.T) {
634636

635637
clientCertID, err := ts.Gw.CertificateManager.Add(clientCertPem, "default")
636638
assert.NoError(t, err)
639+
certHash := "default" + crypto.HexSHA256(clientCert.Certificate[0])
637640

638-
ts.CreateSession(func(s *user.SessionState) {
641+
_, keyHash := ts.CreateSession(func(s *user.SessionState) {
639642
s.AccessRights = map[string]user.AccessDefinition{"apiID-1": {
640643
APIID: "apiID-1",
641644
}}
@@ -650,7 +653,43 @@ func TestDynamicMTLS(t *testing.T) {
650653
Path: "/dynamic-mtls",
651654
Code: http.StatusOK,
652655
})
656+
})
657+
658+
t.Run("missing certificate - should be rejected by default", func(t *testing.T) {
659+
// client that only checks server ca
660+
certClient := GetTLSClient(nil, serverCertPem)
661+
_, _ = ts.Run(t, test.TestCase{
662+
Path: "/dynamic-mtls",
663+
Code: http.StatusUnauthorized,
664+
BodyMatch: MsgAuthCertRequired,
665+
Client: certClient,
666+
})
667+
})
668+
669+
t.Run("missing cert with generated key - should be rejected by default", func(t *testing.T) {
670+
certClient := GetTLSClient(nil, serverCertPem)
671+
_, _ = ts.Run(t, test.TestCase{
672+
Path: "/dynamic-mtls",
673+
Code: http.StatusUnauthorized,
674+
BodyMatch: MsgAuthCertRequired,
675+
Client: certClient,
676+
Headers: map[string]string{
677+
"Authorization": keyHash,
678+
},
679+
})
680+
})
653681

682+
t.Run("missing cert with generated cert - should be rejected by default", func(t *testing.T) {
683+
certClient := GetTLSClient(nil, serverCertPem)
684+
_, _ = ts.Run(t, test.TestCase{
685+
Path: "/dynamic-mtls",
686+
Code: http.StatusUnauthorized,
687+
BodyMatch: MsgAuthCertRequired,
688+
Client: certClient,
689+
Headers: map[string]string{
690+
"Authorization": certHash,
691+
},
692+
})
654693
})
655694

656695
t.Run("expired client cert", func(t *testing.T) {
@@ -668,4 +707,49 @@ func TestDynamicMTLS(t *testing.T) {
668707
BodyMatch: MsgCertificateExpired,
669708
})
670709
})
710+
711+
// KOFO: you are here, trying to make this test pass by rejecting the request if the certificate does not match
712+
t.Run("non-matching certificate - should be rejected", func(t *testing.T) {
713+
differentClientPem, _, _, differentClientCert := certs.GenCertificate(&x509.Certificate{}, false)
714+
_, err := ts.Gw.CertificateManager.Add(differentClientPem, "default")
715+
assert.NoError(t, err)
716+
717+
differentCertClient := GetTLSClient(&differentClientCert, serverCertPem)
718+
_, _ = ts.Run(t, test.TestCase{
719+
Client: differentCertClient,
720+
Path: "/dynamic-mtls",
721+
Code: http.StatusForbidden,
722+
BodyMatch: MsgApiAccessDisallowed,
723+
})
724+
})
725+
726+
t.Run("with AllowUnsafeDynamicMTLSToken=true", func(t *testing.T) {
727+
// Change the configuration to allow token auth without certificates
728+
gatewayConfig := ts.Gw.GetConfig()
729+
gatewayConfig.Security.AllowUnsafeDynamicMTLSToken = true
730+
ts.Gw.SetConfig(gatewayConfig)
731+
ts.ReloadGatewayProxy()
732+
733+
// Create a new token for testing with the relaxed setting
734+
tokenID := CreateSession(ts.Gw, func(s *user.SessionState) {
735+
s.AccessRights = map[string]user.AccessDefinition{"apiID-1": {
736+
APIID: "apiID-1",
737+
}}
738+
})
739+
740+
// Test without certificate - should succeed with relaxed setting
741+
_, _ = ts.Run(t, test.TestCase{
742+
Headers: map[string]string{
743+
"Authorization": tokenID,
744+
},
745+
Path: "/dynamic-mtls",
746+
Code: http.StatusOK,
747+
})
748+
749+
// Reset back to secure mode
750+
gatewayConfig = ts.Gw.GetConfig()
751+
gatewayConfig.Security.AllowUnsafeDynamicMTLSToken = false
752+
ts.Gw.SetConfig(gatewayConfig)
753+
ts.ReloadGatewayProxy()
754+
})
671755
}

0 commit comments

Comments
 (0)