(I've already been thru a lot of Stackoverflow/google results trying to find a fix for this.)
I am validating JWTs signed with RS256 using the default C# JwtSecurityTokenHandler. In some cases, the validation fails when it shouldn't. Concretely, tokens from a given Authorization Server validate properly while tokens form another Authorization Server won't.
BUT... Using the same JWTs and RSA Certificates on JWT.IO validates ALL the tokens succesfully. This is the part that makes me believe that there's something wrong/unusual in the C# implementation. I am also able to validate the same JWTs using the same Certificates using the oidc-client JavaScript library. The one place where the validation sometimes fails is in C#.
I traced the error down to JwtSecurityTokenHandler's ValidateSignature method. Searching the original github code and googling about RSA, I came down with this bare-bone method which allows me to reproduce the problem in a plain console app:
static void ValidateJWT(string token, string modulus, string exponent)
{
string tokenStr = token;
JwtSecurityToken st = new JwtSecurityToken(tokenStr);
string[] tokenParts = tokenStr.Split('.');
RSACryptoServiceProvider rsa = new RSACryptoServiceProvider();
rsa.ImportParameters(
new RSAParameters()
{
Modulus = FromBase64Url(modulus),
Exponent = FromBase64Url(exponent)
});
SHA256 sha256 = SHA256.Create();
byte[] hash = sha256.ComputeHash(Encoding.UTF8.GetBytes(tokenParts[0] + '.' + tokenParts[1]));
RSAPKCS1SignatureDeformatter rsaDeformatter = new RSAPKCS1SignatureDeformatter(rsa);
rsaDeformatter.SetHashAlgorithm("SHA256");
var valid = rsaDeformatter.VerifySignature(hash, FromBase64Url(tokenParts[2]));
Console.WriteLine(valid); // sometimes false when it should be true
}
private static byte[] FromBase64Url(string base64Url)
{
string padded = base64Url.Length % 4 == 0
? base64Url : base64Url + "====".Substring(base64Url.Length % 4);
string base64 = padded.Replace("_", "/")
.Replace("-", "+");
return Convert.FromBase64String(base64);
}
It is from that RSACryptoServiceProvider and using RSAKeys from here (https://gist.github.com/therightstuff/aa65356e95f8d0aae888e9f61aa29414) that I was able to Export the Public Key that allows me to validate JWTs successfully on JWT.IO.
string publicKey = RSAKeys.ExportPublicKey(rsa);
I can't provide actual JWTs to this post (they expire anyways), but does anyone knows of a crypto behavior specific to C# that could explain these validation errors, which don't happen in JavaScript nor on JWT.IO ?
And if so, any solution for this?
Thanks, Martin
https://www.rfc-editor.org/rfc/rfc7518#section-6.3.1.1
Note that implementers have found that some cryptographic libraries prefix an extra zero-valued octet to the modulus representations they return, for instance, returning 257 octets for a 2048-bit key, rather than 256. Implementations using such libraries will need to take care to omit the extra octet from the base64url-encoded representation.
In the case of one of the tokens you provided on a copy of this issue elsewhere, the decode of the modulus includes a prefixed 0x00 byte. This causes downstream problems. But you can fix their non-conformance.
byte[] modulusBytes = FromBase64Url(modulus);
if (modulusBytes[0] == 0)
{
byte[] tmp = new byte[modulusBytes.Length - 1];
Buffer.BlockCopy(modulusBytes, 1, tmp, 0, tmp.Length);
modulusBytes = tmp;
}
It looks like RS256 treats the signature as opaque bytes, so it will encode it as-is. So you probably don't need this correction (though it's where my investigation started):
byte[] sig = FromBase64Url(tokenParts[2]);
if (sig.Length < modulusBytes.Length)
{
byte[] tmp = new byte[modulusBytes.Length];
Buffer.BlockCopy(sig, 0, tmp, tmp.Length - sig.Length, sig.Length);
sig = tmp;
}