rubencaro / sshex

Simple SSH helpers for Elixir. SSH is useful, but we all love SSHEx !
MIT License
160 stars 26 forks source link

Unable to use Custom RSA SSH keys - `:public_key.pem_entry_decode` raises with Case clause error #22

Closed JediLuke closed 4 years ago

JediLuke commented 4 years ago

I am attempting to connect to a server using a custom SSH key. I am using the SSHEx.ConfigurableClientKeys module to do this. I can SSH into this server perfectly fine using normal command line. Here is the code I am using

    {:ok, conn} = SSHEx.connect(
                    ip: to_charlist(droplet_info.ip_addr),
                    user: to_charlist("root"),
                    key_cb: {SSHEx.ConfigurableClientKeys, [
                      key: File.open!("./apps/anvil/ssh_keys/id_rsa"),
                      silently_accept_hosts: true,
                      auth_methods: "publickey",
                      # rsa_pass_phrase: [redacted],
                      known_hosts: File.open!("./apps/anvil/ssh_keys/known_hosts", [:read, :write]),
                      accept_hosts: true]},
                    # rsa_pass_phrase: [redacted],
                    silently_accept_hosts: true)

I believe the source of the problem is the following code. In this part of the code https://github.com/rubencaro/sshex/blob/a878194a6214e3f72a50539ae5908793cf8b4b9b/lib/sshex/configurable_client_keys.ex#L67

This appears to fail silently. I tried putting IO.inspects after it but it failed to print. I tried printing the algorithm out (first arg, _alg) and I see that calls this function many times, each with a different algorithm (I'm guessing it runs through all of them until they all fail, then it claims that the key exchange failed? - of note though is that after the first check, the File is still open, and as per the documentation it returns "" to signify EOF.

If I do that exact same code in Iex, the failure becomes apparent

iex(2)> rr = File.open!("./apps/anvil/ssh_keys/id_rsa")
#PID<0.547.0>
iex(3)> ss = rr |> IO.read(:all)
"-----BEGIN OPENSSH PRIVATE KEY-----\nb3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAABFwAAAAdzc2gtcn\nNhAAAAAwEAAQAAAQEAvYmx4ZtXZX3J5WxYAn1MrQX3e9PEp+9C4WU9MTglAXPqFGz+ZKw6\nyFG
...
AwQF\n-----END OPENSSH PRIVATE KEY-----\n"
iex(4)> tt = ss |> :public_key.pem_decode
[
  {{:no_asn1, :new_openssh},
   <<111, blah blah, 7, ...>>, :not_encrypted}
]
iex(5)> uu = tt |> List.first
{{:no_asn1, :new_openssh},
 <<111, blah blah, 7, 115, ...>>, :not_encrypted}
iex(6)> vv = uu |> :public_key.pem_entry_decode
** (CaseClauseError) no case clause matching: {"ssh-rsa", <<1, 0, 1>>}
    (public_key) pubkey_ssh.erl:204: :pubkey_ssh.new_openssh_decode/6

If I comment out that line, I still can't SSH, but I can see that each time it loops through it at least re-reads the File. So I suspect that this Error (which fails silently in my application somehow) is preventing some other code which closes the File, meaning each time we loop around it is still reading the same EOF from the open file. But this is not really the primary issue - this :public_key.pem_entry_decode is failing with this case clause error.

This is indeed a pretty clear case clause mismatch in the :ssh module https://github.com/erlang/otp/blob/7ed659fd1144b455883eef18dab38cddc904a065/lib/public_key/src/pubkey_ssh.erl#L204

I don't really understand what this function is doing - do we need it for an RSA key? Commenting it out still doesn't work though...

I'm happy to help with a PR when we fix it but at the moment I can't even hack it to make it work for RSA keys

rubencaro commented 4 years ago

@JediLuke maybe this is a usecase that was not expected by @svoynow when he implemented this.

Be aware that you are dealing with the key_cb option http://erlang.org/doc/man/ssh.html#type-key_cb_common_option. The SSHEx.ConfigurableClientKeys is just a helper to use that for a particular case. It actually implements this behaviour http://erlang.org/doc/man/ssh_client_key_api.html. You can see the specific usecase in the test code https://github.com/rubencaro/sshex/blob/a878194a6214e3f72a50539ae5908793cf8b4b9b/test/configurable_client_keys_test.exs.

Also remember that SSHEx is no more than a wrapper over :ssh. It will not hold any error or any crash and fail silently :grin:. It will just give you whatever :ssh returned. You failed to provide the actual response you get from SSHEx, but it's OK. It is whatever it is returned by :ssh itself.

I don't see any rsa_pass_phrase, silently_accept_hosts or auth_methods being documented or tested in the SSHEx.ConfigurableClientKeys. So let's ignore them by now.

I would suggest to make it work directly with :ssh first. You actually found out that your key may be not supported when you called :public_key.pem_entry_decode directly and got the CaseClauseError. That is the way!

I think you're right suspecting your key maybe is being checked several times against different possibilities, and when every option is tried, then fails to connect with some generic could not connect message. Check the actual Erlang version you are using, and also the exact OpenSSH options in compile time for your Erlang. Those may determine which keys (cipher algorithms) are valid and which are not for your system.

Once you know how to do what you want to do with :ssh, then you can make any adaptations to SSHEx.ConfigurableClientKeys itself. That would be to add some test case and then implement the solution to get it to pass.

I close the issue now (nothing else I can do, and no problem with SSHEx itself), but keep asking here if you think I know something you need. If you have the solution just go for it and open the PR.

Good luck Luke! May the force be with you.

JediLuke commented 4 years ago

User @voltone gave this amazing reply in the Elixir Slack

voltone 1:38 AM @jediluke here’s some code I once wrote to read in OpenSSH RSA keys: https://gist.github.com/voltone/4cf8813dbc4161abe84485d084513964 Use it like this to read a key file: File.read!("path/to/key") |> :public_key.pem_decode |> hd() |> SshRsaKey.pem_entry_decode() Though it might be easier to just convert the key file from OpenSSH format to PKCS8 format using the ssh-keygen tool

I cannot get it to work with this, but feel like it is a huge gain in knowledge. I shall continue trying to get it to work, I haven't been using the key_cb_private option field at all so far, it's possible (likely??) I have merely been using the function incorrectly. Give me a few more days and I'll report back with results.

voltone commented 4 years ago

I updated the Gist to fall through to :public_key for PKCS8 and other PEM entries. So now replacing the call to :public_key.pem_entry_decode(pem) with SshRsaKey.pem_entry_decode(pem) in user_key/2 should enable support for OpenSSH RSA keys.

Feel free to copy the SshRsaKey module if it is useful, I added a note to put the code in the public domain.

BTW, I seem to remember some work was being done (has been done?) to add native support for OpenSSH private keys in recent OTP versions, but I haven't had time to look into that...