Marco Ricci commited on 2024-11-13 21:18:07
Zeige 3 geänderte Dateien mit 74 Einfügungen und 10 Löschungen.
Pageant issues deterministic signatures for all key types. Support detecting whether we are running under Pageant (it supports the `list-extended@putty.projects.tartarus.org` extension request). When determining whether a key is suitable for use with the `vault` subsystem, also return success if we are running under Pageant. Detecting Pageant requires that we issue an SSH agent extension request, so also define the necessary enum constants.
... | ... |
@@ -569,6 +569,11 @@ class SSH_AGENTC(enum.Enum): # noqa: N801 |
569 | 569 |
Remove an (SSH2) identity. |
570 | 570 |
ADD_ID_CONSTRAINED: |
571 | 571 |
Add an (SSH2) identity, including key constraints. |
572 |
+ EXTENSION: |
|
573 |
+ Issue a named request that isn't part of the core agent |
|
574 |
+ protocol. Expecting [`SSH_AGENT.EXTENSION_RESPONSE`][] or |
|
575 |
+ [`SSH_AGENT.EXTENSION_FAILURE`][] if the named request is |
|
576 |
+ supported, [`SSH_AGENT.FAILURE`][] otherwise. |
|
572 | 577 |
|
573 | 578 |
""" |
574 | 579 |
|
... | ... |
@@ -582,6 +587,8 @@ class SSH_AGENTC(enum.Enum): # noqa: N801 |
582 | 587 |
"""""" |
583 | 588 |
ADD_ID_CONSTRAINED: int = 25 |
584 | 589 |
"""""" |
590 |
+ EXTENSION: int = 27 |
|
591 |
+ """""" |
|
585 | 592 |
|
586 | 593 |
|
587 | 594 |
class SSH_AGENT(enum.Enum): # noqa: N801 |
... | ... |
@@ -596,6 +603,10 @@ class SSH_AGENT(enum.Enum): # noqa: N801 |
596 | 603 |
Successful answer to [`SSH_AGENTC.REQUEST_IDENTITIES`][]. |
597 | 604 |
SIGN_RESPONSE: |
598 | 605 |
Successful answer to [`SSH_AGENTC.SIGN_REQUEST`][]. |
606 |
+ EXTENSION_FAILURE: |
|
607 |
+ Unsuccessful answer to [`SSH_AGENTC.EXTENSION`][]. |
|
608 |
+ EXTENSION_RESPONSE: |
|
609 |
+ Successful answer to [`SSH_AGENTC.EXTENSION`][]. |
|
599 | 610 |
|
600 | 611 |
""" |
601 | 612 |
|
... | ... |
@@ -607,3 +618,7 @@ class SSH_AGENT(enum.Enum): # noqa: N801 |
607 | 618 |
"""""" |
608 | 619 |
SIGN_RESPONSE: int = 14 |
609 | 620 |
"""""" |
621 |
+ EXTENSION_FAILURE: int = 28 |
|
622 |
+ """""" |
|
623 |
+ EXTENSION_RESPONSE: int = 29 |
|
624 |
+ """""" |
... | ... |
@@ -338,6 +338,25 @@ class SSHAgentClient: |
338 | 338 |
msg = f'invalid connection hint: {conn!r}' |
339 | 339 |
raise TypeError(msg) # noqa: DOC501 |
340 | 340 |
|
341 |
+ def has_deterministic_signatures(self) -> bool: |
|
342 |
+ """Check whether the agent returns deterministic signatures. |
|
343 |
+ |
|
344 |
+ Returns: |
|
345 |
+ True if a known agent was detected where signatures are |
|
346 |
+ deterministic for all SSH key types, false otherwise. |
|
347 |
+ |
|
348 |
+ Note: Known agents with deterministic signatures |
|
349 |
+ | agent | detected via | |
|
350 |
+ |:----------------|:--------------------------------------------------------------| |
|
351 |
+ | Pageant (PuTTY) | `list-extended@putty.projects.tartarus.org` extension request | |
|
352 |
+ |
|
353 |
+ """ # noqa: E501 |
|
354 |
+ returncode, _payload = self.request( |
|
355 |
+ _types.SSH_AGENTC.EXTENSION, |
|
356 |
+ self.string(b'list-extended@putty.projects.tartarus.org'), |
|
357 |
+ ) |
|
358 |
+ return returncode == _types.SSH_AGENT.SUCCESS.value |
|
359 |
+ |
|
341 | 360 |
@overload |
342 | 361 |
def request( # pragma: no cover |
343 | 362 |
self, |
... | ... |
@@ -18,6 +18,7 @@ from typing_extensions import TypeAlias, assert_type |
18 | 18 |
from derivepassphrase import sequin, ssh_agent |
19 | 19 |
|
20 | 20 |
if TYPE_CHECKING: |
21 |
+ import socket |
|
21 | 22 |
from collections.abc import Callable |
22 | 23 |
|
23 | 24 |
__author__ = 'Marco Ricci <software@the13thletter.info>' |
... | ... |
@@ -449,15 +450,20 @@ class Vault: |
449 | 450 |
def _is_suitable_ssh_key(key: bytes | bytearray, /) -> bool: |
450 | 451 |
"""Check whether the key is suitable for passphrase derivation. |
451 | 452 |
|
452 |
- Currently, this only checks whether signatures with this key |
|
453 |
- type are deterministic. |
|
453 |
+ Currently, this only statically checks whether signatures with |
|
454 |
+ this key type are guaranteed to be deterministic. |
|
454 | 455 |
|
455 | 456 |
Args: |
456 | 457 |
key: SSH public key to check. |
457 | 458 |
|
458 | 459 |
Returns: |
459 |
- True if and only if the key is suitable for use in deriving |
|
460 |
- a passphrase deterministically. |
|
460 |
+ True if and only if the key is guaranteed suitable for use in |
|
461 |
+ deriving a passphrase deterministically. |
|
462 |
+ |
|
463 |
+ Note: |
|
464 |
+ Some SSH agents additionally guarantee that all signatures |
|
465 |
+ are deterministic and thus *all* keys are suitable; see |
|
466 |
+ [`ssh_agent.SSHAgentClient.has_deterministic_signatures`][]. |
|
461 | 467 |
|
462 | 468 |
""" |
463 | 469 |
TestFunc: TypeAlias = 'Callable[[bytes | bytearray], bool]' |
... | ... |
@@ -472,21 +478,42 @@ class Vault: |
472 | 478 |
return any(v(key) for v in deterministic_signature_types.values()) |
473 | 479 |
|
474 | 480 |
@classmethod |
475 |
- def phrase_from_key(cls, key: bytes | bytearray, /) -> bytes: |
|
481 |
+ def phrase_from_key( |
|
482 |
+ cls, |
|
483 |
+ key: bytes | bytearray, |
|
484 |
+ /, |
|
485 |
+ *, |
|
486 |
+ conn: ssh_agent.SSHAgentClient | socket.socket | None = None, |
|
487 |
+ ) -> bytes: |
|
476 | 488 |
"""Obtain the master passphrase from a configured SSH key. |
477 | 489 |
|
478 | 490 |
vault allows the usage of certain SSH keys to derive a master |
479 | 491 |
passphrase, by signing the vault UUID with the SSH key. The key |
480 |
- type must ensure that signatures are deterministic. |
|
492 |
+ type or the SSH agent must ensure that signatures are |
|
493 |
+ deterministic. |
|
481 | 494 |
|
482 | 495 |
Args: |
483 |
- key: The (public) SSH key to use for signing. |
|
496 |
+ key: |
|
497 |
+ The (public) SSH key to use for signing. |
|
498 |
+ conn: |
|
499 |
+ An optional connection hint to the SSH agent. See |
|
500 |
+ [`ssh_agent.SSHAgentClient.ensure_agent_subcontext`][]. |
|
484 | 501 |
|
485 | 502 |
Returns: |
486 | 503 |
The signature of the vault UUID under this key, unframed but |
487 | 504 |
encoded in base64. |
488 | 505 |
|
489 | 506 |
Raises: |
507 |
+ KeyError: |
|
508 |
+ `conn` was `None`, and the `SSH_AUTH_SOCK` environment |
|
509 |
+ variable was not found. |
|
510 |
+ NotImplementedError: |
|
511 |
+ `conn` was `None`, and this Python does not support |
|
512 |
+ [`socket.AF_UNIX`][], so the SSH agent client cannot be |
|
513 |
+ automatically set up. |
|
514 |
+ OSError: |
|
515 |
+ `conn` was a socket or `None`, and there was an error |
|
516 |
+ setting up a socket connection to the agent. |
|
490 | 517 |
ValueError: |
491 | 518 |
The SSH key is principally unsuitable for this use case. |
492 | 519 |
Usually this means that the signature is not |
... | ... |
@@ -516,13 +543,16 @@ class Vault: |
516 | 543 |
True |
517 | 544 |
|
518 | 545 |
""" |
519 |
- if not cls._is_suitable_ssh_key(key): |
|
546 |
+ with ssh_agent.SSHAgentClient.ensure_agent_subcontext(conn) as client: |
|
547 |
+ if not ( |
|
548 |
+ client.has_deterministic_signatures() |
|
549 |
+ or cls._is_suitable_ssh_key(key) |
|
550 |
+ ): |
|
520 | 551 |
msg = ( |
521 | 552 |
'unsuitable SSH key: bad key, or ' |
522 |
- 'signature not deterministic' |
|
553 |
+ 'signature not deterministic under this agent' |
|
523 | 554 |
) |
524 | 555 |
raise ValueError(msg) |
525 |
- with ssh_agent.SSHAgentClient.ensure_agent_subcontext() as client: |
|
526 | 556 |
raw_sig = client.sign(key, cls._UUID) |
527 | 557 |
_keytype, trailer = ssh_agent.SSHAgentClient.unstring_prefix(raw_sig) |
528 | 558 |
signature_blob = ssh_agent.SSHAgentClient.unstring(trailer) |
529 | 559 |