From 98a2f2db763ee95a85ea235fe12e8a8d18af176f Mon Sep 17 00:00:00 2001 From: Nick Stott Date: Sat, 26 Oct 2024 11:56:39 -0400 Subject: [PATCH] chore: check for invalid 'typ' headers --- v2/svid/jwtsvid/svid.go | 10 +++++ v2/svid/jwtsvid/svid_test.go | 74 +++++++++++++++++++++++++++--------- 2 files changed, 65 insertions(+), 19 deletions(-) diff --git a/v2/svid/jwtsvid/svid.go b/v2/svid/jwtsvid/svid.go index d46f800..23430af 100644 --- a/v2/svid/jwtsvid/svid.go +++ b/v2/svid/jwtsvid/svid.go @@ -57,6 +57,11 @@ func ParseAndValidate(token string, bundles jwtbundle.Source, audience []string) return nil, jwtsvidErr.New("token header missing key id") } + // forbid tokens which have the `typ` header, which is not either "JOSE" or "JWT" + if typ := tok.Headers[0].ExtraHeaders[jose.HeaderType]; typ != "JOSE" && typ != "JWT" { + return nil, jwtsvidErr.New("token header type not equal to either JWT or JOSE") + } + // Get JWT Bundle bundle, err := bundles.GetJWTBundleForTrustDomain(trustDomain) if err != nil { @@ -83,6 +88,11 @@ func ParseAndValidate(token string, bundles jwtbundle.Source, audience []string) // JWT-SVID. The JWT-SVID signature is not verified. func ParseInsecure(token string, audience []string) (*SVID, error) { return parse(token, audience, func(tok *jwt.JSONWebToken, td spiffeid.TrustDomain) (map[string]interface{}, error) { + // forbid tokens which have the `typ` header, which is not either "JOSE" or "JWT" + if typ := tok.Headers[0].ExtraHeaders[jose.HeaderType]; typ != "JOSE" && typ != "JWT" { + return nil, jwtsvidErr.New("token header type not equal to either JWT or JOSE") + } + // Obtain the token claims insecurely, i.e. without signature verification claimsMap := make(map[string]interface{}) if err := tok.UnsafeClaimsWithoutVerification(&claimsMap); err != nil { diff --git a/v2/svid/jwtsvid/svid_test.go b/v2/svid/jwtsvid/svid_test.go index 78c23e6..c58dcd5 100644 --- a/v2/svid/jwtsvid/svid_test.go +++ b/v2/svid/jwtsvid/svid_test.go @@ -75,7 +75,7 @@ func TestParseAndValidate(t *testing.T) { IssuedAt: issuedAt, } - return generateToken(tb, claims, key1, "authority1") + return generateToken(tb, claims, key1, "authority1", "") }, svid: &jwtsvid.SVID{ ID: spiffeid.RequireFromPath(trustDomain1, "/host"), @@ -110,7 +110,7 @@ func TestParseAndValidate(t *testing.T) { IssuedAt: issuedAt, } - return generateToken(tb, claims, key1, "authority1") + return generateToken(tb, claims, key1, "authority1", "") }, err: "jwtsvid: token missing subject claim", }, @@ -125,7 +125,7 @@ func TestParseAndValidate(t *testing.T) { IssuedAt: issuedAt, } - return generateToken(tb, claims, key1, "authority1") + return generateToken(tb, claims, key1, "authority1", "") }, err: "jwtsvid: token missing exp claim", }, @@ -142,7 +142,7 @@ func TestParseAndValidate(t *testing.T) { IssuedAt: issuedAt, } - return generateToken(tb, claims, key1, "authority1") + return generateToken(tb, claims, key1, "authority1", "") }, err: "jwtsvid: token has expired", }, @@ -159,7 +159,7 @@ func TestParseAndValidate(t *testing.T) { IssuedAt: issuedAt, } - return generateToken(tb, claims, key1, "authority1") + return generateToken(tb, claims, key1, "authority1", "") }, err: `jwtsvid: expected audience in ["another"] (audience=["audience"])`, }, @@ -176,7 +176,7 @@ func TestParseAndValidate(t *testing.T) { IssuedAt: issuedAt, } - return generateToken(tb, claims, key1, "authority1") + return generateToken(tb, claims, key1, "authority1", "") }, err: `jwtsvid: token has an invalid subject claim: scheme is missing or invalid`, }, @@ -193,7 +193,7 @@ func TestParseAndValidate(t *testing.T) { IssuedAt: issuedAt, } - return generateToken(tb, claims, key1, "") + return generateToken(tb, claims, key1, "", "") }, err: "jwtsvid: token header missing key id", }, @@ -210,7 +210,7 @@ func TestParseAndValidate(t *testing.T) { IssuedAt: issuedAt, } - return generateToken(tb, claims, key1, "noAuthority") + return generateToken(tb, claims, key1, "noAuthority", "") }, err: `jwtsvid: no bundle found for trust domain "another.domain"`, }, @@ -227,7 +227,7 @@ func TestParseAndValidate(t *testing.T) { IssuedAt: issuedAt, } - return generateToken(tb, claims, key1, "noKey") + return generateToken(tb, claims, key1, "noKey", "") }, err: `jwtsvid: no JWT authority "noKey" found for trust domain "trustdomain"`, }, @@ -244,10 +244,26 @@ func TestParseAndValidate(t *testing.T) { IssuedAt: issuedAt, } - return generateToken(tb, claims, key2, "authority1") + return generateToken(tb, claims, key2, "authority1", "") }, err: "jwtsvid: unable to get claims from token: go-jose/go-jose: error in cryptographic primitive", }, + { + name: "invalid typ", + bundle: bundle1, + generateToken: func(tb testing.TB) string { + claims := jwt.Claims{ + Subject: spiffeid.RequireFromPath(trustDomain1, "/host").String(), + Issuer: "issuer", + Expiry: expires, + Audience: []string{"audience"}, + IssuedAt: issuedAt, + } + + return generateToken(tb, claims, key1, "authority1", "invalid") + }, + err: "jwtsvid: token header type not equal to either JWT or JOSE", + }, } for _, testCase := range testCases { @@ -304,7 +320,7 @@ func TestParseInsecure(t *testing.T) { IssuedAt: issuedAt, } - return generateToken(tb, claims, key1, "key1") + return generateToken(tb, claims, key1, "key1", "") }, svid: &jwtsvid.SVID{ ID: spiffeid.RequireFromPath(trustDomain1, "/host"), @@ -336,7 +352,7 @@ func TestParseInsecure(t *testing.T) { IssuedAt: issuedAt, } - return generateToken(tb, claims, key1, "key1") + return generateToken(tb, claims, key1, "key1", "") }, err: "jwtsvid: token missing subject claim", }, @@ -350,7 +366,7 @@ func TestParseInsecure(t *testing.T) { IssuedAt: issuedAt, } - return generateToken(tb, claims, key1, "key1") + return generateToken(tb, claims, key1, "key1", "") }, err: "jwtsvid: token missing exp claim", }, @@ -366,7 +382,7 @@ func TestParseInsecure(t *testing.T) { IssuedAt: issuedAt, } - return generateToken(tb, claims, key1, "key1") + return generateToken(tb, claims, key1, "key1", "") }, err: "jwtsvid: token has expired", }, @@ -382,7 +398,7 @@ func TestParseInsecure(t *testing.T) { IssuedAt: issuedAt, } - return generateToken(tb, claims, key1, "key1") + return generateToken(tb, claims, key1, "key1", "") }, err: `jwtsvid: expected audience in ["another"] (audience=["audience"])`, }, @@ -398,10 +414,25 @@ func TestParseInsecure(t *testing.T) { IssuedAt: issuedAt, } - return generateToken(tb, claims, key1, "key1") + return generateToken(tb, claims, key1, "key1", "") }, err: `jwtsvid: token has an invalid subject claim: scheme is missing or invalid`, }, + { + name: "success", + generateToken: func(tb testing.TB) string { + claims := jwt.Claims{ + Subject: spiffeid.RequireFromPath(trustDomain1, "/host").String(), + Issuer: "issuer", + Expiry: expires, + Audience: []string{"audience"}, + IssuedAt: issuedAt, + } + + return generateToken(tb, claims, key1, "key1", "invalid") + }, + err: `jwtsvid: token header type not equal to either JWT or JOSE`, + }, } for _, testCase := range testCases { @@ -443,7 +474,7 @@ func TestMarshal(t *testing.T) { Audience: []string{"audience"}, IssuedAt: jwt.NewNumericDate(time.Now().Add(time.Minute)), } - token := generateToken(t, claims, key1, "key1") + token := generateToken(t, claims, key1, "key1", "") // Create SVID svid, err := jwtsvid.ParseInsecure(token, []string{"audience"}) @@ -470,11 +501,16 @@ func parseToken(t testing.TB, token string) map[string]interface{} { } // Generate generates a signed string token -func generateToken(tb testing.TB, claims jwt.Claims, signer crypto.Signer, keyID string) string { +func generateToken(tb testing.TB, claims jwt.Claims, signer crypto.Signer, keyID string, typ string) string { // Get signer algorithm alg, err := getSignerAlgorithm(signer) require.NoError(tb, err) + options := new(jose.SignerOptions).WithType("JWT") + if typ != "" { + options = options.WithHeader(jose.HeaderType, typ) + } + // Create signer using crypto.Signer and its algorithm along with provided key ID jwtSigner, err := jose.NewSigner( jose.SigningKey{ @@ -484,7 +520,7 @@ func generateToken(tb testing.TB, claims jwt.Claims, signer crypto.Signer, keyID KeyID: keyID, }, }, - new(jose.SignerOptions).WithType("JWT"), + options, ) require.NoError(tb, err)