Skip to content

Commit

Permalink
#33 implement support for Ed25519 keys in PEM format (curdle-pkix)
Browse files Browse the repository at this point in the history
Reviewed by: Tim Kordas <tim.kordas@joyent.com>
  • Loading branch information
Alex Wilson committed Aug 19, 2017
1 parent a3835db commit 0d6852f
Show file tree
Hide file tree
Showing 24 changed files with 449 additions and 52 deletions.
8 changes: 3 additions & 5 deletions lib/algs.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,8 @@ var algInfo = {
sizePart: 'Q'
},
'ed25519': {
parts: ['R'],
normalize: false,
sizePart: 'R'
parts: ['A'],
sizePart: 'A'
}
};
algInfo['curve25519'] = algInfo['ed25519'];
Expand All @@ -32,8 +31,7 @@ var algPrivInfo = {
parts: ['curve', 'Q', 'd']
},
'ed25519': {
parts: ['R', 'r'],
normalize: false
parts: ['A', 'k']
}
};
algPrivInfo['curve25519'] = algPrivInfo['ed25519'];
Expand Down
23 changes: 13 additions & 10 deletions lib/dhe.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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));
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
5 changes: 3 additions & 2 deletions lib/ed-compat.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -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';
Expand Down
11 changes: 8 additions & 3 deletions lib/formats/pem.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 */
Expand Down Expand Up @@ -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();
Expand Down
56 changes: 56 additions & 0 deletions lib/formats/pkcs1.js
Original file line number Diff line number Diff line change
Expand Up @@ -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));
}
Expand Down Expand Up @@ -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');
Expand Down Expand Up @@ -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));
}
Expand Down Expand Up @@ -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'));
}
111 changes: 111 additions & 0 deletions lib/formats/pkcs8.js
Original file line number Diff line number Diff line change
Expand Up @@ -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));
}
Expand Down Expand Up @@ -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();

Expand Down Expand Up @@ -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));
}
Expand Down Expand Up @@ -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();
}
35 changes: 27 additions & 8 deletions lib/formats/rfc4253.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down Expand Up @@ -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);
}

Expand Down
Loading

0 comments on commit 0d6852f

Please sign in to comment.