Artifact c60d9f151e050fe26dc5fbbabca2aa49202e5bc49f44a17b90084f56ec0ad300:
- File
signing.go
— part of check-in
[612f2e7acc]
at
2026-10-23 20:05:29
on branch main
— feat(signing): sign check-ins and horizon seeds with gpg or ssh
go-libfossil signed nothing and verified nothing, so a structural-horizon clone could not tell a seed its origin minted from one substituted on disk (draft-fossil-horizon-00 §5.2.4, HZ-5.2-5 and HZ-5.2-6), and a repository with fossil's clearsign setting on stored its check-ins unsigned.
Signing follows fossil's contract, the same one zit follows. With clearsign on (and the legacy omitsign off), every check-in a commit or branch creation stores is clear-signed by pgp-command: unset, gpg --clearsign -o, run as COMMAND OUTFILE INFILE; a command named ssh* runs as COMMAND INFILE and its detached signature is wrapped in fossil's SSH envelope. A signer that fails, or returns text that does not strip back to the manifest, abandons the commit. NoSign and --nosign store one check-in unsigned.
An origin with clearsign on signs each seed it mints. It signs only after the request is authorized, outside the request's transaction, keeps one signature per seed and command so the seed's name is stable, and re-signs a kept signature its own horizon-seed-key no longer verifies. Keeping is best effort, so a read-only mirror still serves.
A clone given its origin's key (CloneOpts.SeedKey, repo clone --seed-key, kept as horizon-seed-key) verifies every seed on clone, pull and rebuild, and falls back to the content horizon from one unsigned or signed by another key. An SSH key is verified in process: namespace fossilscm, the configured key, no SHA-1 ssh-rsa, and exactly fossil's envelope. An OpenPGP fingerprint is verified by gpg, which must report one good signature by that key over text byte-equal to what the parser reads, with nothing after the signature. A verifier that cannot run keeps the held seed and fails the sync instead of discarding it.
fossil 2.28 and zit read the signed check-ins; a check-in zit signs with the same SSH key verifies here; zit clones a signing origin and holds its seed. (user: [email protected], size: 1956) [annotate] [blame] [check-ins using] [more...]
package libfossil import "github.com/danmestas/go-libfossil/internal/clearsign" // SettingClearsign names the boolean repository setting, read as fossil reads // one, that signs every check-in a commit or a branch creation stores, as // fossil and zit sign them, and every horizon seed an origin mints // (draft-fossil-horizon-00 §5.2.4). Off by default. The signed text is the // artifact, so a signed check-in's name is the hash of the signed bytes; a // signer that fails abandons the commit. CommitOpts.NoSign and // CheckoutCommitOpts.NoSign store one check-in unsigned, as fossil's // --nosign does, and fossil's legacy omitsign setting turns signing off. const SettingClearsign = clearsign.SettingClearsign // SettingPGPCommand names the signing command, as fossil names it. Unset, it // is fossil's default, "gpg --clearsign -o ", run as COMMAND OUTFILE INFILE. // A command whose name starts with "ssh" is run as COMMAND INFILE and writes // a detached INFILE.sig, which is wrapped in fossil's SSH envelope; to sign // with an SSH key, set it to // // ssh-keygen -q -Y sign -n fossilscm -f ~/.ssh/id_ed25519 // // The namespace must be fossilscm for a clone to verify the signature. The // command runs through the shell, so ~ and environment variables expand. const SettingPGPCommand = clearsign.SettingPGPCommand // SettingHorizonSeedKey names the setting holding the public key a clone // verifies its origin's structural horizon seeds with // (draft-fossil-horizon-00 §5.2.4): an SSH public key line, verified in // process, or the fingerprint of an OpenPGP key in the user's gpg keyring, // verified by gpg. With a key set, a seed that is unsigned or not signed by // that key is discarded and the clone falls back to the content horizon; // unset, an unsigned seed is accepted. CloneOpts.SeedKey sets it when the // clone is made. It is the clone's own: an origin never sends it. const SettingHorizonSeedKey = clearsign.SettingSeedKey