在 C# 中使用 RS256(非对称)验证 JWT

Posted

技术标签:

【中文标题】在 C# 中使用 RS256(非对称)验证 JWT【英文标题】:Verify JWT with RS256 (asymmetric) in C# 【发布时间】:2020-12-08 09:35:24 【问题描述】:

我有一些这样的代码,我认为它失败了,因为它使用的是非对称 RS256,但有“SymmetricSecurityKey()”。代币是由https://jwt.io/手工生成的

    如何将其转换为使用我的非对称公钥? 另外,我是 C# 新手,我想以 dotnet 标准为目标,所以我也想知道我是否使用了错误的库? (我依赖于预览版)
λ cat Program.cs
using System;
using System.IdentityModel.Tokens.Jwt;
using System.Text;
using System.Linq;
using Microsoft.IdentityModel.Tokens;
using System.Security.Cryptography;

namespace jwttest

    class Program
    
        static void Main(string[] args)
        
            string jwt = "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWUsImlhdCI6MTUxNjIzOTAyMn0.POstGetfAytaZS82wHcjoTyoqhMyxXiWdR7Nn7A29DNSl0EiXLdwJ6xC6AfgZWF1bOsS_TuYI3OG85AmiExREkrS6tDfTQ2B3WXlrr-wp5AokiRbz3_oB4OxG-W9KcEEbDRcZc0nH3L7LzYptiy1PtAylQGxHTWZXtGz4ht0bAecBgmpdgXMguEIcoqPJ1n3pIWk_dUZegpqx0Lka21H6XxUTxiy8OcaarA8zdnPUnV6AmNP3ecFawIFYdvJB_cm-GvpCSbr8G8y_Mllj8f4x9nBH8pQux89_6gUY618iYv7tuPWBFfEbLxtF2pZS6YC1aSfLQxeNe8djT9YjpvRZA";
            var pubKey = "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAnzyis1ZjfNB0bBgKFMSvvkTtwlvBsaJq7S5wA+kzeVOVpVWwkWdVha4s38XM/pa/yr47av7+z3VTmvDRyAHcaT92whREFpLv9cj5lTeJSibyr/Mrm/YtjCZVWgaOYIhwrXwKLqPr/11inWsAkfIytvHWTxZYEcXLgAXFuUuaS3uF9gEiNQwzGTU1v0FqkqTBr4B8nW3HCN47XUu0t8Y0e+lf4s4OxQawWD79J9/5d3Ry0vbV3Am1FtGJiJvOwRsIfVChDpYStTcHTCMqtvWbV6L11BWkpzGXSW4Hv43qa+GSYOD2QU68Mb59oSk2OB+BtOLpJofmbGEGgvmwyCI9MwIDAQAB";
            var rawKey = Encoding.ASCII.GetBytes(pubKey);

            var tokenHandler = new JwtSecurityTokenHandler();
            // var rsa = ?
            tokenHandler.ValidateToken(jwt, new TokenValidationParameters 
                IssuerSigningKey = new SymmetricSecurityKey(rawKey)
            ,
            out SecurityToken validatedToken);
        
    


C:\src\jwttest (cgt-test-5 -> origin)
λ dotnet run
[2020-08-18T23:41:05.7108585-07:00 Info] raw=System.Byte[] [392]
Unhandled exception. 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.Main(String[] args) in C:\src\jwttest\Program.cs:line 22

λ cat jwttest.csproj
<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>netcoreapp3.1</TargetFramework>
  </PropertyGroup>
  <ItemGroup>
    <!-- Using preview release because it only depends on dotnet standard.  Prior versions need framework. -->
    <PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="6.7.2-preview-10803222715" />
  </ItemGroup>
</Project>

λ cat jwt.json

  "alg": "RS256",
  "typ": "JWT"


  "sub": "1234567890",
  "name": "John Doe",
  "admin": true,
  "iat": 1516239022

【问题讨论】:

***.com/questions/34403823/… 【参考方案1】:

关于您的第一个问题: 根据您发布的堆栈跟踪,您似乎正在使用 .NET Core 3.1。这使您可以轻松地导入您的公共 X.509/SPKI 密钥,如下所示:

var pubKey = "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAnzyis1ZjfNB0bBgKFMSvvkTtwlvBsaJq7S5wA+kzeVOVpVWwkWdVha4s38XM/pa/yr47av7+z3VTmvDRyAHcaT92whREFpLv9cj5lTeJSibyr/Mrm/YtjCZVWgaOYIhwrXwKLqPr/11inWsAkfIytvHWTxZYEcXLgAXFuUuaS3uF9gEiNQwzGTU1v0FqkqTBr4B8nW3HCN47XUu0t8Y0e+lf4s4OxQawWD79J9/5d3Ry0vbV3Am1FtGJiJvOwRsIfVChDpYStTcHTCMqtvWbV6L11BWkpzGXSW4Hv43qa+GSYOD2QU68Mb59oSk2OB+BtOLpJofmbGEGgvmwyCI9MwIDAQAB";

RSACryptoServiceProvider rsa = new RSACryptoServiceProvider();
rsa.ImportSubjectPublicKeyInfo(Convert.FromBase64String(pubKey), out _); // import the public X.509/SPKI DER encoded key

ImportSubjectPublicKeyInfo() 自 .NET Core 3.0 起可用。

编辑开始: 在 .NET Core 的早期版本(3.0 之前)或 .NET Framework 中,ImportSubjectPublicKeyInfo() 不可用,因此至少需要.NET Standard 2.1。

对于早期版本,例如.NET Standard 2.0,一种可能性是使用BouncyCastle,更准确地说是它的Org.BouncyCastle.OpenSsl.PemReader 类,它允许导入X509/SPKI 格式的公钥(与您无关,也可以使用PKCS#1 格式)。在this answer 中,您将找到如何使用PemReader 的示例。 PemReader 顾名思义,处理 PEM 编码,即转换为 DER 编码(即删除页眉、页脚和换行符,以及对其余部分进行 Base64 解码),这是 ImportSubjectPublicKeyInfo() 所要求的。 em>不能这样做。另请注意,PemReader 要求在标题 (-----BEGIN PUBLIC KEY-----\n) 之后至少有一个换行符,在页脚之前 (\n-----END PUBLIC KEY-----) 至少有一个换行符,Base64 编码正文中每 64 个字符后的换行符是可选的PemReader.

另一种可能性是包opensslkey 提供方法opensslkey.DecodeX509PublicKey(),它可以处理类似于ImportSubjectPublicKeyInfo 的DER 编码中的X509/SPKI 密钥。 编辑结束

关于您的第二个问题: 有几个 .NET standard 版本,例如.NET Core 3.0 实现了 .NET Standard 2.1。您正在使用的包 System.IdentityModel.Tokens.Jwt 6.7.2-preview-10803222715 需要 .NET Standard 2.0。

System.IdentityModel.Tokens.Jwt 是一个支持 JSON Web Tokens (JWT) 创建和验证的包。在发布令牌的情况下,验证可以如下实现:

string jwt = "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWUsImlhdCI6MTUxNjIzOTAyMn0.POstGetfAytaZS82wHcjoTyoqhMyxXiWdR7Nn7A29DNSl0EiXLdwJ6xC6AfgZWF1bOsS_TuYI3OG85AmiExREkrS6tDfTQ2B3WXlrr-wp5AokiRbz3_oB4OxG-W9KcEEbDRcZc0nH3L7LzYptiy1PtAylQGxHTWZXtGz4ht0bAecBgmpdgXMguEIcoqPJ1n3pIWk_dUZegpqx0Lka21H6XxUTxiy8OcaarA8zdnPUnV6AmNP3ecFawIFYdvJB_cm-GvpCSbr8G8y_Mllj8f4x9nBH8pQux89_6gUY618iYv7tuPWBFfEbLxtF2pZS6YC1aSfLQxeNe8djT9YjpvRZA";

var tokenHandler = new JwtSecurityTokenHandler();
bool verified = false;
try

    tokenHandler.ValidateToken(jwt, new TokenValidationParameters
    
        ValidateAudience = false,                       
        ValidateLifetime = false,
        ValidateIssuer = false,
        IssuerSigningKey = new RsaSecurityKey(rsa)
    ,
    out _);

    verified = true;

catch 

    verified = false;


Console.WriteLine("Verified: " + verified);

验证可以通过验证参数来控制,即通过ValidateToken()的第二个参数。由于发布的令牌不包含claims issaudexp(这可以在 https://jwt.io/ 上进行验证),它们在我的示例中被排除在验证之外。

在教程Creating And Validating JWT Tokens In ASP.NET Core 中你会找到更详细的解释,尤其是在验证令牌一章中。

ValidateToken()本质上封装了JWT签名的验证过程。 JWT 是一种数据结构,由三部分组成:标头、有效负载和签名,各个部分经过 Base64url 编码并由点分隔。签名是使用各种算法创建的,例如在您的情况下为RS256,这意味着数据(Base64url 编码的标头和有效负载,包括分隔符)使用 RSA 算法和 PKCS#1 v1.5 填充和摘要 SHA256 进行签名。令牌的验证对应于签名的验证,也可以仅使用加密 API 完成(即没有 System.IdentityModel.Tokens.Jwt 的参与),就像在链接问题的 accepted answer 中所做的那样@zaitsman 的评论。

【讨论】:

非常感谢!你是救命稻草——我一直在努力遵循 ms 文档,却发现我使用了错误的 dotnet 风格。这是一个库,我想使用在 dotnet 框架上构建的旧代码和在 dotnet core 3.1 上构建的新代码。我觉得很奇怪 RSACryptoServiceProvider() 不接受字符串并自动检测和隐藏 pki 密钥。 糟糕,一个问题是,如果我以 dotnetstandard2.0 为目标,这部分不起作用,但对于 dotnetstandard2.1 ``` RSACryptoServiceProvider rsa = new RSACryptoServiceProvider(); rsa.ImportSubjectPublicKeyInfo(Convert.FromBase64String(pubKey), out _); // 导入公共 X.509/SPKI DER 编码密钥 ``` 对于早于 .NET Standard 2.1 的版本ImportSubjectPublicKeyInfo 不可用,这里需要另一个选项,请参阅我的第一点的编辑部分。

以上是关于在 C# 中使用 RS256(非对称)验证 JWT的主要内容,如果未能解决你的问题,请参考以下文章

Jwt介绍

.NET:RS256 签名的 JWT 中的签名无效

JWT 是不是总是需要访问令牌和刷新令牌?

在 C# 中不对称地验证 JWT

Azure Api 管理 ValidateJWT 与 Rs256 PrivateKey

在 Knock (rails) 中验证用 RS256 签名的 JWT 令牌