diff --git a/README.md b/README.md index c681459..3fd100e 100644 --- a/README.md +++ b/README.md @@ -259,3 +259,49 @@ you'll want to take a look at the corresponding documentation: - See ECDSA documentation at [Eckles.js](https://git.coolaj86.com/coolaj86/eckles.js/) - See RSA documentation at [Rasha.js](https://git.coolaj86.com/coolaj86/rasha.js/) + +# Contributions + +Did this project save you some time? Maybe make your day? Even save the day? + +Please say "thanks" via Paypal or Patreon: + +- Paypal: [\$5](https://paypal.me/rootprojects/5) | [\$10](https://paypal.me/rootprojects/10) | Any amount: +- Patreon: + +Where does your contribution go? + +[Root](https://therootcompany.com) is a collection of experts +who trust each other and enjoy working together on deep-tech, +Indie Web projects. + +Our goal is to operate as a sustainable community. + +Your contributions - both in code and _especially_ monetarily - +help to not just this project, but also our broader work +of [projects](https://rootprojects.org) that fuel the **Indie Web**. + +Also, we chat on [Keybase](https://keybase.io) +in [#rootprojects](https://keybase.io/team/rootprojects) + +# Commercial Support + +Do you need... + +- more features? +- bugfixes, on _your_ timeline? +- custom code, built by experts? +- commercial support and licensing? + + + +Contact for support options. + +# Legal + +Copyright [AJ ONeal](https://coolaj86.com), +[Root](https://therootcompany.com) 2018-2019 + +MPL-2.0 | +[Terms of Use](https://therootcompany.com/legal/#terms) | +[Privacy Policy](https://therootcompany.com/legal/#privacy) diff --git a/bin/eckles.js b/bin/eckles.js new file mode 100755 index 0000000..edddd8b --- /dev/null +++ b/bin/eckles.js @@ -0,0 +1,81 @@ +#!/usr/bin/env node +'use strict'; + +var fs = require('fs'); +var Eckles = require('../ecdsa'); + +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 { + key = JSON.parse(key); +} catch (e) { + // ignore +} + +var thumbprint = 'thumbprint' === format; +if (thumbprint) { + format = 'public'; +} + +if ('string' === typeof key) { + if (thumbprint) { + Eckles.thumbprint({ pem: key }).then(console.log); + return; + } + var pub = -1 !== ['public', 'spki', 'pkix'].indexOf(format); + 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 { + if (thumbprint) { + Eckles.thumbprint({ jwk: key }).then(console.log); + return; + } + Eckles.export({ jwk: key, format: format }) + .then(function(pem) { + console.log(pem); + }) + .catch(function(err) { + console.error(err); + process.exit(2); + }); +} diff --git a/bin/rasha.js b/bin/rasha.js new file mode 100755 index 0000000..5c86854 --- /dev/null +++ b/bin/rasha.js @@ -0,0 +1,126 @@ +#!/usr/bin/env node +'use strict'; + +var fs = require('fs'); +var Rasha = require('../rsa'); +var PEM = require('@root/pem'); +var ASN1 = require('@root/asn1'); + +var infile = process.argv[2]; +var format = process.argv[3]; +var msg = process.argv[4]; +var sign; +if ('sign' === format) { + sign = true; + format = 'pkcs8'; +} + +if (!infile) { + infile = 'jwk'; +} + +if ( + -1 !== + ['jwk', 'pem', 'json', 'der', 'pkcs1', 'pkcs8', 'spki'].indexOf(infile) +) { + console.info('Generating new key...'); + Rasha.generate({ + format: infile, + modulusLength: parseInt(format, 10) || 2048, + encoding: parseInt(format, 10) ? null : format + }) + .then(function(key) { + if ('der' === infile || 'der' === format) { + key.private = key.private.toString('binary'); + key.public = key.public.toString('binary'); + } + console.info(key.private); + console.info(key.public); + }) + .catch(function(err) { + console.error(err); + process.exit(1); + }); + return; +} +var key = fs.readFileSync(infile, 'ascii'); + +try { + key = JSON.parse(key); +} catch (e) { + // ignore +} + +var thumbprint = 'thumbprint' === format; +if (thumbprint) { + format = 'public'; +} + +if ('string' === typeof key) { + if (thumbprint) { + Rasha.thumbprint({ pem: key }).then(console.info); + return; + } + if ('tpl' === format) { + var block = PEM.parseBlock(key); + var asn1 = ASN1.parse(block.der); + ASN1.tpl(asn1); + return; + } + if (sign) { + signMessage(key, msg); + return; + } + + var pub = -1 !== ['public', 'spki', 'pkix'].indexOf(format); + Rasha.import({ pem: key, public: pub || format }) + .then(function(jwk) { + console.info(JSON.stringify(jwk, null, 2)); + }) + .catch(function(err) { + console.error(err); + process.exit(1); + }); +} else { + if (thumbprint) { + Rasha.thumbprint({ jwk: key }).then(console.info); + return; + } + Rasha.export({ jwk: key, format: format }) + .then(function(pem) { + if (sign) { + signMessage(pem, msg); + return; + } + console.info(pem); + }) + .catch(function(err) { + console.error(err); + process.exit(2); + }); +} + +function signMessage(pem, name) { + var msg; + try { + msg = fs.readFileSync(name); + } catch (e) { + console.warn( + '[info] input string did not exist as a file, signing the string itself' + ); + msg = Buffer.from(name, 'binary'); + } + var crypto = require('crypto'); + var sign = crypto.createSign('SHA256'); + sign.write(msg); + sign.end(); + var buf = sign.sign(pem); + console.info(buf.toString('base64')); + /* + Rasha.sign({ pem: pem, message: msg, alg: 'SHA256' }).then(function (sig) { + }).catch(function () { + console.error(err); + process.exit(3); + }); + */ +} diff --git a/ecdsa.js b/ecdsa.js new file mode 100644 index 0000000..6d1846a --- /dev/null +++ b/ecdsa.js @@ -0,0 +1,240 @@ +/*global Promise*/ +'use strict'; + +var Enc = require('@root/encoding'); + +var EC = module.exports; +var native = require('./lib/node/ecdsa.js'); + +// TODO SSH +var SSH; + +var X509 = require('@root/x509'); +var PEM = require('@root/pem'); +//var SSH = require('./ssh-keys.js'); +var sha2 = require('./lib/node/sha2.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(); + +EC._stance = + "We take the stance that if you're knowledgeable enough to" + + " properly and securely use non-standard crypto then you shouldn't need Bluecrypt anyway."; +native._stance = EC._stance; +EC._universal = + 'Bluecrypt only supports crypto with standard cross-browser and cross-platform support.'; +EC.generate = native.generate; + +EC.export = function(opts) { + return Promise.resolve().then(function() { + 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, not '" + + format + + "'" + ); + } + } + 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, not '" + + jwk.crv + + "'" + ); + } + 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: X509.packSec1(jwk) + }); + } else if ('pkcs8' === format) { + return PEM.packBlock({ + type: 'PRIVATE KEY', + bytes: X509.packPkcs8(jwk) + }); + } else if (-1 !== ['spki', 'pkix'].indexOf(format)) { + return PEM.packBlock({ + type: 'PUBLIC KEY', + bytes: X509.packSpki(jwk) + }); + } else if (-1 !== ['ssh', 'rfc4716'].indexOf(format)) { + return SSH.packSsh(jwk); + } else { + throw new Error( + 'Sanity Error: reached unreachable code block with format: ' + + format + ); + } + }); +}; +native.export = EC.export; + +EC.import = function(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 SSH.parseSsh(opts.pem); + throw new Error('SSH not yet re-supported'); + } + var pem = opts.pem; + var u8 = PEM.parseBlock(pem).bytes; + var hex = Enc.bufToHex(u8); + var jwk = { kty: 'EC', crv: null, x: null, y: null }; + + //console.log(); + if ( + -1 !== hex.indexOf(OBJ_ID_EC) || + -1 !== hex.indexOf(OBJ_ID_EC_384) + ) { + if (-1 !== hex.indexOf(OBJ_ID_EC_384)) { + jwk.crv = 'P-384'; + } else { + 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 = X509.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 = X509.parseSec1(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 = X509.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 = X509.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("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; + }); +}; +native.import = EC.import; + +EC.pack = function(opts) { + return Promise.resolve().then(function() { + return EC.export(opts); + }); +}; + +// Chopping off the private parts is now part of the public API. +// I thought it sounded a little too crude at first, but it really is the best name in every possible way. +EC.neuter = function(opts) { + // trying to find the best balance of an immutable copy with custom attributes + var jwk = {}; + Object.keys(opts.jwk).forEach(function(k) { + if ('undefined' === typeof opts.jwk[k]) { + return; + } + // ignore EC private parts + if ('d' === k) { + return; + } + jwk[k] = JSON.parse(JSON.stringify(opts.jwk[k])); + }); + return jwk; +}; +native.neuter = EC.neuter; + +// https://stackoverflow.com/questions/42588786/how-to-fingerprint-a-jwk +EC.__thumbprint = function(jwk) { + // Use the same entropy for SHA as for key + var alg = 'SHA-256'; + if (/384/.test(jwk.crv)) { + alg = 'SHA-384'; + } + var payload = + '{"crv":"' + + jwk.crv + + '","kty":"EC","x":"' + + jwk.x + + '","y":"' + + jwk.y + + '"}'; + return sha2.sum(alg, payload).then(function(hash) { + return Enc.bufToUrlBase64(Uint8Array.from(hash)); + }); +}; + +EC.thumbprint = function(opts) { + return Promise.resolve().then(function() { + var jwk; + if ('EC' === opts.kty) { + jwk = opts; + } else if (opts.jwk) { + jwk = opts.jwk; + } else { + return native.import(opts).then(function(jwk) { + return EC.__thumbprint(jwk); + }); + } + return EC.__thumbprint(jwk); + }); +}; diff --git a/fixtures/privkey-ec-p256.jwk.json b/fixtures/privkey-ec-p256.jwk.json new file mode 100644 index 0000000..2e9455d --- /dev/null +++ b/fixtures/privkey-ec-p256.jwk.json @@ -0,0 +1,7 @@ +{ + "kty": "EC", + "crv": "P-256", + "d": "iYydo27aNGO9DBUWeGEPD8oNi1LZDqfxPmQlieLBjVQ", + "x": "IT1SWLxsacPiE5Z16jkopAn8_-85rMjgyCokrnjDft4", + "y": "mP2JwOAOdMmXuwpxbKng3KZz27mz-nKWIlXJ3rzSGMo" +} diff --git a/fixtures/privkey-ec-p256.pkcs8.pem b/fixtures/privkey-ec-p256.pkcs8.pem new file mode 100644 index 0000000..8fd16f0 --- /dev/null +++ b/fixtures/privkey-ec-p256.pkcs8.pem @@ -0,0 +1,5 @@ +-----BEGIN PRIVATE KEY----- +MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgiYydo27aNGO9DBUW +eGEPD8oNi1LZDqfxPmQlieLBjVShRANCAAQhPVJYvGxpw+ITlnXqOSikCfz/7zms +yODIKiSueMN+3pj9icDgDnTJl7sKcWyp4Nymc9u5s/pyliJVyd680hjK +-----END PRIVATE KEY----- diff --git a/fixtures/privkey-ec-p256.sec1.pem b/fixtures/privkey-ec-p256.sec1.pem new file mode 100644 index 0000000..6dca08a --- /dev/null +++ b/fixtures/privkey-ec-p256.sec1.pem @@ -0,0 +1,5 @@ +-----BEGIN EC PRIVATE KEY----- +MHcCAQEEIImMnaNu2jRjvQwVFnhhDw/KDYtS2Q6n8T5kJYniwY1UoAoGCCqGSM49 +AwEHoUQDQgAEIT1SWLxsacPiE5Z16jkopAn8/+85rMjgyCokrnjDft6Y/YnA4A50 +yZe7CnFsqeDcpnPbubP6cpYiVcnevNIYyg== +-----END EC PRIVATE KEY----- diff --git a/fixtures/privkey-ec-p384.jwk.json b/fixtures/privkey-ec-p384.jwk.json new file mode 100644 index 0000000..7bb65f8 --- /dev/null +++ b/fixtures/privkey-ec-p384.jwk.json @@ -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/privkey-ec-p384.pkcs8.pem b/fixtures/privkey-ec-p384.pkcs8.pem new file mode 100644 index 0000000..b9f5d02 --- /dev/null +++ b/fixtures/privkey-ec-p384.pkcs8.pem @@ -0,0 +1,6 @@ +-----BEGIN PRIVATE KEY----- +MIG2AgEAMBAGByqGSM49AgEGBSuBBAAiBIGeMIGbAgEBBDBeXK4IRZJNNLxTv078 +zPTnPXy+HiRvXSzO+7/ul1e4ZZXqV8TI8THJRhawDoDaR1WhZANiAATbMRTRsoJr +t6Mosgnyg8acuGqHHKK/j/DfwrZrRI8Lp2xJ33+CU0nyVyq/Sx9/kZtUPiOe7zdL +qxyfr9N4CPcRk5EQZs6zp3OhWlq6Cf5dAwzIL07hUtsMMUYFlMmUyxs= +-----END PRIVATE KEY----- diff --git a/fixtures/privkey-ec-p384.sec1.pem b/fixtures/privkey-ec-p384.sec1.pem new file mode 100644 index 0000000..d6b6b62 --- /dev/null +++ b/fixtures/privkey-ec-p384.sec1.pem @@ -0,0 +1,6 @@ +-----BEGIN EC PRIVATE KEY----- +MIGkAgEBBDBeXK4IRZJNNLxTv078zPTnPXy+HiRvXSzO+7/ul1e4ZZXqV8TI8THJ +RhawDoDaR1WgBwYFK4EEACKhZANiAATbMRTRsoJrt6Mosgnyg8acuGqHHKK/j/Df +wrZrRI8Lp2xJ33+CU0nyVyq/Sx9/kZtUPiOe7zdLqxyfr9N4CPcRk5EQZs6zp3Oh +Wlq6Cf5dAwzIL07hUtsMMUYFlMmUyxs= +-----END EC PRIVATE KEY----- diff --git a/fixtures/privkey-rsa-2048.jwk.json b/fixtures/privkey-rsa-2048.jwk.json new file mode 100644 index 0000000..f344c22 --- /dev/null +++ b/fixtures/privkey-rsa-2048.jwk.json @@ -0,0 +1,11 @@ +{ + "kty": "RSA", + "n": "m2ttVBxPlWw06ZmGBWVDlfjkPAJ4DgnY0TrDwtCohHzLxGhDNzUJefLukC-xu0LBKylYojT5vTkxaOhxeSYo31syu4WhxbkTBLICOFcCGMob6pSQ38P8LdAIlb0pqDHxEJ9adWomjuFf0SUhN1cP7s9m8Yk9trkpEqjskocn2BOnTB57qAZM6-I70on0_iDZm7-jcqOPgADAmbWHhy67BXkk4yy_YzD4yOGZFXZcNp915_TW5bRd__AKPHUHxJasPiyEFqlNKBR2DSD-LbX5eTmzCh2ikrwTMja7mUdBJf2bK3By5AB0Qi49OykUCfNZeQlEz7UNNj9RGps_50-CNw", + "e": "AQAB", + "d": "Cpfo7Mm9Nu8YMC_xrZ54W9mKHPkCG9rZ93Ds9PNp-RXUgb-ljTbFPZWsYxGNKLllFz8LNosr1pT2ZDMrwNk0Af1iWNvD6gkyXaiQdCyiDPSBsJyNv2LJZon-e85X74nv53UlIkmo9SYxdLz2JaJ-iIWEe8Qh-7llLktrTJV_xr98_tbhgSppz_IeOymq3SEZaQHM8pTU7w7XvCj2pb9r8fN0M0XcgWZIaf3LGEfkhF_WtX67XJ0C6-LbkT51jtlLRNGX6haGdscXS0OWWjKOJzKGuV-NbthEn5rmRtVnjRZ3yaxQ0ud8vC-NONn7yvGUlOur1IdDzJ_YfHPt9sHMQQ", + "p": "ynG-t9HwKCN3MWRYFdnFzi9-02Qcy3p8B5pu3ary2E70hYn2pHlUG2a9BNE8c5xHQ3Hx43WoWf6s0zOunPV1G28LkU_UYEbAtPv_PxSmzpQp9n9XnYvBLBF8Y3z7gxgLn1vVFNARrQdRtj87qY3aw7E9S4DsGcAarIuOT2TsTCE", + "q": "xIkAjgUzB1zaUzJtW2Zgvp9cYYr1DmpH30ePZl3c_8397_DZDDo46fnFYjs6uPa03HpmKUnbjwr14QHlfXlntJBEuXxcqLjkdKdJ4ob7xueLTK4suo9V8LSrkLChVxlZQwnFD2E5ll0sVeeDeMJHQw38ahSrBFEVnxjpnPh1Q1c", + "dp": "tzDGjECFOU0ehqtuqhcuT63a7h8hj19-7MJqoFwY9HQ-ALkfXyYLXeBSGxHbyiIYuodZg6LsfMNgUJ3r3Eyhc_nAVfYPEC_2IdAG4WYmq7iXYF9LQV09qEsKbFykm7QekE3hO7wswo5k-q2tp3ieBYdVGAXJoGOdv5VpaZ7B1QE", + "dq": "kh5dyDk7YCz7sUFbpsmuAeuPjoH2ghooh2u3xN7iUVmAg-ToKjwbVnG5-7eXiC779rQVwnrD_0yh1AFJ8wjRPqDIR7ObXGHikIxT1VSQWqiJm6AfZzDsL0LUD4YS3iPdhob7-NxLKWzqao_u4lhnDQaX9PKa12HFlny6K1daL48", + "qi": "AlHWbx1gp6Z9pbw_1hlS7HuXAgWoX7IjbTUelldf4gkriDWLOrj3QCZcO4ZvZvEwJhVlsny9LO8IkbwGJEL6cXraK08ByVS2mwQyflgTgGNnpzixyEUL_mrQLx6y145FHcxfeqNInMhep-0Mxn1D5nlhmIOgRApS0t9VoXtHhFU" +} diff --git a/fixtures/privkey-rsa-2048.pkcs1.pem b/fixtures/privkey-rsa-2048.pkcs1.pem new file mode 100644 index 0000000..246bd35 --- /dev/null +++ b/fixtures/privkey-rsa-2048.pkcs1.pem @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEpAIBAAKCAQEAm2ttVBxPlWw06ZmGBWVDlfjkPAJ4DgnY0TrDwtCohHzLxGhD +NzUJefLukC+xu0LBKylYojT5vTkxaOhxeSYo31syu4WhxbkTBLICOFcCGMob6pSQ +38P8LdAIlb0pqDHxEJ9adWomjuFf0SUhN1cP7s9m8Yk9trkpEqjskocn2BOnTB57 +qAZM6+I70on0/iDZm7+jcqOPgADAmbWHhy67BXkk4yy/YzD4yOGZFXZcNp915/TW +5bRd//AKPHUHxJasPiyEFqlNKBR2DSD+LbX5eTmzCh2ikrwTMja7mUdBJf2bK3By +5AB0Qi49OykUCfNZeQlEz7UNNj9RGps/50+CNwIDAQABAoIBAAqX6OzJvTbvGDAv +8a2eeFvZihz5Ahva2fdw7PTzafkV1IG/pY02xT2VrGMRjSi5ZRc/CzaLK9aU9mQz +K8DZNAH9Yljbw+oJMl2okHQsogz0gbCcjb9iyWaJ/nvOV++J7+d1JSJJqPUmMXS8 +9iWifoiFhHvEIfu5ZS5La0yVf8a/fP7W4YEqac/yHjspqt0hGWkBzPKU1O8O17wo +9qW/a/HzdDNF3IFmSGn9yxhH5IRf1rV+u1ydAuvi25E+dY7ZS0TRl+oWhnbHF0tD +lloyjicyhrlfjW7YRJ+a5kbVZ40Wd8msUNLnfLwvjTjZ+8rxlJTrq9SHQ8yf2Hxz +7fbBzEECgYEAynG+t9HwKCN3MWRYFdnFzi9+02Qcy3p8B5pu3ary2E70hYn2pHlU +G2a9BNE8c5xHQ3Hx43WoWf6s0zOunPV1G28LkU/UYEbAtPv/PxSmzpQp9n9XnYvB +LBF8Y3z7gxgLn1vVFNARrQdRtj87qY3aw7E9S4DsGcAarIuOT2TsTCECgYEAxIkA +jgUzB1zaUzJtW2Zgvp9cYYr1DmpH30ePZl3c/8397/DZDDo46fnFYjs6uPa03Hpm +KUnbjwr14QHlfXlntJBEuXxcqLjkdKdJ4ob7xueLTK4suo9V8LSrkLChVxlZQwnF +D2E5ll0sVeeDeMJHQw38ahSrBFEVnxjpnPh1Q1cCgYEAtzDGjECFOU0ehqtuqhcu +T63a7h8hj19+7MJqoFwY9HQ+ALkfXyYLXeBSGxHbyiIYuodZg6LsfMNgUJ3r3Eyh +c/nAVfYPEC/2IdAG4WYmq7iXYF9LQV09qEsKbFykm7QekE3hO7wswo5k+q2tp3ie +BYdVGAXJoGOdv5VpaZ7B1QECgYEAkh5dyDk7YCz7sUFbpsmuAeuPjoH2ghooh2u3 +xN7iUVmAg+ToKjwbVnG5+7eXiC779rQVwnrD/0yh1AFJ8wjRPqDIR7ObXGHikIxT +1VSQWqiJm6AfZzDsL0LUD4YS3iPdhob7+NxLKWzqao/u4lhnDQaX9PKa12HFlny6 +K1daL48CgYACUdZvHWCnpn2lvD/WGVLse5cCBahfsiNtNR6WV1/iCSuINYs6uPdA +Jlw7hm9m8TAmFWWyfL0s7wiRvAYkQvpxetorTwHJVLabBDJ+WBOAY2enOLHIRQv+ +atAvHrLXjkUdzF96o0icyF6n7QzGfUPmeWGYg6BEClLS31Whe0eEVQ== +-----END RSA PRIVATE KEY----- diff --git a/fixtures/privkey-rsa-2048.pkcs8.pem b/fixtures/privkey-rsa-2048.pkcs8.pem new file mode 100644 index 0000000..53dbf83 --- /dev/null +++ b/fixtures/privkey-rsa-2048.pkcs8.pem @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQCba21UHE+VbDTp +mYYFZUOV+OQ8AngOCdjROsPC0KiEfMvEaEM3NQl58u6QL7G7QsErKViiNPm9OTFo +6HF5JijfWzK7haHFuRMEsgI4VwIYyhvqlJDfw/wt0AiVvSmoMfEQn1p1aiaO4V/R +JSE3Vw/uz2bxiT22uSkSqOyShyfYE6dMHnuoBkzr4jvSifT+INmbv6Nyo4+AAMCZ +tYeHLrsFeSTjLL9jMPjI4ZkVdlw2n3Xn9NbltF3/8Ao8dQfElqw+LIQWqU0oFHYN +IP4ttfl5ObMKHaKSvBMyNruZR0El/ZsrcHLkAHRCLj07KRQJ81l5CUTPtQ02P1Ea +mz/nT4I3AgMBAAECggEACpfo7Mm9Nu8YMC/xrZ54W9mKHPkCG9rZ93Ds9PNp+RXU +gb+ljTbFPZWsYxGNKLllFz8LNosr1pT2ZDMrwNk0Af1iWNvD6gkyXaiQdCyiDPSB +sJyNv2LJZon+e85X74nv53UlIkmo9SYxdLz2JaJ+iIWEe8Qh+7llLktrTJV/xr98 +/tbhgSppz/IeOymq3SEZaQHM8pTU7w7XvCj2pb9r8fN0M0XcgWZIaf3LGEfkhF/W +tX67XJ0C6+LbkT51jtlLRNGX6haGdscXS0OWWjKOJzKGuV+NbthEn5rmRtVnjRZ3 +yaxQ0ud8vC+NONn7yvGUlOur1IdDzJ/YfHPt9sHMQQKBgQDKcb630fAoI3cxZFgV +2cXOL37TZBzLenwHmm7dqvLYTvSFifakeVQbZr0E0TxznEdDcfHjdahZ/qzTM66c +9XUbbwuRT9RgRsC0+/8/FKbOlCn2f1edi8EsEXxjfPuDGAufW9UU0BGtB1G2Pzup +jdrDsT1LgOwZwBqsi45PZOxMIQKBgQDEiQCOBTMHXNpTMm1bZmC+n1xhivUOakff +R49mXdz/zf3v8NkMOjjp+cViOzq49rTcemYpSduPCvXhAeV9eWe0kES5fFyouOR0 +p0nihvvG54tMriy6j1XwtKuQsKFXGVlDCcUPYTmWXSxV54N4wkdDDfxqFKsEURWf +GOmc+HVDVwKBgQC3MMaMQIU5TR6Gq26qFy5PrdruHyGPX37swmqgXBj0dD4AuR9f +Jgtd4FIbEdvKIhi6h1mDoux8w2BQnevcTKFz+cBV9g8QL/Yh0AbhZiaruJdgX0tB +XT2oSwpsXKSbtB6QTeE7vCzCjmT6ra2neJ4Fh1UYBcmgY52/lWlpnsHVAQKBgQCS +Hl3IOTtgLPuxQVumya4B64+OgfaCGiiHa7fE3uJRWYCD5OgqPBtWcbn7t5eILvv2 +tBXCesP/TKHUAUnzCNE+oMhHs5tcYeKQjFPVVJBaqImboB9nMOwvQtQPhhLeI92G +hvv43EspbOpqj+7iWGcNBpf08prXYcWWfLorV1ovjwKBgAJR1m8dYKemfaW8P9YZ +Uux7lwIFqF+yI201HpZXX+IJK4g1izq490AmXDuGb2bxMCYVZbJ8vSzvCJG8BiRC ++nF62itPAclUtpsEMn5YE4BjZ6c4schFC/5q0C8esteORR3MX3qjSJzIXqftDMZ9 +Q+Z5YZiDoEQKUtLfVaF7R4RV +-----END PRIVATE KEY----- diff --git a/fixtures/pub-ec-p256.jwk.json b/fixtures/pub-ec-p256.jwk.json new file mode 100644 index 0000000..22556ab --- /dev/null +++ b/fixtures/pub-ec-p256.jwk.json @@ -0,0 +1,6 @@ +{ + "kty": "EC", + "crv": "P-256", + "x": "IT1SWLxsacPiE5Z16jkopAn8_-85rMjgyCokrnjDft4", + "y": "mP2JwOAOdMmXuwpxbKng3KZz27mz-nKWIlXJ3rzSGMo" +} diff --git a/fixtures/pub-ec-p256.spki.pem b/fixtures/pub-ec-p256.spki.pem new file mode 100644 index 0000000..abb69b3 --- /dev/null +++ b/fixtures/pub-ec-p256.spki.pem @@ -0,0 +1,4 @@ +-----BEGIN PUBLIC KEY----- +MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEIT1SWLxsacPiE5Z16jkopAn8/+85 +rMjgyCokrnjDft6Y/YnA4A50yZe7CnFsqeDcpnPbubP6cpYiVcnevNIYyg== +-----END PUBLIC KEY----- diff --git a/fixtures/pub-ec-p256.ssh.pub b/fixtures/pub-ec-p256.ssh.pub new file mode 100644 index 0000000..1d87fdd --- /dev/null +++ b/fixtures/pub-ec-p256.ssh.pub @@ -0,0 +1 @@ +ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBCE9Uli8bGnD4hOWdeo5KKQJ/P/vOazI4MgqJK54w37emP2JwOAOdMmXuwpxbKng3KZz27mz+nKWIlXJ3rzSGMo= P-256@localhost diff --git a/fixtures/pub-ec-p384.jwk.json b/fixtures/pub-ec-p384.jwk.json new file mode 100644 index 0000000..a225041 --- /dev/null +++ b/fixtures/pub-ec-p384.jwk.json @@ -0,0 +1,6 @@ +{ + "kty": "EC", + "crv": "P-384", + "x": "2zEU0bKCa7ejKLIJ8oPGnLhqhxyiv4_w38K2a0SPC6dsSd9_glNJ8lcqv0sff5Gb", + "y": "VD4jnu83S6scn6_TeAj3EZOREGbOs6dzoVpaugn-XQMMyC9O4VLbDDFGBZTJlMsb" +} diff --git a/fixtures/pub-ec-p384.spki.pem b/fixtures/pub-ec-p384.spki.pem new file mode 100644 index 0000000..31a503a --- /dev/null +++ b/fixtures/pub-ec-p384.spki.pem @@ -0,0 +1,5 @@ +-----BEGIN PUBLIC KEY----- +MHYwEAYHKoZIzj0CAQYFK4EEACIDYgAE2zEU0bKCa7ejKLIJ8oPGnLhqhxyiv4/w +38K2a0SPC6dsSd9/glNJ8lcqv0sff5GbVD4jnu83S6scn6/TeAj3EZOREGbOs6dz +oVpaugn+XQMMyC9O4VLbDDFGBZTJlMsb +-----END PUBLIC KEY----- diff --git a/fixtures/pub-ec-p384.ssh.pub b/fixtures/pub-ec-p384.ssh.pub new file mode 100644 index 0000000..b07ea6b --- /dev/null +++ b/fixtures/pub-ec-p384.ssh.pub @@ -0,0 +1 @@ +ecdsa-sha2-nistp384 AAAAE2VjZHNhLXNoYTItbmlzdHAzODQAAAAIbmlzdHAzODQAAABhBNsxFNGygmu3oyiyCfKDxpy4aoccor+P8N/CtmtEjwunbEnff4JTSfJXKr9LH3+Rm1Q+I57vN0urHJ+v03gI9xGTkRBmzrOnc6FaWroJ/l0DDMgvTuFS2wwxRgWUyZTLGw== P-384@localhost diff --git a/fixtures/pub-rsa-2048.jwk.json b/fixtures/pub-rsa-2048.jwk.json new file mode 100644 index 0000000..333b300 --- /dev/null +++ b/fixtures/pub-rsa-2048.jwk.json @@ -0,0 +1,5 @@ +{ + "kty": "RSA", + "n": "m2ttVBxPlWw06ZmGBWVDlfjkPAJ4DgnY0TrDwtCohHzLxGhDNzUJefLukC-xu0LBKylYojT5vTkxaOhxeSYo31syu4WhxbkTBLICOFcCGMob6pSQ38P8LdAIlb0pqDHxEJ9adWomjuFf0SUhN1cP7s9m8Yk9trkpEqjskocn2BOnTB57qAZM6-I70on0_iDZm7-jcqOPgADAmbWHhy67BXkk4yy_YzD4yOGZFXZcNp915_TW5bRd__AKPHUHxJasPiyEFqlNKBR2DSD-LbX5eTmzCh2ikrwTMja7mUdBJf2bK3By5AB0Qi49OykUCfNZeQlEz7UNNj9RGps_50-CNw", + "e": "AQAB" +} diff --git a/fixtures/pub-rsa-2048.pkcs1.pem b/fixtures/pub-rsa-2048.pkcs1.pem new file mode 100644 index 0000000..e592fda --- /dev/null +++ b/fixtures/pub-rsa-2048.pkcs1.pem @@ -0,0 +1,8 @@ +-----BEGIN RSA PUBLIC KEY----- +MIIBCgKCAQEAm2ttVBxPlWw06ZmGBWVDlfjkPAJ4DgnY0TrDwtCohHzLxGhDNzUJ +efLukC+xu0LBKylYojT5vTkxaOhxeSYo31syu4WhxbkTBLICOFcCGMob6pSQ38P8 +LdAIlb0pqDHxEJ9adWomjuFf0SUhN1cP7s9m8Yk9trkpEqjskocn2BOnTB57qAZM +6+I70on0/iDZm7+jcqOPgADAmbWHhy67BXkk4yy/YzD4yOGZFXZcNp915/TW5bRd +//AKPHUHxJasPiyEFqlNKBR2DSD+LbX5eTmzCh2ikrwTMja7mUdBJf2bK3By5AB0 +Qi49OykUCfNZeQlEz7UNNj9RGps/50+CNwIDAQAB +-----END RSA PUBLIC KEY----- diff --git a/fixtures/pub-rsa-2048.spki.pem b/fixtures/pub-rsa-2048.spki.pem new file mode 100644 index 0000000..465d115 --- /dev/null +++ b/fixtures/pub-rsa-2048.spki.pem @@ -0,0 +1,9 @@ +-----BEGIN PUBLIC KEY----- +MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAm2ttVBxPlWw06ZmGBWVD +lfjkPAJ4DgnY0TrDwtCohHzLxGhDNzUJefLukC+xu0LBKylYojT5vTkxaOhxeSYo +31syu4WhxbkTBLICOFcCGMob6pSQ38P8LdAIlb0pqDHxEJ9adWomjuFf0SUhN1cP +7s9m8Yk9trkpEqjskocn2BOnTB57qAZM6+I70on0/iDZm7+jcqOPgADAmbWHhy67 +BXkk4yy/YzD4yOGZFXZcNp915/TW5bRd//AKPHUHxJasPiyEFqlNKBR2DSD+LbX5 +eTmzCh2ikrwTMja7mUdBJf2bK3By5AB0Qi49OykUCfNZeQlEz7UNNj9RGps/50+C +NwIDAQAB +-----END PUBLIC KEY----- diff --git a/fixtures/pub-rsa-2048.ssh.pub b/fixtures/pub-rsa-2048.ssh.pub new file mode 100644 index 0000000..a00fd4c --- /dev/null +++ b/fixtures/pub-rsa-2048.ssh.pub @@ -0,0 +1 @@ +ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQCba21UHE+VbDTpmYYFZUOV+OQ8AngOCdjROsPC0KiEfMvEaEM3NQl58u6QL7G7QsErKViiNPm9OTFo6HF5JijfWzK7haHFuRMEsgI4VwIYyhvqlJDfw/wt0AiVvSmoMfEQn1p1aiaO4V/RJSE3Vw/uz2bxiT22uSkSqOyShyfYE6dMHnuoBkzr4jvSifT+INmbv6Nyo4+AAMCZtYeHLrsFeSTjLL9jMPjI4ZkVdlw2n3Xn9NbltF3/8Ao8dQfElqw+LIQWqU0oFHYNIP4ttfl5ObMKHaKSvBMyNruZR0El/ZsrcHLkAHRCLj07KRQJ81l5CUTPtQ02P1Eamz/nT4I3 rsa@localhost diff --git a/keypairs.js b/keypairs.js index 7ee27e1..1b9bea6 100644 --- a/keypairs.js +++ b/keypairs.js @@ -1,3 +1,410 @@ +/*global Promise*/ 'use strict'; -module.exports = require('@root/acme/keypairs'); +require('@root/encoding/bytes'); +var Enc = require('@root/encoding/base64'); + +var Keypairs = module.exports; +var Rasha = require('./rsa.js'); +var Eckles = require('./ecdsa.js'); +var native = require('./lib/node/keypairs.js'); + +Keypairs.parse = function(opts) { + opts = opts || {}; + + var err; + var jwk; + var pem; + var p; + + if (!opts.key || !opts.key.kty) { + try { + jwk = JSON.parse(opts.key); + p = Keypairs.export({ jwk: jwk }) + .catch(function(e) { + pem = opts.key; + err = new Error( + "Not a valid jwk '" + + JSON.stringify(jwk) + + "':" + + e.message + ); + err.code = 'EINVALID'; + return Promise.reject(err); + }) + .then(function() { + return jwk; + }); + } catch (e) { + p = Keypairs.import({ pem: opts.key }).catch(function(e) { + err = new Error( + 'Could not parse key (type ' + + typeof opts.key + + ") '" + + opts.key + + "': " + + e.message + ); + err.code = 'EPARSE'; + return Promise.reject(err); + }); + } + } else { + p = Promise.resolve(opts.key); + } + + return p.then(function(jwk) { + var pubopts = JSON.parse(JSON.stringify(opts)); + pubopts.jwk = jwk; + return Keypairs.publish(pubopts).then(function(pub) { + // 'd' happens to be the name of a private part of both RSA and ECDSA keys + if (opts.public || opts.publish || !jwk.d) { + if (opts.private) { + // TODO test that it can actually sign? + err = new Error( + "Not a private key '" + JSON.stringify(jwk) + "'" + ); + err.code = 'ENOTPRIVATE'; + return Promise.reject(err); + } + return { public: pub }; + } else { + return { private: jwk, public: pub }; + } + }); + }); +}; + +Keypairs.parseOrGenerate = function(opts) { + if (!opts.key) { + return Keypairs.generate(opts); + } + opts.private = true; + return Keypairs.parse(opts).catch(function(e) { + return Keypairs.generate(opts).then(function(pair) { + pair.parseError = e; + return pair; + }); + }); +}; + +Keypairs._stance = + "We take the stance that if you're knowledgeable enough to" + + " properly and securely use non-standard crypto then you shouldn't need Bluecrypt anyway."; +Keypairs._universal = + 'Bluecrypt only supports crypto with standard cross-browser and cross-platform support.'; +Keypairs.generate = function(opts) { + opts = opts || {}; + var p; + if (!opts.kty) { + opts.kty = opts.type; + } + if (!opts.kty) { + opts.kty = 'EC'; + } + if (/^EC/i.test(opts.kty)) { + p = Eckles.generate(opts); + } else if (/^RSA$/i.test(opts.kty)) { + p = Rasha.generate(opts); + } else { + return Promise.Reject( + new Error( + "'" + + opts.kty + + "' is not a well-supported key type." + + Keypairs._universal + + " Please choose 'EC', or 'RSA' if you have good reason to." + ) + ); + } + return p.then(function(pair) { + return Keypairs.thumbprint({ jwk: pair.public }).then(function(thumb) { + pair.private.kid = thumb; // maybe not the same id on the private key? + pair.public.kid = thumb; + return pair; + }); + }); +}; + +Keypairs.import = function(opts) { + return Eckles.import(opts) + .catch(function() { + return Rasha.import(opts); + }) + .then(function(jwk) { + return Keypairs.thumbprint({ jwk: jwk }).then(function(thumb) { + jwk.kid = thumb; + return jwk; + }); + }); +}; + +Keypairs.export = function(opts) { + return Eckles.export(opts).catch(function(err) { + return Rasha.export(opts).catch(function() { + return Promise.reject(err); + }); + }); +}; +// XXX +native.export = Keypairs.export; + +/** + * Chopping off the private parts is now part of the public API. + * I thought it sounded a little too crude at first, but it really is the best name in every possible way. + */ +Keypairs.neuter = function(opts) { + /** trying to find the best balance of an immutable copy with custom attributes */ + var jwk = {}; + Object.keys(opts.jwk).forEach(function(k) { + if ('undefined' === typeof opts.jwk[k]) { + return; + } + // ignore RSA and EC private parts + if (-1 !== ['d', 'p', 'q', 'dp', 'dq', 'qi'].indexOf(k)) { + return; + } + jwk[k] = JSON.parse(JSON.stringify(opts.jwk[k])); + }); + return jwk; +}; + +Keypairs.thumbprint = function(opts) { + return Promise.resolve().then(function() { + if (/EC/i.test(opts.jwk.kty)) { + return Eckles.thumbprint(opts); + } else { + return Rasha.thumbprint(opts); + } + }); +}; + +Keypairs.publish = function(opts) { + if ('object' !== typeof opts.jwk || !opts.jwk.kty) { + throw new Error('invalid jwk: ' + JSON.stringify(opts.jwk)); + } + + /** returns a copy */ + var jwk = Keypairs.neuter(opts); + + if (jwk.exp) { + jwk.exp = setTime(jwk.exp); + } else { + if (opts.exp) { + jwk.exp = setTime(opts.exp); + } else if (opts.expiresIn) { + jwk.exp = Math.round(Date.now() / 1000) + opts.expiresIn; + } else if (opts.expiresAt) { + jwk.exp = opts.expiresAt; + } + } + if (!jwk.use && false !== jwk.use) { + jwk.use = 'sig'; + } + + if (jwk.kid) { + return Promise.resolve(jwk); + } + return Keypairs.thumbprint({ jwk: jwk }).then(function(thumb) { + jwk.kid = thumb; + return jwk; + }); +}; + +// JWT a.k.a. JWS with Claims using Compact Serialization +Keypairs.signJwt = function(opts) { + return Keypairs.thumbprint({ jwk: opts.jwk }).then(function(thumb) { + var header = opts.header || {}; + var claims = JSON.parse(JSON.stringify(opts.claims || {})); + header.typ = 'JWT'; + + if (!header.kid && false !== header.kid) { + header.kid = thumb; + } + if (!header.alg && opts.alg) { + header.alg = opts.alg; + } + if (!claims.iat && (false === claims.iat || false === opts.iat)) { + claims.iat = undefined; + } else if (!claims.iat) { + claims.iat = Math.round(Date.now() / 1000); + } + + if (opts.exp) { + claims.exp = setTime(opts.exp); + } else if ( + !claims.exp && + (false === claims.exp || false === opts.exp) + ) { + claims.exp = undefined; + } else if (!claims.exp) { + throw new Error( + "opts.claims.exp should be the expiration date as seconds, human form (i.e. '1h' or '15m') or false" + ); + } + + if (opts.iss) { + claims.iss = opts.iss; + } + if (!claims.iss && (false === claims.iss || false === opts.iss)) { + claims.iss = undefined; + } else if (!claims.iss) { + throw new Error( + 'opts.claims.iss should be in the form of https://example.com/, a secure OIDC base url' + ); + } + + return Keypairs.signJws({ + jwk: opts.jwk, + pem: opts.pem, + protected: header, + header: undefined, + payload: claims + }).then(function(jws) { + return [jws.protected, jws.payload, jws.signature].join('.'); + }); + }); +}; + +Keypairs.signJws = function(opts) { + return Keypairs.thumbprint(opts).then(function(thumb) { + function alg() { + if (!opts.jwk) { + throw new Error("opts.jwk must exist and must declare 'typ'"); + } + if (opts.jwk.alg) { + return opts.jwk.alg; + } + var typ = 'RSA' === opts.jwk.kty ? 'RS' : 'ES'; + return typ + Keypairs._getBits(opts); + } + + function sign() { + var protect = opts.protected; + var payload = opts.payload; + + // Compute JWS signature + var protectedHeader = ''; + // Because unprotected headers are allowed, regrettably... + // https://stackoverflow.com/a/46288694 + if (false !== protect) { + if (!protect) { + protect = {}; + } + if (!protect.alg) { + protect.alg = alg(); + } + // There's a particular request where ACME / Let's Encrypt explicitly doesn't use a kid + if (false === protect.kid) { + protect.kid = undefined; + } else if (!protect.kid) { + protect.kid = thumb; + } + protectedHeader = JSON.stringify(protect); + } + + // Not sure how to handle the empty case since ACME POST-as-GET must be empty + //if (!payload) { + // throw new Error("opts.payload should be JSON, string, or ArrayBuffer (it may be empty, but that must be explicit)"); + //} + // Trying to detect if it's a plain object (not Buffer, ArrayBuffer, Array, Uint8Array, etc) + if ( + payload && + 'string' !== typeof payload && + 'undefined' === typeof payload.byteLength && + 'undefined' === typeof payload.buffer + ) { + payload = JSON.stringify(payload); + } + // Converting to a buffer, even if it was just converted to a string + if ('string' === typeof payload) { + payload = Enc.strToBuf(payload); + } + + var protected64 = Enc.strToUrlBase64(protectedHeader); + var payload64 = Enc.bufToUrlBase64(payload); + var msg = protected64 + '.' + payload64; + + return native._sign(opts, msg).then(function(buf) { + var signedMsg = { + protected: protected64, + payload: payload64, + signature: Enc.bufToUrlBase64(buf) + }; + + return signedMsg; + }); + } + + if (opts.jwk) { + return sign(); + } else { + return Keypairs.import({ pem: opts.pem }).then(function(pair) { + opts.jwk = pair.private; + return sign(); + }); + } + }); +}; + +// TODO expose consistently +Keypairs.sign = native._sign; + +Keypairs._getBits = function(opts) { + if (opts.alg) { + return opts.alg.replace(/[a-z\-]/gi, ''); + } + if (opts.protected && opts.protected.alg) { + return opts.protected.alg.replace(/[a-z\-]/gi, ''); + } + // base64 len to byte len + var len = Math.floor((opts.jwk.n || '').length * 0.75); + + // TODO this may be a bug + // need to confirm that the padding is no more or less than 1 byte + if (/521/.test(opts.jwk.crv) || len >= 511) { + return '512'; + } else if (/384/.test(opts.jwk.crv) || len >= 383) { + return '384'; + } + + return '256'; +}; +// XXX +native._getBits = Keypairs._getBits; + +function setTime(time) { + if ('number' === typeof time) { + return time; + } + + var t = time.match(/^(\-?\d+)([dhms])$/i); + if (!t || !t[0]) { + throw new Error( + "'" + + time + + "' should be datetime in seconds or human-readable format (i.e. 3d, 1h, 15m, 30s" + ); + } + + var now = Math.round(Date.now() / 1000); + var num = parseInt(t[1], 10); + var unit = t[2]; + var mult = 1; + switch (unit) { + // fancy fallthrough, what fun! + case 'd': + mult *= 24; + /*falls through*/ + case 'h': + mult *= 60; + /*falls through*/ + case 'm': + mult *= 60; + /*falls through*/ + case 's': + mult *= 1; + } + + return now + mult * num; +} diff --git a/lib/browser/ecdsa.js b/lib/browser/ecdsa.js new file mode 100644 index 0000000..9f5ef73 --- /dev/null +++ b/lib/browser/ecdsa.js @@ -0,0 +1,57 @@ +'use strict'; + +var native = module.exports; +// XXX received from caller +var EC = native; + +native.generate = function(opts) { + var wcOpts = {}; + if (!opts) { + opts = {}; + } + if (!opts.kty) { + opts.kty = 'EC'; + } + + // ECDSA has only the P curves and an associated bitlength + wcOpts.name = 'ECDSA'; + if (!opts.namedCurve) { + opts.namedCurve = 'P-256'; + } + wcOpts.namedCurve = opts.namedCurve; // true for supported curves + if (/256/.test(wcOpts.namedCurve)) { + wcOpts.namedCurve = 'P-256'; + wcOpts.hash = { name: 'SHA-256' }; + } else if (/384/.test(wcOpts.namedCurve)) { + wcOpts.namedCurve = 'P-384'; + wcOpts.hash = { name: 'SHA-384' }; + } else { + return Promise.Reject( + new Error( + "'" + + wcOpts.namedCurve + + "' is not an NIST approved ECDSA namedCurve. " + + " Please choose either 'P-256' or 'P-384'. " + + // XXX received from caller + EC._stance + ) + ); + } + + var extractable = true; + return window.crypto.subtle + .generateKey(wcOpts, extractable, ['sign', 'verify']) + .then(function(result) { + return window.crypto.subtle + .exportKey('jwk', result.privateKey) + .then(function(privJwk) { + privJwk.key_ops = undefined; + privJwk.ext = undefined; + return { + private: privJwk, + // XXX received from caller + public: EC.neuter({ jwk: privJwk }) + }; + }); + }); +}; diff --git a/lib/browser/keypairs.js b/lib/browser/keypairs.js new file mode 100644 index 0000000..1fa37c3 --- /dev/null +++ b/lib/browser/keypairs.js @@ -0,0 +1,108 @@ +'use strict'; + +var Keypairs = module.exports; + +Keypairs._sign = function(opts, payload) { + return Keypairs._import(opts).then(function(privkey) { + if ('string' === typeof payload) { + payload = new TextEncoder().encode(payload); + } + + return window.crypto.subtle + .sign( + { + name: Keypairs._getName(opts), + hash: { name: 'SHA-' + Keypairs._getBits(opts) } + }, + privkey, + payload + ) + .then(function(signature) { + signature = new Uint8Array(signature); // ArrayBuffer -> u8 + // This will come back into play for CSRs, but not for JOSE + if ('EC' === opts.jwk.kty && /x509|asn1/i.test(opts.format)) { + return Keypairs._ecdsaJoseSigToAsn1Sig(signature); + } else { + // jose/jws/jwt + return signature; + } + }); + }); +}; + +Keypairs._import = function(opts) { + return Promise.resolve().then(function() { + var ops; + // all private keys just happen to have a 'd' + if (opts.jwk.d) { + ops = ['sign']; + } else { + ops = ['verify']; + } + // gotta mark it as extractable, as if it matters + opts.jwk.ext = true; + opts.jwk.key_ops = ops; + + return window.crypto.subtle + .importKey( + 'jwk', + opts.jwk, + { + name: Keypairs._getName(opts), + namedCurve: opts.jwk.crv, + hash: { name: 'SHA-' + Keypairs._getBits(opts) } + }, + true, + ops + ) + .then(function(privkey) { + delete opts.jwk.ext; + return privkey; + }); + }); +}; + +// ECDSA JOSE / JWS / JWT signatures differ from "normal" ASN1/X509 ECDSA signatures +// https://tools.ietf.org/html/rfc7518#section-3.4 +Keypairs._ecdsaJoseSigToAsn1Sig = function(bufsig) { + // it's easier to do the manipulation in the browser with an array + bufsig = Array.from(bufsig); + var hlen = bufsig.length / 2; // should be even + var r = bufsig.slice(0, hlen); + var s = bufsig.slice(hlen); + // unpad positive ints less than 32 bytes wide + while (!r[0]) { + r = r.slice(1); + } + while (!s[0]) { + s = s.slice(1); + } + // pad (or re-pad) ambiguously non-negative BigInts, up to 33 bytes wide + if (0x80 & r[0]) { + r.unshift(0); + } + if (0x80 & s[0]) { + s.unshift(0); + } + + var len = 2 + r.length + 2 + s.length; + var head = [0x30]; + // hard code 0x80 + 1 because it won't be longer than + // two SHA512 plus two pad bytes (130 bytes <= 256) + if (len >= 0x80) { + head.push(0x81); + } + head.push(len); + + return Uint8Array.from( + head.concat([0x02, r.length], r, [0x02, s.length], s) + ); +}; + +Keypairs._getName = function(opts) { + if (/EC/i.test(opts.jwk.kty)) { + return 'ECDSA'; + } else { + return 'RSASSA-PKCS1-v1_5'; + } +}; diff --git a/lib/browser/rsa.js b/lib/browser/rsa.js new file mode 100644 index 0000000..e896a79 --- /dev/null +++ b/lib/browser/rsa.js @@ -0,0 +1,59 @@ +'use strict'; + +var native = module.exports; +// XXX added by caller: _stance, neuter +var RSA = native; + +native.generate = function(opts) { + var wcOpts = {}; + if (!opts) { + opts = {}; + } + if (!opts.kty) { + opts.kty = 'RSA'; + } + + // Support PSS? I don't think it's used for Let's Encrypt + wcOpts.name = 'RSASSA-PKCS1-v1_5'; + if (!opts.modulusLength) { + opts.modulusLength = 2048; + } + wcOpts.modulusLength = opts.modulusLength; + if (wcOpts.modulusLength >= 2048 && wcOpts.modulusLength < 3072) { + // erring on the small side... for no good reason + wcOpts.hash = { name: 'SHA-256' }; + } else if (wcOpts.modulusLength >= 3072 && wcOpts.modulusLength < 4096) { + wcOpts.hash = { name: 'SHA-384' }; + } else if (wcOpts.modulusLength < 4097) { + wcOpts.hash = { name: 'SHA-512' }; + } else { + // Public key thumbprints should be paired with a hash of similar length, + // so anything above SHA-512's keyspace would be left under-represented anyway. + return Promise.Reject( + new Error( + "'" + + wcOpts.modulusLength + + "' is not within the safe and universally" + + ' acceptable range of 2048-4096. Typically you should pick 2048, 3072, or 4096, though other values' + + ' divisible by 8 are allowed. ' + + RSA._stance + ) + ); + } + // TODO maybe allow this to be set to any of the standard values? + wcOpts.publicExponent = new Uint8Array([0x01, 0x00, 0x01]); + + var extractable = true; + return window.crypto.subtle + .generateKey(wcOpts, extractable, ['sign', 'verify']) + .then(function(result) { + return window.crypto.subtle + .exportKey('jwk', result.privateKey) + .then(function(privJwk) { + return { + private: privJwk, + public: RSA.neuter({ jwk: privJwk }) + }; + }); + }); +}; diff --git a/lib/browser/sha2.js b/lib/browser/sha2.js new file mode 100644 index 0000000..8252d19 --- /dev/null +++ b/lib/browser/sha2.js @@ -0,0 +1,13 @@ +'use strict'; + +var sha2 = module.exports; + +var encoder = new TextEncoder(); +sha2.sum = function(alg, str) { + var data = str; + if ('string' === typeof data) { + data = encoder.encode(str); + } + var sha = 'SHA-' + String(alg).replace(/^sha-?/i, ''); + return window.crypto.subtle.digest(sha, data); +}; diff --git a/lib/node/ecdsa.js b/lib/node/ecdsa.js new file mode 100644 index 0000000..54a3ed3 --- /dev/null +++ b/lib/node/ecdsa.js @@ -0,0 +1,113 @@ +'use strict'; + +var native = module.exports; +// XXX provided by caller: import, export +var EC = native; +// TODO SSH + +native.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 Promise.all([ + native.import({ + pem: keypair.private, + format: priv.type + }), + native.import({ + pem: keypair.public, + format: pub.type, + public: true + }) + ]).then(function(pair) { + return { + private: pair[0], + public: pair[1] + }; + }); + } + + if ('ssh' !== opts.format) { + return keypair; + } + + return native + .import({ + pem: keypair.public, + format: format, + public: true + }) + .then(function(jwk) { + return EC.export({ + jwk: jwk, + format: opts.format, + public: true + }).then(function(pub) { + return { + private: keypair.private, + public: pub + }; + }); + }); + }); + }); +}; diff --git a/lib/node/generate-privkey-forge.js b/lib/node/generate-privkey-forge.js new file mode 100644 index 0000000..1ae7b26 --- /dev/null +++ b/lib/node/generate-privkey-forge.js @@ -0,0 +1,55 @@ +// Copyright 2016-2018 AJ ONeal. All rights reserved +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +'use strict'; + +module.exports = function(bitlen, exp) { + var k = require('node-forge').pki.rsa.generateKeyPair({ + bits: bitlen || 2048, + e: exp || 0x10001 + }).privateKey; + var jwk = { + kty: 'RSA', + n: _toUrlBase64(k.n), + e: _toUrlBase64(k.e), + d: _toUrlBase64(k.d), + p: _toUrlBase64(k.p), + q: _toUrlBase64(k.q), + dp: _toUrlBase64(k.dP), + dq: _toUrlBase64(k.dQ), + qi: _toUrlBase64(k.qInv) + }; + return { + private: jwk, + public: { + kty: jwk.kty, + n: jwk.n, + e: jwk.e + } + }; +}; + +function _toUrlBase64(fbn) { + var hex = fbn.toRadix(16); + if (hex.length % 2) { + // Invalid hex string + hex = '0' + hex; + } + while ('00' === hex.slice(0, 2)) { + hex = hex.slice(2); + } + return Buffer.from(hex, 'hex') + .toString('base64') + .replace(/\+/g, '-') + .replace(/\//g, '_') + .replace(/=/g, ''); +} + +if (require.main === module) { + var keypair = module.exports(2048, 0x10001); + console.info(keypair.private); + console.warn(keypair.public); + //console.info(keypair.privateKeyJwk); + //console.warn(keypair.publicKeyJwk); +} diff --git a/lib/node/generate-privkey-node.js b/lib/node/generate-privkey-node.js new file mode 100644 index 0000000..99271fd --- /dev/null +++ b/lib/node/generate-privkey-node.js @@ -0,0 +1,21 @@ +// Copyright 2016-2018 AJ ONeal. All rights reserved +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +'use strict'; + +module.exports = function(bitlen, exp) { + var keypair = require('crypto').generateKeyPairSync('rsa', { + modulusLength: bitlen, + publicExponent: exp, + privateKeyEncoding: { type: 'pkcs1', format: 'pem' }, + publicKeyEncoding: { type: 'pkcs1', format: 'pem' } + }); + var result = { privateKeyPem: keypair.privateKey.trim() }; + return result; +}; + +if (require.main === module) { + var keypair = module.exports(2048, 0x10001); + console.info(keypair.privateKeyPem); +} diff --git a/lib/node/generate-privkey-ursa.js b/lib/node/generate-privkey-ursa.js new file mode 100644 index 0000000..5c9694e --- /dev/null +++ b/lib/node/generate-privkey-ursa.js @@ -0,0 +1,27 @@ +// Copyright 2016-2018 AJ ONeal. All rights reserved +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +'use strict'; + +module.exports = function(bitlen, exp) { + var ursa; + try { + ursa = require('ursa'); + } catch (e) { + ursa = require('ursa-optional'); + } + var keypair = ursa.generatePrivateKey(bitlen, exp); + var result = { + privateKeyPem: keypair + .toPrivatePem() + .toString('ascii') + .trim() + }; + return result; +}; + +if (require.main === module) { + var keypair = module.exports(2048, 0x10001); + console.info(keypair.privateKeyPem); +} diff --git a/lib/node/generate-privkey.js b/lib/node/generate-privkey.js new file mode 100644 index 0000000..d82fa8c --- /dev/null +++ b/lib/node/generate-privkey.js @@ -0,0 +1,90 @@ +// Copyright 2016-2018 AJ ONeal. All rights reserved +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +'use strict'; + +var oldver = false; + +module.exports = function(bitlen, exp) { + bitlen = parseInt(bitlen, 10) || 2048; + exp = parseInt(exp, 10) || 65537; + + try { + return require('./generate-privkey-node.js')(bitlen, exp); + } catch (e) { + if (!/generateKeyPairSync is not a function/.test(e.message)) { + throw e; + } + try { + return require('./generate-privkey-ursa.js')(bitlen, exp); + } catch (e) { + if (e.code !== 'MODULE_NOT_FOUND') { + console.error( + "[rsa-compat] Unexpected error when using 'ursa':" + ); + console.error(e); + } + if (!oldver) { + oldver = true; + console.warn( + '[WARN] rsa-compat: Your version of node does not have crypto.generateKeyPair()' + ); + console.warn( + "[WARN] rsa-compat: Please update to node >= v10.12 or 'npm install --save ursa node-forge'" + ); + console.warn( + '[WARN] rsa-compat: Using node-forge as a fallback may be unacceptably slow.' + ); + if (/arm|mips/i.test(require('os').arch)) { + console.warn( + '================================================================' + ); + console.warn(' WARNING'); + console.warn( + '================================================================' + ); + console.warn(''); + console.warn( + 'WARNING: You are generating an RSA key using pure JavaScript on' + ); + console.warn( + ' a VERY SLOW cpu. This could take DOZENS of minutes!' + ); + console.warn(''); + console.warn( + " We recommend installing node >= v10.12, or 'gcc' and 'ursa'" + ); + console.warn(''); + console.warn('EXAMPLE:'); + console.warn(''); + console.warn( + ' sudo apt-get install build-essential && npm install ursa' + ); + console.warn(''); + console.warn( + '================================================================' + ); + } + } + try { + return require('./generate-privkey-forge.js')(bitlen, exp); + } catch (e) { + if (e.code !== 'MODULE_NOT_FOUND') { + throw e; + } + console.error( + '[ERROR] rsa-compat: could not generate a private key.' + ); + console.error( + 'None of crypto.generateKeyPair, ursa, nor node-forge are present' + ); + } + } + } +}; + +if (require.main === module) { + var keypair = module.exports(2048, 0x10001); + console.info(keypair.privateKeyPem); +} diff --git a/lib/node/keypairs.js b/lib/node/keypairs.js new file mode 100644 index 0000000..cf24a36 --- /dev/null +++ b/lib/node/keypairs.js @@ -0,0 +1,84 @@ +'use strict'; + +var Keypairs = module.exports; +var crypto = require('crypto'); + +Keypairs._sign = function(opts, payload) { + return Keypairs._import(opts).then(function(pem) { + payload = Buffer.from(payload); + + // node specifies RSA-SHAxxx even when it's actually ecdsa (it's all encoded x509 shasums anyway) + // TODO opts.alg = (protect||header).alg + var nodeAlg = 'SHA' + Keypairs._getBits(opts); + var binsig = crypto + .createSign(nodeAlg) + .update(payload) + .sign(pem); + + if ('EC' === opts.jwk.kty && !/x509|asn1/i.test(opts.format)) { + // ECDSA JWT signatures differ from "normal" ECDSA signatures + // https://tools.ietf.org/html/rfc7518#section-3.4 + binsig = Keypairs._ecdsaAsn1SigToJoseSig(binsig); + } + + return binsig; + }); +}; + +Keypairs._import = function(opts) { + if (opts.pem && opts.jwk) { + return Promise.resolve(opts.pem); + } else { + // XXX added by caller + return Keypairs.export({ jwk: opts.jwk }); + } +}; + +Keypairs._ecdsaAsn1SigToJoseSig = function(binsig) { + // should have asn1 sequence header of 0x30 + if (0x30 !== binsig[0]) { + throw new Error('Impossible EC SHA head marker'); + } + var index = 2; // first ecdsa "R" header byte + var len = binsig[1]; + var lenlen = 0; + // Seek length of length if length is greater than 127 (i.e. two 512-bit / 64-byte R and S values) + if (0x80 & len) { + lenlen = len - 0x80; // should be exactly 1 + len = binsig[2]; // should be <= 130 (two 64-bit SHA-512s, plus padding) + index += lenlen; + } + // should be of BigInt type + if (0x02 !== binsig[index]) { + throw new Error('Impossible EC SHA R marker'); + } + index += 1; + + var rlen = binsig[index]; + var bits = 32; + if (rlen > 49) { + bits = 64; + } else if (rlen > 33) { + bits = 48; + } + var r = binsig.slice(index + 1, index + 1 + rlen).toString('hex'); + var slen = binsig[index + 1 + rlen + 1]; // skip header and read length + var s = binsig.slice(index + 1 + rlen + 1 + 1).toString('hex'); + if (2 * slen !== s.length) { + throw new Error('Impossible EC SHA S length'); + } + // There may be one byte of padding on either + while (r.length < 2 * bits) { + r = '00' + r; + } + while (s.length < 2 * bits) { + s = '00' + s; + } + if (2 * (bits + 1) === r.length) { + r = r.slice(2); + } + if (2 * (bits + 1) === s.length) { + s = s.slice(2); + } + return Buffer.concat([Buffer.from(r, 'hex'), Buffer.from(s, 'hex')]); +}; diff --git a/lib/node/rsa.js b/lib/node/rsa.js new file mode 100644 index 0000000..fc3e788 --- /dev/null +++ b/lib/node/rsa.js @@ -0,0 +1,116 @@ +'use strict'; + +var native = module.exports; + +// XXX provided by caller: export +var RSA = native; +var PEM = require('@root/pem'); +var X509 = require('@root/x509'); + +native.generate = function(opts) { + opts.kty = 'RSA'; + return native._generate(opts).then(function(pair) { + var format = opts.format; + var encoding = opts.encoding; + + // The easy way + if ('json' === format && !encoding) { + format = 'jwk'; + encoding = 'json'; + } + if ( + ('jwk' === format || !format) && + ('json' === encoding || !encoding) + ) { + return pair; + } + if ('jwk' === format || 'json' === encoding) { + throw new Error( + "format '" + + format + + "' is incompatible with encoding '" + + encoding + + "'" + ); + } + + // The... less easy way + /* + var priv; + var pub; + + if ('spki' === format || 'pkcs8' === format) { + format = 'pkcs8'; + pub = 'spki'; + } + + if ('pem' === format) { + format = 'pkcs1'; + encoding = 'pem'; + } else if ('der' === format) { + format = 'pkcs1'; + encoding = 'der'; + } + + priv = format; + pub = pub || format; + + if (!encoding) { + encoding = 'pem'; + } + + if (priv) { + priv = { type: priv, format: encoding }; + pub = { type: pub, format: encoding }; + } else { + // jwk + priv = { type: 'pkcs1', format: 'pem' }; + pub = { type: 'pkcs1', format: 'pem' }; + } + */ + if (('pem' === format || 'der' === format) && !encoding) { + encoding = format; + format = 'pkcs1'; + } + + var exOpts = { jwk: pair.private, format: format, encoding: encoding }; + return RSA.export(exOpts).then(function(priv) { + exOpts.public = true; + if ('pkcs8' === exOpts.format) { + exOpts.format = 'spki'; + } + return RSA.export(exOpts).then(function(pub) { + return { private: priv, public: pub }; + }); + }); + }); +}; + +native._generate = function(opts) { + if (!opts) { + opts = {}; + } + return new Promise(function(resolve, reject) { + try { + var modlen = opts.modulusLength || 2048; + var exp = opts.publicExponent || 0x10001; + var pair = require('./generate-privkey.js')(modlen, exp); + if (pair.private) { + resolve(pair); + return; + } + pair = toJwks(pair); + resolve({ private: pair.private, public: pair.public }); + } catch (e) { + reject(e); + } + }); +}; + +// PKCS1 to JWK only +function toJwks(oldpair) { + var block = PEM.parseBlock(oldpair.privateKeyPem); + var jwk = { kty: 'RSA', n: null, e: null }; + jwk = X509.parsePkcs1(block.bytes, jwk); + return { private: jwk, public: RSA.neuter({ jwk: jwk }) }; +} diff --git a/lib/node/sha2.js b/lib/node/sha2.js new file mode 100644 index 0000000..150ee47 --- /dev/null +++ b/lib/node/sha2.js @@ -0,0 +1,17 @@ +/* global Promise */ +'use strict'; + +var sha2 = module.exports; +var crypto = require('crypto'); + +sha2.sum = function(alg, str) { + return Promise.resolve().then(function() { + var sha = 'sha' + String(alg).replace(/^sha-?/i, ''); + // utf8 is the default for strings + var buf = Buffer.from(str); + return crypto + .createHash(sha) + .update(buf) + .digest(); + }); +}; diff --git a/package-lock.json b/package-lock.json index bff564e..f83be38 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,30 +1,35 @@ { "name": "@root/keypairs", - "version": "1.0.0-wip.0", + "version": "0.9.0", "lockfileVersion": 1, "requires": true, "dependencies": { - "@root/acme": { - "version": "3.0.0-wip.0", - "resolved": "https://registry.npmjs.org/@root/acme/-/acme-3.0.0-wip.0.tgz", - "integrity": "sha512-IwnG3ZFt1fl81O1M+FFV91b5Kpw7GYAD1jXwvOWnq9KF50AVO6+L7MUQIAFCK1q/u/weC73DCFrw/6kFN+Vi9A==", + "@root/asn1": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@root/asn1/-/asn1-1.0.0.tgz", + "integrity": "sha512-0lfZNuOULKJDJmdIkP8V9RnbV3XaK6PAHD3swnFy4tZwtlMDzLKoM/dfNad7ut8Hu3r91wy9uK0WA/9zym5mig==", "requires": { - "@root/csr": "^1.0.0-wip.0", "@root/encoding": "^1.0.1" } }, - "@root/csr": { - "version": "1.0.0-wip.0", - "resolved": "https://registry.npmjs.org/@root/csr/-/csr-1.0.0-wip.0.tgz", - "integrity": "sha512-ZrZeGgf/hvfIyMDAZXfD45rYriaZF6LJu7+l0ioPPKgLWVEUAUBkV53z7JbzlcPvXXr6/ZjECzWQ7MYQfMBUAg==", - "requires": { - "@root/acme": "^3.0.0-wip.0" - } - }, "@root/encoding": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/@root/encoding/-/encoding-1.0.1.tgz", "integrity": "sha512-OaEub02ufoU038gy6bsNHQOjIn8nUjGiLcaRmJ40IUykneJkIW5fxDqKxQx48cszuNflYldsJLPPXCrGfHs8yQ==" + }, + "@root/pem": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@root/pem/-/pem-1.0.4.tgz", + "integrity": "sha512-rEUDiUsHtild8GfIjFE9wXtcVxeS+ehCJQBwbQQ3IVfORKHK93CFnRtkr69R75lZFjcmKYVc+AXDB+AeRFOULA==" + }, + "@root/x509": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/@root/x509/-/x509-0.7.2.tgz", + "integrity": "sha512-ENq3LGYORK5NiMFHEVeNMt+fTXaC7DTS6sQXoqV+dFdfT0vmiL5cDLjaXQhaklJQq0NiwicZegzJRl1ZOTp3WQ==", + "requires": { + "@root/asn1": "^1.0.0", + "@root/encoding": "^1.0.1" + } } } } diff --git a/package.json b/package.json index 6608ffd..580590d 100644 --- a/package.json +++ b/package.json @@ -1,13 +1,20 @@ { "name": "@root/keypairs", - "version": "1.0.0-wip.0", + "version": "0.9.0", "description": "Lightweight, Zero-Dependency RSA and EC/ECDSA crypto for Node.js and Browsers", "main": "keypairs.js", + "browser": { + "./lib/node/keypairs.js": "./lib/browser/keypairs.js", + "./lib/node/ecdsa.js": "./lib/browser/ecdsa.js", + "./lib/node/rsa.js": "./lib/browser/rsa.js", + "./lib/node/sha2.js": "./lib/browser/sha2.js" + }, "scripts": { "test": "node tests" }, "files": [ "*.js", + "bin", "lib", "dist" ], @@ -28,6 +35,8 @@ "author": "AJ ONeal (https://coolaj86.com/)", "license": "MPL-2.0", "dependencies": { - "@root/acme": "^3.0.0-wip.0" + "@root/encoding": "^1.0.1", + "@root/pem": "^1.0.4", + "@root/x509": "^0.7.2" } } diff --git a/rsa.js b/rsa.js new file mode 100644 index 0000000..6e00ca3 --- /dev/null +++ b/rsa.js @@ -0,0 +1,192 @@ +/*global Promise*/ +'use strict'; + +var RSA = module.exports; +var native = require('./lib/node/rsa.js'); +var X509 = require('@root/x509'); +var PEM = require('@root/pem'); +//var SSH = require('./ssh-keys.js'); +var sha2 = require('./lib/node/sha2.js'); +var Enc = require('@root/encoding/base64'); + +RSA._universal = + 'Bluecrypt only supports crypto with standard cross-browser and cross-platform support.'; +RSA._stance = + "We take the stance that if you're knowledgeable enough to" + + " properly and securely use non-standard crypto then you shouldn't need Bluecrypt anyway."; +native._stance = RSA._stance; + +RSA.generate = native.generate; + +// Chopping off the private parts is now part of the public API. +// I thought it sounded a little too crude at first, but it really is the best name in every possible way. +RSA.neuter = function(opts) { + // trying to find the best balance of an immutable copy with custom attributes + var jwk = {}; + Object.keys(opts.jwk).forEach(function(k) { + if ('undefined' === typeof opts.jwk[k]) { + return; + } + // ignore RSA private parts + if (-1 !== ['d', 'p', 'q', 'dp', 'dq', 'qi'].indexOf(k)) { + return; + } + jwk[k] = JSON.parse(JSON.stringify(opts.jwk[k])); + }); + return jwk; +}; +native.neuter = RSA.neuter; + +// https://stackoverflow.com/questions/42588786/how-to-fingerprint-a-jwk +RSA.__thumbprint = function(jwk) { + // Use the same entropy for SHA as for key + var len = Math.floor(jwk.n.length * 0.75); + var alg = 'SHA-256'; + // TODO this may be a bug + // need to confirm that the padding is no more or less than 1 byte + if (len >= 511) { + alg = 'SHA-512'; + } else if (len >= 383) { + alg = 'SHA-384'; + } + return sha2 + .sum(alg, '{"e":"' + jwk.e + '","kty":"RSA","n":"' + jwk.n + '"}') + .then(function(hash) { + return Enc.bufToUrlBase64(Uint8Array.from(hash)); + }); +}; + +RSA.thumbprint = function(opts) { + return Promise.resolve().then(function() { + var jwk; + if ('EC' === opts.kty) { + jwk = opts; + } else if (opts.jwk) { + jwk = opts.jwk; + } else { + return RSA.import(opts).then(function(jwk) { + return RSA.__thumbprint(jwk); + }); + } + return RSA.__thumbprint(jwk); + }); +}; + +RSA.export = 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; + var pub = opts.public; + if (pub || -1 !== ['spki', 'pkix', 'ssh', 'rfc4716'].indexOf(format)) { + jwk = RSA.neuter({ jwk: jwk }); + } + if ('RSA' !== jwk.kty) { + throw new Error( + "options.jwk.kty must be 'RSA' for RSA keys: " + + JSON.stringify(jwk) + ); + } + if (!jwk.p) { + // TODO test for n and e + pub = true; + if (!format || 'pkcs1' === format) { + format = 'pkcs1'; + } else if (-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', 'pkcs1', or 'ssh' for public RSA keys, not (" + + typeof format + + ') ' + + format + ); + } + } else { + // TODO test for all necessary keys (d, p, q ...) + if (!format || 'pkcs1' === format) { + format = 'pkcs1'; + } else if ('pkcs8' !== format) { + throw new Error( + "options.format must be 'pkcs1' or 'pkcs8' for private RSA keys" + ); + } + } + + if ('pkcs1' === format) { + if (jwk.d) { + return PEM.packBlock({ + type: 'RSA PRIVATE KEY', + bytes: X509.packPkcs1(jwk) + }); + } else { + return PEM.packBlock({ + type: 'RSA PUBLIC KEY', + bytes: X509.packPkcs1(jwk) + }); + } + } else if ('pkcs8' === format) { + return PEM.packBlock({ + type: 'PRIVATE KEY', + bytes: X509.packPkcs8(jwk) + }); + } else if (-1 !== ['spki', 'pkix'].indexOf(format)) { + return PEM.packBlock({ + type: 'PUBLIC KEY', + bytes: X509.packSpki(jwk) + }); + } else if (-1 !== ['ssh', 'rfc4716'].indexOf(format)) { + //return SSH.pack({ jwk: jwk, comment: opts.comment }); + throw new Error('not supported yet'); + } else { + throw new Error( + 'Sanity Error: reached unreachable code block with format: ' + + format + ); + } + }); +}; +native.export = RSA.export; + +RSA.pack = function(opts) { + // wrapped in a promise for API compatibility + // with the forthcoming browser version + // (and potential future native node capability) + return Promise.resolve().then(function() { + return RSA.export(opts); + }); +}; + +RSA._importSync = function(opts) { + if (!opts || !opts.pem || 'string' !== typeof opts.pem) { + throw new Error('must pass { pem: pem } as a string'); + } + + if (0 === opts.pem.indexOf('ssh-rsa ')) { + //return SSH.parse(opts.pem, jwk); + throw new Error('not supported, yet'); + } + var pem = opts.pem; + var block = PEM.parseBlock(pem); + //var hex = toHex(u8); + + var jwk = X509._parseRsa(block.bytes); + + if (opts.public) { + jwk = RSA.nueter(jwk); + } + return jwk; +}; +RSA.parse = function parseRsa(opts) { + // wrapped in a promise for API compatibility + // with the forthcoming browser version + // (and potential future native node capability) + return Promise.resolve().then(function() { + return RSA._importSync(opts); + }); +}; +RSA.toJwk = RSA.import = RSA.parse; diff --git a/tests/ecdsa.sh b/tests/ecdsa.sh new file mode 100644 index 0000000..b5912a5 --- /dev/null +++ b/tests/ecdsa.sh @@ -0,0 +1,139 @@ +#/bin/bash +set -e + +echo "" +echo "Testing PEM-to-JWK P-256" +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 \ + > 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 \ + > fixtures/pub-ec-p256.jwk.2 +diff fixtures/pub-ec-p256.jwk.json fixtures/pub-ec-p256.jwk.2 +# +echo '[SKIP] SSH-to-JWK P-256' +# 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 "Testing PEM-to-JWK P-384" +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 \ + > 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 \ + > fixtures/pub-ec-p384.jwk.2 +diff fixtures/pub-ec-p384.jwk.json fixtures/pub-ec-p384.jwk.2 +# +echo '[SKIP] SSH-to-JWK P-384' +# 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 "Testing JWK-to-PEM P-256" +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 \ + > 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 \ + > 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 +echo '[SKIP] JWK-to-SSH P-256' +#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 "Testing JWK-to-PEM P-384" +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 \ + > 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 \ + > 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 +echo '[SKIP] JWK-to-SSH P-384' +#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 + + +mkdir -p tmp +echo "" +echo "Testing freshly generated keypair" +# Generate EC P-256 Keypair +openssl ecparam -genkey -name prime256v1 -noout -out ./tmp/privkey-ec-p256.sec1.pem +# Export Public-only EC Key (as SPKI) +openssl ec -in ./tmp/privkey-ec-p256.sec1.pem -pubout -out ./tmp/pub-ec-p256.spki.pem +# Convert SEC1 (traditional) EC Keypair to PKCS8 format +openssl pkcs8 -topk8 -nocrypt -in ./tmp/privkey-ec-p256.sec1.pem -out ./tmp/privkey-ec-p256.pkcs8.pem +# Convert EC public key to SSH format +sshpub=$(ssh-keygen -f ./tmp/pub-ec-p256.spki.pem -i -mPKCS8) +echo "$sshpub P-256@localhost" > ./tmp/pub-ec-p256.ssh.pub +# +node bin/eckles.js ./tmp/privkey-ec-p256.sec1.pem > ./tmp/privkey-ec-p256.jwk.json +node bin/eckles.js ./tmp/privkey-ec-p256.jwk.json sec1 > ./tmp/privkey-ec-p256.sec1.pem.2 +diff ./tmp/privkey-ec-p256.sec1.pem ./tmp/privkey-ec-p256.sec1.pem.2 +# +node bin/eckles.js ./tmp/privkey-ec-p256.pkcs8.pem > ./tmp/privkey-ec-p256.jwk.json +node bin/eckles.js ./tmp/privkey-ec-p256.jwk.json pkcs8 > ./tmp/privkey-ec-p256.pkcs8.pem.2 +diff ./tmp/privkey-ec-p256.pkcs8.pem ./tmp/privkey-ec-p256.pkcs8.pem.2 +# +node bin/eckles.js ./tmp/pub-ec-p256.spki.pem > ./tmp/pub-ec-p256.jwk.json +node bin/eckles.js ./tmp/pub-ec-p256.jwk.json spki > ./tmp/pub-ec-p256.spki.pem.2 +diff ./tmp/pub-ec-p256.spki.pem ./tmp/pub-ec-p256.spki.pem.2 +# +echo '[SKIP] Gen SSH and Convert' +#node bin/eckles.js ./tmp/pub-ec-p256.ssh.pub > ./tmp/pub-ec-p256.jwk.json +#node bin/eckles.js ./tmp/pub-ec-p256.jwk.json ssh > ./tmp/pub-ec-p256.ssh.pub.2 +#diff ./tmp/pub-ec-p256.ssh.pub ./tmp/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 +echo '[SKIP] Gen SSH' +#node bin/eckles.js ssh #> /dev/null +echo "PASS" + +echo "" +echo "Testing Thumbprints" +node bin/eckles.js ./fixtures/privkey-ec-p256.sec1.pem thumbprint +node bin/eckles.js ./fixtures/pub-ec-p256.jwk.json thumbprint +echo "PASS" + +rm ./tmp/*.* + + +echo "" +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, [SKIP] ssh)" +echo "" diff --git a/tests/index.js b/tests/index.js new file mode 100644 index 0000000..9a7dbe7 --- /dev/null +++ b/tests/index.js @@ -0,0 +1,165 @@ +'use strict'; + +var Keypairs = require('../'); + +/* global Promise*/ +Keypairs.parseOrGenerate({ key: null }) + .then(function(pair) { + // should NOT have any warning output + if (!pair.private || !pair.public) { + throw new Error('missing key pairs'); + } + + return Promise.all([ + // Testing Public Part of key + Keypairs.export({ jwk: pair.public }).then(function(pem) { + if (!/--BEGIN PUBLIC/.test(pem)) { + throw new Error('did not export public pem'); + } + return Promise.all([ + Keypairs.parse({ key: pem }).then(function(pair) { + if (pair.private) { + throw new Error("shouldn't have private part"); + } + return true; + }), + Keypairs.parse({ key: pem, private: true }) + .then(function() { + var err = new Error( + 'should have thrown an error when private key was required and public pem was given' + ); + err.code = 'NOERR'; + throw err; + }) + .catch(function(e) { + if ('NOERR' === e.code) { + throw e; + } + return true; + }) + ]).then(function() { + return true; + }); + }), + // Testing Private Part of Key + Keypairs.export({ jwk: pair.private }).then(function(pem) { + if (!/--BEGIN .*PRIVATE KEY--/.test(pem)) { + throw new Error('did not export private pem: ' + pem); + } + return Promise.all([ + Keypairs.parse({ key: pem }).then(function(pair) { + if (!pair.private) { + throw new Error('should have private part'); + } + if (!pair.public) { + throw new Error('should have public part also'); + } + return true; + }), + Keypairs.parse({ key: pem, public: true }).then(function( + pair + ) { + if (pair.private) { + throw new Error('should NOT have private part'); + } + if (!pair.public) { + throw new Error( + 'should have the public part though' + ); + } + return true; + }) + ]).then(function() { + return true; + }); + }), + Keypairs.parseOrGenerate({ key: 'not a key', public: true }).then( + function(pair) { + // SHOULD have warning output + if (!pair.private || !pair.public) { + throw new Error( + "missing key pairs (should ignore 'public')" + ); + } + if (!pair.parseError) { + throw new Error( + 'should pass parseError for malformed string' + ); + } + return true; + } + ), + Keypairs.parse({ key: JSON.stringify(pair.private) }).then(function( + pair + ) { + if (!pair.private || !pair.public) { + throw new Error('missing key pairs (stringified jwt)'); + } + return true; + }), + Keypairs.parse({ + key: JSON.stringify(pair.private), + public: true + }).then(function(pair) { + if (pair.private) { + throw new Error("has private key when it shouldn't"); + } + if (!pair.public) { + throw new Error("doesn't have public key when it should"); + } + return true; + }), + Keypairs.parse({ key: JSON.stringify(pair.public), private: true }) + .then(function() { + var err = new Error( + 'should have thrown an error when private key was required and public jwk was given' + ); + err.code = 'NOERR'; + throw err; + }) + .catch(function(e) { + if ('NOERR' === e.code) { + throw e; + } + return true; + }), + Keypairs.signJwt({ + jwk: pair.private, + // Note: using ES512 won't actually increase the length + // (it would be truncated to fit into the key size) + alg: 'ES256', + iss: 'https://example.com/', + exp: '1h' + }).then(function(jwt) { + var parts = jwt.split('.'); + var now = Math.round(Date.now() / 1000); + var token = { + header: JSON.parse(Buffer.from(parts[0], 'base64')), + payload: JSON.parse(Buffer.from(parts[1], 'base64')), + signature: parts[2] //Buffer.from(parts[2], 'base64') + }; + // allow some leeway just in case we happen to hit a 1ms boundary + if (token.payload.exp - now > 60 * 59.99) { + return true; + } + throw new Error('token was not properly generated'); + }) + ]).then(function(results) { + if ( + results.length && + results.every(function(v) { + return true === v; + }) + ) { + console.log('PASS'); + process.exit(0); + } else { + throw new Error("didn't get all passes (but no errors either)"); + } + }); + }) + .catch(function(e) { + console.error('Caught an unexpected (failing) error:'); + console.error(e); + process.exit(1); + }); diff --git a/tests/rsa.sh b/tests/rsa.sh new file mode 100755 index 0000000..2e5feda --- /dev/null +++ b/tests/rsa.sh @@ -0,0 +1,186 @@ +#!/bin/bash + +# cause errors to hard-fail +# (and diff non-0 exit status will cause failure) +set -e + +pemtojwk() { + keyid=$1 + if [ -z "$keyid" ]; then + echo "" + echo "Testing PEM-to-JWK PKCS#1" + fi + # + node bin/rasha.js ./fixtures/privkey-rsa-2048.pkcs1.${keyid}pem \ + > ./fixtures/privkey-rsa-2048.jwk.1.json + diff ./fixtures/privkey-rsa-2048.jwk.${keyid}json ./fixtures/privkey-rsa-2048.jwk.1.json + # + node bin/rasha.js ./fixtures/pub-rsa-2048.pkcs1.${keyid}pem \ + > ./fixtures/pub-rsa-2048.jwk.1.json + diff ./fixtures/pub-rsa-2048.jwk.${keyid}json ./fixtures/pub-rsa-2048.jwk.1.json + if [ -z "$keyid" ]; then + echo "Pass" + fi + + + if [ -z "$keyid" ]; then + echo "" + echo "Testing PEM-to-JWK PKCS#8" + fi + # + node bin/rasha.js ./fixtures/privkey-rsa-2048.pkcs8.${keyid}pem \ + > ./fixtures/privkey-rsa-2048.jwk.1.json + diff ./fixtures/privkey-rsa-2048.jwk.${keyid}json ./fixtures/privkey-rsa-2048.jwk.1.json + # + node bin/rasha.js ./fixtures/pub-rsa-2048.spki.${keyid}pem \ + > ./fixtures/pub-rsa-2048.jwk.1.json + diff ./fixtures/pub-rsa-2048.jwk.${keyid}json ./fixtures/pub-rsa-2048.jwk.1.json + if [ -z "$keyid" ]; then + echo "Pass" + fi +} + +jwktopem() { + keyid=$1 + if [ -z "$keyid" ]; then + echo "" + echo "Testing JWK-to-PEM PKCS#1" + fi + # + node bin/rasha.js ./fixtures/privkey-rsa-2048.jwk.${keyid}json pkcs1 \ + > ./fixtures/privkey-rsa-2048.pkcs1.1.pem + diff ./fixtures/privkey-rsa-2048.pkcs1.${keyid}pem ./fixtures/privkey-rsa-2048.pkcs1.1.pem + # + node bin/rasha.js ./fixtures/pub-rsa-2048.jwk.${keyid}json pkcs1 \ + > ./fixtures/pub-rsa-2048.pkcs1.1.pem + diff ./fixtures/pub-rsa-2048.pkcs1.${keyid}pem ./fixtures/pub-rsa-2048.pkcs1.1.pem + if [ -z "$keyid" ]; then + echo "Pass" + fi + + if [ -z "$keyid" ]; then + echo "" + echo "Testing JWK-to-PEM PKCS#8" + fi + # + node bin/rasha.js ./fixtures/privkey-rsa-2048.jwk.${keyid}json pkcs8 \ + > ./fixtures/privkey-rsa-2048.pkcs8.1.pem + diff ./fixtures/privkey-rsa-2048.pkcs8.${keyid}pem ./fixtures/privkey-rsa-2048.pkcs8.1.pem + # + node bin/rasha.js ./fixtures/pub-rsa-2048.jwk.${keyid}json spki \ + > ./fixtures/pub-rsa-2048.spki.1.pem + diff ./fixtures/pub-rsa-2048.spki.${keyid}pem ./fixtures/pub-rsa-2048.spki.1.pem + if [ -z "$keyid" ]; then + echo "Pass" + fi + + if [ -z "$keyid" ]; then + echo "" + echo "[SKIP] Testing JWK-to-SSH" + fi + # + #node bin/rasha.js ./fixtures/privkey-rsa-2048.jwk.${keyid}json ssh > ./fixtures/pub-rsa-2048.ssh.1.pub + #diff ./fixtures/pub-rsa-2048.ssh.${keyid}pub ./fixtures/pub-rsa-2048.ssh.1.pub + # + #node bin/rasha.js ./fixtures/pub-rsa-2048.jwk.${keyid}json ssh > ./fixtures/pub-rsa-2048.ssh.1.pub + #diff ./fixtures/pub-rsa-2048.ssh.${keyid}pub ./fixtures/pub-rsa-2048.ssh.1.pub + if [ -z "$keyid" ]; then + echo "Pass" + fi +} + +rndkey() { + keyid="rnd.1." + keysize=$1 + # Generate 2048-bit RSA Keypair + openssl genrsa -out fixtures/privkey-rsa-2048.pkcs1.${keyid}pem $keysize + # Convert PKCS1 (traditional) RSA Keypair to PKCS8 format + openssl rsa -in fixtures/privkey-rsa-2048.pkcs1.${keyid}pem -pubout \ + -out fixtures/pub-rsa-2048.spki.${keyid}pem + # Export Public-only RSA Key in PKCS1 (traditional) format + openssl pkcs8 -topk8 -nocrypt -in fixtures/privkey-rsa-2048.pkcs1.${keyid}pem \ + -out fixtures/privkey-rsa-2048.pkcs8.${keyid}pem + # Convert PKCS1 (traditional) RSA Public Key to SPKI/PKIX format + openssl rsa -in fixtures/pub-rsa-2048.spki.${keyid}pem -pubin -RSAPublicKey_out \ + -out fixtures/pub-rsa-2048.pkcs1.${keyid}pem + # Convert RSA public key to SSH format + sshpub=$(ssh-keygen -f fixtures/pub-rsa-2048.spki.${keyid}pem -i -mPKCS8) + echo "$sshpub rsa@localhost" > fixtures/pub-rsa-2048.ssh.${keyid}pub + + + # to JWK + node bin/rasha.js ./fixtures/privkey-rsa-2048.pkcs1.${keyid}pem \ + > ./fixtures/privkey-rsa-2048.jwk.${keyid}json + node bin/rasha.js ./fixtures/pub-rsa-2048.pkcs1.${keyid}pem \ + > ./fixtures/pub-rsa-2048.jwk.${keyid}json + + pemtojwk "$keyid" + jwktopem "$keyid" +} + +pemtojwk "" +jwktopem "" + +echo "" +echo "testing node key generation" +echo "defaults" +node bin/rasha.js > /dev/null +echo "jwk" +node bin/rasha.js jwk > /dev/null +echo "json 2048" +node bin/rasha.js json 2048 > /dev/null +echo "der" +node bin/rasha.js der > /dev/null +echo "pkcs8 der" +node bin/rasha.js pkcs8 der > /dev/null +echo "pem" +node bin/rasha.js pem > /dev/null +echo "pkcs1" +node bin/rasha.js pkcs1 pem > /dev/null +echo "spki" +node bin/rasha.js spki > /dev/null +echo "PASS" + +echo "" +echo "" +echo "Re-running tests with random keys of varying sizes" +echo "" + +# commented out sizes below 512, since they are below minimum size on some systems. +# rndkey 32 # minimum key size +# rndkey 64 +# rndkey 128 +# rndkey 256 + +rndkey 512 +rndkey 768 +rndkey 1024 +rndkey 2048 # first secure key size + +if [ "${RASHA_TEST_LARGE_KEYS}" == "true" ]; then + rndkey 3072 + rndkey 4096 # largest reasonable key size +else + echo "" + echo "Note:" + echo "Keys larger than 2048 have been tested and work, but are omitted from automated tests to save time." + echo "Set RASHA_TEST_LARGE_KEYS=true to enable testing of keys up to 4096." +fi + +echo "" +echo "Pass" + +rm fixtures/*.1.* + +echo "" +echo "Testing Thumbprints" +node bin/rasha.js ./fixtures/privkey-rsa-2048.pkcs1.pem thumbprint +node bin/rasha.js ./fixtures/pub-rsa-2048.jwk.json thumbprint +echo "PASS" + +echo "" +echo "" +echo "PASSED:" +echo "• All inputs produced valid outputs" +echo "• All outputs matched known-good values" +echo "• All random tests passed reciprosity"