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.