From 941d67aa176a758f2f4d2f99cc4a4a7cd08dd113 Mon Sep 17 00:00:00 2001 From: Alex Wilson Date: Mon, 6 Mar 2017 14:08:09 -0800 Subject: [PATCH] joyent/node-sshpk#26 want support for generating ecdsa keys Reviewed by: Trent Mick Reviewed by: Brittany Wald Approved by: Trent Mick --- README.md | 17 +++++++- lib/dhe.js | 103 +++++++++++++++++++++++++++++++++++++++++++- lib/identity.js | 2 +- lib/index.js | 1 + lib/key.js | 4 +- lib/private-key.js | 24 ++++++++++- test/dhe_compat.js | 16 +++---- test/openssl-cmd.js | 31 ++++++++++++- test/private-key.js | 59 ++++++++++++++++++++++++- 9 files changed, 239 insertions(+), 18 deletions(-) diff --git a/README.md b/README.md index ad569ee..310c2ee 100644 --- a/README.md +++ b/README.md @@ -227,17 +227,30 @@ Parameters - `format` -- String name of format to use, valid options are: - `auto`: choose automatically from all below - `pem`: supports both PKCS#1 and PKCS#8 - - `ssh`, `openssh`: new post-OpenSSH 6.5 internal format, produced by + - `ssh`, `openssh`: new post-OpenSSH 6.5 internal format, produced by `ssh-keygen -o` - `pkcs1`, `pkcs8`: variants of `pem` - `rfc4253`: raw OpenSSH wire format - `options` -- Optional Object, extra options, with keys: - - `filename` -- Optional String, name for the key being parsed + - `filename` -- Optional String, name for the key being parsed (eg. the filename that was opened). Used to generate Error messages - `passphrase` -- Optional String, encryption passphrase used to decrypt an encrypted PEM file +### `generatePrivateKey(type[, options])` + +Generates a new private key of a certain key type, from random data. + +Parameters + +- `type` -- String, type of key to generate. Currently supported are `'ecdsa'` + and `'ed25519'` +- `options` -- optional Object, with keys: + - `curve` -- optional String, for `'ecdsa'` keys, specifies the curve to use. + If ECDSA is specified and this option is not given, defaults to + using `'nistp256'`. + ### `PrivateKey.isPrivateKey(obj)` Returns `true` if the given object is a valid `PrivateKey` object created by a diff --git a/lib/dhe.js b/lib/dhe.js index 8f9548c..74f5e04 100644 --- a/lib/dhe.js +++ b/lib/dhe.js @@ -1,12 +1,17 @@ -// Copyright 2015 Joyent, Inc. +// Copyright 2017 Joyent, Inc. -module.exports = DiffieHellman; +module.exports = { + DiffieHellman: DiffieHellman, + generateECDSA: generateECDSA, + generateED25519: generateED25519 +}; var assert = require('assert-plus'); var crypto = require('crypto'); var algs = require('./algs'); var utils = require('./utils'); var ed; +var nacl; var Key = require('./key'); var PrivateKey = require('./private-key'); @@ -309,3 +314,97 @@ ECPrivate.prototype.deriveSharedSecret = function (pubKey) { var S = pubKey._pub.multiply(this._priv); return (new Buffer(S.getX().toBigInteger().toByteArray())); }; + +function generateED25519() { + if (nacl === undefined) + nacl = require('tweetnacl'); + + var pair = nacl.sign.keyPair(); + var priv = new Buffer(pair.secretKey); + var pub = new Buffer(pair.publicKey); + assert.strictEqual(priv.length, 64); + assert.strictEqual(pub.length, 32); + + var parts = []; + parts.push({name: 'R', data: pub}); + parts.push({name: 'r', data: priv}); + var key = new PrivateKey({ + type: 'ed25519', + parts: parts + }); + return (key); +} + +/* Generates a new ECDSA private key on a given curve. */ +function generateECDSA(curve) { + var parts = []; + var key; + + if (CRYPTO_HAVE_ECDH) { + /* + * Node crypto doesn't expose key generation directly, but the + * ECDH instances can generate keys. It turns out this just + * calls into the OpenSSL generic key generator, and we can + * read its output happily without doing an actual DH. So we + * use that here. + */ + var osCurve = { + 'nistp256': 'prime256v1', + 'nistp384': 'secp384r1', + 'nistp521': 'secp521r1' + }[curve]; + + var dh = crypto.createECDH(osCurve); + dh.generateKeys(); + + parts.push({name: 'curve', + data: new Buffer(curve)}); + parts.push({name: 'Q', data: dh.getPublicKey()}); + parts.push({name: 'd', data: dh.getPrivateKey()}); + + key = new PrivateKey({ + type: 'ecdsa', + curve: curve, + parts: parts + }); + return (key); + + } else { + if (ecdh === undefined) + ecdh = require('ecc-jsbn'); + if (ec === undefined) + ec = require('ecc-jsbn/lib/ec'); + if (jsbn === undefined) + jsbn = require('jsbn').BigInteger; + + var ecParams = new X9ECParameters(curve); + + /* This algorithm taken from FIPS PUB 186-4 (section B.4.1) */ + var n = ecParams.getN(); + /* + * The crypto.randomBytes() function can only give us whole + * bytes, so taking a nod from X9.62, we round up. + */ + var cByteLen = Math.ceil((n.bitLength() + 64) / 8); + var c = new jsbn(crypto.randomBytes(cByteLen)); + + var n1 = n.subtract(jsbn.ONE); + var priv = c.mod(n1).add(jsbn.ONE); + var pub = ecParams.getG().multiply(priv); + + priv = new Buffer(priv.toByteArray()); + pub = new Buffer(ecParams.getCurve(). + encodePointHex(pub), 'hex'); + + parts.push({name: 'curve', data: new Buffer(curve)}); + parts.push({name: 'Q', data: pub}); + parts.push({name: 'd', data: priv}); + + key = new PrivateKey({ + type: 'ecdsa', + curve: curve, + parts: parts + }); + return (key); + } +} diff --git a/lib/identity.js b/lib/identity.js index eeda3a3..5e9021f 100644 --- a/lib/identity.js +++ b/lib/identity.js @@ -1,4 +1,4 @@ -// Copyright 2016 Joyent, Inc. +// Copyright 2017 Joyent, Inc. module.exports = Identity; diff --git a/lib/index.js b/lib/index.js index 96a1384..cb8cd1a 100644 --- a/lib/index.js +++ b/lib/index.js @@ -18,6 +18,7 @@ module.exports = { parseSignature: Signature.parse, PrivateKey: PrivateKey, parsePrivateKey: PrivateKey.parse, + generatePrivateKey: PrivateKey.generate, Certificate: Certificate, parseCertificate: Certificate.parse, createSelfSignedCertificate: Certificate.createSelfSigned, diff --git a/lib/key.js b/lib/key.js index ff5c363..96baa44 100644 --- a/lib/key.js +++ b/lib/key.js @@ -1,4 +1,4 @@ -// Copyright 2015 Joyent, Inc. +// Copyright 2017 Joyent, Inc. module.exports = Key; @@ -7,7 +7,7 @@ var algs = require('./algs'); var crypto = require('crypto'); var Fingerprint = require('./fingerprint'); var Signature = require('./signature'); -var DiffieHellman = require('./dhe'); +var DiffieHellman = require('./dhe').DiffieHellman; var errs = require('./errors'); var utils = require('./utils'); var PrivateKey = require('./private-key'); diff --git a/lib/private-key.js b/lib/private-key.js index f80d939..312ac58 100644 --- a/lib/private-key.js +++ b/lib/private-key.js @@ -1,4 +1,4 @@ -// Copyright 2015 Joyent, Inc. +// Copyright 2017 Joyent, Inc. module.exports = PrivateKey; @@ -10,6 +10,9 @@ var Signature = require('./signature'); var errs = require('./errors'); var util = require('util'); var utils = require('./utils'); +var dhe = require('./dhe'); +var generateECDSA = dhe.generateECDSA; +var generateED25519 = dhe.generateED25519; var edCompat; var ed; @@ -208,6 +211,25 @@ PrivateKey.isPrivateKey = function (obj, ver) { return (utils.isCompatible(obj, PrivateKey, ver)); }; +PrivateKey.generate = function (type, options) { + if (options === undefined) + options = {}; + assert.object(options, 'options'); + + switch (type) { + case 'ecdsa': + if (options.curve === undefined) + options.curve = 'nistp256'; + assert.string(options.curve, 'options.curve'); + return (generateECDSA(options.curve)); + case 'ed25519': + return (generateED25519()); + default: + throw (new Error('Key generation not supported with key ' + + 'type "' + type + '"')); + } +}; + /* * API versions for PrivateKey: * [1,0] -- initial ver diff --git a/test/dhe_compat.js b/test/dhe_compat.js index 398b542..b827aa1 100644 --- a/test/dhe_compat.js +++ b/test/dhe_compat.js @@ -1,4 +1,4 @@ -// Copyright 2015 Joyent, Inc. All rights reserved. +// Copyright 2017 Joyent, Inc. All rights reserved. var test = require('tape').test; @@ -54,33 +54,33 @@ test('setup', function (t) { }); test('ecdhe shared secret', function (t) { - var dh1 = new sshpk_dhe(EC_KEY); + var dh1 = new sshpk_dhe.DiffieHellman(EC_KEY); var secret1 = dh1.computeSecret(EC2_KEY.toPublic()); t.ok(Buffer.isBuffer(secret1)); t.deepEqual(secret1, new Buffer( 'UoKiio/gnWj4BdV41YvoHu9yhjynGBmphZ1JFbpk30o=', 'base64')); - var dh2 = new sshpk_dhe(EC2_KEY); + var dh2 = new sshpk_dhe.DiffieHellman(EC2_KEY); var secret2 = dh2.computeSecret(EC_KEY.toPublic()); t.deepEqual(secret1, secret2); t.end(); }); test('ecdhe generate ephemeral', function (t) { - var dh = new sshpk_dhe(EC_KEY); + var dh = new sshpk_dhe.DiffieHellman(EC_KEY); var ek = dh.generateKey(); t.ok(ek instanceof sshpk.PrivateKey); t.strictEqual(ek.type, 'ecdsa'); t.strictEqual(ek.curve, 'nistp256'); var secret1 = dh.computeSecret(EC_KEY); - var secret2 = (new sshpk_dhe(EC_KEY)).computeSecret(ek); + var secret2 = (new sshpk_dhe.DiffieHellman(EC_KEY)).computeSecret(ek); t.deepEqual(secret1, secret2); t.end(); }); test('ecdhe reject diff curves', function (t) { - var dh = new sshpk_dhe(EC_KEY); + var dh = new sshpk_dhe.DiffieHellman(EC_KEY); t.throws(function () { dh.computeSecret(ECOUT_KEY.toPublic()); }); @@ -93,12 +93,12 @@ test('ecdhe reject diff curves', function (t) { t.strictEqual(dh.getPublicKey().fingerprint().toString(), EC2_KEY.fingerprint().toString()); - var dh2 = new sshpk_dhe(ECOUT_KEY); + var dh2 = new sshpk_dhe.DiffieHellman(ECOUT_KEY); t.throws(function () { dh2.setKey(EC_KEY); }); - dh2 = new sshpk_dhe(EC_KEY); + dh2 = new sshpk_dhe.DiffieHellman(EC_KEY); t.throws(function () { dh2.setKey(C_KEY); }); diff --git a/test/openssl-cmd.js b/test/openssl-cmd.js index 8d51a23..ab27c93 100644 --- a/test/openssl-cmd.js +++ b/test/openssl-cmd.js @@ -1,4 +1,4 @@ -// Copyright 2015 Joyent, Inc. All rights reserved. +// Copyright 2017 Joyent, Inc. All rights reserved. var test = require('tape').test; var sshpk = require('../lib/index'); @@ -358,6 +358,35 @@ function genTests() { kid.stdin.write(certPem); kid.stdin.end(); }); + + test('make a self-signed cert with generated key', function (t) { + if (algo !== 'ecdsa') { + t.end(); + return; + } + + var key = sshpk.generatePrivateKey(algo); + + var id = sshpk.identityFromDN('cn=' + algo); + var cert = sshpk.createSelfSignedCertificate(id, key, + { purposes: ['ca'] }); + var certPem = cert.toBuffer('pem'); + + fs.writeFileSync(path.join(tmp, 'ca.pem'), certPem); + + var kid = spawn('openssl', ['verify', + '-CAfile', path.join(tmp, 'ca.pem')]); + var bufs = []; + kid.stdout.on('data', bufs.push.bind(bufs)); + kid.on('close', function (rc) { + t.equal(rc, 0); + var output = Buffer.concat(bufs).toString(); + t.strictEqual(output.trim(), 'stdin: OK'); + t.end(); + }); + kid.stdin.write(certPem); + kid.stdin.end(); + }); }); test('teardown', function (t) { diff --git a/test/private-key.js b/test/private-key.js index 8c274da..034f148 100644 --- a/test/private-key.js +++ b/test/private-key.js @@ -1,4 +1,4 @@ -// Copyright 2015 Joyent, Inc. All rights reserved. +// Copyright 2017 Joyent, Inc. All rights reserved. var test = require('tape').test; var sshpk = require('../lib/index'); @@ -253,9 +253,66 @@ test('PrivateKey#createSign on ECDSA 256 key', function (t) { t.end(); }); +test('PrivateKey.generate ecdsa default', function (t) { + var key = sshpk.generatePrivateKey('ecdsa'); + t.ok(sshpk.PrivateKey.isPrivateKey(key)); + t.strictEqual(key.type, 'ecdsa'); + t.strictEqual(key.curve, 'nistp256'); + t.strictEqual(key.size, 256); + + var s = key.createSign('sha256'); + s.update('foobar'); + var sig = s.sign(); + t.ok(sig); + t.ok(sig instanceof sshpk.Signature); + + var key2 = sshpk.parsePrivateKey(key.toBuffer('pem')); + + var v = key2.createVerify('sha256'); + v.update('foobar'); + t.ok(v.verify(sig)); + + var key3 = sshpk.generatePrivateKey('ecdsa'); + t.ok(!key3.fingerprint().matches(key)); + + t.end(); +}); + +test('PrivateKey.generate ecdsa p-384', function (t) { + var key = sshpk.generatePrivateKey('ecdsa', { curve: 'nistp384' }); + t.ok(sshpk.PrivateKey.isPrivateKey(key)); + t.strictEqual(key.type, 'ecdsa'); + t.strictEqual(key.curve, 'nistp384'); + t.strictEqual(key.size, 384); + t.end(); +}); + if (process.version.match(/^v0\.[0-9]\./)) return; +test('PrivateKey.generate ed25519', function (t) { + var key = sshpk.generatePrivateKey('ed25519'); + t.ok(sshpk.PrivateKey.isPrivateKey(key)); + t.strictEqual(key.type, 'ed25519'); + t.strictEqual(key.size, 256); + + var s = key.createSign('sha512'); + s.update('foobar'); + var sig = s.sign(); + t.ok(sig); + t.ok(sig instanceof sshpk.Signature); + + var sshPub = key.toPublic().toBuffer('ssh'); + var key2 = sshpk.parseKey(sshPub); + t.ok(key2.fingerprint().matches(key)); + + var v = key2.createVerify('sha512'); + v.update('foobar'); + t.ok(v.verify(sig)); + + t.end(); +}); + test('PrivateKey#createSign on ED25519 key', function (t) { var s = KEY_ED25519.createSign('sha512'); s.write('foobar');