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 |