Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 47 additions & 13 deletions .github/workflows/book-build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,16 @@ on:
branches: [master, main]
paths:
- 'docs/book/**'
# The workflow runs on changes to itself, so a toolchain change here
# gets the run that proves it. Before this, the job only fired on
# docs/book/** and a workflow-only PR could not be tested at all.
- '.github/workflows/book-build.yml'
tags:
- 'v*'
pull_request:
paths:
- 'docs/book/**'
- '.github/workflows/book-build.yml'
release:
types: [published, created]
workflow_dispatch:
Expand All @@ -23,6 +28,14 @@ permissions:

env:
PANDOC_VERSION: "3.6.1"
# 3.4.0, not the newest: 3.5.0 (2026-06-28) migrated the default font from
# the `sourcesanspro` package to `sourcesans`, which the runner's
# texlive-fonts-extra (ubuntu-latest, 24.04 at the time of writing) does
# not ship, so 3.5.x fails with "File `sourcesans.sty' not found" -- seen
# on the first run of this change. 3.4.0 is the last release that uses
# sourcesanspro (present in texlive-fonts-extra, installed below) and
# already carries pandoc's default template merged through 3.9.
EISVOGEL_VERSION: "3.4.0"

jobs:
# ═══════════════════════════════════════════════════
Expand Down Expand Up @@ -95,12 +108,23 @@ jobs:
echo "pandoc $(pandoc --version | head -1)"

- name: Install Eisvogel template
# Eisvogel 2.4.0 with pandoc 3.6.1 fails every run with
# "LaTeX Error: Command \chead already defined" (exit 43). The 2.4.0
# template predates pandoc 3.6; the 3.x line merges pandoc's default
# LaTeX template up to 3.6.1 and beyond (its CHANGELOG names the
# merges) and its README states pandoc 3 compatibility. The exact
# LaTeX-level cause of the clash is not established here -- the run
# on the change is what shows whether the pair works. The 3.x
# tarball extracts into a versioned directory, not flat, so the copy
# names it.
# No `|| true` on tar: a missing or reshaped tarball must fail here,
# at the step that broke, not at the copy after it.
run: |
mkdir -p ~/.local/share/pandoc/templates
wget -q https://github.com/Wandmalfarbe/pandoc-latex-template/releases/download/v2.4.0/Eisvogel-2.4.0.tar.gz
tar xzf Eisvogel-2.4.0.tar.gz 2>/dev/null || true
cp eisvogel.latex ~/.local/share/pandoc/templates/eisvogel.latex
echo "Eisvogel template installed"
wget -q https://github.com/Wandmalfarbe/pandoc-latex-template/releases/download/v${EISVOGEL_VERSION}/Eisvogel-${EISVOGEL_VERSION}.tar.gz
tar xzf Eisvogel-${EISVOGEL_VERSION}.tar.gz
cp Eisvogel-${EISVOGEL_VERSION}/eisvogel.latex ~/.local/share/pandoc/templates/eisvogel.latex
echo "Eisvogel ${EISVOGEL_VERSION} template installed"

- name: Clean source
run: |
Expand Down Expand Up @@ -145,6 +169,22 @@ jobs:
CITE_ARGS="--citeproc --bibliography=references.bib"
fi

# Header and footer go through Eisvogel's own variables (header-left,
# header-right, footer-center; footer-right is the template's default
# page number). The template builds them with KOMA's scrlayer-scrpage,
# which defines \chead. This step used to inject \usepackage{fancyhdr}
# and \fancyhead/\fancyfoot through header-includes, which the template
# places BEFORE its own header setup -- so fancyhdr defined \chead first
# and scrlayer-scrpage's \newcommand failed: "Command \chead already
# defined". Two header packages, one command. Every run since both
# arrived in ef0dc50 died there, whatever the Eisvogel or pandoc version.
#
# No -V author=...: a -V value is inserted into the LaTeX verbatim, and
# the author string contains '&', which LaTeX reads as an alignment tab
# ("Misplaced alignment tab character &", run 3 of #135). book.md's
# metadata already carries the same author, and metadata is escaped by
# pandoc. Any -V value must be LaTeX-safe; title/subtitle/date are.

pandoc \
book.md \
-o "${{ steps.meta.outputs.pdf_name }}" \
Expand All @@ -163,7 +203,6 @@ jobs:
-V page-background-color="ffffff" \
-V "title=${{ steps.meta.outputs.title }}" \
-V "subtitle=Version ${{ steps.meta.outputs.version }}" \
-V "author=Srikanth Patchava & EmbeddedOS Contributors" \
-V "date=$(date +'%B %Y')" \
-V toc=true \
-V toc-depth=3 \
Expand All @@ -179,6 +218,9 @@ jobs:
-V float-placement-figure=H \
-V caption-justification=centering \
-V table-use-row-colors=true \
-V "header-left=\leftmark" \
-V "header-right=${{ steps.meta.outputs.version }}" \
-V "footer-center=EmbeddedOS Press — embeddedos-org.github.io" \
-V "header-includes=\
\usepackage{float}\
\usepackage{booktabs}\
Expand All @@ -187,14 +229,6 @@ jobs:
\captionsetup{font=small,labelfont=bf,format=hang}\
\captionsetup[figure]{name=Figure}\
\captionsetup[table]{name=Table}\
\usepackage{fancyhdr}\
\pagestyle{fancy}\
\fancyhead[L]{\small\leftmark}\
\fancyhead[R]{\small ${{ steps.meta.outputs.version }}}\
\fancyfoot[C]{\small EmbeddedOS Press — embeddedos-org.github.io}\
\fancyfoot[R]{\thepage}\
\renewcommand{\headrulewidth}{0.4pt}\
\renewcommand{\footrulewidth}{0.2pt}\
\usepackage{graphicx}\
\makeatletter\def\maxwidth{\ifdim\Gin@nat@width>\linewidth\linewidth\else\Gin@nat@width\fi}\makeatother\
\setkeys{Gin}{width=\maxwidth,keepaspectratio}" \
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -250,7 +250,7 @@ jobs:
ci-gate:
name: CI Gate
runs-on: ubuntu-22.04
needs: [test, build-arm, static-analysis]
needs: [test, build-arm, fuzz-build, static-analysis]
if: always()
steps:
- uses: actions/checkout@v4
Expand Down
8 changes: 8 additions & 0 deletions .github/workflows/eosim-sanity.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,14 @@ jobs:
install-validate:
name: Install & Validate (${{ matrix.os }}, Python ${{ matrix.python-version }})
runs-on: ${{ matrix.os }}
# Every step here is written in bash: `$(...)`, `|| { ... }`, a heredoc,
# and /tmp. Without this the Windows legs ran them under PowerShell, where
# `SITE_PACKAGES=$(...)` is an unknown command and the job went red, while
# the `|| { exit 1 }` guard in "Verify installation" parsed as an unexecuted
# script block and could never fail.
defaults:
run:
shell: bash
strategy:
fail-fast: false
matrix:
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/scorecard.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ jobs:
- uses: actions/checkout@v4
with:
persist-credentials: false
- uses: ossf/scorecard-action@v2.4.0
- uses: ossf/scorecard-action@v2.4.3
with:
results_file: results.sarif
results_format: sarif
Expand Down
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,13 @@
- **`image_verify.c`:** `eos_image_verify_integrity` rejects a zero `image_size`, and an `addr + hdr_size` that wraps `uint32_t`, instead of computing a payload address that is not the payload.

### Fixed
- **The tree did not configure, compile or link after the 09-07 batch merge.** `tests/CMakeLists.txt` registered `eboot_test_fdt_loader` twice; `core/sha512.c` had been replaced by a version predating the `bitlen[2]`/`buffer_len` context; `core/boot_log.c`, `core/secure_boot.c` and `core/fdt_loader.c` had been dropped from `eboot_core`; `scalarbase()` and `k_low_order[]` were defined twice; and the `eos_boot_log_get_head()` declaration was lost. All restored.
- **Install-path verification order settled: signature before anti-rollback.** `eos_fw_update_finalize()` verifies the Ed25519 signature over the signed header prefix first and reads the TLV security counter only after the prefix that binds it is authenticated (see `docs/adr/ADR-020`). The `fw_update` and `fw_transport` suites now stream genuinely signed images; `tools/gen_fw_update_test_sigs.py` emits their signatures as `tests/vectors/fw_update_test_sigs.h`, and `tests/unit/test_fw_update_test_sigs.py` pins the committed header to the generator's output.
- **`tests/CMakeLists.txt`:** the Valgrind list is derived from the registered suites again; a hand-written copy had replaced it, eleven registered suites were missing from `EBLDR_UNIT_TESTS`, and seven of those (`test_eos_sign_boot_path`, `test_fdt_loader`, `test_fw_decrypt`, `test_fw_update_sig`, `test_jump_app_bounds`, `test_qemu_arm64_timer`, `test_secure_boot_policy`) had no Valgrind run at all; the other four were only in the hand-written list. All eleven are appended.
- **`.github/workflows/ci.yml`:** `fuzz-build` is in the CI gate. It was added after the gate job and the gate never waited for it.
- **Unit suites count `tests_run`** as each test executes instead of assigning it a literal that the summary line then trusted.
- **`.github/workflows/eosim-sanity.yml`:** the install-validate job's steps are bash and now run under `shell: bash` on the Windows legs, where PowerShell rejected `SITE_PACKAGES=$(...)` and parsed `|| { exit 1 }` as an unexecuted script block.
- **`.github/workflows/scorecard.yml`:** `ossf/scorecard-action` moved to v2.4.3, the release hosted on ghcr.io; v2.4.0 pulls from gcr.io, which now requires GCP billing.
- **The tree did not compile.** `include/eos_image.h` declared `eos_crc32()` as `int eos_crc32(uint32_t, size_t, uint32_t *)` while `core/image_verify.c` defined it as `uint32_t eos_crc32(uint32_t, size_t)` -- a conflicting-types error that stopped the build at the first core source file. The declaration now matches the definition and the documented behaviour.
- **`ed25519_verify.c`:** `eos_ed25519_verify()` never performed the verification. Two merged copies of the challenge-hash step had been left in the function, the second referring to identifiers that do not exist (`sha512_ctx_t`, `sc_reduce`), and RFC 8032 step 4 -- the `[S]B == R + [k]A` check -- was absent entirely, leaving the function returning an undeclared `diff`. The duplicate is removed and the group-equation check restored; the function now passes the RFC 8032 test vectors and rejects tampered messages, every single-bit signature flip, wrong keys and malleated signatures.
- **`recovery.c`:** `recovery_handle_write()` declared `slot_size` twice, which does not compile. The bounds check now calls `eos_recovery_write_in_range()` -- the helper the unit tests already exercise -- so the wire-input rule has one definition, and an unmapped slot (`base == 0`) is rejected too.
Expand All @@ -22,6 +29,7 @@
- **`tests/unit/test_slot_manager.c`:** the file was two different test files spliced together mid-function -- stub definitions cut in half, `main()` calling twenty functions that do not exist. Rebuilt as one suite that exercises the real `core/slot_manager.c` through scriptable per-slot verification mocks.
- **`tests/unit/test_recovery.c`:** local stand-ins for the boot-log API conflicted with `include/eos_boot_log.h` and duplicated symbols now linked from `core/boot_log.c`. Removed.
- **`tests/CMakeLists.txt`:** `unit/test_fw_transport.c` existed but was never built or run. It is now registered.
- **`EBLDR_REQUIRE_SIGNATURES` and `EBLDR_RECOVERY_AUTH` were switches wired to nothing.** `CMakeLists.txt` offered both as options, forwarded each as a compile definition, and the README listed both as `ON` by default -- but no source file tested either macro, so a configure with one `OFF` produced byte-for-byte the same objects as `ON` (41 of 41 non-test objects identical with the definitions forced back in). Signature verification and recovery authentication are unconditional; the two options are gone, passing either as `OFF` fails the configure, the README says so, and `docs/book/book.md` no longer lists `EBLDR_SECURE_BOOT`, `EBLDR_MULTICORE`, `EBLDR_RECOVERY` and `EBLDR_BOOT_MENU`, none of which ever existed. `tests/unit/test_build_options_are_read.py` requires every forwarded `EBLDR_` definition to be read by a source file and every documented option to be declared.

### Added
- **`eos_crc32_checked()`** — CRC32 over a flash region that reports read failures through its return value. `eos_crc32()` is retained for API compatibility and documented as unsuitable for verification decisions.
Expand Down
28 changes: 19 additions & 9 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,6 @@ stm32f4 | stm32h7 | nrf52 | rpi4 | riscv64_virt | esp32 | x86_64_efi | \
imx8m | am64x | samd51 | sifive_u | cortex_r5 | none")

option(EBLDR_BUILD_TESTS "Build unit tests (native only)" OFF)
option(EBLDR_REQUIRE_SIGNATURES "Require Ed25519 signatures for boot" ON)
option(EBLDR_RECOVERY_AUTH "Require authentication for recovery commands" ON)
option(EBLDR_VERIFY_STAGE1 "Verify Stage-1 hash before jump" ON)
option(EBLDR_HARDENING "Enable compiler hardening flags" ON)
option(EBLDR_SANITIZE "Enable ASAN/UBSAN for host builds" OFF)
Expand Down Expand Up @@ -54,16 +52,25 @@ if(EBLDR_SANITIZE AND NOT CMAKE_CROSSCOMPILING AND CMAKE_C_COMPILER_ID MATCHES "
endif()

# Security compile definitions
if(EBLDR_REQUIRE_SIGNATURES)
add_compile_definitions(EBLDR_REQUIRE_SIGNATURES)
endif()
if(EBLDR_RECOVERY_AUTH)
add_compile_definitions(EBLDR_RECOVERY_AUTH)
endif()
if(EBLDR_VERIFY_STAGE1)
add_compile_definitions(EBLDR_VERIFY_STAGE1)
endif()

# Signature verification and recovery authentication have no build-time
# switch. EBLDR_REQUIRE_SIGNATURES and EBLDR_RECOVERY_AUTH used to be offered
# as options, but no source file read the definitions they produced, so a
# build with either OFF was byte-for-byte the build with it ON. A script that
# still passes one as OFF is asking for something this bootloader does not
# do; say so instead of configuring as if it had been granted.
foreach(_dead_switch EBLDR_REQUIRE_SIGNATURES EBLDR_RECOVERY_AUTH)
if(DEFINED ${_dead_switch} AND NOT ${_dead_switch})
message(FATAL_ERROR
"${_dead_switch}=OFF: there is no build that skips signature "
"verification or recovery authentication. The option was never "
"read by any source file and has been removed; drop the flag.")
endif()
endforeach()

# ====================================================================
# Core Libraries — platform-agnostic, always built
# ====================================================================
Expand All @@ -80,6 +87,7 @@ target_include_directories(eboot_hal PUBLIC ${EBLDR_INCLUDE_DIR})
# ---- Core boot logic ----
add_library(eboot_core STATIC
core/bootctl.c
core/boot_log.c
core/image_verify.c
core/slot_manager.c
core/boot_policy.c
Expand All @@ -106,8 +114,10 @@ add_library(eboot_core STATIC
core/os_adapter.c
core/ed25519_verify.c
core/sha512.c
core/keystore.c
core/secure_boot.c
core/fdt_loader.c
core/rollback.c
core/keystore.c
core/debug_lock.c
core/fw_decrypt.c
core/image_tlv.c
Expand Down
9 changes: 7 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,14 +64,19 @@ others (default `none` = native core-only build).

| Option | Default | Meaning |
|--------|---------|---------|
| `EBLDR_REQUIRE_SIGNATURES` | `ON` | Require Ed25519 signatures for boot |
| `EBLDR_VERIFY_STAGE1` | `ON` | Verify the Stage-1 hash before jumping |
| `EBLDR_RECOVERY_AUTH` | `ON` | Require authentication for recovery commands |
| `EBLDR_HARDENING` | `ON` | Compiler hardening (`-fstack-protector-strong`, `_FORTIFY_SOURCE=2`) |
| `EBLDR_SANITIZE` | `OFF` | ASan/UBSan for host builds |
| `EBLDR_BUILD_FUZZ` | `OFF` | Build libFuzzer targets |
| `EBLDR_BUILD_TESTS` | `OFF` | Build unit tests (native only); set to `ON` to enable |

Ed25519 signature verification and recovery-command authentication are not
options: no build of this bootloader skips either. `EBLDR_REQUIRE_SIGNATURES`
and `EBLDR_RECOVERY_AUTH` used to be listed here as `ON` by default, but no
source file read the definitions they produced, so `OFF` built the same
firmware as `ON`. Both have been removed; passing either as `OFF` now fails
the configure.

## Test

```bash
Expand Down
10 changes: 0 additions & 10 deletions core/ed25519_verify.c
Original file line number Diff line number Diff line change
Expand Up @@ -300,16 +300,6 @@ static int point_is_identity(gf p[4])
return diff == 0;
}

static void scalarbase(gf r[4], const uint8_t *s)
{
gf q[4];
fe_copy16(q[0], BX);
fe_copy16(q[1], BY);
fe_copy16(q[2], gf1);
fe_mul(q[3], BX, BY);
scalarmult(r, q, s);
}

/* Reject a public key outside the prime-order subgroup.
*
* Decoding a point is not enough. Ed25519 has eight points of low order, and
Expand Down
Loading
Loading