diff --git a/lib/client.js b/lib/client.js index 7291c2ce..80a542ce 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..376ceebe 100644 --- a/lib/protocol/Protocol.js +++ b/lib/protocol/Protocol.js @@ -648,6 +648,15 @@ class Protocol { const userLen = Buffer.byteLength(username); const algoLen = Buffer.byteLength(keyAlgo); + // OpenSSH certificates: the request's algorithm field names the certificate + // ("ssh-ed25519-cert-v01@openssh.com"), but the signature blob must name + // the plain algorithm the key actually signs with ("ssh-ed25519"). For + // plain keys the two are identical and nothing below changes. + const certSuffix = '-cert-v01@openssh.com'; + const sigAlgo = keyAlgo.endsWith(certSuffix) + ? keyAlgo.slice(0, -certSuffix.length) + : keyAlgo; + const sigAlgoLen = Buffer.byteLength(sigAlgo); const pubKeyLen = pubKey.length; const sessionID = this._kex.sessionID; const sesLen = sessionID.length; @@ -697,7 +706,7 @@ class Protocol { } cbSign(packet, (signature) => { - signature = convertSignature(signature, keyType); + signature = convertSignature(signature, sigAlgo); if (signature === false) throw new Error('Error while converting handshake signature'); @@ -705,7 +714,7 @@ class Protocol { 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..90746625 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,15 @@ 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. The type sniff above + // left the parser's `_pos` cursor on `data`; it must not escape with the + // blob, where it would make two identical certificates compare unequal. + if (fullType !== baseType) { + delete data._pos; + return new Cert_Public(fullType, comment, data); + } + return parseDER(data, baseType, comment, fullType); }; } @@ -1458,13 +1482,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 parsing its 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 +1507,7 @@ function parseKey(data, passphrase) { module.exports = { isParsedKey, + Cert_Public, isSupportedKeyType, parseDERKey: (data, type) => parseDER(data, type, '', type), parseKey, diff --git a/test/fixtures/id_ecdsa_cert_key b/test/fixtures/id_ecdsa_cert_key new file mode 100644 index 00000000..cd91098e --- /dev/null +++ b/test/fixtures/id_ecdsa_cert_key @@ -0,0 +1,9 @@ +-----BEGIN OPENSSH PRIVATE KEY----- +b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAaAAAABNlY2RzYS +1zaGEyLW5pc3RwMjU2AAAACG5pc3RwMjU2AAAAQQRmVDua5cBz9KzJqMt7Iur/wiJcvxGE +OPSz26h0HA/QrZL58SnuNIPwNuNkRe3gD6OvVNIgY+0ugEoOtA2td1wZAAAAsEcVnTVHFZ +01AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBGZUO5rlwHP0rMmo +y3si6v/CIly/EYQ49LPbqHQcD9CtkvnxKe40g/A242RF7eAPo69U0iBj7S6ASg60Da13XB +kAAAAgVNtyZE0ka1Uca8Doi9MK+zmnWLgrfVFWi0YMuHW8pSUAAAAWc3NoMiBjZXJ0IHRl +c3QgKGVjZHNhKQEC +-----END OPENSSH PRIVATE KEY----- diff --git a/test/fixtures/id_ecdsa_cert_key-cert.pub b/test/fixtures/id_ecdsa_cert_key-cert.pub new file mode 100644 index 00000000..3619917f --- /dev/null +++ b/test/fixtures/id_ecdsa_cert_key-cert.pub @@ -0,0 +1 @@ +ecdsa-sha2-nistp256-cert-v01@openssh.com AAAAKGVjZHNhLXNoYTItbmlzdHAyNTYtY2VydC12MDFAb3BlbnNzaC5jb20AAAAgTak0HL5RJH9zw9YlAF1gSZcxg1n4REuqJdW6pkkmVg0AAAAIbmlzdHAyNTYAAABBBGZUO5rlwHP0rMmoy3si6v/CIly/EYQ49LPbqHQcD9CtkvnxKe40g/A242RF7eAPo69U0iBj7S6ASg60Da13XBkAAAAAAAAAAAAAAAEAAAAPc3NoMi10ZXN0LWVjZHNhAAAADQAAAAlDZXJ0IFVzZXIAAAAAXgu20AAAAAGwne7QAAAAAAAAAIIAAAAVcGVybWl0LVgxMS1mb3J3YXJkaW5nAAAAAAAAABdwZXJtaXQtYWdlbnQtZm9yd2FyZGluZwAAAAAAAAAWcGVybWl0LXBvcnQtZm9yd2FyZGluZwAAAAAAAAAKcGVybWl0LXB0eQAAAAAAAAAOcGVybWl0LXVzZXItcmMAAAAAAAAAAAAAADMAAAALc3NoLWVkMjU1MTkAAAAg8pFFhI+jq1LhsqtKalxN4zigUFvWZFVIO5rW6WF+q80AAABTAAAAC3NzaC1lZDI1NTE5AAAAQPays/Qg1YOopt5YdWxA6vbkdxsjqzcGI+I0o421mvteSo2KiBmBCqFFMGaIoJz84AbsqSXEA4ceIw9RqB4+TwQ= ssh2 cert test (ecdsa) diff --git a/test/fixtures/id_ecdsa_cert_key.pub b/test/fixtures/id_ecdsa_cert_key.pub new file mode 100644 index 00000000..441ff8eb --- /dev/null +++ b/test/fixtures/id_ecdsa_cert_key.pub @@ -0,0 +1 @@ +ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBGZUO5rlwHP0rMmoy3si6v/CIly/EYQ49LPbqHQcD9CtkvnxKe40g/A242RF7eAPo69U0iBj7S6ASg60Da13XBk= ssh2 cert test (ecdsa) diff --git a/test/fixtures/id_ed25519_cert_key b/test/fixtures/id_ed25519_cert_key new file mode 100644 index 00000000..86c0a904 --- /dev/null +++ b/test/fixtures/id_ed25519_cert_key @@ -0,0 +1,7 @@ +-----BEGIN OPENSSH PRIVATE KEY----- +b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW +QyNTUxOQAAACDi3+ww7F3/p3bj95kkS5a3l0bLSvqLwBusCn4ZCs1jxgAAAKCREaswkRGr +MAAAAAtzc2gtZWQyNTUxOQAAACDi3+ww7F3/p3bj95kkS5a3l0bLSvqLwBusCn4ZCs1jxg +AAAEAbGT1n6QEvc47JS9PWzdz11zGRKyiB8cDVivKsr+bz7uLf7DDsXf+nduP3mSRLlreX +RstK+ovAG6wKfhkKzWPGAAAAGHNzaDIgY2VydCB0ZXN0IChlZDI1NTE5KQECAwQF +-----END OPENSSH PRIVATE KEY----- diff --git a/test/fixtures/id_ed25519_cert_key-cert.pub b/test/fixtures/id_ed25519_cert_key-cert.pub new file mode 100644 index 00000000..0fa90505 --- /dev/null +++ b/test/fixtures/id_ed25519_cert_key-cert.pub @@ -0,0 +1 @@ +ssh-ed25519-cert-v01@openssh.com AAAAIHNzaC1lZDI1NTE5LWNlcnQtdjAxQG9wZW5zc2guY29tAAAAILi3YOlmrVL3wgZpeVUeLdd7jxRkM7vGbxgyl0xtiiiKAAAAIOLf7DDsXf+nduP3mSRLlreXRstK+ovAG6wKfhkKzWPGAAAAAAAAAAAAAAABAAAAEXNzaDItdGVzdC1lZDI1NTE5AAAADQAAAAlDZXJ0IFVzZXIAAAAAXgu20AAAAAGwne7QAAAAAAAAAIIAAAAVcGVybWl0LVgxMS1mb3J3YXJkaW5nAAAAAAAAABdwZXJtaXQtYWdlbnQtZm9yd2FyZGluZwAAAAAAAAAWcGVybWl0LXBvcnQtZm9yd2FyZGluZwAAAAAAAAAKcGVybWl0LXB0eQAAAAAAAAAOcGVybWl0LXVzZXItcmMAAAAAAAAAAAAAADMAAAALc3NoLWVkMjU1MTkAAAAg8pFFhI+jq1LhsqtKalxN4zigUFvWZFVIO5rW6WF+q80AAABTAAAAC3NzaC1lZDI1NTE5AAAAQCss4EFFBJrt9gkvhsRKlt3B96jkNymbyM5QBmVPK7CAukvFx+R+Bx+HIg2Cu1/3kJy9lhixISAkIzn+3MEStwE= ssh2 cert test (ed25519) diff --git a/test/fixtures/id_ed25519_cert_key.pub b/test/fixtures/id_ed25519_cert_key.pub new file mode 100644 index 00000000..1dd2deba --- /dev/null +++ b/test/fixtures/id_ed25519_cert_key.pub @@ -0,0 +1 @@ +ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOLf7DDsXf+nduP3mSRLlreXRstK+ovAG6wKfhkKzWPG ssh2 cert test (ed25519) diff --git a/test/fixtures/id_rsa_cert_key b/test/fixtures/id_rsa_cert_key new file mode 100644 index 00000000..8a790584 --- /dev/null +++ b/test/fixtures/id_rsa_cert_key @@ -0,0 +1,27 @@ +-----BEGIN OPENSSH PRIVATE KEY----- +b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAABFwAAAAdzc2gtcn +NhAAAAAwEAAQAAAQEA2ee/j51zvZRGtIqTz4giqpdfc0mhOIuE3mvEYR4XOCxCBPUdN3Pn +WNpkhlw26uLdqkOP96eRHn5Vv4roj33soHIT5fJ2sJ0s7NmiIpkRBhDe1cmafm4vFZidex +/9Xsyba4JLp+IeyC2GA/3qLn5DoFnJInDgy+jvY6nbCGyaF9ZFSaaNaF9+TLN1qyP51svf +rTh5umYM9AVT6JqERsOQGadW/fHwaBauUJ2Lnos8r9/v81ZDOH8KJW4dLqWUTvE22AcAGf +A6LuMUBmM01FzovYQ01N158nizCzS1JyBH24vN98pZe/ft0npbmXw86g0OCy9ZUTmybYd7 +uRxmJrXrGQAAA9D/VUcP/1VHDwAAAAdzc2gtcnNhAAABAQDZ57+PnXO9lEa0ipPPiCKql1 +9zSaE4i4Tea8RhHhc4LEIE9R03c+dY2mSGXDbq4t2qQ4/3p5EeflW/iuiPfeygchPl8naw +nSzs2aIimREGEN7VyZp+bi8VmJ17H/1ezJtrgkun4h7ILYYD/eoufkOgWckicODL6O9jqd +sIbJoX1kVJpo1oX35Ms3WrI/nWy9+tOHm6Zgz0BVPomoRGw5AZp1b98fBoFq5QnYueizyv +3+/zVkM4fwolbh0upZRO8TbYBwAZ8Dou4xQGYzTUXOi9hDTU3XnyeLMLNLUnIEfbi833yl +l79+3SeluZfDzqDQ4LL1lRObJth3u5HGYmtesZAAAAAwEAAQAAAQEAucGxr4AN9oK8c5Pe +xX/L7Zj1KZaO9WEad3FvC0tXh+9SqF879NW9ViV2pINf3YRgapEF3ZzfPRt9hbeo4Qn7+h +rFk7TKMW0Lqy0r5kqOFJm1HJKsPTM4uDRNq3Rtza6mn1OHoypEC2mjYSvHwrKoe72OlOmc ++Lctu8xEiD3sbnwK1cTKw/nFuIuGRmhxh4W1wrjA4cHNH8AmWPuPjCZXNbzi3BEt2RRX1k +hMsWq+JfPlEGiPJKkqVRLDJkG/XABcpnCjgeMieqJJwcjNSL0PBvStjdJ8OlYPUJPA+BAa +d/ZB1nKJ1ARf7dh5VqS20nrvxOitcECDDJFkgfpwE8lwwQAAAIEA2V59XkQO/iuuLexvyU +PABCFIShx0QM0PF1GRABYeikXxrtaPSiYkf7iEDsAd45Ah/X6iFYOwxBHLGW3PNeLWuKmW +le58f1reH468jB659oPMIwpanzo9pw/gywTtx0QJUQAc4BWGYj/8XQSax2+yuCWGSeGTMu +DnRRljTd5nr+AAAACBAP9FfcJ/iNvDoNTUqOZr1LM/3DwsZsK2NFB046uXEmUtkVMOXW+F +eWbIiFf+lV1OvFdYgD1TXoGOiUyREtwQv0W7KNH9qR2MA43S1YLBVxvfaBsrQd2dOCInWD +dGVJY1cKRT7aPSOurXI8GksT67KJbPetVKJYy2gupveWkgSwVtAAAAgQDahvTKgLWZMgXl +JA37f65BVqhYC/pYqMKGJnxbPpgPWuunjcIrm9Yc+mZP0z9RjA5WzXitB+VH4Mp+WcU1Xd +BRrMyJ7UCDA3HXmYrNzITWzURPwg72p3ca8yIwgzi9SZQR/hf77KWpLaGIo7JHX83kfhvX +6jOjvVbcBHyLEGGs3QAAABRzc2gyIGNlcnQgdGVzdCAocnNhKQECAwQF +-----END OPENSSH PRIVATE KEY----- diff --git a/test/fixtures/id_rsa_cert_key-cert.pub b/test/fixtures/id_rsa_cert_key-cert.pub new file mode 100644 index 00000000..5339ba32 --- /dev/null +++ b/test/fixtures/id_rsa_cert_key-cert.pub @@ -0,0 +1 @@ +ssh-rsa-cert-v01@openssh.com AAAAHHNzaC1yc2EtY2VydC12MDFAb3BlbnNzaC5jb20AAAAgoRYzrqRIH7VnxQSKjgCnQ0cBR7NlIPO88eOqY8x/8bMAAAADAQABAAABAQDZ57+PnXO9lEa0ipPPiCKql19zSaE4i4Tea8RhHhc4LEIE9R03c+dY2mSGXDbq4t2qQ4/3p5EeflW/iuiPfeygchPl8nawnSzs2aIimREGEN7VyZp+bi8VmJ17H/1ezJtrgkun4h7ILYYD/eoufkOgWckicODL6O9jqdsIbJoX1kVJpo1oX35Ms3WrI/nWy9+tOHm6Zgz0BVPomoRGw5AZp1b98fBoFq5QnYueizyv3+/zVkM4fwolbh0upZRO8TbYBwAZ8Dou4xQGYzTUXOi9hDTU3XnyeLMLNLUnIEfbi833yll79+3SeluZfDzqDQ4LL1lRObJth3u5HGYmtesZAAAAAAAAAAAAAAABAAAADXNzaDItdGVzdC1yc2EAAAANAAAACUNlcnQgVXNlcgAAAABeC7bQAAAAAbCd7tAAAAAAAAAAggAAABVwZXJtaXQtWDExLWZvcndhcmRpbmcAAAAAAAAAF3Blcm1pdC1hZ2VudC1mb3J3YXJkaW5nAAAAAAAAABZwZXJtaXQtcG9ydC1mb3J3YXJkaW5nAAAAAAAAAApwZXJtaXQtcHR5AAAAAAAAAA5wZXJtaXQtdXNlci1yYwAAAAAAAAAAAAAAMwAAAAtzc2gtZWQyNTUxOQAAACDykUWEj6OrUuGyq0pqXE3jOKBQW9ZkVUg7mtbpYX6rzQAAAFMAAAALc3NoLWVkMjU1MTkAAABAbL5jRF146WtpsOR7wN6NM3N7IqUqOLRI8MfTFeeVm2YwGFa4nnCUqO8Qi84geZYD6vhIS43Wct+ltAr5tYsqDQ== ssh2 cert test (rsa) diff --git a/test/fixtures/id_rsa_cert_key.pub b/test/fixtures/id_rsa_cert_key.pub new file mode 100644 index 00000000..50c33f88 --- /dev/null +++ b/test/fixtures/id_rsa_cert_key.pub @@ -0,0 +1 @@ +ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDZ57+PnXO9lEa0ipPPiCKql19zSaE4i4Tea8RhHhc4LEIE9R03c+dY2mSGXDbq4t2qQ4/3p5EeflW/iuiPfeygchPl8nawnSzs2aIimREGEN7VyZp+bi8VmJ17H/1ezJtrgkun4h7ILYYD/eoufkOgWckicODL6O9jqdsIbJoX1kVJpo1oX35Ms3WrI/nWy9+tOHm6Zgz0BVPomoRGw5AZp1b98fBoFq5QnYueizyv3+/zVkM4fwolbh0upZRO8TbYBwAZ8Dou4xQGYzTUXOi9hDTU3XnyeLMLNLUnIEfbi833yll79+3SeluZfDzqDQ4LL1lRObJth3u5HGYmtesZ ssh2 cert test (rsa) diff --git a/test/fixtures/ssh_user_ca b/test/fixtures/ssh_user_ca new file mode 100644 index 00000000..40d39d2c --- /dev/null +++ b/test/fixtures/ssh_user_ca @@ -0,0 +1,7 @@ +-----BEGIN OPENSSH PRIVATE KEY----- +b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW +QyNTUxOQAAACDykUWEj6OrUuGyq0pqXE3jOKBQW9ZkVUg7mtbpYX6rzQAAAJCLILSGiyC0 +hgAAAAtzc2gtZWQyNTUxOQAAACDykUWEj6OrUuGyq0pqXE3jOKBQW9ZkVUg7mtbpYX6rzQ +AAAECxVlsIUGiJCdmvEtE2z2+cPi/ChWjf8gSfOHcCm8JYU/KRRYSPo6tS4bKrSmpcTeM4 +oFBb1mRVSDua1ulhfqvNAAAADHNzaDIgdGVzdCBDQQE= +-----END OPENSSH PRIVATE KEY----- diff --git a/test/fixtures/ssh_user_ca.pub b/test/fixtures/ssh_user_ca.pub new file mode 100644 index 00000000..86a5e76f --- /dev/null +++ b/test/fixtures/ssh_user_ca.pub @@ -0,0 +1 @@ +ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPKRRYSPo6tS4bKrSmpcTeM4oFBb1mRVSDua1ulhfqvN ssh2 test CA diff --git a/test/test-userauth-cert.js b/test/test-userauth-cert.js new file mode 100644 index 00000000..7ed2d8f5 --- /dev/null +++ b/test/test-userauth-cert.js @@ -0,0 +1,122 @@ +'use strict'; + +const assert = require('assert'); + +const { sigSSHToASN1 } = require('../lib/protocol/utils.js'); + +const { + fixtureKey, + mustCall, + setup, +} = require('./common.js'); + +// Fixtures were generated with: +// ssh-keygen -t ed25519 -N '' -f ssh_user_ca +// ssh-keygen -t -N '' -f id__cert_key +// ssh-keygen -s ssh_user_ca -I ssh2-test- -n 'Cert User' \ +// -V 20200101:22000101 id__cert_key.pub +// The far-future validity keeps them from expiring under the test suite. + +const serverCfg = { hostKeys: [ fixtureKey('ssh_host_rsa_key').raw ] }; + +const debug = false; + +const CERT_SUFFIX = '-cert-v01@openssh.com'; + +// An SSH signature blob is `string algorithm, string signature`. +function parseSignatureBlob(blob) { + const algoLen = blob.readUInt32BE(0); + const algo = blob.utf8Slice(4, 4 + algoLen); + const sigLen = blob.readUInt32BE(4 + algoLen); + const signature = blob.slice(8 + algoLen, 8 + algoLen + sigLen); + return { algo, signature }; +} + +// Certificates ================================================================ +// +// A publickey request names the algorithm twice: the request's own algorithm +// field says what is offered (the certificate type), while the signature blob +// says what actually signed (the plain key type). These tests check both names +// independently, and that the signature verifies against the plain key. +[ + { desc: 'ed25519 certificate', + keyFile: 'id_ed25519_cert_key', + certFile: 'id_ed25519_cert_key-cert.pub' }, + { desc: 'RSA certificate', + keyFile: 'id_rsa_cert_key', + certFile: 'id_rsa_cert_key-cert.pub' }, + { desc: 'ECDSA certificate', + keyFile: 'id_ecdsa_cert_key', + certFile: 'id_ecdsa_cert_key-cert.pub' }, +].forEach((test) => { + const { desc, keyFile, certFile } = test; + const clientKey = fixtureKey(keyFile); + const clientCert = fixtureKey(certFile); + const plainType = clientKey.key.type; + const certType = clientCert.key.type; + assert(certType === `${plainType}${CERT_SUFFIX}`, + `Fixture mismatch: ${certType} is not a certificate for ${plainType}`); + + const username = 'Cert User'; + const { server } = setup( + desc, + { + client: { + username, + privateKey: clientKey.raw, + publicKey: clientCert.raw, + }, + server: serverCfg, + + debug, + } + ); + + server.on('connection', mustCall((conn) => { + let authAttempt = 0; + conn.on('authentication', mustCall((ctx) => { + assert(ctx.username === username, + `Wrong username: ${ctx.username}`); + switch (++authAttempt) { + case 1: + assert(ctx.method === 'none', `Wrong auth method: ${ctx.method}`); + return ctx.reject(); + case 2: + assert(ctx.method === 'publickey', + `Wrong auth method: ${ctx.method}`); + assert(!ctx.signature, 'Unexpected signature on the check request'); + assert(ctx.key.algo === certType, + `Wrong key algo: ${ctx.key.algo}`); + assert.deepStrictEqual(ctx.key.data, + clientCert.key.getPublicSSH(), + 'Certificate blob mismatch'); + break; + case 3: { + assert(ctx.method === 'publickey', + `Wrong auth method: ${ctx.method}`); + assert(ctx.signature, 'Missing publickey signature'); + assert(ctx.key.algo === certType, + `Wrong key algo: ${ctx.key.algo}`); + + // The server only unwraps the signature blob when its algorithm + // matches the request's, which a certificate's never does — so the + // blob arrives intact and the inner name can be checked directly. + const { algo, signature } = parseSignatureBlob(ctx.signature); + assert(algo === plainType, + `Signature blob names ${algo}, expected ${plainType}`); + + // What signed is the plain key, so the signature verifies against it. + const verifiable = sigSSHToASN1(signature, plainType); + assert(verifiable, 'Malformed signature for the plain key type'); + const result = + clientKey.key.verify(ctx.blob, verifiable, ctx.hashAlgo); + assert(result === true, 'Could not verify certificate signature'); + break; + } + } + ctx.accept(); + }, 3)).on('ready', mustCall(() => { + conn.end(); + })); + })); +});