threads / discuss / 65003

Push Certificates: Privacy Concerns Regarding the "pushee" Header

Subject: Push Certificates: Privacy Concerns Regarding the "pushee" Header

## tl;dr

5 messages between Feb 15, 2026 and Feb 18, 2026.

replies: 4people: 2as markdown or json

Lorenz Leutgeb· Feb 15, 2026, 21:58 UTC · lore
Dear recipients,

I am working on an application built on top of Git, which wants to ingest push certificates in order to keep (something similar to) a transparency log (see <https://people.kernel.org/monsieuricon/signed-git-pushes>).

For reasons that I will only go into detail upon request, in the context of the application, logical "pushes to the network" are split into two parts: The *first part* is always local (from one repository in one directory on the filesystem, to another). Bundled with the application is a daemon. The daemon process then takes over the *second part* of the push, for further propagation of over the network in a peer-to-peer manner. However, one desires the *first* part already to be certified.

This would result in push certificates of the following form (hashes abbreviated, signature omitted):

	certificate version 0.1
	pusher SHA256:xX6bp…T0  1771188983 +0100
	pushee /home/lorenz/.example/storage/foo
	nonce 1771188983-345389c
	
	0000000 ccae4e0 refs/heads/main

As you can see, this push certificate leaks a path on the application users' filesystem. Here it is quite obviously my home directory, but actually the "storage path", is user-configurable at the application level, and considered private.

I do realize that the leak is in part due to the weird application architecture. Who would have guessed that I want to certify a push *on my own filesystem*? I am convinced the main motivation for this feature were pushes (directly) over the network. However, I also believe that it could be of more general interest to allow the Git user to control which/whether the pushee header is emitted.

Handling of the pushee header was introduced to `send-pack.c` in 9be89160e7382a88e56a02bcf38f4694dd6542d6, over 11 years ago, and was not touched since. Remarking on the current implementation, I do realize that `transport_anonymize_url` is used to sanitize, in order not to leak usernames, passwords, etc., which is appreciated. However, file paths are not removed. This makes a great deal of sense as the same function is used in other places where indeed the path on the filesystem is expected. I think the current implementation is sensible. However, allowing the Git user to ultimately control which URL is used allows for the greatest flexibility.

Note that the receiving end might inspect the push certificate in their `pre-receive` handler, and reject to receive if the pushee is malformed.

I would like to provide a patch that adds an option `git send-pack`, which allows the Git user to specify the pushee or indicate that the header should not be emitted. My proposal is as follows. Because of the connection to signed pushes, and therefore to the the already existing `--[no-]-signed=…` option, I would add `--signed-pushee=<string>` as well as `--no-signed-pushee`.

One edge case that I would like to get clarification on is how the empty string should be handled. I would consider `--signed-pushee=""` invalid, making it impossible to specify the empty string as pushee, erroring out and potentially hinting at `--no-signed-pushee`.

Before I do so, I would like to ask for your feedback. Would you accept such patch if implemented cleanly? What do you think, how should the option be named and interpreted? Further, if this option is added to `git send-pack`, one natural question is whether `git push` should also "inherit" it, as is the case with `--signed`?

Kind regards, Lorenz Leutgeb

Junio C Hamano· Feb 17, 2026, 19:42 UTC · re: Lorenz Leutgeb · lore

Re: Push Certificates: Privacy Concerns Regarding the "pushee" Header

Lorenz Leutgeb <lorenz.leutgeb@posteo.eu> writes:
Show 14 quoted lines
> This would result in push certificates of the following form (hashes 
> abbreviated, signature omitted):
>
> 	certificate version 0.1
> 	pusher SHA256:xX6bp…T0  1771188983 +0100
> 	pushee /home/lorenz/.example/storage/foo
> 	nonce 1771188983-345389c
> 	
> 	0000000 ccae4e0 refs/heads/main
>
> As you can see, this push certificate leaks a path on the application 
> users' filesystem.  Here it is quite obviously my home directory, but 
> actually the "storage path", is user-configurable at the application 
> level, and considered private.

Stepping back a bit, the most important question is what the signer is trying to certify, isn't it?

"At this point in time, I am pushing to update these refs because I want these objects to be sitting at the tip of them in the repository listed as pushee" is the statement the pusher is certifying. The auditors can point at the certificate that the hosting site having the object ccae4e0 is not because insiders of the hosting site rewounded the tip to point at a stale object that the pusher did not want to see at the ref, but with that signature, this pusher expressed their desire to have it at the tip of the main branch.

Making it possible to lift that certification and reusing it for a push to different repository smells like opening a door for replay attacks, doesn't it? The pusher did not say anything about updating these refs in other repositories that are different from the pushee repository.

I am not sure what the implication of optionally allowing the pusher to sign a certificate that lacks "pushee" is. Would that be a reasonable way to say "at this point in time, I want this object to be at the tip of main branch in _any_ and _all_ clones of this project anywhere in the world?" Is that the kind of statement the pusher would want to make in the first place? Would such a statement even make sense? When you are dealing with two related repositories of a project (e.g., one may be the main development repository, the other being the repository for the maintenance track), I may fully stand behind putting this commit at the tip of 'main' on the development track, but that no way means I would be OK to put the same commit at the tip of 'main' on the maintenance track. Not naming "pushee" does not sound like it would fly well.

What implication does it have to allow the pusher to sign a certificate that points at a "pushee" that is different from the repository the signer directly pushed into? You give the history together with a signed "I want the object ccae4e0 at the tip of the main branch" statement to your middleman, and your middleman does the actual update and forwards your certificate. I offhand do not see a huge security problem in that arrangement. Anybody can check the certificate and the resulting history and verify the chain of hashes to the same degree as you would trust SHA-1 (or SHA-256) for the object integrity and GPG (or whatever you used to sign the push certificate) for the certificate integrity.

Lorenz Leutgeb· Feb 17, 2026, 20:31 UTC · re: Junio C Hamano · lore

Re: Push Certificates: Privacy Concerns Regarding the "pushee" Header

You point out that a push certificate without a pushee is questionable (to say the least) and opens the door to replay attacks. I understand and agree. Thank you.

So let me cross out the option `--no-signed-pushee` and empty values for `--signed-pushee=<url>` from my previous proposal, and let us continue our discussion under the assumption that the pushee header is always set to a URL(-like) string.

On 2026-02-17 11:42-0800, Junio C Hamano wrote:
 > What implication does it have to allow the pusher to sign a
 > certificate that points at a "pushee" that is different from the
 > repository the signer directly pushed into?  [...]  I offhand do not
 > see a huge security problem in that arrangement.  Anybody can check
 > the certificate and the resulting history and verify the chain of
 > hashes to the same degree as you would trust SHA-1 (or SHA-256) for
 > the object integrity and GPG (or whatever you used to sign the push
 > certificate) for the certificate integrity.

Exactly. And this is very much the situation I am in. The middleman is actually the daemon that comes bundled with the application, which wants share the push certificate over the network. The pusher only has to trust the middleman insofar as the middleman will actually forward the push to its best ability. In my application, distribution of the certificates and syncing of objects is the main purpose of the daemon. But I digress...

Let me zoom in a bit more why I want to override the pushee: The way the application works is by means of a remote helper. That is, the user executes `git push example://foo ccae4e0:main`. This is makes sense in the context of the application, as the user wants to carry out a logical "push to the network", and "foo" is an identifier that also has a well-defined meaning within the context of the application. As you know, this will lead to execution of `git-remote-example`. The remote helper is where the hand-off between "plain Git" and the application happens. The remote helper knows that the push will be split in two parts. It knows that it must carry out the first part immediately, which is a push to `home/lorenz.example/storage/foo`. The repository at that path is the local view of the repository `example://foo` on the users filesystem. This local repository then gets updated in the background by the daemon, and the daemon also notifies other peers on the network about the push. This is the part where it must act as the middleman.

Now, in the context of the application, the global identifier of the repository across the network, and thus the pushee that I would like to see, is `example://foo`. The path `home/lorenz.example/storage/foo` is merely a local name for it, like a cached copy if you will.

Perhaps this even raises a question on how remote helpers interact with signed pushes: If Git allows to register URLs via remote helpers, how should remote helpers that do not have the "connect" capability control the pushee URL, which they likely are concerned with?

Junio C Hamano· Feb 18, 2026, 06:00 UTC · re: Lorenz Leutgeb · lore

Re: Push Certificates: Privacy Concerns Regarding the "pushee" Header

Lorenz Leutgeb <lorenz.leutgeb@posteo.eu> writes:
> Now, in the context of the application, the global identifier of the 
> repository across the network, and thus the pushee that I would like to 
> see, is `example://foo`.  The path `home/lorenz.example/storage/foo` is 
> merely a local name for it, like a cached copy if you will.

"The repo appears as X to me, but it is known as Y to others" is an issue that already exists. "git pull" records from which repository the changes were merged but it uses the repository from the point of the view of the user who ran "git pull", for example. While one of my public repositories are known as https://github.com/gitster/git", the URL I use to push there may be "git@github.com:gitster/git.git", so if they were recording push certificates, the latter would be the pushee in them, but that is not a URL random people can normally use to clone from.

Lorenz Leutgeb· Feb 18, 2026, 09:56 UTC · re: Junio C Hamano · lore

Re: Push Certificates: Privacy Concerns Regarding the "pushee" Header

So, imagine a world where push certificates are more "end-to-end". Think transparency log meets Git (see https://transparency.dev/ for more context). Not only `git push --signed`, but also `git pull --signed` exists. The remote being fetched from must provide evidence for the puller verify the range being pulled, e.g. 0000000..ccae4e0. It does so by sending along the blob that contains the corresponding push certificates[^1].

In this bright future, where any puller is an auditor, you would run into an issue if you want your repository to be pulled from different places (pullees? fetchees?). However, there is a way out: Let the pusher specify with which locations they are happy to have their push end up at. In your case, since you are happy for others to pull from https://github.com/gitster/git, you add to your push certificate:

	certificate version 0.1
	pusher SHA256:xX6bp…T0  1771188983 +0100
	pushee https://example.com/repo.git
	pushee git@github.com:gitster/git.git
	nonce 1771188983-345389c
	0000000 ccae4e0 refs/heads/main

I will also give you the counter arguments: Firstly, with repositories that have many mirrors, the number of headers might become problematically large. One remedy would be to allow configuration of aliases on the side of the puller/verifier. One could accept all values of `remote.<name>.url` as valid. One could introduce `remote.<name>.alias`. Secondly, for repositories with exactly one canonical location, no configuration would be necessary and the value being used today would be correct.

---

[1]: In general, it would have to send multiple push certificates. Firstly because there might be multiple refs being pulled over multiple ranges. Secondly because that range might have been established over multiple pushes.

← back to recent threads