v0.7.0: pem-to-jwk and ssh-to-jwk support complete 💯
This commit is contained in:
parent
5882ce82e7
commit
572edcf75b
5 changed files with 331 additions and 14 deletions
184
README.md
184
README.md
|
@ -1,23 +1,191 @@
|
||||||
Placeholder
|
Rasha.js
|
||||||
|
=========
|
||||||
|
|
||||||
I've just completed these:
|
Sponsored by [Root](https://therootcompany.com).
|
||||||
|
Built for [ACME.js](https://git.coolaj86.com/coolaj86/acme.js)
|
||||||
|
and [Greenlock.js](https://git.coolaj86.com/coolaj86/greenlock.js)
|
||||||
|
|
||||||
* [ECDSA-CSR.js](https://git.coolaj86.com/coolaj86/ecdsa-csr.js)
|
RSA tools. Lightweight. Zero Dependencies. Universal compatibility.
|
||||||
* [eckles.js](https://git.coolaj86.com/coolaj86/eckles.js) - JWK-to-PEM and PEM-to-JWK for EC / ECDSA P-256 and P-384
|
|
||||||
|
|
||||||
I've got working prototypes for the RSA variants as well and I'm in the middle of cleaning them up to publish.
|
* [x] PEM-to-JWK
|
||||||
|
* [ ] JWK-to-PEM (in progress)
|
||||||
|
* [x] SSH "pub" format
|
||||||
|
|
||||||
|
<!-- This project is fully functional and tested (and the code is pretty clean).
|
||||||
|
|
||||||
|
It is considered to be complete, but if you find a bug please open an issue. -->
|
||||||
|
|
||||||
|
## PEM-to-JWK
|
||||||
|
|
||||||
|
* [x] PKCS#1 (traditional), PKCS#8, SPKI/PKIX
|
||||||
|
* [x] 2048-bit, 4096-bit (and ostensibily all others)
|
||||||
|
* [x] SSH (RFC4716), (RFC 4716/SSH2)
|
||||||
|
|
||||||
|
```js
|
||||||
|
var Rasha = require('rasha');
|
||||||
|
var pem = require('fs')
|
||||||
|
.readFileSync('./node_modles/rasha/fixtures/privkey-rsa-2048.pkcs1.pem', 'ascii');
|
||||||
|
|
||||||
|
Rasha.import({ pem: pem }).then(function (jwk) {
|
||||||
|
console.log(jwk);
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
```js
|
||||||
|
{
|
||||||
|
"kty": "RSA",
|
||||||
|
"n": "m2ttVBxPlWw06ZmGBWVDl...QlEz7UNNj9RGps_50-CNw",
|
||||||
|
"e": "AQAB",
|
||||||
|
"d": "Cpfo7Mm9Nu8YMC_xrZ54W...Our1IdDzJ_YfHPt9sHMQQ",
|
||||||
|
"p": "ynG-t9HwKCN3MWRYFdnFz...E9S4DsGcAarIuOT2TsTCE",
|
||||||
|
"q": "xIkAjgUzB1zaUzJtW2Zgv...38ahSrBFEVnxjpnPh1Q1c",
|
||||||
|
"dp": "tzDGjECFOU0ehqtuqhcu...dVGAXJoGOdv5VpaZ7B1QE",
|
||||||
|
"dq": "kh5dyDk7YCz7sUFbpsmu...aX9PKa12HFlny6K1daL48",
|
||||||
|
"qi": "AlHWbx1gp6Z9pbw_1hlS...lhmIOgRApS0t9VoXtHhFU"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
<!--
|
||||||
|
## JWK-to-PEM
|
||||||
|
|
||||||
|
* [x] PKCS#1 (traditional), PKCS#8, SPKI/PKIX
|
||||||
|
* [x] 2048-bit, 4096-bit (and ostensibily all others)
|
||||||
|
* [x] SSH (RFC4716), (RFC 4716/SSH2)
|
||||||
|
|
||||||
|
```js
|
||||||
|
var Rasha = require('rasha');
|
||||||
|
var jwk = require('rasha/fixtures/privkey-rsa-2038.jwk.json');
|
||||||
|
|
||||||
|
Rasha.export({ jwk: jwk }).then(function (pem) {
|
||||||
|
// PEM in PKCS1 (traditional) format
|
||||||
|
console.log(pem);
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
```
|
||||||
|
-----BEGIN RSA PRIVATE KEY-----
|
||||||
|
MIIEpAIBAAKCAQEAm2ttVBxPlWw06ZmGBWVDlfjkPAJ4DgnY0TrDwtCohHzLxGhD
|
||||||
|
NzUJefLukC+xu0LBKylYojT5vTkxaOhxeSYo31syu4WhxbkTBLICOFcCGMob6pSQ
|
||||||
|
38P8LdAIlb0pqDHxEJ9adWomjuFf0...e5cCBahfsiNtNR6WV1/iCSuINYs6uPdA
|
||||||
|
Jlw7hm9m8TAmFWWyfL0s7wiRvAYkQvpxetorTwHJVLabBDJ+WBOAY2enOLHIRQv+
|
||||||
|
atAvHrLXjkUdzF96o0icyF6n7QzGfUPmeWGYg6BEClLS31Whe0eEVQ==
|
||||||
|
-----END RSA PRIVATE KEY-----
|
||||||
|
```
|
||||||
|
|
||||||
|
-->
|
||||||
|
|
||||||
|
### Advanced Options
|
||||||
|
|
||||||
|
<!--
|
||||||
|
|
||||||
|
`format: 'pkcs8'`:
|
||||||
|
|
||||||
|
The default output format `pkcs1` (RSA-specific format) is used for private keys.
|
||||||
|
Use `format: 'pkcs8'` to output in PKCS#8 format instead.
|
||||||
|
|
||||||
|
```js
|
||||||
|
Rasha.export({ jwk: jwk, format: 'pkcs8' }).then(function (pem) {
|
||||||
|
// PEM in PKCS#8 format
|
||||||
|
console.log(pem);
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
```
|
||||||
|
-----BEGIN PRIVATE KEY-----
|
||||||
|
MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQCba21UHE+VbDTp
|
||||||
|
mYYFZUOV+OQ8AngOCdjROsPC0KiEfMvEaEM3NQl58u6QL7G7QsErKViiNPm9OTFo
|
||||||
|
6HF5JijfWzK7haHFuRMEsgI4VwIYy...fLorV1ovjwKBgAJR1m8dYKemfaW8P9YZ
|
||||||
|
Uux7lwIFqF+yI201HpZXX+IJK4g1izq490AmXDuGb2bxMCYVZbJ8vSzvCJG8BiRC
|
||||||
|
+nF62itPAclUtpsEMn5YE4BjZ6c4schFC/5q0C8esteORR3MX3qjSJzIXqftDMZ9
|
||||||
|
Q+Z5YZiDoEQKUtLfVaF7R4RV
|
||||||
|
-----END PRIVATE KEY-----
|
||||||
|
```
|
||||||
|
|
||||||
|
`format: 'ssh'`:
|
||||||
|
|
||||||
|
Although SSH uses PKCS#1 for private keys, it uses ts own special non-ASN1 format
|
||||||
|
(affectionately known as rfc4716) for public keys. I got curious and then decided
|
||||||
|
to add this format as well.
|
||||||
|
|
||||||
|
To get the same format as you
|
||||||
|
would get with `ssh-keygen`, pass `ssh` as the format option:
|
||||||
|
|
||||||
|
```js
|
||||||
|
Rasha.export({ jwk: jwk, format: 'ssh' }).then(function (pub) {
|
||||||
|
// Special SSH2 Public Key format (RFC 4716)
|
||||||
|
console.log(pub);
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
```
|
||||||
|
ssh-rsa TODO-TODO-TODO RSA-2048@localhost
|
||||||
|
```
|
||||||
|
|
||||||
|
-->
|
||||||
|
|
||||||
|
`public: 'true'`:
|
||||||
|
|
||||||
|
If a private key is used as input, a private key will be output.
|
||||||
|
|
||||||
|
If you'd like to output a public key instead you can pass `public: true`.
|
||||||
|
|
||||||
|
<!--
|
||||||
|
or `format: 'spki'`.
|
||||||
|
|
||||||
|
```js
|
||||||
|
Rasha.export({ jwk: jwk, public: true }).then(function (pem) {
|
||||||
|
// PEM in SPKI/PKIX format
|
||||||
|
console.log(pem);
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
```
|
||||||
|
-----BEGIN RSA PUBLIC KEY-----
|
||||||
|
MIIBCgKCAQEAm2ttVBxPlWw06ZmGBWVDlfjkPAJ4DgnY0TrDwtCohHzLxGhDNzUJ
|
||||||
|
efLukC+xu0LBKylYojT5vTkxaOhxe...eTmzCh2ikrwTMja7mUdBJf2bK3By5AB0
|
||||||
|
Qi49OykUCfNZeQlEz7UNNj9RGps/50+CNwIDAQAB
|
||||||
|
-----END RSA PUBLIC KEY-----
|
||||||
|
```
|
||||||
|
|
||||||
|
-->
|
||||||
|
|
||||||
Testing
|
Testing
|
||||||
-------
|
-------
|
||||||
|
|
||||||
```
|
<!-- All cases are tested in `test.sh`. -->
|
||||||
|
|
||||||
|
You can compare these keys to the ones that you get from OpenSSL, ssh-keygen, and WebCrypto:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Generate 2048-bit RSA Keypair
|
||||||
openssl genrsa -out privkey-rsa-2048.pkcs1.pem 2048
|
openssl genrsa -out privkey-rsa-2048.pkcs1.pem 2048
|
||||||
|
|
||||||
|
# Convert PKCS1 (traditional) RSA Keypair to PKCS8 format
|
||||||
openssl rsa -in privkey-rsa-2048.pkcs1.pem -pubout -out pub-rsa-2048.spki.pem
|
openssl rsa -in privkey-rsa-2048.pkcs1.pem -pubout -out pub-rsa-2048.spki.pem
|
||||||
|
|
||||||
|
# Export Public-only RSA Key in PKCS1 (traditional) format
|
||||||
openssl pkcs8 -topk8 -nocrypt -in privkey-rsa-2048.pkcs1.pem -out privkey-rsa-2048.pkcs8.pem
|
openssl pkcs8 -topk8 -nocrypt -in privkey-rsa-2048.pkcs1.pem -out privkey-rsa-2048.pkcs8.pem
|
||||||
|
|
||||||
|
# Convert PKCS1 (traditional) RSA Public Key to SPKI/PKIX format
|
||||||
openssl rsa -in pub-rsa-2048.spki.pem -pubin -RSAPublicKey_out -out pub-rsa-2048.pkcs1.pem
|
openssl rsa -in pub-rsa-2048.spki.pem -pubin -RSAPublicKey_out -out pub-rsa-2048.pkcs1.pem
|
||||||
|
|
||||||
|
# Convert RSA public key to SSH format
|
||||||
ssh-keygen -f ./pub-rsa-2048.spki.pem -i -mPKCS8 > ./pub-rsa-2048.ssh.pub
|
ssh-keygen -f ./pub-rsa-2048.spki.pem -i -mPKCS8 > ./pub-rsa-2048.ssh.pub
|
||||||
```
|
```
|
||||||
|
|
||||||
** unified openssl commands **
|
Goals of this project
|
||||||
|
-----
|
||||||
|
|
||||||
https://gist.github.com/briansmith/2ee42439923d8e65a266994d0f70180b
|
* Zero Dependencies
|
||||||
|
* Focused support for 2048-bit and 4096-bit RSA keypairs (although any size is technically supported)
|
||||||
|
* Convert both ways
|
||||||
|
* Browser support as well (TODO)
|
||||||
|
* OpenSSL, ssh-keygen, and WebCrypto compatibility
|
||||||
|
|
||||||
|
Legal
|
||||||
|
-----
|
||||||
|
|
||||||
|
Licensed MPL-2.0
|
||||||
|
|
||||||
|
[Terms of Use](https://therootcompany.com/legal/#terms) |
|
||||||
|
[Privacy Policy](https://therootcompany.com/legal/#privacy)
|
||||||
|
|
12
lib/rasha.js
12
lib/rasha.js
|
@ -13,13 +13,14 @@ RSA.parse = function parseRsa(opts) {
|
||||||
if (!opts || !opts.pem || 'string' !== typeof opts.pem) {
|
if (!opts || !opts.pem || 'string' !== typeof opts.pem) {
|
||||||
throw new Error("must pass { pem: pem } as a string");
|
throw new Error("must pass { pem: pem } as a string");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var jwk = { kty: 'RSA', n: null, e: null };
|
||||||
if (0 === opts.pem.indexOf('ssh-rsa ')) {
|
if (0 === opts.pem.indexOf('ssh-rsa ')) {
|
||||||
return SSH.parse(opts.pem);
|
return SSH.parse(opts.pem, jwk);
|
||||||
}
|
}
|
||||||
var pem = opts.pem;
|
var pem = opts.pem;
|
||||||
var block = PEM.parseBlock(pem);
|
var block = PEM.parseBlock(pem);
|
||||||
//var hex = toHex(u8);
|
//var hex = toHex(u8);
|
||||||
var jwk = { kty: 'RSA', n: null, e: null };
|
|
||||||
var asn1 = ASN1.parse(block.der);
|
var asn1 = ASN1.parse(block.der);
|
||||||
|
|
||||||
var meta = x509.guess(block.der, asn1);
|
var meta = x509.guess(block.der, asn1);
|
||||||
|
@ -30,6 +31,13 @@ RSA.parse = function parseRsa(opts) {
|
||||||
jwk = RSA.parsePkcs8(block.der, asn1, jwk);
|
jwk = RSA.parsePkcs8(block.der, asn1, jwk);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (opts.public) {
|
||||||
|
jwk = {
|
||||||
|
kty: jwk.kty
|
||||||
|
, n: jwk.n
|
||||||
|
, e: jwk.e
|
||||||
|
};
|
||||||
|
}
|
||||||
return jwk;
|
return jwk;
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
30
lib/ssh.js
30
lib/ssh.js
|
@ -1,10 +1,38 @@
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
var SSH = module.exports;
|
var SSH = module.exports;
|
||||||
|
var Enc = require('./encoding.js');
|
||||||
|
|
||||||
// 7 s s h - r s a
|
// 7 s s h - r s a
|
||||||
SSH.RSA = '00000007 73 73 68 2d 72 73 61'.replace(/\s+/g, '').toLowerCase();
|
SSH.RSA = '00000007 73 73 68 2d 72 73 61'.replace(/\s+/g, '').toLowerCase();
|
||||||
|
|
||||||
SSH.parse = function (pem) {
|
SSH.parse = function (pem, jwk) {
|
||||||
|
|
||||||
|
var parts = pem.split(/\s+/);
|
||||||
|
var buf = Enc.base64ToBuf(parts[1]);
|
||||||
|
var els = [];
|
||||||
|
var index = 0;
|
||||||
|
var len;
|
||||||
|
var i = 0;
|
||||||
|
var offset = (buf.byteOffset || 0);
|
||||||
|
// using dataview to be browser-compatible (I do want _some_ code reuse)
|
||||||
|
var dv = new DataView(buf.buffer.slice(offset, offset + buf.byteLength));
|
||||||
|
|
||||||
|
if (SSH.RSA !== Enc.bufToHex(buf.slice(0, SSH.RSA.length/2))) {
|
||||||
|
throw new Error("does not lead with ssh header");
|
||||||
|
}
|
||||||
|
|
||||||
|
while (index < buf.byteLength) {
|
||||||
|
i += 1;
|
||||||
|
if (i > 3) { throw new Error("15+ elements, probably not a public ssh key"); }
|
||||||
|
len = dv.getUint32(index, false);
|
||||||
|
index += 4;
|
||||||
|
els.push(buf.slice(index, index + len));
|
||||||
|
index += len;
|
||||||
|
}
|
||||||
|
|
||||||
|
jwk.n = Enc.bufToUrlBase64(els[2]);
|
||||||
|
jwk.e = Enc.bufToUrlBase64(els[1]);
|
||||||
|
|
||||||
|
return jwk;
|
||||||
};
|
};
|
||||||
|
|
111
lib/telemetry.js
Normal file
111
lib/telemetry.js
Normal file
|
@ -0,0 +1,111 @@
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
// We believe in a proactive approach to sustainable open source.
|
||||||
|
// As part of that we make it easy for you to opt-in to following our progress
|
||||||
|
// and we also stay up-to-date on telemetry such as operating system and node
|
||||||
|
// version so that we can focus our efforts where they'll have the greatest impact.
|
||||||
|
//
|
||||||
|
// Want to learn more about our Terms, Privacy Policy, and Mission?
|
||||||
|
// Check out https://therootcompany.com/legal/
|
||||||
|
|
||||||
|
var os = require('os');
|
||||||
|
var crypto = require('crypto');
|
||||||
|
var https = require('https');
|
||||||
|
var pkg = require('../package.json');
|
||||||
|
|
||||||
|
// to help focus our efforts in the right places
|
||||||
|
var data = {
|
||||||
|
package: pkg.name
|
||||||
|
, version: pkg.version
|
||||||
|
, node: process.version
|
||||||
|
, arch: process.arch || os.arch()
|
||||||
|
, platform: process.platform || os.platform()
|
||||||
|
, release: os.release()
|
||||||
|
};
|
||||||
|
|
||||||
|
function addCommunityMember(opts) {
|
||||||
|
setTimeout(function () {
|
||||||
|
var req = https.request({
|
||||||
|
hostname: 'api.therootcompany.com'
|
||||||
|
, port: 443
|
||||||
|
, path: '/api/therootcompany.com/public/community'
|
||||||
|
, method: 'POST'
|
||||||
|
, headers: { 'Content-Type': 'application/json' }
|
||||||
|
}, function (resp) {
|
||||||
|
// let the data flow, so we can ignore it
|
||||||
|
resp.on('data', function () {});
|
||||||
|
//resp.on('data', function (chunk) { console.log(chunk.toString()); });
|
||||||
|
resp.on('error', function () { /*ignore*/ });
|
||||||
|
//resp.on('error', function (err) { console.error(err); });
|
||||||
|
});
|
||||||
|
var obj = JSON.parse(JSON.stringify(data));
|
||||||
|
obj.action = 'updates';
|
||||||
|
try {
|
||||||
|
obj.ppid = ppid(obj.action);
|
||||||
|
} catch(e) {
|
||||||
|
// ignore
|
||||||
|
//console.error(e);
|
||||||
|
}
|
||||||
|
obj.name = opts.name || undefined;
|
||||||
|
obj.address = opts.email;
|
||||||
|
obj.community = 'node.js@therootcompany.com';
|
||||||
|
|
||||||
|
req.write(JSON.stringify(obj, 2, null));
|
||||||
|
req.end();
|
||||||
|
req.on('error', function () { /*ignore*/ });
|
||||||
|
//req.on('error', function (err) { console.error(err); });
|
||||||
|
}, 50);
|
||||||
|
}
|
||||||
|
|
||||||
|
function ping(action) {
|
||||||
|
setTimeout(function () {
|
||||||
|
var req = https.request({
|
||||||
|
hostname: 'api.therootcompany.com'
|
||||||
|
, port: 443
|
||||||
|
, path: '/api/therootcompany.com/public/ping'
|
||||||
|
, method: 'POST'
|
||||||
|
, headers: { 'Content-Type': 'application/json' }
|
||||||
|
}, function (resp) {
|
||||||
|
// let the data flow, so we can ignore it
|
||||||
|
resp.on('data', function () { });
|
||||||
|
//resp.on('data', function (chunk) { console.log(chunk.toString()); });
|
||||||
|
resp.on('error', function () { /*ignore*/ });
|
||||||
|
//resp.on('error', function (err) { console.error(err); });
|
||||||
|
});
|
||||||
|
var obj = JSON.parse(JSON.stringify(data));
|
||||||
|
obj.action = action;
|
||||||
|
try {
|
||||||
|
obj.ppid = ppid(obj.action);
|
||||||
|
} catch(e) {
|
||||||
|
// ignore
|
||||||
|
//console.error(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
req.write(JSON.stringify(obj, 2, null));
|
||||||
|
req.end();
|
||||||
|
req.on('error', function (/*e*/) { /*console.error('req.error', e);*/ });
|
||||||
|
}, 50);
|
||||||
|
}
|
||||||
|
|
||||||
|
// to help identify unique installs without getting
|
||||||
|
// the personally identifiable info that we don't want
|
||||||
|
function ppid(action) {
|
||||||
|
var parts = [ action, data.package, data.version, data.node, data.arch, data.platform, data.release ];
|
||||||
|
var ifaces = os.networkInterfaces();
|
||||||
|
Object.keys(ifaces).forEach(function (ifname) {
|
||||||
|
if (/^en/.test(ifname) || /^eth/.test(ifname) || /^wl/.test(ifname)) {
|
||||||
|
if (ifaces[ifname] && ifaces[ifname].length) {
|
||||||
|
parts.push(ifaces[ifname][0].mac);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return crypto.createHash('sha1').update(parts.join(',')).digest('base64');
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports.ping = ping;
|
||||||
|
module.exports.joinCommunity = addCommunityMember;
|
||||||
|
|
||||||
|
if (require.main === module) {
|
||||||
|
ping('install');
|
||||||
|
//addCommunityMember({ name: "AJ ONeal", email: 'coolaj86@gmail.com' });
|
||||||
|
}
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "rasha",
|
"name": "rasha",
|
||||||
"version": "0.0.2",
|
"version": "0.7.0",
|
||||||
"description": "PEM-to-JWK and JWK-to-PEM for RSA keys in a lightweight, zero-dependency library focused on perfect universal compatibility.",
|
"description": "PEM-to-JWK and JWK-to-PEM for RSA keys in a lightweight, zero-dependency library focused on perfect universal compatibility.",
|
||||||
"homepage": "https://git.coolaj86.com/coolaj86/rasha.js",
|
"homepage": "https://git.coolaj86.com/coolaj86/rasha.js",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
|
@ -25,7 +25,6 @@
|
||||||
},
|
},
|
||||||
"xkeywords": [
|
"xkeywords": [
|
||||||
"zero-dependency",
|
"zero-dependency",
|
||||||
"JWK-to-PEM",
|
|
||||||
"PEM-to-JWK",
|
"PEM-to-JWK",
|
||||||
"RSA",
|
"RSA",
|
||||||
"2048",
|
"2048",
|
||||||
|
@ -33,6 +32,9 @@
|
||||||
"asn1",
|
"asn1",
|
||||||
"x509"
|
"x509"
|
||||||
],
|
],
|
||||||
|
"xkeywords": [
|
||||||
|
"JWK-to-PEM"
|
||||||
|
],
|
||||||
"author": "AJ ONeal <coolaj86@gmail.com> (https://coolaj86.com/)",
|
"author": "AJ ONeal <coolaj86@gmail.com> (https://coolaj86.com/)",
|
||||||
"license": "MPL-2.0"
|
"license": "MPL-2.0"
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue