Skip to content

Commit 8593f28

Browse files
committed
Fix issues related to supporting multiple trusted security keys
This commit better handles a use case where a client might be capable of authenticating with one of multiple security keys, all of which are accepted by the server but only some of which are plugged into the client system. With this change, keys in client_keys which correspond to security keys that aren't plugged into the system are ignored. Only keys which are connected, present in client_keys, and trusted by the server will actually be used for signing, possibly triggering a touch requirement. Thanks go to GitHub user zanda8893 for reporting the issue and helping to work out the details of the problem!
1 parent bfa04aa commit 8593f28

File tree

6 files changed

+45
-28
lines changed

6 files changed

+45
-28
lines changed

asyncssh/auth.py

Lines changed: 18 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -159,7 +159,7 @@ async def _start(self) -> None:
159159
await self.send_request(key=self._conn.get_gss_context(),
160160
trivial=False)
161161
else:
162-
self._conn.try_next_auth()
162+
self._conn.try_next_auth(next_method=True)
163163

164164

165165
class _ClientGSSMICAuth(ClientAuth):
@@ -184,7 +184,7 @@ async def _start(self) -> None:
184184
mechs = b''.join(String(mech) for mech in self._gss.mechs)
185185
await self.send_request(UInt32(len(self._gss.mechs)), mechs)
186186
else:
187-
self._conn.try_next_auth()
187+
self._conn.try_next_auth(next_method=True)
188188

189189
def _finish(self) -> None:
190190
"""Finish client GSS MIC authentication"""
@@ -224,7 +224,7 @@ async def _process_response(self, _pkttype: int, _pktid: int,
224224
if exc.token:
225225
self.send_packet(MSG_USERAUTH_GSSAPI_ERRTOK, String(exc.token))
226226

227-
self._conn.try_next_auth()
227+
self._conn.try_next_auth(next_method=True)
228228

229229
async def _process_token(self, _pkttype: int, _pktid: int,
230230
packet: SSHPacket) -> None:
@@ -247,7 +247,7 @@ async def _process_token(self, _pkttype: int, _pktid: int,
247247
if exc.token:
248248
self.send_packet(MSG_USERAUTH_GSSAPI_ERRTOK, String(exc.token))
249249

250-
self._conn.try_next_auth()
250+
self._conn.try_next_auth(next_method=True)
251251

252252
def _process_error(self, _pkttype: int, _pktid: int,
253253
packet: SSHPacket) -> None:
@@ -295,7 +295,7 @@ async def _start(self) -> None:
295295
await self._conn.host_based_auth_requested()
296296

297297
if keypair is None:
298-
self._conn.try_next_auth()
298+
self._conn.try_next_auth(next_method=True)
299299
return
300300

301301
self.logger.debug1('Trying host based auth of user %s on host %s '
@@ -323,7 +323,7 @@ async def _start(self) -> None:
323323
self._keypair = await self._conn.public_key_auth_requested()
324324

325325
if self._keypair is None:
326-
self._conn.try_next_auth()
326+
self._conn.try_next_auth(next_method=True)
327327
return
328328

329329
self.logger.debug1('Trying public key auth with %s key',
@@ -341,10 +341,14 @@ async def _send_signed_request(self) -> None:
341341
self.logger.debug1('Signing request with %s key',
342342
self._keypair.algorithm)
343343

344-
await self.send_request(Boolean(True),
345-
String(self._keypair.algorithm),
346-
String(self._keypair.public_data),
347-
key=self._keypair, trivial=False)
344+
try:
345+
await self.send_request(Boolean(True),
346+
String(self._keypair.algorithm),
347+
String(self._keypair.public_data),
348+
key=self._keypair, trivial=False)
349+
except ValueError as exc:
350+
self.logger.debug1('Public key auth failed: %s', str(exc))
351+
self._conn.try_next_auth()
348352

349353
def _process_public_key_ok(self, _pkttype: int, _pktid: int,
350354
packet: SSHPacket) -> None:
@@ -378,7 +382,7 @@ async def _start(self) -> None:
378382
submethods = await self._conn.kbdint_auth_requested()
379383

380384
if submethods is None:
381-
self._conn.try_next_auth()
385+
self._conn.try_next_auth(next_method=True)
382386
return
383387

384388
self.logger.debug1('Trying keyboard-interactive auth')
@@ -394,7 +398,7 @@ async def _receive_challenge(self, name: str, instruction: str, lang: str,
394398
lang, prompts)
395399

396400
if responses is None:
397-
self._conn.try_next_auth()
401+
self._conn.try_next_auth(next_method=True)
398402
return
399403

400404
self.send_packet(MSG_USERAUTH_INFO_RESPONSE, UInt32(len(responses)),
@@ -455,7 +459,7 @@ async def _start(self) -> None:
455459
password = await self._conn.password_auth_requested()
456460

457461
if password is None:
458-
self._conn.try_next_auth()
462+
self._conn.try_next_auth(next_method=True)
459463
return
460464

461465
self.logger.debug1('Trying password auth')
@@ -470,7 +474,7 @@ async def _change_password(self, prompt: str, lang: str) -> None:
470474

471475
if result == NotImplemented:
472476
# Password change not supported - move on to the next auth method
473-
self._conn.try_next_auth()
477+
self._conn.try_next_auth(next_method=True)
474478
return
475479

476480
self.logger.debug1('Trying to chsnge password')

asyncssh/connection.py

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3513,21 +3513,24 @@ def get_server_auth_methods(self) -> Sequence[str]:
35133513

35143514
return [method.decode('ascii') for method in self._auth_methods]
35153515

3516-
def try_next_auth(self) -> None:
3516+
def try_next_auth(self, *, next_method: bool = False) -> None:
35173517
"""Attempt client authentication using the next compatible method"""
35183518

35193519
if self._auth:
35203520
self._auth.cancel()
35213521
self._auth = None
35223522

3523-
while self._auth_methods:
3524-
method = self._auth_methods.pop(0)
3523+
if next_method:
3524+
self._auth_methods.pop(0)
35253525

3526-
self._auth = lookup_client_auth(self, method)
3526+
while self._auth_methods:
3527+
self._auth = lookup_client_auth(self, self._auth_methods[0])
35273528

35283529
if self._auth:
35293530
return
35303531

3532+
self._auth_methods.pop(0)
3533+
35313534
self.logger.info('Auth failed for user %s', self._username)
35323535

35333536
self._force_close(PermissionDenied('Permission denied for user '

asyncssh/sk.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,11 @@ def _ctap2_sign(dev: 'CtapHidDevice', message_hash: bytes,
147147
allow_creds = [{'type': 'public-key', 'id': key_handle}]
148148
options = {'up': touch_required}
149149

150+
# See if key handle exists before requiring touch
151+
if touch_required:
152+
ctap2.get_assertions(application, message_hash, allow_creds,
153+
options={'up': False})
154+
150155
assertion = ctap2.get_assertions(application, message_hash, allow_creds,
151156
options=options)[0]
152157

tests/test_auth.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -165,9 +165,11 @@ async def get_auth_result(self):
165165

166166
return await self._auth_waiter
167167

168-
def try_next_auth(self):
168+
def try_next_auth(self, *, next_method=False):
169169
"""Handle a request to move to another form of auth"""
170170

171+
# pylint: disable=unused-argument
172+
171173
# Report that the current auth attempt failed
172174
self._auth_waiter.set_result((False, self._password_changed))
173175
self._auth = None

tests/test_connection_auth.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -414,11 +414,11 @@ async def validate_kbdint_response(self, username, responses):
414414
class _UnknownAuthClientConnection(asyncssh.connection.SSHClientConnection):
415415
"""Test getting back an unknown auth method from the SSH server"""
416416

417-
def try_next_auth(self):
417+
def try_next_auth(self, *, next_method=False):
418418
"""Attempt client authentication using an unknown method"""
419419

420420
self._auth_methods = [b'unknown'] + self._auth_methods
421-
super().try_next_auth()
421+
super().try_next_auth(next_method=next_method)
422422

423423

424424
class _TestNullAuth(ServerTestCase):

tests/test_sk.py

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -123,8 +123,9 @@ async def test_auth_ctap1_error(self):
123123
"""Test security key returning a CTAP 1 error"""
124124

125125
with sk_error('err'):
126-
with self.assertRaises(ValueError):
127-
await self.connect(username='ckey', client_keys=[self._privkey])
126+
with self.assertRaises(asyncssh.PermissionDenied):
127+
await self.connect(username='ckey',
128+
client_keys=[self._privkey])
128129

129130

130131
@unittest.skipUnless(sk_available, 'security key support not available')
@@ -169,8 +170,9 @@ async def test_auth_ctap2_error(self):
169170
"""Test security key returning a CTAP 2 error"""
170171

171172
with sk_error('err'):
172-
with self.assertRaises(ValueError):
173-
await self.connect(username='ckey', client_keys=[self._privkey])
173+
with self.assertRaises(asyncssh.PermissionDenied):
174+
await self.connect(username='ckey',
175+
client_keys=[self._privkey])
174176

175177
@asynctest
176178
async def test_enroll_pin_invalid(self):
@@ -201,8 +203,9 @@ async def test_auth_cred_not_found(self):
201203
"""Test authenticating with security credential not found"""
202204

203205
with sk_error('nocred'):
204-
with self.assertRaises(ValueError):
205-
await self.connect(username='ckey', client_keys=[self._privkey])
206+
with self.assertRaises(asyncssh.PermissionDenied):
207+
await self.connect(username='ckey',
208+
client_keys=[self._privkey])
206209

207210

208211
@unittest.skipUnless(sk_available, 'security key support not available')
@@ -255,7 +258,7 @@ async def test_load_resident_ctap2_error(self):
255258
"""Test getting resident keys returning a CTAP 2 error"""
256259

257260
with sk_error('err'):
258-
with self.assertRaises(ValueError):
261+
with self.assertRaises(asyncssh.KeyImportError):
259262
asyncssh.load_resident_keys(b'123456')
260263

261264
@asynctest

0 commit comments

Comments
 (0)