h31mdallr
h31mdallr

Reputation: 23

Difficulty signing JWT in .NET Framework 4.5 using pre-generated ES512 private key

The goal

I'm trying to make a simple HTTP Post request to an e-payment processing API, and their specifications require me to add a Bearer token to the payload. I.e., rest_request.AddHeader("Authorization", "Bearer " + myToken), the token being a JWT like "abcdefg.hijklmop.qrstuvwxyz".

They have provided an ES512 private PEM key to encode into the JWT as a secret, but I have struggled to understand what exactly I need to do to my key (formatted like below).

-----BEGIN PRIVATE KEY-----
...
-----END PRIVATE KEY-----

Environment

I am using .NET Framework 4.5 (it's a legacy system), VB.NET for language, and I have been given a key ID and private key by the API provider.

The API provider needs iss, aud, iat, exp, kid, and jti values in the headers and claims. (I'm not 100% sure what the issuer and audience values should be or why they should be what they are, so any pointers there would also be helpful.)

Roadblocks

Things I've tried

Tldr, I have an ES512 PEM key stored as a string that I need to encode as a secret in a JWT Bearer token, but I am stuck in .NET Framework 4.5 and know very little about encryption. Everything I've tried has either crashed or resulted in a 401 unauthorized error response from the API endpoint.

Upvotes: 0

Views: 288

Answers (2)

Topaco
Topaco

Reputation: 49371

.NET Framework has no support for PEM encoded keys, but is able to import DER encoded PKCS#8 keys into a CngKey, so that the jose-jwt library can be used (your PEM encoded PKCS#8 key can be converted to a DER encoded key by removing header, footer and all line breaks, and Base64 decoding the rest).

Using a test key and dummy values for the claims a possible VB.NET implementation is (tested on .NET Framework 4.5 and with jose-jwt 5.1.0):

Imports System
Imports System.Security.Cryptography
Imports Jose

Public Module module1
    Public Sub Main()
        Dim privatePkcs8Pem = "-----BEGIN PRIVATE KEY-----
MIHuAgEAMBAGByqGSM49AgEGBSuBBAAjBIHWMIHTAgEBBEIBiyAa7aRHFDCh2qga
9sTUGINE5jHAFnmM8xWeT/uni5I4tNqhV5Xx0pDrmCV9mbroFtfEa0XVfKuMAxxf
Z6LM/yKhgYkDgYYABAGBzgdnP798FsLuWYTDDQA7c0r3BVk8NnRUSexpQUsRilPN
v3SchO0lRw9Ru86x1khnVDx+duq4BiDFcvlSAcyjLACJvjvoyTLJiA+TQFdmrear
jMiZNE25pT2yWP1NUndJxPcvVtfBW48kPOmvkY4WlqP5bAwCXwbsKrCgk6xbsp12
ew==
-----END PRIVATE KEY-----"
        Dim privatePkcs8Der = convertPkcs8PemToDer(privatePkcs8Pem)
        Dim privateEcKey = CngKey.Import(privatePkcs8Der, CngKeyBlobFormat.Pkcs8PrivateBlob)
        Dim header = New Dictionary(Of String, Object) From {
            {"typ", "JWT"},
            {"kid", "dcaaddd0-ea67-45bd-bc6b-b35ec6c7fd63"}
        }
        Dim payload = New Dictionary(Of String, Object)() From {
            {"iss", "me"},
            {"aud", "you"},
            {"iat", 1731148967},
            {"exp", 1762681367},
            {"jti", "fc19e8f0-e3bf-49c5-a738-df1461c59d36"}
        }
        Dim jwtSigned = Jose.JWT.Encode(payload, privateEcKey, JwsAlgorithm.ES512, header)
        Console.WriteLine(jwtSigned)
    End Sub

    Private Function convertPkcs8PemToDer(ByVal pkcs8Pem As String) As Byte()
        Dim body As String = pkcs8Pem.Replace("-----BEGIN PRIVATE KEY-----", "").Replace("-----END PRIVATE KEY-----", "").Replace(Environment.NewLine, "")
        Return Convert.FromBase64String(body)
    End Function
End Module

Note that JOSE applies the non-deterministic variant for ES512 and ECDSA, i.e. a different signature is generated each time (even if the header, payload and key are identical).

The signed JWTs obtained with the above code can be verified with jwt.io, e.g. here, using the following public key:

-----BEGIN PUBLIC KEY-----
MIGbMBAGByqGSM49AgEGBSuBBAAjA4GGAAQBgc4HZz+/fBbC7lmEww0AO3NK9wVZ
PDZ0VEnsaUFLEYpTzb90nITtJUcPUbvOsdZIZ1Q8fnbquAYgxXL5UgHMoywAib47
6MkyyYgPk0BXZq3mq4zImTRNuaU9slj9TVJ3ScT3L1bXwVuPJDzpr5GOFpaj+WwM
Al8G7CqwoJOsW7Kddns=
-----END PUBLIC KEY-----

Another solution should be mentioned briefly: C#/BouncyCastle supports the import of PEM encoded keys and also ECDSA with P-521 and SHA-512 (which corresponds to ES512). Thus, the signed token can also be determined this way.
The advantage is that no CngKey type is required for this (e.g. meaning that a wider range of formats is supported), the disadvantage is that BouncyCastle is not a JSON library, so the JSON functionalities have to be implemented by yourself (which however is not particularly complex for JWS and a specific algorithm).

Upvotes: 1

h31mdallr
h31mdallr

Reputation: 23

Many thanks to Topaco for pointing me in the right direction. Using their code, I was able to make some more progress. But, when I tried their solution, I still ran into this weird could not find file specified error. However, all I needed to do was adjust some IIS settings to resolve that issue. I set up a new app pool, enabled 32-bit applications, and set "Load user profile" to True.

Unfortunately, I still couldn't generate valid tokens. One issue was getting the right date format, so after some more Googling I modified some code I found to get dates generating in the correct format.

Later, after meeting with a developer from the payment processor, I realized I was trying to validate my tokens incorrectly. I didn't have the public key to go with my private key, so I was doomed to get "Invalid Token" on jwt.io. I then checked the tokens generated with Topaco's first solution using CngKey's, and my token was indeed valid. Once I knew there wasn't an issue with the tokens, I also fixed the API link I was using and voila, my requests were going through!

All in all, here's a simplified version of the code I have ended up with. All this requires is installing jose-jwt and RestSharp NuGet packages.

Imports Jose
Imports RestSharp
Imports System.Net
Imports System.Net.Http
Imports System.Security.Cryptography
Imports System.Web.Http

Module MyModule

    Public Const KEY_ID = "...."

    Public Const PRIVATE_KEY =
"-----BEGIN PRIVATE KEY-----
.....
-----END PRIVATE KEY-----"

    'Gets proper format for JWT time
    Public Function GetSecondsSinceEpoch(utcTime As DateTime) As Int64
        Return (utcTime - New DateTime(1970, 1, 1, 0, 0, 0)).TotalSeconds
    End Function

    'Removes the enclosure and any whitespace
    Public Shared Function ConvertPkcs8PemToDer(ByVal pkcs8Pem As String) As Byte()
        Dim body As String = pkcs8Pem.Replace("-----BEGIN PRIVATE KEY-----", "").Replace("-----END PRIVATE KEY-----", "").Replace(Environment.NewLine, "")
        Dim reg = New Regex("\s")
        body = reg.Replace(body, "")
        Return Convert.FromBase64String(body)
    End Function

    'Creates a UUID-formatted string.
    Public Shared Function CreateJti() As String
        Return Guid.NewGuid().ToString()
    End Function

    Public Function MakeMyRequest(uri As String, json_body As String) As String

        'Fixes a weird .NET bug
        ServicePointManager.SecurityProtocol = SecurityProtocolType.Ssl3 Or SecurityProtocolType.Tls12 Or SecurityProtocolType.Tls11 Or SecurityProtocolType.Tls
        
        'Create RestRequest things 
        Dim rest_uri As New Uri(uri) 'Uri of the API to call
        Dim rest_client As New RestClient With {.BaseUrl = rest_uri}
        Dim rest_request As New RestRequest With {.Method = Method.POST} 'Or whatever Method your API requires
        Dim rest_response As New RestResponse

        'Add JSON body and headers
        rest_request.AddJsonBody(json_body) 'Make sure this matches the API specifications
        rest_request.AddHeader("accept", "application/json")
        rest_request.AddHeader("Content-Type", "application/json")

        'Conver the PEM string to Pkcs8 format, acceptable to CngKey.Import()
        Dim privatePkcs8Der As Byte() = ConvertPkcs8PemToDer(PRIVATE_KEY)
        Dim privateEcKey As CngKey = CngKey.Import(privatePkcs8Der, CngKeyBlobFormat.Pkcs8PrivateBlob)

        'Claims and headers
        Dim iss = "MyIssuer"
        Dim aud = "MyAudience"
        Dim iat = GetSecondsSinceEpoch(Now().ToUniversalTime()) '.ToUniversalTime ensures your local UTC offset doesn't affect the timestamp
        Dim exp = GetSecondsSinceEpoch(Now().AddMinutes(5).ToUniversalTime())
        Dim kid = KEY_ID

        Dim headers = New Dictionary(Of String, Object)() From {
            {"alg", "ES512"},
            {"typ", "JWT"},
            {"kid", KEY_ID} 'Key ID may not be necessary for your scenario, but put whatever extra parameters are required here like username, password, etc
        }

        Dim payload = New Dictionary(Of String, Object)() From {
            {"iss", iss},
            {"aud", aud},
            {"iat", iat},
            {"exp", exp},
            {"jti", CreateJti()}
        }

        'Generate the token using the payload, CngKey, ES512 algorithm, and extra headers
        Dim token = Jose.JWT.Encode(payload, privateEcKey, JwsAlgorithm.ES512, headers)

        'Excecute the request
        rest_request.AddHeader("Authorization", "Bearer " & token)
        rest_response = rest_client.Execute(rest_request)

        'Check for your API's designated response code
        If rest_response.StatusCode <> 201 Then Throw New Exception("An error occurred processing the request.")
        Return rest_response.Content

    End Function

End Module

Upvotes: 0

Related Questions