container-device-interface 1.1.1

CDI (Container Device Interface), is a specification, for container-runtimes, to support third-party devices.
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
name: Release and Publish Rust Crate

on:
  workflow_dispatch:

# Default to no permissions; each job requests exactly what it needs.
permissions: {}

env:
  CARGO_TERM_COLOR: always

# Ordering: everything reversible first, the irreversible last. The GitHub
# release is fully assembled, verified, and published before crates.io is
# touched - a crates.io version can never be re-uploaded, a release can be
# deleted and the run repeated from scratch.
jobs:
  # Fail fast if any trace of the version already exists. Releases are
  # deterministic: a half-published version is reset manually (delete the
  # tag/release), never skipped over.
  version:
    name: Derive and validate version
    runs-on: ubuntu-24.04
    permissions:
      contents: read
    outputs:
      version: ${{ steps.version.outputs.file_version }}
      tag: ${{ steps.version.outputs.tag }}
    steps:
      - name: Checkout
        uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8
        with:
          persist-credentials: false

      - name: Install pinned Rust toolchain
        uses: ./.github/actions/rust-toolchain

      - name: Derive version and fail if it already exists
        id: version
        shell: bash
        run: |
          set -Eeuo pipefail
          FILE_VERSION=$(awk -F '"' '/^version[[:space:]]*=/ {print $2; exit}' Cargo.toml)
          if [[ -z "${FILE_VERSION:-}" ]]; then
            echo "::error::version not found in Cargo.toml"; exit 1
          fi
          TAG="v${FILE_VERSION}"
          if git ls-remote --exit-code --tags origin "refs/tags/${TAG}" >/dev/null 2>&1; then
            echo "::error::tag ${TAG} already exists"; exit 1
          fi
          echo "file_version=${FILE_VERSION}" >> "$GITHUB_OUTPUT"
          echo "tag=${TAG}" >> "$GITHUB_OUTPUT"

      # Surfaces packaging/manifest errors before any artifact is built,
      # so the real publish at the end cannot fail for packaging reasons.
      - name: Validate crate packaging (dry run)
        run: cargo publish --dry-run --locked

  # Build the release binaries (cdi, validate) and the cdylib per target,
  # package them as a reproducible tarball and keyless-sign it with cosign.
  build-artifacts:
    name: Build artifacts (${{ matrix.target }})
    needs: version
    strategy:
      matrix:
        include:
          - target: x86_64-unknown-linux-gnu
            runner: ubuntu-24.04
          - target: aarch64-unknown-linux-gnu
            runner: ubuntu-24.04-arm
    runs-on: ${{ matrix.runner }}
    permissions:
      contents: read
      id-token: write # keyless signing with Sigstore

    steps:
      - name: Checkout
        uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8
        with:
          persist-credentials: false

      - name: Install pinned Rust toolchain
        uses: ./.github/actions/rust-toolchain
        with:
          targets: ${{ matrix.target }}

      - name: Build (${{ matrix.target }})
        shell: bash
        # build-release.sh remaps machine-specific paths out of the binaries;
        # the reproducible-build workflow double-builds with the same script,
        # so the release bytes stay independently verifiable.
        run: |
          set -euxo pipefail
          mkdir -p dist
          ./scripts/build-release.sh "${{ matrix.target }}"
          cp "target/${{ matrix.target }}/release/cdi" dist/
          cp "target/${{ matrix.target }}/release/validate" dist/
          cp "target/${{ matrix.target }}/release/libcontainer_device_interface.so" dist/

      - name: Package release tarball (reproducible)
        shell: bash
        run: |
          set -euxo pipefail
          SOURCE_DATE_EPOCH=$(git log -1 --pretty=%ct)
          cd dist
          BASE="container-device-interface-${{ matrix.target }}"
          # Create reproducible tarball with stable mtimes/owners
          tar --sort=name --owner=0 --group=0 --numeric-owner --mtime="@${SOURCE_DATE_EPOCH}" \
            -cf "${BASE}.tar" \
            cdi validate libcontainer_device_interface.so
          xz -T0 -9e -f "${BASE}.tar"

      - name: Compute digest (sha256sum format)
        shell: bash
        run: |
          cd dist
          # "<64-hex><space><space><filename>" - consumed by the provenance job
          sha256sum "container-device-interface-${{ matrix.target }}.tar.xz" \
            | tee "container-device-interface-${{ matrix.target }}.tar.xz.sha256"

      - name: Install cosign
        uses: sigstore/cosign-installer@398d4b0eeef1380460a10c8013a76f728fb906ac # v3

      - name: Keyless sign tarball
        env:
          COSIGN_EXPERIMENTAL: "true"
        run: |
          # Cosign signing outputs: *.sig, *.cert, *.bundle.json (Rekor bundle)
          TARXZ="dist/container-device-interface-${{ matrix.target }}.tar.xz"
          cosign sign-blob --yes "$TARXZ" \
            --output-signature "${TARXZ}.sig" \
            --output-certificate "${TARXZ}.cert" \
            --bundle "${TARXZ}.bundle.json"

      - name: Upload dist as artifact (per target)
        uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
        with:
          name: dist-${{ matrix.target }}
          path: |
            dist/*.tar.xz
            dist/*.tar.xz.*
          if-no-files-found: error
          overwrite: true

  # One dependency SBOM from Cargo.lock covers both targets.
  sbom:
    name: Generate and sign SBOM
    needs: version
    runs-on: ubuntu-24.04
    permissions:
      contents: read
      id-token: write # keyless signing with Sigstore

    steps:
      - name: Checkout
        uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8
        with:
          persist-credentials: false

      # sbom-action writes output-file verbatim and does not create parent
      # directories; without this the job dies with ENOENT after the scan.
      - name: Create dist directory
        run: mkdir -p dist

      - name: Generate SBOM (SPDX JSON)
        uses: anchore/sbom-action@f8bdd1d8ac5e901a77a92f111440fdb1b593736b # v0.20.6
        with:
          path: .
          format: spdx-json
          output-file: dist/sbom-container-device-interface.spdx.json
          upload-artifact: false

      - name: Install cosign
        uses: sigstore/cosign-installer@398d4b0eeef1380460a10c8013a76f728fb906ac # v3

      - name: Keyless sign SBOM
        env:
          COSIGN_EXPERIMENTAL: "true"
        run: |
          SBOM="dist/sbom-container-device-interface.spdx.json"
          cosign sign-blob --yes "$SBOM" \
            --output-signature "${SBOM}.sig" \
            --output-certificate "${SBOM}.cert" \
            --bundle "${SBOM}.bundle.json"

      - name: Upload SBOM as artifact
        uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
        with:
          name: dist-sbom
          path: dist/*
          if-no-files-found: error
          overwrite: true

  # Draft release first; it is only published once every asset - including
  # SLSA provenance - is attached and verified. Publishing the draft is what
  # creates the tag (at target_commitish); no job pushes tags via git.
  create-release:
    name: Create draft GitHub Release
    needs: [version, build-artifacts, sbom]
    runs-on: ubuntu-24.04
    permissions:
      contents: write # create the draft release and upload assets
      actions: read   # download build artifacts
    steps:
      - name: Download all dist artifacts
        uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
        with:
          pattern: dist-*
          path: dist
          merge-multiple: true

      - name: Create draft GitHub Release and upload assets
        uses: softprops/action-gh-release@aec2ec56f94eb8180ceec724245f64ef008b89f5  # v2.4.0
        with:
          tag_name: ${{ needs.version.outputs.tag }}
          target_commitish: ${{ github.sha }}
          name: ${{ needs.version.outputs.tag }}
          draft: true
          body: |
            Release ${{ needs.version.outputs.version }}.
            Crate on crates.io: https://crates.io/crates/container-device-interface/${{ needs.version.outputs.version }}
          files: |
            dist/container-device-interface-x86_64-unknown-linux-gnu.tar.xz
            dist/container-device-interface-x86_64-unknown-linux-gnu.tar.xz.sig
            dist/container-device-interface-x86_64-unknown-linux-gnu.tar.xz.cert
            dist/container-device-interface-x86_64-unknown-linux-gnu.tar.xz.bundle.json
            dist/container-device-interface-aarch64-unknown-linux-gnu.tar.xz
            dist/container-device-interface-aarch64-unknown-linux-gnu.tar.xz.sig
            dist/container-device-interface-aarch64-unknown-linux-gnu.tar.xz.cert
            dist/container-device-interface-aarch64-unknown-linux-gnu.tar.xz.bundle.json
            dist/sbom-container-device-interface.spdx.json
            dist/sbom-container-device-interface.spdx.json.sig
            dist/sbom-container-device-interface.spdx.json.cert
            dist/sbom-container-device-interface.spdx.json.bundle.json

  provenance-hash-all:
    name: Collect provenance subjects
    needs: build-artifacts
    runs-on: ubuntu-24.04
    permissions:
      actions: read # download build artifacts
    outputs:
      map: ${{ steps.mkmap.outputs.map }}
    steps:
      - name: Download all dist artifacts
        uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
        with:
          pattern: dist-*
          path: dist
          merge-multiple: true
      - name: Build base64 map from *.sha256
        id: mkmap
        shell: bash
        run: |
          set -euo pipefail
          cd dist
          # Build JSON map: { "<tarball>": "<base64(sha256sum line)>", ... }
          echo '{}' > map.json
          for f in *.sha256; do
            [ -f "$f" ] || continue
            key="${f%.sha256}"
            b64=$(base64 -w0 "$f")
            jq --arg k "$key" --arg v "$b64" '. + {($k): $v}' map.json > tmp.json && mv tmp.json map.json
          done
          echo "map=$(jq -c . map.json)" >> "$GITHUB_OUTPUT"

  provenance:
    needs: provenance-hash-all
    strategy:
      matrix:
        target:
          - x86_64-unknown-linux-gnu
          - aarch64-unknown-linux-gnu
    permissions:
      actions: read
      id-token: write
      contents: write
    # The SLSA generator MUST be referenced by tag, not SHA - their docs and
    # verifier depend on it (slsa-framework/slsa-github-generator#722).
    uses: slsa-framework/slsa-github-generator/.github/workflows/generator_generic_slsa3.yml@v2.1.0 # zizmor: ignore[unpinned-uses]
    with:
      base64-subjects: ${{ fromJSON(needs.provenance-hash-all.outputs.map)[format('container-device-interface-{0}.tar.xz', matrix.target)] }}
      upload-assets: false

  provenance-publish:
    name: Attach provenance to draft Release
    needs: [version, provenance, create-release]
    runs-on: ubuntu-24.04
    permissions:
      contents: write # upload provenance to the draft release
      actions: read   # download provenance artifact
    strategy:
      matrix:
        target:
          - x86_64-unknown-linux-gnu
          - aarch64-unknown-linux-gnu
    steps:
      - name: Download provenance artifact for this target
        uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
        with:
          name: container-device-interface-${{ matrix.target }}.tar.xz.intoto.jsonl
          path: prov
      - name: Upload provenance to draft Release
        uses: softprops/action-gh-release@aec2ec56f94eb8180ceec724245f64ef008b89f5  # v2.4.0
        with:
          tag_name: ${{ needs.version.outputs.tag }}
          target_commitish: ${{ github.sha }}
          draft: true
          files: prov/container-device-interface-${{ matrix.target }}.tar.xz.intoto.jsonl

  publish-release:
    name: Verify assets and publish Release
    needs: [version, provenance-publish]
    runs-on: ubuntu-24.04
    permissions:
      contents: write # flip the draft release to published
    steps:
      - name: Verify draft release has all required assets
        id: draft
        env:
          GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
          TAG: ${{ needs.version.outputs.tag }}
        run: |
          set -euo pipefail

          # Drafts are not addressable by tag (the tag does not exist yet);
          # look the release up by tag_name and address it by id.
          RELEASE=$(gh api "repos/${{ github.repository }}/releases" \
            --jq "[.[] | select(.tag_name==\"${TAG}\" and .draft)][0]")
          if [[ -z "$RELEASE" || "$RELEASE" == "null" ]]; then
            echo "::error::no draft release found for ${TAG}"; exit 1
          fi
          RELEASE_ID=$(jq -r '.id' <<<"$RELEASE")
          echo "release_id=${RELEASE_ID}" >> "$GITHUB_OUTPUT"

          ASSETS=$(jq -r '.assets[].name' <<<"$RELEASE" | sort)

          # Define expected assets
          EXPECTED=(
            "container-device-interface-x86_64-unknown-linux-gnu.tar.xz"
            "container-device-interface-x86_64-unknown-linux-gnu.tar.xz.sig"
            "container-device-interface-x86_64-unknown-linux-gnu.tar.xz.cert"
            "container-device-interface-x86_64-unknown-linux-gnu.tar.xz.bundle.json"
            "container-device-interface-x86_64-unknown-linux-gnu.tar.xz.intoto.jsonl"
            "container-device-interface-aarch64-unknown-linux-gnu.tar.xz"
            "container-device-interface-aarch64-unknown-linux-gnu.tar.xz.sig"
            "container-device-interface-aarch64-unknown-linux-gnu.tar.xz.cert"
            "container-device-interface-aarch64-unknown-linux-gnu.tar.xz.bundle.json"
            "container-device-interface-aarch64-unknown-linux-gnu.tar.xz.intoto.jsonl"
            "sbom-container-device-interface.spdx.json"
            "sbom-container-device-interface.spdx.json.sig"
            "sbom-container-device-interface.spdx.json.cert"
            "sbom-container-device-interface.spdx.json.bundle.json"
          )

          MISSING=()
          for asset in "${EXPECTED[@]}"; do
            if ! echo "$ASSETS" | grep -qx "$asset"; then
              MISSING+=("$asset")
            fi
          done

          if [[ ${#MISSING[@]} -gt 0 ]]; then
            echo "ERROR: Draft release is missing required assets:" >&2
            printf '  - %s\n' "${MISSING[@]}" >&2
            exit 1
          fi

          echo "All ${#EXPECTED[@]} required assets present in draft release"

      # Publishing the draft creates the tag at target_commitish.
      - name: Publish draft release
        env:
          GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
          RELEASE_ID: ${{ steps.draft.outputs.release_id }}
        run: |
          gh api -X PATCH "repos/${{ github.repository }}/releases/${RELEASE_ID}" \
            -F draft=false

  # Trusted Publishing via OIDC: no CARGO_REGISTRY_TOKEN is needed.
  # Make sure the crate is configured on crates.io with a GitHub publisher
  # link (environment: release). Last on purpose: crates.io is the only
  # irreversible step, everything before it can be deleted and redone.
  publish-crate:
    name: Publish to crates.io (Trusted Publishing)
    needs: [version, publish-release]
    runs-on: ubuntu-24.04
    environment: release
    permissions:
      contents: read
      id-token: write # REQUIRED for crates.io trusted publishing (no API token secret)
    steps:
      - name: Checkout
        uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8
        with:
          persist-credentials: false

      - name: Install pinned Rust toolchain
        uses: ./.github/actions/rust-toolchain

      - name: Authenticate to crates.io (OIDC)
        id: auth
        uses: rust-lang/crates-io-auth-action@e919bc7605cde86df457cf5b93c5e103838bd879

      # Deliberately no existence pre-check anywhere: cargo publish itself
      # fails hard on a duplicate version - never skipped over.
      - name: Publish
        run: cargo publish --locked
        env:
          CARGO_REGISTRY_TOKEN: ${{ steps.auth.outputs.token }}

  verify-release:
    name: Verify signatures and provenance
    needs: [version, publish-crate]
    runs-on: ubuntu-24.04
    permissions:
      contents: read # download release assets
    steps:
      - name: Install Cosign
        uses: sigstore/cosign-installer@398d4b0eeef1380460a10c8013a76f728fb906ac # v3

      - name: Install SLSA verifier
        uses: slsa-framework/slsa-verifier/actions/installer@ea584f4502babc6f60d9bc799dbbb13c1caa9ee6

      - name: Download release assets
        env:
          GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
          TAG: ${{ needs.version.outputs.tag }}
        run: |
          gh release download "${TAG}" --repo "${{ github.repository }}" --dir .

      - name: Verify tarballs, SBOM signatures, Rekor, and SLSA provenance
        env:
          # Expanded via env (not inline templates) so no context value can be
          # interpreted as shell code.
          WORKFLOW_REF: ${{ github.workflow_ref }}
          WORKFLOW_NAME: ${{ github.workflow }}
          GH_REF: ${{ github.ref }}
        run: |
          set -euxo pipefail

          # Reusable Cosign identity pins (GitHub Actions keyless cert constraints)
          COSIGN_PINS=(
            --certificate-identity "https://github.com/${WORKFLOW_REF}"
            --certificate-oidc-issuer "https://token.actions.githubusercontent.com"
            --certificate-github-workflow-repository "${{ github.repository }}"
            --certificate-github-workflow-ref "${GH_REF}"
            --certificate-github-workflow-sha "${{ github.sha }}"
            --certificate-github-workflow-name "${WORKFLOW_NAME}"
            --certificate-github-workflow-trigger "${{ github.event_name }}"
          )

          for target in x86_64-unknown-linux-gnu aarch64-unknown-linux-gnu; do
            TARXZ="container-device-interface-${target}.tar.xz"

            # 1) ONLINE verification via Rekor transparency log
            cosign verify-blob \
              --rekor-url https://rekor.sigstore.dev \
              "${COSIGN_PINS[@]}" \
              --certificate "${TARXZ}.cert" \
              --signature   "${TARXZ}.sig" \
              "${TARXZ}"

            # 2) OFFLINE verification using the Sigstore bundle (no network)
            cosign verify-blob \
              --bundle "${TARXZ}.bundle.json" \
              "${COSIGN_PINS[@]}" \
              "${TARXZ}"

            # 3) Verify SLSA provenance (built from main)
            slsa-verifier verify-artifact "${TARXZ}" \
              --provenance-path "${TARXZ}.intoto.jsonl" \
              --source-uri "github.com/${{ github.repository }}" \
              --source-branch "main"
          done

          # SBOM signature - online and offline
          SBOM="sbom-container-device-interface.spdx.json"
          cosign verify-blob \
            --rekor-url https://rekor.sigstore.dev \
            "${COSIGN_PINS[@]}" \
            --certificate "${SBOM}.cert" \
            --signature   "${SBOM}.sig" \
            "${SBOM}"
          cosign verify-blob \
            --bundle "${SBOM}.bundle.json" \
            "${COSIGN_PINS[@]}" \
            "${SBOM}"