Marco Ricci commited on 2024-11-26 14:12:53
Zeige 1 geänderte Dateien mit 60 Einfügungen und 4 Löschungen.
Instead of tying deterministic signatures directly to the detection of Pageant specifically, add a general mechanism for attempting to infer the connected SSH agent from its reported list of extensions. This moves the question of *how* we detect certain SSH agents out of the deterministic signature checking function. Alas, OpenSSH does not support the extension query message we issue, despite them supporting the extension system in general *and* stewarding the SSH agent protocol specification which defines this message normatively. So our implementation must tolerate a moderate level of spec violation.
... | ... |
@@ -18,6 +18,7 @@ from derivepassphrase import _types |
18 | 18 |
|
19 | 19 |
if TYPE_CHECKING: |
20 | 20 |
from collections.abc import Iterable, Iterator, Sequence |
21 |
+ from collections.abc import Set as AbstractSet |
|
21 | 22 |
from types import TracebackType |
22 | 23 |
|
23 | 24 |
from typing_extensions import Buffer |
... | ... |
@@ -338,6 +339,18 @@ class SSHAgentClient: |
338 | 339 |
msg = f'invalid connection hint: {conn!r}' |
339 | 340 |
raise TypeError(msg) # noqa: DOC501 |
340 | 341 |
|
342 |
+ def _agent_is_pageant(self) -> bool: |
|
343 |
+ """Return True if we are connected to Pageant. |
|
344 |
+ |
|
345 |
+ Warning: |
|
346 |
+ This is a heuristic, not a verified query or computation. |
|
347 |
+ |
|
348 |
+ """ |
|
349 |
+ return ( |
|
350 |
+ b'list-extended@putty.projects.tartarus.org' |
|
351 |
+ in self.query_extensions() |
|
352 |
+ ) |
|
353 |
+ |
|
341 | 354 |
def has_deterministic_signatures(self) -> bool: |
342 | 355 |
"""Check whether the agent returns deterministic signatures. |
343 | 356 |
|
... | ... |
@@ -351,11 +364,12 @@ class SSHAgentClient: |
351 | 364 |
| Pageant (PuTTY) | `list-extended@putty.projects.tartarus.org` extension request | |
352 | 365 |
|
353 | 366 |
""" # noqa: E501 |
354 |
- returncode, _payload = self.request( |
|
355 |
- _types.SSH_AGENTC.EXTENSION, |
|
356 |
- self.string(b'list-extended@putty.projects.tartarus.org'), |
|
367 |
+ known_good_agents = { |
|
368 |
+ 'Pageant': self._agent_is_pageant, |
|
369 |
+ } |
|
370 |
+ return any( # pragma: no branch |
|
371 |
+ v() for v in known_good_agents.values() |
|
357 | 372 |
) |
358 |
- return returncode == _types.SSH_AGENT.SUCCESS.value |
|
359 | 373 |
|
360 | 374 |
@overload |
361 | 375 |
def request( # pragma: no cover |
... | ... |
@@ -578,3 +592,45 @@ class SSHAgentClient: |
578 | 592 |
) |
579 | 593 |
) |
580 | 594 |
) |
595 |
+ |
|
596 |
+ def query_extensions(self) -> AbstractSet[bytes]: |
|
597 |
+ """Request a list of extensions supported by the SSH agent. |
|
598 |
+ |
|
599 |
+ Args: |
|
600 |
+ raise_if_no_extension_support: |
|
601 |
+ If true, and if the agent does not support querying |
|
602 |
+ extensions, then raise an error. If false, silently |
|
603 |
+ return an empty result. |
|
604 |
+ |
|
605 |
+ Returns: |
|
606 |
+ A read-only sequence of extension names. |
|
607 |
+ |
|
608 |
+ Raises: |
|
609 |
+ EOFError: |
|
610 |
+ The response from the SSH agent is truncated or missing. |
|
611 |
+ OSError: |
|
612 |
+ There was a communication error with the SSH agent. |
|
613 |
+ SSHAgentFailedError: |
|
614 |
+ The agent failed to complete the request. |
|
615 |
+ |
|
616 |
+ """ |
|
617 |
+ try: |
|
618 |
+ response_data = self.request( |
|
619 |
+ _types.SSH_AGENTC.EXTENSION, |
|
620 |
+ self.string(b'query'), |
|
621 |
+ response_code={ |
|
622 |
+ _types.SSH_AGENT.EXTENSION_RESPONSE, |
|
623 |
+ _types.SSH_AGENT.SUCCESS, |
|
624 |
+ }, |
|
625 |
+ ) |
|
626 |
+ except SSHAgentFailedError: |
|
627 |
+ # Cannot query extension support. Assume no extensions. |
|
628 |
+ # This isn't necessarily true, e.g. for OpenSSH's ssh-agent. |
|
629 |
+ return frozenset() |
|
630 |
+ extensions: set[bytes] = set() |
|
631 |
+ _query, response_data = self.unstring_prefix(response_data) |
|
632 |
+ assert bytes(_query) == b'query' |
|
633 |
+ while response_data: |
|
634 |
+ extension, response_data = self.unstring_prefix(response_data) |
|
635 |
+ extensions.add(bytes(extension)) |
|
636 |
+ return frozenset(extensions) |
|
581 | 637 |