Skip to content

Signed Pushes #2434

@lorenzleutgeb

Description

@lorenzleutgeb

Summary 💡

I would love if GitOxide made it easy to write Git tooling that supports signed pushes.

Background Information

This is how a push certificate looks like (taken from transparency log linked above, you can find many of those using git rev-list --all | xargs git grep 'push-certificate'):

certificate version 0.1
pusher Greg Kroah-Hartman <[redacted]> 1757943553 +0200
pushee gitolite.kernel.org:/pub/scm/linux/security/vulns.git
nonce 1757943500-958ee85bd67f4eaea3157d5cd3112662e25a39b4

03898d124628c5431c0ca6b24b480d3f388cb81a 5127821c48ea9f2b324af37e0b89f984932763d6 refs/heads/master
-----BEGIN PGP SIGNATURE-----

iQJPBAABCgA5FiEEZH8oZUiU471FcZm+ONu9yGCSaT4FAmjIFwEbHGdyZWdraEBs
[boring ASCII armored gibberish redacted]
-----END PGP SIGNATURE-----

Integration

GitOxide would ideally help generate/parse/validate push certificates, and provide an interface for servers to receive push certificates, akin to a pre-receive hook or post-receive hook, in order to inspect/validate/archive the push certificate. For example, with a pre-receive hook, that executes env one gets:

$ git push --signed=true --verbose gitd HEAD:refs/heads/main
Pushing to git://127.0.0.1/test.git
Looking up 127.0.0.1 ... done.
Connecting to 127.0.0.1 (port 9418) ... 127.0.0.1 done.
Enumerating objects: 1, done.
Counting objects: 100% (1/1), done.
Writing objects: 100% (1/1), 173 bytes | 173.00 KiB/s, done.
Total 1 (delta 0), reused 0 (delta 0), pack-reused 0 (from 0)
remote: MEMORY_PRESSURE_WRITE=c29tZSAyMDAwMDAgMjAwMDAwMAA=
remote: GIT_PUSH_CERT_NONCE=1771173562-5f9dae1dd09d9f4a7859a3c0902e6f21355d9b23
remote: GIT_DIR=.
remote: PWD=/tmp/gitd/test.git
remote: GIT_QUARANTINE_PATH=/tmp/gitd/test.git/./objects/tmp_objdir-incoming-8MaCHr
remote: SYSTEMD_EXEC_PID=1679293
remote: _=/nix/store/iiishysy5bzkjrawxl4rld1s04qj0k0c-coreutils-9.8/bin/env
remote: GIT_OBJECT_DIRECTORY=/tmp/gitd/test.git/./objects/tmp_objdir-incoming-8MaCHr
remote: GIT_EXEC_PATH=/nix/store/y7kk3dj7wlls924zlvpwmm13gskfw1hk-git-2.51.2/libexec/git-core
remote: LANG=en_US.UTF-8
remote: MEMORY_PRESSURE_WATCH=/sys/fs/cgroup/system.slice/git-daemon.service/memory.pressure
remote: INVOCATION_ID=0e855ccd667147b2ac4f5f08c43664e8
remote: GIT_PUSH_CERT_NONCE_STATUS=OK
remote: GIT_PUSH_CERT_KEY=
remote: GIT_PUSH_CERT=ee38e77233a5a684bd9c7677637d19c87a42b3ab
remote: REMOTE_PORT=42010
remote: GIT_PUSH_OPTION_COUNT=0
remote: USER=root
remote: TZDIR=/nix/store/xh1ff9c9c0yv1wxrwa5gnfp092yagh7v-tzdata-2025b/share/zoneinfo
remote: SHLVL=2
remote: LOCALE_ARCHIVE=/nix/store/iv6mysgipfmq0ygrrlx4ym9dzajky62n-glibc-locales-2.40-66/lib/locale/locale-archive
remote: GIT_PUSH_CERT_STATUS=N
remote: GIT_ALTERNATE_OBJECT_DIRECTORIES=/tmp/gitd/test.git/./objects
remote: JOURNAL_STREAM=8:16713250
remote: REMOTE_ADDR=127.0.0.1
remote: PATH=[redacted]
remote: GIT_PUSH_CERT_SIGNER=
To git://127.0.0.1/test.git
   0f773f9..3d103dd  HEAD -> main
updating local tracking ref 'refs/remotes/gitd/main'

And, on the server side, one can then inspect the certificate as an object:

$ git show ee38e77233a5a684bd9c7677637d19c87a42b3ab
certificate version 0.1
pusher anonymous <anonymous@example.com> 1771173562 +0100
pushee git://127.0.0.1/test.git
nonce 1771173562-5f9dae1dd09d9f4a7859a3c0902e6f21355d9b23

0f773f9e647a667517b2b41f2a39d19ef3a8ec7b 3d103dd4e2aec2e4de4e99f80be481e7cc2b6699 refs/heads/main
-----BEGIN PGP SIGNATURE-----

iIwEABYKADQWIQQNqO2NtzieI0l9QXjYrmhOiYYWDQUCaZH2uhYcYW5vbnltb3Vz
QGV4YW1wbGUuY29tAAoJENiuaE6JhhYNjmwBANEXz98KA64a6O7fMGhQWRP4Fp9A
BU+mBgi/Vmqohso3AQDLJezVdNH9HURr03R6pIYYjvKA0Jude4MSLzikc+y/Aw==
=amDq
-----END PGP SIGNATURE-----

Experiments

I set up git daemon on a server.

git daemon command
git daemon \
  --reuseaddr \
  --base-path=/tmp/gitd \
  --listen=127.0.0.1 \
  --port=9418 \
  --user=git \
  --group=git \
  --verbose \
  --enable=receive-pack \
  --verbose \
  --export-all /tmp/gitd

On the client I do:

git init .
git remote add gitd git://127.0.0.1/test.git
git config user.email "anonymous@example.com"
git config user.name "anonymous"
git commit --allow-empty --allow-empty-message -m ""

I have prepared a few dumps so you can see how this looks on the wire. (I have *.pcap files, but fail to attach them.)

Success

> 002egit-receive-pack /test.git.host=127.0.0.1.
< 01020000000000000000000000000000000000000000 capabilities^{}.report-status report-status-v2 delete-refs side-band-64k quiet atomic ofs-delta push-cert=1771171633-0c4ddbbe5aec02916c2f5abfc1c2456725228a8e push-options object-format=sha1 agent=git/2.51.2-Linux
< 0000
> 0057push-cert. report-status-v2 side-band-64k object-format=sha1 agent=git/2.51.2-Linux001ccertificate version 0.1
> 003epusher anonymous <anonymous@example.com> 1771171633 +0100
> 0024pushee git://127.0.0.1/test.git
> 003enonce 1771171633-0c4ddbbe5aec02916c2f5abfc1c2456725228a8e
> 0005
> 00660000000000000000000000000000000000000000 0f773f9e647a667517b2b41f2a39d19ef3a8ec7b refs/heads/main
> 0022-----BEGIN PGP SIGNATURE-----
> 0005
> 0045iIwEABYKADQWIQQNqO2NtzieI0l9QXjYrmhOiYYWDQUCaZHvMRYcYW5vbnltb3Vz
> 0045QGV4YW1wbGUuY29tAAoJENiuaE6JhhYNRPEBAOipmlR3jMJJYSnSetm4bln8d6Xk
> 0045gXSdU8EyjaL46ghWAP9BNf2Tv60T9oQN7GtpCSa8m+bsu2CfQSqMFU8ArtT2AA==
> 000a=QmvQ
> 0020-----END PGP SIGNATURE-----
> 0012push-cert-end
> 0000PACK.........
> x....
> .0...{."wA...)..W..C.,2;.....x{.7vU.&iZ:S......:Q[e..D..J.).c.|....1?^p.q.w..C...
> 1..3
> 2.0"..f.1....Z.4> x........cDv.m	.m...........
< 002e.000eunpack ok
< 0017ok refs/heads/main
< 00000000

Signed Push Not Supported

> 002egit-receive-pack /test.git.host=127.0.0.1.
< 00b70000000000000000000000000000000000000000 capabilities^{}.report-status report-status-v2 delete-refs side-band-64k quiet atomic ofs-delta object-format=sha1 agent=git/2.51.2-Linux
< 0000

Client fails with

$ git push --signed=true --verbose gitd HEAD:refs/heads/main
fatal: the receiving end does not support --signed push

Problem

Server does not advertise signed push capability, client bails because git push --signed=true demands signing. The capability that git-send-pack is looking for seems to be push-cert=<NONCE>, see https://github.com/git/git/blob/67ad42147a7acc2af6074753ebd03d904476118f/send-pack.c#L567-L581.

Solution

Change the Git configuration of the server side, e.g. by editing/etc/gitconfig, ensuring the following:

[receive]
    advertisePushOptions = true
    certNonceSeed = "test"

Client Fails to Sign

> 002egit-receive-pack /test.git.host=127.0.0.1.
< 01020000000000000000000000000000000000000000 capabilities^{}.report-status report-status-v2 delete-refs side-band-64k quiet atomic ofs-delta push-cert=1771170392-33ed7d17b5c157eaa6cbc920d3ce7e20ac9bbad9 push-options object-format=sha1 agent=git/2.51.2-Linux
< 0000

Client fails with:

$ git push --signed=true --verbose gitd HEAD:refs/heads/main
error: gpg failed to sign the data:
gpg: skipped "anonymous <anonymous@example.com>": No secret key
[GNUPG:] INV_SGNR 9 anonymous <anonymous@example.com>
[GNUPG:] FAILURE sign 17
gpg: signing failed: No secret key

fatal: failed to sign the push certificate

Problem

Server advertises signed push capability, but client fails to sign.

Solution

Generate a new key:

$ gpg --quick-generate-key "anonymous@example.com" ed25519 sign 0
We need to generate a lot of random bytes. It is a good idea to perform
some other action (type on the keyboard, move the mouse, utilize the
disks) during the prime generation; this gives the random number
generator a better chance to gain enough entropy.
Please enter the passphrase to
protect your new key
Passphrase:
Repeat:
You have not entered a passphrase - this is in general a bad idea!
Please confirm that you do not want to have any protection on your key.
  Yes, protection is not needed
  Enter new passphrase
[ye]? y
gpg: revocation certificate stored as '/home/lorenz/.gnupg/openpgp-revocs.d/0DA8ED8DB7389E23497D4178D8AE684E8986160D.rev'
public and secret key created and signed.

pub   ed25519/0xD8AE684E8986160D 2026-02-15 [SC]
      Key fingerprint = 0DA8 ED8D B738 9E23 497D  4178 D8AE 684E 8986 160D
uid                              anonymous <anonymous@example.com>

(Note that GitHub does not support signed pushes.)

Motivation 🔦

Refer to https://people.kernel.org/monsieuricon/signed-git-pushes

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions