From 24aeb19774e0c25ef250efa78fa364aaef114794 Mon Sep 17 00:00:00 2001 From: AJ ONeal Date: Mon, 19 Nov 2018 22:12:27 -0700 Subject: [PATCH] v0.7.0: add sec1 output --- bin/eckles.js | 28 ++++-- fixtures/privkey-ec-p256.jwk | 7 ++ fixtures/privkey-ec-p384.jwk | 7 ++ fixtures/pub-ec-p256.jwk | 6 ++ fixtures/pub-ec-p384.jwk | 6 ++ lib/eckles.js | 170 ++++++++++++++++++++++++++++++++++- package.json | 2 +- test.sh | 13 +++ 8 files changed, 231 insertions(+), 8 deletions(-) create mode 100644 fixtures/privkey-ec-p256.jwk create mode 100644 fixtures/privkey-ec-p384.jwk create mode 100644 fixtures/pub-ec-p256.jwk create mode 100644 fixtures/pub-ec-p384.jwk diff --git a/bin/eckles.js b/bin/eckles.js index 7b4f6d8..4e1829c 100755 --- a/bin/eckles.js +++ b/bin/eckles.js @@ -5,10 +5,28 @@ var fs = require('fs'); var eckles = require('../index.js'); var infile = process.argv[2]; -//var outfile = process.argv[3]; +var format = process.argv[3]; -var keypem = fs.readFileSync(infile, 'ascii'); +var key = fs.readFileSync(infile, 'ascii'); -eckles.import({ pem: keypem }).then(function (jwk) { - console.log(JSON.stringify(jwk, null, 2)); -}); +try { + key = JSON.parse(key); +} catch(e) { + // ignore +} + +if ('string' === typeof key) { + eckles.import({ pem: key }).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) { + console.log(pem); + }).catch(function (err) { + console.error(err); + process.exit(2); + }); +} diff --git a/fixtures/privkey-ec-p256.jwk b/fixtures/privkey-ec-p256.jwk new file mode 100644 index 0000000..2e9455d --- /dev/null +++ b/fixtures/privkey-ec-p256.jwk @@ -0,0 +1,7 @@ +{ + "kty": "EC", + "crv": "P-256", + "d": "iYydo27aNGO9DBUWeGEPD8oNi1LZDqfxPmQlieLBjVQ", + "x": "IT1SWLxsacPiE5Z16jkopAn8_-85rMjgyCokrnjDft4", + "y": "mP2JwOAOdMmXuwpxbKng3KZz27mz-nKWIlXJ3rzSGMo" +} diff --git a/fixtures/privkey-ec-p384.jwk b/fixtures/privkey-ec-p384.jwk new file mode 100644 index 0000000..7bb65f8 --- /dev/null +++ b/fixtures/privkey-ec-p384.jwk @@ -0,0 +1,7 @@ +{ + "kty": "EC", + "crv": "P-384", + "d": "XlyuCEWSTTS8U79O_Mz05z18vh4kb10szvu_7pdXuGWV6lfEyPExyUYWsA6A2kdV", + "x": "2zEU0bKCa7ejKLIJ8oPGnLhqhxyiv4_w38K2a0SPC6dsSd9_glNJ8lcqv0sff5Gb", + "y": "VD4jnu83S6scn6_TeAj3EZOREGbOs6dzoVpaugn-XQMMyC9O4VLbDDFGBZTJlMsb" +} diff --git a/fixtures/pub-ec-p256.jwk b/fixtures/pub-ec-p256.jwk new file mode 100644 index 0000000..22556ab --- /dev/null +++ b/fixtures/pub-ec-p256.jwk @@ -0,0 +1,6 @@ +{ + "kty": "EC", + "crv": "P-256", + "x": "IT1SWLxsacPiE5Z16jkopAn8_-85rMjgyCokrnjDft4", + "y": "mP2JwOAOdMmXuwpxbKng3KZz27mz-nKWIlXJ3rzSGMo" +} diff --git a/fixtures/pub-ec-p384.jwk b/fixtures/pub-ec-p384.jwk new file mode 100644 index 0000000..a225041 --- /dev/null +++ b/fixtures/pub-ec-p384.jwk @@ -0,0 +1,6 @@ +{ + "kty": "EC", + "crv": "P-384", + "x": "2zEU0bKCa7ejKLIJ8oPGnLhqhxyiv4_w38K2a0SPC6dsSd9_glNJ8lcqv0sff5Gb", + "y": "VD4jnu83S6scn6_TeAj3EZOREGbOs6dzoVpaugn-XQMMyC9O4VLbDDFGBZTJlMsb" +} diff --git a/lib/eckles.js b/lib/eckles.js index 9c43366..115b951 100644 --- a/lib/eckles.js +++ b/lib/eckles.js @@ -1,5 +1,6 @@ 'use strict'; +var ASN1; var EC = module.exports; var Hex = {}; var PEM = {}; @@ -13,7 +14,7 @@ var OBJ_ID_EC_384 = '06 05 2B81040022'.replace(/\s+/g, '').toLowerCase(); // 1.2.840.10045.2.1 // ecPublicKey (ANSI X9.62 public key type) -var OBJ_ID_EC_PUB = '06 07 2A8648CE3D0201'.replace(/\s+/g, '').toLowerCase(); +//var OBJ_ID_EC_PUB = '06 07 2A8648CE3D0201'.replace(/\s+/g, '').toLowerCase(); // The one good thing that came from the b***kchain hysteria: good EC documentation @@ -57,7 +58,15 @@ PEM.parseBlock = function pemToDer(pem) { } } - return { typ: typ, pub: pub, der: der, crv: crv }; + 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) { @@ -78,7 +87,36 @@ function toHex(ab) { } 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; @@ -250,3 +288,131 @@ EC.parse = function parseEc(opts) { }); }; EC.toJwk = EC.import = EC.parse; + +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' ].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 || 'pkix' === format) { + format = 'spki'; + } else if ('spki' !== format) { + throw new Error("options.format must be 'spki' for public EC keys"); + } + } 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) { + throw new Error("pkcs8 output not implemented"); + } else { + throw new Error("spki not implemented"); + } + }); +}; + +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 objId = ('P-256' === jwk.crv) ? OBJ_ID_EC : OBJ_ID_EC_384; + return Hex.toUint8( + ASN1('30' + , ASN1.UInt('01') + , ASN1('04', d) + , ASN1('A0', objId) + , ASN1('A1', ASN1.BitStr('04' + x + y))) + ); +}; +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 objId = ('P-256' === jwk.crv) ? OBJ_ID_EC : OBJ_ID_EC_384; +}; +EC.packSpki = function (jwk) { + var x = toHex(base64ToUint8(urlBase64ToBase64(jwk.x))); + var y = toHex(base64ToUint8(urlBase64ToBase64(jwk.y))); +}; +EC.packPkix = EC.packSpki; + +// +// A dumbed-down, minimal ASN.1 packer +// + +// Almost every ASN.1 type that's important for CSR +// can be represented generically with only a few rules. +ASN1 = function ASN1(/*type, hexstrings...*/) { + var args = Array.prototype.slice.call(arguments); + var typ = args.shift(); + var str = args.join('').replace(/\s+/g, '').toLowerCase(); + var len = (str.length/2); + var lenlen = 0; + var hex = typ; + + // We can't have an odd number of hex chars + if (len !== Math.round(len)) { + throw new Error("invalid hex"); + } + + // The first byte of any ASN.1 sequence is the type (Sequence, Integer, etc) + // The second byte is either the size of the value, or the size of its size + + // 1. If the second byte is < 0x80 (128) it is considered the size + // 2. If it is > 0x80 then it describes the number of bytes of the size + // ex: 0x82 means the next 2 bytes describe the size of the value + // 3. The special case of exactly 0x80 is "indefinite" length (to end-of-file) + + if (len > 127) { + lenlen += 1; + while (len > 255) { + lenlen += 1; + len = len >> 8; + } + } + + if (lenlen) { hex += Hex.fromInt(0x80 + lenlen); } + return hex + Hex.fromInt(str.length/2) + str; +}; + +// The Integer type has some special rules +ASN1.UInt = function UINT() { + var str = Array.prototype.slice.call(arguments).join(''); + var first = parseInt(str.slice(0, 2), 16); + + // If the first byte is 0x80 or greater, the number is considered negative + // Therefore we add a '00' prefix if the 0x80 bit is set + if (0x80 & first) { str = '00' + str; } + + return ASN1('02', str); +}; + +// The Bit String type also has a special rule +ASN1.BitStr = function BITSTR() { + var str = Array.prototype.slice.call(arguments).join(''); + // '00' is a mask of how many bits of the next byte to ignore + return ASN1('03', '00' + str); +}; + +EC.toPem = EC.export = EC.pack; diff --git a/package.json b/package.json index 4392650..8870e73 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "eckles", - "version": "0.1.1", + "version": "0.7.0", "description": "A focused, zero-dependency library for perfect universal ECDSA P-256 (prime256v1, secp256r1) and P-384 (secp384r1) support.", "main": "index.js", "bin": { diff --git a/test.sh b/test.sh index 2706656..ef25083 100644 --- a/test.sh +++ b/test.sh @@ -1,4 +1,5 @@ #/bin/bash +set -e echo "" echo "" @@ -13,3 +14,15 @@ node bin/eckles.js fixtures/privkey-ec-p384.pkcs8.pem node bin/eckles.js fixtures/pub-ec-p384.spki.pem echo "" +echo "" +node bin/eckles.js fixtures/privkey-ec-p256.jwk sec1 +node bin/eckles.js fixtures/privkey-ec-p256.jwk pkcs8 +node bin/eckles.js fixtures/pub-ec-p256.jwk spki + +echo "" +echo "" +node bin/eckles.js fixtures/privkey-ec-p384.jwk sec1 +node bin/eckles.js fixtures/privkey-ec-p384.jwk pkcs8 +node bin/eckles.js fixtures/pub-ec-p384.jwk spki + +echo ""