Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

Multiple RSA object instances lead to failing signature validation #43087

Closed
secana opened this issue Oct 6, 2020 · 8 comments
Closed

Multiple RSA object instances lead to failing signature validation #43087

secana opened this issue Oct 6, 2020 · 8 comments
Labels
area-System.Security untriaged New issue has not been triaged by the area owner

Comments

@secana
Copy link

secana commented Oct 6, 2020

Description

When multiple RSA object instances are created after each other, every second signature validation fails. The code below shows a simple example, where the behavior can be observed.

using System;
using System.IdentityModel.Tokens.Jwt;
using Microsoft.IdentityModel.Tokens;
using System.Security.Cryptography;

namespace JwtTest
{
    class Program
    {
        static void Main(string[] args)
        {

            var pubKey = Convert.FromBase64String("MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAnzyis1ZjfNB0bBgKFMSvvkTtwlvBsaJq7S5wA+kzeVOVpVWwkWdVha4s38XM/pa/yr47av7+z3VTmvDRyAHcaT92whREFpLv9cj5lTeJSibyr/Mrm/YtjCZVWgaOYIhwrXwKLqPr/11inWsAkfIytvHWTxZYEcXLgAXFuUuaS3uF9gEiNQwzGTU1v0FqkqTBr4B8nW3HCN47XUu0t8Y0e+lf4s4OxQawWD79J9/5d3Ry0vbV3Am1FtGJiJvOwRsIfVChDpYStTcHTCMqtvWbV6L11BWkpzGXSW4Hv43qa+GSYOD2QU68Mb59oSk2OB+BtOLpJofmbGEGgvmwyCI9MwIDAQAB");
            var token = "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJOYW1lIjoiSm9obiBEb2UiLCJBZG1pbiI6IlRydWUifQ.ma49T3npXQJVMa-afyfFgIPW5PEYhrrYvX2mUA6rmzXHXq_Wy-ij9MLc0b6UxZX8STcRrSC93meIMa4a8LI7UBe0Pxn8IQBrhXztcElMfktMQoQWb7Osx9XwmqD1CaQWwz3FX963B4fQFdxx7GpxdLPj-CSOJZ4OZbk8fWpurVX1QXMLokaJ8C-gLB026jFVJjIV1APSMOnAzx9lcZfU5m3jwVP8HMIc0yJkm4d7IJO1lQjYnUWQkY_DmwR8-vysqo3N5yY57xQUFRoyHwFofDb25fA6SkKcNHrOX0_bc7KzxzWacoPWgtUolThKasWpXgqHipR-uJ4hz6zahInCmw";

            Validate(token, pubKey);
        }

        static void Validate(string token, byte[] pubKey)
        {
            for (var i = 0; i < 10; i++)
            {
               // This seems to be the problem!
                using var rsa = RSA.Create();
                rsa.ImportSubjectPublicKeyInfo(new ReadOnlySpan<byte>(pubKey), out var bytesRead);
                var validationParameters = new TokenValidationParameters
                {
                    ValidateIssuer = false,
                    ValidateAudience = false,
                    ValidateLifetime = false,
                    ValidateIssuerSigningKey = true,
                    IssuerSigningKey = new RsaSecurityKey(rsa)
                };

                var handler = new JwtSecurityTokenHandler();

                try
                {
                    handler.ValidateToken(token, validationParameters, out var validatedToken);
                    Console.WriteLine("Valid");
                }
                catch (Exception ex)
                {
                    Console.WriteLine(ex);
                }
            }
        }
    }
}

Ouput:

Valid
Microsoft.IdentityModel.Tokens.SecurityTokenInvalidSignatureException: IDX10503: Signature validation failed. Keys tried: 'System.Text.StringBuilder'.
Exceptions caught:
 'System.Text.StringBuilder'.
token: 'System.IdentityModel.Tokens.Jwt.JwtSecurityToken'.
   at System.IdentityModel.Tokens.Jwt.JwtSecurityTokenHandler.ValidateSignature(String token, TokenValidationParameters validationParameters)
   at System.IdentityModel.Tokens.Jwt.JwtSecurityTokenHandler.ValidateToken(String token, TokenValidationParameters validationParameters, SecurityToken& validatedToken)
   at JwtTest.Program.Validate(String token, Byte[] pubKey) in C:\Users\Stefa\Downloads\JwtTest\Program.cs:line 38
Valid
Microsoft.IdentityModel.Tokens.SecurityTokenInvalidSignatureException: IDX10503: Signature validation failed. Keys tried: 'System.Text.StringBuilder'.
Exceptions caught:
 'System.Text.StringBuilder'.
token: 'System.IdentityModel.Tokens.Jwt.JwtSecurityToken'.
   at System.IdentityModel.Tokens.Jwt.JwtSecurityTokenHandler.ValidateSignature(String token, TokenValidationParameters validationParameters)
   at System.IdentityModel.Tokens.Jwt.JwtSecurityTokenHandler.ValidateToken(String token, TokenValidationParameters validationParameters, SecurityToken& validatedToken)
   at JwtTest.Program.Validate(String token, Byte[] pubKey) in C:\Users\Stefa\Downloads\JwtTest\Program.cs:line 38
Valid
Microsoft.IdentityModel.Tokens.SecurityTokenInvalidSignatureException: IDX10503: Signature validation failed. Keys tried: 'System.Text.StringBuilder'.
Exceptions caught:
 'System.Text.StringBuilder'.
token: 'System.IdentityModel.Tokens.Jwt.JwtSecurityToken'.
   at System.IdentityModel.Tokens.Jwt.JwtSecurityTokenHandler.ValidateSignature(String token, TokenValidationParameters validationParameters)
   at System.IdentityModel.Tokens.Jwt.JwtSecurityTokenHandler.ValidateToken(String token, TokenValidationParameters validationParameters, SecurityToken& validatedToken)
   at JwtTest.Program.Validate(String token, Byte[] pubKey) in C:\Users\Stefa\Downloads\JwtTest\Program.cs:line 38
Valid
Microsoft.IdentityModel.Tokens.SecurityTokenInvalidSignatureException: IDX10503: Signature validation failed. Keys tried: 'System.Text.StringBuilder'.
Exceptions caught:
 'System.Text.StringBuilder'.
token: 'System.IdentityModel.Tokens.Jwt.JwtSecurityToken'.
   at System.IdentityModel.Tokens.Jwt.JwtSecurityTokenHandler.ValidateSignature(String token, TokenValidationParameters validationParameters)
   at System.IdentityModel.Tokens.Jwt.JwtSecurityTokenHandler.ValidateToken(String token, TokenValidationParameters validationParameters, SecurityToken& validatedToken)
   at JwtTest.Program.Validate(String token, Byte[] pubKey) in C:\Users\Stefa\Downloads\JwtTest\Program.cs:line 38
Valid
Microsoft.IdentityModel.Tokens.SecurityTokenInvalidSignatureException: IDX10503: Signature validation failed. Keys tried: 'System.Text.StringBuilder'.
Exceptions caught:
 'System.Text.StringBuilder'.
token: 'System.IdentityModel.Tokens.Jwt.JwtSecurityToken'.
   at System.IdentityModel.Tokens.Jwt.JwtSecurityTokenHandler.ValidateSignature(String token, TokenValidationParameters validationParameters)
   at System.IdentityModel.Tokens.Jwt.JwtSecurityTokenHandler.ValidateToken(String token, TokenValidationParameters validationParameters, SecurityToken& validatedToken)
   at JwtTest.Program.Validate(String token, Byte[] pubKey) in C:\Users\Stefa\Downloads\JwtTest\Program.cs:line 38

Moving the using var rsa = RSA.Create(); out of the loop, such that it gets reused instead of a fresh instance, it works fine.

using System;
using System.IdentityModel.Tokens.Jwt;
using Microsoft.IdentityModel.Tokens;
using System.Security.Cryptography;

namespace JwtTest
{
    class Program
    {
        static void Main(string[] args)
        {

            var pubKey = Convert.FromBase64String("MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAnzyis1ZjfNB0bBgKFMSvvkTtwlvBsaJq7S5wA+kzeVOVpVWwkWdVha4s38XM/pa/yr47av7+z3VTmvDRyAHcaT92whREFpLv9cj5lTeJSibyr/Mrm/YtjCZVWgaOYIhwrXwKLqPr/11inWsAkfIytvHWTxZYEcXLgAXFuUuaS3uF9gEiNQwzGTU1v0FqkqTBr4B8nW3HCN47XUu0t8Y0e+lf4s4OxQawWD79J9/5d3Ry0vbV3Am1FtGJiJvOwRsIfVChDpYStTcHTCMqtvWbV6L11BWkpzGXSW4Hv43qa+GSYOD2QU68Mb59oSk2OB+BtOLpJofmbGEGgvmwyCI9MwIDAQAB");
            var token = "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJOYW1lIjoiSm9obiBEb2UiLCJBZG1pbiI6IlRydWUifQ.ma49T3npXQJVMa-afyfFgIPW5PEYhrrYvX2mUA6rmzXHXq_Wy-ij9MLc0b6UxZX8STcRrSC93meIMa4a8LI7UBe0Pxn8IQBrhXztcElMfktMQoQWb7Osx9XwmqD1CaQWwz3FX963B4fQFdxx7GpxdLPj-CSOJZ4OZbk8fWpurVX1QXMLokaJ8C-gLB026jFVJjIV1APSMOnAzx9lcZfU5m3jwVP8HMIc0yJkm4d7IJO1lQjYnUWQkY_DmwR8-vysqo3N5yY57xQUFRoyHwFofDb25fA6SkKcNHrOX0_bc7KzxzWacoPWgtUolThKasWpXgqHipR-uJ4hz6zahInCmw";

            Validate(token, pubKey);
        }

        static void Validate(string token, byte[] pubKey)
        {
            // Now it works!
            using var rsa = RSA.Create();
            for (var i = 0; i < 10; i++)
            {
                rsa.ImportSubjectPublicKeyInfo(new ReadOnlySpan<byte>(pubKey), out var bytesRead);
                var validationParameters = new TokenValidationParameters
                {
                    ValidateIssuer = false,
                    ValidateAudience = false,
                    ValidateLifetime = false,
                    ValidateIssuerSigningKey = true,
                    IssuerSigningKey = new RsaSecurityKey(rsa)
                };

                var handler = new JwtSecurityTokenHandler();

                try
                {
                    handler.ValidateToken(token, validationParameters, out var validatedToken);
                    Console.WriteLine("Valid");
                }
                catch (Exception ex)
                {
                    Console.WriteLine(ex);
                }
            }
        }
    }
}

Output:

Valid
Valid
Valid
Valid
Valid
Valid
Valid
Valid
Valid
Valid

It seems that the RSA object is not disposed correctly, or I'm using it wrong. My use-case is an Azure function where a signature is verified. Unfortunately every second time the verification fails as the function creates a new instance of the RSA object every time it's invoked.

Configuration

  • .NET Core 3.1 and .NET core 5.0 RC1
  • Windows 10, Fedora 32 both x64
@Dotnet-GitSync-Bot Dotnet-GitSync-Bot added area-System.Security untriaged New issue has not been triaged by the area owner labels Oct 6, 2020
@ghost
Copy link

ghost commented Oct 6, 2020

Tagging subscribers to this area: @bartonjs, @vcsjones, @krwq, @jeffhandley
See info in area-owners.md if you want to be subscribed.

@krwq
Copy link
Member

krwq commented Oct 6, 2020

@secana, do you have a repro which doesn't involve JwtSecurityTokenHandler?

@secana
Copy link
Author

secana commented Oct 6, 2020

Sry, don't have that. I ported a F# script as I was not sure if you prefer C#, so that's all I have.

@vcsjones
Copy link
Member

vcsjones commented Oct 6, 2020

I can take a look at this soon.

@secana to make sure I reproduce it as best as possible, can you tell me exactly which packages and versions are being used?

@secana
Copy link
Author

secana commented Oct 6, 2020

<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>netcoreapp3.1</TargetFramework>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="Microsoft.IdentityModel.Tokens" Version="6.7.1" />
    <PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="6.7.1" />
  </ItemGroup>

</Project>

@vcsjones
Copy link
Member

vcsjones commented Oct 7, 2020

This is caused by how Microsoft.IdentityModel.Tokens caches instances of RSA objects in a static cache called CryptoProviderFactory.

When you construct a RsaSecurityKey with an RSA object, it gets added to the InMemoryCryptoProviderCache here:

https://github.com/AzureAD/azure-activedirectory-identitymodel-extensions-for-dotnet/blob/7058653ca8e3fccc3bcab48f521d9b0b10060251/src/Microsoft.IdentityModel.Tokens/CryptoProviderFactory.cs#L485

Then the signature verifies, an the RSA object is disposed since you have a using.

In the next loop, because it's the same key, the RSA object is used from the cache, but it's disposed. So then RSACng throws an ObjectDisposedException.

This exception is caught, and the RSA object is removed from cache here:

https://github.com/AzureAD/azure-activedirectory-identitymodel-extensions-for-dotnet/blob/93334a8e94e66d3df36f2850a03549ed1bfd4234/src/Microsoft.IdentityModel.Tokens/AsymmetricSignatureProvider.cs#L357-L360

Signature verification fails, but the disposed object is no longer in cache. In the next iteration, since a matching key is not in cache, we start all over again: it gets added to the cache, verify, dispose, etc etc etc.

I'm not quite as familiar with the Azure SDKs as I am with anything anything else. One way you can work around this is to disable the cache by adding this before your loop:

CryptoProviderFactory.Default.CacheSignatureProviders = false;

Perhaps there are better recommendations or intentions with the behavior of the Azure SDK. However, the behavior you are seeing is entirely explained by the caching of RSA objects and disposing of them while they are in the cache.

@secana
Copy link
Author

secana commented Oct 8, 2020

@vcsjones thx for the help. With CryptoProviderFactory.Default.CacheSignatureProviders = false; set, it works. If that behavior is the expected way to work, I guess some note in the documentation would be good. I'll close the issue, as you solution resolved it.

@vcsjones
Copy link
Member

Opened AzureAD/azure-activedirectory-identitymodel-extensions-for-dotnet#1539 to see if the AzureAD folks have documentation on this behavior, and / or possibly adding documentation.

@ghost ghost locked as resolved and limited conversation to collaborators Dec 7, 2020
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
area-System.Security untriaged New issue has not been triaged by the area owner
Projects
None yet
Development

No branches or pull requests

4 participants