asyncssh 2.21.0__tar.gz → 2.21.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (177) hide show
  1. {asyncssh-2.21.0 → asyncssh-2.21.1}/.github/workflows/run_tests.yml +12 -1
  2. {asyncssh-2.21.0/asyncssh.egg-info → asyncssh-2.21.1}/PKG-INFO +2 -2
  3. {asyncssh-2.21.0 → asyncssh-2.21.1}/asyncssh/connection.py +7 -5
  4. {asyncssh-2.21.0 → asyncssh-2.21.1}/asyncssh/forward.py +2 -1
  5. {asyncssh-2.21.0 → asyncssh-2.21.1}/asyncssh/misc.py +3 -3
  6. {asyncssh-2.21.0 → asyncssh-2.21.1}/asyncssh/public_key.py +147 -97
  7. {asyncssh-2.21.0 → asyncssh-2.21.1}/asyncssh/scp.py +8 -3
  8. {asyncssh-2.21.0 → asyncssh-2.21.1}/asyncssh/sftp.py +7 -2
  9. {asyncssh-2.21.0 → asyncssh-2.21.1}/asyncssh/version.py +1 -1
  10. {asyncssh-2.21.0 → asyncssh-2.21.1/asyncssh.egg-info}/PKG-INFO +2 -2
  11. {asyncssh-2.21.0 → asyncssh-2.21.1}/asyncssh.egg-info/requires.txt +1 -1
  12. {asyncssh-2.21.0 → asyncssh-2.21.1}/docs/changes.rst +21 -0
  13. {asyncssh-2.21.0 → asyncssh-2.21.1}/pyproject.toml +1 -1
  14. {asyncssh-2.21.0 → asyncssh-2.21.1}/tests/test_agent.py +3 -4
  15. {asyncssh-2.21.0 → asyncssh-2.21.1}/tests/test_connection_auth.py +5 -5
  16. {asyncssh-2.21.0 → asyncssh-2.21.1}/tests/test_public_key.py +5 -16
  17. {asyncssh-2.21.0 → asyncssh-2.21.1}/tests/test_sftp.py +3 -2
  18. {asyncssh-2.21.0 → asyncssh-2.21.1}/.coveragerc +0 -0
  19. {asyncssh-2.21.0 → asyncssh-2.21.1}/.gitignore +0 -0
  20. {asyncssh-2.21.0 → asyncssh-2.21.1}/.readthedocs.yaml +0 -0
  21. {asyncssh-2.21.0 → asyncssh-2.21.1}/CONTRIBUTING.rst +0 -0
  22. {asyncssh-2.21.0 → asyncssh-2.21.1}/COPYRIGHT +0 -0
  23. {asyncssh-2.21.0 → asyncssh-2.21.1}/LICENSE +0 -0
  24. {asyncssh-2.21.0 → asyncssh-2.21.1}/MANIFEST.in +0 -0
  25. {asyncssh-2.21.0 → asyncssh-2.21.1}/README.rst +0 -0
  26. {asyncssh-2.21.0 → asyncssh-2.21.1}/asyncssh/__init__.py +0 -0
  27. {asyncssh-2.21.0 → asyncssh-2.21.1}/asyncssh/agent.py +0 -0
  28. {asyncssh-2.21.0 → asyncssh-2.21.1}/asyncssh/agent_unix.py +0 -0
  29. {asyncssh-2.21.0 → asyncssh-2.21.1}/asyncssh/agent_win32.py +0 -0
  30. {asyncssh-2.21.0 → asyncssh-2.21.1}/asyncssh/asn1.py +0 -0
  31. {asyncssh-2.21.0 → asyncssh-2.21.1}/asyncssh/auth.py +0 -0
  32. {asyncssh-2.21.0 → asyncssh-2.21.1}/asyncssh/auth_keys.py +0 -0
  33. {asyncssh-2.21.0 → asyncssh-2.21.1}/asyncssh/channel.py +0 -0
  34. {asyncssh-2.21.0 → asyncssh-2.21.1}/asyncssh/client.py +0 -0
  35. {asyncssh-2.21.0 → asyncssh-2.21.1}/asyncssh/compression.py +0 -0
  36. {asyncssh-2.21.0 → asyncssh-2.21.1}/asyncssh/config.py +0 -0
  37. {asyncssh-2.21.0 → asyncssh-2.21.1}/asyncssh/constants.py +0 -0
  38. {asyncssh-2.21.0 → asyncssh-2.21.1}/asyncssh/crypto/__init__.py +0 -0
  39. {asyncssh-2.21.0 → asyncssh-2.21.1}/asyncssh/crypto/chacha.py +0 -0
  40. {asyncssh-2.21.0 → asyncssh-2.21.1}/asyncssh/crypto/cipher.py +0 -0
  41. {asyncssh-2.21.0 → asyncssh-2.21.1}/asyncssh/crypto/dh.py +0 -0
  42. {asyncssh-2.21.0 → asyncssh-2.21.1}/asyncssh/crypto/dsa.py +0 -0
  43. {asyncssh-2.21.0 → asyncssh-2.21.1}/asyncssh/crypto/ec.py +0 -0
  44. {asyncssh-2.21.0 → asyncssh-2.21.1}/asyncssh/crypto/ec_params.py +0 -0
  45. {asyncssh-2.21.0 → asyncssh-2.21.1}/asyncssh/crypto/ed.py +0 -0
  46. {asyncssh-2.21.0 → asyncssh-2.21.1}/asyncssh/crypto/kdf.py +0 -0
  47. {asyncssh-2.21.0 → asyncssh-2.21.1}/asyncssh/crypto/misc.py +0 -0
  48. {asyncssh-2.21.0 → asyncssh-2.21.1}/asyncssh/crypto/pq.py +0 -0
  49. {asyncssh-2.21.0 → asyncssh-2.21.1}/asyncssh/crypto/rsa.py +0 -0
  50. {asyncssh-2.21.0 → asyncssh-2.21.1}/asyncssh/crypto/umac.py +0 -0
  51. {asyncssh-2.21.0 → asyncssh-2.21.1}/asyncssh/crypto/x509.py +0 -0
  52. {asyncssh-2.21.0 → asyncssh-2.21.1}/asyncssh/dsa.py +0 -0
  53. {asyncssh-2.21.0 → asyncssh-2.21.1}/asyncssh/ecdsa.py +0 -0
  54. {asyncssh-2.21.0 → asyncssh-2.21.1}/asyncssh/eddsa.py +0 -0
  55. {asyncssh-2.21.0 → asyncssh-2.21.1}/asyncssh/editor.py +0 -0
  56. {asyncssh-2.21.0 → asyncssh-2.21.1}/asyncssh/encryption.py +0 -0
  57. {asyncssh-2.21.0 → asyncssh-2.21.1}/asyncssh/gss.py +0 -0
  58. {asyncssh-2.21.0 → asyncssh-2.21.1}/asyncssh/gss_unix.py +0 -0
  59. {asyncssh-2.21.0 → asyncssh-2.21.1}/asyncssh/gss_win32.py +0 -0
  60. {asyncssh-2.21.0 → asyncssh-2.21.1}/asyncssh/kex.py +0 -0
  61. {asyncssh-2.21.0 → asyncssh-2.21.1}/asyncssh/kex_dh.py +0 -0
  62. {asyncssh-2.21.0 → asyncssh-2.21.1}/asyncssh/kex_rsa.py +0 -0
  63. {asyncssh-2.21.0 → asyncssh-2.21.1}/asyncssh/keysign.py +0 -0
  64. {asyncssh-2.21.0 → asyncssh-2.21.1}/asyncssh/known_hosts.py +0 -0
  65. {asyncssh-2.21.0 → asyncssh-2.21.1}/asyncssh/listener.py +0 -0
  66. {asyncssh-2.21.0 → asyncssh-2.21.1}/asyncssh/logging.py +0 -0
  67. {asyncssh-2.21.0 → asyncssh-2.21.1}/asyncssh/mac.py +0 -0
  68. {asyncssh-2.21.0 → asyncssh-2.21.1}/asyncssh/packet.py +0 -0
  69. {asyncssh-2.21.0 → asyncssh-2.21.1}/asyncssh/pattern.py +0 -0
  70. {asyncssh-2.21.0 → asyncssh-2.21.1}/asyncssh/pbe.py +0 -0
  71. {asyncssh-2.21.0 → asyncssh-2.21.1}/asyncssh/pkcs11.py +0 -0
  72. {asyncssh-2.21.0 → asyncssh-2.21.1}/asyncssh/process.py +0 -0
  73. {asyncssh-2.21.0 → asyncssh-2.21.1}/asyncssh/py.typed +0 -0
  74. {asyncssh-2.21.0 → asyncssh-2.21.1}/asyncssh/rsa.py +0 -0
  75. {asyncssh-2.21.0 → asyncssh-2.21.1}/asyncssh/saslprep.py +0 -0
  76. {asyncssh-2.21.0 → asyncssh-2.21.1}/asyncssh/server.py +0 -0
  77. {asyncssh-2.21.0 → asyncssh-2.21.1}/asyncssh/session.py +0 -0
  78. {asyncssh-2.21.0 → asyncssh-2.21.1}/asyncssh/sk.py +0 -0
  79. {asyncssh-2.21.0 → asyncssh-2.21.1}/asyncssh/sk_ecdsa.py +0 -0
  80. {asyncssh-2.21.0 → asyncssh-2.21.1}/asyncssh/sk_eddsa.py +0 -0
  81. {asyncssh-2.21.0 → asyncssh-2.21.1}/asyncssh/socks.py +0 -0
  82. {asyncssh-2.21.0 → asyncssh-2.21.1}/asyncssh/stream.py +0 -0
  83. {asyncssh-2.21.0 → asyncssh-2.21.1}/asyncssh/subprocess.py +0 -0
  84. {asyncssh-2.21.0 → asyncssh-2.21.1}/asyncssh/tuntap.py +0 -0
  85. {asyncssh-2.21.0 → asyncssh-2.21.1}/asyncssh/x11.py +0 -0
  86. {asyncssh-2.21.0 → asyncssh-2.21.1}/asyncssh.egg-info/SOURCES.txt +0 -0
  87. {asyncssh-2.21.0 → asyncssh-2.21.1}/asyncssh.egg-info/dependency_links.txt +0 -0
  88. {asyncssh-2.21.0 → asyncssh-2.21.1}/asyncssh.egg-info/top_level.txt +0 -0
  89. {asyncssh-2.21.0 → asyncssh-2.21.1}/docs/_templates/sidebarbottom.html +0 -0
  90. {asyncssh-2.21.0 → asyncssh-2.21.1}/docs/_templates/sidebartop.html +0 -0
  91. {asyncssh-2.21.0 → asyncssh-2.21.1}/docs/api.rst +0 -0
  92. {asyncssh-2.21.0 → asyncssh-2.21.1}/docs/conf.py +0 -0
  93. {asyncssh-2.21.0 → asyncssh-2.21.1}/docs/contributing.rst +0 -0
  94. {asyncssh-2.21.0 → asyncssh-2.21.1}/docs/index.rst +0 -0
  95. {asyncssh-2.21.0 → asyncssh-2.21.1}/docs/requirements.txt +0 -0
  96. {asyncssh-2.21.0 → asyncssh-2.21.1}/docs/rftheme/layout.html +0 -0
  97. {asyncssh-2.21.0 → asyncssh-2.21.1}/docs/rftheme/static/rftheme.css_t +0 -0
  98. {asyncssh-2.21.0 → asyncssh-2.21.1}/docs/rftheme/theme.conf +0 -0
  99. {asyncssh-2.21.0 → asyncssh-2.21.1}/docs/rtd-req.txt +0 -0
  100. {asyncssh-2.21.0 → asyncssh-2.21.1}/examples/callback_client.py +0 -0
  101. {asyncssh-2.21.0 → asyncssh-2.21.1}/examples/callback_client2.py +0 -0
  102. {asyncssh-2.21.0 → asyncssh-2.21.1}/examples/callback_client3.py +0 -0
  103. {asyncssh-2.21.0 → asyncssh-2.21.1}/examples/callback_math_server.py +0 -0
  104. {asyncssh-2.21.0 → asyncssh-2.21.1}/examples/chat_server.py +0 -0
  105. {asyncssh-2.21.0 → asyncssh-2.21.1}/examples/check_exit_status.py +0 -0
  106. {asyncssh-2.21.0 → asyncssh-2.21.1}/examples/chroot_sftp_server.py +0 -0
  107. {asyncssh-2.21.0 → asyncssh-2.21.1}/examples/direct_client.py +0 -0
  108. {asyncssh-2.21.0 → asyncssh-2.21.1}/examples/direct_server.py +0 -0
  109. {asyncssh-2.21.0 → asyncssh-2.21.1}/examples/editor.py +0 -0
  110. {asyncssh-2.21.0 → asyncssh-2.21.1}/examples/gather_results.py +0 -0
  111. {asyncssh-2.21.0 → asyncssh-2.21.1}/examples/listening_client.py +0 -0
  112. {asyncssh-2.21.0 → asyncssh-2.21.1}/examples/local_forwarding_client.py +0 -0
  113. {asyncssh-2.21.0 → asyncssh-2.21.1}/examples/local_forwarding_client2.py +0 -0
  114. {asyncssh-2.21.0 → asyncssh-2.21.1}/examples/local_forwarding_server.py +0 -0
  115. {asyncssh-2.21.0 → asyncssh-2.21.1}/examples/math_client.py +0 -0
  116. {asyncssh-2.21.0 → asyncssh-2.21.1}/examples/math_server.py +0 -0
  117. {asyncssh-2.21.0 → asyncssh-2.21.1}/examples/redirect_input.py +0 -0
  118. {asyncssh-2.21.0 → asyncssh-2.21.1}/examples/redirect_local_pipe.py +0 -0
  119. {asyncssh-2.21.0 → asyncssh-2.21.1}/examples/redirect_remote_pipe.py +0 -0
  120. {asyncssh-2.21.0 → asyncssh-2.21.1}/examples/redirect_server.py +0 -0
  121. {asyncssh-2.21.0 → asyncssh-2.21.1}/examples/remote_forwarding_client.py +0 -0
  122. {asyncssh-2.21.0 → asyncssh-2.21.1}/examples/remote_forwarding_client2.py +0 -0
  123. {asyncssh-2.21.0 → asyncssh-2.21.1}/examples/remote_forwarding_server.py +0 -0
  124. {asyncssh-2.21.0 → asyncssh-2.21.1}/examples/reverse_client.py +0 -0
  125. {asyncssh-2.21.0 → asyncssh-2.21.1}/examples/reverse_server.py +0 -0
  126. {asyncssh-2.21.0 → asyncssh-2.21.1}/examples/scp_client.py +0 -0
  127. {asyncssh-2.21.0 → asyncssh-2.21.1}/examples/set_environment.py +0 -0
  128. {asyncssh-2.21.0 → asyncssh-2.21.1}/examples/set_terminal.py +0 -0
  129. {asyncssh-2.21.0 → asyncssh-2.21.1}/examples/sftp_client.py +0 -0
  130. {asyncssh-2.21.0 → asyncssh-2.21.1}/examples/show_environment.py +0 -0
  131. {asyncssh-2.21.0 → asyncssh-2.21.1}/examples/show_terminal.py +0 -0
  132. {asyncssh-2.21.0 → asyncssh-2.21.1}/examples/simple_cert_server.py +0 -0
  133. {asyncssh-2.21.0 → asyncssh-2.21.1}/examples/simple_client.py +0 -0
  134. {asyncssh-2.21.0 → asyncssh-2.21.1}/examples/simple_keyed_server.py +0 -0
  135. {asyncssh-2.21.0 → asyncssh-2.21.1}/examples/simple_scp_server.py +0 -0
  136. {asyncssh-2.21.0 → asyncssh-2.21.1}/examples/simple_server.py +0 -0
  137. {asyncssh-2.21.0 → asyncssh-2.21.1}/examples/simple_sftp_server.py +0 -0
  138. {asyncssh-2.21.0 → asyncssh-2.21.1}/examples/stream_direct_client.py +0 -0
  139. {asyncssh-2.21.0 → asyncssh-2.21.1}/examples/stream_direct_server.py +0 -0
  140. {asyncssh-2.21.0 → asyncssh-2.21.1}/examples/stream_listening_client.py +0 -0
  141. {asyncssh-2.21.0 → asyncssh-2.21.1}/mypy.ini +0 -0
  142. {asyncssh-2.21.0 → asyncssh-2.21.1}/pylintrc +0 -0
  143. {asyncssh-2.21.0 → asyncssh-2.21.1}/setup.cfg +0 -0
  144. {asyncssh-2.21.0 → asyncssh-2.21.1}/tests/__init__.py +0 -0
  145. {asyncssh-2.21.0 → asyncssh-2.21.1}/tests/gss_stub.py +0 -0
  146. {asyncssh-2.21.0 → asyncssh-2.21.1}/tests/gssapi_stub.py +0 -0
  147. {asyncssh-2.21.0 → asyncssh-2.21.1}/tests/keysign_stub.py +0 -0
  148. {asyncssh-2.21.0 → asyncssh-2.21.1}/tests/pkcs11_stub.py +0 -0
  149. {asyncssh-2.21.0 → asyncssh-2.21.1}/tests/server.py +0 -0
  150. {asyncssh-2.21.0 → asyncssh-2.21.1}/tests/sk_stub.py +0 -0
  151. {asyncssh-2.21.0 → asyncssh-2.21.1}/tests/sspi_stub.py +0 -0
  152. {asyncssh-2.21.0 → asyncssh-2.21.1}/tests/test_asn1.py +0 -0
  153. {asyncssh-2.21.0 → asyncssh-2.21.1}/tests/test_auth.py +0 -0
  154. {asyncssh-2.21.0 → asyncssh-2.21.1}/tests/test_auth_keys.py +0 -0
  155. {asyncssh-2.21.0 → asyncssh-2.21.1}/tests/test_channel.py +0 -0
  156. {asyncssh-2.21.0 → asyncssh-2.21.1}/tests/test_compression.py +0 -0
  157. {asyncssh-2.21.0 → asyncssh-2.21.1}/tests/test_config.py +0 -0
  158. {asyncssh-2.21.0 → asyncssh-2.21.1}/tests/test_connection.py +0 -0
  159. {asyncssh-2.21.0 → asyncssh-2.21.1}/tests/test_editor.py +0 -0
  160. {asyncssh-2.21.0 → asyncssh-2.21.1}/tests/test_encryption.py +0 -0
  161. {asyncssh-2.21.0 → asyncssh-2.21.1}/tests/test_forward.py +0 -0
  162. {asyncssh-2.21.0 → asyncssh-2.21.1}/tests/test_kex.py +0 -0
  163. {asyncssh-2.21.0 → asyncssh-2.21.1}/tests/test_known_hosts.py +0 -0
  164. {asyncssh-2.21.0 → asyncssh-2.21.1}/tests/test_logging.py +0 -0
  165. {asyncssh-2.21.0 → asyncssh-2.21.1}/tests/test_mac.py +0 -0
  166. {asyncssh-2.21.0 → asyncssh-2.21.1}/tests/test_packet.py +0 -0
  167. {asyncssh-2.21.0 → asyncssh-2.21.1}/tests/test_pkcs11.py +0 -0
  168. {asyncssh-2.21.0 → asyncssh-2.21.1}/tests/test_process.py +0 -0
  169. {asyncssh-2.21.0 → asyncssh-2.21.1}/tests/test_saslprep.py +0 -0
  170. {asyncssh-2.21.0 → asyncssh-2.21.1}/tests/test_sk.py +0 -0
  171. {asyncssh-2.21.0 → asyncssh-2.21.1}/tests/test_stream.py +0 -0
  172. {asyncssh-2.21.0 → asyncssh-2.21.1}/tests/test_subprocess.py +0 -0
  173. {asyncssh-2.21.0 → asyncssh-2.21.1}/tests/test_tuntap.py +0 -0
  174. {asyncssh-2.21.0 → asyncssh-2.21.1}/tests/test_x11.py +0 -0
  175. {asyncssh-2.21.0 → asyncssh-2.21.1}/tests/test_x509.py +0 -0
  176. {asyncssh-2.21.0 → asyncssh-2.21.1}/tests/util.py +0 -0
  177. {asyncssh-2.21.0 → asyncssh-2.21.1}/tox.ini +0 -0
@@ -1,13 +1,19 @@
1
1
  name: Run tests
2
2
  on: [push, pull_request]
3
3
 
4
+ permissions:
5
+ contents: read
6
+
4
7
  jobs:
5
8
  run-tests:
6
9
  name: Run tests
10
+ permissions:
11
+ contents: read
12
+ actions: write
7
13
  strategy:
8
14
  fail-fast: false
9
15
  matrix:
10
- os: [ubuntu-latest, macos-latest, windows-latest]
16
+ os: [ubuntu-latest, macos-latest, windows-2022]
11
17
  python-version: ["3.8", "3.9", "3.10", "3.11", "3.12", "3.13"]
12
18
  include:
13
19
  - os: macos-latest
@@ -124,6 +130,8 @@ jobs:
124
130
  runs-on: ubuntu-latest
125
131
  needs: run-tests
126
132
  if: ${{ always() }}
133
+ permissions:
134
+ actions: write
127
135
  steps:
128
136
  - name: Merge coverage
129
137
  uses: actions/upload-artifact/merge@v4
@@ -137,6 +145,9 @@ jobs:
137
145
  runs-on: ubuntu-latest
138
146
  needs: merge-coverage
139
147
  if: ${{ always() }}
148
+ permissions:
149
+ contents: read
150
+ actions: read
140
151
  steps:
141
152
  - uses: actions/checkout@v4
142
153
  - uses: actions/setup-python@v5
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: asyncssh
3
- Version: 2.21.0
3
+ Version: 2.21.1
4
4
  Summary: AsyncSSH: Asynchronous SSHv2 client and server library
5
5
  Author-email: Ron Frederick <ronf@timeheart.net>
6
6
  License: EPL-2.0 OR GPL-2.0-or-later
@@ -32,7 +32,7 @@ Requires-Dist: typing_extensions>=4.0.0
32
32
  Provides-Extra: bcrypt
33
33
  Requires-Dist: bcrypt>=3.1.3; extra == "bcrypt"
34
34
  Provides-Extra: fido2
35
- Requires-Dist: fido2>=0.9.2; extra == "fido2"
35
+ Requires-Dist: fido2<2,>=0.9.2; extra == "fido2"
36
36
  Provides-Extra: gssapi
37
37
  Requires-Dist: gssapi>=1.2.0; extra == "gssapi"
38
38
  Provides-Extra: libnacl
@@ -4169,7 +4169,7 @@ class SSHClientConnection(SSHConnection):
4169
4169
  async def create_session(self, session_factory: SSHClientSessionFactory,
4170
4170
  command: DefTuple[Optional[str]] = (), *,
4171
4171
  subsystem: DefTuple[Optional[str]]= (),
4172
- env: DefTuple[Env] = (),
4172
+ env: DefTuple[Optional[Env]] = (),
4173
4173
  send_env: DefTuple[Optional[EnvSeq]] = (),
4174
4174
  request_pty: DefTuple[Union[bool, str]] = (),
4175
4175
  term_type: DefTuple[Optional[str]] = (),
@@ -5687,7 +5687,7 @@ class SSHClientConnection(SSHConnection):
5687
5687
  return cast(SSHForwarder, peer)
5688
5688
 
5689
5689
  @async_context_manager
5690
- async def start_sftp_client(self, env: DefTuple[Env] = (),
5690
+ async def start_sftp_client(self, env: DefTuple[Optional[Env]] = (),
5691
5691
  send_env: DefTuple[Optional[EnvSeq]] = (),
5692
5692
  path_encoding: Optional[str] = 'utf-8',
5693
5693
  path_errors = 'strict',
@@ -8042,7 +8042,7 @@ class SSHClientConnectionOptions(SSHConnectionOptions):
8042
8042
  pkcs11_pin: Optional[str]
8043
8043
  command: Optional[str]
8044
8044
  subsystem: Optional[str]
8045
- env: Env
8045
+ env: Optional[Env]
8046
8046
  send_env: Optional[EnvSeq]
8047
8047
  request_pty: _RequestPTY
8048
8048
  term_type: Optional[str]
@@ -8115,7 +8115,8 @@ class SSHClientConnectionOptions(SSHConnectionOptions):
8115
8115
  pkcs11_provider: DefTuple[Optional[str]] = (),
8116
8116
  pkcs11_pin: Optional[str] = None,
8117
8117
  command: DefTuple[Optional[str]] = (),
8118
- subsystem: Optional[str] = None, env: DefTuple[Env] = (),
8118
+ subsystem: Optional[str] = None,
8119
+ env: DefTuple[Optional[Env]] = (),
8119
8120
  send_env: DefTuple[Optional[EnvSeq]] = (),
8120
8121
  request_pty: DefTuple[_RequestPTY] = (),
8121
8122
  term_type: Optional[str] = None,
@@ -8354,7 +8355,8 @@ class SSHClientConnectionOptions(SSHConnectionOptions):
8354
8355
 
8355
8356
  self.subsystem = subsystem
8356
8357
 
8357
- self.env = cast(Env, env if env != () else config.get('SetEnv'))
8358
+ self.env = cast(Optional[Env], env if env != () else
8359
+ config.get('SetEnv'))
8358
8360
 
8359
8361
  self.send_env = cast(Optional[EnvSeq], send_env if send_env != () else
8360
8362
  config.get('SendEnv'))
@@ -91,7 +91,8 @@ class SSHForwarder(asyncio.BaseProtocol):
91
91
  def write_eof(self) -> None:
92
92
  """Write end of file to the transport"""
93
93
 
94
- assert self._transport is not None
94
+ if not self._transport:
95
+ return # pragma: no cover
95
96
 
96
97
  try:
97
98
  self._transport.write_eof()
@@ -125,7 +125,7 @@ SockAddr = Union[Tuple[str, int], Tuple[str, int, int, int]]
125
125
  EnvMap = Mapping[BytesOrStr, BytesOrStr]
126
126
  EnvItems = Sequence[Tuple[BytesOrStr, BytesOrStr]]
127
127
  EnvSeq = Sequence[BytesOrStr]
128
- Env = Optional[Union[EnvMap, EnvItems, EnvSeq]]
128
+ Env = Union[EnvMap, EnvItems, EnvSeq]
129
129
 
130
130
  # Define a version of randrange which is based on SystemRandom(), so that
131
131
  # we get back numbers suitable for cryptographic use.
@@ -141,8 +141,8 @@ _time_units = {'': 1, 's': 1, 'm': 60, 'h': 60*60,
141
141
  def encode_env(env: Env) -> Iterator[Tuple[bytes, bytes]]:
142
142
  """Convert environemnt dict or list to bytes-based dictionary"""
143
143
 
144
- env = cast(Sequence[Tuple[BytesOrStr, BytesOrStr]],
145
- env.items() if isinstance(env, dict) else env)
144
+ if hasattr(env, 'items'):
145
+ env = cast(Env, env.items())
146
146
 
147
147
  try:
148
148
  for item in env:
@@ -1,4 +1,4 @@
1
- # Copyright (c) 2013-2024 by Ron Frederick <ronf@timeheart.net> and others.
1
+ # Copyright (c) 2013-2025 by Ron Frederick <ronf@timeheart.net> and others.
2
2
  #
3
3
  # This program and the accompanying materials are made available under
4
4
  # the terms of the Eclipse Public License v2.0 which accompanies this
@@ -97,6 +97,8 @@ CertListArg = Union[_CertArg, Sequence[_CertArg]]
97
97
  _KeyPairArg = Union['SSHKeyPair', _KeyArg, Tuple[_KeyArg, _CertArg]]
98
98
  KeyPairListArg = Union[_KeyPairArg, Sequence[_KeyPairArg]]
99
99
 
100
+ _PassphraseCallable = Callable[[str], BytesOrStr]
101
+ _PassphraseArg = Optional[Union[_PassphraseCallable, BytesOrStr]]
100
102
 
101
103
  # Default file names in .ssh directory to read private keys from
102
104
  _DEFAULT_KEY_FILES = (
@@ -192,6 +194,51 @@ def _wrap_base64(data: bytes, wrap: int = 64) -> bytes:
192
194
  for i in range(0, len(data), wrap)) + b'\n'
193
195
 
194
196
 
197
+ def _resolve_passphrase(
198
+ passphrase: _PassphraseArg, filename: str,
199
+ loop: Optional[asyncio.AbstractEventLoop]) -> Optional[BytesOrStr]:
200
+ """Resolve a passphrase used to encrypt/decrypt SSH private keys"""
201
+
202
+ resolved_passphrase: Optional[BytesOrStr]
203
+
204
+ if callable(passphrase):
205
+ resolved_passphrase = passphrase(filename)
206
+ else:
207
+ resolved_passphrase = passphrase
208
+
209
+ if loop and inspect.isawaitable(resolved_passphrase):
210
+ resolved_passphrase = asyncio.run_coroutine_threadsafe(
211
+ resolved_passphrase, loop).result()
212
+
213
+ return resolved_passphrase
214
+
215
+
216
+ class _EncryptedKey:
217
+ """Encrypted SSH private key, decrypted just prior to use"""
218
+
219
+ def __init__(self, key_data: bytes, filename: str,
220
+ passphrase: _PassphraseArg,
221
+ loop: Optional[asyncio.AbstractEventLoop],
222
+ unsafe_skip_rsa_key_validation: bool):
223
+ self._key_data = key_data
224
+ self._filename = filename
225
+ self._passphrase = passphrase
226
+ self._loop = loop
227
+ self._unsafe_skip_rsa_key_validation = unsafe_skip_rsa_key_validation
228
+
229
+ def decrypt(self) -> 'SSHKey':
230
+ """Decrypt this encrypted key data and return an SSH private key"""
231
+
232
+ resolved_passphrase = _resolve_passphrase(self._passphrase,
233
+ self._filename, self._loop)
234
+
235
+ key = import_private_key(self._key_data, resolved_passphrase,
236
+ self._unsafe_skip_rsa_key_validation)
237
+ key.set_filename(self._filename)
238
+
239
+ return key
240
+
241
+
195
242
  class KeyGenerationError(ValueError):
196
243
  """Key generation error
197
244
 
@@ -2238,8 +2285,9 @@ class SSHLocalKeyPair(SSHKeyPair):
2238
2285
 
2239
2286
  _key_type = 'local'
2240
2287
 
2241
- def __init__(self, key: SSHKey, pubkey: Optional[SSHKey] = None,
2242
- cert: Optional[SSHCertificate] = None):
2288
+ def __init__(self, key: SSHKey, pubkey: Optional[SSHKey],
2289
+ cert: Optional[SSHCertificate],
2290
+ enc_key: Optional[_EncryptedKey]):
2243
2291
  if pubkey and pubkey.public_data != key.public_data:
2244
2292
  raise ValueError('Public key mismatch')
2245
2293
 
@@ -2254,10 +2302,11 @@ class SSHLocalKeyPair(SSHKeyPair):
2254
2302
 
2255
2303
  super().__init__(key.algorithm, key.algorithm, key.sig_algorithms,
2256
2304
  key.sig_algorithms, key.public_data, comment,
2257
- cert, key.get_filename(), key.use_executor,
2258
- key.use_webauthn)
2305
+ cert, key.get_filename(), key.use_executor or
2306
+ bool(enc_key), key.use_webauthn)
2259
2307
 
2260
2308
  self._key = key
2309
+ self._enc_key = enc_key
2261
2310
 
2262
2311
  def get_agent_private_key(self) -> bytes:
2263
2312
  """Return binary encoding of keypair for upload to SSH agent"""
@@ -2273,6 +2322,12 @@ class SSHLocalKeyPair(SSHKeyPair):
2273
2322
  def sign(self, data: bytes) -> bytes:
2274
2323
  """Sign a block of data with this private key"""
2275
2324
 
2325
+ if self._enc_key:
2326
+ self._key = self._enc_key.decrypt()
2327
+ self._enc_key = None
2328
+
2329
+ self.use_executor = self._key.use_executor
2330
+
2276
2331
  return self._key.sign(data, self.sig_algorithm)
2277
2332
 
2278
2333
 
@@ -2368,7 +2423,7 @@ def _match_block(data: bytes, start: int, header: bytes,
2368
2423
  """Match a block of data wrapped in a header/footer"""
2369
2424
 
2370
2425
  match = re.compile(b'^' + header[:5] + b'END' + header[10:] +
2371
- rb'[ \t\r\f\v]*$', re.M).search(data, start)
2426
+ rb'[ \t\n\r\f\v]*$', re.M).search(data, start)
2372
2427
 
2373
2428
  if not match:
2374
2429
  raise KeyImportError(f'Missing {fmt} footer')
@@ -3203,21 +3258,6 @@ def import_private_key(
3203
3258
  raise KeyImportError('Invalid private key')
3204
3259
 
3205
3260
 
3206
- def import_private_key_and_certs(
3207
- data: bytes, passphrase: Optional[BytesOrStr] = None,
3208
- unsafe_skip_rsa_key_validation: Optional[bool] = None) -> \
3209
- Tuple[SSHKey, Optional[SSHX509CertificateChain]]:
3210
- """Import a private key and optional certificate chain"""
3211
-
3212
- key, end = _decode_private(data, passphrase,
3213
- unsafe_skip_rsa_key_validation)
3214
-
3215
- if key:
3216
- return key, import_certificate_chain(data[end:])
3217
- else:
3218
- raise KeyImportError('Invalid private key')
3219
-
3220
-
3221
3261
  def import_public_key(data: BytesOrStr) -> SSHKey:
3222
3262
  """Import a public key
3223
3263
 
@@ -3339,20 +3379,6 @@ def read_private_key(
3339
3379
  return key
3340
3380
 
3341
3381
 
3342
- def read_private_key_and_certs(
3343
- filename: FilePath, passphrase: Optional[BytesOrStr] = None,
3344
- unsafe_skip_rsa_key_validation: Optional[bool] = None) -> \
3345
- Tuple[SSHKey, Optional[SSHX509CertificateChain]]:
3346
- """Read a private key and optional certificate chain from a file"""
3347
-
3348
- key, cert = import_private_key_and_certs(read_file(filename), passphrase,
3349
- unsafe_skip_rsa_key_validation)
3350
-
3351
- key.set_filename(filename)
3352
-
3353
- return key, cert
3354
-
3355
-
3356
3382
  def read_public_key(filename: FilePath) -> SSHKey:
3357
3383
  """Read a public key from a file
3358
3384
 
@@ -3512,31 +3538,37 @@ def load_keypairs(
3512
3538
  """
3513
3539
 
3514
3540
  keys_to_load: Sequence[_KeyPairArg]
3541
+ key_data: Optional[bytes]
3542
+ key: Union['SSHKey', 'SSHKeyPair']
3515
3543
  result: List[SSHKeyPair] = []
3516
3544
 
3517
3545
  certlist = load_certificates(certlist)
3518
3546
  certdict = {cert.key.public_data: cert for cert in certlist}
3519
3547
 
3520
3548
  if isinstance(keylist, (PurePath, str)):
3521
- try:
3522
- if callable(passphrase):
3523
- resolved_passphrase = passphrase(str(keylist))
3524
- else:
3525
- resolved_passphrase = passphrase
3549
+ data = read_file(keylist)
3550
+ key_data_list: List[bytes] = []
3526
3551
 
3527
- if loop and inspect.isawaitable(resolved_passphrase):
3528
- resolved_passphrase = asyncio.run_coroutine_threadsafe(
3529
- resolved_passphrase, loop).result()
3552
+ while data:
3553
+ fmt, _, end = _match_next(data, b'PRIVATE KEY')
3554
+ if fmt:
3555
+ key_data_list.append(data[:end])
3530
3556
 
3531
- priv_keys = read_private_key_list(keylist, resolved_passphrase,
3532
- unsafe_skip_rsa_key_validation)
3557
+ data = data[end:]
3533
3558
 
3534
- if len(priv_keys) <= 1:
3535
- keys_to_load = [keylist]
3536
- passphrase = resolved_passphrase
3537
- else:
3538
- keys_to_load = priv_keys
3539
- except KeyImportError:
3559
+ if len(key_data_list) > 1:
3560
+ resolved_passphrase = _resolve_passphrase(passphrase,
3561
+ str(keylist), loop)
3562
+
3563
+ keys_to_load = []
3564
+
3565
+ for key_data in key_data_list:
3566
+ key = import_private_key(key_data, resolved_passphrase,
3567
+ unsafe_skip_rsa_key_validation)
3568
+ key.set_filename(keylist)
3569
+
3570
+ keys_to_load.append(key)
3571
+ else:
3540
3572
  keys_to_load = [keylist]
3541
3573
  elif isinstance(keylist, (tuple, bytes, SSHKey, SSHKeyPair)):
3542
3574
  keys_to_load = [cast(_KeyPairArg, keylist)]
@@ -3545,61 +3577,37 @@ def load_keypairs(
3545
3577
 
3546
3578
  for key_to_load in keys_to_load:
3547
3579
  allow_certs = False
3548
- key_prefix = None
3549
- saved_exc = None
3580
+ key_data = None
3581
+ key_prefix = ''
3550
3582
  pubkey_or_certs = None
3551
- pubkey_to_load: Optional[_KeyArg] = None
3552
3583
  certs_to_load: Optional[_CertArg] = None
3553
- key: Union['SSHKey', 'SSHKeyPair']
3584
+ pubkey_to_load: Optional[_KeyArg] = None
3585
+ saved_exc = None
3586
+ enc_key: Optional[_EncryptedKey] = None
3554
3587
 
3555
3588
  if isinstance(key_to_load, (PurePath, str, bytes)):
3556
3589
  allow_certs = True
3557
3590
  elif isinstance(key_to_load, tuple):
3558
3591
  key_to_load, pubkey_or_certs = key_to_load
3559
3592
 
3560
- try:
3561
- if isinstance(key_to_load, (PurePath, str)):
3562
- key_prefix = str(key_to_load)
3593
+ if isinstance(key_to_load, (PurePath, str)):
3594
+ key_prefix = str(key_to_load)
3595
+ key_data = read_file(key_to_load)
3596
+ elif isinstance(key_to_load, bytes):
3597
+ key_data = key_to_load
3563
3598
 
3564
- if callable(passphrase):
3565
- resolved_passphrase = passphrase(key_prefix)
3566
- else:
3567
- resolved_passphrase = passphrase
3599
+ certs: Optional[Sequence[SSHCertificate]]
3568
3600
 
3569
- if loop and inspect.isawaitable(resolved_passphrase):
3570
- resolved_passphrase = asyncio.run_coroutine_threadsafe(
3571
- resolved_passphrase, loop).result()
3601
+ if allow_certs:
3602
+ assert key_data is not None
3572
3603
 
3573
- if allow_certs:
3574
- key, certs_to_load = read_private_key_and_certs(
3575
- key_to_load, resolved_passphrase,
3576
- unsafe_skip_rsa_key_validation)
3604
+ _, _, end = _match_next(key_data, b'PRIVATE KEY')
3577
3605
 
3578
- if not certs_to_load:
3579
- certs_to_load = key_prefix + '-cert.pub'
3580
- else:
3581
- key = read_private_key(key_to_load, resolved_passphrase,
3582
- unsafe_skip_rsa_key_validation)
3583
-
3584
- pubkey_to_load = key_prefix + '.pub'
3585
- elif isinstance(key_to_load, bytes):
3586
- if allow_certs:
3587
- key, certs_to_load = import_private_key_and_certs(
3588
- key_to_load, passphrase,
3589
- unsafe_skip_rsa_key_validation)
3590
- else:
3591
- key = import_private_key(key_to_load, passphrase,
3592
- unsafe_skip_rsa_key_validation)
3593
- else:
3594
- key = key_to_load
3595
- except KeyImportError as exc:
3596
- if skip_public or \
3597
- (ignore_encrypted and str(exc).startswith('Passphrase')):
3598
- continue
3599
-
3600
- raise
3606
+ certs_to_load = import_certificate_chain(key_data[end:])
3607
+ key_data = key_data[:end]
3601
3608
 
3602
- certs: Optional[Sequence[SSHCertificate]]
3609
+ if not certs_to_load:
3610
+ certs_to_load = key_prefix + '-cert.pub'
3603
3611
 
3604
3612
  if pubkey_or_certs:
3605
3613
  try:
@@ -3613,7 +3621,7 @@ def load_keypairs(
3613
3621
  elif certs_to_load:
3614
3622
  try:
3615
3623
  certs = load_certificates(certs_to_load)
3616
- except (OSError, KeyImportError):
3624
+ except (OSError, KeyImportError) as exc:
3617
3625
  certs = None
3618
3626
  else:
3619
3627
  certs = None
@@ -3628,16 +3636,58 @@ def load_keypairs(
3628
3636
  pubkey = import_public_key(pubkey_to_load)
3629
3637
  else:
3630
3638
  pubkey = pubkey_to_load
3639
+
3640
+ saved_exc = None
3631
3641
  except (OSError, KeyImportError):
3632
3642
  pubkey = None
3633
- else:
3643
+ elif key_prefix:
3644
+ try:
3645
+ pubkey = read_public_key(key_prefix + '.pub')
3634
3646
  saved_exc = None
3647
+ except (OSError, KeyImportError):
3648
+ try:
3649
+ pubkey = read_public_key(key_prefix)
3650
+ saved_exc = None
3651
+ except (OSError, KeyImportError):
3652
+ pubkey = None
3635
3653
  else:
3636
3654
  pubkey = None
3637
3655
 
3638
3656
  if saved_exc:
3639
3657
  raise saved_exc # pylint: disable=raising-bad-type
3640
3658
 
3659
+ if key_data is not None:
3660
+ try:
3661
+ unencrypted_key = import_private_key(
3662
+ key_data, None, unsafe_skip_rsa_key_validation)
3663
+ unencrypted_key.set_filename(key_prefix)
3664
+ except KeyImportError:
3665
+ unencrypted_key = None
3666
+
3667
+ if unencrypted_key:
3668
+ key = unencrypted_key
3669
+ elif callable(passphrase) and key_prefix and (certs or pubkey):
3670
+ enc_key = _EncryptedKey(key_data, key_prefix, passphrase, loop,
3671
+ unsafe_skip_rsa_key_validation)
3672
+
3673
+ key = certs[0].key if certs else pubkey
3674
+ else:
3675
+ try:
3676
+ resolved_passphrase = _resolve_passphrase(passphrase,
3677
+ key_prefix, loop)
3678
+
3679
+ key = import_private_key(key_data, passphrase,
3680
+ unsafe_skip_rsa_key_validation)
3681
+ key.set_filename(key_prefix)
3682
+ except KeyImportError as exc:
3683
+ if skip_public or (ignore_encrypted and
3684
+ str(exc).startswith('Passphrase')):
3685
+ continue
3686
+
3687
+ raise
3688
+ else:
3689
+ key = cast(Union[SSHKey, SSHKeyPair], key_to_load)
3690
+
3641
3691
  if not certs:
3642
3692
  if isinstance(key, SSHKeyPair):
3643
3693
  pubdata = key.key_public_data
@@ -3660,9 +3710,9 @@ def load_keypairs(
3660
3710
  result.append(key)
3661
3711
  else:
3662
3712
  if cert:
3663
- result.append(SSHLocalKeyPair(key, pubkey, cert))
3713
+ result.append(SSHLocalKeyPair(key, pubkey, cert, enc_key))
3664
3714
 
3665
- result.append(SSHLocalKeyPair(key, pubkey))
3715
+ result.append(SSHLocalKeyPair(key, pubkey, None, enc_key))
3666
3716
 
3667
3717
  return result
3668
3718
 
@@ -579,8 +579,9 @@ class _SCPSource(_SCPHandler):
579
579
  for name in await SFTPGlob(self._fs).match(srcpath):
580
580
  await self._send_files(cast(bytes, name.filename),
581
581
  b'', name.attrs)
582
- except asyncio.CancelledError:
582
+ except (KeyboardInterrupt, asyncio.CancelledError):
583
583
  cancelled = True
584
+ raise
584
585
  except (OSError, SFTPError) as exc:
585
586
  self.handle_error(exc)
586
587
  finally:
@@ -745,8 +746,9 @@ class _SCPSink(_SCPHandler):
745
746
  dstpath))
746
747
  else:
747
748
  await self._recv_files(b'', dstpath)
748
- except asyncio.CancelledError:
749
+ except (KeyboardInterrupt, asyncio.CancelledError):
749
750
  cancelled = True
751
+ raise
750
752
  except (OSError, SFTPError, ValueError) as exc:
751
753
  self.handle_error(exc)
752
754
  finally:
@@ -911,8 +913,9 @@ class _SCPCopier:
911
913
 
912
914
  try:
913
915
  await self._copy_files()
914
- except asyncio.CancelledError:
916
+ except (KeyboardInterrupt, asyncio.CancelledError):
915
917
  cancelled = True
918
+ raise
916
919
  except (OSError, SFTPError) as exc:
917
920
  self._handle_error(exc)
918
921
  finally:
@@ -1095,6 +1098,8 @@ async def _scp_handler(sftp_server: MaybeAwait[SFTPServer],
1095
1098
  if inspect.isawaitable(sftp_server):
1096
1099
  sftp_server = await sftp_server
1097
1100
 
1101
+ sftp_server: SFTPServer
1102
+
1098
1103
  fs = SFTPServerFS(sftp_server)
1099
1104
 
1100
1105
  handler: Union[_SCPSource, _SCPSink]
@@ -4656,7 +4656,8 @@ class SFTPClient:
4656
4656
  parts = path.split(b'/')
4657
4657
  last = len(parts) - 1
4658
4658
 
4659
- exc: Type[SFTPError]
4659
+ exc: Union[Type[SFTPNotADirectory], Type[SFTPFailure],
4660
+ Type[SFTPFileAlreadyExists]]
4660
4661
 
4661
4662
  for i, part in enumerate(parts):
4662
4663
  curpath = posixpath.join(curpath, part)
@@ -6775,7 +6776,9 @@ class SFTPServerHandler(SFTPHandler):
6775
6776
  data = self._server.read(src, read_from_offset, size)
6776
6777
 
6777
6778
  if inspect.isawaitable(data):
6778
- data = await cast(Awaitable[bytes], data)
6779
+ data = await data
6780
+
6781
+ data: bytes
6779
6782
 
6780
6783
  result = self._server.write(dst, write_to_offset, data)
6781
6784
 
@@ -8234,6 +8237,8 @@ async def _sftp_handler(sftp_server: MaybeAwait[SFTPServer],
8234
8237
  if inspect.isawaitable(sftp_server):
8235
8238
  sftp_server = await sftp_server
8236
8239
 
8240
+ sftp_server: SFTPServer
8241
+
8237
8242
  handler = SFTPServerHandler(sftp_server, reader, writer, sftp_version)
8238
8243
 
8239
8244
  await handler.run()
@@ -26,4 +26,4 @@ __author_email__ = 'ronf@timeheart.net'
26
26
 
27
27
  __url__ = 'http://asyncssh.timeheart.net'
28
28
 
29
- __version__ = '2.21.0'
29
+ __version__ = '2.21.1'
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: asyncssh
3
- Version: 2.21.0
3
+ Version: 2.21.1
4
4
  Summary: AsyncSSH: Asynchronous SSHv2 client and server library
5
5
  Author-email: Ron Frederick <ronf@timeheart.net>
6
6
  License: EPL-2.0 OR GPL-2.0-or-later
@@ -32,7 +32,7 @@ Requires-Dist: typing_extensions>=4.0.0
32
32
  Provides-Extra: bcrypt
33
33
  Requires-Dist: bcrypt>=3.1.3; extra == "bcrypt"
34
34
  Provides-Extra: fido2
35
- Requires-Dist: fido2>=0.9.2; extra == "fido2"
35
+ Requires-Dist: fido2<2,>=0.9.2; extra == "fido2"
36
36
  Provides-Extra: gssapi
37
37
  Requires-Dist: gssapi>=1.2.0; extra == "gssapi"
38
38
  Provides-Extra: libnacl
@@ -5,7 +5,7 @@ typing_extensions>=4.0.0
5
5
  bcrypt>=3.1.3
6
6
 
7
7
  [fido2]
8
- fido2>=0.9.2
8
+ fido2<2,>=0.9.2
9
9
 
10
10
  [gssapi]
11
11
  gssapi>=1.2.0
@@ -3,6 +3,27 @@
3
3
  Change Log
4
4
  ==========
5
5
 
6
+ Release 2.21.1 (28 Sep 2025)
7
+ ----------------------------
8
+
9
+ * Added the capability to defer invoking passphrase callback until
10
+ an encrypted private key is actually used in a signing operation,
11
+ rather than triggering the callback when keys are loaded. This
12
+ will only work when a public key is provided with an encrypted
13
+ private key either explicitly or as part of the key format (such
14
+ as in OpenSSH's private key format).
15
+
16
+ * Improved handling of KeyboardInterrupt and task cancellation in
17
+ SCP. Thanks go to Viktor Kertesz for reporting this issue and
18
+ helping to understand the behavior in various versions of Python.
19
+
20
+ * Fixed the env option to support mappings other than dict. Thanks
21
+ go to Boris Pavlovic for reporting this issue.
22
+
23
+ * Fixed a potential race condition in SSHForwarder cleanup. Thanks
24
+ go to GitHub user misa-hase for reporting this issue and helping
25
+ to test the fix.
26
+
6
27
  Release 2.21.0 (2 May 2025)
7
28
  ---------------------------
8
29
 
@@ -35,7 +35,7 @@ dynamic = ['version']
35
35
 
36
36
  [project.optional-dependencies]
37
37
  bcrypt = ['bcrypt >= 3.1.3']
38
- fido2 = ['fido2 >= 0.9.2']
38
+ fido2 = ['fido2 >= 0.9.2, < 2']
39
39
  gssapi = ['gssapi >= 1.2.0']
40
40
  libnacl = ['libnacl >= 1.4.2']
41
41
  pkcs11 = ['python-pkcs11 >= 0.7.0']
@@ -1,4 +1,4 @@
1
- # Copyright (c) 2016-2024 by Ron Frederick <ronf@timeheart.net> and others.
1
+ # Copyright (c) 2016-2025 by Ron Frederick <ronf@timeheart.net> and others.
2
2
  #
3
3
  # This program and the accompanying materials are made available under
4
4
  # the terms of the Eclipse Public License v2.0 which accompanies this
@@ -321,9 +321,8 @@ class _TestAgent(AsyncTestCase):
321
321
  async with agent:
322
322
  self.assertIsNone(await agent.add_keys([keypair]))
323
323
 
324
- async with agent:
325
- with self.assertRaises(asyncssh.KeyExportError):
326
- await agent.add_keys([key.convert_to_public()])
324
+ with self.assertRaises(asyncssh.KeyExportError):
325
+ await agent.add_keys([key.convert_to_public()])
327
326
 
328
327
  await mock_agent.stop()
329
328