2

I want to have only one pair of keys that I can use for ECDH functions and for other function in node:crypto module.

I know there are two ways how to generate keys with node:crypto module.

One way is to use crypto.generateKeyPairSync. This generates keys in format that is accepted by almost all cryptographic functions in node:crypto module:

const crypto = require('node:crypto')
const {publicKey, privateKey} = crypto.generateKeyPairSync('ec', {
    namedCurve: 'secp224r1'
})

const pubKey = publicKey.export({type: 'spki', format: 'pem'}).toString()
const privKey = privateKey.export({type: 'pkcs8', format: 'pem'}).toString()

This outputs keys in pem format.

But if I want to use ECDH I need to generate keys in the following way:

const crypto = require('node:crypto')
const ecdh = crypto.createECDH('secp224r1')
ecdh.generateKeys()

const rawPublic = ecdh.getPublicKey('base64', 'uncompressed')
const rawPrivate = ecdh.getPrivateKey('base64')

Which generates just the raw keys.

How do I generate PEM from raw key or raw key from PEM so I can use only one set of keys instead of generating new set of keys for ECDH?

Topaco
  • 40,594
  • 4
  • 35
  • 62
David Novák
  • 1,455
  • 2
  • 18
  • 30
  • AFAIK, the crypto module doesn't support direct conversion ASN.1/DER <-> raw. Also the conversion via JWK doesn't work, because JWK doesn't support secp224r1. What is possible, for a fixed curve, is to replace the raw keys embedded in the ASN.1/DER byte sequences, which is shown e.g. [here](https://stackoverflow.com/a/48102827/9014097) for curve secp256k1 and a SEC1 key. However, there are NodeJS libraries that make those conversions much more convenient. – Topaco Dec 28 '22 at 18:28
  • @Topaco thanks, will you mind sharing the name of the libraries that can help me with that? – David Novák Dec 29 '22 at 08:29
  • Please have a look at my answer. – Topaco Dec 29 '22 at 12:05

1 Answers1

3

The crypto module does not directly support the conversion ASN.1/DER <-> raw. A third party library that supports this is e.g. eckey-utils.

The conversion from raw to PEM key is possible e.g. as follows.

const privKey = Buffer.from('765573f9676d39f1256d01f1fb2806d30bbfaab8b04ae745d0a77c03', 'hex');
const pubKey = Buffer.from('04468a685192db85873baa45dbec2bcc8217f5291e09e1b581c7f27f3f5585dc535a13e1862563aeb99de167a49557f1a2d49fee67af017eba', 'hex'); // uncompressed

const ecKeyUtils = require('eckey-utils');
const curveName = 'secp224r1';
const pems = ecKeyUtils.generatePem({curveName, privateKey: privKey, publicKey: pubKey});
const x509Pem = pems.publicKey;
const sec1Pem = pems.privateKey;

Thereby the private key is exported in SEC1 format. If the PKCS#8 format is needed, a conversion with the crypto module is possible:

const crypto = require('crypto')
const pkcs8PemFromSec1 = crypto.createPrivateKey({key: sec1Pem, format: 'pem', type: 'sec1'}).export({type: 'pkcs8', format: 'pem'}).toString();

The reverse is:

const privKey = ecKeyUtils.parsePem(sec1Pem).privateKey;
const pubKeyFromPriv = ecKeyUtils.parsePem(sec1Pem).publicKey;
const pubKey = ecKeyUtils.parsePem(x509Pem).publicKey;

If the private key is in PKCS#8 format, it must be converted to SEC1 format beforehand:

const crypto = require('crypto');
const sec1PemFromPkcs8 = crypto.createPrivateKey({key: pkcs8Pem, format: 'pem', type: 'pkcs8'}).export({type: 'sec1', format: 'pem'});

Note that a trim() is needed here before use in parsePem() to remove the trailing newline (0x0a), which parsePem() does not allow.


Another approach for the conversion of raw to PEM keys is to replace the raw keys embedded in the ASN.1/DER byte sequences, as e.g. in the following for the conversion of a raw private key into a PKCS#8 key (which also contains the public key) and of a raw public key into an X.509/SPKI key for curve secp224r1:

const privKey = Buffer.from('765573f9676d39f1256d01f1fb2806d30bbfaab8b04ae745d0a77c03', 'hex');
const pubKey = Buffer.from('04468a685192db85873baa45dbec2bcc8217f5291e09e1b581c7f27f3f5585dc535a13e1862563aeb99de167a49557f1a2d49fee67af017eba', 'hex'); // uncompressed

const crypto = require('crypto');
const privA = Buffer.from('3078020100301006072a8648ce3d020106052b810400210461305f020101041c', 'hex');
const privB = Buffer.from('a13c033a00', 'hex');
const pkcs8Der = Buffer.concat([privA, privKey, privB, pubKey]);
const pkcs8 = crypto.createPrivateKey({key: pkcs8Der, format: 'der', type: 'pkcs8'}).export({type: 'pkcs8', format: 'pem'});

const pubA = Buffer.from('304e301006072a8648ce3d020106052b81040021033a00', 'hex');
const x509Der = Buffer.concat([pubA, pubKey]);
const x509 = crypto.createPublicKey({key: x509Der, format: 'der', type: 'spki'}).export({type: 'spki', format: 'pem'});

For the reverse direction, the raw keys can be extracted at their respective positions in the ASN.1/DER encoding.

The advantage of this approach is no dependency, the disadvantage that privA, privB and pubA are ASN.1/DER encodings that contain metadata, such as the curve or length information (as can be seen when examining the PEM keys in an ASN.1 parser, e.g. https://lapo.it/asn1js/), so they are different for each curve.

Topaco
  • 40,594
  • 4
  • 35
  • 62
  • I used asn1.js but this approach seems to be much more easier and straight forward. It's sad that nodejs does not include utility function to convert raw keys to pem format. Is there any reason for that? – David Novák Jan 02 '23 at 14:08
  • 1
    @DavidNovák - Ultimately, only the developers can answer that. In my opinion, it would be useful if the crypto module would support both formats without gaps. – Topaco Jan 02 '23 at 16:35