git2r icon indicating copy to clipboard operation
git2r copied to clipboard

Better search for default SSH keys on Windows

Open stewid opened this issue 7 years ago • 49 comments

This is a follow-up on #329

Currently, the git2r default is to look for SSH keys (id_rsa) in file.path(Sys.getenv("USERPROFILE"), ".ssh"). It seems that this is not always the default location. Can the search be improved?

stewid avatar Jun 11 '18 07:06 stewid

From my (cursory) look through the git2r source, I can't tell how the putative location of SSH keys is even determined. Is that happening in source here in git2r or even in libgit2? Or is this perhaps inherited from some other dependency?

I have experienced this problem personally in the past past couple of days on my Windows VM. And, indeed, using git2r::cred_ssh_key() to explicitly create a credential, then passing that into relevant git2r calls works.

I have found that the defaults are "good enough", e.g. I have success with indirect use in usethis like so:

use_github(credentials = git2r::cred_ssh_key())

That is due to the relatively recent addition of code that honors where .ssh/ usually lives on Windows ():

https://github.com/ropensci/git2r/blob/358fcd2e8ef5d754d51f456f0b059be79ac119be/R/credential.R#L176-L210

More verbose and, in my case equivalent, is to provide explicit paths:

cred <- git2r::cred_ssh_key(
    publickey = fs::path_home(".ssh/id_rsa.pub"),
    privatekey = fs::path_home(".ssh/id_rsa")
)
use_github(credentials = cred)

I am tempted to use credentials = git2r::cred_ssh_key() in usethis, when on Windows and when user has not provided an explicit credential. But this seems a bit aggressive, i.e. too much interfering in the natural order of things. Then I contemplate doing this only after an initial failure of git2r/libgit2 to find SSH keys automagically.

Do you understand @stewid (or @jeroen) where the putative location of SSH keys is truly determined and the most proper place to intervene and get better default behavior on Windows?

jennybc avatar Jun 18 '18 05:06 jennybc

One thing that makes this ambiguous is that R on Windows uses the user's Documents folder as the HOME directory ~. This is different from most programs that use C:\Users\$username by default.

R sets the environment variable $HOME and therefore C libraries may pick up on this. I haven't tested yet but it looks like libgit2 searches the $HOME location first for config files.

However I am not sure this is applies to the ~/.ssh files because ssh is handled by libssh2.

Also note that ssh keys may also be named id_ed25519, id_ecdsa, etc, see also the ssh vignette.

jeroen avatar Jun 18 '18 06:06 jeroen

I'm aware of this ambiguity re: home directory on Windows:

http://happygitwithr.com/ssh-keys.html#git2r-or-some-other-tool-cant-find-ssh-keys-on-windows

I'm beginning to think the least of all evils is to strongly recommend that Windows users create a symbolic link from C:\Users\username\.ssh to C:\Users\username\Documents\.ssh. Or vice versa if they've got it the other way 'round.

Actually interfering in whatever process git2r is (indirectly) using seems a like a good way to create even more headaches and maintenance woes.

jennybc avatar Jun 18 '18 14:06 jennybc

If I pursue 👆, I would create some sort of doctor or helper function in usethis to help user achieve this. usethis already depends on fs, so I can provide code (or execute it, even) to make the link.

jennybc avatar Jun 18 '18 14:06 jennybc

Windows doesn't really have symlinks like we have on unix. This is one of the major problems porting software to Windows.

There is something on Windows called symlinks but as far as I know these are difficult to create and only available to sysadmins so most users may not be able to use it. Windows does have something similar called native links, but I don't think R and libgit2 can read those.

I think the best solution is that git2r checks for both locations for an .ssh directory in R code and then passes this explicitly pass this location to libgit2 C code (I think that is what @stewid suggests).

jeroen avatar Jun 18 '18 15:06 jeroen

In the Happy Git page linked to above, we have example code for doing this:

MKLINK /D "C:\Users\username\Documents\.ssh" "C:\Users\username\.ssh"

provided by @ijlyttle. It's also what's recommended here (re: gitconfig, but same idea):

https://www.onwebsecurity.com/configuration/git-on-windows-location-of-global-configuration-file.html

So there's some evidence that it works in the wild.

I think it would be really nice for git2r and usethis to stay out of the business of actively finding ssh and git config files. I'd rather help the user get a robust set up that's likely to satisfy command line git and libgit2.

I will experiment on Windows and report back ...

jennybc avatar Jun 18 '18 15:06 jennybc

@jennybc thanks for your work on this.

I would like to refactor the callback git2r_cred_acquire_cb (https://github.com/ropensci/git2r/blob/master/src/git2r_cred.c#L214) to try harder to provide working SSH credentials from default locations. If a connection fails, the callback is called again and we can try the next default key location, similar to the ssh package. I think we can also add support for using git credentials (https://git-scm.com/docs/gitcredentials). I can create a new branch credentials for this.

stewid avatar Jun 20 '18 15:06 stewid

I just deleted my last comment above and moved it to a new issue: #356 Set up and troubleshooting advice for ssh-agent on Windows

because I think there are at least two distinct issues around getting SSH to "just work" on Windows.

Yes your plan to look harder in default locations before resorting to the ssh-agent sounds fantastic! It would have benefits beyond Windows, if I understand you correctly.

It seem like the relevant parts exist, but right now a lot of the burden still lies on the user, even though the main hiccup (multiple possible locations for .ssh/) is quite predictable.

jennybc avatar Jun 20 '18 16:06 jennybc

@jennybc I have made an internal test function in the credentialsbranch that tries to list available ssh keys. Does it find the keys on a Windows machine?

Example how to use it:

.Call(git2r:::git2r_ssh_keys)
#> private: /home/stefan/.ssh/id_ed25519 public: /home/stefan/.ssh/id_ed25519.pub
#> private: /home/stefan/.ssh/id_rsa public: /home/stefan/.ssh/id_rsa.pub
#> NULL

stewid avatar Jun 21 '18 17:06 stewid

No it does not find my keys on Windows. But they are there.

> .Call(git2r:::git2r_ssh_keys)
NULL

> fs::dir_ls(fs::path_home(), all = TRUE, regexp = "ssh")
C:/Users/JennyVM/.ssh

> fs::dir_ls(fs::path_home(".ssh"))
C:/Users/JennyVM/.ssh/2017-12-windows-private-key.ppk
C:/Users/JennyVM/.ssh/agent.env
C:/Users/JennyVM/.ssh/id_ed25519
C:/Users/JennyVM/.ssh/id_ed25519.pub
C:/Users/JennyVM/.ssh/id_rsa
C:/Users/JennyVM/.ssh/id_rsa.pub
C:/Users/JennyVM/.ssh/known_hosts

jennybc avatar Jun 21 '18 18:06 jennybc

Ok, I have changed to use R_ExpandFileName("~") to find home.

stewid avatar Jun 21 '18 18:06 stewid

I'm not on Windows right now, but I would assume that will look in C:/Users/JennyVM/Documents, no? Which would miss my keys.

jennybc avatar Jun 21 '18 18:06 jennybc

I think you are right. I have changed again. Now I'm only trying to find the paths to: "%HOME%\\", "%HOMEDRIVE%%HOMEPATH%\\" and "%USERPROFILE%\\"

stewid avatar Jun 21 '18 21:06 stewid

Installed from aea8f2f9a3502fe70e5ed310792f2799490c9e5d, I see this:

> .Call(git2r:::git2r_ssh_keys)
path: C
path: C
path: C
NULL

jennybc avatar Jun 21 '18 23:06 jennybc

I think that is because I didn't map the wide character string (from ExpandEnvironmentStringsW) to utf8. I have now fixed that (I hope)

stewid avatar Jun 22 '18 12:06 stewid

Perhaps it's easier to implement that logic in R using enc2utf8()?

jeroen avatar Jun 22 '18 13:06 jeroen

Definitely easier :) Let us see if this works, or I change approach.

stewid avatar Jun 22 '18 13:06 stewid

The reason I'm trying to do this in C code is that the libgit2 callback (if the remote host requires authentication) provides information about the requested authentication type. So instead of always doing the search before calling e.g. clone, my idea is to find the ssh keys when they are needed.

stewid avatar Jun 22 '18 14:06 stewid

I can't compile on Windows from 6e191c200f63e9cd0af4a6ee1f00fd6403043a68:

> devtools::load_all(".") Loading git2r Re-compiling git2r Running "C:/PROGRA~1/R/R-34~1.3/bin/x64/Rcmd.exe" INSTALL "C:/Users/JennyVM/Documents/git2r" \ "--library=C:\Users\JennyVM\AppData\Local\Temp\RtmpGqLLNN\devtools_install_37c68c712e1" --no-R \ --no-data --no-help --no-demo --no-inst --no-docs --no-exec --no-multiarch --no-test-load * installing *source* package 'git2r' ...

WARNING: this package has a configure script It probably needs manual configuration


** libs rm -f git2r.dll git2r.o git2r_S3.o git2r_arg.o git2r_blame.o git2r_blob.o git2r_branch.o git2r_checkout.o git2r_clone.o git2r_commit.o git2r_config.o git2r_cred.o git2r_diff.o git2r_error.o git2r_graph.o git2r_index.o git2r_libgit2.o git2r_merge.o git2r_note.o git2r_object.o git2r_odb.o git2r_oid.o git2r_push.o git2r_reference.o git2r_reflog.o git2r_remote.o git2r_repository.o git2r_reset.o git2r_revparse.o git2r_revwalk.o git2r_signature.o git2r_stash.o git2r_status.o git2r_tag.o git2r_transfer.o git2r_tree.o "C:/PROGRA~1/R/R-34~1.3/bin/x64/Rscript.exe" "../tools/winlibs.R" 0.27.2 c:/Rtools/mingw_64/bin/gcc -I"C:/PROGRA~1/R/R-34~1.3/include" -DNDEBUG -I../windows/libgit2-0.27.2/include -DR_NO_REMAP -DSTRICT_R_HEADERS -O2 -Wall -std=gnu99 -mtune=generic -UNDEBUG -Wall -pedantic -g -Og -fdiagnostics-color=always -c git2r.c -o git2r.o c:/Rtools/mingw_64/bin/gcc -I"C:/PROGRA~1/R/R-34~1.3/include" -DNDEBUG -I../windows/libgit2-0.27.2/include -DR_NO_REMAP -DSTRICT_R_HEADERS -O2 -Wall -std=gnu99 -mtune=generic -UNDEBUG -Wall -pedantic -g -Og -fdiagnostics-color=always -c git2r_S3.c -o git2r_S3.o c:/Rtools/mingw_64/bin/gcc -I"C:/PROGRA~1/R/R-34~1.3/include" -DNDEBUG -I../windows/libgit2-0.27.2/include -DR_NO_REMAP -DSTRICT_R_HEADERS -O2 -Wall -std=gnu99 -mtune=generic -UNDEBUG -Wall -pedantic -g -Og -fdiagnostics-color=always -c git2r_arg.c -o git2r_arg.o c:/Rtools/mingw_64/bin/gcc -I"C:/PROGRA~1/R/R-34~1.3/include" -DNDEBUG -I../windows/libgit2-0.27.2/include -DR_NO_REMAP -DSTRICT_R_HEADERS -O2 -Wall -std=gnu99 -mtune=generic -UNDEBUG -Wall -pedantic -g -Og -fdiagnostics-color=always -c git2r_blame.c -o git2r_blame.o c:/Rtools/mingw_64/bin/gcc -I"C:/PROGRA~1/R/R-34~1.3/include" -DNDEBUG -I../windows/libgit2-0.27.2/include -DR_NO_REMAP -DSTRICT_R_HEADERS -O2 -Wall -std=gnu99 -mtune=generic -UNDEBUG -Wall -pedantic -g -Og -fdiagnostics-color=always -c git2r_blob.c -o git2r_blob.o c:/Rtools/mingw_64/bin/gcc -I"C:/PROGRA~1/R/R-34~1.3/include" -DNDEBUG -I../windows/libgit2-0.27.2/include -DR_NO_REMAP -DSTRICT_R_HEADERS -O2 -Wall -std=gnu99 -mtune=generic -UNDEBUG -Wall -pedantic -g -Og -fdiagnostics-color=always -c git2r_branch.c -o git2r_branch.o c:/Rtools/mingw_64/bin/gcc -I"C:/PROGRA~1/R/R-34~1.3/include" -DNDEBUG -I../windows/libgit2-0.27.2/include -DR_NO_REMAP -DSTRICT_R_HEADERS -O2 -Wall -std=gnu99 -mtune=generic -UNDEBUG -Wall -pedantic -g -Og -fdiagnostics-color=always -c git2r_checkout.c -o git2r_checkout.o c:/Rtools/mingw_64/bin/gcc -I"C:/PROGRA~1/R/R-34~1.3/include" -DNDEBUG -I../windows/libgit2-0.27.2/include -DR_NO_REMAP -DSTRICT_R_HEADERS -O2 -Wall -std=gnu99 -mtune=generic -UNDEBUG -Wall -pedantic -g -Og -fdiagnostics-color=always -c git2r_clone.c -o git2r_clone.o c:/Rtools/mingw_64/bin/gcc -I"C:/PROGRA~1/R/R-34~1.3/include" -DNDEBUG -I../windows/libgit2-0.27.2/include -DR_NO_REMAP -DSTRICT_R_HEADERS -O2 -Wall -std=gnu99 -mtune=generic -UNDEBUG -Wall -pedantic -g -Og -fdiagnostics-color=always -c git2r_commit.c -o git2r_commit.o c:/Rtools/mingw_64/bin/gcc -I"C:/PROGRA~1/R/R-34~1.3/include" -DNDEBUG -I../windows/libgit2-0.27.2/include -DR_NO_REMAP -DSTRICT_R_HEADERS -O2 -Wall -std=gnu99 -mtune=generic -UNDEBUG -Wall -pedantic -g -Og -fdiagnostics-color=always -c git2r_config.c -o git2r_config.o c:/Rtools/mingw_64/bin/gcc -I"C:/PROGRA~1/R/R-34~1.3/include" -DNDEBUG -I../windows/libgit2-0.27.2/include -DR_NO_REMAP -DSTRICT_R_HEADERS -O2 -Wall -std=gnu99 -mtune=generic -UNDEBUG -Wall -pedantic -g -Og -fdiagnostics-color=always -c git2r_cred.c -o git2r_cred.o git2r_cred.c: In function 'git2r_path_from_environment_variable': git2r_cred.c:233:5: error: 'len_utf8' undeclared (first use in this function) len_utf8 = WideCharToMultiByte( ^ git2r_cred.c:233:5: note: each undeclared identifier is reported only once for each function it appears in git2r_cred.c:234:18: error: 'WC_ERR_INVALID_CHARS' undeclared (first use in this function) CP_UTF8, WC_ERR_INVALID_CHARS, path,-1, NULL, 0, NULL, NULL); ^ git2r_cred.c: At top level: git2r_cred.c:209:12: warning: 'git2r_file_exists' defined but not used [-Wunused-function] static int git2r_file_exists(const char *path) ^ make: *** [git2r_cred.o] Error 1 Warning: running command 'make -f "Makevars.win" -f "C:/PROGRA~1/R/R-34~1.3/etc/x64/Makeconf" -f "C:/PROGRA~1/R/R-34~1.3/share/make/winshlib.mk" -f "C:\Users\JennyVM\AppData\Local\Temp\RtmpGqLLNN\file37c663237b7" SHLIB="git2r.dll" WIN=64 TCLBIN=64 OBJECTS="git2r.o git2r_S3.o git2r_arg.o git2r_blame.o git2r_blob.o git2r_branch.o git2r_checkout.o git2r_clone.o git2r_commit.o git2r_config.o git2r_cred.o git2r_diff.o git2r_error.o git2r_graph.o git2r_index.o git2r_libgit2.o git2r_merge.o git2r_note.o git2r_object.o git2r_odb.o git2r_oid.o git2r_push.o git2r_reference.o git2r_reflog.o git2r_remote.o git2r_repository.o git2r_reset.o git2r_revparse.o git2r_revwalk.o git2r_signature.o git2r_stash.o git2r_status.o git2r_tag.o git2r_transfer.o git2r_tree.o"' had status 2 ERROR: compilation failed for package 'git2r'

  • removing 'C:/Users/JennyVM/AppData/Local/Temp/RtmpGqLLNN/devtools_install_37c68c712e1/git2r' In R CMD INSTALL Error in run(bin, args = real_cmdargs, stdout_line_callback = real_callback(stdout), : System command error

jennybc avatar Jun 22 '18 15:06 jennybc

I have fixed the build

stewid avatar Jun 22 '18 16:06 stewid

Now we're making progress! But still isn't returning path to my keys.

Installed from e6e2de5d23d0b91769ce0a38fc8be9ffbe83cf02

> .Call(git2r:::git2r_ssh_keys)
path: C:/Users/JennyVM/Documents\
path: C:\Users\JennyVM\
path: C:\Users\JennyVM\
NULL

jennybc avatar Jun 22 '18 17:06 jennybc

I have added code to check for the keys.

stewid avatar Jun 22 '18 19:06 stewid

Now it works! Installed from 5e8baea36896cf3a20b1d0d29510d6941b245b3d

> .Call(git2r:::git2r_ssh_keys)
private: C:\Users\JennyVM\.ssh/id_ed25519 public: C:\Users\JennyVM\.ssh/id_ed25519.pub
private: C:\Users\JennyVM\.ssh/id_rsa public: C:\Users\JennyVM\.ssh/id_rsa.pub
private: C:\Users\JennyVM\.ssh/id_ed25519 public: C:\Users\JennyVM\.ssh/id_ed25519.pub
private: C:\Users\JennyVM\.ssh/id_rsa public: C:\Users\JennyVM\.ssh/id_rsa.pub
NULL

Does it make sense that the keys are each showing up twice?

Note you've got a compilation warning now:

c:/Rtools/mingw_64/bin/gcc  -I"C:/PROGRA~1/R/R-34~1.3/include" -DNDEBUG -I../windows/libgit2-0.27.2/include -DR_NO_REMAP -DSTRICT_R_HEADERS         -O2 -Wall  -std=gnu99 -mtune=generic -UNDEBUG -Wall -pedantic -g -Og -fdiagnostics-color=always -c git2r_cred.c -o git2r_cred.o
git2r_cred.c: In function 'git2r_ssh_keys':
git2r_cred.c:273:57: warning: passing argument 2 of 'git2r_path_from_environment_variable' discards 'const' qualifier from pointer target type
         if (git2r_path_from_environment_variable(&path, env[i]))
                                                         ^
git2r_cred.c:226:5: note: expected 'wchar_t *' but argument is of type 'const wchar_t *'
 int git2r_path_from_environment_variable(char** out, wchar_t *env)
     ^

jennybc avatar Jun 22 '18 19:06 jennybc

Great :smile: I will fix the duplicates and compiler warning. The next step is to integrate this code in the libgit2 callback.

stewid avatar Jun 22 '18 19:06 stewid

On my machine, git2r now finds the default keys so that it works to push/pull/clone. Note, it doesn't handle passphrase protected keys yet (https://github.com/ropensci/git2r/blob/credentials/src/git2r_cred.c#L385).

stewid avatar Jun 23 '18 12:06 stewid

which branch should I install to get this behavior? 😸 thanks

maelle avatar Jun 28 '18 06:06 maelle

The credentials branch. Thanks

stewid avatar Jun 28 '18 06:06 stewid

With that branch I still need to write

cred <- git2r::cred_ssh_key(file.path(Sys.getenv("HOME"), ".ssh", "id_rsa.pub"), file.path(Sys.getenv("HOME"), ".ssh", "id_rsa")) 
git2r::push(r, "origin", "refs/heads/master", credentials = cred)

maelle avatar Jun 28 '18 06:06 maelle

@maelle thanks for testing. I have fixed to also handle passphrase protected keys using getPass. Does it work now?

stewid avatar Jun 29 '18 19:06 stewid

It doesn't 😿 I installed the credentials branch ropensci/git2r@5c3659c and still need to first create cred and pass it as an argument to git2r::push

maelle avatar Jun 30 '18 06:06 maelle