Skip to content

Commit ad8bc17

Browse files
committed
feat(s003): the FIPS image, the leg in the gate, the mode tests and the supports record
ci/fips/Containerfile builds OTP 28.5.0.5 from source with --enable-fips against UBI9's OpenSSL (both archives pinned by SHA-256, the base by digest); fips-image.yml pushes it under the tag scripts/fips_image_tag.sh derives from the two files that define it; the gate gains the fips-tag and fips jobs, which enter the mode with OTP's own fips_mode setting through ERL_AFLAGS (the crypto application loaded first, measured as the condition). test/fips/mode_test.exs binds TRINITY_FIPS_LEG to crypto:info_fips/0 everywhere and carries AC1 to AC3 under the :fips tag; scripts/crypto_supports.exs prints the listing and docs/fips-leg/ holds both listings and their diff (51 entries removed). The gate's hex.audit step clears ERL_AFLAGS: Hex 2.5.1 offers TLS 1.0 and 1.1 and ssl in the mode refuses the set (docs/fips-leg.md, finding 1). Measured in the image on this machine: mix gate exit 0 in the mode, 268 passed, 12 excluded; on this machine, 265 passed, 15 excluded. Signed-off-by: Ayla Croft <aylacroft@proton.me>
1 parent 7179cac commit ad8bc17

16 files changed

Lines changed: 761 additions & 3 deletions

‎.github/workflows/fips-image.yml‎

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
# SPDX-FileCopyrightText: Sudo Apt Holdings LLC
2+
# SPDX-License-Identifier: Apache-2.0
3+
#
4+
# Builds the FIPS leg's image (ci/fips/Containerfile, docs/fips-leg.md) and pushes it to the
5+
# repository's container registry under the tag scripts/fips_image_tag.sh derives from the
6+
# files that define it. It runs when one of those files changes and on demand; the gate's
7+
# `fips` job pulls the tag and never builds. A tag that already exists is not rebuilt: the
8+
# image is a function of its inputs, and OTP from source is the slow part.
9+
name: fips-image
10+
11+
on:
12+
push:
13+
paths:
14+
- .tool-versions
15+
- ci/fips/Containerfile
16+
- scripts/fips_image_tag.sh
17+
- .github/workflows/fips-image.yml
18+
workflow_dispatch:
19+
20+
permissions:
21+
contents: read
22+
packages: write
23+
24+
jobs:
25+
image:
26+
runs-on: ubuntu-latest
27+
steps:
28+
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
29+
30+
- name: The tag, and the versions .tool-versions names
31+
id: meta
32+
run: |
33+
echo "ref=$(scripts/fips_image_tag.sh --ref)" >> "$GITHUB_OUTPUT"
34+
echo "otp=$(awk '$1 == "erlang" { print $2 }' .tool-versions)" >> "$GITHUB_OUTPUT"
35+
echo "elixir=$(awk '$1 == "elixir" { print $2 }' .tool-versions | cut -d- -f1)" >> "$GITHUB_OUTPUT"
36+
37+
- name: Log in to the registry
38+
run: echo "${{ secrets.GITHUB_TOKEN }}" | docker login ghcr.io -u "${{ github.actor }}" --password-stdin
39+
40+
- name: Skip when this tag is already published
41+
id: exists
42+
run: |
43+
if docker manifest inspect "${{ steps.meta.outputs.ref }}" >/dev/null 2>&1; then
44+
echo "published=true" >> "$GITHUB_OUTPUT"
45+
echo "::notice::${{ steps.meta.outputs.ref }} is already published; nothing to build"
46+
else
47+
echo "published=false" >> "$GITHUB_OUTPUT"
48+
fi
49+
50+
- name: Build
51+
if: steps.exists.outputs.published == 'false'
52+
run: |
53+
docker build \
54+
--build-arg "OTP_VERSION=${{ steps.meta.outputs.otp }}" \
55+
--build-arg "ELIXIR_VERSION=${{ steps.meta.outputs.elixir }}" \
56+
-t "${{ steps.meta.outputs.ref }}" \
57+
-f ci/fips/Containerfile ci/fips
58+
59+
- name: Push
60+
if: steps.exists.outputs.published == 'false'
61+
run: |
62+
docker push "${{ steps.meta.outputs.ref }}"
63+
docker image inspect "${{ steps.meta.outputs.ref }}" --format '{{ index .RepoDigests 0 }}'

‎.github/workflows/gate.yml‎

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,3 +100,85 @@ jobs:
100100

101101
- run: mix ecto.reset
102102
- run: mix test --exclude sqlite
103+
104+
# Slice 003: the same gate on an OTP built from source with --enable-fips, in FIPS mode,
105+
# against the FIPS provider of a UBI9 container (docs/fips-leg.md). A second job rather than
106+
# a matrix entry, as `postgres` is, so `gate` keeps its context name. The image is built by
107+
# fips-image.yml and pulled here by the tag scripts/fips_image_tag.sh derives from the
108+
# files that define it; this job never builds it.
109+
fips-tag:
110+
runs-on: ubuntu-latest
111+
outputs:
112+
ref: ${{ steps.tag.outputs.ref }}
113+
steps:
114+
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
115+
- id: tag
116+
run: echo "ref=$(scripts/fips_image_tag.sh --ref)" >> "$GITHUB_OUTPUT"
117+
118+
fips:
119+
needs: fips-tag
120+
runs-on: ubuntu-latest
121+
container:
122+
image: ${{ needs.fips-tag.outputs.ref }}
123+
credentials:
124+
username: ${{ github.actor }}
125+
password: ${{ secrets.GITHUB_TOKEN }}
126+
env:
127+
MIX_ENV: test
128+
TRINITY_DB: sqlite
129+
# test/test_helper.exs includes the :fips tests under this, and test/fips/mode_test.exs
130+
# asserts the runtime agrees, so a leg that failed to enter the mode is red, not quiet.
131+
TRINITY_FIPS_LEG: "1"
132+
# FIPS mode is entered by OTP's own configuration, read by the crypto NIF when it loads,
133+
# which needs the crypto application loaded first; ERL_AFLAGS is prepended to every
134+
# erl command line, so every `mix` and `elixir` here runs in the mode. Measured at G1:
135+
# ERL_FLAGS alone leaves the mode off (the NIF loads before the application does).
136+
ERL_AFLAGS: "-crypto fips_mode true -eval application:load(crypto)"
137+
steps:
138+
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
139+
with:
140+
fetch-depth: 0
141+
ref: ${{ github.event.pull_request.head.sha || github.sha }}
142+
143+
- uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
144+
with:
145+
path: |
146+
deps
147+
_build
148+
key: fips-mix-${{ needs.fips-tag.outputs.ref }}-${{ hashFiles('mix.lock') }}
149+
restore-keys: fips-mix-${{ needs.fips-tag.outputs.ref }}-
150+
151+
# The one step outside the mode. The fetch, because Hex's client offers TLS 1.0 and 1.1
152+
# beside 1.2 and ssl in the mode refuses the set (insufficient_crypto_support); the
153+
# dependency compile, because rustler_precompiled downloads mdex's NIF at compile time
154+
# and OTP's TLS 1.3 client in the mode fails that host's HelloRetryRequest
155+
# (erlang/otp#8470). docs/fips-leg.md findings 1 and 2; neither is what the leg
156+
# measures. The gate's own hex.audit step clears ERL_AFLAGS for the same reason (mix.exs).
157+
- name: Fetch and compile dependencies (FIPS mode off)
158+
env:
159+
ERL_AFLAGS: "-eval application:load(crypto)"
160+
run: |
161+
mix deps.get
162+
mix deps.compile
163+
164+
# AC1, before anything else runs: the runtime is in the mode and names the provider.
165+
- name: The runtime is in FIPS mode
166+
run: |
167+
erl -noshell -eval 'enabled = crypto:info_fips(), io:format("~p~n", [crypto:info()]), halt().'
168+
169+
# AC3, the default half: the listing with the mode off equals the committed file; the
170+
# mode-on half is test/fips/mode_test.exs, inside the suite.
171+
- name: crypto:supports/0 with the mode off matches docs/fips-leg/supports-default.txt
172+
env:
173+
ERL_AFLAGS: "-eval application:load(crypto)"
174+
run: elixir scripts/crypto_supports.exs | diff - docs/fips-leg/supports-default.txt
175+
176+
- run: ./scripts/plan_check.sh
177+
178+
- name: DCO sign-off present on every commit
179+
run: |
180+
git log --format=%B ${{ github.event.pull_request.base.sha || 'HEAD~1' }}..HEAD 2>/dev/null \
181+
| grep -q '^Signed-off-by: ' || {
182+
echo "::error::a commit in this range has no Signed-off-by line"; exit 1; }
183+
184+
- run: mix gate

‎VERSIONS.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,7 @@ never pin a version hex marks as retired or vulnerable.
7474
| `Rust` | **1.92.0** | ✅ `rust-toolchain.toml` | Measured at Slice 001 line 3: `rustc --version` reports 1.92.0 (ded5c06cf 2025-12-08), exit 0. Pinned in `rust-toolchain.toml`, **not** `.tool-versions`: `asdf` here has no rust plugin and silently ignores a rust line, whereas `rustup show active-toolchain` reports this file as an override. See NOTES.md deviation D1. Corrected 2026-09-06: this row previously read `Rust + Tauri CLI | stable | ✅ .tool-versions`, which named a file carrying neither. |
7575
| `Tauri CLI` | **2.11.4** | 📐 `_build/_tauri/bin/cargo-tauri tauri --version` | Measured at Slice 001 line 3. Not on `PATH` and not pinned by any file in the tree: `ex_tauri` provisions it with `cargo install tauri-cli --version ^2 --root .` inside `_build/_tauri`, which is gitignored, so `cargo tauri --version` exits 101 on a fresh machine. 📐 rather than ✅ because nothing at this sha verifies it. The `^2` floats; 2.11.4 is what it resolved to on 2026-09-06. |
7676
| `Zig` | **0.16.0** | ✅ `.tool-versions` | Measured at Slice 001 line 3: burrito 1.6.0 compares Zig for **equality**, not a range (`@zig_version_expected` in `deps/burrito/lib/burrito.ex`), and exits 1 on any other version. `zig version` reports 0.16.0, exit 0. Installed through the asdf zig plugin, added this slice. Corrected 2026-09-06: this row previously read `version required by Burrito | ✅ .tool-versions` and that file carried no zig line. |
77+
| `FIPS leg base image (UBI9)` | **sha256:9295c5c688f487fa5cf27a734fa55ecd57aeb7dc0904ba537da4f42dfa1d0acb** | ✅ `ci/fips/Containerfile` | Added at Slice 003: the base of the FIPS build leg's image (docs/fips-leg.md), `registry.access.redhat.com/ubi9/ubi:latest` resolved by digest on 2026-09-20 (Red Hat Enterprise Linux release 9.8, `openssl-libs 3.5.8-1.el9_8`, `openssl-fips-provider-so 3.0.7-11.el9_8`). The image itself is OTP 28.5.0.5 built from source with `--enable-fips` against that OpenSSL, plus Elixir 1.20.4; the two archives are pinned by SHA-256 in the Containerfile and the image tag is `scripts/fips_image_tag.sh` over `.tool-versions` and the Containerfile. The FIPS provider the image runs is what the distribution ships and names; docs/fips-leg.md states what Trinity does and does not claim about it. |
7778

7879
### Core libraries
7980

‎ci/fips/Containerfile‎

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
# SPDX-FileCopyrightText: Sudo Apt Holdings LLC
2+
# SPDX-License-Identifier: Apache-2.0
3+
#
4+
# The FIPS build leg's image (slice 003, docs/fips-leg.md): OTP built from source with
5+
# --enable-fips against the OpenSSL of a UBI9 container, whose FIPS provider is the one Red Hat
6+
# publishes a validation for, plus Elixir and everything `mix gate` shells out to. The gate
7+
# leg pulls this image; nothing here runs Trinity.
8+
#
9+
# The versions arrive as build arguments read from .tool-versions by the image workflow, so
10+
# that file stays the one source. The two archives are checked against digests pinned here;
11+
# a new OTP or Elixir pin is a new pair of digests in this file, read from the release page.
12+
#
13+
# Base pinned by digest, not tag: registry.access.redhat.com/ubi9/ubi:latest resolved on
14+
# 2026-09-20 (Red Hat Enterprise Linux release 9.8, openssl-libs 3.5.8-1.el9_8, FIPS provider
15+
# 3.0.7). A base change is a diff here and a rebuild, never a silent drift under a moving tag.
16+
FROM registry.access.redhat.com/ubi9/ubi@sha256:9295c5c688f487fa5cf27a734fa55ecd57aeb7dc0904ba537da4f42dfa1d0acb
17+
18+
ARG OTP_VERSION
19+
ARG ELIXIR_VERSION
20+
ARG OTP_SHA256=5231ba18f31f8041c2d6514cc8842e46954d3b39a53f1617f03f2abe6fea59c7
21+
ARG ELIXIR_SHA256=ea7ff98bc1ed76c663a5d034c863c6fd37e65c9c855b6bbf6ccb2034d806673c
22+
23+
# The toolchain OTP's configure needs, plus what the gate's steps call from the shell: git
24+
# (plan_check, the names and secrets census), python3 with PyYAML (plan_check rule 12), gcc
25+
# (exqlite compiles SQLite; mdex_native's precompiled artifact loads on glibc), procps-ng (the
26+
# shell tool's tests read `ps`). Kept in the image on purpose: it is a CI image, not a runtime.
27+
RUN dnf -y install --setopt=install_weak_deps=False \
28+
gcc gcc-c++ make autoconf ncurses-devel openssl-devel \
29+
tar gzip unzip git python3-pyyaml procps-ng which \
30+
&& dnf clean all
31+
32+
# OTP from source. --enable-fips compiles the crypto NIF's FIPS support; --with-ssl points at
33+
# the distribution's OpenSSL and its dynamic library, which is where the validated provider
34+
# lives, so the link is dynamic by design (a static OpenSSL would carry no FIPS provider).
35+
# Applications with no place on a CI leg are left out to shorten the build.
36+
RUN set -eu; \
37+
curl -fsSLo /tmp/otp.tar.gz \
38+
"https://github.com/erlang/otp/releases/download/OTP-${OTP_VERSION}/otp_src_${OTP_VERSION}.tar.gz"; \
39+
echo "${OTP_SHA256} /tmp/otp.tar.gz" | sha256sum -c -; \
40+
mkdir -p /tmp/otp && tar -xzf /tmp/otp.tar.gz -C /tmp/otp --strip-components=1; \
41+
cd /tmp/otp; \
42+
./configure --prefix=/usr/local --enable-fips --with-ssl=/usr \
43+
--without-javac --without-jinterface --without-wx --without-odbc \
44+
--without-debugger --without-observer --without-et --without-megaco; \
45+
make -j"$(nproc)"; \
46+
make install; \
47+
rm -rf /tmp/otp /tmp/otp.tar.gz
48+
49+
# Elixir, precompiled for this OTP major (the release's own zip and its published digest).
50+
RUN set -eu; \
51+
curl -fsSLo /tmp/elixir.zip \
52+
"https://github.com/elixir-lang/elixir/releases/download/v${ELIXIR_VERSION}/elixir-otp-28.zip"; \
53+
echo "${ELIXIR_SHA256} /tmp/elixir.zip" | sha256sum -c -; \
54+
mkdir -p /usr/local/elixir && unzip -q /tmp/elixir.zip -d /usr/local/elixir; \
55+
rm -f /tmp/elixir.zip
56+
57+
# Hex and rebar in a path that does not depend on HOME: a GitHub container job sets HOME to
58+
# /github/home, and an install under /root would be invisible there.
59+
ENV PATH=/usr/local/elixir/bin:/usr/local/bin:$PATH \
60+
LANG=C.UTF-8 \
61+
MIX_HOME=/opt/mix \
62+
HEX_HOME=/opt/hex
63+
RUN mix local.hex --force && mix local.rebar --force
64+
65+
# The image proves its own build: the crypto NIF reports the FIPS provider available, and
66+
# entering FIPS mode is possible. Which mode a container runs in is the leg's decision, made
67+
# by OPENSSL_FORCE_FIPS_MODE and `-crypto fips_mode true` at run time, never here.
68+
RUN erl -noshell -eval \
69+
'true = maps:get(fips_provider_available, crypto:info()), ok = crypto:enable_fips_mode(true), enabled = crypto:info_fips(), io:format("~p~n", [crypto:info()]), halt().' \
70+
&& elixir --version

0 commit comments

Comments
 (0)