The stuff this cycle made, archived 2026-10-19 and rendered from
git show rev65-attempt7-iterations10of10:stuff/….
first-parent-fold-run.mdfold-run-side-key-self-revoke-unknown.mdssh-unknown-signer-format.md
The fold from rule 2, run against three throwaway repos. Every case uses one allowedSignersFile holding the union of every key any case adds, so git verifies every signature (%G? is G on every commit below) and the fold, not git, decides trust. The root is pinned by its signer: the root commit's %GK is the only live key at the start.
Result: the fold rejects a [key] signed by the key it adds (exit 1) and a side-branch commit signed by a key revoked on first-parent before the merge (exit 1). The control, the same side-branch commit merged before the revoke, is accepted (exit 0). Script exit 0: all three cases gave the expected code.
What the run settles:
%GK prints the same SHA256:... string as ssh-keygen -lf field 2, so a [key] add <fingerprint> subject compares directly against %GK.fc0ad1f (case 1) and c4c4097 (case 3) are both signed by k1 while k1 was live on their own branch; only the merge order differs.signer not live.
Not tested here: a [key] commit on a side branch (the fold ignores it, since only first-parent [key] commits change the live set), a key signing its own revoke, and an untrusted signer (%G? other than G), which the fold rejects before any key logic.
Run: bash fold-test.sh on macOS, git 2.51.0, OpenSSH ed25519 keys, outside any repo. The global hook template printed `.pre-commit-config.yaml config file not found` once per commit; those lines are removed from the output.
#!/usr/bin/env bash
# First-parent fold over [key] add/revoke commits, on throwaway repos.
# Exit 0 when every case gives the expected fold exit code.
set -u
W=$(mktemp -d)
cd "$W" || exit 2
for k in k0 k1 k2; do ssh-keygen -q -t ed25519 -N '' -C "$k" -f "$k"; done
fp() { ssh-keygen -lf "$W/$1.pub" | awk '{print $2}'; }
# Union of every key any case adds: git can verify every signature, trust is the fold's job.
for k in k0 k1 k2; do printf 't@t %s\n' "$(cat "$k.pub")"; done > allowed
newrepo() {
git init -q -b main "$W/$1"; cd "$W/$1" || exit 2
git config user.name t; git config user.email t@t
git config gpg.format ssh; git config commit.gpgsign true
git config gpg.ssh.allowedSignersFile "$W/allowed"
}
c() { local key=$1; shift; git -c user.signingkey="$W/$key" commit -q --allow-empty -m "$*"; }
m() { git -c user.signingkey="$W/$1" merge -q --no-ff -m "merge $2" "$2"; }
signer() { git log -1 --format=%GK "$1"; }
subj() { git log -1 --format=%s "$1"; }
fold() {
local root live c s k a r
root=$(git rev-list --max-parents=0 HEAD)
live=" $(signer "$root") " # root pinned: its signer is the only live key
for c in $(git rev-list --first-parent --reverse "$root..HEAD"); do
if git rev-parse -q --verify "$c^2" >/dev/null; then
# commits a merge brings in are checked against the live set at that merge
for s in $(git rev-list "$c^1..$c^2"); do
k=$(signer "$s")
[ "$(git log -1 --format=%G? "$s")" = G ] || { echo "REJECT ${s:0:7}: %G? not G"; return 1; }
case $live in *" $k "*) ;; *) echo "REJECT ${s:0:7} '$(subj "$s")': side-branch signer $k not live at merge ${c:0:7}"; return 1 ;; esac
done
fi
k=$(signer "$c")
[ "$(git log -1 --format=%G? "$c")" = G ] || { echo "REJECT ${c:0:7}: %G? not G"; return 1; }
case $(subj "$c") in "[key] add "*)
a=$(subj "$c"); a=${a#"[key] add "}
[ "$a" = "$k" ] && { echo "REJECT ${c:0:7} '$(subj "$c")': signed by the key it adds"; return 1; } ;;
esac
case $live in *" $k "*) ;; *) echo "REJECT ${c:0:7} '$(subj "$c")': signer $k not live"; return 1 ;; esac
case $(subj "$c") in
"[key] add "*) a=$(subj "$c"); live="$live${a#"[key] add "} " ;;
"[key] revoke "*) r=$(subj "$c"); r=${r#"[key] revoke "}; live=${live/" $r "/ } ;;
esac
done
echo "ACCEPT"
}
case_merge_then_revoke() { # control: k1 work merged while k1 live, revoked afterwards
c k0 root; c k0 "[key] add $(fp k1)"
git checkout -q -b side; c k1 "side work"; git checkout -q main
m k0 side; c k0 "[key] revoke $(fp k1)"
}
case_self_add() { # k2 signs the [key] that adds k2
c k0 root; c k2 "[key] add $(fp k2)"
}
case_revoke_then_merge() { # k1 work merged after k1 revoked on first-parent
c k0 root; c k0 "[key] add $(fp k1)"
git checkout -q -b side; c k1 "side work"; git checkout -q main
c k0 "[key] revoke $(fp k1)"; m k0 side
}
git --version
echo "k0=$(fp k0)"; echo "k1=$(fp k1)"; echo "k2=$(fp k2)"
fail=0
for t in merge_then_revoke:0 self_add:1 revoke_then_merge:1; do
name=${t%:*}; want=${t#*:}
echo; echo "== $name (want exit $want)"
( newrepo "$name"; "case_$name"
git log --graph --format='%h %G? %GK %s' | cat
fold )
got=$?
echo "fold exit $got"
[ "$got" = "$want" ] || fail=1
done
echo; echo "script exit $fail"
exit "$fail"
git version 2.51.0
k0=SHA256:VeWUxK7TUIUl42FbPawvpZajm+Ggkb0ZzWJMvr6lzYA
k1=SHA256:Epm9xBW6HtupKjA/WPuQVWTI7iCTQklBG9ym6K3EPzM
k2=SHA256:e/07vFSWTI9z3HcAP+tdGSibU/OLjMgafdENqZCxTdo
== merge_then_revoke (want exit 0)
* e05e66b G SHA256:VeWUxK7TUIUl42FbPawvpZajm+Ggkb0ZzWJMvr6lzYA [key] revoke SHA256:Epm9xBW6HtupKjA/WPuQVWTI7iCTQklBG9ym6K3EPzM
* 7eb2148 G SHA256:VeWUxK7TUIUl42FbPawvpZajm+Ggkb0ZzWJMvr6lzYA merge side
|\
| * fc0ad1f G SHA256:Epm9xBW6HtupKjA/WPuQVWTI7iCTQklBG9ym6K3EPzM side work
|/
* c714f23 G SHA256:VeWUxK7TUIUl42FbPawvpZajm+Ggkb0ZzWJMvr6lzYA [key] add SHA256:Epm9xBW6HtupKjA/WPuQVWTI7iCTQklBG9ym6K3EPzM
* 7a83193 G SHA256:VeWUxK7TUIUl42FbPawvpZajm+Ggkb0ZzWJMvr6lzYA root
ACCEPT
fold exit 0
== self_add (want exit 1)
* 03f9485 G SHA256:e/07vFSWTI9z3HcAP+tdGSibU/OLjMgafdENqZCxTdo [key] add SHA256:e/07vFSWTI9z3HcAP+tdGSibU/OLjMgafdENqZCxTdo
* 3e409f1 G SHA256:VeWUxK7TUIUl42FbPawvpZajm+Ggkb0ZzWJMvr6lzYA root
REJECT 03f9485 '[key] add SHA256:e/07vFSWTI9z3HcAP+tdGSibU/OLjMgafdENqZCxTdo': signed by the key it adds
fold exit 1
== revoke_then_merge (want exit 1)
* dac9998 G SHA256:VeWUxK7TUIUl42FbPawvpZajm+Ggkb0ZzWJMvr6lzYA merge side
|\
| * c4c4097 G SHA256:Epm9xBW6HtupKjA/WPuQVWTI7iCTQklBG9ym6K3EPzM side work
* | 9e08604 G SHA256:VeWUxK7TUIUl42FbPawvpZajm+Ggkb0ZzWJMvr6lzYA [key] revoke SHA256:Epm9xBW6HtupKjA/WPuQVWTI7iCTQklBG9ym6K3EPzM
|/
* 00a74d7 G SHA256:VeWUxK7TUIUl42FbPawvpZajm+Ggkb0ZzWJMvr6lzYA [key] add SHA256:Epm9xBW6HtupKjA/WPuQVWTI7iCTQklBG9ym6K3EPzM
* bd92e62 G SHA256:VeWUxK7TUIUl42FbPawvpZajm+Ggkb0ZzWJMvr6lzYA root
REJECT c4c4097 'side work': side-branch signer SHA256:Epm9xBW6HtupKjA/WPuQVWTI7iCTQklBG9ym6K3EPzM not live at merge dac9998
fold exit 1
script exit 0
The three cases first-parent-fold-run.md left untested, run with its fold copied verbatim. k0..k2 are in gpg.ssh.allowedSignersFile; k3 is not. Every case is expected to end in a reject (exit 1). Script exit 0: all three did.
Results:
[key]: k1 (live) signs [key] add k2 on a side branch, k0 merges it, k2 then signs on first-parent. The merge is accepted, because k1 was live at the merge, but the add does not take effect: the k2 commit is rejected as signer not live. Only first-parent [key] commits change the live set, so a key can only be added by a commit on first-parent.[key] revoke k1. The fold accepts it (nothing in rule 2 forbids it) and the revoke takes effect: k0 work after it is accepted, the next k1 commit is rejected as signer not live. A key may retire itself; if it is the last live key, nothing can sign after it. Whether that should be allowed is open; the fold does not check it.%G? prints U, and %GF and %GK both still print k3's SHA256: fingerprint. The fold rejects the side commit on %G? not G before any key logic. It would also be rejected without that check, since k3 was never added.
Proposal, not tested here: %GK names the signer whether or not git trusts the key, so the fold could check the live set against %GK alone and accept %G? of G or U (git's "good signature, unknown validity"). The allowed signers file would then not need the union of every key. A run with an empty allowed signers file would test it, and should also check that a bad signature (B) still prints a %GK the fold must not trust.
Run: bash fold-test-2.sh on macOS, git 2.51.0, OpenSSH ed25519 keys, outside any repo. The global hook template's `.pre-commit-config.yaml config file not found lines and trailing spaces after the graph's |\ and |/` are removed from the output.
#!/usr/bin/env bash
# The three cases first-parent-fold-run.md left untested, same fold.
# Exit 0 when every case gives the expected fold exit code.
set -u
W=$(mktemp -d)
cd "$W" || exit 2
for k in k0 k1 k2 k3; do ssh-keygen -q -t ed25519 -N '' -C "$k" -f "$k"; done
fp() { ssh-keygen -lf "$W/$1.pub" | awk '{print $2}'; }
# Union of k0..k2 only: k3 is the signer git does not know.
for k in k0 k1 k2; do printf 't@t %s\n' "$(cat "$k.pub")"; done > allowed
newrepo() {
git init -q -b main "$W/$1"; cd "$W/$1" || exit 2
git config user.name t; git config user.email t@t
git config gpg.format ssh; git config commit.gpgsign true
git config gpg.ssh.allowedSignersFile "$W/allowed"
}
c() { local key=$1; shift; git -c user.signingkey="$W/$key" commit -q --allow-empty -m "$*"; }
m() { git -c user.signingkey="$W/$1" merge -q --no-ff -m "merge $2" "$2"; }
signer() { git log -1 --format=%GK "$1"; }
subj() { git log -1 --format=%s "$1"; }
fold() { # verbatim from first-parent-fold-run.md
local root live c s k a r
root=$(git rev-list --max-parents=0 HEAD)
live=" $(signer "$root") "
for c in $(git rev-list --first-parent --reverse "$root..HEAD"); do
if git rev-parse -q --verify "$c^2" >/dev/null; then
for s in $(git rev-list "$c^1..$c^2"); do
k=$(signer "$s")
[ "$(git log -1 --format=%G? "$s")" = G ] || { echo "REJECT ${s:0:7}: %G? not G"; return 1; }
case $live in *" $k "*) ;; *) echo "REJECT ${s:0:7} '$(subj "$s")': side-branch signer $k not live at merge ${c:0:7}"; return 1 ;; esac
done
fi
k=$(signer "$c")
[ "$(git log -1 --format=%G? "$c")" = G ] || { echo "REJECT ${c:0:7}: %G? not G"; return 1; }
case $(subj "$c") in "[key] add "*)
a=$(subj "$c"); a=${a#"[key] add "}
[ "$a" = "$k" ] && { echo "REJECT ${c:0:7} '$(subj "$c")': signed by the key it adds"; return 1; } ;;
esac
case $live in *" $k "*) ;; *) echo "REJECT ${c:0:7} '$(subj "$c")': signer $k not live"; return 1 ;; esac
case $(subj "$c") in
"[key] add "*) a=$(subj "$c"); live="$live${a#"[key] add "} " ;;
"[key] revoke "*) r=$(subj "$c"); r=${r#"[key] revoke "}; live=${live/" $r "/ } ;;
esac
done
echo "ACCEPT"
}
case_side_key_add() { # [key] add k2 on a side branch, merged; k2 then signs on first-parent
c k0 root; c k0 "[key] add $(fp k1)"
git checkout -q -b side; c k1 "[key] add $(fp k2)"; git checkout -q main
m k0 side; c k2 "k2 work"
}
case_self_revoke() { # k1 signs the revoke of k1; k1 then signs again
c k0 root; c k0 "[key] add $(fp k1)"
c k1 "[key] revoke $(fp k1)"; c k0 "k0 work"; c k1 "k1 after self-revoke"
}
case_unknown_signer() { # k3 (absent from allowedSignersFile) signs on a side branch
c k0 root
git checkout -q -b side; c k3 "k3 work"; git checkout -q main
m k0 side
}
git --version
for k in k0 k1 k2 k3; do echo "$k=$(fp "$k")"; done
fail=0
for t in side_key_add:1 self_revoke:1 unknown_signer:1; do
name=${t%:*}; want=${t#*:}
echo; echo "== $name (want exit $want)"
( newrepo "$name"; "case_$name"
git log --graph --format='%h %G? [%GF] [%GK] %s' | cat
fold )
got=$?
echo "fold exit $got"
[ "$got" = "$want" ] || fail=1
done
echo; echo "script exit $fail"
exit "$fail"
git version 2.51.0
k0=SHA256:ke1dcnUZY8vetEygSFW+IXt8K2Ifi/Fbr7Za89Jnv1k
k1=SHA256:zU8RitIpFcoINuflLNxAXLZS5KcVexWtIFY8cSuCdwI
k2=SHA256:h6fwQ+g633TbpNjvRdUff7wKPOa/F/DvhJ1V0ILMqcc
k3=SHA256:uA1I33MiHhCQxH+p1zR+Qyx/JjkOfJzh8uAAbxt6kCs
== side_key_add (want exit 1)
* 4941c28 G [SHA256:h6fwQ+g633TbpNjvRdUff7wKPOa/F/DvhJ1V0ILMqcc] [SHA256:h6fwQ+g633TbpNjvRdUff7wKPOa/F/DvhJ1V0ILMqcc] k2 work
* 3bb1c55 G [SHA256:ke1dcnUZY8vetEygSFW+IXt8K2Ifi/Fbr7Za89Jnv1k] [SHA256:ke1dcnUZY8vetEygSFW+IXt8K2Ifi/Fbr7Za89Jnv1k] merge side
|\
| * b488874 G [SHA256:zU8RitIpFcoINuflLNxAXLZS5KcVexWtIFY8cSuCdwI] [SHA256:zU8RitIpFcoINuflLNxAXLZS5KcVexWtIFY8cSuCdwI] [key] add SHA256:h6fwQ+g633TbpNjvRdUff7wKPOa/F/DvhJ1V0ILMqcc
|/
* a1cf101 G [SHA256:ke1dcnUZY8vetEygSFW+IXt8K2Ifi/Fbr7Za89Jnv1k] [SHA256:ke1dcnUZY8vetEygSFW+IXt8K2Ifi/Fbr7Za89Jnv1k] [key] add SHA256:zU8RitIpFcoINuflLNxAXLZS5KcVexWtIFY8cSuCdwI
* 9e307c4 G [SHA256:ke1dcnUZY8vetEygSFW+IXt8K2Ifi/Fbr7Za89Jnv1k] [SHA256:ke1dcnUZY8vetEygSFW+IXt8K2Ifi/Fbr7Za89Jnv1k] root
REJECT 4941c28 'k2 work': signer SHA256:h6fwQ+g633TbpNjvRdUff7wKPOa/F/DvhJ1V0ILMqcc not live
fold exit 1
== self_revoke (want exit 1)
* 83bc04e G [SHA256:zU8RitIpFcoINuflLNxAXLZS5KcVexWtIFY8cSuCdwI] [SHA256:zU8RitIpFcoINuflLNxAXLZS5KcVexWtIFY8cSuCdwI] k1 after self-revoke
* 892f51a G [SHA256:ke1dcnUZY8vetEygSFW+IXt8K2Ifi/Fbr7Za89Jnv1k] [SHA256:ke1dcnUZY8vetEygSFW+IXt8K2Ifi/Fbr7Za89Jnv1k] k0 work
* e124be2 G [SHA256:zU8RitIpFcoINuflLNxAXLZS5KcVexWtIFY8cSuCdwI] [SHA256:zU8RitIpFcoINuflLNxAXLZS5KcVexWtIFY8cSuCdwI] [key] revoke SHA256:zU8RitIpFcoINuflLNxAXLZS5KcVexWtIFY8cSuCdwI
* 269c3bf G [SHA256:ke1dcnUZY8vetEygSFW+IXt8K2Ifi/Fbr7Za89Jnv1k] [SHA256:ke1dcnUZY8vetEygSFW+IXt8K2Ifi/Fbr7Za89Jnv1k] [key] add SHA256:zU8RitIpFcoINuflLNxAXLZS5KcVexWtIFY8cSuCdwI
* 2cadca1 G [SHA256:ke1dcnUZY8vetEygSFW+IXt8K2Ifi/Fbr7Za89Jnv1k] [SHA256:ke1dcnUZY8vetEygSFW+IXt8K2Ifi/Fbr7Za89Jnv1k] root
REJECT 83bc04e 'k1 after self-revoke': signer SHA256:zU8RitIpFcoINuflLNxAXLZS5KcVexWtIFY8cSuCdwI not live
fold exit 1
== unknown_signer (want exit 1)
* 3c96966 G [SHA256:ke1dcnUZY8vetEygSFW+IXt8K2Ifi/Fbr7Za89Jnv1k] [SHA256:ke1dcnUZY8vetEygSFW+IXt8K2Ifi/Fbr7Za89Jnv1k] merge side
|\
| * ada1887 U [SHA256:uA1I33MiHhCQxH+p1zR+Qyx/JjkOfJzh8uAAbxt6kCs] [SHA256:uA1I33MiHhCQxH+p1zR+Qyx/JjkOfJzh8uAAbxt6kCs] k3 work
|/
* 79dae9c G [SHA256:ke1dcnUZY8vetEygSFW+IXt8K2Ifi/Fbr7Za89Jnv1k] [SHA256:ke1dcnUZY8vetEygSFW+IXt8K2Ifi/Fbr7Za89Jnv1k] root
REJECT ada1887: %G? not G
fold exit 1
script exit 0
Answers the first half of the cycle 43 direction. Rule 1 applies: the run is quoted below with git version and exact command. The fold half (rule 2) is not tested here.
| commit signed by | allowedSignersFile | %G? | %GS | %GF | %GK | %GT |
|---|---|---|---|---|---|---|
| key in the file | set | G | t@example.com | its SHA256 fp | same fp | fully |
| key not in the file | set | U | empty | its SHA256 fp | same fp | undefined |
| key in the file | unset | U | empty | its SHA256 fp | same fp | undefined |
| unsigned | either | N | empty | empty | empty | undefined |
%GF and %GK do name the signer of a key that is not allowed. Cycle 42's unverified assumption holds on git 2.51.0 with OpenSSH 10.5p1.%GF and %GK are the same string (SHA256:..., as ssh-keygen -lf prints it). %GP is always empty.%G? is U with no allowedSignersFile at all, even for the "good" key. So a fold can leave gpg.ssh.allowedSignersFile unset, accept %G? in {G,U} and decide membership itself by comparing %GF against its own key set. It must not treat U as a rejection.git verify-commit on the stranger commit prints Good "git" signature with ED25519 key SHA256:+rom... then No principal matched. and exits 1 (the script stops there under set -e). A fold that shells out to verify-commit would reject every key it learned from history and not from the file.
Not tested: what %G? prints for a tampered or truncated SSH signature (expected B; a fold must reject it), and whether %GF is still filled then.
/tmp/zgk/run.sh > /tmp/zgk/out.txt 2>&1 on macOS, host git (the shim passes through outside the worktree). Script exit 1, from verify-commit.
#!/usr/bin/env bash
set -eu
d=$(mktemp -d)
cd "$d"
git --version
ssh -V 2>&1
ssh-keygen -q -t ed25519 -N '' -C allowed -f "$d/k_allowed"
ssh-keygen -q -t ed25519 -N '' -C stranger -f "$d/k_stranger"
echo "fp allowed: $(ssh-keygen -lf "$d/k_allowed.pub" | cut -d' ' -f2)"
echo "fp stranger: $(ssh-keygen -lf "$d/k_stranger.pub" | cut -d' ' -f2)"
echo "t@example.com $(cat "$d/k_allowed.pub")" > "$d/allowed_signers"
git init -q repo && cd repo
git config user.name t
git config user.email t@example.com
git config gpg.format ssh
git config user.signingkey "$d/k_allowed"
git commit -q --allow-empty -S -m allowed
git -c user.signingkey="$d/k_stranger" commit -q --allow-empty -S -m stranger
git commit -q --allow-empty --no-gpg-sign -m unsigned
fmt='%s|G?=%G?|GS=%GS|GF=%GF|GP=%GP|GK=%GK|GT=%GT'
echo "--- with gpg.ssh.allowedSignersFile set"
git -c gpg.ssh.allowedSignersFile="$d/allowed_signers" log --format="$fmt"
echo "--- with gpg.ssh.allowedSignersFile unset"
git log --format="$fmt"
echo "--- verify-commit HEAD~1 (stranger), allowedSignersFile set"
git -c gpg.ssh.allowedSignersFile="$d/allowed_signers" verify-commit HEAD~1; echo "exit=$?"
Output (six ".pre-commit-config.yaml not found" lines from a global hook removed):
git version 2.51.0
OpenSSH_10.5p1, LibreSSL 3.3.6
fp allowed: SHA256:BZ2d2WAL2sJS04DD//5S3UJ/5tFPtxMeT8UvKFKlZVk
fp stranger: SHA256:+rom06xbywsYwPAWXjZhwj8sWcn4KBfjj/sEDKfSO3Y
--- with gpg.ssh.allowedSignersFile set
unsigned|G?=N|GS=|GF=|GP=|GK=|GT=undefined
stranger|G?=U|GS=|GF=SHA256:+rom06xbywsYwPAWXjZhwj8sWcn4KBfjj/sEDKfSO3Y|GP=|GK=SHA256:+rom06xbywsYwPAWXjZhwj8sWcn4KBfjj/sEDKfSO3Y|GT=undefined
allowed|G?=G|GS=t@example.com|GF=SHA256:BZ2d2WAL2sJS04DD//5S3UJ/5tFPtxMeT8UvKFKlZVk|GP=|GK=SHA256:BZ2d2WAL2sJS04DD//5S3UJ/5tFPtxMeT8UvKFKlZVk|GT=fully
--- with gpg.ssh.allowedSignersFile unset
unsigned|G?=N|GS=|GF=|GP=|GK=|GT=undefined
stranger|G?=U|GS=|GF=SHA256:+rom06xbywsYwPAWXjZhwj8sWcn4KBfjj/sEDKfSO3Y|GP=|GK=SHA256:+rom06xbywsYwPAWXjZhwj8sWcn4KBfjj/sEDKfSO3Y|GT=undefined
allowed|G?=U|GS=|GF=SHA256:BZ2d2WAL2sJS04DD//5S3UJ/5tFPtxMeT8UvKFKlZVk|GP=|GK=SHA256:BZ2d2WAL2sJS04DD//5S3UJ/5tFPtxMeT8UvKFKlZVk|GT=undefined
--- verify-commit HEAD~1 (stranger), allowedSignersFile set
Good "git" signature with ED25519 key SHA256:+rom06xbywsYwPAWXjZhwj8sWcn4KBfjj/sEDKfSO3Y
No principal matched.