Reputation: 23
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-----
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.)
ECDSa.ImportFromPem
does exist, but only in .NET 6.message
string used for? Does it matter?x
, y
, and d
parameters.PemReader
to no avail.CngKey
object. (This is my best lead so far.)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
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
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