Files
ipxe/.github/workflows/build.yml
T
Michael Brown 21b5bd8406 [ci] Add support for building UEFI Secure Boot signed binaries
Add a job that takes the bin-x86_64-efi-sb and bin-arm64-efi-sb build
artifacts and signs them for UEFI Secure Boot.

The hardware token containing the trusted signing key is attached to a
dedicated self-hosted GitHub Actions runner.  Only tagged release
versions (and commits on the "sbsign" testing branch) will be signed
on this dedicated runner.  All other commits will be signed on a
standard GitHub hosted runner using an ephemeral test certificate that
is not trusted for UEFI Secure Boot.

No other work is done as part of the signing job.  The iPXE source
code is not even checked out, minimising any opportunity to grant
untrusted code access to the hardware token.

The hardware token password is held as a deployment environment
secret, with the environment being restricted to allow access only for
tagged release versions (and commits on the "sbsign" testing branch)
to provide an additional layer of security.

The signing certificates and intermediate certificates are obtained
from the iPXE Secure Boot CA repository, with the certificate selected
via deployment environment variables.

To minimise hidden state held on the self-hosted runner, the pcscd
service is run via a service container, with the hardware token passed
in via "--devices /dev/bus/usb".

Select the deployment environment name (and hence runner tag) via a
repository variable SBSIGN_ENVIRONMENT, so that forks do not attempt
to start jobs on a non-existent self-hosted runner.

Signed-off-by: Michael Brown <mcb30@ipxe.org>
2026-02-15 22:50:12 +00:00

426 lines
12 KiB
YAML

name: Build
on:
push:
pull_request:
env:
MAKEFLAGS: "-j4 GITVERSION=${{ github.sha }}"
jobs:
bios:
name: BIOS / ${{ matrix.arch }}
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
arch:
- i386
- x86_64
container:
image: ghcr.io/ipxe/ipxe-builder-${{ matrix.arch }}
env:
bindir: >-
${{ matrix.arch == 'i386' && 'bin' || 'bin-x86_64-pcbios' }}
steps:
- name: Check out code
uses: actions/checkout@v6
- name: Build
working-directory: src
run: |
make ${{ env.bindir }}/10ec8139.rom \
${{ env.bindir }}/8086100e.mrom \
${{ env.bindir }}/ipxe.dsk \
${{ env.bindir }}/ipxe.iso \
${{ env.bindir }}/ipxe.lkrn \
${{ env.bindir }}/ipxe.pxe \
${{ env.bindir }}/ipxe.usb \
${{ env.bindir }}/ipxe-legacy.lkrn \
${{ env.bindir }}/ipxe-legacy.pxe \
${{ env.bindir }}/undionly.kpxe \
${{ env.bindir }}/errors
- name: Hardware list
working-directory: src
run: |
./util/niclist.pl \
--format dokuwiki \
--sort bus-,vendor_id,device_id,device_name \
--output ${{ env.bindir }}/niclist.txt
- name: Upload
uses: actions/upload-artifact@v6
with:
name: ${{ env.bindir }}
if-no-files-found: error
path: |
src/${{ env.bindir }}/ipxe.lkrn
src/${{ env.bindir }}/ipxe.pxe
src/${{ env.bindir }}/ipxe-legacy.lkrn
src/${{ env.bindir }}/ipxe-legacy.pxe
src/${{ env.bindir }}/undionly.kpxe
src/${{ env.bindir }}/errors
src/${{ env.bindir }}/niclist.txt
sbi:
name: SBI / ${{ matrix.arch }}
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
arch:
- riscv32
- riscv64
container:
image: ghcr.io/ipxe/ipxe-builder-${{ matrix.arch }}
env:
bindir: bin-${{ matrix.arch }}
steps:
- name: Check out code
uses: actions/checkout@v6
- name: Build
working-directory: src
run: |
make ${{ env.bindir }}/ipxe.pf32 \
${{ env.bindir }}/ipxe.lkrn \
${{ env.bindir }}/ipxe.sbi \
${{ env.bindir }}/errors
- name: Upload
uses: actions/upload-artifact@v6
with:
name: ${{ env.bindir }}
if-no-files-found: error
path: |
src/${{ env.bindir }}/ipxe.lkrn
src/${{ env.bindir }}/ipxe.sbi
src/${{ env.bindir }}/errors
uefi:
name: UEFI / ${{ matrix.arch }}
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
arch:
- arm32
- arm64
- i386
- loong64
- riscv32
- riscv64
- x86_64
container:
image: ghcr.io/ipxe/ipxe-builder-${{ matrix.arch }}
env:
bindir: bin-${{ matrix.arch }}-efi
steps:
- name: Check out code
uses: actions/checkout@v6
- name: Build
working-directory: src
run: |
make ${{ env.bindir }}/ipxe.efi \
${{ env.bindir }}/ipxe.iso \
${{ env.bindir }}/ipxe.usb \
${{ env.bindir }}/ipxe-legacy.efi \
${{ env.bindir }}/snponly.efi \
${{ env.bindir }}/errors
- name: Upload
uses: actions/upload-artifact@v6
with:
name: ${{ env.bindir }}
if-no-files-found: error
path: |
src/${{ env.bindir }}/ipxe.efi
src/${{ env.bindir }}/ipxe-legacy.efi
src/${{ env.bindir }}/snponly.efi
src/${{ env.bindir }}/errors
uefi-sb:
name: UEFI SB / ${{ matrix.arch }}
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
arch:
- arm64
- x86_64
container:
image: ghcr.io/ipxe/ipxe-builder-${{ matrix.arch }}
env:
bindir: bin-${{ matrix.arch }}-efi-sb
outputs:
sbsignenv: ${{ steps.sbsignenv.outputs.sbsignenv }}
steps:
- name: Check out code
uses: actions/checkout@v6
- name: Build
working-directory: src
run: |
make ${{ env.bindir }}/ipxe.efi \
${{ env.bindir }}/snponly.efi
- name: Upload
uses: actions/upload-artifact@v6
with:
name: unsigned-${{ env.bindir }}
if-no-files-found: error
path: |
src/${{ env.bindir }}/ipxe.efi
src/${{ env.bindir }}/snponly.efi
- name: Select environment
id: sbsignenv
if: >-
github.ref == 'refs/heads/sbsign' ||
startsWith ( github.ref, 'refs/tags/v' )
run: |
echo "sbsignenv=${{ vars.SBSIGN_ENVIRONMENT }}" >> $GITHUB_OUTPUT
sbsign:
name: SB Sign / ${{ matrix.arch }}
runs-on: ${{ needs.uefi-sb.outputs.sbsignenv || 'ubuntu-latest' }}
needs:
- uefi-sb
strategy:
fail-fast: false
matrix:
arch:
- arm64
- x86_64
container:
image: ghcr.io/ipxe/ipxe-signer
volumes:
- run-pcscd:/run/pcscd
services:
pcscd:
image: ghcr.io/ipxe/ipxe-signer-pcscd
volumes:
- run-pcscd:/run/pcscd
options: >-
${{ needs.uefi-sb.outputs.sbsignenv && '--device /dev/bus/usb' }}
--label OPTIONS_VALUE_CANNOT_BE_EMPTY=1
env:
binaries: >-
ipxe.efi
snponly.efi
bindir: bin-${{ matrix.arch }}-efi-sb
cacert: ${{ vars.SBSIGN_CA_CERT || 'testsign.crt' }}
pkcs11: ${{ secrets.SBSIGN_PASSWORD && 'true' }}
signcerts: ${{ vars.SBSIGN_CERTS || 'testsign.crt' }}
signkey: ${{ vars.SBSIGN_KEY || 'testsign.key' }}
signpass: ${{ secrets.SBSIGN_PASSWORD || 'testpw' }}
environment: ${{ needs.uefi-sb.outputs.sbsignenv }}
steps:
- name: Check out code
uses: actions/checkout@v6
with:
repository: ipxe/secure-boot-ca
- name: Download
uses: actions/download-artifact@v7
with:
name: unsigned-${{ env.bindir }}
path: unsigned
- name: Test certificate
run: |
openssl req \
-newkey rsa:2048 -passout 'pass:testpw' -keyout testsign.key \
-subj '/CN=Test Signing/' -x509 -out testsign.crt
- name: Certificate chain
run: |
for cert in ${{ env.signcerts }} ; do
openssl x509 -in ${cert} -noout -text
cat ${cert} >> chain.crts
done
- name: Sign
run: |
for binary in ${{ env.binaries }} ; do
osslsigncode sign \
${{ env.pkcs11 && '-pkcs11module' }} \
${{ env.pkcs11 && '/usr/lib64/opensc-pkcs11.so' }} \
-certs chain.crts \
-key ${{ env.signkey }} \
-pass ${{ env.signpass }} \
-ts http://timestamp.digicert.com \
-in unsigned/${binary} \
-out signed/${binary}
done
- name: Verify
run: |
for binary in ${{ env.binaries }} ; do
osslsigncode verify -CAfile ${{ env.cacert }} signed/${binary}
done
- name: Upload
uses: actions/upload-artifact@v6
with:
name: ${{ env.bindir }}
if-no-files-found: error
path: |
signed/ipxe.efi
signed/snponly.efi
linux:
name: Linux / ${{ matrix.arch }}
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
include:
- arch: arm32
exec: qemu-arm-static
- arch: arm64
exec: qemu-aarch64-static
- arch: i386
exec: valgrind
- arch: loong64
exec: qemu-loongarch64-static
- arch: riscv32
exec: qemu-riscv32-static
- arch: riscv64
exec: qemu-riscv64-static
- arch: x86_64
exec: valgrind
container:
image: ghcr.io/ipxe/ipxe-builder-${{ matrix.arch }}
env:
bindir: bin-${{ matrix.arch }}-linux
steps:
- name: Check out code
uses: actions/checkout@v6
- name: Build
working-directory: src
run: |
make ${{ env.bindir }}/ipxe.linux \
${{ env.bindir }}/tests.linux \
${{ env.bindir }}/errors
- name: Upload
uses: actions/upload-artifact@v6
with:
name: ${{ env.bindir }}
if-no-files-found: error
path: |
src/${{ env.bindir }}/ipxe.linux
src/${{ env.bindir }}/tests.linux
src/${{ env.bindir }}/errors
- name: Test
working-directory: src
run: |
${{ matrix.exec }} ${{ env.bindir }}/tests.linux
combine:
name: BIOS + UEFI
runs-on: ubuntu-latest
needs:
- bios
- uefi
container:
image: ghcr.io/ipxe/ipxe-signer
env:
binaries: >-
bin-x86_64-pcbios/${DRIVERS}.lkrn
bin-arm32-efi/${DRIVERS}.efi
bin-arm64-efi/${DRIVERS}.efi
bin-i386-efi/${DRIVERS}.efi
bin-loong64-efi/${DRIVERS}.efi
bin-riscv32-efi/${DRIVERS}.efi
bin-riscv64-efi/${DRIVERS}.efi
bin-x86_64-efi/${DRIVERS}.efi
steps:
- name: Check out code
uses: actions/checkout@v6
- name: Download
uses: actions/download-artifact@v7
with:
pattern: "{bin-x86_64-pcbios,bin-*-efi}"
- name: Combine
run: |
# Provide an editable placeholder autoexec.ipxe for the USB image
cat > autoexec.ipxe <<'EOF'
#!ipxe
echo
prompt --key 0x02 --timeout 2000 \
Press Ctrl-B for the iPXE command line... \
&& shell || autoboot
EOF
for DRIVERS in ipxe ipxe-legacy ; do
./src/util/genfsimg -o ${DRIVERS}.iso ${{ env.binaries }}
./src/util/genfsimg -o ${DRIVERS}.usb -s autoexec.ipxe \
${{ env.binaries }}
done
- name: Upload
uses: actions/upload-artifact@v6
with:
name: bin-combi
if-no-files-found: error
path: |
ipxe.iso
ipxe.usb
ipxe-legacy.iso
ipxe-legacy.usb
publish:
name: Publish
runs-on: ubuntu-latest
needs:
- bios
- sbi
- uefi
- sbsign
- linux
- combine
if: >-
github.event_name == 'push' &&
github.ref == 'refs/heads/master' &&
vars.PAGES_REPO_NAME
env:
workflow_url: >-
${{ github.server_url }}/${{ vars.PAGES_REPO_OWNER }}/${{ ''
}}${{ vars.PAGES_REPO_NAME }}/actions/workflows/build.yml
environment:
name: publish
url: ${{ env.workflow_url }}
steps:
- name: Get token
id: token
uses: actions/create-github-app-token@v2
with:
app-id: ${{ vars.WORKFLOW_DISPATCHER_ID }}
private-key: ${{ secrets.WORKFLOW_DISPATCHER_KEY }}
owner: ${{ vars.PAGES_REPO_OWNER }}
repositories: ${{ vars.PAGES_REPO_NAME }}
- name: Dispatch
env:
GH_REPO: ${{ vars.PAGES_REPO_OWNER }}/${{ vars.PAGES_REPO_NAME }}
GH_TOKEN: ${{ steps.token.outputs.token }}
run: |
gh workflow run build.yml -f run_id=${{ github.run_id }}
echo "Results at ${{ env.workflow_url }}"