JwtSecurityTokenHandler says JWT signature is valid after changing 1 char

We are trying to validate the IDT (IDT) presented to the .NET client application by the OpenID Connect Provider (OP). IDT is what you expect. Nothing unusual happens there.

To verify the IDT signature, we can get the exponent and the module from the OP by calling the public endpoint. They can be used to generate a public key that matches the private one used by the OP to sign the IDT. With their help, we create an RSACryptoServiceProvider object to verify the signature. To handle this, we pass the crypto service provider as the token validation parameter to the JwtSecurityTokenHandler.

This works great. 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 ask ourselves:

  • This is an issue with the way we generate the signing key that causes the JWT to validate incorrectly.
  • There is a bug in JwtSecurityTokenHandler.
  • We do not fully understand the specification, and this small change is allowed because the last character in the signature portion of the JWT is not actually valid for validation.
  • Something else

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

Below is a simple example of our code.

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;            
        }
    }
}

      

+3


source to share


1 answer


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

Follow the link: http://kjur.github.io/jsjws/tool_b64udec.html

Decode your signature in JWT in your post above:

Z3P4Rt_w7d0oP8x6zfaot8PIxpEJHUw43Z_4VkOzv59nRz1dWopGUXw51DJd5cLjeM_zc14durs5NhJE27WmcKaEuE8-WZ0ubxM_bzykZfmAPa1WVk9KctPKiUH7QZg4OCLaqIX6usi5kkuICiPVdoJPkHmojMkm5nCqeBIbYteasysMTQGq93VtoBGUQomF89ZaFMBlUy0ofH7SEKJEW_4vgy7Umu0h7kNKkh6Aw4x9Bw1AkG1D6H_scsuH2uSxQ7QV-3G60DcjLZ31_R1ZxaUg2WS2ajemb6swKM4LIOR9_mK6ScUVVBxBL4Oh9g6EA93lMg_1GRZi780v_3TR8Q

      



This will give this HEX output:

6773f846dc3b774a0ff31eb37daa2df0f231a44247530e376785643b3bf9f67473d5d5a8a46517c39d4325de5c2e378ccdcd7876eaece4d849136ed699c29a12e13c599d2e6f131bcf29197e600f6b559593d29cb4f2a2507ed0660e0e08b6aa217eaeb22e6492e20288f55da093e41e6a233249b99c2a9e0486d8b5e6accac313406abddd5b68046510a2617cf59685301954cb4a1f1fb484289116e2f832ed49aed21ee434a921e80c38c7d070d40906d43e87b1cb2e1f6b92c50ed05771bad037232d9df5475671694836592d9a8de99beacc0a3382c8391f662ba49c515541c412f83a1f60e8403dde5320d464598bbf34bf74d1f1

      

Changing the last character of the Base64Url encoded signature will actually not always change the hexadecimal value of the signature. This 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 discarded as they do not form a complete byte. So you can use all these QRSTUVQXYZabcdef characters (binary 010000 - 011111), they will all have the same hexadecimal value f1 at the end, since the first two bits for all of these characters are 01.

In conclusion, you haven't actually forged the signature, just with its encoding. You are still checking using a valid key.

+8


source







All Articles