C# RSACryptoServiceProvider - JWT RS256 验证失败

C# RSACryptoServiceProvider - JWT RS256 Validation Fails

(我已经通过很多 Whosebug/google 结果试图找到解决这个问题的方法。)

我正在使用默认的 C# JwtSecurityTokenHandler 验证使用 RS256 签名的 JWT。在某些情况下,验证会在不应该的时候失败。具体来说,来自给定授权服务器的令牌可以正确验证,而来自另一个授权服务器的令牌则不会。

但是...在 JWT.IO 上使用相同的 JWT 和 RSA 证书可成功验证所有令牌。这是让我相信 C# 实现中有某些东西 wrong/unusual 的部分。 我还能够使用 oidc-client JavaScript 库 使用相同的证书验证相同的 JWT。验证有时会失败的一个地方是在 C# 中。

我将错误追溯到 JwtSecurityTokenHandler 的 ValidateSignature 方法。搜索原始 github 代码并在谷歌上搜索 RSA,我找到了这个允许我在普通控制台应用程序中重现问题的简单方法:

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

通过 RSACryptoServiceProvider 并使用此处的 RSAKeys (https://gist.github.com/therightstuff/aa65356e95f8d0aae888e9f61aa29414),我能够导出 Public 密钥,使我能够验证 JWT 成功 在 JWT.IO.

string publicKey = RSAKeys.ExportPublicKey(rsa);

我无法为此 post 提供实际的 JWT(它们无论如何都会过期),但是有没有人知道特定于 C# 的加密行为可以解释这些验证错误,这不会发生在 JavaScript 也不在 JWT.IO ?

如果是这样,有什么解决办法吗?

谢谢, 马丁

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.

对于您在其他地方的本期副本中提供的其中一个标记,模数的解码包括一个前缀为 0x00 的字节。这会导致下游问题。但是你可以修复他们的 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;
}

看起来 RS256 将签名视为不透明字节,因此它将对其进行编码 as-is。所以你可能不需要这个更正(尽管这是我开始调查的地方):

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