Skip to content

Let build.sh sign the kernel for Secure Boot - #130

Merged
mastacontrola merged 2 commits into
masterfrom
claude/secure-boot-shim-fos-ssnjdp
Aug 2, 2026
Merged

Let build.sh sign the kernel for Secure Boot#130
mastacontrola merged 2 commits into
masterfrom
claude/secure-boot-shim-fos-ssnjdp

Conversation

@darksidemilk

Copy link
Copy Markdown
Member

Companion to FOGProject/fogproject#961 (server-side signing and the enrolment kit) and the guide rewrite in FOGProject/fog-docs. This is the piece for people who build FOS themselves rather than using the released kernels.

The problem

FOS kernels ship unsigned, so a site running UEFI Secure Boot has to sign them itself. Doing that after the fact means the published .sha256 no longer matches the file anyone actually boots, and it has to be redone by hand on every build.

What changed

--sign-key / --sign-cert (or FOS_SIGN_KEY / FOS_SIGN_CERT, which is easier in CI) sbsign the artifact in buildKernel() between the copy into dist/ and the sha256sum, so the published hash covers the signed image.

With neither set the build is byte-for-byte what it was. Half a pair is refused rather than silently producing an unsigned kernel someone believes is signed — that failure would otherwise surface much later, at a client, as a Security Policy Violation with nothing on the server to explain it. A signing failure is fatal for the same reason.

Drive-by fix

Grouping the finalize step also fixes a latent bug. The old chain:

[[ ! -f $compiledfile ]] && echo 'File not found.' || cp ... && sha256sum ...

parses as ((A && B) || C) && D, so when the compiled kernel was missing it printed File not found. and then still ran sha256sum, writing an empty .sha256 for a file that was not there. Reproduced against the original line before changing it. buildFilesystem() has the same shape, but it is untouched here — out of scope for this change.

Verified

  • bash -n build.sh.
  • The finalize line exercised in a sandbox with a stubbed sbsign across three cases: no key produces a byte-identical artifact and a checksum that verifies; with a key the artifact is signed and the checksum covers the signed file; a missing compiled kernel now produces neither file.

Not verified

No kernel was compiled — the signing step is verified in isolation, not through a real ./build.sh -nka x64. Worth one real build with a throwaway key before merging.

🤖 Generated with Claude Code

https://claude.ai/code/session_01HZ2jZ7qgen6pXrbtmxShaw


Generated by Claude Code

FOS kernels ship unsigned, so a site running UEFI Secure Boot has to sign
them itself. Doing that after the fact means the published .sha256 no
longer matches the file anyone actually boots, and it has to be redone by
hand on every build.

--sign-key/--sign-cert (or FOS_SIGN_KEY/FOS_SIGN_CERT for CI) sbsign the
artifact between the copy into dist/ and the checksum, so the hash covers
the signed image. With neither set the build is byte-for-byte what it was.
Half a pair is refused rather than silently producing an unsigned kernel
someone believes is signed -- that failure would otherwise surface much
later, at a client, as a Security Policy Violation.

Grouping the finalize step also fixes a latent bug: the old
"a && b || cp && sha256sum" chain ran sha256sum even when the compiled
kernel was missing, printing "File not found." and then writing an empty
.sha256 for a file that was not there.
@mastacontrola

Copy link
Copy Markdown
Member

Heads-up from running this by hand while closing FOGProject/fogproject#960: --sign-cert cannot take the DER certificate that the documented procedure produces.

sbsign reads certificates with PEM_read_bio_X509 and rejects DER:

$ sbsign --key MOK.priv --cert MOK.der --output out.efi in.efi
Can't load certificate from file 'MOK.der'
error:0480006C:PEM routines:get_name:no start line:crypto/pem/pem_lib.c:792:Expecting: CERTIFICATE

fog-docs#84 documents --sign-cert /root/fog-secureboot/MOK.der, and the guide's Step 1 emits only MOK.der (-outform DER) because that is the format mokutil/MokManager need. So a user following the docs hits a signing failure on their first build.

Line ~83 passes $signCert straight through:

if ! sbsign --key "$signKey" --cert "$signCert" \

Cheapest robust fix is to normalise once after the key/cert pair is validated — if the file parses as DER, convert it to a PEM in a temp path and use that for sbsign; otherwise use it as given. Then --sign-cert/$FOS_SIGN_CERT accepts either format and nobody has to know which tool wants which encoding. FOGProject/fogproject#961 has the same issue and already does this normalisation in the other direction for its enrolment kit, so the two can share the same shape.

Everything else here held up in practice — signing before sha256sum is right, and the MOK-signed kernel booted under Secure Boot with LoadImage() consulting MokList. Worth noting the signature is appended in the PE certificate table, so a signed kernel still boots non-Secure-Boot clients unchanged; the only consequence is the byte change, which is exactly what this PR already accounts for by hashing after signing.

mastacontrola added a commit to FOGProject/fog-docs that referenced this pull request Aug 2, 2026
Two things this page got wrong, one of them blocking.

**Blocking: sbsign and sbverify cannot read a DER certificate.** Step 1
produced only MOK.der, and the signing steps then handed that file to
sbsign/sbverify, which load certificates with PEM_read_bio_X509 and
reject DER outright:

    $ sbsign --key MOK.priv --cert MOK.der --output out.efi in.efi
    Can't load certificate from file 'MOK.der'
    error:0480006C:PEM routines:get_name:no start line

mokutil and MokManager want the opposite. Neither tool says which format
it wanted, so anyone following this page hit an OpenSSL error that never
mentions the problem. Step 1 now produces MOK.pem alongside MOK.der and
says which tool takes which; the sbverify and build.sh examples use the
PEM. The installer's --secure-boot-cert accepts either and converts
internally, so this only bites when running the tools by hand.

**Out of date: the manual download steps.** /tftpboot/secureboot/ is
staged on every install from FOG 1.6.0 -- shim, signed snponly.efi,
MokManager, autoexec.ipxe and a MANIFEST, hash- and signer-verified when
the fog-ipxe release is built. So the curl/tar/rename/chown/restorecon
block is replaced by what is already there and how to check it. The
--secure-boot-stage flag it mentioned no longer exists; staging is
unconditional.

The explanatory notes are kept, because they are still why the files are
shaped the way they are -- in particular the -shim filename mechanism and
why the shim must come from the ipxe/shim release rather than
ipxeboot.tar.gz. The dual-signature point is now enforced at release time
rather than left as advice.

Also:

- The autoexec.ipxe section described two locations and one hard link.
  There are six directories an EMBED-less binary can boot from and the
  installer links all six; added an inode check to confirm it.
- Removed the standalone restorecon warning. The installer sets the
  SELinux context itself now (FOGProject/fogproject#963) -- it was
  default_t, which no confined tftpd may read, and it presented as a bare
  file-not-found with nothing in FOG's logs.
- New "Verified" section. The chain has now been run end to end with
  Secure Boot enforcing through to a completed 42 GB deploy, so the
  "MOK-signed bzImage has never been confirmed booting" limit is gone.
  States explicitly that no shim command or ShimRetainProtocol handling
  is needed -- shim's verification protocol survives into iPXE, and a
  reader could reasonably fear the opposite.
- New limit: automatic_next_path() is called only from shim's netboot and
  httpboot paths, so a shim booted from USB or an ESP ignores the -shim
  rename and falls back to ipxe.efi. Anyone building a Secure Boot USB
  from this page would have hit that with no obvious cause.
- fog-sign-kernel lives under $fogprogramdir, not a hardcoded /opt/fog.

Refs FOGProject/fogproject#960, FOGProject/fogproject#961,
FOGProject/fogproject#963, FOGProject/fos#130
--sign-cert went straight to sbsign, which reads certificates with
OpenSSL's PEM_read_bio_X509 and rejects DER outright:

    Can't load certificate from file 'MOK.der'
    error:0480006C:PEM routines:get_name:no start line

mokutil and MokManager -- the tools that enrol the same certificate on a
client -- want the opposite. Anyone following the Secure Boot how-to
therefore ends up holding one of each with nothing saying which tool
takes which, and picking wrong failed with an error that never mentions
the format.

Worse, sbsign's stderr was going to /dev/null, so that line was thrown
away and the operator got only "sbsign could not sign bzImage". The
error above is the whole diagnosis and it was being discarded.

So: accept either format and convert once up front, matching how
--secure-boot-cert now behaves in the installer, and capture sbsign's
stderr to print on failure. That second change is worth as much as the
first -- it covers unreadable keys and malformed images too, not just
this one mistake.

Also removes the partial .signed file when signing fails, rather than
leaving an unsigned-but-suspiciously-named artifact next to the kernel.

Verified: DER and PEM inputs both produce a kernel sbverify reports as
"Signature verification OK"; a non-certificate is refused up front
naming both formats tried; an unreadable key now prints sbsign's actual
complaint; no .signed artifact survives a failure; and the temporary PEM
is removed on exit.

Refs FOGProject/fogproject#960, FOGProject/fog-docs#84
@mastacontrola

Copy link
Copy Markdown
Member

Pushed de288aa — the DER bug, plus a second problem it exposed.

--sign-cert rejected DER

It went straight to sbsign, which reads certificates with OpenSSL's PEM_read_bio_X509 and rejects DER outright:

Can't load certificate from file 'MOK.der'
error:0480006C:PEM routines:get_name:no start line

mokutil and MokManager — the tools that enrol the same certificate on a client — want the opposite. Anyone following the Secure Boot how-to ends up holding one of each with nothing saying which tool takes which, and picking wrong fails with an error that never mentions the format.

Now accepts either and converts once up front, matching how --secure-boot-cert behaves in the installer after FOGProject/fogproject#961.

The bigger one: sbsign's stderr was being discarded

sbsign ... >/dev/null 2>&1

So the line above — the entire diagnosis — was thrown away, and the operator got only sbsign could not sign bzImage. Now captured and printed on failure:

Signing k2.efi for Secure Boot                Failed
 * sbsign could not sign k2.efi
   Can't load key from file '/…/notakey.pem'
   error:1E08010C:DECODER routines:OSSL_DECODER_from_bio:unsupported: … Input type: PEM

That change is worth as much as the format fix — it covers unreadable keys, wrong passphrases and malformed images too, not just this one mistake. Fixing DER without it would just have moved the next person's silent failure one step along.

Also removes the partial .signed file when signing fails, instead of leaving an unsigned-but-suspiciously-named artifact beside the kernel.

Verified

Ran the real validation block and signKernel() extracted verbatim from build.sh:

case result
DER cert converts, signs, Signature verification OK
PEM cert signs, Signature verification OK
non-certificate refused up front, naming both formats tried
unreadable key sbsign's actual complaint now printed
failed signing no .signed artifact left behind
temp PEM removed on exit via trap

The temp file is world-readable, which is fine — the certificate is public; it is the private key beside it that matters. There was no existing trap in build.sh to clobber.

sha256sum still runs after signing, so the published hash covers the signed image.

This was the last open instance of the DER bug — the installer side landed in FOGProject/fogproject#961 and the docs in FOGProject/fog-docs#84.

@mastacontrola
mastacontrola merged commit 6bdb3b1 into master Aug 2, 2026
@mastacontrola
mastacontrola deleted the claude/secure-boot-shim-fos-ssnjdp branch August 2, 2026 13:57
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants