Skip to content

Signing your commits

This page is for anyone who commits to a KTL repository, and above all for a curator.

A signature ties a commit to you cryptographically, so the forge can show it Verified. In the KTL repositories it is required for one thing: a pull request that adds or removes a by: human: verification event in .lokf/knowledge/.

The provenance job in each repository’s knowledge-registrar.yaml checks those. It accepts either an approving review from the confirming account or a verified commit attributed to it. A sole maintainer cannot approve their own pull request, so for them the signature is the only way. The job exists because a claim that a named person checked a concept has to be tied back to that person: threat-model.md says what such a confirmation proves.

You are encouraged to sign everything. It costs one setup and nothing afterwards.

Choose one format: GPG or SSH. Mixing them is the usual failure. Git reads user.signingkey in whichever format gpg.format names, so a file in the wrong format fails with could not load public key. If you have no preference, GPG is the one to pick: it carries an identity and an expiry of its own, which is what the sections below manage. Whichever format you choose, the address in git config user.email must be a verified email on your GitHub account, or every commit stays Unverified.

The key is kept in ~/.gnupg/. You never save it to a file of your own: exporting prints it to the screen for pasting.

  1. Create it. Choose ECC (sign only) with Curve 25519, and an expiry of 1-2 years rather than 0 (never expires). An expiry limits the damage if the key is ever lost, and extending it later is one command.

    Terminal window
    gpg --full-generate-key
  2. Set a passphrase, and record it in your password manager as you type it. There is no recovery: a forgotten passphrase means the key is unusable and you start again.

  3. Find the fingerprint, the 40-character string on the line under sec:

    Terminal window
    gpg --list-secret-keys --keyid-format=long
  4. Point git at it:

    Terminal window
    git config --global gpg.format openpgp
    git config --global user.signingkey <fingerprint>
    git config --global commit.gpgsign true
  5. Give the public half to the forge. Copy the whole block, BEGIN/END lines included, and paste it at github.com/settings/keys → New GPG key, or under your profile’s GPG keys on GitLab, Codeberg or Gitea:

    Terminal window
    gpg --armor --export <fingerprint>
  6. Test it. Make any commit, then:

    Terminal window
    git log --show-signature -1

    You want gpg: Good signature from .... GitHub’s own verdict is the Verified badge next to the commit once pushed.

  7. Optional, but it removes most of the prompts: cache the passphrase for the working day instead of being asked every commit. Put this in ~/.gnupg/gpg-agent.conf, then run gpgconf --reload gpg-agent:

    default-cache-ttl 28800
    max-cache-ttl 86400

SSH is simpler, and reuses a key you may already have. The file must be an SSH public key, one line starting ssh-ed25519 AAAA..., and not a GPG export saved under an SSH-looking name.

Terminal window
ssh-keygen -t ed25519 -C "you@example.com" # skip if ~/.ssh/id_ed25519 exists
git config --global gpg.format ssh
git config --global user.signingkey ~/.ssh/id_ed25519.pub
git config --global commit.gpgsign true
cat ~/.ssh/id_ed25519.pub # paste this into the forge

Add it at github.com/settings/keys under SSH keys → New SSH key → Key type: Signing Key. GitHub keeps authentication and signing keys in separate lists. A key registered only for authentication leaves every commit Unverified, even though it is the same key. GitLab asks the same as the key’s usage type.

Set a calendar reminder for a month before the expiry date gpg --list-secret-keys shows. Renewing takes a minute:

Terminal window
gpg --quick-set-expire <fingerprint> 2y
gpg --armor --export <fingerprint>

Then, on github.com/settings/keys, delete the old entry and add the exported key again. GitHub stores the expiry from the copy you uploaded and does not re-read it, so this step is what renews it there.

Two things to know:

  • Extending keeps the same key and fingerprint, so past commits keep verifying and nothing else needs reconfiguring. Prefer it to making a new key.
  • An expiry is not a compromise. GitHub still shows commits signed before the key expired as Verified. What an expired key stops is new signatures. So renew whenever you notice. But if the key is ever stolen, revoke it instead, and do not extend it.

Put your key on file for the forge-free gate

Section titled “Put your key on file for the forge-free gate”

A repository that runs knowledge-provenance.sh (ktl-sidecar Step 5) checks each confirmation’s signature against a public key it carries under .lokf/curators/. The file is named after your login on the forge (GitHub, GitLab, Codeberg or another). Export the key you sign with and open a pull request holding only that file. The gate, the CI job that checks a pull request, refuses a change that adds a key and a confirmation by its holder together:

Terminal window
gpg --armor --export YOUR_KEY_ID > .lokf/curators/YOUR_LOGIN.asc # GPG (the export includes the subkeys)
cp ~/.ssh/id_ed25519.pub .lokf/curators/YOUR_LOGIN.pub # SSH

Keep one kind of key per login. When the key changes, replace the file in its own pull request the same way. A maintainer reviewing that change is what makes the key on file worth anything.

  • could not load public key: gpg.format does not match the key user.signingkey points at. Check the file is what its name implies: a GPG export begins -----BEGIN PGP PUBLIC KEY BLOCK-----, an SSH key begins ssh-ed25519.
  • No prompt appears and the commit fails: GPG has nowhere to ask for your passphrase, common over SSH or in a terminal-less session. Add export GPG_TTY=$(tty) to your shell profile and open a new shell.
  • Signed locally but GitHub says Unverified: the commit email is not a verified address on your account, or the key was added to the wrong list (authentication instead of signing).