Over the last few years I have standardized my access to remote servers, including GitHub, using a GPG signing subkey as the authentication credential. Having this stored in secure YubiKey hardware and locked behind a PIN is a step up in security; authenticating to the remote resource requires physical possession of an unphishable hardware token and knowledge of a PIN. Moreover, this allows me to sign GPG commits and tags.
While I had set this up on Linux and macOS since 2017 I only had the time and patience to do that on my tertiary machine, a Windows 10 one, in late 2019. I had to redo everything last week and I realised I couldn't remember a few non-obvious but critical steps. Hence this article where I explain how to combine a YubiKey, GPG4WIN, PuTTY and Git for Windows on Windows 10 to access your GitHub account – and any SSH server – securely and sign your commits.
While what I describe is geared towards GitHub, the most popular Git hosting platform, it is by no means GitHub specific. Signing your commits and authenticating with a GPG key is something you can do with any kind of Git hosting, even something you host on your own server. Moreover, authenticating to SSH using a GPG key is definitely something that works in a context outside of Git. I use it, for example, to access my home server from wherever I may be over SSH.
Why do you need to bother with all of that
None of this is strictly necessary. At its simplest, you can use GitHub through its Windows application and you can log into your servers using a username and password.
However, if you are part of a team of more than two people with a small handful of commits every day or if you are publishing your code (e.g. as part of an open source library or application) there's always a fear lurking. How can I be sure that only people who are authorized to do so make changes to the repository? How can I be sure that the commits are attributed to the person who actually authored the code? How can I be sure that nobody uploaded a malicious package of my software on my server by guessing my username and password?
Signing your commits
Think about it for a minute. Git simply attaches a name and an email address to each commit. If you were to use my name and email address it'd appear that I made a change, not you. This can be easily abused. What if you accept a PR which includes a sinister commit bearing your own name to malicious code? How do you prevent damage to your professional reputation?
The answer to that is signing all your commits with a GPG key and requiring everyone else to do too. Signed commits carry a cryptographic signature which cannot be forged. You no longer rely on just a name and email that anyone can forge with no effort at all. You rely on advanced cryptography to say that this person whose GPG key was used to signed the commit is the author of that change.
Authenticating securely
When you are thinking about a remote Git server you are thinking of either of two cases. Accessing the remote server over HTTP (WebDAV) or SSH. That is to say, Git does not have a server protocol, unlike Subversion and other centralized version control systems. In most cases you will be using Git over SSH. This is the default for GitHub repositories for which you have commit rights.
Accessing remote servers, including GitHub, over SSH requires authenticating to the remote server. The easiest way to do it is using a username and a password but this is tedious and insecure; passwords can be phished or brute forced. A better way is using cryptographic certificates which require possession of the file to authenticate. Anyone in possession of the certificate file can impersonate you which necessitates protecting them with a password that you need to type every time. This can become tedious and there is a point to be made about the file potentially getting on the hands of an evil-doer and its password bring phished or brute-forced.
A better method is using cryptographic certificates managed by GnuPG, derived from a signing subkey of your main GnuPG key pair. This has the added benefit that your authentication is tied to a secure cryptographic system. However, if you have your private key on your computer it can be stolen and your passphrase can be phished or brute forced.
The absolute best method is using a GPG SmartCard which stores the private keys in secure hardware. The actual key never leaves the secure hardware therefore it can't be leaked. It's also protected by a PIN which means that even if someone else was in physical possession of your GPG SmartCard they'd be unable to use it without also knowing your PIN. This implements a security principle known as "something you have and something you know".
YubiKey NEO and newer versions (4, 5, ...) including their C, Ci and Nano versions all implement GPG SmartCard. In this article I assume that you are using such a device.
Prerequisites
You need to already have your signing subkey in a YubiKey. I will not go into details on how to generate a GnuPG key, signing subkeys and how to move them on a YubiKey. There are some good tutorials out there already. I assume you have already followed them. This article only deals with enabling SSH authentication on Windows 10 through GnuPG.
You need to have already installed (GPG4WIN)[https://www.gpg4win.org/]. This is what implements the GnuPG system on your Windows 10 machine. If you haven't done so already, make sure Kleopatra -- GPG4WIN's key manager -- can see your YubiKey and knows about your GPG key. If you can't already encrypt and decrypt messages with Kleopatra using your YubiKey this article won't help you any.
I also assume that you have installed and configured (Git for Windows)[https://git-scm.com/download/win].
If you are using GitHub for Windows do note that these instructions will NOT work with it. I recommend not using it. It's a good way to get you started but this article is meant to take you to the next level. Use the command line or third party software like TortoiseGit, Tower or your IDE (e.g. phpStorm) to manage your Git repos.
If you already have an SSH key in %HOMEPATH%/.ssh
(typically named id_rsa
) I'd recommend removing it (after taking a backup!). Otherwise you might end up inadvertently using it which beats the purpose of these instructions. Also, if you have a config
file you will want to remove any lines referencing your existing SSH certificates.
Finally, to tie everything together, you need to install the latest available version of PuTTY. I assume that you have installed the 64-bit version. Important! Do make sure that you have the latest version installed. At the time of this writing, June 2020, using an older version of PuTTY will not let you log into GitHub since it lacks support for the encryption methods GitHub requires for remote connections.
Make sure everything you need is in your PATH
As you might already know, Windows knows where to find executables for the commands you type in a terminal window by looking at the Path environment variable. We need to add Git, GPG and PuTTY to the path so we can both use them from a command line and let them talk to each other. This is absolutely critical for these instructions to work.
Press WIN-BREAK (hold down the Windows key and press the BREAK key). If you do not have this key on your keyboard open Control Panel and click on the green System and Security header, then the green System header.
From the left-hand side menu select Advanced system settings. It opens a new window.
Click on the Environment Variables button towards the bottom.
In the bottom section (System variables) find the Path variable and double click on it.
The following paths should be there:
C:\Program Files\Git\cmd
This is where the Git binaries are installed.C:\Program Files (x86)\Gpg4win\..\GnuPG\bin
This where GPG4WIN's GPG binaries are installed.C:\Program Files\PuTTY\
This is where PuTTY is installed.
If any of these paths is missing add it. Bear in mind that your paths may be a bit different depending on where you installed each software component and your system drive letter. If you had to add any path here click on the OK buttons until all dialogues are closed, log out and log back in.
GPG signature
Telling GitHub about your public key
Go to GitHub's SSH and GPG Keys page.
Scroll down to the GPG Keys and click the New GPG Key button.
In the big field on this new page paste your public GPG key.
If you don't know what your public GPG key is, it's easy to find. Open Kleopatra, GnuPG's key management tool. Find your key; it should be in bold type. If you have more than one find the one with the email address you are using for your Git commits. Double click on it. Click the Export button. It opens a dialogue bog with some lengthy text that looks like gibberish. Copy all of it and paste it to GitHub's page.
Back on GitHub's page click on the "Add GPG Key" button.
GitHub will list the email address(es) associated with the public GPG key you uploaded. If one of them appears as "Unverified" go to the GitHub Emails page. Enter your email address in the Add email address box and click the Add button next to it. GitHub sends you an email to that address. Follow its instructions to verify your email address.
Now every commit that you sign with your GPG key will appear on GitHub with a green "Verified" badge.
Force Git to sign all commits
We need to tell Git that all commits we make need to be automatically signed. For this, we need to open a terminal window such as PowerShell. I recommend using either Windows Terminal or ConEmu but that's just a personal preference irrelevant with the task at hand.
First, we need to know what is the key ID that you will be signing commits with. Run
gpg -K
That's a capital K there. You should get some output similar to
sec# rsa3744 2018-08-26 [SC] [expires: 2028-08-23]
ABCDEF0123456789ABCDEF0123456789ABCDEF01
uid [ultimate] Your Full Name (Primary work email) <This email address is being protected from spambots. You need JavaScript enabled to view it. >;
uid [ultimate] [jpeg image of size 5831]
ssb> rsa2048 2018-08-26 [S] [expires: 2022-06-18]
ssb> rsa2048 2018-08-26 [E] [expires: 2022-06-18]
ssb> rsa2048 2018-08-26 [A] [expires: 2022-06-18]
So I know that my key ID is ABCDEF0123456789ABCDEF0123456789ABCDEF01
.
Now run
git config --global --list
If you see any of the keys user.name
, user.email
and user.signingkey
run
git config --global --remove user.name
git config --global --remove user.email
git config --global --remove user.signingkey
Next up, we'll tell Git who we are and which key to use:
git config --global --add user.name "Your Full Name"
git config --global --add user.email This email address is being protected from spambots. You need JavaScript enabled to view it.
git config --global --add user.signingkey ABCDEF0123456789ABCDEF0123456789ABCDEF01
If it's not painfully obvious, please do replace "Your Full Name",
From now on, every commit you make will be signed with the GPG key with ID ABCDEF0123456789ABCDEF0123456789ABCDEF01.
As a side note, this does not sign tags. If you want to sign a Git tag you need to do it explicitly by running something like:
git tag 1.2.3 -sm "Tagging 1.2.3"
where 1.2.3 is your tag name, typically the version number of your software being released at this point in time. The -s
switch tells Git to sign the tag. Signed tags require a message. That's what the -m
switch does. We combine them into -sm
and then type our commit message within double quotes.
Signing tags is as important, if not more important, than signing commits. A tag's signature is invalidated if anyone tries to insert a commit further up the tree (rewriting the Git history). Therefore if someone tries to surreptitiously inject malicious code in a published tag by rewriting Git's history the signature breaks and their attempt becomes obvious. This is a great consistency feature. Please do use it, it makes it safer for consumers of your code.
GPG authentication for SSH
So far we dealt with the easy bit, signing commits and tags. Now we'll step it up a notch. We will make sure that pulling commits from and pushing commits to GitHub will only be possible when our YubiKey is physically present and we enter the PIN to unlock the signing key.
Configure Git4Win for SSH authentication
First, run Kleopatra -- GPG4Win's key manager -- if you haven't done so already. Kleopatra runs as an icon in the taskbar.
Click on the Kleopatra icon in the taskbar to open its window.
From the top menu select Settings, Configure Kleopatra.
From the left hand side click on the GnuPG system icon.
Select the Private keys tab.
Enable the following options:
- Enable SSH support
- Enable putty support
Click on OK. It takes a few seconds for Kleopatra to restart the GnuPG Agent in the background.
Let GitHub know about your GnuPG managed SSH key
Open the Git Bash application. This opens a special Bash prompt terminal for use with Git.
Run
nano ~/.bashrc
Enter the following lines at the top of the file
# Enable SSH keys over GPG
eval $(/usr/bin/ssh-pageant -r -a "/tmp/.ssh-pageant-$USERNAME")
Press CTRL-X then Y to save the file. This tells Git Bash to use the PuTTY Agent for connecting to SSH. That's a bit of plumbing we need for the next step as well as using Git Bash with the YubiKey.
Now let's load this file
source ~/.bashrc
This ran our plumbing code. Technically, it implements an SSH agent that goes through Pageant, the PuTTY SSH Agent. Only that Pageant doesn't really run, it's GPG4WIN which emulates its interface. Well, it's a bit of plumbing, that's all you need to know. In any case, let's run
ssh-add -L
This lists all the keys known to the Pageant SSH Agent which returns something along the lines of
ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDS3BjUy2xwAIlS4BsNtrsBEBz+Ho6FS+4xiDMGjeqnjBea2bXUpMCQF4ApRNTGaRgjKWtPd7EEwbwnj7JN8j9JGfUUc0+/aYjamyy5nlKZiqLQwfg5Ys1xy6h9NvP7/pumw+jguhYBKymMmXKy9ehq8dPwssmWponIjcvUKYSb6cOvHJfXZ4e6Q2oCT5jEW5scM1b04DMlKeqJflw7gnW9P8WV5hKaLgxMVMop4D09VMc83iP/01v/Ql3yW60MP2O812brwtBLJs77/jHTKcMt5/9sQyhVQkjPPRFW2uNajv7ds5nulN0L2hTT6JzPs25ABT+LmKLhyg3OCOl2bfqZ cardno:000123456789
Note the cardno:000123456789
bit? This tells us that this SSH key is not a file on our computer but managed through GPG4WIN using a GPG SmartCard, our YubiKey. Copy that text. This is your public SSH key.
Go to GitHub's SSH and GPG Keys page.
At the top of the page click on the New SSH Key.
In the Title field enter something like "YubiKey" to remember that this is the SSH key managed by your YubiKey.
In the Key box paste the public SSH key you got on the Git Bash terminal window using the instructions above.
Click on Add SSH Key.
You can now connect to GitHub using your GPG smartcard... with a few more steps that will follow below.
If you are not using GitHub you should be able to enter your SSH key in your repository service's interface. If you are managing your own server (or would like to connect to any server over SSH, not necessarily Git) the publish SSH key text is what you need to add into your /.ssh/authorized_keys
file.
Auto-start the GnuPG Agent
Using your YubiKey as a GPG SmartCard requires a bit of plumbing to be in place. In practical terms this involves having the GnuPG Agent (a.k.a. GPG Agent) and Kleopatra run automatically when you log in. If you don't do that you will not be able to use your YubiKey for GPG signing and SSH authentication. We will need to create two startup program shortcuts.
Press WIN-R (hold down the Windows key and press the R key) to open the Run dialog. In there enter
%APPDATA%\Microsoft\Windows\Start Menu\Programs\Startup
and click on OK. This opens the Startup folder. Any link you create in here will be automatically run a few seconds after you log into your Windows user account.
Click on the Windows start menu, find Kleopatra and drag it into the Startup folder. This creates a link.
Next up, right click on an empty space in the folder and choose New, Shortcut.
In the location textbox enter
%ProgramFiles(x86)%\GnuPG\bin\gpg-connect-agent.exe
and click on Next. Enter anything you want as the Name, e.g. "GnuPG Agent".
Right click on the new shortcut and choose Properties. Verify that the Target reads
%ProgramFiles(x86)%\GnuPG\bin\gpg-connect-agent.exe /bye
The /bye
parameter at the end is important and must be there.
Test the authentication to GitHub via your GnuPG key
Now we can verify everything is working together. Open PuTTY. In the Hostname box enter
This email address is being protected from spambots. You need JavaScript enabled to view it.
Under Close window on exit select Never. Click on Open. You should see a prompt to enter your YubiKey PIN. The LED on the YubiKey will flash and in a few instants you should see something like:
Using username "git".
Authenticating with public key "cardno:000123456789" from agent
Server refused to allocate pty
Hi your_github_username! You've successfully authenticated, but GitHub does not provide shell access.
This means that GPG authentication to GitHub is working!
If you see something about the card refusing to offer a signature it's because the GnuPG Agent randomly stopped working properly. This does happen sometiems, especially after your device goes to sleep and wakes up again. If you are unable to use your YubiKey for any GPG operation open a terminal and run
gpg-connect-agent.exe killagent /bye
gpg-connect-agent.exe /bye
This restarts the GnuPG Agent. Retrying the connection will now work.
Tell Git to use PuTTY for authentication
By default, Git for Windows will be using its own SSH authentication which works great for password and certificate authentication but not with GPG4WIN. Luckily, it's written in such a way that if we give it a hint that it should use PuTTY it will. Since PuTTY works with GPG4WIN, as we already tested, it will allow Git to authenticate with our YubiKey.
Press WIN-BREAK (hold down the Windows key and press the BREAK key). If you do not have this key on your keyboard open Control Panel and click on the green System and Security header, then the green System header.
From the left-hand side menu select Advanced system settings. It opens a new window.
Click on the Environment Variables button towards the bottom.
In the top section click on New. In the Variable Name field enter GIT_SSH
in all caps. In the Variable Value field enter plink
and click on OK. Now click on OK again.
If you have any open terminals or applications using Git close them now. Now try to clone, pull or push a GitHub repository. It will work, possibly asking you to enter your PIN again.
Troubleshooting
As I mentioned earlier, the GnuPG agent will frequently stop working properly. If you get random errors signing commits or authenticating to remote servers you need to restart it. Open a new terminal window and run
gpg-connect-agent.exe killagent /bye
gpg-connect-agent.exe /bye
That's it!
But what about Windows Subsystem for Linux (WSL)?
This section was added on June 24th, 2020, after the article's original publication.
If you try to use Git or SSH under WSL you will be very disappointed. You cannot connect to any server since it doesn't talk to Pageant, the SSH agent protocol used by GPG4WIN. Luckily, there's a solution for that. We will be using WSL-SSH-Pageant, a bridge between Pageant (the SSH agent implemented by GPG4Win) and the Windows Subsystem for Linux.
We'll create a new directory called bin
under your user home directory and download WSL-SSH-Pageant in it:
mkdir ~\bin
cd ~\bin
Invoke-WebRequest -Uri https://github.com/benpye/wsl-ssh-pageant/releases/download/20200408.1/wsl-ssh-pageant-amd64-gui.exe -OutFile ~\bin\wsl-ssh-pageant.exe
echo (Get-Location).Path
Note that the part in bold type is the URL for the WSL-SSH-Pageant binary. This is the April 2020 version, the latest one at the time of this writing. You may have to replace it with the URL for a newer version.
The last command prints out a path name, e.g. C:\Users\MYUSER\bin
. Copy it.
Press WIN-R (hold down the Windows key and press the R key) to open the Run dialog. In there enter
%APPDATA%
and click on OK. This opens the AppData\Roaming folder under your user account. Navigate into the Microsoft\Windows\Start Menu\Programs\Startup
subfolder. Any link you create in here will be automatically run a few seconds after you log into your Windows user account.
Right click an empty space and click on New, Shortcut.
In the new dialog click on Browse.. Double click your username, expand bin
and choose the wsl-ssh-pageant.exe
item. Click on OK, then click on Next.
Enter "WSL Pageant Bridge" as the name and click on Finish.
Right click the WSL Pageant Bridge and choose Properties. In the Target area add a space and then -wsl C:\Users\MYUSER\bin\ssh-agent.sock -winssh ssh-agent -systray
where C:\Users\MYUSER\bin is the path you copied before. Now the Target looks something like
C:\Users\MYUSER\bin\wsl-ssh-pageant.exe -wsl C:\Users\MYUSER\bin\ssh-agent.sock -winssh ssh-agent -systray
Click on OK and double click the WSL Pageant Bridge shortcut. You should see an orange key icon.
Run WSL and edit your profile file e.g. nano ~/.profile
.
Add the following line at the very end
export SSH_AUTH_SOCK=/mnt/c/Users/MYUSER/bin/ssh-agent.sock
and save. For Nano that's CTRL-X and Y to accept the changes.
Log out of WSL and your account, then log back in. You should now be able to connect to SSH and your remote Git repositories from WSL as well.
I am wondering what is it exactly that precludes Git Bash from using ssh-agent (or rather gpg-agent's ssh support) from SSH_AUTH_SOCK, rather than Plink.
I have already addressed that:
Git Bash is using its own ssh-agent which can't go through GPG4WIN's GPG binary. You need to go through GPG4WIN to have SmartCard support since the scdaemon GPG would otherwise be looking for doesn't exist on Windows.
Even if you're not using a SmartCard you can't really use the Git Bash version of GPG because it is not available for other Windows programs so you'd need to maintain two keychains, one in GPG4WIN and one (through command line) for the Git Bash environment.
Using PuTTY bridges that divide.