How to convert JWK (IdentityServ 4) to public key pem? - c#

I have an RSA512 JWKS released by Identity Server 4 which is used by clients to validate the token signature. The problem is that I also wanted to validate it on the https://jwt.io/ site but I don't know how to transform the parameters:
in a public key such as this:
-----BEGIN PUBLIC KEY-----
MIIBCgKCAQEA+xGZ/wcz9ugFpP07Nspo6U17l0YhFiFpxxU4pTk3Lifz9R3zsIsu
ERwta7+fWIfxOo208ett/jhskiVodSEt3QBGh4XBipyWopKwZ93HHaDVZAALi/2A
+xTBtWdEo7XGUujKDvC2/aZKukfjpOiUI8AhLAfjmlcD/UZ1QPh0mHsglRNCmpCw mwSXA9VNmhz+PiB+Dml4WWnKW/VHo2ujTXxq7+efMU4H2fny3Se3KYOsFPFGZ1TN
QSYlFuShWrHPtiLmUdPoP6CV2mML1tk+l7DIIqXrQhLUKDACeM5roMx0kLhUWB8P
+0uj1CNlNN4JRZlC7xFfqiMbFRU9Z4N6YwIDAQAB
-----END PUBLIC KEY-----
that can be used on the site to validate the Token. Is there any kind of library or something free that I can use to do this? At the moment I found this library, but is it paid.
https://www.example-code.com/mono/publickey_rsa_load_jwk.asp
UPDATE
I have tried this snippet of code:
using var httpClient = new HttpClient();
var json = httpClient.GetStringAsync(
"http://..../.well-known/openid-configuration/jwks").GetAwaiter()
.GetResult();
var jsonWebKeySet = new JsonWebKeySet(json);
var jsonWebKey = jsonWebKeySet.Keys.First();
var rsaProvider = new RSACryptoServiceProvider();
rsaProvider.ImportParameters(new RSAParameters
{
Modulus = Base64Url.Decode(jsonWebKey.N),
Exponent = Base64Url.Decode(jsonWebKey.E)
});
var publicKey = Base64Url.Encode(rsaProvider.ExportRSAPublicKey());
Using the Base64Url class of the Jose library, but I get a public key in Base64Url that is not safe, that is, containing the characters _ and -. I also tried to do the substitution of the relative characters with / and +, but anyway and I always get Invalid Signature, while with the other library everything works. What am I doing wrong? Thanks
UPDATE
Thanks to #Topaco's suggestion I modified the last line of code like this:
var publicKey = Convert.ToBase64String(rsaProvider.ExportSubjectPublicKeyInfo());
and after having correctly indented the key and inserted the header
-----BEGIN PUBLIC KEY-----
and footer
-----END PUBLIC KEY-----
the validation of the JWT on the site https://jwt.io/ is done correctly. Thanks

.....
var rsaProvider = new RSACryptoServiceProvider(2048);
.....
string publicKey = $#"-----BEGIN PUBLIC KEY-----
{Base64Url.Encode(rsaProvider.ExportSubjectPublicKeyInfo()).Replace("-", "+").Replace("_", "/")}
-----END PUBLIC KEY-----
";
rsaPublicKey.ImportFromPem(publicKey);

Related

How do I verify jwt token with this public key in Node.js?

We have a legacy code that verifies a jwt token with a public key, and it is written in C#. The key is in this format
{
"keys": [
{
"kty": "RSA",
"kid": "xyz...",
"n": "abc...",
"e": "AQAB"
}
]
}
It is deserialised to RSASecurityKey type. The C# then verifies jwt token by assigning the public key to the IssuerSigningKey field.
TokenValidationParameters validationParams =
new TokenValidationParameters()
{
RequireExpirationTime = true,
...
IssuerSigningKey = **securityKey**,
...
};
tokenHandler.ValidateToken(jwtToken, validationParams, out securityToken);
Now I want to port this code to Node.JS and I've been studying this method jwt.verify(token, key). The key param is string type. How do make the original key string to a format that is accepted in Node? Thanks
Edit:
Now I managed to get the original key into this format which seems to be the right one.
-----BEGIN PUBLIC KEY-----
MIIBI...
....
...........................Tf6LHUDEh/hm
7QIDAQAB
-----END PUBLIC KEY-----
I used this library jwk-to-pem to convert it.
Now I run the verify it shows JsonWebTokenError: invalid signature
Is there anything else I'm doing wrong?
Figured this out, if you are not very familiar with Node like me, you'll need to use the jwk-to-pem package to convert it to the pem format.
Make sure your signing key and verifying key are the same. Then the verification shouldn't have any problem.

Convert from PKCS1 to PKCS8 in C#

I am using a C# google SDK to access Google Drive and GMail. For authentication, sometimes I'm provided a PKCS8 private key and other times it is a PKCS1 private key. I can make the PKCS8 authentication work, since that is what Google SDK expects. However, when I attempt to convert the PKCS1 into a PKCS8...it seems to work at first, until I actually start to make invocations against google's API.
Based on this advice, here is the code I'm using that is designed to receive PKCS1 or PKCS8:
string pkcs8Key = null;
if (creds.PrivateKey.Contains("BEGIN RSA"))
{
// Handle PKCS1
var keyParts = creds.PrivateKey.Split("-----", StringSplitOptions.RemoveEmptyEntries);
var rawKey = keyParts.OrderByDescending(s => s.Length).First();
var base64Key = rawKey.Trim();
var privateKeyBytes = Convert.FromBase64String(base64Key);
using var privateKey = RSA.Create();
privateKey.ImportRSAPrivateKey(privateKeyBytes, out _);
var keyBytes = privateKey.ExportPkcs8PrivateKey();
pkcs8Key = $"-----BEGIN PRIVATE KEY-----{Convert.ToBase64String(keyBytes)}-----END PRIVATE KEY-----\n";
}
else
{
// Handle PKCS8
pkcs8Key = creds.PrivateKey;
}
initializer = new BaseClientService.Initializer
{
HttpClientInitializer = new ServiceAccountCredential(
new ServiceAccountCredential.Initializer(creds.ID)
{
Scopes = new[] { DirectoryService.Scope.AdminDirectoryUserReadonly, DriveService.Scope.Drive },
User = email ?? creds.AdminAccount,
KeyId = creds.PrivateKeyId,
ProjectId = creds.ProjectId
}.FromPrivateKey(pkcs8Key))
};
Although the code above runs successfully, the error I receive later when I make an SDK request is this:
Error:"invalid_grant", Description:"java.security.SignatureException: Invalid signature for token: **JWT Suppressed**", Uri:""
I found this article which talks about causes of google's "invalid grant," and "reason #9" concerns me...because it seems to suggest that my token is indeed malformed, which would correspond with the "invalid signature" the error mentioned. Still, I'm not sure if I'm comparing apples to apples.
Any ideas?
Perhaps the problem is simply that the PKCS1 private key I'm using...doesn't have sufficient privileges?
I figured it out.
I had two sets of credentials to experiment with:
PKCS#1 private RSA key
PKCS#8 private key: Associated with this, I also had a "Private key id" and "project id"
Since both private keys pertained to the same service client email, I had been supplying the (otherwise optional) "private key id" and "project id" values for either private key above.
All I had to do was intentionally omit the "private key id" and "project id", when I was processing the PKCS#1, and then it worked. This leaves me still wondering what purpose those optional values serve...though I suspect it pertains to security audits (e.g. auth logs).

JSON Web Tokens Not Decodable on jwt.io: "JWT payload is not a valid JSON object"

Using the below code, I am able to build a decoded JSON Web Token (decodedToken). When I paste it into the debugger on https://jwt.io, the header and payload are correctly shown on the right. But when I paste the encoded token (encodedToken) into the debugger, I get this message:
Looks like your JWT payload is not a valid JSON object. JWT payloads must be top level JSON objects as per https://www.rfc-editor.org/rfc/rfc7519#section-7.2
It also says, "Invalid Signature," in big red letters at the bottom.
In addition, when using the encoded token, the header area shows my decoded header, and the payload area shows my entire decoded token. So somehow jwt.io is successfully decoding the JWT. But it shouldn't be trying to parse the JSON of the entire decoded token as a single unit. It should be trying to parse the JSON of the header and payload portions separately.
I would like to know why jwt.io can't quite my payload of my encoded token. The values of both the decodedToken and encodedToken are shown as comments in the below source code. Thank you.
private static string GetEncodedToken(string privateKey)
{
JObject header = JObject.FromObject(new { alg = "ES384", typ = "JWT" });
JObject payload = JObject.FromObject(new { name = "value" });//simple test JSON data
string decodedToken = Base64URLEncode(header) + "." + Base64URLEncode(payload);
//eyJhbGciOiJFUzM4NCIsInR5cCI6IkpXVCJ9.eyJuYW1lIjoidmFsdWUifQ
byte[] privateKeyBytes = Convert.FromBase64String(privateKey);
using (ECDsa ecdsa = ECDsa.Create())
{
ecdsa.ImportECPrivateKey(privateKeyBytes, out _);
string encodedToken = JWT.Encode(decodedToken, ecdsa, JwsAlgorithm.ES384);
//eyJhbGciOiJFUzM4NCIsInR5cCI6IkpXVCJ9.ZXlKaGJHY2lPaUpGVXpNNE5DSXNJblI1Y0NJNklrcFhWQ0o5LmV5SnVZVzFsSWpvaWRtRnNkV1VpZlE.kYj0M2GNC5w9oRrIVcHJ9dSQLbOEjNjxd61zSXD5iz0nBRRWMWyxj2l1DAhKPa_hpaFUscuubCzYe_W1SKpo3s06on-hYnICgQH4fVoAwZxiM_N4W761jecOExfivztn
return encodedToken;
}
}
private static string Base64URLEncode(JObject jObject)
{
string jsonString = jObject.ToString(Formatting.None);
byte[] jsonBytes = Encoding.UTF8.GetBytes(jsonString);
string escapedBase64 = Convert.ToBase64String(jsonBytes).TrimEnd('=').Replace('+', '-').Replace('/', '_');
return escapedBase64;
}
Update
Thank you for the answers so far. I think I'm close, but jwt.io still says, "Invalid Signature," even though I've pasted in the public key. Using the below information, I'm hoping someone can tell me why my signature is said to be invalid.
I've generated a temporary private key just for the purpose of debugging this situation on stackoverflow.com:
MIGkAgEBBDBywQ7LVcyOGzxJ0Tpjpww2zUZbbtb3WVm4A3uv7ho31jJzQRYTpSqR7+ORAdoxmamgBwYFK4EEACKhZANiAASG50vW1r/O1XmUbTBb6yx1YSABh1USA6MJ8HJnYJ58tjGGVPL88a6Z1gOUlAsHtNhL44PhnTNTNNFdaH2Z41yn7oZmBhuon0vuUNFic2HDpfa/uFwRUAmhSBQz8hu+980=
I initialized an instance of ecdsa with my private key and generated a public key with Convert.ToBase64String(ecdsa.ExportSubjectPublicKeyInfo()):
MHYwEAYHKoZIzj0CAQYFK4EEACIDYgAEhudL1ta/ztV5lG0wW+ssdWEgAYdVEgOjCfByZ2CefLYxhlTy/PGumdYDlJQLB7TYS+OD4Z0zUzTRXWh9meNcp+6GZgYbqJ9L7lDRYnNhw6X2v7hcEVAJoUgUM/IbvvfN
Here is the updated code:
private static string GetSignedEncodedToken(string privateKey)
{
JObject payload = JObject.FromObject(new { name = "value" });//simple test JSON data
string payloadString = payload.ToString(Formatting.None);
byte[] privateKeyBytes = Convert.FromBase64String(privateKey);
using (ECDsa ecdsa = ECDsa.Create())
{
ecdsa.ImportECPrivateKey(privateKeyBytes, out _);
string signedEncodedToken = JWT.Encode(payloadString, ecdsa, JwsAlgorithm.ES384);
return signedEncodedToken;
}
}
And here is the signed encoded token: eyJhbGciOiJFUzM4NCIsInR5cCI6IkpXVCJ9.ew0KICAibmFtZSI6ICJ2YWx1ZSINCn0.KpJAgc3-yaoGmHGAXHOeH3BPgpxdBRm461yWia60dgjuQHG5iLnwLQtQgdZtsHnI-bEK_wdmvu85ZrF7n-TdWiFb4FQxGeLBeeRfnMLJhKfInu_7MYEWPS2Ohm4yBAqg
Solution to Update
As mentioned by jps, I needed to wrap my public key with "-----BEGIN PUBLIC KEY-----" and "-----END PUBLIC KEY-----". Then jwt.io shows, "Signature Verified."
What you're calling decodedToken here is basically already an encoded token without signature. The function JWT.Encode takes the first parameter and repeats what you already did manually, i.e. it creates a base64url encoded header and payload for you and then signs the token. You don't need to create your own header and you don't need to care about base64url encoding.
Therefore you just need to pass your payload (not base64 encoded) there. The result is a signed token.
And jwt.io shows "invalid signature" because you probably didn't pass the public key to the "public key" field in the right column. Without knowing the public key, the signature can't be verified.
When you paste a public key there, make sure it has the header and footer lines -----BEGIN PUBLIC KEY----- -----END PUBLIC KEY-----, then verification should work fine.
Token that you have provided, I used the same in jwt.io debugger and can see couple of issues:
eyJhbGciOiJFUzM4NCIsInR5cCI6IkpXVCJ9.ZXlKaGJHY2lPaUpGVXpNNE5DSXNJblI1Y0NJNklrcFhWQ0o5LmV5SnVZVzFsSWpvaWRtRnNkV1VpZlE.kYj0M2GNC5w9oRrIVcHJ9dSQLbOEjNjxd61zSXD5iz0nBRRWMWyxj2l1DAhKPa_hpaFUscuubCzYe_W1SKpo3s06on-hYnICgQH4fVoAwZxiM_N4W761jecOExfivztn
Following are decoded details:
Header (which is correct):
{
"alg": "ES384",
"typ": "JWT"
}
Payload (which is not correct):
"eyJhbGciOiJFUzM4NCIsInR5cCI6IkpXVCJ9.eyJuYW1lIjoidmFsdWUifQ"
Since Payload is expected to be Json, not a Base64Encoded string. You shall be able to modify it to generate a new Token.
Now Main Error:
For decoding debugger surely needs Public Key, which can match with Private Key, that's in case you use Asymmetric encryption, in case you use Symmetric encryption then both keys will be same.
Optionally you can also add the Private Key to generate the new token with modified algorithm or payload.
Programmatically you are able to see the details:
You are supplying the right Public key to match the Private Key
You convert Base64String to Json programmatically. which is not feasible for JWT debugger

C# Store Public-Key X.509 encoded key in ANS.1

i am trying to implement an API. (https://www.loxone.com/dede/wp-content/uploads/sites/2/2016/08/0903_Communicating-with-the-Miniserver.pdf)
When you go to Page 5 you will see under 2.b) that I need to store a public key in Format: X.509 encoded key in ANS.1.
The problem is I have no idea how to do this in C#...
Can anyone please help me with that problem :)
Thanks in advance!
Here is an example for the publicKey that I get:
-----BEGIN CERTIFICATE-----MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQC393RoCVFixg3cZicByw05GYqlIkmqfx1lylJqBaF7BMI/FffD5BBoFpFurj4N1pDdMF/QhBEj1Pb3XGOkrHFCinYc0lCMbSjreE+QPjmnWnZeaPd3mtBfdcTSF6hYH0regFU8nvFVgLsoBUCMjuhfsvuaW4sLylU6KhZ9hCqVQwIDAQAB-----END CERTIFICATE-----
I think you need to export this public key as X.509 encoded as ANS.1 which can be a .pem file.
From https://serverfault.com/questions/9708/what-is-a-pem-file-and-how-does-it-differ-from-other-openssl-generated-key-file/9717#9717 :
PEM on it's own isn't a certificate, it's just a way of encoding data. X.509 certificates are one type of data that is commonly encoded using PEM.
PEM is a X.509 certificate (whose structure is defined using ASN.1), encoded using the ASN.1 DER (distinguished encoding rules), then run through Base64 encoding and stuck between plain-text anchor lines (BEGIN CERTIFICATE and END CERTIFICATE).
You can use openssl to make a .pem file which is ANS.1 compliant.
openssl req -new -x509 -days 365 -nodes -out ./cert.pem -keyout ./cert.pem
Presumably, by "ANS.1" they meant "ASN.1" (Abstract Syntax Notation 1), and by "ASN.1" they meant "DER" (Distinguished Encoding Rules for Abstract Syntax Notation 1).
To convert your current PEM certificate into a "DER" certificate you either a) openssl x509 -in cert.pem -out cert.der -outform der or b) read the base64 string between "-----BEGIN CERTIFICATE-----" and "-----END CERTIFICATE-----" and convert it to bytes (e.g. Convert.FromBase64String).
If you still need it after 3 years, you can use the following C# (.NET 6) method:
public static async Task<RSA> GetPublicKey(string certStr)
{
string base64 = certStr;
if (base64.StartsWith("-----BEGIN CERTIFICATE-----"))
{
base64 = base64.Substring(27);
}
if (base64.EndsWith("-----END CERTIFICATE-----"))
{
base64 = base64.Substring(0, base64.Length - 25);
}
var asn = new AsnReader(Convert.FromBase64String(base64), AsnEncodingRules.DER);
asn = asn.ReadSequence();
asn.ReadSequence();
//string objectIdentifier = asn.ReadObjectIdentifier(Asn1Tag.ObjectIdentifier);
//asn.ReadNull();
var data2 = asn.ReadBitString(out int bitCount);
asn = new AsnReader(data2, AsnEncodingRules.DER);
asn = asn.ReadSequence();
var modulus = asn.ReadInteger();
var exponent = asn.ReadInteger();
var rsa = new RSACng();
rsa.ImportParameters(new RSAParameters
{
Modulus = modulus.ToByteArray(true, true),
Exponent = exponent.ToByteArray(true, true),
});
return rsa;
}
Not the best and no error handling, but works.
Then you don't need an X.509 certificate, just this RSA object:
var aes = Aes.Create();
string s = ByteArrayToHex(aes.Key) + ":" + ByteArrayToHex(aes.IV);
var encData = rsa.Encrypt(Encoding.ASCII.GetBytes(s), RSAEncryptionPadding.Pkcs1);
s = "jdev/sys/keyexchange/" + Convert.ToBase64String(encData);
Probably you can implement the ByteArrayToHex yourself :)

Constructing RSACryptoServiceProvider from X509Certificate2 without LDAP request being made

When the line to extract the public key is executed, an LDAP request is sent:
this.certificate = new X509Certificate2(buffer);
System.Security.Cryptography.X509Certificates.PublicKey key = this.certificate.PublicKey;
50 0.853745000 xxx.xxx.xxx.xxx xxx.xxx.xxx.xxx LDAP 404 searchRequest(1) "" baseObject
...which I believe is authenticating the currently logged on user. I really need to avoid this call, as on a customer system this causes a long delay because of network configuration.
I'm assuming it's attempting to do some authentication around some kind of key store, but as in this case the certificate is all contained in the buffer provided, all I want is for the key to be used without this request being sent.
All I actually want is to create an RSACryptoServiceProvider from the private key in the certificate. I tried a few methods I've found on here involving GetPrivateKey, but struggled to get anything to work.
Thanks in advance!
EDIT Test program:
static void Main( string[] args )
{
var certificate = new System.Security.Cryptography.X509Certificates.X509Certificate2(#"E:\Temp\Cert.cer");
System.Security.Cryptography.X509Certificates.PublicKey key = certificate.PublicKey;
}
The certificate I tested with can be found here: Cert.cer
Yes, it's not the strongest signature or key, before I get comments!
Thanks again.
EDIT: I actually worked around this by using a suggestion to use BouncyCastle. I use this to parse the certificate:
X509CertificateParser parser = new X509CertificateParser();
Org.BouncyCastle.X509.X509Certificate cert = parser.ReadCertificate(buffer);
I then extract the modulus and exponent and push them into a Microsoft RSAParameters:
RsaKeyParameters key = (RsaKeyParameters)cert.GetPublicKey();
// Construct a microsoft RSA crypto service provider using the public key in the certificate
RSAParameters param = new RSAParameters();
param.Exponent = key.Exponent.ToByteArrayUnsigned();
param.Modulus = key.Modulus.ToByteArrayUnsigned();
I can then construct the Microsoft RSACryptoServiceProvider from this:
using (RSACryptoServiceProvider provider = new RSACryptoServiceProvider())
{
provider.ImportParameters(param);
byte[] rsaBlock = provider.Encrypt(preMasterSecret, false);
this.Client.Writer.Write(rsaBlock);
}
I never got any other response, so here's the Bouncycastle implementation I used.
X509CertificateParser parser = new X509CertificateParser();
Org.BouncyCastle.X509.X509Certificate cert = parser.ReadCertificate(buffer);
I then extract the modulus and exponent and push them into a Microsoft RSAParameters:
RsaKeyParameters key = (RsaKeyParameters)cert.GetPublicKey();
// Construct a microsoft RSA crypto service provider using the public key in the certificate
RSAParameters param = new RSAParameters();
param.Exponent = key.Exponent.ToByteArrayUnsigned();
param.Modulus = key.Modulus.ToByteArrayUnsigned();
I can then construct the Microsoft RSACryptoServiceProvider from this:
using (RSACryptoServiceProvider provider = new RSACryptoServiceProvider())
{
provider.ImportParameters(param);
byte[] rsaBlock = provider.Encrypt(preMasterSecret, false);
this.Client.Writer.Write(rsaBlock);
}

Categories