Commit signing does not travel with the forge credential: signed-commit forges are unworkable via madtea #424

Open
opened 2026-08-17 15:51:42 +00:00 by hexajon · 3 comments
Owner

Problem

madtea resolves the auth TOKEN per forge host from its secure backend and injects it via GIT_ASKPASS for clone/push/API, so authentication travels automatically with the repo.

Commit SIGNING does not travel. On a forge madtea treats as requiring signed commits, madt_commit and madt_finish refuse:

This repo's origin is git.cheshirecube.cloud, where every commit must be signed, but signing is NOT configured in this clone (commit.gpgsign='unset', user.signingkey unset).

madtea holds the token, not a signing key, so it can authenticate but cannot sign. A fresh madt_clone of such a forge is then unworkable through the madtea flow: the operator must hand-configure commit.gpgsign / user.signingkey / gpg.format per clone, with a key already registered to the forge account. Auth is seamless; signing is manual and per-clone.

Observations

  • The guard fires per forge HOST (git.cheshirecube.cloud), independent of the specific repo's protection. GET /repos/<owner>/<repo>/branch_protections on a repo on that host returns [] (no rule requires signed commits there), yet the guard still blocks every commit in the clone.
  • An unsigned commit created via the contents API on that same repo was accepted (verification reason not_signed_commit). So the repo currently accepts unsigned commits, while the local guard refuses to make one.
  • Net: the guard can block commits the forge would accept, and offers no path to satisfy it.

Enhancement

Let the signing identity travel with the forge credential, the way the token does. Candidate directions, and the security design behind them is still open:

  • Resolve a per-host or per-identity signing key from the same secure backend, and auto-configure the clone (commit.gpgsign / user.signingkey / gpg.format) on clone or before the first commit.
  • Or sign on the caller's behalf through a madtea-managed signing identity (SSH or GPG), keeping the key out of plain repo config.
  • Make the refusal actionable: offer to configure signing from a resolved key instead of only printing manual git commands, and gate the guard on the repo's actual signed-commit protection.

Notes

  • Separate from #391 (forge base-URL resolution).
  • I want signed commits on that forge, so the right end state is signed commits that work through madtea, not a way to bypass them.
## Problem madtea resolves the auth TOKEN per forge host from its secure backend and injects it via GIT_ASKPASS for clone/push/API, so authentication travels automatically with the repo. Commit SIGNING does not travel. On a forge madtea treats as requiring signed commits, `madt_commit` and `madt_finish` refuse: ``` This repo's origin is git.cheshirecube.cloud, where every commit must be signed, but signing is NOT configured in this clone (commit.gpgsign='unset', user.signingkey unset). ``` madtea holds the token, not a signing key, so it can authenticate but cannot sign. A fresh `madt_clone` of such a forge is then unworkable through the madtea flow: the operator must hand-configure `commit.gpgsign` / `user.signingkey` / `gpg.format` per clone, with a key already registered to the forge account. Auth is seamless; signing is manual and per-clone. ## Observations - The guard fires per forge HOST (git.cheshirecube.cloud), independent of the specific repo's protection. `GET /repos/<owner>/<repo>/branch_protections` on a repo on that host returns `[]` (no rule requires signed commits there), yet the guard still blocks every commit in the clone. - An unsigned commit created via the contents API on that same repo was accepted (verification reason `not_signed_commit`). So the repo currently accepts unsigned commits, while the local guard refuses to make one. - Net: the guard can block commits the forge would accept, and offers no path to satisfy it. ## Enhancement Let the signing identity travel with the forge credential, the way the token does. Candidate directions, and the security design behind them is still open: - Resolve a per-host or per-identity signing key from the same secure backend, and auto-configure the clone (`commit.gpgsign` / `user.signingkey` / `gpg.format`) on clone or before the first commit. - Or sign on the caller's behalf through a madtea-managed signing identity (SSH or GPG), keeping the key out of plain repo config. - Make the refusal actionable: offer to configure signing from a resolved key instead of only printing manual git commands, and gate the guard on the repo's actual signed-commit protection. ## Notes - Separate from #391 (forge base-URL resolution). - I want signed commits on that forge, so the right end state is signed commits that work through madtea, not a way to bypass them.
Author
Owner

This is a security-design question, and the three directions differ mainly in where the signing key ends up living:

  • (a) resolve a per-host signing key from the same secure backend and auto-configure the clone;
  • (b) sign on the caller's behalf through a madtea-managed signing identity, keeping the key out of repo config;
  • (c) keep signing manual, but make the refusal actionable and gate the guard on the repo's ACTUAL signed-commit protection. Today it fires per host even when the repo requires no signing at all.

My lean is (c) as the immediate fix, because it stops the guard blocking commits the forge would accept, and it moves no key material anywhere new. (a) is the longer-term convenience, and it is the option that puts a signing key into madtea's secure backend, which is a real widening of what that backend holds. I want that part settled deliberately before any of it is built.

This is a security-design question, and the three directions differ mainly in where the signing key ends up living: - (a) resolve a per-host signing key from the same secure backend and auto-configure the clone; - (b) sign on the caller's behalf through a madtea-managed signing identity, keeping the key out of repo config; - (c) keep signing manual, but make the refusal actionable and gate the guard on the repo's ACTUAL signed-commit protection. Today it fires per host even when the repo requires no signing at all. My lean is (c) as the immediate fix, because it stops the guard blocking commits the forge would accept, and it moves no key material anywhere new. (a) is the longer-term convenience, and it is the option that puts a signing key into madtea's secure backend, which is a real widening of what that backend holds. I want that part settled deliberately before any of it is built.
hexajon self-assigned this 2026-08-17 17:45:48 +00:00
Author
Owner

Decided: madtea does not custody signing keys. It stays inside git's standard model and fixes the guard.

Standard git keeps the private signing key in an agent, the OS store, or hardware, and points git config at it (user.signingkey, gpg.format); the forge verifies the uploaded public key. Since git 2.34, SSH signing with an existing key is the common default. madtea holds the token because it must present that bearer secret on every request; it does not need to hold a signing key, because git already delegates signing.

So option (c), extended:

  1. Gate the signing guard on the repo's ACTUAL require_signed_commits, not per host. Today it fires for every clone on the host, even for a repo whose branch_protections is empty and that accepts unsigned commits. That over-broad guard is the real bug; fix it first.
  2. Make the refusal actionable, and make signing travel without new key material: detect the operator's existing signing key (global git config user.signingkey, an ssh-agent key, or a gpg key) and offer to set the clone's gpg.format/user.signingkey/commit.gpgsign to it.
  3. madtea never stores a private signing key. Option (a) (a key in the secure backend) and option (b) (a madtea-managed signer) are both rejected: no standard tool custodies the signing key, and both widen the ADR 0013/0025 backend contract and add attack surface for a job the OS, agent, and hardware already own.

End state: signed commits that work through madtea, using the operator's own key held where git expects it. This unblocks #425 items 2-3.

Decided: madtea does not custody signing keys. It stays inside git's standard model and fixes the guard. Standard git keeps the private signing key in an agent, the OS store, or hardware, and points git config at it (`user.signingkey`, `gpg.format`); the forge verifies the uploaded public key. Since git 2.34, SSH signing with an existing key is the common default. madtea holds the token because it must present that bearer secret on every request; it does not need to hold a signing key, because git already delegates signing. So option (c), extended: 1. Gate the signing guard on the repo's ACTUAL require_signed_commits, not per host. Today it fires for every clone on the host, even for a repo whose branch_protections is empty and that accepts unsigned commits. That over-broad guard is the real bug; fix it first. 2. Make the refusal actionable, and make signing travel without new key material: detect the operator's existing signing key (global git config `user.signingkey`, an ssh-agent key, or a gpg key) and offer to set the clone's `gpg.format`/`user.signingkey`/`commit.gpgsign` to it. 3. madtea never stores a private signing key. Option (a) (a key in the secure backend) and option (b) (a madtea-managed signer) are both rejected: no standard tool custodies the signing key, and both widen the ADR 0013/0025 backend contract and add attack surface for a job the OS, agent, and hardware already own. End state: signed commits that work through madtea, using the operator's own key held where git expects it. This unblocks #425 items 2-3.
Author
Owner

Recorded in the design of record: ADR 0013 amendment (2026-09-08), merged in PR #471. This issue stays open for the guard rework and the configure-from-existing-key implementation.

Recorded in the design of record: ADR 0013 amendment (2026-09-08), merged in PR #471. This issue stays open for the guard rework and the configure-from-existing-key implementation.
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
sixfold-space/madtea#424
No description provided.