onionjake
onionjake

Reputation: 4045

Why does Git need signed pushes?

In the release notes for Git 2.2.0, it describes a new option to git push, --signed:

"git push" learned "--signed" push, that allows a push (i.e.
request to update the refs on the other side to point at a new
history, together with the transmission of necessary objects) to be
signed, so that it can be verified and audited, using the GPG
signature of the person who pushed, that the tips of branches at a
public repository really point the commits the pusher wanted to,
without having to "trust" the server.

So it sounds like the data being sent to the server during the push is signed so that the server can verify and log who did the push. In the man pages you can confirm this:

--signed
   GPG-sign the push request to update refs on the receiving side, 
   to allow it to be checked by the hooks and/or be logged. See 
   git-receive-pack[1] for the details on the receiving end.

You look in the man pages for git-receive-pack under pre-receive and post-recieve hooks to see exactly how to verify a signed push.

It seems like all of that helps the server verify who is doing the push really is who they say they are.

How does git push --signed help you (the pusher) in not having to "trust" the server? Everything I have seen so far seems to indicate that it helps the server trust you. More importantly, Why are signed commits and signed tags not sufficient to push to an untrusted server? Why do we even need signed pushes?

Upvotes: 28

Views: 7787

Answers (3)

VonC
VonC

Reputation: 1327004

The initial discussion is here, for Git 2.2 (Q4 2014).
It includes:

Each line shows the old and the new object name at the tip of the ref this push tries to update, in the way identical to how the underlying "git push" protocol exchange tells the ref updates to the receiving end (by recording the "old" object name, the push certificate also protects against replaying).

It also records the URL of the intended recipient for a push (after anonymizing it if it has authentication material) on a new "pushee URL" header. This value may not be reliably used for replay-attack prevention purposes, but this will still serve as a human-readable hint to identify the repository the certificate refers to.

In order to prevent a valid push certificate for pushing into an repository from getting replayed to push to an unrelated one, send a nonce string from the receive-pack process and have the signer include it in the push certificate.
The receiving end uses an HMAC hash of the path to the repository it serves and the current time, hashed with a secret key (this does not have to be per-repository but can be defined in /etc/gitconfig) to ensure that a random third party cannot forge a nonce that looks like it originated from it.


Since Git 2.2 (2014), more recent patches highlight the "commit replay prevention technique" mentioned above:

While signed pushes do allow the server to keep a record of every push event and its signature, it can fail to record the right user, before Git 2.19 (Q3 2018):

"git send-pack --signed" (hence "git push --signed" over the http transport) did not read user ident from the config mechanism to determine whom to sign the push certificate as, which has been corrected.

See commit d067d98 (12 Jun 2018) by Masaya Suzuki (draftcode).
(Merged by Junio C Hamano -- gitster -- in commit 8d3661d, 28 Jun 2018)

builtin/send-pack: populate the default configs

builtin/send-pack didn't call git_default_config, and because of this git push --signed didn't respect the username and email in gitconfig in the HTTP transport.


With Git 2.27 (Q2 2020), the validation of push certificate has been made more robust against timing attacks.

See commit 719483e (22 Apr 2020) by Junio C Hamano (gitster).
See commit edc6dcc (09 Apr 2020) by brian m. carlson (bk2204).
(Merged by Junio C Hamano -- gitster -- in commit 2abd648, 28 Apr 2020)

builtin/receive-pack: use constant-time comparison for HMAC value

Signed-off-by: brian m. carlson

When we're comparing a push cert nonce, we currently do so using strcmp.
Most implementations of strcmp short-circuit and exit as soon as they know whether two values are equal.

This, however, is a problem when we're comparing the output of HMAC, as it leaks information in the time taken about how much of the two values match if they do indeed differ.

In our case, the nonce is used to prevent replay attacks against our server via the embedded timestamp and replay attacks using requests from a different server via the HMAC.

Push certs, which contain the nonces, are signed, so an attacker cannot tamper with the nonces without breaking validation of the signature.

They can, of course, create their own signatures with invalid nonces, but they can also create their own signatures with valid nonces, so there's nothing to be gained.

Thus, there is no security problem.

Even though it doesn't appear that there are any negative consequences from the current technique, for safety and to encourage good practices, let's use a constant time comparison function for nonce verification. POSIX does not provide one, but they are easy to write.

The technique we use here is also used in NaCl and the Go standard library and relies on the fact that bitwise or and xor are constant time on all known architectures.

We need not be concerned about exiting early if the actual and expected lengths differ, since the standard cryptographic assumption is that everyone, including an attacker, knows the format of and algorithm used in our nonces (and in any event, they have the source code and can determine it easily).

As a result, we assume everyone knows how long our nonces should be.

This philosophy is also taken by the Go standard library and other cryptographic libraries when performing constant time comparisons on HMAC values.

Upvotes: 1

Igor Levicki
Igor Levicki

Reputation: 1616

Short answer is that the purpose of git push --signed is to prevent commit replay attacks.

If you push a signed commit of untested / unsafe code to experimental branch, then someone else can replay that commit to the master branch and it would still appear as signed by you, even if you never intended to push that code to master branch.

Upvotes: 3

onionjake
onionjake

Reputation: 4045

Here is an excerpt from the commit message that introduced signed pushes:

While signed tags and commits assert that the objects thusly signed came from you, who signed these objects, there is not a good way to assert that you wanted to have a particular object at the tip of a particular branch. My signing v2.0.1 tag only means I want to call the version v2.0.1, and it does not mean I want to push it out to my 'master' branch---it is likely that I only want it in 'maint', so the signature on the object alone is insufficient.

The only assurance to you that 'maint' points at what I wanted to place there comes from your trust on the hosting site and my authentication with it, which cannot easily audited later.

So even though the commit is signed, you cannot be sure that the author intended that commit to be pushed to the branch master or to the branch super-experimental-feature. Signed pushes allow the server to keep a record of every push event and its signature. This log can then be verified to see that each commit was indeed intended to be on a particular branch.

Upvotes: 20

Related Questions