Skip to content

Instantly share code, notes, and snippets.

@jasonk000
Last active September 13, 2023 20:31
Show Gist options
  • Star 16 You must be signed in to star a gist
  • Fork 4 You must be signed in to fork a gist
  • Save jasonk000/26f987681b56fe34c235248c980b5c2e to your computer and use it in GitHub Desktop.
Save jasonk000/26f987681b56fe34c235248c980b5c2e to your computer and use it in GitHub Desktop.
Using x.509 certs with JWS/JWT/JWK
#!/usr/bin/env node
import fs from 'fs'
import jose from 'node-jose'
import pem from 'pem'
async function run () {
try {
// keystore to stick our node-jose keys before we do signing
let keystore = jose.JWK.createKeyStore()
// load in the private key
let privatepem = fs.readFileSync('./device.key', 'utf8')
let privatekey = await keystore.add(privatepem, 'pem')
// and the public key
let publicpem = fs.readFileSync('./device.crt', 'utf8')
let publickey = await keystore.add(publicpem, 'pem')
// we need the public key chain in x5c header. x5c header chain will be used during
// decode, a full cert can be provided to ensure validation all the way to root
// https://tools.ietf.org/html/draft-ietf-jose-json-web-key-41#page-9
// unfortunately we can't just use plain jwk, since jwk is only the *key* and not the
// full *certificate*, so ... x5c it is
let x5cChain = cert_to_x5c(publicpem)
// the message body
let message = JSON.stringify({
iss: 'vendor',
sub: '1234',
exp: Date.now()+10*60*1000, // expires in 10 minutes
iat: Date.now(),
bundle: '...'
})
// and signing options
let signoptions = { fields: { x5c: x5cChain } }
// sign 'message' with the 'privatekey', include the 'x5c' chain in the headers
let signed = await jose.JWS.createSign(signoptions, privatekey).update(message, 'utf8').final()
// bet you didn't think it would be that big
console.log(signed)
console.log('//////////////////////////////')
// a quick sanity check - the cisco/node-jose lib provides x5c verification fortunately
let result = await jose.JWS.createVerify().verify(signed)
console.log(JSON.parse(result.payload))
console.log('//////////////////////////////')
// but .. it doesn't check expiry date on the message
let exp = new Date(JSON.parse(result.payload).exp)
console.log(exp)
if (Date.now() > exp) {
console.log('message is too old')
throw Error(`message expiry [exp] is too old; JWS expires at: ${exp}`)
} else {
console.log('message expiry valid')
}
// and .. it doesn't do the full x509 cert verification, it just checks that the
// key from the first cert in the x5c header can verify the payload so now, we
// need to shell out to openssl to verify that the provided key was signed by the CA
// why oh why is there nothing native for this
let cert = await x5c_to_cert(result.header.x5c)
// load the CA
let cacert = fs.readFileSync('./cachain.crt', 'utf8')
// it actually works!
let trusted = await verifySigningChain(cert, cacert)
console.log('worked?', trusted)
console.log(JSON.parse(result.payload))
} catch (err) {
console.log(err)
}
}
run()
// promisify the thing
function verifySigningChain (cert, cacert) {
return new Promise((resolve, reject) => {
pem.verifySigningChain(cert, cacert, (err, ver) => {
if (err) return reject(err)
return resolve(ver)
})
})
}
// taken from (MIT licensed):
// https://github.com/hildjj/node-posh/blob/master/lib/index.js
function cert_to_x5c (cert, maxdepth) {
if (maxdepth == null) {
maxdepth = 0;
}
/*
* Convert a PEM-encoded certificate to the version used in the x5c element
* of a [JSON Web Key](http://tools.ietf.org/html/draft-ietf-jose-json-web-key).
*
* `cert` PEM-encoded certificate chain
* `maxdepth` The maximum number of certificates to use from the chain.
*/
cert = cert.replace(/-----[^\n]+\n?/gm, ',').replace(/\n/g, '');
cert = cert.split(',').filter(function(c) {
return c.length > 0;
});
if (maxdepth > 0) {
cert = cert.splice(0, maxdepth);
}
return cert;
}
function x5c_to_cert (x5c) {
var cert, y;
cert = ((function() {
var _i, _ref, _results;
_results = [];
for (y = _i = 0, _ref = x5c.length; _i <= _ref; y = _i += 64) {
_results.push(x5c.slice(y, +(y + 63) + 1 || 9e9));
}
return _results;
})()).join('\n');
return ("-----BEGIN CERTIFICATE-----\n" + cert + "\n-----END CERTIFICATE-----");
}
@OR13
Copy link

OR13 commented Feb 11, 2022

Could you add a comment with the OpenSSL commands you used to generate these:

    // load in the private key
    let privatepem = fs.readFileSync('./device.key', 'utf8')
    let privatekey = await keystore.add(privatepem, 'pem')

    // and the public key
    let publicpem = fs.readFileSync('./device.crt', 'utf8')
    let publickey = await keystore.add(publicpem, 'pem')

and

// load the CA
    let cacert = fs.readFileSync("./cachain.crt", "utf8");

@OR13
Copy link

OR13 commented Feb 11, 2022

This almost worked for me:

openssl ecparam -name secp384r1 -genkey -noout -out private-key.pem
openssl ec -in private-key.pem -pubout -out public-key.pem
openssl req -new -x509 -key private-key.pem -out cert.pem -days 360

@OR13
Copy link

OR13 commented Feb 11, 2022

for latest versions on node-jose you need:

 let result = await jose.JWS.createVerify().verify(signed, {
      allowEmbeddedKey: true,
    });

@jasonk000
Copy link
Author

Great to see it's of use - unfortunately I don't recall the exact commands, and I'm not a regular nodejs user anymore, so I'm not sure I'll get it right. If you have some specific updates / suggestions we can definitely modify the script?

@OR13
Copy link

OR13 commented Feb 16, 2022

I think I figured out how to get what I needed working here: https://github.com/transmute-industries/openssl-did-web-tutorial

Its an education example, and its brand new so it may have bugs, but I think I was able to show:

  1. generate root ca with P-384
  2. generate intermediate ca
  3. generate 3 child ca
  4. sign a JWT with a child CA private key and include the ca chain in x5c
  5. verify the JWT with the pub
  6. verify the ca chain with open ssl

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment