Skip to content

Commit 941d67a

Browse files
author
Alex Wilson
committed
#26 want support for generating ecdsa keys
Reviewed by: Trent Mick <[email protected]> Reviewed by: Brittany Wald <[email protected]> Approved by: Trent Mick <[email protected]>
1 parent 1479eb2 commit 941d67a

File tree

9 files changed

+239
-18
lines changed

9 files changed

+239
-18
lines changed

README.md

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -227,17 +227,30 @@ Parameters
227227
- `format` -- String name of format to use, valid options are:
228228
- `auto`: choose automatically from all below
229229
- `pem`: supports both PKCS#1 and PKCS#8
230-
- `ssh`, `openssh`: new post-OpenSSH 6.5 internal format, produced by
230+
- `ssh`, `openssh`: new post-OpenSSH 6.5 internal format, produced by
231231
`ssh-keygen -o`
232232
- `pkcs1`, `pkcs8`: variants of `pem`
233233
- `rfc4253`: raw OpenSSH wire format
234234
- `options` -- Optional Object, extra options, with keys:
235-
- `filename` -- Optional String, name for the key being parsed
235+
- `filename` -- Optional String, name for the key being parsed
236236
(eg. the filename that was opened). Used to generate
237237
Error messages
238238
- `passphrase` -- Optional String, encryption passphrase used to decrypt an
239239
encrypted PEM file
240240

241+
### `generatePrivateKey(type[, options])`
242+
243+
Generates a new private key of a certain key type, from random data.
244+
245+
Parameters
246+
247+
- `type` -- String, type of key to generate. Currently supported are `'ecdsa'`
248+
and `'ed25519'`
249+
- `options` -- optional Object, with keys:
250+
- `curve` -- optional String, for `'ecdsa'` keys, specifies the curve to use.
251+
If ECDSA is specified and this option is not given, defaults to
252+
using `'nistp256'`.
253+
241254
### `PrivateKey.isPrivateKey(obj)`
242255

243256
Returns `true` if the given object is a valid `PrivateKey` object created by a

lib/dhe.js

Lines changed: 101 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,17 @@
1-
// Copyright 2015 Joyent, Inc.
1+
// Copyright 2017 Joyent, Inc.
22

3-
module.exports = DiffieHellman;
3+
module.exports = {
4+
DiffieHellman: DiffieHellman,
5+
generateECDSA: generateECDSA,
6+
generateED25519: generateED25519
7+
};
48

59
var assert = require('assert-plus');
610
var crypto = require('crypto');
711
var algs = require('./algs');
812
var utils = require('./utils');
913
var ed;
14+
var nacl;
1015

1116
var Key = require('./key');
1217
var PrivateKey = require('./private-key');
@@ -309,3 +314,97 @@ ECPrivate.prototype.deriveSharedSecret = function (pubKey) {
309314
var S = pubKey._pub.multiply(this._priv);
310315
return (new Buffer(S.getX().toBigInteger().toByteArray()));
311316
};
317+
318+
function generateED25519() {
319+
if (nacl === undefined)
320+
nacl = require('tweetnacl');
321+
322+
var pair = nacl.sign.keyPair();
323+
var priv = new Buffer(pair.secretKey);
324+
var pub = new Buffer(pair.publicKey);
325+
assert.strictEqual(priv.length, 64);
326+
assert.strictEqual(pub.length, 32);
327+
328+
var parts = [];
329+
parts.push({name: 'R', data: pub});
330+
parts.push({name: 'r', data: priv});
331+
var key = new PrivateKey({
332+
type: 'ed25519',
333+
parts: parts
334+
});
335+
return (key);
336+
}
337+
338+
/* Generates a new ECDSA private key on a given curve. */
339+
function generateECDSA(curve) {
340+
var parts = [];
341+
var key;
342+
343+
if (CRYPTO_HAVE_ECDH) {
344+
/*
345+
* Node crypto doesn't expose key generation directly, but the
346+
* ECDH instances can generate keys. It turns out this just
347+
* calls into the OpenSSL generic key generator, and we can
348+
* read its output happily without doing an actual DH. So we
349+
* use that here.
350+
*/
351+
var osCurve = {
352+
'nistp256': 'prime256v1',
353+
'nistp384': 'secp384r1',
354+
'nistp521': 'secp521r1'
355+
}[curve];
356+
357+
var dh = crypto.createECDH(osCurve);
358+
dh.generateKeys();
359+
360+
parts.push({name: 'curve',
361+
data: new Buffer(curve)});
362+
parts.push({name: 'Q', data: dh.getPublicKey()});
363+
parts.push({name: 'd', data: dh.getPrivateKey()});
364+
365+
key = new PrivateKey({
366+
type: 'ecdsa',
367+
curve: curve,
368+
parts: parts
369+
});
370+
return (key);
371+
372+
} else {
373+
if (ecdh === undefined)
374+
ecdh = require('ecc-jsbn');
375+
if (ec === undefined)
376+
ec = require('ecc-jsbn/lib/ec');
377+
if (jsbn === undefined)
378+
jsbn = require('jsbn').BigInteger;
379+
380+
var ecParams = new X9ECParameters(curve);
381+
382+
/* This algorithm taken from FIPS PUB 186-4 (section B.4.1) */
383+
var n = ecParams.getN();
384+
/*
385+
* The crypto.randomBytes() function can only give us whole
386+
* bytes, so taking a nod from X9.62, we round up.
387+
*/
388+
var cByteLen = Math.ceil((n.bitLength() + 64) / 8);
389+
var c = new jsbn(crypto.randomBytes(cByteLen));
390+
391+
var n1 = n.subtract(jsbn.ONE);
392+
var priv = c.mod(n1).add(jsbn.ONE);
393+
var pub = ecParams.getG().multiply(priv);
394+
395+
priv = new Buffer(priv.toByteArray());
396+
pub = new Buffer(ecParams.getCurve().
397+
encodePointHex(pub), 'hex');
398+
399+
parts.push({name: 'curve', data: new Buffer(curve)});
400+
parts.push({name: 'Q', data: pub});
401+
parts.push({name: 'd', data: priv});
402+
403+
key = new PrivateKey({
404+
type: 'ecdsa',
405+
curve: curve,
406+
parts: parts
407+
});
408+
return (key);
409+
}
410+
}

lib/identity.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// Copyright 2016 Joyent, Inc.
1+
// Copyright 2017 Joyent, Inc.
22

33
module.exports = Identity;
44

lib/index.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ module.exports = {
1818
parseSignature: Signature.parse,
1919
PrivateKey: PrivateKey,
2020
parsePrivateKey: PrivateKey.parse,
21+
generatePrivateKey: PrivateKey.generate,
2122
Certificate: Certificate,
2223
parseCertificate: Certificate.parse,
2324
createSelfSignedCertificate: Certificate.createSelfSigned,

lib/key.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// Copyright 2015 Joyent, Inc.
1+
// Copyright 2017 Joyent, Inc.
22

33
module.exports = Key;
44

@@ -7,7 +7,7 @@ var algs = require('./algs');
77
var crypto = require('crypto');
88
var Fingerprint = require('./fingerprint');
99
var Signature = require('./signature');
10-
var DiffieHellman = require('./dhe');
10+
var DiffieHellman = require('./dhe').DiffieHellman;
1111
var errs = require('./errors');
1212
var utils = require('./utils');
1313
var PrivateKey = require('./private-key');

lib/private-key.js

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// Copyright 2015 Joyent, Inc.
1+
// Copyright 2017 Joyent, Inc.
22

33
module.exports = PrivateKey;
44

@@ -10,6 +10,9 @@ var Signature = require('./signature');
1010
var errs = require('./errors');
1111
var util = require('util');
1212
var utils = require('./utils');
13+
var dhe = require('./dhe');
14+
var generateECDSA = dhe.generateECDSA;
15+
var generateED25519 = dhe.generateED25519;
1316
var edCompat;
1417
var ed;
1518

@@ -208,6 +211,25 @@ PrivateKey.isPrivateKey = function (obj, ver) {
208211
return (utils.isCompatible(obj, PrivateKey, ver));
209212
};
210213

214+
PrivateKey.generate = function (type, options) {
215+
if (options === undefined)
216+
options = {};
217+
assert.object(options, 'options');
218+
219+
switch (type) {
220+
case 'ecdsa':
221+
if (options.curve === undefined)
222+
options.curve = 'nistp256';
223+
assert.string(options.curve, 'options.curve');
224+
return (generateECDSA(options.curve));
225+
case 'ed25519':
226+
return (generateED25519());
227+
default:
228+
throw (new Error('Key generation not supported with key ' +
229+
'type "' + type + '"'));
230+
}
231+
};
232+
211233
/*
212234
* API versions for PrivateKey:
213235
* [1,0] -- initial ver

test/dhe_compat.js

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// Copyright 2015 Joyent, Inc. All rights reserved.
1+
// Copyright 2017 Joyent, Inc. All rights reserved.
22

33
var test = require('tape').test;
44

@@ -54,33 +54,33 @@ test('setup', function (t) {
5454
});
5555

5656
test('ecdhe shared secret', function (t) {
57-
var dh1 = new sshpk_dhe(EC_KEY);
57+
var dh1 = new sshpk_dhe.DiffieHellman(EC_KEY);
5858
var secret1 = dh1.computeSecret(EC2_KEY.toPublic());
5959
t.ok(Buffer.isBuffer(secret1));
6060
t.deepEqual(secret1, new Buffer(
6161
'UoKiio/gnWj4BdV41YvoHu9yhjynGBmphZ1JFbpk30o=', 'base64'));
6262

63-
var dh2 = new sshpk_dhe(EC2_KEY);
63+
var dh2 = new sshpk_dhe.DiffieHellman(EC2_KEY);
6464
var secret2 = dh2.computeSecret(EC_KEY.toPublic());
6565
t.deepEqual(secret1, secret2);
6666
t.end();
6767
});
6868

6969
test('ecdhe generate ephemeral', function (t) {
70-
var dh = new sshpk_dhe(EC_KEY);
70+
var dh = new sshpk_dhe.DiffieHellman(EC_KEY);
7171
var ek = dh.generateKey();
7272
t.ok(ek instanceof sshpk.PrivateKey);
7373
t.strictEqual(ek.type, 'ecdsa');
7474
t.strictEqual(ek.curve, 'nistp256');
7575

7676
var secret1 = dh.computeSecret(EC_KEY);
77-
var secret2 = (new sshpk_dhe(EC_KEY)).computeSecret(ek);
77+
var secret2 = (new sshpk_dhe.DiffieHellman(EC_KEY)).computeSecret(ek);
7878
t.deepEqual(secret1, secret2);
7979
t.end();
8080
});
8181

8282
test('ecdhe reject diff curves', function (t) {
83-
var dh = new sshpk_dhe(EC_KEY);
83+
var dh = new sshpk_dhe.DiffieHellman(EC_KEY);
8484
t.throws(function () {
8585
dh.computeSecret(ECOUT_KEY.toPublic());
8686
});
@@ -93,12 +93,12 @@ test('ecdhe reject diff curves', function (t) {
9393
t.strictEqual(dh.getPublicKey().fingerprint().toString(),
9494
EC2_KEY.fingerprint().toString());
9595

96-
var dh2 = new sshpk_dhe(ECOUT_KEY);
96+
var dh2 = new sshpk_dhe.DiffieHellman(ECOUT_KEY);
9797
t.throws(function () {
9898
dh2.setKey(EC_KEY);
9999
});
100100

101-
dh2 = new sshpk_dhe(EC_KEY);
101+
dh2 = new sshpk_dhe.DiffieHellman(EC_KEY);
102102
t.throws(function () {
103103
dh2.setKey(C_KEY);
104104
});

test/openssl-cmd.js

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// Copyright 2015 Joyent, Inc. All rights reserved.
1+
// Copyright 2017 Joyent, Inc. All rights reserved.
22

33
var test = require('tape').test;
44
var sshpk = require('../lib/index');
@@ -358,6 +358,35 @@ function genTests() {
358358
kid.stdin.write(certPem);
359359
kid.stdin.end();
360360
});
361+
362+
test('make a self-signed cert with generated key', function (t) {
363+
if (algo !== 'ecdsa') {
364+
t.end();
365+
return;
366+
}
367+
368+
var key = sshpk.generatePrivateKey(algo);
369+
370+
var id = sshpk.identityFromDN('cn=' + algo);
371+
var cert = sshpk.createSelfSignedCertificate(id, key,
372+
{ purposes: ['ca'] });
373+
var certPem = cert.toBuffer('pem');
374+
375+
fs.writeFileSync(path.join(tmp, 'ca.pem'), certPem);
376+
377+
var kid = spawn('openssl', ['verify',
378+
'-CAfile', path.join(tmp, 'ca.pem')]);
379+
var bufs = [];
380+
kid.stdout.on('data', bufs.push.bind(bufs));
381+
kid.on('close', function (rc) {
382+
t.equal(rc, 0);
383+
var output = Buffer.concat(bufs).toString();
384+
t.strictEqual(output.trim(), 'stdin: OK');
385+
t.end();
386+
});
387+
kid.stdin.write(certPem);
388+
kid.stdin.end();
389+
});
361390
});
362391

363392
test('teardown', function (t) {

test/private-key.js

Lines changed: 58 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// Copyright 2015 Joyent, Inc. All rights reserved.
1+
// Copyright 2017 Joyent, Inc. All rights reserved.
22

33
var test = require('tape').test;
44
var sshpk = require('../lib/index');
@@ -253,9 +253,66 @@ test('PrivateKey#createSign on ECDSA 256 key', function (t) {
253253
t.end();
254254
});
255255

256+
test('PrivateKey.generate ecdsa default', function (t) {
257+
var key = sshpk.generatePrivateKey('ecdsa');
258+
t.ok(sshpk.PrivateKey.isPrivateKey(key));
259+
t.strictEqual(key.type, 'ecdsa');
260+
t.strictEqual(key.curve, 'nistp256');
261+
t.strictEqual(key.size, 256);
262+
263+
var s = key.createSign('sha256');
264+
s.update('foobar');
265+
var sig = s.sign();
266+
t.ok(sig);
267+
t.ok(sig instanceof sshpk.Signature);
268+
269+
var key2 = sshpk.parsePrivateKey(key.toBuffer('pem'));
270+
271+
var v = key2.createVerify('sha256');
272+
v.update('foobar');
273+
t.ok(v.verify(sig));
274+
275+
var key3 = sshpk.generatePrivateKey('ecdsa');
276+
t.ok(!key3.fingerprint().matches(key));
277+
278+
t.end();
279+
});
280+
281+
test('PrivateKey.generate ecdsa p-384', function (t) {
282+
var key = sshpk.generatePrivateKey('ecdsa', { curve: 'nistp384' });
283+
t.ok(sshpk.PrivateKey.isPrivateKey(key));
284+
t.strictEqual(key.type, 'ecdsa');
285+
t.strictEqual(key.curve, 'nistp384');
286+
t.strictEqual(key.size, 384);
287+
t.end();
288+
});
289+
256290
if (process.version.match(/^v0\.[0-9]\./))
257291
return;
258292

293+
test('PrivateKey.generate ed25519', function (t) {
294+
var key = sshpk.generatePrivateKey('ed25519');
295+
t.ok(sshpk.PrivateKey.isPrivateKey(key));
296+
t.strictEqual(key.type, 'ed25519');
297+
t.strictEqual(key.size, 256);
298+
299+
var s = key.createSign('sha512');
300+
s.update('foobar');
301+
var sig = s.sign();
302+
t.ok(sig);
303+
t.ok(sig instanceof sshpk.Signature);
304+
305+
var sshPub = key.toPublic().toBuffer('ssh');
306+
var key2 = sshpk.parseKey(sshPub);
307+
t.ok(key2.fingerprint().matches(key));
308+
309+
var v = key2.createVerify('sha512');
310+
v.update('foobar');
311+
t.ok(v.verify(sig));
312+
313+
t.end();
314+
});
315+
259316
test('PrivateKey#createSign on ED25519 key', function (t) {
260317
var s = KEY_ED25519.createSign('sha512');
261318
s.write('foobar');

0 commit comments

Comments
 (0)