Marco Ricci commited on 2026-08-31 14:53:33
Zeige 39 geänderte Dateien mit 3331 Einfügungen und 0 Löschungen.
Based on `derivepassphrase` commit 187a871b384a0b282f5bcfb0875d7d71b0f09afc, shortly after the 0.6.1 release and the 1.0 release of `derivepassphrase-sshagentsocketprovider`. Minor changes were made to keep the code runnable, e.g. restructuring the module contents, and removing references to other `derivepassphrase` code that is no longer included in this project.
| ... | ... |
@@ -0,0 +1 @@ |
| 1 |
+(See docs/ directory.) |
| ... | ... |
@@ -0,0 +1,13 @@ |
| 1 |
+zlib License |
|
| 2 |
+ |
|
| 3 |
+Copyright 2026 Marco Ricci <software@the13thletter.info> |
|
| 4 |
+ |
|
| 5 |
+This software is provided 'as-is', without any express or implied warranty. In no event will the authors be held liable for any damages arising from the use of this software. |
|
| 6 |
+ |
|
| 7 |
+Permission is granted to anyone to use this software for any purpose, including commercial applications, and to alter it and redistribute it freely, subject to the following restrictions: |
|
| 8 |
+ |
|
| 9 |
+ 1. The origin of this software must not be misrepresented; you must not claim that you wrote the original software. If you use this software in a product, an acknowledgment in the product documentation would be appreciated but is not required. |
|
| 10 |
+ |
|
| 11 |
+ 2. Altered source versions must be plainly marked as such, and must not be misrepresented as being the original software. |
|
| 12 |
+ |
|
| 13 |
+ 3. This notice may not be removed or altered from any source distribution. |
| ... | ... |
@@ -0,0 +1,35 @@ |
| 1 |
+# fakesshagent |
|
| 2 |
+ |
|
| 3 |
+[](https://pypi.org/project/fakesshagent) |
|
| 4 |
+[](https://pypi.org/project/fakesshagent) |
|
| 5 |
+ |
|
| 6 |
+A non-hardened implementation of the SSH agent protocol, for use in testing. |
|
| 7 |
+ |
|
| 8 |
+`fakesshagent` was originally written to test [`derivepassphrase`][DERIVEPASSPHRASE]'s SSH agent interactions. |
|
| 9 |
+As such, `fakesshagent` also registers as an abstract SSH agent socket provider for [`derivepassphrase`][DERIVEPASSPHRASE]. |
|
| 10 |
+(The implementation is based on memory buffers, not external I/O with sockets.) |
|
| 11 |
+ |
|
| 12 |
+In this first release, `fakesshagent` provides exactly the operations needed for use with `derivepassphrase`: |
|
| 13 |
+listing available keys, and deterministically signing a fixed message (if possible with this key type), for a set of known test keys. |
|
| 14 |
+See the wishlist for planned features of subsequent releases. |
|
| 15 |
+ |
|
| 16 |
+[DERIVEPASSPHRASE]: https://the13thletter.info/derivepassphrase/ |
|
| 17 |
+ |
|
| 18 |
+----- |
|
| 19 |
+ |
|
| 20 |
+## Installation |
|
| 21 |
+ |
|
| 22 |
+`fakesshagent` is a pure Python package, and may be easily installed with any `pip`-compatible Python package manager such as `pip`, `pipx`, or `uv`. |
|
| 23 |
+ |
|
| 24 |
+```console |
|
| 25 |
+pip install fakesshagent |
|
| 26 |
+``` |
|
| 27 |
+ |
|
| 28 |
+`fakesshagent` requires Python 3.9 or higher as well as the [typing-extensions package][TYPING_EXTENSIONS] and the [`derivepassphrase-sshagentsocketprovider` package][SASP] for its core functionality and programmatic interface. |
|
| 29 |
+ |
|
| 30 |
+[TYPING_EXTENSIONS]: https://pypi.org/project/typing-extensions/ |
|
| 31 |
+[SASP]: https://pypi.org/project/derivepassphrase-sshagentsocketprovider/ |
|
| 32 |
+ |
|
| 33 |
+## License |
|
| 34 |
+ |
|
| 35 |
+`fakesshagent` is distributed under the terms of the [zlib/libpng](https://spdx.org/licenses/Zlib.html) license. |
| ... | ... |
@@ -0,0 +1,12 @@ |
| 1 |
+{% extends "base.html" %}
|
|
| 2 |
+{% block analytics %}{% endblock %}
|
|
| 3 |
+{% block config %}{% endblock %}
|
|
| 4 |
+{% block libs %}{% endblock %}
|
|
| 5 |
+{% block scripts %}{% endblock %}
|
|
| 6 |
+{% block fonts %}
|
|
| 7 |
+ {% if config.theme.font != false %}
|
|
| 8 |
+ {% set text = config.theme.font.get("text", "Roboto") %}
|
|
| 9 |
+ {% set code = config.theme.font.get("code", "Roboto Mono") %}
|
|
| 10 |
+ <style>:root{--md-text-font:"{{ text }}";--md-code-font:"{{ code }}"}</style>
|
|
| 11 |
+ {% endif %}
|
|
| 12 |
+{% endblock %}
|
| ... | ... |
@@ -0,0 +1,28 @@ |
| 1 |
+{#-
|
|
| 2 |
+ This file is based on an automatically generated file from MkDocs-Material. |
|
| 3 |
+ |
|
| 4 |
+ Changes: |
|
| 5 |
+ |
|
| 6 |
+ - Some entries below the path docs/wishlist/... are actually from |
|
| 7 |
+ a submodule pointing to this same repository, but at the "wishlist" |
|
| 8 |
+ branch. |
|
| 9 |
+-#} |
|
| 10 |
+{% if page.edit_url %}
|
|
| 11 |
+ {% if "content.action.edit" in features %}
|
|
| 12 |
+ <a href="{{ page.edit_url | replace('master/docs/wishlist/', 'wishlist/') }}" title="{{ lang.t('action.edit') }}" class="md-content__button md-icon">
|
|
| 13 |
+ {% set icon = config.theme.icon.edit or "material/file-edit-outline" %}
|
|
| 14 |
+ {% include ".icons/" ~ icon ~ ".svg" %}
|
|
| 15 |
+ </a> |
|
| 16 |
+ {% endif %}
|
|
| 17 |
+ {% if "content.action.view" in features %}
|
|
| 18 |
+ {% if "/blob/" in page.edit_url %}
|
|
| 19 |
+ {% set part = "blob" %}
|
|
| 20 |
+ {% else %}
|
|
| 21 |
+ {% set part = "edit" %}
|
|
| 22 |
+ {% endif %}
|
|
| 23 |
+ <a href="{{ page.edit_url | replace(part, 'raw') | replace('master/docs/wishlist/', 'wishlist/') }}" title="{{ lang.t('action.view') }}" class="md-content__button md-icon">
|
|
| 24 |
+ {% set icon = config.theme.icon.view or "material/file-eye-outline" %}
|
|
| 25 |
+ {% include ".icons/" ~ icon ~ ".svg" %}
|
|
| 26 |
+ </a> |
|
| 27 |
+ {% endif %}
|
|
| 28 |
+{% endif %}
|
| ... | ... |
@@ -0,0 +1,20 @@ |
| 1 |
+{#-
|
|
| 2 |
+ This file was automatically generated - do not edit |
|
| 3 |
+-#} |
|
| 4 |
+<div class="md-copyright"> |
|
| 5 |
+ {% if config.copyright %}
|
|
| 6 |
+ <div class="md-copyright__highlight"> |
|
| 7 |
+ {{ config.copyright }}
|
|
| 8 |
+ </div> |
|
| 9 |
+ {% endif %}
|
|
| 10 |
+ {% if not config.extra.generator == false %}
|
|
| 11 |
+ Made with |
|
| 12 |
+ <a href="https://squidfunk.github.io/mkdocs-material/" target="_blank" rel="noopener"> |
|
| 13 |
+ Material for MkDocs |
|
| 14 |
+ </a> |
|
| 15 |
+ and |
|
| 16 |
+ <a href="https://mkdocstrings.github.io/python/" target="_blank" rel="noopener"> |
|
| 17 |
+ mkdocstrings-python |
|
| 18 |
+ </a> |
|
| 19 |
+ {% endif %}
|
|
| 20 |
+</div> |
| ... | ... |
@@ -0,0 +1,15 @@ |
| 1 |
+{#-
|
|
| 2 |
+ This file was automatically generated - do not edit |
|
| 3 |
+-#} |
|
| 4 |
+<!doctype html> |
|
| 5 |
+<html> |
|
| 6 |
+ <head> |
|
| 7 |
+ <meta charset="utf-8"> |
|
| 8 |
+ <meta name="viewport" content="width=device-width,initial-scale=1"> |
|
| 9 |
+ <title>{{ config.site_name }}</title>
|
|
| 10 |
+ <meta http-equiv="refresh" content="0;url={{ page.meta.location }}">
|
|
| 11 |
+ </head> |
|
| 12 |
+ <body> |
|
| 13 |
+ <div><a href="{{ page.meta.location | url }}">The document has moved.</a></div>
|
|
| 14 |
+ </body> |
|
| 15 |
+</html> |
| ... | ... |
@@ -0,0 +1,2 @@ |
| 1 |
+<!-- Originally downloaded from https://img.shields.io/badge/Keep-a-changelog-E05735?logoColor=fff&logo=keepachangelog&logoSize=auto on 2024-07-21. Used under CC0, as per https://github.com/badges/shields/blob/1e4c8f54d7743381e723a1ca3f79db7868c2f95e/LICENSE . --> |
|
| 2 |
+<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="127.21161107182145" height="20" role="img" aria-label="Keep a changelog"><title>Keep a changelog</title><linearGradient id="s" x2="0" y2="100%"><stop offset="0" stop-color="#bbb" stop-opacity=".1"/><stop offset="1" stop-opacity=".1"/></linearGradient><clipPath id="r"><rect width="127.21161107182145" height="20" rx="3" fill="#fff"/></clipPath><g clip-path="url(#r)"><rect width="0" height="20" fill="#555"/><rect x="0" width="127.21161107182145" height="20" fill="#e05735"/><rect width="127.21161107182145" height="20" fill="url(#s)"/></g><g fill="#fff" text-anchor="middle" font-family="Verdana,Geneva,DejaVu Sans,sans-serif" text-rendering="geometricPrecision" font-size="110"><image x="5" y="3" width="14.21161107182145" height="14" xlink:href="data:image/svg+xml;base64,PHN2ZyBmaWxsPSIjZmZmIiByb2xlPSJpbWciIHZpZXdCb3g9IjAgMCAyNCAyMy42NDI3NjMyMTAwOTQ4NjYiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PHRpdGxlPktlZXAgYSBDaGFuZ2Vsb2c8L3RpdGxlPjxwYXRoIGQ9Ik05LjgwMzEgMC4wMDAzQzkuMTA4MSAtMC4wMDQ3IDguNDEzMSAwLjA2MTMgNy43MjAxIDAuMjAxM0M1LjYzOTEgMC42MTgzIDMuOTc0MSAxLjY0OTMgMi43NzAxIDMuMjY2M0MxLjIzMjEgNS4zMzYzIDAuMzY3MSA3Ljg5MzMgMC4wNDkxIDExLjMxMzNDLTAuMDc1OSAxMi42NjYzIDAuMDU3MSAxNC4wMjQzIDAuMjMwMSAxNS41MjkzQzAuNDI4MyAxNy40MzczIDEuMTcyNyAxOS4yNDc3IDIuMzc0MSAyMC43NDMzQzMuMzA3MSAyMS45MDAzIDQuMzc4MSAyMi42NjAzIDUuNjQ2MSAyMy4wNjczQzYuNzI1MyAyMy40MTU2IDcuODQ4NiAyMy42MDg5IDguOTgyMSAyMy42NDEzQzkuMjQ3NiAyMy42NDcxIDkuNTEzMSAyMy42MzU4IDkuNzc3MSAyMy42MDczTDEwLjE5MzEgMjMuNTY2M0MxMC43Njc0IDIzLjUwOTYgMTEuMzQxMSAyMy40NDc2IDExLjkxNDEgMjMuMzgwM0MxNC40OTUgMjMuMDgyNyAxNy4wMDAxIDIyLjMxNzkgMTkuMzA3MSAyMS4xMjMzQzIxLjMxNDEgMjAuMDc1MyAyMi43MTcxIDE4LjUyOTMgMjMuNDc3MSAxNi41MjYzQzIzLjgzMTEgMTUuNTk2MyAyNC4wMDAxIDE0LjgxMzMgMjQuMDAwMSAxNC4wODEzQzI0IDEzLjgyNTkgMjMuOTc4NiAxMy41NzEgMjMuOTM2MSAxMy4zMTkzQzIzLjUzMTEgMTAuOTQwMyAyMi43MDExIDguODkxMyAyMS4zODExIDcuMDU1M0MxOS43NDcxIDQuNzc5MyAxOC4wOTMxIDMuMTc3MyAxNi4xNzkxIDIuMDEwM0MxMy45OTMxIDAuNjgxMyAxMS44OTAxIDAuMDE0MyA5LjgwMzEgMC4wMDEzWk05LjM2NDEgMS4wNjQzQzEwLjEzNDEgMS4wNDMzIDEwLjkyNDEgMS4xMjIzIDExLjc0MDEgMS4zMDEzSDExLjczOTFDMTQuNDY5MSAxLjg5ODMgMTYuODc2MSAzLjMwMzMgMTguODkzMSA1LjQ3NDNDMjEuMTgxMSA3LjkzNDMgMjIuNDg0MSAxMC41MTkzIDIyLjg4MTEgMTMuMzczM0MyMi45NTIxIDEzLjkwMTMgMjIuODY4MSAxNC40ODczIDIyLjc4NTEgMTQuOTk1M0MyMi4zOTcxIDE3LjMyMjMgMjEuMTIyMSAxOS4wNTgzIDE4Ljk5OTEgMjAuMTU1M0MxNi43NjA1IDIxLjMwNjIgMTQuMzMzMyAyMi4wNDUzIDExLjgzMzEgMjIuMzM3M0MxMS4yNzUxIDIyLjQwMzMgMTAuNzEzMSAyMi40NjAzIDEwLjA3NjEgMjIuNTI0M0w5Ljk4ODEgMjIuNTM0M0M5LjQzNjMgMjIuNTkxNSA4Ljg3OTUgMjIuNTgxNSA4LjMzMDEgMjIuNTA0M0M3LjY0MDEgMjIuNDA2MyA2Ljg1ODEgMjIuMjk0MyA2LjEwNjEgMjIuMTE1M0M0LjMzOTEgMjEuNjk1MyAzLjAzNzEgMjAuNDkzMyAyLjEyODEgMTguNDU3M0MxLjQ0NDEgMTYuOTE5MyAxLjA5MDEgMTUuMjEyMyAxLjA0NzEgMTMuMjI0M0MwLjk4NjUgMTAuOTA1MiAxLjM3NjcgOC41OTY1IDIuMTk2MSA2LjQyNjNDMi44MTYxIDQuNzg1MyAzLjcxMzEgMi45NzMzIDUuNjU4MSAyLjAwODNDNi44MDg3IDEuNDI0MSA4LjA3NDMgMS4xMDE3IDkuMzY0MSAxLjA2NDNaTTEwLjQ3MjEgMi41MzQzQzkuNzA5MyAyLjU1OTggOC45NTQ1IDIuNjk4IDguMjMyMSAyLjk0NDNDNi42MzIxIDMuNDY5MyA1LjUxNDEgNC4yNDgzIDQuNzA5MSA1LjM4MjNDMy4yMTAxIDcuNTAwMyAyLjQzNDEgOS45NjIzIDIuMzk5MSAxMi43MDczQzIuMzM3MiAxNC4yMTE0IDIuNjExNSAxNS43MTA1IDMuMjAyMSAxNy4wOTUzQzQuMjgzMSAxOS42NjQzIDYuMjU1MSAyMS4xNTkzIDguOTA5MSAyMS40MjEzQzExLjcwNTEgMjEuNjkxMyAxNC40NTIxIDIxLjA0MDMgMTcuMDg3MSAxOS40ODQzQzE4LjIwNjUgMTguODQyMiAxOS4xNTY3IDE3Ljk0MjcgMTkuODU5MSAxNi44NjAzQzIwLjYxMzIgMTUuNzEzNCAyMS4wMTg4IDE0LjM3MjcgMjEuMDI3MSAxMy4wMDAzQzIxLjAyNzEgMTAuODM4MyAyMC4wMTIxIDguNTMwMyAxNy45NzgxIDYuMTM4M0MxNy43NjIyIDUuODg0NiAxNy41MjY4IDUuNjQ4MyAxNy4yNzQxIDUuNDMxM0MxNC45NDExIDMuNDI1MyAxMi43MDcxIDIuNDY1MyAxMC40NzIxIDIuNTMzM1pNMTAuNTcxMSAzLjU2OTNDMTIuMDYzMSAzLjU0NzMgMTMuNTg2MSA0LjA0NDMgMTUuMjQzMSA1LjA3NzNDMTcuMDI2MSA2LjE5NDMgMTguNDA0MSA3Ljg0MDMgMTkuMzM0MSA5Ljk4NjNDMjAuMjIxMSAxMi4wMDYzIDIwLjIzOTEgMTMuOTI0MyAxOS4zOTIxIDE1LjY4MjNDMTguNzU5NCAxNi45NTc3IDE3Ljc0MjcgMTguMDAyNyAxNi40ODUxIDE4LjY3MDNDMTQuMjMyMSAxOS45MDIzIDEyLjE3MDEgMjAuNDQ2MyAxMC4wMjcxIDIwLjM4MzNIMTAuMDA1MUM4LjQ1NTEgMjAuNDQzMyA3LjExNTEgMjAuMDYzMyA1LjkwNzEgMTkuMjIwM0M1LjIwNzEgMTguNzMyMyA0LjY2NjEgMTguMDE4MyA0LjI1MjEgMTcuMDM3M0MzLjY3NzYgMTUuNjg1OSAzLjM4MTEgMTQuMjMyNyAzLjM4MDEgMTIuNzY0M0MzLjM5NzkgMTEuMDk0OSAzLjc0NTkgOS40NDU1IDQuNDA0MSA3LjkxMTNDNC42NTQgNy4zNDkzIDQuOTQ2MiA2LjgwNyA1LjI3ODEgNi4yODkzTDUuNDQxMSA2LjAxOTNDNi4xMTExIDQuOTExMyA3LjE2NjEgNC4zODAzIDguNTQ0MSAzLjkyMDNDOS4xOTggMy43MDA4IDkuODgxNiAzLjU4MjggMTAuNTcxMSAzLjU3MDNaTTEwLjYwMTEgNS4xODkzQzkuNDIxMSA1LjIyNjMgOC4zMjExIDUuNjU0MyA3LjMxMjEgNi40NzIzQzUuOTE3MSA3LjYwMjMgNS4wODIxIDkuMTkyMyA0LjY4MjEgMTEuNDY1M0M0LjM5MjcgMTMuMDM1NCA0LjYzMDQgMTQuNjU3MiA1LjM1ODEgMTYuMDc4M0M2LjQyMDEgMTguMTMyMyA4LjE1ODEgMTkuMTg4MyAxMC4zODQxIDE5LjEyODNIMTAuMzkyMUMxMy4zNTAxIDE4Ljk3MzMgMTUuNTAyMSAxNy44NzkzIDE2Ljk2NTEgMTUuNzgxM0MxNy42NDczIDE0LjgzNjEgMTguMDE3OCAxMy43MDE4IDE4LjAyNTEgMTIuNTM2M0MxOC4wMjMxIDExLjk4MTIgMTcuOTQxNiAxMS40MjkyIDE3Ljc4MzEgMTAuODk3M0MxNy4wNjMxIDguNDYyMyAxNS42MjIxIDYuNzUyMyAxMy40OTMxIDUuODEyM0MxMi40ODQxIDUuMzY3MyAxMS41MTkxIDUuMTYwMyAxMC42MDExIDUuMTg4M1pNMTAuOTEzMSA2LjI1MzNDMTEuNjgzMSA2LjI2MTMgMTIuNDYwMSA2LjQ0NDMgMTMuMjYzMSA2LjgwMzNDMTUuMTI1MSA3LjYzNjMgMTYuMzM2MSA5LjEzNTMgMTYuODYxMSAxMS4yNjUzQzE3LjM4NzEgMTMuMzk1MyAxNi41OTExIDE1LjQ3MDMgMTQuNzk1MSAxNi42ODgzQzEzLjMxNjEgMTcuNjg3MyAxMS44NjUxIDE4LjEzNTMgMTAuMzU3MSAxOC4wNTUzSDEwLjMxNzFDOS4zMTMxIDE4LjEzNTMgOC4zMjMxIDE3LjgyOTMgNy4zNzQxIDE3LjE0NTNDNi43ODIgMTYuNzEzNyA2LjMzMjcgMTYuMTE0NiA2LjA4NDEgMTUuNDI1M0M1Ljc0NzggMTQuNTU1MyA1LjU3MzYgMTMuNjMxIDUuNTcwMSAxMi42OTgzQzUuNTgzOSAxMS41MzIyIDUuODM4MiAxMC4zODE1IDYuMzE3MSA5LjMxODNDNi45OTMxIDcuNzYyMyA4LjA0MTEgNi44MTUzIDkuNTMyMSA2LjQyNTNDOS45OTIxIDYuMzA1MyAxMC40NTExIDYuMjQ3MyAxMC45MTIxIDYuMjUzM1pNOS40NTUxIDguNTc5M1YxNS40NjEzSDEwLjYxMDFWMTMuNTUzM0wxMi4yOTAxIDE1LjQ2MTNIMTMuNzUyMUwxMS40NzkxIDEyLjk2MTNMMTMuMzUwMSAxMS4wMDMzSDExLjg0MTFMMTAuNjEwMSAxMi4zNDAzVjguNTc5M1oiLz48L3N2Zz4="/><text aria-hidden="true" x="727.1161107182145" y="150" fill="#010101" fill-opacity=".3" transform="scale(.1)" textLength="990">Keep a changelog</text><text x="727.1161107182145" y="140" transform="scale(.1)" fill="#fff" textLength="990">Keep a changelog</text></g></svg> |
| ... | ... |
@@ -0,0 +1,2 @@ |
| 1 |
+<!-- Originally downloaded from https://img.shields.io/badge/SemVer-3F4551?logoColor=fff&logo=semver&logoSize=auto on 2024-07-21. Used under CC0, as per https://github.com/badges/shields/blob/1e4c8f54d7743381e723a1ca3f79db7868c2f95e/LICENSE . --> |
|
| 2 |
+<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="71.00018479842694" height="20" role="img" aria-label="SemVer"><title>SemVer</title><linearGradient id="s" x2="0" y2="100%"><stop offset="0" stop-color="#bbb" stop-opacity=".1"/><stop offset="1" stop-opacity=".1"/></linearGradient><clipPath id="r"><rect width="71.00018479842694" height="20" rx="3" fill="#fff"/></clipPath><g clip-path="url(#r)"><rect width="0" height="20" fill="#555"/><rect x="0" width="71.00018479842694" height="20" fill="#3f4551"/><rect width="71.00018479842694" height="20" fill="url(#s)"/></g><g fill="#fff" text-anchor="middle" font-family="Verdana,Geneva,DejaVu Sans,sans-serif" text-rendering="geometricPrecision" font-size="110"><image x="5" y="3" width="14.00018479842695" height="14" xlink:href="data:image/svg+xml;base64,PHN2ZyBmaWxsPSJ3aGl0ZXNtb2tlIiByb2xlPSJpbWciIHZpZXdCb3g9IjAgMCAyNCAyNC4wMDAzNzU5MjkyNjAxNTUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PHRpdGxlPlNlbVZlcjwvdGl0bGU+PHBhdGggZD0iTTAuMzU3IDkuMDI0NkMtMC41OTQzIDEyLjg0ODcgMC4zODExIDE2Ljg5NjUgMi45NyAxOS44Njc2QzUuNTU4IDIyLjgzNjkgOS40MzM5IDI0LjM1NCAxMy4zNSAyMy45MzA2QzIxLjExOCAyMy4yMjc2IDI2LjQzNiAxNC4xMzE2IDIyLjg2NyA3LjEzMDZDMjIuNDUxIDUuOTQwNiAyMC43OTcgNi43NjI2IDIwLjk2NCA3LjcyNjZDMjEuMjUxIDguNDI2NiAyMS40OSA5LjE0NzYgMjEuNjc3IDkuODgxNkMyMi41MzI1IDEzLjc3NTcgMjAuOTg5IDE3LjgwNTQgMTcuNzUxIDIwLjEzMTZDMTIuNzA2NiAyMy43NTI1IDUuNjE0OSAyMS45MjgyIDIuOTQ0IDE2LjMyMjZDMS4yMjY3IDEyLjcyNDMgMS44MTQ4IDguNDQ5NCA0LjQ0IDUuNDQ4NkM1LjcyNDEgMy45ODE0IDcuNDEwNCAyLjkyMyA5LjI5IDIuNDA0NkMxMS42Mjc4IDEuNzQzNSAxNC4xMjc0IDEuOTY4MiAxNi4zMSAzLjAzNTZDMTYuNDY2OCAzLjExNTIgMTYuNTM0NyAzLjMwMzEgMTYuNDY1IDMuNDY0NkwxMi41MDMgMTQuMDg0NkMxMi4zOTYgMTQuODk0NiAxMS44MTMgMTQuODcwNiAxMS43MDYgMTQuMDg0Nkw5LjMyNiA2LjcxNDZDOS4yMTY1IDYuMjkyMiA4LjkzNjYgNS45MzQ0IDguNTUzIDUuNzI2NkM3LjM2MyA1LjE2NjYgNS40NiA2LjM5MzYgNi4xNzQgNy44ODE2TDEwLjA4OCAxOC4zMjI2QzEwLjYxMiAxOS43MTU2IDExLjExMSAyMC4xNTY2IDEyLjE0NiAyMC4xNTY2UzEzLjY4MSAxOS43MTY2IDE0LjIwNCAxOC4zMjI2TDIwIDMuOTQwNkMyMC4xNTU5IDMuNTA2IDIwLjAwNTQgMy4wMjA3IDE5LjYzMSAyLjc1MDZDMTMuMSAtMi45MDY0IDIuMzIgMC42NDE2IDAuMzU3IDkuMDIzNloiLz48L3N2Zz4="/><text aria-hidden="true" x="445.0018479842695" y="150" fill="#010101" fill-opacity=".3" transform="scale(.1)" textLength="430">SemVer</text><text x="445.0018479842695" y="140" transform="scale(.1)" fill="#fff" textLength="430">SemVer</text></g></svg> |
| ... | ... |
@@ -0,0 +1,53 @@ |
| 1 |
+# Changelog for `fakesshagent` |
|
| 2 |
+ |
|
| 3 |
+[![Keeping a changelog][CHANGELOG_BADGE]][KEEP_A_CHANGELOG] |
|
| 4 |
+[![Using Semantic Versioning][SEMVER_BADGE]][SEMANTIC_VERSIONING] |
|
| 5 |
+ |
|
| 6 |
+ [CHANGELOG_BADGE]: Keep_a_changelog-E05735.svg |
|
| 7 |
+ [SEMVER_BADGE]: SemVer-3F4551.svg |
|
| 8 |
+ [KEEP_A_CHANGELOG]: https://keepachangelog.com/en/1.1.0/ 'Keeping a changelog' |
|
| 9 |
+ [SEMANTIC_VERSIONING]: https://semver.org/ 'Using Semantic Versioning' |
|
| 10 |
+ |
|
| 11 |
+??? info "Definition: the <q>public API</q> of `fakesshagent`" |
|
| 12 |
+ |
|
| 13 |
+ The <dfn>public API</dfn>, as defined by Semantic Versioning, is |
|
| 14 |
+ outlined in the Reference section: the set of documented |
|
| 15 |
+ modules, classes, attributes, methods, functions and function |
|
| 16 |
+ parameters, and the documented behavior, options and arguments of the |
|
| 17 |
+ command-line tools. |
|
| 18 |
+ |
|
| 19 |
+ Certain **exceptions to this rule** are explicitly and prominently |
|
| 20 |
+ marked as implementation details/not part of the public API. |
|
| 21 |
+ |
|
| 22 |
+??? info "Interpretation of the version number" |
|
| 23 |
+ |
|
| 24 |
+ The terminology <b>major</b>, <b>minor</b> and <b>patch</b> follows the |
|
| 25 |
+ Semantic Versioning and Keep a Changelog definitions. |
|
| 26 |
+ |
|
| 27 |
+ * For version numbers with major version zero, *any* new release may |
|
| 28 |
+ effectively constitute a new <b>major</b> release. |
|
| 29 |
+ * For version numbers with major version one or higher, |
|
| 30 |
+ * <b>Fixed</b> entries justify a <b>patch</b> release; |
|
| 31 |
+ * <b>Added</b> and <b>Deprecated</b> entries justify |
|
| 32 |
+ a <b>minor</b> release; |
|
| 33 |
+ * <b>Changed</b> and <b>Removed</b> entries justify a <b>major</b> |
|
| 34 |
+ release. |
|
| 35 |
+ * <b>Security</b> can justify any type of release; if <b>major</b> |
|
| 36 |
+ or <b>minor</b>, these are accompanied by corresponding entries |
|
| 37 |
+ of the respective types above. |
|
| 38 |
+ |
|
| 39 |
+!!! abstract inline end "Legend: symbols" |
|
| 40 |
+ |
|
| 41 |
+ <ul style="list-style-type: none;"> |
|
| 42 |
+ <li>:material-bug: --- a related bug entry from the wishlist page |
|
| 43 |
+ <li>:material-file-document-plus-outline: --- a related wishlist entry from the wishlist page |
|
| 44 |
+ </ul> |
|
| 45 |
+ |
|
| 46 |
+<aside markdown><small> |
|
| 47 |
+(All entries are from the perspective of a user of the program or the API. |
|
| 48 |
+As an exception, entries partaining to developers of `fakesshagent` are |
|
| 49 |
+specifically marked as such.) |
|
| 50 |
+</small></aside> |
|
| 51 |
+ |
|
| 52 |
+<!-- scriv changelog start --> |
|
| 53 |
+ |
| ... | ... |
@@ -0,0 +1,26 @@ |
| 1 |
+# How to comply with the "altered versions" clause of the license |
|
| 2 |
+ |
|
| 3 |
+!!! abstract inline end "Local version identifiers reserved by upstream `fakesshagent`" |
|
| 4 |
+ |
|
| 5 |
+ * `jvm` -- Indicates a build of `fakesshagent` running on the Java Virtual Machine. |
|
| 6 |
+ |
|
| 7 |
+**Short answer:** change the package name and/or include a [PEP 440][] "local version identifier" in the version number. |
|
| 8 |
+If `fakesshagent` ever rebrands, this applies to rebranded names too. |
|
| 9 |
+We try not to clash with anyone else, and will yank our offending releases if we do. |
|
| 10 |
+ |
|
| 11 |
+**Long answer:** We, upstream `fakesshagent`, reserve the name `fakesshagent` and certain version numbers for ourselves. |
|
| 12 |
+Specifically, our version numbers adhere to [PEP 440][] (or newer revisions) and generally do not include a "local version identifier" (except for the reserved ones mentioned in the info box). |
|
| 13 |
+To mark an altered version, we thus recommend that you change the software package name `fakesshagent`, or use a version number with a different local version identifier. |
|
| 14 |
+If we (upstream) decide to use a new local version identifier, we will avoid all clashing local version identifiers we are aware of, and if informed of a clashing local version identifier after our release, will yank our offending version(s). |
|
| 15 |
+ |
|
| 16 |
+Should we (upstream) change the package name, we shall apply the same guidelines and checks concerning local version identifiers to the new package name. |
|
| 17 |
+A change of package name does *not* by itself imply permission to use the old package name for future releases of altered versions without marking them. |
|
| 18 |
+ |
|
| 19 |
+--- |
|
| 20 |
+ |
|
| 21 |
+See also [the zlib project's take on how to mark altered versions (question #24)][ZLIB_FAQ]. |
|
| 22 |
+Like them, we recommend keeping our upstream Changelog (up to the point where you introduced modifications) and describing your modifications both there and in the README, in the appropriate level of detail. |
|
| 23 |
+We also request (but do not require) that you provide clear instructions in the README (and potentially other suitable places) on where and how to report problems that stem from your modifications, not from the upstream software package. |
|
| 24 |
+ |
|
| 25 |
+[PEP 440]: https://peps.python.org/pep-0440/ |
|
| 26 |
+[ZLIB_FAQ]: https://github.com/madler/zlib/blob/v1.3.1/FAQ |
| ... | ... |
@@ -0,0 +1 @@ |
| 1 |
+--8<-- "README.md" |
| ... | ... |
@@ -0,0 +1,35 @@ |
| 1 |
+/* Indentation. */ |
|
| 2 |
+div.doc-contents:not(.first) {
|
|
| 3 |
+ padding-left: 25px; |
|
| 4 |
+ border-left: .05rem solid var(--md-typeset-table-color); |
|
| 5 |
+} |
|
| 6 |
+ |
|
| 7 |
+/* Mark external links as such. */ |
|
| 8 |
+div[data-md-component="content"] a:not(.md-icon):is([href^="https://"], [href^="http://"], [href^="//"])::after, |
|
| 9 |
+a.external::after, |
|
| 10 |
+a.autorefs-external::after {
|
|
| 11 |
+ /* https://primer.style/octicons/arrow-up-right-24 */ |
|
| 12 |
+ mask-image: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M18.25 15.5a.75.75 0 00.75-.75v-9a.75.75 0 00-.75-.75h-9a.75.75 0 000 1.5h7.19L6.22 16.72a.75.75 0 101.06 1.06L17.5 7.56v7.19c0 .414.336.75.75.75z"></path></svg>');
|
|
| 13 |
+ -webkit-mask-image: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M18.25 15.5a.75.75 0 00.75-.75v-9a.75.75 0 00-.75-.75h-9a.75.75 0 000 1.5h7.19L6.22 16.72a.75.75 0 101.06 1.06L17.5 7.56v7.19c0 .414.336.75.75.75z"></path></svg>');
|
|
| 14 |
+ content: ' '; |
|
| 15 |
+ |
|
| 16 |
+ display: inline-block; |
|
| 17 |
+ vertical-align: middle; |
|
| 18 |
+ position: relative; |
|
| 19 |
+ |
|
| 20 |
+ height: 1em; |
|
| 21 |
+ width: 1em; |
|
| 22 |
+ background-color: currentColor; |
|
| 23 |
+} |
|
| 24 |
+ |
|
| 25 |
+/* ... but don't mark badges as external */ |
|
| 26 |
+div[data-md-component="content"] a:not(.md-icon):is([href^="https://"], [href^="http://"], [href^="//"]):has(img[src^="https://img.shields.io/"], img[src="Keep_a_changelog-E05735.svg"], img[src="../Keep_a_changelog-E05735.svg"], img[src="SemVer-3F4551.svg"], img[src="../SemVer-3F4551.svg"])::after {
|
|
| 27 |
+ content: ''; |
|
| 28 |
+ width: 0em; |
|
| 29 |
+ height: 0em; |
|
| 30 |
+} |
|
| 31 |
+ |
|
| 32 |
+a.external:hover::after, |
|
| 33 |
+a.autorefs-external:hover::after {
|
|
| 34 |
+ background-color: var(--md-accent-fg-color); |
|
| 35 |
+} |
| ... | ... |
@@ -0,0 +1,87 @@ |
| 1 |
+# Compatibility of `fakesshagent` with different Python versions |
|
| 2 |
+ |
|
| 3 |
+## Python 3.15 {#py3.15}
|
|
| 4 |
+ |
|
| 5 |
+Currently supported without any known issues. |
|
| 6 |
+ |
|
| 7 |
+## Python 3.14 {#py3.14}
|
|
| 8 |
+ |
|
| 9 |
+Currently supported without any known issues. |
|
| 10 |
+ |
|
| 11 |
+### After end-of-life {#after-eol-py3.14}
|
|
| 12 |
+ |
|
| 13 |
+After Python 3.14 reaches end-of-life, future `fakesshagent` versions may make use of the following Python functionality: |
|
| 14 |
+ |
|
| 15 |
+ * lazy imports |
|
| 16 |
+ * the `frozendict` type |
|
| 17 |
+ |
|
| 18 |
+The declared minimum required Python version will then be updated accordingly. |
|
| 19 |
+ |
|
| 20 |
+## Python 3.13 {#py3.13}
|
|
| 21 |
+ |
|
| 22 |
+Currently supported without any known issues. |
|
| 23 |
+ |
|
| 24 |
+### After end-of-life {#after-eol-py3.13}
|
|
| 25 |
+ |
|
| 26 |
+After Python 3.13 reaches end-of-life, future `fakesshagent` versions may make use of the following Python functionality: |
|
| 27 |
+ |
|
| 28 |
+ * template strings |
|
| 29 |
+ |
|
| 30 |
+The declared minimum required Python version will then be updated accordingly. |
|
| 31 |
+ |
|
| 32 |
+## Python 3.12 {#py3.12}
|
|
| 33 |
+ |
|
| 34 |
+Currently supported without any known issues. |
|
| 35 |
+ |
|
| 36 |
+### After end-of-life {#after-eol-py3.12}
|
|
| 37 |
+ |
|
| 38 |
+Python 3.13 does not offer significantly different functionality than Python 3.12. |
|
| 39 |
+There are thus no plans to make use of additional functionality once Python 3.12 reaches end-of-life. |
|
| 40 |
+ |
|
| 41 |
+## Python 3.11 {#py3.11}
|
|
| 42 |
+ |
|
| 43 |
+Currently supported without any known issues. |
|
| 44 |
+ |
|
| 45 |
+### After end-of-life {#after-eol-py3.11}
|
|
| 46 |
+ |
|
| 47 |
+After Python 3.11 reaches end-of-life, future `fakesshagent` versions may make use of the following Python functionality: |
|
| 48 |
+ |
|
| 49 |
+ * complex expressions in f-strings |
|
| 50 |
+ * type parameter syntax and the `type` statement |
|
| 51 |
+ |
|
| 52 |
+The declared minimum required Python version will then be updated accordingly. |
|
| 53 |
+ |
|
| 54 |
+## Python 3.10 {#py3.10}
|
|
| 55 |
+ |
|
| 56 |
+Currently supported without any known issues. |
|
| 57 |
+ |
|
| 58 |
+### After end-of-life {#after-eol-py3.10}
|
|
| 59 |
+ |
|
| 60 |
+After Python 3.10 reaches end-of-life, future `fakesshagent` versions may make use of the following Python functionality: |
|
| 61 |
+ |
|
| 62 |
+ * exception groups |
|
| 63 |
+ * exception notes |
|
| 64 |
+ * [`contextlib.chdir`][] |
|
| 65 |
+ |
|
| 66 |
+The declared minimum required Python version will be then updated accordingly. |
|
| 67 |
+ |
|
| 68 |
+## Python 3.9 {#py3.9}
|
|
| 69 |
+ |
|
| 70 |
+End-of-life since October 2025, but still supported at least until `fakesshagent` v1.0, without any known issues. |
|
| 71 |
+ |
|
| 72 |
+### After end-of-life {#after-eol-py3.9}
|
|
| 73 |
+ |
|
| 74 |
+After Python 3.9 reaches end-of-life, future `fakesshagent` versions may make use of the following Python functionality: |
|
| 75 |
+ |
|
| 76 |
+ * structural pattern matching (`match`/`case` blocks) |
|
| 77 |
+ * parenthesized `with` statements |
|
| 78 |
+ |
|
| 79 |
+The declared minimum required Python version will be then updated accordingly. |
|
| 80 |
+ |
|
| 81 |
+## Python 3.8 and below {#py3.8-and-below}
|
|
| 82 |
+ |
|
| 83 |
+These versions were never explicitly supported, neither in CPython nor in PyPy. |
|
| 84 |
+ |
|
| 85 |
+## PyPy |
|
| 86 |
+ |
|
| 87 |
+As per the respective CPython version above. |
| ... | ... |
@@ -0,0 +1 @@ |
| 1 |
+Subproject commit 892cce5eece34e7f06e98348540f8491d94d69fe |
| ... | ... |
@@ -0,0 +1,94 @@ |
| 1 |
+p:has(+ table#bug-summary) {
|
|
| 2 |
+ margin: 0em; |
|
| 3 |
+} |
|
| 4 |
+table#bug-summary {
|
|
| 5 |
+ width: 100%; |
|
| 6 |
+ border: none; |
|
| 7 |
+} |
|
| 8 |
+table#bug-summary + p {
|
|
| 9 |
+ margin: 0em; |
|
| 10 |
+} |
|
| 11 |
+table#bug-summary th, table#bug-summary td {
|
|
| 12 |
+ padding: 0.25em; |
|
| 13 |
+ border: none; |
|
| 14 |
+} |
|
| 15 |
+table#bug-summary td:first-of-type {
|
|
| 16 |
+ padding-right: 1.5em; |
|
| 17 |
+} |
|
| 18 |
+table#bug-summary td:first-of-type > a:has(+ a) {
|
|
| 19 |
+ margin-right: 0.5em; |
|
| 20 |
+} |
|
| 21 |
+ |
|
| 22 |
+a.scheduled-next {
|
|
| 23 |
+ font-weight: bold; |
|
| 24 |
+} |
|
| 25 |
+ |
|
| 26 |
+a.scheduled {
|
|
| 27 |
+ font-style: oblique; |
|
| 28 |
+} |
|
| 29 |
+ |
|
| 30 |
+a.unscheduled {
|
|
| 31 |
+ opacity: 0.75; |
|
| 32 |
+} |
|
| 33 |
+a.fixed {
|
|
| 34 |
+ font-weight: normal; |
|
| 35 |
+ font-style: normal; |
|
| 36 |
+ text-decoration: line-through; |
|
| 37 |
+} |
|
| 38 |
+ |
|
| 39 |
+.md-typeset div.admonition.abstract .emojione, |
|
| 40 |
+.md-typeset div.admonition.abstract .gemoji, |
|
| 41 |
+.md-typeset div.admonition.abstract .twemoji, |
|
| 42 |
+.md-typeset a[href^="../wishlist/"] .emojione, |
|
| 43 |
+.md-typeset a[href^="../wishlist/"] .gemoji, |
|
| 44 |
+.md-typeset a[href^="../wishlist/"] .twemoji, |
|
| 45 |
+.md-typeset a[href^="wishlist/"] .emojione, |
|
| 46 |
+.md-typeset a[href^="wishlist/"] .gemoji, |
|
| 47 |
+.md-typeset a[href^="wishlist/"] .twemoji {
|
|
| 48 |
+ vertical-align: middle; |
|
| 49 |
+} |
|
| 50 |
+ |
|
| 51 |
+:root {
|
|
| 52 |
+ --md-admonition-icon--bug: url('data:image/svg+xml;charset=utf-8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M14 12h-4v-2h4m0 6h-4v-2h4m6-6h-2.81a6 6 0 0 0-1.82-1.96L17 4.41 15.59 3l-2.17 2.17a6 6 0 0 0-2.83 0L8.41 3 7 4.41l1.62 1.63C7.88 6.55 7.26 7.22 6.81 8H4v2h2.09c-.05.33-.09.66-.09 1v1H4v2h2v1c0 .34.04.67.09 1H4v2h2.81c1.04 1.79 2.97 3 5.19 3s4.15-1.21 5.19-3H20v-2h-2.09c.05-.33.09-.66.09-1v-1h2v-2h-2v-1c0-.34-.04-.67-.09-1H20z"/></svg>');
|
|
| 53 |
+ --md-admonition-icon--bug-success: url('data:image/svg+xml;charset=utf-8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M19 7h-2.81c-.45-.8-1.07-1.5-1.82-2L16 3.41 14.59 2l-2.17 2.17a6 6 0 0 0-2.83 0L7.41 2 6 3.41 7.62 5c-.75.5-1.36 1.21-1.81 2H3v2h2.09c-.06.33-.09.66-.09 1v1H3v2h2v1c0 .34.03.67.09 1H3v2h2.81A6 6 0 0 0 13 19.65V19c0-2.54 1.61-4.8 4-5.65V13h2v-2h-2v-1c0-.34-.03-.67-.09-1H19zm-6 8H9v-2h4zm0-4H9V9h4zm4.75 11.16-2.75-3L16.16 18l1.59 1.59L21.34 16l1.16 1.41z"/></svg>');
|
|
| 54 |
+ --md-admonition-icon--wish: url('data:image/svg+xml;charset=utf-8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M23 18h-3v-3h-2v3h-3v2h3v3h2v-3h3M6 2a2 2 0 0 0-2 2v16c0 1.11.89 2 2 2h7.81c-.36-.62-.61-1.3-.73-2H6V4h7v5h5v4.08c.33-.05.67-.08 1-.08.34 0 .67.03 1 .08V8l-6-6M8 12v2h8v-2m-8 4v2h5v-2Z"/></svg>');
|
|
| 55 |
+ --md-admonition-icon--wish-success: url('data:image/svg+xml;charset=utf-8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="m23.5 17-5 5-3.5-3.5 1.5-1.5 2 2 3.5-3.5zM6 2a2 2 0 0 0-2 2v16c0 1.11.89 2 2 2h7.81c-.36-.62-.61-1.3-.73-2H6V4h7v5h5v4.08c.33-.05.67-.08 1-.08.34 0 .67.03 1 .08V8l-6-6M8 12v2h8v-2m-8 4v2h5v-2Z"/></svg>');
|
|
| 56 |
+} |
|
| 57 |
+.md-typeset .admonition.wish, |
|
| 58 |
+.md-typeset details.wish {
|
|
| 59 |
+ border-color: #888888; |
|
| 60 |
+} |
|
| 61 |
+.md-typeset .wish > .admonition-title, |
|
| 62 |
+.md-typeset .wish > summary {
|
|
| 63 |
+ background-color: #8888881a; |
|
| 64 |
+} |
|
| 65 |
+.md-typeset .wish > .admonition-title::before, |
|
| 66 |
+.md-typeset .wish > summary::before {
|
|
| 67 |
+ background-color: #888888; |
|
| 68 |
+ -webkit-mask-image: var(--md-admonition-icon--wish); |
|
| 69 |
+ mask-image: var(--md-admonition-icon--wish); |
|
| 70 |
+} |
|
| 71 |
+.md-typeset .admonition.bug-success, |
|
| 72 |
+.md-typeset details.bug-success, |
|
| 73 |
+.md-typeset .admonition.wish-success, |
|
| 74 |
+.md-typeset details.wish-success {
|
|
| 75 |
+ border-color: #00c853; |
|
| 76 |
+} |
|
| 77 |
+.md-typeset .bug-success > .admonition-title, |
|
| 78 |
+.md-typeset .bug-success > summary, |
|
| 79 |
+.md-typeset .wish-success > .admonition-title, |
|
| 80 |
+.md-typeset .wish-success > summary {
|
|
| 81 |
+ background-color: #00c8531a; |
|
| 82 |
+} |
|
| 83 |
+.md-typeset .bug-success > .admonition-title::before, |
|
| 84 |
+.md-typeset .bug-success > summary::before {
|
|
| 85 |
+ background-color: #00c853; |
|
| 86 |
+ -webkit-mask-image: var(--md-admonition-icon--bug-success); |
|
| 87 |
+ mask-image: var(--md-admonition-icon--bug-success); |
|
| 88 |
+} |
|
| 89 |
+.md-typeset .wish-success > .admonition-title::before, |
|
| 90 |
+.md-typeset .wish-success > summary::before {
|
|
| 91 |
+ background-color: #00c853; |
|
| 92 |
+ -webkit-mask-image: var(--md-admonition-icon--wish-success); |
|
| 93 |
+ mask-image: var(--md-admonition-icon--wish-success); |
|
| 94 |
+} |
| ... | ... |
@@ -0,0 +1,148 @@ |
| 1 |
+site_name: fakesshagent |
|
| 2 |
+site_url: 'https://the13thletter.info/fakesshagent/' |
|
| 3 |
+repo_url: 'https://git.schokokeks.org/fakesshagent.git' |
|
| 4 |
+edit_uri: 'raw/master/docs/' |
|
| 5 |
+repo_name: the-13th-letter/fakesshagent |
|
| 6 |
+site_description: A non-hardened implementation of the SSH agent protocol, for use in testing. |
|
| 7 |
+site_author: Marco Ricci |
|
| 8 |
+copyright: Copyright © 2026 Marco Ricci (the-13th-letter) |
|
| 9 |
+ |
|
| 10 |
+remote_branch: documentation-tree |
|
| 11 |
+remote_name: '.' |
|
| 12 |
+ |
|
| 13 |
+docs_dir: docs |
|
| 14 |
+site_dir: html |
|
| 15 |
+extra_css: |
|
| 16 |
+ - mkdocstrings_recommended_styles.css |
|
| 17 |
+ - wishlist_styling.css |
|
| 18 |
+ |
|
| 19 |
+theme: |
|
| 20 |
+ name: "material" |
|
| 21 |
+ #font: false |
|
| 22 |
+ font: |
|
| 23 |
+ text: Noto Sans |
|
| 24 |
+ code: Noto Mono |
|
| 25 |
+ #code: Inconsolata |
|
| 26 |
+ #palette: |
|
| 27 |
+ # - media: "(prefers-color-scheme)" |
|
| 28 |
+ # toggle: |
|
| 29 |
+ # icon: material/brightness-auto |
|
| 30 |
+ # name: Switch to light mode |
|
| 31 |
+ # - media: "(prefers-color-scheme: light)" |
|
| 32 |
+ # scheme: default |
|
| 33 |
+ # toggle: |
|
| 34 |
+ # icon: material/brightness-7 |
|
| 35 |
+ # name: Switch to dark mode |
|
| 36 |
+ # - media: "(prefers-color-scheme: dark)" |
|
| 37 |
+ # scheme: slate |
|
| 38 |
+ # toggle: |
|
| 39 |
+ # icon: material/brightness-4 |
|
| 40 |
+ # name: Switch to system preference |
|
| 41 |
+ icon: |
|
| 42 |
+ logo: material/library |
|
| 43 |
+ repo: fontawesome/brands/git-alt |
|
| 44 |
+ features: |
|
| 45 |
+ - content.action.view |
|
| 46 |
+ - content.code.annotate |
|
| 47 |
+ - navigation.tracking |
|
| 48 |
+ - navigation.tabs |
|
| 49 |
+ - navigation.sections |
|
| 50 |
+ - navigation.indexes |
|
| 51 |
+ - navigation.footer |
|
| 52 |
+ custom_dir: docs-overrides |
|
| 53 |
+ |
|
| 54 |
+plugins: |
|
| 55 |
+ offline: |
|
| 56 |
+ enabled: false # Use mkdocs_offline.yml for offline versions instead! |
|
| 57 |
+ #privacy: |
|
| 58 |
+ # enabled: true |
|
| 59 |
+ mkdocstrings: |
|
| 60 |
+ handlers: |
|
| 61 |
+ python: |
|
| 62 |
+ inventories: |
|
| 63 |
+ - 'https://docs.python.org/3/objects.inv' |
|
| 64 |
+ - 'https://typing-extensions.readthedocs.io/en/latest/objects.inv' |
|
| 65 |
+ #- 'https://cryptography.readthedocs.io/en/latest/objects.inv' |
|
| 66 |
+ #- 'https://hypothesis.readthedocs.io/en/latest/objects.inv' |
|
| 67 |
+ options: |
|
| 68 |
+ docstring_options: |
|
| 69 |
+ ignore_init_summary: true |
|
| 70 |
+ returns_multiple_items: false |
|
| 71 |
+ merge_init_into_class: true |
|
| 72 |
+ show_source: false |
|
| 73 |
+ heading_level: 2 |
|
| 74 |
+ show_object_full_path: false |
|
| 75 |
+ show_root_members_full_path: false |
|
| 76 |
+ show_root_heading: true |
|
| 77 |
+ show_symbol_type_heading: true |
|
| 78 |
+ show_symbol_type_toc: true |
|
| 79 |
+ members_order: 'source' |
|
| 80 |
+ show_signature_annotations: true |
|
| 81 |
+ separate_signature: true |
|
| 82 |
+ signature_crossrefs: true |
|
| 83 |
+ unwrap_annotated: true |
|
| 84 |
+ scoped_crossrefs: true |
|
| 85 |
+ paths: |
|
| 86 |
+ - src |
|
| 87 |
+ - . |
|
| 88 |
+ mike: |
|
| 89 |
+ alias_type: symlink |
|
| 90 |
+ canonical_version: '0.x' |
|
| 91 |
+ version_selector: false |
|
| 92 |
+ |
|
| 93 |
+nav: |
|
| 94 |
+ - Overview: index.md |
|
| 95 |
+ #- Tutorials & Examples: |
|
| 96 |
+ # - tutorials/index.md |
|
| 97 |
+ #- How-Tos: |
|
| 98 |
+ # - how-tos/index.md |
|
| 99 |
+ #- Reference: |
|
| 100 |
+ # - reference/index.md |
|
| 101 |
+ - Design & Background: |
|
| 102 |
+ - explanation/index.md |
|
| 103 |
+ - '"altered versions" license requirement': explanation/faq-altered-versions.md |
|
| 104 |
+ - Changelog: |
|
| 105 |
+ - Changelog: changelog.md |
|
| 106 |
+ - Upgrade notes: upgrade-notes.md |
|
| 107 |
+ - Python compatibility: pycompatibility.md |
|
| 108 |
+ - Wishlist: |
|
| 109 |
+ - wishlist/index.md |
|
| 110 |
+not_in_nav: | |
|
| 111 |
+ wishlist/*.md |
|
| 112 |
+ !wishlist/index.md |
|
| 113 |
+draft_docs: | |
|
| 114 |
+ changelog.d |
|
| 115 |
+ _future.md |
|
| 116 |
+ reference/tests.* |
|
| 117 |
+ _release_checklist.md |
|
| 118 |
+ |
|
| 119 |
+markdown_extensions: |
|
| 120 |
+ - abbr |
|
| 121 |
+ - attr_list |
|
| 122 |
+ - def_list |
|
| 123 |
+ - footnotes |
|
| 124 |
+ - md_in_html |
|
| 125 |
+ - admonition |
|
| 126 |
+ - smarty |
|
| 127 |
+ - toc: |
|
| 128 |
+ permalink: true |
|
| 129 |
+ - pymdownx.caret |
|
| 130 |
+ - pymdownx.details |
|
| 131 |
+ - pymdownx.emoji: |
|
| 132 |
+ emoji_index: !!python/name:material.extensions.emoji.twemoji |
|
| 133 |
+ emoji_generator: !!python/name:material.extensions.emoji.to_svg |
|
| 134 |
+ - pymdownx.snippets: |
|
| 135 |
+ base_path: |
|
| 136 |
+ - 'docs' |
|
| 137 |
+ - '.' |
|
| 138 |
+ check_paths: true |
|
| 139 |
+ - pymdownx.superfences |
|
| 140 |
+ - pymdownx.tabbed: |
|
| 141 |
+ alternate_style: true |
|
| 142 |
+ |
|
| 143 |
+extra: |
|
| 144 |
+ version: |
|
| 145 |
+ provider: mike |
|
| 146 |
+ default: |
|
| 147 |
+ - latest |
|
| 148 |
+ - stable |
| ... | ... |
@@ -0,0 +1,478 @@ |
| 1 |
+[build-system] |
|
| 2 |
+requires = ["hatchling"] |
|
| 3 |
+build-backend = "hatchling.build" |
|
| 4 |
+ |
|
| 5 |
+[project] |
|
| 6 |
+name = "fakesshagent" |
|
| 7 |
+description = "A non-hardened implementation of the SSH agent protocol, for use in testing." |
|
| 8 |
+readme = "README.md" |
|
| 9 |
+version = "0.1a1" |
|
| 10 |
+requires-python = ">= 3.9" |
|
| 11 |
+license = "Zlib" |
|
| 12 |
+keywords = [] |
|
| 13 |
+authors = [ |
|
| 14 |
+ { name = "Marco Ricci", email = "software@the13thletter.info" },
|
|
| 15 |
+] |
|
| 16 |
+classifiers = [ |
|
| 17 |
+ "Development Status :: 4 - Beta", |
|
| 18 |
+ "Environment :: Console", |
|
| 19 |
+ "Operating System :: OS Independent", |
|
| 20 |
+ "Programming Language :: Python :: 3", |
|
| 21 |
+ "Programming Language :: Python :: 3.9", |
|
| 22 |
+ "Programming Language :: Python :: 3.10", |
|
| 23 |
+ "Programming Language :: Python :: 3.11", |
|
| 24 |
+ "Programming Language :: Python :: 3.12", |
|
| 25 |
+ "Programming Language :: Python :: 3.13", |
|
| 26 |
+ "Programming Language :: Python :: 3.14", |
|
| 27 |
+ "Programming Language :: Python :: 3.15", |
|
| 28 |
+ "Programming Language :: Python :: Implementation :: CPython", |
|
| 29 |
+ "Programming Language :: Python :: Implementation :: PyPy", |
|
| 30 |
+ "Topic :: Software Development :: Testing", |
|
| 31 |
+ "Typing :: Typed", |
|
| 32 |
+] |
|
| 33 |
+dependencies = [ |
|
| 34 |
+ "derivepassphrase-sshagentsocketprovider >= 1.0", |
|
| 35 |
+ "typing_extensions", |
|
| 36 |
+] |
|
| 37 |
+ |
|
| 38 |
+[project.optional-dependencies] |
|
| 39 |
+dev = [ |
|
| 40 |
+ # dev - developers' setup |
|
| 41 |
+ # |
|
| 42 |
+ # Development uses the hatch build system, to isolate all tools in |
|
| 43 |
+ # their own virtual environment... |
|
| 44 |
+ "hatch >= 1.10", |
|
| 45 |
+ # ...but it is still sensible to have most static analysis tools |
|
| 46 |
+ # available in the main virtual environment as well, so that your |
|
| 47 |
+ # editor/IDE setup can access them as well. |
|
| 48 |
+ "mypy >= 1.0", |
|
| 49 |
+ "ruff >= 0.16.0", |
|
| 50 |
+ # Test dependencies should be installed as well, to have static |
|
| 51 |
+ # analysis in the test suite working too. |
|
| 52 |
+ # |
|
| 53 |
+ # `packaging` is needed because the test suite compares version |
|
| 54 |
+ # numbers. |
|
| 55 |
+ # |
|
| 56 |
+ # `pytest-xdist` is used by `hatch test -p`, but for the `psutil` |
|
| 57 |
+ # extra, the `psutil` package provides no wheels for PyPy. Users on |
|
| 58 |
+ # The Annoying OS are especially screwed if they lack the admin |
|
| 59 |
+ # priviledges necessary to install the developers' tools. So skip |
|
| 60 |
+ # the extra, and accept that for PyPy environments, `hatch test -p` |
|
| 61 |
+ # will not be able to distinguish logical and physical CPU core |
|
| 62 |
+ # counts. |
|
| 63 |
+ # |
|
| 64 |
+ # We use `hypothesis.is_hypothesis_test`, which was introduced in |
|
| 65 |
+ # 6.131.0, and `exc_type` for `pytest.importorskip`, which was |
|
| 66 |
+ # introduced in 8.2.0. |
|
| 67 |
+ "coverage[toml] >= 7.4", |
|
| 68 |
+ "coverage-enable-subprocess >= 1.0", |
|
| 69 |
+ "hypothesis >= 6.131.0", |
|
| 70 |
+ "packaging", |
|
| 71 |
+ "pytest >= 8.2", |
|
| 72 |
+ "pytest-randomly >= 3.15", |
|
| 73 |
+ "pytest-xdist >= 3.6.0", |
|
| 74 |
+ 'pytest-xdist[psutil] >= 3.6.0; platform_python_implementation != "PyPy"', |
|
| 75 |
+ # `pytest` 8.4 and higher, and `hypothesis` versions 6.130.13 |
|
| 76 |
+ # through 6.135.1, use syntax only properly available on Python 3.10 |
|
| 77 |
+ # and later: `with (x as y, a as b)`. However, those versions are |
|
| 78 |
+ # not marked as incompatible with Python 3.9. |
|
| 79 |
+ 'pytest < 8.4 ; python_version < "3.10"', |
|
| 80 |
+ 'hypothesis != 6.130.13, != 6.131.*, != 6.132.*, != 6.133.*, != 6.134.*, != 6.135.0, != 6.135.1 ; python_version < "3.10"', |
|
| 81 |
+ # `hypothesis` requires a Rust toolchain since version 6.156, and |
|
| 82 |
+ # publishes wheels for PyPy 3.11 only. PyO3 version 0.27 also dropped |
|
| 83 |
+ # support for PyPy 3.9 and 3.10, and cannot be overridden within |
|
| 84 |
+ # pyproject.toml. |
|
| 85 |
+ 'hypothesis < 6.156 ; platform_python_implementation == "PyPy" and python_version < "3.11"', |
|
| 86 |
+] |
|
| 87 |
+ |
|
| 88 |
+[project.urls] |
|
| 89 |
+Documentation = "https://the13thletter.info/fakesshagent/" |
|
| 90 |
+Issues = "https://the13thletter.info/fakesshagent/latest/wishlist/" |
|
| 91 |
+Source = "https://git.schokokeks.org/fakesshagent.git" |
|
| 92 |
+ |
|
| 93 |
+[project.entry-points."derivepassphrase.ssh_agent_socket_providers"] |
|
| 94 |
+stub_agent = "fakesshagent.machinery: STUB_AGENT_ENTRY_POINT" |
|
| 95 |
+stub_agent_with_address = "fakesshagent.machinery: STUB_AGENT_WITH_ADDRESS_ENTRY_POINT" |
|
| 96 |
+stub_agent_with_address_and_deterministic_dsa = "fakesshagent.machinery: STUB_AGENT_WITH_ADDRESS_AND_DETERMINISTIC_DSA_ENTRY_POINT" |
|
| 97 |
+ |
|
| 98 |
+[tool.coverage.html] |
|
| 99 |
+directory = "html/coverage" |
|
| 100 |
+ |
|
| 101 |
+[tool.coverage.paths] |
|
| 102 |
+src = ["src"] |
|
| 103 |
+tests = ["tests"] |
|
| 104 |
+ |
|
| 105 |
+[tool.coverage.report] |
|
| 106 |
+skip_covered = false |
|
| 107 |
+skip_empty = true |
|
| 108 |
+precision = 3 |
|
| 109 |
+partial_branches = [ |
|
| 110 |
+ 'pragma: no branch', |
|
| 111 |
+] |
|
| 112 |
+exclude_also = [ |
|
| 113 |
+ "if __name__ == .__main__.:", |
|
| 114 |
+ 'if (?:typing\.)?TYPE_CHECKING:', |
|
| 115 |
+ "raise AssertionError", |
|
| 116 |
+ "raise NotImplementedError", |
|
| 117 |
+ 'assert False', |
|
| 118 |
+ '(?:typing\.)?assert_never\(',
|
|
| 119 |
+ '@overload', |
|
| 120 |
+ 'class .*\(Protocol\):', |
|
| 121 |
+ 'pytest\.fail\(',
|
|
| 122 |
+ '@(?:(?:pytest\.)?mark\.)?xfail\(',
|
|
| 123 |
+ 'class _Omitted:', |
|
| 124 |
+ 'class _DummyModule:', |
|
| 125 |
+] |
|
| 126 |
+ |
|
| 127 |
+# We use a bunch of "codes", similar to the "type: ignore" comments, as |
|
| 128 |
+# shorthand notation for why we are excluding a certain piece of code |
|
| 129 |
+# manually from coverage, instead of writing a verbose explanation at each |
|
| 130 |
+# call site: |
|
| 131 |
+# |
|
| 132 |
+# "external-api" and "unused": The code is defined by an external API not |
|
| 133 |
+# under our control, or it implements an external interface, and some of the |
|
| 134 |
+# branches of that code we do not yet use ("unused") or don't ever intend to
|
|
| 135 |
+# use ("external-api"). In particular, in production and in testing use, we
|
|
| 136 |
+# do not expect to hit these code branches unless there are errors in our |
|
| 137 |
+# code. |
|
| 138 |
+# |
|
| 139 |
+# "external": Whether or not we hit this code branch is non-deterministic, |
|
| 140 |
+# and dependent on external factors that are difficult or impossible for us |
|
| 141 |
+# to control, predict, or simulate. We therefore categorically exclude all |
|
| 142 |
+# branches from coverage. (Typically though, one particular branch *will* |
|
| 143 |
+# actually run. We just don't know ahead of time which one.) (While this |
|
| 144 |
+# applies to "choice of operating system" and "installed Python/library |
|
| 145 |
+# version" as well, these cases are usually tagged with more specific codes, |
|
| 146 |
+# or explained in long-form commentary.) |
|
| 147 |
+# |
|
| 148 |
+# "failsafe": Akin to assertions, these are used to catch "impossible" |
|
| 149 |
+# situations and to still fail in a controlled manner. We do not expect to |
|
| 150 |
+# hit these code branches during production or testing. |
|
| 151 |
+# |
|
| 152 |
+# "debug", "internal" and "interactive": These code branches are not |
|
| 153 |
+# intended to be called by the general public ("debug", "internal"), or they
|
|
| 154 |
+# are only intended to be called interactively ("interactive"). We do not
|
|
| 155 |
+# expect to hit these code branches during production or testing. |
|
| 156 |
+ |
|
| 157 |
+[tool.coverage.run] |
|
| 158 |
+source_pkgs = ["fakesshagent", "tests"] |
|
| 159 |
+branch = true |
|
| 160 |
+parallel = true |
|
| 161 |
+omit = [ |
|
| 162 |
+ "__main__.py", |
|
| 163 |
+] |
|
| 164 |
+dynamic_context = 'test_function' |
|
| 165 |
+concurrency = ['thread', 'multiprocessing'] |
|
| 166 |
+sigterm = true |
|
| 167 |
+relative_files = true |
|
| 168 |
+ |
|
| 169 |
+[tool.hatch.build.targets.sdist] |
|
| 170 |
+exclude = [ |
|
| 171 |
+ 'docs/changelog.d/*.md', |
|
| 172 |
+] |
|
| 173 |
+ |
|
| 174 |
+[tool.hatch.env] |
|
| 175 |
+requires = [ |
|
| 176 |
+ "hatch-mkdocs", |
|
| 177 |
+] |
|
| 178 |
+ |
|
| 179 |
+[tool.hatch.env.collectors.mkdocs.docs] |
|
| 180 |
+path = "mkdocs.yml" |
|
| 181 |
+ |
|
| 182 |
+[tool.hatch.envs.docs] |
|
| 183 |
+extra-dependencies = [ |
|
| 184 |
+ # Our documentation uses formatted function signatures (i.e. with |
|
| 185 |
+ # formatted type annotations), which requires `black`. |
|
| 186 |
+ "black", |
|
| 187 |
+ # We actually use functionality from MkDocs 1.6, so ensure the |
|
| 188 |
+ # version is at least this high. |
|
| 189 |
+ "mkdocs >= 1.6", |
|
| 190 |
+ # Our changelog is assembled from singular entries, orchestrated by |
|
| 191 |
+ # `scriv`. |
|
| 192 |
+ "scriv >= 1.4", |
|
| 193 |
+] |
|
| 194 |
+detached = false |
|
| 195 |
+ |
|
| 196 |
+[tool.hatch.envs.hatch-check-code] |
|
| 197 |
+config-path = "/dev/null" |
|
| 198 |
+dependencies = [ |
|
| 199 |
+ # Override the default dependencies, lest the ruff requirements |
|
| 200 |
+ # conflict. |
|
| 201 |
+] |
|
| 202 |
+features = [ |
|
| 203 |
+ "dev", |
|
| 204 |
+] |
|
| 205 |
+[tool.hatch.envs.hatch-check-fmt] |
|
| 206 |
+config-path = "/dev/null" |
|
| 207 |
+dependencies = [ |
|
| 208 |
+ # Override the default dependencies, lest the ruff requirements |
|
| 209 |
+ # conflict. |
|
| 210 |
+] |
|
| 211 |
+features = [ |
|
| 212 |
+ "dev", |
|
| 213 |
+] |
|
| 214 |
+[tool.hatch.envs.hatch-check-types] |
|
| 215 |
+config-path = "/dev/null" |
|
| 216 |
+dependencies = [ |
|
| 217 |
+ # Override the default dependencies, lest the ruff requirements |
|
| 218 |
+ # conflict. |
|
| 219 |
+ "pyrefly >= 1.0.0", |
|
| 220 |
+] |
|
| 221 |
+features = [ |
|
| 222 |
+ "dev", |
|
| 223 |
+] |
|
| 224 |
+ |
|
| 225 |
+[tool.hatch.envs.hatch-test] |
|
| 226 |
+default-args = ['src', 'tests'] |
|
| 227 |
+dependencies = [ |
|
| 228 |
+ "coverage[toml] >= 7.4", |
|
| 229 |
+ "coverage-enable-subprocess >= 1.0", |
|
| 230 |
+ "hypothesis >= 6.131.0", |
|
| 231 |
+ "packaging", |
|
| 232 |
+ "pytest >= 8.2", |
|
| 233 |
+ "pytest-randomly >= 3.15", |
|
| 234 |
+ "pytest-xdist >= 3.6.0", |
|
| 235 |
+ 'pytest-xdist[psutil] >= 3.6.0; platform_python_implementation != "PyPy"', |
|
| 236 |
+ 'pytest < 8.4 ; python_version < "3.10"', |
|
| 237 |
+ 'hypothesis != 6.130.13, != 6.131.*, != 6.132.*, != 6.133.*, != 6.134.*, != 6.135.0, != 6.135.1 ; python_version < "3.10"', |
|
| 238 |
+ 'hypothesis < 6.156 ; platform_python_implementation == "PyPy" and python_version < "3.11"', |
|
| 239 |
+] |
|
| 240 |
+matrix-name-format = '{variable}_{value}'
|
|
| 241 |
+ |
|
| 242 |
+[[tool.hatch.envs.hatch-test.matrix]] |
|
| 243 |
+python = ["3.15", "3.14", "3.13", "3.12", "3.11", "3.10", "3.9", "pypy3.11", "pypy3.10", "pypy3.9"] |
|
| 244 |
+parser-version = ["PEG"] |
|
| 245 |
+ |
|
| 246 |
+[[tool.hatch.envs.hatch-test.matrix]] |
|
| 247 |
+python = ["3.9", "pypy3.9"] |
|
| 248 |
+parser-version = ["LL1"] |
|
| 249 |
+ |
|
| 250 |
+[tool.hatch.envs.hatch-test.overrides] |
|
| 251 |
+matrix.parser-version.env-vars = [ |
|
| 252 |
+ { key = "PYTHONOLDPARSER", value = "1", if = ["LL1"] },
|
|
| 253 |
+] |
|
| 254 |
+ |
|
| 255 |
+[tool.hatch.envs.hatch-test.scripts] |
|
| 256 |
+run = "pytest --hypothesis-profile={env:HYPOTHESIS_PROFILE:default}{env:HATCH_TEST_ARGS:} {args}"
|
|
| 257 |
+run-cov = "coverage run -m pytest --hypothesis-profile={env:HYPOTHESIS_PROFILE:default}{env:HATCH_TEST_ARGS:} {args}"
|
|
| 258 |
+cov-combine = "coverage combine" |
|
| 259 |
+cov-report = "coverage report" |
|
| 260 |
+ |
|
| 261 |
+[tool.hatch.envs.types] |
|
| 262 |
+extra-dependencies = [ |
|
| 263 |
+ "mypy>=1.0.0", |
|
| 264 |
+] |
|
| 265 |
+[tool.hatch.envs.types.scripts] |
|
| 266 |
+check = "mypy --install-types --non-interactive {args:src/fakesshagent tests}"
|
|
| 267 |
+ |
|
| 268 |
+[tool.mypy] |
|
| 269 |
+files = ['src/**/*.py', 'tests/**/*.py'] |
|
| 270 |
+mypy_path = '$MYPY_CONFIG_FILE_DIR/src:$MYPY_CONFIG_FILE_DIR/other-stubs' |
|
| 271 |
+explicit_package_bases = true |
|
| 272 |
+implicit_reexport = false |
|
| 273 |
+sqlite_cache = true |
|
| 274 |
+enable_error_code = ['ignore-without-code'] |
|
| 275 |
+ |
|
| 276 |
+[tool.pyrefly] |
|
| 277 |
+project-includes = [ |
|
| 278 |
+ "src/**/*.py", |
|
| 279 |
+ "tests/**/*.py", |
|
| 280 |
+] |
|
| 281 |
+search-path = [ |
|
| 282 |
+ "src", |
|
| 283 |
+ ".", |
|
| 284 |
+] |
|
| 285 |
+preset = "legacy" |
|
| 286 |
+infer-return-types = "never" |
|
| 287 |
+permissive-ignores = true |
|
| 288 |
+ |
|
| 289 |
+[tool.pyrefly.errors] |
|
| 290 |
+unnecessary-type-conversion = "ignore" |
|
| 291 |
+ |
|
| 292 |
+[tool.pytest.ini_options] |
|
| 293 |
+addopts = '--doctest-modules --dist=loadgroup --import-mode=importlib' |
|
| 294 |
+pythonpath = [ |
|
| 295 |
+ 'tests', # needed for re-importing test machinery with multiprocessing (spawn) |
|
| 296 |
+] |
|
| 297 |
+testpaths = [ |
|
| 298 |
+ 'src', # doctests in API documentation |
|
| 299 |
+ 'tests', # other tests |
|
| 300 |
+] |
|
| 301 |
+xfail_strict = true |
|
| 302 |
+ |
|
| 303 |
+[tool.ruff] |
|
| 304 |
+line-length = 79 |
|
| 305 |
+src = ["src"] |
|
| 306 |
+ |
|
| 307 |
+[tool.ruff.format] |
|
| 308 |
+docstring-code-format = true |
|
| 309 |
+docstring-code-line-length = "dynamic" |
|
| 310 |
+preview = true |
|
| 311 |
+quote-style = "double" |
|
| 312 |
+ |
|
| 313 |
+[tool.ruff.lint] |
|
| 314 |
+ignore = [ |
|
| 315 |
+ # Suggested ignore by ruff when also using ruff to format. We *do* |
|
| 316 |
+ # check for E501, because this usually only happens when there is |
|
| 317 |
+ # a text string that should be manually broken. However, for |
|
| 318 |
+ # automated quality control, that specific check is turned off so |
|
| 319 |
+ # that the linting and formatting can proceed, and then *afterwards* |
|
| 320 |
+ # the files are re-linted. We hope that any *true* E501 errors left |
|
| 321 |
+ # over from this *are* text strings that should be manually broken. |
|
| 322 |
+ 'tab-indentation', 'indentation-with-invalid-multiple', |
|
| 323 |
+ 'indentation-with-invalid-multiple-comment', 'over-indented', |
|
| 324 |
+ 'docstring-tab-indentation', 'triple-single-quotes', |
|
| 325 |
+ 'bad-quotes-inline-string', 'bad-quotes-multiline-string', |
|
| 326 |
+ 'bad-quotes-docstring', 'avoidable-escaped-quote', |
|
| 327 |
+ 'missing-trailing-comma', 'prohibited-trailing-comma', |
|
| 328 |
+ 'single-line-implicit-string-concatenation', |
|
| 329 |
+ 'multi-line-implicit-string-concatenation', |
|
| 330 |
+ # We use `assert` regularly to appease the type checker, and because |
|
| 331 |
+ # it is the right language tool for this job. |
|
| 332 |
+ 'assert', |
|
| 333 |
+ # The formatter takes care of trailing commas and docstring code |
|
| 334 |
+ # automatically. |
|
| 335 |
+ 'missing-trailing-comma', 'doc-line-too-long', |
|
| 336 |
+ # We document transitive exceptions as well (if we feel they would |
|
| 337 |
+ # be surprising to the user otherwise). |
|
| 338 |
+ 'docstring-extraneous-exception', |
|
| 339 |
+ # We currently don't have issues for every TODO. Forcing an issue |
|
| 340 |
+ # also goes against the philosophy of TODOs as low-overhead markers |
|
| 341 |
+ # for future work; see |
|
| 342 |
+ # https://gist.github.com/dmnd/ed5d8ef8de2e4cfea174bd5dafcda382 . |
|
| 343 |
+ 'missing-todo-link', |
|
| 344 |
+ # We somewhat regularly use loops where each iteration needs |
|
| 345 |
+ # a separate try-except block. |
|
| 346 |
+ 'try-except-in-loop', |
|
| 347 |
+ # We do not currently use pathlib. The PTH rules are unselected, |
|
| 348 |
+ # but FURB includes several pathlib-related rules. |
|
| 349 |
+ 'read-whole-file', 'write-whole-file', |
|
| 350 |
+ # We catch type-ignore comments without specific code via the mypy |
|
| 351 |
+ # configuration, not via ruff. |
|
| 352 |
+ 'blanket-type-ignore', |
|
| 353 |
+ # Provisionally ignore the `__init__.py` rule, until we can adapt our |
|
| 354 |
+ # codebase. |
|
| 355 |
+ 'non-empty-init-module', |
|
| 356 |
+ # We prefer noqa comments over ruff-specific ignores, for tool |
|
| 357 |
+ # compatibility reasons, even if the comments themselves become harder |
|
| 358 |
+ # to read. Using them in the ruff config section is fine thought, |
|
| 359 |
+ # because that *is* ruff-specific. |
|
| 360 |
+ 'noqa-comments', |
|
| 361 |
+ # Still undecided about this one. |
|
| 362 |
+ 'too-many-statements-in-try-clause', |
|
| 363 |
+] |
|
| 364 |
+preview = true |
|
| 365 |
+# We select here in the order of presentation on the ruff documentation |
|
| 366 |
+# website. ruff default selection (v0.6.2) is merely E4, E7, E9 and F. |
|
| 367 |
+select = [ |
|
| 368 |
+ 'F', 'E', 'W', 'C90', 'I', 'N', 'D', 'UP', 'YTT', |
|
| 369 |
+ 'ANN', 'ASYNC', 'S', 'BLE', 'FBT', 'B', 'A', 'COM', |
|
| 370 |
+ 'CPY', 'C4', 'DTZ', 'T10', 'DJ', 'EM', 'EXE', 'FA', |
|
| 371 |
+ 'ISC', 'ICN', 'LOG', 'G', 'INP', 'PIE', 'T20', 'PYI', |
|
| 372 |
+ 'PT', 'Q', 'RET', 'SLF', 'SLOT', 'SIM', 'TID', 'TC', |
|
| 373 |
+ 'INT', 'ARG', 'PTH', 'TD', |
|
| 374 |
+ # We use TODOs and FIXMEs as notes for later, and don't want the |
|
| 375 |
+ # linter to nag about every occurrence. Disable 'FIX'. |
|
| 376 |
+ # |
|
| 377 |
+ # The "eradicate" rule is prone to a lot of false positives, and it |
|
| 378 |
+ # is unclear to me, and probably confusing to read, where to apply |
|
| 379 |
+ # a noqa marker. Instead, disable 'ERA', and if necessary, specify |
|
| 380 |
+ # it on the command-line. |
|
| 381 |
+ 'PD', 'PGH', 'PL', 'TRY', 'FLY', 'NPY', 'FAST', |
|
| 382 |
+ 'AIR', 'PERF', 'FURB', 'DOC', 'RUF', |
|
| 383 |
+] |
|
| 384 |
+ |
|
| 385 |
+[tool.ruff.lint.per-file-ignores] |
|
| 386 |
+"**/scripts/*" = [ |
|
| 387 |
+ # Suggested by hatch. |
|
| 388 |
+ 'INP', |
|
| 389 |
+ # Suggested by hatch. |
|
| 390 |
+ 'T20', |
|
| 391 |
+] |
|
| 392 |
+"**/tests/**/*" = [ |
|
| 393 |
+ # Suggested by hatch, assumingly because it may be important to |
|
| 394 |
+ # verify that the value is exactly the empty string, and not just |
|
| 395 |
+ # any falsy value. |
|
| 396 |
+ 'compare-to-empty-string', |
|
| 397 |
+ # Suggested by hatch, assumingly because tests may use "magic |
|
| 398 |
+ # values". |
|
| 399 |
+ 'magic-value-comparison', |
|
| 400 |
+ # Suggested by hatch, because tests are typically organized as |
|
| 401 |
+ # classes and instance methods but may not really be using the |
|
| 402 |
+ # `self` argument. |
|
| 403 |
+ 'no-self-use', |
|
| 404 |
+ # Suggested by hatch, because these warnings may be precisely what |
|
| 405 |
+ # the tests are supposed to test. |
|
| 406 |
+ 'S', |
|
| 407 |
+ # Suggested by hatch, because pytest-style tests conventionally |
|
| 408 |
+ # import code from each other via relative imports. |
|
| 409 |
+ 'relative-imports', |
|
| 410 |
+ # Our tests regularly use arguments named `input` to store an input |
|
| 411 |
+ # (text-/byte-)string. |
|
| 412 |
+ 'builtin-argument-shadowing', |
|
| 413 |
+ # We regularly annotate pytest fixtures like monkeypatch as `Any`. |
|
| 414 |
+ 'any-type', |
|
| 415 |
+ # Our tests generally don't contain docstrings. |
|
| 416 |
+ 'D', 'DOC', |
|
| 417 |
+ # Our tests are regularly parametrized with booleans, for benign |
|
| 418 |
+ # purposes. |
|
| 419 |
+ 'FBT', |
|
| 420 |
+ # One of our standard modules is called `derivepassphrase._types`. |
|
| 421 |
+ # Importing this from the tests directory would then automatically |
|
| 422 |
+ # trigger `PLC2701`. |
|
| 423 |
+ 'import-private-name', |
|
| 424 |
+ # Too many public methods/arguments/returns/branches/locals doesn't |
|
| 425 |
+ # really apply here. |
|
| 426 |
+ 'too-many-public-methods', 'too-many-return-statements', |
|
| 427 |
+ 'too-many-branches', 'too-many-arguments', 'too-many-locals', |
|
| 428 |
+ 'too-many-statements', 'too-many-boolean-expressions', |
|
| 429 |
+ 'too-many-positional-arguments', |
|
| 430 |
+ # To fully test the `derivepassphrase.cli` module (and a couple |
|
| 431 |
+ # other things), we need to call and to mock several internal |
|
| 432 |
+ # functions, which would automatically trigger `SLF001`. |
|
| 433 |
+ 'private-member-access', |
|
| 434 |
+ # pytest does not support sensible introspection of |
|
| 435 |
+ # `assert all(...)` expressions in tests the same way it supports |
|
| 436 |
+ # introspection in `asssert all([...])`. So the extra list |
|
| 437 |
+ # comprehension actually improves debuggability in this case. |
|
| 438 |
+ 'unnecessary-comprehension-in-call', |
|
| 439 |
+ # The tests sometimes include long strings (in non-Python formats) |
|
| 440 |
+ # that should be included verbatim, without artificial line |
|
| 441 |
+ # breaking, so they can be grepped for. |
|
| 442 |
+ 'line-too-long', |
|
| 443 |
+] |
|
| 444 |
+ |
|
| 445 |
+[tool.ruff.lint.flake8-copyright] |
|
| 446 |
+# Include hatch-enforced SPDX-FileCopyrightText in check. |
|
| 447 |
+notice-rgx = '(?i)(?:Copyright\s+((?:\(C\)|©)\s+)?|SPDX-FileCopyrightText:\s+)\d{4}((-|,\s)\d{4})*'
|
|
| 448 |
+ |
|
| 449 |
+[tool.ruff.lint.flake8-pytest-style] |
|
| 450 |
+fixture-parentheses = false |
|
| 451 |
+mark-parentheses = false |
|
| 452 |
+parametrize-names-type = 'list' |
|
| 453 |
+ |
|
| 454 |
+[tool.ruff.lint.flake8-tidy-imports] |
|
| 455 |
+ban-relative-imports = "all" |
|
| 456 |
+ |
|
| 457 |
+[tool.ruff.lint.isort] |
|
| 458 |
+known-first-party = [ |
|
| 459 |
+ "derivepassphrase_sshagentsocketprovider", |
|
| 460 |
+ "fakesshagent", |
|
| 461 |
+] |
|
| 462 |
+ |
|
| 463 |
+[tool.ruff.lint.pycodestyle] |
|
| 464 |
+ignore-overlong-task-comments = true # for E501 |
|
| 465 |
+max-doc-length = 72 # for W505 |
|
| 466 |
+ |
|
| 467 |
+[tool.ruff.lint.pydocstyle] |
|
| 468 |
+convention = 'google' |
|
| 469 |
+ |
|
| 470 |
+[tool.scriv] |
|
| 471 |
+version = "literal: pyproject.toml: project.version" |
|
| 472 |
+format = "md" |
|
| 473 |
+fragment_directory = "docs/changelog.d" |
|
| 474 |
+output_file = "docs/changelog.md" |
|
| 475 |
+insert_marker = "<!-- scriv changelog start -->" |
|
| 476 |
+end_marker = "<!-- scriv changelog end -->" |
|
| 477 |
+md_header_level = "2" |
|
| 478 |
+entry_title_template = "{% if version %}{{ version }} ({% endif %}{{ date.strftime('%Y-%m-%d') }}{% if version %}){% endif %}"
|
| ... | ... |
@@ -0,0 +1,13 @@ |
| 1 |
+# SPDX-FileCopyrightText: 2026 Marco Ricci <software@the13thletter.info> |
|
| 2 |
+# |
|
| 3 |
+# SPDX-License-Identifier: Zlib |
|
| 4 |
+ |
|
| 5 |
+"""A non-hardened implementation of the SSH agent protocol.""" |
|
| 6 |
+ |
|
| 7 |
+from fakesshagent.machinery import ( |
|
| 8 |
+ StubbedSSHAgentSocket as StubbedSSHAgentSocket, |
|
| 9 |
+) |
|
| 10 |
+ |
|
| 11 |
+__version__ = "0.1" |
|
| 12 |
+ |
|
| 13 |
+__all__ = ("StubbedSSHAgentSocket",)
|
| ... | ... |
@@ -0,0 +1,87 @@ |
| 1 |
+# SPDX-FileCopyrightText: 2026 Marco Ricci <software@the13thletter.info> |
|
| 2 |
+# |
|
| 3 |
+# SPDX-License-Identifier: Zlib |
|
| 4 |
+ |
|
| 5 |
+"""Types for the fake SSH agent.""" |
|
| 6 |
+ |
|
| 7 |
+from __future__ import annotations |
|
| 8 |
+ |
|
| 9 |
+import enum |
|
| 10 |
+ |
|
| 11 |
+ |
|
| 12 |
+class SSH_AGENTC(int, enum.Enum): # noqa: N801 |
|
| 13 |
+ """SSH agent protocol numbers: client requests. |
|
| 14 |
+ |
|
| 15 |
+ Attributes: |
|
| 16 |
+ REQUEST_IDENTITIES (int): |
|
| 17 |
+ List identities. Expecting |
|
| 18 |
+ [`SSH_AGENT.IDENTITIES_ANSWER`][]. |
|
| 19 |
+ SIGN_REQUEST (int): |
|
| 20 |
+ Sign data. Expecting [`SSH_AGENT.SIGN_RESPONSE`][]. |
|
| 21 |
+ ADD_IDENTITY (int): |
|
| 22 |
+ Add an (SSH2) identity. |
|
| 23 |
+ REMOVE_IDENTITY (int): |
|
| 24 |
+ Remove an (SSH2) identity. |
|
| 25 |
+ ADD_ID_CONSTRAINED (int): |
|
| 26 |
+ Add an (SSH2) identity, including key constraints. |
|
| 27 |
+ EXTENSION (int): |
|
| 28 |
+ Issue a named request that isn't part of the core agent |
|
| 29 |
+ protocol. Expecting [`SSH_AGENT.EXTENSION_RESPONSE`][] or |
|
| 30 |
+ [`SSH_AGENT.EXTENSION_FAILURE`][] if the named request is |
|
| 31 |
+ supported, [`SSH_AGENT.FAILURE`][] otherwise. |
|
| 32 |
+ |
|
| 33 |
+ """ |
|
| 34 |
+ |
|
| 35 |
+ REQUEST_IDENTITIES = 11 |
|
| 36 |
+ """""" |
|
| 37 |
+ SIGN_REQUEST = 13 |
|
| 38 |
+ """""" |
|
| 39 |
+ ADD_IDENTITY = 17 |
|
| 40 |
+ """""" |
|
| 41 |
+ REMOVE_IDENTITY = 18 |
|
| 42 |
+ """""" |
|
| 43 |
+ ADD_ID_CONSTRAINED = 25 |
|
| 44 |
+ """""" |
|
| 45 |
+ EXTENSION = 27 |
|
| 46 |
+ """""" |
|
| 47 |
+ |
|
| 48 |
+ def __bytes__(self) -> bytes: # pragma: no cover [unused] |
|
| 49 |
+ """Return the corresponding request code.""" |
|
| 50 |
+ return int.to_bytes(self, 1, "big", signed=False) |
|
| 51 |
+ |
|
| 52 |
+ |
|
| 53 |
+class SSH_AGENT(int, enum.Enum): # noqa: N801 |
|
| 54 |
+ """SSH agent protocol numbers: server replies. |
|
| 55 |
+ |
|
| 56 |
+ Attributes: |
|
| 57 |
+ FAILURE (int): |
|
| 58 |
+ Generic failure code. |
|
| 59 |
+ SUCCESS (int): |
|
| 60 |
+ Generic success code. |
|
| 61 |
+ IDENTITIES_ANSWER (int): |
|
| 62 |
+ Successful answer to [`SSH_AGENTC.REQUEST_IDENTITIES`][]. |
|
| 63 |
+ SIGN_RESPONSE (int): |
|
| 64 |
+ Successful answer to [`SSH_AGENTC.SIGN_REQUEST`][]. |
|
| 65 |
+ EXTENSION_FAILURE (int): |
|
| 66 |
+ Unsuccessful answer to [`SSH_AGENTC.EXTENSION`][]. |
|
| 67 |
+ EXTENSION_RESPONSE (int): |
|
| 68 |
+ Successful answer to [`SSH_AGENTC.EXTENSION`][]. |
|
| 69 |
+ |
|
| 70 |
+ """ |
|
| 71 |
+ |
|
| 72 |
+ FAILURE = 5 |
|
| 73 |
+ """""" |
|
| 74 |
+ SUCCESS = 6 |
|
| 75 |
+ """""" |
|
| 76 |
+ IDENTITIES_ANSWER = 12 |
|
| 77 |
+ """""" |
|
| 78 |
+ SIGN_RESPONSE = 14 |
|
| 79 |
+ """""" |
|
| 80 |
+ EXTENSION_FAILURE = 28 |
|
| 81 |
+ """""" |
|
| 82 |
+ EXTENSION_RESPONSE = 29 |
|
| 83 |
+ """""" |
|
| 84 |
+ |
|
| 85 |
+ def __bytes__(self) -> bytes: # pragma: no cover [unused] |
|
| 86 |
+ """Return the corresponding response code.""" |
|
| 87 |
+ return int.to_bytes(self, 1, "big", signed=False) |
| ... | ... |
@@ -0,0 +1,837 @@ |
| 1 |
+# SPDX-FileCopyrightText: 2026 Marco Ricci <software@the13thletter.info> |
|
| 2 |
+# |
|
| 3 |
+# SPDX-License-Identifier: Zlib |
|
| 4 |
+ |
|
| 5 |
+"""Data for the fake SSH agent.""" |
|
| 6 |
+ |
|
| 7 |
+# ruff: noqa: E501 |
|
| 8 |
+ |
|
| 9 |
+from __future__ import annotations |
|
| 10 |
+ |
|
| 11 |
+import enum |
|
| 12 |
+from typing import TYPE_CHECKING |
|
| 13 |
+ |
|
| 14 |
+from typing_extensions import NamedTuple |
|
| 15 |
+ |
|
| 16 |
+if TYPE_CHECKING: |
|
| 17 |
+ from collections.abc import Mapping |
|
| 18 |
+ |
|
| 19 |
+ from typing_extensions import Any |
|
| 20 |
+ |
|
| 21 |
+ |
|
| 22 |
+class SSHTestKeyDeterministicSignatureClass(str, enum.Enum): |
|
| 23 |
+ """The class of a deterministic signature from an SSH test key. |
|
| 24 |
+ |
|
| 25 |
+ Attributes: |
|
| 26 |
+ SPEC: |
|
| 27 |
+ A deterministic signature directly implied by the |
|
| 28 |
+ specification of the signature algorithm. |
|
| 29 |
+ RFC_6979: |
|
| 30 |
+ A deterministic signature as specified by RFC 6979. Only |
|
| 31 |
+ used with DSA and ECDSA keys (that aren't also EdDSA keys). |
|
| 32 |
+ Pageant_068_080: |
|
| 33 |
+ A deterministic signature as specified by Pageant 0.68. |
|
| 34 |
+ Only used with DSA and ECDSA keys (that aren't also EdDSA |
|
| 35 |
+ keys), and only used with Pageant from 0.68 up to and |
|
| 36 |
+ including 0.80. |
|
| 37 |
+ |
|
| 38 |
+ Usage of this signature class together with an ECDSA NIST |
|
| 39 |
+ P-521 key [turned out to leak enough information per |
|
| 40 |
+ signature to quickly compromise the entire private key |
|
| 41 |
+ (CVE-2024-31497)][PUTTY_CVE_2024_31497], so newer Pageant |
|
| 42 |
+ versions abandon this signature class in favor of RFC 6979. |
|
| 43 |
+ |
|
| 44 |
+ [PUTTY_CVE_2024_31497]: https://www.chiark.greenend.org.uk/~sgtatham/putty/wishlist/vuln-p521-bias.html |
|
| 45 |
+ |
|
| 46 |
+ """ |
|
| 47 |
+ |
|
| 48 |
+ SPEC = enum.auto() |
|
| 49 |
+ """""" |
|
| 50 |
+ RFC_6979 = enum.auto() |
|
| 51 |
+ """""" |
|
| 52 |
+ Pageant_068_080 = enum.auto() |
|
| 53 |
+ """""" |
|
| 54 |
+ |
|
| 55 |
+ |
|
| 56 |
+class SSHTestKeyDeterministicSignature(NamedTuple): |
|
| 57 |
+ """An SSH test key deterministic signature. |
|
| 58 |
+ |
|
| 59 |
+ Attributes: |
|
| 60 |
+ signature: |
|
| 61 |
+ The binary signature of the [vault UUID][vault.Vault.UUID] |
|
| 62 |
+ under this signature class. |
|
| 63 |
+ derived_passphrase: |
|
| 64 |
+ The equivalent master passphrase derived from this |
|
| 65 |
+ signature. |
|
| 66 |
+ signature_class: |
|
| 67 |
+ The [signature |
|
| 68 |
+ class][SSHTestKeyDeterministicSignatureClass]. |
|
| 69 |
+ |
|
| 70 |
+ """ |
|
| 71 |
+ |
|
| 72 |
+ signature: bytes |
|
| 73 |
+ """""" |
|
| 74 |
+ derived_passphrase: bytes |
|
| 75 |
+ """""" |
|
| 76 |
+ signature_class: SSHTestKeyDeterministicSignatureClass = ( |
|
| 77 |
+ SSHTestKeyDeterministicSignatureClass.SPEC |
|
| 78 |
+ ) |
|
| 79 |
+ """""" |
|
| 80 |
+ |
|
| 81 |
+ |
|
| 82 |
+class SSHTestKey(NamedTuple): |
|
| 83 |
+ """An SSH test key. |
|
| 84 |
+ |
|
| 85 |
+ Attributes: |
|
| 86 |
+ public_key: |
|
| 87 |
+ The SSH public key string, as used e.g. by OpenSSH's |
|
| 88 |
+ `authorized_keys` file. Includes a comment. |
|
| 89 |
+ public_key_data: |
|
| 90 |
+ The SSH protocol wire format of the public key. |
|
| 91 |
+ private_key: |
|
| 92 |
+ A base64 encoded representation of the private key, in |
|
| 93 |
+ OpenSSH's v1 private key format. |
|
| 94 |
+ private_key_blob: |
|
| 95 |
+ The SSH protocol wire format of the private key. |
|
| 96 |
+ expected_signatures: |
|
| 97 |
+ A mapping of deterministic signature classes to the |
|
| 98 |
+ expected, deterministic signature (of that class) of the |
|
| 99 |
+ vault UUID for this key, together with the respective |
|
| 100 |
+ "equivalent master passphrase" derived from this signature. |
|
| 101 |
+ |
|
| 102 |
+ """ |
|
| 103 |
+ |
|
| 104 |
+ public_key: bytes |
|
| 105 |
+ """""" |
|
| 106 |
+ public_key_data: bytes |
|
| 107 |
+ """""" |
|
| 108 |
+ private_key: bytes |
|
| 109 |
+ """""" |
|
| 110 |
+ private_key_blob: bytes |
|
| 111 |
+ """""" |
|
| 112 |
+ expected_signatures: Mapping[ |
|
| 113 |
+ SSHTestKeyDeterministicSignatureClass, SSHTestKeyDeterministicSignature |
|
| 114 |
+ ] |
|
| 115 |
+ """""" |
|
| 116 |
+ |
|
| 117 |
+ def is_suitable( |
|
| 118 |
+ self, |
|
| 119 |
+ *, |
|
| 120 |
+ client: Any = None, # noqa: ANN401 |
|
| 121 |
+ ) -> bool: |
|
| 122 |
+ """Return if this key is suitable for use with vault. |
|
| 123 |
+ |
|
| 124 |
+ Args: |
|
| 125 |
+ client: |
|
| 126 |
+ An optional SSH agent client to check for additional |
|
| 127 |
+ deterministic key types. If not given, assume no such |
|
| 128 |
+ types. |
|
| 129 |
+ |
|
| 130 |
+ """ |
|
| 131 |
+ raise NotImplementedError |
|
| 132 |
+ |
|
| 133 |
+ |
|
| 134 |
+ALL_KEYS: Mapping[str, SSHTestKey] = {
|
|
| 135 |
+ "ed25519": SSHTestKey( |
|
| 136 |
+ private_key=rb"""-----BEGIN OPENSSH PRIVATE KEY----- |
|
| 137 |
+b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW |
|
| 138 |
+QyNTUxOQAAACCBeIFoJtYCSF8P/zJIb+TBMIncHGpFBgnpCQ/7whJpdgAAAKDweO7H8Hju |
|
| 139 |
+xwAAAAtzc2gtZWQyNTUxOQAAACCBeIFoJtYCSF8P/zJIb+TBMIncHGpFBgnpCQ/7whJpdg |
|
| 140 |
+AAAEAbM/A869nkWZbe2tp3Dm/L6gitvmpH/aRZt8sBII3ExYF4gWgm1gJIXw//Mkhv5MEw |
|
| 141 |
+idwcakUGCekJD/vCEml2AAAAG3Rlc3Qga2V5IHdpdGhvdXQgcGFzc3BocmFzZQEC |
|
| 142 |
+-----END OPENSSH PRIVATE KEY----- |
|
| 143 |
+""", |
|
| 144 |
+ private_key_blob=bytes.fromhex("""
|
|
| 145 |
+ 00 00 00 0b 73 73 68 2d 65 64 32 35 35 31 39 |
|
| 146 |
+ 00 00 00 20 |
|
| 147 |
+ 81 78 81 68 26 d6 02 48 5f 0f ff 32 48 6f e4 c1 |
|
| 148 |
+ 30 89 dc 1c 6a 45 06 09 e9 09 0f fb c2 12 69 76 |
|
| 149 |
+ 00 00 00 40 |
|
| 150 |
+ 1b 33 f0 3c eb d9 e4 59 96 de da da 77 0e 6f cb |
|
| 151 |
+ ea 08 ad be 6a 47 fd a4 59 b7 cb 01 20 8d c4 c5 |
|
| 152 |
+ 81 78 81 68 26 d6 02 48 5f 0f ff 32 48 6f e4 c1 |
|
| 153 |
+ 30 89 dc 1c 6a 45 06 09 e9 09 0f fb c2 12 69 76 |
|
| 154 |
+ 00 00 00 1b 74 65 73 74 20 6b 65 79 20 77 69 74 |
|
| 155 |
+ 68 6f 75 74 20 70 61 73 73 70 68 72 61 73 65 |
|
| 156 |
+"""), |
|
| 157 |
+ public_key=rb"""ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIIF4gWgm1gJIXw//Mkhv5MEwidwcakUGCekJD/vCEml2 test key without passphrase |
|
| 158 |
+""", |
|
| 159 |
+ public_key_data=bytes.fromhex("""
|
|
| 160 |
+ 00 00 00 0b 73 73 68 2d 65 64 32 35 35 31 39 |
|
| 161 |
+ 00 00 00 20 |
|
| 162 |
+ 81 78 81 68 26 d6 02 48 5f 0f ff 32 48 6f e4 c1 |
|
| 163 |
+ 30 89 dc 1c 6a 45 06 09 e9 09 0f fb c2 12 69 76 |
|
| 164 |
+"""), |
|
| 165 |
+ expected_signatures={
|
|
| 166 |
+ SSHTestKeyDeterministicSignatureClass.SPEC: SSHTestKeyDeterministicSignature( |
|
| 167 |
+ signature=bytes.fromhex("""
|
|
| 168 |
+ 00 00 00 0b 73 73 68 2d 65 64 32 35 35 31 39 |
|
| 169 |
+ 00 00 00 40 |
|
| 170 |
+ f0 98 19 80 6c 1a 97 d5 26 03 6e cc e3 65 8f 86 |
|
| 171 |
+ 66 07 13 19 13 09 21 33 33 f9 e4 36 53 1d af fd |
|
| 172 |
+ 0d 08 1f ec f8 73 9b 8c 5f 55 39 16 7c 53 54 2c |
|
| 173 |
+ 1e 52 bb 30 ed 7f 89 e2 2f 69 51 55 d8 9e a6 02 |
|
| 174 |
+"""), |
|
| 175 |
+ derived_passphrase=rb"""8JgZgGwal9UmA27M42WPhmYHExkTCSEzM/nkNlMdr/0NCB/s+HObjF9VORZ8U1QsHlK7MO1/ieIvaVFV2J6mAg==""", |
|
| 176 |
+ ), |
|
| 177 |
+ }, |
|
| 178 |
+ ), |
|
| 179 |
+ # Currently only supported by PuTTY (which is deficient in other |
|
| 180 |
+ # niceties of the SSH agent and the agent's client). |
|
| 181 |
+ "ed448": SSHTestKey( |
|
| 182 |
+ private_key=rb"""-----BEGIN OPENSSH PRIVATE KEY----- |
|
| 183 |
+b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAASgAAAAlz |
|
| 184 |
+c2gtZWQ0NDgAAAA54vZy009Wu8wExjvEb3hqtLz1GO/+d5vmGUbErWQ4AUO9mYLT |
|
| 185 |
+zHJHc2m4s+yWzP29Cc3EcxizLG8AAAAA8BdhfCcXYXwnAAAACXNzaC1lZDQ0OAAA |
|
| 186 |
+ADni9nLTT1a7zATGO8RveGq0vPUY7/53m+YZRsStZDgBQ72ZgtPMckdzabiz7JbM |
|
| 187 |
+/b0JzcRzGLMsbwAAAAByM7GIMRvWJB3YD6SIpAF2uudX4ozZe0X917wPwiBrs373 |
|
| 188 |
+9TM1n94Nib6hrxGNmCk2iBQDe2KALPgA4vZy009Wu8wExjvEb3hqtLz1GO/+d5vm |
|
| 189 |
+GUbErWQ4AUO9mYLTzHJHc2m4s+yWzP29Cc3EcxizLG8AAAAAG3Rlc3Qga2V5IHdp |
|
| 190 |
+dGhvdXQgcGFzc3BocmFzZQECAwQFBgcICQ== |
|
| 191 |
+-----END OPENSSH PRIVATE KEY----- |
|
| 192 |
+""", |
|
| 193 |
+ private_key_blob=bytes.fromhex("""
|
|
| 194 |
+ 00 00 00 09 73 73 68 2d 65 64 34 34 38 |
|
| 195 |
+ 00 00 00 39 e2 f6 72 d3 4f 56 bb cc 04 |
|
| 196 |
+ c6 3b c4 6f 78 6a b4 bc f5 18 ef fe 77 9b e6 19 |
|
| 197 |
+ 46 c4 ad 64 38 01 43 bd 99 82 d3 cc 72 47 73 69 |
|
| 198 |
+ b8 b3 ec 96 cc fd bd 09 cd c4 73 18 b3 2c 6f 00 |
|
| 199 |
+ 00 00 00 72 33 b1 |
|
| 200 |
+ 88 31 1b d6 24 1d d8 0f a4 88 a4 01 76 ba e7 57 |
|
| 201 |
+ e2 8c d9 7b 45 fd d7 bc 0f c2 20 6b b3 7e f7 f5 |
|
| 202 |
+ 33 35 9f de 0d 89 be a1 af 11 8d 98 29 36 88 14 |
|
| 203 |
+ 03 7b 62 80 2c f8 00 e2 f6 72 d3 4f 56 bb cc 04 |
|
| 204 |
+ c6 3b c4 6f 78 6a b4 bc f5 18 ef fe 77 9b e6 19 |
|
| 205 |
+ 46 c4 ad 64 38 01 43 bd 99 82 d3 cc 72 47 73 69 |
|
| 206 |
+ b8 b3 ec 96 cc fd bd 09 cd c4 73 18 b3 2c 6f 00 |
|
| 207 |
+ 00 00 00 1b 74 65 73 74 20 6b 65 79 20 77 69 |
|
| 208 |
+ 74 68 6f 75 74 20 70 61 73 73 70 68 72 61 73 65 |
|
| 209 |
+"""), |
|
| 210 |
+ public_key=rb"""ssh-ed448 AAAACXNzaC1lZDQ0OAAAADni9nLTT1a7zATGO8RveGq0vPUY7/53m+YZRsStZDgBQ72ZgtPMckdzabiz7JbM/b0JzcRzGLMsbwA= test key without passphrase |
|
| 211 |
+""", |
|
| 212 |
+ public_key_data=bytes.fromhex("""
|
|
| 213 |
+ 00 00 00 09 73 73 68 2d 65 64 34 34 38 |
|
| 214 |
+ 00 00 00 39 e2 f6 72 d3 4f 56 bb cc 04 |
|
| 215 |
+ c6 3b c4 6f 78 6a b4 bc f5 18 ef fe 77 9b e6 19 |
|
| 216 |
+ 46 c4 ad 64 38 01 43 bd 99 82 d3 cc 72 47 73 69 |
|
| 217 |
+ b8 b3 ec 96 cc fd bd 09 cd c4 73 18 b3 2c 6f 00 |
|
| 218 |
+ """), |
|
| 219 |
+ expected_signatures={
|
|
| 220 |
+ SSHTestKeyDeterministicSignatureClass.SPEC: SSHTestKeyDeterministicSignature( |
|
| 221 |
+ signature=bytes.fromhex("""
|
|
| 222 |
+ 00 00 00 09 73 73 68 2d 65 64 34 34 38 |
|
| 223 |
+ 00 00 00 72 06 86 |
|
| 224 |
+ f4 64 a4 a6 ba d9 c3 22 c4 93 49 99 fc 11 de 67 |
|
| 225 |
+ 97 08 f2 d8 b7 3c 2c 13 e7 c5 1c 1e 92 a6 0e d8 |
|
| 226 |
+ 2f 6d 81 03 82 00 e3 72 e4 32 6d 72 d2 6d 32 84 |
|
| 227 |
+ 3f cc a9 1e 57 2c 00 9a b3 99 de 45 da ce 2e d1 |
|
| 228 |
+ db e5 89 f3 35 be 24 58 90 c6 ca 04 f0 db 88 80 |
|
| 229 |
+ db bd 77 7c 80 20 7f 3a 48 61 f6 1f ae a9 5e 53 |
|
| 230 |
+ 7b e0 9d 93 1e ea dc eb b5 cd 56 4c ea 8f 08 00 |
|
| 231 |
+"""), |
|
| 232 |
+ derived_passphrase=rb"""Bob0ZKSmutnDIsSTSZn8Ed5nlwjy2Lc8LBPnxRwekqYO2C9tgQOCAONy5DJtctJtMoQ/zKkeVywAmrOZ3kXazi7R2+WJ8zW+JFiQxsoE8NuIgNu9d3yAIH86SGH2H66pXlN74J2THurc67XNVkzqjwgA""", |
|
| 233 |
+ ), |
|
| 234 |
+ }, |
|
| 235 |
+ ), |
|
| 236 |
+ "rsa": SSHTestKey( |
|
| 237 |
+ private_key=rb"""-----BEGIN OPENSSH PRIVATE KEY----- |
|
| 238 |
+b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAABlwAAAAdzc2gtcn |
|
| 239 |
+NhAAAAAwEAAQAAAYEAsaHu6Xs4cVsuDSNJlMCqoPVgmDgEviI8TfXmHKqX3JkIqI3LsvV7 |
|
| 240 |
+Ijf8WCdTveEq7CkuZhImtsR52AOEVAoU8mDXDNr+nJ5wUPzf1UIaRjDe0lcXW4SlF01hQs |
|
| 241 |
+G4wYDuqxshwelraB/L3e0zhD7fjYHF8IbFsqGlFHWEwOtlfhhfbxJsTGguLm4A8/gdEJD5 |
|
| 242 |
+2rkqDcZpIXCHtJbCzW9aQpWcs/PDw5ylwl/3dB7jfxyfrGz4O3QrzsqhWEsip97mOmwl6q |
|
| 243 |
+CHbq8V8x9zu89D/H+bG5ijqxhijbjcVUW3lZfw/97gy9J6rG31HNar5H8GycLTFwuCFepD |
|
| 244 |
+mTEpNgQLKoe8ePIEPq4WHhFUovBdwlrOByUKKqxreyvWt5gkpTARz+9Lt8OjBO3rpqK8sZ |
|
| 245 |
+VKH3sE3de2RJM3V9PJdmZSs2b8EFK3PsUGdlMPM9pn1uk4uIItKWBmooOynuD8Ll6aPwuW |
|
| 246 |
+AFn3l8nLLyWdrmmEYzHWXiRjQJxy1Bi5AbHMOWiPAAAFkDPkuBkz5LgZAAAAB3NzaC1yc2 |
|
| 247 |
+EAAAGBALGh7ul7OHFbLg0jSZTAqqD1YJg4BL4iPE315hyql9yZCKiNy7L1eyI3/FgnU73h |
|
| 248 |
+KuwpLmYSJrbEedgDhFQKFPJg1wza/pyecFD839VCGkYw3tJXF1uEpRdNYULBuMGA7qsbIc |
|
| 249 |
+Hpa2gfy93tM4Q+342BxfCGxbKhpRR1hMDrZX4YX28SbExoLi5uAPP4HRCQ+dq5Kg3GaSFw |
|
| 250 |
+h7SWws1vWkKVnLPzw8OcpcJf93Qe438cn6xs+Dt0K87KoVhLIqfe5jpsJeqgh26vFfMfc7 |
|
| 251 |
+vPQ/x/mxuYo6sYYo243FVFt5WX8P/e4MvSeqxt9RzWq+R/BsnC0xcLghXqQ5kxKTYECyqH |
|
| 252 |
+vHjyBD6uFh4RVKLwXcJazgclCiqsa3sr1reYJKUwEc/vS7fDowTt66aivLGVSh97BN3Xtk |
|
| 253 |
+STN1fTyXZmUrNm/BBStz7FBnZTDzPaZ9bpOLiCLSlgZqKDsp7g/C5emj8LlgBZ95fJyy8l |
|
| 254 |
+na5phGMx1l4kY0CcctQYuQGxzDlojwAAAAMBAAEAAAF/cNVYT+Om4x9+SItcz5bOByGIOj |
|
| 255 |
+yWUH8f9rRjnr5ILuwabIDgvFaVG+xM1O1hWADqzMnSEcknHRkTYEsqYPykAtxFvjOFEh70 |
|
| 256 |
+6qRUJ+fVZkqRGEaI3oWyWKTOhcCIYImtONvb0LOv/HQ2H2AXCoeqjST1qr/xSuljBtcB8u |
|
| 257 |
+wxs3EqaO1yU7QoZpDcMX9plH7Rmc9nNfZcgrnktPk2deX2+Y/A5tzdVgG1IeqYp6CBMLNM |
|
| 258 |
+uhL0OPdDehgBoDujx+rhkZ1gpo1wcULIM94NL7VSHBPX0Lgh9T+3j1HVP+YnMAvhfOvfct |
|
| 259 |
+LlbJ06+TYGRAMuF2LPCAZM/m0FEyAurRgWxAjLXm+4kp2GAJXlw82deDkQ+P8cHNT6s9ZH |
|
| 260 |
+R5YSy3lpZ35594ZMOLR8KqVvhgJGF6i9019BiF91SDxjE+sp6dNGfN8W+64tHdDv2a0Mso |
|
| 261 |
++8Qjyx7sTpi++EjLU8Iy73/e4B8qbXMyheyA/UUfgMtNKShh6sLlrD9h2Sm9RFTuEAAADA |
|
| 262 |
+Jh3u7WfnjhhKZYbAW4TsPNXDMrB0/t7xyAQgFmko7JfESyrJSLg1cO+QMOiDgD7zuQ9RSp |
|
| 263 |
+NIKdPsnIna5peh979mVjb2HgnikjyJECmBpLdwZKhX7MnIvgKw5lnQXHboEtWCa1N58l7f |
|
| 264 |
+srzwbi9pFUuUp9dShXNffmlUCjDRsVLbK5C6+iaIQyCWFYK8mc6dpNkIoPKf+Xg+EJCIFQ |
|
| 265 |
+oITqeu30Gc1+M+fdZc2ghq0b6XLthh/uHEry8b68M5KglMAAAAwQDw1i+IdcvPV/3u/q9O |
|
| 266 |
+/kzLpKO3tbT89sc1zhjZsDNjDAGluNr6n38iq/XYRZu7UTL9BG+EgFVfIUV7XsYT5e+BPf |
|
| 267 |
+13VS94rzZ7maCsOlULX+VdMO2zBucHIoec9RUlRZrfB21B2W7YGMhbpoa5lN3lKJQ7afHo |
|
| 268 |
+dXZUMp0cTFbOmbzJgSzO2/NE7BhVwmvcUzTDJGMMKuxBO6w99YKDKRKm0PNLFDz26rWm9L |
|
| 269 |
+dNS2MVfVuPMTpzT26HQG4pFageq9cAAADBALzRBXdZF8kbSBa5MTUBVTTzgKQm1C772gJ8 |
|
| 270 |
+T01DJEXZsVtOv7mUC1/m/by6Hk4tPyvDBuGj9hHq4N7dPqGutHb1q5n0ADuoQjRW7BXw5Q |
|
| 271 |
+vC2EAD91xexdorIA5BgXU+qltBqzzBVzVtF7+jOZOjfzOlaTX9I5I5veyeTaTxZj1XXUzi |
|
| 272 |
+btBNdMEJJp7ifucYmoYAAwE7K+VlWagDEK2y8Mte9y9E+N0uO2j+h85sQt/UIb2iE/vhcg |
|
| 273 |
+Bgp6142WnSCQAAABt0ZXN0IGtleSB3aXRob3V0IHBhc3NwaHJhc2UB |
|
| 274 |
+-----END OPENSSH PRIVATE KEY----- |
|
| 275 |
+""", |
|
| 276 |
+ private_key_blob=bytes.fromhex("""
|
|
| 277 |
+ 00 00 00 07 73 73 68 2d 72 73 61 |
|
| 278 |
+ 00 00 01 81 00 |
|
| 279 |
+ b1 a1 ee e9 7b 38 71 5b 2e 0d 23 49 94 c0 aa a0 |
|
| 280 |
+ f5 60 98 38 04 be 22 3c 4d f5 e6 1c aa 97 dc 99 |
|
| 281 |
+ 08 a8 8d cb b2 f5 7b 22 37 fc 58 27 53 bd e1 2a |
|
| 282 |
+ ec 29 2e 66 12 26 b6 c4 79 d8 03 84 54 0a 14 f2 |
|
| 283 |
+ 60 d7 0c da fe 9c 9e 70 50 fc df d5 42 1a 46 30 |
|
| 284 |
+ de d2 57 17 5b 84 a5 17 4d 61 42 c1 b8 c1 80 ee |
|
| 285 |
+ ab 1b 21 c1 e9 6b 68 1f cb dd ed 33 84 3e df 8d |
|
| 286 |
+ 81 c5 f0 86 c5 b2 a1 a5 14 75 84 c0 eb 65 7e 18 |
|
| 287 |
+ 5f 6f 12 6c 4c 68 2e 2e 6e 00 f3 f8 1d 10 90 f9 |
|
| 288 |
+ da b9 2a 0d c6 69 21 70 87 b4 96 c2 cd 6f 5a 42 |
|
| 289 |
+ 95 9c b3 f3 c3 c3 9c a5 c2 5f f7 74 1e e3 7f 1c |
|
| 290 |
+ 9f ac 6c f8 3b 74 2b ce ca a1 58 4b 22 a7 de e6 |
|
| 291 |
+ 3a 6c 25 ea a0 87 6e af 15 f3 1f 73 bb cf 43 fc |
|
| 292 |
+ 7f 9b 1b 98 a3 ab 18 62 8d b8 dc 55 45 b7 95 97 |
|
| 293 |
+ f0 ff de e0 cb d2 7a ac 6d f5 1c d6 ab e4 7f 06 |
|
| 294 |
+ c9 c2 d3 17 0b 82 15 ea 43 99 31 29 36 04 0b 2a |
|
| 295 |
+ 87 bc 78 f2 04 3e ae 16 1e 11 54 a2 f0 5d c2 5a |
|
| 296 |
+ ce 07 25 0a 2a ac 6b 7b 2b d6 b7 98 24 a5 30 11 |
|
| 297 |
+ cf ef 4b b7 c3 a3 04 ed eb a6 a2 bc b1 95 4a 1f |
|
| 298 |
+ 7b 04 dd d7 b6 44 93 37 57 d3 c9 76 66 52 b3 66 |
|
| 299 |
+ fc 10 52 b7 3e c5 06 76 53 0f 33 da 67 d6 e9 38 |
|
| 300 |
+ b8 82 2d 29 60 66 a2 83 b2 9e e0 fc 2e 5e 9a 3f |
|
| 301 |
+ 0b 96 00 59 f7 97 c9 cb 2f 25 9d ae 69 84 63 31 |
|
| 302 |
+ d6 5e 24 63 40 9c 72 d4 18 b9 01 b1 cc 39 68 8f |
|
| 303 |
+ 00 00 00 03 01 00 01 |
|
| 304 |
+ 00 00 01 7f |
|
| 305 |
+ 70 d5 58 4f e3 a6 e3 1f 7e 48 8b 5c cf 96 ce |
|
| 306 |
+ 07 21 88 3a 3c 96 50 7f 1f f6 b4 63 9e be 48 2e |
|
| 307 |
+ ec 1a 6c 80 e0 bc 56 95 1b ec 4c d4 ed 61 58 00 |
|
| 308 |
+ ea cc c9 d2 11 c9 27 1d 19 13 60 4b 2a 60 fc a4 |
|
| 309 |
+ 02 dc 45 be 33 85 12 1e f4 ea a4 54 27 e7 d5 66 |
|
| 310 |
+ 4a 91 18 46 88 de 85 b2 58 a4 ce 85 c0 88 60 89 |
|
| 311 |
+ ad 38 db db d0 b3 af fc 74 36 1f 60 17 0a 87 aa |
|
| 312 |
+ 8d 24 f5 aa bf f1 4a e9 63 06 d7 01 f2 ec 31 b3 |
|
| 313 |
+ 71 2a 68 ed 72 53 b4 28 66 90 dc 31 7f 69 94 7e |
|
| 314 |
+ d1 99 cf 67 35 f6 5c 82 b9 e4 b4 f9 36 75 e5 f6 |
|
| 315 |
+ f9 8f c0 e6 dc dd 56 01 b5 21 ea 98 a7 a0 81 30 |
|
| 316 |
+ b3 4c ba 12 f4 38 f7 43 7a 18 01 a0 3b a3 c7 ea |
|
| 317 |
+ e1 91 9d 60 a6 8d 70 71 42 c8 33 de 0d 2f b5 52 |
|
| 318 |
+ 1c 13 d7 d0 b8 21 f5 3f b7 8f 51 d5 3f e6 27 30 |
|
| 319 |
+ 0b e1 7c eb df 72 d2 e5 6c 9d 3a f9 36 06 44 03 |
|
| 320 |
+ 2e 17 62 cf 08 06 4c fe 6d 05 13 20 2e ad 18 16 |
|
| 321 |
+ c4 08 cb 5e 6f b8 92 9d 86 00 95 e5 c3 cd 9d 78 |
|
| 322 |
+ 39 10 f8 ff 1c 1c d4 fa b3 d6 47 47 96 12 cb 79 |
|
| 323 |
+ 69 67 7e 79 f7 86 4c 38 b4 7c 2a a5 6f 86 02 46 |
|
| 324 |
+ 17 a8 bd d3 5f 41 88 5f 75 48 3c 63 13 eb 29 e9 |
|
| 325 |
+ d3 46 7c df 16 fb ae 2d 1d d0 ef d9 ad 0c b2 8f |
|
| 326 |
+ bc 42 3c b1 ee c4 e9 8b ef 84 8c b5 3c 23 2e f7 |
|
| 327 |
+ fd ee 01 f2 a6 d7 33 28 5e c8 0f d4 51 f8 0c b4 |
|
| 328 |
+ d2 92 86 1e ac 2e 5a c3 f6 1d 92 9b d4 45 4e e1 |
|
| 329 |
+ 00 00 00 c0 |
|
| 330 |
+ 26 1d ee ed 67 e7 8e 18 4a 65 86 c0 5b 84 ec 3c |
|
| 331 |
+ d5 c3 32 b0 74 fe de f1 c8 04 20 16 69 28 ec 97 |
|
| 332 |
+ c4 4b 2a c9 48 b8 35 70 ef 90 30 e8 83 80 3e f3 |
|
| 333 |
+ b9 0f 51 4a 93 48 29 d3 ec 9c 89 da e6 97 a1 f7 |
|
| 334 |
+ bf 66 56 36 f6 1e 09 e2 92 3c 89 10 29 81 a4 b7 |
|
| 335 |
+ 70 64 a8 57 ec c9 c8 be 02 b0 e6 59 d0 5c 76 e8 |
|
| 336 |
+ 12 d5 82 6b 53 79 f2 5e df b2 bc f0 6e 2f 69 15 |
|
| 337 |
+ 4b 94 a7 d7 52 85 73 5f 7e 69 54 0a 30 d1 b1 52 |
|
| 338 |
+ db 2b 90 ba fa 26 88 43 20 96 15 82 bc 99 ce 9d |
|
| 339 |
+ a4 d9 08 a0 f2 9f f9 78 3e 10 90 88 15 0a 08 4e |
|
| 340 |
+ a7 ae df 41 9c d7 e3 3e 7d d6 5c da 08 6a d1 be |
|
| 341 |
+ 97 2e d8 61 fe e1 c4 af 2f 1b eb c3 39 2a 09 4c |
|
| 342 |
+ 00 00 00 c1 00 |
|
| 343 |
+ f0 d6 2f 88 75 cb cf 57 fd ee fe af 4e fe 4c cb |
|
| 344 |
+ a4 a3 b7 b5 b4 fc f6 c7 35 ce 18 d9 b0 33 63 0c |
|
| 345 |
+ 01 a5 b8 da fa 9f 7f 22 ab f5 d8 45 9b bb 51 32 |
|
| 346 |
+ fd 04 6f 84 80 55 5f 21 45 7b 5e c6 13 e5 ef 81 |
|
| 347 |
+ 3d fd 77 55 2f 78 af 36 7b 99 a0 ac 3a 55 0b 5f |
|
| 348 |
+ e5 5d 30 ed b3 06 e7 07 22 87 9c f5 15 25 45 9a |
|
| 349 |
+ df 07 6d 41 d9 6e d8 18 c8 5b a6 86 b9 94 dd e5 |
|
| 350 |
+ 28 94 3b 69 f1 e8 75 76 54 32 9d 1c 4c 56 ce 99 |
|
| 351 |
+ bc c9 81 2c ce db f3 44 ec 18 55 c2 6b dc 53 34 |
|
| 352 |
+ c3 24 63 0c 2a ec 41 3b ac 3d f5 82 83 29 12 a6 |
|
| 353 |
+ d0 f3 4b 14 3c f6 ea b5 a6 f4 b7 4d 4b 63 15 7d |
|
| 354 |
+ 5b 8f 31 3a 73 4f 6e 87 40 6e 29 15 a8 1e ab d7 |
|
| 355 |
+ 00 00 00 c1 00 |
|
| 356 |
+ bc d1 05 77 59 17 c9 1b 48 16 b9 31 35 01 55 34 |
|
| 357 |
+ f3 80 a4 26 d4 2e fb da 02 7c 4f 4d 43 24 45 d9 |
|
| 358 |
+ b1 5b 4e bf b9 94 0b 5f e6 fd bc ba 1e 4e 2d 3f |
|
| 359 |
+ 2b c3 06 e1 a3 f6 11 ea e0 de dd 3e a1 ae b4 76 |
|
| 360 |
+ f5 ab 99 f4 00 3b a8 42 34 56 ec 15 f0 e5 0b c2 |
|
| 361 |
+ d8 40 03 f7 5c 5e c5 da 2b 20 0e 41 81 75 3e aa |
|
| 362 |
+ 5b 41 ab 3c c1 57 35 6d 17 bf a3 39 93 a3 7f 33 |
|
| 363 |
+ a5 69 35 fd 23 92 39 bd ec 9e 4d a4 f1 66 3d 57 |
|
| 364 |
+ 5d 4c e2 6e d0 4d 74 c1 09 26 9e e2 7e e7 18 9a |
|
| 365 |
+ 86 00 03 01 3b 2b e5 65 59 a8 03 10 ad b2 f0 cb |
|
| 366 |
+ 5e f7 2f 44 f8 dd 2e 3b 68 fe 87 ce 6c 42 df d4 |
|
| 367 |
+ 21 bd a2 13 fb e1 72 00 60 a7 ad 78 d9 69 d2 09 |
|
| 368 |
+ 00 00 00 1b 74 65 73 74 20 6b 65 79 20 77 69 |
|
| 369 |
+ 74 68 6f 75 74 20 70 61 73 73 70 68 72 61 73 65 |
|
| 370 |
+"""), |
|
| 371 |
+ public_key=rb"""ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQCxoe7pezhxWy4NI0mUwKqg9WCYOAS+IjxN9eYcqpfcmQiojcuy9XsiN/xYJ1O94SrsKS5mEia2xHnYA4RUChTyYNcM2v6cnnBQ/N/VQhpGMN7SVxdbhKUXTWFCwbjBgO6rGyHB6WtoH8vd7TOEPt+NgcXwhsWyoaUUdYTA62V+GF9vEmxMaC4ubgDz+B0QkPnauSoNxmkhcIe0lsLNb1pClZyz88PDnKXCX/d0HuN/HJ+sbPg7dCvOyqFYSyKn3uY6bCXqoIdurxXzH3O7z0P8f5sbmKOrGGKNuNxVRbeVl/D/3uDL0nqsbfUc1qvkfwbJwtMXC4IV6kOZMSk2BAsqh7x48gQ+rhYeEVSi8F3CWs4HJQoqrGt7K9a3mCSlMBHP70u3w6ME7eumoryxlUofewTd17ZEkzdX08l2ZlKzZvwQUrc+xQZ2Uw8z2mfW6Ti4gi0pYGaig7Ke4PwuXpo/C5YAWfeXycsvJZ2uaYRjMdZeJGNAnHLUGLkBscw5aI8= test key without passphrase |
|
| 372 |
+""", |
|
| 373 |
+ public_key_data=bytes.fromhex("""
|
|
| 374 |
+ 00 00 00 07 73 73 68 2d 72 73 61 |
|
| 375 |
+ 00 00 00 03 01 00 01 |
|
| 376 |
+ 00 00 01 81 00 |
|
| 377 |
+ b1 a1 ee e9 7b 38 71 5b 2e 0d 23 49 94 c0 aa a0 |
|
| 378 |
+ f5 60 98 38 04 be 22 3c 4d f5 e6 1c aa 97 dc 99 |
|
| 379 |
+ 08 a8 8d cb b2 f5 7b 22 37 fc 58 27 53 bd e1 2a |
|
| 380 |
+ ec 29 2e 66 12 26 b6 c4 79 d8 03 84 54 0a 14 f2 |
|
| 381 |
+ 60 d7 0c da fe 9c 9e 70 50 fc df d5 42 1a 46 30 |
|
| 382 |
+ de d2 57 17 5b 84 a5 17 4d 61 42 c1 b8 c1 80 ee |
|
| 383 |
+ ab 1b 21 c1 e9 6b 68 1f cb dd ed 33 84 3e df 8d |
|
| 384 |
+ 81 c5 f0 86 c5 b2 a1 a5 14 75 84 c0 eb 65 7e 18 |
|
| 385 |
+ 5f 6f 12 6c 4c 68 2e 2e 6e 00 f3 f8 1d 10 90 f9 |
|
| 386 |
+ da b9 2a 0d c6 69 21 70 87 b4 96 c2 cd 6f 5a 42 |
|
| 387 |
+ 95 9c b3 f3 c3 c3 9c a5 c2 5f f7 74 1e e3 7f 1c |
|
| 388 |
+ 9f ac 6c f8 3b 74 2b ce ca a1 58 4b 22 a7 de e6 |
|
| 389 |
+ 3a 6c 25 ea a0 87 6e af 15 f3 1f 73 bb cf 43 fc |
|
| 390 |
+ 7f 9b 1b 98 a3 ab 18 62 8d b8 dc 55 45 b7 95 97 |
|
| 391 |
+ f0 ff de e0 cb d2 7a ac 6d f5 1c d6 ab e4 7f 06 |
|
| 392 |
+ c9 c2 d3 17 0b 82 15 ea 43 99 31 29 36 04 0b 2a |
|
| 393 |
+ 87 bc 78 f2 04 3e ae 16 1e 11 54 a2 f0 5d c2 5a |
|
| 394 |
+ ce 07 25 0a 2a ac 6b 7b 2b d6 b7 98 24 a5 30 11 |
|
| 395 |
+ cf ef 4b b7 c3 a3 04 ed eb a6 a2 bc b1 95 4a 1f |
|
| 396 |
+ 7b 04 dd d7 b6 44 93 37 57 d3 c9 76 66 52 b3 66 |
|
| 397 |
+ fc 10 52 b7 3e c5 06 76 53 0f 33 da 67 d6 e9 38 |
|
| 398 |
+ b8 82 2d 29 60 66 a2 83 b2 9e e0 fc 2e 5e 9a 3f |
|
| 399 |
+ 0b 96 00 59 f7 97 c9 cb 2f 25 9d ae 69 84 63 31 |
|
| 400 |
+ d6 5e 24 63 40 9c 72 d4 18 b9 01 b1 cc 39 68 8f |
|
| 401 |
+"""), |
|
| 402 |
+ expected_signatures={
|
|
| 403 |
+ SSHTestKeyDeterministicSignatureClass.SPEC: SSHTestKeyDeterministicSignature( |
|
| 404 |
+ signature=bytes.fromhex("""
|
|
| 405 |
+ 00 00 00 07 73 73 68 2d 72 73 61 |
|
| 406 |
+ 00 00 01 80 |
|
| 407 |
+ a2 10 7c 2e f6 bb 53 a8 74 2a a1 19 99 ad 81 be |
|
| 408 |
+ 79 9c ed d6 9d 09 4e 6e c5 18 48 33 90 77 99 68 |
|
| 409 |
+ f7 9e 03 5a cd 4e 18 eb 89 7d 85 a2 ee ae 4a 92 |
|
| 410 |
+ f6 6f ce b9 fe 86 7f 2a 6b 31 da 6e 1a fe a2 a5 |
|
| 411 |
+ 88 b8 44 7f a1 76 73 b3 ec 75 b5 d0 a6 b9 15 97 |
|
| 412 |
+ 65 09 13 7d 94 21 d1 fb 5d 0f 8b 23 04 77 c2 c3 |
|
| 413 |
+ 55 22 b1 a0 09 8a f5 38 2a d6 7f 1b 87 29 a0 25 |
|
| 414 |
+ d3 25 6f cb 64 61 07 98 dc 14 c5 84 f8 92 24 5e |
|
| 415 |
+ 50 11 6b 49 e5 f0 cc 29 cb 29 a9 19 d8 a7 71 1f |
|
| 416 |
+ 91 0b 05 b1 01 4b c2 5f 00 a5 b6 21 bf f8 2c 9d |
|
| 417 |
+ 67 9b 47 3b 0a 49 6b 79 2d fc 1d ec 0c b0 e5 27 |
|
| 418 |
+ 22 d5 a9 f8 d3 c3 f9 df 48 68 e9 fb ef 3c dc 26 |
|
| 419 |
+ bf cf ea 29 43 01 a6 e3 c5 51 95 f4 66 6d 8a 55 |
|
| 420 |
+ e2 47 ec e8 30 45 4c ae 47 e7 c9 a4 21 8b 64 ba |
|
| 421 |
+ b6 88 f6 21 f8 73 b9 cb 11 a1 78 75 92 c6 5a e5 |
|
| 422 |
+ 64 fe ed 42 d9 95 99 e6 2b 6f 3c 16 3c 28 74 a4 |
|
| 423 |
+ 72 2f 0d 3f 2c 33 67 aa 35 19 8e e7 b5 11 2f b3 |
|
| 424 |
+ f7 6a c5 02 e2 6f a3 42 e3 62 19 99 03 ea a5 20 |
|
| 425 |
+ e7 a1 e3 bc c8 06 a3 b5 7c d6 76 5d df 6f 60 46 |
|
| 426 |
+ 83 2a 08 00 d6 d3 d9 a4 c1 41 8c f8 60 56 45 81 |
|
| 427 |
+ da 3b a2 16 1f 9e 4e 75 83 17 da c3 53 c3 3e 19 |
|
| 428 |
+ a4 1b bc d2 29 b8 78 61 2b 78 e6 b1 52 b0 d5 ec |
|
| 429 |
+ de 69 2c 48 62 d9 fd d1 9b 6b b0 49 db d3 ff 38 |
|
| 430 |
+ e7 10 d9 2d ce 9f 0d 5e 09 7b 37 d2 7b c3 bf ce |
|
| 431 |
+"""), |
|
| 432 |
+ derived_passphrase=rb"""ohB8Lva7U6h0KqEZma2Bvnmc7dadCU5uxRhIM5B3mWj3ngNazU4Y64l9haLurkqS9m/Ouf6GfyprMdpuGv6ipYi4RH+hdnOz7HW10Ka5FZdlCRN9lCHR+10PiyMEd8LDVSKxoAmK9Tgq1n8bhymgJdMlb8tkYQeY3BTFhPiSJF5QEWtJ5fDMKcspqRnYp3EfkQsFsQFLwl8ApbYhv/gsnWebRzsKSWt5Lfwd7Ayw5Sci1an408P530ho6fvvPNwmv8/qKUMBpuPFUZX0Zm2KVeJH7OgwRUyuR+fJpCGLZLq2iPYh+HO5yxGheHWSxlrlZP7tQtmVmeYrbzwWPCh0pHIvDT8sM2eqNRmO57URL7P3asUC4m+jQuNiGZkD6qUg56HjvMgGo7V81nZd329gRoMqCADW09mkwUGM+GBWRYHaO6IWH55OdYMX2sNTwz4ZpBu80im4eGEreOaxUrDV7N5pLEhi2f3Rm2uwSdvT/zjnENktzp8NXgl7N9J7w7/O""", |
|
| 433 |
+ ), |
|
| 434 |
+ }, |
|
| 435 |
+ ), |
|
| 436 |
+ "dsa1024": SSHTestKey( |
|
| 437 |
+ private_key=rb"""-----BEGIN OPENSSH PRIVATE KEY----- |
|
| 438 |
+b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAABsQAAAAdzc2gtZH |
|
| 439 |
+NzAAAAgQC7KAZXqBGNVLBQPrcMYAoNW54BhD8aIhe7BDWYzJcsaMt72VKSkguZ8+XR7nRa |
|
| 440 |
+0C/ZsBi+uJp0dpxy9ZMTOWX4u5YPMeQcXEdGExZIfimGqSOAsy6fCld2IfJZJZExcCmhe9 |
|
| 441 |
+Ssjsd3YSAPJRluOXFQc95MZoR5hMwlIDD8QzrE7QAAABUA99nOZOgd7aHMVGoXpUEBcn7H |
|
| 442 |
+ossAAACALr2Ag3hxM3rKdxzVUw8fX0VVPXO+3+Kr8hGe0Kc/7NwVaBVL1GQ8fenBuWynpA |
|
| 443 |
+UbH0wo3h1wkB/8hX6p+S8cnu5rIBlUuVNwLw/bIYohK98LfqTYK/V+g6KD+8m34wvEiXZm |
|
| 444 |
+qywY54n2bksch1Nqvj/tNpLzExSx/XS0kSM1aigAAACAbQNRPcVEuGDrEcf+xg5tgAejPX |
|
| 445 |
+BPXr/Jss+Chk64km3mirMYjAWyWYtVcgT+7hOYxtYRin8LyMLqKRmqa0Q5UrvDfChgLhvs |
|
| 446 |
+G9YSb/Mpw5qm8PiHSafwhkaz/te3+8hKogqoe7sd+tCF06IpJr5k70ACiNtRGqssNF8Elr |
|
| 447 |
+l1efYAAAH4swlfVrMJX1YAAAAHc3NoLWRzcwAAAIEAuygGV6gRjVSwUD63DGAKDVueAYQ/ |
|
| 448 |
+GiIXuwQ1mMyXLGjLe9lSkpILmfPl0e50WtAv2bAYvriadHaccvWTEzll+LuWDzHkHFxHRh |
|
| 449 |
+MWSH4phqkjgLMunwpXdiHyWSWRMXApoXvUrI7Hd2EgDyUZbjlxUHPeTGaEeYTMJSAw/EM6 |
|
| 450 |
+xO0AAAAVAPfZzmToHe2hzFRqF6VBAXJ+x6LLAAAAgC69gIN4cTN6yncc1VMPH19FVT1zvt |
|
| 451 |
+/iq/IRntCnP+zcFWgVS9RkPH3pwblsp6QFGx9MKN4dcJAf/IV+qfkvHJ7uayAZVLlTcC8P |
|
| 452 |
+2yGKISvfC36k2Cv1foOig/vJt+MLxIl2ZqssGOeJ9m5LHIdTar4/7TaS8xMUsf10tJEjNW |
|
| 453 |
+ooAAAAgG0DUT3FRLhg6xHH/sYObYAHoz1wT16/ybLPgoZOuJJt5oqzGIwFslmLVXIE/u4T |
|
| 454 |
+mMbWEYp/C8jC6ikZqmtEOVK7w3woYC4b7BvWEm/zKcOapvD4h0mn8IZGs/7Xt/vISqIKqH |
|
| 455 |
+u7HfrQhdOiKSa+ZO9AAojbURqrLDRfBJa5dXn2AAAAFQDJHfenj4EJ9WkehpdJatPBlqCW |
|
| 456 |
+0gAAABt0ZXN0IGtleSB3aXRob3V0IHBhc3NwaHJhc2UBAgMEBQYH |
|
| 457 |
+-----END OPENSSH PRIVATE KEY----- |
|
| 458 |
+""", |
|
| 459 |
+ private_key_blob=bytes.fromhex("""
|
|
| 460 |
+ 00 00 00 07 73 73 68 2d 64 73 73 |
|
| 461 |
+ 00 00 00 81 00 |
|
| 462 |
+ bb 28 06 57 a8 11 8d 54 b0 50 3e b7 0c 60 0a 0d |
|
| 463 |
+ 5b 9e 01 84 3f 1a 22 17 bb 04 35 98 cc 97 2c 68 |
|
| 464 |
+ cb 7b d9 52 92 92 0b 99 f3 e5 d1 ee 74 5a d0 2f |
|
| 465 |
+ d9 b0 18 be b8 9a 74 76 9c 72 f5 93 13 39 65 f8 |
|
| 466 |
+ bb 96 0f 31 e4 1c 5c 47 46 13 16 48 7e 29 86 a9 |
|
| 467 |
+ 23 80 b3 2e 9f 0a 57 76 21 f2 59 25 91 31 70 29 |
|
| 468 |
+ a1 7b d4 ac 8e c7 77 61 20 0f 25 19 6e 39 71 50 |
|
| 469 |
+ 73 de 4c 66 84 79 84 cc 25 20 30 fc 43 3a c4 ed |
|
| 470 |
+ 00 00 00 15 00 f7 d9 ce 64 |
|
| 471 |
+ e8 1d ed a1 cc 54 6a 17 a5 41 01 72 7e c7 a2 cb |
|
| 472 |
+ 00 00 00 80 |
|
| 473 |
+ 2e bd 80 83 78 71 33 7a ca 77 1c d5 53 0f 1f 5f |
|
| 474 |
+ 45 55 3d 73 be df e2 ab f2 11 9e d0 a7 3f ec dc |
|
| 475 |
+ 15 68 15 4b d4 64 3c 7d e9 c1 b9 6c a7 a4 05 1b |
|
| 476 |
+ 1f 4c 28 de 1d 70 90 1f fc 85 7e a9 f9 2f 1c 9e |
|
| 477 |
+ ee 6b 20 19 54 b9 53 70 2f 0f db 21 8a 21 2b df |
|
| 478 |
+ 0b 7e a4 d8 2b f5 7e 83 a2 83 fb c9 b7 e3 0b c4 |
|
| 479 |
+ 89 76 66 ab 2c 18 e7 89 f6 6e 4b 1c 87 53 6a be |
|
| 480 |
+ 3f ed 36 92 f3 13 14 b1 fd 74 b4 91 23 35 6a 28 |
|
| 481 |
+ 00 00 00 80 |
|
| 482 |
+ 6d 03 51 3d c5 44 b8 60 eb 11 c7 fe c6 0e 6d 80 |
|
| 483 |
+ 07 a3 3d 70 4f 5e bf c9 b2 cf 82 86 4e b8 92 6d |
|
| 484 |
+ e6 8a b3 18 8c 05 b2 59 8b 55 72 04 fe ee 13 98 |
|
| 485 |
+ c6 d6 11 8a 7f 0b c8 c2 ea 29 19 aa 6b 44 39 52 |
|
| 486 |
+ bb c3 7c 28 60 2e 1b ec 1b d6 12 6f f3 29 c3 9a |
|
| 487 |
+ a6 f0 f8 87 49 a7 f0 86 46 b3 fe d7 b7 fb c8 4a |
|
| 488 |
+ a2 0a a8 7b bb 1d fa d0 85 d3 a2 29 26 be 64 ef |
|
| 489 |
+ 40 02 88 db 51 1a ab 2c 34 5f 04 96 b9 75 79 f6 |
|
| 490 |
+ 00 00 00 15 00 c9 1d f7 a7 |
|
| 491 |
+ 8f 81 09 f5 69 1e 86 97 49 6a d3 c1 96 a0 96 d2 |
|
| 492 |
+ 00 00 00 1b 74 65 73 74 20 6b 65 79 20 77 69 |
|
| 493 |
+ 74 68 6f 75 74 20 70 61 73 73 70 68 72 61 73 65 |
|
| 494 |
+"""), |
|
| 495 |
+ public_key=rb"""ssh-dss AAAAB3NzaC1kc3MAAACBALsoBleoEY1UsFA+twxgCg1bngGEPxoiF7sENZjMlyxoy3vZUpKSC5nz5dHudFrQL9mwGL64mnR2nHL1kxM5Zfi7lg8x5BxcR0YTFkh+KYapI4CzLp8KV3Yh8lklkTFwKaF71KyOx3dhIA8lGW45cVBz3kxmhHmEzCUgMPxDOsTtAAAAFQD32c5k6B3tocxUahelQQFyfseiywAAAIAuvYCDeHEzesp3HNVTDx9fRVU9c77f4qvyEZ7Qpz/s3BVoFUvUZDx96cG5bKekBRsfTCjeHXCQH/yFfqn5Lxye7msgGVS5U3AvD9shiiEr3wt+pNgr9X6DooP7ybfjC8SJdmarLBjnifZuSxyHU2q+P+02kvMTFLH9dLSRIzVqKAAAAIBtA1E9xUS4YOsRx/7GDm2AB6M9cE9ev8myz4KGTriSbeaKsxiMBbJZi1VyBP7uE5jG1hGKfwvIwuopGaprRDlSu8N8KGAuG+wb1hJv8ynDmqbw+IdJp/CGRrP+17f7yEqiCqh7ux360IXToikmvmTvQAKI21Eaqyw0XwSWuXV59g== test key without passphrase |
|
| 496 |
+""", |
|
| 497 |
+ public_key_data=bytes.fromhex("""
|
|
| 498 |
+ 00 00 00 07 73 73 68 2d 64 73 73 |
|
| 499 |
+ 00 00 00 81 00 |
|
| 500 |
+ bb 28 06 57 a8 11 8d 54 b0 50 3e b7 0c 60 0a 0d |
|
| 501 |
+ 5b 9e 01 84 3f 1a 22 17 bb 04 35 98 cc 97 2c 68 |
|
| 502 |
+ cb 7b d9 52 92 92 0b 99 f3 e5 d1 ee 74 5a d0 2f |
|
| 503 |
+ d9 b0 18 be b8 9a 74 76 9c 72 f5 93 13 39 65 f8 |
|
| 504 |
+ bb 96 0f 31 e4 1c 5c 47 46 13 16 48 7e 29 86 a9 |
|
| 505 |
+ 23 80 b3 2e 9f 0a 57 76 21 f2 59 25 91 31 70 29 |
|
| 506 |
+ a1 7b d4 ac 8e c7 77 61 20 0f 25 19 6e 39 71 50 |
|
| 507 |
+ 73 de 4c 66 84 79 84 cc 25 20 30 fc 43 3a c4 ed |
|
| 508 |
+ 00 00 00 15 00 f7 d9 ce 64 |
|
| 509 |
+ e8 1d ed a1 cc 54 6a 17 a5 41 01 72 7e c7 a2 cb |
|
| 510 |
+ 00 00 00 80 |
|
| 511 |
+ 2e bd 80 83 78 71 33 7a ca 77 1c d5 53 0f 1f 5f |
|
| 512 |
+ 45 55 3d 73 be df e2 ab f2 11 9e d0 a7 3f ec dc |
|
| 513 |
+ 15 68 15 4b d4 64 3c 7d e9 c1 b9 6c a7 a4 05 1b |
|
| 514 |
+ 1f 4c 28 de 1d 70 90 1f fc 85 7e a9 f9 2f 1c 9e |
|
| 515 |
+ ee 6b 20 19 54 b9 53 70 2f 0f db 21 8a 21 2b df |
|
| 516 |
+ 0b 7e a4 d8 2b f5 7e 83 a2 83 fb c9 b7 e3 0b c4 |
|
| 517 |
+ 89 76 66 ab 2c 18 e7 89 f6 6e 4b 1c 87 53 6a be |
|
| 518 |
+ 3f ed 36 92 f3 13 14 b1 fd 74 b4 91 23 35 6a 28 |
|
| 519 |
+ 00 00 00 80 |
|
| 520 |
+ 6d 03 51 3d c5 44 b8 60 eb 11 c7 fe c6 0e 6d 80 |
|
| 521 |
+ 07 a3 3d 70 4f 5e bf c9 b2 cf 82 86 4e b8 92 6d |
|
| 522 |
+ e6 8a b3 18 8c 05 b2 59 8b 55 72 04 fe ee 13 98 |
|
| 523 |
+ c6 d6 11 8a 7f 0b c8 c2 ea 29 19 aa 6b 44 39 52 |
|
| 524 |
+ bb c3 7c 28 60 2e 1b ec 1b d6 12 6f f3 29 c3 9a |
|
| 525 |
+ a6 f0 f8 87 49 a7 f0 86 46 b3 fe d7 b7 fb c8 4a |
|
| 526 |
+ a2 0a a8 7b bb 1d fa d0 85 d3 a2 29 26 be 64 ef |
|
| 527 |
+ 40 02 88 db 51 1a ab 2c 34 5f 04 96 b9 75 79 f6 |
|
| 528 |
+"""), |
|
| 529 |
+ expected_signatures={
|
|
| 530 |
+ SSHTestKeyDeterministicSignatureClass.RFC_6979: SSHTestKeyDeterministicSignature( |
|
| 531 |
+ signature=bytes.fromhex("""
|
|
| 532 |
+ 00 00 00 07 73 73 68 2d 64 73 73 |
|
| 533 |
+ 00 00 00 28 11 5f 4d 13 c2 ee 61 97 |
|
| 534 |
+ 1e f6 23 14 3b 2b dd cf 06 c0 71 13 cc ac 34 19 |
|
| 535 |
+ ad 36 8d 79 aa 25 fb 5e 4f ea fe 6b 5b fa 57 42 |
|
| 536 |
+"""), |
|
| 537 |
+ derived_passphrase=rb"""EV9NE8LuYZce9iMUOyvdzwbAcRPMrDQZrTaNeaol+15P6v5rW/pXQg==""", |
|
| 538 |
+ signature_class=SSHTestKeyDeterministicSignatureClass.RFC_6979, |
|
| 539 |
+ ), |
|
| 540 |
+ SSHTestKeyDeterministicSignatureClass.Pageant_068_080: SSHTestKeyDeterministicSignature( |
|
| 541 |
+ signature=bytes.fromhex("""
|
|
| 542 |
+ 00 00 00 07 73 73 68 2d 64 73 73 |
|
| 543 |
+ 00 00 00 28 0b f7 a8 ab 89 f5 b6 c4 |
|
| 544 |
+ 1c 9b 78 2c 46 35 69 e2 88 b7 eb 55 37 48 7f 6d |
|
| 545 |
+ 49 a1 e6 de 58 1a 04 eb e6 28 99 0e 3c fd 3b 48 |
|
| 546 |
+"""), |
|
| 547 |
+ derived_passphrase=rb"""C/eoq4n1tsQcm3gsRjVp4oi361U3SH9tSaHm3lgaBOvmKJkOPP07SA==""", |
|
| 548 |
+ signature_class=SSHTestKeyDeterministicSignatureClass.Pageant_068_080, |
|
| 549 |
+ ), |
|
| 550 |
+ }, |
|
| 551 |
+ ), |
|
| 552 |
+ "ecdsa256": SSHTestKey( |
|
| 553 |
+ private_key=rb"""-----BEGIN OPENSSH PRIVATE KEY----- |
|
| 554 |
+b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAaAAAABNlY2RzYS |
|
| 555 |
+1zaGEyLW5pc3RwMjU2AAAACG5pc3RwMjU2AAAAQQTLbU0zDwsk2Dvp+VYIrsNVf5gWwz2S |
|
| 556 |
+3SZ8TbxiQRkpnGSVqyIoHJOJc+NQItAa7xlJ/8Z6gfz57Z3apUkaMJm6AAAAuKeY+YinmP |
|
| 557 |
+mIAAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBMttTTMPCyTYO+n5 |
|
| 558 |
+Vgiuw1V/mBbDPZLdJnxNvGJBGSmcZJWrIigck4lz41Ai0BrvGUn/xnqB/PntndqlSRowmb |
|
| 559 |
+oAAAAhAKIl/3n0pKVIxpZkXTGtii782Qr4yIcvHdpxjO/QsIqKAAAAG3Rlc3Qga2V5IHdp |
|
| 560 |
+dGhvdXQgcGFzc3BocmFzZQECAwQ= |
|
| 561 |
+-----END OPENSSH PRIVATE KEY----- |
|
| 562 |
+""", |
|
| 563 |
+ private_key_blob=bytes.fromhex("""
|
|
| 564 |
+ 00 00 00 13 65 63 64 |
|
| 565 |
+ 73 61 2d 73 68 61 32 2d 6e 69 73 74 70 32 35 36 |
|
| 566 |
+ 00 00 00 08 6e 69 73 74 70 32 35 36 |
|
| 567 |
+ 00 00 00 41 04 |
|
| 568 |
+ cb 6d 4d 33 0f 0b 24 d8 3b e9 f9 56 08 ae c3 55 |
|
| 569 |
+ 7f 98 16 c3 3d 92 dd 26 7c 4d bc 62 41 19 29 9c |
|
| 570 |
+ 64 95 ab 22 28 1c 93 89 73 e3 50 22 d0 1a ef 19 |
|
| 571 |
+ 49 ff c6 7a 81 fc f9 ed 9d da a5 49 1a 30 99 ba |
|
| 572 |
+ 00 00 00 21 00 |
|
| 573 |
+ a2 25 ff 79 f4 a4 a5 48 c6 96 64 5d 31 ad 8a 2e |
|
| 574 |
+ fc d9 0a f8 c8 87 2f 1d da 71 8c ef d0 b0 8a 8a |
|
| 575 |
+ 00 00 00 1b 74 65 73 74 20 6b 65 79 20 77 69 |
|
| 576 |
+ 74 68 6f 75 74 20 70 61 73 73 70 68 72 61 73 65 |
|
| 577 |
+"""), |
|
| 578 |
+ public_key=rb"""ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBMttTTMPCyTYO+n5Vgiuw1V/mBbDPZLdJnxNvGJBGSmcZJWrIigck4lz41Ai0BrvGUn/xnqB/PntndqlSRowmbo= test key without passphrase |
|
| 579 |
+""", |
|
| 580 |
+ public_key_data=bytes.fromhex("""
|
|
| 581 |
+ 00 00 00 13 65 63 64 |
|
| 582 |
+ 73 61 2d 73 68 61 32 2d 6e 69 73 74 70 32 35 36 |
|
| 583 |
+ 00 00 00 08 6e 69 73 74 70 32 35 36 |
|
| 584 |
+ 00 00 00 41 04 |
|
| 585 |
+ cb 6d 4d 33 0f 0b 24 d8 3b e9 f9 56 08 ae c3 55 |
|
| 586 |
+ 7f 98 16 c3 3d 92 dd 26 7c 4d bc 62 41 19 29 9c |
|
| 587 |
+ 64 95 ab 22 28 1c 93 89 73 e3 50 22 d0 1a ef 19 |
|
| 588 |
+ 49 ff c6 7a 81 fc f9 ed 9d da a5 49 1a 30 99 ba |
|
| 589 |
+"""), |
|
| 590 |
+ expected_signatures={
|
|
| 591 |
+ SSHTestKeyDeterministicSignatureClass.RFC_6979: SSHTestKeyDeterministicSignature( |
|
| 592 |
+ signature=bytes.fromhex("""
|
|
| 593 |
+ 00 00 00 13 65 63 64 |
|
| 594 |
+ 73 61 2d 73 68 61 32 2d 6e 69 73 74 70 32 35 36 |
|
| 595 |
+ 00 00 00 49 |
|
| 596 |
+ 00 00 00 20 |
|
| 597 |
+ 22 ad 23 8a 9c 5d ca 4e ea 73 e7 29 77 ab a8 b2 |
|
| 598 |
+ 2e 01 d8 de 11 ae c9 b3 57 ce d5 84 9c 85 73 eb |
|
| 599 |
+ 00 00 00 21 00 |
|
| 600 |
+ 9b 1a cb dd 45 89 f0 37 95 9c a2 d8 ac c3 f7 71 |
|
| 601 |
+ 55 33 50 86 9e cb 3a 95 e4 68 80 1a 9d d6 d5 bc |
|
| 602 |
+"""), |
|
| 603 |
+ derived_passphrase=rb"""AAAAICKtI4qcXcpO6nPnKXerqLIuAdjeEa7Js1fO1YSchXPrAAAAIQCbGsvdRYnwN5Wcotisw/dxVTNQhp7LOpXkaIAandbVvA==""", |
|
| 604 |
+ signature_class=SSHTestKeyDeterministicSignatureClass.RFC_6979, |
|
| 605 |
+ ), |
|
| 606 |
+ SSHTestKeyDeterministicSignatureClass.Pageant_068_080: SSHTestKeyDeterministicSignature( |
|
| 607 |
+ signature=bytes.fromhex("""
|
|
| 608 |
+ 00 00 00 13 65 63 64 |
|
| 609 |
+ 73 61 2d 73 68 61 32 2d 6e 69 73 74 70 32 35 36 |
|
| 610 |
+ 00 00 00 49 |
|
| 611 |
+ 00 00 00 21 00 |
|
| 612 |
+ b7 9e 4f ec ec 9b 77 dd 12 d9 43 a2 f5 bf b5 34 |
|
| 613 |
+ 91 e0 89 44 e6 20 48 36 fa 75 22 77 86 38 de 21 |
|
| 614 |
+ 00 00 00 20 |
|
| 615 |
+ 3f d8 04 0f fa f5 bc d2 26 e0 4c 0c 77 5d 0e 08 |
|
| 616 |
+ ec 30 04 8e 42 58 41 96 f6 7e 4f d2 14 39 f4 87 |
|
| 617 |
+"""), |
|
| 618 |
+ derived_passphrase=rb"""AAAAIQC3nk/s7Jt33RLZQ6L1v7U0keCJROYgSDb6dSJ3hjjeIQAAACA/2AQP+vW80ibgTAx3XQ4I7DAEjkJYQZb2fk/SFDn0hw==""", |
|
| 619 |
+ signature_class=SSHTestKeyDeterministicSignatureClass.Pageant_068_080, |
|
| 620 |
+ ), |
|
| 621 |
+ }, |
|
| 622 |
+ ), |
|
| 623 |
+ "ecdsa384": SSHTestKey( |
|
| 624 |
+ private_key=rb"""-----BEGIN OPENSSH PRIVATE KEY----- |
|
| 625 |
+b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAiAAAABNlY2RzYS |
|
| 626 |
+1zaGEyLW5pc3RwMzg0AAAACG5pc3RwMzg0AAAAYQSgkOjkAvq7v5vHuj3KBL4/EAWcn5hZ |
|
| 627 |
+DyKcbyV0eBMGFq7hKXQlZqIahLVqeMR0QqmkxNJ2rly2VHcXneq3vZ+9fIsWCOdYk5WP3N |
|
| 628 |
+ZPzv911Xn7wbEkC7QndD5zKlm4pBUAAADomhj+IZoY/iEAAAATZWNkc2Etc2hhMi1uaXN0 |
|
| 629 |
+cDM4NAAAAAhuaXN0cDM4NAAAAGEEoJDo5AL6u7+bx7o9ygS+PxAFnJ+YWQ8inG8ldHgTBh |
|
| 630 |
+au4Sl0JWaiGoS1anjEdEKppMTSdq5ctlR3F53qt72fvXyLFgjnWJOVj9zWT87/ddV5+8Gx |
|
| 631 |
+JAu0J3Q+cypZuKQVAAAAMQD5sTy8p+B1cn/DhOmXquui1BcxvASqzzevkBlbQoBa73y04B |
|
| 632 |
+2OdqVOVRkwZWRROz0AAAAbdGVzdCBrZXkgd2l0aG91dCBwYXNzcGhyYXNlAQIDBA== |
|
| 633 |
+-----END OPENSSH PRIVATE KEY----- |
|
| 634 |
+""", |
|
| 635 |
+ private_key_blob=bytes.fromhex("""
|
|
| 636 |
+ 00 00 00 13 65 63 64 |
|
| 637 |
+ 73 61 2d 73 68 61 32 2d 6e 69 73 74 70 33 38 34 |
|
| 638 |
+ 00 00 00 08 6e 69 73 74 70 33 38 34 |
|
| 639 |
+ 00 00 00 61 04 |
|
| 640 |
+ a0 90 e8 e4 02 fa bb bf 9b c7 ba 3d ca 04 be 3f |
|
| 641 |
+ 10 05 9c 9f 98 59 0f 22 9c 6f 25 74 78 13 06 16 |
|
| 642 |
+ ae e1 29 74 25 66 a2 1a 84 b5 6a 78 c4 74 42 a9 |
|
| 643 |
+ a4 c4 d2 76 ae 5c b6 54 77 17 9d ea b7 bd 9f bd |
|
| 644 |
+ 7c 8b 16 08 e7 58 93 95 8f dc d6 4f ce ff 75 d5 |
|
| 645 |
+ 79 fb c1 b1 24 0b b4 27 74 3e 73 2a 59 b8 a4 15 |
|
| 646 |
+ 00 00 00 31 00 |
|
| 647 |
+ f9 b1 3c bc a7 e0 75 72 7f c3 84 e9 97 aa eb a2 |
|
| 648 |
+ d4 17 31 bc 04 aa cf 37 af 90 19 5b 42 80 5a ef |
|
| 649 |
+ 7c b4 e0 1d 8e 76 a5 4e 55 19 30 65 64 51 3b 3d |
|
| 650 |
+ 00 00 00 1b 74 65 73 74 20 6b 65 79 20 77 69 |
|
| 651 |
+ 74 68 6f 75 74 20 70 61 73 73 70 68 72 61 73 65 |
|
| 652 |
+"""), |
|
| 653 |
+ public_key=rb"""ecdsa-sha2-nistp384 AAAAE2VjZHNhLXNoYTItbmlzdHAzODQAAAAIbmlzdHAzODQAAABhBKCQ6OQC+ru/m8e6PcoEvj8QBZyfmFkPIpxvJXR4EwYWruEpdCVmohqEtWp4xHRCqaTE0nauXLZUdxed6re9n718ixYI51iTlY/c1k/O/3XVefvBsSQLtCd0PnMqWbikFQ== test key without passphrase |
|
| 654 |
+""", |
|
| 655 |
+ public_key_data=bytes.fromhex("""
|
|
| 656 |
+ 00 00 00 13 65 63 64 |
|
| 657 |
+ 73 61 2d 73 68 61 32 2d 6e 69 73 74 70 33 38 34 |
|
| 658 |
+ 00 00 00 08 6e 69 73 74 70 33 38 34 |
|
| 659 |
+ 00 00 00 61 04 |
|
| 660 |
+ a0 90 e8 e4 02 fa bb bf 9b c7 ba 3d ca 04 be 3f |
|
| 661 |
+ 10 05 9c 9f 98 59 0f 22 9c 6f 25 74 78 13 06 16 |
|
| 662 |
+ ae e1 29 74 25 66 a2 1a 84 b5 6a 78 c4 74 42 a9 |
|
| 663 |
+ a4 c4 d2 76 ae 5c b6 54 77 17 9d ea b7 bd 9f bd |
|
| 664 |
+ 7c 8b 16 08 e7 58 93 95 8f dc d6 4f ce ff 75 d5 |
|
| 665 |
+ 79 fb c1 b1 24 0b b4 27 74 3e 73 2a 59 b8 a4 15 |
|
| 666 |
+"""), |
|
| 667 |
+ expected_signatures={
|
|
| 668 |
+ SSHTestKeyDeterministicSignatureClass.RFC_6979: SSHTestKeyDeterministicSignature( |
|
| 669 |
+ signature=bytes.fromhex("""
|
|
| 670 |
+ 00 00 00 13 65 63 64 |
|
| 671 |
+ 73 61 2d 73 68 61 32 2d 6e 69 73 74 70 33 38 34 |
|
| 672 |
+ 00 00 00 68 |
|
| 673 |
+ 00 00 00 30 |
|
| 674 |
+ 78 e1 a8 f5 8c d2 7a 21 e5 a2 ca e6 d0 1a 19 f8 |
|
| 675 |
+ 3a 1c 39 7e 71 a0 e6 7e 93 83 49 95 05 01 d0 3e |
|
| 676 |
+ 23 22 cd 09 63 7f 7c 6c b0 97 44 6d 7e 48 39 87 |
|
| 677 |
+ 00 00 00 30 |
|
| 678 |
+ 10 ee 85 51 77 2b 91 2c e9 42 79 66 59 8a a2 c0 |
|
| 679 |
+ d2 c8 8a 8f 2f 8f 33 87 9e 12 54 e4 da 02 f9 e7 |
|
| 680 |
+ 95 f5 82 6f 82 2b 38 6d 6e 5d 17 15 ac 12 e7 62 |
|
| 681 |
+"""), |
|
| 682 |
+ derived_passphrase=rb"""AAAAMHjhqPWM0noh5aLK5tAaGfg6HDl+caDmfpODSZUFAdA+IyLNCWN/fGywl0Rtfkg5hwAAADAQ7oVRdyuRLOlCeWZZiqLA0siKjy+PM4eeElTk2gL555X1gm+CKzhtbl0XFawS52I=""", |
|
| 683 |
+ signature_class=SSHTestKeyDeterministicSignatureClass.RFC_6979, |
|
| 684 |
+ ), |
|
| 685 |
+ SSHTestKeyDeterministicSignatureClass.Pageant_068_080: SSHTestKeyDeterministicSignature( |
|
| 686 |
+ signature=bytes.fromhex("""
|
|
| 687 |
+ 00 00 00 13 65 63 64 |
|
| 688 |
+ 73 61 2d 73 68 61 32 2d 6e 69 73 74 70 33 38 34 |
|
| 689 |
+ 00 00 00 69 |
|
| 690 |
+ 00 00 00 30 |
|
| 691 |
+ 4b 3e b7 22 c2 87 77 6d e0 3e f5 05 75 36 b6 0f |
|
| 692 |
+ cd 9f a4 49 c7 48 ef 76 fd ea 4b 49 e3 b1 f2 22 |
|
| 693 |
+ d5 41 22 d7 96 b2 29 70 ff bb 81 97 27 e2 35 60 |
|
| 694 |
+ 00 00 00 31 00 |
|
| 695 |
+ c8 a4 d8 62 fe f2 a6 63 97 98 08 c7 39 24 b2 55 |
|
| 696 |
+ 0a b8 e7 79 ab a6 62 96 3e cc ea 73 e2 fb dc 46 |
|
| 697 |
+ d6 25 b9 c8 0c e8 3e 33 91 51 78 25 a8 c5 46 85 |
|
| 698 |
+"""), |
|
| 699 |
+ derived_passphrase=rb"""AAAAMEs+tyLCh3dt4D71BXU2tg/Nn6RJx0jvdv3qS0njsfIi1UEi15ayKXD/u4GXJ+I1YAAAADEAyKTYYv7ypmOXmAjHOSSyVQq453mrpmKWPszqc+L73EbWJbnIDOg+M5FReCWoxUaF""", |
|
| 700 |
+ signature_class=SSHTestKeyDeterministicSignatureClass.Pageant_068_080, |
|
| 701 |
+ ), |
|
| 702 |
+ }, |
|
| 703 |
+ ), |
|
| 704 |
+ "ecdsa521": SSHTestKey( |
|
| 705 |
+ private_key=rb"""-----BEGIN OPENSSH PRIVATE KEY----- |
|
| 706 |
+b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAArAAAABNlY2RzYS |
|
| 707 |
+1zaGEyLW5pc3RwNTIxAAAACG5pc3RwNTIxAAAAhQQASVOdwDznmlcGqiLvFtYeVtrAEiVz |
|
| 708 |
+iIfsL7jEM8Utu/m8WSkPFQtjwqdFw+WfZ0mi6qMbEFgi/ELzZSKVteCSbcMAhqAkOMFKiD |
|
| 709 |
+u4bxvsM6bT02Ru7q2yT41ySyGhUD0QySBnI6Ckt/wnQ1TEpj8zDKiRErxs9e6QLGElNRkz |
|
| 710 |
+LPMs+mMAAAEY2FXeh9hV3ocAAAATZWNkc2Etc2hhMi1uaXN0cDUyMQAAAAhuaXN0cDUyMQ |
|
| 711 |
+AAAIUEAElTncA855pXBqoi7xbWHlbawBIlc4iH7C+4xDPFLbv5vFkpDxULY8KnRcPln2dJ |
|
| 712 |
+ouqjGxBYIvxC82UilbXgkm3DAIagJDjBSog7uG8b7DOm09Nkbu6tsk+NckshoVA9EMkgZy |
|
| 713 |
+OgpLf8J0NUxKY/MwyokRK8bPXukCxhJTUZMyzzLPpjAAAAQSFqUmKK7lGQzxT6GKZSLDju |
|
| 714 |
+U3otwLYnuj+/5AdzuB/zotu95UdFv9I2DNXzd9E4WAyz6IqBBNcsMkxrzHAdqsYDAAAAG3 |
|
| 715 |
+Rlc3Qga2V5IHdpdGhvdXQgcGFzc3BocmFzZQ== |
|
| 716 |
+-----END OPENSSH PRIVATE KEY----- |
|
| 717 |
+""", |
|
| 718 |
+ private_key_blob=bytes.fromhex("""
|
|
| 719 |
+ 00 00 00 13 65 63 64 |
|
| 720 |
+ 73 61 2d 73 68 61 32 2d 6e 69 73 74 70 35 32 31 |
|
| 721 |
+ 00 00 00 08 6e 69 73 74 70 35 32 31 |
|
| 722 |
+ 00 00 00 85 04 00 49 53 9d |
|
| 723 |
+ c0 3c e7 9a 57 06 aa 22 ef 16 d6 1e 56 da c0 12 |
|
| 724 |
+ 25 73 88 87 ec 2f b8 c4 33 c5 2d bb f9 bc 59 29 |
|
| 725 |
+ 0f 15 0b 63 c2 a7 45 c3 e5 9f 67 49 a2 ea a3 1b |
|
| 726 |
+ 10 58 22 fc 42 f3 65 22 95 b5 e0 92 6d c3 00 86 |
|
| 727 |
+ a0 24 38 c1 4a 88 3b b8 6f 1b ec 33 a6 d3 d3 64 |
|
| 728 |
+ 6e ee ad b2 4f 8d 72 4b 21 a1 50 3d 10 c9 20 67 |
|
| 729 |
+ 23 a0 a4 b7 fc 27 43 54 c4 a6 3f 33 0c a8 91 12 |
|
| 730 |
+ bc 6c f5 ee 90 2c 61 25 35 19 33 2c f3 2c fa 63 |
|
| 731 |
+ 00 00 00 41 21 |
|
| 732 |
+ 6a 52 62 8a ee 51 90 cf 14 fa 18 a6 52 2c 38 ee |
|
| 733 |
+ 53 7a 2d c0 b6 27 ba 3f bf e4 07 73 b8 1f f3 a2 |
|
| 734 |
+ db bd e5 47 45 bf d2 36 0c d5 f3 77 d1 38 58 0c |
|
| 735 |
+ b3 e8 8a 81 04 d7 2c 32 4c 6b cc 70 1d aa c6 03 |
|
| 736 |
+ 00 00 00 1b 74 65 73 74 20 6b 65 79 20 77 69 |
|
| 737 |
+ 74 68 6f 75 74 20 70 61 73 73 70 68 72 61 73 65 |
|
| 738 |
+"""), |
|
| 739 |
+ public_key=rb"""ecdsa-sha2-nistp521 AAAAE2VjZHNhLXNoYTItbmlzdHA1MjEAAAAIbmlzdHA1MjEAAACFBABJU53APOeaVwaqIu8W1h5W2sASJXOIh+wvuMQzxS27+bxZKQ8VC2PCp0XD5Z9nSaLqoxsQWCL8QvNlIpW14JJtwwCGoCQ4wUqIO7hvG+wzptPTZG7urbJPjXJLIaFQPRDJIGcjoKS3/CdDVMSmPzMMqJESvGz17pAsYSU1GTMs8yz6Yw== test key without passphrase |
|
| 740 |
+""", |
|
| 741 |
+ public_key_data=bytes.fromhex("""
|
|
| 742 |
+ 00 00 00 13 65 63 64 |
|
| 743 |
+ 73 61 2d 73 68 61 32 2d 6e 69 73 74 70 35 32 31 |
|
| 744 |
+ 00 00 00 08 6e 69 73 74 70 35 32 31 |
|
| 745 |
+ 00 00 00 85 04 00 49 53 9d |
|
| 746 |
+ c0 3c e7 9a 57 06 aa 22 ef 16 d6 1e 56 da c0 12 |
|
| 747 |
+ 25 73 88 87 ec 2f b8 c4 33 c5 2d bb f9 bc 59 29 |
|
| 748 |
+ 0f 15 0b 63 c2 a7 45 c3 e5 9f 67 49 a2 ea a3 1b |
|
| 749 |
+ 10 58 22 fc 42 f3 65 22 95 b5 e0 92 6d c3 00 86 |
|
| 750 |
+ a0 24 38 c1 4a 88 3b b8 6f 1b ec 33 a6 d3 d3 64 |
|
| 751 |
+ 6e ee ad b2 4f 8d 72 4b 21 a1 50 3d 10 c9 20 67 |
|
| 752 |
+ 23 a0 a4 b7 fc 27 43 54 c4 a6 3f 33 0c a8 91 12 |
|
| 753 |
+ bc 6c f5 ee 90 2c 61 25 35 19 33 2c f3 2c fa 63 |
|
| 754 |
+"""), |
|
| 755 |
+ expected_signatures={
|
|
| 756 |
+ SSHTestKeyDeterministicSignatureClass.RFC_6979: SSHTestKeyDeterministicSignature( |
|
| 757 |
+ signature=bytes.fromhex("""
|
|
| 758 |
+ 00 00 00 13 65 63 64 |
|
| 759 |
+ 73 61 2d 73 68 61 32 2d 6e 69 73 74 70 35 32 31 |
|
| 760 |
+ 00 00 00 8b |
|
| 761 |
+ 00 00 00 42 01 d8 |
|
| 762 |
+ ea c2 1e 55 c6 9e dd 4b 00 ed 1b 93 19 cc 9b 74 |
|
| 763 |
+ 27 44 c0 c0 e3 5b 3d 81 15 00 12 cc 07 89 54 97 |
|
| 764 |
+ ec 60 42 ad e6 40 c1 c6 5f c0 1b c3 0a 8e 58 6e |
|
| 765 |
+ da 3f a9 57 90 04 79 46 1d 48 bb 19 67 e9 65 19 |
|
| 766 |
+ 00 00 00 41 7d |
|
| 767 |
+ 58 e0 2e d7 86 2e 36 8c 1a 44 23 af 19 e7 51 97 |
|
| 768 |
+ bb fb 32 90 a1 35 bb 88 d7 b5 22 37 b3 99 ba e4 |
|
| 769 |
+ a7 9d 2d 56 14 0a f5 68 f5 cc 38 84 e9 b6 c6 71 |
|
| 770 |
+ 7a 3b 87 e7 7a b1 37 e7 1d e6 80 96 d1 a6 1e bc |
|
| 771 |
+"""), |
|
| 772 |
+ derived_passphrase=rb"""AAAAQgHY6sIeVcae3UsA7RuTGcybdCdEwMDjWz2BFQASzAeJVJfsYEKt5kDBxl/AG8MKjlhu2j+pV5AEeUYdSLsZZ+llGQAAAEF9WOAu14YuNowaRCOvGedRl7v7MpChNbuI17UiN7OZuuSnnS1WFAr1aPXMOITptsZxejuH53qxN+cd5oCW0aYevA==""", |
|
| 773 |
+ signature_class=SSHTestKeyDeterministicSignatureClass.RFC_6979, |
|
| 774 |
+ ), |
|
| 775 |
+ SSHTestKeyDeterministicSignatureClass.Pageant_068_080: SSHTestKeyDeterministicSignature( |
|
| 776 |
+ signature=bytes.fromhex("""
|
|
| 777 |
+ 00 00 00 13 65 63 64 |
|
| 778 |
+ 73 61 2d 73 68 61 32 2d 6e 69 73 74 70 35 32 31 |
|
| 779 |
+ 00 00 00 8c |
|
| 780 |
+ 00 00 00 42 01 ce |
|
| 781 |
+ fe 9d 66 b6 01 76 2e 86 c2 ab 68 62 73 44 05 23 |
|
| 782 |
+ fd d1 79 07 fc 45 f5 c0 83 36 88 61 d4 04 79 90 |
|
| 783 |
+ b0 ef 8b 3c b5 55 0e cc 26 6b a0 3e 6a 04 48 ca |
|
| 784 |
+ e4 6a a5 a0 cf 91 5f 71 6f 37 9a 0f 6b a9 fb 9b |
|
| 785 |
+ 00 00 00 42 01 6d |
|
| 786 |
+ 21 77 c6 13 fa ea ac de 90 19 24 5a d2 61 39 d9 |
|
| 787 |
+ 66 9b 86 1a 41 04 58 a2 9b b8 93 b6 6f 82 23 f2 |
|
| 788 |
+ 01 23 c7 ff 5a d3 86 95 0f da 28 f9 3b e3 9c 27 |
|
| 789 |
+ e7 b2 d7 66 4e 5f 38 36 4c 8c be 76 4e fa 0a 2d |
|
| 790 |
+"""), |
|
| 791 |
+ derived_passphrase=rb"""AAAAQgHO/p1mtgF2LobCq2hic0QFI/3ReQf8RfXAgzaIYdQEeZCw74s8tVUOzCZroD5qBEjK5GqloM+RX3FvN5oPa6n7mwAAAEIBbSF3xhP66qzekBkkWtJhOdlmm4YaQQRYopu4k7ZvgiPyASPH/1rThpUP2ij5O+OcJ+ey12ZOXzg2TIy+dk76Ci0=""", |
|
| 792 |
+ signature_class=SSHTestKeyDeterministicSignatureClass.Pageant_068_080, |
|
| 793 |
+ ), |
|
| 794 |
+ }, |
|
| 795 |
+ ), |
|
| 796 |
+} |
|
| 797 |
+"""The master list of SSH test keys.""" |
|
| 798 |
+SUPPORTED_KEYS: Mapping[str, SSHTestKey] = {
|
|
| 799 |
+ k: v |
|
| 800 |
+ for k, v in ALL_KEYS.items() |
|
| 801 |
+ if SSHTestKeyDeterministicSignatureClass.SPEC |
|
| 802 |
+ in v.expected_signatures |
|
| 803 |
+} |
|
| 804 |
+"""The subset of SSH test keys suitable for use with vault. |
|
| 805 |
+ |
|
| 806 |
+Suitability is tested via the presence the `SPEC` expected signature |
|
| 807 |
+class. |
|
| 808 |
+ |
|
| 809 |
+""" |
|
| 810 |
+CONDITIONALLY_SUPPORTED_KEYS: Mapping[str, SSHTestKey] = {
|
|
| 811 |
+ k: v |
|
| 812 |
+ for k, v in ALL_KEYS.items() |
|
| 813 |
+ if k not in SUPPORTED_KEYS |
|
| 814 |
+ and ( |
|
| 815 |
+ SSHTestKeyDeterministicSignatureClass.RFC_6979 |
|
| 816 |
+ in v.expected_signatures |
|
| 817 |
+ or SSHTestKeyDeterministicSignatureClass.Pageant_068_080 |
|
| 818 |
+ in v.expected_signatures |
|
| 819 |
+ ) |
|
| 820 |
+} |
|
| 821 |
+"""The subset of SSH test keys suitable for use with vault. |
|
| 822 |
+ |
|
| 823 |
+Suitability is tested via the presence the `SPEC` expected signature |
|
| 824 |
+class. |
|
| 825 |
+ |
|
| 826 |
+""" |
|
| 827 |
+UNSUITABLE_KEYS: Mapping[str, SSHTestKey] = {
|
|
| 828 |
+ k: v |
|
| 829 |
+ for k, v in ALL_KEYS.items() |
|
| 830 |
+ if k not in SUPPORTED_KEYS and k not in CONDITIONALLY_SUPPORTED_KEYS |
|
| 831 |
+} |
|
| 832 |
+"""The subset of SSH test keys not suitable for use with vault. |
|
| 833 |
+ |
|
| 834 |
+Suitability is tested via the absence of the `SPEC`, `RFC_6979` and |
|
| 835 |
+`Pageant_068_080` expected signature classes. |
|
| 836 |
+ |
|
| 837 |
+""" |
| ... | ... |
@@ -0,0 +1,468 @@ |
| 1 |
+# SPDX-FileCopyrightText: 2026 Marco Ricci <software@the13thletter.info> |
|
| 2 |
+# |
|
| 3 |
+# SPDX-License-Identifier: Zlib |
|
| 4 |
+ |
|
| 5 |
+"""Machinery for the fake SSH agent.""" |
|
| 6 |
+ |
|
| 7 |
+from __future__ import annotations |
|
| 8 |
+ |
|
| 9 |
+import errno |
|
| 10 |
+import os |
|
| 11 |
+import struct |
|
| 12 |
+from typing import TYPE_CHECKING, cast |
|
| 13 |
+ |
|
| 14 |
+import derivepassphrase_sshagentsocketprovider as d_sasp |
|
| 15 |
+from fakesshagent import _types, data |
|
| 16 |
+ |
|
| 17 |
+if TYPE_CHECKING: |
|
| 18 |
+ from collections.abc import Generator, Iterable |
|
| 19 |
+ |
|
| 20 |
+ from typing_extensions import Buffer, Self |
|
| 21 |
+ |
|
| 22 |
+__all__ = ("StubbedSSHAgentSocket",)
|
|
| 23 |
+ |
|
| 24 |
+VAULT_UUID = b"e87eb0f4-34cb-46b9-93ad-766c5ab063e7" |
|
| 25 |
+uint32_format = struct.Struct(">I")
|
|
| 26 |
+ |
|
| 27 |
+ |
|
| 28 |
+def string(bstr: Buffer, /) -> bytes: |
|
| 29 |
+ """Return the serialized payload as per the SSH agent protocol.""" |
|
| 30 |
+ payload = memoryview(bstr) |
|
| 31 |
+ buffer = bytearray(payload) |
|
| 32 |
+ buffer[:0] = bytes(4) |
|
| 33 |
+ uint32_format.pack_into(buffer, 0, len(payload)) |
|
| 34 |
+ return bytes(buffer) |
|
| 35 |
+ |
|
| 36 |
+ |
|
| 37 |
+def uint32(i: int, /) -> bytes: |
|
| 38 |
+ """Return the serialized integer as per the SSH agent protocol.""" |
|
| 39 |
+ return uint32_format.pack(i) |
|
| 40 |
+ |
|
| 41 |
+ |
|
| 42 |
+def unstring_prefix(buf: Buffer, /) -> tuple[bytes, bytes]: |
|
| 43 |
+ """Decode a string encoded as per the SSH agent protocol. |
|
| 44 |
+ |
|
| 45 |
+ Any data remaining after the encoded string is returned unchanged. |
|
| 46 |
+ |
|
| 47 |
+ Args: |
|
| 48 |
+ buf: |
|
| 49 |
+ A binary buffer, beginning with an encoded string. |
|
| 50 |
+ |
|
| 51 |
+ Returns: |
|
| 52 |
+ The decoded string, and the remaining contents of the buffer, as |
|
| 53 |
+ a 2-tuple. |
|
| 54 |
+ |
|
| 55 |
+ Raises: |
|
| 56 |
+ ValueError: |
|
| 57 |
+ The encoded string is invalid or truncated. |
|
| 58 |
+ |
|
| 59 |
+ """ |
|
| 60 |
+ payload = memoryview(buf) |
|
| 61 |
+ pos = 0 |
|
| 62 |
+ try: |
|
| 63 |
+ size = cast("tuple[int]", uint32_format.unpack_from(payload, 0))[0]
|
|
| 64 |
+ except struct.error as exc: |
|
| 65 |
+ msg = f"Invalid or truncated SSH protocol string value: {buf!r}"
|
|
| 66 |
+ raise ValueError(msg) from exc |
|
| 67 |
+ pos += uint32_format.size |
|
| 68 |
+ head = bytes(payload[pos : pos + size]) |
|
| 69 |
+ if len(head) < size: |
|
| 70 |
+ msg = f"Invalid or truncated SSH protocol string value: {buf!r}"
|
|
| 71 |
+ raise ValueError(msg) |
|
| 72 |
+ tail = bytes(payload[pos + size :]) |
|
| 73 |
+ return head, tail |
|
| 74 |
+ |
|
| 75 |
+ |
|
| 76 |
+class StubbedSSHAgentSocket: |
|
| 77 |
+ """A stubbed SSH agent presenting an [`_types.SSHAgentSocket`][]. |
|
| 78 |
+ |
|
| 79 |
+ On the network protocol side, the agent implements the full |
|
| 80 |
+ [`_types.SSHAgentSocket`][] interface, including pipelined and |
|
| 81 |
+ unaligned agent requests. However, on the application side, the |
|
| 82 |
+ agent is intrinsically tied to [the set of SSH test |
|
| 83 |
+ keys][data.ALL_KEYS], and only gives meaningful answers for |
|
| 84 |
+ operations on the test keys and for agent operations in use by |
|
| 85 |
+ [`ssh_agent.SSHAgentClient`][]. The agent does not actually |
|
| 86 |
+ implement any cryptography; all cryptography-related answers are |
|
| 87 |
+ derived from the recorded test key data. |
|
| 88 |
+ |
|
| 89 |
+ It is not safe to further monkeypatch the agent's [`recv`][] or |
|
| 90 |
+ [`sendall`][] methods on their own: either monkeypatch both of them, |
|
| 91 |
+ or manipulate the [`send_to_client`][] bytes queue directly instead. |
|
| 92 |
+ Given an [`ssh_agent.SSHAgentClient`][] connected to |
|
| 93 |
+ a [`StubbedSSHAgentSocket`][], if the test ensures proper message |
|
| 94 |
+ serialization and protocol framing and if the monkeypatching can be |
|
| 95 |
+ expressed in terms of full request messages and full response |
|
| 96 |
+ messages, prefer using a [`AgentProtocolResponseQueue`][] to |
|
| 97 |
+ monkeypatch the client's high-level request/response-loop instead of |
|
| 98 |
+ monkeypatching the low-level socket communication in this agent |
|
| 99 |
+ socket. |
|
| 100 |
+ |
|
| 101 |
+ """ |
|
| 102 |
+ |
|
| 103 |
+ _NO_FLAG_SUPPORT = "This stubbed SSH agent socket does not support flags." |
|
| 104 |
+ _PROTOCOL_VIOLATION = "SSH agent protocol violation." |
|
| 105 |
+ _INVALID_REQUEST = "Invalid request." |
|
| 106 |
+ _UNSUPPORTED_REQUEST = "Unsupported request." |
|
| 107 |
+ _INCOMPLETE_REQUEST = "The last request was incomplete." |
|
| 108 |
+ |
|
| 109 |
+ HEADER_SIZE = 4 |
|
| 110 |
+ CODE_SIZE = 1 |
|
| 111 |
+ |
|
| 112 |
+ KNOWN_EXTENSIONS = frozenset({
|
|
| 113 |
+ "query", |
|
| 114 |
+ "list-extended@putty.projects.tartarus.org", |
|
| 115 |
+ }) |
|
| 116 |
+ """Known and implemented protocol extensions.""" |
|
| 117 |
+ |
|
| 118 |
+ def __init__(self, *extensions: str) -> None: |
|
| 119 |
+ """Initialize the agent.""" |
|
| 120 |
+ self.send_to_client = bytearray() |
|
| 121 |
+ """ |
|
| 122 |
+ The buffered response to the client, read piecemeal by [`recv`][]. |
|
| 123 |
+ """ |
|
| 124 |
+ self.receive_from_client = bytearray() |
|
| 125 |
+ """The last request issued by the client.""" |
|
| 126 |
+ self.closed = False |
|
| 127 |
+ """True if the connection is closed, false otherwise.""" |
|
| 128 |
+ self.enabled_extensions = frozenset(extensions) & self.KNOWN_EXTENSIONS |
|
| 129 |
+ """ |
|
| 130 |
+ Extensions actually enabled in this particular stubbed SSH agent. |
|
| 131 |
+ """ |
|
| 132 |
+ self.try_rfc6979 = False |
|
| 133 |
+ """ |
|
| 134 |
+ Attempt to issue DSA and ECDSA signatures according to RFC 6979? |
|
| 135 |
+ """ |
|
| 136 |
+ self.try_pageant_068_080 = False |
|
| 137 |
+ """ |
|
| 138 |
+ Attempt to issue DSA and ECDSA signatures as per Pageant 0.68–0.80? |
|
| 139 |
+ """ # noqa: RUF001 |
|
| 140 |
+ |
|
| 141 |
+ def __enter__(self) -> Self: |
|
| 142 |
+ """Return self.""" |
|
| 143 |
+ return self |
|
| 144 |
+ |
|
| 145 |
+ def __exit__(self, *args: object) -> None: |
|
| 146 |
+ """Mark the agent's socket as closed. |
|
| 147 |
+ |
|
| 148 |
+ Raises: |
|
| 149 |
+ AssertionError: |
|
| 150 |
+ The last request to the agent was incomplete. |
|
| 151 |
+ |
|
| 152 |
+ """ |
|
| 153 |
+ self.closed = True |
|
| 154 |
+ assert not self.receive_from_client, self._INCOMPLETE_REQUEST |
|
| 155 |
+ |
|
| 156 |
+ def sendall(self, data: Buffer, flags: int = 0, /) -> None: |
|
| 157 |
+ """Send data to the SSH agent. |
|
| 158 |
+ |
|
| 159 |
+ The signature, and behavior, is identical to |
|
| 160 |
+ [`socket.socket.sendall`][]. Upon successful sending, this |
|
| 161 |
+ agent will parse the request, call the appropriate handler, and |
|
| 162 |
+ buffer the result such that it can be read via [`recv`][], in |
|
| 163 |
+ accordance with the SSH agent protocol. |
|
| 164 |
+ |
|
| 165 |
+ Args: |
|
| 166 |
+ data: Binary data to send to the agent. |
|
| 167 |
+ flags: Reserved. Must be 0. |
|
| 168 |
+ |
|
| 169 |
+ Raises: |
|
| 170 |
+ AssertionError: |
|
| 171 |
+ The flags argument, if specified, must be 0. |
|
| 172 |
+ OSError: |
|
| 173 |
+ The socket connection is already closed. |
|
| 174 |
+ |
|
| 175 |
+ Note: |
|
| 176 |
+ The result should be requested via [`recv`][], and |
|
| 177 |
+ interpreted in accordance with the SSH agent protocol. |
|
| 178 |
+ |
|
| 179 |
+ """ |
|
| 180 |
+ assert not flags, self._NO_FLAG_SUPPORT |
|
| 181 |
+ self._check_for_io_on_closed_connection() |
|
| 182 |
+ self.receive_from_client.extend(memoryview(data)) |
|
| 183 |
+ while self.receive_from_client: |
|
| 184 |
+ result: Buffer | Iterable[int] |
|
| 185 |
+ if len(self.receive_from_client) < self.HEADER_SIZE: |
|
| 186 |
+ break |
|
| 187 |
+ count = int.from_bytes( |
|
| 188 |
+ self.receive_from_client[: self.HEADER_SIZE], |
|
| 189 |
+ "big", |
|
| 190 |
+ signed=False, |
|
| 191 |
+ ) |
|
| 192 |
+ if count: |
|
| 193 |
+ code = int.from_bytes( |
|
| 194 |
+ self.receive_from_client[ |
|
| 195 |
+ self.HEADER_SIZE : self.HEADER_SIZE + self.CODE_SIZE |
|
| 196 |
+ ], |
|
| 197 |
+ "big", |
|
| 198 |
+ signed=False, |
|
| 199 |
+ ) |
|
| 200 |
+ request = bytes( |
|
| 201 |
+ self.receive_from_client[: self.HEADER_SIZE + count] |
|
| 202 |
+ ) |
|
| 203 |
+ if len(request) < self.HEADER_SIZE + count: |
|
| 204 |
+ break |
|
| 205 |
+ request_payload = request[self.HEADER_SIZE + self.CODE_SIZE :] |
|
| 206 |
+ |
|
| 207 |
+ if code == _types.SSH_AGENTC.REQUEST_IDENTITIES: |
|
| 208 |
+ result = self.request_identities(list_extended=False) |
|
| 209 |
+ elif code == _types.SSH_AGENTC.SIGN_REQUEST: |
|
| 210 |
+ result = self.sign(request_payload) |
|
| 211 |
+ elif self._check_for_extension(code, "query"): |
|
| 212 |
+ result = self.query_extensions() |
|
| 213 |
+ elif self._check_for_extension( |
|
| 214 |
+ code, "list-extended@putty.projects.tartarus.org" |
|
| 215 |
+ ): |
|
| 216 |
+ result = self.request_identities(list_extended=True) |
|
| 217 |
+ else: |
|
| 218 |
+ result = self._failure() |
|
| 219 |
+ else: |
|
| 220 |
+ request = bytes(self.receive_from_client[: self.HEADER_SIZE]) |
|
| 221 |
+ result = self._failure() |
|
| 222 |
+ self.send_to_client.extend(string(bytes(result))) |
|
| 223 |
+ self.receive_from_client[: len(request)] = b"" |
|
| 224 |
+ |
|
| 225 |
+ def recv(self, count: int, flags: int = 0, /) -> bytes: |
|
| 226 |
+ """Read data from the SSH agent. |
|
| 227 |
+ |
|
| 228 |
+ As per the SSH agent protocol, data is only available to be read |
|
| 229 |
+ immediately after a request via [`sendall`][] and if the socket |
|
| 230 |
+ connection is still open. Calls to [`recv`][] at other points |
|
| 231 |
+ in time that attempt to read data violate the protocol, and will |
|
| 232 |
+ fail. (A [`recv`][] of zero bytes does not read data.) Calls |
|
| 233 |
+ to [`recv`][] when the socket connection is closed always fail. |
|
| 234 |
+ |
|
| 235 |
+ Args: |
|
| 236 |
+ count: |
|
| 237 |
+ Number of bytes to read from the agent. |
|
| 238 |
+ flags: |
|
| 239 |
+ Reserved. Must be 0. |
|
| 240 |
+ |
|
| 241 |
+ Returns: |
|
| 242 |
+ (A chunk of) the SSH agent's response to the most recent |
|
| 243 |
+ request. If reading 0 bytes, the returned chunk is always |
|
| 244 |
+ an empty byte string. |
|
| 245 |
+ |
|
| 246 |
+ Raises: |
|
| 247 |
+ AssertionError: |
|
| 248 |
+ The flags argument, if specified, must be 0. |
|
| 249 |
+ |
|
| 250 |
+ Alternatively, `recv` was called when there was no |
|
| 251 |
+ response to be obtained, in violation of the SSH agent |
|
| 252 |
+ protocol. |
|
| 253 |
+ OSError: |
|
| 254 |
+ The socket connection is already closed. |
|
| 255 |
+ |
|
| 256 |
+ """ |
|
| 257 |
+ assert not flags, self._NO_FLAG_SUPPORT |
|
| 258 |
+ self._check_for_io_on_closed_connection() |
|
| 259 |
+ assert not count or self.send_to_client, self._PROTOCOL_VIOLATION |
|
| 260 |
+ ret = bytes(self.send_to_client[:count]) |
|
| 261 |
+ del self.send_to_client[:count] |
|
| 262 |
+ return ret |
|
| 263 |
+ |
|
| 264 |
+ def _failure(self) -> bytes: # noqa: PLR6301 |
|
| 265 |
+ return bytes(_types.SSH_AGENT.FAILURE) |
|
| 266 |
+ |
|
| 267 |
+ def _check_for_io_on_closed_connection(self) -> None: |
|
| 268 |
+ if self.closed: |
|
| 269 |
+ raise OSError(errno.EBADF, os.strerror(errno.EBADF)) |
|
| 270 |
+ |
|
| 271 |
+ def _check_for_extension(self, code: int, extension: str) -> bool: |
|
| 272 |
+ if ( |
|
| 273 |
+ extension not in self.enabled_extensions |
|
| 274 |
+ or code != _types.SSH_AGENTC.EXTENSION |
|
| 275 |
+ ): |
|
| 276 |
+ return False |
|
| 277 |
+ extension_marker = b"\x1b" + string(extension.encode("ascii"))
|
|
| 278 |
+ return self.receive_from_client.startswith(extension_marker, 4) |
|
| 279 |
+ |
|
| 280 |
+ def query_extensions(self) -> Generator[int, None, None]: # noqa: PLR6301 |
|
| 281 |
+ """Answer an `SSH_AGENTC_EXTENSION` request. |
|
| 282 |
+ |
|
| 283 |
+ Yields: |
|
| 284 |
+ The bytes payload of the response, without the protocol |
|
| 285 |
+ framing. The payload is yielded byte by byte, as an |
|
| 286 |
+ iterable of 8-bit integers. |
|
| 287 |
+ |
|
| 288 |
+ """ |
|
| 289 |
+ yield _types.SSH_AGENT.EXTENSION_RESPONSE |
|
| 290 |
+ yield from string(b"query") |
|
| 291 |
+ extension_answers = [ |
|
| 292 |
+ b"query", |
|
| 293 |
+ b"list-extended@putty.projects.tartarus.org", |
|
| 294 |
+ ] |
|
| 295 |
+ for a in extension_answers: |
|
| 296 |
+ yield from string(a) |
|
| 297 |
+ |
|
| 298 |
+ def request_identities( |
|
| 299 |
+ self, *, list_extended: bool = False |
|
| 300 |
+ ) -> Generator[int, None, None]: |
|
| 301 |
+ """Answer an `SSH_AGENTC_REQUEST_IDENTITIES` request. |
|
| 302 |
+ |
|
| 303 |
+ Args: |
|
| 304 |
+ list_extended: |
|
| 305 |
+ If true, answer an `SSH_AGENTC_EXTENSION` request for |
|
| 306 |
+ the `list-extended@putty.projects.tartarus.org` |
|
| 307 |
+ extension. Otherwise, answer an |
|
| 308 |
+ `SSH_AGENTC_REQUEST_IDENTITIES` request. |
|
| 309 |
+ |
|
| 310 |
+ Yields: |
|
| 311 |
+ The bytes payload of the response, without the protocol |
|
| 312 |
+ framing. The payload is yielded byte by byte, as an |
|
| 313 |
+ iterable of 8-bit integers. |
|
| 314 |
+ |
|
| 315 |
+ """ |
|
| 316 |
+ if list_extended: |
|
| 317 |
+ yield _types.SSH_AGENT.SUCCESS |
|
| 318 |
+ else: |
|
| 319 |
+ yield _types.SSH_AGENT.IDENTITIES_ANSWER |
|
| 320 |
+ signature_classes = [ |
|
| 321 |
+ data.SSHTestKeyDeterministicSignatureClass.SPEC, |
|
| 322 |
+ ] |
|
| 323 |
+ if ( |
|
| 324 |
+ "list-extended@putty.projects.tartarus.org" |
|
| 325 |
+ in self.enabled_extensions |
|
| 326 |
+ ): |
|
| 327 |
+ signature_classes.append( |
|
| 328 |
+ data.SSHTestKeyDeterministicSignatureClass.RFC_6979 |
|
| 329 |
+ ) |
|
| 330 |
+ keys = [ |
|
| 331 |
+ v |
|
| 332 |
+ for v in data.ALL_KEYS.values() |
|
| 333 |
+ if any(cls in v.expected_signatures for cls in signature_classes) |
|
| 334 |
+ ] |
|
| 335 |
+ yield from uint32(len(keys)) |
|
| 336 |
+ for key in keys: |
|
| 337 |
+ yield from string(key.public_key_data) |
|
| 338 |
+ yield from string(b"test key without passphrase") |
|
| 339 |
+ if list_extended: |
|
| 340 |
+ yield from string(uint32(0)) |
|
| 341 |
+ |
|
| 342 |
+ def sign(self, request_payload: bytes, /) -> bytes: # noqa: PLR0911 |
|
| 343 |
+ """Answer an `SSH_AGENTC_SIGN_REQUEST` request. |
|
| 344 |
+ |
|
| 345 |
+ Args: |
|
| 346 |
+ request_payload: |
|
| 347 |
+ The data of the sign request, without the protocol |
|
| 348 |
+ framing or the request code. |
|
| 349 |
+ |
|
| 350 |
+ Returns: |
|
| 351 |
+ The bytes payload of the response, without the protocol |
|
| 352 |
+ framing. |
|
| 353 |
+ |
|
| 354 |
+ """ |
|
| 355 |
+ try_rfc6979 = ( |
|
| 356 |
+ "list-extended@putty.projects.tartarus.org" |
|
| 357 |
+ in self.enabled_extensions |
|
| 358 |
+ ) |
|
| 359 |
+ spec = data.SSHTestKeyDeterministicSignatureClass.SPEC |
|
| 360 |
+ rfc6979 = data.SSHTestKeyDeterministicSignatureClass.RFC_6979 |
|
| 361 |
+ try: |
|
| 362 |
+ key_blob, rest = unstring_prefix(request_payload) |
|
| 363 |
+ sign_data, rest = unstring_prefix(rest) |
|
| 364 |
+ except ValueError: |
|
| 365 |
+ return self._failure() |
|
| 366 |
+ if len(rest) != uint32_format.size: # flags are uint32 |
|
| 367 |
+ return self._failure() |
|
| 368 |
+ flags = int.from_bytes(rest, "big") |
|
| 369 |
+ if flags: |
|
| 370 |
+ return self._failure() |
|
| 371 |
+ if sign_data != VAULT_UUID: |
|
| 372 |
+ return self._failure() |
|
| 373 |
+ for key in data.ALL_KEYS.values(): |
|
| 374 |
+ if key.public_key_data == key_blob: |
|
| 375 |
+ if spec in key.expected_signatures: |
|
| 376 |
+ return int.to_bytes( |
|
| 377 |
+ _types.SSH_AGENT.SIGN_RESPONSE, 1, "big" |
|
| 378 |
+ ) + string(key.expected_signatures[spec].signature) |
|
| 379 |
+ if ( |
|
| 380 |
+ try_rfc6979 and rfc6979 in key.expected_signatures |
|
| 381 |
+ ): # pragma: no cover [external] |
|
| 382 |
+ return int.to_bytes( |
|
| 383 |
+ _types.SSH_AGENT.SIGN_RESPONSE, 1, "big" |
|
| 384 |
+ ) + string(key.expected_signatures[rfc6979].signature) |
|
| 385 |
+ return self._failure() |
|
| 386 |
+ return self._failure() |
|
| 387 |
+ |
|
| 388 |
+ |
|
| 389 |
+class StubbedSSHAgentSocketWithAddress(StubbedSSHAgentSocket): |
|
| 390 |
+ """A [`StubbedSSHAgentSocket`][] requiring a specific address.""" |
|
| 391 |
+ |
|
| 392 |
+ ADDRESS = "stub-ssh-agent:" |
|
| 393 |
+ """The correct address for connecting to this stubbed agent.""" |
|
| 394 |
+ |
|
| 395 |
+ def __init__(self, *extensions: str) -> None: |
|
| 396 |
+ """Initialize the agent, based on `SSH_AUTH_SOCK`. |
|
| 397 |
+ |
|
| 398 |
+ Socket addresses of the form `stub-ssh-agent:<errno_value>` will |
|
| 399 |
+ raise an [`OSError`][] (or the respective subclass) with the |
|
| 400 |
+ specified [`errno`][] value. For example, |
|
| 401 |
+ `stub-ssh-agent:EPERM` will raise a [`PermissionError`][]. |
|
| 402 |
+ |
|
| 403 |
+ Raises: |
|
| 404 |
+ KeyError: |
|
| 405 |
+ The `SSH_AUTH_SOCK` environment variable is not set. |
|
| 406 |
+ OSError: |
|
| 407 |
+ The address in `SSH_AUTH_SOCK` is unsuited. |
|
| 408 |
+ |
|
| 409 |
+ """ |
|
| 410 |
+ super().__init__(*extensions) |
|
| 411 |
+ try: |
|
| 412 |
+ orig_address = os.environ["SSH_AUTH_SOCK"] |
|
| 413 |
+ except KeyError as exc: |
|
| 414 |
+ msg = "SSH_AUTH_SOCK environment variable" |
|
| 415 |
+ raise KeyError(msg) from exc |
|
| 416 |
+ address = orig_address |
|
| 417 |
+ if not address.startswith(self.ADDRESS): |
|
| 418 |
+ address = self.ADDRESS + "ENOENT" |
|
| 419 |
+ errcode = address.removeprefix(self.ADDRESS) |
|
| 420 |
+ if errcode and not ( |
|
| 421 |
+ errcode.startswith("E") and hasattr(errno, errcode)
|
|
| 422 |
+ ): |
|
| 423 |
+ errcode = "EINVAL" |
|
| 424 |
+ if errcode: |
|
| 425 |
+ errno_val = getattr(errno, errcode) |
|
| 426 |
+ raise OSError(errno_val, os.strerror(errno_val), orig_address) |
|
| 427 |
+ |
|
| 428 |
+ |
|
| 429 |
+class StubbedSSHAgentSocketWithAddressAndDeterministicDSA( |
|
| 430 |
+ StubbedSSHAgentSocketWithAddress |
|
| 431 |
+): |
|
| 432 |
+ """A [`StubbedSSHAgentSocketWithAddress`][] supporting deterministic DSA.""" # noqa: E501 |
|
| 433 |
+ |
|
| 434 |
+ def __init__(self) -> None: |
|
| 435 |
+ """Initialize the agent. |
|
| 436 |
+ |
|
| 437 |
+ Set the supported extensions, and try issuing RFC 6979 and |
|
| 438 |
+ Pageant 0.68–0.80 DSA/ECDSA signatures, if possible. See the |
|
| 439 |
+ [superclass constructor][StubbedSSHAgentSocketWithAddress] for |
|
| 440 |
+ other details. |
|
| 441 |
+ |
|
| 442 |
+ Raises: |
|
| 443 |
+ KeyError: See superclass. |
|
| 444 |
+ OSError: See superclass. |
|
| 445 |
+ |
|
| 446 |
+ """ # noqa: RUF002 |
|
| 447 |
+ super().__init__("query", "list-extended@putty.projects.tartarus.org")
|
|
| 448 |
+ self.try_rfc6979 = True |
|
| 449 |
+ self.try_pageant_068_080 = True |
|
| 450 |
+ |
|
| 451 |
+ |
|
| 452 |
+STUB_AGENT_ENTRY_POINT = d_sasp.SSHAgentSocketProviderEntry( |
|
| 453 |
+ provider=StubbedSSHAgentSocket, |
|
| 454 |
+ key="stub_agent", |
|
| 455 |
+ aliases=(), |
|
| 456 |
+) |
|
| 457 |
+STUB_AGENT_WITH_ADDRESS_ENTRY_POINT = d_sasp.SSHAgentSocketProviderEntry( |
|
| 458 |
+ provider=StubbedSSHAgentSocketWithAddress, |
|
| 459 |
+ key="stub_agent_with_address", |
|
| 460 |
+ aliases=(), |
|
| 461 |
+) |
|
| 462 |
+STUB_AGENT_WITH_ADDRESS_AND_DETERMINISTIC_DSA_ENTRY_POINT = ( |
|
| 463 |
+ d_sasp.SSHAgentSocketProviderEntry( |
|
| 464 |
+ provider=StubbedSSHAgentSocketWithAddressAndDeterministicDSA, |
|
| 465 |
+ key="stub_agent_with_address_and_deterministic_dsa", |
|
| 466 |
+ aliases=(), |
|
| 467 |
+ ) |
|
| 468 |
+) |
| ... | ... |
@@ -0,0 +1,819 @@ |
| 1 |
+# SPDX-FileCopyrightText: 2026 Marco Ricci <software@the13thletter.info> |
|
| 2 |
+# |
|
| 3 |
+# SPDX-License-Identifier: Zlib |
|
| 4 |
+ |
|
| 5 |
+"""Tests for the test suite's data and machinery. |
|
| 6 |
+ |
|
| 7 |
+Currently, this entails testing [the SSH test keys][fakesshagent.ALL_KEYS] |
|
| 8 |
+for internal consistency, and testing the functionality of the [stubbed |
|
| 9 |
+SSH agent][fakesshagent.StubbedSSHAgentSocket], in all variations. |
|
| 10 |
+ |
|
| 11 |
+""" |
|
| 12 |
+ |
|
| 13 |
+from __future__ import annotations |
|
| 14 |
+ |
|
| 15 |
+import base64 |
|
| 16 |
+import contextlib |
|
| 17 |
+import errno |
|
| 18 |
+import math |
|
| 19 |
+import os |
|
| 20 |
+import pathlib |
|
| 21 |
+import re |
|
| 22 |
+from typing import TYPE_CHECKING |
|
| 23 |
+ |
|
| 24 |
+import hypothesis |
|
| 25 |
+import pytest |
|
| 26 |
+from hypothesis import strategies |
|
| 27 |
+ |
|
| 28 |
+from fakesshagent import _types, data, machinery |
|
| 29 |
+ |
|
| 30 |
+if TYPE_CHECKING: |
|
| 31 |
+ from collections.abc import Generator |
|
| 32 |
+ |
|
| 33 |
+ from typing_extensions import Buffer |
|
| 34 |
+ |
|
| 35 |
+OPENSSH_MAGIC = b"openssh-key-v1\x00" |
|
| 36 |
+OPENSSH_HEADER = ( |
|
| 37 |
+ OPENSSH_MAGIC # magic |
|
| 38 |
+ + b"\x00\x00\x00\x04none" # ciphername |
|
| 39 |
+ + b"\x00\x00\x00\x04none" # kdfname |
|
| 40 |
+ + b"\x00\x00\x00\x00" # kdfoptions |
|
| 41 |
+ + b"\x00\x00\x00\x01" # number of keys |
|
| 42 |
+) |
|
| 43 |
+OPENSSH_NONE_CIPHER_BLOCKSIZE = 8 |
|
| 44 |
+ |
|
| 45 |
+ |
|
| 46 |
+def as_openssh_keyfile_payload( |
|
| 47 |
+ public_key: bytes, private_key: bytes, checkint: int |
|
| 48 |
+) -> bytes: |
|
| 49 |
+ """Format an SSH private key in OpenSSH format. |
|
| 50 |
+ |
|
| 51 |
+ Args: |
|
| 52 |
+ public_key: |
|
| 53 |
+ The unframed public key, in SSH wire format. |
|
| 54 |
+ private_key: |
|
| 55 |
+ The unframed private key, in SSH wire format, including the |
|
| 56 |
+ comment. |
|
| 57 |
+ checkint: |
|
| 58 |
+ The "check" integer to use. |
|
| 59 |
+ |
|
| 60 |
+ Returns: |
|
| 61 |
+ The payload for a formatted OpenSSH private key, as a byte |
|
| 62 |
+ string, without the base64 encoding and the framing lines. |
|
| 63 |
+ |
|
| 64 |
+ """ |
|
| 65 |
+ # The OpenSSH private key file format is described in PROTOCOL.key |
|
| 66 |
+ # in their git repository; see below for links to OpenSSH 10.0p2. |
|
| 67 |
+ # The block size of the "none" cipher is 8 bytes; see line 108 of |
|
| 68 |
+ # cipher.c, with definitions from line 67 onwards. Padding is not |
|
| 69 |
+ # used if the payload already is a multiple of 8 bytes long; see |
|
| 70 |
+ # line 2935 onwards of sshkey.c |
|
| 71 |
+ # |
|
| 72 |
+ # https://github.com/openssh/openssh-portable/raw/2593769fb291fe6c542173927698c69e9f9a08b9/PROTOCOL.key |
|
| 73 |
+ # https://github.com/openssh/openssh-portable/raw/2593769fb291fe6c542173927698c69e9f9a08b9/cipher.c |
|
| 74 |
+ # https://github.com/openssh/openssh-portable/raw/2593769fb291fe6c542173927698c69e9f9a08b9/sshkey.c |
|
| 75 |
+ string = machinery.string |
|
| 76 |
+ uint32 = machinery.uint32 |
|
| 77 |
+ payload = bytearray(OPENSSH_HEADER) |
|
| 78 |
+ payload.extend(string(public_key)) |
|
| 79 |
+ secret = bytearray() |
|
| 80 |
+ secret.extend(uint32(checkint)) # checkint |
|
| 81 |
+ secret.extend(uint32(checkint)) # checkint |
|
| 82 |
+ secret.extend(private_key) # privatekey1 and comment1 |
|
| 83 |
+ i = 1 |
|
| 84 |
+ while len(secret) % OPENSSH_NONE_CIPHER_BLOCKSIZE != 0: |
|
| 85 |
+ secret.append(i) |
|
| 86 |
+ i += 1 |
|
| 87 |
+ payload.extend(string(secret)) # encrypted, padded list of private keys |
|
| 88 |
+ return bytes(payload) |
|
| 89 |
+ |
|
| 90 |
+ |
|
| 91 |
+def minimize_openssh_keyfile_padding( |
|
| 92 |
+ decoded_openssh_private_key: bytes, |
|
| 93 |
+) -> bytes: |
|
| 94 |
+ """Minimize the padding used in an OpenSSH private key file. |
|
| 95 |
+ |
|
| 96 |
+ Args: |
|
| 97 |
+ decoded_openssh_private_key: |
|
| 98 |
+ The non-base64-encoded, unframed, formatted OpenSSH private |
|
| 99 |
+ key. |
|
| 100 |
+ |
|
| 101 |
+ Returns: |
|
| 102 |
+ The same non-base64-encoded, unframed, formatted OpenSSH private |
|
| 103 |
+ key, but with minimal padding applied. |
|
| 104 |
+ |
|
| 105 |
+ """ |
|
| 106 |
+ string = machinery.string |
|
| 107 |
+ unstring_prefix = machinery.unstring_prefix |
|
| 108 |
+ |
|
| 109 |
+ _public_key, framed_private_block = unstring_prefix( |
|
| 110 |
+ decoded_openssh_private_key.removeprefix(OPENSSH_HEADER) |
|
| 111 |
+ ) |
|
| 112 |
+ result = bytearray(decoded_openssh_private_key).removesuffix( |
|
| 113 |
+ framed_private_block |
|
| 114 |
+ ) |
|
| 115 |
+ private_block, trailer = unstring_prefix(framed_private_block) |
|
| 116 |
+ assert not trailer |
|
| 117 |
+ |
|
| 118 |
+ # Skip two checkint values. |
|
| 119 |
+ key_type, remainder = unstring_prefix(private_block[8:]) |
|
| 120 |
+ # We need to semi-generically skip private key payloads. Currently, |
|
| 121 |
+ # all supported (test) key types exclusively store multi-precision |
|
| 122 |
+ # integers or strings as their private key payload (which are both |
|
| 123 |
+ # parsed the same way, but interpreted differently). We can |
|
| 124 |
+ # therefore generically parse `k` strings/mpints (for different |
|
| 125 |
+ # values of `k`, depending on key type) to correctly skip the |
|
| 126 |
+ # private key payload, and don't have to deal with having to parse |
|
| 127 |
+ # and skip other types of data such as uint32s. |
|
| 128 |
+ # |
|
| 129 |
+ # (This scheme needs updating if ever a different data type needs to |
|
| 130 |
+ # be parsed.) |
|
| 131 |
+ num_mpints = {
|
|
| 132 |
+ b"ssh-ed25519": 2, |
|
| 133 |
+ b"ssh-ed448": 2, |
|
| 134 |
+ b"ssh-rsa": 6, |
|
| 135 |
+ b"ssh-dss": 5, |
|
| 136 |
+ b"ecdsa-sha2-nistp256": 3, |
|
| 137 |
+ b"ecdsa-sha2-nistp384": 3, |
|
| 138 |
+ b"ecdsa-sha2-nistp521": 3, |
|
| 139 |
+ } |
|
| 140 |
+ for _ in range(num_mpints[key_type]): |
|
| 141 |
+ _, remainder = unstring_prefix(remainder) |
|
| 142 |
+ # Skip comment. |
|
| 143 |
+ _comment, remainder = unstring_prefix(remainder) |
|
| 144 |
+ new_private_block = bytearray(private_block).removesuffix(remainder) |
|
| 145 |
+ padding = bytearray(remainder) |
|
| 146 |
+ |
|
| 147 |
+ expected_padding = bytearray() |
|
| 148 |
+ for i in range(1, len(padding) + 1): |
|
| 149 |
+ expected_padding.append(i & 0xFF) |
|
| 150 |
+ assert padding == expected_padding |
|
| 151 |
+ while len(padding) >= OPENSSH_NONE_CIPHER_BLOCKSIZE: |
|
| 152 |
+ padding[-OPENSSH_NONE_CIPHER_BLOCKSIZE:] = b"" |
|
| 153 |
+ |
|
| 154 |
+ new_private_block.extend(padding) |
|
| 155 |
+ result.extend(string(new_private_block)) |
|
| 156 |
+ return bytes(result) |
|
| 157 |
+ |
|
| 158 |
+ |
|
| 159 |
+class Parametrize: |
|
| 160 |
+ """Common test parametrizations.""" |
|
| 161 |
+ |
|
| 162 |
+ LIST_IDENTITIES = pytest.mark.parametrize( |
|
| 163 |
+ ["extended_agent", "query_request"], |
|
| 164 |
+ [ |
|
| 165 |
+ pytest.param( |
|
| 166 |
+ None, |
|
| 167 |
+ ( |
|
| 168 |
+ # SSH string header |
|
| 169 |
+ b"\x00\x00\x00\x01" |
|
| 170 |
+ # request code: SSH_AGENTC_REQUEST_IDENTITIES |
|
| 171 |
+ b"\x0b" |
|
| 172 |
+ ), |
|
| 173 |
+ id="base", |
|
| 174 |
+ ), |
|
| 175 |
+ pytest.param( |
|
| 176 |
+ True, |
|
| 177 |
+ ( |
|
| 178 |
+ # SSH string header |
|
| 179 |
+ b"\x00\x00\x00\x2e" |
|
| 180 |
+ # request code: SSH_AGENTC_REQUEST_IDENTITIES |
|
| 181 |
+ b"\x1b" |
|
| 182 |
+ # extension type: list-extended@putty.projects.tartarus.org |
|
| 183 |
+ b"\x00\x00\x00\x29list-extended@putty.projects.tartarus.org" |
|
| 184 |
+ # (no payload) |
|
| 185 |
+ ), |
|
| 186 |
+ id="extended", |
|
| 187 |
+ ), |
|
| 188 |
+ ], |
|
| 189 |
+ ) |
|
| 190 |
+ QUERY_EXTENSION = pytest.mark.parametrize( |
|
| 191 |
+ ["extended_agent", "query_response"], |
|
| 192 |
+ [ |
|
| 193 |
+ pytest.param( |
|
| 194 |
+ None, |
|
| 195 |
+ ( |
|
| 196 |
+ # SSH string header |
|
| 197 |
+ b"\x00\x00\x00\x01" |
|
| 198 |
+ # response code: SSH_AGENT_FAILURE |
|
| 199 |
+ b"\x05" |
|
| 200 |
+ ), |
|
| 201 |
+ id="base", |
|
| 202 |
+ ), |
|
| 203 |
+ pytest.param( |
|
| 204 |
+ True, |
|
| 205 |
+ ( |
|
| 206 |
+ # SSH string header |
|
| 207 |
+ b"\x00\x00\x00\x40" |
|
| 208 |
+ # response code: SSH_AGENT_EXTENSION_RESPONSE |
|
| 209 |
+ b"\x1d" |
|
| 210 |
+ # extension response: extension type ("query")
|
|
| 211 |
+ b"\x00\x00\x00\x05query" |
|
| 212 |
+ # supported extension #1: query |
|
| 213 |
+ b"\x00\x00\x00\x05query" |
|
| 214 |
+ # supported extension #2: |
|
| 215 |
+ # list-extended@putty.projects.tartarus.org |
|
| 216 |
+ b"\x00\x00\x00\x29list-extended@putty.projects.tartarus.org" |
|
| 217 |
+ ), |
|
| 218 |
+ id="extended", |
|
| 219 |
+ ), |
|
| 220 |
+ ], |
|
| 221 |
+ ) |
|
| 222 |
+ TEST_KEYS = pytest.mark.parametrize( |
|
| 223 |
+ ["keyname", "key"], |
|
| 224 |
+ data.ALL_KEYS.items(), |
|
| 225 |
+ ids=data.ALL_KEYS.keys(), |
|
| 226 |
+ ) |
|
| 227 |
+ SUPPORTED_SSH_TEST_KEYS = pytest.mark.parametrize( |
|
| 228 |
+ ["ssh_test_key_type", "ssh_test_key", "requires_deterministic_dsa"], |
|
| 229 |
+ [(k, v, False) for k, v in data.SUPPORTED_KEYS.items()] |
|
| 230 |
+ + [(k, v, True) for k, v in data.CONDITIONALLY_SUPPORTED_KEYS.items()], |
|
| 231 |
+ ids=list(data.SUPPORTED_KEYS.keys()) |
|
| 232 |
+ + list(data.CONDITIONALLY_SUPPORTED_KEYS.keys()), |
|
| 233 |
+ ) |
|
| 234 |
+ |
|
| 235 |
+ |
|
| 236 |
+class Strategies: |
|
| 237 |
+ """Common hypothesis data generation strategies.""" |
|
| 238 |
+ |
|
| 239 |
+ @strategies.composite |
|
| 240 |
+ @staticmethod |
|
| 241 |
+ def proper_bytestring_partition( |
|
| 242 |
+ draw: strategies.DrawFn, bs: bytes | bytearray, / |
|
| 243 |
+ ) -> list[bytes]: |
|
| 244 |
+ """Partition a non-empty string into non-empty parts. |
|
| 245 |
+ |
|
| 246 |
+ A string of length `n` is partitioned [`math.isqrt(n |
|
| 247 |
+ - 1)`][math.isqrt] many times, such that each substring is |
|
| 248 |
+ non-empty. |
|
| 249 |
+ |
|
| 250 |
+ """ |
|
| 251 |
+ n = len(bs) |
|
| 252 |
+ hypothesis.assume(n > 0) |
|
| 253 |
+ num_divisions = math.isqrt(n - 1) |
|
| 254 |
+ divisions = draw( |
|
| 255 |
+ strategies.lists( |
|
| 256 |
+ strategies.integers(1, n - 1), |
|
| 257 |
+ min_size=num_divisions, |
|
| 258 |
+ max_size=num_divisions, |
|
| 259 |
+ unique=True, |
|
| 260 |
+ ).map(sorted), |
|
| 261 |
+ label="divisions", |
|
| 262 |
+ ) |
|
| 263 |
+ result: list[bytes] = [] |
|
| 264 |
+ i = 0 |
|
| 265 |
+ for div in divisions: |
|
| 266 |
+ result.append(bytes(bs[i:div])) |
|
| 267 |
+ i = div |
|
| 268 |
+ result.append(bytes(bs[i:n])) |
|
| 269 |
+ return result |
|
| 270 |
+ |
|
| 271 |
+ @strategies.composite |
|
| 272 |
+ @staticmethod |
|
| 273 |
+ def truncated_agent_request( |
|
| 274 |
+ draw: strategies.DrawFn, /, min_size: int = 0, max_size: int = 100 |
|
| 275 |
+ ) -> bytes: |
|
| 276 |
+ """Generate a private-use SSH agent request with truncated payload. |
|
| 277 |
+ |
|
| 278 |
+ The message will adhere to specified size bounds before |
|
| 279 |
+ truncation. The request message code will be from the private |
|
| 280 |
+ use area. |
|
| 281 |
+ |
|
| 282 |
+ Args: |
|
| 283 |
+ min_size: |
|
| 284 |
+ The minimum size of the request payload, after truncation. |
|
| 285 |
+ max_size: |
|
| 286 |
+ The maximum size of the request payload, before truncation. |
|
| 287 |
+ |
|
| 288 |
+ """ |
|
| 289 |
+ request_code = draw( |
|
| 290 |
+ strategies.integers(240, 255), label="request_code" |
|
| 291 |
+ ) |
|
| 292 |
+ full_message_size = draw( |
|
| 293 |
+ strategies.integers(min_size + 1, max_size), |
|
| 294 |
+ label="full_message_size", |
|
| 295 |
+ ) |
|
| 296 |
+ truncated_message = draw( |
|
| 297 |
+ strategies.binary( |
|
| 298 |
+ min_size=min_size, max_size=full_message_size - 1 |
|
| 299 |
+ ) |
|
| 300 |
+ ) |
|
| 301 |
+ payload = bytearray( |
|
| 302 |
+ int.to_bytes(1 + full_message_size, 4, "big", signed=False) |
|
| 303 |
+ ) |
|
| 304 |
+ payload.extend(int.to_bytes(request_code, 1, "big", signed=False)) |
|
| 305 |
+ payload.extend(truncated_message) |
|
| 306 |
+ return bytes(payload) |
|
| 307 |
+ |
|
| 308 |
+ @staticmethod |
|
| 309 |
+ def invalid_ssh_agent_messages() -> strategies.SearchStrategy[bytes]: |
|
| 310 |
+ """Generate invalid agent request messages.""" |
|
| 311 |
+ string = machinery.string |
|
| 312 |
+ empty_message = [b""] |
|
| 313 |
+ invalid_extension_name = [b"\x1b", b"\x00\x00\x00\x01\xff"] |
|
| 314 |
+ sign_with_trailing_data = [ |
|
| 315 |
+ b"\x0d", |
|
| 316 |
+ b"\x00\x00\x00\x00", |
|
| 317 |
+ b"\x00\x00\x00\x00", |
|
| 318 |
+ b"\x00\x00\x00\x00", |
|
| 319 |
+ b"\x00\x00\x00\x00", |
|
| 320 |
+ ] |
|
| 321 |
+ sign_without_payload = [b"\x0d", b"\x00\x00\x00\x00"] |
|
| 322 |
+ sign_with_truncated_payload = [b"\x0d", b"\x00\x00\x00\x10"] |
|
| 323 |
+ return ( |
|
| 324 |
+ strategies |
|
| 325 |
+ .sampled_from([ |
|
| 326 |
+ empty_message, |
|
| 327 |
+ invalid_extension_name, |
|
| 328 |
+ sign_with_trailing_data, |
|
| 329 |
+ sign_without_payload, |
|
| 330 |
+ sign_with_truncated_payload, |
|
| 331 |
+ ]) |
|
| 332 |
+ .map(b"".join) |
|
| 333 |
+ .map(string) |
|
| 334 |
+ ) |
|
| 335 |
+ |
|
| 336 |
+ @staticmethod |
|
| 337 |
+ def unsupported_ssh_agent_messages() -> strategies.SearchStrategy[bytes]: |
|
| 338 |
+ """Generate agent request messages unsupported by the stubbed agent.""" |
|
| 339 |
+ string = machinery.string |
|
| 340 |
+ sign_with_flags = [ |
|
| 341 |
+ b"\x0d", |
|
| 342 |
+ string(data.ALL_KEYS["rsa"].public_key_data), |
|
| 343 |
+ string(machinery.VAULT_UUID), |
|
| 344 |
+ b"\x00\x00\x00\x02", |
|
| 345 |
+ ] |
|
| 346 |
+ sign_with_nonstandard_passphrase = [ |
|
| 347 |
+ b"\x0d", |
|
| 348 |
+ string(data.ALL_KEYS["ed25519"].public_key_data), |
|
| 349 |
+ b"\x00\x00\x00\x08\x00\x01\x02\x03\x04\x05\x06\x07", |
|
| 350 |
+ b"\x00\x00\x00\x00", |
|
| 351 |
+ ] |
|
| 352 |
+ # NOTE: only unsupported when stubbed agent has RFC 6979 support |
|
| 353 |
+ # disabled |
|
| 354 |
+ sign_key_no_expected_signature = [ |
|
| 355 |
+ b"\x0d", |
|
| 356 |
+ string(data.ALL_KEYS["dsa1024"].public_key_data), |
|
| 357 |
+ string(machinery.VAULT_UUID), |
|
| 358 |
+ b"\x00\x00\x00\x00", |
|
| 359 |
+ ] |
|
| 360 |
+ sign_key_unregistered_test_key = [ |
|
| 361 |
+ b"\x0d", |
|
| 362 |
+ b"\x00\x00\x00\x00", |
|
| 363 |
+ string(machinery.VAULT_UUID), |
|
| 364 |
+ b"\x00\x00\x00\x00", |
|
| 365 |
+ ] |
|
| 366 |
+ return ( |
|
| 367 |
+ strategies |
|
| 368 |
+ .sampled_from([ |
|
| 369 |
+ sign_with_flags, |
|
| 370 |
+ sign_with_nonstandard_passphrase, |
|
| 371 |
+ sign_key_no_expected_signature, |
|
| 372 |
+ sign_key_unregistered_test_key, |
|
| 373 |
+ ]) |
|
| 374 |
+ .map(b"".join) |
|
| 375 |
+ .map(string) |
|
| 376 |
+ ) |
|
| 377 |
+ |
|
| 378 |
+ @staticmethod |
|
| 379 |
+ def stubbed_agent_addresses() -> strategies.SearchStrategy[ |
|
| 380 |
+ tuple[str, type[OSError], str] |
|
| 381 |
+ ]: |
|
| 382 |
+ """Generate agent addresses for the stubbed agents. |
|
| 383 |
+ |
|
| 384 |
+ These are all error calls. Non-error calls are supplied by the |
|
| 385 |
+ explicit examples. |
|
| 386 |
+ |
|
| 387 |
+ """ |
|
| 388 |
+ invalid_url = ( |
|
| 389 |
+ str(pathlib.Path("~").expanduser()),
|
|
| 390 |
+ FileNotFoundError, |
|
| 391 |
+ os.strerror(errno.ENOENT), |
|
| 392 |
+ ) |
|
| 393 |
+ protocol_not_supported = ( |
|
| 394 |
+ "stub-ssh-agent:EPROTONOSUPPORT", |
|
| 395 |
+ OSError, |
|
| 396 |
+ os.strerror(errno.EPROTONOSUPPORT), |
|
| 397 |
+ ) |
|
| 398 |
+ invalid_error_code = ( |
|
| 399 |
+ "stub-ssh-agent:ABCDEFGHIJKLMNOPQRSTUVWXYZ", |
|
| 400 |
+ OSError, |
|
| 401 |
+ os.strerror(errno.EINVAL), |
|
| 402 |
+ ) |
|
| 403 |
+ return strategies.sampled_from([ |
|
| 404 |
+ invalid_url, |
|
| 405 |
+ protocol_not_supported, |
|
| 406 |
+ invalid_error_code, |
|
| 407 |
+ ]) |
|
| 408 |
+ |
|
| 409 |
+ |
|
| 410 |
+class TestTestKeys: |
|
| 411 |
+ """Tests testing the test keys.""" |
|
| 412 |
+ |
|
| 413 |
+ @Parametrize.TEST_KEYS |
|
| 414 |
+ def test_public_keys_are_internally_consistent( |
|
| 415 |
+ self, |
|
| 416 |
+ keyname: str, |
|
| 417 |
+ key: data.SSHTestKey, |
|
| 418 |
+ ) -> None: |
|
| 419 |
+ """The public key data structures are internally consistent.""" |
|
| 420 |
+ del keyname |
|
| 421 |
+ string = machinery.string |
|
| 422 |
+ public_key_lines = key.public_key.splitlines(keepends=False) |
|
| 423 |
+ assert len(public_key_lines) == 1 |
|
| 424 |
+ line_parts = public_key_lines[0].strip(b"\r\n").split(None, 2) |
|
| 425 |
+ key_type_name, public_key_b64 = line_parts[:2] |
|
| 426 |
+ assert base64.standard_b64encode(key.public_key_data) == public_key_b64 |
|
| 427 |
+ assert key.public_key_data.startswith(string(key_type_name)) |
|
| 428 |
+ |
|
| 429 |
+ # TODO(the-13th-letter): Put RSA key mangling into helper method. |
|
| 430 |
+ @Parametrize.TEST_KEYS |
|
| 431 |
+ def test_private_keys_are_consistent_with_public_keys( |
|
| 432 |
+ self, |
|
| 433 |
+ keyname: str, |
|
| 434 |
+ key: data.SSHTestKey, |
|
| 435 |
+ ) -> None: |
|
| 436 |
+ """The private key data are consistent with their public parts.""" |
|
| 437 |
+ del keyname |
|
| 438 |
+ string = machinery.string |
|
| 439 |
+ |
|
| 440 |
+ if key.public_key_data.startswith(string(b"ssh-rsa")): |
|
| 441 |
+ # RSA public keys are *not* prefixes of the corresponding |
|
| 442 |
+ # private key in OpenSSH format! RSA public keys consist of |
|
| 443 |
+ # an exponent e and a modulus n, which in the public key are |
|
| 444 |
+ # in the order (e, n), but in the order (n, e) in the |
|
| 445 |
+ # OpenSSH private key. We thus need to parse and rearrange |
|
| 446 |
+ # the components of the public key into a new "mangled" |
|
| 447 |
+ # public key that then *is* a prefix of the respective |
|
| 448 |
+ # private key. |
|
| 449 |
+ unstring_prefix = machinery.unstring_prefix |
|
| 450 |
+ key_type, numbers = unstring_prefix(key.public_key_data) |
|
| 451 |
+ e, encoded_n = unstring_prefix(numbers) |
|
| 452 |
+ n, trailer = unstring_prefix(encoded_n) |
|
| 453 |
+ assert not trailer |
|
| 454 |
+ mangled_public_key_data = string(key_type) + string(n) + string(e) |
|
| 455 |
+ assert ( |
|
| 456 |
+ key.private_key_blob[: len(mangled_public_key_data)] |
|
| 457 |
+ == mangled_public_key_data |
|
| 458 |
+ ) |
|
| 459 |
+ else: |
|
| 460 |
+ assert ( |
|
| 461 |
+ key.private_key_blob[: len(key.public_key_data)] |
|
| 462 |
+ == key.public_key_data |
|
| 463 |
+ ) |
|
| 464 |
+ |
|
| 465 |
+ @Parametrize.TEST_KEYS |
|
| 466 |
+ def test_private_keys_are_internally_consistent( |
|
| 467 |
+ self, |
|
| 468 |
+ keyname: str, |
|
| 469 |
+ key: data.SSHTestKey, |
|
| 470 |
+ ) -> None: |
|
| 471 |
+ """The private key data structures are internally consistent.""" |
|
| 472 |
+ del keyname |
|
| 473 |
+ string = machinery.string |
|
| 474 |
+ |
|
| 475 |
+ private_key_lines = [ |
|
| 476 |
+ line |
|
| 477 |
+ for line in key.private_key.splitlines(keepends=False) |
|
| 478 |
+ if line and not line.startswith((b"-----BEGIN", b"-----END")) |
|
| 479 |
+ ] |
|
| 480 |
+ private_key_from_openssh = base64.standard_b64decode( |
|
| 481 |
+ b"".join(private_key_lines) |
|
| 482 |
+ ) |
|
| 483 |
+ wrapped_public_key = string(key.public_key_data) |
|
| 484 |
+ assert ( |
|
| 485 |
+ private_key_from_openssh[ |
|
| 486 |
+ len(OPENSSH_HEADER) : len(OPENSSH_HEADER) |
|
| 487 |
+ + len(wrapped_public_key) |
|
| 488 |
+ ] |
|
| 489 |
+ == wrapped_public_key |
|
| 490 |
+ ) |
|
| 491 |
+ |
|
| 492 |
+ # Offset skips the header, the wrapped public key, and the |
|
| 493 |
+ # framing of the private keys section. |
|
| 494 |
+ offset = len(OPENSSH_HEADER) + len(wrapped_public_key) + 4 |
|
| 495 |
+ checkint = int.from_bytes( |
|
| 496 |
+ private_key_from_openssh[offset : offset + 4], "big" |
|
| 497 |
+ ) |
|
| 498 |
+ assert minimize_openssh_keyfile_padding( |
|
| 499 |
+ private_key_from_openssh |
|
| 500 |
+ ) == minimize_openssh_keyfile_padding( |
|
| 501 |
+ as_openssh_keyfile_payload( |
|
| 502 |
+ public_key=key.public_key_data, |
|
| 503 |
+ private_key=key.private_key_blob, |
|
| 504 |
+ checkint=checkint, |
|
| 505 |
+ ) |
|
| 506 |
+ ) |
|
| 507 |
+ |
|
| 508 |
+ |
|
| 509 |
+class TestStubbedSSHAgentSocket: |
|
| 510 |
+ """Test the stubbed SSH agent socket: common machinery.""" |
|
| 511 |
+ |
|
| 512 |
+ @contextlib.contextmanager |
|
| 513 |
+ def _get_agent( |
|
| 514 |
+ self, *, extended_agent: bool | None = False |
|
| 515 |
+ ) -> Generator[machinery.StubbedSSHAgentSocket, None, None]: |
|
| 516 |
+ agent_class: type[machinery.StubbedSSHAgentSocket] = ( |
|
| 517 |
+ machinery.StubbedSSHAgentSocketWithAddressAndDeterministicDSA |
|
| 518 |
+ if extended_agent |
|
| 519 |
+ else machinery.StubbedSSHAgentSocketWithAddress |
|
| 520 |
+ if extended_agent is not None |
|
| 521 |
+ else machinery.StubbedSSHAgentSocket |
|
| 522 |
+ ) |
|
| 523 |
+ with contextlib.ExitStack() as stack: |
|
| 524 |
+ monkeypatch = stack.enter_context(pytest.MonkeyPatch.context()) |
|
| 525 |
+ if issubclass( |
|
| 526 |
+ agent_class, machinery.StubbedSSHAgentSocketWithAddress |
|
| 527 |
+ ): |
|
| 528 |
+ monkeypatch.setenv("SSH_AUTH_SOCK", agent_class.ADDRESS)
|
|
| 529 |
+ else: |
|
| 530 |
+ monkeypatch.delenv("SSH_AUTH_SOCK", raising=False)
|
|
| 531 |
+ yield stack.enter_context(agent_class()) |
|
| 532 |
+ |
|
| 533 |
+ |
|
| 534 |
+class TestStubbedSSHAgentSocketRequests(TestStubbedSSHAgentSocket): |
|
| 535 |
+ """Test the stubbed SSH agent socket: normal requests.""" |
|
| 536 |
+ |
|
| 537 |
+ @Parametrize.QUERY_EXTENSION |
|
| 538 |
+ def test_query_extensions( |
|
| 539 |
+ self, extended_agent: bool, query_response: bytes |
|
| 540 |
+ ) -> None: |
|
| 541 |
+ """The agent implements a known list of extensions. |
|
| 542 |
+ |
|
| 543 |
+ The list is empty for the base agent, and non-empty for the |
|
| 544 |
+ extended agent. |
|
| 545 |
+ |
|
| 546 |
+ """ |
|
| 547 |
+ query_request = ( |
|
| 548 |
+ # SSH string header |
|
| 549 |
+ b"\x00\x00\x00\x0a" |
|
| 550 |
+ # request code: SSH_AGENTC_EXTENSION |
|
| 551 |
+ b"\x1b" |
|
| 552 |
+ # payload: SSH string "query" |
|
| 553 |
+ b"\x00\x00\x00\x05query" |
|
| 554 |
+ ) |
|
| 555 |
+ with self._get_agent(extended_agent=extended_agent) as agent: |
|
| 556 |
+ assert ("query" in agent.enabled_extensions) == bool(
|
|
| 557 |
+ extended_agent |
|
| 558 |
+ ) |
|
| 559 |
+ agent.sendall(query_request) |
|
| 560 |
+ assert agent.recv(1000) == query_response |
|
| 561 |
+ |
|
| 562 |
+ @Parametrize.LIST_IDENTITIES |
|
| 563 |
+ def test_request_identities( |
|
| 564 |
+ self, extended_agent: bool | None, query_request: bytes |
|
| 565 |
+ ) -> None: |
|
| 566 |
+ """The agent implements a known list of identities. |
|
| 567 |
+ |
|
| 568 |
+ The extended agent implements PuTTY's `list-extended` extension. |
|
| 569 |
+ |
|
| 570 |
+ """ |
|
| 571 |
+ unstring_prefix = machinery.unstring_prefix |
|
| 572 |
+ with self._get_agent(extended_agent=extended_agent) as agent: |
|
| 573 |
+ agent.sendall(query_request) |
|
| 574 |
+ message_length = int.from_bytes(agent.recv(4), "big") |
|
| 575 |
+ orig_message: bytes | bytearray = bytearray( |
|
| 576 |
+ agent.recv(message_length) |
|
| 577 |
+ ) |
|
| 578 |
+ assert ( |
|
| 579 |
+ orig_message[0] == _types.SSH_AGENT.SUCCESS |
|
| 580 |
+ if extended_agent |
|
| 581 |
+ else _types.SSH_AGENT.IDENTITIES_ANSWER |
|
| 582 |
+ ) |
|
| 583 |
+ identity_count = int.from_bytes(orig_message[1:5], "big") |
|
| 584 |
+ message = bytes(orig_message[5:]) |
|
| 585 |
+ for _ in range(identity_count): |
|
| 586 |
+ key, message = unstring_prefix(message) |
|
| 587 |
+ _comment, message = unstring_prefix(message) |
|
| 588 |
+ flags, message = ( |
|
| 589 |
+ unstring_prefix(message) |
|
| 590 |
+ if extended_agent |
|
| 591 |
+ else (b"", message) |
|
| 592 |
+ ) |
|
| 593 |
+ assert not extended_agent or flags == b"\x00\x00\x00\x00" |
|
| 594 |
+ assert key |
|
| 595 |
+ assert key in {
|
|
| 596 |
+ k.public_key_data for k in data.ALL_KEYS.values() |
|
| 597 |
+ } |
|
| 598 |
+ assert not message |
|
| 599 |
+ |
|
| 600 |
+ @Parametrize.SUPPORTED_SSH_TEST_KEYS |
|
| 601 |
+ def test_sign( |
|
| 602 |
+ self, |
|
| 603 |
+ ssh_test_key_type: str, |
|
| 604 |
+ ssh_test_key: data.SSHTestKey, |
|
| 605 |
+ requires_deterministic_dsa: bool, |
|
| 606 |
+ ) -> None: |
|
| 607 |
+ """The agent signs known key/message pairs.""" |
|
| 608 |
+ del ssh_test_key_type |
|
| 609 |
+ signature_types = ( |
|
| 610 |
+ [ |
|
| 611 |
+ data.SSHTestKeyDeterministicSignatureClass.RFC_6979, |
|
| 612 |
+ data.SSHTestKeyDeterministicSignatureClass.Pageant_068_080, |
|
| 613 |
+ ] |
|
| 614 |
+ if requires_deterministic_dsa |
|
| 615 |
+ else [data.SSHTestKeyDeterministicSignatureClass.SPEC] |
|
| 616 |
+ ) |
|
| 617 |
+ signature_names = [t.name for t in signature_types] |
|
| 618 |
+ expected_signatures = {
|
|
| 619 |
+ ssh_test_key.expected_signatures[t].signature |
|
| 620 |
+ for t in signature_types |
|
| 621 |
+ if ssh_test_key.expected_signatures[t].signature is not None |
|
| 622 |
+ } |
|
| 623 |
+ assert expected_signatures, ( |
|
| 624 |
+ f"expected a known {'/'.join(signature_names)} deterministic signature"
|
|
| 625 |
+ ) |
|
| 626 |
+ string = machinery.string |
|
| 627 |
+ query_request = string( |
|
| 628 |
+ # request code: SSH_AGENTC_SIGN_REQUEST |
|
| 629 |
+ b"\x0d" |
|
| 630 |
+ # key: SSH string of the public key |
|
| 631 |
+ + string(ssh_test_key.public_key_data) |
|
| 632 |
+ # payload: SSH string of the vault UUID |
|
| 633 |
+ + string(machinery.VAULT_UUID) |
|
| 634 |
+ # signing flags (uint32, empty) |
|
| 635 |
+ + b"\x00\x00\x00\x00" |
|
| 636 |
+ ) |
|
| 637 |
+ query_responses = {
|
|
| 638 |
+ string( |
|
| 639 |
+ # response code: SSH_AGENT_SIGN_RESPONSE |
|
| 640 |
+ b"\x0e" |
|
| 641 |
+ # expected payload: the binary signature as recorded in the test key data structure |
|
| 642 |
+ + string(sig) |
|
| 643 |
+ ) |
|
| 644 |
+ for sig in expected_signatures |
|
| 645 |
+ } |
|
| 646 |
+ with pytest.MonkeyPatch.context() as monkeypatch: |
|
| 647 |
+ monkeypatch.setenv( |
|
| 648 |
+ "SSH_AUTH_SOCK", |
|
| 649 |
+ machinery.StubbedSSHAgentSocketWithAddress.ADDRESS, |
|
| 650 |
+ ) |
|
| 651 |
+ agent: machinery.StubbedSSHAgentSocket = ( |
|
| 652 |
+ machinery.StubbedSSHAgentSocketWithAddressAndDeterministicDSA() |
|
| 653 |
+ if requires_deterministic_dsa |
|
| 654 |
+ else machinery.StubbedSSHAgentSocket() |
|
| 655 |
+ ) |
|
| 656 |
+ with agent: |
|
| 657 |
+ agent.sendall(query_request) |
|
| 658 |
+ assert agent.recv(1000) in query_responses |
|
| 659 |
+ |
|
| 660 |
+ |
|
| 661 |
+class TestStubbedSSHAgentSocketProperOperations(TestStubbedSSHAgentSocket): |
|
| 662 |
+ """Test the stubbed SSH agent socket: proper use and misuse.""" |
|
| 663 |
+ |
|
| 664 |
+ def test_close_multiple( |
|
| 665 |
+ self, |
|
| 666 |
+ ) -> None: |
|
| 667 |
+ """The agent can be closed repeatedly.""" |
|
| 668 |
+ with self._get_agent(extended_agent=None) as agent: |
|
| 669 |
+ pass |
|
| 670 |
+ with agent: |
|
| 671 |
+ pass |
|
| 672 |
+ del agent |
|
| 673 |
+ |
|
| 674 |
+ def test_closed_agents_cannot_be_interacted_with( |
|
| 675 |
+ self, |
|
| 676 |
+ ) -> None: |
|
| 677 |
+ """The agent cannot be usefully used after close.""" |
|
| 678 |
+ with self._get_agent(extended_agent=None) as agent: |
|
| 679 |
+ pass |
|
| 680 |
+ query_request = ( |
|
| 681 |
+ # SSH string header |
|
| 682 |
+ b"\x00\x00\x00\x0a" |
|
| 683 |
+ # request code: SSH_AGENTC_EXTENSION |
|
| 684 |
+ b"\x1b" |
|
| 685 |
+ # payload: SSH string "query" |
|
| 686 |
+ b"\x00\x00\x00\x05query" |
|
| 687 |
+ ) |
|
| 688 |
+ with pytest.raises(OSError, match=re.escape(os.strerror(errno.EBADF))): |
|
| 689 |
+ agent.sendall(query_request) |
|
| 690 |
+ with pytest.raises(OSError, match=re.escape(os.strerror(errno.EBADF))): |
|
| 691 |
+ agent.recv(100) |
|
| 692 |
+ |
|
| 693 |
+ def test_no_recv_without_sendall( |
|
| 694 |
+ self, |
|
| 695 |
+ ) -> None: |
|
| 696 |
+ """The agent requires a message before sending a response.""" |
|
| 697 |
+ with self._get_agent(extended_agent=None) as agent: # noqa: SIM117 |
|
| 698 |
+ with pytest.raises( |
|
| 699 |
+ AssertionError, |
|
| 700 |
+ match=re.escape( |
|
| 701 |
+ machinery.StubbedSSHAgentSocket._PROTOCOL_VIOLATION |
|
| 702 |
+ ), |
|
| 703 |
+ ): |
|
| 704 |
+ agent.recv(100) |
|
| 705 |
+ |
|
| 706 |
+ @hypothesis.given( |
|
| 707 |
+ query_request_parts=Strategies.proper_bytestring_partition( |
|
| 708 |
+ b"\x00\x00\x00\x0a\x1b\x00\x00\x00\x05query" |
|
| 709 |
+ ) |
|
| 710 |
+ ) |
|
| 711 |
+ @hypothesis.example( |
|
| 712 |
+ query_request_parts=[ |
|
| 713 |
+ b"", |
|
| 714 |
+ b"\x00\x00\x00", |
|
| 715 |
+ b"\x0a", |
|
| 716 |
+ b"\x1b", |
|
| 717 |
+ b"\x00\x00\x00\x05", |
|
| 718 |
+ b"query", |
|
| 719 |
+ ] |
|
| 720 |
+ ) |
|
| 721 |
+ def test_piecemeal_sendall( |
|
| 722 |
+ self, |
|
| 723 |
+ query_request_parts: list[bytes], |
|
| 724 |
+ ) -> None: |
|
| 725 |
+ """The agent supports receiving messages incrementally.""" |
|
| 726 |
+ with self._get_agent(extended_agent=None) as agent: |
|
| 727 |
+ agent.enabled_extensions = frozenset({"query"})
|
|
| 728 |
+ for part in query_request_parts: |
|
| 729 |
+ agent.sendall(part) |
|
| 730 |
+ header = agent.recv(agent.HEADER_SIZE) |
|
| 731 |
+ count = int.from_bytes(header, "big", signed=False) |
|
| 732 |
+ payload = agent.recv(count) |
|
| 733 |
+ assert len(payload) == count |
|
| 734 |
+ assert bytes.startswith( |
|
| 735 |
+ payload, |
|
| 736 |
+ ( |
|
| 737 |
+ bytes(_types.SSH_AGENT.SUCCESS), |
|
| 738 |
+ bytes(_types.SSH_AGENT.EXTENSION_RESPONSE), |
|
| 739 |
+ ), |
|
| 740 |
+ ) |
|
| 741 |
+ code = payload[: agent.CODE_SIZE] |
|
| 742 |
+ assert code == bytes(_types.SSH_AGENT.SUCCESS) or code == bytes( |
|
| 743 |
+ _types.SSH_AGENT.EXTENSION_RESPONSE |
|
| 744 |
+ ) |
|
| 745 |
+ |
|
| 746 |
+ @hypothesis.given(message=Strategies.invalid_ssh_agent_messages()) |
|
| 747 |
+ def test_invalid_ssh_agent_messages( |
|
| 748 |
+ self, |
|
| 749 |
+ message: Buffer, |
|
| 750 |
+ ) -> None: |
|
| 751 |
+ """The agent responds with errors on invalid messages.""" |
|
| 752 |
+ query_response = ( |
|
| 753 |
+ # SSH string header |
|
| 754 |
+ b"\x00\x00\x00\x01" |
|
| 755 |
+ # response code: SSH_AGENT_FAILURE |
|
| 756 |
+ b"\x05" |
|
| 757 |
+ ) |
|
| 758 |
+ with machinery.StubbedSSHAgentSocket() as agent: |
|
| 759 |
+ agent.sendall(message) |
|
| 760 |
+ assert agent.recv(100) == query_response |
|
| 761 |
+ |
|
| 762 |
+ @hypothesis.given(message=Strategies.truncated_agent_request()) |
|
| 763 |
+ @hypothesis.example(message=b"\x00\x00\x00\x0f\xff") |
|
| 764 |
+ def test_truncated_ssh_agent_message( |
|
| 765 |
+ self, |
|
| 766 |
+ message: bytes, |
|
| 767 |
+ ) -> None: |
|
| 768 |
+ """The agent diagnoses a final truncated request message.""" |
|
| 769 |
+ with pytest.raises( # noqa: SIM117 |
|
| 770 |
+ AssertionError, |
|
| 771 |
+ match=re.escape( |
|
| 772 |
+ machinery.StubbedSSHAgentSocket._INCOMPLETE_REQUEST |
|
| 773 |
+ ), |
|
| 774 |
+ ): |
|
| 775 |
+ with self._get_agent(extended_agent=None) as agent: |
|
| 776 |
+ agent.sendall(message) |
|
| 777 |
+ |
|
| 778 |
+ |
|
| 779 |
+class TestStubbedSSHAgentSocketSupportedAndUnsupportedFeatures( |
|
| 780 |
+ TestStubbedSSHAgentSocket |
|
| 781 |
+): |
|
| 782 |
+ """Test the stubbed SSH agent socket: supported/unsupported features.""" |
|
| 783 |
+ |
|
| 784 |
+ @hypothesis.given(message=Strategies.unsupported_ssh_agent_messages()) |
|
| 785 |
+ def test_unsupported_ssh_agent_messages( |
|
| 786 |
+ self, |
|
| 787 |
+ message: Buffer, |
|
| 788 |
+ ) -> None: |
|
| 789 |
+ """The agent responds with errors on unsupported messages.""" |
|
| 790 |
+ query_response = ( |
|
| 791 |
+ # SSH string header |
|
| 792 |
+ b"\x00\x00\x00\x01" |
|
| 793 |
+ # response code: SSH_AGENT_FAILURE |
|
| 794 |
+ b"\x05" |
|
| 795 |
+ ) |
|
| 796 |
+ with self._get_agent(extended_agent=None) as agent: |
|
| 797 |
+ agent.sendall(message) |
|
| 798 |
+ assert agent.recv(100) == query_response |
|
| 799 |
+ |
|
| 800 |
+ @hypothesis.given(args_tuple=Strategies.stubbed_agent_addresses()) |
|
| 801 |
+ @hypothesis.example(args_tuple=(None, KeyError, "SSH_AUTH_SOCK")) |
|
| 802 |
+ @hypothesis.example(args_tuple=("stub-ssh-agent:", None, ""))
|
|
| 803 |
+ def test_addresses( |
|
| 804 |
+ self, |
|
| 805 |
+ args_tuple: tuple[str | None, type[Exception] | None, str], |
|
| 806 |
+ ) -> None: |
|
| 807 |
+ """The agent accepts addresses.""" |
|
| 808 |
+ address, exception, match = args_tuple |
|
| 809 |
+ with contextlib.ExitStack() as stack: |
|
| 810 |
+ monkeypatch = stack.enter_context(pytest.MonkeyPatch.context()) |
|
| 811 |
+ if address: |
|
| 812 |
+ monkeypatch.setenv("SSH_AUTH_SOCK", address)
|
|
| 813 |
+ else: |
|
| 814 |
+ monkeypatch.delenv("SSH_AUTH_SOCK", raising=False)
|
|
| 815 |
+ if exception: |
|
| 816 |
+ stack.enter_context( |
|
| 817 |
+ pytest.raises(exception, match=re.escape(match)) |
|
| 818 |
+ ) |
|
| 819 |
+ machinery.StubbedSSHAgentSocketWithAddress() |
|
| 0 | 820 |