How to sign a JWT using RS256 with RSA private key
Asked Answered
F

11

46

I am using the jose-jwt library and want to create a signed JWT in C# using the RS256 algorithm for encryption. I have no experience with cryptography, so please excuse my ignorance. I see the following example in the docs:

var payload = new Dictionary<string, object>()
{
    { "sub", "[email protected]" },
    { "exp", 1300819380 }
};

var privateKey=new X509Certificate2("my-key.p12", "password", X509KeyStorageFlags.Exportable | X509KeyStorageFlags.MachineKeySet).PrivateKey as RSACryptoServiceProvider;

string token=Jose.JWT.Encode(payload, privateKey, JwsAlgorithm.RS256);

which shows the use of a p12 file, but how do I use an RSA key file of the form below? I am looking at the docs for X509Certificate2, but I see no option for RSA private keys. It appears to only accept PKCS7, which I understand to be public keys.

-----BEGIN RSA PRIVATE KEY-----
MIICXAIBAAKBgQCqGKukO1De7zhZj6+H0qtjTkVxwTCpvKe4eCZ0FPqri0cb2JZfXJ/DgYSF6vUp
wmJG8wVQZKjeGcjDOL5UlsuusFncCzWBQ7RKNUSesmQRMSGkVb1/3j+skZ6UtW+5u09lHNsj6tQ5
1s1SPrCBkedbNf0Tp0GbMJDyR4e9T04ZZwIDAQABAoGAFijko56+qGyN8M0RVyaRAXz++xTqHBLh
3tx4VgMtrQ+WEgCjhoTwo23KMBAuJGSYnRmoBZM3lMfTKevIkAidPExvYCdm5dYq3XToLkkLv5L2
pIIVOFMDG+KESnAFV7l2c+cnzRMW0+b6f8mR1CJzZuxVLL6Q02fvLi55/mbSYxECQQDeAw6fiIQX
GukBI4eMZZt4nscy2o12KyYner3VpoeE+Np2q+Z3pvAMd/aNzQ/W9WaI+NRfcxUJrmfPwIGm63il
AkEAxCL5HQb2bQr4ByorcMWm/hEP2MZzROV73yF41hPsRC9m66KrheO9HPTJuo3/9s5p+sqGxOlF
L0NDt4SkosjgGwJAFklyR1uZ/wPJjj611cdBcztlPdqoxssQGnh85BzCj/u3WqBpE2vjvyyvyI5k
X6zk7S0ljKtt2jny2+00VsBerQJBAJGC1Mg5Oydo5NwD6BiROrPxGo2bpTbu/fhrT8ebHkTz2epl
U9VQQSQzY1oZMVX8i1m5WUTLPz2yLJIBQVdXqhMCQBGoiuSoSjafUhV7i1cEGpb88h5NBYZzWXGZ
37sJ5QsW+sJyoNde3xH8vdXhzU7eT82D6X/scw9RZz+/6rCJ4p0=
-----END RSA PRIVATE KEY-----

Finally, what is the difference between the two options listed in the docs, and how do I choose between the two?

-------------------------- OPTION 1 --------------------------

RS-* and PS-* family

CLR:

RS256, RS384, RS512 and PS256, PS384, PS512 signatures require RSACryptoServiceProvider (usually private) key of corresponding length. CSP need to be forced to use Microsoft Enhanced RSA and AES Cryptographic Provider. Which usually can be done be re-importing RSAParameters. See http://clrsecurity.codeplex.com/discussions/243156 for details.

-------------------------- OPTION 2 --------------------------

CORECLR: RS256, RS384, RS512 signatures require RSA (usually private) key of corresponding length.

Fromenty answered 5/8, 2016 at 17:17 Comment(2)
Just to check... the key in your post... that's a test key, right?Cadmus
@Cadmus yes, I didn't want to show my real key.Fromenty
S
86

I know this post is old, but it took me forever to figure this out, so I thought I would share.

To test I created RSA keys using OpenSSL:

openssl genrsa -out privateKey.pem 512
openssl rsa -in privateKey.pem -pubout -out publicKey.pem

You will need the following 2 nuget packages:

  1. https://github.com/dvsekhvalnov/jose-jwt
  2. http://www.bouncycastle.org/csharp/

Test Code

public static void Test()
{
        string publicKey = File.ReadAllText(@"W:\Dev\Temp\rsa_keys\publicKey.pem");
        string privateKey = File.ReadAllText(@"W:\Dev\Temp\rsa_keys\privateKey.pem");
        
        var claims = new List<Claim>();
        claims.Add(new Claim("claim1", "value1"));
        claims.Add(new Claim("claim2", "value2"));
        claims.Add(new Claim("claim3", "value3"));

        var token = CreateToken(claims, privateKey);
        var payload = DecodeToken(token, publicKey);
}

Create Token

public static string CreateToken(List<Claim> claims, string privateRsaKey)
{
     RSAParameters rsaParams;
     using (var tr = new StringReader(privateRsaKey))
     {
          var pemReader = new PemReader(tr);
          var keyPair = pemReader.ReadObject() as AsymmetricCipherKeyPair;
          if (keyPair == null)
          {
               throw new Exception("Could not read RSA private key");
          } 
          var privateRsaParams = keyPair.Private as RsaPrivateCrtKeyParameters;
          rsaParams = DotNetUtilities.ToRSAParameters(privateRsaParams);
     }
     using (RSACryptoServiceProvider rsa = new RSACryptoServiceProvider())
     {
          rsa.ImportParameters(rsaParams);
          Dictionary<string, object> payload = claims.ToDictionary(k => k.Type, v => (object)v.Value);
          return Jose.JWT.Encode(payload, rsa, Jose.JwsAlgorithm.RS256);
     }
}

Decode Token

public static string DecodeToken(string token, string publicRsaKey)
{
     RSAParameters rsaParams;

     using (var tr = new StringReader(publicRsaKey))
     {
          var pemReader = new PemReader(tr);
          var publicKeyParams = pemReader.ReadObject() as RsaKeyParameters;
          if (publicKeyParams == null)
          {
               throw new Exception("Could not read RSA public key");
          }
          rsaParams = DotNetUtilities.ToRSAParameters(publicKeyParams);
     }
     using (RSACryptoServiceProvider rsa = new RSACryptoServiceProvider())
     {
          rsa.ImportParameters(rsaParams);
          // This will throw if the signature is invalid
          return Jose.JWT.Decode(token, rsa, Jose.JwsAlgorithm.RS256);  
     }
}

I found https://jwt.io/ a great resource to test your tokens

Side answered 1/7, 2017 at 5:15 Comment(8)
Thanks @Roche, this is THE answer. I was wasting days and days. Thanks!Disquisition
Thank you! I was desperate until I find your answer!Boulanger
What if my private token looks like: -----BEGIN PRIVATE KEY----- {some string data} -----END PRIVATE KEY-----. And could you please say how to generate token using RS512 in this case?Kelso
I was getting this error: "Could not read RSA private key". This was a cast problem in line: "pemReader.ReadObject() as Org.BouncyCastle.Crypto.AsymmetricCipherKeyPair"... So, I changed to: var privateRsaParams = pemReader.ReadObject() as Org.BouncyCastle.Crypto.Parameters.RsaPrivateCrtKeyParameters;Carleencarlen
Thanks @Roche, really helpfullAguirre
@Side The problem is that I can decode it with ant Public key I generate. doesn't seems so secured.Sciential
On jwt.io ut says it has an invalid signature.Will
this code always throws "Could not read RSA public key"Mainland
D
19

I used http://travistidwell.com/jsencrypt/demo/ to create 2048 bit keys

Using .NET 6:

      string CreateRsaToken(string someClaimValue)
      {
var rsaPrivateKey = @"-----BEGIN RSA PRIVATE KEY-----
MIIEogIBA...";
    
        using var rsa = RSA.Create();
        rsa.ImportFromPem(rsaPrivateKey);
    
        var signingCredentials = new SigningCredentials(new RsaSecurityKey(rsa), SecurityAlgorithms.RsaSha256)
        {
          CryptoProviderFactory = new CryptoProviderFactory { CacheSignatureProviders = false }
        };
    
        var now = DateTime.Now;
    
        var jwt = new JwtSecurityToken(
          audience: "all",
          issuer: "TheBitCoinKing",
          claims: new[] {
    
            new Claim("SomeClaim", someClaimValue),
          },
          notBefore: now,
          expires: now.AddMinutes(1),
          signingCredentials: signingCredentials
        );
    
        return new JwtSecurityTokenHandler().WriteToken(jwt);
      }

packages:

Microsoft.IdentityModel.Tokens

System.IdentityModel.Tokens.Jwt
Decimalize answered 17/6, 2022 at 14:43 Comment(1)
You, sir, are an absolute legend. This code snippet pointed me in the right direction.Sorrow
C
9

The key to this question is using JWT and Bouncy castle libraries for encoding the token and signing it respectively.

  1. JWT for encoding and decoding JWT tokens
  2. Bouncy Castle supports encryption and decryption, especially RS256 get it here

First, you need to transform the private key to the form of RSA parameters. Then you need to pass the RSA parameters to the RSA algorithm as the private key. Lastly, you use the JWT library to encode and sign the token.

    public string GenerateJWTToken(string rsaPrivateKey)
    {
        var rsaParams = GetRsaParameters(rsaPrivateKey);
        var encoder = GetRS256JWTEncoder(rsaParams);

        // create the payload according to your need
        var payload = new Dictionary<string, object>
        {
            { "iss", ""},
            { "sub", "" },
            // and other key-values 
        };

        // add headers. 'alg' and 'typ' key-values are added automatically.
        var header = new Dictionary<string, object>
        {
            { "{header_key}", "{your_private_key_id}" },
        };

        var token = encoder.Encode(header,payload, new byte[0]);

        return token;
    }

    private static IJwtEncoder GetRS256JWTEncoder(RSAParameters rsaParams)
    {
        var csp = new RSACryptoServiceProvider();
        csp.ImportParameters(rsaParams);

        var algorithm = new RS256Algorithm(csp, csp);
        var serializer = new JsonNetSerializer();
        var urlEncoder = new JwtBase64UrlEncoder();
        var encoder = new JwtEncoder(algorithm, serializer, urlEncoder);

        return encoder;
    }

    private static RSAParameters GetRsaParameters(string rsaPrivateKey)
    {
        var byteArray = Encoding.ASCII.GetBytes(rsaPrivateKey);
        using (var ms = new MemoryStream(byteArray))
        {
            using (var sr = new StreamReader(ms))
            {
                // use Bouncy Castle to convert the private key to RSA parameters
                var pemReader = new PemReader(sr);
                var keyPair = pemReader.ReadObject() as AsymmetricCipherKeyPair;
                return DotNetUtilities.ToRSAParameters(keyPair.Private as RsaPrivateCrtKeyParameters);
            }
        }
    }

ps: the RSA private key should have the following format:

-----BEGIN RSA PRIVATE KEY-----

{base64 formatted value}

-----END RSA PRIVATE KEY-----

Checkrein answered 22/5, 2019 at 3:57 Comment(1)
Looking for same in .net core but not able to do it. Can you post some sample private Rsa key from which this code work? I have a RSA Public /private key in -----BEGIN PUBLIC KEY----- -----END PUBLIC KEY----- format. anyway we can work from it?Damales
V
3

NET 5.0 or later has a method called "ImportFromPem", so you can read private-key in PEM format as it is.

void GetToken(System.IO.FileInfo privateKey, string service_account, string client_id)
{
    using (var rsa = System.Security.Cryptography.RSA.Create())
    {
        rsa.ImportFromPem(System.IO.File.ReadAllText(privateKey.FullName));

        var descriptor = new Microsoft.IdentityModel.Tokens.SecurityTokenDescriptor
        {
            Issuer = client_id,
            Claims = new System.Collections.Generic.Dictionary<string, object>() { ["sub"] = service_account },
            SigningCredentials = new Microsoft.IdentityModel.Tokens.SigningCredentials(new Microsoft.IdentityModel.Tokens.RsaSecurityKey(rsa), SecurityAlgorithms.RsaSha256),
            IssuedAt = System.DateTime.UtcNow,
            Expires = System.DateTime.UtcNow.AddMinutes(60),
        };

        var handler = new System.IdentityModel.Tokens.Jwt.JwtSecurityTokenHandler();
        string token = handler.WriteToken(handler.CreateJwtSecurityToken(descriptor));
    }
}

Nuget: 「System.IdentityModel.Tokens.Jwt」

And, maybe error: cannot access a disposed object. object name 'rsa' -> Ref

        var signingCredentials = new SigningCredentials(rsaSecurityKey, SecurityAlgorithms.RsaSha256)
        {
            CryptoProviderFactory = new CryptoProviderFactory { CacheSignatureProviders = false }
        };
Veiled answered 10/11, 2022 at 5:26 Comment(3)
Thank you. Very nice & clear answer. Can you please give idea for validating rsa tokenTransposition
@Transposition thank you for comment, token verification I have never done it but it seems that there is a VerifySignature function, this here hope it helps ^^Veiled
@Transposition Or this hereVeiled
J
2

If you want to use a certificate, you can retrieve it by it's thumbprint using this method

private X509Certificate2 GetByThumbprint(string Thumbprint)
{
    var localStore = new X509Store(StoreName.My, StoreLocation.LocalMachine);
    localStore.Open(OpenFlags.ReadOnly);
    return localStore.Certificates//.Find(X509FindType.FindByKeyUsage, X509KeyUsageFlags.DigitalSignature, false)
        .Find(X509FindType.FindByThumbprint, Thumbprint, false)
        .OfType<X509Certificate2>().First();
}

and then:

private JwtSecurityToken GenerateJWT()
{
    var securityKey = new Microsoft.IdentityModel.Tokens.X509SecurityKey(GetByThumbprint("YOUR-CERT-THUMBPRINT-HERE"));

    var credentials = new Microsoft.IdentityModel.Tokens.SigningCredentials(securityKey, "RS256");

    var JWTHeader = new JwtHeader(credentials);

    var payload = new JwtPayload
    {
        { "iss", "Issuer-here"},
        { "exp", (Int32)(DateTime.UtcNow.AddHours(1).Subtract(new DateTime(1970, 1, 1))).TotalSeconds},
        { "iat", (Int32)(DateTime.UtcNow.Subtract(new DateTime(1970, 1, 1))).TotalSeconds}
    };

    var token = new JwtSecurityToken(JWTHeader, payload);
    return token;
}
Jutland answered 30/4, 2018 at 9:25 Comment(2)
How come you declare var credentials after using it? Did you test this?Larva
You are right, i screwed up the order. But you get the idea of the code...Jutland
G
2

Using BouncyCastle and Jose nuget package, the following code works for me.

public static string CreateToken(Dictionary<string, object> payload)
{
  string jwt = string.Empty;
  RsaPrivateCrtKeyParameters keyPair;

  var cert = ConfigurationManager.AppSettings["cert"];
  /// cert begins -----BEGIN PRIVATE KEY----- and ends with -END PRIVATE KEY-----";

  using (var sr = new StringReader(cert))
  {
    PemReader pr = new PemReader(sr);
    keyPair = (RsaPrivateCrtKeyParameters)pr.ReadObject();
  }

  RSAParameters rsaParams = DotNetUtilities.ToRSAParameters(keyPair);

  using (RSACryptoServiceProvider rsa = new RSACryptoServiceProvider())
  {
    rsa.ImportParameters(rsaParams);

    jwt = Jose.JWT.Encode(payload, rsa, Jose.JwsAlgorithm.RS256);
  }

  return jwt;
}
Gordon answered 31/5, 2020 at 9:28 Comment(0)
S
1

The GetRSAPrivateKey is only available in .NET 4.6. See the URL below.

https://msdn.microsoft.com/en-us/library/system.security.cryptography.x509certificates.rsacertificateextensions.getrsaprivatekey(v=vs.110).aspx

Suck answered 23/8, 2016 at 17:21 Comment(0)
D
1

Posting the code to create RS256 JWT token for GCP OAuth Token API call, using the Service Account JSON Key-:

using JWT;
using JWT.Algorithms;
using JWT.Serializers;
using Org.BouncyCastle.Crypto.Parameters;
using Org.BouncyCastle.Security;
using System;
using System.Collections.Generic;
using System.Security.Cryptography;

namespace GCP
{
    class JWTTokenGenerationForGCPOAuthTokenAPI
    {


        public static string GenerateJWTToken()
        {
            var rsaParams = ReadAsymmetricKeyParameter();
            var encoder = GetRS256JWTEncoder(rsaParams);
            var iat = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
            var exp = DateTimeOffset.UtcNow.AddMinutes(60).ToUnixTimeSeconds();

            // create the payload according to your need 
            // iss is the Service Account Email ID
            var payload = new Dictionary<string, object>
            {
                { "iss",   "<service-account>@<project-id>.iam.gserviceaccount.com"},
                { "scope", "https://www.googleapis.com/auth/cloud-platform" },
                { "aud",   "https://oauth2.googleapis.com/token" },
                { "exp",    exp},
                { "iat",    iat}
            };
            //Final token
            var token = encoder.Encode(payload, new byte[0]);

            return token;
        }

        private static IJwtEncoder GetRS256JWTEncoder(RSAParameters rsaParams)
        {
            var csp = new RSACryptoServiceProvider();
            csp.ImportParameters(rsaParams);
            var algorithm = new RS256Algorithm(csp, csp);
            var serializer = new JsonNetSerializer();
            var urlEncoder = new JwtBase64UrlEncoder();
            var encoder = new JwtEncoder(algorithm, serializer, urlEncoder);

            return encoder;
        }



        public static RSAParameters ReadAsymmetricKeyParameter()


        {

\\ This key is fetched from the GCP Service Account JSON File. 
\\"private_key": "-----BEGIN PRIVATE KEY-----\n<long-code>-----END PRIVATE KEY-----\n",
\\ pick <long-code> from above. Replace all \n with actual new line like shown below.

                string pkey = @"MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQDSoGKK/Dzb8MBy
################################################################
################################################################
################################################################
################################################################
twySMqKKWnIC/zZljrvp4w==";



            RsaPrivateCrtKeyParameters rsaPrivateCrtKeyParameters1;
            var keyBytes = Convert.FromBase64String(pkey);
            var asymmetricKeyParameter = PrivateKeyFactory.CreateKey(keyBytes);


            rsaPrivateCrtKeyParameters1 = (RsaPrivateCrtKeyParameters)asymmetricKeyParameter;

            RSAParameters r = DotNetUtilities.ToRSAParameters(rsaPrivateCrtKeyParameters1);

            return r;


        }
    }

}

Code Done in : .NET Framework 4.6.1

Nuget Packages :

Bounty Castle - Install-Package BouncyCastle -Version 1.8.6.1

Diatom answered 29/6, 2020 at 8:14 Comment(0)
M
1

I thought I would add my findings as I had to do this last night. The examples in this page really helped but they didn't work straightaway.

In my specific case, I was trying to generate a JWT token for DocuSign, for some other reason I can't use their SDK and generating the JWT token manually was the right approach for my use case.

var privateKeybyteArray = Encoding.ASCII.GetBytes(@"-----BEGIN RSA PRIVATE KEY----- 
xxxxxxxxxxxxxxxxxx 
-----END RSA PRIVATE KEY-----");

var payload = new Dictionary<string, object>
{
    { "iss", "3a31fd58-xxxx-xxxx-xxxx-17639ade3c1b" },
    { "sub", "40a3a606-xxxx-xxxx-xxxx-762c6e7dadb6" },
    { "aud", "account-d.docusign.com" },
    { "iat", DateTime.UtcNow.Subtract(new DateTime(1970, 1, 1)).TotalSeconds },
    { "exp", DateTime.UtcNow.AddHours(1).Subtract(new DateTime(1970, 1, 1)).TotalSeconds }, 
    { "scope",  "signature" }
};

var rsaPrivateKey = new RSAParameters();
using (var ms = new MemoryStream(privateKeybyteArray))
{
    using (var sr = new StreamReader(ms))
    {
        var pemReader = new PemReader(sr);
        var keyPair = pemReader.ReadObject() as AsymmetricCipherKeyPair;
        rsaPrivateKey = DotNetUtilities.ToRSAParameters(keyPair.Private as RsaPrivateCrtKeyParameters);
    }
}

var csprivate = new RSACryptoServiceProvider();
csprivate.ImportParameters(rsaPrivateKey);

var algorithm = new RS256Algorithm(csprivate, csprivate);
var serializer = new JsonNetSerializer();
var urlEncoder = new JwtBase64UrlEncoder();
var encoder = new JwtEncoder(algorithm, serializer, urlEncoder);

var token = encoder.Encode(payload, privateKeybyteArray);

I used the JWT-dotnet package. I found the website jsonwebtoken.io really good as it generates the .NET code needed for generating the token, it didn't quite work but it helped for figuring out what I was doing wrong

enter image description here

Mouth answered 9/3, 2021 at 21:22 Comment(0)
S
0

If you use a public certificate and .NET 4.6,for decoding you can use:

string token = "eyJhbGciOiJSUzI1NiIsInR....";
string certificate = "MIICnzCCAYcCBgFd2yEPx....";
var publicKey = new X509Certificate2(Convert.FromBase64String(certificate )).GetRSAPublicKey();
string decoded = JWT.Decode(token, publicKey, JwsAlgorithm.RS256);
Sassy answered 23/8, 2017 at 15:58 Comment(0)
E
-1
  1. RS256 is a Signature Algorithm not an Encryption Algorithm
  2. Encryption is done with the public key
  3. Here is the code to create an encrypted JWT:

    var cert = new X509Certificate2(".\\key.cer");
    var rsa = (RSACryptoServiceProvider) cert.PublicKey.Key;
    
    var payload = new Dictionary<string, object>()
    {
      {"sub", "[email protected]"},
      {"exp", 1300819380}
    };
    
    var encryptedToken =
      JWT.Encode(
        payload,
        rsa,
        JweAlgorithm.RSA_OAEP,
        JweEncryption.A256CBC_HS512,
        null);
    
Excommunicatory answered 20/9, 2017 at 13:4 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.