Marco Ricci commited on 2024-11-13 20:54:26
Zeige 3 geänderte Dateien mit 67 Einfügungen und 51 Löschungen.
Centralize functionality for constructing one-off SSH agent clients in child contexts.
| ... | ... |
@@ -8,7 +8,6 @@ from __future__ import annotations |
| 8 | 8 |
|
| 9 | 9 |
import base64 |
| 10 | 10 |
import collections |
| 11 |
-import contextlib |
|
| 12 | 11 |
import copy |
| 13 | 12 |
import enum |
| 14 | 13 |
import importlib |
| ... | ... |
@@ -16,7 +15,6 @@ import inspect |
| 16 | 15 |
import json |
| 17 | 16 |
import logging |
| 18 | 17 |
import os |
| 19 |
-import socket |
|
| 20 | 18 |
import unicodedata |
| 21 | 19 |
from typing import ( |
| 22 | 20 |
TYPE_CHECKING, |
| ... | ... |
@@ -37,6 +35,7 @@ from derivepassphrase import _types, exporter, ssh_agent, vault |
| 37 | 35 |
|
| 38 | 36 |
if TYPE_CHECKING: |
| 39 | 37 |
import pathlib |
| 38 |
+ import socket |
|
| 40 | 39 |
import types |
| 41 | 40 |
from collections.abc import ( |
| 42 | 41 |
Iterator, |
| ... | ... |
@@ -484,21 +483,8 @@ def _get_suitable_ssh_keys( |
| 484 | 483 |
|
| 485 | 484 |
Args: |
| 486 | 485 |
conn: |
| 487 |
- An optional connection hint to the SSH agent; specifically, |
|
| 488 |
- an SSH agent client, or a socket connected to an SSH agent. |
|
| 489 |
- |
|
| 490 |
- If an existing SSH agent client, then this client will be |
|
| 491 |
- queried for the SSH keys, and otherwise left intact. |
|
| 492 |
- |
|
| 493 |
- If a socket, then a one-shot client will be constructed |
|
| 494 |
- based on the socket to query the agent, and deconstructed |
|
| 495 |
- afterwards. |
|
| 496 |
- |
|
| 497 |
- If neither are given, then the agent's socket location is |
|
| 498 |
- looked up in the `SSH_AUTH_SOCK` environment variable, and |
|
| 499 |
- used to construct/deconstruct a one-shot client, as in the |
|
| 500 |
- previous case. This requires the [`socket.AF_UNIX`][] |
|
| 501 |
- symbol to exist. |
|
| 486 |
+ An optional connection hint to the SSH agent. See |
|
| 487 |
+ [`ssh_agent.SSHAgentClient.ensure_agent_subcontext`][]. |
|
| 502 | 488 |
|
| 503 | 489 |
Yields: |
| 504 | 490 |
Every SSH key from the SSH agent that is suitable for passphrase |
| ... | ... |
@@ -524,20 +510,7 @@ def _get_suitable_ssh_keys( |
| 524 | 510 |
The agent failed to supply a list of loaded keys. |
| 525 | 511 |
|
| 526 | 512 |
""" |
| 527 |
- client: ssh_agent.SSHAgentClient |
|
| 528 |
- client_context: contextlib.AbstractContextManager[Any] |
|
| 529 |
- # Use match/case here once Python 3.9 becomes unsupported. |
|
| 530 |
- if isinstance(conn, ssh_agent.SSHAgentClient): |
|
| 531 |
- client = conn |
|
| 532 |
- client_context = contextlib.nullcontext() |
|
| 533 |
- elif isinstance(conn, socket.socket) or conn is None: |
|
| 534 |
- client = ssh_agent.SSHAgentClient(socket=conn) |
|
| 535 |
- client_context = client |
|
| 536 |
- else: # pragma: no cover |
|
| 537 |
- assert_never(conn) |
|
| 538 |
- msg = f'invalid connection hint: {conn!r}'
|
|
| 539 |
- raise TypeError(msg) # noqa: DOC501 |
|
| 540 |
- with client_context: |
|
| 513 |
+ with ssh_agent.SSHAgentClient.ensure_agent_subcontext(conn) as client: |
|
| 541 | 514 |
try: |
| 542 | 515 |
all_key_comment_pairs = list(client.list_keys()) |
| 543 | 516 |
except EOFError as e: # pragma: no cover |
| ... | ... |
@@ -633,21 +606,8 @@ def _select_ssh_key( |
| 633 | 606 |
|
| 634 | 607 |
Args: |
| 635 | 608 |
conn: |
| 636 |
- An optional connection hint to the SSH agent; specifically, |
|
| 637 |
- an SSH agent client, or a socket connected to an SSH agent. |
|
| 638 |
- |
|
| 639 |
- If an existing SSH agent client, then this client will be |
|
| 640 |
- queried for the SSH keys, and otherwise left intact. |
|
| 641 |
- |
|
| 642 |
- If a socket, then a one-shot client will be constructed |
|
| 643 |
- based on the socket to query the agent, and deconstructed |
|
| 644 |
- afterwards. |
|
| 645 |
- |
|
| 646 |
- If neither are given, then the agent's socket location is |
|
| 647 |
- looked up in the `SSH_AUTH_SOCK` environment variable, and |
|
| 648 |
- used to construct/deconstruct a one-shot client, as in the |
|
| 649 |
- previous case. This requires the [`socket.AF_UNIX`][] |
|
| 650 |
- symbol to exist. |
|
| 609 |
+ An optional connection hint to the SSH agent. See |
|
| 610 |
+ [`ssh_agent.SSHAgentClient.ensure_agent_subcontext`][]. |
|
| 651 | 611 |
|
| 652 | 612 |
Returns: |
| 653 | 613 |
The selected SSH key. |
| ... | ... |
@@ -7,16 +7,17 @@ |
| 7 | 7 |
from __future__ import annotations |
| 8 | 8 |
|
| 9 | 9 |
import collections |
| 10 |
+import contextlib |
|
| 10 | 11 |
import os |
| 11 | 12 |
import socket |
| 12 | 13 |
from typing import TYPE_CHECKING, overload |
| 13 | 14 |
|
| 14 |
-from typing_extensions import Self |
|
| 15 |
+from typing_extensions import Self, assert_never |
|
| 15 | 16 |
|
| 16 | 17 |
from derivepassphrase import _types |
| 17 | 18 |
|
| 18 | 19 |
if TYPE_CHECKING: |
| 19 |
- from collections.abc import Iterable, Sequence |
|
| 20 |
+ from collections.abc import Iterable, Iterator, Sequence |
|
| 20 | 21 |
from types import TracebackType |
| 21 | 22 |
|
| 22 | 23 |
from typing_extensions import Buffer |
| ... | ... |
@@ -282,6 +283,61 @@ class SSHAgentClient: |
| 282 | 283 |
bytes(bytestring[m + HEAD_LEN :]), |
| 283 | 284 |
) |
| 284 | 285 |
|
| 286 |
+ @classmethod |
|
| 287 |
+ @contextlib.contextmanager |
|
| 288 |
+ def ensure_agent_subcontext( |
|
| 289 |
+ cls, |
|
| 290 |
+ conn: SSHAgentClient | socket.socket | None = None, |
|
| 291 |
+ ) -> Iterator[SSHAgentClient]: |
|
| 292 |
+ """Return an SSH agent client subcontext. |
|
| 293 |
+ |
|
| 294 |
+ If necessary, construct an SSH agent client first using the |
|
| 295 |
+ connection hint. |
|
| 296 |
+ |
|
| 297 |
+ Args: |
|
| 298 |
+ conn: |
|
| 299 |
+ If an existing SSH agent client, then enter a context |
|
| 300 |
+ within this client's scope. After exiting the context, |
|
| 301 |
+ the client persists, including its socket. |
|
| 302 |
+ |
|
| 303 |
+ If a socket, then construct a client using this socket, |
|
| 304 |
+ then enter a context within this client's scope. After |
|
| 305 |
+ exiting the context, the client is destroyed and the |
|
| 306 |
+ socket is closed. |
|
| 307 |
+ |
|
| 308 |
+ If `None`, construct a client using agent |
|
| 309 |
+ auto-discovery, then enter a context within this |
|
| 310 |
+ client's scope. After exiting the context, both the |
|
| 311 |
+ client and its socket are destroyed. |
|
| 312 |
+ |
|
| 313 |
+ Yields: |
|
| 314 |
+ When entering this context, return the SSH agent client. |
|
| 315 |
+ |
|
| 316 |
+ Raises: |
|
| 317 |
+ KeyError: |
|
| 318 |
+ `conn` was `None`, and the `SSH_AUTH_SOCK` environment |
|
| 319 |
+ variable was not found. |
|
| 320 |
+ NotImplementedError: |
|
| 321 |
+ `conn` was `None`, and this Python does not support |
|
| 322 |
+ [`socket.AF_UNIX`][], so the SSH agent client cannot be |
|
| 323 |
+ automatically set up. |
|
| 324 |
+ OSError: |
|
| 325 |
+ `conn` was a socket or `None`, and there was an error |
|
| 326 |
+ setting up a socket connection to the agent. |
|
| 327 |
+ |
|
| 328 |
+ """ |
|
| 329 |
+ # Use match/case here once Python 3.9 becomes unsupported. |
|
| 330 |
+ if isinstance(conn, SSHAgentClient): |
|
| 331 |
+ with contextlib.nullcontext(): |
|
| 332 |
+ yield conn |
|
| 333 |
+ elif isinstance(conn, socket.socket) or conn is None: |
|
| 334 |
+ with SSHAgentClient(socket=conn) as client: |
|
| 335 |
+ yield client |
|
| 336 |
+ else: # pragma: no cover |
|
| 337 |
+ assert_never(conn) |
|
| 338 |
+ msg = f'invalid connection hint: {conn!r}'
|
|
| 339 |
+ raise TypeError(msg) # noqa: DOC501 |
|
| 340 |
+ |
|
| 285 | 341 |
@overload |
| 286 | 342 |
def request( # pragma: no cover |
| 287 | 343 |
self, |
| ... | ... |
@@ -522,10 +522,10 @@ class Vault: |
| 522 | 522 |
'signature not deterministic' |
| 523 | 523 |
) |
| 524 | 524 |
raise ValueError(msg) |
| 525 |
- with ssh_agent.SSHAgentClient() as client: |
|
| 525 |
+ with ssh_agent.SSHAgentClient.ensure_agent_subcontext() as client: |
|
| 526 | 526 |
raw_sig = client.sign(key, cls._UUID) |
| 527 |
- _keytype, trailer = client.unstring_prefix(raw_sig) |
|
| 528 |
- signature_blob = client.unstring(trailer) |
|
| 527 |
+ _keytype, trailer = ssh_agent.SSHAgentClient.unstring_prefix(raw_sig) |
|
| 528 |
+ signature_blob = ssh_agent.SSHAgentClient.unstring(trailer) |
|
| 529 | 529 |
return bytes(base64.standard_b64encode(signature_blob)) |
| 530 | 530 |
|
| 531 | 531 |
@staticmethod |
| 532 | 532 |