1

I've recently been working on implementing a web service that signs and issues JWT and also exposes the JWKs endpoint for the JWT validation purposes.

It's all fairly straightforward with the JWT / JWK according to the IETF spec, but I noticed something curious which I cannot explain just yet:

TL;DR: why EC P-256 source key works for the signing JWT with RSA algo?

Long story:

I am using a pre-existing private key file to sign the JWT and also to import the JWK to the node-jose keystore.

Keystore:

const jose = require('node-jose');
const fs = require('fs')

const PRIVATE_KEY_PATH = process.env.PRIVATE_KEY_PATH

const privateKey = fs.readFileSync(PRIVATE_KEY_PATH,"utf8");

console.log('Begin import of private key to the JWKS...')
let jwkJson = {}
const jwks = jose.JWK.createKeyStore();
jwks.add(privateKey, 'pem').then(function(jwk) {
  console.log(`Imported JWK:\n${JSON.stringify(jwk, null, 2)}`)
  jwkJson = jwks.toJSON(true)
});


const getPrivateKey = () => {
  return privateKey;
}


const getJwks = () => {
  return jwkJson
}

module.exports = {
  getPrivateKey,
  getJwks
}

JWT:

const jwt = require('jsonwebtoken')
const keystore = require('../util/keystore')

const payload = {
  cert_hash: 'whatever'
}
const signOptions = {
  algorithm: "RS256",
  expiresIn: Math.floor(Date.now() / 1000) + 1800,
  issuer: 'whatever',
  subject: 'whatever'
}

return jwt.sign(payload, keystore.getPrivateKey(), signOptions);

Looks very standard and seems to be working.

The catch is in the key file type. It's Elliptic Curve (ES) P-256 or in JWT-world ES256 created via openssl:

openssl ecparam -genkey -name P-256 -noout -out api-gw.key

Sample key:

-----BEGIN PRIVATE KEY-----
MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDNkiJxzt3uGhxh
OzlG1Zs4HRx0rn1caAcyrzGiIK3LxD4ewcUPqjco8ONbrMSxhurMZbbZyasUyDs5
2guveFhGVYQzfgMmJLK77eAkEB6zHcAP76tpv6LYFuDNTep9pUc+5tQIyNDY5hKe
UMkxrgzckTVyLpzcH2tsugjUUT4A7ad4brXR1/3hqusA53DNucI4Sv7CJTAGMenN
8SwifbIwxvlORFQ8QvYCBdW8HZ1TvoBz3E6vk7gypNUvRNAVMquQapb6LS0VkmSg
bk5cSaopcvyn2Oq+cxnSh7glgZcfeopZ80j4bFmsZnK2UDJnAGm+RC7Q1SvMDhhb
7F/1x97fAgMBAAECggEAPMLj8XWuvVD0cHzb2icLARQBtC9bGHQbJI0KA5zbIe54
Wgj2IUIzmaNR4Gf5n2t8fTvXRxpHuhXRA7GCYLQWi3t9XubxMVYJimiuJpqpKFIz
0cIKjXA6RtrESYqtM8Qlgd8ibxJEQMgIWskQHuIOJSe6f2xtqtaSnwmB0JfO1uDg
RnZoUEd+oZMqu2vqz0cL0e1Q6UR9WYpSH68uS05KgYlZOzOsHMadkXfwW6BEqYd6
2gh9I1GkrY0ZsuCOVhCAc7rtuUQRMnpACNuWci/qPKQLcvSJBojS/W9xEVrVBXkQ
AZyZltjw1B5CJ9/2lbFVRTM4D/JSS84BsQEwsZm2MQKBgQDrL9kIlKCTXNl4lTYk
G8JTKV85/MUY2seWmg2K18EL8ovovY15TjKF5N6g518phmiMUPg+QxkFiAY3gZ4A
+aXsdkxo2XeTEOxFQ3ysbI4H0L0+GMCCAvqCGJr4c1kGSB1kFnGDlo7bSXBUxwlZ
4tTnVQEX0H+mfTJ7yOxv7A/lWQKBgQDfw1XtbnykCEbWXNsmgzzrmk3vvZKlNfc8
CR6PD6fCCa/yYwCv0ItJMVcF14z0vIlsaw6TmsotQIpn6tzv+ttA9KxIKPVtUNFi
O5RTgTFQUq4NO0uUje6Wj1M6uRJRo9yHUted+wcCHoQ4k1xGg7jCMiw/8O7K3Diy
Kxz9Ve2G9wKBgQDbsPd4t3WEElCm/iLz+eY9TsFAZqkqfXvBZ6hM0RvocCpXP3G/
Jde2EUQRY/AV1xMkN6KcbosaCqVcBj01Rf7DcwIPU00KWN2MGe2FF2ZZUJjmP7Lb
/7JIAnoIqZ84afbifsCMngBWQTSoTMCkcWpVqab6uu3y9LJKxTZvmkCDCQKBgGNb
LNBceuOq+Sk92eFj7K0AuxJ0rqTFLZ5uvi7v2KGEA6gw5aErjG1XhziE2YXiIXMO
pk5MMPGe8tXpp2i3jpttCQKRjUiY1iA0LExX1TnBPJ+LcKfpzcL0qRQuEUBG7ij4
U91GFXqPak5kwFhfLK6t8JADv0Q8PMB//ENQ4ENJAoGAf8fdXSYwWEhdcQcxA3JV
MHDKSOblhapT52oXnNN6VW63ccv0mRD0gRQL5nOUQdGghvtFixBoasIMz0K2i8tH
kRz56CTLpLFop+gMz+j1PPL+hjRJOGpqoPjnHkWbvK6iGadTE4nT8iqGq4p11yuc
K+HDZxjTgVXEEbOuIpbAd/8=
-----END PRIVATE KEY-----

And while I explicitly state that I use RS256 as algo in JWT case, the node-jose upon import of the key deduces it's RSA type by itself:

{
  "keys": [
    {
      "kty": "RSA",
      "kid": "gxGP1sgIVSrKTUlfBKKqdKk97UQEhEDFSMRY2vstxQk",
      "n": "zZIicc7d7hocYTs5RtWbOB0cdK59XGgHMq8xoiCty8Q-HsHFD6o3KPDjW6zEsYbqzGW22cmrFMg7OdoLr3hYRlWEM34DJiSyu-3gJBAesx3AD--rab-i2BbgzU3qfaVHPubUCMjQ2OYSnlDJMa4M3JE1ci6c3B9rbLoI1FE-AO2neG610df94arrAOdwzbnCOEr-wiUwBjHpzfEsIn2yMMb5TkRUPEL2AgXVvB2dU76Ac9xOr5O4MqTVL0TQFTKrkGqW-i0tFZJkoG5OXEmqKXL8p9jqvnMZ0oe4JYGXH3qKWfNI-GxZrGZytlAyZwBpvkQu0NUrzA4YW-xf9cfe3w",
      "e": "AQAB"
    }
  ]
}

Can somebody please explain how it all makes sense? My understanding was that the underlying key type must match the signing algo.

UPDATE: The crypto output:

const keystore = require('./util/keystore')

const key = require('crypto').createPrivateKey(
    {
      key: keystore.getPrivateKey(),
      format: 'pem',
      encoding: 'utf8'
    }
)

console.log(`key.asymmetricKeyType: ${key.asymmetricKeyType}`) // rsa
console.log(`key.asymmetricKeyDetails: ${key.asymmetricKeyDetails}`) // undefined
Dmitry Kankalovich
  • 553
  • 2
  • 8
  • 19
  • 1
    What do you get when you pass your, presumably pem-encoded, private key to crypto.createPrivateKey()? https://nodejs.org/api/crypto.html#crypto_keyobject_asymmetrickeytype –  Aug 24 '21 at 11:42
  • Added update with the formatted code and output of crypto. – Dmitry Kankalovich Aug 24 '21 at 12:39
  • 2
    Well, then what you have stored on disk is not an EC key. It is an RSA one and node-jose behaves correctly. –  Aug 24 '21 at 12:43
  • @FilipSkokan you must be right. Key source visually looks like RSA key rather than a much compact EC key. Also, the source is typically prepended with the key type explicitly. This could be some sort of bug in the `openssl` version I use. Gonna look further into this. – Dmitry Kankalovich Aug 24 '21 at 13:06
  • @FilipSkokan yes, misuse of `openssl` was the root cause. Posted answer with the explanation. Thanks a lot! – Dmitry Kankalovich Aug 24 '21 at 13:22

1 Answers1

2

Apparently, the problem was not in the JWT / JWK dependencies, but in the chain of openssl commands which were involved in the key and X509 generation process.

One of the commands - openssl req - mistakenly contained param -keyout (instead of -key) which was implicitly generating an RSA key without any mention of the key type in its source, as well as overriding the original EC key.

Unfortunately, I couldn't find a simple openssl command to verify the key type - something that I tried to do before posting this question - but my general recommendation is to look for the key type in the key source file. Looks like it is either explicitly stated, or implicitly assumed as RSA.

Dmitry Kankalovich
  • 553
  • 2
  • 8
  • 19