AJ ONeal
5 years ago
5 changed files with 331 additions and 14 deletions
@ -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) |
|||
* [eckles.js](https://git.coolaj86.com/coolaj86/eckles.js) - JWK-to-PEM and PEM-to-JWK for EC / ECDSA P-256 and P-384 |
|||
RSA tools. Lightweight. Zero Dependencies. Universal compatibility. |
|||
|
|||
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 |
|||
------- |
|||
|
|||
``` |
|||
<!-- 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 |
|||
|
|||
# Convert PKCS1 (traditional) RSA Keypair to PKCS8 format |
|||
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 |
|||
|
|||
# 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 |
|||
|
|||
# Convert RSA public key to SSH format |
|||
ssh-keygen -f ./pub-rsa-2048.spki.pem -i -mPKCS8 > ./pub-rsa-2048.ssh.pub |
|||
``` |
|||
|
|||
** unified openssl commands ** |
|||
Goals of this project |
|||
----- |
|||
|
|||
* 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 |
|||
|
|||
https://gist.github.com/briansmith/2ee42439923d8e65a266994d0f70180b |
|||
[Terms of Use](https://therootcompany.com/legal/#terms) | |
|||
[Privacy Policy](https://therootcompany.com/legal/#privacy) |
|||
|
@ -1,10 +1,38 @@ |
|||
'use strict'; |
|||
|
|||
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.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; |
|||
}; |
|||
|
@ -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' });
|
|||
} |
Loading…
Reference in new issue