Artifact fbaaa17e813b8f1da245b773d371221f991eca457798de025e96f6cf84b29b1c:
- File
repo_admin.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: 3229) [annotate] [blame] [check-ins using] [more...]
package libfossil import ( "fmt" "github.com/danmestas/go-libfossil/internal/auth" "github.com/danmestas/go-libfossil/internal/clearsign" ) // UserOpts configures a user creation or update. type UserOpts struct { Login string Password string Caps string } // User describes a Fossil user. type User struct { Login string Caps string } // CreateUser creates a new user in the repository. func (r *Repo) CreateUser(opts UserOpts) error { projectCode, err := r.Config("project-code") if err != nil { return fmt.Errorf("libfossil: create user: %w", err) } return auth.CreateUser(r.inner.DB(), projectCode, opts.Login, opts.Password, opts.Caps) } // GetUser returns information about a user. func (r *Repo) GetUser(login string) (User, error) { u, err := auth.GetUser(r.inner.DB(), login) if err != nil { return User{}, fmt.Errorf("libfossil: get user: %w", err) } return User{Login: u.Login, Caps: u.Cap}, nil } // ListUsers returns all users in the repository. func (r *Repo) ListUsers() ([]User, error) { users, err := auth.ListUsers(r.inner.DB()) if err != nil { return nil, fmt.Errorf("libfossil: list users: %w", err) } result := make([]User, len(users)) for i, u := range users { result[i] = User{Login: u.Login, Caps: u.Cap} } return result, nil } // DeleteUser removes a user from the repository. func (r *Repo) DeleteUser(login string) error { return auth.DeleteUser(r.inner.DB(), login) } // SetPassword updates a user's password. func (r *Repo) SetPassword(login, password string) error { projectCode, err := r.Config("project-code") if err != nil { return fmt.Errorf("libfossil: set password: %w", err) } return auth.SetPassword(r.inner.DB(), projectCode, login, password) } // SetCaps updates a user's capability string. func (r *Repo) SetCaps(login, caps string) error { return auth.UpdateCaps(r.inner.DB(), login, caps) } // Config reads a configuration value from the repo's config table. func (r *Repo) Config(key string) (string, error) { val, err := r.inner.Config(key) if err != nil { return "", fmt.Errorf("libfossil: config %q: %w", key, err) } return val, nil } // SetConfig writes a configuration value to the repo's config table. A // SettingHorizonSeedKey value that is neither an SSH public key nor an // OpenPGP fingerprint is refused. func (r *Repo) SetConfig(key, value string) error { if key == SettingHorizonSeedKey && value != "" { if _, err := clearsign.ParseKey(value); err != nil { return fmt.Errorf("libfossil: set config %q: %w", key, err) } } if err := r.inner.SetConfig(key, value); err != nil { return fmt.Errorf("libfossil: set config %q: %w", key, err) } return nil } // SetHashPolicy sets the repository's hash policy by name, as // `fossil hash-policy NEW-POLICY` does. name must be exactly one of sha1, // auto, sha3, sha3-only or shun-sha1: the match is case-sensitive, and any // other string is an error that leaves the policy unchanged. Asking for auto // in a repository that already holds a SHA3-named artifact sets sha3. func (r *Repo) SetHashPolicy(name string) error { if err := r.inner.SetHashPolicy(name); err != nil { return fmt.Errorf("libfossil: set hash policy: %w", err) } return nil }