{"thread":{"id":"65003","subject":"Push Certificates: Privacy Concerns Regarding the \"pushee\" Header","startedAt":"2026-02-15T21:58:45Z","lastAt":"2026-02-18T09:56:41Z","messageCount":5,"participants":["Lorenz Leutgeb","Junio C Hamano"],"isPatch":false,"patchVersion":null,"patchTotal":null},"messages":[{"id":"536075","messageId":"d180884c-8108-4c8a-9cc7-5314a4f5a45a@posteo.eu","threadId":"65003","inReplyTo":null,"subject":"Push Certificates: Privacy Concerns Regarding the \"pushee\" Header","fromName":"Lorenz Leutgeb","fromEmail":"lorenz.leutgeb@posteo.eu","sentAt":"2026-02-15T21:58:42Z","receivedAt":"2026-02-15T21:58:45Z","isPatch":false,"sender":{"key":"lorenz.leutgeb@posteo.eu","avatar":"https://gravatar.com/avatar/27187b6fdcf910e66ebc91e827a98237a83c007288d3eb471ca294d8bb8b9645?d=mp&s=160"},"body":"Dear recipients,\n\nI am working on an application built on top of Git, which wants to \ningest push certificates in order to keep (something similar to) a \ntransparency log (see \n<https://people.kernel.org/monsieuricon/signed-git-pushes>).\n\nFor reasons that I will only go into detail upon request, in the context \nof the application, logical \"pushes to the network\" are split into two \nparts: The *first part* is always local (from one repository in one \ndirectory on the filesystem, to another).  Bundled with the application \nis a daemon.  The daemon process then takes over the *second part* of \nthe push, for further propagation of over the network in a peer-to-peer \nmanner.  However, one desires the *first* part already to be certified.\n\nThis would result in push certificates of the following form (hashes \nabbreviated, signature omitted):\n\n\tcertificate version 0.1\n\tpusher SHA256:xX6bp…T0  1771188983 +0100\n\tpushee /home/lorenz/.example/storage/foo\n\tnonce 1771188983-345389c\n\t\n\t0000000 ccae4e0 refs/heads/main\n\nAs you can see, this push certificate leaks a path on the application \nusers' filesystem.  Here it is quite obviously my home directory, but \nactually the \"storage path\", is user-configurable at the application \nlevel, and considered private.\n\nI do realize that the leak is in part due to the weird application \narchitecture.  Who would have guessed that I want to certify a push *on \nmy own filesystem*?  I am convinced the main motivation for this feature \nwere pushes (directly) over the network.  However, I also believe that \nit could be of more general interest to allow the Git user to control \nwhich/whether the pushee header is emitted.\n\nHandling of the pushee header was introduced to `send-pack.c` in \n9be89160e7382a88e56a02bcf38f4694dd6542d6, over 11 years ago, and was not \ntouched since. Remarking on the current implementation, I do realize \nthat `transport_anonymize_url` is used to sanitize, in order not to leak \nusernames, passwords, etc., which is appreciated.  However, file paths \nare not removed.  This makes a great deal of sense as the same function \nis used in other places where indeed the path on the filesystem is \nexpected.  I think the current implementation is sensible.  However, \nallowing the Git user to ultimately control which URL is used allows for \nthe greatest flexibility.\n\nNote that the receiving end might inspect the push certificate in their \n`pre-receive` handler, and reject to receive if the pushee is malformed.\n\nI would like to provide a patch that adds an option `git send-pack`, \nwhich allows the Git user to specify the pushee or indicate that the \nheader should not be emitted.  My proposal is as follows.  Because of \nthe connection to signed pushes, and therefore to the the already \nexisting `--[no-]-signed=…` option, I would add \n`--signed-pushee=<string>` as well as `--no-signed-pushee`.\n\nOne edge case that I would like to get clarification on is how the empty \nstring should be handled.  I would consider `--signed-pushee=\"\"` \ninvalid, making it impossible to specify the empty string as pushee, \nerroring out and potentially hinting at `--no-signed-pushee`.\n\nBefore I do so, I would like to ask for your feedback.  Would you accept \nsuch patch if implemented cleanly?  What do you think, how should the \noption be named and interpreted?  Further, if this option is added to \n`git send-pack`, one natural question is whether `git push` should also \n\"inherit\" it, as is the case with `--signed`?\n\nKind regards,\nLorenz Leutgeb\n"},{"id":"536220","messageId":"xmqqldgrb1ha.fsf@gitster.g","threadId":"65003","inReplyTo":"d180884c-8108-4c8a-9cc7-5314a4f5a45a@posteo.eu","subject":"Re: Push Certificates: Privacy Concerns Regarding the \"pushee\" Header","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2026-02-17T19:42:57Z","receivedAt":"2026-02-17T19:43:00Z","isPatch":false,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Lorenz Leutgeb <lorenz.leutgeb@posteo.eu> writes:\n\n> This would result in push certificates of the following form (hashes \n> abbreviated, signature omitted):\n>\n> \tcertificate version 0.1\n> \tpusher SHA256:xX6bp…T0  1771188983 +0100\n> \tpushee /home/lorenz/.example/storage/foo\n> \tnonce 1771188983-345389c\n> \t\n> \t0000000 ccae4e0 refs/heads/main\n>\n> As you can see, this push certificate leaks a path on the application \n> users' filesystem.  Here it is quite obviously my home directory, but \n> actually the \"storage path\", is user-configurable at the application \n> level, and considered private.\n\nStepping back a bit, the most important question is what the signer\nis trying to certify, isn't it?\n\n\"At this point in time, I am pushing to update these refs because I\nwant these objects to be sitting at the tip of them in the\nrepository listed as pushee\" is the statement the pusher is\ncertifying.  The auditors can point at the certificate that the\nhosting site having the object ccae4e0 is not because insiders of\nthe hosting site rewounded the tip to point at a stale object that\nthe pusher did not want to see at the ref, but with that signature,\nthis pusher expressed their desire to have it at the tip of the main\nbranch.\n\nMaking it possible to lift that certification and reusing it for a\npush to different repository smells like opening a door for replay\nattacks, doesn't it?  The pusher did not say anything about updating\nthese refs in other repositories that are different from the pushee\nrepository.\n\nI am not sure what the implication of optionally allowing the pusher\nto sign a certificate that lacks \"pushee\" is.  Would that be a\nreasonable way to say \"at this point in time, I want this object to\nbe at the tip of main branch in _any_ and _all_ clones of this\nproject anywhere in the world?\"  Is that the kind of statement the\npusher would want to make in the first place?  Would such a\nstatement even make sense?  When you are dealing with two related\nrepositories of a project (e.g., one may be the main development\nrepository, the other being the repository for the maintenance\ntrack), I may fully stand behind putting this commit at the tip of\n'main' on the development track, but that no way means I would be OK\nto put the same commit at the tip of 'main' on the maintenance\ntrack.  Not naming \"pushee\" does not sound like it would fly well.\n\nWhat implication does it have to allow the pusher to sign a\ncertificate that points at a \"pushee\" that is different from the\nrepository the signer directly pushed into?  You give the history\ntogether with a signed \"I want the object ccae4e0 at the tip of the\nmain branch\" statement to your middleman, and your middleman does\nthe actual update and forwards your certificate.  I offhand do not\nsee a huge security problem in that arrangement.  Anybody can check\nthe certificate and the resulting history and verify the chain of\nhashes to the same degree as you would trust SHA-1 (or SHA-256) for\nthe object integrity and GPG (or whatever you used to sign the push\ncertificate) for the certificate integrity.\n\n"},{"id":"536225","messageId":"19c5dd32-6752-43fa-a664-5e6d29d9e681@posteo.eu","threadId":"65003","inReplyTo":"xmqqldgrb1ha.fsf@gitster.g","subject":"Re: Push Certificates: Privacy Concerns Regarding the \"pushee\" Header","fromName":"Lorenz Leutgeb","fromEmail":"lorenz.leutgeb@posteo.eu","sentAt":"2026-02-17T20:31:25Z","receivedAt":"2026-02-17T20:31:26Z","isPatch":false,"sender":{"key":"lorenz.leutgeb@posteo.eu","avatar":"https://gravatar.com/avatar/27187b6fdcf910e66ebc91e827a98237a83c007288d3eb471ca294d8bb8b9645?d=mp&s=160"},"body":"You point out that a push certificate without a pushee is questionable \n(to say the least) and opens the door to replay attacks.  I understand \nand agree.  Thank you.\n\nSo let me cross out the option `--no-signed-pushee` and empty values for \n`--signed-pushee=<url>` from my previous proposal, and let us continue \nour discussion under the assumption that the pushee header is always set \nto a URL(-like) string.\n\nOn 2026-02-17 11:42-0800, Junio C Hamano wrote:\n > What implication does it have to allow the pusher to sign a\n > certificate that points at a \"pushee\" that is different from the\n > repository the signer directly pushed into?  [...]  I offhand do not\n > see a huge security problem in that arrangement.  Anybody can check\n > the certificate and the resulting history and verify the chain of\n > hashes to the same degree as you would trust SHA-1 (or SHA-256) for\n > the object integrity and GPG (or whatever you used to sign the push\n > certificate) for the certificate integrity.\n\nExactly.  And this is very much the situation I am in.  The middleman is \nactually the daemon that comes bundled with the application, which wants \nshare the push certificate over the network.  The pusher only has to \ntrust the middleman insofar as the middleman will actually forward the \npush to its best ability.  In my application, distribution of the \ncertificates and syncing of objects is the main purpose of the daemon. \nBut I digress...\n\nLet me zoom in a bit more why I want to override the pushee: The way the \napplication works is by means of a remote helper. That is, the user \nexecutes `git push example://foo ccae4e0:main`.  This is makes sense in \nthe context of the application, as the user wants to carry out a logical \n\"push to the network\", and \"foo\" is an identifier that also has a \nwell-defined meaning within the context of the application.  As you \nknow, this will lead to execution of `git-remote-example`.  The remote \nhelper is where the hand-off between \"plain Git\" and the application \nhappens.  The remote helper knows that the push will be split in two \nparts.  It knows that it must carry out the first part immediately, \nwhich is a push to `home/lorenz.example/storage/foo`.  The repository at \nthat path is the local view of the repository `example://foo` on the \nusers filesystem.  This local repository then gets updated in the \nbackground by the daemon, and the daemon also notifies other peers on \nthe network about the push.  This is the part where it must act as the \nmiddleman.\n\nNow, in the context of the application, the global identifier of the \nrepository across the network, and thus the pushee that I would like to \nsee, is `example://foo`.  The path `home/lorenz.example/storage/foo` is \nmerely a local name for it, like a cached copy if you will.\n\nPerhaps this even raises a question on how remote helpers interact with \nsigned pushes:  If Git allows to register URLs via remote helpers, how \nshould remote helpers that do not have the \"connect\" capability control \nthe pushee URL, which they likely are concerned with?\n"},{"id":"536257","messageId":"xmqqo6lm8ubv.fsf@gitster.g","threadId":"65003","inReplyTo":"19c5dd32-6752-43fa-a664-5e6d29d9e681@posteo.eu","subject":"Re: Push Certificates: Privacy Concerns Regarding the \"pushee\" Header","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2026-02-18T06:00:20Z","receivedAt":"2026-02-18T06:00:23Z","isPatch":false,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Lorenz Leutgeb <lorenz.leutgeb@posteo.eu> writes:\n\n> Now, in the context of the application, the global identifier of the \n> repository across the network, and thus the pushee that I would like to \n> see, is `example://foo`.  The path `home/lorenz.example/storage/foo` is \n> merely a local name for it, like a cached copy if you will.\n\n\"The repo appears as X to me, but it is known as Y to others\" is an\nissue that already exists.  \"git pull\" records from which repository\nthe changes were merged but it uses the repository from the point of\nthe view of the user who ran \"git pull\", for example.  While one of\nmy public repositories are known as https://github.com/gitster/git\",\nthe URL I use to push there may be \"git@github.com:gitster/git.git\",\nso if they were recording push certificates, the latter would be the\npushee in them, but that is not a URL random people can normally use\nto clone from.\n"},{"id":"536266","messageId":"abfb7c91-3065-4569-a080-ab0e0c259a12@posteo.eu","threadId":"65003","inReplyTo":"xmqqo6lm8ubv.fsf@gitster.g","subject":"Re: Push Certificates: Privacy Concerns Regarding the \"pushee\" Header","fromName":"Lorenz Leutgeb","fromEmail":"lorenz.leutgeb@posteo.eu","sentAt":"2026-02-18T09:56:39Z","receivedAt":"2026-02-18T09:56:41Z","isPatch":false,"sender":{"key":"lorenz.leutgeb@posteo.eu","avatar":"https://gravatar.com/avatar/27187b6fdcf910e66ebc91e827a98237a83c007288d3eb471ca294d8bb8b9645?d=mp&s=160"},"body":"So, imagine a world where push certificates are more \"end-to-end\". \nThink transparency log meets Git (see https://transparency.dev/ for more \ncontext).  Not only `git push --signed`, but also `git pull --signed` \nexists.  The remote being fetched from must provide evidence for the \npuller verify the range being pulled, e.g. 0000000..ccae4e0.  It does so \nby sending along the blob that contains the corresponding push \ncertificates[^1].\n\nIn this bright future, where any puller is an auditor, you would run \ninto an issue if you want your repository to be pulled from different \nplaces (pullees? fetchees?).  However, there is a way out: Let the \npusher specify with which locations they are happy to have their push \nend up at.  In your case, since you are happy for others to pull from \nhttps://github.com/gitster/git, you add to your push certificate:\n\n\tcertificate version 0.1\n\tpusher SHA256:xX6bp…T0  1771188983 +0100\n\tpushee https://example.com/repo.git\n\tpushee git@github.com:gitster/git.git\n\tnonce 1771188983-345389c\n\n\t0000000 ccae4e0 refs/heads/main\n\nI will also give you the counter arguments:  Firstly, with repositories \nthat have many mirrors, the number of headers might become \nproblematically large.  One remedy would be to allow configuration of \naliases on the side of the puller/verifier.  One could accept all values \nof `remote.<name>.url` as valid.  One could introduce \n`remote.<name>.alias`.  Secondly, for repositories with exactly one \ncanonical location, no configuration would be necessary and the value \nbeing used today would be correct.\n\n---\n\n[1]: In general, it would have to send multiple push certificates. \nFirstly because there might be multiple refs being pulled over multiple \nranges.  Secondly because that range might have been established over \nmultiple pushes.\n"}]}