Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions cli/linter/schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -1166,6 +1166,9 @@
"default": 86400
}
}
},
"disable_certificate_token_binding": {
"type": "boolean"
}
}
},
Expand Down
7 changes: 7 additions & 0 deletions config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -689,8 +689,15 @@

// CertificateExpiryMonitor configures the certificate expiry monitoring and notification feature
CertificateExpiryMonitor CertificateExpiryMonitorConfig `json:"certificate_expiry_monitor"`

// DisableCertificateTokenBinding enables certificate-token binding for static mTLS authentication.
// When enabled, access tokens will be linked (bound) to one or more client certificates during creation or update.
// Any subsequent request with that token must present one of the bound certificates, otherwise the request will be rejected.

Check warning on line 695 in config/config.go

View check run for this annotation

probelabs / Visor: security

security Issue

The configuration property is named `DisableCertificateTokenBinding`, while the corresponding environment variable mentioned in the comment is `TYK_GW_SECURITY_ENABLECERTIFICATETOKENBINDING`. This inverted naming is highly confusing and increases the risk of misconfiguration, where an operator might unintentionally disable this important security feature.
Raw output
Align the naming of the configuration property and the environment variable to avoid confusion. For example, rename the struct field to `EnableCertificateTokenBinding` (and update its `json` tag) and adjust the logic where it's used (e.g., `if k.Gw.GetConfig().Security.EnableCertificateTokenBinding`). This makes the configuration explicit and less error-prone.
// This provides protection against token theft and misuse in mTLS environments.

Check warning on line 696 in config/config.go

View check run for this annotation

probelabs / Visor: architecture

style Issue

The configuration option `DisableCertificateTokenBinding` uses a double negative, which makes its purpose less intuitive and the code harder to read. A name like `EnableCertificateTokenBinding` would be clearer.
Raw output
Rename the configuration option to `EnableCertificateTokenBinding` and adjust its usage in `gateway/mw_auth_key.go`. The default value should be `false` to maintain backward compatibility.
// Environment variable: TYK_GW_SECURITY_ENABLECERTIFICATETOKENBINDING
DisableCertificateTokenBinding bool `json:"disable_certificate_token_binding"`
}

Check failure on line 700 in config/config.go

View check run for this annotation

probelabs / Visor: quality

architecture Issue

The configuration option `DisableCertificateTokenBinding` has a confusing name, comment, and associated environment variable. The name uses negative logic (`Disable...`), while the comment and environment variable (`...ENABLE...`) use positive logic. This inconsistency makes the configuration difficult to understand and prone to misconfiguration. The default boolean value of `false` means the feature is enabled by default, which could be a surprising change for users.
Raw output
Rename the field to `EnableCertificateTokenBinding` and update the comment accordingly to make the logic straightforward (`true` enables, `false` disables) and align it with the environment variable name. If backward compatibility of the JSON field name is a concern, at a minimum, correct the comment to accurately describe the field's behavior and reconsider if this feature should be enabled by default.
type NewRelicConfig struct {
// New Relic Application name
AppName string `json:"app_name"`
Expand Down
169 changes: 153 additions & 16 deletions gateway/mw_auth_key.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,11 +27,13 @@
ErrAuthKeyNotFound = "auth.key_not_found"
ErrAuthCertNotFound = "auth.cert_not_found"
ErrAuthCertExpired = "auth.cert_expired"
ErrAuthCertMismatch = "auth.cert_mismatch"
ErrAuthKeyIsInvalid = "auth.key_is_invalid"

MsgNonExistentKey = "Attempted access with non-existent key."
MsgNonExistentCert = "Attempted access with non-existent cert."
MsgInvalidKey = "Attempted access with invalid key."
MsgNonExistentKey = "Attempted access with non-existent key."
MsgNonExistentCert = "Attempted access with non-existent cert."
MsgCertificateMismatch = "Attempted access with incorrect certificate."
MsgInvalidKey = "Attempted access with invalid key."
)

func initAuthKeyErrors() {
Expand Down Expand Up @@ -59,6 +61,11 @@
Message: MsgCertificateExpired,
Code: http.StatusForbidden,
}

TykErrors[ErrAuthCertMismatch] = config.TykError{
Message: MsgApiAccessDisallowed,
Code: http.StatusForbidden,
}
}

// KeyExists will check if the key being used to access the API is in the request data,
Expand Down Expand Up @@ -97,11 +104,11 @@
if ses := ctxGetSession(r); ses != nil && httpctx.IsSelfLooping(r) {
return nil, http.StatusOK
}

key, authConfig := k.getAuthToken(k.getAuthType(), r)
var certHash string

keyExists := false

Check failure on line 111 in gateway/mw_auth_key.go

View check run for this annotation

probelabs / Visor: security

security Issue

The comparison of the client certificate hash against the bound certificate hashes is performed using the standard `==` operator, which is not constant-time. This could potentially expose the application to a timing attack, where an attacker could measure response time variations to incrementally guess a valid certificate hash.
Raw output
Replace the direct string comparison with a constant-time comparison function to mitigate timing attacks. Use `crypto/subtle.ConstantTimeCompare` for all security-sensitive comparisons.
var session user.SessionState
updateSession := false
if key != "" {
Expand Down Expand Up @@ -129,19 +136,15 @@
}
}

if authConfig.UseCertificate {
certLookup := session.Certificate

if r.TLS != nil && len(r.TLS.PeerCertificates) > 0 {
certLookup = certHash
if session.Certificate != certHash {
session.Certificate = certHash
updateSession = true
}
}

if _, err := k.Gw.CertificateManager.GetRaw(certLookup); err != nil {
return k.reportInvalidKey(key, r, MsgNonExistentCert, ErrAuthCertNotFound)
// Validate certificate binding or legacy certificate auth
// Certificate binding validation runs when:
// 1. Certificate binding is globally enabled AND the session has certificate bindings, OR
// 2. UseCertificate is explicitly set for this API (legacy dynamic mTLS mode)
bindingEnabled := !k.Gw.GetConfig().Security.DisableCertificateTokenBinding
hasBindings := len(session.MtlsStaticCertificateBindings) > 0
if authConfig.UseCertificate || (bindingEnabled && hasBindings) {
if code, err := k.validateCertificate(r, key, &session, &certHash, &updateSession); err != nil {
return err, code
}
}

Expand Down Expand Up @@ -272,3 +275,137 @@
Key: token,
})
}

// validateCertificate performs certificate validation for UseCertificate authentication
// It handles both certificate binding mode and legacy auto-update mode
func (k *AuthKey) validateCertificate(r *http.Request, key string, session *user.SessionState, certHash *string, updateSession *bool) (int, error) {
if r.TLS != nil && len(r.TLS.PeerCertificates) > 0 {
return k.validateWithTLSCertificate(r, key, session, certHash, updateSession)
}
return k.validateWithoutTLSCertificate(r, key, session)
}

// validateWithTLSCertificate handles validation when a TLS client certificate is provided
func (k *AuthKey) validateWithTLSCertificate(r *http.Request, key string, session *user.SessionState, certHash *string, updateSession *bool) (int, error) {
// Check certificate expiry when a token is provided (not checked earlier in the flow)
if code, err := k.checkCertificateExpiry(r, key); err != nil {
return code, err
}

// Compute cert hash for comparison
*certHash = k.computeCertHash(r, *certHash)

// Use binding mode only if:
// 1. Certificate binding is enabled globally, AND
// 2. The session has static certificate bindings configured
// Otherwise, use legacy mode for backward compatibility with dynamic mTLS
bindingEnabled := !k.Gw.GetConfig().Security.DisableCertificateTokenBinding
hasBindings := len(session.MtlsStaticCertificateBindings) > 0
if bindingEnabled && hasBindings {
return k.validateCertificateBinding(r, key, session, *certHash)
}
return k.validateLegacyMode(r, session, *certHash, updateSession)
}

// validateWithoutTLSCertificate handles validation when no TLS client certificate is provided
func (k *AuthKey) validateWithoutTLSCertificate(r *http.Request, key string, session *user.SessionState) (int, error) {
// Use binding mode only if:
// 1. Certificate binding is enabled globally, AND
// 2. The session has static certificate bindings configured
// Otherwise, use legacy mode for backward compatibility with dynamic mTLS
bindingEnabled := !k.Gw.GetConfig().Security.DisableCertificateTokenBinding
hasBindings := len(session.MtlsStaticCertificateBindings) > 0

if bindingEnabled && hasBindings {
return k.validateBindingWithoutCert(r, key, session)
}
return k.validateLegacyWithoutCert(r, session)
}

// checkCertificateExpiry validates that the certificate hasn't expired
func (k *AuthKey) checkCertificateExpiry(r *http.Request, key string) (int, error) {
if key != "" && time.Now().After(r.TLS.PeerCertificates[0].NotAfter) {
err, code := errorAndStatusCode(ErrAuthCertExpired)
return code, err
}
return http.StatusOK, nil
}

// computeCertHash computes the certificate hash if not already computed
func (k *AuthKey) computeCertHash(r *http.Request, existingHash string) string {
if existingHash == "" {
return k.Spec.OrgID + crypto.HexSHA256(r.TLS.PeerCertificates[0].Raw)
}
return existingHash
}

// validateCertificateBinding validates certificate-to-token binding
// This enforces that the presented certificate matches the one bound to the session
func (k *AuthKey) validateCertificateBinding(r *http.Request, key string, session *user.SessionState, certHash string) (int, error) {
// Only validate if both token and session have certificate bindings
if key == "" || len(session.MtlsStaticCertificateBindings) == 0 {
return http.StatusOK, nil
}

// Check if the presented certificate hash matches any of the bound certificates
certMatched := false
for _, boundCert := range session.MtlsStaticCertificateBindings {
if certHash == boundCert {
certMatched = true
break
}
}

// If certificates don't match, reject the request
if !certMatched {
k.Logger().WithField("key", k.Gw.obfuscateKey(key)).Warn("Certificate mismatch detected for token")
err, code := k.reportInvalidKey(key, r, MsgCertificateMismatch, ErrAuthCertMismatch)
return code, err
}

// Note: In binding mode, certificate whitelist validation is performed at TLS handshake level (UseMutualTLSAuth)
// or by CertificateCheckMW middleware. We don't validate against cert manager here because
// MtlsStaticCertificateBindings contains hashes for binding, not cert IDs for whitelist lookup
return http.StatusOK, nil
}

// validateLegacyMode handles the legacy auto-update behavior
// Updates session certificate with current cert hash and validates whitelist
func (k *AuthKey) validateLegacyMode(r *http.Request, session *user.SessionState, certHash string, updateSession *bool) (int, error) {
// Auto-update session certificate with current cert hash
if session.Certificate != certHash {
session.Certificate = certHash
*updateSession = true
}

// Validate the certificate exists in cert manager (whitelist check)
if _, err := k.Gw.CertificateManager.GetRaw(certHash); err != nil {
err, code := k.reportInvalidKey("", r, MsgNonExistentCert, ErrAuthCertNotFound)
return code, err
}

return http.StatusOK, nil
}

// validateBindingWithoutCert validates when binding is enabled but no certificate is provided
// Rejects only if the session has a certificate bound to it
func (k *AuthKey) validateBindingWithoutCert(r *http.Request, key string, session *user.SessionState) (int, error) {
if len(session.MtlsStaticCertificateBindings) > 0 {
k.Logger().WithField("key", k.Gw.obfuscateKey(key)).Warn("Certificate required but not provided")
err, code := k.reportInvalidKey(key, r, MsgCertificateMismatch, ErrAuthCertMismatch)
return code, err
}
return http.StatusOK, nil
}

// validateLegacyWithoutCert validates session certificate in legacy mode when no TLS cert is provided
// Checks if stored certificate exists in cert manager to catch corrupted data
func (k *AuthKey) validateLegacyWithoutCert(r *http.Request, session *user.SessionState) (int, error) {
if session.Certificate != "" {
if _, err := k.Gw.CertificateManager.GetRaw(session.Certificate); err != nil {
err, code := k.reportInvalidKey("", r, MsgNonExistentCert, ErrAuthCertNotFound)
return code, err
}
}
return http.StatusOK, nil
}
Loading
Loading