Skip to main content

hg-git-sign

A Mercurial extension that signs changesets the way git commit -S does, so that hg-git pushes them to GitHub (or any Git host) as signed commits that show up as Verified.

Unlike the bundled gpg extension, no extra changeset is created. The signature is computed when the changeset is committed and stored inside it, in the gpgsig extra field, which is the field hg-git already uses for signatures on commits it pulls from Git.

Requirements

  • Mercurial 6.6 through 7.1
  • hg-git 1.2.0, with the dulwich it requires (0.21.6 or newer, before 0.23)
  • Python 3.9 or newer, as supported by the Mercurial version in use
  • gpg for OpenPGP signatures, or ssh-keygen (OpenSSH 8.0+) for SSH signatures

The full test suite passes on every combination below:

Mercurial hg-git dulwich Python
6.6.3, 6.7.4 1.2.0 0.22.8 3.11
6.8.2 1.2.0 0.22.8 3.9, 3.12
6.9.5, 7.0.3 1.2.0 0.22.8 3.12
7.1.2 1.2.0 0.21.6, 0.22.8 3.12, 3.13

Setup

Install from PyPI into the same Python environment that runs hg; this also installs hg-git if needed:

pip install hg-git-sign

Then add to ~/.hgrc:

[extensions]
hggit =
hggitsign =

[hggitsign]
# Default OpenPGP key id or fingerprint, can be overwritten per-repository
# in .hg/hgrc
key = 0123456789ABCDEF

To run from a checkout instead, install hg-git separately and point at the file: hggitsign = /path/to/hg-git-sign/hggitsign.py.

For SSH signing instead:

[hggitsign]
format = ssh
key = ~/.ssh/id_ed25519
# optional, used by `hg gitsign-verify`
allowedsigners = ~/.ssh/allowed_signers

key may also be a literal public key (ssh-ed25519 AAAA... or key::ssh-ed25519 AAAA...), in which case the private key must be in ssh-agent.

Then upload the public key to GitHub under Settings > SSH and GPG keys (see Signing keys if you don't have one yet). The email in ui.username must be a verified email on your GitHub account and, for OpenPGP, a user id on the key. hg-git uses ui.username as both author and committer, which is what GitHub checks.

Signing keys

Creating and managing keys is outside the scope of this project, but these are the essentials. The signatures are the same ones git commit -S makes, so any guide written for Git applies here too.

OpenPGP

gpg --quick-generate-key "Your Name <you@example.com>" ed25519 sign 2y
gpg --list-secret-keys --keyid-format=long   # the key id follows "sec   ed25519/"
gpg --armor --export <key id>                # paste into GitHub as a GPG key
  • Can replace 2y in the --quick-generate-key command with never for a key that never expires. See these docs
  • You may consider uploading your key(s) for public consumption
  • Installing: brew install gnupg pinentry-mac or GPG Suite on macOS, the gnupg package on Linux, Gpg4win on Windows.
  • If hg commit aborts with Inappropriate ioctl for device, or never asks for the passphrase, gpg has no terminal for its passphrase prompt. Add export GPG_TTY=$(tty) to your shell startup file. On macOS you can instead put pinentry-program /opt/homebrew/bin/pinentry-mac (check the path with which pinentry-mac) in ~/.gnupg/gpg-agent.conf and run gpgconf --kill gpg-agent.
  • GitHub: Generating a new GPG key, Adding a GPG key to your account.
  • The GnuPG manual.

SSH

ssh-keygen -t ed25519 -C "you@example.com"   # or reuse an existing key
echo "you@example.com $(cat ~/.ssh/id_ed25519.pub)" >> ~/.ssh/allowed_signers
  • On GitHub, add the public key with Key type: Signing Key. A key added only as an authentication key does not verify commits, even if it is the same key, so add it a second time for signing.
  • If the key has a passphrase, load it into ssh-agent with ssh-add (ssh-add --apple-use-keychain on macOS) so signing doesn't prompt on every commit.
  • The allowed_signers format is described under ALLOWED SIGNERS in ssh-keygen(1).
  • GitHub: Generating a new SSH key, Adding a new SSH key to your account.

Further reading

Configuration

Setting Default Meaning
hggitsign.enabled True Sign new changesets.
hggitsign.format openpgp openpgp or ssh, like git's gpg.format.
hggitsign.key committer id Key id (OpenPGP) or key file / literal key (SSH, required).
hggitsign.program gpg / ssh-keygen Signing program, like gpg.program / gpg.ssh.program.
hggitsign.allowedsigners unset Allowed signers file for SSH verification.

Add Ons Directory

Please check out the add-ons directory for examples of how to integrate signing commits with password managers. This can simplify signing commits, e.g. with TouchID. An example implementation for 1Password is included at the moment, others may be included in the future.

Usage

hg commit -m "..."               # signed
hg commit --no-gitsign -m "..."  # not signed, once
hg commit --gitsign -m "..."     # signed even if hggitsign.enabled=false
hg rebase --config hggitsign.enabled=false ...   # other commands: config override

hg gitsign-verify                # verify the working copy parent
hg gitsign-verify -r 'all()'     # verify everything; exit 1 if anything fails
hg log -r "extra('gpgsig')"      # list signed changesets

hg gitsign-verify checks the signature against the exact Git commit that hg-git has exported (exporting first if needed), so it validates what GitHub will see. For SSH signatures it reports untrusted unless the signer is listed in hggitsign.allowedsigners.

If gpg or ssh-keygen cannot sign, the commit is aborted and the working copy is left untouched, just like Git.

How it works

A Git commit signature covers the bytes of the Git commit object. For a Mercurial changeset, that object is what hg-git builds at export time: a tree id, the Git ids of the parents, author and committer lines, the message and hg-git's HG: metadata headers.

hggitsign hooks commitctx, the single place where every new changeset passes through (hg commit, --amend, rebase, histedit, graft, import, ...). Before the changeset is written it:

  1. makes sure the parents already have Git ids, exporting to the hg-git cache in .hg/git if they do not, exactly as the first hg push would;
  2. builds the Git commit object the changeset will export to, using hg-git's own author, message and tree code on the pending context;
  3. signs those bytes with gpg or ssh-keygen;
  4. stores the signature in the gpgsig extra of the changeset.

On export, the extension attaches the stored signature as the Git gpgsig header. The resulting Git commit is byte-for-byte what Git itself would have produced, so GitHub verifies it, and a clone of the Git repository back into Mercurial yields the identical changeset ids.

Changesets without a signature export exactly as they would without the extension. Rewriting a signed changeset drops the old signature and signs the new one (or leaves it unsigned if signing is disabled), as Git does.

Caveats

  • Load the extension on every machine that pushes. hg-git 1.2 on its own does not export the gpgsig extra as a signature header; it would turn it into an HG:extra line instead, producing an unsigned commit with a different id.
  • The first signed commit in a repository that has never talked to Git exports the whole history into .hg/git. That is a one-time cost, the same as the first hg push to a Git remote.
  • The signature needs the parents' Git ids, so the Git ids of ancestors are frozen once a child is signed. That is inherent in signing and is the same in Git.
  • Not supported: experimental.copies.write-to=changeset-only (signing aborts with a message), X.509 / S/MIME signatures, signed tags.
  • Changesets pulled from Git keep whatever signature they came with; they are never re-signed.

Tests

tests/test-hggitsign.sh

The script runs in an isolated HOME with throwaway OpenPGP and SSH keys and exercises plain, partial, amended, rebased, grafted, merge and named-branch commits, round trips through a bare Git repository, commits made by Git, a tampered commit, and SSH signing. If git is on the PATH, every pushed commit is also checked with git verify-commit, which is the same check GitHub performs.

License

GNU General Public License, version 2 or later, like Mercurial and hg-git.

Metadata

Release files for hg-git-sign 0.1.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for hg-git-sign 0.1.0
File Size Uploaded Provenance
hg_git_sign-0.1.0.tar.gz 22.7 kB Without provenance Details

Built distribution (wheel)

Table of built distributions (wheels) for hg-git-sign 0.1.0
File Interpreter ABI Platform
hg_git_sign-0.1.0-py3-none-any.whl Python 3 none any Details

Total release size: 44.6 kB

Release files / hg_git_sign-0.1.0.tar.gz

Download URL hg_git_sign-0.1.0.tar.gz
Size 22.7 kB
Tags Source
SHA-256 checksum
How to use checksums
7d912d3286edff96039e40054004b8b89689a3a84153c64bdeb964e51d4fcd7f
BLAKE2b-256 checksum
How to use checksums
8ffcdb171110fc6556fd03db9793c5459db5a182e0df9364f26b5caed846be12
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.12.12

Release files / hg_git_sign-0.1.0-py3-none-any.whl

Download URL hg_git_sign-0.1.0-py3-none-any.whl
Size 21.9 kB
Tags Python 3
SHA-256 checksum
How to use checksums
d280de91c6e7b1b868ff5c4fb3aa14916d44712aac128a7961a4e7b95a4736bc
BLAKE2b-256 checksum
How to use checksums
5738fac22bef39d00cb9e769dd60ef2110205d5caf82088236b71118e53b3698
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.12.12

Security for hg-git-sign 0.1.0

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

No provenance found

None of the files in this release were uploaded with provenance metadata.

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

Release history Release notifications | RSS feed

This release

0.1.0 This release

2 release files No provenance

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page