Skip to content

Commit

Permalink
lib: support ECDSA private keys
Browse files Browse the repository at this point in the history
  • Loading branch information
mscdex committed Feb 26, 2016
1 parent d59be91 commit 8aee5df
Show file tree
Hide file tree
Showing 3 changed files with 144 additions and 27 deletions.
11 changes: 7 additions & 4 deletions lib/keyParser.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@
var utils;

var RE_PPK = /^PuTTY-User-Key-File-2: ssh-(rsa|dss)\r?\nEncryption: (aes256-cbc|none)\r?\nComment: ([^\r\n]*)\r?\nPublic-Lines: \d+\r?\n([\s\S]+?)\r?\nPrivate-Lines: \d+\r?\n([\s\S]+?)\r?\nPrivate-MAC: ([^\r\n]+)/;
var RE_HEADER_OPENSSH_PRIV = /^-----BEGIN (RSA|DSA) PRIVATE KEY-----$/i;
var RE_FOOTER_OPENSSH_PRIV = /^-----END (?:RSA|DSA) PRIVATE KEY-----$/i;
var RE_HEADER_OPENSSH_PUB = /^(ssh-(rsa|dss)(?:-cert-v0[01]@openssh.com)?) ([A-Z0-9a-z\/+=]+)(?:$|\s+([\S].*)?)$/i;
var RE_HEADER_OPENSSH_PRIV = /^-----BEGIN (RSA|DSA|EC) PRIVATE KEY-----$/i;
var RE_FOOTER_OPENSSH_PRIV = /^-----END (?:RSA|DSA|EC) PRIVATE KEY-----$/i;
var RE_HEADER_OPENSSH_PUB = /^((?:(?:ssh-(rsa|dss))|ecdsa-sha2-nistp(256|384|521))(?:-cert-v0[01]@openssh.com)?) ([A-Z0-9a-z\/+=]+)(?:$|\s+([\S].*)?)$/i;
var RE_HEADER_RFC4716_PUB = /^---- BEGIN SSH2 PUBLIC KEY ----$/i;
var RE_FOOTER_RFC4716_PUB = /^---- END SSH2 PUBLIC KEY ----$/i;
var RE_HEADER_OPENSSH = /^([^:]+):\s*([\S].*)?$/i;
Expand Down Expand Up @@ -45,7 +45,10 @@ module.exports = function(data) {
if ((m = RE_HEADER_OPENSSH_PRIV.exec(data[0]))
&& RE_FOOTER_OPENSSH_PRIV.test(data.slice(-1))) {
// OpenSSH private key
ret.type = (m[1].toLowerCase() === 'dsa' ? 'dss' : 'rsa');
var keyType = m[1].toLowerCase();
if (keyType === 'dsa')
keyType = 'dss';
ret.type = keyType;
if (!RE_HEADER_OPENSSH.test(data[1])) {
// unencrypted, no headers
ret.private = new Buffer(data.slice(1, -1).join(''), 'base64');
Expand Down
76 changes: 58 additions & 18 deletions lib/ssh.js
Original file line number Diff line number Diff line change
Expand Up @@ -119,22 +119,34 @@ function SSH2Stream(cfg) {
var publicKeyInfo;
var self = this;

if (this.server
&& typeof cfg.privateKey !== 'string'
&& !Buffer.isBuffer(cfg.privateKey))
throw new Error('Invalid/missing privateKey');
else if (this.server) {
privKeyInfo = parseKey(cfg.privateKey);
if (privKeyInfo instanceof Error)
throw new Error('Cannot parse privateKey: ' + privKeyInfo.message);
if (!privKeyInfo.private)
throw new Error('privateKey value does not contain a (valid) private key');
if (privKeyInfo.encryption) {
if (typeof cfg.passphrase !== 'string')
throw new Error('Encrypted private key detected, but no passphrase given');
decryptKey(privKeyInfo, cfg.passphrase);
var privateKey = cfg.privateKey;
if (this.server) {
if (typeof privateKey !== 'string'
&& !Buffer.isBuffer(privateKey)
&& (typeof privateKey !== 'object'
|| privateKey === null
|| typeof privateKey.privateKey !== 'object'
|| typeof privateKey.publicKey !== 'object')) {
throw new Error('Invalid/missing privateKey');
}

if (typeof privateKey === 'object' && !Buffer.isBuffer(privateKey)) {
// Allow pre-parsed key to avoid constant re-parsing
privKeyInfo = privateKey.privateKey;
publicKeyInfo = privateKey.publicKey;
} else {
privKeyInfo = parseKey(privateKey);
if (privKeyInfo instanceof Error)
throw new Error('Cannot parse privateKey: ' + privKeyInfo.message);
if (!privKeyInfo.private)
throw new Error('privateKey value contains an invalid private key');
if (privKeyInfo.encryption) {
if (typeof cfg.passphrase !== 'string')
throw new Error('Missing passphrase for encrypted private key');
decryptKey(privKeyInfo, cfg.passphrase);
}
publicKeyInfo = genPublicKey(privKeyInfo);
}
publicKeyInfo = genPublicKey(privKeyInfo);
}

this.config = {
Expand Down Expand Up @@ -4938,13 +4950,26 @@ function KEXDH_REPLY(self, e) { // server
outstate.sessionId = outstate.exchangeHash;
outstate.kexsecret = secret;

var algo = (hostkeyAlgo === 'ssh-rsa' ? 'RSA' : 'DSA');
var signer = crypto.createSign(algo + '-SHA1');
var keyAlgo;
var keyAlgoBytes; // only needed for ECDSA which has varying bit sizes
switch (hostkeyAlgo) {
case 'ssh-rsa':
keyAlgo = 'RSA-SHA1';
break;
case 'ssh-dss':
keyAlgo = 'DSA-SHA1';
break;
case 'ecdsa-sha2-nistp256':
keyAlgo = 'sha256';
keyAlgoBytes = 32;
break;
}
var signer = crypto.createSign(keyAlgo);
var signature;
signer.update(outstate.exchangeHash, 'binary');
signature = signer.sign(privateKey, 'binary');

if (hostkeyAlgo === 'ssh-dss') {
if (keyAlgo === 'DSA-SHA1') {
// strip ASN.1 notation to bare r and s values only (minus leading zeros),
// 40 bytes total
var asn1Reader = new Ber.Reader(new Buffer(signature, 'binary'));
Expand All @@ -4965,6 +4990,21 @@ function KEXDH_REPLY(self, e) { // server
if (zeros > 0)
s = s.slice(zeros);
signature = r.toString('binary') + s.toString('binary');
} else if (keyAlgoBytes !== undefined) { // ECDSA
var asn1Reader = new Ber.Reader(new Buffer(signature, 'binary'));
var zeros;
var r;
var s;
asn1Reader.readSequence();
r = asn1Reader.readString(Ber.Integer, true);
s = asn1Reader.readString(Ber.Integer, true);
var newSig = new Buffer(4 + r.length + 4 + s.length);
var p = 0;
newSig.writeUInt32BE(r.length, p, true);
r.copy(newSig, p += 4);
newSig.writeUInt32BE(s.length, p += r.length, true);
s.copy(newSig, p += 4);
signature = newSig.toString('binary');
}

/*
Expand Down
84 changes: 79 additions & 5 deletions lib/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -216,7 +216,7 @@ function genPublicKey(keyInfo) {
}
}
eStart = i;
} else { // DSA
} else if (keyInfo.type === 'dss') { // DSA
// prime (p) -- integer
if (privKey[i++] !== 0x02)
throw new Error('Malformed private key (expected integer for p)');
Expand Down Expand Up @@ -276,9 +276,45 @@ function genPublicKey(keyInfo) {
}
yStart = i;
i += yLen;
} else { // ECDSA
var asnReader = new Ber.Reader(privKey.slice(i));
var d = asnReader.readString(Ber.OctetString, true);
if (d === null)
throw new Error('Malformed private key (missing private key data)');
asnReader.readByte(); // Skip "complex" context type byte
var offset = asnReader.readLength(); // Skip context length
if (offset === null)
throw new Error('Malformed private key (missing context component)');
asnReader._offset = offset;
var ecCurveOID = asnReader.readOID();
if (ecCurveOID === null)
throw new Error('Malformed private key (missing EC curve)');
var ecCurveName;
var tempECDH;
switch (ecCurveOID) {
case '1.2.840.10045.3.1.7':
// prime256v1/secp256r1
ecCurveName = 'nistp256';
tempECDH = crypto.createECDH('prime256v1');
break;
case '1.3.132.0.34':
// secp384r1
ecCurveName = 'nistp384';
tempECDH = crypto.createECDH('secp384r1');
break;
case '1.3.132.0.35':
// secp521r1
ecCurveName = 'nistp521';
tempECDH = crypto.createECDH('secp521r1');
break;
default:
throw new Error('Malformed private key (unsupported EC curve)');
}
tempECDH.setPrivateKey(d);
var Q = tempECDH.getPublicKey();
var QLen = Q.length;
}

p = 4 + 7;

if (keyInfo.type === 'rsa') {
publicKey = new Buffer(4 + 7 // ssh-rsa
Expand All @@ -288,12 +324,13 @@ function genPublicKey(keyInfo) {
publicKey.writeUInt32BE(7, 0, true);
publicKey.write('ssh-rsa', 4, 7, 'ascii');

p = 4 + 7;
publicKey.writeUInt32BE(eLen, p, true);
privKey.copy(publicKey, p += 4, eStart, eStart + eLen);

publicKey.writeUInt32BE(nLen, p += eLen, true);
privKey.copy(publicKey, p += 4, nStart, nStart + nLen);
} else { // DSA
} else if (keyInfo.type === 'dss') { // DSA
publicKey = new Buffer(4 + 7 // ssh-dss
+ 4 + pLen
+ 4 + qLen
Expand All @@ -303,6 +340,7 @@ function genPublicKey(keyInfo) {
publicKey.writeUInt32BE(7, 0, true);
publicKey.write('ssh-dss', 4, 7, 'ascii');

p = 4 + 7;
publicKey.writeUInt32BE(pLen, p, true);
privKey.copy(publicKey, p += 4, pStart, pStart + pLen);

Expand All @@ -314,6 +352,19 @@ function genPublicKey(keyInfo) {

publicKey.writeUInt32BE(yLen, p += gLen, true);
privKey.copy(publicKey, p += 4, yStart, yStart + yLen);
} else { // ECDSA
publicKey = new Buffer(4 + 19 // ecdsa-sha2-<curve name>
+ 4 + 8 // <curve name>
+ 4 + QLen);

publicKey.writeUInt32BE(19, 0, true);
publicKey.write('ecdsa-sha2-' + ecCurveName, 4, 19, 'ascii');

publicKey.writeUInt32BE(8, 23, true);
publicKey.write(ecCurveName, 27, 8, 'ascii');

publicKey.writeUInt32BE(QLen, 35, true);
Q.copy(publicKey, 39);
}
} else {
var errMsg = 'Malformed private key (expected sequence)';
Expand All @@ -323,6 +374,7 @@ function genPublicKey(keyInfo) {
}
} else if (keyInfo.public) {
publicKey = keyInfo.public;
// TODO: support ECDSA
// check for missing ssh-{dsa,rsa} prefix
if (publicKey[0] !== 0
|| publicKey[1] !== 0
Expand Down Expand Up @@ -355,9 +407,11 @@ function genPublicKey(keyInfo) {

p = 4 + 7;

var fulltype;
var asnWriter = new Ber.Writer();
asnWriter.startSequence();
if (keyInfo.type === 'rsa') {
fulltype = 'ssh-rsa';
eLen = publicKey.readUInt32BE(p, true);
p += 4;
eStart = p;
Expand All @@ -381,7 +435,8 @@ function genPublicKey(keyInfo) {
asnWriter.writeBuffer(e, Ber.Integer);
asnWriter.endSequence();
asnWriter.endSequence();
} else {
} else if (keyInfo.type === 'dss') {
fulltype = 'ssh-dss';
pLen = publicKey.readUInt32BE(p, true);
p += 4;
pStart = p;
Expand Down Expand Up @@ -419,6 +474,25 @@ function genPublicKey(keyInfo) {
asnWriter.writeByte(0x00);
asnWriter.writeBuffer(y, Ber.Integer);
asnWriter.endSequence();
} else { // ECDSA
fulltype = 'ecdsa-sha2-' + ecCurveName;

// algorithm
asnWriter.startSequence();
asnWriter.writeOID('1.2.840.10045.2.1'); // id-ecPublicKey
// algorithm parameters (namedCurve)
asnWriter.writeOID(ecCurveOID);
asnWriter.endSequence();

// subjectPublicKey
asnWriter.startSequence(Ber.BitString);
asnWriter.writeByte(0x00);
// XXX: hack to write a raw buffer without a tag -- yuck
asnWriter._ensure(Q.length);
Q.copy(asnWriter._buf, asnWriter._offset, 0, Q.length);
asnWriter._offset += Q.length;
// end hack
asnWriter.endSequence();
}
asnWriter.endSequence();

Expand All @@ -430,7 +504,7 @@ function genPublicKey(keyInfo) {

return {
type: keyInfo.type,
fulltype: 'ssh-' + keyInfo.type,
fulltype: fulltype,
public: publicKey,
publicOrig: new Buffer(fullkey)
};
Expand Down

0 comments on commit 8aee5df

Please sign in to comment.