Creating JWT token with x5c header parameter using System.IdentityModel.Tokens.Jwt - security

My project is building an authentication service based on .NET Core and the System.IdentityModel.Tokens.Jwt nuget package. We want to create JWT tokens that include the public key certificate (or certificate chain) that can be used to verify the JWT digital signatures. This is possible with commercial identity providers (SaaS), and is supported in the JWT specification by means of a header parameter called "x5c". But I have so far been unable to get this to work using System.IdentityModel.Tokens.Jwt.
I am able to create a JWT token signed using a certificate. The certificate is self-signed and created using openssl (commands included underneath).
My test code in C# looks like this:
using Microsoft.IdentityModel.Tokens;
using System.IdentityModel.Tokens.Jwt;
// more usings..
public static string GenerateJwtToken(int exampleAccountId, string x509CertFilePath, string x509CertFilePassword)
{
var tokenHandler = new JwtSecurityTokenHandler();
var signingCert = new X509Certificate2(x509CertFilePath, x509CertFilePassword);
var tokenDescriptor = new SecurityTokenDescriptor
{
Subject = new ClaimsIdentity(new[] { new Claim(ClaimTypes.Name, exampleAccountId.ToString()) }),
Expires = DateTime.UtcNow.AddDays(30),
Audience = "myapp:1",
Issuer = "self",
SigningCredentials = new X509SigningCredentials(signingCert, SecurityAlgorithms.RsaSha512Signature),
Claims = new Dictionary<string, object>()
{
["test1"] = "hello world",
["test2"] = new List<int> { 1, 2, 4, 9 }
}
};
var token = tokenHandler.CreateToken(tokenDescriptor);
return tokenHandler.WriteToken(token);
}
The generated token header deserializes to this in jwt.io:
{
"alg": "RS512",
"kid": "193A49ED67F22850F4A95258FF07571A985BFCBE",
"x5t": "GTpJ7WfyKFD0qVJY_wdXGphb_L4",
"typ": "JWT"
}
Thing is, I would like to get the "x5c" header parameter output as well. The reason for this is that my project is trying to include the certificate with the public key to validate the token signature inside the token itself, and "x5c" is a good way to do this. But I just cannot get this to work.
I have tried adding x5c manually with AdditionalHeaderClaims on SecurityTokenDescriptor, but it just isn't being output in the token.
Does anybody know how to do this, or can you point me to some solid resources on the subject?
By the way, this is how I generated the certificate used (on Windows):
openssl genrsa -out private2048b.key 2048
openssl req -new -key private2048b.key -out myrequest2048.csr -config <path to openssl.cfg>
openssl x509 -req -days 3650 -in myrequest2048.csr -signkey private2048b.key -out public2048b.crt
openssl pkcs12 -keypbe PBE-SHA1-3DES -certpbe PBE-SHA1-3DES -export -in public2048b.crt -inkey private2048b.key -out mypkcs2048.pfx -name "Testtest"
The PFX is the file being read and used in the code.
Update for posterity
Using Abdulrahman Falyoun's answer, the final part of the code was updated to use token.Header.Add to manually add in the "x5c" header parameter, before serializing the JWT token. Token had to be cast as JwtSecurityToken.
This worked, and created a token that was valid (and had a signature that could immediatly be verified) in https://jwt.io :
// create JwtSecurityTokenHandler and SecurityTokenDescriptor instance before here..
var exportedCertificate = Convert.ToBase64String(signingCert.Export(X509ContentType.Cert, x509CertFilePassword));
// Add x5c header parameter containing the signing certificate:
var token = tokenHandler.CreateToken(tokenDescriptor) as JwtSecurityToken;
token.Header.Add(JwtHeaderParameterNames.X5c, new List<string> { exportedCertificate });
return tokenHandler.WriteToken(token);

What is x5c?
The "x5c" (X.509 certificate chain) Header Parameter contains the X.509 public key certificate or certificate chain [RFC5280] corresponding to the key used to digitally sign the JWS. The certificate or certificate chain is represented as a JSON array of certificate value strings. Each string in the array is a base64-encoded (not base64url-encoded) DER [ITU.X690.2008] PKIX certificate value. The certificate containing the public key corresponding to the key used to digitally sign the JWS MUST be the first certificate. This MAY be followed by additional certificates, with each subsequent certificate being the one used to certify the previous one. The recipient MUST validate the certificate chain according to RFC 5280 [RFC5280] and consider the certificate or certificate chain to be invalid if any validation failure occurs. The use of this Header Parameter is OPTIONAL.
Note
From the security point of view - do not use the x5c certificate to validate the signature directly. In that case, anybody could just provide their own certificate and spoof any identity.
The purpose of the x5t / x5t#S256 header is to identify the signer - check you trust the certificate provided by x5c or x5t#S256 (or its issuer) under the specified iss, only then you should validate the signature.
so to build the X509 chain
X509Chain chain = new X509Chain()
bool success = chain.Build(cert);
if (!success) throw Error
Then for each chain.ChainElements value, take the Certificate property RawValue property (and base64 encode it).
finally, you got the string for x5c and should only provide it to the headers of jwt.
See the following links
Create JWK Set Containing Certificates
Generate x5c certificate chain from JWK
How to obtain JWKs and use them in JWT signing?
How to get x5c from RSACryptoServiceProvider
Hope it's useful.
#Edit
If the issue was to supply the x5c to the header, you have to add it using
token.Header.Add(name, value)

Related

How to verify JWT token with ES384 algorithm with Nodejs tools signed with JwtSecurityTokenHandler using CNG keys

I'm trying to verify JWT token with Node.js tools signed with JwtSecurityTokenHandler using CNG generated keys
I tried many Nood.js tools e.g. jsonwebtoken
jwt.verify(token, publickey,{ algorithms: ['ES384'], ...
But get wrong tag errors every time
["error:0D07803A:asn1 encoding routines:asn1_item_embed_d2i:nested asn1 error"],"library":"asn1 encoding routines","function":"asn1_check_tlen","reason":"wrong tag","code":"ERR_OSSL_ASN1_WRONG_TAG"
The public and private keys generated with CNG
var key = CngKey.Create(CngAlgorithm.ECDsaP384, "keyName",
new CngKeyCreationParameters
{
KeyCreationOptions = CngKeyCreationOptions.OverwriteExistingKey,
KeyUsage = CngKeyUsages.AllUsages,
ExportPolicy = CngExportPolicies.AllowPlaintextExport,
});
txtPrivateKey = Convert.ToBase64String(key.Export(CngKeyBlobFormat.EccPrivateBlob));
txtPublicKey = Convert.ToBase64String(key.Export(CngKeyBlobFormat.EccPublicBlob));
I tried with converting the keys, but still getting the same exception.
How can I generate a valid public key for Node.js tools using CNG and ES384 algorithm?
It seems I found the solution:
export private key in pkcs8 format
Convert.ToBase64String(key.Export(CngKeyBlobFormat.Pkcs8PrivateBlob));
save into pem and add BEGIN/END PRIVATE KEY
generate public key with openssl
openssl ec -in pkcs8.pem -pubout -out pkcs8genpubkey.pem

error:0409A06E:rsa routines data too large for key size

I'm generating a SAML response and it needs to be encrypted and signed with public and private keys. I generated private.pem and public.pem in the terminal with the commands
openssl genrsa -out private.pem 2048
openssl rsa -in ./private.pem -pubout -out public.pem
Then in nodeJS.
encrypt: function(message) {
return new Promise(function (resolve, reject) {
var publicKey = require("fs").readFileSync(__dirname + "/public.pem", "utf8");
var encrypted = require("crypto").publicEncrypt(publicKey, new Buffer(message));
resolve(encrypted.toString("base64"));
});
},
Once I call the message encrypt(xml), I get the following error
{
library: 'rsa routines',
function: 'RSA_padding_add_PKCS1_OAEP_mgf1',
reason: 'data too large for key size',
code: 'ERR_OSSL_RSA_DATA_TOO_LARGE_FOR_KEY_SIZE'
}
Objective:
I've to sign the message as per the demo here samltools.com (Mode: SignMessage), my SAML message looks like this. (see SAML Response section).
Sign the message
Base64Encode the message
The problem here is that you cannot directly encrypted with RSA, a piece of data which is larger than the key size.
Surprising I know, it surprised me too.
In reality very little payload data is encrypted directly with RSA or even elliptic curves.
You should be using RSA Diffie-Hellman to generate a shared secret.
Signature of the file, is really signature of the hash of the file.

Programmatically create certificate and certificate key in Node

Using node.js, I'd like to write code to programmatically do the equivalent of the following:
openssl genrsa -des3 -passout pass:x -out server.pass.key 2048
openssl rsa -passin pass:x -in server.pass.key -out server.key
rm server.pass.key
openssl req -new -key server.key -out server.csr
openssl x509 -req -sha256 -days 365 -in server.csr -signkey server.key -out server.crt
When complete, I need the RSA key server.key and the self-signed SSL certificate server.crt.
forge looks the most promising, but so far I haven't figured out how to get it to work. I have the following code:
var pki = forge.pki;
var keys = pki.rsa.generateKeyPair(2048);
var privKey = forge.pki.privateKeyToPem(keys.privateKey);
var pubKey = forge.pki.publicKeyToPem(keys.publicKey);
But when I write the pubKey to a file, I've noticed it starts with ...
-----BEGIN PUBLIC KEY-----
MIIB...
-----END PUBLIC KEY-----
... and isn't recognized, whereas using openssl above it starts with:
-----BEGIN CERTIFICATE-----
MIID...
-----END CERTIFICATE-----
Since the original link went dead, I've made my own code that generates a self-signed certificate using node-forge (which it looks like they already have based on the original question), so I thought I'd put it here for someone who wants it
Simply creating a public and private key pair isn't enough to work as a certificate, you have to put in attributes, node-forge is incredibly useful this way, as its pki submodule is designed for this.
First, you need to create a certificate via pki.createCertificate(), this is where you'll assign all of your certificate attributes.
You need to set the certificate public key, serial number, and the valid from date and valid to date. In this example, the public key is set to the generated public key from before, the serial number is randomly generated, and the valid from and to dates are set to one day ago and one year in the future.
You then need to assign a subject, and extensions to your certificate, this is a very basic example, so the subject is just a name you can define (or let it default to 'Testing CA - DO NOT TRUST'), and the extensions are just a single 'Basic Constraints' extension, with certificate authority set to true.
We then set the issuer to itself, as all certificates need an issuer, and we don't have one.
Then we tell the certificate to sign itself, with the private key (corresponding to its public key we've assigned) that we generated earlier, this part is important when signing certificates (or child certificates), they need to be signed with the private key of its parent (this prevents you from making fake certificates with a trusted certificate parent, as you don't have that trusted parent's private key)
Then we return the new certificate in a PEM-encoded format, you could save this to a file or convert it to a buffer and use it for a https server.
const forge = require('node-forge')
const crypto = require('crypto')
const pki = forge.pki
//using a blank options is perfectly fine here
async function genCACert(options = {}) {
options = {...{
commonName: 'Testing CA - DO NOT TRUST',
bits: 2048
}, ...options}
let keyPair = await new Promise((res, rej) => {
pki.rsa.generateKeyPair({ bits: options.bits }, (error, pair) => {
if (error) rej(error);
else res(pair)
})
})
let cert = pki.createCertificate()
cert.publicKey = keyPair.publicKey
cert.serialNumber = crypto.randomUUID().replace(/-/g, '')
cert.validity.notBefore = new Date()
cert.validity.notBefore.setDate(cert.validity.notBefore.getDate() - 1)
cert.validity.notAfter = new Date()
cert.validity.notAfter.setFullYear(cert.validity.notBefore.getFullYear() + 1)
cert.setSubject([{name: 'commonName', value: options.commonName}])
cert.setExtensions([{ name: 'basicConstraints', cA: true }])
cert.setIssuer(cert.subject.attributes)
cert.sign(keyPair.privateKey, forge.md.sha256.create())
return {
ca: {
key: pki.privateKeyToPem(keyPair.privateKey),
cert: pki.certificateToPem(cert)
},
fingerprint: forge.util.encode64(
pki.getPublicKeyFingerprint(keyPair.publicKey, {
type: 'SubjectPublicKeyInfo',
md: forge.md.sha256.create(),
encoding: 'binary'
})
)
}
}
//you need to put the output from genCACert() through this if you want to use it for a https server
/* e.g
let cert = await genCACert();
let buffers = caToBuffer(cert.ca);
let options = {};
options.key = buffers.key;
options.cert = buffers.cert;
let server = https.createServer(options, <listener here>);
*/
function caToBuffer(ca) {
return {
key: Buffer.from(ca.key),
cert: Buffer.from(ca.cert)
}
}
Do with this what you will.
Okay, as you probably realized, I wasn't generating a certificate. It required quite a bit more work, which you can find here.
Essentially, after a bunch of setup, I had to create, sign, and convert the certificate to Pem:
cert.sign(keys.privateKey);
var pubKey = pki.certificateToPem(cert);
Hope this helps someone else!

NodeJS Crypto RS-SHA256 and JWT Bearer

In implementing an oauth2 stack utilizing passport and oauth2orize, in this case the issue is specifically in utilizing the oauth2orize jwt bearer. The oauth2orize jwt bearer is great in getting everything going, however it has the RSA SHA pieces marked as to do.
In attempting to put in the pieces for the RSA SHA encryption handling, I cannot get the signature to verify as verifier.verify always seems to return false. If anyone has cleared this hurdle, a little help would be super.
What I've done:
Created the private / public keys:
openssl genrsa -out private.pem 1024
//extract public key
openssl rsa -in private.pem -out public.pem -outform PEM -pubout
now the data to sign:
{"alg":"RS256","typ":"JWT"}{"iss": "myclient"}
I've tried multiple ways as to how to sign this, too many to list here, but my understanding of the correct signature is to sign the bas64 encoding of these items, so i ran base64 on {"alg":"RS256","typ":"JWT"} and base64 on {"iss": "myclient"} then ran base64 on those encodings. So the result is:
eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9
eyJpc3MiOiAibXljbGllbnQifQ
then encode:
{eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9}.{eyJpc3MiOiAibXljbGllbnQifQ}
which gives me:
e2V5SmhiR2NpT2lKU1V6STFOaUlzSW5SNWNDSTZJa3BYVkNKOX0ue2V5SnBjM01pT2lBaWJYbGpiR2xsYm5RaWZRfQ
At this point I sign the above base64 by doing:
openssl sha -sha256 -sign priv.pem < signThis > signedData
Then I run base64 on that to get the data to pass into the signature part of the assertion.
I then pass in the object:
{
"grant_type": "urn:ietf:params:oauth:grant-type:jwt-bearer",
"assertion": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiAibXljbGllbnQifQ.signedData"
}
now in the code base I have:
var crypto = require('crypto')
, fs = require('fs')
, pub = fs.readFileSync('/path/to/pub.pem')
, verifier = crypto.createVerify("RSA-SHA256");
verifier.update(JSON.stringify(data));
var result = verifier.verify(pub, signature, 'base64');
console.log('vf: ', result);
however, result is always false.
I do properly receive the data, the signature variable in the code is a match for what I'm passing in, I just always receive false and have exhausted all options I can think of on how to tweak this to get verifier.verify to return true. Thank you for the time and help!
I am not sure if this is exactly what you were looking for, but this will successfully create a JWT in a google api fashion using jwt-simple (which uses crypto and such):
var fs = require('fs')
, jwt = require('jwt-simple')
, keypath = '/path/to/your.pem'
, secret = fs.readFileSync( keypath, { encoding: 'ascii' })
, now = Date.now()
, payload = {
scope: 'https://www.googleapis.com/auth/<service>',
iss : '<iss_id>#developer.gserviceaccount.com',
aud : 'https://accounts.google.com/o/oauth2/token',
iat : now,
exp : now+3600
}
, token = jwt.encode( payload, secret, 'RS256' )
, decoded = jwt.decode( token, secret, 'RS256' );
console.log( token );
console.log( decoded );
I think this code sample is completely unsecure. If you look at the latest JWT code, it doesn't event use the secret on your decode call.
https://github.com/hokaccha/node-jwt-simple/blob/master/lib/jwt.js
It basically just decodes the second segment and returns it which means anyone could have changed the value and its not verified.

Passing authentication data to another website

There are two applications AppA and AppB. User is authenticated in AppA. When users clicks a link in AppA I want to redirect him to AppB and pass authentication data to AppB, so user can be automatically authenticated in AppB under the same credentials. Is there a common approach of passing authentication data from one website to another? How can I do it in the most secure way?
You could pass the username and password encrypted over a query string or a shared cookie.
If you have the private_key shared on AppB you can decrypt anything you encrypt on AppA.
Run this to generate your public and private key.
openssl genrsa -out public_key.pem 2048
openssl rsa -in public_key.pem -pubout > private_key.pub
Here you can encrypt\decrypt using your new key.
function encrypt($data)
{
if (openssl_public_encrypt($data, $encrypted, file_get_contents("/home/blah/key/private_key.pem")))
$data = base64_encode($encrypted);
else
throw new Exception('Unable to encrypt data. Perhaps it is bigger than the key size?');
return $data;
}
function decrypt($data)
{
if (openssl_private_decrypt(base64_decode($data), $decrypted, file_get_contents("/home/blah/key/private_key.pem")))
$data = $decrypted;
else
$data = '';
return $data;
}

Resources