azure-activedirectory-identitymodel-extensions-for-dotnet icon indicating copy to clipboard operation
azure-activedirectory-identitymodel-extensions-for-dotnet copied to clipboard

[Bug] JWE is still valid after appending data to the Authenticaton Tag

Open vladpirlog opened this issue 2 years ago • 5 comments

Which version of Microsoft.IdentityModel are you using? System.IdentityModel.Tokens.Jwt 7.1.0-preview

Where is the issue?

  • [x] M.IM.JsonWebTokens
  • [ ] M.IM.KeyVaultExtensions
  • [ ] M.IM.Logging
  • [ ] M.IM.ManagedKeyVaultSecurityKey
  • [ ] M.IM.Protocols
  • [ ] M.IM.Protocols.OpenIdConnect
  • [ ] M.IM.Protocols.SignedHttpRequest
  • [ ] M.IM.Protocols.WsFederation
  • [ ] M.IM.TestExtensions
  • [x] M.IM.Tokens
  • [ ] M.IM.Tokens.Saml
  • [ ] M.IM.Validators
  • [ ] M.IM.Xml
  • [x] S.IM.Tokens.Jwt
  • Other (please describe)

Is this a new or an existing app? This is a new app or an experiment.

Repro

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

var jwtHandler = new JsonWebTokenHandler();

var claims = new Dictionary<string, object>
{
    { "claim1", "value1" },
    { "claim2", "value2" },
};

var signingKey = RSA.Create();
var validationKey = RSA.Create(signingKey.ExportParameters(false));

var decryptionKey = RSA.Create();
var encryptionKey = RSA.Create(decryptionKey.ExportParameters(false));

var token = jwtHandler.CreateToken(new SecurityTokenDescriptor
{
    Claims = claims,
    SigningCredentials = new SigningCredentials(
        new RsaSecurityKey(signingKey),
        SecurityAlgorithms.RsaSha256),
    EncryptingCredentials = new EncryptingCredentials(
        new RsaSecurityKey(encryptionKey),
        SecurityAlgorithms.RsaOAEP,
        SecurityAlgorithms.Aes256CbcHmacSha512)
});

// altering the JWE by appending some stuff to the Authentication Tag
var invalidToken = token + "the_token_has_been_tampered_with";

var validationResult = jwtHandler.ValidateToken(invalidToken, new()
{
    IssuerSigningKey = new RsaSecurityKey(validationKey),
    TokenDecryptionKey = new RsaSecurityKey(decryptionKey),
    ValidateAudience = false,
    ValidateIssuer = false,
    ValidateLifetime = false
});

if (validationResult.IsValid) // token is valid when it shouldn't be
    throw new Exception("The invalid token has been incorrectly marked as valid!");

Expected behavior The validation of the JWE should fail, as the Authentication Tag has been altered, thus no longer valid.

Extracted from RFC 7516, Section 5.2, regarding JWE decryption:

2.   Base64url decode the encoded representations of the JWE
       Protected Header, the JWE Encrypted Key, the JWE Initialization
       Vector, the JWE Ciphertext, the JWE Authentication Tag, and the
       JWE AAD, following the restriction that no line breaks,
       whitespace, or other additional characters have been used.

Actual behavior The JWE is successfully validated when the Authentication Tag has been altered.

Possible solution Check the actual length of the Authentication Tag and fail the validation if it does not match the expected value, according to the JWE encryption algorithm used.

vladpirlog avatar Aug 02 '23 12:08 vladpirlog

This is not a security issue, but should be fixed as it keeps coming up.

brentschmaltz avatar Feb 21 '24 23:02 brentschmaltz

@kellyyangsong, I think it's a duplicate of #1641 (also in scope for .NET9)

jmprieur avatar Apr 21 '24 18:04 jmprieur

Currently

var payload = new JObject()
{
    { JwtRegisteredClaimNames.Email, "[email protected]" },
    { JwtRegisteredClaimNames.GivenName, "Bob" },
    { JwtRegisteredClaimNames.Iss, "http://Default.Issuer.com"},
    { JwtRegisteredClaimNames.Aud, "http://Default.Audience.com" },
    { JwtRegisteredClaimNames.Iat, EpochTime.GetIntDate(DateTime.Parse("2017-03-17T18:33:37.095Z")).ToString() },
    { JwtRegisteredClaimNames.Nbf, EpochTime.GetIntDate(DateTime.Parse("2017-03-17T18:33:37.080Z")).ToString() },
    { JwtRegisteredClaimNames.Exp, EpochTime.GetIntDate(DateTime.Parse("2025-03-17T18:33:37.080Z")).ToString() },
}.ToString();
 
var jsonWebTokenHandler = new JsonWebTokenHandler();
var signingCredentials = Default.SymmetricSigningCredentials;
var encryptingCredentials = new EncryptingCredentials(KeyingMaterial.RsaSecurityKey_2048, SecurityAlgorithms.RsaPKCS1, SecurityAlgorithms.Aes128CbcHmacSha256);
var jwe = jsonWebTokenHandler.CreateToken(payload, signingCredentials, encryptingCredentials);
 
// altering the JWE by appending some stuff to the Authentication Tag
var invalidToken = jwe + "the_token_has_been_tampered_with";
 
var tokenValidationParameters = new TokenValidationParameters
{
    TokenDecryptionKey = KeyingMaterial.RsaSecurityKey_2048,
    IssuerSigningKey = Default.SymmetricSigningKey256,
    ValidAudience = "http://Default.Audience.com",
    ValidIssuer = "http://Default.Issuer.com",
};
var tokenValidationResult = await jsonWebTokenHandler.ValidateTokenAsync(invalidToken, tokenValidationParameters).ConfigureAwait(false);
 
// tokenValidationResult.IsValid == true

Desired Behavior

// tokenValidationResult.IsValid == false

How

  • We will evaluate that authenticationTag is the correct length based on the given Algorithm.
  • This will be wrapped in an app context switch.
  • auth tag length validation will be on by default.
Supported Algorithms Auth Tag size BaseUrl64 encoded length
Aes128Gcm 128 bits 24
Aes192Gcm 128 bits 24
Aes256Gcm 128 bits 24
Aes128CbcHmacSha256 128 bits 24
Aes192CbcHmacSha384 192 bits 32
Aes256CbcHmacSha512 256 bits 44

kllysng avatar Apr 23 '24 16:04 kllysng

@kevinchalet fyi on the comment above, which is a duplicate of #1641

kllysng avatar Apr 26 '24 21:04 kllysng

Thanks for fixing that 👍🏻

That said, I'm not so sure it's the best approach:

  • You're adding yet another mapping (one already exists in SymmetricSignatureProvider for the same exact purpose).
  • The validation logic is completely skipped if the algorithm is not recognized, which seems fragile.
  • Your mapping dictionary includes AES-GCM algorithms but it's unnecessary as they are never handled in DecryptWithAesCbc, that only deals with AES-CBC (hence the name I guess 😄).

I wonder if simply replacing _authenticatedkeys.Value.HmacKey.Key.Length by authenticationTag.Length here isn't a better fix:

https://github.com/AzureAD/azure-activedirectory-identitymodel-extensions-for-dotnet/blob/d40999cd87ac57da125c641cf8211327936abeb3/src/Microsoft.IdentityModel.Tokens/Encryption/AuthenticatedEncryptionProvider.cs#L194

With that in place, I'd expect SymmetricSignatureProvider.Verify() to end up properly validating the "signature" length (or HMAC to be 100% correct):

https://github.com/AzureAD/azure-activedirectory-identitymodel-extensions-for-dotnet/blob/d353b5a67200933361941550e99d4a15a11de04e/src/Microsoft.IdentityModel.Tokens/SymmetricSignatureProvider.cs#L429-L444

kevinchalet avatar Apr 27 '24 01:04 kevinchalet