Verifying Jwt Signed with the Rs256 Algorithm Using Public Key in C#

Verifying JWT signed with the RS256 algorithm using public key in C#

Thanks to jwilleke, I have got a solution. To verify the RS256 signature of a JWT, it is needed to use the RSAPKCS1SignatureDeformatter class and its VerifySignature method.

Here is the exact code for my sample data:

  string tokenStr = "eyJraWQiOiIxZTlnZGs3IiwiYWxnIjoiUlMyNTYifQ.ewogImlzcyI6ICJodHRwOi8vc2VydmVyLmV4YW1wbGUuY29tIiwKICJzdWIiOiAiMjQ4Mjg5NzYxMDAxIiwKICJhdWQiOiAiczZCaGRSa3F0MyIsCiAibm9uY2UiOiAibi0wUzZfV3pBMk1qIiwKICJleHAiOiAxMzExMjgxOTcwLAogImlhdCI6IDEzMTEyODA5NzAsCiAiY19oYXNoIjogIkxEa3RLZG9RYWszUGswY25YeENsdEEiCn0.XW6uhdrkBgcGx6zVIrCiROpWURs-4goO1sKA4m9jhJIImiGg5muPUcNegx6sSv43c5DSn37sxCRrDZZm4ZPBKKgtYASMcE20SDgvYJdJS0cyuFw7Ijp_7WnIjcrl6B5cmoM6ylCvsLMwkoQAxVublMwH10oAxjzD6NEFsu9nipkszWhsPePf_rM4eMpkmCbTzume-fzZIi5VjdWGGEmzTg32h3jiex-r5WTHbj-u5HL7u_KP3rmbdYNzlzd1xWRYTUs4E8nOTgzAUwvwXkIQhOh5TPcSMBYy6X3E7-_gr9Ue6n4ND7hTFhtjYs3cjNKIA08qm5cpVYFMFMG6PkhzLQ";
string[] tokenParts = tokenStr.Split('.');

RSACryptoServiceProvider rsa = new RSACryptoServiceProvider();
rsa.ImportParameters(
new RSAParameters() {
Modulus = FromBase64Url("w7Zdfmece8iaB0kiTY8pCtiBtzbptJmP28nSWwtdjRu0f2GFpajvWE4VhfJAjEsOcwYzay7XGN0b-X84BfC8hmCTOj2b2eHT7NsZegFPKRUQzJ9wW8ipn_aDJWMGDuB1XyqT1E7DYqjUCEOD1b4FLpy_xPn6oV_TYOfQ9fZdbE5HGxJUzekuGcOKqOQ8M7wfYHhHHLxGpQVgL0apWuP2gDDOdTtpuld4D2LK1MZK99s9gaSjRHE8JDb1Z4IGhEcEyzkxswVdPndUWzfvWBBWXWxtSUvQGBRkuy1BHOa4sP6FKjWEeeF7gm7UMs2Nm2QUgNZw6xvEDGaLk4KASdIxRQ"),
Exponent = FromBase64Url("AQAB")
});

SHA256 sha256 = SHA256.Create();
byte[] hash = sha256.ComputeHash(Encoding.UTF8.GetBytes(tokenParts[0] + '.' + tokenParts[1]));

RSAPKCS1SignatureDeformatter rsaDeformatter = new RSAPKCS1SignatureDeformatter(rsa);
rsaDeformatter.SetHashAlgorithm("SHA256");
if (rsaDeformatter.VerifySignature(hash, FromBase64Url(tokenParts[2])))
MessageBox.Show("Signature is verified");

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

Verify JWT using public key in string

In the case of the .NET Framework (e.g. 4.7.2), a public PKCS#1 key can be imported for example using BouncyCastle (e.g. v1.8.6.1):

using System.IO;
using System.Security.Cryptography;

using Org.BouncyCastle.Crypto;
using Org.BouncyCastle.Crypto.Parameters;
using Org.BouncyCastle.OpenSsl;
using Org.BouncyCastle.Security;

...

string publicPKCS1 = @"-----BEGIN RSA PUBLIC KEY-----
MIIBCgKCAQEA1pjRK+cOnEsh5L1wrt1Tmx+FvyNRj4wuAotlqIWtHS8pIqRzsIOJ
g+tlbUtEXYju+KOcIohZnnmfj0cq28RcQ19HIohqUXypmUbNy9np/M+Aj9NcyKIa
NyuEZ+UhcU/OIStHK4eTUJt+RWL+0Q1/rl49tg7h+toB9Y6Y3SeytXvZhMx3N5qq
mHHNorpfb65bvvHsicGDB7vK5kn55o+C2LhRIxfnw87nKnhPBMRbZg+BKCTXD4sv
z4a2xiR/uM4rxebIBgB3Sm4X1w3yoRr0F3IDhAgXmEhSZ078wm3ohPfuuwymfVhv
dzavm42NwzixZ7n52SowFE2v0DSJh3IbPwIDAQAB
-----END RSA PUBLIC KEY-----";

PemReader pemReader = new PemReader(new StringReader(publicPKCS1));
AsymmetricKeyParameter asymmetricKeyParameter = (AsymmetricKeyParameter)pemReader.ReadObject();
RSAParameters rsaParameters = DotNetUtilities.ToRSAParameters((RsaKeyParameters)asymmetricKeyParameter);

RSACryptoServiceProvider rsa = new RSACryptoServiceProvider();
rsa.ImportParameters(rsaParameters);

...

The validation of the JWT can be done with a JWT library (e.g. System.IdentityModel.Tokens.Jwt) or with a pure cryptography API. Both are described in detail in the answers to the linked question. Regarding the former, see also here, 2nd paragraph.

Verify JWT with RS256 (asymmetric) in C#

  • Regarding your 1st question:

    According to your posted stack trace, you seem to be using .NET Core 3.1. This allows you to easily import your public X.509/SPKI key as follows:

    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() is available since .NET Core 3.0.

    Edit start:
    In earlier versions of .NET Core (before 3.0) or in the .NET Framework ImportSubjectPublicKeyInfo() is not available, so at least .NET Standard 2.1 is required.

    For earlier versions, e.g. .NET Standard 2.0, one possibility is to use BouncyCastle, more precisely its Org.BouncyCastle.OpenSsl.PemReader class, which allows the import of public keys in X509/SPKI format (and, irrelevant for you, also in PKCS#1 format). In this answer you will find an example of how to use PemReader. PemReader processes, as the name suggests, a PEM encoding, i.e. the conversion to a DER encoding (i.e. the removal of header, footer and line breaks, as well as the Base64 decoding of the remainder) as required by ImportSubjectPublicKeyInfo() must not be done. Also note that PemReader expects at least one line break immediately after the header (-----BEGIN PUBLIC KEY-----\n) and a second one immediately before the footer (\n-----END PUBLIC KEY-----), the line breaks in the Base64 encoded body after every 64 characters are optional for PemReader.

    Another possibility is the package opensslkey providing the method opensslkey.DecodeX509PublicKey(), which can process an X509/SPKI key in DER encoding analogous to ImportSubjectPublicKeyInfo. Edit end

  • Regarding your 2nd question:

    There are several .NET standard versions, e.g. .NET Core 3.0 implements .NET Standard 2.1. The package System.IdentityModel.Tokens.Jwt 6.7.2-preview-10803222715 you are using requires .NET Standard 2.0.

    System.IdentityModel.Tokens.Jwt is a package that supports the creation and validation of JSON Web Tokens (JWT). In the case of the posted token, the validation could be implemented as follows:

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

    The validation can be controlled via the validation parameters, i.e. via the 2nd parameter of ValidateToken(). Since the posted token does not contain the claims iss, aud and exp (this can be verified e.g. on https://jwt.io/), they are excluded from the validation in my example.

    In the tutorial Creating And Validating JWT Tokens In ASP.NET Core you will find a more detailed explanation, especially in the chapter Validating A Token.

    ValidateToken() essentially encapsulates the verification process of the JWT signature. A JWT is a data structure that consists of three parts: header, payload and signature, the individual parts being Base64url encoded and separated from each other by a dot.
    The signature is created using various algorithms, e.g. in your case RS256, which means that the data (Base64url encoded header and payload including separator) is signed using the algorithm RSA with PKCS#1 v1.5 padding and digest SHA256.
    The verification of a token corresponds to the verification of the signature, which can also be done solely with cryptographic APIs (i.e. without participation of System.IdentityModel.Tokens.Jwt), as it is done in the accepted answer of the linked question in the comment of @zaitsman.

Verifying a JWT using the public key with C#

Assuming that the environment variable "Userfront_PublicKey" contains a PEM-encoded RSA public key, i.e.:

-----BEGIN RSA PUBLIC KEY-----
(your base64-encoded RSA public key)
-----END RSA PUBLIC KEY-----

then I would try the following (not tested, sorry):

var headers = req.Headers;
if (!headers.TryGetValue("Authorization", out var tokenHeader))
return new StatusCodeResult(StatusCodes.Status403Forbidden);

var token = tokenHeader[0].Replace("Bearer ", "");

var publicKeyPem = Environment.GetEnvironmentVariable("Userfront_PublicKey");
var publicKey = RSA.Create();
publicKey.ImportFromPem(publicKeyPem);

try
{
var json = JwtBuilder.Create()
.WithAlgorithm(new RS256Algorithm(publicKey))
.MustVerifySignature()
.Decode(token);
}
catch (TokenExpiredException)
...
catch (SignatureVerificationException)
...

Verifying JWT (RS256) using OpenSSL

The signature of a JWT is base64url encoded and needs to be decoded first.
The suggested duplicate only deals with a base64 encoded signature and openssl seems not to be working with base64url encoding.

If you're working on a Windows system, you can decode the signature file with certutil, which can directly decode bas64url:

certutil -decode signature.txt signature.sha256

Then use the signature.sha256 as input for openssl:

openssl.exe dgst -sha256 -verify pubkey.pem -signature signature.sha256 data.txt

Then you should get the result:

Verified OK



Related Topics



Leave a reply



Submit