Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 29 additions & 4 deletions lib/client.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ const {
} = require('./protocol/constants.js');
const { init: cryptoInit } = require('./protocol/crypto.js');
const Protocol = require('./protocol/Protocol.js');
const { parseKey } = require('./protocol/keyParser.js');
const { isParsedKey, parseKey } = require('./protocol/keyParser.js');
const { SFTP } = require('./protocol/SFTP.js');
const {
bufferCopy,
Expand Down Expand Up @@ -207,8 +207,16 @@ class Client extends EventEmitter {
: undefined);
this.config.privateKey = (typeof cfg.privateKey === 'string'
|| Buffer.isBuffer(cfg.privateKey)
|| isParsedKey(cfg.privateKey)
? cfg.privateKey
: undefined);
// publicKey may be a certificate file (e.g. id_ed25519-cert.pub) to use
// for identification while privateKey is used for signing.
this.config.publicKey = (typeof cfg.publicKey === 'string'
|| Buffer.isBuffer(cfg.publicKey)
|| isParsedKey(cfg.publicKey)
? cfg.publicKey
: undefined);
this.config.localHostname = (typeof cfg.localHostname === 'string'
? cfg.localHostname
: undefined);
Expand Down Expand Up @@ -254,6 +262,7 @@ class Client extends EventEmitter {
this._agent = (this.config.agent ? this.config.agent : undefined);
this._remoteVer = undefined;
let privateKey;
let authKey; // key object used for publickey auth (may be a certificate)

if (this.config.privateKey) {
privateKey = parseKey(this.config.privateKey, cfg.passphrase);
Expand All @@ -268,6 +277,18 @@ class Client extends EventEmitter {
'privateKey value does not contain a (valid) private key'
);
}

if (this.config.publicKey) {
// A separate certificate was provided for identification. Use it as
// the key sent to the server while privateKey is used for signing.
authKey = parseKey(this.config.publicKey);
if (authKey instanceof Error)
throw new Error(`Cannot parse publicKey: ${authKey.message}`);
if (Array.isArray(authKey))
authKey = authKey[0];
} else {
authKey = privateKey;
}
}

let hostVerifier;
Expand Down Expand Up @@ -462,7 +483,10 @@ class Client extends EventEmitter {
});
} else if (curAuth.type === 'publickey') {
proto.authPK(curAuth.username, curAuth.key, keyAlgo, (buf, cb) => {
const signature = curAuth.key.sign(buf, hashAlgo);
// Sign with privateKey (may differ from curAuth.key when a
// certificate is used for identification).
const signingKey = curAuth.privateKey || curAuth.key;
const signature = signingKey.sign(buf, hashAlgo);
if (signature instanceof Error) {
signature.message =
`Error signing data with key: ${signature.message}`;
Expand Down Expand Up @@ -882,13 +906,14 @@ class Client extends EventEmitter {
nextAuth = { type, username, password: this.config.password };
break;
case 'publickey':
nextAuth = { type, username, key: privateKey };
nextAuth = { type, username, key: authKey, privateKey };
break;
case 'hostbased':
nextAuth = {
type,
username,
key: privateKey,
key: authKey,
privateKey,
localHostname: this.config.localHostname,
localUsername: this.config.localUsername,
};
Expand Down
19 changes: 14 additions & 5 deletions lib/protocol/Protocol.js
Original file line number Diff line number Diff line change
Expand Up @@ -701,11 +701,20 @@ class Protocol {
if (signature === false)
throw new Error('Error while converting handshake signature');

// For certificate key types the signature algorithm must be the
// underlying key algorithm (e.g. "ssh-ed25519"), not the certificate
// type name (e.g. "ssh-ed25519-cert-v01@openssh.com").
const certSuffix = '-cert-v01@openssh.com';
const sigAlgo = keyType.endsWith(certSuffix)
? keyType.slice(0, -certSuffix.length)
: keyAlgo;
const sigAlgoLen = Buffer.byteLength(sigAlgo);

const sigLen = signature.length;
p = this._packetRW.write.allocStart;
packet = this._packetRW.write.alloc(
1 + 4 + userLen + 4 + 14 + 4 + 9 + 1 + 4 + algoLen + 4 + pubKeyLen + 4
+ 4 + algoLen + 4 + sigLen
+ 4 + sigAlgoLen + 4 + sigLen
);

// TODO: simply copy from original "packet" to new `packet` to avoid
Expand All @@ -729,12 +738,12 @@ class Protocol {
writeUInt32BE(packet, pubKeyLen, p += algoLen);
packet.set(pubKey, p += 4);

writeUInt32BE(packet, 4 + algoLen + 4 + sigLen, p += pubKeyLen);
writeUInt32BE(packet, 4 + sigAlgoLen + 4 + sigLen, p += pubKeyLen);

writeUInt32BE(packet, algoLen, p += 4);
packet.utf8Write(keyAlgo, p += 4, algoLen);
writeUInt32BE(packet, sigAlgoLen, p += 4);
packet.utf8Write(sigAlgo, p += 4, sigAlgoLen);

writeUInt32BE(packet, sigLen, p += algoLen);
writeUInt32BE(packet, sigLen, p += sigAlgoLen);
packet.set(signature, p += 4);

// Servers shouldn't send packet type 60 in response to signed publickey
Expand Down
42 changes: 34 additions & 8 deletions lib/protocol/keyParser.js
Original file line number Diff line number Diff line change
Expand Up @@ -1186,6 +1186,21 @@ function OpenSSH_Public(type, comment, pubPEM, pubSSH, algo) {
this[SYM_DECRYPTED] = false;
}
OpenSSH_Public.prototype = BaseKey;

// Represents an SSH certificate (e.g. ssh-ed25519-cert-v01@openssh.com).
// The full certificate blob is stored as-is and returned from getPublicSSH()
// so it can be used directly in publickey auth requests.
function Cert_Public(type, comment, certBlob) {
this.type = type;
this.comment = comment;
this[SYM_PRIV_PEM] = null;
this[SYM_PUB_PEM] = null;
this[SYM_PUB_SSH] = certBlob;
this[SYM_HASH_ALGO] = null;
this[SYM_DECRYPTED] = false;
}
Cert_Public.prototype = BaseKey;

{
let regexp;
if (eddsaSupported)
Expand All @@ -1198,7 +1213,7 @@ OpenSSH_Public.prototype = BaseKey;
return null;
// m[1] = full type
// m[2] = base type
// m[3] = base64-encoded public key
// m[3] = base64-encoded public key / certificate blob
// m[4] = comment

const fullType = m[1];
Expand All @@ -1210,6 +1225,11 @@ OpenSSH_Public.prototype = BaseKey;
if (type === undefined || type.indexOf(baseType) !== 0)
return new Error('Malformed OpenSSH public key');

// Certificate: preserve the full blob so getPublicSSH() returns it intact
// for use in SSH_MSG_USERAUTH_REQUEST publickey auth.
if (fullType !== baseType)
return new Cert_Public(fullType, comment, data);

return parseDER(data, baseType, comment, fullType);
};
}
Expand Down Expand Up @@ -1458,13 +1478,18 @@ function parseKey(data, passphrase) {
binaryKeyParser.init(origBuffer, 0);
const type = binaryKeyParser.readString(true);
if (type !== undefined) {
data = binaryKeyParser.readRaw();
if (data !== undefined) {
ret = parseDER(data, type, '', type);
// Ignore potentially useless errors in case the data was not actually
// in the binary format
if (ret instanceof Error)
ret = null;
// Certificate blob: preserve it whole rather than trying to parse internals
if (type.indexOf('-cert-v0') !== -1) {
ret = new Cert_Public(type, '', origBuffer);
} else {
data = binaryKeyParser.readRaw();
if (data !== undefined) {
ret = parseDER(data, type, '', type);
// Ignore potentially useless errors in case the data was not actually
// in the binary format
if (ret instanceof Error)
ret = null;
}
}
}
binaryKeyParser.clear();
Expand All @@ -1478,6 +1503,7 @@ function parseKey(data, passphrase) {

module.exports = {
isParsedKey,
Cert_Public,
isSupportedKeyType,
parseDERKey: (data, type) => parseDER(data, type, '', type),
parseKey,
Expand Down