diff --git a/.github/dependabot.yml b/.github/dependabot.yml deleted file mode 100644 index f1084db..0000000 --- a/.github/dependabot.yml +++ /dev/null @@ -1,22 +0,0 @@ -version: 2 -updates: - # Cargo dependencies - - package-ecosystem: cargo - directory: / - schedule: - interval: weekly - commit-message: - prefix: "deps" - groups: - rust-dependencies: - patterns: - - "*" - open-pull-requests-limit: 5 - - # GitHub Actions - - package-ecosystem: github-actions - directory: / - schedule: - interval: weekly - commit-message: - prefix: "ci" diff --git a/.github/workflows/attestation-check.yml b/.github/workflows/attestation-check.yml new file mode 100644 index 0000000..21d162a --- /dev/null +++ b/.github/workflows/attestation-check.yml @@ -0,0 +1,106 @@ +name: Attestation Health Check + +on: + schedule: + - cron: '23 3 * * 1' # Weekly Monday 3:23am UTC + workflow_dispatch: + +jobs: + verify: + name: Verify Latest Release Attestations + runs-on: cachekit + permissions: + contents: read + issues: write + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + + - name: Get latest release tag + id: release + run: | + TAG=$(gh release list --repo ${{ github.repository }} --limit 1 --json tagName --jq '.[0].tagName' 2>/dev/null || echo "") + if [ -z "$TAG" ]; then + echo "No releases found, skipping" + echo "skip=true" >> "$GITHUB_OUTPUT" + else + echo "tag=$TAG" >> "$GITHUB_OUTPUT" + echo "skip=false" >> "$GITHUB_OUTPUT" + fi + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Download release asset + if: steps.release.outputs.skip != 'true' + id: asset + run: | + TAG="${{ steps.release.outputs.tag }}" + mkdir -p /tmp/release-assets + gh release download "$TAG" --repo ${{ github.repository }} \ + --pattern "*.crate" --dir /tmp/release-assets 2>/dev/null || true + CRATE=$(find /tmp/release-assets -name '*.crate' -print -quit) + if [ -z "$CRATE" ]; then + echo "::warning::No .crate asset found for $TAG, checking repo-level attestations" + echo "has_asset=false" >> "$GITHUB_OUTPUT" + else + echo "asset=$CRATE" >> "$GITHUB_OUTPUT" + echo "has_asset=true" >> "$GITHUB_OUTPUT" + fi + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Verify provenance attestation + if: steps.release.outputs.skip != 'true' + run: | + TAG="${{ steps.release.outputs.tag }}" + echo "Verifying provenance attestation for $TAG" + if [ "${{ steps.asset.outputs.has_asset }}" = "true" ]; then + gh attestation verify "${{ steps.asset.outputs.asset }}" \ + --repo ${{ github.repository }} \ + --predicate-type https://slsa.dev/provenance/v1 + else + # No downloadable .crate asset — verify attestations exist for the repo + PROVENANCE=$(gh attestation list --repo ${{ github.repository }} \ + --predicate-type https://slsa.dev/provenance/v1 --limit 1 --jq 'length') + if [ "$PROVENANCE" = "0" ] || [ -z "$PROVENANCE" ]; then + echo "::error::No provenance attestation found" + exit 1 + fi + fi + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Verify SBOM attestation + if: steps.release.outputs.skip != 'true' + run: | + TAG="${{ steps.release.outputs.tag }}" + echo "Verifying SBOM attestation for $TAG" + if [ "${{ steps.asset.outputs.has_asset }}" = "true" ]; then + gh attestation verify "${{ steps.asset.outputs.asset }}" \ + --repo ${{ github.repository }} \ + --predicate-type https://spdx.dev/Document/v2.3 || \ + gh attestation verify "${{ steps.asset.outputs.asset }}" \ + --repo ${{ github.repository }} \ + --predicate-type https://cyclonedx.org/bom/v1.6 + else + SBOM=$(gh attestation list --repo ${{ github.repository }} \ + --predicate-type https://cyclonedx.org/bom/v1.6 --limit 1 --jq 'length' 2>/dev/null || echo "0") + SPDX=$(gh attestation list --repo ${{ github.repository }} \ + --predicate-type https://spdx.dev/Document/v2.3 --limit 1 --jq 'length' 2>/dev/null || echo "0") + if [ "$SBOM" = "0" ] && [ "$SPDX" = "0" ]; then + echo "::error::No SBOM attestation found (checked CycloneDX and SPDX)" + exit 1 + fi + fi + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Open issue on failure + if: failure() + run: | + gh issue create \ + --repo ${{ github.repository }} \ + --title "Attestation verification failed for ${{ steps.release.outputs.tag }}" \ + --body "Weekly attestation health check failed. Verify that the release workflow produced valid provenance and SBOM attestations." \ + --label "bug" + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 78fae2a..84b5730 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -11,37 +11,42 @@ concurrency: jobs: test: name: ${{ matrix.rust }} / ${{ matrix.os }} - runs-on: ${{ matrix.os }} + runs-on: ${{ matrix.runner }} continue-on-error: ${{ matrix.rust == 'beta' }} strategy: fail-fast: false matrix: - # Full OS matrix for stable only; MSRV and beta on ubuntu only + # Full OS matrix for stable only; MSRV and beta on self-hosted only include: # MSRV - ensures we don't use newer Rust features - rust: "1.85" os: ubuntu-latest + runner: cachekit # Stable - primary target, all platforms - rust: stable os: ubuntu-latest + runner: cachekit - rust: stable os: macos-latest + runner: macos-latest - rust: stable os: windows-latest + runner: windows-latest # Beta - early warning (allowed to fail) - rust: beta os: ubuntu-latest + runner: cachekit steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 - name: Install Rust toolchain - uses: dtolnay/rust-toolchain@master + uses: dtolnay/rust-toolchain@3c5f7ea28cd621ae0bf5283f0e981fb97b8a7af9 # master with: toolchain: ${{ matrix.rust }} - components: ${{ matrix.rust != '1.80' && 'rustfmt, clippy' || '' }} + components: ${{ matrix.rust != '1.85' && 'rustfmt, clippy' || '' }} - name: Cache cargo registry - uses: Swatinem/rust-cache@v2 + uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # v2 with: cache-all-crates: true @@ -62,15 +67,15 @@ jobs: run: cargo test --features ffi security: - runs-on: ubuntu-latest + runs-on: cachekit steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 - name: Install Rust toolchain - uses: dtolnay/rust-toolchain@stable + uses: dtolnay/rust-toolchain@29eef336d9b2848a0b548edc03f92a220660cdb8 # stable - name: Cache cargo registry - uses: Swatinem/rust-cache@v2 + uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # v2 with: cache-all-crates: true diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 8229a7c..6ac6a19 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -16,20 +16,20 @@ concurrency: jobs: analyze: name: Analyze - runs-on: ubuntu-latest + runs-on: cachekit permissions: actions: read contents: read security-events: write steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 - name: Install Rust toolchain - uses: dtolnay/rust-toolchain@stable + uses: dtolnay/rust-toolchain@29eef336d9b2848a0b548edc03f92a220660cdb8 # stable - name: Initialize CodeQL - uses: github/codeql-action/init@v4 + uses: github/codeql-action/init@95e58e9a2cdfd71adc6e0353d5c52f41a045d225 # v4 with: languages: cpp # Analyze generated C FFI headers and cross-language test code @@ -45,6 +45,6 @@ jobs: cc -std=c99 -Wall -Wextra -I../../include -c test_c.c -o test_c.o - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v4 + uses: github/codeql-action/analyze@95e58e9a2cdfd71adc6e0353d5c52f41a045d225 # v4 with: category: "/language:cpp" diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index c3e43fe..d2f5331 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -11,18 +11,18 @@ permissions: jobs: release-please: - runs-on: ubuntu-latest + runs-on: cachekit outputs: release_created: ${{ steps.release.outputs.release_created }} tag_name: ${{ steps.release.outputs.tag_name }} steps: - - uses: actions/create-github-app-token@v2 + - uses: actions/create-github-app-token@fee1f7d63c2ff003460e3d139729b119787bc349 # v2 id: app-token with: app-id: ${{ secrets.APP_ID }} private-key: ${{ secrets.APP_PRIVATE_KEY }} - - uses: googleapis/release-please-action@v4 + - uses: googleapis/release-please-action@8b8fd2cc23b2e18957157a9d923d75aa0c6f6ad5 # v4 id: release with: token: ${{ steps.app-token.outputs.token }} @@ -30,19 +30,45 @@ jobs: publish: needs: release-please if: ${{ needs.release-please.outputs.release_created }} - runs-on: ubuntu-latest + runs-on: cachekit + permissions: + contents: read + id-token: write + attestations: write steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 - name: Install Rust toolchain - uses: dtolnay/rust-toolchain@stable + uses: dtolnay/rust-toolchain@29eef336d9b2848a0b548edc03f92a220660cdb8 # stable - name: Cache cargo registry - uses: Swatinem/rust-cache@v2 + uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # v2 - name: Run tests before publish run: cargo test --all-features + - name: Package crate + run: cargo package + + - name: Attest Build Provenance + uses: actions/attest-build-provenance@96b4a1ef7235a096b17240c259729fdd70c83d45 # v2 + with: + subject-path: target/package/*.crate + + - name: Install cargo-sbom + run: cargo install cargo-sbom --locked + + - name: Generate SBOM + run: | + cargo sbom --output-format cyclone_dx_json_1_6 > sbom.cdx.json + test -s sbom.cdx.json || { echo "::error::SBOM file is empty"; exit 1; } + + - name: Attest SBOM + uses: actions/attest-sbom@10926c72720ffc3f7b666661c8e55b1344e2a365 # v2 + with: + subject-path: target/package/*.crate + sbom-path: sbom.cdx.json + - name: Publish to crates.io run: cargo publish env: diff --git a/.github/workflows/security.yml b/.github/workflows/security.yml index e796787..ef3c867 100644 --- a/.github/workflows/security.yml +++ b/.github/workflows/security.yml @@ -21,20 +21,20 @@ env: jobs: fast-security: name: Fast Security Checks - runs-on: ubuntu-latest + runs-on: cachekit if: github.event_name == 'push' || github.event_name == 'pull_request' steps: - name: Checkout code - uses: actions/checkout@v6 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 - name: Install Rust toolchain - uses: dtolnay/rust-toolchain@stable + uses: dtolnay/rust-toolchain@29eef336d9b2848a0b548edc03f92a220660cdb8 # stable with: toolchain: "stable" components: clippy - name: Cache Rust dependencies - uses: actions/cache@v5 + uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5 with: path: | ~/.cargo/registry/index/ @@ -66,7 +66,7 @@ jobs: quick-fuzz: name: Quick Fuzz (Corpus Only) - runs-on: ubuntu-latest + runs-on: cachekit if: github.event_name == 'push' || github.event_name == 'pull_request' strategy: fail-fast: false @@ -90,13 +90,13 @@ jobs: - key_derivation steps: - name: Checkout code - uses: actions/checkout@v6 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 - name: Install Rust toolchain - uses: dtolnay/rust-toolchain@nightly + uses: dtolnay/rust-toolchain@5b842231ba77f5c045dba54ac5560fed2db780e2 # nightly - name: Cache Rust dependencies - uses: actions/cache@v5 + uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5 with: path: | ~/.cargo/registry/index/ @@ -121,7 +121,7 @@ jobs: deep-fuzz: name: Deep Fuzzing (8 hours) - runs-on: ubuntu-latest + runs-on: cachekit if: github.event_name == 'schedule' strategy: fail-fast: false @@ -145,13 +145,13 @@ jobs: - key_derivation steps: - name: Checkout code - uses: actions/checkout@v6 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 - name: Install Rust toolchain - uses: dtolnay/rust-toolchain@nightly + uses: dtolnay/rust-toolchain@5b842231ba77f5c045dba54ac5560fed2db780e2 # nightly - name: Cache Rust dependencies - uses: actions/cache@v5 + uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5 with: path: | ~/.cargo/registry/index/ @@ -176,7 +176,7 @@ jobs: - name: Upload crash artifacts if: always() - uses: actions/upload-artifact@v7 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 with: name: fuzz-crashes-${{ matrix.target }} path: fuzz/artifacts/${{ matrix.target }}/ @@ -184,19 +184,19 @@ jobs: kani: name: Kani Formal Verification - runs-on: ubuntu-latest + runs-on: cachekit if: github.event_name == 'schedule' steps: - name: Checkout code - uses: actions/checkout@v6 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 - name: Install Rust toolchain - uses: dtolnay/rust-toolchain@stable + uses: dtolnay/rust-toolchain@3c5f7ea28cd621ae0bf5283f0e981fb97b8a7af9 # master with: toolchain: "1.85" - name: Cache Rust dependencies - uses: actions/cache@v5 + uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5 with: path: | ~/.cargo/registry/index/ @@ -219,19 +219,19 @@ jobs: cargo-vet: name: Cargo Vet (Supply Chain) - runs-on: ubuntu-latest + runs-on: cachekit if: github.event_name == 'schedule' steps: - name: Checkout code - uses: actions/checkout@v6 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 - name: Install Rust toolchain - uses: dtolnay/rust-toolchain@stable + uses: dtolnay/rust-toolchain@3c5f7ea28cd621ae0bf5283f0e981fb97b8a7af9 # master with: toolchain: "1.85" - name: Cache Rust dependencies - uses: actions/cache@v5 + uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5 with: path: | ~/.cargo/registry/index/ @@ -251,19 +251,19 @@ jobs: sbom: name: Generate SBOM - runs-on: ubuntu-latest + runs-on: cachekit if: github.event_name == 'release' steps: - name: Checkout code - uses: actions/checkout@v6 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 - name: Install Rust toolchain - uses: dtolnay/rust-toolchain@stable + uses: dtolnay/rust-toolchain@3c5f7ea28cd621ae0bf5283f0e981fb97b8a7af9 # master with: toolchain: "1.85" - name: Cache Rust dependencies - uses: actions/cache@v5 + uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5 with: path: | ~/.cargo/registry/index/ @@ -282,7 +282,7 @@ jobs: run: cargo sbom > cachekit-core-sbom.json - name: Upload SBOM as release asset - uses: actions/upload-release-asset@v1 + uses: actions/upload-release-asset@e8f9f06c4b078e705bd2ea027f0926603fc9b4d5 # v1 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} with: diff --git a/deny.toml b/deny.toml index 17ea1fd..d897dce 100644 --- a/deny.toml +++ b/deny.toml @@ -70,16 +70,11 @@ deny = [] # Skip specific dependencies from multiple-version checks # These are transitive dependencies where version duplication is unavoidable skip = [ - # getrandom has 3 major versions in the dep tree: - # 0.2.x via aes-gcm/crypto-common (encryption) - # 0.3.x via proptest (dev-dependency) - # 0.4.x via tempfile/cbindgen (build-dependency) + # getrandom 0.2.x (aes-gcm crypto chain) vs 0.4.x (tempfile/cbindgen build-dep) { crate = "getrandom@0.2", reason = "Transitive via aes-gcm crypto chain" }, - { crate = "getrandom@0.3", reason = "Transitive via proptest (dev-dependency)" }, - # rand_core duplication from aes-gcm (0.6.x) vs proptest (0.9.x) - { crate = "rand_core@0.6", reason = "Transitive via aes-gcm crypto chain" }, - # libc duplication unavoidable (getrandom versions pull different libc) - { crate = "libc@0.2", reason = "Transitive via multiple getrandom versions" }, + # winnow 0.7.x vs 1.x — both pulled by toml@0.9 (direct + via toml_parser) + # through cbindgen build-dependency; internal split, can't resolve + { crate = "winnow@0.7", reason = "Transitive via toml/cbindgen (build-dependency)" }, ] # Skip crate trees entirely (e.g., frequently-updated foundational crates) diff --git a/renovate.json b/renovate.json new file mode 100644 index 0000000..262f53f --- /dev/null +++ b/renovate.json @@ -0,0 +1,4 @@ +{ + "$schema": "https://docs.renovatebot.com/renovate-schema.json", + "extends": ["github>cachekit-io/renovate-config"] +}