Skip to content

Add SSH commit signing for HEAD commits - #2789

Open
kausters wants to merge 7 commits into
git-up:masterfrom
kausters:ssh-signed-commits
Open

Add SSH commit signing for HEAD commits#2789
kausters wants to merge 7 commits into
git-up:masterfrom
kausters:ssh-signed-commits

Conversation

@kausters

@kausters kausters commented Jun 14, 2026

Copy link
Copy Markdown

Summary

Hi! This PR adds SSH commit signing support for GitUpKit user-facing HEAD commit flows. Repositories configured like the Git CLI with commit.gpgsign=true and gpg.format=ssh now create commits with a gpgsig SSH signature for normal commits, merge commits, amend, and conflict-resolution commits.

To limit changes scope, the change intentionally supports only SSH signing. Non-SSH signing formats, including OpenPGP and X.509, continue to produce unsigned GitUp commits rather than blocking existing workflows. History replay/reorder paths also remain unsigned; signing those GitUp-created rewrite commits can be considered separately from signing commits users make directly.

What changed

  • Adds a private GCCommitSigning.m helper that creates the unsigned commit buffer, signs it with SSH, and writes the signed object through libgit2.
  • Resolves SSH signing config from user.signingkey, inline SSH public keys, and gpg.ssh.defaultKeyCommand.
  • Uses gpg.ssh.program or ssh-keygen, launched with GitUp login-shell PATH handling so GUI-launched GitUp can find shell-installed signers.
  • Routes user-facing HEAD commit creation through the signing-aware helper while leaving replay/rewrite helpers unchanged.
  • Adds focused signing tests plus HEAD integration coverage for signed normal, merge, and amended commits.

Tests

  • Focused GitUpKit signing and HEAD integration tests pass.
  • Test coverage includes unsigned behavior when signing is disabled, unsigned behavior for unsupported non-SSH formats, missing key errors, inline key and default key command resolution, key path signing, signer failure, and Git CLI verify-commit verification with an allowed signers file.

Notes

  • I AGREE TO THE GITUP CONTRIBUTOR LICENSE AGREEMENT
  • I am not an Obj-C or even MacOS developer at all so if anything's not up to the project standards, please let me know
  • I'm not clear on the wishes of the contribution guidelines around commits, as I normally just have GitHub squash PR commits on merge. Please let me know if you'd like them pre-flattened or whatever.
  • Fixes (partially? because we're not signing all commits yet, might follow up with another PR) Support SSH signing #1036.
  • Note how all the commits in this PR show up as "Verified" on GitHub 😉 Some repos actually require that, hence my doing this.

@lucasderraugh

Copy link
Copy Markdown
Collaborator

Thanks for the contribution @kausters. I'm out for some time so I probably won't review it this week. I'm also looking to push out the revamped release I've had in the continuous channel for quite some time to stable. So ballpark 2+ weeks from now we'll get this in a build if all looks good.

@lucasderraugh
lucasderraugh self-requested a review June 14, 2026 23:06
@lucasderraugh

Copy link
Copy Markdown
Collaborator

@greptile Can you review this PR?

@greptile-apps

greptile-apps Bot commented Jul 27, 2026

Copy link
Copy Markdown

Greptile Summary

This PR adds SSH commit signing support for user-facing HEAD commit flows in GitUpKit. Repositories configured with commit.gpgsign=true and gpg.format=ssh now produce cryptographically signed commits for normal, merge, amend, and conflict-resolution paths, while replay/rewrite paths remain intentionally unsigned.

  • Introduces GCCommitSigning.m with two new C-level helpers (GCCreateCommitFromTreeWithOptionalSignature, GCCreateCommitFromCommitWithIndexAndOptionalSignature) that check signing config, resolve the SSH key via user.signingkey, inline key, or gpg.ssh.defaultKeyCommand, call the signer via gpg.ssh.program / ssh-keygen, and write the result through git_commit_create_with_signature.
  • Routes createCommitFromHEAD* and createCommitByAmendingHEAD* through the new signing-aware helpers; replay and bare-repo paths keep using the existing unsigned helpers.
  • Adds thorough unit tests covering unsigned fallback, missing key error, inline key, default key command, file path key, signer failure, and end-to-end git verify-commit verification.

Confidence Score: 5/5

Safe to merge. The signing path is additive and gated behind config checks; repos without signing config continue to produce identical unsigned commits through the unchanged code paths.

The new signing helpers correctly replicate the amend-path behavior (same author, freshly-minted committer, same parents), parent-commit memory is properly freed in the cleanup block, inline-key temp files are always cleaned up after the signer exits, and the libgit2 buffer → sign → write flow matches the expected API usage. Test coverage is comprehensive: unsigned fallback, missing key error, inline key, default key command, file-path key, signer failure, PATH refresh, and end-to-end git verify-commit all have dedicated cases.

Files Needing Attention: No files require special attention.

Important Files Changed

Filename Overview
GitUpKit/Core/GCCommitSigning.m Core signing implementation. Key resolution, PATH retrieval, temp-file lifecycle, and the libgit2 buffer/signature/write flow are all correct. Error propagation and cleanup are handled throughout.
GitUpKit/Core/GCRepository+HEAD.m Routes createCommitFromHEAD* and createCommitByAmendingHEAD* through the new signing helpers. The amend path is behaviorally equivalent to the old git_commit_create_from_callback path: same author, same committer (refreshed via git_signature_default), same parents, same message.
GitUpKit/Core/GCCommitSigning-Tests.m Comprehensive focused signing tests covering unsigned fallback, missing key, inline key, default key command, file path, signer failure, and PATH refresh behavior. Shared helpers correctly moved to GCCommitSigningTestHelpers.
GitUpKit/Core/GCCommitSigningTestHelpers.m Shared test helpers GCCommitSignature, GCCommitHasSSHSignature, GCConfigureSSHSigningWithKeyPath correctly extracted and used by both test files.
GitUpKit/Core/GCRepository+HEAD-Tests.m Adds testSSHSignsUserFacingCommits covering normal, merge, and amend commits with a real ssh-keygen-generated key and git verify-commit end-to-end verification.
GitUpKit/Core/GCPrivate.h Adds declarations for the two new C-level commit helpers and getPATHUsingShell: behind the existing TARGET_OS_IPHONE guard. Correct placement.
GitUpKit/GitUpKit.xcodeproj/project.pbxproj GCCommitSigning.m added to all three library targets; test files added to the test target only. GCCommitSigningTestHelpers.h correctly not in Sources (header only).

Sequence Diagram

sequenceDiagram
    participant C as Caller (HEAD.m)
    participant S as GCCommitSigning.m
    participant L as libgit2
    participant K as ssh-keygen / signer

    C->>S: GCCreateCommitFromTreeWithOptionalSignature(repo, tree, parents, ...)
    S->>L: git_repository_config → git_config_get_bool(commit.gpgsign)
    alt "gpgsign=false or format≠ssh"
        S->>L: git_commit_create(unsigned)
        L-->>S: oid
    else "gpgsign=true and format=ssh"
        S->>L: git_commit_create_buffer → commitBuffer
        S->>S: _CommitSigningPATH (shell → $PATH)
        S->>S: _SSHSigningKeyPath (user.signingkey / defaultKeyCommand)
        opt inline key
            S->>S: write public key to tmp file
        end
        S->>K: /usr/bin/env signer -Y sign -n git -f keyPath
        K-->>S: SSH signature (stdout)
        S->>S: cleanup tmp key file
        S->>L: git_commit_create_with_signature(commitBuffer, signature, gpgsig)
        L-->>S: oid
    end
    S->>L: git_commit_lookup(oid) → GCCommit
    S-->>C: GCCommit (signed or unsigned)
Loading

Reviews (3): Last reviewed commit: "Refresh signing PATH for each commit" | Re-trigger Greptile

Comment thread GitUpKit/Core/GCCommitSigning-Tests.m Outdated
Comment thread GitUpKit/Core/GCCommitSigning.m
@kausters

kausters commented Jul 30, 2026

Copy link
Copy Markdown
Author

Added another fix mentioned but not explicitly posted by Greptile: signing now (aa0f81d) resolves PATH once per signing operation and shares it between the default-key command and signer, instead of caching it for the process lifetime. I also added a test verifying that PATH is looked up only once within an operation and refreshed for the next commit. All focused signing tests pass.

@kausters
kausters force-pushed the ssh-signed-commits branch from ee89a07 to aa0f81d Compare July 30, 2026 13:05
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.

2 participants