From 42082189993751675cf21f1b2af7238f48e6ca32 Mon Sep 17 00:00:00 2001 From: Nena Raab Date: Wed, 10 Nov 2021 17:18:53 +0100 Subject: [PATCH 01/23] Add Token.GetClaimAsMap() method --- auth/token.go | 49 +++++++++++++++++++++++++++++----------------- auth/token_test.go | 19 ++++++++++++++++++ 2 files changed, 50 insertions(+), 18 deletions(-) diff --git a/auth/token.go b/auth/token.go index 79da646..7b6c220 100644 --- a/auth/token.go +++ b/auth/token.go @@ -25,24 +25,25 @@ const ( // Token is the public API to access claims of the token type Token interface { - TokenValue() string // TokenValue returns encoded token string - Audience() []string // Audience returns "aud" claim, if it doesn't exist empty string is returned - Expiration() time.Time // Expiration returns "exp" claim, if it doesn't exist empty string is returned - IsExpired() bool // IsExpired returns true, if 'exp' claim + leeway time of 1 minute is before current time - IssuedAt() time.Time // IssuedAt returns "iat" claim, if it doesn't exist empty string is returned - CustomIssuer() string // CustomIssuer returns "iss" claim if it is a custom domain ("ias_iss" claim available), if it doesn't exist empty string is returned - Issuer() string // Issuer returns "ias_iss" (SAP domain, only set if a custom non-SAP domain is used as "iss") claim, otherwise the standard "iss" claim is returned - NotBefore() time.Time // NotBefore returns "nbf" claim, if it doesn't exist empty string is returned - Subject() string // Subject returns "sub" claim, if it doesn't exist empty string is returned - GivenName() string // GivenName returns "given_name" claim, if it doesn't exist empty string is returned - FamilyName() string // FamilyName returns "family_name" claim, if it doesn't exist empty string is returned - Email() string // Email returns "email" claim, if it doesn't exist empty string is returned - ZoneID() string // ZoneID returns "zone_uuid" claim, if it doesn't exist empty string is returned - UserUUID() string // UserUUID returns "user_uuid" claim, if it doesn't exist empty string is returned - HasClaim(claim string) bool // HasClaim returns true if the provided claim exists in the token - GetClaimAsString(claim string) (string, error) // GetClaimAsString returns a custom claim type asserted as string. Returns error if the claim is not available or not a string. - GetClaimAsStringSlice(claim string) ([]string, error) // GetClaimAsStringSlice returns a custom claim type asserted as string slice. The claim name is case sensitive. Returns error if the claim is not available or not an array - GetAllClaimsAsMap() map[string]interface{} // GetAllClaimsAsMap returns a map of all claims contained in the token. The claim name is case sensitive. Includes also custom claims + TokenValue() string // TokenValue returns encoded token string + Audience() []string // Audience returns "aud" claim, if it doesn't exist empty string is returned + Expiration() time.Time // Expiration returns "exp" claim, if it doesn't exist empty string is returned + IsExpired() bool // IsExpired returns true, if 'exp' claim + leeway time of 1 minute is before current time + IssuedAt() time.Time // IssuedAt returns "iat" claim, if it doesn't exist empty string is returned + CustomIssuer() string // CustomIssuer returns "iss" claim if it is a custom domain ("ias_iss" claim available), if it doesn't exist empty string is returned + Issuer() string // Issuer returns "ias_iss" (SAP domain, only set if a custom non-SAP domain is used as "iss") claim, otherwise the standard "iss" claim is returned + NotBefore() time.Time // NotBefore returns "nbf" claim, if it doesn't exist empty string is returned + Subject() string // Subject returns "sub" claim, if it doesn't exist empty string is returned + GivenName() string // GivenName returns "given_name" claim, if it doesn't exist empty string is returned + FamilyName() string // FamilyName returns "family_name" claim, if it doesn't exist empty string is returned + Email() string // Email returns "email" claim, if it doesn't exist empty string is returned + ZoneID() string // ZoneID returns "zone_uuid" claim, if it doesn't exist empty string is returned + UserUUID() string // UserUUID returns "user_uuid" claim, if it doesn't exist empty string is returned + HasClaim(claim string) bool // HasClaim returns true if the provided claim exists in the token + GetClaimAsString(claim string) (string, error) // GetClaimAsString returns a custom claim type asserted as string. Returns error if the claim is not available or not a string + GetClaimAsStringSlice(claim string) ([]string, error) // GetClaimAsStringSlice returns a custom claim type asserted as string slice. The claim name is case sensitive. Returns error if the claim is not available or not an array + GetClaimAsMap(claim string) (map[string]interface{}, error) // GetClaimAsMap returns a map of all members and its values of a custom claim in the token. The member name is case sensitive. Returns error if the claim is not available or not a map + GetAllClaimsAsMap() map[string]interface{} // GetAllClaimsAsMap returns a map of all claims contained in the token. The claim name is case sensitive. Includes also custom claims getJwtToken() jwt.Token } @@ -172,6 +173,18 @@ func (t stdToken) GetAllClaimsAsMap() map[string]interface{} { return mapClaims } +func (t stdToken) GetClaimAsMap(claim string) (map[string]interface{}, error) { + value, exists := t.jwtToken.Get(claim) + if !exists { + return nil, ErrClaimNotExists + } + res, ok := value.(map[string]interface{}) + if !ok { + return nil, fmt.Errorf("unable to assert type of claim %s to map[string]interface{}. Actual type: %T", claim, value) + } + return res, nil +} + func (t stdToken) getJwtToken() jwt.Token { return t.jwtToken } diff --git a/auth/token_test.go b/auth/token_test.go index 0d5cca6..9b9381b 100644 --- a/auth/token_test.go +++ b/auth/token_test.go @@ -131,6 +131,25 @@ func TestOIDCClaims_getAllClaimsAsMap(t *testing.T) { } } +func TestOIDCClaims_getClaimAsMap(t *testing.T) { + token, err := NewToken("eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJjbmYiOnsieDV0I1MyNTYiOiIwX3daeG5EUXd6dkxqLWh0NHNZbFQ3RzBIMURuT2ZPUC02MGFxeU1PVDI4IiwicHJvb2Z0b2tlbiI6InRydWUifX0.3Xi2fe-m-6lc1Ze9_AsnNpkYAG-LKFPHCld5EggQTW4") + if err != nil { + t.Errorf("Error while preparing test: %v", err) + } + + got, err := token.GetClaimAsMap("cnf") + if err != nil { + t.Errorf("GetClaimAsStringSlice() error = %v", err) + return + } + if len(got) != 2 { + t.Errorf("GetClaimAsMap() number of members got = %v, want %v", len(got), 2) + } + if got["x5t#S256"] != "0_wZxnDQwzvLj-ht4sYlT7G0H1DnOfOP-60aqyMOT28" { + t.Errorf("GetClaimAsMap()[\"x5t#S256\"] got = %v", got["x5t#S256"]) + } +} + func TestOIDCClaims_getSAPIssuer(t *testing.T) { tests := []struct { name string From 6f0c7b3232691baa860a10c6da639b2569b2ee42 Mon Sep 17 00:00:00 2001 From: Nena Raab Date: Thu, 11 Nov 2021 09:18:01 +0100 Subject: [PATCH 02/23] Add Token.getCnfClaimMember() method --- auth/token.go | 19 ++++++++++++++++++- auth/token_test.go | 7 ++++--- 2 files changed, 22 insertions(+), 4 deletions(-) diff --git a/auth/token.go b/auth/token.go index 7b6c220..d6b8011 100644 --- a/auth/token.go +++ b/auth/token.go @@ -15,6 +15,8 @@ import ( ) const ( + claimCnf = "cnf" + claimCnfMemberX5t = "x5t#S256" claimGivenName = "given_name" claimFamilyName = "family_name" claimEmail = "email" @@ -43,8 +45,9 @@ type Token interface { GetClaimAsString(claim string) (string, error) // GetClaimAsString returns a custom claim type asserted as string. Returns error if the claim is not available or not a string GetClaimAsStringSlice(claim string) ([]string, error) // GetClaimAsStringSlice returns a custom claim type asserted as string slice. The claim name is case sensitive. Returns error if the claim is not available or not an array GetClaimAsMap(claim string) (map[string]interface{}, error) // GetClaimAsMap returns a map of all members and its values of a custom claim in the token. The member name is case sensitive. Returns error if the claim is not available or not a map - GetAllClaimsAsMap() map[string]interface{} // GetAllClaimsAsMap returns a map of all claims contained in the token. The claim name is case sensitive. Includes also custom claims + GetAllClaimsAsMap() map[string]interface{} // GetAllClaimsAsMap returns a map of all claims contained in the token. The claim names are case sensitive. Includes also custom claims getJwtToken() jwt.Token + getCnfClaimMember(memberName string) string // getCnfClaimMember returns "cnf" claim. The cnf member name is case sensitive. If it doesn't exist empty string is returned } type stdToken struct { @@ -188,3 +191,17 @@ func (t stdToken) GetClaimAsMap(claim string) (map[string]interface{}, error) { func (t stdToken) getJwtToken() jwt.Token { return t.jwtToken } + +func (t stdToken) getCnfClaimMember(memberName string) string { + if t.HasClaim(claimCnf) { + cnfClaim, err := t.GetClaimAsMap(claimCnf) + fmt.Printf("Error getting cnf claim as map: %v", err) + if cnfClaim != nil { + res, ok := cnfClaim[memberName] + if ok { + return res.(string) + } + } + } + return "" +} diff --git a/auth/token_test.go b/auth/token_test.go index 9b9381b..fbe92bf 100644 --- a/auth/token_test.go +++ b/auth/token_test.go @@ -137,7 +137,7 @@ func TestOIDCClaims_getClaimAsMap(t *testing.T) { t.Errorf("Error while preparing test: %v", err) } - got, err := token.GetClaimAsMap("cnf") + got, err := token.GetClaimAsMap(claimCnf) if err != nil { t.Errorf("GetClaimAsStringSlice() error = %v", err) return @@ -145,8 +145,9 @@ func TestOIDCClaims_getClaimAsMap(t *testing.T) { if len(got) != 2 { t.Errorf("GetClaimAsMap() number of members got = %v, want %v", len(got), 2) } - if got["x5t#S256"] != "0_wZxnDQwzvLj-ht4sYlT7G0H1DnOfOP-60aqyMOT28" { - t.Errorf("GetClaimAsMap()[\"x5t#S256\"] got = %v", got["x5t#S256"]) + cnfClaimMemberX5t := token.getCnfClaimMember(claimCnfMemberX5t) + if cnfClaimMemberX5t != "0_wZxnDQwzvLj-ht4sYlT7G0H1DnOfOP-60aqyMOT28" { + t.Errorf("getCnfClaimMember()[%v] got = %v", claimCnfMemberX5t, cnfClaimMemberX5t) } } From 75e469b51cd70b17cca21c4e97b1713a3d649225 Mon Sep 17 00:00:00 2001 From: Nena Raab Date: Thu, 11 Nov 2021 17:40:48 +0100 Subject: [PATCH 03/23] Proof of Possession x5t thumbprint validation --- auth/middleware.go | 9 ++++- auth/proofOfPossession.go | 74 +++++++++++++++++++++++++++++++++++++++ auth/token.go | 4 ++- go.mod | 1 + 4 files changed, 86 insertions(+), 2 deletions(-) create mode 100644 auth/proofOfPossession.go diff --git a/auth/middleware.go b/auth/middleware.go index 86f8a15..5940eff 100644 --- a/auth/middleware.go +++ b/auth/middleware.go @@ -54,7 +54,7 @@ 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. +// 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 @@ -100,6 +100,13 @@ func (m *Middleware) Authenticate(r *http.Request) (Token, error) { return nil, err } + // TODO integrate proof of possession into middleware + //const FORWARDED_CLIENT_CERT_HEADER = "x-forwarded-client-cert" + //err = parseAndValidateCertificate(r.Header.Get(FORWARDED_CLIENT_CERT_HEADER), token) + //if err != nil { + // return nil, err + //} + return token, nil } diff --git a/auth/proofOfPossession.go b/auth/proofOfPossession.go new file mode 100644 index 0000000..cd48d68 --- /dev/null +++ b/auth/proofOfPossession.go @@ -0,0 +1,74 @@ +package auth + +// parseAndValidateCertificate 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 ( + "bytes" + "crypto/sha256" + "crypto/x509" + "encoding/base64" + "encoding/pem" + "fmt" +) + +func parseAndValidateCertificate(clientCertificate string, token Token) error { + if clientCertificate == "" { + return fmt.Errorf("there is no client certificate provided") + } + if token == nil { + return fmt.Errorf("there is no token provided") + } + + x509ClientCert, err := ParseCertHeader(clientCertificate) + if err != nil { + return fmt.Errorf("cannot parse client certificate: %v", err) + } + return ValidateX5tThumbprint(x509ClientCert, token) +} + +func ValidateX5tThumbprint(clientCertificate *x509.Certificate, token Token) error { + if clientCertificate == nil { + return fmt.Errorf("there is no x509 client certificate provided") + } + if token == nil { + return fmt.Errorf("there is no token provided") + } + + cnfThumbprint := token.getCnfClaimMember(claimCnfMemberX5t) + if cnfThumbprint == "" { + return fmt.Errorf("token provides no cnf member for thumbprint confirmation") + } + + certThumbprintBytes := sha256.Sum256(clientCertificate.Raw) + certThumbprint := base64.URLEncoding.WithPadding(base64.NoPadding).EncodeToString(certThumbprintBytes[:]) + + if cnfThumbprint != certThumbprint { + return fmt.Errorf("token thumbprint confirmation failed") + } + return nil +} + +func ParseCertHeader(certHeader string) (*x509.Certificate, error) { + if certHeader == "" { + return nil, fmt.Errorf("there is no certificate header provided") + } + const PEM_INDICATOR string = "-----BEGIN" + decoded, err := base64.StdEncoding.DecodeString(certHeader) + if err != nil { + return nil, fmt.Errorf("cannot base64 decode certificate header: %w", err) + } + if bytes.HasPrefix(decoded, []byte(PEM_INDICATOR)) { // in case of apache proxy + pemBlock, _ := 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 +} diff --git a/auth/token.go b/auth/token.go index d6b8011..5d4f60f 100644 --- a/auth/token.go +++ b/auth/token.go @@ -195,7 +195,9 @@ func (t stdToken) getJwtToken() jwt.Token { func (t stdToken) getCnfClaimMember(memberName string) string { if t.HasClaim(claimCnf) { cnfClaim, err := t.GetClaimAsMap(claimCnf) - fmt.Printf("Error getting cnf claim as map: %v", err) + if err != nil { + fmt.Printf("Error getting cnf claim as map: %v", err) + } if cnfClaim != nil { res, ok := cnfClaim[memberName] if ok { diff --git a/go.mod b/go.mod index ff1b2ab..8892cba 100644 --- a/go.mod +++ b/go.mod @@ -9,6 +9,7 @@ require ( github.com/lestrrat-go/jwx v1.2.9 github.com/patrickmn/go-cache v2.1.0+incompatible github.com/pquerna/cachecontrol v0.1.0 + github.com/stretchr/testify v1.7.0 golang.org/x/sync v0.0.0-20210220032951-036812b2e83c gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c ) From f0914acf6f921fb2ee926fda549d7e73984884da Mon Sep 17 00:00:00 2001 From: Nena Raab Date: Fri, 12 Nov 2021 16:55:03 +0100 Subject: [PATCH 04/23] add tests --- auth/proofOfPossession_test.go | 165 ++++++++++++++++++++++ auth/testdata/x-forwarded-client-cert.txt | 1 + 2 files changed, 166 insertions(+) create mode 100644 auth/proofOfPossession_test.go create mode 100644 auth/testdata/x-forwarded-client-cert.txt diff --git a/auth/proofOfPossession_test.go b/auth/proofOfPossession_test.go new file mode 100644 index 0000000..9f0707e --- /dev/null +++ b/auth/proofOfPossession_test.go @@ -0,0 +1,165 @@ +package auth + +import ( + "crypto/rand" + "crypto/rsa" + "crypto/x509" + "crypto/x509/pkix" + "encoding/base64" + "encoding/pem" + "github.com/lestrrat-go/jwx/jwt" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "math/big" + "os" + "path" + "testing" +) + +func TestProofOfPossession_ParseCertHeader_edgeCases(t *testing.T) { + t.Run("ParseCertHeader() fails when no certificate is given", func(t *testing.T) { + _, err := ParseCertHeader("") + assert.Equal(t, "there is no certificate header provided", err.Error()) + }) + + t.Run("ParseCertHeader() fails when DER certificate is corrupt", func(t *testing.T) { + _, err := ParseCertHeader("abc123") + assert.Contains(t, err.Error(), "cannot base64 decode certificate header:") + }) + + t.Run("ParseCertHeader() fails when PEM certificate is corrupt", func(t *testing.T) { + _, err := ParseCertHeader("LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUQxVENDQXIyZ0F3SUJBZ0lNSUxvRXNuTFFCdQotLS0tLUVORCBDRVJUSUZJQ0FURS0tLS0t") + assert.Contains(t, err.Error(), "cannot decode PEM formatted certificate header:") + }) +} + +func TestProofOfPossession_validateX5tThumbprint_edgeCases(t *testing.T) { + t.Run("ValidateX5tThumbprint() fails when no cert is given", func(t *testing.T) { + err := ValidateX5tThumbprint(nil, createToken(t, "abc")) + assert.Equal(t, "there is no x509 client certificate provided", err.Error()) + }) + + t.Run("ValidateX5tThumbprint() fails when no token is given", func(t *testing.T) { + x509Cert, err := ParseCertHeader(generateCert(t, "test-issuer-org", "test-subject-org", false)) + require.NoError(t, err, "Failed to parse cert header: %v", err) + err = ValidateX5tThumbprint(x509Cert, nil) + assert.Equal(t, "there is no token provided", err.Error()) + }) +} + +func TestProofOfPossession_validateX5tThumbprint(t *testing.T) { + tests := []struct { + name string + claimCnfMemberX5t string + certFile string // in case of empty string it gets generated + pemEncoded bool + expectedErrMsg string // in case of empty string no error is expected + }{ + { + name: "x5t should match with DER certificate (go router)", + claimCnfMemberX5t: "fU-XoQlhMTpQsz9ArXl6zHIpMGuRO4ExLKdLRTc5VjM", + certFile: "x-forwarded-client-cert.txt", + pemEncoded: false, + expectedErrMsg: "", + }, { + name: "x5t should match with PEM certificate (apache proxy)", + claimCnfMemberX5t: "fU-XoQlhMTpQsz9ArXl6zHIpMGuRO4ExLKdLRTc5VjM", + certFile: "x-forwarded-client-cert.txt", + pemEncoded: true, + expectedErrMsg: "", + }, { + name: "expect error when x5t does not match with generated DER certificate (go router)", + claimCnfMemberX5t: "fU-XoQlhMTpQsz9ArXl6zHIpMGuRO4ExLKdLRTc5VjM", + certFile: "", + pemEncoded: false, + expectedErrMsg: "token thumbprint confirmation failed", + }, { + name: "expect error when x5t does not match with generated PEM certificate (apache proxy)", + claimCnfMemberX5t: "fU-XoQlhMTpQsz9ArXl6zHIpMGuRO4ExLKdLRTc5VjM", + certFile: "", + pemEncoded: true, + expectedErrMsg: "token thumbprint confirmation failed", + }, { + name: "expect error when x5t is empty", + claimCnfMemberX5t: "", + certFile: "", + pemEncoded: false, + expectedErrMsg: "token provides no cnf member for thumbprint confirmation", + }, + } + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + var cert string + if tt.certFile == "" { + cert = generateCert(t, "test-issuer-org", "test-subject-org", tt.pemEncoded) + } else { + cert = readCert(t, tt.certFile, tt.pemEncoded) + } + x509cert, err := ParseCertHeader(cert) + require.NoError(t, err, "Failed to validate client cert with token cnf thumbprint: %v", err) + + err = ValidateX5tThumbprint(x509cert, createToken(t, tt.claimCnfMemberX5t)) + if tt.expectedErrMsg != "" { + assert.Equal(t, tt.expectedErrMsg, err.Error()) + } else { + require.NoError(t, err, "Failed to validate client cert with token cnf thumbprint: %v", err) + } + }) + } +} + +func createToken(t *testing.T, claimCnfMemberX5tValue string) Token { + token := jwt.New() + cnfClaim := map[string]interface{}{ + claimCnfMemberX5t: claimCnfMemberX5tValue, + } + err := token.Set(claimCnf, cnfClaim) + require.NoError(t, err, "Failed to create token: %v", err) + + return stdToken{jwtToken: token} +} + +func readCert(t *testing.T, fileName string, pemEncoded bool) string { + pwd, _ := os.Getwd() + certFilePath := path.Join(pwd, "testdata", fileName) + certificate, err := os.ReadFile(certFilePath) + require.NoError(t, err, "Failed to read certificate from %v: %v", certFilePath, err) + + x509Cert, err := ParseCertHeader(string(certificate)) + require.NoError(t, err, "failed to create certificate: %v", err) + + return encodeDERBytes(x509Cert.Raw, pemEncoded) +} + +func generateCert(t *testing.T, issuerOrg, subjectOrg string, pemEncoded bool) string { + key, err := rsa.GenerateKey(rand.Reader, 2048) + require.NoError(t, err, "") + + issuerName := pkix.Name{ + Organization: []string{issuerOrg}, + } + template := x509.Certificate{ + SerialNumber: big.NewInt(125), + Subject: pkix.Name{ + Organization: []string{subjectOrg}, + }, + Issuer: issuerName, + } + issTemplate := x509.Certificate{ + SerialNumber: big.NewInt(12345), + Subject: issuerName, + Issuer: issuerName, + } + derBytes, err := x509.CreateCertificate(rand.Reader, &template, &issTemplate, &key.PublicKey, key) + require.NoError(t, err, "failed to generate certificate: %v", err) + + return encodeDERBytes(derBytes, pemEncoded) +} + +func encodeDERBytes(derBytes []byte, pemEncoded bool) string { + if pemEncoded { + derBytes = pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: derBytes}) + } + return base64.StdEncoding.EncodeToString(derBytes) +} diff --git a/auth/testdata/x-forwarded-client-cert.txt b/auth/testdata/x-forwarded-client-cert.txt new file mode 100644 index 0000000..55aa9e1 --- /dev/null +++ b/auth/testdata/x-forwarded-client-cert.txt @@ -0,0 +1 @@ +MIIGHzCCBAegAwIBAgIQG9beRcjzTikmC1QhzBARdjANBgkqhkiG9w0BAQsFADCBgDELMAkGA1UEBhMCREUxFDASBgNVBAcMC0VVMTAtQ2FuYXJ5MQ8wDQYDVQQKDAZTQVAgU0UxIzAhBgNVBAsMGlNBUCBDbG91ZCBQbGF0Zm9ybSBDbGllbnRzMSUwIwYDVQQDDBxTQVAgQ2xvdWQgUGxhdGZvcm0gQ2xpZW50IENBMB4XDTIxMDMxMjA3MzMwOVoXDTIxMDQxMTA4MzMwOVowggEEMQswCQYDVQQGEwJERTEPMA0GA1UEChMGU0FQIFNFMSMwIQYDVQQLExpTQVAgQ2xvdWQgUGxhdGZvcm0gQ2xpZW50czEPMA0GA1UECxMGQ2FuYXJ5MS0wKwYDVQQLEyQ4ZTFhZmZiMi02MmExLTQzY2MtYTY4Ny0yYmE3NWU0YjNkODQxKzApBgNVBAcTImFveGsyYWRkaC5hY2NvdW50czQwMC5vbmRlbWFuZC5jb20xUjBQBgNVBAMTSWJkY2QzMDBjLWIyMDItNGE3YS1iYjk1LTJhN2U2ZDE1ZmU0Ny8yYjU4NTQwNS1kMzkxLTQ5ODYtYjc2ZC1iNGYyNDY4NWYzYzgwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDIHzklky3oEib0ovw27WIIyvjkAPQtCYmShS2uQzHsj7Nn9Zp9EPQL2mjYxS2xNj/HXAsbxr6jk+lLrzvdYUNtlQ1XnHIZSBhPyRtQDw1BGHFd38Be70D6rif5s+vDUApvDuOYpLgDBFVgzr25F+47t83lcyWQ1wrKj4wo+aJt5rZrFUAGQCjOqHvccLK8YLmE0p7F444I0CCGxxXg4yQshNFjb3V2Bg+G/gXSYC3gLHX3SPYBhEyM8mm9HoUZ67JEkfM+sPT7OhFL7sLQe2jQ3MK4Z3DgeWLKAxnxLRRdho8sm29fdnt4d8eWbw7N8A2dHkASYvRk2I/tVoVaoAfrAgMBAAGjggEMMIIBCDAJBgNVHRMEAjAAMB8GA1UdIwQYMBaAFMwghFuHFYs3+X6Hsck+w2+VBG6RMB0GA1UdDgQWBBRqE7s38FCAhklB5+Cskt/xL0JaxjAOBgNVHQ8BAf8EBAMCBaAwEwYDVR0lBAwwCgYIKwYBBQUHAwIwgZUGA1UdHwSBjTCBijCBh6CBhKCBgYZ/aHR0cDovL3NhcC1jbG91ZC1wbGF0Zm9ybS1jbGllbnQtY2EtZXUxMC1jYW5hcnktY3Jscy5zMy5ldS1jZW50cmFsLTEuYW1hem9uYXdzLmNvbS9jcmwvZjhhYmU1NGUtMTk1MS00NzBlLWFlMmQtZGU0MGMxNjMzNDFjLmNybDANBgkqhkiG9w0BAQsFAAOCAgEAMwpK54htes+cuSB/iXZRe9bcNpELl7c8eVozEEa5BN1AmFHdEwFNS9M55Y5fHobBGxhOGjAy6UlrbHqV1Wo2d5LxGIF1a1hs7xPbP1GU+moCVnMH60w8d7kPcL7kb5y/RITk7NDgxEhcUC6TrVEiQtfkbw4i3rRyP8Yf6x8QuNwr6c2Zf7yrmFWlFKBrjMMN4dJ+AVKIJyFqPlbdbbBsJegtgo2KqYV8cXCzXMLRhBDkvvHx7Hoz94or6PZ7SPw8bKwwKrAP6/+6Z81q/WKGZwrmYkGi+aji0ocm6n8RyVFQP+wnBJPzn0qD3RNj0afhzvMproRr9O6WJ+tYFhQ2Lnx27+jNP33KBDWLc1XcUanyVtt/pJmbDtzTGD9LDhARX2+a0MciVZcC5WLV4zr9EaGT+a0NrlA1VjxjVTDcXwB3T2KnlskM6aEGodoWGPgfvpaq5IX+pfb/aoeTR3EcXHl8E8WR0aPbpDi8GWGCkqovGDbisnCKJurHRVOx8YE+69tX2qJAuevUENQfUovEWUYtA8UufTpu8TizXxTlt1/X69AdUgnFqLmyNkdd4g7NJoc7A3zjJXin5jwM6a8yjwxvco0fTlqobzngeJhwckcSkR3QvYlVl61ErxTdEhBlH5q1fsQ88tTwZ+x/RKApN80koCrqkj8ubeXc/fDQhj0= \ No newline at end of file From c797cec0c4985539f76e8508cb79fa87d385d6c0 Mon Sep 17 00:00:00 2001 From: Nena Raab Date: Fri, 12 Nov 2021 17:14:00 +0100 Subject: [PATCH 05/23] fix linter issues and add tests --- auth/middleware.go | 14 +++++++------- auth/proofOfPossession.go | 4 ++-- auth/proofOfPossession_test.go | 31 ++++++++++++++++++++++++++----- 3 files changed, 35 insertions(+), 14 deletions(-) diff --git a/auth/middleware.go b/auth/middleware.go index 5940eff..d5b63ab 100644 --- a/auth/middleware.go +++ b/auth/middleware.go @@ -100,13 +100,13 @@ func (m *Middleware) Authenticate(r *http.Request) (Token, error) { return nil, err } - // TODO integrate proof of possession into middleware - //const FORWARDED_CLIENT_CERT_HEADER = "x-forwarded-client-cert" - //err = parseAndValidateCertificate(r.Header.Get(FORWARDED_CLIENT_CERT_HEADER), token) - //if err != nil { - // return nil, err - //} - + if "1" == "" { // TODO integrate proof of possession into middleware + const forwardedClientCertHeader = "x-forwarded-client-cert" + err = parseAndValidateCertificate(r.Header.Get(forwardedClientCertHeader), token) + if err != nil { + return nil, err + } + } return token, nil } diff --git a/auth/proofOfPossession.go b/auth/proofOfPossession.go index cd48d68..575c1bc 100644 --- a/auth/proofOfPossession.go +++ b/auth/proofOfPossession.go @@ -54,12 +54,12 @@ func ParseCertHeader(certHeader string) (*x509.Certificate, error) { if certHeader == "" { return nil, fmt.Errorf("there is no certificate header provided") } - const PEM_INDICATOR string = "-----BEGIN" + const PEMIndicator string = "-----BEGIN" decoded, err := base64.StdEncoding.DecodeString(certHeader) if err != nil { return nil, fmt.Errorf("cannot base64 decode certificate header: %w", err) } - if bytes.HasPrefix(decoded, []byte(PEM_INDICATOR)) { // in case of apache proxy + if bytes.HasPrefix(decoded, []byte(PEMIndicator)) { // in case of apache proxy pemBlock, _ := pem.Decode(decoded) if pemBlock == nil { return nil, fmt.Errorf("cannot decode PEM formatted certificate header: %v", err) diff --git a/auth/proofOfPossession_test.go b/auth/proofOfPossession_test.go index 9f0707e..f6c6f79 100644 --- a/auth/proofOfPossession_test.go +++ b/auth/proofOfPossession_test.go @@ -33,6 +33,27 @@ func TestProofOfPossession_ParseCertHeader_edgeCases(t *testing.T) { }) } +func TestProofOfPossession_parseAndValidateCertificate_edgeCases(t *testing.T) { + t.Run("parseAndValidateCertificate() fails when no cert is given", func(t *testing.T) { + err := ValidateX5tThumbprint(nil, createToken(t, "abc")) + assert.Equal(t, "there is no x509 client certificate provided", err.Error()) + }) + + t.Run("parseAndValidateCertificate() fails when no token is given", func(t *testing.T) { + x509Cert, err := ParseCertHeader(generateCert(t, false)) + require.NoError(t, err, "Failed to parse cert header: %v", err) + err = ValidateX5tThumbprint(x509Cert, nil) + assert.Equal(t, "there is no token provided", err.Error()) + }) + + t.Run("parseAndValidateCertificate() fails when cert does not match x5t", func(t *testing.T) { + x509Cert, err := ParseCertHeader(generateCert(t, false)) + require.NoError(t, err, "Failed to parse cert header: %v", err) + err = ValidateX5tThumbprint(x509Cert, createToken(t, "abc")) + assert.Equal(t, "token thumbprint confirmation failed", err.Error()) + }) +} + func TestProofOfPossession_validateX5tThumbprint_edgeCases(t *testing.T) { t.Run("ValidateX5tThumbprint() fails when no cert is given", func(t *testing.T) { err := ValidateX5tThumbprint(nil, createToken(t, "abc")) @@ -40,7 +61,7 @@ func TestProofOfPossession_validateX5tThumbprint_edgeCases(t *testing.T) { }) t.Run("ValidateX5tThumbprint() fails when no token is given", func(t *testing.T) { - x509Cert, err := ParseCertHeader(generateCert(t, "test-issuer-org", "test-subject-org", false)) + x509Cert, err := ParseCertHeader(generateCert(t, false)) require.NoError(t, err, "Failed to parse cert header: %v", err) err = ValidateX5tThumbprint(x509Cert, nil) assert.Equal(t, "there is no token provided", err.Error()) @@ -92,7 +113,7 @@ func TestProofOfPossession_validateX5tThumbprint(t *testing.T) { t.Run(tt.name, func(t *testing.T) { var cert string if tt.certFile == "" { - cert = generateCert(t, "test-issuer-org", "test-subject-org", tt.pemEncoded) + cert = generateCert(t, tt.pemEncoded) } else { cert = readCert(t, tt.certFile, tt.pemEncoded) } @@ -132,17 +153,17 @@ func readCert(t *testing.T, fileName string, pemEncoded bool) string { return encodeDERBytes(x509Cert.Raw, pemEncoded) } -func generateCert(t *testing.T, issuerOrg, subjectOrg string, pemEncoded bool) string { +func generateCert(t *testing.T, pemEncoded bool) string { key, err := rsa.GenerateKey(rand.Reader, 2048) require.NoError(t, err, "") issuerName := pkix.Name{ - Organization: []string{issuerOrg}, + Organization: []string{"my-issuer-org"}, } template := x509.Certificate{ SerialNumber: big.NewInt(125), Subject: pkix.Name{ - Organization: []string{subjectOrg}, + Organization: []string{"my-subject-org"}, }, Issuer: issuerName, } From 5add03d74ca5059f83297932944c8884fdeacf07 Mon Sep 17 00:00:00 2001 From: Nena Raab Date: Fri, 12 Nov 2021 17:16:34 +0100 Subject: [PATCH 06/23] satisfy compliance check --- .reuse/dep5 | 1 + auth/proofOfPossession.go | 4 ++++ auth/proofOfPossession_test.go | 3 +++ 3 files changed, 8 insertions(+) diff --git a/.reuse/dep5 b/.reuse/dep5 index 9282174..f1c90a4 100644 --- a/.reuse/dep5 +++ b/.reuse/dep5 @@ -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 \ No newline at end of file diff --git a/auth/proofOfPossession.go b/auth/proofOfPossession.go index 575c1bc..5838041 100644 --- a/auth/proofOfPossession.go +++ b/auth/proofOfPossession.go @@ -1,3 +1,6 @@ +// SPDX-FileCopyrightText: 2021 SAP SE or an SAP affiliate company and Cloud Security Client Go contributors +// +// SPDX-License-Identifier: Apache-2.0 package auth // parseAndValidateCertificate checks proof of possession in addition to audience validation @@ -28,6 +31,7 @@ func parseAndValidateCertificate(clientCertificate string, token Token) error { return ValidateX5tThumbprint(x509ClientCert, token) } +// Checks whether func ValidateX5tThumbprint(clientCertificate *x509.Certificate, token Token) error { if clientCertificate == nil { return fmt.Errorf("there is no x509 client certificate provided") diff --git a/auth/proofOfPossession_test.go b/auth/proofOfPossession_test.go index f6c6f79..45f6a97 100644 --- a/auth/proofOfPossession_test.go +++ b/auth/proofOfPossession_test.go @@ -1,3 +1,6 @@ +// 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 ( From 6bc293b51fe3c8ce81f41af33c9008418c3ec9ba Mon Sep 17 00:00:00 2001 From: Nena Raab Date: Fri, 12 Nov 2021 17:21:50 +0100 Subject: [PATCH 07/23] add go doc --- auth/proofOfPossession.go | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/auth/proofOfPossession.go b/auth/proofOfPossession.go index 5838041..164f090 100644 --- a/auth/proofOfPossession.go +++ b/auth/proofOfPossession.go @@ -31,7 +31,10 @@ func parseAndValidateCertificate(clientCertificate string, token Token) error { return ValidateX5tThumbprint(x509ClientCert, token) } -// Checks whether +// In order to check whether the token was issued for the sender, +// the cnf token claim with the confirmation method “x5t#S256” needs to be compared with the thumbprint of the +// provided X509 client certificate. +// See also RFC 8705 func ValidateX5tThumbprint(clientCertificate *x509.Certificate, token Token) error { if clientCertificate == nil { return fmt.Errorf("there is no x509 client certificate provided") @@ -54,6 +57,8 @@ func ValidateX5tThumbprint(clientCertificate *x509.Certificate, token Token) err return nil } +// Parses the X509 client certificate which is provided via the "x-forwarded-client-cert". +// It supports DER encoded and PEM encoded certificates. func ParseCertHeader(certHeader string) (*x509.Certificate, error) { if certHeader == "" { return nil, fmt.Errorf("there is no certificate header provided") From 34d265601c3d2bcc26c143a9e50b66dd4a237aa9 Mon Sep 17 00:00:00 2001 From: Nena Raab Date: Mon, 15 Nov 2021 17:06:47 +0100 Subject: [PATCH 08/23] provide auth.auth.ClientCertificateFromCtx(r) and make authenticate package private --- auth/middleware.go | 52 ++++++++++++++++++++++------------ auth/middleware_test.go | 4 +++ auth/proofOfPossession.go | 22 ++++++-------- auth/proofOfPossession_test.go | 25 ++++++++-------- samples/middleware.go | 1 + 5 files changed, 61 insertions(+), 43 deletions(-) diff --git a/auth/middleware.go b/auth/middleware.go index d5b63ab..c4e452a 100644 --- a/auth/middleware.go +++ b/auth/middleware.go @@ -6,6 +6,7 @@ package auth import ( "context" + "crypto/x509" "log" "net/http" "time" @@ -19,11 +20,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 @@ -54,8 +56,14 @@ func TokenFromCtx(r *http.Request) Token { return r.Context().Value(TokenCtxKey).(Token) } +// ClientCertificateFromCtx retrieves the X.509 client certificate of a request which +// have been injected before via the auth middleware +func ClientCertificateFromCtx(r *http.Request) *x509.Certificate { + return r.Context().Value(ClientCertificateCtxKey).(*x509.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. +// 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 options Options @@ -87,44 +95,52 @@ func NewMiddleware(oAuthConfig OAuthConfig, options Options) *Middleware { return m } -// 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) { +// authenticate authenticates a request and returns the Token if validation was successful, otherwise error is returned +func (m *Middleware) authenticate(r *http.Request) (Token, *x509.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 } - if "1" == "" { // TODO integrate proof of possession into middleware - const forwardedClientCertHeader = "x-forwarded-client-cert" - err = parseAndValidateCertificate(r.Header.Get(forwardedClientCertHeader), token) + const forwardedClientCertHeader = "x-forwarded-client-cert" + var cert *x509.Certificate + cert, err = parseCertString(r.Header.Get(forwardedClientCertHeader)) + if err != nil { + return nil, nil, err + } + if "1" == "" && cert != nil { // TODO integrate proof of possession into middleware + err = parseAndValidateCertificate(cert, token) if err != nil { - return nil, err + 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 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.authenticate(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) diff --git a/auth/middleware_test.go b/auth/middleware_test.go index 12928f5..91acb55 100644 --- a/auth/middleware_test.go +++ b/auth/middleware_test.go @@ -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.Raw))) + } _, _ = rw.Write([]byte("entered test handler")) } } diff --git a/auth/proofOfPossession.go b/auth/proofOfPossession.go index 164f090..ca093f9 100644 --- a/auth/proofOfPossession.go +++ b/auth/proofOfPossession.go @@ -16,19 +16,14 @@ import ( "fmt" ) -func parseAndValidateCertificate(clientCertificate string, token Token) error { - if clientCertificate == "" { - return fmt.Errorf("there is no client certificate provided") +func parseAndValidateCertificate(clientCertificate *x509.Certificate, token Token) error { + if clientCertificate == nil { + return fmt.Errorf("there is no x509 client certificate provided") } if token == nil { return fmt.Errorf("there is no token provided") } - - x509ClientCert, err := ParseCertHeader(clientCertificate) - if err != nil { - return fmt.Errorf("cannot parse client certificate: %v", err) - } - return ValidateX5tThumbprint(x509ClientCert, token) + return ValidateX5tThumbprint(clientCertificate, token) } // In order to check whether the token was issued for the sender, @@ -59,12 +54,13 @@ func ValidateX5tThumbprint(clientCertificate *x509.Certificate, token Token) err // Parses the X509 client certificate which is provided via the "x-forwarded-client-cert". // It supports DER encoded and PEM encoded certificates. -func ParseCertHeader(certHeader string) (*x509.Certificate, error) { - if certHeader == "" { - return nil, fmt.Errorf("there is no certificate header provided") +// Returns nil, if certString is empty string. +func parseCertString(certString string) (*x509.Certificate, error) { + if certString == "" { + return nil, nil } const PEMIndicator string = "-----BEGIN" - decoded, err := base64.StdEncoding.DecodeString(certHeader) + decoded, err := base64.StdEncoding.DecodeString(certString) if err != nil { return nil, fmt.Errorf("cannot base64 decode certificate header: %w", err) } diff --git a/auth/proofOfPossession_test.go b/auth/proofOfPossession_test.go index 45f6a97..b676cbb 100644 --- a/auth/proofOfPossession_test.go +++ b/auth/proofOfPossession_test.go @@ -20,18 +20,19 @@ import ( ) func TestProofOfPossession_ParseCertHeader_edgeCases(t *testing.T) { - t.Run("ParseCertHeader() fails when no certificate is given", func(t *testing.T) { - _, err := ParseCertHeader("") - assert.Equal(t, "there is no certificate header provided", err.Error()) + t.Run("parseCertString() returns nil when no certificate is given", func(t *testing.T) { + cert, err := parseCertString("") + assert.Nil(t, cert) + assert.Nil(t, err) }) - t.Run("ParseCertHeader() fails when DER certificate is corrupt", func(t *testing.T) { - _, err := ParseCertHeader("abc123") + t.Run("parseCertString() fails when DER certificate is corrupt", func(t *testing.T) { + _, err := parseCertString("abc123") assert.Contains(t, err.Error(), "cannot base64 decode certificate header:") }) - t.Run("ParseCertHeader() fails when PEM certificate is corrupt", func(t *testing.T) { - _, err := ParseCertHeader("LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUQxVENDQXIyZ0F3SUJBZ0lNSUxvRXNuTFFCdQotLS0tLUVORCBDRVJUSUZJQ0FURS0tLS0t") + t.Run("parseCertString() fails when PEM certificate is corrupt", func(t *testing.T) { + _, err := parseCertString("LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUQxVENDQXIyZ0F3SUJBZ0lNSUxvRXNuTFFCdQotLS0tLUVORCBDRVJUSUZJQ0FURS0tLS0t") assert.Contains(t, err.Error(), "cannot decode PEM formatted certificate header:") }) } @@ -43,14 +44,14 @@ func TestProofOfPossession_parseAndValidateCertificate_edgeCases(t *testing.T) { }) t.Run("parseAndValidateCertificate() fails when no token is given", func(t *testing.T) { - x509Cert, err := ParseCertHeader(generateCert(t, false)) + x509Cert, err := parseCertString(generateCert(t, false)) require.NoError(t, err, "Failed to parse cert header: %v", err) err = ValidateX5tThumbprint(x509Cert, nil) assert.Equal(t, "there is no token provided", err.Error()) }) t.Run("parseAndValidateCertificate() fails when cert does not match x5t", func(t *testing.T) { - x509Cert, err := ParseCertHeader(generateCert(t, false)) + x509Cert, err := parseCertString(generateCert(t, false)) require.NoError(t, err, "Failed to parse cert header: %v", err) err = ValidateX5tThumbprint(x509Cert, createToken(t, "abc")) assert.Equal(t, "token thumbprint confirmation failed", err.Error()) @@ -64,7 +65,7 @@ func TestProofOfPossession_validateX5tThumbprint_edgeCases(t *testing.T) { }) t.Run("ValidateX5tThumbprint() fails when no token is given", func(t *testing.T) { - x509Cert, err := ParseCertHeader(generateCert(t, false)) + x509Cert, err := parseCertString(generateCert(t, false)) require.NoError(t, err, "Failed to parse cert header: %v", err) err = ValidateX5tThumbprint(x509Cert, nil) assert.Equal(t, "there is no token provided", err.Error()) @@ -120,7 +121,7 @@ func TestProofOfPossession_validateX5tThumbprint(t *testing.T) { } else { cert = readCert(t, tt.certFile, tt.pemEncoded) } - x509cert, err := ParseCertHeader(cert) + x509cert, err := parseCertString(cert) require.NoError(t, err, "Failed to validate client cert with token cnf thumbprint: %v", err) err = ValidateX5tThumbprint(x509cert, createToken(t, tt.claimCnfMemberX5t)) @@ -150,7 +151,7 @@ func readCert(t *testing.T, fileName string, pemEncoded bool) string { certificate, err := os.ReadFile(certFilePath) require.NoError(t, err, "Failed to read certificate from %v: %v", certFilePath, err) - x509Cert, err := ParseCertHeader(string(certificate)) + x509Cert, err := parseCertString(string(certificate)) require.NoError(t, err, "failed to create certificate: %v", err) return encodeDERBytes(x509Cert.Raw, pemEncoded) diff --git a/samples/middleware.go b/samples/middleware.go index 9d7f805..b3e75ed 100644 --- a/samples/middleware.go +++ b/samples/middleware.go @@ -39,4 +39,5 @@ func main() { func helloWorld(w http.ResponseWriter, r *http.Request) { user := auth.TokenFromCtx(r) _, _ = w.Write([]byte(fmt.Sprintf("Hello world!\nYou're logged in as %s", user.Email()))) + fmt.Printf("Certificate from context %s", string(auth.ClientCertificateFromCtx(r).Raw)) } From 7565c0ab2172ec302e726c5328a7aa646a76e9d1 Mon Sep 17 00:00:00 2001 From: Nena Raab Date: Mon, 15 Nov 2021 17:26:34 +0100 Subject: [PATCH 09/23] introduce std errors! --- auth/middleware.go | 3 ++- auth/proofOfPossession.go | 12 ++++++++---- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/auth/middleware.go b/auth/middleware.go index c4e452a..8fbece0 100644 --- a/auth/middleware.go +++ b/auth/middleware.go @@ -95,7 +95,8 @@ func NewMiddleware(oAuthConfig OAuthConfig, options Options) *Middleware { return m } -// authenticate authenticates a request and returns the Token if validation was successful, otherwise error is returned +// authenticate authenticates a request and returns the Token and the client certificate if validation was successful, +// otherwise error is returned func (m *Middleware) authenticate(r *http.Request) (Token, *x509.Certificate, error) { // get Token from Header rawToken, err := extractRawToken(r) diff --git a/auth/proofOfPossession.go b/auth/proofOfPossession.go index ca093f9..0bc3149 100644 --- a/auth/proofOfPossession.go +++ b/auth/proofOfPossession.go @@ -13,15 +13,19 @@ import ( "crypto/x509" "encoding/base64" "encoding/pem" + "errors" "fmt" ) +var ErrNoClientCert = errors.New("there is no x509 client certificate provided") +var ErrNoToken = errors.New("there is no token provided") + func parseAndValidateCertificate(clientCertificate *x509.Certificate, token Token) error { if clientCertificate == nil { - return fmt.Errorf("there is no x509 client certificate provided") + return ErrNoClientCert } if token == nil { - return fmt.Errorf("there is no token provided") + return ErrNoToken } return ValidateX5tThumbprint(clientCertificate, token) } @@ -32,10 +36,10 @@ func parseAndValidateCertificate(clientCertificate *x509.Certificate, token Toke // See also RFC 8705 func ValidateX5tThumbprint(clientCertificate *x509.Certificate, token Token) error { if clientCertificate == nil { - return fmt.Errorf("there is no x509 client certificate provided") + return ErrNoClientCert } if token == nil { - return fmt.Errorf("there is no token provided") + return ErrNoToken } cnfThumbprint := token.getCnfClaimMember(claimCnfMemberX5t) From 209cd7dbf5d03cdd8558d34d1266a891d76f6c94 Mon Sep 17 00:00:00 2001 From: nenaraab Date: Tue, 16 Nov 2021 10:06:13 +0100 Subject: [PATCH 10/23] Update middleware.go --- samples/middleware.go | 1 - 1 file changed, 1 deletion(-) diff --git a/samples/middleware.go b/samples/middleware.go index b3e75ed..9d7f805 100644 --- a/samples/middleware.go +++ b/samples/middleware.go @@ -39,5 +39,4 @@ func main() { func helloWorld(w http.ResponseWriter, r *http.Request) { user := auth.TokenFromCtx(r) _, _ = w.Write([]byte(fmt.Sprintf("Hello world!\nYou're logged in as %s", user.Email()))) - fmt.Printf("Certificate from context %s", string(auth.ClientCertificateFromCtx(r).Raw)) } From 86fdd855402c7a39eef0a202de2641cb9c9dae23 Mon Sep 17 00:00:00 2001 From: nenaraab Date: Wed, 17 Nov 2021 18:33:03 +0100 Subject: [PATCH 11/23] Update auth/proofOfPossession.go Co-authored-by: f-blass <42929142+f-blass@users.noreply.github.com> --- auth/proofOfPossession.go | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/auth/proofOfPossession.go b/auth/proofOfPossession.go index 0bc3149..29c6840 100644 --- a/auth/proofOfPossession.go +++ b/auth/proofOfPossession.go @@ -30,10 +30,8 @@ func parseAndValidateCertificate(clientCertificate *x509.Certificate, token Toke return ValidateX5tThumbprint(clientCertificate, token) } -// In order to check whether the token was issued for the sender, -// the cnf token claim with the confirmation method “x5t#S256” needs to be compared with the thumbprint of the -// provided X509 client certificate. -// See also RFC 8705 +// 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 *x509.Certificate, token Token) error { if clientCertificate == nil { return ErrNoClientCert From 7d99faaf8dc46b7a5ff0b09757bc22e8526a2167 Mon Sep 17 00:00:00 2001 From: Nena Raab Date: Tue, 16 Nov 2021 18:18:15 +0100 Subject: [PATCH 12/23] incorporate Ligas Review comments, rename method --- auth/middleware.go | 2 +- auth/proofOfPossession.go | 2 +- auth/proofOfPossession_test.go | 22 +++++++++++----------- 3 files changed, 13 insertions(+), 13 deletions(-) diff --git a/auth/middleware.go b/auth/middleware.go index 8fbece0..6074046 100644 --- a/auth/middleware.go +++ b/auth/middleware.go @@ -111,7 +111,7 @@ func (m *Middleware) authenticate(r *http.Request) (Token, *x509.Certificate, er const forwardedClientCertHeader = "x-forwarded-client-cert" var cert *x509.Certificate - cert, err = parseCertString(r.Header.Get(forwardedClientCertHeader)) + cert, err = parseCertificate(r.Header.Get(forwardedClientCertHeader)) if err != nil { return nil, nil, err } diff --git a/auth/proofOfPossession.go b/auth/proofOfPossession.go index 29c6840..9c950cc 100644 --- a/auth/proofOfPossession.go +++ b/auth/proofOfPossession.go @@ -57,7 +57,7 @@ func ValidateX5tThumbprint(clientCertificate *x509.Certificate, token Token) err // Parses the X509 client certificate which is provided via the "x-forwarded-client-cert". // It supports DER encoded and PEM encoded certificates. // Returns nil, if certString is empty string. -func parseCertString(certString string) (*x509.Certificate, error) { +func parseCertificate(certString string) (*x509.Certificate, error) { if certString == "" { return nil, nil } diff --git a/auth/proofOfPossession_test.go b/auth/proofOfPossession_test.go index b676cbb..a8ee028 100644 --- a/auth/proofOfPossession_test.go +++ b/auth/proofOfPossession_test.go @@ -20,19 +20,19 @@ import ( ) func TestProofOfPossession_ParseCertHeader_edgeCases(t *testing.T) { - t.Run("parseCertString() returns nil when no certificate is given", func(t *testing.T) { - cert, err := parseCertString("") + t.Run("parseCertificate() returns nil when no certificate is given", func(t *testing.T) { + cert, err := parseCertificate("") assert.Nil(t, cert) assert.Nil(t, err) }) - t.Run("parseCertString() fails when DER certificate is corrupt", func(t *testing.T) { - _, err := parseCertString("abc123") + t.Run("parseCertificate() fails when DER certificate is corrupt", func(t *testing.T) { + _, err := parseCertificate("abc123") assert.Contains(t, err.Error(), "cannot base64 decode certificate header:") }) - t.Run("parseCertString() fails when PEM certificate is corrupt", func(t *testing.T) { - _, err := parseCertString("LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUQxVENDQXIyZ0F3SUJBZ0lNSUxvRXNuTFFCdQotLS0tLUVORCBDRVJUSUZJQ0FURS0tLS0t") + t.Run("parseCertificate() fails when PEM certificate is corrupt", func(t *testing.T) { + _, err := parseCertificate("LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUQxVENDQXIyZ0F3SUJBZ0lNSUxvRXNuTFFCdQotLS0tLUVORCBDRVJUSUZJQ0FURS0tLS0t") assert.Contains(t, err.Error(), "cannot decode PEM formatted certificate header:") }) } @@ -44,14 +44,14 @@ func TestProofOfPossession_parseAndValidateCertificate_edgeCases(t *testing.T) { }) t.Run("parseAndValidateCertificate() fails when no token is given", func(t *testing.T) { - x509Cert, err := parseCertString(generateCert(t, false)) + x509Cert, err := parseCertificate(generateCert(t, false)) require.NoError(t, err, "Failed to parse cert header: %v", err) err = ValidateX5tThumbprint(x509Cert, nil) assert.Equal(t, "there is no token provided", err.Error()) }) t.Run("parseAndValidateCertificate() fails when cert does not match x5t", func(t *testing.T) { - x509Cert, err := parseCertString(generateCert(t, false)) + x509Cert, err := parseCertificate(generateCert(t, false)) require.NoError(t, err, "Failed to parse cert header: %v", err) err = ValidateX5tThumbprint(x509Cert, createToken(t, "abc")) assert.Equal(t, "token thumbprint confirmation failed", err.Error()) @@ -65,7 +65,7 @@ func TestProofOfPossession_validateX5tThumbprint_edgeCases(t *testing.T) { }) t.Run("ValidateX5tThumbprint() fails when no token is given", func(t *testing.T) { - x509Cert, err := parseCertString(generateCert(t, false)) + x509Cert, err := parseCertificate(generateCert(t, false)) require.NoError(t, err, "Failed to parse cert header: %v", err) err = ValidateX5tThumbprint(x509Cert, nil) assert.Equal(t, "there is no token provided", err.Error()) @@ -121,7 +121,7 @@ func TestProofOfPossession_validateX5tThumbprint(t *testing.T) { } else { cert = readCert(t, tt.certFile, tt.pemEncoded) } - x509cert, err := parseCertString(cert) + x509cert, err := parseCertificate(cert) require.NoError(t, err, "Failed to validate client cert with token cnf thumbprint: %v", err) err = ValidateX5tThumbprint(x509cert, createToken(t, tt.claimCnfMemberX5t)) @@ -151,7 +151,7 @@ func readCert(t *testing.T, fileName string, pemEncoded bool) string { certificate, err := os.ReadFile(certFilePath) require.NoError(t, err, "Failed to read certificate from %v: %v", certFilePath, err) - x509Cert, err := parseCertString(string(certificate)) + x509Cert, err := parseCertificate(string(certificate)) require.NoError(t, err, "failed to create certificate: %v", err) return encodeDERBytes(x509Cert.Raw, pemEncoded) From e2af8cde68da3a386ffe151cf924076ae7dd7d25 Mon Sep 17 00:00:00 2001 From: Nena Raab Date: Tue, 16 Nov 2021 18:21:08 +0100 Subject: [PATCH 13/23] undo middelware.authenticate -> middelware.Authenticate --- auth/middleware.go | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/auth/middleware.go b/auth/middleware.go index 6074046..891bcf3 100644 --- a/auth/middleware.go +++ b/auth/middleware.go @@ -63,7 +63,7 @@ func ClientCertificateFromCtx(r *http.Request) *x509.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. +// 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 options Options @@ -95,9 +95,9 @@ func NewMiddleware(oAuthConfig OAuthConfig, options Options) *Middleware { return m } -// authenticate authenticates a request and returns the Token and the client certificate if validation was successful, +// Authenticate authenticates a request and returns the Token and the client certificate if validation was successful, // otherwise error is returned -func (m *Middleware) authenticate(r *http.Request) (Token, *x509.Certificate, error) { +func (m *Middleware) Authenticate(r *http.Request) (Token, *x509.Certificate, error) { // get Token from Header rawToken, err := extractRawToken(r) if err != nil { @@ -126,14 +126,14 @@ func (m *Middleware) authenticate(r *http.Request) (Token, *x509.Certificate, er } // AuthenticationHandler authenticates a request and injects the claims into -// the request context. If the authentication (see authenticate) does not succeed, +// 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, cert, err := m.authenticate(r) + token, cert, err := m.Authenticate(r) if err != nil { m.options.ErrorHandler(w, r, err) From e9bdda5a2e9cc9c35886470f90f5eb3d5caa3eb8 Mon Sep 17 00:00:00 2001 From: Nena Raab Date: Wed, 17 Nov 2021 18:50:39 +0100 Subject: [PATCH 14/23] felix' comments --- auth/middleware.go | 11 +++++++++-- auth/proofOfPossession.go | 2 +- auth/token.go | 18 +++++++----------- 3 files changed, 17 insertions(+), 14 deletions(-) diff --git a/auth/middleware.go b/auth/middleware.go index 891bcf3..a544f5b 100644 --- a/auth/middleware.go +++ b/auth/middleware.go @@ -95,9 +95,16 @@ func NewMiddleware(oAuthConfig OAuthConfig, options Options) *Middleware { return m } +// 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) Authenticate(r *http.Request) (Token, *x509.Certificate, error) { +func (m *Middleware) AuthenticateWithProofOfPossession(r *http.Request) (Token, *x509.Certificate, error) { // get Token from Header rawToken, err := extractRawToken(r) if err != nil { @@ -133,7 +140,7 @@ func (m *Middleware) Authenticate(r *http.Request) (Token, *x509.Certificate, er // 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, cert, err := m.Authenticate(r) + token, cert, err := m.AuthenticateWithProofOfPossession(r) if err != nil { m.options.ErrorHandler(w, r, err) diff --git a/auth/proofOfPossession.go b/auth/proofOfPossession.go index 9c950cc..ea6b81b 100644 --- a/auth/proofOfPossession.go +++ b/auth/proofOfPossession.go @@ -67,7 +67,7 @@ func parseCertificate(certString string) (*x509.Certificate, error) { return nil, fmt.Errorf("cannot base64 decode certificate header: %w", err) } if bytes.HasPrefix(decoded, []byte(PEMIndicator)) { // in case of apache proxy - pemBlock, _ := pem.Decode(decoded) + pemBlock, err := pem.Decode(decoded) if pemBlock == nil { return nil, fmt.Errorf("cannot decode PEM formatted certificate header: %v", err) } diff --git a/auth/token.go b/auth/token.go index 8d73442..0077044 100644 --- a/auth/token.go +++ b/auth/token.go @@ -193,17 +193,13 @@ func (t stdToken) getJwtToken() jwt.Token { } func (t stdToken) getCnfClaimMember(memberName string) string { - if t.HasClaim(claimCnf) { - cnfClaim, err := t.GetClaimAsMap(claimCnf) - if err != nil { - fmt.Printf("Error getting cnf claim as map: %v", err) - } - if cnfClaim != nil { - res, ok := cnfClaim[memberName] - if ok { - return res.(string) - } - } + cnfClaim, err := t.GetClaimAsMap(claimCnf) + if errors.Is(err, ErrClaimNotExists) || cnfClaim == nil { + return "" + } + res, ok := cnfClaim[memberName] + if ok { + return res.(string) } return "" } From 056f9e64515b7a7b8cdd51f2e2f4e66556ef94eb Mon Sep 17 00:00:00 2001 From: Nena Raab Date: Wed, 17 Nov 2021 18:51:36 +0100 Subject: [PATCH 15/23] apply linter --- auth/proofOfPossession.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/auth/proofOfPossession.go b/auth/proofOfPossession.go index ea6b81b..480cb36 100644 --- a/auth/proofOfPossession.go +++ b/auth/proofOfPossession.go @@ -30,7 +30,7 @@ func parseAndValidateCertificate(clientCertificate *x509.Certificate, token Toke return ValidateX5tThumbprint(clientCertificate, token) } -// ValidateX5tThumbprint compares the thumbprint of the provided X509 client certificate against the cnf claim with the confirmation method "x5t#S256". +// 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 *x509.Certificate, token Token) error { if clientCertificate == nil { From 4120a6e3ec65c4e0fda2643fc5197ca750d65863 Mon Sep 17 00:00:00 2001 From: Nena Raab Date: Thu, 18 Nov 2021 19:05:36 +0100 Subject: [PATCH 16/23] add How to use standalone X5t Validator and fix doc --- README.md | 14 ++++++++++++++ auth/proofOfPossession.go | 2 +- 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 05532a4..4ca46bf 100644 --- a/README.md +++ b/README.md @@ -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) diff --git a/auth/proofOfPossession.go b/auth/proofOfPossession.go index 480cb36..881ae61 100644 --- a/auth/proofOfPossession.go +++ b/auth/proofOfPossession.go @@ -54,7 +54,7 @@ func ValidateX5tThumbprint(clientCertificate *x509.Certificate, token Token) err return nil } -// Parses the X509 client certificate which is provided via the "x-forwarded-client-cert". +// parseCertificate parses the X509 client certificate which is provided via the "x-forwarded-client-cert". // It supports DER encoded and PEM encoded certificates. // Returns nil, if certString is empty string. func parseCertificate(certString string) (*x509.Certificate, error) { From 3d4f7a6e7aefd7e22987155b8519651b5979c8f9 Mon Sep 17 00:00:00 2001 From: Nena Raab Date: Fri, 19 Nov 2021 11:23:32 +0100 Subject: [PATCH 17/23] rename parseAndValidateCertificate -> validateCertificate --- auth/middleware.go | 2 +- auth/proofOfPossession.go | 6 ++++-- auth/proofOfPossession_test.go | 6 +++--- 3 files changed, 8 insertions(+), 6 deletions(-) diff --git a/auth/middleware.go b/auth/middleware.go index a544f5b..4e94a8b 100644 --- a/auth/middleware.go +++ b/auth/middleware.go @@ -123,7 +123,7 @@ func (m *Middleware) AuthenticateWithProofOfPossession(r *http.Request) (Token, return nil, nil, err } if "1" == "" && cert != nil { // TODO integrate proof of possession into middleware - err = parseAndValidateCertificate(cert, token) + err = validateCertificate(cert, token) if err != nil { return nil, nil, err } diff --git a/auth/proofOfPossession.go b/auth/proofOfPossession.go index 881ae61..0a31321 100644 --- a/auth/proofOfPossession.go +++ b/auth/proofOfPossession.go @@ -3,7 +3,7 @@ // SPDX-License-Identifier: Apache-2.0 package auth -// parseAndValidateCertificate checks proof of possession in addition to audience validation +// 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. @@ -20,7 +20,9 @@ import ( var ErrNoClientCert = errors.New("there is no x509 client certificate provided") var ErrNoToken = errors.New("there is no token provided") -func parseAndValidateCertificate(clientCertificate *x509.Certificate, token Token) error { +// validateCertificate runs all proof of possession checks. +// This ensures that the token was issued for the sender. +func validateCertificate(clientCertificate *x509.Certificate, token Token) error { if clientCertificate == nil { return ErrNoClientCert } diff --git a/auth/proofOfPossession_test.go b/auth/proofOfPossession_test.go index a8ee028..8580567 100644 --- a/auth/proofOfPossession_test.go +++ b/auth/proofOfPossession_test.go @@ -38,19 +38,19 @@ func TestProofOfPossession_ParseCertHeader_edgeCases(t *testing.T) { } func TestProofOfPossession_parseAndValidateCertificate_edgeCases(t *testing.T) { - t.Run("parseAndValidateCertificate() fails when no cert is given", func(t *testing.T) { + t.Run("validateCertificate() fails when no cert is given", func(t *testing.T) { err := ValidateX5tThumbprint(nil, createToken(t, "abc")) assert.Equal(t, "there is no x509 client certificate provided", err.Error()) }) - t.Run("parseAndValidateCertificate() fails when no token is given", func(t *testing.T) { + t.Run("validateCertificate() fails when no token is given", func(t *testing.T) { x509Cert, err := parseCertificate(generateCert(t, false)) require.NoError(t, err, "Failed to parse cert header: %v", err) err = ValidateX5tThumbprint(x509Cert, nil) assert.Equal(t, "there is no token provided", err.Error()) }) - t.Run("parseAndValidateCertificate() fails when cert does not match x5t", func(t *testing.T) { + t.Run("validateCertificate() fails when cert does not match x5t", func(t *testing.T) { x509Cert, err := parseCertificate(generateCert(t, false)) require.NoError(t, err, "Failed to parse cert header: %v", err) err = ValidateX5tThumbprint(x509Cert, createToken(t, "abc")) From 69d49ab170d1438d97017d3ca474e260bcd71795 Mon Sep 17 00:00:00 2001 From: Nena Raab Date: Fri, 19 Nov 2021 15:37:47 +0100 Subject: [PATCH 18/23] introduce Certificate instance --- auth/certificate.go | 61 ++++++++++++++++++++++++++++++++++++++++ auth/certificate_test.go | 39 +++++++++++++++++++++++++ 2 files changed, 100 insertions(+) create mode 100644 auth/certificate.go create mode 100644 auth/certificate_test.go diff --git a/auth/certificate.go b/auth/certificate.go new file mode 100644 index 0000000..a4462e2 --- /dev/null +++ b/auth/certificate.go @@ -0,0 +1,61 @@ +// 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" +) + +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 +} + +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 +} diff --git a/auth/certificate_test.go b/auth/certificate_test.go new file mode 100644 index 0000000..5283179 --- /dev/null +++ b/auth/certificate_test.go @@ -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()) + }) +} From 8a86b2fc824c3480d5318880b4f6d9c0f25d83c7 Mon Sep 17 00:00:00 2001 From: Nena Raab Date: Mon, 22 Nov 2021 09:36:20 +0100 Subject: [PATCH 19/23] refactor project to use Certificate instead of x509.Certificate --- auth/middleware.go | 11 +++++------ auth/middleware_test.go | 2 +- auth/proofOfPossession.go | 35 +++------------------------------- auth/proofOfPossession_test.go | 26 ++++--------------------- 4 files changed, 13 insertions(+), 61 deletions(-) diff --git a/auth/middleware.go b/auth/middleware.go index 4e94a8b..47a9864 100644 --- a/auth/middleware.go +++ b/auth/middleware.go @@ -6,7 +6,6 @@ package auth import ( "context" - "crypto/x509" "log" "net/http" "time" @@ -58,8 +57,8 @@ func TokenFromCtx(r *http.Request) Token { // ClientCertificateFromCtx retrieves the X.509 client certificate of a request which // have been injected before via the auth middleware -func ClientCertificateFromCtx(r *http.Request) *x509.Certificate { - return r.Context().Value(ClientCertificateCtxKey).(*x509.Certificate) +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. @@ -104,7 +103,7 @@ func (m *Middleware) Authenticate(r *http.Request) (Token, error) { // 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, *x509.Certificate, error) { +func (m *Middleware) AuthenticateWithProofOfPossession(r *http.Request) (Token, *Certificate, error) { // get Token from Header rawToken, err := extractRawToken(r) if err != nil { @@ -117,8 +116,8 @@ func (m *Middleware) AuthenticateWithProofOfPossession(r *http.Request) (Token, } const forwardedClientCertHeader = "x-forwarded-client-cert" - var cert *x509.Certificate - cert, err = parseCertificate(r.Header.Get(forwardedClientCertHeader)) + var cert *Certificate + cert, err = newCertificate(r.Header.Get(forwardedClientCertHeader)) if err != nil { return nil, nil, err } diff --git a/auth/middleware_test.go b/auth/middleware_test.go index 91acb55..1148915 100644 --- a/auth/middleware_test.go +++ b/auth/middleware_test.go @@ -274,7 +274,7 @@ 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.Raw))) + _, _ = rw.Write([]byte("entered test handler using cert: " + string(cert.x509Cert.Raw))) } _, _ = rw.Write([]byte("entered test handler")) } diff --git a/auth/proofOfPossession.go b/auth/proofOfPossession.go index 0a31321..5daa859 100644 --- a/auth/proofOfPossession.go +++ b/auth/proofOfPossession.go @@ -8,11 +8,8 @@ package auth // 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 ( - "bytes" "crypto/sha256" - "crypto/x509" "encoding/base64" - "encoding/pem" "errors" "fmt" ) @@ -22,7 +19,7 @@ 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 *x509.Certificate, token Token) error { +func validateCertificate(clientCertificate *Certificate, token Token) error { if clientCertificate == nil { return ErrNoClientCert } @@ -34,7 +31,7 @@ func validateCertificate(clientCertificate *x509.Certificate, token Token) error // 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 *x509.Certificate, token Token) error { +func ValidateX5tThumbprint(clientCertificate *Certificate, token Token) error { if clientCertificate == nil { return ErrNoClientCert } @@ -47,7 +44,7 @@ func ValidateX5tThumbprint(clientCertificate *x509.Certificate, token Token) err return fmt.Errorf("token provides no cnf member for thumbprint confirmation") } - certThumbprintBytes := sha256.Sum256(clientCertificate.Raw) + certThumbprintBytes := sha256.Sum256(clientCertificate.x509Cert.Raw) certThumbprint := base64.URLEncoding.WithPadding(base64.NoPadding).EncodeToString(certThumbprintBytes[:]) if cnfThumbprint != certThumbprint { @@ -55,29 +52,3 @@ func ValidateX5tThumbprint(clientCertificate *x509.Certificate, token Token) err } return nil } - -// parseCertificate parses the X509 client certificate which is provided via the "x-forwarded-client-cert". -// It supports DER encoded and PEM encoded certificates. -// Returns nil, if certString is empty string. -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 -} diff --git a/auth/proofOfPossession_test.go b/auth/proofOfPossession_test.go index 8580567..f22a249 100644 --- a/auth/proofOfPossession_test.go +++ b/auth/proofOfPossession_test.go @@ -19,24 +19,6 @@ import ( "testing" ) -func TestProofOfPossession_ParseCertHeader_edgeCases(t *testing.T) { - t.Run("parseCertificate() returns nil when no certificate is given", func(t *testing.T) { - cert, err := parseCertificate("") - assert.Nil(t, cert) - assert.Nil(t, err) - }) - - t.Run("parseCertificate() fails when DER certificate is corrupt", func(t *testing.T) { - _, err := parseCertificate("abc123") - assert.Contains(t, err.Error(), "cannot base64 decode certificate header:") - }) - - t.Run("parseCertificate() fails when PEM certificate is corrupt", func(t *testing.T) { - _, err := parseCertificate("LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUQxVENDQXIyZ0F3SUJBZ0lNSUxvRXNuTFFCdQotLS0tLUVORCBDRVJUSUZJQ0FURS0tLS0t") - assert.Contains(t, err.Error(), "cannot decode PEM formatted certificate header:") - }) -} - func TestProofOfPossession_parseAndValidateCertificate_edgeCases(t *testing.T) { t.Run("validateCertificate() fails when no cert is given", func(t *testing.T) { err := ValidateX5tThumbprint(nil, createToken(t, "abc")) @@ -44,14 +26,14 @@ func TestProofOfPossession_parseAndValidateCertificate_edgeCases(t *testing.T) { }) t.Run("validateCertificate() fails when no token is given", func(t *testing.T) { - x509Cert, err := parseCertificate(generateCert(t, false)) + x509Cert, err := newCertificate(generateCert(t, false)) require.NoError(t, err, "Failed to parse cert header: %v", err) err = ValidateX5tThumbprint(x509Cert, nil) assert.Equal(t, "there is no token provided", err.Error()) }) t.Run("validateCertificate() fails when cert does not match x5t", func(t *testing.T) { - x509Cert, err := parseCertificate(generateCert(t, false)) + x509Cert, err := newCertificate(generateCert(t, false)) require.NoError(t, err, "Failed to parse cert header: %v", err) err = ValidateX5tThumbprint(x509Cert, createToken(t, "abc")) assert.Equal(t, "token thumbprint confirmation failed", err.Error()) @@ -65,7 +47,7 @@ func TestProofOfPossession_validateX5tThumbprint_edgeCases(t *testing.T) { }) t.Run("ValidateX5tThumbprint() fails when no token is given", func(t *testing.T) { - x509Cert, err := parseCertificate(generateCert(t, false)) + x509Cert, err := newCertificate(generateCert(t, false)) require.NoError(t, err, "Failed to parse cert header: %v", err) err = ValidateX5tThumbprint(x509Cert, nil) assert.Equal(t, "there is no token provided", err.Error()) @@ -121,7 +103,7 @@ func TestProofOfPossession_validateX5tThumbprint(t *testing.T) { } else { cert = readCert(t, tt.certFile, tt.pemEncoded) } - x509cert, err := parseCertificate(cert) + x509cert, err := newCertificate(cert) require.NoError(t, err, "Failed to validate client cert with token cnf thumbprint: %v", err) err = ValidateX5tThumbprint(x509cert, createToken(t, tt.claimCnfMemberX5t)) From a94d882d8589fa48c4a65a26ad5bc07922c3cac5 Mon Sep 17 00:00:00 2001 From: Nena Raab Date: Mon, 22 Nov 2021 09:52:16 +0100 Subject: [PATCH 20/23] cleanup and doc --- auth/certificate.go | 4 +++- auth/certificate_test.go | 8 ++++---- auth/proofOfPossession_test.go | 4 ++-- 3 files changed, 9 insertions(+), 7 deletions(-) diff --git a/auth/certificate.go b/auth/certificate.go index a4462e2..ea857e3 100644 --- a/auth/certificate.go +++ b/auth/certificate.go @@ -12,6 +12,7 @@ import ( "fmt" ) +// Certificate is the public API to access claims of the X509 client certificate. type Certificate struct { x509Cert *x509.Certificate } @@ -30,7 +31,8 @@ func newCertificate(certString string) (*Certificate, error) { return nil, err } -func (c *Certificate) getThumbprint() string { +// 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[:]) diff --git a/auth/certificate_test.go b/auth/certificate_test.go index 5283179..89cb128 100644 --- a/auth/certificate_test.go +++ b/auth/certificate_test.go @@ -27,13 +27,13 @@ func TestCertificate(t *testing.T) { assert.Contains(t, err.Error(), "cannot decode PEM formatted certificate header:") }) - t.Run("getThumbprint() for PEM formatted cert", func(t *testing.T) { + 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()) + assert.Equal(t, "fU-XoQlhMTpQsz9ArXl6zHIpMGuRO4ExLKdLRTc5VjM", cert.GetThumbprint()) }) - t.Run("getThumbprint() for DER formatted cert", func(t *testing.T) { + 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()) + assert.Equal(t, "fU-XoQlhMTpQsz9ArXl6zHIpMGuRO4ExLKdLRTc5VjM", cert.GetThumbprint()) }) } diff --git a/auth/proofOfPossession_test.go b/auth/proofOfPossession_test.go index f22a249..5fbd9a8 100644 --- a/auth/proofOfPossession_test.go +++ b/auth/proofOfPossession_test.go @@ -133,10 +133,10 @@ func readCert(t *testing.T, fileName string, pemEncoded bool) string { certificate, err := os.ReadFile(certFilePath) require.NoError(t, err, "Failed to read certificate from %v: %v", certFilePath, err) - x509Cert, err := parseCertificate(string(certificate)) + x509Cert, err := newCertificate(string(certificate)) require.NoError(t, err, "failed to create certificate: %v", err) - return encodeDERBytes(x509Cert.Raw, pemEncoded) + return encodeDERBytes(x509Cert.x509Cert.Raw, pemEncoded) } func generateCert(t *testing.T, pemEncoded bool) string { From f2a832772a91508c68e6f54bc3c5fbac2c32b716 Mon Sep 17 00:00:00 2001 From: Nena Raab Date: Wed, 24 Nov 2021 15:15:23 +0100 Subject: [PATCH 21/23] refactor and optimize tests --- auth/certificate_test.go | 8 +++- auth/proofOfPossession_test.go | 71 ++++++++++++++-------------------- 2 files changed, 34 insertions(+), 45 deletions(-) diff --git a/auth/certificate_test.go b/auth/certificate_test.go index 89cb128..8aedef5 100644 --- a/auth/certificate_test.go +++ b/auth/certificate_test.go @@ -4,10 +4,14 @@ package auth import ( + _ "embed" "github.com/stretchr/testify/assert" "testing" ) +//go:embed testdata/x-forwarded-client-cert.txt +var derCertFromFile string + func TestCertificate(t *testing.T) { t.Run("newCertificate() returns nil when no certificate is given", func(t *testing.T) { cert, err := newCertificate("") @@ -28,12 +32,12 @@ func TestCertificate(t *testing.T) { }) t.Run("GetThumbprint() for PEM formatted cert", func(t *testing.T) { - cert, _ := newCertificate(readCert(t, "x-forwarded-client-cert.txt", true)) + cert, _ := newCertificate(convertToPEM(t, derCertFromFile)) 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)) + cert, _ := newCertificate(derCertFromFile) assert.Equal(t, "fU-XoQlhMTpQsz9ArXl6zHIpMGuRO4ExLKdLRTc5VjM", cert.GetThumbprint()) }) } diff --git a/auth/proofOfPossession_test.go b/auth/proofOfPossession_test.go index 5fbd9a8..4a6a33d 100644 --- a/auth/proofOfPossession_test.go +++ b/auth/proofOfPossession_test.go @@ -14,40 +14,40 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "math/big" - "os" - "path" "testing" ) +var derCertGenerated = generateDERCert() + func TestProofOfPossession_parseAndValidateCertificate_edgeCases(t *testing.T) { t.Run("validateCertificate() fails when no cert is given", func(t *testing.T) { - err := ValidateX5tThumbprint(nil, createToken(t, "abc")) + err := ValidateX5tThumbprint(nil, generateToken(t, "abc")) assert.Equal(t, "there is no x509 client certificate provided", err.Error()) }) t.Run("validateCertificate() fails when no token is given", func(t *testing.T) { - x509Cert, err := newCertificate(generateCert(t, false)) + x509Cert, err := newCertificate(derCertGenerated) require.NoError(t, err, "Failed to parse cert header: %v", err) err = ValidateX5tThumbprint(x509Cert, nil) assert.Equal(t, "there is no token provided", err.Error()) }) t.Run("validateCertificate() fails when cert does not match x5t", func(t *testing.T) { - x509Cert, err := newCertificate(generateCert(t, false)) + x509Cert, err := newCertificate(derCertGenerated) require.NoError(t, err, "Failed to parse cert header: %v", err) - err = ValidateX5tThumbprint(x509Cert, createToken(t, "abc")) + err = ValidateX5tThumbprint(x509Cert, generateToken(t, "abc")) assert.Equal(t, "token thumbprint confirmation failed", err.Error()) }) } func TestProofOfPossession_validateX5tThumbprint_edgeCases(t *testing.T) { t.Run("ValidateX5tThumbprint() fails when no cert is given", func(t *testing.T) { - err := ValidateX5tThumbprint(nil, createToken(t, "abc")) + err := ValidateX5tThumbprint(nil, generateToken(t, "abc")) assert.Equal(t, "there is no x509 client certificate provided", err.Error()) }) t.Run("ValidateX5tThumbprint() fails when no token is given", func(t *testing.T) { - x509Cert, err := newCertificate(generateCert(t, false)) + x509Cert, err := newCertificate(derCertGenerated) require.NoError(t, err, "Failed to parse cert header: %v", err) err = ValidateX5tThumbprint(x509Cert, nil) assert.Equal(t, "there is no token provided", err.Error()) @@ -58,38 +58,38 @@ func TestProofOfPossession_validateX5tThumbprint(t *testing.T) { tests := []struct { name string claimCnfMemberX5t string - certFile string // in case of empty string it gets generated + cert string pemEncoded bool expectedErrMsg string // in case of empty string no error is expected }{ { - name: "x5t should match with DER certificate (go router)", + name: "x5t should match with DER certificate (HAProxy)", claimCnfMemberX5t: "fU-XoQlhMTpQsz9ArXl6zHIpMGuRO4ExLKdLRTc5VjM", - certFile: "x-forwarded-client-cert.txt", + cert: derCertFromFile, pemEncoded: false, expectedErrMsg: "", }, { name: "x5t should match with PEM certificate (apache proxy)", claimCnfMemberX5t: "fU-XoQlhMTpQsz9ArXl6zHIpMGuRO4ExLKdLRTc5VjM", - certFile: "x-forwarded-client-cert.txt", + cert: derCertFromFile, pemEncoded: true, expectedErrMsg: "", }, { - name: "expect error when x5t does not match with generated DER certificate (go router)", + name: "expect error when x5t does not match with generated DER certificate (HAProxy)", claimCnfMemberX5t: "fU-XoQlhMTpQsz9ArXl6zHIpMGuRO4ExLKdLRTc5VjM", - certFile: "", + cert: derCertGenerated, pemEncoded: false, expectedErrMsg: "token thumbprint confirmation failed", }, { name: "expect error when x5t does not match with generated PEM certificate (apache proxy)", claimCnfMemberX5t: "fU-XoQlhMTpQsz9ArXl6zHIpMGuRO4ExLKdLRTc5VjM", - certFile: "", + cert: derCertGenerated, pemEncoded: true, expectedErrMsg: "token thumbprint confirmation failed", }, { name: "expect error when x5t is empty", claimCnfMemberX5t: "", - certFile: "", + cert: derCertGenerated, pemEncoded: false, expectedErrMsg: "token provides no cnf member for thumbprint confirmation", }, @@ -97,16 +97,14 @@ func TestProofOfPossession_validateX5tThumbprint(t *testing.T) { for _, tt := range tests { tt := tt t.Run(tt.name, func(t *testing.T) { - var cert string - if tt.certFile == "" { - cert = generateCert(t, tt.pemEncoded) - } else { - cert = readCert(t, tt.certFile, tt.pemEncoded) + cert := tt.cert + if tt.pemEncoded == true { + cert = convertToPEM(t, tt.cert) } x509cert, err := newCertificate(cert) require.NoError(t, err, "Failed to validate client cert with token cnf thumbprint: %v", err) - err = ValidateX5tThumbprint(x509cert, createToken(t, tt.claimCnfMemberX5t)) + err = ValidateX5tThumbprint(x509cert, generateToken(t, tt.claimCnfMemberX5t)) if tt.expectedErrMsg != "" { assert.Equal(t, tt.expectedErrMsg, err.Error()) } else { @@ -116,7 +114,7 @@ func TestProofOfPossession_validateX5tThumbprint(t *testing.T) { } } -func createToken(t *testing.T, claimCnfMemberX5tValue string) Token { +func generateToken(t *testing.T, claimCnfMemberX5tValue string) Token { token := jwt.New() cnfClaim := map[string]interface{}{ claimCnfMemberX5t: claimCnfMemberX5tValue, @@ -127,21 +125,16 @@ func createToken(t *testing.T, claimCnfMemberX5tValue string) Token { return stdToken{jwtToken: token} } -func readCert(t *testing.T, fileName string, pemEncoded bool) string { - pwd, _ := os.Getwd() - certFilePath := path.Join(pwd, "testdata", fileName) - certificate, err := os.ReadFile(certFilePath) - require.NoError(t, err, "Failed to read certificate from %v: %v", certFilePath, err) - - x509Cert, err := newCertificate(string(certificate)) +func convertToPEM(t *testing.T, derCert string) string { + x509Cert, err := newCertificate(derCert) require.NoError(t, err, "failed to create certificate: %v", err) - return encodeDERBytes(x509Cert.x509Cert.Raw, pemEncoded) + bytes := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: x509Cert.x509Cert.Raw}) + return base64.StdEncoding.EncodeToString(bytes) } -func generateCert(t *testing.T, pemEncoded bool) string { - key, err := rsa.GenerateKey(rand.Reader, 2048) - require.NoError(t, err, "") +func generateDERCert() string { + key, _ := rsa.GenerateKey(rand.Reader, 512) //nolint:gosec issuerName := pkix.Name{ Organization: []string{"my-issuer-org"}, @@ -158,15 +151,7 @@ func generateCert(t *testing.T, pemEncoded bool) string { Subject: issuerName, Issuer: issuerName, } - derBytes, err := x509.CreateCertificate(rand.Reader, &template, &issTemplate, &key.PublicKey, key) - require.NoError(t, err, "failed to generate certificate: %v", err) + derBytes, _ := x509.CreateCertificate(rand.Reader, &template, &issTemplate, &key.PublicKey, key) - return encodeDERBytes(derBytes, pemEncoded) -} - -func encodeDERBytes(derBytes []byte, pemEncoded bool) string { - if pemEncoded { - derBytes = pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: derBytes}) - } return base64.StdEncoding.EncodeToString(derBytes) } From 0c14dce9d95b119457b31da9229494adc67f6e7f Mon Sep 17 00:00:00 2001 From: Nena Raab Date: Wed, 24 Nov 2021 15:20:46 +0100 Subject: [PATCH 22/23] remove duplicate code --- auth/proofOfPossession.go | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/auth/proofOfPossession.go b/auth/proofOfPossession.go index 5daa859..8ae0a1c 100644 --- a/auth/proofOfPossession.go +++ b/auth/proofOfPossession.go @@ -8,8 +8,6 @@ package auth // 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" ) @@ -44,10 +42,7 @@ func ValidateX5tThumbprint(clientCertificate *Certificate, token Token) error { 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 { + if cnfThumbprint != clientCertificate.GetThumbprint() { return fmt.Errorf("token thumbprint confirmation failed") } return nil From 4d682041584bbc62721f2559f5ee55fde83bc951 Mon Sep 17 00:00:00 2001 From: Nena Raab Date: Wed, 24 Nov 2021 15:36:36 +0100 Subject: [PATCH 23/23] undo option to use Certificate Thumbprint (x5t) validator standalone --- README.md | 14 -------------- auth/proofOfPossession.go | 6 +++--- auth/proofOfPossession_test.go | 16 ++++++++-------- 3 files changed, 11 insertions(+), 25 deletions(-) diff --git a/README.md b/README.md index 4ca46bf..05532a4 100644 --- a/README.md +++ b/README.md @@ -52,20 +52,6 @@ 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) diff --git a/auth/proofOfPossession.go b/auth/proofOfPossession.go index 8ae0a1c..40c8194 100644 --- a/auth/proofOfPossession.go +++ b/auth/proofOfPossession.go @@ -24,12 +24,12 @@ func validateCertificate(clientCertificate *Certificate, token Token) error { if token == nil { return ErrNoToken } - return ValidateX5tThumbprint(clientCertificate, token) + return validateX5tThumbprint(clientCertificate, token) } -// ValidateX5tThumbprint compares the thumbprint of the provided X509 client certificate against the cnf claim with the confirmation method "x5t#S256". +// 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 { +func validateX5tThumbprint(clientCertificate *Certificate, token Token) error { if clientCertificate == nil { return ErrNoClientCert } diff --git a/auth/proofOfPossession_test.go b/auth/proofOfPossession_test.go index 4a6a33d..1a10aec 100644 --- a/auth/proofOfPossession_test.go +++ b/auth/proofOfPossession_test.go @@ -21,35 +21,35 @@ var derCertGenerated = generateDERCert() func TestProofOfPossession_parseAndValidateCertificate_edgeCases(t *testing.T) { t.Run("validateCertificate() fails when no cert is given", func(t *testing.T) { - err := ValidateX5tThumbprint(nil, generateToken(t, "abc")) + err := validateX5tThumbprint(nil, generateToken(t, "abc")) assert.Equal(t, "there is no x509 client certificate provided", err.Error()) }) t.Run("validateCertificate() fails when no token is given", func(t *testing.T) { x509Cert, err := newCertificate(derCertGenerated) require.NoError(t, err, "Failed to parse cert header: %v", err) - err = ValidateX5tThumbprint(x509Cert, nil) + err = validateX5tThumbprint(x509Cert, nil) assert.Equal(t, "there is no token provided", err.Error()) }) t.Run("validateCertificate() fails when cert does not match x5t", func(t *testing.T) { x509Cert, err := newCertificate(derCertGenerated) require.NoError(t, err, "Failed to parse cert header: %v", err) - err = ValidateX5tThumbprint(x509Cert, generateToken(t, "abc")) + err = validateX5tThumbprint(x509Cert, generateToken(t, "abc")) assert.Equal(t, "token thumbprint confirmation failed", err.Error()) }) } func TestProofOfPossession_validateX5tThumbprint_edgeCases(t *testing.T) { - t.Run("ValidateX5tThumbprint() fails when no cert is given", func(t *testing.T) { - err := ValidateX5tThumbprint(nil, generateToken(t, "abc")) + t.Run("validateX5tThumbprint() fails when no cert is given", func(t *testing.T) { + err := validateX5tThumbprint(nil, generateToken(t, "abc")) assert.Equal(t, "there is no x509 client certificate provided", err.Error()) }) - t.Run("ValidateX5tThumbprint() fails when no token is given", func(t *testing.T) { + t.Run("validateX5tThumbprint() fails when no token is given", func(t *testing.T) { x509Cert, err := newCertificate(derCertGenerated) require.NoError(t, err, "Failed to parse cert header: %v", err) - err = ValidateX5tThumbprint(x509Cert, nil) + err = validateX5tThumbprint(x509Cert, nil) assert.Equal(t, "there is no token provided", err.Error()) }) } @@ -104,7 +104,7 @@ func TestProofOfPossession_validateX5tThumbprint(t *testing.T) { x509cert, err := newCertificate(cert) require.NoError(t, err, "Failed to validate client cert with token cnf thumbprint: %v", err) - err = ValidateX5tThumbprint(x509cert, generateToken(t, tt.claimCnfMemberX5t)) + err = validateX5tThumbprint(x509cert, generateToken(t, tt.claimCnfMemberX5t)) if tt.expectedErrMsg != "" { assert.Equal(t, tt.expectedErrMsg, err.Error()) } else {