Skip to content
Closed
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
6 changes: 4 additions & 2 deletions docs/skills/ddi-installer.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,9 +73,11 @@ container runtime pod sandboxes.
`/usr/lib/repart.sysinstall.d/` if it is populated; otherwise it falls back
to `/usr/lib/repart.d/`. The target recipes are staged at
`/usr/lib/repart.d/` (`10-esp.conf`, `20-root-a.conf`, `30-var.conf`).
6. `20-root-a.conf` copies the DDI block-for-block from
6. `20-root-a.conf` (or `20-usr-a.conf`) copies the DDI block-for-block from
`/dev/disk/by-partlabel/bluefin-installer-data` (the embedded DDI data
partition on the installer media).
partition on the installer media). The DDI payload is built as a dm-verity
protected image with the verity hash tree appended at offset 1,065,345,024,
and its root hash is pinned by `verity.usrhash` on the target UKI command line.
7. Target OS volume expansion is handled by `systemd-growfs`; the target OS
stack includes `xfsprogs` so the root and `/var` filesystems can grow to fill
their partitions on first boot.
Expand Down
53 changes: 35 additions & 18 deletions elements/oci/bluefin-server-ddi.bst
Original file line number Diff line number Diff line change
@@ -1,14 +1,16 @@
kind: script
description: |
Produce the Bluefin Server OS DDI payload filesystem image.
Produce the Bluefin Server OS DDI payload filesystem image as a dm-verity
/usr image pinned by verity.usrhash.

Output: bluefin-server-ddi-<ver>.raw.zst + SHA256SUMS
Output: bluefin-server-ddi-<ver>.raw.zst + SHA256SUMS + verity root hash

The output .raw is a filesystem image consumed by installer repart config:
files/installer/repart.d/20-root-a.conf via CopyBlocks=.

build-depends:
- base/base-stack.bst
- freedesktop-sdk.bst:components/cryptsetup.bst
- freedesktop-sdk.bst:components/xfsprogs.bst
- freedesktop-sdk.bst:components/findutils.bst
- freedesktop-sdk.bst:components/kmod.bst
Expand Down Expand Up @@ -85,24 +87,39 @@ config:
chmod 0600 /layer/etc/ssh/ssh_host_*_key
chmod 0644 /layer/etc/ssh/ssh_host_*.pub

# Sum actual file sizes across bind mounts (du -sb reports content bytes, not overlay blocks).
ROOT_BYTES=$(du -sb /layer | cut -f1)
echo "Computed root filesystem size: ${ROOT_BYTES} bytes"

# 25% overhead and 100MB margin for XFS metadata, journal, and structures.
# ponytail: 25% overhead + 100MB margin for XFS.
TARGET_BYTES=$(( ROOT_BYTES + ROOT_BYTES / 4 + 100 * 1024 * 1024 ))

# Align TARGET_BYTES to 4096-byte boundary (multiple of 512/4096) for sector compatibility
# ponytail: sector alignment is required by systemd-repart CopyBlocks=.
TARGET_BYTES=$(( (TARGET_BYTES + 4095) / 4096 * 4096 ))
# Enforce the hard budget for the /usr payload: filesystem occupies the
# first 1,065,345,024 bytes; the verity hash tree follows in the same partition.
HASH_OFFSET=1065345024

# Pre-allocate the raw file to the exact target size.
# ponytail: pre-allocating is the standard way to enforce the target size of mkfs.xfs.
truncate -s "${TARGET_BYTES}" "${OUT}/${FNAME}"
ROOT_BYTES=$(du -sb /layer | cut -f1)
echo "Computed root filesystem size: ${ROOT_BYTES} bytes (budget: ${HASH_OFFSET} bytes)"
if [ "${ROOT_BYTES}" -ge "${HASH_OFFSET}" ]; then
echo "ERROR: /usr payload size (${ROOT_BYTES} bytes) exceeds the hard verity budget of ${HASH_OFFSET} bytes" >&2
exit 1
fi

echo "Allocating XFS image of ${TARGET_BYTES} bytes"
mkfs.xfs -f -L bluefin-root -p /layer "${OUT}/${FNAME}"
# Pre-allocate the raw file to the exact budget size for the XFS filesystem.
truncate -s "${HASH_OFFSET}" "${OUT}/${FNAME}"

echo "Allocating XFS image of ${HASH_OFFSET} bytes"
mkfs.xfs -f -L bluefin-usr -p /layer "${OUT}/${FNAME}"

# Append the dm-verity hash tree at offset 1,065,345,024.
# 1,065,345,024 bytes = 260,094 blocks of 4096 bytes.
DATA_BLOCKS=$(( HASH_OFFSET / 4096 ))
veritysetup format \
--hash=sha256 \
--data-block-size=4096 \
--hash-block-size=4096 \
--data-blocks="${DATA_BLOCKS}" \
--hash-offset="${HASH_OFFSET}" \
--root-hash-file="${OUT}/${FNAME}.verity" \
"${OUT}/${FNAME}" "${OUT}/${FNAME}"

ROOT_HASH="$(cat "${OUT}/${FNAME}.verity")"
echo "Verity root hash: ${ROOT_HASH}"
cp "${OUT}/${FNAME}.verity" "${OUT}/verity.usrhash"
cp "${OUT}/${FNAME}.verity" "${OUT}/bluefin-server-ddi-%{ddi-version}.verity"

zstd --rm -T0 -19 -q "${OUT}/${FNAME}" -o "${OUT}/${FNAME}.zst"
(
Expand Down
11 changes: 9 additions & 2 deletions elements/oci/bluefin-server-installer.bst
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,13 @@ config:
rm -f /target-root/tmp/ld.so.conf
cp /target-root/etc/ld.so.cache /etc/ld.so.cache

# Read the verity root hash from the staged DDI payload
if [ ! -f /ddi/verity.usrhash ] && ! ls /ddi/*.verity >/dev/null 2>&1; then
echo "ERROR: verity root hash not found in /ddi" >&2
exit 1
fi
USR_HASH="$(cat /ddi/verity.usrhash 2>/dev/null || head -n1 /ddi/*.verity)"

# Run dracut in generic (non-hostonly) mode and explicitly add XFS, VirtIO, and NVMe storage drivers.
dracut -v --reproducible --no-machineid --no-hostonly \
--add-drivers "virtio virtio_blk virtio_pci virtio_scsi nvme nvme_core xfs erofs overlay zfs spl" \
Expand All @@ -131,13 +138,13 @@ config:
--kmoddir "/target-root/usr/lib/modules/${TARGET_KVER}" \
--kernel-image "${TARGET_KERNEL}" \
--kver "${TARGET_KVER}" \
--kernel-cmdline "rw console=ttyS0,115200 console=tty0 quiet loglevel=3 audit=0" \
--kernel-cmdline "rw console=ttyS0,115200 console=tty0 quiet loglevel=3 audit=0 verity.usr=PARTLABEL=USR-A verity.usrhash=${USR_HASH}" \
/tmp/bluefin-server-target.initramfs

ukify build \
--linux="${TARGET_KERNEL}" \
--initrd=/tmp/bluefin-server-target.initramfs \
--cmdline="rw console=ttyS0,115200 console=tty0 quiet loglevel=3 audit=0" \
--cmdline="rw console=ttyS0,115200 console=tty0 quiet loglevel=3 audit=0 verity.usr=PARTLABEL=USR-A verity.usrhash=${USR_HASH}" \
--output=/target-root/boot/EFI/Linux/bluefin-server.efi
cp /target-root/boot/EFI/Linux/bluefin-server.efi /layer/usr/lib/bluefin-server/bluefin-server.efi

Expand Down
111 changes: 111 additions & 0 deletions tests/unit/test_ddi_verity.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
"""Contract tests for dm-verity /usr image payload pinned by verity.usrhash.

Covers projectbluefin/server#135 (Phase 4 of the Flatcar base migration):
- DDI element emits an XFS /usr image with an appended verity hash tree at
offset 1,065,345,024 and prints the root hash.
- Build enforces a hard size budget of 1,065,345,024 bytes and fails loudly when exceeded.
- Target UKI cmdline carries verity.usr=PARTLABEL=USR-A and verity.usrhash=<roothash>.
"""

from __future__ import annotations

import re
from pathlib import Path
import yaml

REPO_ROOT = Path(__file__).resolve().parents[2]
DDI_ELEMENT = REPO_ROOT / "elements" / "oci" / "bluefin-server-ddi.bst"
INSTALLER_ELEMENT = REPO_ROOT / "elements" / "oci" / "bluefin-server-installer.bst"

VERITY_HASH_OFFSET = 1065345024
VERITY_BLOCK_SIZE = 4096
EXPECTED_DATA_BLOCKS = 260094


def _load_ddi_data() -> dict:
content = DDI_ELEMENT.read_text(encoding="utf-8")
return yaml.safe_load(content)


def test_verity_budget_and_blocks_alignment() -> None:
"""Verify that the verity offset aligns with standard 4096-byte sectors."""
assert VERITY_HASH_OFFSET % VERITY_BLOCK_SIZE == 0
assert VERITY_HASH_OFFSET // VERITY_BLOCK_SIZE == EXPECTED_DATA_BLOCKS


def test_ddi_depends_on_cryptsetup() -> None:
"""Verify bluefin-server-ddi.bst includes cryptsetup for veritysetup."""
data = _load_ddi_data()
build_depends = data.get("build-depends", [])
dep_names = []
for dep in build_depends:
if isinstance(dep, str):
dep_names.append(dep)
elif isinstance(dep, dict) and "filename" in dep:
dep_names.append(dep["filename"])

assert "freedesktop-sdk.bst:components/cryptsetup.bst" in dep_names, (
"bluefin-server-ddi.bst must include cryptsetup.bst in build-depends "
"to format the dm-verity hash tree"
)


def test_ddi_enforces_hard_size_budget() -> None:
"""Verify that the DDI checks payload size against the 1,065,345,024 budget and fails loudly."""
content = DDI_ELEMENT.read_text(encoding="utf-8")

assert "HASH_OFFSET=1065345024" in content
assert re.search(r'\[\s*"\$\{ROOT_BYTES\}"\s+-ge\s*"\$\{HASH_OFFSET\}"\s*\]', content), (
"DDI element must compare ROOT_BYTES against HASH_OFFSET"
)
assert 'truncate -s "${HASH_OFFSET}"' in content or "truncate -s 1065345024" in content, (
"DDI image must be pre-allocated to the exact hash-offset budget"
)


def test_ddi_appends_verity_hash_tree() -> None:
"""Verify veritysetup format is invoked with exact upstream parameters."""
content = DDI_ELEMENT.read_text(encoding="utf-8")

assert "veritysetup format" in content
assert "--hash=sha256" in content
assert "--data-block-size=4096" in content
assert "--hash-block-size=4096" in content
assert (
'--data-blocks="${DATA_BLOCKS}"' in content
or f"--data-blocks={EXPECTED_DATA_BLOCKS}" in content
)
assert (
'--hash-offset="${HASH_OFFSET}"' in content
or f"--hash-offset={VERITY_HASH_OFFSET}" in content
)
assert "--root-hash-file=" in content


def test_ddi_prints_and_exports_root_hash() -> None:
"""Verify that the DDI prints the root hash and saves verity.usrhash for UKI assembly."""
content = DDI_ELEMENT.read_text(encoding="utf-8")

assert 'echo "Verity root hash: ' in content
assert "verity.usrhash" in content


def test_installer_reads_verity_usrhash_and_configures_uki() -> None:
"""Verify that the installer reads the verity root hash and pins it in UKI cmdline."""
content = INSTALLER_ELEMENT.read_text(encoding="utf-8")

assert "/ddi/verity.usrhash" in content
assert "verity.usr=PARTLABEL=USR-A" in content
assert "verity.usrhash=" in content

# UKI command line assertion
uki_match = re.search(
r'ukify build\s+.*?--cmdline="([^"]+)"\s+'
r"[ \t\\\r\n]+--output=/target-root/boot/EFI/Linux/bluefin-server\.efi",
content,
flags=re.DOTALL,
)
assert uki_match, "ukify build command for target UKI must be present"
cmdline = uki_match.group(1)
assert "verity.usr=PARTLABEL=USR-A" in cmdline
assert "verity.usrhash=${USR_HASH}" in cmdline
5 changes: 4 additions & 1 deletion tests/unit/test_installer_contract.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,10 @@ def test_installer_runtime_and_boot_contracts() -> None:
assert "freedesktop-sdk.bst:bootstrap/bash.bst" in installer_stack
assert "console=ttyS0,115200 rw" in installer_element
assert "unattended" not in published_uki_cmdline
assert target_uki_cmdline == "rw console=ttyS0,115200 console=tty0 quiet loglevel=3 audit=0"
assert target_uki_cmdline == (
"rw console=ttyS0,115200 console=tty0 quiet loglevel=3 audit=0 "
"verity.usr=PARTLABEL=USR-A verity.usrhash=${USR_HASH}"
)
assert (
'-append "systemd.unit=system-install.target '
'console=tty0 console=ttyS0,115200 rw unattended"'
Expand Down
Loading