From 11b069dcfa18037993a7d28a72dcfc2e682faff2 Mon Sep 17 00:00:00 2001 From: Jascha Date: Wed, 28 Oct 2026 22:32:29 -0700 Subject: [PATCH 1/2] release: add isolated publisher for reviewed AgentPin 0.3.1 --- .github/workflows/publish-agentpin-0.3.1.yml | 106 +++++++++++++++++++ 1 file changed, 106 insertions(+) create mode 100644 .github/workflows/publish-agentpin-0.3.1.yml diff --git a/.github/workflows/publish-agentpin-0.3.1.yml b/.github/workflows/publish-agentpin-0.3.1.yml new file mode 100644 index 0000000..037fd96 --- /dev/null +++ b/.github/workflows/publish-agentpin-0.3.1.yml @@ -0,0 +1,106 @@ +name: Publish AgentPin 0.3.1 security backport + +# Deliberately single-use: no caller-controlled source, package, or version. +on: + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: publish-agentpin-0.3.1 + cancel-in-progress: false + +env: + RELEASE_COMMIT: 780144e9cf173a8295574a0a6bf7e443d6ca7ac8 + RUSTUP_TOOLCHAIN: 1.99.0 + CARGO_PROFILE_DEV_DEBUG: "0" + CARGO_PROFILE_TEST_DEBUG: "0" + CARGO_INCREMENTAL: "0" + +jobs: + validate: + if: >- + github.repository == 'ThirdKeyAI/AgentPin' && + github.ref == 'refs/heads/main' && + github.actor == 'jaschadub' && + github.triggering_actor == 'jaschadub' + runs-on: ubuntu-latest + timeout-minutes: 20 + outputs: + lock_hash: ${{ steps.lock.outputs.sha256 }} + artifact_id: ${{ steps.upload.outputs.artifact-id }} + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + repository: ThirdKeyAI/AgentPin + ref: 780144e9cf173a8295574a0a6bf7e443d6ca7ac8 + persist-credentials: false + - name: Install pinned Rust + run: rustup toolchain install 1.99.0 --profile minimal + - name: Verify source and package identity + run: | + test "$(git rev-parse HEAD)" = "$RELEASE_COMMIT" + cargo metadata --no-deps --format-version 1 | + jq -e '.packages[] | select(.name == "agentpin") | .version == "0.3.1"' + - name: Resolve, test, and verify package without publication credentials + run: | + cargo generate-lockfile + cargo test --workspace --all-features --locked + cargo package --package agentpin@0.3.1 --locked + cargo install cargo-audit --version 0.22.2 --locked + cargo audit --deny warnings + git diff --exit-code + - name: Record tested lockfile + id: lock + run: | + digest="$(sha256sum Cargo.lock | cut -d ' ' -f 1)" + echo "sha256=$digest" >> "$GITHUB_OUTPUT" + - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + id: upload + with: + name: agentpin-0.3.1-lockfile + path: Cargo.lock + if-no-files-found: error + retention-days: 1 + + publish: + needs: validate + if: >- + github.repository == 'ThirdKeyAI/AgentPin' && + github.ref == 'refs/heads/main' && + github.actor == 'jaschadub' && + github.triggering_actor == 'jaschadub' + # Fresh runner: no dependency build scripts/tests execute with access to + # publication credentials. Transfer lockfile data only, never build caches. + runs-on: ubuntu-latest + timeout-minutes: 10 + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + repository: ThirdKeyAI/AgentPin + ref: 780144e9cf173a8295574a0a6bf7e443d6ca7ac8 + persist-credentials: false + - name: Install pinned Rust + run: rustup toolchain install 1.99.0 --profile minimal + - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + artifact-ids: ${{ needs.validate.outputs.artifact_id }} + path: . + merge-multiple: true + digest-mismatch: error + - name: Check exact source, package, and tested lockfile + env: + LOCK_SHA256: ${{ needs.validate.outputs.lock_hash }} + run: | + test "$(git rev-parse HEAD)" = "$RELEASE_COMMIT" + [[ "$LOCK_SHA256" =~ ^[0-9a-f]{64}$ ]] + printf '%s Cargo.lock\n' "$LOCK_SHA256" | sha256sum --check + cargo metadata --locked --no-deps --format-version 1 | + jq -e '.packages[] | select(.name == "agentpin") | .version == "0.3.1"' + git diff --exit-code + - name: Publish only the verified AgentPin 0.3.1 crate + env: + CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }} + # Package verification ran in the separate, unprivileged job. + run: cargo publish --package agentpin@0.3.1 --registry crates-io --locked --no-verify From 99181592d7066886dbf6a0a0538846163a1fa205 Mon Sep 17 00:00:00 2001 From: Jascha Date: Wed, 28 Oct 2026 22:37:32 -0700 Subject: [PATCH 2/2] security: restrict publisher artifacts to validated lockfile data --- .github/workflows/publish-agentpin-0.3.1.yml | 51 +++++++++++++++++++- 1 file changed, 50 insertions(+), 1 deletion(-) diff --git a/.github/workflows/publish-agentpin-0.3.1.yml b/.github/workflows/publish-agentpin-0.3.1.yml index 037fd96..617d4aa 100644 --- a/.github/workflows/publish-agentpin-0.3.1.yml +++ b/.github/workflows/publish-agentpin-0.3.1.yml @@ -86,9 +86,58 @@ jobs: - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: artifact-ids: ${{ needs.validate.outputs.artifact_id }} - path: . + path: ${{ runner.temp }}/agentpin-lock-artifact merge-multiple: true digest-mismatch: error + skip-decompress: true + - name: Accept only the tested regular lockfile from the untrusted archive + env: + LOCK_SHA256: ${{ needs.validate.outputs.lock_hash }} + LOCK_ARTIFACT_DIR: ${{ runner.temp }}/agentpin-lock-artifact + run: | + python3 - <<'PY' + import hashlib + import os + from pathlib import Path + import re + import stat + import zipfile + + def reject(message): + raise SystemExit(message) + + expected = os.environ["LOCK_SHA256"] + if re.fullmatch(r"[0-9a-f]{64}", expected) is None: + reject("Invalid lockfile digest") + entries = list(Path(os.environ["LOCK_ARTIFACT_DIR"]).iterdir()) + if len(entries) != 1 or not stat.S_ISREG(entries[0].lstat().st_mode): + reject("Expected exactly one regular archive") + if entries[0].stat().st_size > 2 * 1024 * 1024: + reject("Archive exceeds size limit") + with zipfile.ZipFile(entries[0]) as archive: + members = archive.infolist() + if len(members) != 1: + reject("Archive must contain only Cargo.lock") + member = members[0] + file_type = stat.S_IFMT(member.external_attr >> 16) + if member.filename != "Cargo.lock" or member.is_dir(): + reject("Unexpected archive path") + if file_type not in (0, stat.S_IFREG): + reject("Lockfile must be a regular file") + if member.file_size > 1024 * 1024: + reject("Lockfile exceeds size limit") + if member.compress_type not in (zipfile.ZIP_STORED, zipfile.ZIP_DEFLATED): + reject("Unsupported archive compression") + with archive.open(member) as stream: + data = stream.read(1024 * 1024 + 1) + if len(data) != member.file_size or len(data) > 1024 * 1024: + reject("Invalid lockfile size") + if hashlib.sha256(data).hexdigest() != expected: + reject("Lockfile digest mismatch") + # Never extract archive paths or configuration into the clean checkout. + with open("Cargo.lock", "xb") as destination: + destination.write(data) + PY - name: Check exact source, package, and tested lockfile env: LOCK_SHA256: ${{ needs.validate.outputs.lock_hash }}