Keypairs are great for device-based authentication, JWTs, JWKs, and all sorts of goodies that are going to make authentication much simpler and more convenient (as well as an order of magnitude more secure).

And it's been a long time coming, but Exposing OpenSSL RSA KeyGen (#15116) was finally closed by crypto: add key pair generation (#22660).

This means that we no longer need uRSA* or elliptic**. We have native RSA and EC support in node.

Yay!

* uRSA does convert from the one-off OpenSSH public key format to PEM, for which I haven't seen another implementation in node.

** elliptic is still a dependency of jwk-to-pem and pem-to-jwk, but it could reasonably be removed with a patch

Today's Topics

  • Generating RSA keys
  • Generating EC keys (for ECDSA or ECDH)
    • Options
    • RSA vs EC
    • Stardards & Encoding Formats
  • Using a passphrase
  • What it doesn't do: JWK, etc
  • Node's crypto docs, etc

Now Let's explore

First, let's see what generating a public/private RSA keypair looks like with this new addition to node crypto:

Generating RSA Keypairs

Update: I wrote Rasha.js to make RSA key generation simpler and to support PEM to JWK and JWK to PEM (including the SSH format).

'use strict';
var crypto = require('crypto');

crypto.generateKeyPair('rsa', {
  modulusLength: 2048
, publicKeyEncoding: {
    type: 'spki'
  , format: 'pem'
  }
, privateKeyEncoding: {
    type: 'pkcs8'
  , format: 'pem'
  }
}, function (err, publicKey, privateKey) {
  if (err) { throw err; } // may signify a bad 'type' name, etc

  console.log(publicKey);
  console.log(privateKey);
});

The output will look like this:

-----BEGIN PUBLIC KEY-----
MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDGDp6RwIzRoKvLg6uT6+wWoIcs
YbXYVU+dzPoteu7NKS0dX7DgIxjeh4JtjDdrX8UyaCb2y6UdbRKW8NV1MKmvM8dc
2JU7O2pHEaBsuYjP0bgygXmdnnaufsmV98YjVltYOOI3Nh0PVi+XefKL2K0UQWgO
hOIlU4dNYrran9+epQIDAQAB
-----END PUBLIC KEY-----
-----BEGIN PRIVATE KEY-----
MIICeQIBADANBgkqhkiG9w0BAQEFAASCAmMwggJfAgEAAoGBAMYOnpHAjNGgq8uD
q5Pr7BaghyxhtdhVT53M+i167s0pLR1fsOAjGN6Hgm2MN2tfxTJoJvbLpR1tEpbw
1XUwqa8zx1zYlTs7akcRoGy5iM/RuDKBeZ2edq5+yZX3xiNWW1g44jc2HQ9WL5d5
8ovYrRRBaA6E4iVTh01iutqf356lAgMBAAECgYEAxRP1C7mbJlkHuboquEWRJi7U
cwBDj6HMWIyIAUuLZlDr2IfInC+wZnZW/aUB3HFu6yqiYv/fLDnFvrak4TjsDm3N
2kXjSjcW/MOwMqEwgvEaE7qmaxx9TNdy6C1q4P0IvftkZ5+Fu7F8nwJ2e4F8J1+B
LBMTtqnhmOrfgws9aZ0CQQD14/D2lSs24IFH2hlK+wPlf4IvUabcsQl+tsQIshCU
Z6H7xbsxfho9IkF2IIlljjqO7bUybRsZ7bJXddyND/o7AkEAzjM4fWqXUuBpP1ay
RNMBHLfb8XfSwXDQo2IfXXNGeEkI4D3sMTnhWerdBhReNOy3lceYNIyEGsh2XYVX
WLJcnwJBAIhzRXSQsrpxO0y0KvUA9tiUOZoopYAyfiJjKcXpimnQWINu5sJASC9E
oy76P0Sr+LL4FmU1RqTM0vrV3N4qz6ECQQCNfwCIr5hfurb+S9PQ/qqItnIrZPou
2+eP9klnqy70Y8m/dz6ZGQrW1SAOh/ONhdME6Q49IR+V8XGoA1RI/TwpAkEA3M17
NwmhfI9On5NI98FMADBdPBYzNqfgFPCE/WTvq5W/nN2wSyjFnarXaX35WueNEXeC
YKwB7K/MaZMFB5kjQA==
-----END PRIVATE KEY-----

In fact, it will be about twice as long,

Options

For the most part, you won't adjust the options for RSA.

That said, it's worth mentioning modulusLength, which is the number of bits (i.e. a 2048-bit key is 256 bytes) in the key.

  • 1024 is too low (insecure)
  • 4096 is a bit high (needless cpu waste and jwt size bloat)
  • 2048 is just right

Note that bit-entropy is exponential, not multiplicative. 2048-bit keys are not twice as secure as 1024-bit keys, they're 4.3 billion times more secure (for reference Math.pow(2, 1024) === Infinity).

If the scale weren't exponential, 4096-bit keys would be insecure too, but it is and so 2048 is more than safe.

Generating EC Keypairs

Update: I wrote Eckles.js to make EC key generation simpler and to support PEM to JWK and JWK to PEM (including the SSH format).

You'll notice that the code here is almost identical, so we'll expand on the differences below.

'use strict';
var crypto = require('crypto');

crypto.generateKeyPair('ec', {
  namedCurve: 'P-256'
, publicKeyEncoding: {
    type: 'spki'
  , format: 'pem'
  }
, privateKeyEncoding: {
    type: 'pkcs8'
  , format: 'pem'
  }
}, function (err, publicKey, privateKey) {
  if (err) { throw err; } // may signify a bad 'type' name, etc

  console.log(publicKey);
  console.log(privateKey);
});

What you'll notice about this output is that they keys are significantly smaller (remember that I only used half the normal keysize in the previous example).

-----BEGIN PUBLIC KEY-----
MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEmLbGRn/RYT9WvttwkjDaMf0uleim
+QrbNejSzJPD8fgPpJ2bzoppoBnN5YI7xjr8faY/N/VsvcZxIJrQGsLQpA==
-----END PUBLIC KEY-----
-----BEGIN PRIVATE KEY-----
MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgbcbMs7jqcSD6Y1mq
p0IKmy9yS6bS4RPD+oZ+d1s93MWhRANCAASYtsZGf9FhP1a+23CSMNox/S6V6Kb5
Cts16NLMk8Px+A+knZvOimmgGc3lgjvGOvx9pj839Wy9xnEgmtAawtCk
-----END PRIVATE KEY-----

Options

You should first notice that we pass ec rather than rsa.

After that, see that the namedCurve option replaces modulusLength and, although the value is one of the strings 'P-256' or 'P-384', and 'P-521' (not a typo), the number part of the name defines bit entropy of strength directly comparable to that of RSA.

Unlike RSA, however, these names actually refer to entirely different EC algorithms!

  • 'P-256' (the one you want) refers to prime256v1, which is widely adopted
  • 'P-384' refers to secp384r1, which is less widely adopted, but theoretically more secure
  • 'P-521' (not a typo) refers to... some other standard which is... not necessary

Let's continue.

Why RSA vs ECDSA? (Bonus Material)

As I was writing on this I went on a definite aside and decided to pull it out into it's own thing:

The TL;DR of knowing when to pick which and why is this:

  • Both are equally secure (for all existing known mathematics)
  • EC has smaller keys, faster keygen, but slower sign/verify (and encrypt/decrypt)
  • RSA has much larger keys, much slower keygen, but faster sign/verify (and encrypt/decrypt)
  • Both only really use encrypt/decrypt to handshake AES keys (so it's always fast enough)

spki, sec1, pkix, pem, and DER... Oh my! (Aside)

If you want the quick and dirty on SPKI / PKCS#8 / PKCS#1 / SEC1 / PEM / DER, I mention it briefly in RSA vs EC / ECDSA.

If you want to go a little deeper on that, I think I cover it enough to get you started in ASN.1 for Dummies.

The TL;DR is this:

  • PEM is just base64 encoded DER
  • DER is a binary serialization of ASN.1 (essentially protobuf of the dark ages)
  • PKCS#1, SEC1, and PKCS#8 / SPKI (a.k.a. PKIX) are all x.509 schemas for ASN.1
  • ASN.1 is... Abstract... (a written notation that can describe binary packing)

Passphrase-protected keys

Going back to our previous RSA example, all we need to do to passphrase-protect our private key is to define the cipher and passphrase arguments:

'use strict';
var crypto = require('crypto');

crypto.generateKeyPair('rsa', {
  modulusLength: 2048
, publicKeyEncoding: {
    type: 'spki'
  , format: 'pem'
  }
, privateKeyEncoding: {
    type: 'pkcs8'
  , format: 'pem'
  , cipher: 'aes-128-cbc'     // the recommended cipher (use this)
  , passphrase: 'top secret'  // used locally
  }
}, function (err, publicKey, privateKey) {
  if (err) { throw err; } // may signify a bad 'type' name, etc

  console.log(publicKey);
  console.log(privateKey);
});

Options

Again, unless you have a really good reason not to, you'll want to stick with 'aes-128-cbc', which is a great cipher. I won't go into all the reasons, but suffice it to say that aes-256 is just a waste of cpu cycles and battery life, so give aes-128 some love.

Also, a crucial detail that you need to remember about key passphrases is that they never leave the device. This is like the PIN on your iPhone or Windows 10 computer. It isn't sent out to the Internet (or so we hope). It's a local authentication mechanism, before you ever sign a key or connect to a server.

As such, you'll need to use this same passphrase when you use crypto's signer. In contrast, verify uses the public key, which is not passphrase-protected (it's public).

One last thing I'll mention here is that we don't have to worry about the initialization vector. Thankfully, how that's handled is part of the pkcs8 spec, so that's why we don't talk about it here (for those of you familiar with using AES IVs previous with node crypto).

Now, wrapping up...

What it doesn't do (and why)

The new APIs don't directly support either JWKs, which you need for all sorts of web authentication API stuff (JOSE, etc) nor CSRs, which you need for SSL certificates, Let's Encrypt clients (i.e. ACME.js, Greenlock.js), and such.

I hope that a future version of node has native JWK support as well. Until then the modules pem-to-jwk, rsa-pem-to-jwk, and jwk-to-pem will have to suffice (and they're not bad). This is still a HUGE leap forward.

I suspect the reason that some of these other issues aren't tacked at this time is that JavaScript still doesn't have BigInt support, and node has been bitten by trying to create standards and then just being bulldozed by the committies of JS proper, as well as a lack of time.

Also, although most of node's APIs now support promises, some APIs, like this one, don't. In this case, it kinda makes sense because it has multiple resolvable values, and it's still pretty easy to wrap.

Node's crypto docs (and other resources)

Node's crypto docs are at https://nodejs.org/api/crypto.html.

Specifically you'll want to take a look at crypto.generateKeyPair, and then probably continue on to the sections about signing and verifying - which I believ you'll be able to grok without too much trouble if you followed the examples above. And if not, ask me.

If you click on "Blog" up at the top and then "ctrl+f" to search titles of articles, you'll see that I have plenty of other related topics.

If I could hand pick for you (which I can), I'd say you might be interested in learning more about symmetric and asymmetric encryption, or perhaps Let's Encrypt ACME v2. Take a peek, there are tons of other fun goodies.


By AJ ONeal

If you loved this and want more like it, sign up!


Did I make your day?
Buy me a coffeeBuy me a coffee  

(you can learn about the bigger picture I'm working towards on my patreon page )