Compare commits

...

16 Commits

6 changed files with 508 additions and 53 deletions

155
README.md
View File

@ -1,4 +1,4 @@
# Keypairs for node.js # Keypairs.js
Lightweight JavaScript RSA and ECDSA utils that work on Windows, Mac, and Linux Lightweight JavaScript RSA and ECDSA utils that work on Windows, Mac, and Linux
using modern node.js APIs (no need for C compiler). using modern node.js APIs (no need for C compiler).
@ -11,41 +11,67 @@ and [Rasha.js (RSA)](https://git.coolaj86.com/coolaj86/rasha.js/).
* [x] Generate keypairs * [x] Generate keypairs
* [x] RSA * [x] RSA
* [x] ECDSA (P-256, P-384) * [x] ECDSA (P-256, P-384)
* [x] PEM-to-JWK * [x] PEM-to-JWK (and SSH-to-JWK)
* [x] JWK-to-PEM * [x] JWK-to-PEM (and JWK-to-SSH)
* [x] Create JWTs (and sign JWS)
* [x] SHA256 JWK Thumbprints * [x] SHA256 JWK Thumbprints
* [ ] JWK fetching. See [Keyfetch.js](https://npmjs.com/packages/keyfetch/) * [ ] JWK fetching. See [Keyfetch.js](https://npmjs.com/packages/keyfetch/)
* [ ] OIDC * [ ] OIDC
* [ ] Auth0 * [ ] Auth0
* [ ] CLI
* See [keypairs-cli](https://npmjs.com/packages/keypairs-cli/)
<!-- <!--
* [ ] sign JWS
* [ ] generate CSR (DER as PEM or base64url) * [ ] generate CSR (DER as PEM or base64url)
--> -->
# Usage # Usage
A brief (albeit somewhat nonsensical) introduction to the APIs: A brief introduction to the APIs:
``` ```
// generate a new keypair as jwk
// (defaults to EC P-256 when no options are specified)
Keypairs.generate().then(function (pair) { Keypairs.generate().then(function (pair) {
return Keypairs.export({ jwk: pair.private }).then(function (pem) { console.log(pair.private);
return Keypairs.import({ pem: pem }).then(function (jwk) { console.log(pair.public);
return Keypairs.thumbprint({ jwk: jwk }).then(function (thumb) { });
console.log(thumb); ```
return Keypairs.signJwt({
jwk: keypair.private ```
, claims: { // JWK to PEM
iss: 'https://example.com' // (supports various 'format' and 'encoding' options)
, sub: 'jon.doe@gmail.com' return Keypairs.export({ jwk: pair.private, format: 'pkcs8' }).then(function (pem) {
, exp: Math.round(Date.now()/1000) + (3 * 24 * 60 * 60) console.log(pem);
} });
}); ```
});
}); ```
}); // PEM to JWK
return Keypairs.import({ pem: pem }).then(function (jwk) {
console.log(jwk);
});
```
```
// Thumbprint a JWK (SHA256)
return Keypairs.thumbprint({ jwk: jwk }).then(function (thumb) {
console.log(thumb);
});
```
```
// Sign a JWT (aka compact JWS)
return Keypairs.signJwt({
jwk: pair.private
, iss: 'https://example.com'
, exp: '1h'
// optional claims
, claims: {
, sub: 'jon.doe@gmail.com'
}
}); });
``` ```
@ -54,6 +80,16 @@ _much_ longer than RSA has, and they're smaller, and faster to generate.
## API Overview ## API Overview
* generate (JWK)
* parse (PEM)
* parseOrGenerate (PEM to JWK)
* import (PEM-to-JWK)
* export (JWK-to-PEM, private or public)
* publish (Private JWK to Public JWK)
* thumbprint (JWK SHA256)
* signJwt
* signJws
#### Keypairs.generate(options) #### Keypairs.generate(options)
Generates a public/private pair of JWKs as `{ private, public }` Generates a public/private pair of JWKs as `{ private, public }`
@ -65,6 +101,50 @@ Option examples:
When no options are supplied EC P-256 (also known as `prime256v1` and `secp256r1`) is used by default. When no options are supplied EC P-256 (also known as `prime256v1` and `secp256r1`) is used by default.
#### Keypairs.parse(options)
Parses either a JWK (encoded as JSON) or an x509 (encdode as PEM) and gives
back the JWK representation.
Option Examples:
* JWK { key: '{ "kty":"EC", ... }' }
* PEM { key: '-----BEGIN PRIVATE KEY-----\n...' }
* Public Key Only { key: '-----BEGIN PRIVATE KEY-----\n...', public: true }
* Must Have Private Key { key: '-----BEGIN PUBLIC KEY-----\n...', private: true }
Example:
```js
Keypairs.parse({ key: '...' }).catch(function (e) {
// could not be parsed or was a public key
console.warn(e);
return Keypairs.generate();
});
```
#### Keypairs.parseOrGenerate({ key, throw, [generate opts]... })
Parses the key. Logs a warning on failure, marches on.
(a shortcut for the above, with `private: true`)
Option Examples:
* parse key if exist, otherwise generate `{ key: process.env["PRIVATE_KEY"] }`
* generated key curve `{ key: null, namedCurve: 'P-256' }`
* generated key modulus `{ key: null, modulusLength: 2048 }`
Example:
```js
Keypairs.parseOrGenerate({ key: process.env["PRIVATE_KEY"] }).then(function (pair) {
console.log(pair.public);
})
```
Great for when you have a set of shared keys for development and randomly
generated keys in
#### Keypairs.import({ pem: '...' } #### Keypairs.import({ pem: '...' }
Takes a PEM in pretty much any format (PKCS1, SEC1, PKCS8, SPKI) and returns a JWK. Takes a PEM in pretty much any format (PKCS1, SEC1, PKCS8, SPKI) and returns a JWK.
@ -85,6 +165,23 @@ Options
} }
``` ```
#### Keypairs.publish({ jwk: jwk, exp: '3d', use: 'sig' })
Promises a public key that adheres to the OIDC and Auth0 spec (plus expiry), suitable to be published to a JWKs URL:
```
{ "kty": "EC"
, "crv": "P-256"
, "x": "..."
, "y": "..."
, "kid": "..."
, "use": "sig"
, "exp": 1552074208
}
```
In particular this adds "use" and "exp".
#### Keypairs.thumbprint({ jwk: jwk }) #### Keypairs.thumbprint({ jwk: jwk })
Promises a JWK-spec thumbprint: URL Base64-encoded sha256 Promises a JWK-spec thumbprint: URL Base64-encoded sha256
@ -95,11 +192,17 @@ Returns a JWT (otherwise known as a protected JWS in "compressed" format).
```js ```js
{ jwk: jwk { jwk: jwk
// required claims
, iss: 'https://example.com'
, exp: '15m'
// all optional claims
, claims: { , claims: {
} }
} }
``` ```
Exp may be human readable duration (i.e. 1h, 15m, 30s) or a datetime in seconds.
Header defaults: Header defaults:
```js ```js
@ -134,11 +237,15 @@ Options:
# Additional Documentation # Additional Documentation
Keypairs.js provides a 1-to-1 mapping to the Rasha.js and Eckles.js APIs, Keypairs.js provides a 1-to-1 mapping to the Rasha.js and Eckles.js APIs for the following:
but it also includes the additional convenience methods `signJwt` and `signJws`.
That is to say that any option you pass to Keypairs will be passed directly to the corresponding API * generate(options)
of either Rasha or Eckles. * import({ pem: '---BEGIN...' })
* export({ jwk: { kty: 'EC', ... })
* thumbprint({ jwk: jwk })
If you want to know the algorithm-specific options that are available for those
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/)

12
bin/keypairs.js Executable file
View File

@ -0,0 +1,12 @@
#!/usr/bin/env node
'use strict';
var cmd = "npm install --global keypairs-cli";
console.error(cmd);
require('child_process').exec(cmd, function (err) {
if (err) {
console.error(err);
return;
}
console.info("Run 'keypairs help' to see what you can do!");
});

View File

@ -10,15 +10,88 @@ var Keypairs = module.exports;
Keypairs.generate = function (opts) { Keypairs.generate = function (opts) {
opts = opts || {}; opts = opts || {};
var kty = opts.kty || opts.type; var kty = opts.kty || opts.type;
var p;
if ('RSA' === kty) { if ('RSA' === kty) {
return Rasha.generate(opts); p = Rasha.generate(opts);
} else {
p = Eckles.generate(opts);
} }
return Eckles.generate(opts); 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.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) {
console.warn(e.message);
return Keypairs.generate(opts);
});
}; };
Keypairs.import = function (opts) { Keypairs.import = function (opts) {
return Eckles.import(opts.pem).catch(function () { return Eckles.import(opts).catch(function () {
return Rasha.import(opts.pem); return Rasha.import(opts);
}).then(function (jwk) {
return Keypairs.thumbprint({ jwk: jwk }).then(function (thumb) {
jwk.kid = thumb;
return jwk;
});
}); });
}; };
@ -32,6 +105,39 @@ Keypairs.export = function (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.
Keypairs.neuter = 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.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; });
};
Keypairs.thumbprint = function (opts) { Keypairs.thumbprint = function (opts) {
return Promise.resolve().then(function () { return Promise.resolve().then(function () {
if ('RSA' === opts.jwk.kty) { if ('RSA' === opts.jwk.kty) {
@ -48,20 +154,25 @@ Keypairs.signJwt = function (opts) {
var header = opts.header || {}; var header = opts.header || {};
var claims = JSON.parse(JSON.stringify(opts.claims || {})); var claims = JSON.parse(JSON.stringify(opts.claims || {}));
header.typ = 'JWT'; header.typ = 'JWT';
if (!header.kid) {
header.kid = thumb; if (!header.kid) { header.kid = thumb; }
} if (!header.alg && opts.alg) { header.alg = opts.alg; }
if (false === claims.iat) { if (!claims.iat && (false === claims.iat || false === opts.iat)) {
claims.iat = undefined; claims.iat = undefined;
} else if (!claims.iat) { } else if (!claims.iat) {
claims.iat = Math.round(Date.now()/1000); claims.iat = Math.round(Date.now()/1000);
} }
if (false === claims.exp) {
if (opts.exp) {
claims.exp = setTime(opts.exp);
} else if (!claims.exp && (false === claims.exp || false === opts.exp)) {
claims.exp = undefined; claims.exp = undefined;
} else if (!claims.exp) { } else if (!claims.exp) {
throw new Error("opts.claims.exp should be the expiration date (as seconds since the Unix epoch) or false"); throw new Error("opts.claims.exp should be the expiration date as seconds, human form (i.e. '1h' or '15m') or false");
} }
if (false === claims.iss) {
if (opts.iss) { claims.iss = opts.iss; }
if (!claims.iss && (false === claims.iss || false === opts.iss)) {
claims.iss = undefined; claims.iss = undefined;
} else if (!claims.iss) { } else if (!claims.iss) {
throw new Error("opts.claims.iss should be in the form of https://example.com/, a secure OIDC base url"); throw new Error("opts.claims.iss should be in the form of https://example.com/, a secure OIDC base url");
@ -86,7 +197,7 @@ Keypairs.signJws = function (opts) {
if (!opts.jwk) { if (!opts.jwk) {
throw new Error("opts.jwk must exist and must declare 'typ'"); throw new Error("opts.jwk must exist and must declare 'typ'");
} }
return ('RSA' === opts.jwk.typ) ? "RS256" : "ES256"; return ('RSA' === opts.jwk.kty) ? "RS256" : "ES256";
} }
function sign(pem) { function sign(pem) {
@ -118,13 +229,21 @@ Keypairs.signJws = function (opts) {
} }
// node specifies RSA-SHAxxx even whet it's actually ecdsa (it's all encoded x509 shasums anyway) // node specifies RSA-SHAxxx even whet it's actually ecdsa (it's all encoded x509 shasums anyway)
var nodeAlg = "RSA-SHA" + (((protect||header).alg||'').replace(/^[^\d]+/, '')||'256'); var nodeAlg = "SHA" + (((protect||header).alg||'').replace(/^[^\d]+/, '')||'256');
var protected64 = Enc.strToUrlBase64(protectedHeader); var protected64 = Enc.strToUrlBase64(protectedHeader);
var payload64 = Enc.bufToUrlBase64(payload); var payload64 = Enc.bufToUrlBase64(payload);
var sig = require('crypto') var binsig = require('crypto')
.createSign(nodeAlg) .createSign(nodeAlg)
.update(protect ? (protected64 + "." + payload64) : payload64) .update(protect ? (protected64 + "." + payload64) : payload64)
.sign(pem, 'base64') .sign(pem)
;
if ('EC' === opts.jwk.kty) {
// ECDSA JWT signatures differ from "normal" ECDSA signatures
// https://tools.ietf.org/html/rfc7518#section-3.4
binsig = ecdsaAsn1SigToJoseSig(binsig);
}
var sig = binsig.toString('base64')
.replace(/\+/g, '-') .replace(/\+/g, '-')
.replace(/\//g, '_') .replace(/\//g, '_')
.replace(/=/g, '') .replace(/=/g, '')
@ -138,6 +257,41 @@ Keypairs.signJws = function (opts) {
}; };
} }
function ecdsaAsn1SigToJoseSig(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')]);
}
if (opts.pem && opts.jwk) { if (opts.pem && opts.jwk) {
return sign(opts.pem); return sign(opts.pem);
} else { } else {
@ -146,6 +300,36 @@ Keypairs.signJws = function (opts) {
}); });
}; };
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);
}
Enc.strToUrlBase64 = function (str) { Enc.strToUrlBase64 = function (str) {
// node automatically can tell the difference // node automatically can tell the difference
// between uc2 (utf-8) strings and binary strings // between uc2 (utf-8) strings and binary strings
@ -158,3 +342,25 @@ Enc.bufToUrlBase64 = function (buf) {
return Buffer.from(buf).toString('base64') return Buffer.from(buf).toString('base64')
.replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, ''); .replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, '');
}; };
// For 'rsa-compat' module only
// PLEASE do not use these sync methods, they are deprecated
Keypairs._importSync = function (opts) {
try {
return Eckles.importSync(opts);
} catch(e) {
try {
return Rasha.importSync(opts);
} catch(e) {
console.error("options.pem does not appear to be a valid RSA or ECDSA public or private key");
}
}
};
// PLEASE do not use these, they are deprecated
Keypairs._exportSync = function (opts) {
if ('RSA' === opts.jwk.kty) {
return Rasha.exportSync(opts);
} else {
return Eckles.exportSync(opts);
}
};

14
package-lock.json generated
View File

@ -1,18 +1,18 @@
{ {
"name": "keypairs", "name": "keypairs",
"version": "1.0.1", "version": "1.2.0",
"lockfileVersion": 1, "lockfileVersion": 1,
"requires": true, "requires": true,
"dependencies": { "dependencies": {
"eckles": { "eckles": {
"version": "1.4.0", "version": "1.4.1",
"resolved": "https://registry.npmjs.org/eckles/-/eckles-1.4.0.tgz", "resolved": "https://registry.npmjs.org/eckles/-/eckles-1.4.1.tgz",
"integrity": "sha512-Bm5dpwhsBuoCHvKCY3gAvP8XFyXH7im8uAu3szykpVNbFBdC+lOuV8vLC8fvTYRZBfFqB+k/P6ud/ZPVO2V2tA==" "integrity": "sha512-auWyk/k8oSkVHaD4RxkPadKsLUcIwKgr/h8F7UZEueFDBO7BsE4y+H6IMUDbfqKIFPg/9MxV6KcBdJCmVVcxSA=="
}, },
"rasha": { "rasha": {
"version": "1.2.1", "version": "1.2.4",
"resolved": "https://registry.npmjs.org/rasha/-/rasha-1.2.1.tgz", "resolved": "https://registry.npmjs.org/rasha/-/rasha-1.2.4.tgz",
"integrity": "sha512-cs4Hu/rVF3/Qucq+V7lxSz449VfHNMVXJaeajAHno9H5FC1PWlmS4NM6IAX5jPKFF0IC2rOdHdf7iNxQuIWZag==" "integrity": "sha512-GsIwKv+hYSumJyK9wkTDaERLwvWaGYh1WuI7JMTBISfYt13TkKFU/HFzlY4n72p8VfXZRUYm0AqaYhkZVxOC3Q=="
} }
} }
} }

View File

@ -1,11 +1,16 @@
{ {
"name": "keypairs", "name": "keypairs",
"version": "1.0.1", "version": "1.2.14",
"description": "Lightweight RSA/ECDSA keypair generation and JWK <-> PEM", "description": "Lightweight RSA/ECDSA keypair generation and JWK <-> PEM using node's native RSA and ECDSA support",
"main": "keypairs.js", "main": "keypairs.js",
"files": [], "files": [
"bin/keypairs.js"
],
"scripts": { "scripts": {
"test": "echo \"Error: no test specified\" && exit 1" "test": "node test.js"
},
"bin": {
"keypairs-install": "bin/keypairs.js"
}, },
"repository": { "repository": {
"type": "git", "type": "git",
@ -16,12 +21,16 @@
"RSA", "RSA",
"ECDSA", "ECDSA",
"PEM", "PEM",
"JWK" "JWK",
"keypair",
"crypto",
"sign",
"verify"
], ],
"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": {
"eckles": "^1.4.0", "eckles": "^1.4.1",
"rasha": "^1.2.1" "rasha": "^1.2.4"
} }
} }

121
test.js Normal file
View File

@ -0,0 +1,121 @@
var Keypairs = require('./');
/* global Promise*/
console.info("This SHOULD result in an error message:");
Keypairs.parseOrGenerate({ key: '' }).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')");
}
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, alg: 'ES512', 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.info("If a warning prints right above this, it's a pass");
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);
});