Compare commits
33 Commits
Author | SHA1 | Date | |
---|---|---|---|
b22f957124 | |||
48bee9204d | |||
5dc3795a17 | |||
3c84a7e1bd | |||
65db78a3c5 | |||
e04557c84a | |||
083cc6d73e | |||
885a00c3ae | |||
34d0ca9f13 | |||
ad312edfa6 | |||
a5236f6c2f | |||
286606720d | |||
f1b4ccf792 | |||
4ab4cbd2ab | |||
4ac6e7f8dd | |||
7099943db7 | |||
738be9b656 | |||
1a0e92eb52 | |||
265977e2c0 | |||
1d2a4d7b9e | |||
d0157d3270 | |||
76b4528e19 | |||
c228d73bd0 | |||
17021fa2cb | |||
2babbeb1dc | |||
b9b403ef4f | |||
1c1b9d00a9 | |||
665a2d2f8e | |||
6b3f1cb6a1 | |||
bf86aa8964 | |||
0d4db1bb19 | |||
c71f28e1d7 | |||
d928c2bdc0 |
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
||||
node_modules
|
341
README.md
341
README.md
@ -1,101 +1,252 @@
|
||||
This is being ported from code from rsa-compat.js, greenlock.html (bacme.js), and others.
|
||||
# Keypairs.js
|
||||
|
||||
This is my project for the weekend. I expect to be finished today (Monday Nov 12th, 2018)
|
||||
* 2018-10-10 (Saturday) work has begun
|
||||
* 2018-10-11 (Sunday) W00T! got a CSR generated for RSA with VanillaJS ArrayBuffer
|
||||
* 2018-10-12 (Monday) Figuring out ECDSA CSRs right now
|
||||
Lightweight JavaScript RSA and ECDSA utils that work on Windows, Mac, and Linux
|
||||
using modern node.js APIs (no need for C compiler).
|
||||
|
||||
A thin wrapper around [Eckles.js (ECDSA)](https://git.coolaj86.com/coolaj86/eckles.js/)
|
||||
and [Rasha.js (RSA)](https://git.coolaj86.com/coolaj86/rasha.js/).
|
||||
|
||||
# Features
|
||||
|
||||
* [x] Generate keypairs
|
||||
* [x] RSA
|
||||
* [x] ECDSA (P-256, P-384)
|
||||
* [x] PEM-to-JWK (and SSH-to-JWK)
|
||||
* [x] JWK-to-PEM (and JWK-to-SSH)
|
||||
* [x] Create JWTs (and sign JWS)
|
||||
* [x] SHA256 JWK Thumbprints
|
||||
* [ ] JWK fetching. See [Keyfetch.js](https://npmjs.com/packages/keyfetch/)
|
||||
* [ ] OIDC
|
||||
* [ ] Auth0
|
||||
* [ ] CLI
|
||||
* See [keypairs-cli](https://npmjs.com/packages/keypairs-cli/)
|
||||
|
||||
<!--
|
||||
Keypairs™ for node.js
|
||||
===========================
|
||||
|
||||
JavaScript RSA and ECDSA utils that work on Windows, Mac, and Linux with or without C compiler.
|
||||
|
||||
There are many different RSA and ECDSA libraries for node and it seems like they're
|
||||
all incompatible in different ways. This isn't [yet another library](https://xkcd.com/927/),
|
||||
but rather [one to rule them all and bind them](https://en.wikipedia.org/wiki/One_Ring).
|
||||
|
||||
Features
|
||||
========
|
||||
|
||||
* [x] RSA
|
||||
* [ ] ECDSA (in-progress)
|
||||
* [x] generate keypair
|
||||
* [x] export to JWK
|
||||
* [x] import from JWK
|
||||
* [x] export to PEM
|
||||
* [x] import from PEM
|
||||
* [x] sign JWS
|
||||
* [x] generate CSR (DER as PEM or base64url)
|
||||
|
||||
API
|
||||
===
|
||||
|
||||
* `Keypairs.generate(options)`
|
||||
* options example `{ type: 'RSA' || 'ECDSA', bitlength: 2048 || 256 }`
|
||||
* `Keypairs.import(options)`
|
||||
* options example `{ pem: '...', crv: 'P-256' || 'ECC', bitlength: 2048 || 256 }`
|
||||
* `Keypairs.export(options)`
|
||||
* options example `{ private: true || false, pem: true || false }`
|
||||
* `Keypairs.jws.sign(options)`
|
||||
* options example `{ keypair, header, protected, payload }`
|
||||
* `Keypairs.csr.generate(options)`
|
||||
* options example `{ keypair, [ 'example.com' ] }`
|
||||
|
||||
`keypair` can be any object with
|
||||
any of these keys `publicKeyPem, privateKeyPem, publicKeyJwk, privateKeyJwk`.
|
||||
|
||||
Examples
|
||||
========
|
||||
|
||||
These are quick examples of how to use the library.
|
||||
If you have a specific question, please open an issue.
|
||||
|
||||
Keypairs.generate(options)
|
||||
-------------------
|
||||
|
||||
Simple RSA
|
||||
|
||||
```js
|
||||
return Keypairs.generate({
|
||||
type: 'RSA'
|
||||
, bitlength: 2048
|
||||
}).then(function (keypair) {
|
||||
|
||||
// we won't bother describing this object
|
||||
// because it's only useful once exported
|
||||
|
||||
});
|
||||
```
|
||||
|
||||
Advanced RSA
|
||||
|
||||
```js
|
||||
return Keypairs.generate({
|
||||
type: 'RSA'
|
||||
, bitlength: 2048 // or 4096
|
||||
, exponent: 65537 // don't change this
|
||||
, public: true // pre-cache public key
|
||||
, pem: true // pre-export the PEM
|
||||
, internal: true // pre-cache internal representations
|
||||
}).then(function (keypair) {
|
||||
|
||||
// we won't bother describing this object
|
||||
// because it's only useful once exported
|
||||
|
||||
});
|
||||
```
|
||||
|
||||
Keypairs.export(options)
|
||||
-------------------
|
||||
|
||||
Keypairs.import(options)
|
||||
-------------------
|
||||
|
||||
Keypairs.jws.sign(options)
|
||||
-------------------
|
||||
|
||||
Keypairs.csr.generate(options)
|
||||
-------------------
|
||||
* [ ] generate CSR (DER as PEM or base64url)
|
||||
|
||||
-->
|
||||
|
||||
# Usage
|
||||
|
||||
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) {
|
||||
console.log(pair.private);
|
||||
console.log(pair.public);
|
||||
});
|
||||
```
|
||||
|
||||
```
|
||||
// JWK to PEM
|
||||
// (supports various 'format' and 'encoding' options)
|
||||
return Keypairs.export({ jwk: pair.private, format: 'pkcs8' }).then(function (pem) {
|
||||
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'
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
By default ECDSA keys will be used since they've had native support in node
|
||||
_much_ longer than RSA has, and they're smaller, and faster to generate.
|
||||
|
||||
## 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)
|
||||
|
||||
Generates a public/private pair of JWKs as `{ private, public }`
|
||||
|
||||
Option examples:
|
||||
|
||||
* RSA `{ kty: 'RSA', modulusLength: 2048 }`
|
||||
* ECDSA `{ kty: 'ECDSA', namedCurve: 'P-256' }`
|
||||
|
||||
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: '...' }
|
||||
|
||||
Takes a PEM in pretty much any format (PKCS1, SEC1, PKCS8, SPKI) and returns a JWK.
|
||||
|
||||
#### Keypairs.export(options)
|
||||
|
||||
Exports a JWK as a PEM.
|
||||
|
||||
Exports PEM in PKCS8 (private) or SPKI (public) by default.
|
||||
|
||||
Options
|
||||
|
||||
```js
|
||||
{ jwk: jwk
|
||||
, public: true
|
||||
, encoding: 'pem' // or 'der'
|
||||
, format: 'pkcs8' // or 'ssh', 'pkcs1', 'sec1', 'spki'
|
||||
}
|
||||
```
|
||||
|
||||
#### 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 })
|
||||
|
||||
Promises a JWK-spec thumbprint: URL Base64-encoded sha256
|
||||
|
||||
#### Keypairs.signJwt({ jwk, header, claims })
|
||||
|
||||
Returns a JWT (otherwise known as a protected JWS in "compressed" format).
|
||||
|
||||
```js
|
||||
{ jwk: jwk
|
||||
// required claims
|
||||
, iss: 'https://example.com'
|
||||
, exp: '15m'
|
||||
// all optional claims
|
||||
, claims: {
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Exp may be human readable duration (i.e. 1h, 15m, 30s) or a datetime in seconds.
|
||||
|
||||
Header defaults:
|
||||
|
||||
```js
|
||||
{ kid: thumbprint
|
||||
, alg: 'xS256'
|
||||
, typ: 'JWT'
|
||||
}
|
||||
```
|
||||
|
||||
Payload notes:
|
||||
|
||||
* `iat: now` is added by default (set `false` to disable)
|
||||
* `exp` must be set (set `false` to disable)
|
||||
* `iss` should be the base URL for JWK lookup (i.e. via OIDC, Auth0)
|
||||
|
||||
Notes:
|
||||
|
||||
`header` is actually the JWS `protected` value, as all JWTs use protected headers (yay!)
|
||||
and `claims` are really the JWS `payload`.
|
||||
|
||||
#### Keypairs.signJws({ jwk, header, protected, payload })
|
||||
|
||||
This is provided for APIs like ACME (Let's Encrypt) that use uncompressed JWS (instead of JWT, which is compressed).
|
||||
|
||||
Options:
|
||||
|
||||
* `header` not what you think. Leave undefined unless you need this for the spec you're following.
|
||||
* `protected` is the typical JWT-style header
|
||||
* `kid` and `alg` will be added by default (these are almost always required), set `false` explicitly to disable
|
||||
* `payload` can be JSON, a string, or even a buffer (which gets URL Base64 encoded)
|
||||
* you must set this to something, even if it's an empty string, object, or Buffer
|
||||
|
||||
# Additional Documentation
|
||||
|
||||
Keypairs.js provides a 1-to-1 mapping to the Rasha.js and Eckles.js APIs for the following:
|
||||
|
||||
* generate(options)
|
||||
* 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 RSA documentation at [Rasha.js](https://git.coolaj86.com/coolaj86/rasha.js/)
|
||||
|
||||
|
12
bin/keypairs.js
Executable file
12
bin/keypairs.js
Executable 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!");
|
||||
});
|
@ -1,16 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
var fs = require('fs');
|
||||
var path = require('path');
|
||||
|
||||
function convert(name) {
|
||||
var ext = path.extname(name);
|
||||
var csr = fs.readFileSync(name, 'ascii').split(/\n/).filter(function (line) {
|
||||
return !/---/.test(line);
|
||||
}).join('');
|
||||
console.log(csr);
|
||||
var der = Buffer.from(csr, 'base64');
|
||||
fs.writeFileSync(name.replace(new RegExp('\\' + ext + '$'), '') + '.der', der);
|
||||
}
|
||||
|
||||
convert(process.argv[2]);
|
33
example.js
Normal file
33
example.js
Normal file
@ -0,0 +1,33 @@
|
||||
'use strict';
|
||||
|
||||
var Keypairs = require('./keypairs.js');
|
||||
var Keyfetch = require('keyfetch');
|
||||
|
||||
Keypairs.generate().then(function (keypair) {
|
||||
return Keypairs.thumbprint({ jwk: keypair.public }).then(function (thumb) {
|
||||
var iss = 'https://coolaj86.com/';
|
||||
|
||||
// shim so that no http request is necessary
|
||||
keypair.private.kid = thumb;
|
||||
Keyfetch._setCache(iss, { thumbprint: thumb, jwk: keypair.private });
|
||||
|
||||
return Keypairs.signJwt({
|
||||
jwk: keypair.private
|
||||
, claims: {
|
||||
iss: iss
|
||||
, sub: 'coolaj86@gmail.com'
|
||||
, exp: Math.round(Date.now()/1000) + (3 * 24 * 60 * 60)
|
||||
}
|
||||
});
|
||||
});
|
||||
}).then(function (jwt) {
|
||||
console.log(jwt);
|
||||
return Keyfetch.verify({ jwt: jwt }).then(function (ok) {
|
||||
if (!ok) {
|
||||
throw new Error("SANITY: did not verify (should have failed)");
|
||||
}
|
||||
console.log("Verified token");
|
||||
});
|
||||
}).catch(function (err) {
|
||||
console.error(err);
|
||||
});
|
134
index.js
134
index.js
@ -1,134 +0,0 @@
|
||||
;(function (exports) {
|
||||
'use strict';
|
||||
|
||||
var PromiseA;
|
||||
try {
|
||||
/*global Promise*/
|
||||
PromiseA = Promise;
|
||||
} catch(e) {
|
||||
PromiseA = require('bluebird');
|
||||
}
|
||||
|
||||
|
||||
// https://stackoverflow.com/questions/40314257/export-webcrypto-key-to-pem-format
|
||||
function derToPem(keydata, pemName, privacy){
|
||||
var keydataS = arrayBufferToString(keydata);
|
||||
var keydataB64 = window.btoa(keydataS);
|
||||
var keydataB64Pem = formatAsPem(keydataB64, pemName, privacy);
|
||||
return keydataB64Pem;
|
||||
}
|
||||
|
||||
function arrayBufferToString( buffer ) {
|
||||
var binary = [];
|
||||
var bytes = new Uint8Array( buffer );
|
||||
var len = bytes.byteLength;
|
||||
for (var i = 0; i < len; i++) {
|
||||
binary.push(String.fromCharCode( bytes[ i ] ));
|
||||
}
|
||||
return binary.join('');
|
||||
}
|
||||
|
||||
|
||||
function formatAsPem(str, pemName, privacy) {
|
||||
var privstr = (privacy ? privacy + ' ' : '');
|
||||
var finalString = '-----BEGIN ' + pemName + ' ' + privstr + 'KEY-----\n';
|
||||
|
||||
while (str.length > 0) {
|
||||
finalString += str.substring(0, 64) + '\n';
|
||||
str = str.substring(64);
|
||||
}
|
||||
|
||||
finalString = finalString + '-----END ' + pemName + ' ' + privstr + 'KEY-----';
|
||||
|
||||
return finalString;
|
||||
}
|
||||
|
||||
var Keypairs = exports.Keypairs = {
|
||||
generate: function(opts) {
|
||||
if (!opts) { opts = {}; }
|
||||
if (!opts.type) { opts.type = 'EC'; }
|
||||
|
||||
var supported = [ 'EC', 'RSA' ];
|
||||
if (-1 === supported.indexOf(opts.type)) {
|
||||
return PromiseA.reject(new Error("'" + opts.type + "' not implemented. Try one of " + supported.join(', ')));
|
||||
}
|
||||
|
||||
if ('EC' === opts.type) {
|
||||
return Keypairs._generateEc(opts);
|
||||
}
|
||||
if ('RSA' === opts.type) {
|
||||
return Keypairs._generateRsa(opts);
|
||||
}
|
||||
}
|
||||
, _generateEc: function (opts) {
|
||||
if (!opts.namedCurve) { opts.namedCurve = 'P-256'; }
|
||||
if ('P-256' !== opts.namedCurve) {
|
||||
console.warn("'" + opts.namedCurve + "' is not supported, but it _might_ happen to work anyway.");
|
||||
}
|
||||
|
||||
// https://github.com/diafygi/webcrypto-examples#ecdsa---generatekey
|
||||
var extractable = true;
|
||||
|
||||
return window.crypto.subtle.generateKey(
|
||||
{ name: "ECDSA", namedCurve: opts.namedCurve }
|
||||
, extractable
|
||||
, [ 'sign', 'verify' ]
|
||||
).then(function (result) {
|
||||
return window.crypto.subtle.exportKey(
|
||||
"jwk"
|
||||
, result.privateKey
|
||||
).then(function (jwk) {
|
||||
return window.crypto.subtle.exportKey(
|
||||
"pkcs8"
|
||||
, result.privateKey
|
||||
).then(function (keydata) {
|
||||
return {
|
||||
type: 'EC'
|
||||
, privateJwk: jwk
|
||||
, privatePem: derToPem(keydata, 'EC', 'PRIVATE')
|
||||
};
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
, _generateRsa: function (opts) {
|
||||
if (!opts.bitlength) { opts.bitlength = 2048; }
|
||||
if (-1 === [ 2048, 4096 ].indexOf(opts.bitlength)) {
|
||||
return PromiseA.reject("opts.bitlength = (" + typeof opts.bitlength + ") " + opts.bitlength + ": Are you serious?");
|
||||
}
|
||||
|
||||
// https://github.com/diafygi/webcrypto-examples#rsa---generatekey
|
||||
var extractable = true;
|
||||
|
||||
return window.crypto.subtle.generateKey(
|
||||
{ name: "RSASSA-PKCS1-v1_5"
|
||||
, modulusLength: opts.bitlength
|
||||
, publicExponent: new Uint8Array([0x01, 0x00, 0x01])
|
||||
, hash: { name: "SHA-256" }
|
||||
}
|
||||
, extractable
|
||||
, [ 'sign', 'verify' ]
|
||||
).then(function (result) {
|
||||
return window.crypto.subtle.exportKey(
|
||||
"jwk"
|
||||
, result.privateKey
|
||||
).then(function (jwk) {
|
||||
return window.crypto.subtle.exportKey(
|
||||
"pkcs8"
|
||||
, result.privateKey
|
||||
).then(function (keydata) {
|
||||
return {
|
||||
type: 'RSA'
|
||||
, privateJwk: jwk
|
||||
, privatePem: derToPem(keydata, 'RSA', 'PRIVATE')
|
||||
};
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
}('undefined' === typeof module ? window : module.exports));
|
||||
|
||||
// How we might use this
|
||||
// var Keypairs = require('keypairs').Keypairs
|
366
keypairs.js
Normal file
366
keypairs.js
Normal file
@ -0,0 +1,366 @@
|
||||
'use strict';
|
||||
|
||||
var Eckles = require('eckles');
|
||||
var Rasha = require('rasha');
|
||||
var Enc = {};
|
||||
var Keypairs = module.exports;
|
||||
|
||||
/*global Promise*/
|
||||
|
||||
Keypairs.generate = function (opts) {
|
||||
opts = opts || {};
|
||||
var kty = opts.kty || opts.type;
|
||||
var p;
|
||||
if ('RSA' === kty) {
|
||||
p = Rasha.generate(opts);
|
||||
} else {
|
||||
p = 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) {
|
||||
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 Promise.resolve().then(function () {
|
||||
if ('RSA' === opts.jwk.kty) {
|
||||
return Rasha.export(opts);
|
||||
} else {
|
||||
return Eckles.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.
|
||||
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) {
|
||||
return Promise.resolve().then(function () {
|
||||
if ('RSA' === opts.jwk.kty) {
|
||||
return Rasha.thumbprint(opts);
|
||||
} else {
|
||||
return Eckles.thumbprint(opts);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// 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) { 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'");
|
||||
}
|
||||
return ('RSA' === opts.jwk.kty) ? "RS256" : "ES256";
|
||||
}
|
||||
|
||||
function sign(pem) {
|
||||
var header = opts.header;
|
||||
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 Let's Encrypt explicitly doesn't use a kid
|
||||
if (!protect.kid && false !== protect.kid) { protect.kid = thumb; }
|
||||
protectedHeader = JSON.stringify(protect);
|
||||
}
|
||||
|
||||
// Convert payload to Buffer
|
||||
if ('string' !== typeof payload && !Buffer.isBuffer(payload)) {
|
||||
if (!payload) {
|
||||
throw new Error("opts.payload should be JSON, string, or Buffer (it may be empty, but that must be explicit)");
|
||||
}
|
||||
payload = JSON.stringify(payload);
|
||||
}
|
||||
if ('string' === typeof payload) {
|
||||
payload = Buffer.from(payload, 'binary');
|
||||
}
|
||||
|
||||
// node specifies RSA-SHAxxx even whet it's actually ecdsa (it's all encoded x509 shasums anyway)
|
||||
var nodeAlg = "SHA" + (((protect||header).alg||'').replace(/^[^\d]+/, '')||'256');
|
||||
var protected64 = Enc.strToUrlBase64(protectedHeader);
|
||||
var payload64 = Enc.bufToUrlBase64(payload);
|
||||
var binsig = require('crypto')
|
||||
.createSign(nodeAlg)
|
||||
.update(protect ? (protected64 + "." + payload64) : payload64)
|
||||
.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, '')
|
||||
;
|
||||
|
||||
return {
|
||||
header: header
|
||||
, protected: protected64 || undefined
|
||||
, payload: payload64
|
||||
, signature: sig
|
||||
};
|
||||
}
|
||||
|
||||
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) {
|
||||
return sign(opts.pem);
|
||||
} else {
|
||||
return Keypairs.export({ jwk: opts.jwk }).then(sign);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
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) {
|
||||
// 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')
|
||||
.replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, '');
|
||||
};
|
||||
Enc.bufToUrlBase64 = function (buf) {
|
||||
// allow for Uint8Array as a Buffer
|
||||
return Buffer.from(buf).toString('base64')
|
||||
.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);
|
||||
}
|
||||
};
|
18
package-lock.json
generated
Normal file
18
package-lock.json
generated
Normal file
@ -0,0 +1,18 @@
|
||||
{
|
||||
"name": "keypairs",
|
||||
"version": "1.2.0",
|
||||
"lockfileVersion": 1,
|
||||
"requires": true,
|
||||
"dependencies": {
|
||||
"eckles": {
|
||||
"version": "1.4.1",
|
||||
"resolved": "https://registry.npmjs.org/eckles/-/eckles-1.4.1.tgz",
|
||||
"integrity": "sha512-auWyk/k8oSkVHaD4RxkPadKsLUcIwKgr/h8F7UZEueFDBO7BsE4y+H6IMUDbfqKIFPg/9MxV6KcBdJCmVVcxSA=="
|
||||
},
|
||||
"rasha": {
|
||||
"version": "1.2.4",
|
||||
"resolved": "https://registry.npmjs.org/rasha/-/rasha-1.2.4.tgz",
|
||||
"integrity": "sha512-GsIwKv+hYSumJyK9wkTDaERLwvWaGYh1WuI7JMTBISfYt13TkKFU/HFzlY4n72p8VfXZRUYm0AqaYhkZVxOC3Q=="
|
||||
}
|
||||
}
|
||||
}
|
26
package.json
26
package.json
@ -1,10 +1,16 @@
|
||||
{
|
||||
"name": "keypairs",
|
||||
"version": "0.0.2",
|
||||
"description": "Interchangeably use RSA & ECDSA with PEM and JWK for Signing, Verifying, CSR generation and JOSE. Ugh... that was a mouthful.",
|
||||
"main": "index.js",
|
||||
"version": "1.2.14",
|
||||
"description": "Lightweight RSA/ECDSA keypair generation and JWK <-> PEM using node's native RSA and ECDSA support",
|
||||
"main": "keypairs.js",
|
||||
"files": [
|
||||
"bin/keypairs.js"
|
||||
],
|
||||
"scripts": {
|
||||
"test": "echo \"Error: no test specified\" && exit 1"
|
||||
"test": "node test.js"
|
||||
},
|
||||
"bin": {
|
||||
"keypairs-install": "bin/keypairs.js"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
@ -16,9 +22,15 @@
|
||||
"ECDSA",
|
||||
"PEM",
|
||||
"JWK",
|
||||
"CSR",
|
||||
"JOSE"
|
||||
"keypair",
|
||||
"crypto",
|
||||
"sign",
|
||||
"verify"
|
||||
],
|
||||
"author": "AJ ONeal <coolaj86@gmail.com> (https://coolaj86.com/)",
|
||||
"license": "(MIT OR Apache-2.0)"
|
||||
"license": "MPL-2.0",
|
||||
"dependencies": {
|
||||
"eckles": "^1.4.1",
|
||||
"rasha": "^1.2.4"
|
||||
}
|
||||
}
|
||||
|
@ -1,23 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
var pubkey = require('./pubkey.js');
|
||||
var pubname = process.argv[2];
|
||||
var fs = require('fs');
|
||||
var pem = fs.readFileSync(pubname);
|
||||
|
||||
var key = pubkey.parsePem(pem);
|
||||
if ('RSA' !== key.typ) {
|
||||
throw new Error(key.typ + " not supported");
|
||||
}
|
||||
if (key.pub) {
|
||||
var pubbuf = pubkey.readPubkey(key.der);
|
||||
} else {
|
||||
var pubbuf = pubkey.readPrivkey(key.der);
|
||||
}
|
||||
|
||||
console.log(pubbuf.byteLength, pubkey.toHex(pubbuf));
|
||||
var der = pubkey.toRsaPub(pubbuf);
|
||||
var b64 = pubkey.toBase64(der);
|
||||
var pem = pubkey.formatAsPublicPem(b64);
|
||||
console.log('Pub:\n');
|
||||
console.log(pem);
|
181
pubkey.js
181
pubkey.js
@ -1,181 +0,0 @@
|
||||
(function (exports) {
|
||||
'use strict';
|
||||
|
||||
// 30 sequence
|
||||
// 03 bit string
|
||||
// 05 null
|
||||
// 06 object id
|
||||
|
||||
// 00 00 00 00
|
||||
// 30 82 01 22 30 0D 06 09 2A 86 48 86 F7 0D 01 01 01 05 00 03 82 01 0F 00 30 82 01 0A 02 82 01 01
|
||||
// 00 ... 02 03 01 00 01
|
||||
// 30 82 02 22 30 0D 06 09 2A 86 48 86 F7 0D 01 01 01 05 00 03 82 02 0F 00 30 82 02 0A 02 82 02 01
|
||||
// 00 ... 02 03 01 00 01
|
||||
|
||||
function parsePem(pem) {
|
||||
var typ;
|
||||
var pub;
|
||||
var der = fromBase64(pem.toString('ascii').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';
|
||||
} else if (/ EC/.test(line)) {
|
||||
typ = 'EC';
|
||||
}
|
||||
}
|
||||
return !/---/.test(line);
|
||||
}).join(''));
|
||||
|
||||
if (!typ) {
|
||||
if (pub) {
|
||||
// This is the RSA object ID
|
||||
if ('06092A864886F70D010101'.toLowerCase() === der.slice(6, 6 + 11).toString('hex')) {
|
||||
typ = 'RSA';
|
||||
}
|
||||
} else {
|
||||
// TODO
|
||||
}
|
||||
}
|
||||
|
||||
return { typ: typ, pub: pub, der: der };
|
||||
}
|
||||
|
||||
function toHex(ab) {
|
||||
var hex = [];
|
||||
var u8 = new Uint8Array(ab);
|
||||
var size = u8.byteLength;
|
||||
var i;
|
||||
var h;
|
||||
for (i = 0; i < size; i += 1) {
|
||||
h = u8[i].toString(16);
|
||||
if (2 === h.length) {
|
||||
hex.push(h);
|
||||
} else {
|
||||
hex.push('0' + h);
|
||||
}
|
||||
}
|
||||
return hex.join('');
|
||||
}
|
||||
|
||||
function readPubkey(der) {
|
||||
var offset = 28 + 5; // header plus size
|
||||
var ksBytes = der.slice(30, 32);
|
||||
// not sure why it shows 257 instead of 256
|
||||
var keysize = new DataView(ksBytes).getUint16(0, false) - 1;
|
||||
var pub = der.slice(offset, offset + keysize);
|
||||
return pub;
|
||||
}
|
||||
|
||||
function readPrivkey(der) {
|
||||
var offset = 7 + 5; // header plus size
|
||||
var ksBytes = der.slice(9, 11);
|
||||
// not sure why it shows 257 instead of 256
|
||||
var keysize = new DataView(ksBytes).getUint16(0, false) - 1;
|
||||
var pub = der.slice(offset, offset + keysize);
|
||||
return pub;
|
||||
}
|
||||
|
||||
|
||||
// I used OpenSSL to create RSA keys with sizes 2048 and 4096.
|
||||
// Then I used https://lapo.it/asn1js/ to see which bits changed.
|
||||
// And I created a template from the bits that do and don't.
|
||||
// No ASN.1 and X.509 parsers or generators. Yay!
|
||||
var rsaAsn1Head = (
|
||||
'30 82 xx 22 30 0D 06 09'
|
||||
+ '2A 86 48 86 F7 0D 01 01'
|
||||
+ '01 05 00 03 82 xx 0F 00'
|
||||
+ '30 82 xx 0A 02 82 xx 01'
|
||||
+ '00').replace(/\s+/g, '');
|
||||
var rsaAsn1Foot = ('02 03 01 00 01').replace(/\s+/g, '');
|
||||
function toRsaPub(pub) {
|
||||
// 256 // 2048-bit
|
||||
var len = '0' + (pub.byteLength / 256);
|
||||
var head = rsaAsn1Head.replace(/xx/g, len);
|
||||
var headSize = (rsaAsn1Head.length / 2);
|
||||
var foot = rsaAsn1Foot;
|
||||
var footSize = (foot.length / 2);
|
||||
|
||||
var size = headSize + pub.byteLength + footSize;
|
||||
var der = new Uint8Array(new ArrayBuffer(size));
|
||||
|
||||
var i, j;
|
||||
for (i = 0, j = 0; i < headSize; i += 1) {
|
||||
der[i] = parseInt(head.slice(j,j+2), 16);
|
||||
j += 2;
|
||||
}
|
||||
|
||||
pub = new Uint8Array(pub);
|
||||
for (i = 0; i < pub.byteLength; i += 1) {
|
||||
der[headSize + i] = pub[i];
|
||||
}
|
||||
|
||||
for (i = 0, j = 0; i < footSize; i += 1) {
|
||||
der[headSize + pub.byteLength + i] = parseInt(foot.slice(j,j+2), 16);
|
||||
j += 2;
|
||||
}
|
||||
|
||||
return der.buffer;
|
||||
}
|
||||
|
||||
function formatAsPem(str, privacy, pemName) {
|
||||
var pemstr = (pemName ? pemName + ' ' : '');
|
||||
var privstr = (privacy ? privacy + ' ' : '');
|
||||
var finalString = '-----BEGIN ' + pemstr + privstr + 'KEY-----\n';
|
||||
|
||||
while (str.length > 0) {
|
||||
finalString += str.substring(0, 64) + '\n';
|
||||
str = str.substring(64);
|
||||
}
|
||||
|
||||
finalString = finalString + '-----END ' + pemstr + privstr + 'KEY-----';
|
||||
|
||||
return finalString;
|
||||
}
|
||||
|
||||
function formatAsPublicPem(str) {
|
||||
return formatAsPem(str, 'PUBLIC', '');
|
||||
}
|
||||
|
||||
function toBase64(der) {
|
||||
if ('undefined' === typeof btoa) {
|
||||
return Buffer.from(der).toString('base64');
|
||||
}
|
||||
var chs = [];
|
||||
der = new Uint8Array(der);
|
||||
der.forEach(function (b) {
|
||||
chs.push(String.fromCharCode(b));
|
||||
});
|
||||
return btoa(chs.join(''));
|
||||
}
|
||||
|
||||
function fromBase64(b64) {
|
||||
var buf;
|
||||
var ab;
|
||||
if ('undefined' === typeof atob) {
|
||||
buf = Buffer.from(b64, 'base64');
|
||||
return buf.buffer.slice(buf.byteOffset, buf.byteOffset + buf.byteLength);
|
||||
}
|
||||
buf = atob(b64);
|
||||
ab = new ArrayBuffer(buf.length);
|
||||
ab = new Uint8Array(ab);
|
||||
buf.split('').forEach(function (ch, i) {
|
||||
ab[i] = ch.charCodeAt(0);
|
||||
});
|
||||
return ab.buffer;
|
||||
}
|
||||
|
||||
exports.parsePem = parsePem;
|
||||
exports.toBase64 = toBase64;
|
||||
exports.toRsaPub = toRsaPub;
|
||||
exports.formatAsPublicPem = formatAsPublicPem;
|
||||
exports.formatAsPem = formatAsPem;
|
||||
exports.readPubkey = readPubkey;
|
||||
exports.readPrivkey = readPrivkey;
|
||||
exports.toHex = toHex;
|
||||
|
||||
}('undefined' !== typeof module ? module.exports: window));
|
404
re-sign.js
404
re-sign.js
@ -1,404 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
var crypto = require('crypto');
|
||||
var fs = require('fs');
|
||||
var pubkey = require('./pubkey.js');
|
||||
|
||||
var keyname = process.argv[2];
|
||||
var dername = process.argv[3];
|
||||
|
||||
var keypem = fs.readFileSync(keyname);
|
||||
var csrFull = fs.readFileSync(dername);
|
||||
var csrFull = csrFull.buffer.slice(csrFull.byteOffset, csrFull.byteOffset + csrFull.byteLength);
|
||||
|
||||
// these are static ASN.1 segments
|
||||
// The head specifies that there will be 3 segments and a content length
|
||||
// (those segments will be content, signature header, and signature)
|
||||
var csrHead = '30 82 {0seq0len}'.replace(/\s+/g, '');
|
||||
// The tail specifies the RSA256 signature header (and is followed by the signature
|
||||
var csrRsaFoot =
|
||||
( '30 0D 06 09 2A 86 48 86 F7 0D 01 01 0B'
|
||||
+ '05 00'
|
||||
+ '03 82 01 01 00'
|
||||
).replace(/\s+/g, '');
|
||||
var csrDomains = '82 {dlen} {domain.tld}'; // 2+n bytes (type 82?)
|
||||
/*
|
||||
var csrRsaContent =
|
||||
'30 82 {1.1.0seqlen}' // 7+n 4 bytes, sequence
|
||||
+ '02 01 00' // 3 3 bytes, int 0
|
||||
|
||||
+ '30 {3.2.0seqlen}' // 13+n 2 bytes, sequence
|
||||
+ '31 {4.3.0setlen}' // 11+n 2 bytes, set
|
||||
+ '30 {5.4.0seqlen}' // 9+n 2 bytes, sequence
|
||||
+ '06 03 55 04 03' // 7+n 5 bytes, object id (commonName)
|
||||
+ '0C {dlen} {domain.tld}' // 2+n 2+n bytes, utf8string
|
||||
|
||||
+ '30 82 {8.2.2seqlen}' // 19 4 bytes, sequence
|
||||
+ '30 0D' // 15 2 bytes, sequence
|
||||
+ '06 09 2A 86 48 86 F7 0D 01 01 01' // 13 11 bytes, rsaEncryption (PKCS #1)
|
||||
+ '05 00' // 2 2 bytes, null (why?)
|
||||
+ '03 82 {12.3.0bslen} 00' // +18+m 5 bytes, bit string [01 0F]
|
||||
+ '30 82 {13.4.0seqlen}' // +13+m 4 bytes, sequence
|
||||
+ '02 82 {klen} 00 {key}' // +9+m 4+1+n bytes, int (RSA Pub Key)
|
||||
+ '02 03 {mod}' // +5 5 bytes, key and modules [01 00 01]
|
||||
|
||||
+ 'A0 {16.2.3ellen}' // 30+n 2 bytes, ?? [4B]
|
||||
+ '30 {17.3.9seqlen}' // 28+n 2 bytes, sequence
|
||||
+ '06 09 2A 86 48 86 F7 0D 01 09 0E' // 26+n 11 bytes, object id (extensionRequest (PKCS #9 via CRMF))
|
||||
+ '31 {19.5.0setlen}' // 15+n 2 bytes, set
|
||||
+ '30 {20.6.0seqlen}' // 13+n 2 bytes, sequence
|
||||
+ '30 {21.7.0seqlen}' // 11+n 2 bytes, sequence
|
||||
+ '06 03 55 1D 11' // 9+n 5 bytes, object id (subjectAltName (X.509 extension))
|
||||
+ '04 {23.8.0octlen}' // 4+n 2 bytes, octet string
|
||||
+ '30 {24.9.0seqlen}' // 2+n 2 bytes, sequence
|
||||
+ '{altnames}' // n n bytes
|
||||
;
|
||||
*/
|
||||
|
||||
function privateToPub(pem) {
|
||||
var pubbuf;
|
||||
var key = pubkey.parsePem(pem);
|
||||
if ('RSA' !== key.typ) {
|
||||
throw new Error(key.typ + " not supported");
|
||||
}
|
||||
if (key.pub) {
|
||||
pubbuf = pubkey.readPubkey(key.der);
|
||||
} else {
|
||||
pubbuf = pubkey.readPrivkey(key.der);
|
||||
}
|
||||
|
||||
//console.log(pubbuf.byteLength, pubkey.toHex(pubbuf));
|
||||
var der = pubkey.toRsaPub(pubbuf);
|
||||
var b64 = pubkey.toBase64(der);
|
||||
return pubkey.formatAsPublicPem(b64);
|
||||
}
|
||||
|
||||
function strToHex(str) {
|
||||
return str.split('').map(function (ch) {
|
||||
var h = ch.charCodeAt(0).toString(16);
|
||||
if (2 === h.length) {
|
||||
return h;
|
||||
}
|
||||
return '0' + h;
|
||||
}).join('');
|
||||
}
|
||||
|
||||
function pubToPem(pubbuf) {
|
||||
var der = pubkey.toRsaPub(pubbuf);
|
||||
var b64 = pubkey.toBase64(der);
|
||||
return pubkey.formatAsPublicPem(b64);
|
||||
}
|
||||
|
||||
var sigend = (csrFull.byteLength - (2048 / 8));
|
||||
var sig = csrFull.slice(sigend);
|
||||
|
||||
console.log();
|
||||
console.log();
|
||||
console.log('csr (' + csrFull.byteLength + ')');
|
||||
console.log(pubkey.toHex(csrFull));
|
||||
console.log();
|
||||
|
||||
// First 4 bytes define Segment, segment length, and content length
|
||||
console.log(sigend, csrRsaFoot, csrRsaFoot.length/2);
|
||||
var csrbody = csrFull.slice(4, sigend - (csrRsaFoot.length/2));
|
||||
console.log('csr body (' + csrbody.byteLength + ')');
|
||||
console.log(pubkey.toHex(csrbody));
|
||||
console.log();
|
||||
|
||||
var csrpub = csrFull.slice(63 + 5, 63 + 5 + 256);
|
||||
console.log('csr pub (' + csrpub.byteLength + ')');
|
||||
console.log(pubkey.toHex(csrpub));
|
||||
console.log();
|
||||
|
||||
console.log('sig (' + sig.byteLength + ')');
|
||||
console.log(pubkey.toHex(sig));
|
||||
console.log();
|
||||
|
||||
var csrpem = pubToPem(csrpub);
|
||||
console.log(csrpem);
|
||||
console.log();
|
||||
|
||||
var prvpem = privateToPub(keypem);
|
||||
console.log(prvpem);
|
||||
console.log();
|
||||
|
||||
if (csrpem === prvpem) {
|
||||
console.log("Public Keys Match");
|
||||
} else {
|
||||
throw new Error("public key read from keyfile doesn't match public key read from CSR");
|
||||
}
|
||||
|
||||
function h(d) {
|
||||
d = d.toString(16);
|
||||
if (d.length % 2) {
|
||||
return '0' + d;
|
||||
}
|
||||
return d;
|
||||
}
|
||||
|
||||
function fromHex(hex) {
|
||||
if ('undefined' !== typeof Buffer) {
|
||||
return Buffer.from(hex, 'hex');
|
||||
}
|
||||
var ab = new ArrayBuffer(hex.length/2);
|
||||
var i;
|
||||
var j;
|
||||
ab = new Uint8Array(ab);
|
||||
for (i = 0, j = 0; i < (hex.length/2); i += 1) {
|
||||
ab[i] = parseInt(hex.slice(j, j+1), 16);
|
||||
j += 2;
|
||||
}
|
||||
return ab.buffer;
|
||||
}
|
||||
|
||||
function createCsrBodyRsa(domains, csrpub) {
|
||||
var altnames = domains.map(function (d) {
|
||||
return csrDomains.replace(/{dlen}/, h(d.length)).replace(/{domain\.tld}/, strToHex(d));
|
||||
}).join('').replace(/\s+/g, '');
|
||||
var publen = csrpub.byteLength;
|
||||
var sublen = domains[0].length;
|
||||
var sanlen = (altnames.length/2);
|
||||
|
||||
var body = [ '30 82 {1.1.0seqlen}' // 4 bytes, sequence
|
||||
.replace(/{[^}]+}/, h(
|
||||
3
|
||||
+ 13 + sublen
|
||||
+ 38 + publen
|
||||
+ 30 + sanlen
|
||||
))
|
||||
|
||||
// #0 Total 3
|
||||
, '02 01 00' // 3 bytes, int 0
|
||||
|
||||
// #1 Total 2+11+n
|
||||
, '30 {3.2.0seqlen}' // 2 bytes, sequence
|
||||
.replace(/{[^}]+}/, h(2+2+5+2+sublen))
|
||||
, '31 {4.3.0setlen}' // 2 bytes, set
|
||||
.replace(/{[^}]+}/, h(2+5+2+sublen))
|
||||
, '30 {5.4.0seqlen}' // 2 bytes, sequence
|
||||
.replace(/{[^}]+}/, h(5+2+sublen))
|
||||
, '06 03 55 04 03' // 5 bytes, object id (commonName)
|
||||
, '0C {dlen} {domain.tld}' // 2+n bytes, utf8string
|
||||
.replace(/{dlen}/, h(sublen))
|
||||
.replace(/{domain\.tld}/, strToHex(domains[0]))
|
||||
|
||||
// #2 Total 4+28+n+1+5
|
||||
, '30 82 {8.2.2seqlen}' // 4 bytes, sequence
|
||||
.replace(/{[^}]+}/, h(2+11+2+4+1+4+4+publen+1+5))
|
||||
, '30 0D' // 2 bytes, sequence
|
||||
, '06 09 2A 86 48 86 F7 0D 01 01 01' // 11 bytes, rsaEncryption (PKCS #1)
|
||||
, '05 00' // 2 bytes, null (why?)
|
||||
, '03 82 {12.3.0bslen} 00' // 4+1 bytes, bit string [01 0F]
|
||||
.replace(/{[^}]+}/, h(1+4+4+publen+1+5))
|
||||
, '30 82 {13.4.0seqlen}' // 4 bytes, sequence
|
||||
.replace(/{[^}]+}/, h(4+publen+1+5))
|
||||
, '02 82 {klen} 00 {key}' // 4+n bytes, int (RSA Pub Key)
|
||||
.replace(/{klen}/, h(publen+1))
|
||||
.replace(/{key}/, pubkey.toHex(csrpub))
|
||||
, '02 03 {mod}' // 5 bytes, key and modules [01 00 01]
|
||||
.replace(/{mod}/, '01 00 01')
|
||||
|
||||
// #3 Total 2+28+n
|
||||
, 'A0 {16.2.3ellen}' // 2 bytes, ?? [4B]
|
||||
.replace(/{[^}]+}/, h(2+11+2+2+2+5+2+2+sanlen))
|
||||
, '30 {17.3.9seqlen}' // 2 bytes, sequence
|
||||
.replace(/{[^}]+}/, h(11+2+2+2+5+2+2+sanlen))
|
||||
, '06 09 2A 86 48 86 F7 0D 01 09 0E' // 11 bytes, object id (extensionRequest (PKCS #9 via CRMF))
|
||||
, '31 {19.5.0setlen}' // 2 bytes, set
|
||||
.replace(/{[^}]+}/, h(2+2+5+2+2+sanlen))
|
||||
, '30 {20.6.0seqlen}' // 2 bytes, sequence
|
||||
.replace(/{[^}]+}/, h(2+5+2+2+sanlen))
|
||||
, '30 {21.7.0seqlen}' // 2 bytes, sequence
|
||||
.replace(/{[^}]+}/, h(5+2+2+sanlen))
|
||||
, '06 03 55 1D 11' // 5 bytes, object id (subjectAltName (X.509 extension))
|
||||
, '04 {23.8.0octlen}' // 2 bytes, octet string
|
||||
.replace(/{[^}]+}/, h(2+sanlen))
|
||||
, '30 {24.9.0seqlen}' // 2 bytes, sequence
|
||||
.replace(/{[^}]+}/, h(sanlen))
|
||||
, '{altnames}' // n (elements of sequence)
|
||||
.replace(/{altnames}/, altnames)
|
||||
];
|
||||
body = body.join('').replace(/\s+/g, '');
|
||||
return fromHex(body);
|
||||
}
|
||||
|
||||
function createCsrBodyEc(domains, csrpub) {
|
||||
var altnames = domains.map(function (d) {
|
||||
return csrDomains.replace(/{dlen}/, h(d.length)).replace(/{domain\.tld}/, strToHex(d));
|
||||
}).join('').replace(/\s+/g, '');
|
||||
var publen = csrpub.byteLength;
|
||||
var sublen = domains[0].length;
|
||||
var sanlen = (altnames.length/2);
|
||||
|
||||
var body = [ '30 82 {1.1.0seqlen}' // 4 bytes, sequence
|
||||
.replace(/{[^}]+}/, h(
|
||||
3
|
||||
+ 13 + sublen
|
||||
+ 38 + publen
|
||||
+ 30 + sanlen
|
||||
))
|
||||
|
||||
// #0 Total 3
|
||||
, '02 01 00' // 3 bytes, int 0
|
||||
|
||||
// #1 Total 2+11+n
|
||||
, '30 {3.2.0seqlen}' // 2 bytes, sequence
|
||||
.replace(/{[^}]+}/, h(2+2+5+2+sublen))
|
||||
, '31 {4.3.0setlen}' // 2 bytes, set
|
||||
.replace(/{[^}]+}/, h(2+5+2+sublen))
|
||||
, '30 {5.4.0seqlen}' // 2 bytes, sequence
|
||||
.replace(/{[^}]+}/, h(5+2+sublen))
|
||||
, '06 03 55 04 03' // 5 bytes, object id (commonName)
|
||||
, '0C {dlen} {domain.tld}' // 2+n bytes, utf8string
|
||||
.replace(/{dlen}/, h(sublen))
|
||||
.replace(/{domain\.tld}/, strToHex(domains[0]))
|
||||
|
||||
// #2 Total 4+28+n+1+5
|
||||
, '30 82 {8.2.2seqlen}' // 4 bytes, sequence
|
||||
.replace(/{[^}]+}/, h(2+11+2+4+1+4+4+publen+1+5))
|
||||
, '30 0D' // 2 bytes, sequence
|
||||
, '06 09 2A 86 48 86 F7 0D 01 01 01' // 11 bytes, rsaEncryption (PKCS #1)
|
||||
, '05 00' // 2 bytes, null (why?)
|
||||
, '03 82 {12.3.0bslen} 00' // 4+1 bytes, bit string [01 0F]
|
||||
.replace(/{[^}]+}/, h(1+4+4+publen+1+5))
|
||||
, '30 82 {13.4.0seqlen}' // 4 bytes, sequence
|
||||
.replace(/{[^}]+}/, h(4+publen+1+5))
|
||||
, '02 82 {klen} 00 {key}' // 4+n bytes, int (RSA Pub Key)
|
||||
.replace(/{klen}/, h(publen+1))
|
||||
.replace(/{key}/, pubkey.toHex(csrpub))
|
||||
, '02 03 {mod}' // 5 bytes, key and modules [01 00 01]
|
||||
.replace(/{mod}/, '01 00 01')
|
||||
|
||||
// #3 Total 2+28+n
|
||||
, 'A0 {16.2.3ellen}' // 2 bytes, ?? [4B]
|
||||
.replace(/{[^}]+}/, h(2+11+2+2+2+5+2+2+sanlen))
|
||||
, '30 {17.3.9seqlen}' // 2 bytes, sequence
|
||||
.replace(/{[^}]+}/, h(11+2+2+2+5+2+2+sanlen))
|
||||
, '06 09 2A 86 48 86 F7 0D 01 09 0E' // 11 bytes, object id (extensionRequest (PKCS #9 via CRMF))
|
||||
, '31 {19.5.0setlen}' // 2 bytes, set
|
||||
.replace(/{[^}]+}/, h(2+2+5+2+2+sanlen))
|
||||
, '30 {20.6.0seqlen}' // 2 bytes, sequence
|
||||
.replace(/{[^}]+}/, h(2+5+2+2+sanlen))
|
||||
, '30 {21.7.0seqlen}' // 2 bytes, sequence
|
||||
.replace(/{[^}]+}/, h(5+2+2+sanlen))
|
||||
, '06 03 55 1D 11' // 5 bytes, object id (subjectAltName (X.509 extension))
|
||||
, '04 {23.8.0octlen}' // 2 bytes, octet string
|
||||
.replace(/{[^}]+}/, h(2+sanlen))
|
||||
, '30 {24.9.0seqlen}' // 2 bytes, sequence
|
||||
.replace(/{[^}]+}/, h(sanlen))
|
||||
, '{altnames}' // n (elements of sequence)
|
||||
.replace(/{altnames}/, altnames)
|
||||
];
|
||||
body = body.join('').replace(/\s+/g, '');
|
||||
return fromHex(body);
|
||||
}
|
||||
|
||||
function createCsr(domains, keypem) {
|
||||
// TODO get pub from priv
|
||||
var body = createCsrBodyRsa(domains, csrpub);
|
||||
var sign = crypto.createSign('SHA256');
|
||||
sign.write(new Uint8Array(body));
|
||||
sign.end();
|
||||
var sig = sign.sign(keypem);
|
||||
var len = body.byteLength + (csrRsaFoot.length/2) + sig.byteLength;
|
||||
console.log('headlen', h(len));
|
||||
var head = csrHead.replace(/{[^}]+}/, h(len));
|
||||
var ab = new Uint8Array(new ArrayBuffer(4 + len));
|
||||
var i = 0;
|
||||
fromHex(head).forEach(function (b) {
|
||||
ab[i] = b;
|
||||
i += 1;
|
||||
});
|
||||
body.forEach(function (b) {
|
||||
ab[i] = b;
|
||||
i += 1;
|
||||
});
|
||||
fromHex(csrRsaFoot).forEach(function (b) {
|
||||
ab[i] = b;
|
||||
i += 1;
|
||||
});
|
||||
new Uint8Array(sig).forEach(function (b) {
|
||||
ab[i] = b;
|
||||
i += 1;
|
||||
});
|
||||
|
||||
var pem = pubkey.formatAsPublicPem(pubkey.toBase64(ab));
|
||||
|
||||
return pem;
|
||||
}
|
||||
|
||||
//var pem = createCsr([ 'example.com', 'www.example.com', 'api.example.com' ], keypem);
|
||||
var pem = createCsr([ 'whatever.net', 'api.whatever.net' ], keypem);
|
||||
console.log('pem:');
|
||||
console.log(pem);
|
||||
return;
|
||||
|
||||
function signagain(csrbody) {
|
||||
var longEnough = csrbody.byteLength > 256;
|
||||
|
||||
if (!longEnough) { return false; }
|
||||
|
||||
var domains = [ 'example.com', 'www.example.com', 'api.example.com' ];
|
||||
var body = createCsrBodyRsa(domains);
|
||||
|
||||
var sign = crypto.createSign('SHA256');
|
||||
sign.write(new Uint8Array(body));
|
||||
sign.end();
|
||||
var sig2 = sign.sign(keypem);
|
||||
var hexsig = pubkey.toHex(sig);
|
||||
var hexsig2 = pubkey.toHex(sig2);
|
||||
|
||||
console.log('hexsig:');
|
||||
console.log(hexsig);
|
||||
if (hexsig2 !== hexsig) {
|
||||
throw new Error("sigs didn't match");
|
||||
}
|
||||
|
||||
console.log('Winner winner!', csrbody.byteLength);
|
||||
console.log(pubkey.toHex(csrbody));
|
||||
console.log();
|
||||
console.log('Test:', body.byteLength);
|
||||
console.log(pubkey.toHex(body));
|
||||
console.log();
|
||||
|
||||
var len = body.byteLength + (csrRsaFoot.length/2) + sig.byteLength;
|
||||
console.log('headlen', h(len));
|
||||
var head = csrHead.replace(/{[^}]+}/, h(len));
|
||||
var ab = new Uint8Array(new ArrayBuffer(4 + len));
|
||||
var i = 0;
|
||||
fromHex(head).forEach(function (b) {
|
||||
ab[i] = b;
|
||||
i += 1;
|
||||
});
|
||||
body.forEach(function (b) {
|
||||
ab[i] = b;
|
||||
i += 1;
|
||||
});
|
||||
fromHex(csrRsaFoot).forEach(function (b) {
|
||||
ab[i] = b;
|
||||
i += 1;
|
||||
});
|
||||
new Uint8Array(sig2).forEach(function (b) {
|
||||
ab[i] = b;
|
||||
i += 1;
|
||||
});
|
||||
|
||||
console.log("Whole enchilada:", pubkey.toHex(ab.buffer) === pubkey.toHex(csrFull));
|
||||
console.log(pubkey.toHex(ab.buffer));
|
||||
console.log();
|
||||
|
||||
// subject + 2-byte altname headers + altnames themselves + public key
|
||||
//var extralen = domains[0].length + (domains.length * 2) + domains.join('').length + csrpub.byteLength;
|
||||
//var foot = csrRsaFoot.replace(/xxxx/g, (csrpub.byteLength + 1));
|
||||
//var head = csrHead.replace(/xxxx/);
|
||||
return false;
|
||||
}
|
||||
|
||||
console.log();
|
||||
console.log("CSR");
|
||||
console.log(pubkey.toHex(csrFull));
|
||||
console.log();
|
||||
console.log(pubkey.toHex(csrbody.slice(0, csrbody.byteLength - sig.byteLength)));
|
||||
console.log();
|
||||
console.log();
|
||||
//signagain(csrbody);
|
121
test.js
Normal file
121
test.js
Normal 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);
|
||||
});
|
Loading…
x
Reference in New Issue
Block a user