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
5 changes: 3 additions & 2 deletions acl/docs/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,8 +53,9 @@ ACL's primary boot path uses **systemd-boot** with **Unified Kernel Images (UKI)

The `/usr` partition (USR-A) is a read-only btrfs filesystem with zstd compression. **dm-verity** provides block-level integrity verification:

- The verity hash tree is appended to the USR partition data.
- At boot, `systemd-veritysetup` activates the verity device using kernel command-line parameters embedded in the UKI: `systemd.verity_usr_data`, `systemd.verity_usr_hash`, and `systemd.verity_usr_options=hash-offset=<N>,panic-on-corruption`.
- The verity hash tree is stored in a dedicated hash partition (HASH-A, immediately following USR-A on disk).
- At boot, `systemd-veritysetup` activates the verity device using kernel command-line parameters embedded in the UKI: `systemd.verity_usr_data=UUID=<btrfs-uuid>`, `systemd.verity_usr_hash=UUID=<verity-uuid>`, and `systemd.verity_usr_options=panic-on-corruption`.
- The UKI uses filesystem/verity UUIDs (not partition UUIDs) so the same UKI works for both A and B slots.
- Any corruption of `/usr` causes an immediate kernel panic, preventing the system from running a tampered image.

The A/B partition scheme (USR-A / USR-B) enables safe updates: the inactive slot is written, verified, and then atomically switched on reboot.
Expand Down
11 changes: 11 additions & 0 deletions build_library/build_image_util.sh
Original file line number Diff line number Diff line change
Expand Up @@ -886,6 +886,8 @@ EOF

"${BUILD_LIBRARY_DIR}/disk_util" --disk_layout="${disk_layout}" --arch="${BOARD}" verity \
--root_hash="${BUILD_DIR}/${image_name%.bin}_verity.txt" \
--verity_uuid="${BUILD_DIR}/${image_name%.bin}_verity_uuid.txt" \
--fs_uuid="${BUILD_DIR}/${image_name%.bin}_fs_uuid.txt" \
"${BUILD_DIR}/${image_name}"

# Magic alert! Root hash injection works by writing the hash value to a
Expand Down Expand Up @@ -953,6 +955,15 @@ EOF
if [[ -f "${verity_hash_file}" ]]; then
bootloader_args+=(--verity_hash="${verity_hash_file}")
fi
# Pass UUID files for UUID-based cmdline construction
local verity_uuid_file="${BUILD_DIR}/${image_name%.bin}_verity_uuid.txt"
if [[ -f "${verity_uuid_file}" ]]; then
bootloader_args+=(--verity_uuid="${verity_uuid_file}")
fi
local fs_uuid_file="${BUILD_DIR}/${image_name%.bin}_fs_uuid.txt"
if [[ -f "${fs_uuid_file}" ]]; then
bootloader_args+=(--fs_uuid="${fs_uuid_file}")
fi
fi
else
bootloader_args+=(--noverity)
Expand Down
34 changes: 27 additions & 7 deletions build_library/disk_layout_uki.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,23 +23,37 @@
"uuid": "7130c94a-213a-4e5a-8e26-6cce9662f132",
"type": "flatcar-rootfs",
"blocks": "2097152",
"fs_blocks": "260094",
"fs_blocks": "262144",
"fs_type": "btrfs",
"fs_compression": "zstd",
"mount": "/usr",
"verity_hash": "3",
"features": [
"prioritize",
"verity"
]
},
"3": {
"label": "HASH-A",
"uuid": "b736baf1-cdb4-4535-beba-ddaaa30ad7b7",
"type": "dps-usr-verity",
"blocks": "20480"
},
"4": {
"label": "USR-B",
"uuid": "e03dd35c-7c2d-4a47-b3fe-27f15780a57c",
"type": "flatcar-rootfs",
"blocks": "2097152",
"fs_blocks": "262144"
"fs_blocks": "262144",
"verity_hash": "5"
},
"4": {
"5": {
"label": "HASH-B",
"uuid": "35bdf78b-c453-4661-98e6-f834f534ef5b",
"type": "dps-usr-verity",
"blocks": "20480"
},
"6": {
"label": "OEM",
"fs_label": "OEM",
"type": "data",
Expand All @@ -48,7 +62,7 @@
"fs_compression": "zlib",
"mount": "/oem"
},
"5": {
"7": {
"label": "ROOT",
"fs_label": "ROOT",
"type": "dps-root",
Expand All @@ -58,21 +72,21 @@
}
},
"vm": {
"5": {
"7": {
"label": "ROOT",
"fs_label": "ROOT",
"blocks": "12943360"
}
},
"azure": {
"5": {
"7": {
"label": "ROOT",
"fs_label": "ROOT",
"blocks": "58875904"
}
},
"vagrant": {
"5": {
"7": {
"label": "ROOT",
"fs_label": "ROOT",
"blocks": "33845248"
Expand All @@ -92,6 +106,12 @@
"type": "blank"
},
"5": {
"type": "blank"
},
"6": {
"type": "blank"
},
"7": {
"label": "ROOT",
"fs_label": "ROOT",
"type": "0fc63daf-8483-4772-8e79-3d69d8477de4",
Expand Down
105 changes: 83 additions & 22 deletions build_library/disk_util
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,11 @@ DPS_ROOT_GUIDS = {
'aarch64': 'B921B045-1DF0-41C3-AF44-4C6F280D3FAE',
}

DPS_USR_VERITY_GUIDS = {
'x86_64': '77FF5F63-E7B6-4633-ACF4-1565B864C0E6',
'aarch64': '6E11A4E7-FBCA-4DED-B9E9-E1A512BB664E',
}

# Map BOARD names used by the build system to machine architecture values.
BOARD_TO_ARCH = {
'amd64-usr': 'x86_64',
Expand All @@ -44,18 +49,24 @@ def _resolve_dps_types(config, arch):
"""Replace symbolic DPS partition type names with architecture-specific GUIDs.

Currently supported placeholders:
dps-root → DPS root partition GUID for the target architecture
dps-root → DPS root partition GUID for the target architecture
dps-usr-verity → DPS /usr verity hash partition GUID
"""
dps_map = {
'dps-root': DPS_ROOT_GUIDS,
'dps-usr-verity': DPS_USR_VERITY_GUIDS,
}
for layout in config.get('layouts', {}).values():
for part in layout.values():
if not isinstance(part, dict):
continue
ptype = part.get('type', '')
if ptype == 'dps-root':
guid = DPS_ROOT_GUIDS.get(arch)
guids = dps_map.get(ptype)
if guids is not None:
guid = guids.get(arch)
if guid is None:
raise InvalidLayout(
'No DPS root GUID defined for architecture %r' % arch)
'No %s GUID defined for architecture %r' % (ptype, arch))
part['type'] = guid


Expand All @@ -73,7 +84,7 @@ def LoadPartitionConfig(options):
'_comment', 'type', 'num', 'label', 'blocks', 'block_size', 'fs_blocks',
'fs_block_size', 'fs_type', 'features', 'uuid', 'part_alignment', 'mount',
'binds', 'fs_subvolume', 'fs_bytes_per_inode', 'fs_inode_size', 'fs_label',
'fs_compression'))
'fs_compression', 'verity_hash'))
integer_layout_keys = set((
'blocks', 'block_size', 'fs_blocks', 'fs_block_size', 'part_alignment',
'fs_bytes_per_inode', 'fs_inode_size'))
Expand Down Expand Up @@ -807,6 +818,12 @@ def Tune(options):
def Verity(options):
"""Hash verity protected filesystems.

Only processes partitions that have a filesystem (fs_type) and either a
verity_hash cross-reference to a dedicated hash partition or the legacy
'verity' feature flag for inline hash mode. At build time only the
active USR slot (the one with 'prioritize') is formatted, so the
inactive slot is skipped naturally.

Args:
options: Flags passed to the script
"""
Expand All @@ -815,7 +832,15 @@ def Verity(options):
GetPartitionTableFromImage(options, config, partitions)

for part_num, part in partitions.items():
if 'verity' not in part.get('features', []):
hash_part_num = part.get('verity_hash')
has_verity_feature = 'verity' in part.get('features', [])

if not hash_part_num and not has_verity_feature:
continue

# Skip partitions without a filesystem — at build time only the active
# slot (USR-A with 'prioritize') is formatted; USR-B is empty.
if not part.get('fs_type'):
continue

if not part['image_compat']:
Expand All @@ -826,22 +851,56 @@ def Verity(options):
elif part.get('fs_type', None) == 'btrfs':
ReadWriteSubvol(options, part, disable_rw=True)

with PartitionLoop(options, part) as loop_dev:
verityout = SudoOutput(['veritysetup', 'format', '--hash=sha256',
'--data-block-size', part['fs_block_size'],
'--hash-block-size', part['fs_block_size'],
'--data-blocks', part['fs_blocks'],
'--hash-offset', part['fs_bytes'],
loop_dev, loop_dev]).decode('utf8')
print(verityout.strip())
m = re.search(r'Root hash:\s+([a-f0-9]{64})$', verityout, re.IGNORECASE|re.MULTILINE)
if not m:
raise Exception("Failed to parse verity output!")

if options.root_hash != None:
with open(options.root_hash, "w") as hash_file:
hash_file.write(m.group(1))
hash_file.write("\n")
if hash_part_num:
# Separate hash partition mode: write hash tree to a dedicated partition.
hash_part = partitions[hash_part_num]
if not hash_part['image_compat']:
raise InvalidLayout("Hash partition is incompatible with existing image")

with PartitionLoop(options, part) as data_loop, \
PartitionLoop(options, hash_part) as hash_loop:
verityout = SudoOutput(['veritysetup', 'format', '--hash=sha256',
'--data-block-size', part['fs_block_size'],
'--hash-block-size', part['fs_block_size'],
'--data-blocks', part['fs_blocks'],
data_loop, hash_loop]).decode('utf8')
print(verityout.strip())

# Read the btrfs FS UUID from the data partition for cmdline use.
if options.fs_uuid:
fs_uuid_out = SudoOutput(
['blkid', '-s', 'UUID', '-o', 'value', data_loop]).decode('utf8')
with open(options.fs_uuid, 'w') as f:
f.write(fs_uuid_out.strip() + '\n')

else:
# Legacy inline mode: hash tree appended within the same partition.
with PartitionLoop(options, part) as loop_dev:
verityout = SudoOutput(['veritysetup', 'format', '--hash=sha256',
'--data-block-size', part['fs_block_size'],
'--hash-block-size', part['fs_block_size'],
'--data-blocks', part['fs_blocks'],
'--hash-offset', part['fs_bytes'],
loop_dev, loop_dev]).decode('utf8')
print(verityout.strip())

m = re.search(r'Root hash:\s+([a-f0-9]{64})$', verityout, re.IGNORECASE|re.MULTILINE)
if not m:
raise Exception("Failed to parse verity root hash!")

if options.root_hash != None:
with open(options.root_hash, "w") as hash_file:
hash_file.write(m.group(1))
hash_file.write("\n")

# Extract the verity superblock UUID from veritysetup output.
if options.verity_uuid:
m_uuid = re.search(r'UUID:\s+([a-f0-9-]{36})$', verityout, re.IGNORECASE|re.MULTILINE)
if not m_uuid:
raise Exception("Failed to parse verity UUID!")
with open(options.verity_uuid, 'w') as f:
f.write(m_uuid.group(1))
f.write("\n")


def Extract(options):
Expand Down Expand Up @@ -1118,6 +1177,8 @@ def main(argv):
a = actions.add_parser('verity', help='compute verity hashes')
a.add_argument('disk_image', help='path to disk image file')
a.add_argument('--root_hash', help='name of file to contain root hash')
a.add_argument('--verity_uuid', help='name of file to contain verity superblock UUID')
a.add_argument('--fs_uuid', help='name of file to contain data partition filesystem UUID')
a.set_defaults(func=Verity)

a = actions.add_parser('extract', help='extract a single partition')
Expand Down
5 changes: 5 additions & 0 deletions build_library/rpm/build_image_util.sh
Original file line number Diff line number Diff line change
Expand Up @@ -592,6 +592,11 @@ EOF
sudo sed -i 's|/var/run/|/run/|g' "${root_fs_dir}/usr/lib/systemd/system/rpcbind.socket"
fi

# Enable tridentd.socket - listens for trident API requests
info "RPM mode: Enabling tridentd.socket"
sudo mkdir -p "${root_fs_dir}/usr/lib/systemd/system/sockets.target.wants"
sudo ln -sf ../tridentd.socket "${root_fs_dir}/usr/lib/systemd/system/sockets.target.wants/tridentd.socket"

# Create /var/lib/nfs directories needed by rpc-statd and NFS server via tmpfiles
# The nfs-utils RPM only creates v4recovery; sm and sm.bak are missing from the package
# /var is stateful so we use tmpfiles.d to create these at boot, not mkdir at build time
Expand Down
16 changes: 8 additions & 8 deletions build_library/rpm/grub.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -116,17 +116,17 @@ set azl_kernel="@@KERNEL@@"
# systemd-veritysetup-generator reads usrhash= to create /dev/mapper/usr
set verity_usrhash="@@USRHASH@@"

# Azure Linux GRUB doesn't have gptprio.next command
# Boot directly from USR-A partition. A/B updates would require custom handling.
# USR-A partition UUID from disk_layout.json
# Filesystem UUID (btrfs) and verity superblock UUID, injected at build time.
# These are A/B agnostic: the update agent swaps which partition holds the
# active UUID, and systemd resolves UUID= to the correct device.
set usr_fs_uuid="@@FSUUID@@"
set verity_uuid="@@VERITYUUID@@"

# Partition UUIDs for non-verity recovery menu entries only.
set usr_a_uuid="7130c94a-213a-4e5a-8e26-6cce9662f132"
set usr_b_uuid="e03dd35c-7c2d-4a47-b3fe-27f15780a57c"

# Verity hash offset: fs_blocks * fs_block_size = 260094 * 4096 = 1065345024
# The hash tree is stored at the end of the USR partition after the filesystem data
set verity_hash_offset="1065345024"

set verity_cmdline="@@MOUNTUSR@@ usrhash=$verity_usrhash systemd.verity_usr_data=PARTUUID=$usr_a_uuid systemd.verity_usr_hash=PARTUUID=$usr_a_uuid systemd.verity_usr_options=hash-offset=$verity_hash_offset,panic-on-corruption"
set verity_cmdline="@@MOUNTUSR@@ usrhash=$verity_usrhash systemd.verity_usr_data=UUID=$usr_fs_uuid systemd.verity_usr_hash=UUID=$verity_uuid systemd.verity_usr_options=panic-on-corruption"

menuentry "Azure Container Linux default" --id=flatcar --unrestricted {
linux$suf $azl_kernel $verity_cmdline $linux_cmdline
Expand Down
22 changes: 22 additions & 0 deletions build_library/rpm/grub_install.sh
Original file line number Diff line number Diff line change
Expand Up @@ -125,9 +125,31 @@ grub_provision_rpm() {
warn "RPM mode: Verity enabled but no hash file provided"
sed_cmds+=(-e 's/@@USRHASH@@//')
fi

# Inject filesystem and verity UUIDs for A/B agnostic boot
if [[ -n "${FLAGS_fs_uuid}" && -f "${FLAGS_fs_uuid}" ]]; then
local grub_fs_uuid
grub_fs_uuid=$(cat "${FLAGS_fs_uuid}")
info "RPM mode: Injecting FS UUID ${grub_fs_uuid}"
sed_cmds+=(-e "s/@@FSUUID@@/${grub_fs_uuid}/")
else
warn "RPM mode: No FS UUID file provided"
sed_cmds+=(-e 's/@@FSUUID@@//')
fi
if [[ -n "${FLAGS_verity_uuid}" && -f "${FLAGS_verity_uuid}" ]]; then
local grub_verity_uuid
grub_verity_uuid=$(cat "${FLAGS_verity_uuid}")
info "RPM mode: Injecting verity UUID ${grub_verity_uuid}"
sed_cmds+=(-e "s/@@VERITYUUID@@/${grub_verity_uuid}/")
else
warn "RPM mode: No verity UUID file provided"
sed_cmds+=(-e 's/@@VERITYUUID@@//')
fi
else
sed_cmds+=(-e 's/@@MOUNTUSR@@/mount.usr/')
sed_cmds+=(-e 's/@@USRHASH@@//')
sed_cmds+=(-e 's/@@FSUUID@@//')
sed_cmds+=(-e 's/@@VERITYUUID@@//')
fi
if [[ -n "${kernel_name}" ]]; then
sed_cmds+=(-e "s|@@KERNEL@@|/flatcar/${kernel_name}|")
Expand Down
1 change: 1 addition & 0 deletions build_library/rpm/package_catalog.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ packages:
- policycoreutils
- ca-certificates
- irqbalance
- trident
sys-libs/systemd-libs: systemd-libs
sys-apps/systemd-networkd: systemd-networkd
net-misc/systemd-networkd: systemd-networkd
Expand Down
Loading
Loading