From 933c32477391d4381090bf4c5d7bdf310154583c Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 27 Jun 2026 14:11:56 +0000 Subject: [PATCH 1/7] Add AUR package and move distribution actions to subfolder (#1319) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Create aur/PKGBUILD and aur/.SRCINFO for the dfetch-bin AUR package - Add .github/workflows/distribution/aur-publish.yml to publish to AUR on each release using AUR_SSH_KEY stored in the 'aur' environment - Move python-publish.yml and winget-publish.yml into .github/workflows/distribution/ subfolder alongside the new aur-publish.yml - Update installation docs with Arch Linux / AUR installation instructions - Extend threat model with AUR boundary (A-11), AUR_SSH_KEY asset (A-12), dataflows DF-30–DF-32, threat DFT-36, and controls C-044/C-046 - Update security/tm_controls_data.py file-path references for moved workflows Co-Authored-By: Claude Sonnet 4.6 Claude-Session: https://claude.ai/code/session_013RYDLTQX5YRoR7gV5vFRM9 --- .../workflows/distribution/aur-publish.yml | 77 +++++++++++ .../{ => distribution}/python-publish.yml | 0 .../{ => distribution}/winget-publish.yml | 0 CHANGELOG.rst | 1 + aur/.SRCINFO | 13 ++ aur/PKGBUILD | 16 +++ doc/explanation/threat_model_supply_chain.rst | 96 +++++++++++++- doc/tutorials/installation.rst | 8 ++ security/threats.json | 15 +++ security/tm_controls_data.py | 43 +++++- security/tm_supply_chain.py | 124 +++++++++++++++++- 11 files changed, 381 insertions(+), 12 deletions(-) create mode 100644 .github/workflows/distribution/aur-publish.yml rename .github/workflows/{ => distribution}/python-publish.yml (100%) rename .github/workflows/{ => distribution}/winget-publish.yml (100%) create mode 100644 aur/.SRCINFO create mode 100644 aur/PKGBUILD diff --git a/.github/workflows/distribution/aur-publish.yml b/.github/workflows/distribution/aur-publish.yml new file mode 100644 index 00000000..3d7e9e01 --- /dev/null +++ b/.github/workflows/distribution/aur-publish.yml @@ -0,0 +1,77 @@ +name: Publish to AUR + +on: + release: + types: [published] + workflow_dispatch: + inputs: + release-tag: + description: 'Release tag (e.g. 0.14.2)' + required: true + type: string + +permissions: + contents: read + +jobs: + publish: + name: Publish to AUR + # For releases: skip the rolling 'latest' tag on main. + # For manual dispatch: always run (the operator knows what they're doing). + if: >- + github.event_name != 'release' || + github.event.release.tag_name != 'latest' + runs-on: ubuntu-latest + concurrency: + group: aur-publish-${{ github.event.release.tag_name || inputs.release-tag }} + cancel-in-progress: true + + environment: + name: aur + url: https://aur.archlinux.org/packages/dfetch-bin + + steps: + - name: "Harden the runner (Block egress traffic: Only allow calls to allowed endpoints)" + uses: step-security/harden-runner@9af89fc71515a100421586dfdb3dc9c984fbf411 # v2.19.4 + with: + egress-policy: block + allowed-endpoints: >+ + github.com:443 + api.github.com:443 + objects.githubusercontent.com:443 + release-assets.githubusercontent.com:443 + uploads.github.com:443 + aur.archlinux.org:443 + aur.archlinux.org:22 + + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + with: + persist-credentials: false + + - name: Update pkgver and checksums + # Requires AUR_SSH_KEY secret in the 'aur' environment. + # + # Setup — create an SSH key pair: + # 1. ssh-keygen -t ed25519 -C "dfetch-aur-bot" -f aur_key + # 2. Add the public key (aur_key.pub) to your AUR account at + # https://aur.archlinux.org/account// + # 3. Store the private key as secret AUR_SSH_KEY in: + # Repo → Settings → Environments → aur → Environment secrets + env: + TAG: ${{ github.event.release.tag_name || inputs.release-tag }} + run: | + URL="https://github.com/dfetch-org/dfetch/releases/download/${TAG}/dfetch-${TAG}-nix.tar.gz" + SHA256=$(curl -fsSL "$URL" | sha256sum | awk '{print $1}') + sed -i "s/^pkgver=.*/pkgver=${TAG}/" aur/PKGBUILD + sed -i "s/^sha256sums=.*/sha256sums=('${SHA256}')/" aur/PKGBUILD + + - name: Publish to AUR + uses: KSXGitHub/github-actions-deploy-aur@a6ca8e3a2da2a9e461d694e80248ba1a3ab1b3e1 # v3.0.1 + with: + pkgname: dfetch-bin + pkgbuild: ./aur/PKGBUILD + commit_username: dfetch-bot + commit_email: bot@dfetch.dev + ssh_private_key: ${{ secrets.AUR_SSH_KEY }} + commit_message: "Update to ${{ github.event.release.tag_name || inputs.release-tag }}" + updpkgsums: false diff --git a/.github/workflows/python-publish.yml b/.github/workflows/distribution/python-publish.yml similarity index 100% rename from .github/workflows/python-publish.yml rename to .github/workflows/distribution/python-publish.yml diff --git a/.github/workflows/winget-publish.yml b/.github/workflows/distribution/winget-publish.yml similarity index 100% rename from .github/workflows/winget-publish.yml rename to .github/workflows/distribution/winget-publish.yml diff --git a/CHANGELOG.rst b/CHANGELOG.rst index e37ceb3b..367ae4c7 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -1,6 +1,7 @@ Release 0.14.3 (released 2026-06-25) ==================================== +* Add AUR (Arch Linux User Repository) package and automated publish workflow (#1319) * Update ``dfetch environment`` to show newer version inline (#1310) Release 0.14.2 (released 2026-06-21) diff --git a/aur/.SRCINFO b/aur/.SRCINFO new file mode 100644 index 00000000..4502e5ed --- /dev/null +++ b/aur/.SRCINFO @@ -0,0 +1,13 @@ +pkgbase = dfetch-bin + pkgdesc = Vendor tool for fetching external dependencies + pkgver = 0.14.3 + pkgrel = 1 + url = https://github.com/dfetch-org/dfetch + arch = x86_64 + license = MIT + provides = dfetch + conflicts = dfetch + source = https://github.com/dfetch-org/dfetch/releases/download/0.14.3/dfetch-0.14.3-nix.tar.gz + sha256sums = SKIP + +pkgname = dfetch-bin diff --git a/aur/PKGBUILD b/aur/PKGBUILD new file mode 100644 index 00000000..ebb0f7b5 --- /dev/null +++ b/aur/PKGBUILD @@ -0,0 +1,16 @@ +# Maintainer: dfetch-org +pkgname=dfetch-bin +pkgver=0.14.3 +pkgrel=1 +pkgdesc="Vendor tool for fetching external dependencies" +arch=('x86_64') +url="https://github.com/dfetch-org/dfetch" +license=('MIT') +provides=('dfetch') +conflicts=('dfetch') +source=("https://github.com/dfetch-org/dfetch/releases/download/${pkgver}/dfetch-${pkgver}-nix.tar.gz") +sha256sums=('SKIP') # replaced by CI with real checksum + +package() { + install -Dm755 dfetch "$pkgdir/usr/bin/dfetch" +} diff --git a/doc/explanation/threat_model_supply_chain.rst b/doc/explanation/threat_model_supply_chain.rst index 7d88c7cc..0dffce59 100644 --- a/doc/explanation/threat_model_supply_chain.rst +++ b/doc/explanation/threat_model_supply_chain.rst @@ -20,7 +20,7 @@ Chapter 5. The Sev / Risk rating scale and treatment vocabulary (Mitigate / Accept / Transfer) are defined in the :ref:`Risk Rating Methodology ` section of the main security page. -Threat model for dfetch. Covers the pre-install lifecycle: code contribution, CI/CD, build (wheel / sdist), PyPI distribution, Winget manifest submission, and consumer installation. The installed dfetch package is the handoff point to tm_usage.py. +Threat model for dfetch. Covers the pre-install lifecycle: code contribution, CI/CD, build (wheel / sdist), PyPI distribution, Winget manifest submission, AUR PKGBUILD publication, and consumer installation. The installed dfetch package is the handoff point to tm_usage.py. Assumptions ----------- @@ -84,6 +84,9 @@ Boundaries * - Winget Community Repository - The Windows Package Manager Community Repository (https://github.com/microsoft/winget-pkgs) where dfetch's Winget manifest is hosted. Manifest PRs are submitted automatically by the CI release pipeline (winget-publish.yml) using the stored WINGET_TOKEN PAT (A-10). Consumer installations resolve manifests from this repository; winget downloads the MSI installer from the URL declared in the manifest (pointing to GitHub Releases, A-01) and verifies its SHA256 hash. + * - AUR Repository + - The Arch Linux User Repository (https://aur.archlinux.org) where the ``dfetch-bin`` PKGBUILD is hosted. CI pushes updated PKGBUILDs directly to the AUR git repository using ``aur-publish.yml`` and the AUR_SSH_KEY (A-12). Unlike Winget (which goes through a community PR review), AUR accepts direct git pushes from authenticated maintainers — there is no intermediate PR review gate. + Data Flow Diagram ----------------- @@ -264,6 +267,25 @@ Data Flow Diagram } + subgraph cluster_boundary_AURRepository_aur { + graph [ + fontsize = 10; + fontcolor = black; + style = dashed; + color = firebrick2; + label = <AUR\nRepository>; + ] + + externalentity_AAURRepositoryaurarchlinuxorgdfetchbin_aur [ + shape = square; + color = black; + fontcolor = black; + label = "A-11: AUR\nRepository\n(aur.archlinux.org/\ndfetch-bin)"; + margin = 0.02; + ] + + } + actor_DeveloperContributor_d2006ce1bb -> externalentity_AbGitHubRepositoryfeaturebranchesPRs_0291419f72 [ color = black; fontcolor = black; @@ -397,6 +419,27 @@ Data Flow Diagram label = "DF-29: Consumer\ndownloads MSI via\nwinget"; ] + externalentity_AGitHubActionsInfrastructure_c76a0a7067 -> externalentity_AAURRepositoryaurarchlinuxorgdfetchbin_aur [ + color = black; + fontcolor = black; + dir = forward; + label = "DF-30: AUR\nPKGBUILD push"; + ] + + actor_ConsumerEndUser_f8af758679 -> externalentity_AAURRepositoryaurarchlinuxorgdfetchbin_aur [ + color = black; + fontcolor = black; + dir = forward; + label = "DF-31: yay install\ndfetch-bin"; + ] + + externalentity_AAURRepositoryaurarchlinuxorgdfetchbin_aur -> actor_ConsumerEndUser_f8af758679 [ + color = black; + fontcolor = black; + dir = forward; + label = "DF-32: Consumer\ndownloads tarball\nvia AUR"; + ] + } @enddot @@ -459,6 +502,7 @@ Sequence Diagram database datastore_AdfetchBuildDevDependencies_990b886585 as "A-07: dfetch\nBuild / Dev\nDependencies" database datastore_AbGitHubActionsBuildCache_9df04f8dae as "A-08b:\nGitHub\nActions\nBuild Cache" entity externalentity_AWingetCommunityRepositorymicrosoftwingetpkgs_7113ed0f48 as "A-09: Winget\nCommunity\nRepository\n(microsoft/winget-pkgs)" + entity externalentity_AAURRepositoryaurarchlinuxorgdfetchbin_aur as "A-11: AUR\nRepository\n(aur.archlinux.org/\ndfetch-bin)" actor_DeveloperContributor_d2006ce1bb -> externalentity_AbGitHubRepositoryfeaturebranchesPRs_0291419f72: DF-11: Push commits / open PR externalentity_AbGitHubRepositoryfeaturebranchesPRs_0291419f72 -> process_AReleaseGateCodeReview_9345ab4c19: DF-22: PR enters code review @@ -479,6 +523,9 @@ Sequence Diagram externalentity_AGitHubActionsInfrastructure_c76a0a7067 -> externalentity_AWingetCommunityRepositorymicrosoftwingetpkgs_7113ed0f48: DF-27: Winget manifest PR submission actor_ConsumerEndUser_f8af758679 -> externalentity_AWingetCommunityRepositorymicrosoftwingetpkgs_7113ed0f48: DF-28: winget install dfetch externalentity_AWingetCommunityRepositorymicrosoftwingetpkgs_7113ed0f48 -> actor_ConsumerEndUser_f8af758679: DF-29: Consumer downloads MSI via winget + externalentity_AGitHubActionsInfrastructure_c76a0a7067 -> externalentity_AAURRepositoryaurarchlinuxorgdfetchbin_aur: DF-30: AUR PKGBUILD push + actor_ConsumerEndUser_f8af758679 -> externalentity_AAURRepositoryaurarchlinuxorgdfetchbin_aur: DF-31: yay install dfetch-bin + externalentity_AAURRepositoryaurarchlinuxorgdfetchbin_aur -> actor_ConsumerEndUser_f8af758679: DF-32: Consumer downloads tarball via AUR @enduml .. raw:: html @@ -554,7 +601,7 @@ Asset Identification - Data - High / High / High * - A-06: GitHub Actions Workflow - - CI/CD pipelines: test, build (wheel/msi/deb/rpm), lint, CodeQL, Scorecard, dependency-review, docs, release, winget-publish. All actions pinned by commit SHA. harden-runner used in every workflow that executes steps on a runner (egress: block with endpoint allowlist); ci.yml is a dispatcher-only workflow with no runner steps and does not include harden-runner. winget-publish.yml uses a stored PAT (WINGET_TOKEN, A-10) to submit manifest PRs to the Winget Community Repository (A-09). + - CI/CD pipelines: test, build (wheel/msi/deb/rpm), lint, CodeQL, Scorecard, dependency-review, docs, release, distribution/winget-publish, distribution/python-publish, distribution/aur-publish. All actions pinned by commit SHA. harden-runner used in every workflow that executes steps on a runner (egress: block with endpoint allowlist); ci.yml is a dispatcher-only workflow with no runner steps and does not include harden-runner. distribution/winget-publish.yml uses a stored PAT (WINGET_TOKEN, A-10) to submit manifest PRs to the Winget Community Repository (A-09). distribution/aur-publish.yml uses an SSH key (AUR_SSH_KEY, A-12) to push PKGBUILDs directly to the AUR repository (A-11). - Process - Medium / Medium / Medium * - A-07: dfetch Build / Dev Dependencies @@ -577,6 +624,14 @@ Asset Identification - Long-lived classic GitHub Personal Access Token (fine-grained PATs are not supported by the ``vedantmgoyal9/winget-releaser`` action) with ``public_repo`` scope, stored as a GitHub Actions environment secret in the ``winget`` environment. Used by ``winget-publish.yml`` to push a manifest branch to the pre-existing fork ``dfetch-org/winget-pkgs`` and open a PR against ``microsoft/winget-pkgs``. Unlike the PyPI OIDC token (A-05) which is short-lived and not stored, this PAT persists indefinitely until rotated. If exfiltrated from the CI environment, an attacker could submit fraudulent manifest PRs from outside the project's pipeline. - Data - High / High / — + * - A-11: AUR Repository (aur.archlinux.org/dfetch-bin) + - The Arch Linux User Repository entry for the ``dfetch-bin`` package (https://aur.archlinux.org/packages/dfetch-bin). CI pushes updated PKGBUILDs directly via git using the AUR_SSH_KEY (A-12); there is no PR review step — the push goes live immediately. PKGBUILDs contain the SHA256 checksum of the release tarball; AUR helpers such as ``yay`` download and verify the tarball before installing. A compromised SSH key or a malicious PKGBUILD push could redirect consumers to a tampered binary (DFT-36). + - ExternalEntity + - High / High / — + * - A-12: AUR_SSH_KEY + - Ed25519 SSH private key used to authenticate git pushes to the dfetch-bin AUR repository. Stored as a GitHub Actions environment secret in the ``aur`` environment. Used by ``aur-publish.yml`` to push updated PKGBUILDs directly to AUR. Unlike the PyPI OIDC token (A-05) which is short-lived and not stored, this key persists indefinitely until rotated. If exfiltrated from the CI environment, an attacker could push a malicious PKGBUILD directly to AUR without any review gate (DFT-36). + - Data + - High / High / — @@ -690,6 +745,21 @@ Dataflows - Consumer / End User - HTTPS + * - DF-30: AUR PKGBUILD push + - A-02: GitHub Actions Infrastructure + - A-11: AUR Repository (aur.archlinux.org/dfetch-bin) + - SSH + + * - DF-31: yay install dfetch-bin + - Consumer / End User + - A-11: AUR Repository (aur.archlinux.org/dfetch-bin) + - HTTPS + + * - DF-32: Consumer downloads tarball via AUR + - A-11: AUR Repository (aur.archlinux.org/dfetch-bin) + - Consumer / End User + - HTTPS + Threats ------- @@ -864,6 +934,14 @@ Threats | **STRIDE:** T S | **Status:** Mitigate - C-041 + * - DFT-36 + - Compromised AUR SSH key enables direct malicious PKGBUILD push without review gate + - A-11: AUR Repository (aur.archlinux.org/dfetch-bin) + - | **Sev:** 🟠H + | **Risk:** 🟠H + | **STRIDE:** T S + | **Status:** Mitigate + - C-044, C-046 Controls @@ -885,7 +963,7 @@ Controls * - C-010 - OIDC trusted publishing - DFT-07 - - PyPI publishes via ``pypa/gh-action-pypi-publish`` with ``id-token: write`` and no stored long-lived API token. ``.github/workflows/python-publish.yml`` + - PyPI publishes via ``pypa/gh-action-pypi-publish`` with ``id-token: write`` and no stored long-lived API token. ``.github/workflows/distribution/python-publish.yml`` * - C-011 - Minimal workflow permissions - DFT-07 @@ -953,8 +1031,16 @@ Controls * - C-041 - Winget manifest PRs reviewed by community maintainers - DFT-35 - - Manifest update PRs submitted to ``microsoft/winget-pkgs`` by ``winget-publish.yml`` go through the standard Winget community review process before merging. ``microsoft/winget-pkgs`` maintainers verify the publisher identity and inspect manifest changes including installer URLs and hashes. This provides a manual review gate between a fraudulent PR submission and consumer exposure. Residual risk: a reviewer who approves without independently verifying the installer URL origin could merge a fraudulent manifest. ``.github/workflows/winget-publish.yml`` + - Manifest update PRs submitted to ``microsoft/winget-pkgs`` by ``winget-publish.yml`` go through the standard Winget community review process before merging. ``microsoft/winget-pkgs`` maintainers verify the publisher identity and inspect manifest changes including installer URLs and hashes. This provides a manual review gate between a fraudulent PR submission and consumer exposure. Residual risk: a reviewer who approves without independently verifying the installer URL origin could merge a fraudulent manifest. ``.github/workflows/distribution/winget-publish.yml`` * - C-042 - WINGET_TOKEN scoped to dedicated Winget environment - DFT-34 - - ``WINGET_TOKEN`` is stored in the ``winget`` GitHub Actions deployment environment, limiting its exposure: the PAT is only injected into workflows that explicitly reference that environment. Only ``winget-publish.yml`` references the ``winget`` environment, so the PAT is not available to other workflows. Residual risk: unlike PyPI which uses OIDC (A-05, no stored long-lived token), Winget does not support OIDC trusted publishing; the PAT must be stored and rotated manually (DFT-34). ``.github/workflows/winget-publish.yml`` + - ``WINGET_TOKEN`` is stored in the ``winget`` GitHub Actions deployment environment, limiting its exposure: the PAT is only injected into workflows that explicitly reference that environment. Only ``winget-publish.yml`` references the ``winget`` environment, so the PAT is not available to other workflows. Residual risk: unlike PyPI which uses OIDC (A-05, no stored long-lived token), Winget does not support OIDC trusted publishing; the PAT must be stored and rotated manually (DFT-34). ``.github/workflows/distribution/winget-publish.yml`` + * - C-044 + - AUR_SSH_KEY scoped to dedicated AUR environment + - DFT-36 + - ``AUR_SSH_KEY`` is stored in the ``aur`` GitHub Actions deployment environment, limiting its exposure: the SSH key is only injected into workflows that explicitly reference that environment. Only ``aur-publish.yml`` references the ``aur`` environment, so the key is not available to other workflows. Unlike PyPI which uses OIDC (A-05, no stored long-lived token) or Winget which uses a PAT (A-10), AUR authentication requires an SSH key for git push access; the key must be stored and rotated manually. A compromised key enables direct (no review gate) PKGBUILD pushes to AUR. ``.github/workflows/distribution/aur-publish.yml`` + * - C-046 + - AUR PKGBUILD uses real SHA256 checksum of release tarball + - DFT-36 + - The ``aur-publish.yml`` workflow computes the SHA256 checksum of the release tarball at publish time and embeds it in the PKGBUILD, replacing the placeholder ``SKIP``. AUR helpers (``yay``, ``paru``) verify this checksum when building the package, so a tampered tarball would be rejected before installation. Residual risk: a compromised AUR_SSH_KEY (A-12) allows pushing a PKGBUILD with a fraudulent checksum — the review gate present in Winget (C-041) does not exist for AUR direct pushes. ``.github/workflows/distribution/aur-publish.yml`` diff --git a/doc/tutorials/installation.rst b/doc/tutorials/installation.rst index 3e05efe1..fe0bea88 100644 --- a/doc/tutorials/installation.rst +++ b/doc/tutorials/installation.rst @@ -53,6 +53,14 @@ The version is automatically determined from the project and used to name the in # or $ sudo rpm -i dfetch--nix.rpm + Arch Linux (via AUR): + + .. code-block:: bash + + $ yay -S dfetch-bin + # or with any other AUR helper, e.g. + $ paru -S dfetch-bin + .. tab:: macOS Download the ``.pkg`` package from the releases page and install it. diff --git a/security/threats.json b/security/threats.json index a150bd92..a4a637d7 100644 --- a/security/threats.json +++ b/security/threats.json @@ -523,5 +523,20 @@ "mitigations": "Pin installer URLs in manifests to the project's official release infrastructure and document that URL changes must be scrutinised as carefully as hash changes. Store publish credentials in a deployment environment requiring reviewer approval. Use OIDC-based credentials where available so exfiltrated tokens expire quickly. Monitor PAT usage for activity outside expected CI build windows.", "example": "An attacker exfiltrates the WINGET_TOKEN PAT from a CI run via a backdoored dependency. They fork microsoft/winget-pkgs, update the dfetch manifest to point the installer URL to attacker.example/dfetch-0.14.0-win.msi (a trojaned MSI), compute its correct SHA256 hash, and open a PR. A reviewer sees the hash matches the declared value and approves. Consumers who run winget install DFetch-org.DFetch download and install the malicious binary.", "references": "https://slsa.dev/spec/v1.0/threats#f-artifact-publication, https://capec.mitre.org/data/definitions/186.html, https://cwe.mitre.org/data/definitions/494.html" + }, + { + "SID": "DFT-36", + "target": [ + "ExternalEntity" + ], + "description": "Compromised AUR SSH key enables direct malicious PKGBUILD push without review gate", + "details": "The AUR (Arch Linux User Repository) accepts direct git pushes from authenticated maintainers — unlike Winget (which requires a PR reviewed by community maintainers before merging), a valid AUR SSH push goes live immediately. An attacker who exfiltrates the AUR_SSH_KEY from the CI environment can push a malicious PKGBUILD that: (1) replaces the source URL with an attacker-controlled tarball, (2) computes its correct SHA256 hash. Arch users running 'yay -Syu' or equivalent will receive the tampered PKGBUILD and install the malicious binary. Because there is no review gate between CI push and consumer exposure, the window of opportunity for detection is shorter than for manifest-based registries.", + "Likelihood Of Attack": "Low", + "severity": "High", + "condition": "target.controls.hasAccessControl is True and target.controls.providesIntegrity is True and target.controls.usesCodeSigning is False and target.controls.isHardened is False", + "prerequisites": "The attacker has obtained the AUR_SSH_KEY private key from the CI environment. The AUR repository for dfetch-bin accepts pushes authenticated with this key. The attacker can craft a PKGBUILD with a correct SHA256 checksum for a malicious tarball.", + "mitigations": "Store the AUR_SSH_KEY in a deployment environment (C-044) that requires explicit environment protection rules before the secret is injected into the workflow. Block all non-allowlisted egress from the CI runner so the key cannot be exfiltrated. Use a dedicated bot key with the minimum required AUR permissions. Rotate the key regularly and immediately on suspected compromise. Monitor AUR commit history for pushes outside expected CI release windows.", + "example": "A backdoored build dependency reads AUR_SSH_KEY from the runner environment and posts it to an attacker-controlled server. The attacker pushes a PKGBUILD pointing to a trojanised tarball on attacker.example with a matching SHA256. Arch users updating dfetch-bin via yay receive and install the malicious binary before the compromise is detected.", + "references": "https://slsa.dev/spec/v1.0/threats#f-artifact-publication, https://wiki.archlinux.org/title/AUR_submission_guidelines, https://capec.mitre.org/data/definitions/560.html" } ] diff --git a/security/tm_controls_data.py b/security/tm_controls_data.py index 737ee413..eb3c746d 100644 --- a/security/tm_controls_data.py +++ b/security/tm_controls_data.py @@ -30,7 +30,7 @@ def to_pytm(self) -> str: return f"[{self.id}] {self.name}" -# ── Supply-chain controls (C-009 … C-042) ──────────────────────────────────── +# ── Supply-chain controls (C-009 … C-046) ──────────────────────────────────── SC_CONTROLS: list[Control] = [ Control( @@ -49,7 +49,7 @@ def to_pytm(self) -> str: name="OIDC trusted publishing", assets=["A-05", "A-03"], threats=["DFT-07"], - reference=".github/workflows/python-publish.yml", + reference=".github/workflows/distribution/python-publish.yml", description=( "PyPI publishes via ``pypa/gh-action-pypi-publish`` with " "``id-token: write`` and no stored long-lived API token." @@ -303,7 +303,7 @@ def to_pytm(self) -> str: name="Winget manifest PRs reviewed by community maintainers", assets=["A-09"], threats=["DFT-35"], - reference=".github/workflows/winget-publish.yml", + reference=".github/workflows/distribution/winget-publish.yml", description=( "Manifest update PRs submitted to ``microsoft/winget-pkgs`` by " "``winget-publish.yml`` go through the standard Winget community review " @@ -321,7 +321,7 @@ def to_pytm(self) -> str: name="WINGET_TOKEN scoped to dedicated Winget environment", assets=["A-10"], threats=["DFT-34"], - reference=".github/workflows/winget-publish.yml", + reference=".github/workflows/distribution/winget-publish.yml", description=( "``WINGET_TOKEN`` is stored in the ``winget`` GitHub Actions deployment " "environment, limiting its exposure: the PAT is only injected into " @@ -333,6 +333,41 @@ def to_pytm(self) -> str: "be stored and rotated manually (DFT-34)." ), ), + Control( + id="C-044", + name="AUR_SSH_KEY scoped to dedicated AUR environment", + assets=["A-12", "A-11"], + threats=["DFT-36"], + reference=".github/workflows/distribution/aur-publish.yml", + description=( + "``AUR_SSH_KEY`` is stored in the ``aur`` GitHub Actions deployment " + "environment, limiting its exposure: the SSH key is only injected into " + "workflows that explicitly reference that environment. " + "Only ``aur-publish.yml`` references the ``aur`` environment, so the " + "key is not available to other workflows. " + "Unlike PyPI which uses OIDC (A-05, no stored long-lived token) or " + "Winget which uses a PAT (A-10), AUR authentication requires an SSH key " + "for git push access; the key must be stored and rotated manually. " + "A compromised key enables direct (no review gate) PKGBUILD pushes to AUR." + ), + ), + Control( + id="C-046", + name="AUR PKGBUILD uses real SHA256 checksum of release tarball", + assets=["A-11"], + threats=["DFT-36"], + reference=".github/workflows/distribution/aur-publish.yml", + description=( + "The ``aur-publish.yml`` workflow computes the SHA256 checksum of the " + "release tarball at publish time and embeds it in the PKGBUILD, replacing " + "the placeholder ``SKIP``. " + "AUR helpers (``yay``, ``paru``) verify this checksum when building the " + "package, so a tampered tarball would be rejected before installation. " + "Residual risk: a compromised AUR_SSH_KEY (A-12) allows pushing a " + "PKGBUILD with a fraudulent checksum — the review gate present in Winget " + "(C-041) does not exist for AUR direct pushes." + ), + ), ] # ── Runtime-usage controls (C-001 … C-045) ─────────────────────────────────── diff --git a/security/tm_supply_chain.py b/security/tm_supply_chain.py index 4c6afa5b..05a4ef0c 100644 --- a/security/tm_supply_chain.py +++ b/security/tm_supply_chain.py @@ -196,13 +196,16 @@ def _make_sc_processes(b_github: Boundary) -> tuple[Process, Process, Process]: gh_actions_workflow.inBoundary = b_github gh_actions_workflow.description = ( "CI/CD pipelines: test, build (wheel/msi/deb/rpm), lint, CodeQL, Scorecard, " - "dependency-review, docs, release, winget-publish. " + "dependency-review, docs, release, distribution/winget-publish, " + "distribution/python-publish, distribution/aur-publish. " "All actions pinned by commit SHA. " "harden-runner used in every workflow that executes steps on a runner " "(egress: block with endpoint allowlist); ci.yml is a dispatcher-only workflow " "with no runner steps and does not include harden-runner. " - "winget-publish.yml uses a stored PAT (WINGET_TOKEN, A-10) to submit manifest " - "PRs to the Winget Community Repository (A-09)." + "distribution/winget-publish.yml uses a stored PAT (WINGET_TOKEN, A-10) to " + "submit manifest PRs to the Winget Community Repository (A-09). " + "distribution/aur-publish.yml uses an SSH key (AUR_SSH_KEY, A-12) to push " + "PKGBUILDs directly to the AUR repository (A-11)." ) gh_actions_workflow.controls.isHardened = ( True # SHA-pinned actions, harden-runner egress:block on all runner workflows @@ -557,6 +560,110 @@ def _make_sc_consumer_install_flows( df26.controls.isNetworkFlow = True +def _make_sc_aur_elements_and_flows( + gh_actions_runner: ExternalEntity, + consumer: Actor, +) -> None: + """Create AUR assets and publishing/installation flows (DF-30 through DF-32).""" + b_aur = Boundary("AUR Repository") + b_aur.description = ( + "The Arch Linux User Repository (https://aur.archlinux.org) where the " + "``dfetch-bin`` PKGBUILD is hosted. " + "CI pushes updated PKGBUILDs directly to the AUR git repository using " + "``aur-publish.yml`` and the AUR_SSH_KEY (A-12). " + "Unlike Winget (which goes through a community PR review), AUR accepts " + "direct git pushes from authenticated maintainers — there is no " + "intermediate PR review gate." + ) + + Data( + "A-12: AUR_SSH_KEY", + description=( + "Ed25519 SSH private key used to authenticate git pushes to the dfetch-bin " + "AUR repository. Stored as a GitHub Actions environment secret in the " + "``aur`` environment. " + "Used by ``aur-publish.yml`` to push updated PKGBUILDs directly to AUR. " + "Unlike the PyPI OIDC token (A-05) which is short-lived and not stored, " + "this key persists indefinitely until rotated. " + "If exfiltrated from the CI environment, an attacker could push a malicious " + "PKGBUILD directly to AUR without any review gate (DFT-36)." + ), + classification=Classification.SECRET, + isCredentials=True, + isPII=False, + isStored=True, + isDestEncryptedAtRest=True, + isSourceEncryptedAtRest=True, + ) + + aur_repo = ExternalEntity("A-11: AUR Repository (aur.archlinux.org/dfetch-bin)") + aur_repo.inBoundary = b_aur + aur_repo.classification = Classification.SENSITIVE + aur_repo.description = ( + "The Arch Linux User Repository entry for the ``dfetch-bin`` package " + "(https://aur.archlinux.org/packages/dfetch-bin). " + "CI pushes updated PKGBUILDs directly via git using the AUR_SSH_KEY (A-12); " + "there is no PR review step — the push goes live immediately. " + "PKGBUILDs contain the SHA256 checksum of the release tarball; AUR helpers " + "such as ``yay`` download and verify the tarball before installing. " + "A compromised SSH key or a malicious PKGBUILD push could redirect consumers " + "to a tampered binary (DFT-36)." + ) + aur_repo.controls.hasAccessControl = True + aur_repo.controls.providesIntegrity = True + aur_repo.controls.usesCodeSigning = False + aur_repo.controls.isHardened = False + + df30 = Dataflow( + gh_actions_runner, + aur_repo, + "DF-30: AUR PKGBUILD push", + ) + df30.description = ( + "On release event, ``aur-publish.yml`` updates the PKGBUILD in ``aur/`` with " + "the new version and the real SHA256 checksum of the release tarball, then " + "pushes directly to the AUR git repository using the AUR_SSH_KEY (A-12). " + "Unlike Winget (DF-27), this push goes live immediately without a review gate. " + "Mitigated by: harden-runner egress block (C-013), SHA-pinned action (C-009), " + "AUR environment secret scoping (C-044), and real checksum in PKGBUILD (C-046)." + ) + df30.protocol = "SSH" + df30.controls.isEncrypted = True + df30.controls.isHardened = True + df30.controls.hasAccessControl = True + df30.controls.providesIntegrity = True + df30.controls.isNetworkFlow = True + + df31 = Dataflow(consumer, aur_repo, "DF-31: yay install dfetch-bin") + df31.description = ( + "Consumer runs ``yay -S dfetch-bin`` or equivalent AUR helper command. " + "The AUR helper clones the PKGBUILD from AUR (A-11), builds the package, " + "and installs it. " + "The PKGBUILD downloads the release tarball from GitHub Releases and verifies " + "its SHA256 checksum (C-046) before installing. " + "The consumer can additionally verify the binary attestations using " + "``gh attestation verify`` as documented in C-026 and C-039." + ) + df31.protocol = "HTTPS" + df31.controls.isEncrypted = True + df31.controls.isNetworkFlow = True + df31.controls.providesIntegrity = True + + df32 = Dataflow(aur_repo, consumer, "DF-32: Consumer downloads tarball via AUR") + df32.description = ( + "The AUR helper resolves the source URL from the PKGBUILD in A-11 and " + "downloads the tarball from GitHub Releases (A-01). " + "The SHA256 checksum declared in the PKGBUILD (C-046) is verified against " + "the downloaded archive before the binary is installed. " + "The consumer can additionally verify SLSA build provenance, SBOM, and VSA " + "attestations using ``gh attestation verify`` as documented in C-026 and C-039." + ) + df32.protocol = "HTTPS" + df32.controls.isEncrypted = True + df32.controls.providesIntegrity = True + df32.controls.isNetworkFlow = True + + def _make_sc_winget_elements_and_flows( gh_actions_runner: ExternalEntity, consumer: Actor, @@ -687,6 +794,7 @@ def _make_sc_elements_and_flows( _make_sc_publish_flow(gh_actions_runner, pypi) _make_sc_consumer_install_flows(consumer, pypi) _make_sc_winget_elements_and_flows(gh_actions_runner, consumer) + _make_sc_aur_elements_and_flows(gh_actions_runner, consumer) def build_model() -> TM: @@ -699,6 +807,7 @@ def build_model() -> TM: "Threat model for dfetch. " "Covers the pre-install lifecycle: code contribution, CI/CD, " "build (wheel / sdist), PyPI distribution, Winget manifest submission, " + "AUR PKGBUILD publication, " "and consumer installation. " "The installed dfetch package is the handoff point to tm_usage.py." ), @@ -725,6 +834,8 @@ def build_model() -> TM: "A-08b", "A-09", "A-10", + "A-11", + "A-12", } ASSET_CONTROLS: dict[str, list[Control]] = build_asset_controls_index( CONTROLS, _SUPPLY_CHAIN_ASSET_IDS @@ -930,6 +1041,13 @@ def build_model() -> TM: stride=["Tampering", "Spoofing"], target="A-09: Winget Community Repository (microsoft/winget-pkgs)", ), + ThreatResponse( + "DFT-36", + "mitigate", + risk="High", + stride=["Tampering", "Spoofing"], + target="A-11: AUR Repository (aur.archlinux.org/dfetch-bin)", + ), ] if __name__ == "__main__": From dc58e43cc6f7d63559ec1319248c7c08a17ab593 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 27 Jun 2026 14:21:20 +0000 Subject: [PATCH 2/7] Expand aur-publish.yml with full one-time setup instructions Add a header comment block covering: AUR account creation, dedicated SSH key pair generation, public key registration with AUR, GitHub 'aur' environment setup with the AUR_SSH_KEY secret, initial package creation via git clone/push, and local testing with makepkg. Co-Authored-By: Claude Sonnet 4.6 Claude-Session: https://claude.ai/code/session_013RYDLTQX5YRoR7gV5vFRM9 --- .../workflows/distribution/aur-publish.yml | 64 ++++++++++++++++--- 1 file changed, 56 insertions(+), 8 deletions(-) diff --git a/.github/workflows/distribution/aur-publish.yml b/.github/workflows/distribution/aur-publish.yml index 3d7e9e01..cf2034ff 100644 --- a/.github/workflows/distribution/aur-publish.yml +++ b/.github/workflows/distribution/aur-publish.yml @@ -1,3 +1,58 @@ +# Publish to AUR (Arch Linux User Repository) +# +# Pushes an updated PKGBUILD for dfetch-bin to AUR on every GitHub release. +# AUR uses a plain git repository — there is no PR or review gate; every push +# is immediately visible to users who run `yay -S dfetch-bin` or equivalent. +# +# ─── ONE-TIME SETUP ────────────────────────────────────────────────────────── +# +# 1. Create an AUR account +# Register at https://aur.archlinux.org/register/ if you do not already have +# one. Choose a bot username (e.g. "dfetch-bot") that clearly signals it is +# a machine account. +# +# 2. Generate a dedicated SSH key pair (do NOT reuse an existing key) +# +# ssh-keygen -t ed25519 -C "dfetch-aur-bot" -f aur_key +# +# This creates two files: aur_key (private) and aur_key.pub (public). +# Never commit either file; delete them from disk once the secrets are stored. +# +# 3. Register the public key with AUR +# Log in to https://aur.archlinux.org, go to My Account → SSH Public Keys, +# and paste the contents of aur_key.pub. +# +# 4. Create the GitHub 'aur' environment and store the private key as a secret +# a. Go to: Repository → Settings → Environments → New environment +# b. Name it exactly "aur". +# c. Recommended: add "Required reviewers" so every push is approved by a +# human before it reaches AUR users. +# d. Under "Environment secrets", click "Add secret": +# Name: AUR_SSH_KEY +# Value: paste the full contents of the aur_key *private* key file +# (including the -----BEGIN/END OPENSSH PRIVATE KEY----- lines). +# +# 5. Create or claim the dfetch-bin AUR package (first time only) +# Either submit a new package at https://aur.archlinux.org/submit/ or ask an +# existing maintainer to add the bot account as a co-maintainer. +# If creating fresh: +# git clone ssh://aur@aur.archlinux.org/dfetch-bin.git +# cp aur/PKGBUILD aur/.SRCINFO dfetch-bin/ +# cd dfetch-bin +# git add PKGBUILD .SRCINFO +# git commit -m "Initial import" +# git push +# The package page will appear at https://aur.archlinux.org/packages/dfetch-bin +# within seconds of the first push. +# +# 6. Test locally before triggering CI (optional but recommended) +# On an Arch Linux system (or in the archlinux Docker image): +# cd aur/ +# makepkg -si # builds and installs the package from source +# Verify `dfetch --version` matches the expected release. +# +# ───────────────────────────────────────────────────────────────────────────── + name: Publish to AUR on: @@ -49,14 +104,7 @@ jobs: persist-credentials: false - name: Update pkgver and checksums - # Requires AUR_SSH_KEY secret in the 'aur' environment. - # - # Setup — create an SSH key pair: - # 1. ssh-keygen -t ed25519 -C "dfetch-aur-bot" -f aur_key - # 2. Add the public key (aur_key.pub) to your AUR account at - # https://aur.archlinux.org/account// - # 3. Store the private key as secret AUR_SSH_KEY in: - # Repo → Settings → Environments → aur → Environment secrets + # Requires AUR_SSH_KEY secret — see one-time setup instructions at the top of this file. env: TAG: ${{ github.event.release.tag_name || inputs.release-tag }} run: | From ae4e1c2edf7947f0f23327266fa3388b6758229d Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 27 Jun 2026 14:28:31 +0000 Subject: [PATCH 3/7] Add reference doc links to aur-publish.yml setup instructions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Link each setup step to the relevant Arch Wiki or GitHub Docs page: - Step 1 → AUR submission guidelines (creating a new package) - Step 2 → AUR submission guidelines (authentication / SSH keys) - Step 4c → GitHub docs on required reviewers for environments - Step 4d → GitHub docs on environment secrets - Step 5 → AUR submission guidelines (submitting packages) - Step 6 → Arch Wiki makepkg page Co-Authored-By: Claude Sonnet 4.6 Claude-Session: https://claude.ai/code/session_013RYDLTQX5YRoR7gV5vFRM9 --- .github/workflows/distribution/aur-publish.yml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.github/workflows/distribution/aur-publish.yml b/.github/workflows/distribution/aur-publish.yml index cf2034ff..b493de09 100644 --- a/.github/workflows/distribution/aur-publish.yml +++ b/.github/workflows/distribution/aur-publish.yml @@ -10,6 +10,7 @@ # Register at https://aur.archlinux.org/register/ if you do not already have # one. Choose a bot username (e.g. "dfetch-bot") that clearly signals it is # a machine account. +# Ref: https://wiki.archlinux.org/title/AUR_submission_guidelines#Creating_a_new_package # # 2. Generate a dedicated SSH key pair (do NOT reuse an existing key) # @@ -17,6 +18,7 @@ # # This creates two files: aur_key (private) and aur_key.pub (public). # Never commit either file; delete them from disk once the secrets are stored. +# Ref: https://wiki.archlinux.org/title/AUR_submission_guidelines#Authentication # # 3. Register the public key with AUR # Log in to https://aur.archlinux.org, go to My Account → SSH Public Keys, @@ -27,10 +29,12 @@ # b. Name it exactly "aur". # c. Recommended: add "Required reviewers" so every push is approved by a # human before it reaches AUR users. +# Ref: https://docs.github.com/en/actions/managing-workflow-runs-and-deployments/managing-deployments/managing-environments-for-deployment#required-reviewers # d. Under "Environment secrets", click "Add secret": # Name: AUR_SSH_KEY # Value: paste the full contents of the aur_key *private* key file # (including the -----BEGIN/END OPENSSH PRIVATE KEY----- lines). +# Ref: https://docs.github.com/en/actions/security-for-github-actions/security-guides/using-secrets-in-github-actions#creating-secrets-for-an-environment # # 5. Create or claim the dfetch-bin AUR package (first time only) # Either submit a new package at https://aur.archlinux.org/submit/ or ask an @@ -44,12 +48,14 @@ # git push # The package page will appear at https://aur.archlinux.org/packages/dfetch-bin # within seconds of the first push. +# Ref: https://wiki.archlinux.org/title/AUR_submission_guidelines#Submitting_packages # # 6. Test locally before triggering CI (optional but recommended) # On an Arch Linux system (or in the archlinux Docker image): # cd aur/ # makepkg -si # builds and installs the package from source # Verify `dfetch --version` matches the expected release. +# Ref: https://wiki.archlinux.org/title/Makepkg # # ───────────────────────────────────────────────────────────────────────────── From 16a274b0971aa75abda564f9450563c3fad89da0 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 27 Jun 2026 14:37:41 +0000 Subject: [PATCH 4/7] Add makepkg test job to aur-publish.yml for push/PR runs On push or pull_request touching aur/ or the workflow itself, build the PKGBUILD in an archlinux container: resolve the latest release tag, compute the real SHA256, run makepkg, then install and verify with dfetch --version. The existing publish job is unchanged and still fires only on releases. Co-Authored-By: Claude Sonnet 4.6 Claude-Session: https://claude.ai/code/session_013RYDLTQX5YRoR7gV5vFRM9 --- .../workflows/distribution/aur-publish.yml | 46 +++++++++++++++++++ 1 file changed, 46 insertions(+) diff --git a/.github/workflows/distribution/aur-publish.yml b/.github/workflows/distribution/aur-publish.yml index b493de09..55e9d4ef 100644 --- a/.github/workflows/distribution/aur-publish.yml +++ b/.github/workflows/distribution/aur-publish.yml @@ -70,11 +70,57 @@ on: description: 'Release tag (e.g. 0.14.2)' required: true type: string + push: + paths: + - 'aur/**' + - '.github/workflows/distribution/aur-publish.yml' + pull_request: + paths: + - 'aur/**' + - '.github/workflows/distribution/aur-publish.yml' permissions: contents: read jobs: + test: + name: Test PKGBUILD + if: github.event_name == 'push' || github.event_name == 'pull_request' + runs-on: ubuntu-latest + container: + image: archlinux:latest + + steps: + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + with: + persist-credentials: false + + - name: Install build dependencies + run: | + pacman-key --init + pacman-key --populate archlinux + pacman -Syu --noconfirm base-devel curl python + + - name: Resolve latest release tag and update checksums + run: | + TAG=$(curl -fsSL "https://api.github.com/repos/dfetch-org/dfetch/releases/latest" \ + | python3 -c "import sys,json; print(json.load(sys.stdin)['tag_name'])") + URL="https://github.com/dfetch-org/dfetch/releases/download/${TAG}/dfetch-${TAG}-nix.tar.gz" + SHA256=$(curl -fsSL "$URL" | sha256sum | awk '{print $1}') + sed -i "s/^pkgver=.*/pkgver=${TAG}/" aur/PKGBUILD + sed -i "s/^sha256sums=.*/sha256sums=('${SHA256}')/" aur/PKGBUILD + + - name: Build package with makepkg + run: | + useradd -m builder + chown -R builder:builder aur/ + su builder -c "cd aur && makepkg --noconfirm" + + - name: Install package and verify + run: | + pacman -U --noconfirm aur/dfetch-bin-*.pkg.tar.zst + dfetch --version + publish: name: Publish to AUR # For releases: skip the rolling 'latest' tag on main. From 662482a311965bf4e25dee7119736463ffb5780d Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 27 Jun 2026 14:45:37 +0000 Subject: [PATCH 5/7] Rebuild AUR test job: build from main commit, fix PKGBUILD install Test job changes: - Trigger only on push to main (removes pull_request and release-based API call) - Split into build-binary (ubuntu-latest, Nuitka) + test (archlinux container) - build-binary compiles the current commit and creates dfetch--nix.tar.gz - test downloads the artifact, patches PKGBUILD with local tarball + real SHA256, runs makepkg, installs, and verifies dfetch --version PKGBUILD fix: - Nuitka standalone mode creates a binary + companion dfetch.dist/ directory; copying just the binary to /usr/bin is insufficient - Install entire build output to /opt/dfetch/ (mirroring the .deb layout) and symlink /usr/bin/dfetch -> /opt/dfetch/dfetch publish job fix: - Tighten if: condition to explicitly match only release and workflow_dispatch, preventing the new push trigger from inadvertently firing publish Co-Authored-By: Claude Sonnet 4.6 Claude-Session: https://claude.ai/code/session_013RYDLTQX5YRoR7gV5vFRM9 --- .../workflows/distribution/aur-publish.yml | 80 +++++++++++++++---- aur/PKGBUILD | 5 +- 2 files changed, 67 insertions(+), 18 deletions(-) diff --git a/.github/workflows/distribution/aur-publish.yml b/.github/workflows/distribution/aur-publish.yml index 55e9d4ef..0bb843e2 100644 --- a/.github/workflows/distribution/aur-publish.yml +++ b/.github/workflows/distribution/aur-publish.yml @@ -71,10 +71,8 @@ on: required: true type: string push: - paths: - - 'aur/**' - - '.github/workflows/distribution/aur-publish.yml' - pull_request: + branches: + - main paths: - 'aur/**' - '.github/workflows/distribution/aur-publish.yml' @@ -83,9 +81,51 @@ permissions: contents: read jobs: + build-binary: + name: Build Linux binary from source + if: github.event_name == 'push' + runs-on: ubuntu-latest + + steps: + - name: "Harden the runner (Block egress traffic: Only allow calls to allowed endpoints)" + uses: step-security/harden-runner@9af89fc71515a100421586dfdb3dc9c984fbf411 # v2.19.4 + with: + egress-policy: block + allowed-endpoints: >+ + github.com:443 + api.github.com:443 + pypi.org:443 + files.pythonhosted.org:443 + + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + with: + persist-credentials: false + fetch-depth: 0 + + - uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0 + with: + python-version: '3.13' + cache: 'pip' + + - name: Build binary with Nuitka + run: | + pip install .[build] + python script/build.py + + - name: Create tarball from build output + run: | + VERSION=$(git describe --tags --abbrev=0) + tar czf "dfetch-${VERSION}-nix.tar.gz" -C build/dfetch.dist . + + - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: dfetch-nix-tarball + path: dfetch-*-nix.tar.gz + test: name: Test PKGBUILD - if: github.event_name == 'push' || github.event_name == 'pull_request' + needs: build-binary + if: github.event_name == 'push' runs-on: ubuntu-latest container: image: archlinux:latest @@ -95,20 +135,26 @@ jobs: with: persist-credentials: false + - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: dfetch-nix-tarball + - name: Install build dependencies run: | pacman-key --init pacman-key --populate archlinux - pacman -Syu --noconfirm base-devel curl python + pacman -Syu --noconfirm base-devel - - name: Resolve latest release tag and update checksums + - name: Patch PKGBUILD to use local tarball run: | - TAG=$(curl -fsSL "https://api.github.com/repos/dfetch-org/dfetch/releases/latest" \ - | python3 -c "import sys,json; print(json.load(sys.stdin)['tag_name'])") - URL="https://github.com/dfetch-org/dfetch/releases/download/${TAG}/dfetch-${TAG}-nix.tar.gz" - SHA256=$(curl -fsSL "$URL" | sha256sum | awk '{print $1}') - sed -i "s/^pkgver=.*/pkgver=${TAG}/" aur/PKGBUILD - sed -i "s/^sha256sums=.*/sha256sums=('${SHA256}')/" aur/PKGBUILD + TARBALL=$(ls dfetch-*-nix.tar.gz) + VERSION=${TARBALL#dfetch-} + VERSION=${VERSION%-nix.tar.gz} + SHA256=$(sha256sum "$TARBALL" | awk '{print $1}') + cp "$TARBALL" aur/ + sed -i "s|^pkgver=.*|pkgver=${VERSION}|" aur/PKGBUILD + sed -i "s|^source=.*|source=(\"${TARBALL}\")|" aur/PKGBUILD + sed -i "s|^sha256sums=.*|sha256sums=('${SHA256}')|" aur/PKGBUILD - name: Build package with makepkg run: | @@ -123,11 +169,11 @@ jobs: publish: name: Publish to AUR - # For releases: skip the rolling 'latest' tag on main. - # For manual dispatch: always run (the operator knows what they're doing). + # Only on a real versioned release or a manual dispatch; skip the rolling + # 'latest' tag that main pushes create. if: >- - github.event_name != 'release' || - github.event.release.tag_name != 'latest' + github.event_name == 'workflow_dispatch' || + (github.event_name == 'release' && github.event.release.tag_name != 'latest') runs-on: ubuntu-latest concurrency: group: aur-publish-${{ github.event.release.tag_name || inputs.release-tag }} diff --git a/aur/PKGBUILD b/aur/PKGBUILD index ebb0f7b5..e8768e9b 100644 --- a/aur/PKGBUILD +++ b/aur/PKGBUILD @@ -12,5 +12,8 @@ source=("https://github.com/dfetch-org/dfetch/releases/download/${pkgver}/dfetch sha256sums=('SKIP') # replaced by CI with real checksum package() { - install -Dm755 dfetch "$pkgdir/usr/bin/dfetch" + install -dm755 "$pkgdir/opt/dfetch" + cp -a . "$pkgdir/opt/dfetch/" + install -dm755 "$pkgdir/usr/bin" + ln -sf /opt/dfetch/dfetch "$pkgdir/usr/bin/dfetch" } From 1db27f58f99ddd215b75d42f1b6a1aa22807fd96 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 27 Jun 2026 19:11:05 +0000 Subject: [PATCH 6/7] Fix AUR tarball gap, add provenance attestation, improve docs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit build.yml: - Create dfetch--nix.tar.gz (Linux only) from the Nuitka dist dir; this was the critical missing piece — the AUR publish job downloaded a URL that never existed in any release - Include *.tar.gz in SBOM attest, build-provenance attest, VSA attest, and all corresponding verify loops, so the AUR tarball has the same attestation chain as every other binary format aur-publish.yml (publish job): - Split the single "update checksums" step into two: first download the tarball, verify its Sigstore SLSA build-provenance attestation with gh attestation verify, then compute SHA256 from the already-downloaded file; workflow aborts before any AUR push if attestation fails - Add sigstore egress endpoints (fulcio, rekor, tuf-repo-cdn) to harden-runner allowlist to support gh attestation verify aur/PKGBUILD: - Expand placeholder comment: warn explicitly that the SKIP checksum means manual makepkg from this repo will not verify the binary, and direct users to the AUR package where the real checksum is set installation.rst: - Note that dfetch-bin is a pre-built binary (no Python or compilation needed on the user's machine) - Mention that the AUR helper verifies the SHA256, and link to the gh attestation tool for independent provenance verification security/tm_controls_data.py + threat_model_supply_chain.rst: - Add C-047: AUR release tarball build-provenance verified before PKGBUILD update; referenced from DFT-36 mitigations Co-Authored-By: Claude Sonnet 4.6 Claude-Session: https://claude.ai/code/session_013RYDLTQX5YRoR7gV5vFRM9 --- .github/workflows/build.yml | 16 ++++++++++++-- .../workflows/distribution/aur-publish.yml | 20 ++++++++++++++++-- aur/PKGBUILD | 5 ++++- doc/explanation/threat_model_supply_chain.rst | 6 +++++- doc/tutorials/installation.rst | 8 +++++++ security/tm_controls_data.py | 21 +++++++++++++++++-- 6 files changed, 68 insertions(+), 8 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index d7e93356..5b094dff 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -138,6 +138,13 @@ jobs: python script/build.py python script/package.py + - name: Create AUR tarball (Linux only) + if: runner.os == 'Linux' + run: | + VERSION=$(python -c "from dfetch import __version__; print(__version__)") + tar czf "build/dfetch-package/dfetch-${VERSION}-nix.tar.gz" \ + -C build/dfetch.dist . + - name: Save cache for clcache if: ${{ always() && matrix.platform == 'windows-latest' && steps.clcache-restore.outputs.cache-hit != 'true' }} uses: actions/cache/save@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 @@ -159,6 +166,7 @@ jobs: build/dfetch-package/*.rpm build/dfetch-package/*.pkg build/dfetch-package/*.msi + build/dfetch-package/*.tar.gz predicate-type: 'https://cyclonedx.org/bom' predicate-path: ${{ steps.find-sbom.outputs.path }} - name: Verify SBOM attestation @@ -166,7 +174,7 @@ jobs: env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | - for f in build/dfetch-package/*.deb build/dfetch-package/*.rpm build/dfetch-package/*.pkg build/dfetch-package/*.msi; do + for f in build/dfetch-package/*.deb build/dfetch-package/*.rpm build/dfetch-package/*.pkg build/dfetch-package/*.msi build/dfetch-package/*.tar.gz; do [ -f "$f" ] || continue gh attestation verify "$f" \ --repo "${{ github.repository }}" \ @@ -182,12 +190,13 @@ jobs: build/dfetch-package/*.rpm build/dfetch-package/*.pkg build/dfetch-package/*.msi + build/dfetch-package/*.tar.gz - name: Verify build provenance shell: bash env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | - for f in build/dfetch-package/*.deb build/dfetch-package/*.rpm build/dfetch-package/*.pkg build/dfetch-package/*.msi; do + for f in build/dfetch-package/*.deb build/dfetch-package/*.rpm build/dfetch-package/*.pkg build/dfetch-package/*.msi build/dfetch-package/*.tar.gz; do [ -f "$f" ] || continue gh attestation verify "$f" \ --repo "${{ github.repository }}" \ @@ -222,6 +231,7 @@ jobs: build/dfetch-package/*.rpm build/dfetch-package/*.pkg build/dfetch-package/*.msi + build/dfetch-package/*.tar.gz predicate-type: 'https://slsa.dev/verification_summary/v1' predicate-path: vsa-predicate.json - name: Store the distribution packages @@ -233,6 +243,7 @@ jobs: build/dfetch-package/*.rpm build/dfetch-package/*.pkg build/dfetch-package/*.msi + build/dfetch-package/*.tar.gz build/dfetch-package/*.cdx.json - name: Upload installer to release @@ -245,6 +256,7 @@ jobs: build/dfetch-package/*.rpm build/dfetch-package/*.pkg build/dfetch-package/*.msi + build/dfetch-package/*.tar.gz build/dfetch-package/*.cdx.json draft: true preserve_order: true diff --git a/.github/workflows/distribution/aur-publish.yml b/.github/workflows/distribution/aur-publish.yml index 0bb843e2..7cef0454 100644 --- a/.github/workflows/distribution/aur-publish.yml +++ b/.github/workflows/distribution/aur-publish.yml @@ -196,18 +196,34 @@ jobs: uploads.github.com:443 aur.archlinux.org:443 aur.archlinux.org:22 + fulcio.sigstore.dev:443 + rekor.sigstore.dev:443 + tuf-repo-cdn.sigstore.dev:443 - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: persist-credentials: false - - name: Update pkgver and checksums + - name: Download and verify release tarball + # Verifies Sigstore build-provenance attestation before trusting the tarball. # Requires AUR_SSH_KEY secret — see one-time setup instructions at the top of this file. env: + GH_TOKEN: ${{ github.token }} TAG: ${{ github.event.release.tag_name || inputs.release-tag }} run: | URL="https://github.com/dfetch-org/dfetch/releases/download/${TAG}/dfetch-${TAG}-nix.tar.gz" - SHA256=$(curl -fsSL "$URL" | sha256sum | awk '{print $1}') + curl -fsSL "$URL" -o dfetch-nix.tar.gz + gh attestation verify dfetch-nix.tar.gz \ + --repo "${{ github.repository }}" \ + --predicate-type https://slsa.dev/provenance/v1 \ + --cert-identity-regex "^https://github\.com/${{ github.repository }}/\.github/workflows/build\.yml@refs/tags/[0-9]+\.[0-9]+\.[0-9]+$" \ + --cert-oidc-issuer https://token.actions.githubusercontent.com + + - name: Update pkgver and checksums + env: + TAG: ${{ github.event.release.tag_name || inputs.release-tag }} + run: | + SHA256=$(sha256sum dfetch-nix.tar.gz | awk '{print $1}') sed -i "s/^pkgver=.*/pkgver=${TAG}/" aur/PKGBUILD sed -i "s/^sha256sums=.*/sha256sums=('${SHA256}')/" aur/PKGBUILD diff --git a/aur/PKGBUILD b/aur/PKGBUILD index e8768e9b..1a4cd2dd 100644 --- a/aur/PKGBUILD +++ b/aur/PKGBUILD @@ -9,7 +9,10 @@ license=('MIT') provides=('dfetch') conflicts=('dfetch') source=("https://github.com/dfetch-org/dfetch/releases/download/${pkgver}/dfetch-${pkgver}-nix.tar.gz") -sha256sums=('SKIP') # replaced by CI with real checksum +sha256sums=('SKIP') +# ^ CI replaces SKIP with the real SHA256 before pushing to AUR. +# Do NOT run makepkg manually from this repository copy — use the AUR package +# at https://aur.archlinux.org/packages/dfetch-bin where the real checksum is set. package() { install -dm755 "$pkgdir/opt/dfetch" diff --git a/doc/explanation/threat_model_supply_chain.rst b/doc/explanation/threat_model_supply_chain.rst index 0dffce59..9cf29e22 100644 --- a/doc/explanation/threat_model_supply_chain.rst +++ b/doc/explanation/threat_model_supply_chain.rst @@ -941,7 +941,7 @@ Threats | **Risk:** 🟠H | **STRIDE:** T S | **Status:** Mitigate - - C-044, C-046 + - C-044, C-046, C-047 Controls @@ -1044,3 +1044,7 @@ Controls - AUR PKGBUILD uses real SHA256 checksum of release tarball - DFT-36 - The ``aur-publish.yml`` workflow computes the SHA256 checksum of the release tarball at publish time and embeds it in the PKGBUILD, replacing the placeholder ``SKIP``. AUR helpers (``yay``, ``paru``) verify this checksum when building the package, so a tampered tarball would be rejected before installation. Residual risk: a compromised AUR_SSH_KEY (A-12) allows pushing a PKGBUILD with a fraudulent checksum — the review gate present in Winget (C-041) does not exist for AUR direct pushes. ``.github/workflows/distribution/aur-publish.yml`` + * - C-047 + - AUR release tarball build-provenance verified before PKGBUILD update + - DFT-36 + - Before computing the SHA256 to embed in the PKGBUILD, ``aur-publish.yml`` runs ``gh attestation verify`` against the downloaded release tarball. This confirms the tarball's Sigstore SLSA build-provenance attestation chain: the artifact was produced by the trusted ``build.yml`` workflow from a signed release tag, not tampered with in transit or substituted by a compromised release asset. If attestation fails the workflow aborts before any PKGBUILD update or AUR push. ``.github/workflows/distribution/aur-publish.yml`` diff --git a/doc/tutorials/installation.rst b/doc/tutorials/installation.rst index fe0bea88..2978b2fe 100644 --- a/doc/tutorials/installation.rst +++ b/doc/tutorials/installation.rst @@ -55,12 +55,20 @@ The version is automatically determined from the project and used to name the in Arch Linux (via AUR): + ``dfetch-bin`` is a pre-built binary — no Python installation or + compilation required on your machine. + .. code-block:: bash $ yay -S dfetch-bin # or with any other AUR helper, e.g. $ paru -S dfetch-bin + The AUR helper verifies the SHA256 checksum of the downloaded binary + before installing. You can also verify the release tarball's build + provenance independently using the `GitHub attestation tool + `_. + .. tab:: macOS Download the ``.pkg`` package from the releases page and install it. diff --git a/security/tm_controls_data.py b/security/tm_controls_data.py index eb3c746d..793606d9 100644 --- a/security/tm_controls_data.py +++ b/security/tm_controls_data.py @@ -2,7 +2,7 @@ All implemented security controls for dfetch, split by threat model scope: -* ``SC_CONTROLS`` — supply-chain controls (C-009 … C-042) +* ``SC_CONTROLS`` — supply-chain controls (C-009 … C-047) * ``USAGE_CONTROLS`` — runtime-usage controls (C-001 … C-045) This module is intentionally pytm-free so that ``compliance.py`` and other @@ -30,7 +30,7 @@ def to_pytm(self) -> str: return f"[{self.id}] {self.name}" -# ── Supply-chain controls (C-009 … C-046) ──────────────────────────────────── +# ── Supply-chain controls (C-009 … C-047) ──────────────────────────────────── SC_CONTROLS: list[Control] = [ Control( @@ -368,6 +368,23 @@ def to_pytm(self) -> str: "(C-041) does not exist for AUR direct pushes." ), ), + Control( + id="C-047", + name="AUR release tarball build-provenance verified before PKGBUILD update", + assets=["A-11", "A-12"], + threats=["DFT-36"], + reference=".github/workflows/distribution/aur-publish.yml", + description=( + "Before computing the SHA256 to embed in the PKGBUILD, ``aur-publish.yml`` " + "runs ``gh attestation verify`` against the downloaded release tarball. " + "This confirms the tarball's Sigstore SLSA build-provenance attestation " + "chain: the artifact was produced by the trusted " + "``build.yml`` workflow from a signed release tag, not tampered with " + "in transit or substituted by a compromised release asset. " + "If attestation fails the workflow aborts before any PKGBUILD update or " + "AUR push." + ), + ), ] # ── Runtime-usage controls (C-001 … C-045) ─────────────────────────────────── From 0c13704d879da715fa5d6fb5d380b9f072780dba Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 27 Jun 2026 19:41:03 +0000 Subject: [PATCH 7/7] Fix code review findings: PR trigger, tarball naming, container digest, docs - Add pull_request trigger to aur-publish.yml so makepkg validation runs on PRs touching aur/ or the workflow file, not just pushes to main - Fix AUR tarball naming in build.yml to use inputs.release_id directly instead of dfetch.__version__, which can drift for dev builds; also gate the step on versioned releases (not 'latest') - Pin archlinux container to immutable digest for reproducible test runs - Expand step-5 setup instructions to compute real SHA256 before the initial AUR push rather than copying the SKIP placeholder - Correct installation.rst wording: the AUR helper verifies the release tarball checksum, not the downloaded binary Co-Authored-By: Claude Sonnet 4.6 Claude-Session: https://claude.ai/code/session_013RYDLTQX5YRoR7gV5vFRM9 --- .github/workflows/build.yml | 7 +++-- .../workflows/distribution/aur-publish.yml | 26 +++++++++++++++---- doc/tutorials/installation.rst | 6 ++--- 3 files changed, 27 insertions(+), 12 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 5b094dff..cd104262 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -138,11 +138,10 @@ jobs: python script/build.py python script/package.py - - name: Create AUR tarball (Linux only) - if: runner.os == 'Linux' + - name: Create AUR tarball (Linux, versioned releases only) + if: runner.os == 'Linux' && inputs.release_id != 'latest' && inputs.release_id != '' run: | - VERSION=$(python -c "from dfetch import __version__; print(__version__)") - tar czf "build/dfetch-package/dfetch-${VERSION}-nix.tar.gz" \ + tar czf "build/dfetch-package/dfetch-${{ inputs.release_id }}-nix.tar.gz" \ -C build/dfetch.dist . - name: Save cache for clcache diff --git a/.github/workflows/distribution/aur-publish.yml b/.github/workflows/distribution/aur-publish.yml index 7cef0454..e2c05da0 100644 --- a/.github/workflows/distribution/aur-publish.yml +++ b/.github/workflows/distribution/aur-publish.yml @@ -39,10 +39,22 @@ # 5. Create or claim the dfetch-bin AUR package (first time only) # Either submit a new package at https://aur.archlinux.org/submit/ or ask an # existing maintainer to add the bot account as a co-maintainer. -# If creating fresh: +# If creating fresh, replace the SKIP placeholder with the real checksum +# *before* the first push — AUR users rely on it for integrity verification: +# # git clone ssh://aur@aur.archlinux.org/dfetch-bin.git -# cp aur/PKGBUILD aur/.SRCINFO dfetch-bin/ +# cp aur/PKGBUILD dfetch-bin/ # cd dfetch-bin +# +# # Compute the real SHA256 for the existing release tarball: +# VERSION= # e.g. 0.14.3 +# URL="https://github.com/dfetch-org/dfetch/releases/download/${VERSION}/dfetch-${VERSION}-nix.tar.gz" +# SHA256=$(curl -fsSL "$URL" | sha256sum | awk '{print $1}') +# sed -i "s|pkgver=.*|pkgver=${VERSION}|" PKGBUILD +# sed -i "s|sha256sums=.*|sha256sums=('${SHA256}')|" PKGBUILD +# # Regenerate .SRCINFO from the updated PKGBUILD (requires makepkg): +# makepkg --printsrcinfo > .SRCINFO +# # git add PKGBUILD .SRCINFO # git commit -m "Initial import" # git push @@ -76,6 +88,10 @@ on: paths: - 'aur/**' - '.github/workflows/distribution/aur-publish.yml' + pull_request: + paths: + - 'aur/**' + - '.github/workflows/distribution/aur-publish.yml' permissions: contents: read @@ -83,7 +99,7 @@ permissions: jobs: build-binary: name: Build Linux binary from source - if: github.event_name == 'push' + if: github.event_name == 'push' || github.event_name == 'pull_request' runs-on: ubuntu-latest steps: @@ -125,10 +141,10 @@ jobs: test: name: Test PKGBUILD needs: build-binary - if: github.event_name == 'push' + if: github.event_name == 'push' || github.event_name == 'pull_request' runs-on: ubuntu-latest container: - image: archlinux:latest + image: archlinux:latest@sha256:cc54f12fae7c81bd0d1a38f1694b4c28ffc708b358dd5876ab77dde49d37925a steps: - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 diff --git a/doc/tutorials/installation.rst b/doc/tutorials/installation.rst index 2978b2fe..0e11cf96 100644 --- a/doc/tutorials/installation.rst +++ b/doc/tutorials/installation.rst @@ -64,9 +64,9 @@ The version is automatically determined from the project and used to name the in # or with any other AUR helper, e.g. $ paru -S dfetch-bin - The AUR helper verifies the SHA256 checksum of the downloaded binary - before installing. You can also verify the release tarball's build - provenance independently using the `GitHub attestation tool + The AUR helper verifies the SHA256 checksum of the release tarball + before installing. You can also verify that tarball's build provenance + independently using the `GitHub attestation tool `_. .. tab:: macOS