Skip to content

Commit

Permalink
lib: add server-sig-algs support
Browse files Browse the repository at this point in the history
Fixes: #989
  • Loading branch information
mscdex committed Apr 29, 2023
1 parent ac7f9bc commit 41fa3b6
Show file tree
Hide file tree
Showing 13 changed files with 346 additions and 52 deletions.
6 changes: 6 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,12 @@ jobs:
node-version: ${{ matrix.node-version }}
- name: Check Node.js version
run: node -pe process.versions
- name: Install Python 2.x
if: ${{ matrix.node-version == '10.16.0' }}
uses: actions/setup-python@v4
with:
python-version: '2.7'
update-environment: true
- name: Install module
run: npm install
- name: Run tests
Expand Down
6 changes: 5 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -525,7 +525,7 @@ new Server({
case 'publickey':
if (ctx.key.algo !== allowedPubKey.type
|| !checkValue(ctx.key.data, allowedPubKey.getPublicSSH())
|| (ctx.signature && allowedPubKey.verify(ctx.blob, ctx.signature) !== true)) {
|| (ctx.signature && allowedPubKey.verify(ctx.blob, ctx.signature, ctx.hashAlgo) !== true)) {
return ctx.reject();
}
break;
Expand Down Expand Up @@ -1093,6 +1093,8 @@ You can find more examples in the `examples` directory of this repository.

* **signature** - _Buffer_ - This contains a signature to be verified that is passed to (along with the blob) `key.verify()` where `key` is a public key parsed with [`parseKey()`](#utilities).

* **hashAlgo** - _mixed_ - This is either `undefined` or a _string_ containing an explicit hash algorithm to be used during verification (passed to `key.verify()`).

* `keyboard-interactive`:

* **prompt**(< _array_ >prompts[, < _string_ >title[, < _string_ >instructions]], < _function_ >callback) - _(void)_ - Send prompts to the client. `prompts` is an array of `{ prompt: 'Prompt text', echo: true }` objects (`prompt` being the prompt text and `echo` indicating whether the client's response to the prompt should be echoed to their display). `callback` is called with `(responses)`, where `responses` is an array of string responses matching up to the `prompts`.
Expand All @@ -1117,6 +1119,8 @@ You can find more examples in the `examples` directory of this repository.

* **signature** - _mixed_ - If the value is `undefined`, the client is only checking the validity of the `key`. If the value is a _Buffer_, then this contains a signature to be verified that is passed to (along with the blob) `key.verify()` where `key` is a public key parsed with [`parseKey()`](#utilities).

* **hashAlgo** - _mixed_ - This is either `undefined` or a _string_ containing an explicit hash algorithm to be used during verification (passed to `key.verify()`).

* **close**() - The client socket was closed.

* **end**() - The client socket disconnected.
Expand Down
141 changes: 132 additions & 9 deletions lib/client.js
Original file line number Diff line number Diff line change
Expand Up @@ -297,6 +297,7 @@ class Client extends EventEmitter {
const DEBUG_HANDLER = (!debug ? undefined : (p, display, msg) => {
debug(`Debug output from server: ${JSON.stringify(msg)}`);
});
let serverSigAlgs;
const proto = this._protocol = new Protocol({
ident: this.config.ident,
offer: (allOfferDefaults ? undefined : algorithms),
Expand Down Expand Up @@ -348,6 +349,17 @@ class Client extends EventEmitter {
if (name === 'ssh-userauth')
tryNextAuth();
},
EXT_INFO: (p, exts) => {
if (serverSigAlgs === undefined) {
for (const ext of exts) {
if (ext.name === 'server-sig-algs') {
serverSigAlgs = ext.algs;
return;
}
}
serverSigAlgs = null;
}
},
USERAUTH_BANNER: (p, msg) => {
this.emit('banner', msg);
},
Expand All @@ -360,6 +372,51 @@ class Client extends EventEmitter {
this.emit('ready');
},
USERAUTH_FAILURE: (p, authMethods, partialSuccess) => {
// For key-based authentication, check if we should retry the current
// key with a different algorithm first
if (curAuth.keyAlgos) {
const oldKeyAlgo = curAuth.keyAlgos[0][0];
if (debug)
debug(`Client: ${curAuth.type} (${oldKeyAlgo}) auth failed`);
curAuth.keyAlgos.shift();
if (curAuth.keyAlgos.length) {
const [keyAlgo, hashAlgo] = curAuth.keyAlgos[0];
switch (curAuth.type) {
case 'agent':
proto.authPK(
curAuth.username,
curAuth.agentCtx.currentKey(),
keyAlgo
);
return;
case 'publickey':
proto.authPK(curAuth.username, curAuth.key, keyAlgo);
return;
case 'hostbased':
proto.authHostbased(curAuth.username,
curAuth.key,
curAuth.localHostname,
curAuth.localUsername,
keyAlgo,
(buf, cb) => {
const signature = curAuth.key.sign(buf, hashAlgo);
if (signature instanceof Error) {
signature.message =
`Error while signing with key: ${signature.message}`;
signature.level = 'client-authentication';
this.emit('error', signature);
return tryNextAuth();
}

cb(signature);
});
return;
}
} else {
curAuth.keyAlgos = undefined;
}
}

if (curAuth.type === 'agent') {
const pos = curAuth.agentCtx.pos();
debug && debug(`Client: Agent key #${pos + 1} failed`);
Expand All @@ -386,10 +443,15 @@ class Client extends EventEmitter {
}
},
USERAUTH_PK_OK: (p) => {
let keyAlgo;
let hashAlgo;
if (curAuth.keyAlgos)
[keyAlgo, hashAlgo] = curAuth.keyAlgos[0];
if (curAuth.type === 'agent') {
const key = curAuth.agentCtx.currentKey();
proto.authPK(curAuth.username, key, (buf, cb) => {
curAuth.agentCtx.sign(key, buf, {}, (err, signed) => {
proto.authPK(curAuth.username, key, keyAlgo, (buf, cb) => {
const opts = { hash: hashAlgo };
curAuth.agentCtx.sign(key, buf, opts, (err, signed) => {
if (err) {
err.level = 'agent';
this.emit('error', err);
Expand All @@ -401,8 +463,8 @@ class Client extends EventEmitter {
});
});
} else if (curAuth.type === 'publickey') {
proto.authPK(curAuth.username, curAuth.key, (buf, cb) => {
const signature = curAuth.key.sign(buf);
proto.authPK(curAuth.username, curAuth.key, keyAlgo, (buf, cb) => {
const signature = curAuth.key.sign(buf, hashAlgo);
if (signature instanceof Error) {
signature.message =
`Error signing data with key: ${signature.message}`;
Expand Down Expand Up @@ -937,16 +999,42 @@ class Client extends EventEmitter {
case 'password':
proto.authPassword(username, curAuth.password);
break;
case 'publickey':
proto.authPK(username, curAuth.key);
case 'publickey': {
let keyAlgo;
curAuth.keyAlgos = getKeyAlgos(this, curAuth.key, serverSigAlgs);
if (curAuth.keyAlgos) {
if (curAuth.keyAlgos.length) {
keyAlgo = curAuth.keyAlgos[0][0];
} else {
return skipAuth(
'Skipping key authentication (no mutual hash algorithm)'
);
}
}
proto.authPK(username, curAuth.key, keyAlgo);
break;
case 'hostbased':
}
case 'hostbased': {
let keyAlgo;
let hashAlgo;
curAuth.keyAlgos = getKeyAlgos(this, curAuth.key, serverSigAlgs);
if (curAuth.keyAlgos) {
if (curAuth.keyAlgos.length) {
[keyAlgo, hashAlgo] = curAuth.keyAlgos[0];
} else {
return skipAuth(
'Skipping hostbased authentication (no mutual hash algorithm)'
);
}
}

proto.authHostbased(username,
curAuth.key,
curAuth.localHostname,
curAuth.localUsername,
keyAlgo,
(buf, cb) => {
const signature = curAuth.key.sign(buf);
const signature = curAuth.key.sign(buf, hashAlgo);
if (signature instanceof Error) {
signature.message =
`Error while signing with key: ${signature.message}`;
Expand All @@ -958,6 +1046,7 @@ class Client extends EventEmitter {
cb(signature);
});
break;
}
case 'agent':
curAuth.agentCtx.init((err) => {
if (err) {
Expand Down Expand Up @@ -1002,8 +1091,21 @@ class Client extends EventEmitter {
tryNextAuth();
} else {
const pos = curAuth.agentCtx.pos();
let keyAlgo;
curAuth.keyAlgos = getKeyAlgos(this, key, serverSigAlgs);
if (curAuth.keyAlgos) {
if (curAuth.keyAlgos.length) {
keyAlgo = curAuth.keyAlgos[0][0];
} else {
debug && debug(
`Agent: Skipping key #${pos + 1} (no mutual hash algorithm)`
);
tryNextAgentKey();
return;
}
}
debug && debug(`Agent: Trying key #${pos + 1}`);
proto.authPK(curAuth.username, key);
proto.authPK(curAuth.username, key, keyAlgo);
}
}
};
Expand Down Expand Up @@ -2017,4 +2119,25 @@ function hostKeysProve(client, keys_, cb) {
);
}

function getKeyAlgos(client, key, serverSigAlgs) {
switch (key.type) {
case 'ssh-rsa':
if (client._protocol._compatFlags & COMPAT.IMPLY_RSA_SHA2_SIGALGS) {
if (!Array.isArray(serverSigAlgs))
serverSigAlgs = ['rsa-sha2-256', 'rsa-sha2-512'];
else
serverSigAlgs = ['rsa-sha2-256', 'rsa-sha2-512', ...serverSigAlgs];
}
if (Array.isArray(serverSigAlgs)) {
if (serverSigAlgs.indexOf('rsa-sha2-256') !== -1)
return [['rsa-sha2-256', 'sha256']];
if (serverSigAlgs.indexOf('rsa-sha2-512') !== -1)
return [['rsa-sha2-512', 'sha512']];
if (serverSigAlgs.indexOf('ssh-rsa') === -1)
return [];
}
return [['ssh-rsa', 'sha1']];
}
}

module.exports = Client;
Loading

0 comments on commit 41fa3b6

Please sign in to comment.