Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

X509 Certificate Validation - x5t Thumbprint #35

Merged
merged 25 commits into from
Nov 24, 2021
Merged
Show file tree
Hide file tree
Changes from 21 commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
4208218
Add Token.GetClaimAsMap() method
nenaraab Nov 10, 2021
6f0c7b3
Add Token.getCnfClaimMember() method
nenaraab Nov 11, 2021
75e469b
Proof of Possession x5t thumbprint validation
nenaraab Nov 11, 2021
f0914ac
add tests
nenaraab Nov 12, 2021
c797cec
fix linter issues and add tests
nenaraab Nov 12, 2021
5add03d
satisfy compliance check
nenaraab Nov 12, 2021
6bc293b
add go doc
nenaraab Nov 12, 2021
34d2656
provide auth.auth.ClientCertificateFromCtx(r) and make authenticate p…
nenaraab Nov 15, 2021
7565c0a
introduce std errors!
nenaraab Nov 15, 2021
59ad87c
Merge remote-tracking branch 'origin/master' into cert_validation
nenaraab Nov 15, 2021
209cd7d
Update middleware.go
nenaraab Nov 16, 2021
86fdd85
Update auth/proofOfPossession.go
nenaraab Nov 17, 2021
7d99faa
incorporate Ligas Review comments, rename method
nenaraab Nov 16, 2021
e2af8cd
undo middelware.authenticate -> middelware.Authenticate
nenaraab Nov 16, 2021
e9bdda5
felix' comments
nenaraab Nov 17, 2021
056f9e6
apply linter
nenaraab Nov 17, 2021
4120a6e
add How to use standalone X5t Validator
nenaraab Nov 18, 2021
3d4f7a6
rename parseAndValidateCertificate -> validateCertificate
nenaraab Nov 19, 2021
69d49ab
introduce Certificate instance
nenaraab Nov 19, 2021
8a86b2f
refactor project to use Certificate instead of x509.Certificate
nenaraab Nov 22, 2021
a94d882
cleanup and doc
nenaraab Nov 22, 2021
3ef2426
Merge remote-tracking branch 'origin/master' into cert_validation
nenaraab Nov 24, 2021
f2a8327
refactor and optimize tests
nenaraab Nov 24, 2021
0c14dce
remove duplicate code
nenaraab Nov 24, 2021
4d68204
undo option to use Certificate Thumbprint (x5t) validator standalone
nenaraab Nov 24, 2021
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
1 change: 1 addition & 0 deletions .reuse/dep5
Original file line number Diff line number Diff line change
Expand Up @@ -34,5 +34,6 @@ Files:
.github/**
.golangci.yml
env/testdata/**
auth/testdata/**
Copyright: 2020-2021 SAP SE or an SAP affiliate company and Cloud Security Client Go contributors
License: Apache-2.0
14 changes: 14 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,20 @@ if err != nil {
```
Full example: [samples/middleware.go](samples/middleware.go)

### Proof of Possession
Additionally, you may want to make sure, whether you have been called by a trust-worthy consumer. Trust between applications/services is established with certificates in principle. So, in case of mTls based communication, you can check, whether the token was issued for the consumer. This can be done by
performing the JWT Certificate Thumbprint X5t confirmation method's validation. See specification [RFC 8705](https://tools.ietf.org/html/rfc8705#section-3.1). It can be done in the following manner:

```go
func myEndpoint(w http.ResponseWriter, r *http.Request) {
err := auth.ValidateX5tThumbprint(auth.ClientCertificateFromCtx(r), auth.TokenFromCtx(r))
if err != nil {
panic(err)
}
...
}
```

### Testing
The client library offers an OIDC Mock Server with means to create arbitrary tokens for testing purposes. Examples for the usage of the Mock Server in combination with the OIDC Token Builder can be found in [auth/middleware_test.go](auth/middleware_test.go)

Expand Down
63 changes: 63 additions & 0 deletions auth/certificate.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
// SPDX-FileCopyrightText: 2021 SAP SE or an SAP affiliate company and Cloud Security Client Go contributors
//
// SPDX-License-Identifier: Apache-2.0
package auth

import (
"bytes"
"crypto/sha256"
"crypto/x509"
"encoding/base64"
"encoding/pem"
"fmt"
)

// Certificate is the public API to access claims of the X509 client certificate.
type Certificate struct {
x509Cert *x509.Certificate
}

// newCertificate parses the X509 client certificate string.
// It supports DER and PEM formatted certificates.
// Returns nil, if certString is empty string.
// Returns error in case of parsing error.
func newCertificate(certString string) (*Certificate, error) {
x509Cert, err := parseCertificate(certString)
if x509Cert != nil {
return &Certificate{
x509Cert: x509Cert,
}, nil
}
return nil, err
}

// GetThumbprint returns the thumbprint without padding.
func (c *Certificate) GetThumbprint() string {
thumbprintBytes := sha256.Sum256(c.x509Cert.Raw)
thumbprint := base64.URLEncoding.WithPadding(base64.NoPadding).EncodeToString(thumbprintBytes[:])

return thumbprint
}

func parseCertificate(certString string) (*x509.Certificate, error) {
if certString == "" {
return nil, nil
}
const PEMIndicator string = "-----BEGIN"
decoded, err := base64.StdEncoding.DecodeString(certString)
if err != nil {
return nil, fmt.Errorf("cannot base64 decode certificate header: %w", err)
}
if bytes.HasPrefix(decoded, []byte(PEMIndicator)) { // in case of apache proxy
pemBlock, err := pem.Decode(decoded)
if pemBlock == nil {
return nil, fmt.Errorf("cannot decode PEM formatted certificate header: %v", err)
}
decoded = pemBlock.Bytes
}
cert, err := x509.ParseCertificate(decoded)
if err != nil {
return nil, err
}
return cert, nil
}
39 changes: 39 additions & 0 deletions auth/certificate_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
// SPDX-FileCopyrightText: 2021 SAP SE or an SAP affiliate company and Cloud Security Client Go contributors
//
// SPDX-License-Identifier: Apache-2.0
package auth

import (
"github.com/stretchr/testify/assert"
"testing"
)

func TestCertificate(t *testing.T) {
t.Run("newCertificate() returns nil when no certificate is given", func(t *testing.T) {
cert, err := newCertificate("")
assert.Nil(t, cert)
assert.Nil(t, err)
})

t.Run("newCertificate() fails when DER certificate is corrupt", func(t *testing.T) {
cert, err := newCertificate("abc123")
assert.Nil(t, cert)
assert.Contains(t, err.Error(), "cannot base64 decode certificate header:")
})

t.Run("newCertificate() fails when PEM certificate is corrupt", func(t *testing.T) {
cert, err := newCertificate("LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUQxVENDQXIyZ0F3SUJBZ0lNSUxvRXNuTFFCdQotLS0tLUVORCBDRVJUSUZJQ0FURS0tLS0t")
assert.Nil(t, cert)
assert.Contains(t, err.Error(), "cannot decode PEM formatted certificate header:")
})

t.Run("GetThumbprint() for PEM formatted cert", func(t *testing.T) {
cert, _ := newCertificate(readCert(t, "x-forwarded-client-cert.txt", true))
assert.Equal(t, "fU-XoQlhMTpQsz9ArXl6zHIpMGuRO4ExLKdLRTc5VjM", cert.GetThumbprint())
})

t.Run("GetThumbprint() for DER formatted cert", func(t *testing.T) {
cert, _ := newCertificate(readCert(t, "x-forwarded-client-cert.txt", false))
assert.Equal(t, "fU-XoQlhMTpQsz9ArXl6zHIpMGuRO4ExLKdLRTc5VjM", cert.GetThumbprint())
})
}
52 changes: 41 additions & 11 deletions auth/middleware.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,12 @@ import (
type ContextKey int

// TokenCtxKey is the key that holds the authorization value (*OIDCClaims) in the request context
// ClientCertificateCtxKey is the key that holds the x509 client certificate in the request context
const (
TokenCtxKey ContextKey = 0

cacheExpiration = 12 * time.Hour
cacheCleanupInterval = 24 * time.Hour
TokenCtxKey ContextKey = 0
ClientCertificateCtxKey ContextKey = 1
cacheExpiration = 12 * time.Hour
cacheCleanupInterval = 24 * time.Hour
)

// ErrorHandler is the type for the Error Handler which is called on unsuccessful token validation and if the AuthenticationHandler middleware func is used
Expand Down Expand Up @@ -54,7 +55,13 @@ func TokenFromCtx(r *http.Request) Token {
return r.Context().Value(TokenCtxKey).(Token)
}

// Middleware is the main entrypoint to the client library, instantiate with NewMiddleware. It holds information about the oAuth config and configured options.
// ClientCertificateFromCtx retrieves the X.509 client certificate of a request which
// have been injected before via the auth middleware
func ClientCertificateFromCtx(r *http.Request) *Certificate {
return r.Context().Value(ClientCertificateCtxKey).(*Certificate)
}

// Middleware is the main entrypoint to the authn client library, instantiate with NewMiddleware. It holds information about the oAuth config and configured options.
// Use either the ready to use AuthenticationHandler as a middleware or implement your own middleware with the help of Authenticate.
type Middleware struct {
oAuthConfig OAuthConfig
Expand Down Expand Up @@ -89,35 +96,58 @@ func NewMiddleware(oAuthConfig OAuthConfig, options Options) *Middleware {

// Authenticate authenticates a request and returns the Token if validation was successful, otherwise error is returned
func (m *Middleware) Authenticate(r *http.Request) (Token, error) {
token, _, err := m.AuthenticateWithProofOfPossession(r)

return token, err
}

// Authenticate authenticates a request and returns the Token and the client certificate if validation was successful,
// otherwise error is returned
func (m *Middleware) AuthenticateWithProofOfPossession(r *http.Request) (Token, *Certificate, error) {
// get Token from Header
rawToken, err := extractRawToken(r)
if err != nil {
return nil, err
return nil, nil, err
}

token, err := m.parseAndValidateJWT(rawToken)
if err != nil {
return nil, err
return nil, nil, err
}

const forwardedClientCertHeader = "x-forwarded-client-cert"
var cert *Certificate
cert, err = newCertificate(r.Header.Get(forwardedClientCertHeader))
if err != nil {
return nil, nil, err
}
if "1" == "" && cert != nil { // TODO integrate proof of possession into middleware
err = validateCertificate(cert, token)
if err != nil {
return nil, nil, err
}
}

return token, nil
return token, cert, nil
}

// AuthenticationHandler authenticates a request and injects the claims into
// the request context. If the authentication (see Authenticate) does not succeed,
// the specified error handler (see Options.ErrorHandler) will be called and
// the current request will stop.
// In case of successful authentication the request context is enriched with the token,
// as well as the client certificate (if given).
func (m *Middleware) AuthenticationHandler(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
token, err := m.Authenticate(r)
token, cert, err := m.AuthenticateWithProofOfPossession(r)

if err != nil {
m.options.ErrorHandler(w, r, err)
return
}

reqWithContext := r.WithContext(context.WithValue(r.Context(), TokenCtxKey, token))
*r = *reqWithContext
ctx := context.WithValue(context.WithValue(r.Context(), TokenCtxKey, token), ClientCertificateCtxKey, cert)
*r = *r.WithContext(ctx)

// Continue serving http if jwt was valid
next.ServeHTTP(w, r)
Expand Down
4 changes: 4 additions & 0 deletions auth/middleware_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -272,6 +272,10 @@ func TestEnd2End(t *testing.T) {

func GetTestHandler() http.HandlerFunc {
return func(rw http.ResponseWriter, req *http.Request) {
cert := ClientCertificateFromCtx(req)
if cert != nil {
_, _ = rw.Write([]byte("entered test handler using cert: " + string(cert.x509Cert.Raw)))
}
_, _ = rw.Write([]byte("entered test handler"))
}
}
Expand Down
54 changes: 54 additions & 0 deletions auth/proofOfPossession.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
// SPDX-FileCopyrightText: 2021 SAP SE or an SAP affiliate company and Cloud Security Client Go contributors
//
// SPDX-License-Identifier: Apache-2.0
package auth

// validateCertificate checks proof of possession in addition to audience validation
// to make sure that it was called by a trust-worthy consumer.
// Trust between application and applications/services is established with certificates in principle.
// Proof of possession uses certificates as proof token and therefore, x.509 based mTLS communication is demanded.
import (
"crypto/sha256"
"encoding/base64"
"errors"
"fmt"
)

var ErrNoClientCert = errors.New("there is no x509 client certificate provided")
var ErrNoToken = errors.New("there is no token provided")

// validateCertificate runs all proof of possession checks.
// This ensures that the token was issued for the sender.
func validateCertificate(clientCertificate *Certificate, token Token) error {
if clientCertificate == nil {
return ErrNoClientCert
}
if token == nil {
return ErrNoToken
}
return ValidateX5tThumbprint(clientCertificate, token)
}

// ValidateX5tThumbprint compares the thumbprint of the provided X509 client certificate against the cnf claim with the confirmation method "x5t#S256".
// This ensures that the token was issued for the sender.
func ValidateX5tThumbprint(clientCertificate *Certificate, token Token) error {
if clientCertificate == nil {
return ErrNoClientCert
}
if token == nil {
return ErrNoToken
}

cnfThumbprint := token.getCnfClaimMember(claimCnfMemberX5t)
if cnfThumbprint == "" {
return fmt.Errorf("token provides no cnf member for thumbprint confirmation")
}

certThumbprintBytes := sha256.Sum256(clientCertificate.x509Cert.Raw)
certThumbprint := base64.URLEncoding.WithPadding(base64.NoPadding).EncodeToString(certThumbprintBytes[:])

if cnfThumbprint != certThumbprint {
return fmt.Errorf("token thumbprint confirmation failed")
}
return nil
}
Loading