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
gpgfor OpenPGP signatures, orssh-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
2yin the--quick-generate-keycommand withneverfor a key that never expires. See these docs - You may consider uploading your key(s) for public consumption
- Installing:
brew install gnupg pinentry-macor GPG Suite on macOS, thegnupgpackage on Linux, Gpg4win on Windows. - If
hg commitaborts withInappropriate ioctl for device, or never asks for the passphrase, gpg has no terminal for its passphrase prompt. Addexport GPG_TTY=$(tty)to your shell startup file. On macOS you can instead putpinentry-program /opt/homebrew/bin/pinentry-mac(check the path withwhich pinentry-mac) in~/.gnupg/gpg-agent.confand rungpgconf --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-agentwithssh-add(ssh-add --apple-use-keychainon macOS) so signing doesn't prompt on every commit. - The
allowed_signersformat 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
- Signing Your Work in Pro Git
- Troubleshooting commit signature verification on GitHub
- Signed commits on GitLab
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:
- makes sure the parents already have Git ids, exporting to the hg-git cache
in
.hg/gitif they do not, exactly as the firsthg pushwould; - builds the Git commit object the changeset will export to, using hg-git's own author, message and tree code on the pending context;
- signs those bytes with
gpgorssh-keygen; - stores the signature in the
gpgsigextra 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
gpgsigextra as a signature header; it would turn it into anHG:extraline 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 firsthg pushto 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)
| File | Size | Uploaded | Provenance | |
|---|---|---|---|---|
| hg_git_sign-0.1.0.tar.gz | 22.7 kB | Without provenance | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| 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.