v1.3.0: Add key generation, document CLI

This commit is contained in:
AJ ONeal 2018-11-24 20:21:27 -07:00
parent b4f15f5f29
commit 3102515664
6 changed files with 497 additions and 290 deletions

103
README.md
View File

@ -1,19 +1,76 @@
eckles.js
[Eckles.js](https://git.coolaj86.com/coolaj86/eckles.js)
=========
Sponsored by [Root](https://therootcompany.com).
Built for [ACME.js](https://git.coolaj86.com/coolaj86/acme.js)
and [Greenlock.js](https://git.coolaj86.com/coolaj86/greenlock.js)
| < 600 lines of code | 3kb gzipped | 10kb minified | 17kb with comments |
ECDSA (elliptic curve) tools. Lightweight. Zero Dependencies. Universal compatibility.
* [x] Fast and Easy EC Key Generation
* [x] PEM-to-JWK
* [x] JWK-to-PEM
* [x] SSH "pub" format
* [x] CLI
* [ ] RSA
* **Need RSA tools?** Check out [Rasha.js](https://git.coolaj86.com/coolaj86/rasha.js)
This project is fully functional and tested (and the code is pretty clean).
## Install
It is considered to be complete, but if you find a bug please open an issue.
For node.js:
```bash
npm install --save eckles
```
CLI:
```bash
npm install -g eckles
```
## Generate EC (ECDSA/ECDH) Key
Achieves the *fastest possible key generation* using node's native EC bindings to OpenSSL,
then converts to JWK for ease-of-use.
```js
Eckles.generate({ format: 'jwk' }).then(function (keypair) {
console.log(keypair.private);
console.log(keypair.public);
});
```
**options**
* `format` defaults to `'jwk'`
* `'sec1'` (traditional)
* `'pkcs8'`
* `'ssh'`
* `encoding` defaults to `'json'`
* `'pem'` (type + DER.toString('base64'))
* `'der'`
**advanced options**
* `namedCurve` defaults to `'P-256'`
* `P-384` is also supported
* larger keys have not been implemented
* A) because they're a senseless waste
* B) they have similar, but slightly different formats
#### Generate EC Key CLI
```bash
# Generate a key in each format
# eckles [format] [curve|encoding]
eckles jwk
eckles sec1 pem
eckles pkcs8 der
eckles ssh P-256
```
## PEM-to-JWK
@ -22,11 +79,11 @@ It is considered to be complete, but if you find a bug please open an issue.
* [x] SSH (RFC4716), (RFC 4716/SSH2)
```js
var eckles = require('eckles');
var Eckles = require('eckles');
var pem = require('fs')
.readFileSync('./node_modles/eckles/fixtures/privkey-ec-p256.sec1.pem', 'ascii');
eckles.import({ pem: pem }).then(function (jwk) {
Eckles.import({ pem: pem }).then(function (jwk) {
console.log(jwk);
});
```
@ -41,6 +98,17 @@ eckles.import({ pem: pem }).then(function (jwk) {
}
```
#### EC PEM to JWK CLI
```bash
# Convert SEC1, PKCS8, SPKI, SSH to JWK
# eckles [keyfile]
eckles node_modules/eckles/fixtures/privkey-ec-p256.sec1.pem
eckles node_modules/eckles/fixtures/privkey-ec-p384.pkcs8.pem
eckles node_modules/eckles/fixtures/pub-ec-p256.spki.pem
eckles node_modules/eckles/fixtures/pub-ec-p384.ssh.pub
```
## JWK-to-PEM
* [x] SEC1/X9.62, PKCS#8, SPKI/PKIX
@ -48,10 +116,10 @@ eckles.import({ pem: pem }).then(function (jwk) {
* [x] SSH (RFC4716), (RFC 4716/SSH2)
```js
var eckles = require('eckles');
var Eckles = require('eckles');
var jwk = require('eckles/fixtures/privkey-ec-p256.jwk.json');
eckles.export({ jwk: jwk }).then(function (pem) {
Eckles.export({ jwk: jwk }).then(function (pem) {
// PEM in SEC1 (x9.62) format
console.log(pem);
});
@ -65,6 +133,17 @@ yZe7CnFsqeDcpnPbubP6cpYiVcnevNIYyg==
-----END EC PRIVATE KEY-----
```
#### EC PEM to JWK CLI
```bash
# Convert JWK to SEC1, PKCS8, SPKI, SSH
# eckles [keyfile] [format]
eckles node_modules/eckles/fixtures/privkey-ec-p256.jwk.json sec1
eckles node_modules/eckles/fixtures/privkey-ec-p384.jwk.json pkcs8
eckles node_modules/eckles/fixtures/pub-ec-p256.jwk.json spki
eckles node_modules/eckles/fixtures/pub-ec-p384.jwk.json ssh
```
### Advanced Options
`format: 'pkcs8'`:
@ -73,7 +152,7 @@ The default output format is `sec1`/`x9.62` (EC-specific format) is used for pri
Use `format: 'pkcs8'` to output in PKCS#8 format instead.
```js
eckles.export({ jwk: jwk, format: 'pkcs8' }).then(function (pem) {
Eckles.export({ jwk: jwk, format: 'pkcs8' }).then(function (pem) {
// PEM in PKCS#8 format
console.log(pem);
});
@ -97,7 +176,7 @@ To get the same format as you
would get with `ssh-keygen`, pass `ssh` as the format option:
```js
eckles.export({ jwk: jwk, format: 'ssh' }).then(function (pub) {
Eckles.export({ jwk: jwk, format: 'ssh' }).then(function (pub) {
// Special SSH2 Public Key format (RFC 4716)
console.log(pub);
});
@ -114,7 +193,7 @@ If a private key is used as input, a private key will be output.
If you'd like to output a public key instead you can pass `public: true` or `format: 'spki'`.
```js
eckles.export({ jwk: jwk, public: true }).then(function (pem) {
Eckles.export({ jwk: jwk, public: true }).then(function (pem) {
// PEM in SPKI/PKIX format
console.log(pem);
});
@ -160,7 +239,7 @@ Goals of this project
Legal
-----
Licensed MPL-2.0
[Eckles.js](https://git.coolaj86.com/coolaj86/eckles.js) |
MPL-2.0 |
[Terms of Use](https://therootcompany.com/legal/#terms) |
[Privacy Policy](https://therootcompany.com/legal/#privacy)

View File

@ -2,11 +2,35 @@
'use strict';
var fs = require('fs');
var eckles = require('../index.js');
var Eckles = require('../index.js');
var infile = process.argv[2];
var format = process.argv[3];
if (!infile) {
infile = 'jwk';
}
if (-1 !== [ 'jwk', 'pem', 'json', 'der', 'sec1', 'pkcs8', 'spki', 'ssh' ].indexOf(infile)) {
console.log("Generating new key...");
Eckles.generate({
format: infile
, namedCurve: format === 'P-384' ? 'P-384' : 'P-256'
, encoding: format === 'der' ? 'der' : 'pem'
}).then(function (key) {
if ('der' === infile || 'der' === format) {
key.private = key.private.toString('binary');
key.public = key.public.toString('binary');
}
console.log(key.private);
console.log(key.public);
}).catch(function (err) {
console.error(err);
process.exit(1);
});
return;
}
var key = fs.readFileSync(infile, 'ascii');
try {
@ -17,14 +41,14 @@ try {
if ('string' === typeof key) {
var pub = (-1 !== [ 'public', 'spki', 'pkix' ].indexOf(format));
eckles.import({ pem: key, public: (pub || format) }).then(function (jwk) {
Eckles.import({ pem: key, public: (pub || format) }).then(function (jwk) {
console.log(JSON.stringify(jwk, null, 2));
}).catch(function (err) {
console.error(err);
process.exit(1);
});
} else {
eckles.export({ jwk: key, format: format }).then(function (pem) {
Eckles.export({ jwk: key, format: format }).then(function (pem) {
console.log(pem);
}).catch(function (err) {
console.error(err);

View File

@ -1,9 +1,10 @@
'use strict';
var ASN1;
var EC = module.exports;
var Hex = {};
var PEM = {};
var Enc = require('./encoding.js');
var ASN1;
var PEM = require('./pem.js');
// 1.2.840.10045.3.1.7
// prime256v1 (ANSI X9.62 named elliptic curve)
@ -27,104 +28,6 @@ var SSH_EC_P384 = '00000013 65 63 64 73 61 2d 73 68 61 32 2d 6e 69 73 74 70 33 3
// The one good thing that came from the b***kchain hysteria: good EC documentation
// https://davidederosa.com/basic-blockchain-programming/elliptic-curve-keys/
PEM._toUrlSafeBase64 = function (u8) {
//console.log('Len:', u8.byteLength);
return Buffer.from(u8).toString('base64')
.replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, '');
};
PEM.parseBlock = function pemToDer(pem) {
var typ;
var pub;
var crv;
var der = Buffer.from(pem.split(/\n/).filter(function (line, i) {
if (0 === i) {
if (/ PUBLIC /.test(line)) {
pub = true;
} else if (/ PRIVATE /.test(line)) {
pub = false;
}
if (/ EC/.test(line)) {
typ = 'EC';
}
}
return !/---/.test(line);
}).join(''), 'base64');
if (!typ || 'EC' === typ) {
var hex = toHex(der);
if (-1 !== hex.indexOf(OBJ_ID_EC)) {
typ = 'EC';
crv = 'P-256';
} else if (-1 !== hex.indexOf(OBJ_ID_EC_384)) {
typ = 'EC';
crv = 'P-384';
} else {
// TODO support P-384 as well (but probably nothing else)
console.warn("unsupported ec curve");
}
}
return { kty: typ, pub: pub, der: der, crv: crv };
};
PEM.packBlock = function (opts) {
// TODO allow for headers?
return '-----BEGIN ' + opts.type + '-----\n'
+ toBase64(opts.bytes).match(/.{1,64}/g).join('\n') + '\n'
+ '-----END ' + opts.type + '-----'
;
};
function toHex(ab) {
var hex = [];
var u8 = new Uint8Array(ab);
var size = u8.byteLength;
var i;
var h;
for (i = 0; i < size; i += 1) {
h = u8[i].toString(16);
if (2 === h.length) {
hex.push(h);
} else {
hex.push('0' + h);
}
}
return hex.join('').replace(/\s+/g, '').toLowerCase();
}
Hex.fromAB = toHex;
Hex.fromInt = function numToHex(d) {
d = d.toString(16);
if (d.length % 2) {
return '0' + d;
}
return d;
};
Hex.toUint8 = function (hex) {
var buf = Buffer.from(hex, 'hex');
var ab = buf.buffer;
return new Uint8Array(ab.slice(buf.offset, buf.offset + buf.byteLength));
};
function toBase64(u8) {
return Buffer.from(u8).toString('base64');
}
function urlBase64ToBase64(ub64) {
var r = ub64 % 4;
if (2 === r) {
ub64 += '==';
} else if (3 === r) {
ub64 += '=';
}
return ub64.replace(/-/g, '+').replace(/_/g, '/');
}
function base64ToUint8(b64) {
var buf = Buffer.from(b64, 'base64');
return new Uint8Array(buf.buffer.slice(buf.byteOffset, buf.byteOffset + buf.byteLength));
}
EC.parseSec1 = function parseEcOnlyPrivkey(u8, jwk) {
var index = 7;
var len = 32;
@ -156,12 +59,12 @@ EC.parseSec1 = function parseEcOnlyPrivkey(u8, jwk) {
return {
kty: jwk.kty
, crv: jwk.crv
, d: PEM._toUrlSafeBase64(d)
//, dh: toHex(d)
, x: PEM._toUrlSafeBase64(x)
//, xh: toHex(x)
, y: PEM._toUrlSafeBase64(y)
//, yh: toHex(y)
, d: Enc.bufToUrlBase64(d)
//, dh: Enc.bufToHex(d)
, x: Enc.bufToUrlBase64(x)
//, xh: Enc.bufToHex(x)
, y: Enc.bufToUrlBase64(y)
//, yh: Enc.bufToHex(y)
};
};
@ -193,12 +96,12 @@ EC.parsePkcs8 = function parseEcPkcs8(u8, jwk) {
return {
kty: jwk.kty
, crv: jwk.crv
, d: PEM._toUrlSafeBase64(d)
//, dh: toHex(d)
, x: PEM._toUrlSafeBase64(x)
//, xh: toHex(x)
, y: PEM._toUrlSafeBase64(y)
//, yh: toHex(y)
, d: Enc.bufToUrlBase64(d)
//, dh: Enc.bufToHex(d)
, x: Enc.bufToUrlBase64(x)
//, xh: Enc.bufToHex(x)
, y: Enc.bufToUrlBase64(y)
//, yh: Enc.bufToHex(y)
};
};
@ -225,10 +128,10 @@ EC.parseSpki = function parsePem(u8, jwk) {
return {
kty: jwk.kty
, crv: jwk.crv
, x: PEM._toUrlSafeBase64(x)
//, xh: toHex(x)
, y: PEM._toUrlSafeBase64(y)
//, yh: toHex(y)
, x: Enc.bufToUrlBase64(x)
//, xh: Enc.bufToHex(x)
, y: Enc.bufToUrlBase64(y)
//, yh: Enc.bufToHex(y)
};
};
EC.parsePkix = EC.parseSpki;
@ -237,7 +140,7 @@ EC.parseSsh = function (pem) {
var jwk = { kty: 'EC', crv: null, x: null, y: null };
var b64 = pem.split(/\s+/g)[1];
var buf = Buffer.from(b64, 'base64');
var hex = buf.toString('hex');
var hex = Enc.bufToHex(buf);
var index = 40;
var len;
if (0 === hex.indexOf(SSH_EC_P256)) {
@ -249,141 +152,224 @@ EC.parseSsh = function (pem) {
}
var x = buf.slice(index, index + len);
var y = buf.slice(index + len, index + len + len);
jwk.x = PEM._toUrlSafeBase64(x);
jwk.y = PEM._toUrlSafeBase64(y);
jwk.x = Enc.bufToUrlBase64(x);
jwk.y = Enc.bufToUrlBase64(y);
return jwk;
};
/*global Promise*/
EC.generate = function (opts) {
return Promise.resolve().then(function () {
var typ = 'ec';
var format = opts.format;
var encoding = opts.encoding;
var priv;
var pub = 'spki';
if (!format) {
format = 'jwk';
}
if (-1 !== [ 'spki', 'pkcs8', 'ssh' ].indexOf(format)) {
format = 'pkcs8';
}
if ('pem' === format) {
format = 'sec1';
encoding = 'pem';
} else if ('der' === format) {
format = 'sec1';
encoding = 'der';
}
if ('jwk' === format || 'json' === format) {
format = 'jwk';
encoding = 'json';
} else {
priv = format;
}
if (!encoding) {
encoding = 'pem';
}
if (priv) {
priv = { type: priv, format: encoding };
pub = { type: pub, format: encoding };
} else {
// jwk
priv = { type: 'sec1', format: 'pem' };
pub = { type: 'spki', format: 'pem' };
}
return new Promise(function (resolve, reject) {
return require('crypto').generateKeyPair(typ, {
namedCurve: opts.crv || opts.namedCurve || 'P-256'
, privateKeyEncoding: priv
, publicKeyEncoding: pub
}, function (err, pubkey, privkey) {
if (err) { reject(err); }
resolve({
private: privkey
, public: pubkey
});
});
}).then(function (keypair) {
if ('jwk' === format) {
return {
private: EC.importSync({ pem: keypair.private, format: priv.type })
, public: EC.importSync({ pem: keypair.public, format: pub.type, public: true })
};
}
if ('ssh' !== opts.format) {
return keypair;
}
return {
private: keypair.private
, public: EC.exportSync({ jwk: EC.importSync({
pem: keypair.public, format: format, public: true
}), format: opts.format, public: true })
};
});
});
};
EC.importSync = function importEcSync(opts) {
if (!opts || !opts.pem || 'string' !== typeof opts.pem) {
throw new Error("must pass { pem: pem } as a string");
}
if (0 === opts.pem.indexOf('ecdsa-sha2-')) {
return EC.parseSsh(opts.pem);
}
var pem = opts.pem;
var u8 = PEM.parseBlock(pem).der;
var hex = Enc.bufToHex(u8);
var jwk = { kty: 'EC', crv: null, x: null, y: null };
//console.log();
if (-1 !== hex.indexOf(OBJ_ID_EC)) {
jwk.crv = "P-256";
// PKCS8
if (0x02 === u8[3] && 0x30 === u8[6] && 0x06 === u8[8]) {
//console.log("PKCS8", u8[3].toString(16), u8[6].toString(16), u8[8].toString(16));
jwk = EC.parsePkcs8(u8, jwk);
// EC-only
} else if (0x02 === u8[2] && 0x04 === u8[5] && 0xA0 === u8[39]) {
//console.log("EC---", u8[2].toString(16), u8[5].toString(16), u8[39].toString(16));
jwk = EC.parseSec1(u8, jwk);
// SPKI/PKIK (Public)
} else if (0x30 === u8[2] && 0x06 === u8[4] && 0x06 === u8[13]) {
//console.log("SPKI-", u8[2].toString(16), u8[4].toString(16), u8[13].toString(16));
jwk = EC.parseSpki(u8, jwk);
// Error
} else {
//console.log("PKCS8", u8[3].toString(16), u8[6].toString(16), u8[8].toString(16));
//console.log("EC---", u8[2].toString(16), u8[5].toString(16), u8[39].toString(16));
//console.log("SPKI-", u8[2].toString(16), u8[4].toString(16), u8[13].toString(16));
throw new Error("unrecognized key format");
}
} else if (-1 !== hex.indexOf(OBJ_ID_EC_384)) {
jwk.crv = "P-384";
// PKCS8
if (0x02 === u8[3] && 0x30 === u8[6] && 0x06 === u8[8]) {
//console.log("PKCS8", u8[3].toString(16), u8[6].toString(16), u8[8].toString(16));
jwk = EC.parsePkcs8(u8, jwk);
// EC-only
} else if (0x02 === u8[3] && 0x04 === u8[6] && 0xA0 === u8[56]) {
//console.log("EC---", u8[3].toString(16), u8[6].toString(16), u8[56].toString(16));
jwk = EC.parseSec1(u8, jwk);
// SPKI/PKIK (Public)
} else if (0x30 === u8[2] && 0x06 === u8[4] && 0x06 === u8[13]) {
//console.log("SPKI-", u8[2].toString(16), u8[4].toString(16), u8[13].toString(16));
jwk = EC.parseSpki(u8, jwk);
// Error
} else {
//console.log("PKCS8", u8[3].toString(16), u8[6].toString(16), u8[8].toString(16));
//console.log("EC---", u8[3].toString(16), u8[6].toString(16), u8[56].toString(16));
//console.log("SPKI-", u8[2].toString(16), u8[4].toString(16), u8[13].toString(16));
throw new Error("unrecognized key format");
}
} else {
throw new Error("Supported key types are P-256 and P-384");
}
if (opts.public) {
if (true !== opts.public) {
throw new Error("options.public must be either `true` or `false` not ("
+ typeof opts.public + ") '" + opts.public + "'");
}
delete jwk.d;
}
return jwk;
};
EC.parse = function parseEc(opts) {
return Promise.resolve().then(function () {
if (!opts || !opts.pem || 'string' !== typeof opts.pem) {
throw new Error("must pass { pem: pem } as a string");
}
if (0 === opts.pem.indexOf('ecdsa-sha2-')) {
return EC.parseSsh(opts.pem);
}
var pem = opts.pem;
var u8 = PEM.parseBlock(pem).der;
var hex = toHex(u8);
var jwk = { kty: 'EC', crv: null, x: null, y: null };
//console.log();
if (-1 !== hex.indexOf(OBJ_ID_EC)) {
jwk.crv = "P-256";
// PKCS8
if (0x02 === u8[3] && 0x30 === u8[6] && 0x06 === u8[8]) {
//console.log("PKCS8", u8[3].toString(16), u8[6].toString(16), u8[8].toString(16));
jwk = EC.parsePkcs8(u8, jwk);
// EC-only
} else if (0x02 === u8[2] && 0x04 === u8[5] && 0xA0 === u8[39]) {
//console.log("EC---", u8[2].toString(16), u8[5].toString(16), u8[39].toString(16));
jwk = EC.parseSec1(u8, jwk);
// SPKI/PKIK (Public)
} else if (0x30 === u8[2] && 0x06 === u8[4] && 0x06 === u8[13]) {
//console.log("SPKI-", u8[2].toString(16), u8[4].toString(16), u8[13].toString(16));
jwk = EC.parseSpki(u8, jwk);
// Error
} else {
//console.log("PKCS8", u8[3].toString(16), u8[6].toString(16), u8[8].toString(16));
//console.log("EC---", u8[2].toString(16), u8[5].toString(16), u8[39].toString(16));
//console.log("SPKI-", u8[2].toString(16), u8[4].toString(16), u8[13].toString(16));
throw new Error("unrecognized key format");
}
} else if (-1 !== hex.indexOf(OBJ_ID_EC_384)) {
jwk.crv = "P-384";
// PKCS8
if (0x02 === u8[3] && 0x30 === u8[6] && 0x06 === u8[8]) {
//console.log("PKCS8", u8[3].toString(16), u8[6].toString(16), u8[8].toString(16));
jwk = EC.parsePkcs8(u8, jwk);
// EC-only
} else if (0x02 === u8[3] && 0x04 === u8[6] && 0xA0 === u8[56]) {
//console.log("EC---", u8[3].toString(16), u8[6].toString(16), u8[56].toString(16));
jwk = EC.parseSec1(u8, jwk);
// SPKI/PKIK (Public)
} else if (0x30 === u8[2] && 0x06 === u8[4] && 0x06 === u8[13]) {
//console.log("SPKI-", u8[2].toString(16), u8[4].toString(16), u8[13].toString(16));
jwk = EC.parseSpki(u8, jwk);
// Error
} else {
//console.log("PKCS8", u8[3].toString(16), u8[6].toString(16), u8[8].toString(16));
//console.log("EC---", u8[3].toString(16), u8[6].toString(16), u8[56].toString(16));
//console.log("SPKI-", u8[2].toString(16), u8[4].toString(16), u8[13].toString(16));
throw new Error("unrecognized key format");
}
} else {
throw new Error("Supported key types are P-256 and P-384");
}
if (opts.public) {
if (true !== opts.public) {
throw new Error("options.public must be either `true` or `false` not ("
+ typeof opts.public + ") '" + opts.public + "'");
}
delete jwk.d;
}
return jwk;
return EC.importSync(opts);
});
};
EC.toJwk = EC.import = EC.parse;
EC.exportSync = function (opts) {
if (!opts || !opts.jwk || 'object' !== typeof opts.jwk) {
throw new Error("must pass { jwk: jwk } as a JSON object");
}
var jwk = JSON.parse(JSON.stringify(opts.jwk));
var format = opts.format;
if (opts.public || -1 !== [ 'spki', 'pkix', 'ssh', 'rfc4716' ].indexOf(format)) {
jwk.d = null;
}
if ('EC' !== jwk.kty) {
throw new Error("options.jwk.kty must be 'EC' for EC keys");
}
if (!jwk.d) {
if (!format || -1 !== [ 'spki', 'pkix' ].indexOf(format)) {
format = 'spki';
} else if (-1 !== [ 'ssh', 'rfc4716' ].indexOf(format)) {
format = 'ssh';
} else {
throw new Error("options.format must be 'spki' or 'ssh' for public EC keys, not ("
+ typeof format + ") " + format);
}
} else {
if (!format || 'sec1' === format) {
format = 'sec1';
} else if ('pkcs8' !== format) {
throw new Error("options.format must be 'sec1' or 'pkcs8' for private EC keys");
}
}
if (-1 === [ 'P-256', 'P-384' ].indexOf(jwk.crv)) {
throw new Error("options.jwk.crv must be either P-256 or P-384 for EC keys");
}
if (!jwk.y) {
throw new Error("options.jwk.y must be a urlsafe base64-encoded either P-256 or P-384");
}
if ('sec1' === format) {
return PEM.packBlock({ type: "EC PRIVATE KEY", bytes: EC.packSec1(jwk) });
} else if ('pkcs8' === format) {
return PEM.packBlock({ type: "PRIVATE KEY", bytes: EC.packPkcs8(jwk) });
} else if (-1 !== [ 'spki', 'pkix' ].indexOf(format)) {
return PEM.packBlock({ type: "PUBLIC KEY", bytes: EC.packSpki(jwk) });
} else if (-1 !== [ 'ssh', 'rfc4716' ].indexOf(format)) {
return EC.packSsh(jwk);
} else {
throw new Error("Sanity Error: reached unreachable code block with format: " + format);
}
};
EC.pack = function (opts) {
return Promise.resolve().then(function () {
if (!opts || !opts.jwk || 'object' !== typeof opts.jwk) {
throw new Error("must pass { jwk: jwk }");
}
var jwk = JSON.parse(JSON.stringify(opts.jwk));
var format = opts.format;
if (opts.public || -1 !== [ 'spki', 'pkix', 'ssh', 'rfc4716' ].indexOf(format)) {
jwk.d = null;
}
if ('EC' !== jwk.kty) {
throw new Error("options.jwk.kty must be 'EC' for EC keys");
}
if (!jwk.d) {
if (!format || -1 !== [ 'spki', 'pkix' ].indexOf(format)) {
format = 'spki';
} else if (-1 !== [ 'ssh', 'rfc4716' ].indexOf(format)) {
format = 'ssh';
} else {
throw new Error("options.format must be 'spki' or 'ssh' for public EC keys, not ("
+ typeof format + ") " + format);
}
} else {
if (!format || 'sec1' === format) {
format = 'sec1';
} else if ('pkcs8' !== format) {
throw new Error("options.format must be 'sec1' or 'pkcs8' for private EC keys");
}
}
if (-1 === [ 'P-256', 'P-384' ].indexOf(jwk.crv)) {
throw new Error("options.jwk.crv must be either P-256 or P-384 for EC keys");
}
if (!jwk.y) {
throw new Error("options.jwk.y must be a urlsafe base64-encoded either P-256 or P-384");
}
if ('sec1' === format) {
return PEM.packBlock({ type: "EC PRIVATE KEY", bytes: EC.packSec1(jwk) });
} else if ('pkcs8' === format) {
return PEM.packBlock({ type: "PRIVATE KEY", bytes: EC.packPkcs8(jwk) });
} else if (-1 !== [ 'spki', 'pkix' ].indexOf(format)) {
return PEM.packBlock({ type: "PUBLIC KEY", bytes: EC.packSpki(jwk) });
} else if (-1 !== [ 'ssh', 'rfc4716' ].indexOf(format)) {
return EC.packSsh(jwk);
} else {
throw new Error("Sanity Error: reached unreachable code block with format: " + format);
}
return EC.exportSync(opts);
});
};
EC.packSec1 = function (jwk) {
var d = toHex(base64ToUint8(urlBase64ToBase64(jwk.d)));
var x = toHex(base64ToUint8(urlBase64ToBase64(jwk.x)));
var y = toHex(base64ToUint8(urlBase64ToBase64(jwk.y)));
var d = Enc.base64ToHex(jwk.d);
var x = Enc.base64ToHex(jwk.x);
var y = Enc.base64ToHex(jwk.y);
var objId = ('P-256' === jwk.crv) ? OBJ_ID_EC : OBJ_ID_EC_384;
return Hex.toUint8(
return Enc.hexToUint8(
ASN1('30'
, ASN1.UInt('01')
, ASN1('04', d)
@ -392,11 +378,11 @@ EC.packSec1 = function (jwk) {
);
};
EC.packPkcs8 = function (jwk) {
var d = toHex(base64ToUint8(urlBase64ToBase64(jwk.d)));
var x = toHex(base64ToUint8(urlBase64ToBase64(jwk.x)));
var y = toHex(base64ToUint8(urlBase64ToBase64(jwk.y)));
var d = Enc.base64ToHex(jwk.d);
var x = Enc.base64ToHex(jwk.x);
var y = Enc.base64ToHex(jwk.y);
var objId = ('P-256' === jwk.crv) ? OBJ_ID_EC : OBJ_ID_EC_384;
return Hex.toUint8(
return Enc.hexToUint8(
ASN1('30'
, ASN1.UInt('00')
, ASN1('30'
@ -411,10 +397,10 @@ EC.packPkcs8 = function (jwk) {
);
};
EC.packSpki = function (jwk) {
var x = toHex(base64ToUint8(urlBase64ToBase64(jwk.x)));
var y = toHex(base64ToUint8(urlBase64ToBase64(jwk.y)));
var x = Enc.base64ToHex(jwk.x);
var y = Enc.base64ToHex(jwk.y);
var objId = ('P-256' === jwk.crv) ? OBJ_ID_EC : OBJ_ID_EC_384;
return Hex.toUint8(
return Enc.hexToUint8(
ASN1('30'
, ASN1('30'
, OBJ_ID_EC_PUB
@ -435,15 +421,15 @@ EC.packSsh = function (jwk) {
a = '33 38 34';
b = '61';
}
var x = toHex(base64ToUint8(urlBase64ToBase64(jwk.x)));
var y = toHex(base64ToUint8(urlBase64ToBase64(jwk.y)));
var ssh = Hex.toUint8(
var x = Enc.base64ToHex(jwk.x);
var y = Enc.base64ToHex(jwk.y);
var ssh = Enc.hexToUint8(
('00 00 00 13 65 63 64 73 61 2d 73 68 61 32 2d 6e 69 73 74 70'
+ a + '00 00 00 08 6e 69 73 74 70' + a + '00 00 00' + b
+ '04' + x + y).replace(/\s+/g, '').toLowerCase()
);
return typ + ' ' + toBase64(ssh) + ' ' + comment;
return typ + ' ' + Enc.bufToBase64(ssh) + ' ' + comment;
};
//
@ -481,8 +467,8 @@ ASN1 = function ASN1(/*type, hexstrings...*/) {
}
}
if (lenlen) { hex += Hex.fromInt(0x80 + lenlen); }
return hex + Hex.fromInt(str.length/2) + str;
if (lenlen) { hex += Enc.numToHex(0x80 + lenlen); }
return hex + Enc.numToHex(str.length/2) + str;
};
// The Integer type has some special rules

42
lib/encoding.js Normal file
View File

@ -0,0 +1,42 @@
'use strict';
var Enc = module.exports;
Enc.base64ToBuf = function base64ToBuf(str) {
// node handles both base64 and urlBase64 equally
return Buffer.from(str, 'base64');
};
Enc.base64ToHex = function base64ToHex(b64) {
return Enc.bufToHex(Enc.base64ToBuf(b64));
};
Enc.bufToBase64 = function toHex(u8) {
// Ensure a node buffer, even if TypedArray
return Buffer.from(u8).toString('base64');
};
Enc.bufToHex = function bufToHex(u8) {
// Ensure a node buffer, even if TypedArray
return Buffer.from(u8).toString('hex');
};
Enc.bufToUrlBase64 = function bufToUrlBase64(u8) {
return Enc.bufToBase64(u8)
.replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, '');
};
Enc.hexToUint8 = function (hex) {
// TODO: I don't remember why I chose Uint8Array over Buffer...
var buf = Buffer.from(hex, 'hex');
var ab = buf.buffer.slice(buf.offset, buf.offset + buf.byteLength);
return new Uint8Array(ab);
};
Enc.numToHex = function numToHex(d) {
d = d.toString(16);
if (d.length % 2) {
return '0' + d;
}
return d;
};

56
lib/pem.js Normal file
View File

@ -0,0 +1,56 @@
'use strict';
var PEM = module.exports;
var Enc = require('./encoding.js');
// TODO move object id hinting to x509.js
// 1.2.840.10045.3.1.7
// prime256v1 (ANSI X9.62 named elliptic curve)
var OBJ_ID_EC = '06 08 2A8648CE3D030107'.replace(/\s+/g, '').toLowerCase();
// 1.3.132.0.34
// secp384r1 (SECG (Certicom) named elliptic curve)
var OBJ_ID_EC_384 = '06 05 2B81040022'.replace(/\s+/g, '').toLowerCase();
PEM.parseBlock = function pemToDer(pem) {
var typ;
var pub;
var crv;
var der = Buffer.from(pem.split(/\n/).filter(function (line, i) {
if (0 === i) {
if (/ PUBLIC /.test(line)) {
pub = true;
} else if (/ PRIVATE /.test(line)) {
pub = false;
}
if (/ EC/.test(line)) {
typ = 'EC';
}
}
return !/---/.test(line);
}).join(''), 'base64');
if (!typ || 'EC' === typ) {
var hex = Enc.bufToHex(der);
if (-1 !== hex.indexOf(OBJ_ID_EC)) {
typ = 'EC';
crv = 'P-256';
} else if (-1 !== hex.indexOf(OBJ_ID_EC_384)) {
typ = 'EC';
crv = 'P-384';
} else {
// TODO support P-384 as well (but probably nothing else)
console.warn("unsupported ec curve");
}
}
return { kty: typ, pub: pub, der: der, crv: crv };
};
PEM.packBlock = function (opts) {
// TODO allow for headers?
return '-----BEGIN ' + opts.type + '-----\n'
+ Enc.bufToBase64(opts.bytes).match(/.{1,64}/g).join('\n') + '\n'
+ '-----END ' + opts.type + '-----'
;
};

72
test.sh
View File

@ -1,77 +1,87 @@
#/bin/bash
set -e
echo ""
echo ""
echo "Testing PEM-to-JWK P-256"
echo ""
node bin/eckles.js fixtures/privkey-ec-p256.sec1.pem | tee fixtures/privkey-ec-p256.jwk.2
node bin/eckles.js fixtures/privkey-ec-p256.sec1.pem \
> fixtures/privkey-ec-p256.jwk.2
diff fixtures/privkey-ec-p256.jwk.json fixtures/privkey-ec-p256.jwk.2
node bin/eckles.js fixtures/privkey-ec-p256.pkcs8.pem | tee fixtures/privkey-ec-p256.jwk.2
node bin/eckles.js fixtures/privkey-ec-p256.pkcs8.pem \
> fixtures/privkey-ec-p256.jwk.2
diff fixtures/privkey-ec-p256.jwk.json fixtures/privkey-ec-p256.jwk.2
node bin/eckles.js fixtures/pub-ec-p256.spki.pem | tee fixtures/pub-ec-p256.jwk.2
node bin/eckles.js fixtures/pub-ec-p256.spki.pem \
> fixtures/pub-ec-p256.jwk.2
diff fixtures/pub-ec-p256.jwk.json fixtures/pub-ec-p256.jwk.2
#
node bin/eckles.js fixtures/pub-ec-p256.ssh.pub | tee fixtures/pub-ec-p256.jwk.2
node bin/eckles.js fixtures/pub-ec-p256.ssh.pub \
> fixtures/pub-ec-p256.jwk.2
diff fixtures/pub-ec-p256.jwk.2 fixtures/pub-ec-p256.jwk.2
echo "PASS"
echo ""
echo ""
echo "Testing PEM-to-JWK P-384"
echo ""
node bin/eckles.js fixtures/privkey-ec-p384.sec1.pem | tee fixtures/privkey-ec-p384.jwk.2
node bin/eckles.js fixtures/privkey-ec-p384.sec1.pem \
> fixtures/privkey-ec-p384.jwk.2
diff fixtures/privkey-ec-p384.jwk.json fixtures/privkey-ec-p384.jwk.2
node bin/eckles.js fixtures/privkey-ec-p384.pkcs8.pem | tee fixtures/privkey-ec-p384.jwk.2.2
node bin/eckles.js fixtures/privkey-ec-p384.pkcs8.pem \
> fixtures/privkey-ec-p384.jwk.2.2
diff fixtures/privkey-ec-p384.jwk.json fixtures/privkey-ec-p384.jwk.2.2
node bin/eckles.js fixtures/pub-ec-p384.spki.pem | tee fixtures/pub-ec-p384.jwk.2
node bin/eckles.js fixtures/pub-ec-p384.spki.pem \
> fixtures/pub-ec-p384.jwk.2
diff fixtures/pub-ec-p384.jwk.json fixtures/pub-ec-p384.jwk.2
#
node bin/eckles.js fixtures/pub-ec-p384.ssh.pub | tee fixtures/pub-ec-p384.jwk.2
node bin/eckles.js fixtures/pub-ec-p384.ssh.pub \
> fixtures/pub-ec-p384.jwk.2
diff fixtures/pub-ec-p384.jwk.2 fixtures/pub-ec-p384.jwk.2
echo "PASS"
echo ""
echo ""
echo "Testing JWK-to-PEM P-256"
echo ""
node bin/eckles.js fixtures/privkey-ec-p256.jwk.json sec1 | tee fixtures/privkey-ec-p256.sec1.pem.2
node bin/eckles.js fixtures/privkey-ec-p256.jwk.json sec1 \
> fixtures/privkey-ec-p256.sec1.pem.2
diff fixtures/privkey-ec-p256.sec1.pem fixtures/privkey-ec-p256.sec1.pem.2
#
node bin/eckles.js fixtures/privkey-ec-p256.jwk.json pkcs8 | tee fixtures/privkey-ec-p256.pkcs8.pem.2
node bin/eckles.js fixtures/privkey-ec-p256.jwk.json pkcs8 \
> fixtures/privkey-ec-p256.pkcs8.pem.2
diff fixtures/privkey-ec-p256.pkcs8.pem fixtures/privkey-ec-p256.pkcs8.pem.2
#
node bin/eckles.js fixtures/pub-ec-p256.jwk.json spki | tee fixtures/pub-ec-p256.spki.pem.2
node bin/eckles.js fixtures/pub-ec-p256.jwk.json spki \
> fixtures/pub-ec-p256.spki.pem.2
diff fixtures/pub-ec-p256.spki.pem fixtures/pub-ec-p256.spki.pem.2
# ssh-keygen -f fixtures/pub-ec-p256.spki.pem -i -mPKCS8 > fixtures/pub-ec-p256.ssh.pub
node bin/eckles.js fixtures/pub-ec-p256.jwk.json ssh | tee fixtures/pub-ec-p256.ssh.pub.2
node bin/eckles.js fixtures/pub-ec-p256.jwk.json ssh \
> fixtures/pub-ec-p256.ssh.pub.2
diff fixtures/pub-ec-p256.ssh.pub fixtures/pub-ec-p256.ssh.pub.2
echo "PASS"
echo ""
echo ""
echo "Testing JWK-to-PEM P-384"
echo ""
node bin/eckles.js fixtures/privkey-ec-p384.jwk.json sec1 | tee fixtures/privkey-ec-p384.sec1.pem.2
node bin/eckles.js fixtures/privkey-ec-p384.jwk.json sec1 \
> fixtures/privkey-ec-p384.sec1.pem.2
diff fixtures/privkey-ec-p384.sec1.pem fixtures/privkey-ec-p384.sec1.pem.2
#
node bin/eckles.js fixtures/privkey-ec-p384.jwk.json pkcs8 | tee fixtures/privkey-ec-p384.pkcs8.pem.2
node bin/eckles.js fixtures/privkey-ec-p384.jwk.json pkcs8 \
> fixtures/privkey-ec-p384.pkcs8.pem.2
diff fixtures/privkey-ec-p384.pkcs8.pem fixtures/privkey-ec-p384.pkcs8.pem.2
#
node bin/eckles.js fixtures/pub-ec-p384.jwk.json spki | tee fixtures/pub-ec-p384.spki.pem.2
node bin/eckles.js fixtures/pub-ec-p384.jwk.json spki \
> fixtures/pub-ec-p384.spki.pem.2
diff fixtures/pub-ec-p384.spki.pem fixtures/pub-ec-p384.spki.pem.2
# ssh-keygen -f fixtures/pub-ec-p384.spki.pem -i -mPKCS8 > fixtures/pub-ec-p384.ssh.pub
node bin/eckles.js fixtures/pub-ec-p384.jwk.json ssh | tee fixtures/pub-ec-p384.ssh.pub.2
node bin/eckles.js fixtures/pub-ec-p384.jwk.json ssh \
> fixtures/pub-ec-p384.ssh.pub.2
diff fixtures/pub-ec-p384.ssh.pub fixtures/pub-ec-p384.ssh.pub.2
echo "PASS"
rm fixtures/*.2
echo ""
echo ""
echo "Testing freshly generated keypair"
echo ""
# Generate EC P-256 Keypair
openssl ecparam -genkey -name prime256v1 -noout -out ./privkey-ec-p256.sec1.pem
# Export Public-only EC Key (as SPKI)
@ -97,7 +107,16 @@ diff ./pub-ec-p256.spki.pem ./pub-ec-p256.spki.pem.2
node bin/eckles.js ./pub-ec-p256.ssh.pub > ./pub-ec-p256.jwk.json
node bin/eckles.js ./pub-ec-p256.jwk.json ssh > ./pub-ec-p256.ssh.pub.2
diff ./pub-ec-p256.ssh.pub ./pub-ec-p256.ssh.pub.2
echo "PASS"
echo ""
echo "Testing key generation"
node bin/eckles.js jwk > /dev/null
node bin/eckles.js jwk P-384 > /dev/null
node bin/eckles.js sec1 > /dev/null
node bin/eckles.js pkcs8 > /dev/null
node bin/eckles.js ssh #> /dev/null
echo "PASS"
rm *.2
@ -107,4 +126,5 @@ echo ""
echo "PASSED:"
echo "• All inputs produced valid outputs"
echo "• All outputs matched known-good values"
echo "• Generated keys in each format (sec1, pkcs8, jwk, ssh)"
echo ""