commit 0b21269c95ae72bae267c9959d9aba6834608d99 Author: AJ ONeal Date: Sat Nov 24 22:50:10 2018 -0700 v1.0.1: 💯 ECDSA Command line tools diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..3c3629e --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +node_modules diff --git a/README.md b/README.md new file mode 100644 index 0000000..3578b87 --- /dev/null +++ b/README.md @@ -0,0 +1,162 @@ +[Eckles CLI](https://git.coolaj86.com/coolaj86/eckles-cli.js) +========= + +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 (elliptic curve) tools. + +## Install + +```bash +npm install -g eckles +``` + +Table of Contents +================= + +* [x] Generate EC Keys +* [x] PEM to JWK +* [x] JWK to PEM +* [x] SSH "pub" format +* [ ] RSA + * **Need RSA tools?** Check out [Rasha.js](https://git.coolaj86.com/coolaj86/rasha.js) + +## Generate EC (ECDSA/ECDH) Keypair + +``` +eckles [format] [curve|encoding] +``` + +#### Generate ECDSA JWK + +``` +eckles [jwk] [P-256|P-384] +``` + +```bash +# Default P-256 (prime256v1, secp256r1) +eckles jwk + +# Use P-384 (secp384r1) +eckles jwk P-384 +``` + +#### Generate ECDSA PEM + +``` +eckles [sec1|pkcs8|ssh] [P-256|P-384] +``` + +```bash +eckles sec1 + +eckles pkcs8 P-256 + +eckles ssh P-384 +``` + +#### Generate ECDSA DER + +``` +eckles [sec1|pkcs8] [der] +``` + +```bash +eckles sec1 der > privkey.ec.sec1.der 2> pub.ec.spki.der + +eckles pkcs8 der > privkey.ec.pkcs8.der 2> pub.ec.spki.der +``` + +## Convert ECDSA PEM to JWK + +``` +eckles [pemfile] [public] +``` + +```bash +eckles privkey.pem > privkey.jwk.json + +eckles pub.pem > pub.jwk.json + +eckles privkey.pem public > pub.jwk.json +``` + +```bash +eckles id_rsa > privkey.jwk.json + +eckles id_rsa public > pub.jwk.json + +eckles id_rsa.pub > pub.jwk.json +``` + +## Convert ECDSA JWK to PEM + +``` +eckles [jwk-keyfile] [format] +``` + +```bash +eckles privkey.jwk.json sec1 > privkey.pem + +eckles privkey.jwk.json pkcs8 > privkey.pem + +eckles privkey.jwk.json spki > pub.pem + +eckles privkey.jwk.json ssh > id_rsa.pub +``` + +```bash +eckles pub.jwk.json spki > id_rsa.pub + +eckles pub.jwk.json ssh > id_rsa.pub +``` + +## Convert ECDSA PEM to SSH + +This is a two-step process, at the moment. + +Only public keys are necessary, but private keys may be used. + +```bash +eckles privkey.pem > privkey.jwk.json + +eckles privkey.jwk.json pkcs8 > id_rsa + +eckles privkey.jwk.json ssh > id_rsa.pub +``` + +```bash +eckles pub.pem > pub.jwk.json + +eckles pub.jwk.json ssh > id_rsa.pub +``` + +#### Convert ECDSA SSH to PEM + +This is a two-step process, at the moment. + +Only public keys are necessary, but private keys may be used. + +```bash +eckles id_rsa > privkey.jwk.json + +eckles privkey.jwk.json sec1 > privkey.pem + +eckles privkey.jwk.json pkcs8 > privkey.pem +``` + +```bash +eckles id_rsa.pub > pub.jwk.json + +eckles privkey.jwk.json spki > pub.pem +``` + +Legal +----- + +[Eckles CLI](https://git.coolaj86.com/coolaj86/eckles-cli.js) | +MPL-2.0 | +[Terms of Use](https://therootcompany.com/legal/#terms) | +[Privacy Policy](https://therootcompany.com/legal/#privacy) diff --git a/bin/eckles.js b/bin/eckles.js new file mode 100755 index 0000000..bce03e7 --- /dev/null +++ b/bin/eckles.js @@ -0,0 +1,80 @@ +#!/usr/bin/env node +'use strict'; + +var fs = require('fs'); +var Eckles = require('eckles'); +var ecdsacsr = require('ecdsa-csr'); + +var infile = process.argv[2]; +var format = process.argv[3]; +var domains = process.argv[3]; +var key; + +function errout(err) { + console.error(err); + process.exit(1); +} + +if (!infile) { + infile = 'jwk'; +} + +if (-1 !== [ 'jwk', 'pem', 'json', 'der', 'sec1', 'pkcs8', 'spki', 'ssh' ].indexOf(infile)) { + return 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'); + } + if ('jwk' === infile) { + key.private = JSON.stringify(key.private, null, 2); + key.public = JSON.stringify(key.public, null, 2); + } + console.info(key.private); + // so that the pub key can be directed separately + console.error(key.public); + process.exit(0); + }).catch(errout); +} + +if ('csr' === infile) { + key = fs.readFileSync(format, 'ascii'); +} else { + key = fs.readFileSync(infile, 'ascii'); +} + +try { + key = JSON.parse(key); +} catch(e) { + // ignore +} + + +if ('csr' === infile) { + return ecdsacsr({ + // don't remember which it was... whatever + pem: key + , key: key + , jwk: key + , domains: domains.split(/,/) + }).then(function (csr) { + console.info(csr); + }).catch(errout); +} + +console.log(typeof key, key); +if ('string' === typeof key) { + var pub = (-1 !== [ 'public', 'spki', 'pkix' ].indexOf(format)); + return Eckles.import({ pem: key, public: (pub || format) }).then(function (jwk) { + console.info(JSON.stringify(jwk, null, 2)); + }).catch(errout); +} else { + var pub = (-1 !== [ 'public', 'spki', 'pkix', 'ssh' ].indexOf(format)); + if ('public' === format) { format = 'spki'; } + return Eckles.export({ jwk: key, format: format, public: pub }).then(function (pem) { + console.info(pem); + }).catch(errout); +} diff --git a/lib/telemetry.js b/lib/telemetry.js new file mode 100644 index 0000000..c628a2d --- /dev/null +++ b/lib/telemetry.js @@ -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' }); +} diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..7c11443 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,18 @@ +{ + "name": "eckles-cli", + "version": "1.3.1", + "lockfileVersion": 1, + "requires": true, + "dependencies": { + "ecdsa-csr": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ecdsa-csr/-/ecdsa-csr-1.1.1.tgz", + "integrity": "sha512-j0ssynuUi2ZPzkzEhUwPN5h5nOzJak7kExju29fEOVvIXvtU9o97puMNVQnEAM4uAM3u4G2Wp0YcusJKaSecCQ==" + }, + "eckles": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/eckles/-/eckles-1.3.2.tgz", + "integrity": "sha512-UBpiRqM/YqpuWSyQucnwmjWvd/umb1WvH2GEkdWiDRGzU6DLsjk8kHitmud4VtHXkggZXY8Oy+2zeMlqwYfIgw==" + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..f24f156 --- /dev/null +++ b/package.json @@ -0,0 +1,43 @@ +{ + "name": "eckles-cli", + "version": "1.0.1", + "description": "Command line ECDSA tools to generating key pairs and converting between JWK, various PEM formats, and SSH", + "homepage": "https://git.coolaj86.com/coolaj86/eckles-cli.js", + "main": "bin/eckles.js", + "bin": { + "eckles": "bin/eckles.js" + }, + "files": [ + "bin", + "fixtures", + "lib", + "test.sh" + ], + "directories": { + "lib": "lib" + }, + "scripts": { + "postinstall": "node lib/telemetry.js event:install", + "test": "bash test.sh" + }, + "repository": { + "type": "git", + "url": "https://git.coolaj86.com/coolaj86/eckles-cli.js" + }, + "keywords": [ + "EC", + "ECDSA", + "PEM", + "JWK", + "SSH", + "tested", + "working", + "complete" + ], + "author": "AJ ONeal (https://coolaj86.com/)", + "license": "MPL-2.0", + "dependencies": { + "ecdsa-csr": "^1.1.1", + "eckles": "^1.3.2" + } +} diff --git a/test.sh b/test.sh new file mode 100644 index 0000000..3b9aa27 --- /dev/null +++ b/test.sh @@ -0,0 +1,46 @@ +#!/bin/bash +set -e + +echo "" +echo "Generate" +node bin/eckles.js jwk > privkey.1.jwk.json 2> pub.1.jwk.json +node bin/eckles.js sec1 > privkey.2.sec1.pem 2> pub.2.spki.pem +node bin/eckles.js pkcs8 > privkey.3.pkcs8.pem 2> pub.3.spki.pem +node bin/eckles.js ssh > id_rsa 2> id_rsa.pub +echo "PASS" + +# JWK +echo "" +echo "Read JWK" +node bin/eckles.js privkey.1.jwk.json > /dev/null +node bin/eckles.js privkey.1.jwk.json public > /dev/null +node bin/eckles.js pub.1.jwk.json > /dev/null +echo "PASS" + +# SEC1 + SPKI +echo "" +echo "Read SEC1" +node bin/eckles.js privkey.2.sec1.pem > /dev/null +node bin/eckles.js privkey.2.sec1.pem public > /dev/null +node bin/eckles.js pub.2.spki.pem > /dev/null +echo "PASS" + +# PKCS8 (SPKI already tested) +echo "" +echo "Read PKCS8" +node bin/eckles.js privkey.3.pkcs8.pem > /dev/null +node bin/eckles.js privkey.3.pkcs8.pem public > /dev/null +echo "PASS" + +# SSH (PKCS8 + PUB) +echo "" +echo "Read SSH" +node bin/eckles.js privkey.3.pkcs8.pem > /dev/null +node bin/eckles.js id_rsa > /dev/null +node bin/eckles.js id_rsa public > /dev/null +node bin/eckles.js id_rsa.pub > /dev/null +echo "PASS" + +echo "" +echo "" +echo "Passed all tests"