diff --git a/CHANGELOG.md b/CHANGELOG.md index 6eb3b8a..8da75ca 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -48,9 +48,10 @@ and a batch of table and layout fixes. built from that source": `gh attestation verify BeanNetworkTester-v0.5.0-windows-x64.zip -R donislawdev/BeanNetworkTester`. A checksum proves the file matches the release page; this proves the page came out of this - repository's workflow, from a specific commit. It covers the SBOM too. The proof now ships - **as a file** as well, `BeanNetworkTester-vX.Y.Z.sigstore.json`, so `--bundle` checks the - archive from a mirror, or with no network at all. + repository's workflow, from a specific commit. The proof also ships **as a file**, + `BeanNetworkTester-vX.Y.Z.sigstore.json`, so the archive can be checked against evidence that + travelled with it. The README gives the exact command - it needs `--repo` and + `--predicate-type` as well as `--bundle`. - **Every release now carries an SBOM, signed against the download.** A standard SPDX file listing each third-party component with its version, licence and source - the same list `--license` diff --git a/README.md b/README.md index b9cb8c6..b755b33 100644 --- a/README.md +++ b/README.md @@ -1489,15 +1489,19 @@ A checksum proves the file matches what the release page says. This proves the r was produced by this repository's own workflow, from a specific commit, on a GitHub-hosted runner. The same command also verifies the SBOM that ships beside the archive. -That command asks GitHub. The proof also ships **as a file**, `BeanNetworkTester-vX.Y.Z.sigstore.json`, -so you can check the archive without one: +That command asks GitHub which attestations exist. The proof also ships **as a file**, +`BeanNetworkTester-vX.Y.Z.sigstore.json`, so you can check the archive against evidence that +travelled with it: ```bash -gh attestation verify BeanNetworkTester-v0.5.0-windows-x64.zip --bundle BeanNetworkTester-v0.5.0.sigstore.json +gh attestation verify BeanNetworkTester-v0.5.0-windows-x64.zip --bundle BeanNetworkTester-v0.5.0.sigstore.json --repo donislawdev/BeanNetworkTester --predicate-type https://spdx.dev/Document/v2.3 ``` -Useful if you got the files from a mirror, or from a machine that cannot reach the API - the -evidence travelled with the download instead of living somewhere you have to trust separately. +Both extra flags are needed and neither is decoration. `--repo` names the repository the +signing identity has to match. `--predicate-type` is there because this bundle is the **SBOM** +attestation, and `gh` looks for a build-provenance one unless you say otherwise - without it you +get "no attestations found with predicate type", which is the tool being precise rather than +broken. When the file is good it prints nothing and exits 0. Change one byte and it exits 1. Three statements, and it is worth knowing they answer different questions. The **signature** says who stands behind the file. The **bundle above** binds this exact archive to its bill of materials, diff --git a/README.pl.md b/README.pl.md index 031da82..815d168 100644 --- a/README.pl.md +++ b/README.pl.md @@ -1352,15 +1352,19 @@ Suma kontrolna dowodzi, że plik zgadza się z tym, co mówi strona wydania. To strona wydania powstała z workflow tego repozytorium, z konkretnego commita, na maszynie GitHuba. Tym samym poleceniem sprawdzisz też SBOM, który jedzie obok archiwum. -To polecenie pyta GitHuba. Dowód jedzie też **jako plik**, `BeanNetworkTester-vX.Y.Z.sigstore.json`, -więc archiwum sprawdzisz bez pytania kogokolwiek: +To polecenie pyta GitHuba, jakie atestacje istnieją. Dowód jedzie też **jako plik**, +`BeanNetworkTester-vX.Y.Z.sigstore.json`, więc archiwum sprawdzisz wobec dowodu, który +przyjechał razem z nim: ```bash -gh attestation verify BeanNetworkTester-v0.5.0-windows-x64.zip --bundle BeanNetworkTester-v0.5.0.sigstore.json +gh attestation verify BeanNetworkTester-v0.5.0-windows-x64.zip --bundle BeanNetworkTester-v0.5.0.sigstore.json --repo donislawdev/BeanNetworkTester --predicate-type https://spdx.dev/Document/v2.3 ``` -Przydaje się, gdy pliki masz z kopii lustrzanej albo na maszynie bez dostępu do API - dowód -przyjechał razem z pobranym plikiem, zamiast leżeć w miejscu, któremu trzeba osobno ufać. +Obie dodatkowe flagi są potrzebne i żadna nie jest ozdobą. `--repo` mówi, z jakim repozytorium +ma się zgadzać tożsamość podpisująca. `--predicate-type` jest dlatego, że ten bundle to atestacja +**SBOM-u**, a `gh` domyślnie szuka atestacji prowenancji - bez tej flagi dostaniesz „no attestations +found with predicate type", czyli narzędzie jest precyzyjne, a nie zepsute. Przy dobrym pliku nic +nie wypisuje i kończy się zerem. Zmień jeden bajt, a skończy się jedynką. Trzy oświadczenia, i warto wiedzieć, że odpowiadają na różne pytania. **Podpis** mówi, kto za tym plikiem stoi. **Powyższy bundle** wiąże dokładnie to archiwum z jego listą składników i powstaje diff --git a/tests/test_cli_docs.py b/tests/test_cli_docs.py index a039c1e..fe66a78 100644 --- a/tests/test_cli_docs.py +++ b/tests/test_cli_docs.py @@ -20,7 +20,15 @@ from fakes import ROOT, check -IGNORE = {"--help", "--noconfirm", "--noconsole", "--onefile", "--uac-admin"} +# Flags the READMEs mention that belong to OTHER programs, so the parser will never +# have them. PyInstaller's are here because the build section explains what the spec +# does and does not pass; `gh`'s are here because the verification section spells out +# the exact command a user runs against a release, and that command needs three flags +# (measured on v0.5.0-rc.2 - without them `gh` refuses, or looks for the wrong kind of +# attestation). Keep this list short and keep knowing whose each flag is: its job is to +# stop a real typo in OUR flags hiding behind somebody else's. +IGNORE = {"--help", "--noconfirm", "--noconsole", "--onefile", "--uac-admin", + "--bundle", "--repo", "--predicate-type"} READMES = ("README.md", "README.pl.md") diff --git a/tests/test_mutation_registry.py b/tests/test_mutation_registry.py index 163b2c1..cf65046 100644 --- a/tests/test_mutation_registry.py +++ b/tests/test_mutation_registry.py @@ -1047,6 +1047,16 @@ "new": r' return term.rstrip("\\")', "test": "test_a_term_may_not_end_in_an_escape_that_swallows_the_separator", }, + { + # The command in the README stops matching what we attest, and every user who + # follows it gets an error. Nothing here runs `gh`, so only this pairing can + # notice. + "label": "release: the documented verify command loses its predicate type", + "file": "README.md", + "old": " --predicate-type https://spdx.dev/Document/v2.3", + "new": "", + "test": "test_the_documented_verify_command_matches_what_we_actually_attest", + }, { # The escaping removed as "noise" - and the tool that writes our supply-chain # hashes can again be pointed at a different PyPI endpoint by a `?` in a diff --git a/tests/test_version_and_release.py b/tests/test_version_and_release.py index 4017860..52bf275 100644 --- a/tests/test_version_and_release.py +++ b/tests/test_version_and_release.py @@ -859,3 +859,41 @@ def test_the_release_workflow_grants_write_on_the_job_not_the_whole_file(): for scope in ("contents: write", "id-token: write", "attestations: write"): check(f"the release job still asks for {scope}", f" {scope}" in body, "(a six-space indent is the job's own block)") + + +def test_the_documented_verify_command_matches_what_we_actually_attest(): + """The README hands users a command. It has to be the command that works. + + 🔴 Paid for on 2026-08-19, on a real release: the first version of that line was + `gh attestation verify --bundle ` and it fails twice over. `gh` + refuses without `--repo` or `--owner`, and then looks for a build-provenance + attestation and reports "no attestations found with predicate type" - because the + bundle we publish is the SBOM attestation, made after signing over the bytes a + user downloads. Both are the tool being precise; the documentation was wrong. + + So this pins the two halves together: the predicate type the README tells people + to ask for must be the one `attest-release.yml` actually produces. Change the + workflow to attest something else and the README's command starts failing for + every user, silently, because nothing here runs `gh`. + """ + workflow = os.path.join(ROOT, ".github", "workflows", "attest-release.yml") + with open(workflow, encoding="utf-8") as handle: + yaml_body = handle.read() + # An SBOM attestation is what `sbom-path` makes, and its predicate type is SPDX. + makes_sbom = "sbom-path:" in yaml_body + check("the attestation workflow still makes an SBOM attestation", makes_sbom, + "(if this changed, the predicate type in both READMEs has to change with it)") + + for readme in ("README.md", "README.pl.md"): + with open(os.path.join(ROOT, readme), encoding="utf-8") as handle: + text = handle.read() + line = [ln for ln in text.splitlines() if "gh attestation verify" in ln + and "--bundle" in ln] + check(f"{readme} documents the offline verify command", bool(line)) + command = line[0] if line else "" + check(f"{readme}: it names the repository", "--repo " in command, f"({command[:120]})") + check(f"{readme}: it names the predicate type", + "--predicate-type " in command, f"({command[:120]})") + if makes_sbom: + check(f"{readme}: the predicate type is the SPDX one the workflow makes", + "https://spdx.dev/Document/v2.3" in command, f"({command[:160]})")