container-device-interface 1.1.0

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

jobs:
  publish:
    name: Publish to crates.io (Trusted Publishing)
    runs-on: ubuntu-24.04
    environment: release
    permissions:
      contents: write   # needed to create/push tags
      id-token: write   # REQUIRED for crates.io trusted publishing (no API token secret)
    outputs:
      version: ${{ steps.version.outputs.file_version }}
      tag: ${{ steps.version.outputs.tag }}
    steps:
      - name: Checkout
        # Credentials are deliberately persisted: this job pushes the release
        # tag back to the repository.
        uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # zizmor: ignore[artipacked]
        with:
          fetch-depth: 0  # ensure tags/history are available

      - 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
      - name: Derive version from Cargo.toml and create tag
        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}"
          echo "file_version=${FILE_VERSION}" >> "$GITHUB_OUTPUT"
          echo "tag=${TAG}" >> "$GITHUB_OUTPUT"
          # Create and push tag; if it already exists, this will fail (desired)
          git tag "${TAG}"
          git push origin "${TAG}"

      # Trusted Publishing via OIDC: no CARGO_REGISTRY_TOKEN is needed.
      # Make sure the crate is configured on crates.io with a GitHub publisher link.
      - name: Publish
        run: cargo publish
        env:
          CARGO_REGISTRY_TOKEN: ${{ steps.auth.outputs.token }}

  # 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: publish
    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: publish
    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

      - 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.
  create-release:
    name: Create draft GitHub Release
    needs: [publish, 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.publish.outputs.tag }}
          name: ${{ needs.publish.outputs.tag }}
          draft: true
          body: |
            Release ${{ needs.publish.outputs.version }} published to crates.io.
            Crate on crates.io: https://crates.io/crates/container-device-interface
          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: [publish, 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.publish.outputs.tag }}
          draft: true
          files: prov/container-device-interface-${{ matrix.target }}.tar.xz.intoto.jsonl

  publish-release:
    name: Verify assets and publish Release
    needs: [publish, 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
        env:
          GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
          TAG: ${{ needs.publish.outputs.tag }}
        run: |
          set -euo pipefail

          # Get list of assets in the draft release
          ASSETS=$(gh release view "$TAG" --repo "${{ github.repository }}" --json assets --jq '.assets[].name' | 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"

      - name: Publish draft release
        env:
          GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
          TAG: ${{ needs.publish.outputs.tag }}
        run: |
          gh release edit "${TAG}" \
            --repo "${{ github.repository }}" \
            --draft=false

  verify-release:
    name: Verify signatures and provenance
    needs: [publish, publish-release]
    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.publish.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}"