Compare commits

..

33 Commits

Author SHA1 Message Date
b22f957124 update name: convertIfEcdsa => ecdsaAsn1SigToJoseSig 2019-05-06 03:07:30 -06:00
48bee9204d v1.2.14: use try/catch on PEM import 2019-03-14 11:50:37 -06:00
5dc3795a17 v1.2.13: add support for rsa-compat 2019-03-14 11:38:02 -06:00
3c84a7e1bd v1.2.12: fix EC sig padding issues 2019-03-08 19:03:55 -07:00
65db78a3c5 v1.2.11: convert ECDSA ASN.1 signature to ECDSA JWT type 2019-03-08 16:46:50 -07:00
e04557c84a v1.2.9: bugfix RSA hash type 2019-03-08 15:24:52 -07:00
083cc6d73e v1.2.8: allow non-stringified jwk in parse, ignore undefineds when neutering 2019-03-08 12:57:50 -07:00
885a00c3ae v1.2.7: update docs, handle human readable 'exp' claims, denest required claims 2019-03-08 12:30:32 -07:00
34d0ca9f13 v1.2.6: Make neuter public, replace bin with installer 2019-03-07 00:04:55 -07:00
ad312edfa6 v1.2.5: fix multiple bugs with conversion 2019-03-05 12:23:50 -07:00
a5236f6c2f v1.2.4: remove stray logs 2019-03-05 07:45:55 -07:00
286606720d v1.2.3: bugfix not passing pair 2019-03-05 07:42:44 -07:00
f1b4ccf792 v1.2.3: bugfix double convert 2019-03-05 07:37:43 -07:00
4ab4cbd2ab bugfix readKeypair 2019-03-05 07:35:22 -07:00
4ac6e7f8dd v1.2.0: add CLI stuff 2019-03-05 07:27:17 -07:00
7099943db7 v1.1.0: Add tests, more convenience methods, more docs 2019-03-04 17:16:25 -07:00
738be9b656 v1.0.1: export and update docs 2019-02-27 00:11:50 -07:00
1a0e92eb52 typo fix 2019-02-23 02:27:52 -07:00
265977e2c0 v1.0.0: just wrap Rasha and Eckles for now 2019-02-23 02:27:20 -07:00
1d2a4d7b9e beginning to take shape 2018-12-18 01:06:37 -07:00
d0157d3270 shorten 2018-12-18 00:46:07 -07:00
76b4528e19 update x509 support 2018-12-18 00:35:47 -07:00
c228d73bd0 update x509 support 2018-12-18 00:34:40 -07:00
17021fa2cb update comment 2018-12-17 23:48:44 -07:00
2babbeb1dc rm asn1.js 2018-12-17 23:44:16 -07:00
b9b403ef4f wip 2018-12-17 23:42:43 -07:00
1c1b9d00a9 add comment 2018-12-17 21:29:45 -07:00
665a2d2f8e asn1-packer.js 2018-12-17 21:29:25 -07:00
6b3f1cb6a1 add PEM parser / packer 2018-12-17 21:08:46 -07:00
bf86aa8964 Merge branch 'master' of ssh://git.coolaj86.com:22042/coolaj86/keypairs.js 2018-12-17 21:00:16 -07:00
0d4db1bb19 add ASN.1 parser 2018-12-17 20:58:53 -07:00
c71f28e1d7 v0.0.4: update (and partial commits) 2018-12-03 00:57:41 -07:00
d928c2bdc0 v0.0.3: ecdsa is soooooo close 2018-11-16 01:40:07 -07:00
13 changed files with 816 additions and 860 deletions

1
.gitignore vendored Normal file
View File

@ -0,0 +1 @@
node_modules

341
README.md
View File

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

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

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

View File

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

View File

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

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

View File

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