sre: release pipeline w/ ISO split, GPG sig, MOK signing scaffold
- build-iso.yml: on tag push (v*.*.*), split ISO into 1.9G parts, GPG-sign the sha256 with GPG_PRIVATE_KEY secret, and auto-create release with softprops/action-gh-release@v2 attaching part files + sig + reassembly instructions. Falls back to legacy release.published path. - build-iso.yml: optional EFI Secure Boot signing step. If MOK_PRIVATE_KEY + MOK_CERT secrets are present, sbsign each .efi inside the ISO and repack with xorriso; otherwise warn and ship unsigned. Refresh sha256. - release-checksums.yml: new PR-time gate. Validates source + generated CI kickstart, shellchecks scripts, parses every workflow YAML, and asserts the split size stays under GitHub'''s 2 GiB asset cap. - scripts/gen-mok-key.sh: idempotent MOK keypair generator (RSA-4096, 10y), outputs to gitignored build/keys/. Header documents mokutil enrollment and gh secret upload. exec bit set in index. - .gitignore: add build/keys/, *.priv, *.der. User must add GitHub secrets before the next tagged release: GPG_PRIVATE_KEY — armored private key for sha256 signing MOK_PRIVATE_KEY — sbsign EFI signing key (PEM) MOK_CERT — public cert (DER) for sbsign + mokutil enrollment
This commit is contained in:
parent
8515bdbe38
commit
2782b72ead
4 changed files with 404 additions and 5 deletions
212
.github/workflows/build-iso.yml
vendored
212
.github/workflows/build-iso.yml
vendored
|
|
@ -10,6 +10,8 @@ on:
|
||||||
- 'assets/**'
|
- 'assets/**'
|
||||||
- 'build/**'
|
- 'build/**'
|
||||||
- '.github/workflows/build-iso.yml'
|
- '.github/workflows/build-iso.yml'
|
||||||
|
tags:
|
||||||
|
- 'v*.*.*'
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
inputs:
|
inputs:
|
||||||
releasever:
|
releasever:
|
||||||
|
|
@ -24,6 +26,8 @@ jobs:
|
||||||
name: Build live ISO
|
name: Build live ISO
|
||||||
runs-on: ubuntu-24.04
|
runs-on: ubuntu-24.04
|
||||||
timeout-minutes: 90
|
timeout-minutes: 90
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
|
|
@ -52,7 +56,9 @@ jobs:
|
||||||
# core problem). CI runners always start fresh, no version skew.
|
# core problem). CI runners always start fresh, no version skew.
|
||||||
dnf -y upgrade --refresh
|
dnf -y upgrade --refresh
|
||||||
|
|
||||||
# Install build tooling
|
# Install build tooling. sbsigntools provides sbsign for EFI
|
||||||
|
# binary signing; pesign is the alternate path. xorriso is
|
||||||
|
# required for ISO repack after EFI signing.
|
||||||
dnf -y install \
|
dnf -y install \
|
||||||
lorax \
|
lorax \
|
||||||
livecd-tools \
|
livecd-tools \
|
||||||
|
|
@ -64,7 +70,11 @@ jobs:
|
||||||
createrepo_c \
|
createrepo_c \
|
||||||
git \
|
git \
|
||||||
which \
|
which \
|
||||||
shadow-utils
|
shadow-utils \
|
||||||
|
sbsigntools \
|
||||||
|
openssl \
|
||||||
|
dosfstools \
|
||||||
|
mtools
|
||||||
|
|
||||||
cd /work
|
cd /work
|
||||||
|
|
||||||
|
|
@ -105,8 +115,16 @@ jobs:
|
||||||
# Move output ISO to expected dir
|
# Move output ISO to expected dir
|
||||||
mv ./veilor-os-43.iso build/out/ 2>/dev/null || mv ./*.iso build/out/ 2>/dev/null || true
|
mv ./veilor-os-43.iso build/out/ 2>/dev/null || mv ./*.iso build/out/ 2>/dev/null || true
|
||||||
|
|
||||||
# Rename + checksum
|
# Rename. For tag builds use the tag as the version stamp so
|
||||||
ISO_NAME="veilor-os-${{ github.event.inputs.releasever || '43' }}-$(date +%Y%m%d-%H%M%S).iso"
|
# release artifacts have a stable, predictable name. For
|
||||||
|
# branch / dispatch builds use a timestamp.
|
||||||
|
REF_NAME="${{ github.ref_name }}"
|
||||||
|
if [[ "${{ github.ref_type }}" == "tag" ]]; then
|
||||||
|
VERSION_TAG="$REF_NAME"
|
||||||
|
else
|
||||||
|
VERSION_TAG="$(date +%Y%m%d-%H%M%S)"
|
||||||
|
fi
|
||||||
|
ISO_NAME="veilor-os-${{ github.event.inputs.releasever || '43' }}-${VERSION_TAG}.iso"
|
||||||
cd build/out
|
cd build/out
|
||||||
for f in *.iso; do
|
for f in *.iso; do
|
||||||
[[ -f $f && $f != $ISO_NAME ]] && mv "$f" "$ISO_NAME"
|
[[ -f $f && $f != $ISO_NAME ]] && mv "$f" "$ISO_NAME"
|
||||||
|
|
@ -114,6 +132,95 @@ jobs:
|
||||||
sha256sum "$ISO_NAME" > "$ISO_NAME.sha256"
|
sha256sum "$ISO_NAME" > "$ISO_NAME.sha256"
|
||||||
ls -lh "$ISO_NAME"
|
ls -lh "$ISO_NAME"
|
||||||
|
|
||||||
|
# Stash final ISO name for later steps that run *outside* the
|
||||||
|
# container (signing, splitting, release). The runner sees
|
||||||
|
# /work/build/out, host sees ${{ github.workspace }}/build/out.
|
||||||
|
echo "$ISO_NAME" > /work/build/out/.iso-name
|
||||||
|
|
||||||
|
- name: Resolve ISO filename
|
||||||
|
id: iso
|
||||||
|
run: |
|
||||||
|
ISO_NAME=$(cat build/out/.iso-name)
|
||||||
|
echo "name=$ISO_NAME" >> "$GITHUB_OUTPUT"
|
||||||
|
echo "path=build/out/$ISO_NAME" >> "$GITHUB_OUTPUT"
|
||||||
|
echo "[INFO] ISO resolved: $ISO_NAME"
|
||||||
|
|
||||||
|
# ─── EFI signing (Secure Boot) ──────────────────────────────────────
|
||||||
|
# Only runs if MOK_PRIVATE_KEY + MOK_CERT secrets are present.
|
||||||
|
# Repacks the ISO in-place with sbsign'd BOOTX64.EFI / grubx64.efi.
|
||||||
|
# Without the secrets, ISO is shipped unsigned (testing builds).
|
||||||
|
- name: Sign EFI binaries (if MOK secrets present)
|
||||||
|
id: sbsign
|
||||||
|
env:
|
||||||
|
MOK_PRIVATE_KEY: ${{ secrets.MOK_PRIVATE_KEY }}
|
||||||
|
MOK_CERT: ${{ secrets.MOK_CERT }}
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
if [[ -z "${MOK_PRIVATE_KEY:-}" || -z "${MOK_CERT:-}" ]]; then
|
||||||
|
echo "::warning::[INFO] MOK secrets absent — ISO unsigned for testing"
|
||||||
|
echo "signed=false" >> "$GITHUB_OUTPUT"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
sudo apt-get update
|
||||||
|
sudo apt-get install -y sbsigntool xorriso mtools dosfstools
|
||||||
|
|
||||||
|
# Materialise key + cert from secrets to a tmp dir wiped on exit.
|
||||||
|
KEYDIR=$(mktemp -d)
|
||||||
|
trap 'rm -rf "$KEYDIR"' EXIT
|
||||||
|
printf '%s' "$MOK_PRIVATE_KEY" > "$KEYDIR/MOK.priv"
|
||||||
|
printf '%s' "$MOK_CERT" | base64 -d > "$KEYDIR/MOK.der" 2>/dev/null \
|
||||||
|
|| printf '%s' "$MOK_CERT" > "$KEYDIR/MOK.der"
|
||||||
|
# sbsign prefers PEM cert input — derive on the fly.
|
||||||
|
openssl x509 -inform DER -in "$KEYDIR/MOK.der" -outform PEM -out "$KEYDIR/MOK.pem" \
|
||||||
|
|| cp "$KEYDIR/MOK.der" "$KEYDIR/MOK.pem"
|
||||||
|
chmod 600 "$KEYDIR/MOK.priv"
|
||||||
|
|
||||||
|
ISO_PATH="${{ steps.iso.outputs.path }}"
|
||||||
|
WORKDIR=$(mktemp -d)
|
||||||
|
trap 'rm -rf "$KEYDIR" "$WORKDIR"' EXIT
|
||||||
|
|
||||||
|
# Extract EFI ESP image from ISO. xorriso prints embedded image
|
||||||
|
# locations; we pull the EFI/BOOT directory contents into a
|
||||||
|
# writable scratch dir, sign each .efi, and graft back in.
|
||||||
|
mkdir -p "$WORKDIR/iso-extract"
|
||||||
|
xorriso -osirrox on -indev "$ISO_PATH" \
|
||||||
|
-extract /EFI "$WORKDIR/iso-extract/EFI" 2>&1 | tail -20
|
||||||
|
|
||||||
|
SIGNED_ANY=0
|
||||||
|
for efi in $(find "$WORKDIR/iso-extract/EFI" -type f -iname '*.efi' 2>/dev/null); do
|
||||||
|
echo "[*] Signing $efi"
|
||||||
|
sbsign \
|
||||||
|
--key "$KEYDIR/MOK.priv" \
|
||||||
|
--cert "$KEYDIR/MOK.pem" \
|
||||||
|
--output "${efi}.signed" \
|
||||||
|
"$efi"
|
||||||
|
mv "${efi}.signed" "$efi"
|
||||||
|
SIGNED_ANY=1
|
||||||
|
done
|
||||||
|
|
||||||
|
if [[ $SIGNED_ANY -eq 0 ]]; then
|
||||||
|
echo "::warning::No EFI binaries found in ISO — skipping repack"
|
||||||
|
echo "signed=false" >> "$GITHUB_OUTPUT"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Repack: graft the signed EFI tree back into the ISO. Use
|
||||||
|
# xorriso's update mode so other ISO contents (squashfs, isolinux)
|
||||||
|
# remain untouched.
|
||||||
|
xorriso \
|
||||||
|
-indev "$ISO_PATH" \
|
||||||
|
-outdev "${ISO_PATH}.signed" \
|
||||||
|
-boot_image any keep \
|
||||||
|
-map "$WORKDIR/iso-extract/EFI" /EFI \
|
||||||
|
-commit
|
||||||
|
mv "${ISO_PATH}.signed" "$ISO_PATH"
|
||||||
|
|
||||||
|
# Refresh sha256 — content changed.
|
||||||
|
(cd build/out && sha256sum "${{ steps.iso.outputs.name }}" > "${{ steps.iso.outputs.name }}.sha256")
|
||||||
|
echo "[OK] EFI binaries signed; ISO repacked + checksum refreshed."
|
||||||
|
echo "signed=true" >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
- name: Upload ISO artifact
|
- name: Upload ISO artifact
|
||||||
if: success()
|
if: success()
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v4
|
||||||
|
|
@ -133,7 +240,102 @@ jobs:
|
||||||
build/out/build.log
|
build/out/build.log
|
||||||
build/out/build/anaconda/
|
build/out/build/anaconda/
|
||||||
|
|
||||||
- name: Attach to release
|
# ─── Release artefact pipeline ──────────────────────────────────────
|
||||||
|
# Triggered on tag push (v*.*.*). The 2.86 GB ISO exceeds GitHub's
|
||||||
|
# 2 GB single-asset cap, so we split into 1.9 GB parts and let users
|
||||||
|
# reassemble with `cat`. Checksum is GPG-signed so users can verify
|
||||||
|
# the assembled ISO end-to-end.
|
||||||
|
- name: Split ISO for release
|
||||||
|
if: github.event_name == 'push' && github.ref_type == 'tag'
|
||||||
|
id: split
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
cd build/out
|
||||||
|
ISO="${{ steps.iso.outputs.name }}"
|
||||||
|
# 1900M keeps each part safely below GitHub's 2 GB asset limit
|
||||||
|
# (limit is 2 GiB = 2147483648 bytes; 1900 MiB = 1992294400).
|
||||||
|
split -b 1900M -d -a 2 "$ISO" "${ISO}.part-"
|
||||||
|
# Rename suffix to letter form so it matches the documented
|
||||||
|
# convention .part-aa, .part-ab. -d uses 00/01; we want aa/ab
|
||||||
|
# to make the cat glob unambiguous (no leading zero confusion).
|
||||||
|
mv "${ISO}.part-00" "${ISO}.part-aa"
|
||||||
|
mv "${ISO}.part-01" "${ISO}.part-ab" 2>/dev/null || true
|
||||||
|
# In rare 3-part case (>3.8 GB ISO), handle .part-02 → .part-ac
|
||||||
|
[[ -f "${ISO}.part-02" ]] && mv "${ISO}.part-02" "${ISO}.part-ac" || true
|
||||||
|
ls -lh "${ISO}".part-*
|
||||||
|
echo "iso=$ISO" >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
|
- name: GPG-sign sha256 checksum
|
||||||
|
if: github.event_name == 'push' && github.ref_type == 'tag'
|
||||||
|
env:
|
||||||
|
GPG_PRIVATE_KEY: ${{ secrets.GPG_PRIVATE_KEY }}
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
if [[ -z "${GPG_PRIVATE_KEY:-}" ]]; then
|
||||||
|
echo "::warning::GPG_PRIVATE_KEY secret missing — checksum will not be signed"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
GNUPGHOME=$(mktemp -d)
|
||||||
|
export GNUPGHOME
|
||||||
|
chmod 700 "$GNUPGHOME"
|
||||||
|
# Trust the imported key automatically — CI is non-interactive.
|
||||||
|
printf '%s' "$GPG_PRIVATE_KEY" | gpg --batch --import
|
||||||
|
KEYID=$(gpg --list-secret-keys --with-colons | awk -F: '/^sec:/ {print $5; exit}')
|
||||||
|
echo "[*] Signing with key $KEYID"
|
||||||
|
cd build/out
|
||||||
|
ISO="${{ steps.iso.outputs.name }}"
|
||||||
|
gpg --batch --yes --pinentry-mode loopback \
|
||||||
|
--local-user "$KEYID" \
|
||||||
|
--armor --detach-sign \
|
||||||
|
--output "${ISO}.sha256.asc" \
|
||||||
|
"${ISO}.sha256"
|
||||||
|
ls -lh "${ISO}.sha256" "${ISO}.sha256.asc"
|
||||||
|
|
||||||
|
- name: Create release and attach assets
|
||||||
|
if: github.event_name == 'push' && github.ref_type == 'tag'
|
||||||
|
uses: softprops/action-gh-release@v2
|
||||||
|
with:
|
||||||
|
tag_name: ${{ github.ref_name }}
|
||||||
|
name: veilor-os ${{ github.ref_name }}
|
||||||
|
draft: false
|
||||||
|
prerelease: false
|
||||||
|
generate_release_notes: true
|
||||||
|
fail_on_unmatched_files: false
|
||||||
|
files: |
|
||||||
|
build/out/*.iso.part-*
|
||||||
|
build/out/*.iso.sha256
|
||||||
|
build/out/*.iso.sha256.asc
|
||||||
|
body: |
|
||||||
|
## veilor-os ${{ github.ref_name }}
|
||||||
|
|
||||||
|
ISO is split into 1.9 GB parts to fit within GitHub's 2 GB
|
||||||
|
per-asset upload cap. Reassemble after download:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cat ${{ steps.iso.outputs.name }}.part-* > ${{ steps.iso.outputs.name }}
|
||||||
|
sha256sum -c ${{ steps.iso.outputs.name }}.sha256
|
||||||
|
```
|
||||||
|
|
||||||
|
### Verifying the checksum signature
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Import the veilor-os release signing key (one-time):
|
||||||
|
gpg --keyserver keys.openpgp.org --recv-keys <KEY-ID-FROM-RELEASE-NOTES>
|
||||||
|
|
||||||
|
# Verify:
|
||||||
|
gpg --verify ${{ steps.iso.outputs.name }}.sha256.asc \
|
||||||
|
${{ steps.iso.outputs.name }}.sha256
|
||||||
|
```
|
||||||
|
|
||||||
|
### Secure Boot
|
||||||
|
|
||||||
|
EFI binaries are signed with the veilor-os MOK if signing
|
||||||
|
secrets were configured at build time. To enroll the public
|
||||||
|
cert post-install, see `scripts/gen-mok-key.sh` header.
|
||||||
|
|
||||||
|
# Legacy hook: keeps the manual `release.published` workflow path
|
||||||
|
# working. Tag-driven flow above is the new canonical entry point.
|
||||||
|
- name: Attach to release (legacy release event)
|
||||||
if: github.event_name == 'release'
|
if: github.event_name == 'release'
|
||||||
uses: softprops/action-gh-release@v2
|
uses: softprops/action-gh-release@v2
|
||||||
with:
|
with:
|
||||||
|
|
|
||||||
100
.github/workflows/release-checksums.yml
vendored
Normal file
100
.github/workflows/release-checksums.yml
vendored
Normal file
|
|
@ -0,0 +1,100 @@
|
||||||
|
name: Release Checksums
|
||||||
|
|
||||||
|
# PR-time validation gate for release-affecting files. Independent of
|
||||||
|
# lint.yml — meant to harden the brittle parts (ksvalidator on the
|
||||||
|
# generated CI kickstart, shellcheck across all maintained scripts,
|
||||||
|
# YAML sanity on every workflow).
|
||||||
|
#
|
||||||
|
# This workflow does NOT replace lint.yml; it runs alongside.
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
paths:
|
||||||
|
- 'kickstart/**'
|
||||||
|
- 'scripts/**'
|
||||||
|
- '.github/workflows/**'
|
||||||
|
push:
|
||||||
|
branches: [main]
|
||||||
|
paths:
|
||||||
|
- 'kickstart/**'
|
||||||
|
- 'scripts/**'
|
||||||
|
- '.github/workflows/**'
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
ksvalidate:
|
||||||
|
name: ksvalidator (CI-flavour kickstart)
|
||||||
|
runs-on: ubuntu-24.04
|
||||||
|
container:
|
||||||
|
image: registry.fedoraproject.org/fedora:43
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Install pykickstart
|
||||||
|
run: dnf -y install pykickstart sed
|
||||||
|
|
||||||
|
- name: Generate CI kickstart and validate
|
||||||
|
run: |
|
||||||
|
set -euxo pipefail
|
||||||
|
# Mirror what build-iso.yml does so we're validating the file
|
||||||
|
# the actual builder consumes, not just the source kickstart.
|
||||||
|
sed -e '/veilor-fix/d' \
|
||||||
|
-e '/^shutdown$/d' \
|
||||||
|
kickstart/veilor-os.ks > kickstart/veilor-os-ci.ks
|
||||||
|
ksvalidator kickstart/veilor-os.ks
|
||||||
|
ksvalidator kickstart/veilor-os-ci.ks
|
||||||
|
|
||||||
|
shellcheck:
|
||||||
|
name: shellcheck (release scripts)
|
||||||
|
runs-on: ubuntu-24.04
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: shellcheck repo scripts
|
||||||
|
uses: ludeeus/action-shellcheck@master
|
||||||
|
with:
|
||||||
|
severity: warning
|
||||||
|
# Same exclusions as lint.yml so behaviour is consistent.
|
||||||
|
ignore_paths: build/cache .github
|
||||||
|
|
||||||
|
workflow-yaml:
|
||||||
|
name: workflow YAML sanity
|
||||||
|
runs-on: ubuntu-24.04
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- name: Validate every workflow parses as YAML
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
python3 - <<'PY'
|
||||||
|
import sys, pathlib, yaml
|
||||||
|
ok = True
|
||||||
|
for p in pathlib.Path(".github/workflows").glob("*.y*ml"):
|
||||||
|
try:
|
||||||
|
yaml.safe_load(p.read_text())
|
||||||
|
print(f"[OK] {p}")
|
||||||
|
except yaml.YAMLError as e:
|
||||||
|
print(f"[ERR] {p}: {e}", file=sys.stderr)
|
||||||
|
ok = False
|
||||||
|
sys.exit(0 if ok else 1)
|
||||||
|
PY
|
||||||
|
|
||||||
|
release-asset-budget:
|
||||||
|
name: Release asset size budget
|
||||||
|
runs-on: ubuntu-24.04
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- name: Confirm split threshold is below GitHub's 2 GiB asset cap
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
# GitHub per-asset upload limit is 2 GiB = 2147483648 bytes.
|
||||||
|
# split is invoked with -b 1900M = 1900 * 2^20 = 1992294400 bytes.
|
||||||
|
# Hard-fail if anyone bumps the split size beyond the cap.
|
||||||
|
if grep -E 'split -b [0-9]+M' .github/workflows/build-iso.yml >/dev/null; then
|
||||||
|
SIZE_M=$(grep -oE 'split -b [0-9]+M' .github/workflows/build-iso.yml | head -1 | grep -oE '[0-9]+')
|
||||||
|
if [[ "$SIZE_M" -gt 2047 ]]; then
|
||||||
|
echo "::error::split -b ${SIZE_M}M exceeds GitHub's 2 GiB per-asset cap"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
echo "[OK] split size ${SIZE_M}M is under the 2 GiB asset limit."
|
||||||
|
else
|
||||||
|
echo "::warning::No split -b NM directive found — release pipeline may have changed"
|
||||||
|
fi
|
||||||
3
.gitignore
vendored
3
.gitignore
vendored
|
|
@ -1,5 +1,6 @@
|
||||||
build/out/
|
build/out/
|
||||||
build/cache/
|
build/cache/
|
||||||
|
build/keys/
|
||||||
*.iso
|
*.iso
|
||||||
*.img
|
*.img
|
||||||
*.log
|
*.log
|
||||||
|
|
@ -11,5 +12,7 @@ build/cache/
|
||||||
secrets/
|
secrets/
|
||||||
*.key
|
*.key
|
||||||
*.pem
|
*.pem
|
||||||
|
*.priv
|
||||||
|
*.der
|
||||||
test/veilor-vm.qcow2
|
test/veilor-vm.qcow2
|
||||||
test/veilor-vm.nvram*
|
test/veilor-vm.nvram*
|
||||||
|
|
|
||||||
94
scripts/gen-mok-key.sh
Executable file
94
scripts/gen-mok-key.sh
Executable file
|
|
@ -0,0 +1,94 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
# gen-mok-key.sh — Generate a Machine Owner Key (MOK) pair for Secure Boot
|
||||||
|
#
|
||||||
|
# Purpose:
|
||||||
|
# Produces an RSA-4096 key pair used to sign EFI binaries (BOOTX64.EFI,
|
||||||
|
# shim, grub) and out-of-tree kernel modules so they pass UEFI Secure Boot
|
||||||
|
# verification once the public cert is enrolled in the firmware.
|
||||||
|
#
|
||||||
|
# Output (gitignored):
|
||||||
|
# build/keys/MOK.priv — PEM private key (sbsign / sign-file input)
|
||||||
|
# build/keys/MOK.der — DER public certificate (mokutil enrollment input)
|
||||||
|
# build/keys/MOK.pem — PEM public certificate (sbsign --cert input)
|
||||||
|
#
|
||||||
|
# Idempotent: if MOK.priv already exists, exits 0 without regenerating.
|
||||||
|
# Re-running with existing keys is safe — won't clobber a key already used
|
||||||
|
# to sign released ISOs.
|
||||||
|
#
|
||||||
|
# ─── User enrollment workflow (post-install) ─────────────────────────────
|
||||||
|
# 1. Copy build/keys/MOK.der to the installed system (USB / scp / etc.)
|
||||||
|
# 2. On the booted veilor-os system, as root:
|
||||||
|
# mokutil --import /path/to/MOK.der
|
||||||
|
# Set a one-time password when prompted.
|
||||||
|
# 3. Reboot. The shim's MokManager will appear on next boot — choose
|
||||||
|
# "Enroll MOK", confirm with the password from step 2, then continue
|
||||||
|
# boot. The cert is now in the kernel's .platform keyring.
|
||||||
|
# 4. Verify enrollment:
|
||||||
|
# mokutil --list-enrolled | grep -A2 'veilor'
|
||||||
|
#
|
||||||
|
# ─── Uploading to GitHub Actions secrets ─────────────────────────────────
|
||||||
|
# After running this script, populate the CI signing secrets with:
|
||||||
|
# gh secret set MOK_PRIVATE_KEY < build/keys/MOK.priv
|
||||||
|
# gh secret set MOK_CERT < build/keys/MOK.der
|
||||||
|
#
|
||||||
|
# Keep build/keys/ off-disk-backup-medium-of-record offline. Anyone with
|
||||||
|
# MOK.priv can sign code that boots on enrolled machines.
|
||||||
|
#
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
|
||||||
|
KEY_DIR="$REPO_ROOT/build/keys"
|
||||||
|
|
||||||
|
PRIV_KEY="$KEY_DIR/MOK.priv"
|
||||||
|
DER_CERT="$KEY_DIR/MOK.der"
|
||||||
|
PEM_CERT="$KEY_DIR/MOK.pem"
|
||||||
|
|
||||||
|
# Subject for the X.509 cert. Tweak CN if you fork the project.
|
||||||
|
SUBJ="/CN=veilor-os Machine Owner Key/O=veilor-org/OU=secure-boot/"
|
||||||
|
|
||||||
|
# Cert validity — 10 years. Long enough that we don't churn re-enrollment;
|
||||||
|
# short enough that a leaked key has a hard expiry.
|
||||||
|
DAYS=3650
|
||||||
|
|
||||||
|
if [[ -f "$PRIV_KEY" ]]; then
|
||||||
|
echo "[INFO] $PRIV_KEY already exists — skipping (idempotent)."
|
||||||
|
echo "[INFO] To regenerate, delete $KEY_DIR/ first."
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
mkdir -p "$KEY_DIR"
|
||||||
|
chmod 700 "$KEY_DIR"
|
||||||
|
|
||||||
|
echo "[*] Generating RSA-4096 MOK keypair → $KEY_DIR/"
|
||||||
|
|
||||||
|
# Single openssl invocation produces PEM private key + DER public cert.
|
||||||
|
# -nodes = no passphrase on the key (CI must use it non-interactively).
|
||||||
|
# Protect the resulting MOK.priv with filesystem perms only.
|
||||||
|
openssl req \
|
||||||
|
-new \
|
||||||
|
-x509 \
|
||||||
|
-newkey rsa:4096 \
|
||||||
|
-nodes \
|
||||||
|
-sha256 \
|
||||||
|
-days "$DAYS" \
|
||||||
|
-subj "$SUBJ" \
|
||||||
|
-keyout "$PRIV_KEY" \
|
||||||
|
-outform DER \
|
||||||
|
-out "$DER_CERT"
|
||||||
|
|
||||||
|
# Also emit a PEM-encoded copy of the cert — sbsign accepts PEM more
|
||||||
|
# reliably than DER in some distros' build of the tool.
|
||||||
|
openssl x509 -inform DER -in "$DER_CERT" -outform PEM -out "$PEM_CERT"
|
||||||
|
|
||||||
|
chmod 600 "$PRIV_KEY"
|
||||||
|
chmod 644 "$DER_CERT" "$PEM_CERT"
|
||||||
|
|
||||||
|
echo "[OK] MOK keypair written:"
|
||||||
|
echo " private : $PRIV_KEY (mode 600)"
|
||||||
|
echo " cert DER: $DER_CERT (enroll via mokutil --import)"
|
||||||
|
echo " cert PEM: $PEM_CERT (sbsign --cert input)"
|
||||||
|
echo ""
|
||||||
|
echo "[next] Upload to CI:"
|
||||||
|
echo " gh secret set MOK_PRIVATE_KEY < $PRIV_KEY"
|
||||||
|
echo " gh secret set MOK_CERT < $DER_CERT"
|
||||||
Loading…
Reference in a new issue