-
Notifications
You must be signed in to change notification settings - Fork 155
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Prompt for passphrase if private key is encrypted #426
Comments
This is an interesting idea! You wouldn't be able to invoke a regular callable directly if it was going to block waiting for user input, as that would prevent the asyncio event loop from making progress on other tasks. However, you could run something like that in an asyncio executor. Here's a simple example of how you might do something like this yourself: async def read_key(path, prompt_passphrase):
try:
return asyncssh.read_private_key(path)
except asyncssh.KeyImportError:
loop = asyncio.get_event_loop()
passphrase = await loop.run_in_executor(None, prompt_passphrase)
return asyncssh.read_private_key(path, passphrase) In my test case, I used getpass to read the passphrase: def prompt_passphrase():
return getpass.getpass('Passphrase: ') Then, to actually read the key, I used something like: k = await read_key('/tmp/k', prompt_passphrase) If the key isn't encrypted, this reads and returns it immediately. If it is encrypted, though, the except block runs and requests the passphrase in an executor so that the event loop keeps running in the meantime. Once the passphrase is entered, it returns the decrypted key (or an exception if the passphrase was wrong). With a tiny bit more effort, this could probably also be changed to support passing in an awaitable, which could be used without have to involve an executor while still allowing the event loop to keep running. I think something like the following would work: async def read_key(path, passphrase):
try:
return asyncssh.read_private_key(path)
except asyncssh.KeyImportError:
if inspect.iscoroutine(passphrase):
passphrase = await passphrase
elif callable(passphrase):
loop = asyncio.get_event_loop()
passphrase = await loop.run_in_executor(None, passphrase)
return asyncssh.read_private_key(path, passphrase) This version accepts awaitables, callables, or a static value. I see only one problem with integrating this into the existing AsyncSSH code. All of the functions that actually load the keys are non-async, expecting to always return immediately and with no guarantee that they are even being invoked from inside an asyncio event loop. So, at the point where the code knows if a passphrase is going to be required or not, there's no way to trigger this new behavior. It would have to be done up front in a async function (like in the example above). |
Thanks for the explanation! I think I understand the problem you are pointing out. I do not have much experience writing async code in python, but I wonder if we could check if we are running in an event loop and trigger the correct behavior based on that information. It looks like it is possible to check if we are inside an event loop: import asyncio
def regular_function():
try:
asyncio.get_running_loop()
except RuntimeError:
print('Not in an event loop')
else:
print('In an event loop right now')
async def main():
regular_function()
asyncio.run(main()) # Output: In an event loop right now
regular_function() # Output: Not in an event loop |
Yes - it's possible to tell if a non-async function is called from within an event loop or not. However, a non-async function can't actually await anything. That's only possible from an async function, and the existing AsyncSSH API functions that take passphrases are almost all non-async functions right now. There are some exceptions, like the top-level connect() call, but all the functions that import or read private keys or key pairs are all non-async. A new API (using an async function) would be needed for them. Given that a caller of AsyncSSH can do this sort of thing themselves in just a few lines of code, I'm not sure it's worth the disruption it would cause in the existing library code to try and add it there. I do like the concept, but if the caller knows they need this, it could be as simple as replacing: k = asyncssh.read_private_key(path, passphrase) with: k = asyncssh.read_private_key(path, await prompt_passphrase()) ...if prompt_passphrase was an awaitable. If it is a plain callable, it's one extra line, but still quite easy: loop = asyncio.get_event_loop()
k = asyncssh.read_private_key(path, await loop.run_in_executor(None, prompt_passphrase)) In Python 3.9 and later, this gets even simpler for callables: k = asyncssh.read_private_key(path, await asyncio.to_thread(prompt_passphrase)) |
Closing due to inactivity. Feel free to re-open this or create a new issue if you're unable to get the above suggestions to work. |
The approach you've suggested relies on knowing the path to the key, and knowing that a key file will be used at all. AsyncSSH already determines this, but doesn't seem to export functions that could ease this for the user (i.e. something like I think it would be pretty nice if |
Interesting idea. I'll take a closer look at this over the weekend. My thinking is to simply allow the existing For passwords, there's already support for a callback by subclassing SSHClient and implementing |
Yes, the solution you're proposing is even better. I also don't think Many thanks! |
Ok - part 1 of this change is now in the "develop" branch as commit f4df7f4. It adds support for the password argument in SSHClientConnectionOptions to be a callable, coroutine function, or an awaitable which return either a string or Note that you if you are trying to prompt for a password interactively through stdin/stdout, you need to be careful not to do blocking I/O there, since that will block the event loop from running. If you choose to do async I/O on stdin/stdout, that may cause other problems if you later try to use blocking functions like print() or do logging to there. Next up is a similar change for setting the passphrase to use when loading private keys from files. |
As it turns out, supporting an awaitable when loading key pairs is a bit tricky, as the load function itself is not currently an async function. All of the functions which determine the config (including the key-loading functions) are synchronous right now. It would be easy to add support for a callable here, but it would be much trickier to add support for an awaitable. So, I think as an initial attempt I'll add support for only a callable in this case. |
Ok - I've got something working here, but I'm not sure it will solve your use case, if you really need to make a blocking call after finding out which key file is being loaded. You'd basically have to do the key loading prior to starting the event loop, or be ok with the loop blocking all tasks until the passphrase was provided. Once the key is decrypted, you could then start up an event loop with the already-loaded set of keys. |
Looking at the code more closely, the main AsyncSSH connect/listen functions already do the config evaluation in a separate thread (using an executor), so it turns out that running the synchronous callable won't block the asyncio event loop after all. It could if you created your own SSHClientConnectionOptions object, but if you wanted to do that you'd just need to make sure that you created that options object from within an executor as well, and the callable you provide will be run from within that. A first cut of this code is now checked in as commit 56e533b if you'd like to give it a try. |
I was hoping to prompt for the passphrase using Textual's ModalScreen dialog. Textual also uses Do I understand correctly, you're saying that:
If that's the case, then I believe I could use the thread-safe |
Allright, it looks like it works :-) I've created a stub app that:
Here it is: https://gist.github.com/goblin/b0651fa0bc90c711b5a0d9bf74adbb08 I've decided to use Two caveats:
In case of 1., I believe AsyncSSH shouldn't call the callable on unencrypted keys. In case of 2., we might argue that it's the app's job to re-ask for the passphrase upon seeing Overall I'm very impressed with your lightning-fast response, thank you very much for adding this new feature so quickly! |
Also, I just wanted to point out: if you'd like to also support awaitables instead of just callables (which could be very handy for my use case), you could perhaps use janus internally in a similar fashion. I haven't considered all the edge cases, so my stub app might be prone to deadlocks or other races, but the general idea seems to work. |
Not quite. The actual Because this is running in a separate thread, anything happening in the main thread's asyncio event loop will continue to run unaffected by whatever the thread might be doing. If you want to actually use something in the event loop to read the key, you should be able to use I can see where you'd want the callback to not be invoked on unencrypted keys, but that turns out to be quite a bit more difficult and invasive. First, this would involve pushing this change done MUCH deeper than it is right now, and it would need to be customized for each of the different key formats, since each one has a different way to represent encrypted keys. Also, in at least one case, there is no way to identify in advance whether a key is encrypted or not. The code currently just tries to decrypt the key blindly if you provide a passphrase and if that fails then it falls back to seeing if the read is readable without decryption, but it actually has to try both PKCS#8 decoding and PKCS#1 decoding on that data, and there are actually multiple PKCS#1 encodings that need to be tried. Also, I can see an argument where you'd want to wait and see what keys the server will even let you attempt to authenticate with before trying to decrypt the keys, but that's not how the current code works, and depending the key format it might require access to a separate public key file which isn't always present on the client. Normally, the public key can be determined from the private key, but only if you decrypt it first. I'm just not sure the code disruption that would be needed here is really justified.
At the very least, a syntax change would be required here, as there's no such thing as an async constructor. So, instead of When I started out, this options construction code did not involve any blocking calls, so there was no need for an executor. However, when I added support for things like PKCS#11 smart cards and Fido keys, I ran into the problem that those libraries weren't async. By then, though, there was no way to change the code without breaking backward compatibility. So, this issue isn't really new to adding this callback. It's just another example where it might come up, depending on how you choose to determine the passphrase to return. |
Indeed! I've updated the gist and it's way simpler now, without janus. Thanks!
OK, Revision 3 of the gist now works around this by simply trying to
OK, fair enough. It would be nice to have, but the way it currently works is good enough, I think. At least in my simple case where the correct
Yes, however, I'm no longer using Instead, I'm using |
Happy to help!
Yeah - that should work, though it a bit wasteful in terms of throwing away the key in the case where it is unencrypted and having to read it all over again. I know you wanted to leverage the existing SSH config file to determine which key to load, but if you had that mapping available already in your code, you could just load the keys yourself, prompting for passphrase only if the read without a passphrase failed, and you could also do the retry on a bad passphrase in that case. I'm still thinking about what it would take to defer calling the passphrase callback until the code was actually doing the public key authentication with an SSH server. That would require adding some way to create an SSHKeyPair object that stored something like a public key, the encrypted version of a private key, and an associated passphrase callback to call when the server accepted the initial public key proposal and it was time to actually do the signing with a private key. This probably wouldn't be possible in all cases, unless the user kept both an encrypted private key and a corresponding public key on the client machine as two separate files (which is often the case, but technically isn't required). The good news is that the new OpenSSH private key format actually lets you do this all in one file, but many people using RSA or ECDSA which are still using PKCS#8 or even PKCS#1 format. My main concern here is that this is just too big a change, especially when there are other ways to do this today by just having the application read the keys itself and passing the already decrypted version of them into AsyncSSH.
The problem is actually not with passing in an awaitable. That could be done just fine even in the options constructor case. However, the fact remains that all of the options processing is currently synchronous. None of the functions used there would be able to do an "await" on something if an awaitable was passed in. All that code would need to be reworked to allow awaitables, and even then we'd have the problem that some of the calls currently block, so I'd still need to use an executor for those. I suppose one option might be to leave everything running in the executor as it is now, but use |
Agreed, it's a bit wasteful, but good enough as a workaround for me for now.
Is there a way for me to get that mapping from OpenSSH config files, short of parsing them all on my own? I mean, can I leverage the existing code in AsyncSSH somehow to aid in this?
If it was me, I'd create an issue for it and consider doing it later, maybe together with some other bigger changes. Like you say, it's probably too big of a change for what it's worth just for this.
It's just hard for the application to redo all the SSH config parsing work (and check all the conditionals etc.). If the application uses totally it's own, separate config, then yeah, that would be a viable solution. Unless I'm missing something and the interface for parsing those files and determining which key to use is already available somewhere in AsyncSSH?
Cool, yeah, that definitely looks like a good option. The async context manager creator ( |
There is an "SSHConfig" class with a
That's fair. If you want to create a new issue to track this, I'm open to that. |
I've gone ahead and add a note in |
I've also updated my gist at https://gist.github.com/goblin/b0651fa0bc90c711b5a0d9bf74adbb08 to Revision 4, which now makes use of the awaitable support you've added. Great stuff, many thanks! |
Looks good! Thanks for all your help working through this... |
Hi there.
By browsing the code and looking a some Github issues, it looks like that passing a passphrase as argument is required when the private key to be used is encrypted. While that makes a lot of sense, I think it would be nice if
asyncssh
provided a way to automatically prompt for the passphrase if the private key is encrypted. For example:Where
prompt_passphrase
is a callable that will interactively read the passphrase from the keyboard. Such a callable would be used only ifmy_ssh_key
is encrypted.The text was updated successfully, but these errors were encountered: