From 0d6852fa472e0d3d6083b18ca3508cb3966fffe8 Mon Sep 17 00:00:00 2001 From: Alex Wilson Date: Thu, 11 Feb 2016 16:19:40 -0800 Subject: [PATCH] joyent/node-sshpk#33 implement support for Ed25519 keys in PEM format (curdle-pkix) Reviewed by: Tim Kordas --- lib/algs.js | 8 +- lib/dhe.js | 23 +++--- lib/ed-compat.js | 5 +- lib/formats/pem.js | 11 ++- lib/formats/pkcs1.js | 56 ++++++++++++++ lib/formats/pkcs8.js | 111 +++++++++++++++++++++++++++ lib/formats/rfc4253.js | 35 +++++++-- lib/formats/x509.js | 3 +- lib/key.js | 3 +- lib/private-key.js | 19 ++--- lib/utils.js | 63 ++++++++++++++- test/assets/curdle-pkix-privonly.pem | 3 + test/assets/curdle-pkix-withpub.pem | 5 ++ test/assets/ed25519-invalid-ber.pem | 4 + test/assets/ed25519-invalid-mask.pem | 4 + test/assets/ed25519-invalid-zero.pem | 4 + test/assets/ed25519-pkix-cert.pem | 9 +++ test/assets/ed25519-pkix.pem | 3 + test/assets/id_ed25519.pem | 4 + test/certs.js | 26 +++++-- test/dhe.js | 8 +- test/fingerprint.js | 6 ++ test/pem.js | 18 +++++ test/private-key.js | 70 +++++++++++++++++ 24 files changed, 449 insertions(+), 52 deletions(-) create mode 100644 test/assets/curdle-pkix-privonly.pem create mode 100644 test/assets/curdle-pkix-withpub.pem create mode 100644 test/assets/ed25519-invalid-ber.pem create mode 100644 test/assets/ed25519-invalid-mask.pem create mode 100644 test/assets/ed25519-invalid-zero.pem create mode 100644 test/assets/ed25519-pkix-cert.pem create mode 100644 test/assets/ed25519-pkix.pem create mode 100644 test/assets/id_ed25519.pem diff --git a/lib/algs.js b/lib/algs.js index f30af56..da5d0c7 100644 --- a/lib/algs.js +++ b/lib/algs.js @@ -14,9 +14,8 @@ var algInfo = { sizePart: 'Q' }, 'ed25519': { - parts: ['R'], - normalize: false, - sizePart: 'R' + parts: ['A'], + sizePart: 'A' } }; algInfo['curve25519'] = algInfo['ed25519']; @@ -32,8 +31,7 @@ var algPrivInfo = { parts: ['curve', 'Q', 'd'] }, 'ed25519': { - parts: ['R', 'r'], - normalize: false + parts: ['A', 'k'] } }; algPrivInfo['curve25519'] = algPrivInfo['ed25519']; diff --git a/lib/dhe.js b/lib/dhe.js index f94a583..2e844e7 100644 --- a/lib/dhe.js +++ b/lib/dhe.js @@ -79,7 +79,8 @@ function DiffieHellman(key) { nacl = require('tweetnacl'); if (this._isPriv) { - this._priv = key.part.r.data; + utils.assertCompatible(key, PrivateKey, [1, 5], 'key'); + this._priv = key.part.k.data; } } else { @@ -143,7 +144,10 @@ DiffieHellman.prototype.setKey = function (pk) { } } else if (pk.type === 'curve25519') { - this._priv = pk.part.r.data; + var k = pk.part.k; + if (!pk.part.k) + k = pk.part.r; + this._priv = k.data; if (this._priv[0] === 0x00) this._priv = this._priv.slice(1); this._priv = this._priv.slice(0, 32); @@ -175,13 +179,12 @@ DiffieHellman.prototype.computeSecret = function (otherpk) { } } else if (this._algo === 'curve25519') { - pub = otherpk.part.R.data; + pub = otherpk.part.A.data; while (pub[0] === 0x00 && pub.length > 32) pub = pub.slice(1); + var priv = this._priv; assert.strictEqual(pub.length, 32); - assert.strictEqual(this._priv.length, 64); - - var priv = this._priv.slice(0, 32); + assert.strictEqual(priv.length, 32); var secret = nacl.box.before(new Uint8Array(pub), new Uint8Array(priv)); @@ -261,8 +264,8 @@ DiffieHellman.prototype.generateKey = function () { assert.strictEqual(priv.length, 64); assert.strictEqual(pub.length, 32); - parts.push({name: 'R', data: pub}); - parts.push({name: 'r', data: priv}); + parts.push({name: 'A', data: pub}); + parts.push({name: 'k', data: priv}); this._key = new PrivateKey({ type: 'curve25519', parts: parts @@ -327,8 +330,8 @@ function generateED25519() { assert.strictEqual(pub.length, 32); var parts = []; - parts.push({name: 'R', data: pub}); - parts.push({name: 'r', data: priv}); + parts.push({name: 'A', data: pub}); + parts.push({name: 'k', data: priv.slice(0, 32)}); var key = new PrivateKey({ type: 'ed25519', parts: parts diff --git a/lib/ed-compat.js b/lib/ed-compat.js index 5365fb1..6d906b7 100644 --- a/lib/ed-compat.js +++ b/lib/ed-compat.js @@ -56,7 +56,7 @@ Verifier.prototype.verify = function (signature, fmt) { return (nacl.sign.detached.verify( new Uint8Array(Buffer.concat(this.chunks)), new Uint8Array(sig), - new Uint8Array(this.key.part.R.data))); + new Uint8Array(this.key.part.A.data))); }; function Signer(key, hashAlgo) { @@ -88,7 +88,8 @@ Signer.prototype.update = function (chunk) { Signer.prototype.sign = function () { var sig = nacl.sign.detached( new Uint8Array(Buffer.concat(this.chunks)), - new Uint8Array(this.key.part.r.data)); + new Uint8Array(Buffer.concat([ + this.key.part.k.data, this.key.part.A.data]))); var sigBuf = new Buffer(sig); var sigObj = Signature.parse(sigBuf, 'ed25519', 'raw'); sigObj.hashAlgorithm = 'sha512'; diff --git a/lib/formats/pem.js b/lib/formats/pem.js index c254e4e..9196449 100644 --- a/lib/formats/pem.js +++ b/lib/formats/pem.js @@ -34,11 +34,11 @@ function read(buf, options, forceType) { var lines = buf.trim().split('\n'); var m = lines[0].match(/*JSSTYLED*/ - /[-]+[ ]*BEGIN ([A-Z0-9]+ )?(PUBLIC|PRIVATE) KEY[ ]*[-]+/); + /[-]+[ ]*BEGIN ([A-Z0-9][A-Za-z0-9]+ )?(PUBLIC|PRIVATE) KEY[ ]*[-]+/); assert.ok(m, 'invalid PEM header'); var m2 = lines[lines.length - 1].match(/*JSSTYLED*/ - /[-]+[ ]*END ([A-Z0-9]+ )?(PUBLIC|PRIVATE) KEY[ ]*[-]+/); + /[-]+[ ]*END ([A-Z0-9][A-Za-z0-9]+ )?(PUBLIC|PRIVATE) KEY[ ]*[-]+/); assert.ok(m2, 'invalid PEM footer'); /* Begin and end banners must match key type */ @@ -135,7 +135,12 @@ function read(buf, options, forceType) { function write(key, options, type) { assert.object(key); - var alg = {'ecdsa': 'EC', 'rsa': 'RSA', 'dsa': 'DSA'}[key.type]; + var alg = { + 'ecdsa': 'EC', + 'rsa': 'RSA', + 'dsa': 'DSA', + 'ed25519': 'EdDSA' + }[key.type]; var header; var der = new asn1.BerWriter(); diff --git a/lib/formats/pkcs1.js b/lib/formats/pkcs1.js index a5676af..9d7246d 100644 --- a/lib/formats/pkcs1.js +++ b/lib/formats/pkcs1.js @@ -55,6 +55,11 @@ function readPkcs1(alg, type, der) { else if (type === 'public') return (readPkcs1ECDSAPublic(der)); throw (new Error('Unknown key type: ' + type)); + case 'EDDSA': + case 'EdDSA': + if (type === 'private') + return (readPkcs1EdDSAPrivate(der)); + throw (new Error(type + ' keys not supported with EdDSA')); default: throw (new Error('Unknown key algo: ' + alg)); } @@ -134,6 +139,31 @@ function readPkcs1DSAPrivate(der) { return (new PrivateKey(key)); } +function readPkcs1EdDSAPrivate(der) { + var version = readMPInt(der, 'version'); + assert.strictEqual(version.readUInt8(0), 1); + + // private key + var k = der.readString(asn1.Ber.OctetString, true); + + der.readSequence(0xa0); + var oid = der.readOID(); + assert.strictEqual(oid, '1.3.101.112', 'the ed25519 curve identifier'); + + der.readSequence(0xa1); + var A = utils.readBitString(der); + + var key = { + type: 'ed25519', + parts: [ + { name: 'A', data: utils.zeroPadToLength(A, 32) }, + { name: 'k', data: k } + ] + }; + + return (new PrivateKey(key)); +} + function readPkcs1DSAPublic(der) { var y = readMPInt(der, 'y'); var p = readMPInt(der, 'p'); @@ -236,6 +266,12 @@ function writePkcs1(der, key) { else writePkcs1ECDSAPublic(der, key); break; + case 'ed25519': + if (PrivateKey.isPrivateKey(key)) + writePkcs1EdDSAPrivate(der, key); + else + writePkcs1EdDSAPublic(der, key); + break; default: throw (new Error('Unknown key algo: ' + key.type)); } @@ -318,3 +354,23 @@ function writePkcs1ECDSAPrivate(der, key) { der.writeBuffer(Q, asn1.Ber.BitString); der.endSequence(); } + +function writePkcs1EdDSAPrivate(der, key) { + var ver = new Buffer(1); + ver[0] = 1; + der.writeBuffer(ver, asn1.Ber.Integer); + + der.writeBuffer(key.part.k.data, asn1.Ber.OctetString); + + der.startSequence(0xa0); + der.writeOID('1.3.101.112'); + der.endSequence(); + + der.startSequence(0xa1); + utils.writeBitString(der, key.part.A.data); + der.endSequence(); +} + +function writePkcs1EdDSAPublic(der, key) { + throw (new Error('Public keys are not supported for EdDSA PKCS#1')); +} diff --git a/lib/formats/pkcs8.js b/lib/formats/pkcs8.js index 4ccbefc..0838b76 100644 --- a/lib/formats/pkcs8.js +++ b/lib/formats/pkcs8.js @@ -62,6 +62,18 @@ function readPkcs8(alg, type, der) { return (readPkcs8ECDSAPublic(der)); else return (readPkcs8ECDSAPrivate(der)); + case '1.3.101.112': + if (type === 'public') { + return (readPkcs8EdDSAPublic(der)); + } else { + return (readPkcs8EdDSAPrivate(der)); + } + case '1.3.101.110': + if (type === 'public') { + return (readPkcs8X25519Public(der)); + } else { + return (readPkcs8X25519Private(der)); + } default: throw (new Error('Unknown key type OID ' + oid)); } @@ -322,6 +334,83 @@ function readPkcs8ECDSAPublic(der) { return (new Key(key)); } +function readPkcs8EdDSAPublic(der) { + if (der.peek() === 0x00) + der.readByte(); + + var A = utils.readBitString(der); + + var key = { + type: 'ed25519', + parts: [ + { name: 'A', data: utils.zeroPadToLength(A, 32) } + ] + }; + + return (new Key(key)); +} + +function readPkcs8X25519Public(der) { + var A = utils.readBitString(der); + + var key = { + type: 'curve25519', + parts: [ + { name: 'A', data: utils.zeroPadToLength(A, 32) } + ] + }; + + return (new Key(key)); +} + +function readPkcs8EdDSAPrivate(der) { + if (der.peek() === 0x00) + der.readByte(); + + der.readSequence(asn1.Ber.OctetString); + var k = der.readString(asn1.Ber.OctetString, true); + k = utils.zeroPadToLength(k, 32); + + var A; + if (der.peek() === asn1.Ber.BitString) { + A = utils.readBitString(der); + A = utils.zeroPadToLength(A, 32); + } else { + A = utils.calculateED25519Public(k); + } + + var key = { + type: 'ed25519', + parts: [ + { name: 'A', data: utils.zeroPadToLength(A, 32) }, + { name: 'k', data: utils.zeroPadToLength(k, 32) } + ] + }; + + return (new PrivateKey(key)); +} + +function readPkcs8X25519Private(der) { + if (der.peek() === 0x00) + der.readByte(); + + der.readSequence(asn1.Ber.OctetString); + var k = der.readString(asn1.Ber.OctetString, true); + k = utils.zeroPadToLength(k, 32); + + var A = utils.calculateX25519Public(k); + + var key = { + type: 'curve25519', + parts: [ + { name: 'A', data: utils.zeroPadToLength(A, 32) }, + { name: 'k', data: utils.zeroPadToLength(k, 32) } + ] + }; + + return (new PrivateKey(key)); +} + function writePkcs8(der, key) { der.startSequence(); @@ -354,6 +443,13 @@ function writePkcs8(der, key) { else writePkcs8ECDSAPublic(key, der); break; + case 'ed25519': + der.writeOID('1.3.101.112'); + if (PrivateKey.isPrivateKey(key)) + throw (new Error('Ed25519 private keys in pkcs8 ' + + 'format are not supported')); + writePkcs8EdDSAPublic(key, der); + break; default: throw (new Error('Unsupported key type: ' + key.type)); } @@ -503,3 +599,18 @@ function writePkcs8ECDSAPrivate(key, der) { der.endSequence(); der.endSequence(); } + +function writePkcs8EdDSAPublic(key, der) { + der.endSequence(); + + utils.writeBitString(der, key.part.A.data); +} + +function writePkcs8EdDSAPrivate(key, der) { + der.endSequence(); + + var k = utils.mpNormalize(key.part.k.data, true); + der.startSequence(asn1.Ber.OctetString); + der.writeBuffer(k, asn1.Ber.OctetString); + der.endSequence(); +} diff --git a/lib/formats/rfc4253.js b/lib/formats/rfc4253.js index 9d436dd..56b7682 100644 --- a/lib/formats/rfc4253.js +++ b/lib/formats/rfc4253.js @@ -97,12 +97,25 @@ function read(partial, type, buf, options) { var normalized = true; for (var i = 0; i < algInfo.parts.length; ++i) { - parts[i].name = algInfo.parts[i]; - if (parts[i].name !== 'curve' && - algInfo.normalize !== false) { - var p = parts[i]; - var nd = utils.mpNormalize(p.data); - if (nd !== p.data) { + var p = parts[i]; + p.name = algInfo.parts[i]; + /* + * OpenSSH stores ed25519 "private" keys as seed + public key + * concat'd together (k followed by A). We want to keep them + * separate for other formats that don't do this. + */ + if (key.type === 'ed25519' && p.name === 'k') + p.data = p.data.slice(0, 32); + + if (p.name !== 'curve' && algInfo.normalize !== false) { + var nd; + if (key.type === 'ed25519') { + nd = utils.zeroPadToLength(p.data, 32); + } else { + nd = utils.mpNormalize(p.data); + } + if (nd.toString('binary') !== + p.data.toString('binary')) { p.data = nd; normalized = false; } @@ -137,8 +150,14 @@ function write(key, options) { for (i = 0; i < parts.length; ++i) { var data = key.part[parts[i]].data; - if (algInfo.normalize !== false) - data = utils.mpNormalize(data); + if (algInfo.normalize !== false) { + if (key.type === 'ed25519') + data = utils.zeroPadToLength(data, 32); + else + data = utils.mpNormalize(data); + } + if (key.type === 'ed25519' && parts[i] === 'k') + data = Buffer.concat([data, key.part.A.data]); buf.writeBuffer(data); } diff --git a/lib/formats/x509.js b/lib/formats/x509.js index 23acd24..2396574 100644 --- a/lib/formats/x509.js +++ b/lib/formats/x509.js @@ -70,7 +70,8 @@ var SIGN_ALGS = { 'ecdsa-sha1': '1.2.840.10045.4.1', 'ecdsa-sha256': '1.2.840.10045.4.3.2', 'ecdsa-sha384': '1.2.840.10045.4.3.3', - 'ecdsa-sha512': '1.2.840.10045.4.3.4' + 'ecdsa-sha512': '1.2.840.10045.4.3.4', + 'ed25519-sha512': '1.3.101.112' }; Object.keys(SIGN_ALGS).forEach(function (k) { SIGN_ALGS[SIGN_ALGS[k]] = k; diff --git a/lib/key.js b/lib/key.js index 64305fc..f8ef22d 100644 --- a/lib/key.js +++ b/lib/key.js @@ -256,8 +256,9 @@ Key.isKey = function (obj, ver) { * [1,3] -- added defaultHashAlgorithm * [1,4] -- added ed support, createDH * [1,5] -- first explicitly tagged version + * [1,6] -- changed ed25519 part names */ -Key.prototype._sshpkApiVersion = [1, 5]; +Key.prototype._sshpkApiVersion = [1, 6]; Key._oldVersionDetect = function (obj) { assert.func(obj.toBuffer); diff --git a/lib/private-key.js b/lib/private-key.js index 5b05fe0..4c98be2 100644 --- a/lib/private-key.js +++ b/lib/private-key.js @@ -92,40 +92,36 @@ PrivateKey.prototype.derive = function (newType) { if (nacl === undefined) nacl = require('tweetnacl'); - priv = this.part.r.data; + priv = this.part.k.data; if (priv[0] === 0x00) priv = priv.slice(1); - priv = priv.slice(0, 32); pair = nacl.box.keyPair.fromSecretKey(new Uint8Array(priv)); pub = new Buffer(pair.publicKey); - priv = Buffer.concat([priv, pub]); return (new PrivateKey({ type: 'curve25519', parts: [ - { name: 'R', data: utils.mpNormalize(pub) }, - { name: 'r', data: priv } + { name: 'A', data: utils.mpNormalize(pub) }, + { name: 'k', data: utils.mpNormalize(priv) } ] })); } else if (this.type === 'curve25519' && newType === 'ed25519') { if (nacl === undefined) nacl = require('tweetnacl'); - priv = this.part.r.data; + priv = this.part.k.data; if (priv[0] === 0x00) priv = priv.slice(1); - priv = priv.slice(0, 32); pair = nacl.sign.keyPair.fromSeed(new Uint8Array(priv)); pub = new Buffer(pair.publicKey); - priv = Buffer.concat([priv, pub]); return (new PrivateKey({ type: 'ed25519', parts: [ - { name: 'R', data: utils.mpNormalize(pub) }, - { name: 'r', data: priv } + { name: 'A', data: utils.mpNormalize(pub) }, + { name: 'k', data: utils.mpNormalize(priv) } ] })); } @@ -239,8 +235,9 @@ PrivateKey.generate = function (type, options) { * [1,2] -- added defaultHashAlgorithm * [1,3] -- added derive, ed, createDH * [1,4] -- first tagged version + * [1,5] -- changed ed25519 part names and format */ -PrivateKey.prototype._sshpkApiVersion = [1, 4]; +PrivateKey.prototype._sshpkApiVersion = [1, 5]; PrivateKey._oldVersionDetect = function (obj) { assert.func(obj.toPublic); diff --git a/lib/utils.js b/lib/utils.js index 0eccaab..d2e9c0d 100644 --- a/lib/utils.js +++ b/lib/utils.js @@ -4,6 +4,8 @@ module.exports = { bufferSplit: bufferSplit, addRSAMissing: addRSAMissing, calculateDSAPublic: calculateDSAPublic, + calculateED25519Public: calculateED25519Public, + calculateX25519Public: calculateX25519Public, mpNormalize: mpNormalize, mpDenormalize: mpDenormalize, ecNormalize: ecNormalize, @@ -12,7 +14,10 @@ module.exports = { isCompatible: isCompatible, opensslKeyDeriv: opensslKeyDeriv, opensshCipherInfo: opensshCipherInfo, - publicFromPrivateECDSA: publicFromPrivateECDSA + publicFromPrivateECDSA: publicFromPrivateECDSA, + zeroPadToLength: zeroPadToLength, + writeBitString: writeBitString, + readBitString: readBitString }; var assert = require('assert-plus'); @@ -20,8 +25,10 @@ var PrivateKey = require('./private-key'); var Key = require('./key'); var crypto = require('crypto'); var algs = require('./algs'); +var asn1 = require('asn1'); var ec, jsbn; +var nacl; var MAX_CLASS_DEPTH = 3; @@ -184,6 +191,24 @@ function ecNormalize(buf, addZero) { return (b); } +function readBitString(der, tag) { + if (tag === undefined) + tag = asn1.Ber.BitString; + var buf = der.readString(tag, true); + assert.strictEqual(buf[0], 0x00, 'bit strings with unused bits are ' + + 'not supported (0x' + buf[0].toString(16) + ')'); + return (buf.slice(1)); +} + +function writeBitString(der, buf, tag) { + if (tag === undefined) + tag = asn1.Ber.BitString; + var b = new Buffer(buf.length + 1); + b[0] = 0x00; + buf.copy(b, 1); + der.writeBuffer(b, tag); +} + function mpNormalize(buf) { assert.buffer(buf); while (buf.length > 1 && buf[0] === 0x00 && (buf[1] & 0x80) === 0x00) @@ -204,6 +229,22 @@ function mpDenormalize(buf) { return (buf); } +function zeroPadToLength(buf, len) { + assert.buffer(buf); + assert.number(len); + while (buf.length > len) { + assert.equal(buf[0], 0x00); + buf = buf.slice(1); + } + while (buf.length < len) { + var b = new Buffer(buf.length + 1); + b[0] = 0x00; + buf.copy(b, 1); + buf = b; + } + return (buf); +} + function bigintToMpBuf(bigint) { var buf = new Buffer(bigint.toByteArray()); buf = mpNormalize(buf); @@ -228,6 +269,26 @@ function calculateDSAPublic(g, p, x) { return (ybuf); } +function calculateED25519Public(k) { + assert.buffer(k); + + if (nacl === undefined) + nacl = require('tweetnacl'); + + var kp = nacl.sign.keyPair.fromSeed(new Uint8Array(k)); + return (new Buffer(kp.publicKey)); +} + +function calculateX25519Public(k) { + assert.buffer(k); + + if (nacl === undefined) + nacl = require('tweetnacl'); + + var kp = nacl.box.keyPair.fromSeed(new Uint8Array(k)); + return (new Buffer(kp.publicKey)); +} + function addRSAMissing(key) { assert.object(key); assertCompatible(key, PrivateKey, [1, 1]); diff --git a/test/assets/curdle-pkix-privonly.pem b/test/assets/curdle-pkix-privonly.pem new file mode 100644 index 0000000..e447080 --- /dev/null +++ b/test/assets/curdle-pkix-privonly.pem @@ -0,0 +1,3 @@ +-----BEGIN PRIVATE KEY----- +MC4CAQAwBQYDK2VwBCIEINTuctv5E1hK1bbY8fdp+K06/nwoy/HU++CXqI9EdVhC +-----END PRIVATE KEY----- diff --git a/test/assets/curdle-pkix-withpub.pem b/test/assets/curdle-pkix-withpub.pem new file mode 100644 index 0000000..e56a7d9 --- /dev/null +++ b/test/assets/curdle-pkix-withpub.pem @@ -0,0 +1,5 @@ +-----BEGIN PRIVATE KEY----- +MHICAQEwBQYDK2VwBCIEINTuctv5E1hK1bbY8fdp+K06/nwoy/HU++CXqI9EdVhC +oB8wHQYKKoZIhvcNAQkJFDEPDA1DdXJkbGUgQ2hhaXJzgSEAGb9ECWmEzf6FQbrB +Z9w7lshQhqowtrbLDFw4rXAxZuE= +-----END PRIVATE KEY------ diff --git a/test/assets/ed25519-invalid-ber.pem b/test/assets/ed25519-invalid-ber.pem new file mode 100644 index 0000000..69ef810 --- /dev/null +++ b/test/assets/ed25519-invalid-ber.pem @@ -0,0 +1,4 @@ +-----BEGIN PRIVATE KEY----- +MIACAQAwgAYDK2VwAAAEIgQg1O5y2/kTWErVttjx92n4rTr+fCjL8dT74Jeoj0R1W +EIAAA== +-----END PRIVATE KEY----- diff --git a/test/assets/ed25519-invalid-mask.pem b/test/assets/ed25519-invalid-mask.pem new file mode 100644 index 0000000..b050f4b --- /dev/null +++ b/test/assets/ed25519-invalid-mask.pem @@ -0,0 +1,4 @@ +-----BEGIN PRIVATE KEY----- +MFMCAQEwBQYDK2VuBCIEIPj///////////////////////////////////////8/oS +MDIQCEfA0sN1I082XmYJVRh6NzWg92E9FgnTpqTYxTrqpaIg== +-----END PRIVATE KEY----- diff --git a/test/assets/ed25519-invalid-zero.pem b/test/assets/ed25519-invalid-zero.pem new file mode 100644 index 0000000..ec8cde8 --- /dev/null +++ b/test/assets/ed25519-invalid-zero.pem @@ -0,0 +1,4 @@ +-----BEGIN PRIVATE KEY----- +MFICAQEwBQYDK2VwBCIEIC3GfeUYbZGTAhwLEE2cbvJL7ivTlcy17VottfN6L8HwoS +IDIADBfk2Lv/J8H7YYwj/OmIcDx++jzVkKrKwS0/HjyQyM +-----END PRIVATE KEY----- diff --git a/test/assets/ed25519-pkix-cert.pem b/test/assets/ed25519-pkix-cert.pem new file mode 100644 index 0000000..3f4b5b2 --- /dev/null +++ b/test/assets/ed25519-pkix-cert.pem @@ -0,0 +1,9 @@ +-----BEGIN CERTIFICATE----- +MIIBLDCB36ADAgECAghWAUdKKo3DMDAFBgMrZXAwGTEXMBUGA1UEAwwOSUVURiBUZX +N0IERlbW8wHhcNMTYwODAxMTIxOTI0WhcNNDAxMjMxMjM1OTU5WjAZMRcwFQYDVQQD +DA5JRVRGIFRlc3QgRGVtbzAqMAUGAytlbgMhAIUg8AmJMKdUdIt93LQ+91oNvzoNJj +ga9OukqY6qm05qo0UwQzAPBgNVHRMBAf8EBTADAQEAMA4GA1UdDwEBAAQEAwIDCDAg +BgNVHQ4BAQAEFgQUmx9e7e0EM4Xk97xiPFl1uQvIuzswBQYDK2VwA0EAryMB/t3J5v +/BzKc9dNZIpDmAgs3babFOTQbs+BolzlDUwsPrdGxO3YNGhW7Ibz3OGhhlxXrCe1Cg +w1AH9efZBw== +-----END CERTIFICATE----- diff --git a/test/assets/ed25519-pkix.pem b/test/assets/ed25519-pkix.pem new file mode 100644 index 0000000..e447080 --- /dev/null +++ b/test/assets/ed25519-pkix.pem @@ -0,0 +1,3 @@ +-----BEGIN PRIVATE KEY----- +MC4CAQAwBQYDK2VwBCIEINTuctv5E1hK1bbY8fdp+K06/nwoy/HU++CXqI9EdVhC +-----END PRIVATE KEY----- diff --git a/test/assets/id_ed25519.pem b/test/assets/id_ed25519.pem new file mode 100644 index 0000000..a9fbbcd --- /dev/null +++ b/test/assets/id_ed25519.pem @@ -0,0 +1,4 @@ +-----BEGIN EdDSA PRIVATE KEY----- +MFECAQEEIERoX2iLQWYqcQEqloBMIIFX9c/pynu4W3y23wQ1cr1goAUGAytlcKEj +AyEASLSmR897/6RuadNIfSZ+vQngWrsztuyEUAoHq4LIsOY= +-----END EdDSA PRIVATE KEY----- diff --git a/test/certs.js b/test/certs.js index 80cb4df..68dbf75 100644 --- a/test/certs.js +++ b/test/certs.js @@ -178,12 +178,11 @@ test('create ed25519 self-signed, loopback', function (t) { var id = sshpk.identityForHost('foobar.com'); var cert = sshpk.createSelfSignedCertificate(id, SUE_KEY); - t.throws(function () { - cert.toBuffer('pem'); - }); - t.throws(function () { - cert.toBuffer('x509'); - }); + var x509 = cert.toBuffer('pem'); + var cert2 = sshpk.parseCertificate(x509, 'pem'); + t.ok(SUE_KEY.fingerprint().matches(cert2.subjectKey)); + t.ok(cert2.subjects[0].equals(cert.subjects[0])); + t.ok(cert2.isSignedByKey(SUE_KEY)); var ossh = cert.toBuffer('openssh'); var cert3 = sshpk.parseCertificate(ossh, 'openssh'); @@ -342,3 +341,18 @@ test('example cert: openssh rsa with sha256 (7.0p1+)', function (t) { 'sha256'); t.end(); }); + +test('example cert: ed25519 cert from curdle-pkix-04', function (t) { + var cert = sshpk.parseCertificate( + fs.readFileSync(path.join(testDir, 'ed25519-pkix-cert.pem')), + 'pem'); + t.strictEqual(cert.subjectKey.type, 'curve25519'); + t.strictEqual(cert.subjects[0].type, 'user'); + t.strictEqual(cert.subjects[0].cn, 'IETF Test Demo'); + + var key = sshpk.parsePrivateKey( + fs.readFileSync(path.join(testDir, 'ed25519-pkix.pem')), 'pem'); + t.ok(cert.isSignedByKey(key)); + + t.end(); +}); diff --git a/test/dhe.js b/test/dhe.js index 511bebb..44f69f3 100644 --- a/test/dhe.js +++ b/test/dhe.js @@ -52,8 +52,8 @@ test('derive ed25519 -> curve25519 -> back (negative seed)', function (t) { t.strictEqual(key.size, 256); var key2 = key.derive('ed25519'); t.ok(key2.fingerprint().matches(NG_KEY)); - t.strictEqual(key2.part.r.toString('base64'), - key.part.r.toString('base64')); + t.strictEqual(key2.part.k.toString('base64'), + key.part.k.toString('base64')); t.end(); }); @@ -64,8 +64,8 @@ test('derive curve25519 -> ed25519', function (t) { var k2 = k.derive('ed25519'); t.strictEqual(k2.type, 'ed25519'); t.ok(k2.fingerprint().matches(ED_KEY)); - t.strictEqual(k2.part.r.toString('base64'), - ED_KEY.part.r.toString('base64')); + t.strictEqual(k2.part.k.toString('base64'), + ED_KEY.part.k.toString('base64')); t.end(); }); diff --git a/test/fingerprint.js b/test/fingerprint.js index 0edce2a..a74b5ac 100644 --- a/test/fingerprint.js +++ b/test/fingerprint.js @@ -30,6 +30,10 @@ var ED_SSH = 'ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIEi0pkfPe/+kbmnTSH0mfr0J4' + var ED2_SSH = 'ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPu+h5Zu8GHgh8seZ9GittT' + 'WHfpbi0vkNksH77yaMKqD'; +var ED2_PEM = '-----BEGIN PUBLIC KEY-----\n' + + 'MCowBQYDK2VwAyEA+76Hlm7wYeCHyx5n0aK21NYd+luLS+Q2SwfvvJowqoM=\n' + + '-----END PUBLIC KEY-----\n'; + test('fingerprint', function(t) { var k = sshpk.parseKey(SSH_1024, 'ssh'); var fp = k.fingerprint('md5').toString(); @@ -96,6 +100,8 @@ test('fingerprint of non-normalized ed25519 key', function(t) { var f = sshpk.parseFingerprint( 'SHA256:k1NS4bL2M1fG3JKd8WI9t6ETq+6VeRtvLAxt8DC0exE'); t.ok(f.matches(k)); + k = sshpk.parseKey(ED2_PEM, 'pem'); + t.ok(f.matches(k)); t.end(); }); diff --git a/test/pem.js b/test/pem.js index d60aabc..fb084d2 100644 --- a/test/pem.js +++ b/test/pem.js @@ -147,6 +147,16 @@ var ENC_ECDSA = '-----BEGIN EC PRIVATE KEY-----\n' + var ED_SSH = 'ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIEi0pkfPe/+kbmnTSH0mfr0J4' + 'Fq7M7bshFAKB6uCyLDm foo@bar'; +var ED_PKCS8 = '-----BEGIN PUBLIC KEY-----\n' + + 'MCowBQYDK2VwAyEAGb9ECWmEzf6FQbrBZ9w7lshQhqowtrbLDFw4rXAxZuE=\n' + + '-----END PUBLIC KEY-----\n'; + +var ED_PKIX_SSH = 'ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIQAZv0QJaYTN/oVBusFn3' + + 'DuWyFCGqjC2tssMXDitcDFm4Q== test'; + +var ED_PKIX_NORM = 'ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIBm/RAlphM3+hUG6wWfc' + + 'O5bIUIaqMLa2ywxcOK1wMWbh test'; + var OAKLEY_PEM = '-----BEGIN PUBLIC KEY-----\n' + 'MIGpMHsGByqGSM49AgEwcAIBATAdBgcqhkjOPQECMBICAgCbBgkqhkjOPQECAwIC\n' + 'AT4wCAQBAAQDBzOPBCkEAAAAAAAAAAAAAAAAAAAAAAAAAHsAAAAAAAAAAAAAAAAA\n' + @@ -310,6 +320,14 @@ test('ed25519 ssh key with auto', function(t) { t.end(); }); +test('ed25519 key from pkcs8', function(t) { + var k = sshpk.parseKey(ED_PKCS8, 'auto'); + t.equal(k.type, 'ed25519'); + k.comment = 'test'; + t.equal(k.toString('ssh'), ED_PKIX_NORM); + t.end(); +}); + test('encrypted rsa private key', function(t) { t.throws(function () { var k = sshpk.parseKey(ENC_PRIVATE, 'pem'); diff --git a/test/private-key.js b/test/private-key.js index 034f148..9e4cd9b 100644 --- a/test/private-key.js +++ b/test/private-key.js @@ -85,6 +85,76 @@ test('PrivateKey load ED25519 256 key', function (t) { keyPem = key.toBuffer('openssh'); var key2 = sshpk.parsePrivateKey(keyPem, 'openssh'); t.ok(ID_ED25519_FP.matches(key2)); + + keyPem = key.toBuffer('pkcs1'); + var realKeyPem = fs.readFileSync(path.join(testDir, 'id_ed25519.pem')); + t.strictEqual(keyPem.toString('base64'), realKeyPem.toString('base64')); + t.end(); +}); + +test('PrivateKey load ed25519 pem key', function (t) { + var keyPem = fs.readFileSync(path.join(testDir, 'id_ed25519.pem')); + var key = sshpk.parsePrivateKey(keyPem, 'pem'); + t.strictEqual(key.type, 'ed25519'); + t.strictEqual(key.size, 256); + t.ok(ID_ED25519_FP.matches(key)); + t.end(); +}); + +test('PrivateKey load ed25519 key (ex. from curdle-pkix-04)', function (t) { + var keyPem = fs.readFileSync(path.join(testDir, 'ed25519-pkix.pem')); + var key = sshpk.parsePrivateKey(keyPem, 'pem'); + t.strictEqual(key.type, 'ed25519'); + t.strictEqual(key.size, 256); + t.end(); +}); + +test('PrivateKey load ed25519 key (no public curdle-pkix-05)', function (t) { + var keyPem = fs.readFileSync(path.join(testDir, + 'curdle-pkix-privonly.pem')); + var key = sshpk.parsePrivateKey(keyPem, 'pem'); + t.strictEqual(key.type, 'ed25519'); + t.strictEqual(key.size, 256); + t.end(); +}); + +test('PrivateKey load ed25519 key (w/ public curdle-pkix-05)', function (t) { + var keyPem = fs.readFileSync(path.join(testDir, + 'curdle-pkix-withpub.pem')); + var key = sshpk.parsePrivateKey(keyPem, 'pem'); + t.strictEqual(key.type, 'ed25519'); + t.strictEqual(key.size, 256); + t.end(); +}); + +test('PrivateKey invalid ed25519 key (not DER)', function (t) { + var keyPem = fs.readFileSync(path.join(testDir, + 'ed25519-invalid-ber.pem')); + t.throws(function () { + sshpk.parsePrivateKey(keyPem, 'pem'); + }); + t.end(); +}); + +test('PrivateKey invalid ed25519 key (invalid curve point)', function (t) { + var keyPem = fs.readFileSync(path.join(testDir, + 'ed25519-invalid-mask.pem')); + t.throws(function () { + sshpk.parsePrivateKey(keyPem, 'pem'); + }); + t.end(); +}); + +test('PrivateKey invalid ed25519 key (elided zero)', function (t) { + var keyPem = fs.readFileSync(path.join(testDir, + 'ed25519-invalid-zero.pem')); + /* + * We're actually more forgiving of this kind of invalid input than + * the RFC says we need to be. Since this is purely about the format of + * the data, and not about the validity of the point itself this should + * be safe. + */ + var key = sshpk.parsePrivateKey(keyPem, 'pem'); t.end(); });