Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

JwtSecurityTokenHandler says signature of JWT valid after changing 1 char

We are trying to validate the ID Token (IDT) presented to a .NET client application by an OpenID Connect Provider (OP). The IDT is what you would expect. Nothing unusual going on there.

To verify the signature of the IDT, we can get the exponent and modulus from the OP by calling a public endpoint. These can be used to create a public key that corresponds to the private one used by the OP to sign the IDT. With these, we create a RSACryptoServiceProvider object to do the signature verification. To help with this, we are are passing the crypto service provider as a token validation parameter to a JwtSecurityTokenHandler.

This works fine. We thought we were done and ready for the weekend. However, we found that we can change the last character in the signature and the JwtSecurityTokenHandler will still tell us the JWT is valid. We cannot find an explanation for this and are wondering if:

  1. It is a problem with the way we are creating the signing key which is causing it to improperly validate JWTs.
  2. There is a bug in the JwtSecurityTokenHandler.
  3. We are not understanding the spec completely and this small alteration is allowed because the last character in the signature part of the JWT is actually not pertinent for verification.
  4. Something else

We are using System.IdentityModel.Tokens.JwtSecurityTokenHandler from System.IdentityModel.Tokens.Jwt.dll v4.0.30319.

A very simple sample of our code is below.

Program.cs

using System;
using System.Configuration;
using System.IdentityModel.Tokens;
using System.Text;

namespace ConsoleApplication1
{
    class Program
    {
        static void Main(string[] args)
        {
            var token = "eyJhbGciOiJSUzI1NiIsImtpZCI6IjEifQ.eyJzdWIiOiJ1c2VyMSIsImF1ZCI6ImNsaWVudDEiLCJqdGkiOiJKcUFDVVFiTlRQR201U0ZJRXY3MWR0IiwiaXNzIjoiaHR0cHM6XC9cL2xvY2FsaG9zdDo5MDMxIiwiaWF0IjoxNDEzNTcwNjEyLCJleHAiOjE0MTM1NzA5MTJ9.Z3P4Rt_w7d0oP8x6zfaot8PIxpEJHUw43Z_4VkOzv59nRz1dWopGUXw51DJd5cLjeM_zc14durs5NhJE27WmcKaEuE8-WZ0ubxM_bzykZfmAPa1WVk9KctPKiUH7QZg4OCLaqIX6usi5kkuICiPVdoJPkHmojMkm5nCqeBIbYteasysMTQGq93VtoBGUQomF89ZaFMBlUy0ofH7SEKJEW_4vgy7Umu0h7kNKkh6Aw4x9Bw1AkG1D6H_scsuH2uSxQ7QV-3G60DcjLZ31_R1ZxaUg2WS2ajemb6swKM4LIOR9_mK6ScUVVBxBL4Oh9g6EA93lMg_1GRZi780v_3TR8Q";

            var tokenValidator = new TokenValidator(new CacheProvider(), new DebugOpenIdConnectProviderClient(), 
                ConfigurationManager.AppSettings["AUDIENCE"], ConfigurationManager.AppSettings["ISSUER"]);
            SecurityToken securityToken;
            var principal = tokenValidator.Validate(token, out securityToken);

            if (principal != null)
            {
                Console.Out.WriteLine("Security token is valid");
            }

            foreach (var claim in principal.Claims)
            {
                Console.Out.WriteLine("{0} = {1}", claim.Type, claim.Value);
            }

            Console.ReadLine();
        }
    }
}

TokenValidator.cs

using System;
using System.Collections.Generic;
using System.IdentityModel.Tokens;
using System.Security.Claims;
using System.Security.Cryptography;
using Newtonsoft.Json;

namespace ConsoleApplication1
{
    public class TokenValidator
    {
        private readonly CacheProvider cacheProvider;
        private readonly IOpenIdConnectProviderClient openIdConnectProviderClient;
        private readonly string audience;
        private readonly string issuer;

        public TokenValidator(CacheProvider cacheProvider, IOpenIdConnectProviderClient openIdConnectProviderClient, string audience, string issuer)
        {
            this.cacheProvider = cacheProvider;
            this.openIdConnectProviderClient = openIdConnectProviderClient;
            this.audience = audience;
            this.issuer = issuer;
        }

        public ClaimsPrincipal Validate(string tokenString, out SecurityToken securityToken)
        {
            var jwtSecurityTokenHandler = new JwtSecurityTokenHandler();
            var jwt = jwtSecurityTokenHandler.ReadToken(tokenString) as JwtSecurityToken;
            var publicKey = GetPublicKey(jwt.Header.SigningKeyIdentifier[0].Id);
            var rsaPublicKey = CreatePublicKey(publicKey.n, publicKey.e);

            return jwtSecurityTokenHandler.ValidateToken(tokenString, new TokenValidationParameters()
            {
                IssuerSigningToken = new RsaSecurityToken(rsaPublicKey, publicKey.kid),
                IssuerSigningKeyResolver = (token, securityToken2, keyIdentifier, validationParameters) => {
                    return new RsaSecurityKey(rsaPublicKey);
                },
#if DEBUG
                ClockSkew = new TimeSpan(0, 30, 0),
#endif
                ValidIssuer = issuer,
                ValidAudience = audience,
            }, out securityToken);
        }

        public static RSACryptoServiceProvider CreatePublicKey(string modulus, string exponent)
        {
            var cryptoProvider = new RSACryptoServiceProvider();

            cryptoProvider.ImportParameters(new RSAParameters()
            {
                Exponent = Base64UrlEncoder.DecodeBytes(exponent),
                Modulus = Base64UrlEncoder.DecodeBytes(modulus),
            });

            return cryptoProvider;
        }

        private PublicKeyData GetPublicKey(string kid)
        {
            var keys = cacheProvider["PUBLIC_KEYS"] as Dictionary<string, PublicKeyData>;

            if (keys == null)
            {
                keys = GetPublicKeysFromPingFederate();

                cacheProvider["PUBLIC_KEYS"] = keys;
            }

            var currentKey = keys[kid];

            if (currentKey != null)
            {
                return currentKey;
            }

            throw new Exception("Could not find public key for kid: " + kid);
        }

        private Dictionary<string, PublicKeyData> GetPublicKeysFromPingFederate()
        {
            var keyString = openIdConnectProviderClient.Execute();            
            var keys = JsonConvert.DeserializeObject<PublicKeysJsonResult>(keyString);
            var result = new Dictionary<string, PublicKeyData>();

            foreach (var key in keys.Keys)
            {
                result[key.kid] = key;
            }

            return result;            
        }
    }
}
like image 501
Travis Spencer Avatar asked Oct 17 '14 19:10

Travis Spencer


1 Answers

This seems to be happening in the decoding of the Base64Url encoded signature. I can't tell you exactly why, but try this out:

Go to: http://kjur.github.io/jsjws/tool_b64udec.html

Decode your signature in the JWT in your post above:

Z3P4Rt_w7d0oP8x6zfaot8PIxpEJHUw43Z_4VkOzv59nRz1dWopGUXw51DJd5cLjeM_zc14durs5NhJE27WmcKaEuE8-WZ0ubxM_bzykZfmAPa1WVk9KctPKiUH7QZg4OCLaqIX6usi5kkuICiPVdoJPkHmojMkm5nCqeBIbYteasysMTQGq93VtoBGUQomF89ZaFMBlUy0ofH7SEKJEW_4vgy7Umu0h7kNKkh6Aw4x9Bw1AkG1D6H_scsuH2uSxQ7QV-3G60DcjLZ31_R1ZxaUg2WS2ajemb6swKM4LIOR9_mK6ScUVVBxBL4Oh9g6EA93lMg_1GRZi780v_3TR8Q

This will yield this HEX output:

6773f846dc3b774a0ff31eb37daa2df0f231a44247530e376785643b3bf9f67473d5d5a8a46517c39d4325de5c2e378ccdcd7876eaece4d849136ed699c29a12e13c599d2e6f131bcf29197e600f6b559593d29cb4f2a2507ed0660e0e08b6aa217eaeb22e6492e20288f55da093e41e6a233249b99c2a9e0486d8b5e6accac313406abddd5b68046510a2617cf59685301954cb4a1f1fb484289116e2f832ed49aed21ee434a921e80c38c7d070d40906d43e87b1cb2e1f6b92c50ed05771bad037232d9df5475671694836592d9a8de99beacc0a3382c8391f662ba49c515541c412f83a1f60e8403dde5320d464598bbf34bf74d1f1

Changing the last character of the Base64Url Encoded signature will actually not always change the signature value in hex. That is because only the first two bits of the last Base64 character (Q = 16 = 010000) in the string are significant. The last four bits are thrown out since they do not form a complete byte. So, you can actually use all these characters QRSTUVQXYZabcdef (binary 010000 - 011111), they will all produce the same hex value f1 in the end since the two first bits for all those characters are 01.

To conclude, you have not actually tampered with the signature, merely with the encoding of it. You are still validating using the valid key.

like image 115
Dennis Skantz Avatar answered Nov 13 '22 21:11

Dennis Skantz