ci: add checks and a release path - #17
Conversation
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>
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>
🛡️ AI Agentic Security Code Review4 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
🗺️ Scan CoverageModules scanned: 2 · with findings: 1 · files: 4 · findings: 7
Executive Summary
🔒 Security IssuesConfirmed Vulnerabilities (4)🟡 pull_request trigger permits untrusted fork code execution during cargo clippy/test
🧠 AI Triage:
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. Vulnerable lines: 1, 35 🔁 Reproduction Steps:
🔎 Evidence: 💥 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:
⚖️ Triage Factors:
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:
Add an explicit least-privilege Vulnerable code: Secure code: Additional recommendations:
🟡 Third-party GitHub Actions pinned by mutable tag, not immutable SHA
🧠 AI Triage:
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. Vulnerable lines: 13, 35 🔁 Reproduction Steps:
🔎 Evidence: 💥 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:
⚖️ Triage Factors:
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:
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: Secure code: Additional recommendations:
🔵 Missing explicit least-privilege permissions block in ci.yml
🧠 AI Triage:
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 Vulnerable lines: 1, 17 🔁 Reproduction Steps:
🔎 Evidence: 💥 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:
⚖️ Triage Factors:
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:
Adding Vulnerable code: Secure code: Additional recommendations:
🟡 Tag-to-version string check provides no cryptographic provenance guarantee in publish.yml
🧠 AI Triage:
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. Vulnerable lines: 19, 33 🔁 Reproduction Steps:
🔎 Evidence: 💥 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:
⚖️ Triage Factors:
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:
Adding Vulnerable code: Secure code: Additional recommendations:
|
| 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.
Details
🛡️ Threat Model & Affect Analysis — PR #17
| Field | Value |
|---|---|
| Repository | OpenVTC/dtg-credentials |
| Branch | ci/workflows → main |
| 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:
- 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 againstdtolnay/rust-toolchain@stableorSwatinem/rust-cache@v2. - 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.ymland.github/workflows/publish.yml. - Next CI run on
push:branches[main]orpull_request(EP-001) or the tag-triggeredpublish.yml(EP-002) automatically pulls the malicious action version. - Malicious action code executes within the GitHub Actions runner context, which in
publish.ymlholdspermissions: id-token: writeand callsrust-lang/crates-io-auth-action@v1to mint a crates.io publishing token. - 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 legitimatecargo publish --lockedstep runs. - Attacker uses the exfiltrated token to publish a malicious version of
dtg-credentialsto crates.io, or directly runscargo publishwith 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:
- External attacker forks
dtg-credentialsand opens a pull request adding or modifying abuild.rs, aproc-macrodependency, or code underexamples/ortests/containing malicious logic (e.g. reading environment variables or network exfiltration). - GitHub Actions
pull_requesttrigger in.github/workflows/ci.ymlfires automatically for the fork's PR (no explicit approval gate is defined in the provided workflow). - The
clippy,test, andno-default-featuresjobs runcargo clippy --all-targets --all-features,cargo test --all-features, andcargo test --no-default-features, all of which compile and execute attacker-supplied Rust code (includingbuild.rsscripts and test binaries) within the ubuntu-latest runner. - Malicious code executes inside the runner with access to the job's default
GITHUB_TOKENand any repository/organization secrets exposed topull_request-triggered workflows (note: publish.yml is not triggered bypull_request, limiting but not eliminating blast radius since ci.yml runner secrets could still be probed). - 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:
- Organization or repository default workflow permissions are set to
read and write(a common legacy default for older repos/orgs). - Attacker-controlled pull request triggers
ci.ymlas described in STRIDE-2. - Because no
permissions:block is declared inci.yml, the implicitGITHUB_TOKENinherits the repository/organization default rather than being scoped tocontents: readonly. - Malicious code running inside
clippy/testjobs (per STRIDE-2) can use the ambientGITHUB_TOKEN(if write-scoped) to push commits, create releases, or modify repository settings via the GitHub API. - Attacker leverages this write access to inject a backdoor commit directly, bypassing further review, or to create a malicious tag matching
v*.*.*and triggerpublish.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:
- Maintainer or CI re-triggers
publish.ymltwice in quick succession for the same tag (e.g. accidental double tag push, orworkflow_dispatchtriggered manually while a tag-push run is still in flight). - Both runs independently execute the
curl -sSf https://index.crates.io/dt/g-/dtg-credentialscheck and both observe the version as NOT yet present (crates.io index CDN can lag behind the authoritative state after a publish). - Both runs proceed to
cargo publish --lockedfor the same crate version concurrently. - 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.
- 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:
- 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.
- 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 keepingCargo.tomlversion identical. publish.yml's tag push trigger (EP-002) fires on the re-pointed tag.- The 'Tag must match the crate version' step only string-compares
$GITHUB_REF_NAMEagainstCargo.toml's version field extracted viacargo metadata; it performs no verification of tag signature, commit provenance, or whether this tag was already published previously from a different commit SHA. - 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). - No signed-tag verification or GitHub OIDC
subclaim 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:
- Attacker or compromised low-privilege collaborator with
writeaccess (sufficient forworkflow_dispatch) navigates to the Actions tab and manually runspublish.ymlselecting an arbitrary branch (e.g. a feature branch, or evenmainbefore a version bump lands). - Because the run is
workflow_dispatchand not a tag push,github.refisrefs/heads/<branch>, notrefs/tags/vX.Y.Z. - 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. - Execution proceeds directly to 'Authenticate to crates.io' and then
cargo publish --locked, publishing whateverCargo.tomlversion currently exists on the selected branch, without any tag-based provenance check. - 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.
- 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:
- A release is published via
publish.yml(either through a legitimate tag push or theworkflow_dispatchpath described in STRIDE-6). - 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.
- No SLSA provenance attestation or
cargo publishbuild-provenance metadata is generated and attached to the crates.io release. - 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. - There is no independent, cryptographically verifiable record to resolve the dispute, undermining supply-chain trust for consumers of
dtg-credentials(e.g. theverifiable-trust-infrastructureproject 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:
- 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).
- The 'Publish' step's
curl -sSf https://index.crates.io/dt/g-/dtg-credentialsrequest is intercepted and answered with a forged JSON response indicating the target version IS already present. - The
jq -se --arg v "$version" 'any(.[]; .vers == $v)'check evaluates true based on the forged data. - The workflow logs
::notice::... is already on crates.io — nothing to doand 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). - 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:
- A downstream consumer (e.g. the
verifiable-trust-infrastructureproject referenced in publish.yml comments) addsdtg-credentialsas a dependency withdefault-features = falsefor a minimal build, unaware thataffinidi-signing(containing signing/verification logic) is part of the default feature set per the CI commentdefault = ["affinidi-signing"]. - 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. - Consumer code that expects to call signing/verification functionality (conceptually adjacent to
authority::verify_chainreferenced 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). - If any authority/authenticity verification code path degrades gracefully rather than failing to compile when
affinidi-signingis 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 aboutauthority::verify_chaindeciding '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:
- Attacker compromises an Ubuntu APT mirror or achieves a position to serve malicious packages during the CI runner's
apt-get update/installwindow (low likelihood given APT's GPG-signed repository metadata, but not zero given historical mirror compromise incidents). - The
clippy,test, andno-default-featuresjobs in ci.yml each independently runsudo apt-get install -y libpcsclite-dev libdbus-1-devwith no explicit version pin (e.g.libpcsclite-dev=1.9.9-2). - A malicious or backdoored version of
libdbus-1-dev/libpcsclite-devis installed, potentially including a malicious postinst script that executes withsudoprivileges on the runner. - 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:
- 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.
- The
msrvjob only runscargo check --all-features, which performs type/borrow checking but does not execute any test code, so the regression is not exercised. - The
testandno-default-featuresjobs run ondtolnay/rust-toolchain@stable(the newer stable toolchain), which does not reproduce the MSRV-specific behavior. - The change merges to main and is released via
publish.yml. - 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
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
📊 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 | |
|---|---|
| 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
…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>
This repository had no
.githubdirectory. Two releases' worth of a credential library — includingauthority::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 runningcargo publishfrom 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-infrastructureneedsauthority::verify_chainand cannot compile against it; that workspace deliberately has no[patch.crates-io], andvti-roomspublishes, 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), andcargo 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 avX.Y.Ztag push.release: published: a Release created with the defaultGITHUB_TOKENdoes not cascade-trigger other workflows, so that handler would silently never fire.OpenVTC, repositorydtg-credentials, workflowpublish.yml. Until that exists the job fails at the auth step naming exactly this.Cargo.tomlbefore 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.crate version already uploaded, which would otherwise force a version bump nothing needed.What CI found on its first run
The
--no-default-featuresjob failed. Both examples call.sign(), which lives behindaffinidi-signing, so a consumer who disables default features got a missing-method error naming the method rather than the disabled backend. They now declarerequired-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
git tag v0.6.0 && git push origin v0.6.0.Cargo.tomlalready reads0.6.0and the changelog section is already written, so nothing else is needed.