diff --git a/lib/client.js b/lib/client.js index 7291c2ce..6f3bd2ba 100644 --- a/lib/client.js +++ b/lib/client.js @@ -84,6 +84,7 @@ class Client extends EventEmitter { username: undefined, password: undefined, privateKey: undefined, + certificate: undefined, tryKeyboard: undefined, agent: undefined, allowAgentFwd: undefined, @@ -209,6 +210,10 @@ class Client extends EventEmitter { || Buffer.isBuffer(cfg.privateKey) ? cfg.privateKey : undefined); + this.config.certificate = (typeof cfg.certificate === 'string' + || Buffer.isBuffer(cfg.certificate) + ? cfg.certificate + : undefined); this.config.localHostname = (typeof cfg.localHostname === 'string' ? cfg.localHostname : undefined); @@ -254,6 +259,7 @@ class Client extends EventEmitter { this._agent = (this.config.agent ? this.config.agent : undefined); this._remoteVer = undefined; let privateKey; + let certificate; if (this.config.privateKey) { privateKey = parseKey(this.config.privateKey, cfg.passphrase); @@ -270,6 +276,24 @@ class Client extends EventEmitter { } } + // SSH certificate authentication support (RFC 4252, OpenSSH PROTOCOL.certkeys) + // We store both the parsed certificate (for validation) and the raw string + // (needed by authPK to extract the full certificate blob for transmission) + let certificateRaw; + if (this.config.certificate) { + certificateRaw = this.config.certificate; + certificate = parseKey(this.config.certificate); + if (certificate instanceof Error) + throw new Error(`Cannot parse certificate: ${certificate.message}`); + if (Array.isArray(certificate)) + certificate = certificate[0]; + if (!certificate.type.includes('-cert-')) { + throw new Error( + 'certificate value is not a valid OpenSSH certificate' + ); + } + } + let hostVerifier; if (typeof cfg.hostVerifier === 'function') { const hashCb = cfg.hostVerifier; @@ -388,7 +412,8 @@ class Client extends EventEmitter { ); return; case 'publickey': - proto.authPK(curAuth.username, curAuth.key, keyAlgo); + proto.authPK(curAuth.username, curAuth.key, keyAlgo, + undefined, curAuth.certificateRaw); return; case 'hostbased': proto.authHostbased(curAuth.username, @@ -471,7 +496,7 @@ class Client extends EventEmitter { return tryNextAuth(); } cb(signature); - }); + }, curAuth.certificateRaw); } }, USERAUTH_INFO_REQUEST: (p, name, instructions, prompts) => { @@ -882,7 +907,7 @@ class Client extends EventEmitter { nextAuth = { type, username, password: this.config.password }; break; case 'publickey': - nextAuth = { type, username, key: privateKey }; + nextAuth = { type, username, key: privateKey, certificate, certificateRaw }; break; case 'hostbased': nextAuth = { @@ -941,7 +966,19 @@ class Client extends EventEmitter { return skipAuth('Skipping invalid key auth attempt'); if (!key.isPrivateKey()) return skipAuth('Skipping non-private key'); - nextAuth = { type, username, key }; + let cert; + let certRaw; + if (nextAuth.certificate) { + certRaw = nextAuth.certificate; // Keep raw string + cert = parseKey(nextAuth.certificate); + if (cert instanceof Error) + return skipAuth('Skipping invalid certificate'); + if (Array.isArray(cert)) + cert = cert[0]; + if (!cert.type.includes('-cert-')) + return skipAuth('Skipping non-certificate'); + } + nextAuth = { type, username, key, certificate: cert, certificateRaw: certRaw }; break; } case 'hostbased': { @@ -1009,7 +1046,8 @@ class Client extends EventEmitter { ); } } - proto.authPK(username, curAuth.key, keyAlgo); + proto.authPK(username, curAuth.key, keyAlgo, + undefined, curAuth.certificateRaw); break; } case 'hostbased': { diff --git a/lib/protocol/Protocol.js b/lib/protocol/Protocol.js index 73024881..daf5a47b 100644 --- a/lib/protocol/Protocol.js +++ b/lib/protocol/Protocol.js @@ -628,7 +628,7 @@ class Protocol { sendPacket(this, this._packetRW.write.finalize(packet)); } - authPK(username, pubKey, keyAlgo, cbSign) { + authPK(username, pubKey, keyAlgo, cbSign, certificate) { if (this._server) throw new Error('Client-only method called in server mode'); @@ -636,8 +636,33 @@ class Protocol { if (pubKey instanceof Error) throw new Error('Invalid key'); - const keyType = pubKey.type; - pubKey = pubKey.getPublicSSH(); + let keyType = pubKey.type; + // For certificate auth, signatures use the underlying key algorithm + // (e.g., 'ssh-ed25519'), not the certificate type + const underlyingKeyType = keyType; + let pubKeyBlob = pubKey.getPublicSSH(); + + // SSH certificate authentication (OpenSSH PROTOCOL.certkeys) + // When a certificate is provided, we send the full certificate blob + // instead of the raw public key, and use the certificate type + // (e.g., 'ssh-ed25519-cert-v01@openssh.com') as the key algorithm + if (certificate) { + // Parse certificate from OpenSSH format: "type base64-blob [comment]" + let certStr = certificate; + if (Buffer.isBuffer(certificate)) + certStr = certificate.toString('utf8'); + + const certParts = certStr.trim().split(/\s+/); + if (certParts.length < 2) + throw new Error('Invalid certificate format'); + + keyType = certParts[0]; + pubKeyBlob = Buffer.from(certParts[1], 'base64'); + + // this._debug && this._debug( + // `authPK: certificate auth, type=${keyType}, blobLen=${pubKeyBlob.length}` + // ); + } if (typeof keyAlgo === 'function') { cbSign = keyAlgo; @@ -648,7 +673,7 @@ class Protocol { const userLen = Buffer.byteLength(username); const algoLen = Buffer.byteLength(keyAlgo); - const pubKeyLen = pubKey.length; + const pubKeyLen = pubKeyBlob.length; const sessionID = this._kex.sessionID; const sesLen = sessionID.length; const payloadLen = @@ -684,7 +709,7 @@ class Protocol { packet.utf8Write(keyAlgo, p += 4, algoLen); writeUInt32BE(packet, pubKeyLen, p += algoLen); - packet.set(pubKey, p += 4); + packet.set(pubKeyBlob, p += 4); if (!cbSign) { this._authsQueue.push('publickey'); @@ -697,15 +722,20 @@ class Protocol { } cbSign(packet, (signature) => { - signature = convertSignature(signature, keyType); + // For certificate auth, the signature algorithm must be the underlying + // key type (e.g., 'ssh-ed25519'), not the certificate type + signature = convertSignature(signature, underlyingKeyType); if (signature === false) throw new Error('Error while converting handshake signature'); const sigLen = signature.length; + const sigAlgo = underlyingKeyType; + const sigAlgoLen = Buffer.byteLength(sigAlgo); + 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 @@ -727,14 +757,15 @@ class Protocol { packet.utf8Write(keyAlgo, p += 4, algoLen); writeUInt32BE(packet, pubKeyLen, p += algoLen); - packet.set(pubKey, p += 4); + packet.set(pubKeyBlob, p += 4); - writeUInt32BE(packet, 4 + algoLen + 4 + sigLen, p += pubKeyLen); + // Signature blob uses underlying key algorithm, not certificate type + 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