Skip to content

Add OpenPGP and X.509 commit signing - #2827

Open
anandghegde wants to merge 1 commit into
git-up:masterfrom
anandghegde:feature/openpgp-commit-signing
Open

anandghegde wants to merge 1 commit into
git-up:masterfrom
anandghegde:feature/openpgp-commit-signing

Conversation

@anandghegde

Copy link
Copy Markdown

I AGREE TO THE GITUP CONTRIBUTOR LICENSE AGREEMENT

Closes #42.

SSH commit signing landed in #2789. That change deliberately scoped itself to SSH and left a comment saying so:

// Only SSH commit signing is currently supported.
// Preserve existing GitUp behavior for OpenPGP/X.509 configs by creating an unsigned commit.

This picks up the other two formats, so commit.gpgsign is now honoured for every gpg.format Git supports. That closes the long-standing request in #42, where people have been dropping to the command line just to commit.

The signing call

The signature comes from running the configured program exactly the way Git runs it — <program> --status-fd=2 -bsau <key> with the commit buffer on stdin — reusing the GCTask plumbing and the once-per-operation PATH refresh that #2789 already put in place.

Program resolution follows Git's rules:

Format Config, in order Default
openpgp (and unset) gpg.openpgp.program, then gpg.program gpg
x509 gpg.x509.program gpgsm

gpg.program is Git's legacy synonym for gpg.openpgp.program and deliberately does not apply to X.509 — easy to get wrong, so there's a test pinning it.

When user.signingkey is unset, the committer identity is passed as the user ID. Git does this so GnuPG picks the key matching the commit rather than whichever key GnuPG considers its own default; omitting -u entirely would quietly sign with a different key than git commit would have used.

Why --status-fd=2

GnuPG can exit zero without having produced a signature. Git guards against that by asking for the machine-readable status and requiring SIG_CREATED, and this does the same. Trusting the exit code alone would mean writing a commit with a garbage gpgsig header.

Behaviour change worth calling out

An unrecognised gpg.format is now an error rather than a silently unsigned commit. Git rejects it too (error: invalid value for 'gpg.format': 'pgp'), and for someone who explicitly asked for signed commits, silently dropping the signature is the worst of the available outcomes. The existing test that asserted "OpenPGP config remains unsigned" is updated accordingly — it now covers the still-correct case of a format configured without commit.gpgsign.

Verification

Every rule above was checked against the real thing rather than against the documentation. I pointed gpg.program / gpg.openpgp.program / gpg.x509.program at a stub that records its argument vector, ran git commit under git 2.50.1, and compared:

  • --status-fd=2 -bsau <key>, in that order
  • user.signingkey forwarded verbatim when set
  • Bot <bot@example.com> — the committer identity — when it is genuinely unset
  • gpg.openpgp.program beating gpg.program
  • gpg.x509.program used for X.509 with gpg.program ignored

The unit tests drive the same stub through GCCreateCommitFromTreeWithOptionalSignature and assert the same argument vectors, so the parity is pinned rather than just observed once.

Seven new tests in GCCommitSigning-Tests.m cover the default format, program precedence, the signing key fallback, X.509, a GnuPG that exits zero without signing, a GnuPG that fails outright, and an invalid gpg.format. ./format-source.sh is clean and the app builds.

One gap I want to be upfront about: there is no end-to-end test against a real gpg, because that would need a throwaway keyring generated at test time and GnuPG isn't guaranteed on the runner. The stub tests pin the invocation; they can't prove GnuPG is happy with it. The SSH tests could go further there only because ssh-keygen is always present.

Unrelated: GCSingleCommitRepositoryTests/testClone fails for me on a clean master too — it clones over the network.

SSH commit signing landed in git-up#2789, which deliberately left the other two
formats alone: a repository configured with commit.gpgsign and an OpenPGP or
X.509 gpg.format silently produced an unsigned commit. This extends the same
machinery to cover both, so GitUp now honours commit.gpgsign for every format
Git itself supports.

The signature is produced by running the configured signing program the way
Git runs it, "<program> --status-fd=2 -bsau <key>", with the commit buffer on
stdin. Program resolution follows Git's rules: gpg.openpgp.program then
gpg.program (its legacy synonym) then "gpg" for OpenPGP, and gpg.x509.program
then "gpgsm" for X.509, with gpg.program deliberately not applying to X.509.
When user.signingkey is unset, the committer identity is passed as the user
ID, which is what Git does so that GnuPG selects the key matching the commit
rather than whichever key it considers its own default.

Success is decided by GnuPG's machine-readable status rather than its exit
code alone, because GnuPG can exit zero without having produced a signature.
That is the reason for --status-fd=2, and it matches how Git validates the
same invocation.

An unrecognised gpg.format is now an error instead of quietly producing an
unsigned commit. Git rejects it too, and silently dropping the signature is
the worst available outcome for someone who asked for signed commits.

Every one of these rules was verified against git 2.50.1 by pointing
gpg.program at a recording stub and comparing the resulting argument vector.
@greptile-apps

greptile-apps Bot commented Sep 19, 2026

Copy link
Copy Markdown

RetriggerConfidence Score: 5/5

The PR appears safe to merge; no actionable correctness, security, or repository-rule violations were identified.

Summary

This PR extends commit signing from SSH-only support to OpenPGP and X.509 while preserving failure propagation between external signers and commit creation.

  • Resolves signing format and executable selection from Git-compatible configuration.
  • Invokes GPG or GPGSM with the commit buffer and validates successful signature creation.
  • Rejects unknown formats and signer failures instead of silently creating unsigned commits.
  • Adds focused tests for format defaults, program precedence, signing-key fallback, X.509 signing, and failure paths.
Diagram
%%{init: {'theme': 'neutral'}}%%
flowchart TD
  A[Create commit] --> B{commit.gpgsign enabled?}
  B -- No --> C[Create unsigned commit]
  B -- Yes --> D{gpg.format}
  D -- SSH --> E[Resolve SSH key and signer]
  D -- OpenPGP --> F[Resolve gpg program and signing key]
  D -- X.509 --> G[Resolve gpgsm program and signing key]
  D -- Unknown --> H[Return configuration error]
  E --> I[Sign commit buffer]
  F --> I
  G --> I
  I --> J{Signer succeeded and produced signature?}
  J -- No --> K[Fail commit creation]
  J -- Yes --> L[Create commit with gpgsig header]
Loading

Reviews (1) · Last reviewed commit: "Add OpenPGP and X.509 commit signing"

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

Support GPG signing

1 participant