Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -660,7 +660,7 @@ jobs:
dry-run: 'false'

repository-pendant:
name: Verify repository pendant
name: Repository pendant admission and readback
needs: [release-please, release]
if: always() && needs.release.result == 'success'
runs-on: ubuntu-22.04
Expand All @@ -670,11 +670,16 @@ jobs:
env:
VERSION: ${{ inputs.version || (startsWith(github.ref, 'refs/tags/') && github.ref_name) || needs.release-please.outputs.tag_name }}
GH_TOKEN: ${{ github.token }}
TRUSTED_REPOSITORY_SHARE_ENABLED: ${{ vars.TRUSTED_REPOSITORY_SHARE_ENABLED }}
steps:
- name: Checkout exact published source
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
with:
ref: refs/tags/${{ env.VERSION }}
fetch-depth: 0
- name: Report trusted publisher admission
id: publisher
run: python3 scripts/verify-repository-share.py --admission
- name: Require repository-owned trusted-host pendant
if: steps.publisher.outputs.admitted == 'true'
run: python3 scripts/verify-repository-share.py "$VERSION" "$(git rev-parse HEAD)"
14 changes: 12 additions & 2 deletions docs/knowledge/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,18 @@ record may produce derived identity through the engine's existing apply path.
The current source profile deliberately does not import arbitrary nonempty
mapping data until an actual curated artifact and its transport are reviewed.

The copied release workflow verifies the three public assets only after the
ordinary release finishes. The managed trusted host runs this repository's own
The release workflow always reports publisher admission after the ordinary
release finishes. Only the exact repository variable
`TRUSTED_REPOSITORY_SHARE_ENABLED=true` admits the bounded public readback.
Unset, empty or `false` reports `NOT_ADMITTED` without downloading or waiting;
malformed values fail. This is not a verified pendant or a publication success.
When admitted, absent assets still exhaust the bounded wait and fail, and the
archive, checksum and exact source/tag provenance checks remain mandatory.

The variable attests an independently configured trusted producer; it does not
enable one. Keep it unset until the reviewed producer configuration and pins
are actually installed. No publication, credential or admission change is made
by this source correction. The managed trusted host runs this repository's own
publisher, keeps its publication policy private, pins both native engine
executables and the indexer, and refuses retained uncertain upload outcomes.
Default configuration still publishes only the existing Quipu corpus. Enabling
Expand Down
63 changes: 62 additions & 1 deletion scripts/test-repository-share-publication.py
Original file line number Diff line number Diff line change
Expand Up @@ -249,11 +249,72 @@ def run(args, **kwargs):

with self.assertRaisesRegex(ValueError, "bounded"):
verifier.wait(
"v1.2.3", self.revision, attempts=2, pause=pauses.append, run=run
"v1.2.3", self.revision, attempts=2, pause=pauses.append, run=run, admitted=True
)
self.assertEqual(len(calls), 2)
self.assertEqual(pauses, [30])

def test_unadmitted_release_makes_no_read_or_wait(self):
def forbidden(*args, **kwargs):
self.fail("unadmitted release attempted a read or pause")

result = verifier.wait("v1.2.3", self.revision, run=forbidden, pause=forbidden)
self.assertEqual(result, {
"status": "NOT_ADMITTED", "source_repository": "scbrown/bobbin",
"release_tag": "v1.2.3", "source_revision": self.revision,
})
with self.assertRaisesRegex(ValueError, "invalid release identity"):
verifier.wait("bad-tag", self.revision, run=forbidden)
with self.assertRaisesRegex(ValueError, "boolean"):
verifier.wait("v1.2.3", self.revision, admitted="false", run=forbidden)

def test_admitted_release_still_verifies_downloaded_bytes(self):
self.prepare()
calls = []

def download(args, **kwargs):
calls.append(args)
folder = Path(args[args.index("--dir") + 1])
for source in (self.archive, self.proof_path, Path(str(self.archive) + ".sha256")):
shutil.copyfile(source, folder / source.name)
return subprocess.CompletedProcess(args, 0)

result = verifier.wait("v1.2.3", self.revision, attempts=1, run=download, admitted=True)
self.assertEqual(result["status"], "VERIFIED")
self.assertEqual(result["source_revision"], self.revision)
self.assertEqual(len(calls), 1)
self.archive.with_name(self.archive.name + ".sha256").write_text("wrong")
with self.assertRaisesRegex(ValueError, "checksum"):
verifier.wait("v1.2.3", self.revision, attempts=1, run=download, admitted=True)

def test_admission_cli_reports_output_and_refuses_malformed_values(self):
for value, status, output in [(None, "NOT_ADMITTED", "false"), ("", "NOT_ADMITTED", "false"),
("false", "NOT_ADMITTED", "false"), ("true", "ADMITTED", "true")]:
with self.subTest(value=value):
env = dict(os.environ)
env.pop("TRUSTED_REPOSITORY_SHARE_ENABLED", None)
if value is not None:
env["TRUSTED_REPOSITORY_SHARE_ENABLED"] = value
target = self.root / "github-output"
target.write_text("existing=retained\n")
env["GITHUB_OUTPUT"] = str(target)
result = subprocess.run([sys.executable, str(ROOT / "verify-repository-share.py"), "--admission"],
env=env, capture_output=True, text=True)
self.assertEqual(result.returncode, 0, result.stderr)
self.assertEqual(json.loads(result.stdout)["status"], status)
self.assertEqual(target.read_text(), "existing=retained\nadmitted=" + output + "\n")
for value in ("TRUE", "1", " false "):
with self.subTest(value=value), self.assertRaisesRegex(ValueError, "admission"):
verifier.publisher_admitted(value)

def test_release_workflow_binds_admission_before_readback(self):
workflow = (ROOT.parent / ".github/workflows/release.yml").read_text()
job = workflow.split(" repository-pendant:\n", 1)[1]
self.assertIn("TRUSTED_REPOSITORY_SHARE_ENABLED: ${{ vars.TRUSTED_REPOSITORY_SHARE_ENABLED }}", job)
self.assertIn("id: publisher\n run: python3 scripts/verify-repository-share.py --admission", job)
self.assertIn("if: steps.publisher.outputs.admitted == 'true'", job)
self.assertLess(job.index("--admission"), job.index("Require repository-owned trusted-host pendant"))


if __name__ == "__main__":
unittest.main()
44 changes: 37 additions & 7 deletions scripts/verify-repository-share.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import hashlib
import importlib.util
import json
import os
import re
import subprocess
import sys
Expand Down Expand Up @@ -67,11 +68,29 @@ def verify(folder, tag, revision):
return expected


def wait(tag, revision, attempts=80, pause=time.sleep, run=subprocess.run):
def publisher_admitted(value):
"""Release-side attestation; this does not enable the trusted producer."""
if value in (None, "", "false"):
return False
if value == "true":
return True
raise ValueError("publisher admission must be true, false or unset")


def wait(tag, revision, attempts=80, pause=time.sleep, run=subprocess.run, *, admitted=False):
if not re.fullmatch(
r"v\d+\.\d+\.\d+(?:[.-][A-Za-z0-9.-]+)?", tag
) or not re.fullmatch("[0-9a-f]{40}", revision):
raise ValueError("invalid release identity before lookup")
if type(admitted) is not bool:
raise ValueError("publisher admission must be a boolean")
if not admitted:
return {
"status": "NOT_ADMITTED",
"source_repository": "scbrown/bobbin",
"release_tag": tag,
"source_revision": revision,
}
stem = "bobbin-" + tag + "-repository.pendant.tar.gz"
last_read = "not attempted"
for number in range(attempts):
Expand Down Expand Up @@ -101,7 +120,7 @@ def wait(tag, revision, attempts=80, pause=time.sleep, run=subprocess.run):
)
last_read = "exit=" + str(result.returncode)
if result.returncode == 0:
return verify(folder, tag, revision)
return {"status": "VERIFIED", **verify(folder, tag, revision)}
except subprocess.TimeoutExpired:
last_read = "read timeout"
if number + 1 < attempts:
Expand All @@ -112,9 +131,20 @@ def wait(tag, revision, attempts=80, pause=time.sleep, run=subprocess.run):
)


def main(args):
admitted = publisher_admitted(os.environ.get("TRUSTED_REPOSITORY_SHARE_ENABLED"))
if args == ["--admission"]:
if output := os.environ.get("GITHUB_OUTPUT"):
with Path(output).open("a") as handle:
handle.write("admitted=" + str(admitted).lower() + "\n")
return {
"status": "ADMITTED" if admitted else "NOT_ADMITTED",
"source_repository": "scbrown/bobbin",
}
if len(args) != 2:
raise SystemExit("usage: verify-repository-share.py --admission | <release-tag> <exact-release-sha>")
return wait(args[0], args[1], admitted=admitted)


if __name__ == "__main__":
if len(sys.argv) != 3:
raise SystemExit(
"usage: verify-repository-share.py <release-tag> <exact-release-sha>"
)
print(json.dumps(wait(sys.argv[1], sys.argv[2])))
print(json.dumps(main(sys.argv[1:])))
Loading