Any registered key could sign a release's SHA256SUMS, so a release pushed by the owner could carry the word of a user with no say over the repo. pre-receive now wants the signer's principal to be the repo's owner or listed in forge/collaborators, as for pushing the ref itself.
- commit
- 49243f1e974926e5980facf27975ce8859ca890b
- parent
- 3e0e6259bd67e28e82c05677fe81384015ff01d4
- author
- cat <admin@dev.site>
- kind
- fix risk score 2.2 of 3, Jev's estimate
| M | README.md | 7 | +++++-- |
| M | bin/ | 6 | ++++-- |
| M | guide/ | 2 | +- |
| M | lib/ | 5 | +++-- |
| M | test/ | 17 | +++++++++++------ |
@@ -384,7 +384,8 @@ tag, and only if:
384384 git's k, m and g work);385385 - `SHA256SUMS` lists every file once with its sha256, and nothing else;386386 - `SHA256SUMS.sig` is a `forge-release` signature of it for this repo and387- the ref's tag, by any key in the forge's keys file;387+ the ref's tag, by the key of the repo's owner or a collaborator: another388+ registered user's won't do, even pushed by the owner;388389 - with `git config forge.releaserequireci true`, the tag's tree has a389390 verified passing attestation, as `forge.requireci` asks of `main`390391 (`forge attest v1.0` before publishing).@@ -423,7 +424,9 @@ forge release verify --repo dami/notes --tag v1.0 tool
423424 424425 A verified download proves what a verified attestation does, about these425426 bytes and the release they name: the holder of a key the forge knows426-vouched for this `SHA256SUMS` as that repo's tag.427+vouched for this `SHA256SUMS` as that repo's tag. The forge took it only428+from an owner's or collaborator's key; `verify` checks the key against the429+forge's keys file, and leaves who administers the repo to that.427430 428431 ### Search and highlighting429432 @@ -1822,8 +1822,8 @@ scan_secrets() {
18221822 # trailers name the tag and its commit (as this push leaves it), whose tree18231823 # is regular files only, none over forge.releasemaxsize, each listed in18241824 # SHA256SUMS with its sha256, and SHA256SUMS.sig a forge-release signature1825-# of SHA256SUMS for this repo and tag (release_message) by a key of this1826-# forge. With forge.releaserequireci true,1825+# of SHA256SUMS for this repo and tag (release_message) by the key of the1826+# repo's owner or a collaborator. With forge.releaserequireci true,18271827 # the tag's tree needs a verified passing CI attestation too. Releases skip18281828 # the secret check, which reads branches and tags only. Uses $updates,18291829 # $notes_new and the scratch directory $rt.@@ -1874,6 +1874,8 @@ check_release() {
18741874 release_message "$OWNER/$NAME" "$2" "$rt/sums" "$rt/message"18751875 rsigner=$(verify_block "$rt/message" "$rt/sums.sig" "$FORGE_STATE/allowed_signers" "$RNS") ||18761876 deny "$rn: SHA256SUMS.sig is not a good $RNS signature of SHA256SUMS for $OWNER/$NAME $2 by a key of this forge"1877+ [ "$rsigner" = "$OWNER" ] || grep -qxF -- "$rsigner" "$GIT_DIR/forge/collaborators" 2>/dev/null ||1878+ deny "$rn: SHA256SUMS.sig is $rsigner's, and only $OWNER and collaborators of $OWNER/$NAME sign its releases"18771879 [ "$(git config --bool forge.releaserequireci || echo false)" = true ] || return 018781880 notes=${notes_new:-refs/notes/ci}18791881 [ "$notes" != "$ZERO" ] || notes=refs/notes/ci@@ -215,7 +215,7 @@ log sha256:6cc79019... 4 lines
215215 <p>The pages server serves them the way Cloudflare Pages does, so a site built for there works unchanged: <code>_headers</code> and <code>_redirects</code> apply, <code>/about</code> finds <code>about.html</code> or <code>about/index.html</code>, a missing page gets the nearest <code>404.html</code> (or <code>index.html</code>, for a single-page app), and previews are sent with <code>noindex</code>. Old <code>/sites/...</code> paths redirect to the hostname. Without a pages domain, sites are plain files under <code>/sites/<owner>/<repo>/<name>/</code> on the forge itself.</p>216216 <p>Deleting a branch deletes its previews: the ref, the served link and the hostname, so the sites and review pages stop listing them. Previews of merged branches stay until their branch is deleted. The extracted files stay in the store until <code>forge gc</code>, which removes store trees nothing links to, hostnames whose deploy is gone and leftover previews, skipping store entries changed in the last ten minutes, so it can run while people push.</p>217217 <h3>Releases are the same idea</h3>218-<p><code>forge release publish v1.0 <file>...</code> commits a tag's files to <code>refs/releases/v1.0</code> with a <code>SHA256SUMS</code> signed by your key for that repo and tag (<code>ssh-keygen -n forge-release</code>), so it can't pass for another release. <code>pre-receive</code> checks every sum, the signature, the tag and a size cap, and with <code>forge.releaserequireci</code> a verified pass for the tag's tree. <code>post-receive</code> extracts the tree into the store and hard-links the files into <code>/dami/greet/releases/download/v1.0/</code>, plus <code>latest/</code>; hard links, so the mirror copies them. Clones and fetches never see <code>refs/releases</code>. A private repo's releases aren't served; its readers get them with <code>ssh git@host release get</code>.</p>218+<p><code>forge release publish v1.0 <file>...</code> commits a tag's files to <code>refs/releases/v1.0</code> with a <code>SHA256SUMS</code> signed by your key for that repo and tag (<code>ssh-keygen -n forge-release</code>), so it can't pass for another release. <code>pre-receive</code> checks every sum, the signature (an owner's or collaborator's key), the tag and a size cap, and with <code>forge.releaserequireci</code> a verified pass for the tag's tree. <code>post-receive</code> extracts the tree into the store and hard-links the files into <code>/dami/greet/releases/download/v1.0/</code>, plus <code>latest/</code>; hard links, so the mirror copies them. Clones and fetches never see <code>refs/releases</code>. A private repo's releases aren't served; its readers get them with <code>ssh git@host release get</code>.</p>219219 </div>220220 </section>221221 @@ -1,8 +1,9 @@
11 // Release files, for the releases page. A release is a commit on22 // refs/releases/<tag> whose tree is its files plus SHA256SUMS and33 // SHA256SUMS.sig, an ssh-keygen signature (namespace forge-release) of4-// SHA256SUMS behind a header naming the repo and tag (releaseMessage)5-// (forge release publish; pre-receive checks it all). The hooks and the build hard-link the files under4+// SHA256SUMS behind a header naming the repo and tag (releaseMessage), by5+// the key of the repo's owner or a collaborator (forge release publish;6+// pre-receive checks it all). The hooks and the build hard-link the files under67 // /<owner>/<repo>/releases/download/<tag>/; this module reads what the page78 // shows about them: each file's size and sha256, who signed SHA256SUMS, and89 // where the release came from.@@ -1,7 +1,8 @@
11 # SHA256SUMS.sig must be a forge-release signature of SHA256SUMS behind a2-# header naming this repo and tag, by a key of this forge: none, a3-# stranger's, one for CI results, one of SHA256SUMS alone, or one of other4-# text is refused. allowed_signers lets every key sign releases.2+# header naming this repo and tag, by the key of the repo's owner or a3+# collaborator: none, a stranger's, a registered non-admin's (even pushed4+# by the owner), one for CI results, one of SHA256SUMS alone, or one of5+# other text is refused. allowed_signers lets every key sign releases.56 . "$(dirname "$0")/../lib.sh"67 user dami78 user ana@@ -39,9 +40,13 @@ ssh-keygen -q -Y sign -n forge-release -f "$T/keys/dami" "$T/sig-other" 2>/dev/n
3940 cp "$T/sig-other.sig" "$T/f/SHA256SUMS.sig"4041 fails_with "$bad" push4142 42-missing "$FORGE_WWW/dami/greet/releases"43-44-# Any registered key will do, not only the pusher's.43+# ana is registered but not an admin of dami/greet: her signature won't44+# do, even when dami pushes it.4545 files "$T/keys/ana"46+fails_with "release v1: SHA256SUMS.sig is ana's, and only dami and collaborators of dami/greet sign its releases" push47+missing "$FORGE_WWW/dami/greet/releases"48+# Once she is a collaborator, it does.49+mkdir -p "$FORGE_ROOT/dami/greet.git/forge"50+printf 'ana\n' >"$FORGE_ROOT/dami/greet.git/forge/collaborators"4651 ok push4752 exists "$FORGE_WWW/dami/greet/releases/download/v1/greet"