enprot 0.5.77

Engyon Protected Text (EPT) — confidentiality processor and capability ledger
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
name: deploy

# Prevent overlapping deploy runs for the same tag. Don't cancel
# in-progress runs — a partially-built release is worse than waiting.
concurrency:
  group: deploy-${{ github.ref }}
  cancel-in-progress: false

on:
  push:
    tags:
      # release-plz now uses per-crate tags in a workspace:
      #   v0.5.X            (legacy, pre-split)
      #   enprot-v0.5.X     (enprot CLI + library, post-split)
      #   enprot-ffi-v0.5.X (the cdylib — not deployed, no binary)
      # Match the first two; enprot-ffi has no standalone binary to ship.
      - 'v[0-9]*.[0-9]*.[0-9]*'
      - '[0-9]*.[0-9]*.[0-9]*'
      - 'enprot-v[0-9]*.[0-9]*.[0-9]*'
  # Allow manual trigger to rebuild binaries for existing tags
  # (e.g., v0.5.0 was tagged before the deploy pattern was fixed).
  # Test builds: omit `tag` and set `ref` to a branch to exercise
  # the archive matrix without publishing anything (used to iterate
  # on cross-compile fixes without cutting releases — issue #368),
  # optionally narrowed to one matrix leg with `target`.
  workflow_dispatch:
    inputs:
      tag:
        description: 'Tag to deploy (e.g., v0.5.0, enprot-v0.5.13). Empty = test build (no publish).'
        required: false
        type: string
      ref:
        description: 'Checkout ref override (defaults to the tag; a branch for test builds)'
        required: false
        type: string
      target:
        description: 'Build only this matrix target (e.g., x86_64-unknown-linux-musl)'
        required: false
        type: string

env:
  CROSS_VERSION: 0.2.5
  BOTAN_VERSION: 3.7.0
  PROJECT_NAME: enprot
  EXE_NAME: enprot
  # Reproducible-build timestamp: pinned to the deploy tag's commit
  # time so rustc/linker timestamps are stable across reruns (see
  # docs/src/pages/docs/reference/reproducible-builds.md). Overridden per-job from git after
  # checkout.
  SOURCE_DATE_EPOCH: '0'
  # Resolved tag for both trigger types:
  #   tag push → github.ref_name = "enprot-v0.5.13" or "v0.5.13"
  #   workflow_dispatch → inputs.tag = "enprot-v0.5.13"
  # Jobs strip any non-digit prefix (both `v` and `enprot-v`) to get
  # the bare version (0.5.13) via `sed 's/^[^0-9]*//'`.
  # Tag pushes always carry a tag; dispatch carries one only when
  # `tag` is set (empty otherwise — a test build).
  DEPLOY_TAG: ${{ github.event_name == 'workflow_dispatch' && inputs.tag || github.event_name != 'workflow_dispatch' && github.ref_name || '' }}
  # What every job checks out: an explicit ref override (test
  # builds), else the tag, else the pushed ref.
  DEPLOY_REF: ${{ inputs.ref || (github.event_name == 'workflow_dispatch' && inputs.tag) || github.ref_name }}

jobs:
  checks:
    name: Checks
    runs-on: ubuntu-latest
    timeout-minutes: 5
    steps:
      - uses: actions/checkout@v7
        with:
          ref: ${{ env.DEPLOY_REF }}
      - name: Check version
        if: env.DEPLOY_TAG != ''
        run: |
          set -euxo pipefail
          # Strip any non-digit prefix (handles 'v', 'enprot-v', bare).
          TAG="$(echo "$DEPLOY_TAG" | sed 's/^[^0-9]*//')"
          if [ "$(grep '^version' Cargo.toml | cut -d '"' -f2)" != "$TAG" ]; then
            echo "Version tag ($TAG) does not match manifest"
            exit 1
          fi

  generate-extras:
    name: man page + completions
    needs: [checks]
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v7
        with:
          ref: ${{ env.DEPLOY_REF }}
      - name: Install rust toolchain
        uses: dtolnay/rust-toolchain@stable
      - name: Restore /usr/local C-stack cache
        # install.sh builds Botan + librnp (~20 min); the guards we
        # added there skip both wholesale when the cache is warm.
        # /usr/local on a bare ubuntu runner is tiny, so caching it
        # wholesale is cheap and cannot shadow runner tooling that
        # would matter (that lives in /usr, which we leave alone).
        uses: actions/cache@v6
        with:
          path: /usr/local
          key: extras-stack-${{ env.BOTAN_VERSION }}-${{ hashFiles('ci/install.sh', 'ci/build-librnp.sh') }}
      - name: Install dependencies
        env:
          BOTAN_VERSION: ${{ env.BOTAN_VERSION }}
          PREFIX: /usr/local
        run: ./ci/install.sh
      - name: Build enprot
        env:
          PKG_CONFIG_PATH: /usr/local/lib/pkgconfig
        run: cargo build --features cli --release
      - name: Generate man page
        run: cargo run --features cli --example gen-manpage > enprot.1
      - name: Generate shell completions
        run: |
          mkdir -p completions
          target/release/enprot completions bash > completions/enprot.bash
          target/release/enprot completions fish > completions/enprot.fish
          target/release/enprot completions zsh  > completions/_enprot
      - name: Generate SBOMs
        # TODO.complete/62: the binary itself emits its SBOM — Rust
        # crates are baked in from Cargo.lock at build time; the C
        # library versions come from the linked libraries at runtime.
        # SOURCE_DATE_EPOCH (commit timestamp) keeps the documents
        # byte-identical for identical releases (TODO.complete/45).
        run: |
          set -euxo pipefail
          epoch=$(git log -1 --format=%ct)
          ver=$(echo "$DEPLOY_TAG" | sed 's/^[^0-9]*//')
          SOURCE_DATE_EPOCH=$epoch target/release/enprot sbom --output "enprot-${ver}.spdx.json"
          SOURCE_DATE_EPOCH=$epoch target/release/enprot sbom --sbom-format cyclonedx-json --output "enprot-${ver}.cdx.json"
      - name: Upload extras
        uses: actions/upload-artifact@v7
        with:
          name: extras
          path: |
            enprot.1
            completions/
            enprot-*.spdx.json
            enprot-*.cdx.json

  # Emits the archive matrix as JSON — all six legs normally, or
  # just the -f target=<triple> one for single-leg debug dispatches.
  # The static dynamic-array + include shape cannot express this:
  # include entries union all six targets back in regardless of the
  # dynamic array (verified live, twice), and GHA rejects
  # matrix-context job-level ifs at parse time. So the whole matrix,
  # per-leg os+env included, is data in ci/deploy-legs.json.
  matrix:
    runs-on: ubuntu-latest
    outputs:
      matrix: ${{ steps.emit.outputs.matrix }}
    steps:
      - uses: actions/checkout@v7
        with:
          ref: ${{ env.DEPLOY_REF }}
      - id: emit
        run: |
          set -eu
          if [ "${{ github.event_name }}" = "workflow_dispatch" ] && [ -n "${{ inputs.target }}" ]; then
            echo "single-leg dispatch: ${{ inputs.target }}" >&2
            sel=$(jq -c --arg t '${{ inputs.target }}' '{include: map(select(.target == $t))}' ci/deploy-legs.json)
            if [ "$(printf '%s' "$sel" | jq '.include | length')" != "1" ]; then
              echo "unknown target '${{ inputs.target }}'" >&2; exit 1
            fi
            echo "matrix=$sel" >> "$GITHUB_OUTPUT"
          else
            echo "matrix={\"include\":$(jq -c . ci/deploy-legs.json)}" >> "$GITHUB_OUTPUT"
          fi

  release-archive:
    needs: [checks, generate-extras, matrix]
    name: archive
    # Docker cross builds compile Botan up to THREE times per leg
    # (botan-sys's own vendored copy, rnp-src's copy for librnp,
    # and the host librnp for bindgen); amalgamation cut the file
    # count but the legs still need ~2h on 2-core runners. Every
    # shorter limit (20/45/90) killed them mid-build, surfacing as
    # "cancelled" at exactly the limit.
    timeout-minutes: 240
    strategy:
      # Don't cancel native builds (macOS, Windows MSVC) when Docker
      # cross-compile targets fail (blocked on upstream librnp).
      fail-fast: false
      # Whole matrix emitted by the `matrix` prepare job: all six legs
      # normally, or just the -f target=<triple> one for single-leg
      # debug dispatches. The previous dynamic-array + include shape
      # could not scope — include entries union all six targets back
      # in regardless of the array (verified live, twice), and GHA
      # rejects matrix-context job-level ifs at parse time.
      matrix: ${{ fromJSON(needs.matrix.outputs.matrix) }}
    runs-on: ${{ matrix.os }}
    steps:
      - uses: actions/checkout@v7
        with:
          ref: ${{ env.DEPLOY_REF }}

      - name: Set environment (unix)
        if: "!startsWith(matrix.os, 'windows')"
        env: ${{ matrix.env }}
        run: |
          set -euxo pipefail
          # Strip any non-digit prefix (handles 'v', 'enprot-v', bare).
          TAG="$(echo "$DEPLOY_TAG" | sed 's/^[^0-9]*//')"
          echo "RELEASE_TAG=$TAG" >> $GITHUB_ENV
          echo "PREFIX=${PREFIX-$GITHUB_WORKSPACE/installs}" >> $GITHUB_ENV
          echo "EXE_PATH=target/$TARGET/release/$EXE_NAME" >> $GITHUB_ENV
          # Reproducible-build timestamp: pin to the tag's commit time.
          echo "SOURCE_DATE_EPOCH=$(git log -1 --format=%ct)" >> $GITHUB_ENV
      - name: Set environment (windows)
        if: startsWith(matrix.os, 'windows')
        env: ${{ matrix.env }}
        run: |
          # Strip any non-digit prefix (handles 'v', 'enprot-v', bare).
          $Tag = $Env:DEPLOY_TAG -replace '^[^0-9]+',''
          "RELEASE_TAG=$Tag" | Out-File -Append -Encoding UTF8 -LiteralPath $Env:GITHUB_ENV
          $Prefix = (Join-Path $PWD 'installs').Replace('\', '/')
          "PREFIX=$Prefix" | Out-File -Append -Encoding UTF8 -LiteralPath $Env:GITHUB_ENV
          "EXE_PATH=target/$Env:TARGET/release/$Env:EXE_NAME" | Out-File -Append -Encoding UTF8 -LiteralPath $Env:GITHUB_ENV
          # Reproducible-build timestamp: pin to the tag's commit time.
          $CommitEpoch = git log -1 --format=%ct
          "SOURCE_DATE_EPOCH=$CommitEpoch" | Out-File -Append -Encoding UTF8 -LiteralPath $Env:GITHUB_ENV

      - name: Install rust toolchain
        uses: dtolnay/rust-toolchain@stable
        with:
          target: ${{ matrix.env.TARGET }}

      - name: Rust toolchain fingerprint
        # Fingerprints are only stable for a given rustc; key the
        # cache on it so toolchain rotation means a cold rebuild.
        shell: bash
        run: echo "v=$(rustc -vV | shasum -a 256 | cut -c1-12)" >> "$GITHUB_OUTPUT"
        id: rc

      - name: Restore cross C-stack cache
        # The vendored C stack (Botan x3, librnp, json-c, sexpp,
        # zlib, bzip2) lives inside cargo build-script OUT_DIRs
        # under target/. Cache exactly those trees plus their
        # fingerprints: cargo then skips the build scripts outright
        # (~90 min per leg on 2-core runners) and only recompiles
        # the Rust side. Rust rlibs are deliberately NOT cached —
        # they dominate the cache size for far less wall time.
        # Strictly keyed per (target, Cargo.lock, rustc, ci files,
        # this workflow) so a change in any input — including the
        # BOTAN_CONFIGURE_* matrix values — is a cold rebuild, and
        # legs never see another leg's artifacts.
        uses: actions/cache@v6
        with:
          path: |
            target/release/build/botan-sys-*/out
            target/release/build/rnp-src-*/out
            target/release/.fingerprint/botan-sys-*
            target/release/.fingerprint/rnp-src-*
            target/*/release/build/botan-sys-*/out
            target/*/release/build/rnp-src-*/out
            target/*/release/build/ring-*/out
            target/*/.fingerprint/botan-sys-*
            target/*/.fingerprint/rnp-src-*
          key: cross-${{ matrix.target }}-${{ hashFiles('Cargo.lock') }}-${{ steps.rc.outputs.v }}-${{ hashFiles('ci/**', '.cargo/**', '.github/workflows/deploy.yml') }}

      - name: Install python (windows)
        if: startsWith(matrix.os, 'windows')
        uses: actions/setup-python@v7
        with:
          python-version: '3.x'

      - name: Restore MSVC C-stack cache
        # install.ps1 builds the full C stack into ./installs
        # (~30 min); its $stackCacheHit guard skips it all when
        # warm. Keyed on the script + Botan version only.
        if: startsWith(matrix.os, 'windows')
        uses: actions/cache@v6
        with:
          path: installs
          key: msvc-stack-${{ matrix.env.BOTAN_VERSION || env.BOTAN_VERSION }}-${{ hashFiles('ci/install.ps1') }}
      - name: Install dependencies (windows)
        if: startsWith(matrix.os, 'windows')
        env:
          GIT_REDIRECT_STDERR: '2>&1'
        shell: pwsh
        run: |
          $Env:Path += ";$Env:PREFIX/lib"
          $Env:TARGET = "x86_64-pc-windows-msvc"
          ./ci/install.ps1

      - name: Build (unix)
        if: "!startsWith(matrix.os, 'windows')"
        env: ${{ matrix.env }}
        run: |
          set -euxo pipefail
          . ci/build-static.sh
      - name: Build (windows)
        if: startsWith(matrix.os, 'windows')
        env: ${{ matrix.env }}
        shell: pwsh
        run: |
          . .\ci\build-static.ps1

      - name: Verify binary (windows)
        if: startsWith(matrix.os, 'windows')
        shell: pwsh
        run: |
          $v = & "$Env:EXE_PATH" --version
          if ($v -notmatch "enprot") { throw "Binary verification failed: $v" }

      - name: Download extras
        uses: actions/download-artifact@v8
        with:
          name: extras
          path: extras

      - name: Archive (unix)
        if: "!startsWith(matrix.os, 'windows')"
        env: ${{ matrix.env }}
        run: |
          set -euxo pipefail
          . ci/archive.sh
      - name: Archive (windows)
        if: startsWith(matrix.os, 'windows')
        env: ${{ matrix.env }}
        shell: pwsh
        run: |
          Set-StrictMode -Version 3.0
          . .\ci\archive.ps1

      - name: Upload artifacts
        # One artifact per matrix leg: parallel uploads to a single
        # name collide (only the first leg's files survive), which
        # shipped releases with one asset instead of six. The
        # download step merges the per-leg artifacts back together.
        uses: actions/upload-artifact@v7
        with:
          name: archives-${{ matrix.target }}
          path: archives

  github-release:
    name: github release
    needs: [release-archive]
    # Run even if some archive targets failed (e.g., Docker cross-compile
    # targets blocked on upstream librnp). Publish whatever archives
    # succeeded so users get macOS + Windows MSVC binaries.
    # Test builds (dispatch without `tag`) must not publish.
    if: always() && !cancelled() && (github.event_name != 'workflow_dispatch' || inputs.tag != '')
    runs-on: ubuntu-latest
    steps:
      - name: Download artifacts
        uses: actions/download-artifact@v8
        with:
          pattern: archives-*
          merge-multiple: true
      # The extras artifact (man page, completions, SBOMs) was
      # uploaded by generate-extras but never downloaded here —
      # releases shipped binaries only. Pull it into the same
      # upload set.
      - name: Download extras
        uses: actions/download-artifact@v8
        with:
          name: extras
          path: extras
      - name: Flatten extras into the upload set
        # gh release upload takes FILES only — a directory argument
        # aborts the whole upload ("read <dir>: is a directory"),
        # which clobbered v0.5.59 down to 3 assets mid-failure.
        # Completions ship as one tarball; man page and SBOMs are
        # files already.
        run: |
          set -euxo pipefail
          mv extras/enprot.1 .
          mv extras/enprot-*.spdx.json extras/enprot-*.cdx.json . 2>/dev/null || true
          comp=$(ls extras/completions 2>/dev/null | wc -l)
          if [ "$comp" -gt 0 ]; then
            tar -C extras -czf "enprot-completions.tar.gz" completions
          fi
          rm -rf extras
          # The upload step globs '*' at the top level — nothing
          # directory-shaped may remain there.
          if find . -mindepth 1 -maxdepth 1 -type d | grep -q .; then
            echo "directory in upload set:"; find . -mindepth 1 -maxdepth 1 -type d; exit 1
          fi
      - name: Upload release assets
        # release-plz already created the release object (the tag is
        # its); this step only attaches artifacts. The previous
        # Ruby-based create-github-release helper broke on current
        # runners (bundler/rubygems mismatch) — gh does the same job
        # with one dependency the workflow already trusts.
        env:
          GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
        run: |
          set -euxo pipefail
          # Create-if-missing (manual dispatch for a pre-release-plz
          # tag); idempotent otherwise. --clobber makes re-dispatching
          # a tag safe.
          gh release view "$DEPLOY_TAG" -R "$GITHUB_REPOSITORY" ||
            gh release create "$DEPLOY_TAG" -R "$GITHUB_REPOSITORY" \
              --title "$DEPLOY_TAG" --generate-notes
          if compgen -G "*" > /dev/null; then
            gh release upload "$DEPLOY_TAG" -R "$GITHUB_REPOSITORY" --clobber *
          else
            echo "no archive artifacts to upload"
          fi

  publish-snap:
    name: snap
    needs: [github-release]
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v7
        with:
          ref: ${{ env.DEPLOY_REF }}
      # snapcore/action-build@v1 invokes snapcraft as part of its
      # action — it needs the version replaced BEFORE this step.
      # Step is named "build" so ${{ steps.build.outputs.snap }} works.
      - name: Set snap version
        run: |
          # Strip any non-digit prefix (handles 'v', 'enprot-v', bare).
          RELEASE_TAG="$(echo "$DEPLOY_TAG" | sed 's/^[^0-9]*//')"
          sed -i "s/RELEASE_TAG/${RELEASE_TAG}/g" snap/snapcraft.yaml
      - name: Install snapcraft
        id: build
        uses: snapcore/action-build@v1
        with:
          snapcraft_token: ${{ secrets.SNAPCRAFT_RELEASE_LOGIN }}
      - name: Install review-tools
        run: sudo snap install review-tools
      - name: Publish
        # Direct upload: snapcore/action-publish@v1 is obsolete against
        # the snapcraft on current runners — it logs in via
        # 'snapcraft login --with <file>', which snapcraft 8+ removed
        # ("use SNAPCRAFT_STORE_CREDENTIALS instead", exit 64). The
        # env var accepts the same export-login payload the org
        # secret carries.
        env:
          SNAPCRAFT_STORE_CREDENTIALS: ${{ secrets.SNAPCRAFT_RELEASE_LOGIN }}
        run: snapcraft upload --release=stable "${{ steps.build.outputs.snap }}"