Compare commits

...

31 Commits

Author SHA1 Message Date
481015940e remove junk logs 2019-05-14 03:57:02 -06:00
1c67c47131 v1.3.0: integrate node v6 support (rsa-compat backport) 2019-05-14 03:54:27 -06:00
adc8eaec85 v1.2.5: remove junk vim swap file 2019-03-29 11:46:19 -06:00
57b27cb9d8 v1.2.4: typo fix in error message 2019-03-04 17:02:44 -07:00
acdad0065b v1.2.3: remove stray warning 2019-03-04 16:56:17 -07:00
b7508bba6c v1.2.2: remove telemetry and stray logs, lint some 2019-03-04 16:50:38 -07:00
6ae7076185 v1.2.1: update README 2019-02-07 00:13:17 -07:00
83dad00aaa Merge branch 'master' of ssh://git.coolaj86.com:22042/coolaj86/rasha.js 2019-02-07 00:12:45 -07:00
e088eedc8f v1.2.0: add jwk thumbprinting 2019-02-07 00:12:12 -07:00
6617d2c352 Removed buil status for now. 2019-01-24 20:18:48 -08:00
ef29727655 Merge branch 'master' of ssh://git.coolaj86.com:22042/coolaj86/rasha.js 2018-12-15 14:56:16 -07:00
691212ff13 sign a message 2018-12-15 14:55:54 -07:00
cd0533a72b Fixed the env var for testing extended bit lengths.
Some checks are pending
continuous-integration/drone/push Build is passing
2018-12-03 12:31:43 -08:00
c80e07ee7d cleanup 2018-12-01 16:28:44 -07:00
5bddc62731 re-order functions alphabetically 2018-12-01 15:33:43 -07:00
eb02693c1f Updated build status badge. 2018-11-30 13:41:44 -08:00
e3babcd6a9 Removed testing key sizes below 512. Added optional testing of larger keys. 2018-11-30 13:16:56 -08:00
54ccf6fa37 Added test status to README. 2018-11-30 11:30:12 -08:00
e8837a0721 output der as binstr, add comment 2018-11-24 19:47:56 -07:00
7a1df742aa catch generate errors, duh 2018-11-24 19:03:26 -07:00
bb272a9689 correct format hint 2018-11-24 18:36:32 -07:00
c17f0338e1 note on urlsafe base64 2018-11-24 17:37:23 -07:00
6cd0c28b86 generate RSA keys with node 10.12+ 2018-11-24 01:33:12 -07:00
40bb9a0fdd add backlinks (for code and doc mirrors) 2018-11-23 23:52:24 -07:00
0840749319 note OpenSSH 2018-11-23 23:36:57 -07:00
6e1a9a4e2a misc cleanup 2018-11-23 22:02:37 -07:00
bbb7fc99ec cleaner output 2018-11-23 14:24:40 -07:00
9aa463d3d2 move use of node Buffer to encoding.js 2018-11-23 14:24:21 -07:00
ec826aa906 v1.0.4: 'npm test' will now run the tests 2018-11-23 12:53:48 -07:00
d3b459aaf5 v1.0.3: update README 2018-11-23 12:44:06 -07:00
57edc8641b expose importSync and exportSync 2018-11-23 12:42:07 -07:00
19 changed files with 738 additions and 363 deletions

11
.drone.yml Normal file
View File

@ -0,0 +1,11 @@
kind: pipeline
name: default
pipeline:
build:
image: node
environment:
RASHA_TEST_LARGE_KEYS: "true"
commands:
- npm install --ignore-scripts
- npm test

2
.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
all.*
.*.sw*

View File

@ -1,27 +1,59 @@
Rasha.js
[Rasha.js](https://git.coolaj86.com/coolaj86/rasha.js)
=========
Sponsored by [Root](https://therootcompany.com).
A [Root](https://therootcompany.com) Project.
Built for [ACME.js](https://git.coolaj86.com/coolaj86/acme.js)
and [Greenlock.js](https://git.coolaj86.com/coolaj86/greenlock.js)
RSA tools. Lightweight. Zero Dependencies. Universal compatibility.
| ~550 lines of code | 3kb gzipped | 10kb minified | 18kb with comments |
RSA tools. Lightweight. Zero Dependencies. Universal compatibility.
* [x] Fast and Easy RSA Key Generation
* [x] PEM-to-JWK
* [x] JWK-to-PEM
* [x] JWK thumbprint
* [x] SSH "pub" format
* [ ] ECDSA
* **Need EC or ECDSA tools?** Check out [Eckles.js](https://git.coolaj86.com/coolaj86/eckles.js)
This project is fully functional and tested (and the code is pretty clean).
It is considered to be complete, but if you find a bug please open an issue.
## Generate RSA Key
Achieves the *fastest possible key generation* using node's native RSA bindings to OpenSSL,
then converts to JWK for ease-of-use.
```
Rasha.generate({ format: 'jwk' }).then(function (keypair) {
console.log(keypair.private);
console.log(keypair.public);
});
```
**options**
* `format` defaults to `'jwk'`
* `'pkcs1'` (traditional)
* `'pkcs8'` <!-- * `'ssh'` -->
* `modulusLength` defaults to 2048 (must not be lower)
* generally you shouldn't pick a larger key size - they're slow
* **2048** is more than sufficient
* 3072 is way, way overkill and takes a few seconds to generate
* 4096 can take about a minute to generate and is just plain wasteful
**advanced options**
These options are provided for debugging and should not be used.
* `publicExponent` defaults to 65537 (`0x10001`)
## PEM-to-JWK
* [x] PKCS#1 (traditional)
* [x] PKCS#8, SPKI/PKIX
* [x] 2048-bit, 4096-bit (and ostensibily all others)
* [x] 2048-bit, 3072-bit, 4096-bit (and ostensibily all others)
* [x] SSH (RFC4716), (RFC 4716/SSH2)
```js
@ -143,12 +175,20 @@ Qi49OykUCfNZeQlEz7UNNj9RGps/50+CNwIDAQAB
-----END PUBLIC KEY-----
```
## JWK Thumbprint
```js
Rasha.thumbprint({ jwk: jwk }).then(function (thumbprint) {
console.log(thumbprint);
});
```
Testing
-------
All cases are tested in `test.sh`.
You can compare these keys to the ones that you get from OpenSSL, ssh-keygen, and WebCrypto:
You can compare these keys to the ones that you get from OpenSSL, OpenSSH/ssh-keygen, and WebCrypto:
```bash
# Generate 2048-bit RSA Keypair
@ -181,7 +221,7 @@ Goals of this project
Legal
-----
Licensed MPL-2.0
[Rasha.js](https://git.coolaj86.com/coolaj86/rasha.js) |
MPL-2.0 |
[Terms of Use](https://therootcompany.com/legal/#terms) |
[Privacy Policy](https://therootcompany.com/legal/#privacy)

View File

@ -8,7 +8,36 @@ var ASN1 = require('../lib/asn1.js');
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 {
@ -17,13 +46,24 @@ try {
// 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));
@ -32,10 +72,38 @@ if ('string' === typeof key) {
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);
});
*/
}

View File

@ -1,2 +1,2 @@
'use strict';
module.exports = require('./lib/rasha.js');
module.exports = require('./lib/rsa.js');

View File

@ -76,13 +76,14 @@ ASN1.BitStr = function BITSTR() {
//
ASN1.ELOOP = "uASN1.js Error: iterated over 15+ elements (probably a malformed file)";
ASN1.EDEEP = "uASN1.js Error: element nested 10+ layers deep (probably a malformed file)";
ASN1.EDEEP = "uASN1.js Error: element nested 20+ layers deep (probably a malformed file)";
// Container Types are Sequence 0x30, Octect String 0x04, Array? (0xA0, 0xA1)
// Value Types are Integer 0x02, Bit String 0x03, Null 0x05, Object ID 0x06,
// Sometimes Bit String is used as a container (RSA Pub Spki)
ASN1.VTYPES = [ 0x02, 0x03, 0x05, 0x06 ];
ASN1.parse = function parseAsn1(buf, depth) {
if (depth >= 10) { throw new Error(ASN1.EDEEP); }
ASN1.VTYPES = [ 0x02, 0x03, 0x05, 0x06, 0x0c, 0x82 ];
ASN1.parse = function parseAsn1(buf, depth, ws) {
if (!ws) { ws = ''; }
if (depth >= 20) { throw new Error(ASN1.EDEEP); }
var index = 2; // we know, at minimum, data starts after type (0) and lengthSize (1)
var asn1 = { type: buf[0], lengthSize: 0, length: buf[1] };
@ -108,9 +109,10 @@ ASN1.parse = function parseAsn1(buf, depth) {
adjust = -1;
}
}
adjustedLen = asn1.length + adjust;
//console.warn(ws + '0x' + Enc.numToHex(asn1.type), index, 'len:', asn1.length, asn1);
// this is a primitive value type
if (-1 !== ASN1.VTYPES.indexOf(asn1.type)) {
asn1.value = buf.slice(index, index + adjustedLen);
@ -118,16 +120,31 @@ ASN1.parse = function parseAsn1(buf, depth) {
}
asn1.children = [];
while (iters < 15 && index <= asn1.length) {
//console.warn('1 len:', (2 + asn1.lengthSize + asn1.length), 'idx:', index, 'clen:', 0);
while (iters < 15 && index < (2 + asn1.length + asn1.lengthSize)) {
iters += 1;
child = ASN1.parse(buf.slice(index, index + adjustedLen), (depth || 0) + 1);
child = ASN1.parse(buf.slice(index, index + adjustedLen), (depth || 0) + 1, ws + ' ');
// The numbers don't match up exactly and I don't remember why...
// probably something with adjustedLen or some such, but the tests pass
index += (2 + child.lengthSize + child.length);
//console.warn('2 len:', (2 + asn1.lengthSize + asn1.length), 'idx:', index, 'clen:', (2 + child.lengthSize + child.length));
if (index > (2 + asn1.lengthSize + asn1.length)) {
console.error(JSON.stringify(asn1, toPrettyHex, 2));
throw new Error("Parse error: child value length (" + child.length
+ ") is greater than remaining parent length (" + (asn1.length - index)
+ " = " + asn1.length + " - " + index + ")");
}
asn1.children.push(child);
//console.warn(ws + '0x' + Enc.numToHex(asn1.type), index, 'len:', asn1.length, asn1);
}
if (index !== (2 + asn1.lengthSize + asn1.length)) {
throw new Error("premature end-of-file (" + 'index: ' + index + ' length: ' + (2 + asn1.lengthSize + asn1.length) + ")");
}
if (iters >= 15) { throw new Error(ASN1.ELOOP); }
return asn1;
};
function toPrettyHex(k, v) { if ('value' === k) { return '0x' + Enc.bufToHex(v.data); } return v; }
/*
ASN1._stringify = function(asn1) {
@ -136,7 +153,7 @@ ASN1._stringify = function(asn1) {
var ws = '';
function write(asn1) {
console.log(ws, 'ch', Buffer.from([asn1.type]).toString('hex'), asn1.length);
console.log(ws, 'ch', Enc.numToHex(asn1.type), asn1.length);
if (!asn1.children) {
return;
}
@ -153,7 +170,8 @@ ASN1._stringify = function(asn1) {
ASN1.tpl = function (asn1) {
//console.log(JSON.stringify(asn1, null, 2));
//console.log(asn1);
var ws = '\t';
var sp = ' ';
var ws = sp;
var i = 0;
var vars = [];
var str = ws;
@ -176,8 +194,8 @@ ASN1.tpl = function (asn1) {
if (!asn1.children) {
if (0x05 !== asn1.type) {
if (0x06 !== asn1.type) {
val = Buffer.from(asn1.value || '');
vars.push("// 0x" + Enc.numToHex(val.byteLength) + " (" + val.byteLength + " bytes)\nopts.tpl" + i + " = '"
val = asn1.value || new Uint8Array(0);
vars.push("\n// 0x" + Enc.numToHex(val.byteLength) + " (" + val.byteLength + " bytes)\nopts.tpl" + i + " = '"
+ Enc.bufToHex(val) + "';");
if (0x02 !== asn1.type && 0x03 !== asn1.type) {
str += ", ";
@ -186,26 +204,30 @@ ASN1.tpl = function (asn1) {
} else {
str += ", '" + Enc.bufToHex(asn1.value) + "'";
}
} else {
console.warn("XXXXXXXXXXXXXXXXXXXXX");
}
str += ")";
return ;
}
asn1.children.forEach(function (a, j) {
i += 1;
ws += '\t';
ws += sp;
write(a, j);
ws = ws.slice(1);
ws = ws.slice(sp.length);
});
str += "\n" + ws + ")";
}
write(asn1);
console.log('var opts = {};');
console.log(vars.join('\n') + '\n');
console.log();
console.log('function buildSchema(opts) {');
console.log('\treturn Enc.hexToBuf(' + str.slice(3) + ');');
console.log('}');
console.info('var opts = {};');
console.info(vars.join('\n') + '\n');
console.info();
console.info('function buildSchema(opts) {');
console.info(sp + 'return Enc.hexToBuf(' + str.slice(3) + ');');
console.info('}');
console.info();
console.info('buildSchema(opts);');
};
module.exports = ASN1;

51
lib/crypto.js Normal file
View File

@ -0,0 +1,51 @@
'use strict';
/*global Promise*/
var PEM = require('./pem.js');
var x509 = require('./x509.js');
var ASN1 = require('./asn1.js');
// Hacky-do, wrappy-do
module.exports.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 asn1 = ASN1.parse(block.bytes);
var jwk = { kty: 'RSA', n: null, e: null };
jwk = x509.parsePkcs1(block.bytes, asn1, jwk);
return { private: jwk, public: neuter(jwk) };
}
// Copied from rsa.js to prevent circular dep
var privates = [ 'p', 'q', 'd', 'dp', 'dq', 'qi' ];
function neuter(priv) {
var pub = {};
Object.keys(priv).forEach(function (key) {
if (!privates.includes(key)) {
pub[key] = priv[key];
}
});
return pub;
}
if (require.main === module) {
module.exports.generate().then(function (pair) {
console.info(JSON.stringify(pair.private, null, 2));
console.info(JSON.stringify(pair.public, null, 2));
});
}

View File

@ -2,11 +2,41 @@
var Enc = module.exports;
Enc.bufToHex = function toHex(u8) {
Enc.base64ToBuf = function (str) {
// always convert from urlsafe base64, just in case
//return Buffer.from(Enc.urlBase64ToBase64(str)).toString('base64');
// node handles urlBase64 automatically
return Buffer.from(str, 'base64');
};
Enc.base64ToHex = function (b64) {
return Enc.bufToHex(Enc.base64ToBuf(b64));
};
Enc.bufToBase64 = function (u8) {
// we want to maintain api compatability with browser APIs,
// so we assume that this could be a Uint8Array
return Buffer.from(u8).toString('base64');
};
/*
Enc.bufToUint8 = function bufToUint8(buf) {
return new Uint8Array(buf.buffer.slice(buf.byteOffset, buf.byteOffset + buf.byteLength));
};
*/
Enc.bufToUrlBase64 = function (u8) {
return Enc.bufToBase64(u8)
.replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, '');
};
Enc.bufToHex = function (u8) {
var hex = [];
var i, h;
var len = (u8.byteLength || u8.length);
for (i = 0; i < u8.byteLength; i += 1) {
for (i = 0; i < len; i += 1) {
h = u8[i].toString(16);
if (2 !== h.length) { h = '0' + h; }
hex.push(h);
@ -23,7 +53,7 @@ Enc.hexToBuf = function (hex) {
return Buffer.from(hex, 'hex');
};
Enc.numToHex = function numToHex(d) {
Enc.numToHex = function (d) {
d = d.toString(16);
if (d.length % 2) {
return '0' + d;
@ -31,37 +61,17 @@ Enc.numToHex = function numToHex(d) {
return d;
};
Enc.base64ToHex = function base64ToHex(b64) {
return Enc.bufToHex(Enc.base64ToBuf(b64));
};
Enc.bufToBase64 = function toHex(u8) {
// we want to maintain api compatability with browser APIs,
// so we assume that this could be a Uint8Array
return Buffer.from(u8).toString('base64');
};
/*
Enc.bufToUint8 = function bufToUint8(buf) {
return new Uint8Array(buf.buffer.slice(buf.byteOffset, buf.byteOffset + buf.byteLength));
Enc.strToBase64 = function (str) {
// node automatically can tell the difference
// between uc2 (utf-8) strings and binary strings
// so we don't have to re-encode the strings
return Buffer.from(str).toString('base64');
};
*/
Enc.bufToUrlBase64 = function toHex(u8) {
return Enc.bufToBase64(u8)
.replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, '');
};
Enc.strToHex = function strToHex(str) {
return Buffer.from(str).toString('hex');
};
Enc.strToBuf = function strToBuf(str) {
return Buffer.from(str);
};
/*
Enc.strToBin = function strToBin(str) {
Enc.strToBin = function (str) {
var escstr = encodeURIComponent(str);
// replaces any uri escape sequence, such as %0A,
// with binary escape, such as 0x0A
@ -73,17 +83,16 @@ Enc.strToBin = function strToBin(str) {
};
*/
/*
Enc.strToBase64 = function strToBase64(str) {
// node automatically can tell the difference
// between uc2 (utf-8) strings and binary strings
// so we don't have to re-encode the strings
return Buffer.from(str).toString('base64');
Enc.strToBuf = function (str) {
return Buffer.from(str);
};
Enc.strToHex = function (str) {
return Buffer.from(str).toString('hex');
};
*/
/*
Enc.urlBase64ToBase64 = function urlsafeBase64ToBase64(str) {
Enc.urlBase64ToBase64 = function (str) {
var r = str % 4;
if (2 === r) {
str += '==';
@ -93,9 +102,3 @@ Enc.urlBase64ToBase64 = function urlsafeBase64ToBase64(str) {
return str.replace(/-/g, '+').replace(/_/g, '/');
};
*/
Enc.base64ToBuf = function base64ToBuf(str) {
// always convert from urlsafe base64, just in case
//return Buffer.from(Enc.urlBase64ToBase64(str)).toString('base64');
return Buffer.from(str, 'base64');
};

View File

@ -0,0 +1,53 @@
// 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,23 @@
// 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,22 @@
// 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);
}

64
lib/generate-privkey.js Normal file
View File

@ -0,0 +1,64 @@
// 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);
}

View File

@ -3,43 +3,24 @@
var PEM = module.exports;
var Enc = require('./encoding.js');
PEM.RSA_OBJID = '06 09 2A864886F70D010101'
.replace(/\s+/g, '').toLowerCase();
PEM.parseBlock = function pemToDer(pem) {
var typ;
var pub;
var hex;
var der = Enc.base64ToBuf(pem.split(/\n/).filter(function (line, i) {
if (0 === i) {
if (/ PUBLIC /.test(line)) {
pub = true;
} else if (/ PRIVATE /.test(line)) {
pub = false;
}
if (/ RSA /.test(line)) {
typ = 'RSA';
}
}
return !/---/.test(line);
}).join(''));
var lines = pem.trim().split(/\n/);
var end = lines.length - 1;
var head = lines[0].match(/-----BEGIN (.*)-----/);
var foot = lines[end].match(/-----END (.*)-----/);
if (!typ) {
hex = Enc.bufToHex(der);
if (-1 !== hex.indexOf(PEM.RSA_OBJID)) {
typ = 'RSA';
if (head) {
lines = lines.slice(1, end);
head = head[1];
if (head !== foot[1]) {
throw new Error("headers and footers do not match");
}
}
if (!typ) {
console.warn("Definitely not an RSA PKCS#8 because there's no RSA Object ID in the DER body.");
console.warn("Probably not an RSA PKCS#1 because 'RSA' wasn't in the PEM type string.");
}
return { kty: typ, pub: pub, der: der };
return { type: head, bytes: Enc.base64ToBuf(lines.join('')) };
};
PEM.packBlock = function (opts) {
// TODO allow for headers?
return '-----BEGIN ' + opts.type + '-----\n'
+ Enc.bufToBase64(opts.bytes).match(/.{1,64}/g).join('\n') + '\n'
+ '-----END ' + opts.type + '-----'

View File

@ -1,121 +0,0 @@
'use strict';
var RSA = module.exports;
var SSH = require('./ssh.js');
var PEM = require('./pem.js');
var x509 = require('./x509.js');
var ASN1 = require('./asn1.js');
/*global Promise*/
RSA.parse = function parseRsa(opts) {
return Promise.resolve().then(function () {
if (!opts || !opts.pem || 'string' !== typeof opts.pem) {
throw new Error("must pass { pem: pem } as a string");
}
var jwk = { kty: 'RSA', n: null, e: null };
if (0 === opts.pem.indexOf('ssh-rsa ')) {
return SSH.parse(opts.pem, jwk);
}
var pem = opts.pem;
var block = PEM.parseBlock(pem);
//var hex = toHex(u8);
var asn1 = ASN1.parse(block.der);
var meta = x509.guess(block.der, asn1);
if ('pkcs1' === meta.format) {
jwk = x509.parsePkcs1(block.der, asn1, jwk);
} else {
jwk = x509.parsePkcs8(block.der, asn1, jwk);
}
if (opts.public) {
jwk = RSA.nueter(jwk);
}
return jwk;
});
};
RSA.toJwk = RSA.import = RSA.parse;
/*
RSAPrivateKey ::= SEQUENCE {
version Version,
modulus INTEGER, -- n
publicExponent INTEGER, -- e
privateExponent INTEGER, -- d
prime1 INTEGER, -- p
prime2 INTEGER, -- q
exponent1 INTEGER, -- d mod (p-1)
exponent2 INTEGER, -- d mod (q-1)
coefficient INTEGER, -- (inverse of q) mod p
otherPrimeInfos OtherPrimeInfos OPTIONAL
}
*/
RSA.pack = function (opts) {
return Promise.resolve().then(function () {
if (!opts || !opts.jwk || 'object' !== typeof opts.jwk) {
throw new Error("must pass { jwk: jwk }");
}
var jwk = JSON.parse(JSON.stringify(opts.jwk));
var format = opts.format;
var pub = opts.public;
if (pub || -1 !== [ 'spki', 'pkix', 'ssh', 'rfc4716' ].indexOf(format)) {
jwk = RSA.nueter(jwk);
}
if ('RSA' !== jwk.kty) {
throw new Error("options.jwk.kty must be 'RSA' for RSA keys");
}
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 });
} else {
throw new Error("Sanity Error: reached unreachable code block with format: " + format);
}
});
};
RSA.toPem = RSA.export = RSA.pack;
// snip the _private_ parts... hAHAHAHA!
RSA.nueter = function (jwk) {
// (snip rather than new object to keep potential extra data)
// otherwise we could just do this:
// return { kty: jwk.kty, n: jwk.n, e: jwk.e };
[ 'p', 'q', 'd', 'dp', 'dq', 'qi' ].forEach(function (key) {
if (key in jwk) { jwk[key] = undefined; }
return jwk;
});
return jwk;
};

225
lib/rsa.js Normal file
View File

@ -0,0 +1,225 @@
'use strict';
var RSA = module.exports;
var SSH = require('./ssh.js');
var PEM = require('./pem.js');
var x509 = require('./x509.js');
var ASN1 = require('./asn1.js');
var Enc = require('./encoding.js');
var Crypto = require('./crypto.js');
/*global Promise*/
RSA.generate = function (opts) {
opts.kty = "RSA";
return Crypto.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 };
});
});
});
};
RSA.importSync = function (opts) {
if (!opts || !opts.pem || 'string' !== typeof opts.pem) {
throw new Error("must pass { pem: pem } as a string");
}
var jwk = { kty: 'RSA', n: null, e: null };
if (0 === opts.pem.indexOf('ssh-rsa ')) {
return SSH.parse(opts.pem, jwk);
}
var pem = opts.pem;
var block = PEM.parseBlock(pem);
//var hex = toHex(u8);
var asn1 = ASN1.parse(block.bytes);
var meta = x509.guess(block.bytes, asn1);
if ('pkcs1' === meta.format) {
jwk = x509.parsePkcs1(block.bytes, asn1, jwk);
} else {
jwk = x509.parsePkcs8(block.bytes, asn1, jwk);
}
if (opts.public) {
jwk = RSA.neuter(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;
/*
RSAPrivateKey ::= SEQUENCE {
version Version,
modulus INTEGER, -- n
publicExponent INTEGER, -- e
privateExponent INTEGER, -- d
prime1 INTEGER, -- p
prime2 INTEGER, -- q
exponent1 INTEGER, -- d mod (p-1)
exponent2 INTEGER, -- d mod (q-1)
coefficient INTEGER, -- (inverse of q) mod p
otherPrimeInfos OtherPrimeInfos OPTIONAL
}
*/
RSA.exportSync = function (opts) {
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);
}
if ('RSA' !== jwk.kty) {
throw new Error("options.jwk.kty must be 'RSA' for RSA keys");
}
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 });
} else {
throw new Error("Sanity Error: reached unreachable code block with format: " + format);
}
};
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.exportSync(opts);
});
};
RSA.toPem = RSA.export = RSA.pack;
// snip the _private_ parts... hAHAHAHA!
var privates = [ 'p', 'q', 'd', 'dp', 'dq', 'qi' ];
// fix misspelling without breaking the API
RSA.neuter = RSA.nueter = function (priv) {
var pub = {};
Object.keys(priv).forEach(function (key) {
if (!privates.includes(key)) {
pub[key] = priv[key];
}
});
return pub;
};
RSA.__thumbprint = function (jwk) {
var buf = require('crypto').createHash('sha256')
// alphabetically sorted keys [ 'e', 'kty', 'n' ]
.update('{"e":"' + jwk.e + '","kty":"RSA","n":"' + jwk.n + '"}')
.digest()
;
return Enc.bufToUrlBase64(buf);
};
RSA.thumbprint = function (opts) {
return Promise.resolve().then(function () {
var jwk;
if ('RSA' === opts.kty) {
jwk = opts;
} else if (opts.jwk) {
jwk = opts.jwk;
} else {
jwk = RSA.importSync(opts);
}
return RSA.__thumbprint(jwk);
});
};

View File

@ -1,111 +0,0 @@
'use strict';
// We believe in a proactive approach to sustainable open source.
// As part of that we make it easy for you to opt-in to following our progress
// and we also stay up-to-date on telemetry such as operating system and node
// version so that we can focus our efforts where they'll have the greatest impact.
//
// Want to learn more about our Terms, Privacy Policy, and Mission?
// Check out https://therootcompany.com/legal/
var os = require('os');
var crypto = require('crypto');
var https = require('https');
var pkg = require('../package.json');
// to help focus our efforts in the right places
var data = {
package: pkg.name
, version: pkg.version
, node: process.version
, arch: process.arch || os.arch()
, platform: process.platform || os.platform()
, release: os.release()
};
function addCommunityMember(opts) {
setTimeout(function () {
var req = https.request({
hostname: 'api.therootcompany.com'
, port: 443
, path: '/api/therootcompany.com/public/community'
, method: 'POST'
, headers: { 'Content-Type': 'application/json' }
}, function (resp) {
// let the data flow, so we can ignore it
resp.on('data', function () {});
//resp.on('data', function (chunk) { console.log(chunk.toString()); });
resp.on('error', function () { /*ignore*/ });
//resp.on('error', function (err) { console.error(err); });
});
var obj = JSON.parse(JSON.stringify(data));
obj.action = 'updates';
try {
obj.ppid = ppid(obj.action);
} catch(e) {
// ignore
//console.error(e);
}
obj.name = opts.name || undefined;
obj.address = opts.email;
obj.community = 'node.js@therootcompany.com';
req.write(JSON.stringify(obj, 2, null));
req.end();
req.on('error', function () { /*ignore*/ });
//req.on('error', function (err) { console.error(err); });
}, 50);
}
function ping(action) {
setTimeout(function () {
var req = https.request({
hostname: 'api.therootcompany.com'
, port: 443
, path: '/api/therootcompany.com/public/ping'
, method: 'POST'
, headers: { 'Content-Type': 'application/json' }
}, function (resp) {
// let the data flow, so we can ignore it
resp.on('data', function () { });
//resp.on('data', function (chunk) { console.log(chunk.toString()); });
resp.on('error', function () { /*ignore*/ });
//resp.on('error', function (err) { console.error(err); });
});
var obj = JSON.parse(JSON.stringify(data));
obj.action = action;
try {
obj.ppid = ppid(obj.action);
} catch(e) {
// ignore
//console.error(e);
}
req.write(JSON.stringify(obj, 2, null));
req.end();
req.on('error', function (/*e*/) { /*console.error('req.error', e);*/ });
}, 50);
}
// to help identify unique installs without getting
// the personally identifiable info that we don't want
function ppid(action) {
var parts = [ action, data.package, data.version, data.node, data.arch, data.platform, data.release ];
var ifaces = os.networkInterfaces();
Object.keys(ifaces).forEach(function (ifname) {
if (/^en/.test(ifname) || /^eth/.test(ifname) || /^wl/.test(ifname)) {
if (ifaces[ifname] && ifaces[ifname].length) {
parts.push(ifaces[ifname][0].mac);
}
}
});
return crypto.createHash('sha1').update(parts.join(',')).digest('base64');
}
module.exports.ping = ping;
module.exports.joinCommunity = addCommunityMember;
if (require.main === module) {
ping('install');
//addCommunityMember({ name: "AJ ONeal", email: 'coolaj86@gmail.com' });
}

View File

@ -1,7 +1,5 @@
'use strict';
// TODO fun idea: create a template from an existing file
var x509 = module.exports;
var ASN1 = require('./asn1.js');
var Enc = require('./encoding.js');

View File

@ -1,6 +1,6 @@
{
"name": "rasha",
"version": "1.0.1",
"version": "1.3.0",
"description": "💯 PEM-to-JWK and JWK-to-PEM for RSA keys in a lightweight, zero-dependency library focused on perfect universal compatibility.",
"homepage": "https://git.coolaj86.com/coolaj86/rasha.js",
"main": "index.js",
@ -16,8 +16,7 @@
"lib": "lib"
},
"scripts": {
"postinstall": "node lib/telemetry.js event:install",
"test": "echo \"Error: no test specified\" && exit 1"
"test": "bash test.sh"
},
"repository": {
"type": "git",
@ -36,5 +35,8 @@
"PEM-to-SSH"
],
"author": "AJ ONeal <coolaj86@gmail.com> (https://coolaj86.com/)",
"license": "MPL-2.0"
"license": "MPL-2.0",
"trulyOptionalDependencies": {
"node-forge": "^0.8.2"
}
}

80
test.sh
View File

@ -1,13 +1,14 @@
#!/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 ""
echo "Testing PEM-to-JWK PKCS#1"
echo ""
fi
#
node bin/rasha.js ./fixtures/privkey-rsa-2048.pkcs1.${keyid}pem \
@ -17,13 +18,14 @@ pemtojwk() {
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 ""
echo "Testing PEM-to-JWK PKCS#8"
echo ""
fi
#
node bin/rasha.js ./fixtures/privkey-rsa-2048.pkcs8.${keyid}pem \
@ -33,15 +35,16 @@ pemtojwk() {
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 ""
echo "Testing JWK-to-PEM PKCS#1"
echo ""
fi
#
node bin/rasha.js ./fixtures/privkey-rsa-2048.jwk.${keyid}json pkcs1 \
@ -51,13 +54,13 @@ jwktopem() {
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 ""
echo "Testing JWK-to-PEM PKCS#8"
echo ""
fi
#
node bin/rasha.js ./fixtures/privkey-rsa-2048.jwk.${keyid}json pkcs8 \
@ -67,13 +70,13 @@ jwktopem() {
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 ""
echo "Testing JWK-to-SSH"
echo ""
fi
#
node bin/rasha.js ./fixtures/privkey-rsa-2048.jwk.${keyid}json ssh > ./fixtures/pub-rsa-2048.ssh.1.pub
@ -81,6 +84,9 @@ jwktopem() {
#
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() {
@ -116,29 +122,65 @@ 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 "Testing different size random keys"
echo ""
rndkey 32 # minimum key size
rndkey 64
rndkey 128
rndkey 256
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 4096 work as well, but they take minutes to generate, so we stop here."
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 ""
echo "• All random tests passed reciprosity"