fix(#3514): deny internal fetch hosts; disclose unverified integrity (#3516)

* test(#3514): add failing-first denylist and integrity suites

* fix(#3514): deny internal fetch hosts; disclose unverified integrity

* docs(#3514): trust-model, glossary, and changeset entries

* fix(#3514): scope v6 checks to literals; exact pin kinds in prompt

* chore(#3514): backfill changeset pr number

---------

Co-authored-by: sim <sim@local>
This commit is contained in:
Tom Boucher
2026-08-14 21:34:29 -04:00
committed by GitHub
parent 268ca7e32d
commit 6badb839a0
18 changed files with 842 additions and 602 deletions

View File

@@ -288,6 +288,37 @@ An `integrity` field in `capability.json` carries a `sha512-<base64>` digest
of the capability bundle. When present, GSD verifies this digest before
extracting any files. A mismatch aborts the install.
When NO pin is supplied, the consent prompt says so plainly: a
`content: NO PINNED HASH — staged unverified` line distinguishes an install
whose bytes were verified against a commitment from one that was not
([#3514](https://github.com/open-gsd/gsd-core/issues/3514)). A computed
sha512 of what was actually fetched is still recorded in the ledger at
install, so a later `trust` inspection shows exactly which bytes landed.
Prompt claims are exact per kind: a sha512 `--integrity` pin renders as
*supplied and verified*, a git source checked out at a `#sha:<commit>` ref
renders as *pinned to a git commit* (never as a sha512 pin — none was
supplied), and a mutable `#sha:<branch>` ref is not a pin at all.
### Fetch-host denylist
The URL importer's fetch transport refuses, before any bytes leave
([#3514](https://github.com/open-gsd/gsd-core/issues/3514)):
- **loopback, link-local, and unspecified hosts** — `127.0.0.0/8`,
`169.254.0.0/16` (which contains the cloud metadata addresses), `0.0.0.0/8`,
`::1`, `fe80::/10`, `::`, their IPv4-mapped IPv6 spellings, and
`localhost`/`*.localhost` names. No legitimate capability install fetches
these.
- **plaintext `http://` URLs** — the transport is `https`-only; an `http://`
tarball spec still *classifies* (so an internal-mirror workflow fails with a
clear, named reason instead of a raw protocol error) but never fetches.
Deliberate limits: RFC1918 private ranges (`10/8`, `172.16/12`,
`192.168/16`) are **not** denied — an internal https mirror is a legitimate
install source, and the denylist is not an allowlist. The check is on the
URL's host literal; a public hostname that *resolves* via DNS to a denied
range (rebinding) is out of scope.
What integrity pinning defends against: a capability hosted at a URL or in a
registry that is later replaced with a different bundle (whether by an attacker
who has compromised the hosting, or by an author publishing a silent breaking