Compare commits
1 commit
main
...
backup/pre
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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/**'
|
||||
- 'build/**'
|
||||
- '.github/workflows/build-iso.yml'
|
||||
tags:
|
||||
- 'v*.*.*'
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
releasever:
|
||||
|
|
@ -24,6 +26,8 @@ jobs:
|
|||
name: Build live ISO
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 90
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
|
|
@ -52,7 +56,9 @@ jobs:
|
|||
# core problem). CI runners always start fresh, no version skew.
|
||||
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 \
|
||||
lorax \
|
||||
livecd-tools \
|
||||
|
|
@ -64,7 +70,11 @@ jobs:
|
|||
createrepo_c \
|
||||
git \
|
||||
which \
|
||||
shadow-utils
|
||||
shadow-utils \
|
||||
sbsigntools \
|
||||
openssl \
|
||||
dosfstools \
|
||||
mtools
|
||||
|
||||
cd /work
|
||||
|
||||
|
|
@ -105,8 +115,16 @@ jobs:
|
|||
# Move output ISO to expected dir
|
||||
mv ./veilor-os-43.iso build/out/ 2>/dev/null || mv ./*.iso build/out/ 2>/dev/null || true
|
||||
|
||||
# Rename + checksum
|
||||
ISO_NAME="veilor-os-${{ github.event.inputs.releasever || '43' }}-$(date +%Y%m%d-%H%M%S).iso"
|
||||
# Rename. For tag builds use the tag as the version stamp so
|
||||
# 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
|
||||
for f in *.iso; do
|
||||
[[ -f $f && $f != $ISO_NAME ]] && mv "$f" "$ISO_NAME"
|
||||
|
|
@ -114,6 +132,95 @@ jobs:
|
|||
sha256sum "$ISO_NAME" > "$ISO_NAME.sha256"
|
||||
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
|
||||
if: success()
|
||||
uses: actions/upload-artifact@v4
|
||||
|
|
@ -133,7 +240,102 @@ jobs:
|
|||
build/out/build.log
|
||||
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'
|
||||
uses: softprops/action-gh-release@v2
|
||||
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/cache/
|
||||
build/keys/
|
||||
*.iso
|
||||
*.img
|
||||
*.log
|
||||
|
|
@ -11,5 +12,7 @@ build/cache/
|
|||
secrets/
|
||||
*.key
|
||||
*.pem
|
||||
*.priv
|
||||
*.der
|
||||
test/veilor-vm.qcow2
|
||||
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