https://codon.org.uk/~mjg59/blog/p/ssh-certificates-and-git-signing/
When you’re looking at source code it can be helpful to have some evidence
indicating who wrote it. Author tags give a surface level indication, but
it turns out you can just
lie
and if someone isn’t paying attention when merging stuff there’s certainly a
risk that a commit could be merged with an author field that doesn’t
represent reality. Account compromise can make this even worse - a PR being
opened by a compromised user is going to be hard to distinguish from the
authentic user. In a world where supply chain security is an increasing
concern, it’s easy to understand why people would want more evidence that
code was actually written by the person it’s attributed to.
git has support for cryptographically signing
commits and tags. Because git is about choice even if Linux isn’t, you can
do this signing with OpenPGP keys, X.509 certificates, or SSH keys. You’re
probably going to be unsurprised about my feelings around OpenPGP and the
web of trust, and X.509 certificates are an absolute nightmare. That leaves
SSH keys, but bare cryptographic keys aren’t terribly helpful in isolation -
you need some way to make a determination about which keys you trust. If
you’re using someting like GitHub you can extract that
information from the set of keys associated with a user account, but
that means that a compromised GitHub account is now also a way to alter the
set of trusted keys and also when was the last time you audited your keys
and how certain are you that every trusted key there is still 100% under
your control? Surely there’s a better way.
SSH Certificates
And, thankfully, there is. OpenSSH supports
certificates, an SSH public key that’s been signed by some trusted party and
so now you can assert that it’s trustworthy in some form. SSH Certificates
also contain metadata in the form of Principals, a list of identities that
the trusted party included in the certificate. These might simply be
usernames, but they might also provide information about group
membership. There’s also, unsurprisingly, native support in SSH for
forwarding them (using the agent forwarding protocol), so you can keep your
keys on your local system, ssh into your actual dev system, and have access
to them without any additional complexity.
And, wonderfully, you can use them in git! Let’s find out how.
Local config
There’s two main parameters you need to set. First,
1
|
git config set gpg.format ssh
|
because unfortunately for historical reasons all the git signing config is
under the gpg namespace even if you’re not using OpenPGP. Yes, this makes
me sad. But you’re also going to need something else. Either
user.signingkey needs to be set to the path of your certificate, or you
need to set gpg.ssh.defaultKeyCommand to a command that will talk to an
SSH agent and find the certificate for you (this can be helpful if it’s
stored on a smartcard or something rather than on disk). Thankfully for you,
I’ve written one. It will
talk to an SSH agent (either whatever’s pointed at by the SSH_AUTH_SOCK
environment variable or with the -agent argument), find a certificate
signed with the key provided with the -ca argument, and then pass that
back to git. Now you can simply pass -S to git commit and various other
commands, and you’ll have a signature.
Validating signatures
This is a bit more annoying. Using native git tooling ends up calling out to
ssh-keygen, which validates signatures against a file in a format
that looks somewhat like authorized-keys. This lets you add something like:
1
|
* cert-authority ssh-rsa AAAA…
|
which will match all principals (the wildcard) and succeed if the signature
is made with a certificate that’s signed by the key following
cert-authority. I recommend you don’t read the code that does this in
git
because I made that mistake myself, but it does work. Unfortunately it
doesn’t provide a lot of granularity around things like “Does the
certificate need to be valid at this specific time” and “Should the user
only be able to modify specific files” and that kind of thing, but also if
you’re using GitHub or GitLab you wouldn’t need to do this at all because
they’ll just do this magically and put a “verified” tag against anything
with a valid signature, right?
Haha. No.
Unfortunately while both GitHub and GitLab support using SSH certificates
for authentication (so a user can’t push to a repo unless they have a
certificate signed by the configured CA), there’s currently no way to say
“Trust all commits with an SSH certificate signed by this CA”. I am unclear
on why. So, I wrote my
own. It takes a range of
commits, and verifies that each one is signed with either a certificate
signed by the key in CA_PUB_KEY or (optionally) an OpenPGP key provided in
ALLOWED_PGP_KEYS. Why OpenPGP? Because even if you sign all of your own
commits with an SSH certificate, anyone using the API or web interface will
end up with their commits signed by an OpenPGP key, and if you want to have
those commits validate you’ll need to handle that.
In any case, this should be easy enough to integrate into whatever CI
pipeline you have. This is currently very much a proof of concept and I
wouldn’t recommend deploying it anywhere, but I am interested in merging
support for additional policy around things like expiry dates or group
membership.
Doing it in hardware
Of course, certificates don’t buy you any additional security if an attacker
is able to steal your private key material - they can steal the certificate
at the same time. This can be avoided on almost all modern hardware by
storing the private key in a separate cryptographic coprocessor - a Trusted
Platform Module on
PCs, or the Secure
Enclave
on Macs. If you’re on a Mac then Secretive has
been around for some time, but things are a little harder on Windows and
Linux - there’s various things you can do with
PKCS#11 but you’ll hate yourself
even more than you’ll hate me for suggesting it in the first place, and
there’s ssh-tpm-agent except
it’s Linux only and quite tied to Linux.
So, obviously, I wrote my
own. This makes use of the
go-attestation library my team
at Google wrote, and is able to generate TPM-backed keys and export them
over the SSH agent protocol. It’s also able to proxy requests back to an
existing agent, so you can just have it take care of your TPM-backed keys
and continue using your existing agent for everything else. In theory it
should also work on Windows but this is all in preparation for a
talk I only found out I was giving about two weeks
beforehand, so I haven’t actually had time to test anything other than that
it builds.
And, delightfully, because the agent protocol doesn’t care about where the
keys are actually stored, this still works just fine with forwarding - you
can ssh into a remote system and sign something using a private key that’s
stored in your local TPM or Secure Enclave. Remote use can be as transparent
as local use.
Wait, attestation?
Ah yes you may be wondering why I’m using go-attestation and why the term
“attestation” is in my agent’s name. It’s because when I’m generating the
key I’m also generating all the artifacts required to prove that the key was
generated on a particular TPM. I haven’t actually implemented the other end
of that yet, but if implemented this would allow you to verify that a key
was generated in hardware before you issue it with an SSH certificate - and
in an age of agentic bots accidentally exfiltrating whatever they find on
disk, that gives you a lot more confidence that a commit was signed on
hardware you own.
Conclusion
Using SSH certificates for git commit signing is great - the tooling is a
bit rough but otherwise they’re basically better than every other
alternative, and also if you already have infrastructure for issuing SSH
certificates then you can just reuse it and everyone wins.
https://codon.org.uk/~mjg59/blog/p/ssh-certificates-and-git-signing/