diff --git a/lib/client.js b/lib/client.js index 7291c2ce..4b80e401 100644 --- a/lib/client.js +++ b/lib/client.js @@ -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, @@ -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); @@ -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); @@ -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; @@ -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}`; @@ -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, }; diff --git a/lib/protocol/Protocol.js b/lib/protocol/Protocol.js index 73024881..79c0ce1c 100644 --- a/lib/protocol/Protocol.js +++ b/lib/protocol/Protocol.js @@ -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 @@ -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 diff --git a/lib/protocol/keyParser.js b/lib/protocol/keyParser.js index a276c1ae..d20fcad4 100644 --- a/lib/protocol/keyParser.js +++ b/lib/protocol/keyParser.js @@ -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) @@ -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]; @@ -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); }; } @@ -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(); @@ -1478,6 +1503,7 @@ function parseKey(data, passphrase) { module.exports = { isParsedKey, + Cert_Public, isSupportedKeyType, parseDERKey: (data, type) => parseDER(data, type, '', type), parseKey,