v0.9.0: lightweight ecdsa / rsa key generation, conversion, and signing
This commit is contained in:
parent
6d0ab30620
commit
e325694ed6
46
README.md
46
README.md
|
@ -259,3 +259,49 @@ 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/)
|
||||
|
||||
# Contributions
|
||||
|
||||
Did this project save you some time? Maybe make your day? Even save the day?
|
||||
|
||||
Please say "thanks" via Paypal or Patreon:
|
||||
|
||||
- Paypal: [\$5](https://paypal.me/rootprojects/5) | [\$10](https://paypal.me/rootprojects/10) | Any amount: <paypal@therootcompany.com>
|
||||
- Patreon: <https://patreon.com/rootprojects>
|
||||
|
||||
Where does your contribution go?
|
||||
|
||||
[Root](https://therootcompany.com) is a collection of experts
|
||||
who trust each other and enjoy working together on deep-tech,
|
||||
Indie Web projects.
|
||||
|
||||
Our goal is to operate as a sustainable community.
|
||||
|
||||
Your contributions - both in code and _especially_ monetarily -
|
||||
help to not just this project, but also our broader work
|
||||
of [projects](https://rootprojects.org) that fuel the **Indie Web**.
|
||||
|
||||
Also, we chat on [Keybase](https://keybase.io)
|
||||
in [#rootprojects](https://keybase.io/team/rootprojects)
|
||||
|
||||
# Commercial Support
|
||||
|
||||
Do you need...
|
||||
|
||||
- more features?
|
||||
- bugfixes, on _your_ timeline?
|
||||
- custom code, built by experts?
|
||||
- commercial support and licensing?
|
||||
|
||||
<!-- Please visit <https://therootcompany.com> or contact -->
|
||||
|
||||
Contact <aj@therootcompany.com> for support options.
|
||||
|
||||
# Legal
|
||||
|
||||
Copyright [AJ ONeal](https://coolaj86.com),
|
||||
[Root](https://therootcompany.com) 2018-2019
|
||||
|
||||
MPL-2.0 |
|
||||
[Terms of Use](https://therootcompany.com/legal/#terms) |
|
||||
[Privacy Policy](https://therootcompany.com/legal/#privacy)
|
||||
|
|
|
@ -0,0 +1,81 @@
|
|||
#!/usr/bin/env node
|
||||
'use strict';
|
||||
|
||||
var fs = require('fs');
|
||||
var Eckles = require('../ecdsa');
|
||||
|
||||
var infile = process.argv[2];
|
||||
var format = process.argv[3];
|
||||
|
||||
if (!infile) {
|
||||
infile = 'jwk';
|
||||
}
|
||||
|
||||
if (
|
||||
-1 !==
|
||||
['jwk', 'pem', 'json', 'der', 'sec1', 'pkcs8', 'spki', 'ssh'].indexOf(
|
||||
infile
|
||||
)
|
||||
) {
|
||||
console.log('Generating new key...');
|
||||
Eckles.generate({
|
||||
format: infile,
|
||||
namedCurve: format === 'P-384' ? 'P-384' : 'P-256',
|
||||
encoding: format === 'der' ? 'der' : 'pem'
|
||||
})
|
||||
.then(function(key) {
|
||||
if ('der' === infile || 'der' === format) {
|
||||
key.private = key.private.toString('binary');
|
||||
key.public = key.public.toString('binary');
|
||||
}
|
||||
console.log(key.private);
|
||||
console.log(key.public);
|
||||
})
|
||||
.catch(function(err) {
|
||||
console.error(err);
|
||||
process.exit(1);
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
var key = fs.readFileSync(infile, 'ascii');
|
||||
|
||||
try {
|
||||
key = JSON.parse(key);
|
||||
} catch (e) {
|
||||
// ignore
|
||||
}
|
||||
|
||||
var thumbprint = 'thumbprint' === format;
|
||||
if (thumbprint) {
|
||||
format = 'public';
|
||||
}
|
||||
|
||||
if ('string' === typeof key) {
|
||||
if (thumbprint) {
|
||||
Eckles.thumbprint({ pem: key }).then(console.log);
|
||||
return;
|
||||
}
|
||||
var pub = -1 !== ['public', 'spki', 'pkix'].indexOf(format);
|
||||
Eckles.import({ pem: key, public: pub || format })
|
||||
.then(function(jwk) {
|
||||
console.log(JSON.stringify(jwk, null, 2));
|
||||
})
|
||||
.catch(function(err) {
|
||||
console.error(err);
|
||||
process.exit(1);
|
||||
});
|
||||
} else {
|
||||
if (thumbprint) {
|
||||
Eckles.thumbprint({ jwk: key }).then(console.log);
|
||||
return;
|
||||
}
|
||||
Eckles.export({ jwk: key, format: format })
|
||||
.then(function(pem) {
|
||||
console.log(pem);
|
||||
})
|
||||
.catch(function(err) {
|
||||
console.error(err);
|
||||
process.exit(2);
|
||||
});
|
||||
}
|
|
@ -0,0 +1,126 @@
|
|||
#!/usr/bin/env node
|
||||
'use strict';
|
||||
|
||||
var fs = require('fs');
|
||||
var Rasha = require('../rsa');
|
||||
var PEM = require('@root/pem');
|
||||
var ASN1 = require('@root/asn1');
|
||||
|
||||
var infile = process.argv[2];
|
||||
var format = process.argv[3];
|
||||
var msg = process.argv[4];
|
||||
var sign;
|
||||
if ('sign' === format) {
|
||||
sign = true;
|
||||
format = 'pkcs8';
|
||||
}
|
||||
|
||||
if (!infile) {
|
||||
infile = 'jwk';
|
||||
}
|
||||
|
||||
if (
|
||||
-1 !==
|
||||
['jwk', 'pem', 'json', 'der', 'pkcs1', 'pkcs8', 'spki'].indexOf(infile)
|
||||
) {
|
||||
console.info('Generating new key...');
|
||||
Rasha.generate({
|
||||
format: infile,
|
||||
modulusLength: parseInt(format, 10) || 2048,
|
||||
encoding: parseInt(format, 10) ? null : format
|
||||
})
|
||||
.then(function(key) {
|
||||
if ('der' === infile || 'der' === format) {
|
||||
key.private = key.private.toString('binary');
|
||||
key.public = key.public.toString('binary');
|
||||
}
|
||||
console.info(key.private);
|
||||
console.info(key.public);
|
||||
})
|
||||
.catch(function(err) {
|
||||
console.error(err);
|
||||
process.exit(1);
|
||||
});
|
||||
return;
|
||||
}
|
||||
var key = fs.readFileSync(infile, 'ascii');
|
||||
|
||||
try {
|
||||
key = JSON.parse(key);
|
||||
} catch (e) {
|
||||
// ignore
|
||||
}
|
||||
|
||||
var thumbprint = 'thumbprint' === format;
|
||||
if (thumbprint) {
|
||||
format = 'public';
|
||||
}
|
||||
|
||||
if ('string' === typeof key) {
|
||||
if (thumbprint) {
|
||||
Rasha.thumbprint({ pem: key }).then(console.info);
|
||||
return;
|
||||
}
|
||||
if ('tpl' === format) {
|
||||
var block = PEM.parseBlock(key);
|
||||
var asn1 = ASN1.parse(block.der);
|
||||
ASN1.tpl(asn1);
|
||||
return;
|
||||
}
|
||||
if (sign) {
|
||||
signMessage(key, msg);
|
||||
return;
|
||||
}
|
||||
|
||||
var pub = -1 !== ['public', 'spki', 'pkix'].indexOf(format);
|
||||
Rasha.import({ pem: key, public: pub || format })
|
||||
.then(function(jwk) {
|
||||
console.info(JSON.stringify(jwk, null, 2));
|
||||
})
|
||||
.catch(function(err) {
|
||||
console.error(err);
|
||||
process.exit(1);
|
||||
});
|
||||
} else {
|
||||
if (thumbprint) {
|
||||
Rasha.thumbprint({ jwk: key }).then(console.info);
|
||||
return;
|
||||
}
|
||||
Rasha.export({ jwk: key, format: format })
|
||||
.then(function(pem) {
|
||||
if (sign) {
|
||||
signMessage(pem, msg);
|
||||
return;
|
||||
}
|
||||
console.info(pem);
|
||||
})
|
||||
.catch(function(err) {
|
||||
console.error(err);
|
||||
process.exit(2);
|
||||
});
|
||||
}
|
||||
|
||||
function signMessage(pem, name) {
|
||||
var msg;
|
||||
try {
|
||||
msg = fs.readFileSync(name);
|
||||
} catch (e) {
|
||||
console.warn(
|
||||
'[info] input string did not exist as a file, signing the string itself'
|
||||
);
|
||||
msg = Buffer.from(name, 'binary');
|
||||
}
|
||||
var crypto = require('crypto');
|
||||
var sign = crypto.createSign('SHA256');
|
||||
sign.write(msg);
|
||||
sign.end();
|
||||
var buf = sign.sign(pem);
|
||||
console.info(buf.toString('base64'));
|
||||
/*
|
||||
Rasha.sign({ pem: pem, message: msg, alg: 'SHA256' }).then(function (sig) {
|
||||
}).catch(function () {
|
||||
console.error(err);
|
||||
process.exit(3);
|
||||
});
|
||||
*/
|
||||
}
|
|
@ -0,0 +1,240 @@
|
|||
/*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('@root/x509');
|
||||
var PEM = require('@root/pem');
|
||||
//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);
|
||||
throw new Error('SSH not yet re-supported');
|
||||
}
|
||||
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);
|
||||
});
|
||||
};
|
|
@ -0,0 +1,7 @@
|
|||
{
|
||||
"kty": "EC",
|
||||
"crv": "P-256",
|
||||
"d": "iYydo27aNGO9DBUWeGEPD8oNi1LZDqfxPmQlieLBjVQ",
|
||||
"x": "IT1SWLxsacPiE5Z16jkopAn8_-85rMjgyCokrnjDft4",
|
||||
"y": "mP2JwOAOdMmXuwpxbKng3KZz27mz-nKWIlXJ3rzSGMo"
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
-----BEGIN PRIVATE KEY-----
|
||||
MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgiYydo27aNGO9DBUW
|
||||
eGEPD8oNi1LZDqfxPmQlieLBjVShRANCAAQhPVJYvGxpw+ITlnXqOSikCfz/7zms
|
||||
yODIKiSueMN+3pj9icDgDnTJl7sKcWyp4Nymc9u5s/pyliJVyd680hjK
|
||||
-----END PRIVATE KEY-----
|
|
@ -0,0 +1,5 @@
|
|||
-----BEGIN EC PRIVATE KEY-----
|
||||
MHcCAQEEIImMnaNu2jRjvQwVFnhhDw/KDYtS2Q6n8T5kJYniwY1UoAoGCCqGSM49
|
||||
AwEHoUQDQgAEIT1SWLxsacPiE5Z16jkopAn8/+85rMjgyCokrnjDft6Y/YnA4A50
|
||||
yZe7CnFsqeDcpnPbubP6cpYiVcnevNIYyg==
|
||||
-----END EC PRIVATE KEY-----
|
|
@ -0,0 +1,7 @@
|
|||
{
|
||||
"kty": "EC",
|
||||
"crv": "P-384",
|
||||
"d": "XlyuCEWSTTS8U79O_Mz05z18vh4kb10szvu_7pdXuGWV6lfEyPExyUYWsA6A2kdV",
|
||||
"x": "2zEU0bKCa7ejKLIJ8oPGnLhqhxyiv4_w38K2a0SPC6dsSd9_glNJ8lcqv0sff5Gb",
|
||||
"y": "VD4jnu83S6scn6_TeAj3EZOREGbOs6dzoVpaugn-XQMMyC9O4VLbDDFGBZTJlMsb"
|
||||
}
|
|
@ -0,0 +1,6 @@
|
|||
-----BEGIN PRIVATE KEY-----
|
||||
MIG2AgEAMBAGByqGSM49AgEGBSuBBAAiBIGeMIGbAgEBBDBeXK4IRZJNNLxTv078
|
||||
zPTnPXy+HiRvXSzO+7/ul1e4ZZXqV8TI8THJRhawDoDaR1WhZANiAATbMRTRsoJr
|
||||
t6Mosgnyg8acuGqHHKK/j/DfwrZrRI8Lp2xJ33+CU0nyVyq/Sx9/kZtUPiOe7zdL
|
||||
qxyfr9N4CPcRk5EQZs6zp3OhWlq6Cf5dAwzIL07hUtsMMUYFlMmUyxs=
|
||||
-----END PRIVATE KEY-----
|
|
@ -0,0 +1,6 @@
|
|||
-----BEGIN EC PRIVATE KEY-----
|
||||
MIGkAgEBBDBeXK4IRZJNNLxTv078zPTnPXy+HiRvXSzO+7/ul1e4ZZXqV8TI8THJ
|
||||
RhawDoDaR1WgBwYFK4EEACKhZANiAATbMRTRsoJrt6Mosgnyg8acuGqHHKK/j/Df
|
||||
wrZrRI8Lp2xJ33+CU0nyVyq/Sx9/kZtUPiOe7zdLqxyfr9N4CPcRk5EQZs6zp3Oh
|
||||
Wlq6Cf5dAwzIL07hUtsMMUYFlMmUyxs=
|
||||
-----END EC PRIVATE KEY-----
|
|
@ -0,0 +1,11 @@
|
|||
{
|
||||
"kty": "RSA",
|
||||
"n": "m2ttVBxPlWw06ZmGBWVDlfjkPAJ4DgnY0TrDwtCohHzLxGhDNzUJefLukC-xu0LBKylYojT5vTkxaOhxeSYo31syu4WhxbkTBLICOFcCGMob6pSQ38P8LdAIlb0pqDHxEJ9adWomjuFf0SUhN1cP7s9m8Yk9trkpEqjskocn2BOnTB57qAZM6-I70on0_iDZm7-jcqOPgADAmbWHhy67BXkk4yy_YzD4yOGZFXZcNp915_TW5bRd__AKPHUHxJasPiyEFqlNKBR2DSD-LbX5eTmzCh2ikrwTMja7mUdBJf2bK3By5AB0Qi49OykUCfNZeQlEz7UNNj9RGps_50-CNw",
|
||||
"e": "AQAB",
|
||||
"d": "Cpfo7Mm9Nu8YMC_xrZ54W9mKHPkCG9rZ93Ds9PNp-RXUgb-ljTbFPZWsYxGNKLllFz8LNosr1pT2ZDMrwNk0Af1iWNvD6gkyXaiQdCyiDPSBsJyNv2LJZon-e85X74nv53UlIkmo9SYxdLz2JaJ-iIWEe8Qh-7llLktrTJV_xr98_tbhgSppz_IeOymq3SEZaQHM8pTU7w7XvCj2pb9r8fN0M0XcgWZIaf3LGEfkhF_WtX67XJ0C6-LbkT51jtlLRNGX6haGdscXS0OWWjKOJzKGuV-NbthEn5rmRtVnjRZ3yaxQ0ud8vC-NONn7yvGUlOur1IdDzJ_YfHPt9sHMQQ",
|
||||
"p": "ynG-t9HwKCN3MWRYFdnFzi9-02Qcy3p8B5pu3ary2E70hYn2pHlUG2a9BNE8c5xHQ3Hx43WoWf6s0zOunPV1G28LkU_UYEbAtPv_PxSmzpQp9n9XnYvBLBF8Y3z7gxgLn1vVFNARrQdRtj87qY3aw7E9S4DsGcAarIuOT2TsTCE",
|
||||
"q": "xIkAjgUzB1zaUzJtW2Zgvp9cYYr1DmpH30ePZl3c_8397_DZDDo46fnFYjs6uPa03HpmKUnbjwr14QHlfXlntJBEuXxcqLjkdKdJ4ob7xueLTK4suo9V8LSrkLChVxlZQwnFD2E5ll0sVeeDeMJHQw38ahSrBFEVnxjpnPh1Q1c",
|
||||
"dp": "tzDGjECFOU0ehqtuqhcuT63a7h8hj19-7MJqoFwY9HQ-ALkfXyYLXeBSGxHbyiIYuodZg6LsfMNgUJ3r3Eyhc_nAVfYPEC_2IdAG4WYmq7iXYF9LQV09qEsKbFykm7QekE3hO7wswo5k-q2tp3ieBYdVGAXJoGOdv5VpaZ7B1QE",
|
||||
"dq": "kh5dyDk7YCz7sUFbpsmuAeuPjoH2ghooh2u3xN7iUVmAg-ToKjwbVnG5-7eXiC779rQVwnrD_0yh1AFJ8wjRPqDIR7ObXGHikIxT1VSQWqiJm6AfZzDsL0LUD4YS3iPdhob7-NxLKWzqao_u4lhnDQaX9PKa12HFlny6K1daL48",
|
||||
"qi": "AlHWbx1gp6Z9pbw_1hlS7HuXAgWoX7IjbTUelldf4gkriDWLOrj3QCZcO4ZvZvEwJhVlsny9LO8IkbwGJEL6cXraK08ByVS2mwQyflgTgGNnpzixyEUL_mrQLx6y145FHcxfeqNInMhep-0Mxn1D5nlhmIOgRApS0t9VoXtHhFU"
|
||||
}
|
|
@ -0,0 +1,27 @@
|
|||
-----BEGIN RSA PRIVATE KEY-----
|
||||
MIIEpAIBAAKCAQEAm2ttVBxPlWw06ZmGBWVDlfjkPAJ4DgnY0TrDwtCohHzLxGhD
|
||||
NzUJefLukC+xu0LBKylYojT5vTkxaOhxeSYo31syu4WhxbkTBLICOFcCGMob6pSQ
|
||||
38P8LdAIlb0pqDHxEJ9adWomjuFf0SUhN1cP7s9m8Yk9trkpEqjskocn2BOnTB57
|
||||
qAZM6+I70on0/iDZm7+jcqOPgADAmbWHhy67BXkk4yy/YzD4yOGZFXZcNp915/TW
|
||||
5bRd//AKPHUHxJasPiyEFqlNKBR2DSD+LbX5eTmzCh2ikrwTMja7mUdBJf2bK3By
|
||||
5AB0Qi49OykUCfNZeQlEz7UNNj9RGps/50+CNwIDAQABAoIBAAqX6OzJvTbvGDAv
|
||||
8a2eeFvZihz5Ahva2fdw7PTzafkV1IG/pY02xT2VrGMRjSi5ZRc/CzaLK9aU9mQz
|
||||
K8DZNAH9Yljbw+oJMl2okHQsogz0gbCcjb9iyWaJ/nvOV++J7+d1JSJJqPUmMXS8
|
||||
9iWifoiFhHvEIfu5ZS5La0yVf8a/fP7W4YEqac/yHjspqt0hGWkBzPKU1O8O17wo
|
||||
9qW/a/HzdDNF3IFmSGn9yxhH5IRf1rV+u1ydAuvi25E+dY7ZS0TRl+oWhnbHF0tD
|
||||
lloyjicyhrlfjW7YRJ+a5kbVZ40Wd8msUNLnfLwvjTjZ+8rxlJTrq9SHQ8yf2Hxz
|
||||
7fbBzEECgYEAynG+t9HwKCN3MWRYFdnFzi9+02Qcy3p8B5pu3ary2E70hYn2pHlU
|
||||
G2a9BNE8c5xHQ3Hx43WoWf6s0zOunPV1G28LkU/UYEbAtPv/PxSmzpQp9n9XnYvB
|
||||
LBF8Y3z7gxgLn1vVFNARrQdRtj87qY3aw7E9S4DsGcAarIuOT2TsTCECgYEAxIkA
|
||||
jgUzB1zaUzJtW2Zgvp9cYYr1DmpH30ePZl3c/8397/DZDDo46fnFYjs6uPa03Hpm
|
||||
KUnbjwr14QHlfXlntJBEuXxcqLjkdKdJ4ob7xueLTK4suo9V8LSrkLChVxlZQwnF
|
||||
D2E5ll0sVeeDeMJHQw38ahSrBFEVnxjpnPh1Q1cCgYEAtzDGjECFOU0ehqtuqhcu
|
||||
T63a7h8hj19+7MJqoFwY9HQ+ALkfXyYLXeBSGxHbyiIYuodZg6LsfMNgUJ3r3Eyh
|
||||
c/nAVfYPEC/2IdAG4WYmq7iXYF9LQV09qEsKbFykm7QekE3hO7wswo5k+q2tp3ie
|
||||
BYdVGAXJoGOdv5VpaZ7B1QECgYEAkh5dyDk7YCz7sUFbpsmuAeuPjoH2ghooh2u3
|
||||
xN7iUVmAg+ToKjwbVnG5+7eXiC779rQVwnrD/0yh1AFJ8wjRPqDIR7ObXGHikIxT
|
||||
1VSQWqiJm6AfZzDsL0LUD4YS3iPdhob7+NxLKWzqao/u4lhnDQaX9PKa12HFlny6
|
||||
K1daL48CgYACUdZvHWCnpn2lvD/WGVLse5cCBahfsiNtNR6WV1/iCSuINYs6uPdA
|
||||
Jlw7hm9m8TAmFWWyfL0s7wiRvAYkQvpxetorTwHJVLabBDJ+WBOAY2enOLHIRQv+
|
||||
atAvHrLXjkUdzF96o0icyF6n7QzGfUPmeWGYg6BEClLS31Whe0eEVQ==
|
||||
-----END RSA PRIVATE KEY-----
|
|
@ -0,0 +1,28 @@
|
|||
-----BEGIN PRIVATE KEY-----
|
||||
MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQCba21UHE+VbDTp
|
||||
mYYFZUOV+OQ8AngOCdjROsPC0KiEfMvEaEM3NQl58u6QL7G7QsErKViiNPm9OTFo
|
||||
6HF5JijfWzK7haHFuRMEsgI4VwIYyhvqlJDfw/wt0AiVvSmoMfEQn1p1aiaO4V/R
|
||||
JSE3Vw/uz2bxiT22uSkSqOyShyfYE6dMHnuoBkzr4jvSifT+INmbv6Nyo4+AAMCZ
|
||||
tYeHLrsFeSTjLL9jMPjI4ZkVdlw2n3Xn9NbltF3/8Ao8dQfElqw+LIQWqU0oFHYN
|
||||
IP4ttfl5ObMKHaKSvBMyNruZR0El/ZsrcHLkAHRCLj07KRQJ81l5CUTPtQ02P1Ea
|
||||
mz/nT4I3AgMBAAECggEACpfo7Mm9Nu8YMC/xrZ54W9mKHPkCG9rZ93Ds9PNp+RXU
|
||||
gb+ljTbFPZWsYxGNKLllFz8LNosr1pT2ZDMrwNk0Af1iWNvD6gkyXaiQdCyiDPSB
|
||||
sJyNv2LJZon+e85X74nv53UlIkmo9SYxdLz2JaJ+iIWEe8Qh+7llLktrTJV/xr98
|
||||
/tbhgSppz/IeOymq3SEZaQHM8pTU7w7XvCj2pb9r8fN0M0XcgWZIaf3LGEfkhF/W
|
||||
tX67XJ0C6+LbkT51jtlLRNGX6haGdscXS0OWWjKOJzKGuV+NbthEn5rmRtVnjRZ3
|
||||
yaxQ0ud8vC+NONn7yvGUlOur1IdDzJ/YfHPt9sHMQQKBgQDKcb630fAoI3cxZFgV
|
||||
2cXOL37TZBzLenwHmm7dqvLYTvSFifakeVQbZr0E0TxznEdDcfHjdahZ/qzTM66c
|
||||
9XUbbwuRT9RgRsC0+/8/FKbOlCn2f1edi8EsEXxjfPuDGAufW9UU0BGtB1G2Pzup
|
||||
jdrDsT1LgOwZwBqsi45PZOxMIQKBgQDEiQCOBTMHXNpTMm1bZmC+n1xhivUOakff
|
||||
R49mXdz/zf3v8NkMOjjp+cViOzq49rTcemYpSduPCvXhAeV9eWe0kES5fFyouOR0
|
||||
p0nihvvG54tMriy6j1XwtKuQsKFXGVlDCcUPYTmWXSxV54N4wkdDDfxqFKsEURWf
|
||||
GOmc+HVDVwKBgQC3MMaMQIU5TR6Gq26qFy5PrdruHyGPX37swmqgXBj0dD4AuR9f
|
||||
Jgtd4FIbEdvKIhi6h1mDoux8w2BQnevcTKFz+cBV9g8QL/Yh0AbhZiaruJdgX0tB
|
||||
XT2oSwpsXKSbtB6QTeE7vCzCjmT6ra2neJ4Fh1UYBcmgY52/lWlpnsHVAQKBgQCS
|
||||
Hl3IOTtgLPuxQVumya4B64+OgfaCGiiHa7fE3uJRWYCD5OgqPBtWcbn7t5eILvv2
|
||||
tBXCesP/TKHUAUnzCNE+oMhHs5tcYeKQjFPVVJBaqImboB9nMOwvQtQPhhLeI92G
|
||||
hvv43EspbOpqj+7iWGcNBpf08prXYcWWfLorV1ovjwKBgAJR1m8dYKemfaW8P9YZ
|
||||
Uux7lwIFqF+yI201HpZXX+IJK4g1izq490AmXDuGb2bxMCYVZbJ8vSzvCJG8BiRC
|
||||
+nF62itPAclUtpsEMn5YE4BjZ6c4schFC/5q0C8esteORR3MX3qjSJzIXqftDMZ9
|
||||
Q+Z5YZiDoEQKUtLfVaF7R4RV
|
||||
-----END PRIVATE KEY-----
|
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"kty": "EC",
|
||||
"crv": "P-256",
|
||||
"x": "IT1SWLxsacPiE5Z16jkopAn8_-85rMjgyCokrnjDft4",
|
||||
"y": "mP2JwOAOdMmXuwpxbKng3KZz27mz-nKWIlXJ3rzSGMo"
|
||||
}
|
|
@ -0,0 +1,4 @@
|
|||
-----BEGIN PUBLIC KEY-----
|
||||
MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEIT1SWLxsacPiE5Z16jkopAn8/+85
|
||||
rMjgyCokrnjDft6Y/YnA4A50yZe7CnFsqeDcpnPbubP6cpYiVcnevNIYyg==
|
||||
-----END PUBLIC KEY-----
|
|
@ -0,0 +1 @@
|
|||
ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBCE9Uli8bGnD4hOWdeo5KKQJ/P/vOazI4MgqJK54w37emP2JwOAOdMmXuwpxbKng3KZz27mz+nKWIlXJ3rzSGMo= P-256@localhost
|
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"kty": "EC",
|
||||
"crv": "P-384",
|
||||
"x": "2zEU0bKCa7ejKLIJ8oPGnLhqhxyiv4_w38K2a0SPC6dsSd9_glNJ8lcqv0sff5Gb",
|
||||
"y": "VD4jnu83S6scn6_TeAj3EZOREGbOs6dzoVpaugn-XQMMyC9O4VLbDDFGBZTJlMsb"
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
-----BEGIN PUBLIC KEY-----
|
||||
MHYwEAYHKoZIzj0CAQYFK4EEACIDYgAE2zEU0bKCa7ejKLIJ8oPGnLhqhxyiv4/w
|
||||
38K2a0SPC6dsSd9/glNJ8lcqv0sff5GbVD4jnu83S6scn6/TeAj3EZOREGbOs6dz
|
||||
oVpaugn+XQMMyC9O4VLbDDFGBZTJlMsb
|
||||
-----END PUBLIC KEY-----
|
|
@ -0,0 +1 @@
|
|||
ecdsa-sha2-nistp384 AAAAE2VjZHNhLXNoYTItbmlzdHAzODQAAAAIbmlzdHAzODQAAABhBNsxFNGygmu3oyiyCfKDxpy4aoccor+P8N/CtmtEjwunbEnff4JTSfJXKr9LH3+Rm1Q+I57vN0urHJ+v03gI9xGTkRBmzrOnc6FaWroJ/l0DDMgvTuFS2wwxRgWUyZTLGw== P-384@localhost
|
|
@ -0,0 +1,5 @@
|
|||
{
|
||||
"kty": "RSA",
|
||||
"n": "m2ttVBxPlWw06ZmGBWVDlfjkPAJ4DgnY0TrDwtCohHzLxGhDNzUJefLukC-xu0LBKylYojT5vTkxaOhxeSYo31syu4WhxbkTBLICOFcCGMob6pSQ38P8LdAIlb0pqDHxEJ9adWomjuFf0SUhN1cP7s9m8Yk9trkpEqjskocn2BOnTB57qAZM6-I70on0_iDZm7-jcqOPgADAmbWHhy67BXkk4yy_YzD4yOGZFXZcNp915_TW5bRd__AKPHUHxJasPiyEFqlNKBR2DSD-LbX5eTmzCh2ikrwTMja7mUdBJf2bK3By5AB0Qi49OykUCfNZeQlEz7UNNj9RGps_50-CNw",
|
||||
"e": "AQAB"
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
-----BEGIN RSA PUBLIC KEY-----
|
||||
MIIBCgKCAQEAm2ttVBxPlWw06ZmGBWVDlfjkPAJ4DgnY0TrDwtCohHzLxGhDNzUJ
|
||||
efLukC+xu0LBKylYojT5vTkxaOhxeSYo31syu4WhxbkTBLICOFcCGMob6pSQ38P8
|
||||
LdAIlb0pqDHxEJ9adWomjuFf0SUhN1cP7s9m8Yk9trkpEqjskocn2BOnTB57qAZM
|
||||
6+I70on0/iDZm7+jcqOPgADAmbWHhy67BXkk4yy/YzD4yOGZFXZcNp915/TW5bRd
|
||||
//AKPHUHxJasPiyEFqlNKBR2DSD+LbX5eTmzCh2ikrwTMja7mUdBJf2bK3By5AB0
|
||||
Qi49OykUCfNZeQlEz7UNNj9RGps/50+CNwIDAQAB
|
||||
-----END RSA PUBLIC KEY-----
|
|
@ -0,0 +1,9 @@
|
|||
-----BEGIN PUBLIC KEY-----
|
||||
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAm2ttVBxPlWw06ZmGBWVD
|
||||
lfjkPAJ4DgnY0TrDwtCohHzLxGhDNzUJefLukC+xu0LBKylYojT5vTkxaOhxeSYo
|
||||
31syu4WhxbkTBLICOFcCGMob6pSQ38P8LdAIlb0pqDHxEJ9adWomjuFf0SUhN1cP
|
||||
7s9m8Yk9trkpEqjskocn2BOnTB57qAZM6+I70on0/iDZm7+jcqOPgADAmbWHhy67
|
||||
BXkk4yy/YzD4yOGZFXZcNp915/TW5bRd//AKPHUHxJasPiyEFqlNKBR2DSD+LbX5
|
||||
eTmzCh2ikrwTMja7mUdBJf2bK3By5AB0Qi49OykUCfNZeQlEz7UNNj9RGps/50+C
|
||||
NwIDAQAB
|
||||
-----END PUBLIC KEY-----
|
|
@ -0,0 +1 @@
|
|||
ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQCba21UHE+VbDTpmYYFZUOV+OQ8AngOCdjROsPC0KiEfMvEaEM3NQl58u6QL7G7QsErKViiNPm9OTFo6HF5JijfWzK7haHFuRMEsgI4VwIYyhvqlJDfw/wt0AiVvSmoMfEQn1p1aiaO4V/RJSE3Vw/uz2bxiT22uSkSqOyShyfYE6dMHnuoBkzr4jvSifT+INmbv6Nyo4+AAMCZtYeHLrsFeSTjLL9jMPjI4ZkVdlw2n3Xn9NbltF3/8Ao8dQfElqw+LIQWqU0oFHYNIP4ttfl5ObMKHaKSvBMyNruZR0El/ZsrcHLkAHRCLj07KRQJ81l5CUTPtQ02P1Eamz/nT4I3 rsa@localhost
|
409
keypairs.js
409
keypairs.js
|
@ -1,3 +1,410 @@
|
|||
/*global Promise*/
|
||||
'use strict';
|
||||
|
||||
module.exports = require('@root/acme/keypairs');
|
||||
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.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) {
|
||||
return Keypairs.generate(opts).then(function(pair) {
|
||||
pair.parseError = e;
|
||||
return pair;
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
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.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 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, '');
|
||||
}
|
||||
if (opts.protected && opts.protected.alg) {
|
||||
return opts.protected.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;
|
||||
}
|
||||
|
|
|
@ -0,0 +1,57 @@
|
|||
'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 })
|
||||
};
|
||||
});
|
||||
});
|
||||
};
|
|
@ -0,0 +1,108 @@
|
|||
'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';
|
||||
}
|
||||
};
|
|
@ -0,0 +1,59 @@
|
|||
'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 })
|
||||
};
|
||||
});
|
||||
});
|
||||
};
|
|
@ -0,0 +1,13 @@
|
|||
'use strict';
|
||||
|
||||
var sha2 = module.exports;
|
||||
|
||||
var encoder = new TextEncoder();
|
||||
sha2.sum = function(alg, str) {
|
||||
var data = str;
|
||||
if ('string' === typeof data) {
|
||||
data = encoder.encode(str);
|
||||
}
|
||||
var sha = 'SHA-' + String(alg).replace(/^sha-?/i, '');
|
||||
return window.crypto.subtle.digest(sha, data);
|
||||
};
|
|
@ -0,0 +1,113 @@
|
|||
'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
|
||||
};
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
|
@ -0,0 +1,55 @@
|
|||
// 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< |