16

I'm doing some development work on an untrusted computer. I'd like to sign my commits I make from it, but I don't want my personal PGP key to touch this computer. Or maybe I'm using a PGP smart card and I can't present it to the computer because of a lack of smart card reader.

Usually Git commit signing works by calling into the gpg executable on the local machine somehow. If I need to use a key that can't leave a computer, I'd do the commit from the computer that has the key. However, in this case, I can only make commits from this untrusted PC because it lives inside a private network and I can't get at the Git repository from outside.

I'd like to create a commit, then export some data object from the untrusted PC, bring it to the trusted PC, run a PGP command on this data, then bring the result back to the untrusted PC, and as a result the Git commit would become signed.

Is this possible and how would I set it up?

11
  • 9
    The smart way to do this is to have a hardware token like a Yubikey that keeps the private key used for your signatures, such that that token is never directly readable by any general-purpose CPU at all and the signatures are performed off-board, by the smartcard, where even if the host system has been infected it can only use the card while it's actively plugged in (and potentially, based on configuration, when the user is pushing a button to approve the signature operation), but not duplicate the key it contains for offline use. Commented Mar 10 at 23:15
  • 7
    (A Yubikey only needs a USB slot, not a smartcard reader). Mind, there's also gpg-agent support, which can be forwarded over SSH much in the same way a SSH agent can be forwarded. Commented Mar 10 at 23:17
  • 2
    "export some data object from the untrusted PC, bring it to the trusted PC, run a command on this data, then bring the result back to the untrusted PC" - if you can do that, it means that you actually do have access to the git repository from the trusted PC: just bring the entire repository! That's what decentralised version control is all about in git. You don't need to have access to the same remotes as the untrusted PC.
    – Bergi
    Commented Mar 10 at 23:55
  • 14
    Is there some reason you can’t create a one-off key for this purpose? If you truly don’t trust the computer you’re working on, I would question whether you want your usual key associated with the commits generated there. Commented Mar 11 at 0:48
  • 10
    In a comment below you mention that, "the people [you're] working for don't trust [you] with their code except on their PC." You really should add this information to your question since it's has a huge influence on the kinds of solutions that we can propose.
    – cjs
    Commented Mar 11 at 11:31

5 Answers 5

6

This is an attempt to turn cjs's answer into concrete steps.

  1. git log --oneline HEAD to extract the guid of the commit you want to sign.

  2. git cat-file commit COMMIT_GUID > commit.txt to generate a copy of the raw commit. commit.txt has the change description and hashes, no actual code in it.

  3. Transfer commit.txt to your trusted computer.

  4. Run gpg --armor --output commit.sig --detach-sig commit.txt to generate a GPG signature in commit.sig. I don't think gpg knows how git expects the signature to be embedded, so you have to do it manually.

  5. Carefully create a new signed-commit.txt by starting with commit.txt and inserting gpgsig followed by the contents of commit.sig as the last header. Be careful about whitespace; experimentation may be required.

  6. Transfer signed-commit.txt back to your untrusted computer you are editing.

  7. Add the raw signed commit to the git repo. This prints the hash you'll need next: cat signed-commit.txt | git hash-object -w --stdin -t commit. (getting the newlines right here could be tricky; I don't want to use filters, as we have signed the file based on the binary data, but...)

  8. git update-ref refs/heads/master RAW_SIGNED_COMMIT_HASH using the hash from step 7.

Then do a git verify-commit HEAD to see if something went wrong, and debug.

To debug this, do the entire work on your trusted computer. Have a toy repo to work with and a toy commit.

Take an unsigned commit and do steps 1-5 on it. Then use git tools to sign the commit with the same identity. You can compare the signed-commit.txt to the git cat-file commit SIGNED_COMMIT_GUID and ensure they are binary identical.

If the signature differs, then something is wrong with either step 2 or 4; otherwise, there is something wrong with how we built signed-commit.txt from commit.sig and commit.txt.

You can then check steps 7-8 locally. Step 7 should return the SIGNED_COMMIT_GUID if it is correct, and 8 should do nothing if you are already pointing at the signed commit.

Once this debugging is complete, what is left is the possibility of text file format differences or something going wrong in the transfer.

10
  • 1
    The first step is rather git rev-parse HEAD but you don't need it, git cat-file commit HEAD works fine. In step 7-8 , you can just merge them into git update-ref refs/heads/master $(git hash-object -w --stdin < signed-commit.txt) I guess.
    – chx
    Commented Mar 13 at 15:15
  • @chx sure, but I tried not to assume you have a *nix shell (ie, I think all of the commands I provide are both *nix shell and windows compatible)
    – Yakk
    Commented Mar 13 at 15:27
  • You should give git hash-object the -t commit option to ensure that you produce a commit object and not a generic blob object. You also probably want to give the filename to git hash-object rather than using --stdin because --stdin also implies --no-filters.
    – cjs
    Commented Mar 13 at 15:39
  • @cjs Given that we are getting the raw output and editing it, isn't no filters correct? I guess the risk is that the text editor we use to build the object has the wrong newlines? It is key that the file we sign is binary-identical to the file in the git repo without filters, right?
    – Yakk
    Commented Mar 13 at 16:20
  • Since you're editing the output before returning it, it's a good idea to enable whatever checks you can when inserting the object into the database. Your example of wrong newlines is one obvious error that could happen (though hash-object --stdin appears to catch this error). I'm not clear on exactly what errors --no-filters allows through, but it seems unlikely that they are any errors you'd want in your Git database.
    – cjs
    Commented Mar 13 at 16:51
23

If you are working for hire, a solution is to create a new PGP key just for the process, and sign it with your normal key, and sign the commits with that. In the event that your employer actually earns your lack of trust, you can then disclaim that key.

4
  • 1
    What does "disclaim" mean, technically? (What's the effect of desclaiming?)
    – Pablo H
    Commented Mar 12 at 14:01
  • 2
    @PabloH Public keys are only worth anything as long as they can be actually linked to the person that is said to be controlling that key. Usually this is done via public key repositories. disclaiming a key usually means leaving an entry in "your" "this-is-my-key" place that says that a specific key is NOT connected to you (anymore) and should not be trusted
    – Hobbamok
    Commented Mar 12 at 14:08
  • 1
    Note that reliable key revocation is a difficult task. Everything else in PKI is typically designed so that signature verification can be done offline as long as the verifier has access to the signature and a limited set of public root keys, but the cryptographic methods used to achieve this don't allow "un-signing" a key. The best you can do is sign a message effectively saying "sorry, this key is no longer valid", but that won't help if the verifier never gets that message. Commented Mar 13 at 11:31
  • @IlmariKaronen You can mitigate this risk by creating short-lived keys (e.g. a few days) so the time for misuse will be very short.
    – Falco
    Commented Mar 13 at 18:23
17

Note that this answer takes into account your comment on an answer indicating that you cannot copy the code from the work machine you are using (which I will call, using your personal viewpoint, the "untrusted host") to a personal machine (the "trusted host") that holds your signing key. (You really should update your question with this information since it rules out a whole class of fairly obvious solutions.)

A commit signature in Git is a signature over just the Git commit object from the repo database.¹ Here's an example of signed commit object data:

tree eebfed94e75e7760540d1485c740902590a00332
parent 04b871796dc0420f8e7561a895b52484b701d51a
author A U Thor <[email protected]> 1465981137 +0000
committer C O Mitter <[email protected]> 1465981137 +0000
gpgsig -----BEGIN PGP SIGNATURE-----
 Version: GnuPG v1
 $
 iQEcBAABAgAGBQJXYRjRAAoJEGEJLoW3InGJ3IwIAIY4SA6GxY3BjL60YyvsJPh/
 HRCJwH+w7wt3Yc/9/bW2F+gF72kdHOOs2jfv+OZhq0q4OAN6fvVSczISY/82LpS7
 DVdMQj2/YcHDT4xrDNBnXnviDO9G7am/9OE77kEbXrp7QPxvhjkicHNwy2rEflAA
 zn075rtEERDHr8nRYiDh8eVrefSO7D+bdQ7gv+7GsYMsd2auJWi1dHOSfTr9HIF4
 HJhWXT9d2f8W+diRYXGh4X0wYiGg6na/soXc+vdtDYBzIxanRqjg8jCAeo1eOTk1
 EdTwhcTZlI0x5pvJ3H0+4hA2jtldVtmPM4OTB0cTrEWBad7XV6YgiyuII73Ve3I=
 =jKHM
 -----END PGP SIGNATURE-----

signed commit

signed commit message body

In the above, note that:

  • The $ is not part of the data, it highlights that that line consists of a single space.
  • The "signed commit" and "signed commit message body" are the commit message.)
  • The signature is (obviously) over the commit object data excluding the gpgsig field.

So if the people you're working for are ok with sending just the above information (various hashes, commit author and committer names and e-mail addresses, and the commit message) being sent to your trusted host you can (in theory at least) sign the commits with your personal key. What you will need to do is, as part of the commit process:

  1. Have Git generate the commit object data like the above, less the gpgsig field. (This can be done with, e.g., git cat-file commit <commit-ish>, or you could even just generate it yourself as there's no special magic to it.) Note that this commit object data is itself not yet a commit; the commit hash (i.e., the name of the object in the database) cannot yet be calculated because the gpgsig field is also covered under the commit hash.
  2. Send that information to your trusted host where you will somehow sign it in a secure way. (Be careful with this: merely sshing into your trusted host from the untrusted host gives you significant exposure to attacks from or on the untrusted host.) One way of doing this is to run gpg --armor --output commit-data.sig --detach-sig commit-data.txt and then adding this commit-data.sig output as a gpgsig field to commit-data.txt, matching the example above.
  3. Return the commit object data with the newly-added gpgsig field to the untrusted host and then generate the commit object from that. One way of doing this is with git hash-object -w -t commit commit-data.txt, which will return the identifier (SHA-1 hash) of the commit object that this creates. (Note that this does not do much, if any, validity checking; in particular it will accept invalid parent and tree IDs that you will find out about only later when you try to make use of the commit.) You will likely also want to update a ref (branch) to include this new commit with something like git update-ref refs/heads/main ID.

(Further note: per Charles Duffy's comment below, it is technically not even necessary to send the above information, but just the same hash of it that PGP would generate and sign. But that is extra work that may or may not be relatively simple, dependig on who's helping you, and it would also preclude the ability to sign a chain of commits as mentioned below; you would instead always have to return the signature for a commit and generate that commit before you could generate the PGP hash to be signed for a child commit.)

Note that this does not have to happen "all at once;" depending on how you set up your system it's perfectly possible to start the process at work, send the proposed commit object data to your personal system, go home, do the signatures there, send them back, and create the actual commits when you return to work, though this obviously could be somewhat slow and awkward. More likely you would want to bring a trusted laptop into work and simply switch between the two machines.

But I do believe it would also be possible to send a chain of commit objects or commit object data to your trusted machine, sign each commit in the chain (updating the parent field for each subsequent commit as you go), and then return them all to be inserted into the Git database on the untrusted machine.

The technicalities of doing this appear to be fairly difficult; it would require a good knowledge of Git and custom software that connects to Git itself (or, preferably, its code exposed as a library API), possibly some reimplementation of parts of Git, and some trusted means of transferring the commit object data from the untrusted to the trusted host. (You don't need it to be trusted the other way around since you can verify the signature on the commit object data when that is returned to your work.) This is all complex enough that, though I can see where I would get started on doing this, I think I will leave it as an exercise for the reader. :-/

(FWIW, though I don't have a lot of time to spend on this kind of thing unless I were getting paid for it, if someone out there wants to start a project to try to build tools to do this, feel free to contact me privately via the e-mail address in my profile and I'll be happy to help out as best I can—for free, of course, if the result is under an appropriate open source licence.)


¹ It's worth nothing that this covers only the commit metadata, not the directory tree or files themselves. The proof that those are correct is dependent on the SHA-1 hashes that are referenced from the commit object. SHA-1 has not been considered secure against well-funded attackers since the mid-2000s. This can be mitigated by using SHA-256 hashes, but this is not the default and is still considered experimental functionality, though the format is not expected to change in a backward incompatible way. Further, many (most?) remote repo storage systems do not support SHA-256 hashes, so you cannot transfer SHA-256 hashes through them.

10
  • 5
    Under the hood, even the commit doesn't need to be sent to the trusted machine -- just the padded hash of that commit (as extended by OpenPGP -- it's not derivable just from the commit data). Signing a 100gb file with a smartcard doesn't send all 100gb of data over the wire to the card, f/e. Commented Mar 11 at 15:10
  • 2
    (Background: In a past life, I built a shim between OpenPGP -- Go's implementation, not the super-crufty GnuPG codebase -- and a corporate key escrow system; so I speak from firsthand experience when saying that this is very, very possible). Commented Mar 11 at 15:13
  • 1
    A concrete set of git (ideally command-line) commands would improve this answer. Ie, how to get the "raw" unsigned commit text via an explicit git command, how to generate the gpgsig using gpg command line tools, and how to take the gigsig included file and make a commit of it using git command-line tools.
    – Yakk
    Commented Mar 12 at 16:02
  • 1
    For the first: git cat-file commit COMMIT_GUID gets you the raw commit (with tree guids and everything) you want to sign.
    – Yakk
    Commented Mar 12 at 16:14
  • 1
    For the third, stackoverflow.com/questions/16064968/… provides a framework for doing it. That only leaves a command-line way to do the signature of the cat-file output.
    – Yakk
    Commented Mar 12 at 16:17
12

If you have access to the remote repository from your trusted PC,you can fetch the usigned commits on your trusted PC, sign the commits, and then push them again. This will alter the commit hashes, so you will need to force-push or push them to a different branch.

You can use this command to sign the previous commit

git commit --amend --no-edit -S

See this SO answer for more options

7
  • 5
    Good idea, but unfortunately it doesn't work for my usecase: the people I'm working for don't trust me with their code except on their PC, and I don't trust their PC.
    – Danya02
    Commented Mar 10 at 14:43
  • 3
    ah. You should add that condition to the question, it makes a big difference. You would need to split the signing process in half - the "untrusted PC" would need to mangle the commit contents into something that cannot be transformed back into the commit, and your "trusted PC" would need to transform this mangled commit into a signature. I don't know enough about how Git signatures work to tell whether this is theoretically possible or not
    – Yogu
    Commented Mar 10 at 14:47
  • 2
    One idea I had was to replace the gpg executable on the untrusted PC with a script that would echo its arguments to stdout, where I would take them, put them into the real gpg program on my other PC, and then bring the result back into the terminal. That sounds very hackish though, and I was wondering if anyone had solved the problem of security separation like this.
    – Danya02
    Commented Mar 10 at 14:54
  • so your coworkers would be fine with the commit message being leaked to your "trusted PC"? Then you could externalize the signature creation like you described.
    – Yogu
    Commented Mar 10 at 14:57
  • 2
    @Yogu, re: "how git signatures work" -- normal OpenPGP. You get a hash of the signature and its framing &c, and the key needs to sign that hash, same as most other mechanisms. (That means the whole message doesn't need to be sent over the wire -- just the fixed-length hash) Commented Mar 10 at 23:18
0

An option that seems the closest in spirit to what I want is to set up a gpg-agent program that would delegate the actual cryptography to a human.

When you use GPG to sign things, it talks to the gpg-agent to actually perform the operations with the private key. That way, the gpg-agent can remember the password for a while, interact with smartcards, and figure out which pinentry program to show to the user. Importantly, it listens on a Unix domain socket, so it can be forwarded over connections like SSH with the -R remote listening setup.

The original motivation for my question was in the context of remote working: you get a computer somewhere, you can only work on the code via a remote connection, and you can't (as in, not supposed to) bring the code away from the remote connection. It is for these setups that gpg-agent forwarding works best: a hidden channel is established in addition to your session, and over that channel the remote gpg program talks to your local gpg-agent, and the gpg-agent retains the private keys while only providing the result to the remote computer.

But this could also work for air-gapped setups. I've tried opening two terminal panes, one on the remote computer with a nc -Ul listening on the Unix socket where the gpg-agent would normally live, and one on my local computer with gpg-connect-agent, and copying the commands back and forth between the two windows. I almost went through the entire signature transaction, and the only reason it failed is at the very last step, when executing PKSIGN, it produced a bunch of binary data that did not get copied over correctly in the clipboard.

gpg-agent transaction

So, the plan is:

  1. On the untrusted computer, replace the standard gpg-agent program with one that responds with OKs until the PKSIGN command. When a signature operation is requested, the program pops up an alert containing the previous commands in a compressed text-only form.
  2. A human copies the data on the screen into the trusted computer that holds the keys. There, a program replays the commands into the real gpg-agent, which performs the signature operation, and then returns a compressed text-only response.
  3. The human then copies this data back into the untrusted computer. The pretend gpg-agent returns the response to the calling gpg program, which completes the signature.

This is basically similar to gpg-agent forwarding over SSH, except that the protocol is modified to require less back-and-forth interactions (since those require the attention of the human now).


This is of course very theoretical and very paranoid: the computer I'm working on is obviously not airgapped because I'd be remoting into it. A realistic solution for my problem is to use normal gpg-agent forwarding, and/or generate a new key to keep on the untrusted computer (perhaps signed by my main key, perhaps not).

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .