Browse Source

moved common code to own modules

wip-v3
AJ ONeal 5 years ago
parent
commit
21f7e87606
  1. 4
      acme.js
  2. 322
      csr.js
  3. 239
      ecdsa.js
  4. 315
      keypairs.js
  5. 57
      lib/browser/ecdsa.js
  6. 108
      lib/browser/keypairs.js
  7. 59
      lib/browser/rsa.js
  8. 113
      lib/node/ecdsa.js
  9. 55
      lib/node/generate-privkey-forge.js
  10. 21
      lib/node/generate-privkey-node.js
  11. 27
      lib/node/generate-privkey-ursa.js
  12. 90
      lib/node/generate-privkey.js
  13. 84
      lib/node/keypairs.js
  14. 154
      lib/node/rsa.js
  15. 4844
      package-lock.json
  16. 11
      package.json
  17. 158
      rsa.js
  18. 4
      tests/index.js

4
acme.js

@ -10,7 +10,7 @@ var Enc = require('@root/encoding/base64');
var ACME = module.exports;
//var Keypairs = exports.Keypairs || {};
//var CSR = exports.CSR;
var sha2 = require('./lib/node/sha2.js');
var sha2 = require('@root/keypairs/lib/node/sha2.js');
var http = require('./lib/node/http.js');
ACME.formatPemChain = function formatPemChain(str) {
@ -1318,7 +1318,7 @@ ACME.create = function create(me) {
// me.debug = true;
me.challengePrefixes = ACME.challengePrefixes;
me.Keypairs = me.Keypairs || require('@root/keypairs');
me.CSR = me.CSR || require('./csr.js');
me.CSR = me.CSR || require('@root/csr');
me._nonces = [];
me._canUse = {};
if (!me._baseUrl) {

322
csr.js

@ -1,322 +0,0 @@
// Copyright 2018-present AJ ONeal. All rights reserved
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
'use strict';
/*global Promise*/
var Enc = require('@root/encoding');
var ASN1 = require('./asn1/packer.js'); // DER, actually
var Asn1 = ASN1.Any;
var BitStr = ASN1.BitStr;
var UInt = ASN1.UInt;
var Asn1Parser = require('./asn1/parser.js');
var PEM = require('./pem.js');
var X509 = require('./x509.js');
var Keypairs = require('@root/keypairs');
// TODO find a way that the prior node-ish way of `module.exports = function () {}` isn't broken
var CSR = module.exports;
CSR.csr = function(opts) {
// We're using a Promise here to be compatible with the browser version
// which will probably use the webcrypto API for some of the conversions
return CSR._prepare(opts).then(function(opts) {
return CSR.create(opts).then(function(bytes) {
return CSR._encode(opts, bytes);
});
});
};
CSR._prepare = function(opts) {
return Promise.resolve().then(function() {
opts = JSON.parse(JSON.stringify(opts));
// We do a bit of extra error checking for user convenience
if (!opts) {
throw new Error(
'You must pass options with key and domains to rsacsr'
);
}
if (!Array.isArray(opts.domains) || 0 === opts.domains.length) {
new Error('You must pass options.domains as a non-empty array');
}
// I need to check that 例.中国 is a valid domain name
if (
!opts.domains.every(function(d) {
// allow punycode? xn--
if (
'string' === typeof d /*&& /\./.test(d) && !/--/.test(d)*/
) {
return true;
}
})
) {
throw new Error('You must pass options.domains as strings');
}
if (opts.jwk) {
return opts;
}
if (opts.key && opts.key.kty) {
opts.jwk = opts.key;
return opts;
}
if (!opts.pem && !opts.key) {
throw new Error('You must pass options.key as a JSON web key');
}
return Keypairs.import({ pem: opts.pem || opts.key }).then(function(
pair
) {
opts.jwk = pair.private;
return opts;
});
});
};
CSR._encode = function(opts, bytes) {
if ('der' === (opts.encoding || '').toLowerCase()) {
return bytes;
}
return PEM.packBlock({
type: 'CERTIFICATE REQUEST',
bytes: bytes /* { jwk: jwk, domains: opts.domains } */
});
};
CSR.create = function createCsr(opts) {
var hex = CSR.request(opts.jwk, opts.domains);
return CSR._sign(opts.jwk, hex).then(function(csr) {
return Enc.hexToBuf(csr);
});
};
//
// EC / RSA
//
CSR.request = function createCsrBodyEc(jwk, domains) {
var asn1pub;
if (/^EC/i.test(jwk.kty)) {
asn1pub = X509.packCsrEcPublicKey(jwk);
} else {
asn1pub = X509.packCsrRsaPublicKey(jwk);
}
return X509.packCsr(asn1pub, domains);
};
CSR._sign = function csrEcSig(jwk, request) {
// Took some tips from https://gist.github.com/codermapuche/da4f96cdb6d5ff53b7ebc156ec46a10a
// TODO will have to convert web ECDSA signatures to PEM ECDSA signatures (but RSA should be the same)
// TODO have a consistent non-private way to sign
return Keypairs.sign(
{ jwk: jwk, format: 'x509' },
Enc.hexToBuf(request)
).then(function(sig) {
return CSR._toDer({
request: request,
signature: sig,
kty: jwk.kty
});
});
};
CSR._toDer = function encode(opts) {
var sty;
if (/^EC/i.test(opts.kty)) {
// 1.2.840.10045.4.3.2 ecdsaWithSHA256 (ANSI X9.62 ECDSA algorithm with SHA256)
sty = Asn1('30', Asn1('06', '2a8648ce3d040302'));
} else {
// 1.2.840.113549.1.1.11 sha256WithRSAEncryption (PKCS #1)
sty = Asn1('30', Asn1('06', '2a864886f70d01010b'), Asn1('05'));
}
return Asn1(
'30',
// The Full CSR Request Body
opts.request,
// The Signature Type
sty,
// The Signature
BitStr(Enc.bufToHex(opts.signature))
);
};
X509.packCsr = function(asn1pubkey, domains) {
return Asn1(
'30',
// Version (0)
UInt('00'),
// 2.5.4.3 commonName (X.520 DN component)
Asn1(
'30',
Asn1(
'31',
Asn1(
'30',
Asn1('06', '550403'),
// TODO utf8 => punycode
Asn1('0c', Enc.strToHex(domains[0]))
)
)
),
// Public Key (RSA or EC)
asn1pubkey,
// Request Body
Asn1(
'a0',
Asn1(
'30',
// 1.2.840.113549.1.9.14 extensionRequest (PKCS #9 via CRMF)
Asn1('06', '2a864886f70d01090e'),
Asn1(
'31',
Asn1(
'30',
Asn1(
'30',
// 2.5.29.17 subjectAltName (X.509 extension)
Asn1('06', '551d11'),
Asn1(
'04',
Asn1(
'30',
domains
.map(function(d) {
// TODO utf8 => punycode
return Asn1('82', Enc.strToHex(d));
})
.join('')
)
)
)
)
)
)
)
);
};
// TODO finish this later
// we want to parse the domains, the public key, and verify the signature
CSR._info = function(der) {
// standard base64 PEM
if ('string' === typeof der && '-' === der[0]) {
der = PEM.parseBlock(der).bytes;
}
// jose urlBase64 not-PEM
if ('string' === typeof der) {
der = Enc.base64ToBuf(der);
}
// not supporting binary-encoded bas64
var c = Asn1Parser.parse(der);
var kty;
// A cert has 3 parts: cert, signature meta, signature
if (c.children.length !== 3) {
throw new Error(
"doesn't look like a certificate request: expected 3 parts of header"
);
}
var sig = c.children[2];
if (sig.children.length) {
// ASN1/X509 EC
sig = sig.children[0];
sig = Asn1(
'30',
UInt(Enc.bufToHex(sig.children[0].value)),
UInt(Enc.bufToHex(sig.children[1].value))
);
sig = Enc.hexToBuf(sig);
kty = 'EC';
} else {
// Raw RSA Sig
sig = sig.value;
kty = 'RSA';
}
//c.children[1]; // signature type
var req = c.children[0];
if (4 !== req.children.length) {
throw new Error(
"doesn't look like a certificate request: expected 4 parts to request"
);
}
// 0 null
// 1 commonName / subject
var sub = Enc.bufToStr(
req.children[1].children[0].children[0].children[1].value
);
// 3 public key (type, key)
//console.log('oid', Enc.bufToHex(req.children[2].children[0].children[0].value));
var pub;
// TODO reuse ASN1 parser for these?
if ('EC' === kty) {
// throw away compression byte
pub = req.children[2].children[1].value.slice(1);
pub = { kty: kty, x: pub.slice(0, 32), y: pub.slice(32) };
while (0 === pub.x[0]) {
pub.x = pub.x.slice(1);
}
while (0 === pub.y[0]) {
pub.y = pub.y.slice(1);
}
if ((pub.x.length || pub.x.byteLength) > 48) {
pub.crv = 'P-521';
} else if ((pub.x.length || pub.x.byteLength) > 32) {
pub.crv = 'P-384';
} else {
pub.crv = 'P-256';
}
pub.x = Enc.bufToUrlBase64(pub.x);
pub.y = Enc.bufToUrlBase64(pub.y);
} else {
pub = req.children[2].children[1].children[0];
pub = {
kty: kty,
n: pub.children[0].value,
e: pub.children[1].value
};
while (0 === pub.n[0]) {
pub.n = pub.n.slice(1);
}
while (0 === pub.e[0]) {
pub.e = pub.e.slice(1);
}
pub.n = Enc.bufToUrlBase64(pub.n);
pub.e = Enc.bufToUrlBase64(pub.e);
}
// 4 extensions
var domains = req.children[3].children
.filter(function(seq) {
// 1.2.840.113549.1.9.14 extensionRequest (PKCS #9 via CRMF)
if ('2a864886f70d01090e' === Enc.bufToHex(seq.children[0].value)) {
return true;
}
})
.map(function(seq) {
return seq.children[1].children[0].children
.filter(function(seq2) {
// subjectAltName (X.509 extension)
if ('551d11' === Enc.bufToHex(seq2.children[0].value)) {
return true;
}
})
.map(function(seq2) {
return seq2.children[1].children[0].children.map(function(
name
) {
// TODO utf8 => punycode
return Enc.bufToStr(name.value);
});
})[0];
})[0];
return {
subject: sub,
altnames: domains,
jwk: pub,
signature: sig
};
};

239
ecdsa.js

@ -1,239 +0,0 @@
/*global Promise*/
'use strict';
var Enc = require('@root/encoding');
var EC = module.exports;
var native = require('./lib/node/ecdsa.js');
// TODO SSH
var SSH;
var x509 = require('./x509.js');
var PEM = require('./pem.js');
//var SSH = require('./ssh-keys.js');
var sha2 = require('./lib/node/sha2.js');
// 1.2.840.10045.3.1.7
// prime256v1 (ANSI X9.62 named elliptic curve)
var OBJ_ID_EC = '06 08 2A8648CE3D030107'.replace(/\s+/g, '').toLowerCase();
// 1.3.132.0.34
// secp384r1 (SECG (Certicom) named elliptic curve)
var OBJ_ID_EC_384 = '06 05 2B81040022'.replace(/\s+/g, '').toLowerCase();
EC._stance =
"We take the stance that if you're knowledgeable enough to" +
" properly and securely use non-standard crypto then you shouldn't need Bluecrypt anyway.";
native._stance = EC._stance;
EC._universal =
'Bluecrypt only supports crypto with standard cross-browser and cross-platform support.';
EC.generate = native.generate;
EC.export = function(opts) {
return Promise.resolve().then(function() {
if (!opts || !opts.jwk || 'object' !== typeof opts.jwk) {
throw new Error('must pass { jwk: jwk } as a JSON object');
}
var jwk = JSON.parse(JSON.stringify(opts.jwk));
var format = opts.format;
if (
opts.public ||
-1 !== ['spki', 'pkix', 'ssh', 'rfc4716'].indexOf(format)
) {
jwk.d = null;
}
if ('EC' !== jwk.kty) {
throw new Error("options.jwk.kty must be 'EC' for EC keys");
}
if (!jwk.d) {
if (!format || -1 !== ['spki', 'pkix'].indexOf(format)) {
format = 'spki';
} else if (-1 !== ['ssh', 'rfc4716'].indexOf(format)) {
format = 'ssh';
} else {
throw new Error(
"options.format must be 'spki' or 'ssh' for public EC keys, not (" +
typeof format +
') ' +
format
);
}
} else {
if (!format || 'sec1' === format) {
format = 'sec1';
} else if ('pkcs8' !== format) {
throw new Error(
"options.format must be 'sec1' or 'pkcs8' for private EC keys, not '" +
format +
"'"
);
}
}
if (-1 === ['P-256', 'P-384'].indexOf(jwk.crv)) {
throw new Error(
"options.jwk.crv must be either P-256 or P-384 for EC keys, not '" +
jwk.crv +
"'"
);
}
if (!jwk.y) {
throw new Error(
'options.jwk.y must be a urlsafe base64-encoded either P-256 or P-384'
);
}
if ('sec1' === format) {
return PEM.packBlock({
type: 'EC PRIVATE KEY',
bytes: x509.packSec1(jwk)
});
} else if ('pkcs8' === format) {
return PEM.packBlock({
type: 'PRIVATE KEY',
bytes: x509.packPkcs8(jwk)
});
} else if (-1 !== ['spki', 'pkix'].indexOf(format)) {
return PEM.packBlock({
type: 'PUBLIC KEY',
bytes: x509.packSpki(jwk)
});
} else if (-1 !== ['ssh', 'rfc4716'].indexOf(format)) {
return SSH.packSsh(jwk);
} else {
throw new Error(
'Sanity Error: reached unreachable code block with format: ' +
format
);
}
});
};
native.export = EC.export;
EC.import = function(opts) {
return Promise.resolve().then(function() {
if (!opts || !opts.pem || 'string' !== typeof opts.pem) {
throw new Error('must pass { pem: pem } as a string');
}
if (0 === opts.pem.indexOf('ecdsa-sha2-')) {
return SSH.parseSsh(opts.pem);
}
var pem = opts.pem;
var u8 = PEM.parseBlock(pem).bytes;
var hex = Enc.bufToHex(u8);
var jwk = { kty: 'EC', crv: null, x: null, y: null };
//console.log();
if (
-1 !== hex.indexOf(OBJ_ID_EC) ||
-1 !== hex.indexOf(OBJ_ID_EC_384)
) {
if (-1 !== hex.indexOf(OBJ_ID_EC_384)) {
jwk.crv = 'P-384';
} else {
jwk.crv = 'P-256';
}
// PKCS8
if (0x02 === u8[3] && 0x30 === u8[6] && 0x06 === u8[8]) {
//console.log("PKCS8", u8[3].toString(16), u8[6].toString(16), u8[8].toString(16));
jwk = x509.parsePkcs8(u8, jwk);
// EC-only
} else if (0x02 === u8[2] && 0x04 === u8[5] && 0xa0 === u8[39]) {
//console.log("EC---", u8[2].toString(16), u8[5].toString(16), u8[39].toString(16));
jwk = x509.parseSec1(u8, jwk);
// EC-only
} else if (0x02 === u8[3] && 0x04 === u8[6] && 0xa0 === u8[56]) {
//console.log("EC---", u8[3].toString(16), u8[6].toString(16), u8[56].toString(16));
jwk = x509.parseSec1(u8, jwk);
// SPKI/PKIK (Public)
} else if (0x30 === u8[2] && 0x06 === u8[4] && 0x06 === u8[13]) {
//console.log("SPKI-", u8[2].toString(16), u8[4].toString(16), u8[13].toString(16));
jwk = x509.parseSpki(u8, jwk);
// Error
} else {
//console.log("PKCS8", u8[3].toString(16), u8[6].toString(16), u8[8].toString(16));
//console.log("EC---", u8[2].toString(16), u8[5].toString(16), u8[39].toString(16));
//console.log("EC---", u8[3].toString(16), u8[6].toString(16), u8[56].toString(16));
//console.log("SPKI-", u8[2].toString(16), u8[4].toString(16), u8[13].toString(16));
throw new Error('unrecognized key format');
}
} else {
throw new Error('Supported key types are P-256 and P-384');
}
if (opts.public) {
if (true !== opts.public) {
throw new Error(
'options.public must be either `true` or `false` not (' +
typeof opts.public +
") '" +
opts.public +
"'"
);
}
delete jwk.d;
}
return jwk;
});
};
native.import = EC.import;
EC.pack = function(opts) {
return Promise.resolve().then(function() {
return EC.export(opts);
});
};
// Chopping off the private parts is now part of the public API.
// I thought it sounded a little too crude at first, but it really is the best name in every possible way.
EC.neuter = function(opts) {
// trying to find the best balance of an immutable copy with custom attributes
var jwk = {};
Object.keys(opts.jwk).forEach(function(k) {
if ('undefined' === typeof opts.jwk[k]) {
return;
}
// ignore EC private parts
if ('d' === k) {
return;
}
jwk[k] = JSON.parse(JSON.stringify(opts.jwk[k]));
});
return jwk;
};
native.neuter = EC.neuter;
// https://stackoverflow.com/questions/42588786/how-to-fingerprint-a-jwk
EC.__thumbprint = function(jwk) {
// Use the same entropy for SHA as for key
var alg = 'SHA-256';
if (/384/.test(jwk.crv)) {
alg = 'SHA-384';
}
var payload =
'{"crv":"' +
jwk.crv +
'","kty":"EC","x":"' +
jwk.x +
'","y":"' +
jwk.y +
'"}';
return sha2.sum(alg, payload).then(function(hash) {
return Enc.bufToUrlBase64(Uint8Array.from(hash));
});
};
EC.thumbprint = function(opts) {
return Promise.resolve().then(function() {
var jwk;
if ('EC' === opts.kty) {
jwk = opts;
} else if (opts.jwk) {
jwk = opts.jwk;
} else {
return native.import(opts).then(function(jwk) {
return EC.__thumbprint(jwk);
});
}
return EC.__thumbprint(jwk);
});
};

315
keypairs.js

@ -1,315 +0,0 @@
/*global Promise*/
'use strict';
require('@root/encoding/bytes');
var Enc = require('@root/encoding/base64');
var Keypairs = module.exports;
var Rasha = require('./rsa.js');
var Eckles = require('./ecdsa.js');
var native = require('./lib/node/keypairs.js');
Keypairs._stance =
"We take the stance that if you're knowledgeable enough to" +
" properly and securely use non-standard crypto then you shouldn't need Bluecrypt anyway.";
Keypairs._universal =
'Bluecrypt only supports crypto with standard cross-browser and cross-platform support.';
Keypairs.generate = function(opts) {
opts = opts || {};
var p;
if (!opts.kty) {
opts.kty = opts.type;
}
if (!opts.kty) {
opts.kty = 'EC';
}
if (/^EC/i.test(opts.kty)) {
p = Eckles.generate(opts);
} else if (/^RSA$/i.test(opts.kty)) {
p = Rasha.generate(opts);
} else {
return Promise.Reject(
new Error(
"'" +
opts.kty +
"' is not a well-supported key type." +
Keypairs._universal +
" Please choose 'EC', or 'RSA' if you have good reason to."
)
);
}
return p.then(function(pair) {
return Keypairs.thumbprint({ jwk: pair.public }).then(function(thumb) {
pair.private.kid = thumb; // maybe not the same id on the private key?
pair.public.kid = thumb;
return pair;
});
});
};
Keypairs.export = function(opts) {
return Eckles.export(opts).catch(function(err) {
return Rasha.export(opts).catch(function() {
return Promise.reject(err);
});
});
};
// XXX
native.export = Keypairs.export;
/**
* Chopping off the private parts is now part of the public API.
* I thought it sounded a little too crude at first, but it really is the best name in every possible way.
*/
Keypairs.neuter = function(opts) {
/** trying to find the best balance of an immutable copy with custom attributes */
var jwk = {};
Object.keys(opts.jwk).forEach(function(k) {
if ('undefined' === typeof opts.jwk[k]) {
return;
}
// ignore RSA and EC private parts
if (-1 !== ['d', 'p', 'q', 'dp', 'dq', 'qi'].indexOf(k)) {
return;
}
jwk[k] = JSON.parse(JSON.stringify(opts.jwk[k]));
});
return jwk;
};
Keypairs.thumbprint = function(opts) {
return Promise.resolve().then(function() {
if (/EC/i.test(opts.jwk.kty)) {
return Eckles.thumbprint(opts);
} else {
return Rasha.thumbprint(opts);
}
});
};
Keypairs.publish = function(opts) {
if ('object' !== typeof opts.jwk || !opts.jwk.kty) {
throw new Error('invalid jwk: ' + JSON.stringify(opts.jwk));
}
/** returns a copy */
var jwk = Keypairs.neuter(opts);
if (jwk.exp) {
jwk.exp = setTime(jwk.exp);
} else {
if (opts.exp) {
jwk.exp = setTime(opts.exp);
} else if (opts.expiresIn) {
jwk.exp = Math.round(Date.now() / 1000) + opts.expiresIn;
} else if (opts.expiresAt) {
jwk.exp = opts.expiresAt;
}
}
if (!jwk.use && false !== jwk.use) {
jwk.use = 'sig';
}
if (jwk.kid) {
return Promise.resolve(jwk);
}
return Keypairs.thumbprint({ jwk: jwk }).then(function(thumb) {
jwk.kid = thumb;
return jwk;
});
};
// JWT a.k.a. JWS with Claims using Compact Serialization
Keypairs.signJwt = function(opts) {
return Keypairs.thumbprint({ jwk: opts.jwk }).then(function(thumb) {
var header = opts.header || {};
var claims = JSON.parse(JSON.stringify(opts.claims || {}));
header.typ = 'JWT';
if (!header.kid && false !== header.kid) {
header.kid = thumb;
}
if (!header.alg && opts.alg) {
header.alg = opts.alg;
}
if (!claims.iat && (false === claims.iat || false === opts.iat)) {
claims.iat = undefined;
} else if (!claims.iat) {
claims.iat = Math.round(Date.now() / 1000);
}
if (opts.exp) {
claims.exp = setTime(opts.exp);
} else if (
!claims.exp &&
(false === claims.exp || false === opts.exp)
) {
claims.exp = undefined;
} else if (!claims.exp) {
throw new Error(
"opts.claims.exp should be the expiration date as seconds, human form (i.e. '1h' or '15m') or false"
);
}
if (opts.iss) {
claims.iss = opts.iss;
}
if (!claims.iss && (false === claims.iss || false === opts.iss)) {
claims.iss = undefined;
} else if (!claims.iss) {
throw new Error(
'opts.claims.iss should be in the form of https://example.com/, a secure OIDC base url'
);
}
return Keypairs.signJws({
jwk: opts.jwk,
pem: opts.pem,
protected: header,
header: undefined,
payload: claims
}).then(function(jws) {
return [jws.protected, jws.payload, jws.signature].join('.');
});
});
};
Keypairs.signJws = function(opts) {
return Keypairs.thumbprint(opts).then(function(thumb) {
function alg() {
if (!opts.jwk) {
throw new Error("opts.jwk must exist and must declare 'typ'");
}
if (opts.jwk.alg) {
return opts.jwk.alg;
}
var typ = 'RSA' === opts.jwk.kty ? 'RS' : 'ES';
return typ + Keypairs._getBits(opts);
}
function sign() {
var protect = opts.protected;
var payload = opts.payload;
// Compute JWS signature
var protectedHeader = '';
// Because unprotected headers are allowed, regrettably...
// https://stackoverflow.com/a/46288694
if (false !== protect) {
if (!protect) {
protect = {};
}
if (!protect.alg) {
protect.alg = alg();
}
// There's a particular request where ACME / Let's Encrypt explicitly doesn't use a kid
if (false === protect.kid) {
protect.kid = undefined;
} else if (!protect.kid) {
protect.kid = thumb;
}
protectedHeader = JSON.stringify(protect);
}
// Not sure how to handle the empty case since ACME POST-as-GET must be empty
//if (!payload) {
// throw new Error("opts.payload should be JSON, string, or ArrayBuffer (it may be empty, but that must be explicit)");
//}
// Trying to detect if it's a plain object (not Buffer, ArrayBuffer, Array, Uint8Array, etc)
if (
payload &&
'string' !== typeof payload &&
'undefined' === typeof payload.byteLength &&
'undefined' === typeof payload.buffer
) {
payload = JSON.stringify(payload);
}
// Converting to a buffer, even if it was just converted to a string
if ('string' === typeof payload) {
payload = Enc.strToBuf(payload);
}
var protected64 = Enc.strToUrlBase64(protectedHeader);
var payload64 = Enc.bufToUrlBase64(payload);
var msg = protected64 + '.' + payload64;
return native._sign(opts, msg).then(function(buf) {
var signedMsg = {
protected: protected64,
payload: payload64,
signature: Enc.bufToUrlBase64(buf)
};
return signedMsg;
});
}
if (opts.jwk) {
return sign();
} else {
return Keypairs.import({ pem: opts.pem }).then(function(pair) {
opts.jwk = pair.private;
return sign();
});
}
});
};
// TODO expose consistently
Keypairs.sign = native._sign;
Keypairs._getBits = function(opts) {
if (opts.alg) {
return opts.alg.replace(/[a-z\-]/gi, '');
}
// base64 len to byte len
var len = Math.floor((opts.jwk.n || '').length * 0.75);
// TODO this may be a bug
// need to confirm that the padding is no more or less than 1 byte
if (/521/.test(opts.jwk.crv) || len >= 511) {
return '512';
} else if (/384/.test(opts.jwk.crv) || len >= 383) {
return '384';
}
return '256';
};
// XXX
native._getBits = Keypairs._getBits;
function setTime(time) {
if ('number' === typeof time) {
return time;
}
var t = time.match(/^(\-?\d+)([dhms])$/i);
if (!t || !t[0]) {
throw new Error(
"'" +
time +
"' should be datetime in seconds or human-readable format (i.e. 3d, 1h, 15m, 30s"
);
}
var now = Math.round(Date.now() / 1000);
var num = parseInt(t[1], 10);
var unit = t[2];
var mult = 1;
switch (unit) {
// fancy fallthrough, what fun!
case 'd':
mult *= 24;
/*falls through*/
case 'h':
mult *= 60;
/*falls through*/
case 'm':
mult *= 60;
/*falls through*/
case 's':
mult *= 1;
}
return now + mult * num;
}

57
lib/browser/ecdsa.js

@ -1,57 +0,0 @@
'use strict';
var native = module.exports;
// XXX received from caller
var EC = native;
native.generate = function(opts) {
var wcOpts = {};
if (!opts) {
opts = {};
}
if (!opts.kty) {
opts.kty = 'EC';
}
// ECDSA has only the P curves and an associated bitlength
wcOpts.name = 'ECDSA';
if (!opts.namedCurve) {
opts.namedCurve = 'P-256';
}
wcOpts.namedCurve = opts.namedCurve; // true for supported curves
if (/256/.test(wcOpts.namedCurve)) {
wcOpts.namedCurve = 'P-256';
wcOpts.hash = { name: 'SHA-256' };
} else if (/384/.test(wcOpts.namedCurve)) {
wcOpts.namedCurve = 'P-384';
wcOpts.hash = { name: 'SHA-384' };
} else {
return Promise.Reject(
new Error(
"'" +
wcOpts.namedCurve +
"' is not an NIST approved ECDSA namedCurve. " +
" Please choose either 'P-256' or 'P-384'. " +
// XXX received from caller
EC._stance
)
);
}
var extractable = true;
return window.crypto.subtle
.generateKey(wcOpts, extractable, ['sign', 'verify'])
.then(function(result) {
return window.crypto.subtle
.exportKey('jwk', result.privateKey)
.then(function(privJwk) {
privJwk.key_ops = undefined;
privJwk.ext = undefined;
return {
private: privJwk,
// XXX received from caller
public: EC.neuter({ jwk: privJwk })
};
});
});
};

108
lib/browser/keypairs.js

@ -1,108 +0,0 @@
'use strict';
var Keypairs = module.exports;
Keypairs._sign = function(opts, payload) {
return Keypairs._import(opts).then(function(privkey) {
if ('string' === typeof payload) {
payload = new TextEncoder().encode(payload);
}
return window.crypto.subtle
.sign(
{
name: Keypairs._getName(opts),
hash: { name: 'SHA-' + Keypairs._getBits(opts) }
},
privkey,
payload
)
.then(function(signature) {
signature = new Uint8Array(signature); // ArrayBuffer -> u8
// This will come back into play for CSRs, but not for JOSE
if ('EC' === opts.jwk.kty && /x509|asn1/i.test(opts.format)) {
return Keypairs._ecdsaJoseSigToAsn1Sig(signature);
} else {
// jose/jws/jwt
return signature;
}
});
});
};
Keypairs._import = function(opts) {
return Promise.resolve().then(function() {
var ops;
// all private keys just happen to have a 'd'
if (opts.jwk.d) {
ops = ['sign'];
} else {
ops = ['verify'];
}
// gotta mark it as extractable, as if it matters
opts.jwk.ext = true;
opts.jwk.key_ops = ops;
return window.crypto.subtle
.importKey(
'jwk',
opts.jwk,
{
name: Keypairs._getName(opts),
namedCurve: opts.jwk.crv,
hash: { name: 'SHA-' + Keypairs._getBits(opts) }
},
true,
ops
)
.then(function(privkey) {
delete opts.jwk.ext;
return privkey;
});
});
};
// ECDSA JOSE / JWS / JWT signatures differ from "normal" ASN1/X509 ECDSA signatures
// https://tools.ietf.org/html/rfc7518#section-3.4
Keypairs._ecdsaJoseSigToAsn1Sig = function(bufsig) {
// it's easier to do the manipulation in the browser with an array
bufsig = Array.from(bufsig);
var hlen = bufsig.length / 2; // should be even
var r = bufsig.slice(0, hlen);
var s = bufsig.slice(hlen);
// unpad positive ints less than 32 bytes wide
while (!r[0]) {
r = r.slice(1);
}
while (!s[0]) {
s = s.slice(1);
}
// pad (or re-pad) ambiguously non-negative BigInts, up to 33 bytes wide
if (0x80 & r[0]) {
r.unshift(0);
}
if (0x80 & s[0]) {
s.unshift(0);
}
var len = 2 + r.length + 2 + s.length;
var head = [0x30];
// hard code 0x80 + 1 because it won't be longer than
// two SHA512 plus two pad bytes (130 bytes <= 256)
if (len >= 0x80) {
head.push(0x81);
}
head.push(len);
return Uint8Array.from(
head.concat([0x02, r.length], r, [0x02, s.length], s)
);
};
Keypairs._getName = function(opts) {
if (/EC/i.test(opts.jwk.kty)) {
return 'ECDSA';
} else {
return 'RSASSA-PKCS1-v1_5';
}
};

59
lib/browser/rsa.js

@ -1,59 +0,0 @@
'use strict';
var native = module.exports;
// XXX added by caller: _stance, neuter
var RSA = native;
native.generate = function(opts) {
var wcOpts = {};
if (!opts) {
opts = {};
}
if (!opts.kty) {
opts.kty = 'RSA';
}
// Support PSS? I don't think it's used for Let's Encrypt
wcOpts.name = 'RSASSA-PKCS1-v1_5';
if (!opts.modulusLength) {
opts.modulusLength = 2048;
}
wcOpts.modulusLength = opts.modulusLength;
if (wcOpts.modulusLength >= 2048 && wcOpts.modulusLength < 3072) {
// erring on the small side... for no good reason
wcOpts.hash = { name: 'SHA-256' };
} else if (wcOpts.modulusLength >= 3072 && wcOpts.modulusLength < 4096) {
wcOpts.hash = { name: 'SHA-384' };
} else if (wcOpts.modulusLength < 4097) {
wcOpts.hash = { name: 'SHA-512' };
} else {
// Public key thumbprints should be paired with a hash of similar length,
// so anything above SHA-512's keyspace would be left under-represented anyway.
return Promise.Reject(
new Error(
"'" +
wcOpts.modulusLength +
"' is not within the safe and universally" +
' acceptable range of 2048-4096. Typically you should pick 2048, 3072, or 4096, though other values' +
' divisible by 8 are allowed. ' +
RSA._stance
)
);
}
// TODO maybe allow this to be set to any of the standard values?
wcOpts.publicExponent = new Uint8Array([0x01, 0x00, 0x01]);
var extractable = true;
return window.crypto.subtle
.generateKey(wcOpts, extractable, ['sign', 'verify'])
.then(function(result) {
return window.crypto.subtle
.exportKey('jwk', result.privateKey)
.then(function(privJwk) {
return {
private: privJwk,
public: RSA.neuter({ jwk: privJwk })
};
});
});
};

113
lib/node/ecdsa.js

@ -1,113 +0,0 @@
'use strict';
var native = module.exports;
// XXX provided by caller: import, export
var EC = native;
// TODO SSH
native.generate = function(opts) {
return Promise.resolve().then(function() {
var typ = 'ec';
var format = opts.format;
var encoding = opts.encoding;
var priv;
var pub = 'spki';
if (!format) {
format = 'jwk';
}
if (-1 !== ['spki', 'pkcs8', 'ssh'].indexOf(format)) {
format = 'pkcs8';
}
if ('pem' === format) {
format = 'sec1';
encoding = 'pem';
} else if ('der' === format) {
format = 'sec1';
encoding = 'der';
}
if ('jwk' === format || 'json' === format) {
format = 'jwk';
encoding = 'json';
} else {
priv = format;
}
if (!encoding) {
encoding = 'pem';
}
if (priv) {
priv = { type: priv, format: encoding };
pub = { type: pub, format: encoding };
} else {
// jwk
priv = { type: 'sec1', format: 'pem' };
pub = { type: 'spki', format: 'pem' };
}
return new Promise(function(resolve, reject) {
return require('crypto').generateKeyPair(
typ,
{
namedCurve: opts.crv || opts.namedCurve || 'P-256',
privateKeyEncoding: priv,
publicKeyEncoding: pub
},
function(err, pubkey, privkey) {
if (err) {
reject(err);
}
resolve({
private: privkey,
public: pubkey
});
}
);
}).then(function(keypair) {
if ('jwk' === format) {
return Promise.all([
native.import({
pem: keypair.private,
format: priv.type
}),
native.import({
pem: keypair.public,
format: pub.type,
public: true
})
]).then(function(pair) {
return {
private: pair[0],
public: pair[1]
};
});
}
if ('ssh' !== opts.format) {
return keypair;
}
return native
.import({
pem: keypair.public,
format: format,
public: true
})
.then(function(jwk) {
return EC.export({
jwk: jwk,
format: opts.format,
public: true
}).then(function(pub) {
return {
private: keypair.private,
public: pub
};
});
});
});
});
};

55
lib/node/generate-privkey-forge.js

@ -1,55 +0,0 @@
// Copyright 2016-2018 AJ ONeal. All rights reserved
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
'use strict';
module.exports = function(bitlen, exp) {
var k = require('node-forge').pki.rsa.generateKeyPair({
bits: bitlen || 2048,
e: exp || 0x10001
}).privateKey;
var jwk = {
kty: 'RSA',
n: _toUrlBase64(k.n),
e: _toUrlBase64(k.e),
d: _toUrlBase64(k.d),
p: _toUrlBase64(k.p),
q: _toUrlBase64(k.q),
dp: _toUrlBase64(k.dP),
dq: _toUrlBase64(k.dQ),
qi: _toUrlBase64(k.qInv)
};
return {
private: jwk,
public: {
kty: jwk.kty,
n: jwk.n,
e: jwk.e
}
};
};
function _toUrlBase64(fbn) {
var hex = fbn.toRadix(16);
if (hex.length % 2) {
// Invalid hex string
hex = '0' + hex;
}
while ('00' === hex.slice(0, 2)) {
hex = hex.slice(2);
}
return Buffer.from(hex, 'hex')
.toString('base64')
.replace(/\+/g, '-')
.replace(/\//g, '_')
.replace(/=/g, '');
}
if (require.main === module) {
var keypair = module.exports(2048, 0x10001);
console.info(keypair.private);
console.warn(keypair.public);
//console.info(keypair.privateKeyJwk);
//console.warn(keypair.publicKeyJwk);
}

21
lib/node/generate-privkey-node.js

@ -1,21 +0,0 @@
// Copyright 2016-2018 AJ ONeal. All rights reserved
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
'use strict';
module.exports = function(bitlen, exp) {
var keypair = require('crypto').generateKeyPairSync('rsa', {
modulusLength: bitlen,
publicExponent: exp,
privateKeyEncoding: { type: 'pkcs1', format: 'pem' },
publicKeyEncoding: { type: 'pkcs1', format: 'pem' }
});
var result = { privateKeyPem: keypair.privateKey.trim() };
return result;
};
if (require.main === module) {
var keypair = module.exports(2048, 0x10001);
console.info(keypair.privateKeyPem);
}

27
lib/node/generate-privkey-ursa.js

@ -1,27 +0,0 @@
// Copyright 2016-2018 AJ ONeal. All rights reserved
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
'use strict';
module.exports = function(bitlen, exp) {
var ursa;
try {
ursa = require('ursa');
} catch (e) {
ursa = require('ursa-optional');
}
var keypair = ursa.generatePrivateKey(bitlen, exp);
var result = {
privateKeyPem: keypair
.toPrivatePem()
.toString('ascii')
.trim()
};
return result;
};
if (require.main === module) {
var keypair = module.exports(2048, 0x10001);
console.info(keypair.privateKeyPem);
}

90
lib/node/generate-privkey.js

@ -1,90 +0,0 @@
// Copyright 2016-2018 AJ ONeal. All rights reserved
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
'use strict';
var oldver = false;
module.exports = function(bitlen, exp) {
bitlen = parseInt(bitlen, 10) || 2048;
exp = parseInt(exp, 10) || 65537;
try {
return require('./generate-privkey-node.js')(bitlen, exp);
} catch (e) {
if (!/generateKeyPairSync is not a function/.test(e.message)) {
throw e;
}
try {
return require('./generate-privkey-ursa.js')(bitlen, exp);
} catch (e) {
if (e.code !== 'MODULE_NOT_FOUND') {
console.error(
"[rsa-compat] Unexpected error when using 'ursa':"
);
console.error(e);
}
if (!oldver) {
oldver = true;
console.warn(
'[WARN] rsa-compat: Your version of node does not have crypto.generateKeyPair()'
);
console.warn(
"[WARN] rsa-compat: Please update to node >= v10.12 or 'npm install --save ursa node-forge'"
);
console.warn(
'[WARN] rsa-compat: Using node-forge as a fallback may be unacceptably slow.'
);
if (/arm|mips/i.test(require('os').arch)) {
console.warn(
'================================================================'
);
console.warn(' WARNING');
console.warn(
'================================================================'
);
console.warn('');
console.warn(
'WARNING: You are generating an RSA key using pure JavaScript on'
);
console.warn(
' a VERY SLOW cpu. This could take DOZENS of minutes!'
);
console.warn('');
console.warn(
" We recommend installing node >= v10.12, or 'gcc' and 'ursa'"
);
console.warn('');
console.warn('EXAMPLE:');
console.warn('');
console.warn(
' sudo apt-get install build-essential && npm install ursa'
);
console.warn('');
console.warn(
'================================================================'
);
}
}
try {
return require('./generate-privkey-forge.js')(bitlen, exp);
} catch (e) {
if (e.code !== 'MODULE_NOT_FOUND') {
throw e;
}
console.error(
'[ERROR] rsa-compat: could not generate a private key.'
);
console.error(
'None of crypto.generateKeyPair, ursa, nor node-forge are present'
);
}
}
}
};
if (require.main === module) {
var keypair = module.exports(2048, 0x10001);
console.info(keypair.privateKeyPem);
}

84
lib/node/keypairs.js

@ -1,84 +0,0 @@
'use strict';
var Keypairs = module.exports;
var crypto = require('crypto');
Keypairs._sign = function(opts, payload) {
return Keypairs._import(opts).then(function(pem) {
payload = Buffer.from(payload);
// node specifies RSA-SHAxxx even when it's actually ecdsa (it's all encoded x509 shasums anyway)
// TODO opts.alg = (protect||header).alg
var nodeAlg = 'SHA' + Keypairs._getBits(opts);
var binsig = crypto
.createSign(nodeAlg)
.update(payload)
.sign(pem);
if ('EC' === opts.jwk.kty && !/x509|asn1/i.test(opts.format)) {
// ECDSA JWT signatures differ from "normal" ECDSA signatures
// https://tools.ietf.org/html/rfc7518#section-3.4
binsig = Keypairs._ecdsaAsn1SigToJoseSig(binsig);
}
return binsig;
});
};
Keypairs._import = function(opts) {
if (opts.pem && opts.jwk) {
return Promise.resolve(opts.pem);
} else {
// XXX added by caller
return Keypairs.export({ jwk: opts.jwk });
}
};
Keypairs._ecdsaAsn1SigToJoseSig = function(binsig) {
// should have asn1 sequence header of 0x30
if (0x30 !== binsig[0]) {
throw new Error('Impossible EC SHA head marker');
}
var index = 2; // first ecdsa "R" header byte
var len = binsig[1];
var lenlen = 0;
// Seek length of length if length is greater than 127 (i.e. two 512-bit / 64-byte R and S values)
if (0x80 & len) {
lenlen = len - 0x80; // should be exactly 1
len = binsig[2]; // should be <= 130 (two 64-bit SHA-512s, plus padding)
index += lenlen;
}
// should be of BigInt type
if (0x02 !== binsig[index]) {
throw new Error('Impossible EC SHA R marker');
}
index += 1;
var rlen = binsig[index];
var bits = 32;
if (rlen > 49) {
bits = 64;
} else if (rlen > 33) {
bits = 48;
}
var r = binsig.slice(index + 1, index + 1 + rlen).toString('hex');
var slen = binsig[index + 1 + rlen + 1]; // skip header and read length
var s = binsig.slice(index + 1 + rlen + 1 + 1).toString('hex');
if (2 * slen !== s.length) {
throw new Error('Impossible EC SHA S length');
}
// There may be one byte of padding on either
while (r.length < 2 * bits) {
r = '00' + r;
}
while (s.length < 2 * bits) {
s = '00' + s;
}
if (2 * (bits + 1) === r.length) {
r = r.slice(2);
}
if (2 * (bits + 1) === s.length) {
s = s.slice(2);
}
return Buffer.concat([Buffer.from(r, 'hex'), Buffer.from(s, 'hex')]);
};

154
lib/node/rsa.js

@ -1,154 +0,0 @@
'use strict';
var native = module.exports;
// XXX provided by caller: export
var RSA = native;
var PEM = require('../../pem.js');
var x509 = require('../../x509.js');
var ASN1 = require('../../asn1/parser.js');
native.generate = function(opts) {
opts.kty = 'RSA';
return native._generate(opts).then(function(pair) {
var format = opts.format;
var encoding = opts.encoding;
// The easy way
if ('json' === format && !encoding) {
format = 'jwk';
encoding = 'json';
}
if (
('jwk' === format || !format) &&
('json' === encoding || !encoding)
) {
return pair;
}
if ('jwk' === format || 'json' === encoding) {
throw new Error(
"format '" +
format +
"' is incompatible with encoding '" +
encoding +
"'"
);
}
// The... less easy way
/*
var priv;
var pub;
if ('spki' === format || 'pkcs8' === format) {
format = 'pkcs8';
pub = 'spki';
}
if ('pem' === format) {
format = 'pkcs1';
encoding = 'pem';
} else if ('der' === format) {
format = 'pkcs1';
encoding = 'der';
}
priv = format;
pub = pub || format;
if (!encoding) {
encoding = 'pem';
}
if (priv) {
priv = { type: priv, format: encoding };
pub = { type: pub, format: encoding };
} else {
// jwk
priv = { type: 'pkcs1', format: 'pem' };
pub = { type: 'pkcs1', format: 'pem' };
}
*/
if (('pem' === format || 'der' === format) && !encoding) {
encoding = format;
format = 'pkcs1';
}
var exOpts = { jwk: pair.private, format: format, encoding: encoding };
return RSA.export(exOpts).then(function(priv) {
exOpts.public = true;
if ('pkcs8' === exOpts.format) {
exOpts.format = 'spki';
}
return RSA.export(exOpts).then(function(pub) {
return { private: priv, public: pub };
});
});
});
};
native._generate = function(opts) {
if (!opts) {
opts = {};
}
return new Promise(function(resolve, reject) {
try {
var modlen = opts.modulusLength || 2048;
var exp = opts.publicExponent || 0x10001;
var pair = require('./generate-privkey.js')(modlen, exp);
if (pair.private) {
resolve(pair);
return;
}
pair = toJwks(pair);
resolve({ private: pair.private, public: pair.public });
} catch (e) {
reject(e);
}
});
};
// PKCS1 to JWK only
function toJwks(oldpair) {
var block = PEM.parseBlock(oldpair.privateKeyPem);
var asn1 = ASN1.parse(block.bytes);
var jwk = { kty: 'RSA', n: null, e: null };
jwk = x509.parsePkcs1(block.bytes, asn1, jwk);
return { private: jwk, public: RSA.neuter({ jwk: jwk }) };
}
// TODO
var Enc = require('@root/encoding/base64');
x509.parsePkcs1 = function parseRsaPkcs1(buf, asn1, jwk) {
if (
!asn1.children.every(function(el) {
return 0x02 === el.type;
})
) {
throw new Error(
'not an RSA PKCS#1 public or private key (not all ints)'
);
}
if (2 === asn1.children.length) {
jwk.n = Enc.bufToUrlBase64(asn1.children[0].value);
jwk.e = Enc.bufToUrlBase64(asn1.children[1].value);
return jwk;
} else if (asn1.children.length >= 9) {
// the standard allows for "otherPrimeInfos", hence at least 9
jwk.n = Enc.bufToUrlBase64(asn1.children[1].value);
jwk.e = Enc.bufToUrlBase64(asn1.children[2].value);
jwk.d = Enc.bufToUrlBase64(asn1.children[3].value);
jwk.p = Enc.bufToUrlBase64(asn1.children[4].value);
jwk.q = Enc.bufToUrlBase64(asn1.children[5].value);
jwk.dp = Enc.bufToUrlBase64(asn1.children[6].value);
jwk.dq = Enc.bufToUrlBase64(asn1.children[7].value);
jwk.qi = Enc.bufToUrlBase64(asn1.children[8].value);
return jwk;
} else {
throw new Error(
'not an RSA PKCS#1 public or private key (wrong number of ints)'
);
}
};

4844
package-lock.json

File diff suppressed because it is too large

11
package.json

@ -40,15 +40,20 @@
"author": "AJ ONeal <coolaj86@gmail.com> (https://coolaj86.com/)",
"license": "MPL-2.0",
"dependencies": {
"@root/csr": "^1.0.0-wip.0",
"@root/csr": "^0.8.0",
"@root/encoding": "^1.0.1",
"@root/keypairs": "^1.0.0-wip.0"
"@root/keypairs": "^0.9.0",
"@root/pem": "^1.0.4",
"@root/x509": "^0.7.2"
},
"devDependencies": {
"@root/request": "^1.3.10",
"dig.js": "^1.3.9",
"dns-suite": "^1.2.12",
"dotenv": "^8.1.0",
"punycode": "^1.4.1"
"eslint": "^6.5.1",
"punycode": "^1.4.1",
"webpack": "^4.41.0",
"webpack-cli": "^3.3.9"
}
}

158
rsa.js

@ -1,158 +0,0 @@
/*global Promise*/
'use strict';
var RSA = module.exports;
var native = require('./lib/node/rsa.js');
var x509 = require('./x509.js');
var PEM = require('./pem.js');
//var SSH = require('./ssh-keys.js');
var sha2 = require('./lib/node/sha2.js');
var Enc = require('@root/encoding/base64');
RSA._universal =
'Bluecrypt only supports crypto with standard cross-browser and cross-platform support.';
RSA._stance =
"We take the stance that if you're knowledgeable enough to" +
" properly and securely use non-standard crypto then you shouldn't need Bluecrypt anyway.";
native._stance = RSA._stance;
RSA.generate = native.generate;
// Chopping off the private parts is now part of the public API.
// I thought it sounded a little too crude at first, but it really is the best name in every possible way.
RSA.neuter = function(opts) {
// trying to find the best balance of an immutable copy with custom attributes
var jwk = {};
Object.keys(opts.jwk).forEach(function(k) {
if ('undefined' === typeof opts.jwk[k]) {
return;
}
// ignore RSA private parts
if (-1 !== ['d', 'p', 'q', 'dp', 'dq', 'qi'].indexOf(k)) {
return;
}
jwk[k] = JSON.parse(JSON.stringify(opts.jwk[k]));
});
return jwk;
};
native.neuter = RSA.neuter;
// https://stackoverflow.com/questions/42588786/how-to-fingerprint-a-jwk
RSA.__thumbprint = function(jwk) {
// Use the same entropy for SHA as for key
var len = Math.floor(jwk.n.length * 0.75);
var alg = 'SHA-256';
// TODO this may be a bug
// need to confirm that the padding is no more or less than 1 byte
if (len >= 511) {
alg = 'SHA-512';
} else if (len >= 383) {
alg = 'SHA-384';
}
return sha2
.sum(alg, '{"e":"' + jwk.e + '","kty":"RSA","n":"' + jwk.n + '"}')
.then(function(hash) {
return Enc.bufToUrlBase64(Uint8Array.from(hash));
});
};
RSA.thumbprint = function(opts) {
return Promise.resolve().then(function() {
var jwk;
if ('EC' === opts.kty) {
jwk = opts;
} else if (opts.jwk) {
jwk = opts.jwk;
} else {
return RSA.import(opts).then(function(jwk) {
return RSA.__thumbprint(jwk);
});
}
return RSA.__thumbprint(jwk);
});
};
RSA.export = function(opts) {
return Promise.resolve().then(function() {
if (!opts || !opts.jwk || 'object' !== typeof opts.jwk) {
throw new Error('must pass { jwk: jwk }');
}
var jwk = JSON.parse(JSON.stringify(opts.jwk));
var format = opts.format;
var pub = opts.public;
if (pub || -1 !== ['spki', 'pkix', 'ssh', 'rfc4716'].indexOf(format)) {
jwk = RSA.neuter({ jwk: jwk });
}
if ('RSA' !== jwk.kty) {
throw new Error("options.jwk.kty must be 'RSA' for RSA keys");
}
if (!jwk.p) {
// TODO test for n and e
pub = true;
if (!format || 'pkcs1' === format) {
format = 'pkcs1';
} else if (-1 !== ['spki', 'pkix'].indexOf(format)) {
format = 'spki';
} else if (-1 !== ['ssh', 'rfc4716'].indexOf(format)) {
format = 'ssh';
} else {
throw new Error(
"options.format must be 'spki', 'pkcs1', or 'ssh' for public RSA keys, not (" +
typeof format +
') ' +
format
);
}
} else {
// TODO test for all necessary keys (d, p, q ...)
if (!format || 'pkcs1' === format) {
format = 'pkcs1';
} else if ('pkcs8' !== format) {
throw new Error(
"options.format must be 'pkcs1' or 'pkcs8' for private RSA keys"
);
}
}
if ('pkcs1' === format) {
if (jwk.d) {
return PEM.packBlock({
type: 'RSA PRIVATE KEY',
bytes: x509.packPkcs1(jwk)
});
} else {
return PEM.packBlock({
type: 'RSA PUBLIC KEY',
bytes: x509.packPkcs1(jwk)
});
}
} else if ('pkcs8' === format) {
return PEM.packBlock({
type: 'PRIVATE KEY',
bytes: x509.packPkcs8(jwk)
});
} else if (-1 !== ['spki', 'pkix'].indexOf(format)) {
return PEM.packBlock({
type: 'PUBLIC KEY',
bytes: x509.packSpki(jwk)
});
} else if (-1 !== ['ssh', 'rfc4716'].indexOf(format)) {
return SSH.pack({ jwk: jwk, comment: opts.comment });
} else {
throw new Error(
'Sanity Error: reached unreachable code block with format: ' +
format
);
}
});
};
native.export = RSA.export;
RSA.pack = function(opts) {
// wrapped in a promise for API compatibility
// with the forthcoming browser version
// (and potential future native node capability)
return Promise.resolve().then(function() {
return RSA.export(opts);
});
};

4
tests/index.js

@ -4,10 +4,10 @@ require('dotenv').config();
var CSR = require('@root/csr');
var Enc = require('@root/encoding/base64');
var PEM = require('../pem.js');
var PEM = require('@root/pem');
var punycode = require('punycode');
var ACME = require('../acme.js');
var Keypairs = require('../keypairs.js');
var Keypairs = require('@root/keypairs');
var acme = ACME.create({
// debug: true
});

Loading…
Cancel
Save