v0.9.0: lightweight ecdsa / rsa key generation, conversion, and signing

This commit is contained in:
AJ ONeal 2019-10-15 04:12:46 -06:00
parent 6d0ab30620
commit e325694ed6
42 changed files with 2521 additions and 17 deletions

View File

@ -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 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/) - 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: <paypal@therootcompany.com>
- Patreon: <https://patreon.com/rootprojects>
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?
<!-- Please visit <https://therootcompany.com> or contact -->
Contact <aj@therootcompany.com> 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)

81
bin/eckles.js Executable file
View File

@ -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);
});
}

126
bin/rasha.js Executable file
View File

@ -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);
});
*/
}

240
ecdsa.js Normal file
View File

@ -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);
});
};

View File

@ -0,0 +1,7 @@
{
"kty": "EC",
"crv": "P-256",
"d": "iYydo27aNGO9DBUWeGEPD8oNi1LZDqfxPmQlieLBjVQ",
"x": "IT1SWLxsacPiE5Z16jkopAn8_-85rMjgyCokrnjDft4",
"y": "mP2JwOAOdMmXuwpxbKng3KZz27mz-nKWIlXJ3rzSGMo"
}

View File

@ -0,0 +1,5 @@
-----BEGIN PRIVATE KEY-----
MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgiYydo27aNGO9DBUW
eGEPD8oNi1LZDqfxPmQlieLBjVShRANCAAQhPVJYvGxpw+ITlnXqOSikCfz/7zms
yODIKiSueMN+3pj9icDgDnTJl7sKcWyp4Nymc9u5s/pyliJVyd680hjK
-----END PRIVATE KEY-----

View File

@ -0,0 +1,5 @@
-----BEGIN EC PRIVATE KEY-----
MHcCAQEEIImMnaNu2jRjvQwVFnhhDw/KDYtS2Q6n8T5kJYniwY1UoAoGCCqGSM49
AwEHoUQDQgAEIT1SWLxsacPiE5Z16jkopAn8/+85rMjgyCokrnjDft6Y/YnA4A50
yZe7CnFsqeDcpnPbubP6cpYiVcnevNIYyg==
-----END EC PRIVATE KEY-----

View File

@ -0,0 +1,7 @@
{
"kty": "EC",
"crv": "P-384",
"d": "XlyuCEWSTTS8U79O_Mz05z18vh4kb10szvu_7pdXuGWV6lfEyPExyUYWsA6A2kdV",
"x": "2zEU0bKCa7ejKLIJ8oPGnLhqhxyiv4_w38K2a0SPC6dsSd9_glNJ8lcqv0sff5Gb",
"y": "VD4jnu83S6scn6_TeAj3EZOREGbOs6dzoVpaugn-XQMMyC9O4VLbDDFGBZTJlMsb"
}

View File

@ -0,0 +1,6 @@
-----BEGIN PRIVATE KEY-----
MIG2AgEAMBAGByqGSM49AgEGBSuBBAAiBIGeMIGbAgEBBDBeXK4IRZJNNLxTv078
zPTnPXy+HiRvXSzO+7/ul1e4ZZXqV8TI8THJRhawDoDaR1WhZANiAATbMRTRsoJr
t6Mosgnyg8acuGqHHKK/j/DfwrZrRI8Lp2xJ33+CU0nyVyq/Sx9/kZtUPiOe7zdL
qxyfr9N4CPcRk5EQZs6zp3OhWlq6Cf5dAwzIL07hUtsMMUYFlMmUyxs=
-----END PRIVATE KEY-----

View File

@ -0,0 +1,6 @@
-----BEGIN EC PRIVATE KEY-----
MIGkAgEBBDBeXK4IRZJNNLxTv078zPTnPXy+HiRvXSzO+7/ul1e4ZZXqV8TI8THJ
RhawDoDaR1WgBwYFK4EEACKhZANiAATbMRTRsoJrt6Mosgnyg8acuGqHHKK/j/Df
wrZrRI8Lp2xJ33+CU0nyVyq/Sx9/kZtUPiOe7zdLqxyfr9N4CPcRk5EQZs6zp3Oh
Wlq6Cf5dAwzIL07hUtsMMUYFlMmUyxs=
-----END EC PRIVATE KEY-----

View File

@ -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"
}

View File

@ -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-----

View File

@ -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-----

View File

@ -0,0 +1,6 @@
{
"kty": "EC",
"crv": "P-256",
"x": "IT1SWLxsacPiE5Z16jkopAn8_-85rMjgyCokrnjDft4",
"y": "mP2JwOAOdMmXuwpxbKng3KZz27mz-nKWIlXJ3rzSGMo"
}

View File

@ -0,0 +1,4 @@
-----BEGIN PUBLIC KEY-----
MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEIT1SWLxsacPiE5Z16jkopAn8/+85
rMjgyCokrnjDft6Y/YnA4A50yZe7CnFsqeDcpnPbubP6cpYiVcnevNIYyg==
-----END PUBLIC KEY-----

View File

@ -0,0 +1 @@
ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBCE9Uli8bGnD4hOWdeo5KKQJ/P/vOazI4MgqJK54w37emP2JwOAOdMmXuwpxbKng3KZz27mz+nKWIlXJ3rzSGMo= P-256@localhost

View File

@ -0,0 +1,6 @@
{
"kty": "EC",
"crv": "P-384",
"x": "2zEU0bKCa7ejKLIJ8oPGnLhqhxyiv4_w38K2a0SPC6dsSd9_glNJ8lcqv0sff5Gb",
"y": "VD4jnu83S6scn6_TeAj3EZOREGbOs6dzoVpaugn-XQMMyC9O4VLbDDFGBZTJlMsb"
}

View File

@ -0,0 +1,5 @@
-----BEGIN PUBLIC KEY-----
MHYwEAYHKoZIzj0CAQYFK4EEACIDYgAE2zEU0bKCa7ejKLIJ8oPGnLhqhxyiv4/w
38K2a0SPC6dsSd9/glNJ8lcqv0sff5GbVD4jnu83S6scn6/TeAj3EZOREGbOs6dz
oVpaugn+XQMMyC9O4VLbDDFGBZTJlMsb
-----END PUBLIC KEY-----

View File

@ -0,0 +1 @@
ecdsa-sha2-nistp384 AAAAE2VjZHNhLXNoYTItbmlzdHAzODQAAAAIbmlzdHAzODQAAABhBNsxFNGygmu3oyiyCfKDxpy4aoccor+P8N/CtmtEjwunbEnff4JTSfJXKr9LH3+Rm1Q+I57vN0urHJ+v03gI9xGTkRBmzrOnc6FaWroJ/l0DDMgvTuFS2wwxRgWUyZTLGw== P-384@localhost

View File

@ -0,0 +1,5 @@
{
"kty": "RSA",
"n": "m2ttVBxPlWw06ZmGBWVDlfjkPAJ4DgnY0TrDwtCohHzLxGhDNzUJefLukC-xu0LBKylYojT5vTkxaOhxeSYo31syu4WhxbkTBLICOFcCGMob6pSQ38P8LdAIlb0pqDHxEJ9adWomjuFf0SUhN1cP7s9m8Yk9trkpEqjskocn2BOnTB57qAZM6-I70on0_iDZm7-jcqOPgADAmbWHhy67BXkk4yy_YzD4yOGZFXZcNp915_TW5bRd__AKPHUHxJasPiyEFqlNKBR2DSD-LbX5eTmzCh2ikrwTMja7mUdBJf2bK3By5AB0Qi49OykUCfNZeQlEz7UNNj9RGps_50-CNw",
"e": "AQAB"
}

View File

@ -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-----

View File

@ -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-----

View File

@ -0,0 +1 @@
ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQCba21UHE+VbDTpmYYFZUOV+OQ8AngOCdjROsPC0KiEfMvEaEM3NQl58u6QL7G7QsErKViiNPm9OTFo6HF5JijfWzK7haHFuRMEsgI4VwIYyhvqlJDfw/wt0AiVvSmoMfEQn1p1aiaO4V/RJSE3Vw/uz2bxiT22uSkSqOyShyfYE6dMHnuoBkzr4jvSifT+INmbv6Nyo4+AAMCZtYeHLrsFeSTjLL9jMPjI4ZkVdlw2n3Xn9NbltF3/8Ao8dQfElqw+LIQWqU0oFHYNIP4ttfl5ObMKHaKSvBMyNruZR0El/ZsrcHLkAHRCLj07KRQJ81l5CUTPtQ02P1Eamz/nT4I3 rsa@localhost

View File

@ -1,3 +1,410 @@
/*global Promise*/
'use strict'; '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;
}

57
lib/browser/ecdsa.js Normal file
View File

@ -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 })
};
});
});
};

108
lib/browser/keypairs.js Normal file
View File

@ -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';
}
};

59
lib/browser/rsa.js Normal file
View File

@ -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 })
};
});
});
};

13
lib/browser/sha2.js Normal file
View File

@ -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);
};

113
lib/node/ecdsa.js Normal file
View File

@ -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
};
});
});
});
});
};

View File

@ -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);
}

View File

@ -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);
}

View File

@ -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);
}

View File

@ -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);
}

84
lib/node/keypairs.js Normal file
View File

@ -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')]);
};

116
lib/node/rsa.js Normal file
View File

@ -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 }) };
}

17
lib/node/sha2.js Normal file
View File

@ -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();
});
};

33
package-lock.json generated
View File

@ -1,30 +1,35 @@
{ {
"name": "@root/keypairs", "name": "@root/keypairs",
"version": "1.0.0-wip.0", "version": "0.9.0",
"lockfileVersion": 1, "lockfileVersion": 1,
"requires": true, "requires": true,
"dependencies": { "dependencies": {
"@root/acme": { "@root/asn1": {
"version": "3.0.0-wip.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/@root/acme/-/acme-3.0.0-wip.0.tgz", "resolved": "https://registry.npmjs.org/@root/asn1/-/asn1-1.0.0.tgz",
"integrity": "sha512-IwnG3ZFt1fl81O1M+FFV91b5Kpw7GYAD1jXwvOWnq9KF50AVO6+L7MUQIAFCK1q/u/weC73DCFrw/6kFN+Vi9A==", "integrity": "sha512-0lfZNuOULKJDJmdIkP8V9RnbV3XaK6PAHD3swnFy4tZwtlMDzLKoM/dfNad7ut8Hu3r91wy9uK0WA/9zym5mig==",
"requires": { "requires": {
"@root/csr": "^1.0.0-wip.0",
"@root/encoding": "^1.0.1" "@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": { "@root/encoding": {
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/@root/encoding/-/encoding-1.0.1.tgz", "resolved": "https://registry.npmjs.org/@root/encoding/-/encoding-1.0.1.tgz",
"integrity": "sha512-OaEub02ufoU038gy6bsNHQOjIn8nUjGiLcaRmJ40IUykneJkIW5fxDqKxQx48cszuNflYldsJLPPXCrGfHs8yQ==" "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"
}
} }
} }
} }

View File

@ -1,13 +1,20 @@
{ {
"name": "@root/keypairs", "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", "description": "Lightweight, Zero-Dependency RSA and EC/ECDSA crypto for Node.js and Browsers",
"main": "keypairs.js", "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": { "scripts": {
"test": "node tests" "test": "node tests"
}, },
"files": [ "files": [
"*.js", "*.js",
"bin",
"lib", "lib",
"dist" "dist"
], ],
@ -28,6 +35,8 @@
"author": "AJ ONeal <coolaj86@gmail.com> (https://coolaj86.com/)", "author": "AJ ONeal <coolaj86@gmail.com> (https://coolaj86.com/)",
"license": "MPL-2.0", "license": "MPL-2.0",
"dependencies": { "dependencies": {
"@root/acme": "^3.0.0-wip.0" "@root/encoding": "^1.0.1",
"@root/pem": "^1.0.4",
"@root/x509": "^0.7.2"
} }
} }

192
rsa.js Normal file
View File

@ -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;

139
tests/ecdsa.sh Normal file
View File

@ -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 ""

165
tests/index.js Normal file
View File

@ -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);
});

186
tests/rsa.sh Executable file
View File

@ -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"