Teaching git to Sign Inside an Enclave
Last post I built up an AWS Nitro Enclave that generates an ed25519 signing key it won’t let anyone read, serves it over TLS, and proves — by attestation — exactly which code is holding that key. We ended with an encrypted channel to a box we can trust, and a public key we’ve verified.
Now the cheeky part: getting git to sign with that key, without patching git,
without a custom credential helper, without anyone learning a new command.
Making git cooperate, with zero patches
git has supported SSH signing for a while now (gpg.format = ssh). When it
signs, it shells out to a program — ssh-keygen by default — like this:
ssh-keygen -Y sign -n git -f <key> <bufferfile>…and reads the signature back from <bufferfile>.sig. That’s a seam. Nothing
says the program on the other end has to be ssh-keygen. So I wrote a shim that
git calls instead. When git asks it to sign, it forwards the bytes to the
enclave and writes back the answer. When git asks it to do anything else
(verifying, finding principals), it just execs the real ssh-keygen, which
verifies the enclave’s signatures perfectly well — because they’re ordinary
SSHSIG signatures.
The enclave produces the entire armored SSHSIG — magic preamble, the public
key, the namespace, the signature blob — so the shim stays dumb. It reads the
buffer git handed it, POSTs it, and drops the result at <file>.sig. That’s the
whole shim.
The byte format that has to be exactly right
The reason this works at all is that I’m not inventing a signature format — I’m
reproducing OpenSSH’s, to the byte. SSHSIG doesn’t sign your message directly;
it signs a sha512 of it, wrapped in a small framed blob with a magic preamble
and a namespace:
// per OpenSSH's PROTOCOL.sshsig — sign H(message), not the message
h := sha512.Sum512(message)
var signed bytes.Buffer
signed.WriteString("SSHSIG")
writeSSHString(&signed, []byte(namespace))
writeSSHString(&signed, nil) // reserved
writeSSHString(&signed, []byte("sha512"))
writeSSHString(&signed, h[:])
sig, err := signer.Sign(rand.Reader, signed.Bytes())Get a length prefix wrong and nothing complains loudly — you just produce a blob
that silently fails to verify later. So the most important test in the whole
project isn’t a mock or a unit assertion. It signs something inside the enclave,
then shells out to the real ssh-keygen -Y verify and asserts it’s happy:
--- PASS: TestSSHSignatureVerifiesWithSSHKeygenIf stock OpenSSH accepts the signature, so will git. That one test is the
contract.
Wiring it up
Point git at the shim once and you never think about it again:
export ENCLAVE_URL=https://my-enclave-host
git config gpg.format ssh
git config gpg.ssh.program "$(pwd)/git-enclave-signer"
git config user.signingkey ~/.config/git/enclave-signer.pub
git config commit.gpgsign trueThe enclave-signer.pub there is the key you got back from the attest step in
Part 1 — the one you verified. From here, git commit is signed by a
key that has never touched your disk:
$ git commit -m "signed in an enclave"
$ git log --show-signature -1
Good "git" signature with ED25519 key SHA256:…Deploying the whole thing
The repo ships a single-file CDK stack that stands up one Nitro-enabled EC2 host, configures the enclave allocator, and runs a tiny systemd unit that keeps the enclave alive. The loop end to end:
make eif # build the enclave image, capture its PCR0
cd iac && npx cdk deploy # one Nitro host + an S3 bucket for the image
aws s3 cp build/app.eif s3://<bucket>/app.eif # hand it the image
git-enclave-signer attest https://<host> # verify + grab the keyNotably, there’s no key material to provision and no secret to inject. The enclave makes its own key on boot. The host never sees it. You verify it from your laptop and you’re done.
What I left out on purpose
This is an example, and examples earn their keep by being small. Two honest simplifications:
- The key is ephemeral. It’s regenerated on every enclave boot, so a restart is a new identity. For a key you actually want to keep, you’d wrap it with KMS and gate the decrypt on PCR0 — so the key only comes back to life inside an enclave running this exact image, and nowhere else. That’s the genuinely powerful pattern, and it deserves its own post.
- The cert is self-signed. Covered in Part 1 — trust is from attestation. Bolt on ACME when you want the green padlock too.
What I like about the result is how little ceremony it takes to get a real
security property: a signing key with a hardware-rooted, remotely verifiable
boundary, behind a git commit -S that looks exactly like it always did. A
decade ago I was poking at TLS handshakes to understand what trust
even means on the wire. This is the same itch, scratched one layer deeper — the
trust isn’t in a name on a certificate, it’s in a measurement of the code.
The whole thing is at github.com/husobee/secure-git-signer. Clone it, read it, break it.
Hope this was helpful to someone.