├── .github └── workflows │ ├── ci.yml │ └── spelling.yml ├── .gitignore ├── LICENSE.txt ├── Makefile ├── Readme.md ├── setup.py ├── src └── verity_squash_root │ ├── __init__.py │ ├── cmdline.py │ ├── config.py │ ├── decrypt.py │ ├── default_config.ini │ ├── distributions │ ├── __init__.py │ ├── arch.py │ ├── autodetect.py │ ├── base.py │ └── debian.py │ ├── efi.py │ ├── encrypt.py │ ├── exec.py │ ├── file_names.py │ ├── file_op.py │ ├── image.py │ ├── initramfs │ ├── __init__.py │ ├── autodetect.py │ ├── base.py │ ├── dracut.py │ └── mkinitcpio.py │ ├── main.py │ ├── mount.py │ ├── parsing.py │ └── setup.py ├── tests └── unit │ ├── cmdline.py │ ├── config.py │ ├── decrypt.py │ ├── distributions │ ├── __init__.py │ ├── arch.py │ ├── autodetect.py │ ├── base.py │ └── debian.py │ ├── efi.py │ ├── encrypt.py │ ├── exec.py │ ├── file_names.py │ ├── file_op.py │ ├── files │ ├── decrypt │ │ └── keys.tar │ ├── distributions │ │ ├── arch │ │ ├── debian │ │ ├── debian-like │ │ ├── etc-os-release │ │ └── usr-os-release │ ├── efi │ │ ├── cmdline │ │ ├── initrd │ │ ├── no_sections_or_info │ │ ├── stub_broken.efi │ │ ├── stub_empty.efi │ │ ├── stub_slot_a.efi │ │ ├── stub_slot_b.efi │ │ ├── stub_slot_unkown.efi │ │ └── vmlinuz │ ├── file_op │ │ └── read.txt │ └── initramfs │ │ └── mkinitcpio │ │ └── linux_name.preset │ ├── image.py │ ├── initramfs │ ├── __init__.py │ ├── autodetect.py │ ├── dracut.py │ └── mkinitcpio.py │ ├── main.py │ ├── mount.py │ ├── parsing.py │ ├── pep_checker.py │ ├── setup.py │ ├── test_helper.py │ └── tests.py └── usr ├── lib ├── dracut │ └── modules.d │ │ └── 99verity-squash-root │ │ ├── cryptsetup_overlay.conf │ │ ├── dracut_mount_overlay.conf │ │ └── module-setup.sh ├── initcpio │ └── install │ │ └── verity-squash-root ├── systemd │ └── system │ │ └── verity-squash-root-notify.service └── verity-squash-root │ ├── create_encrypted_tar_file │ ├── functions │ ├── generate_secure_boot_keys │ ├── mkinitcpio_list_presets │ ├── mount_handler │ ├── mount_handler_dracut │ └── show_boot_info └── share └── bash-completion └── completions └── verity-squash-root /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: Test code 2 | on: [push] 3 | jobs: 4 | build: 5 | runs-on: ubuntu-latest 6 | strategy: 7 | matrix: 8 | python-version: ['3.10', '3.11', '3.12', '3.13'] 9 | name: Tests 10 | steps: 11 | - uses: actions/checkout@v3 12 | - name: Setup python 13 | uses: actions/setup-python@v2 14 | with: 15 | python-version: ${{ matrix.python-version }} 16 | architecture: x64 17 | # Setup 18 | - run: sudo apt-get install binutils sbsigntool 19 | # directory is needed for arch linux tests 20 | - run: sudo mkdir -p /etc/mkinitcpio.d 21 | 22 | # Setup workspace 23 | - run: set -e 24 | - run: python3 --version 25 | - run: python3 -m venv .venv --system-site-packages 26 | - run: . .venv/bin/activate 27 | - run: pip3 install --upgrade setuptools pip 28 | - run: pip3 install pycodestyle pyflakes 29 | - run: pip3 install -e . --no-deps 30 | 31 | # Run Tests 32 | - run: python3 -m unittest tests/unit/tests.py 33 | 34 | # Check syntax 35 | - run: sudo apt-get install shellcheck 36 | - run: shellcheck usr/lib/initcpio/install/verity-squash-root 37 | usr/lib/verity-squash-root/* 38 | usr/lib/dracut/modules.d/99verity-squash-root/*.sh 39 | usr/share/bash-completion/completions/verity-squash-root 40 | - run: pip3 install flake8 mypy 41 | - run: flake8 src/ tests/ setup.py 42 | - run: mypy src 43 | -------------------------------------------------------------------------------- /.github/workflows/spelling.yml: -------------------------------------------------------------------------------- 1 | name: Check spelling 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | check-spelling: 7 | name: Check spelling 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: actions/checkout@v2 11 | - name: Check spelling 12 | uses: codespell-project/actions-codespell@v1 13 | with: 14 | skip: ./tests/unit/files 15 | check_filenames: true 16 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .venv 2 | __pycache__ 3 | *.egg-info 4 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright © 2022 Simon Brand 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 10 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | BASHCOMPDIR := usr/share/bash-completion/completions 2 | DRACUTDIR := usr/lib/dracut/modules.d/99verity-squash-root 3 | INITCPIODIR := usr/lib/initcpio/install 4 | SERVICEDIR := usr/lib/systemd/system 5 | 6 | .PHONY: all install 7 | 8 | all: dist/*.whl 9 | 10 | dist/*.whl: 11 | python -m build --wheel --no-isolation 12 | 13 | install-no-py: 14 | install -dm 755 $(DESTDIR) 15 | install -dm 755 $(DESTDIR)/usr/lib/verity-squash-root 16 | install -dm 755 $(DESTDIR)/$(INITCPIODIR) 17 | install -dm 755 $(DESTDIR)/$(BASHCOMPDIR) 18 | install -dm 755 $(DESTDIR)/usr/share/licenses/verity-squash-root 19 | install -dm 755 $(DESTDIR)/usr/share/verity_squash_root/ \ 20 | $(DESTDIR)/etc/verity_squash_root/ 21 | install -Dm 644 src/verity_squash_root/default_config.ini \ 22 | $(DESTDIR)/usr/share/verity_squash_root/default.ini 23 | install -Dm 600 src/verity_squash_root/default_config.ini \ 24 | $(DESTDIR)/etc/verity_squash_root/config.ini 25 | install -Dm 755 usr/lib/verity-squash-root/* \ 26 | $(DESTDIR)/usr/lib/verity-squash-root 27 | install -dm 755 $(DESTDIR)/$(DRACUTDIR) 28 | install -Dm 755 $(DRACUTDIR)/module-setup.sh \ 29 | $(DESTDIR)/$(DRACUTDIR) 30 | install -Dm 644 $(DRACUTDIR)/cryptsetup_overlay.conf \ 31 | $(DESTDIR)/$(DRACUTDIR) 32 | install -Dm 644 $(DRACUTDIR)/dracut_mount_overlay.conf \ 33 | $(DESTDIR)/$(DRACUTDIR) 34 | install -Dm 644 $(INITCPIODIR)/verity-squash-root \ 35 | $(DESTDIR)/$(INITCPIODIR) 36 | install -Dm 755 $(BASHCOMPDIR)/verity-squash-root \ 37 | $(DESTDIR)/$(BASHCOMPDIR) 38 | install -dm 755 $(DESTDIR)/$(SERVICEDIR) 39 | install -Dm 644 $(SERVICEDIR)/verity-squash-root-notify.service \ 40 | $(DESTDIR)/$(SERVICEDIR)/verity-squash-root-notify.service 41 | install -Dm 644 LICENSE.txt \ 42 | $(DESTDIR)/usr/share/licenses/verity-squash-root 43 | 44 | install: dist/*.whl install-no-py 45 | python -m installer --destdir=$(DESTDIR) dist/*.whl 46 | -------------------------------------------------------------------------------- /Readme.md: -------------------------------------------------------------------------------- 1 | # verity-squash-root 2 | ## Build signed efi binaries which mount a dm-verity verified squashfs image as rootfs on boot. 3 | 4 | ### [Install](#install) - [Configuration](#configuration) - [Usage](#usage) - [Development](#development) 5 | 6 | The goal of this project is to reduce the harm a successful attacker can 7 | do to a system. It prevents an attacker of persistently modifying the 8 | root-filesystem. This gets accomplished via building a chain-of-trust based 9 | on UEFI secure boot. The system-firmware verifies the signature of the UKI 10 | (unified-kernel-image). The UKI verifies the squashfs-rootfs via dm-verity. 11 | On boot you can choose to boot the squashfs with a tmpfs overlay. You start 12 | with the system you signed the last time and changes are discarded on power 13 | off. 14 | So if you reboot in the tmpfs variant, the system is in a state you signed 15 | before and the modifications in the time between by an attacker are gone. 16 | You can also boot in persistent mode, which loads changes from disk and saves 17 | them to the disk. 18 | 19 | This project also provides A/B-style update support. The current booted files 20 | (GKI and squashfs) won't be overridden on updates, so you can boot an old 21 | known-good image if there are problems). The squashfs images are stored on the 22 | configured root-partition, so they will still be encrypted, if encryption of 23 | the root image is configured. 24 | 25 | #### What happens on boot? 26 | 27 | - The initramfs mounts the root-partition as before. 28 | This is why encryption of the root-partition still works. 29 | Cmdline parameters to decrypt still need to be configured. 30 | - Depending on the kernel cmdline, either the A or B image will be verified 31 | via dm-verity and used. (The build command will set these automatically.) 32 | If you boot a tmpfs image, a tmpfs will be used as overlay image for 33 | volatile changes (Note: the changes will be stored in the system RAM). 34 | If you boot a non-tmpfs image, the folder overlay on the root-partition 35 | will be used as overlayfs upper directory to save persistent changes. 36 | 37 | ## Install 38 | 39 | - Install verity-squash-root from [AUR](https://aur.archlinux.org/packages/verity-squash-root/) 40 | or install a [package built by CI](https://github.com/brandsimon/verity-squash-root-packages). 41 | - Create your encrypted secure-boot keys: 42 | ```bash 43 | verity-squash-root --ignore-warnings create-keys 44 | ``` 45 | - Mount your EFI partition and [configure](#configuration) it. 46 | - Add your EFI partition to `/etc/fstab`. 47 | - Make sure your EFI partition is big enough (1 GB recommended). 48 | - Create directory `/mnt/root`. 49 | - Mount your root-partition to `/mnt/root` and configure it in fstab file. 50 | - Configure your kernel cmdline (see: [Configuration](#configuration)) 51 | - Exclude directories or files not wanted in the squashfs in the config file (`EXCLUDE_DIRS`) 52 | - Configure a bind-mount for every excluded directory from `/mnt/root/...` 53 | - Configure distribution specific options (see [Configuration](#configuration)) 54 | - Install systemd-boot, configure it and build the first image: 55 | ``` 56 | verity-squash-root --ignore-warnings setup systemd 57 | verity-squash-root --ignore-warnings build 58 | ``` 59 | - Now reboot into the squashfs 60 | - If everything works as expected, enable secure-boot with the keys 61 | from `/etc/verity_squash_root/public_keys.tar`. 62 | 63 | ### Updates 64 | 65 | - Boot into a tmpfs image. 66 | - Update your distribution 67 | - Create new squashfs image with signed efis: 68 | ``` 69 | verity-squash-root build 70 | ``` 71 | 72 | ### Using custom keys 73 | 74 | Make yourself familiar with the process of creating, installing and using 75 | custom Secure Boot keys. See: 76 | - https://wiki.archlinux.org/index.php/Secure_Boot 77 | - https://www.rodsbooks.com/efi-bootloaders/controlling-sb.html 78 | 79 | After you have generated your custom keys: 80 | ```bash 81 | cd to/your/keys/directory 82 | tar cf keys.tar db.key db.crt 83 | age -p -e -o keys.tar.age keys.tar 84 | mv keys.tar.age /etc/verity_squash_root/ 85 | rm keys.tar 86 | ``` 87 | - Remove your plaintext keys 88 | 89 | ## Configuration 90 | 91 | The config file is located at `/etc/verity_squash_root/config.ini`. 92 | These config options are available: 93 | 94 | #### Section `DEFAULT` 95 | 96 | - `CMDLINE`: configures kernel cmdline, if not configured, 97 | fallback to `/etc/kernel/cmdline`. 98 | - `EFI_STUB`: path to efi stub, default is the one provided by systemd. 99 | - `DECRYPT_SECURE_BOOT_KEYS_CMD`: Command to decrypt your secure-boot keys 100 | tarfile, {} will be replaced with the output tar file. `db.key` and `db.crt` 101 | in the tarfile are used to sign the efi binaries. 102 | - `EXCLUDE_DIRS`: These directories are not included in the squashfs image. 103 | `EFI_PARTITION` and `ROOT_MOUNT` are excluded automatically. 104 | - `EFI_PARTITION`: Path to your efi partition. Efi binaries and systemd-boot 105 | configuration files are stored there. 106 | - `ROOT_MOUNT`: Path to your "original" root partition. 107 | - `IGNORE_KERNEL_EFIS`: Which efi binaries are not built. You can use the 108 | `list` parameter to show which can exist and which are excluded already. 109 | 110 | #### Section `EXTRA_SIGN` 111 | 112 | You can specify files to be signed when running with the `sign_extra_files` 113 | command. The format is: 114 | ``` 115 | NAME = SOURCE_PATH => DESTINATION_PATH 116 | ``` 117 | e.g. to sign the systemd-boot efi files: 118 | ``` 119 | [EXTRA_SIGN] 120 | systemd-boot = /usr/lib/systemd/boot/efi/systemd-bootx64.efi => /boot/efi/EFI/systemd/systemd-bootx64.efi 121 | ``` 122 | 123 | Be careful to not specify files from untrusted sources, e.g. the ESP 124 | partition. An attacker could exchange these files. 125 | 126 | ### Supported setups 127 | 128 | Currently Arch Linux and Debian are supported with mkinitcpio and dracut. 129 | Mkinitcpio is only supported, if it is used with systemd-hooks. 130 | 131 | ## Considerations / Recommendations 132 | 133 | - Directly before updating, reboot into a tmpfs overlay, so modifications made 134 | by an attacker are removed and you have your trusted environment from the last 135 | update. 136 | - If you enable automatic decryption of your secure-boot keys, an 137 | attacker who gets access can also sign efi binaries. So manually decrypting 138 | secure-boot keys via age is more secure. 139 | - To be sure to only enter the password for your secure-boot keys 140 | on your machine, you can verify your machine on boot with 141 | [tpm2-totp](https://github.com/tpm2-software/tpm2-totp) or 142 | [cryptographic-id](https://gitlab.com/cryptographic_id/cryptographic-id-rs). 143 | - Encrypt your root partition! If your encryption was handled by the 144 | initramfs (dracut/mkinitcpio) before installation, it will work with the 145 | squashfs root image as well. 146 | - You can only configure the network in the persistent system. This way you 147 | can download updates, reboot into the tmpfs system and install them offline. 148 | 149 | ## Usage 150 | 151 | To list all efi images, which will be created or ignored via 152 | `IGNORE_KERNEL_EFIS`: 153 | ``` 154 | verity-squash-root list 155 | ``` 156 | 157 | To install systemd-boot and create a UEFI Boot Manager entry for it: 158 | ``` 159 | verity-squash-root setup systemd 160 | ``` 161 | 162 | To add efi files to the UEFI Boot Manager with /dev/sda1 as EFI partition: 163 | ``` 164 | verity-squash-root setup uefi /dev/sda 1 165 | ``` 166 | 167 | To build a new squashfs image and efi files: 168 | ``` 169 | verity-squash-root build 170 | ``` 171 | 172 | If you are not yet booted in a verified image, you need `--ignore-warnings`, 173 | since there will be a warning if the root image is not fully verified. 174 | 175 | ## Files 176 | 177 | The following files will be used on your root-partition: 178 | 179 | Images with verity info: 180 | 181 | - `image_a.squashfs`, `image_a.squashfs.verity`, 182 | - `image_b.squashfs` `image_b.squashfs.verity` 183 | 184 | Overlayfs directories: 185 | 186 | - `overlay` `workdir` 187 | 188 | ## Development 189 | 190 | ### Dependencies 191 | 192 | ``` 193 | age (only when used for decryption of secure-boot keys) 194 | binutils 195 | cryptsetup-bin 196 | efitools 197 | python 198 | sbsigntool 199 | squashfs-tools 200 | systemd-boot-efi (only when no other efi-stub is configured) 201 | tar 202 | ``` 203 | 204 | #### Development 205 | 206 | ``` 207 | python-pyflakes 208 | python-pycodestyle 209 | ``` 210 | 211 | ### Setup 212 | 213 | Setup a python3 virtual environment: 214 | 215 | ```shell 216 | git clone git@github.com:brandsimon/verity-squash-root.git 217 | python3 -m venv .venv 218 | .venv/bin/pip install -e . --no-deps 219 | ``` 220 | 221 | Run unit tests: 222 | 223 | ```shell 224 | sudo mkdir -p /etc/mkinitcpio.d # Otherwise mkinitcpio test will fail 225 | .venv/bin/python -m unittest tests/unit/tests.py 226 | ``` 227 | 228 | ## Related resources 229 | 230 | * https://wiki.archlinux.org/index.php/Unified_Extensible_Firmware_Interface 231 | * https://wiki.archlinux.org/index.php/Secure_Boot 232 | * https://www.rodsbooks.com/efi-bootloaders/index.html 233 | * https://bentley.link/secureboot/ 234 | * [sbupdate](https://github.com/andreyv/sbupdate) — tool to automatically sign 235 | Arch Linux kernels 236 | * [Foxboron/sbctl](https://github.com/Foxboron/sbctl) — Secure Boot Manager 237 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup, find_packages 2 | 3 | with open('Readme.md', encoding='utf-8') as f: 4 | readme = f.read() 5 | 6 | 7 | setup( 8 | name='verity-squash-root', 9 | version='0.3.5', 10 | description='Build a signed efi binary which mounts a ' 11 | 'verified squashfs image as rootfs', 12 | long_description=readme, 13 | long_description_content_type='text/markdown', 14 | classifiers=[ 15 | 'Environment :: Console', 16 | 'Intended Audience :: Developers', 17 | 'License :: OSI Approved :: MIT License', 18 | 'Operating System :: Linux :: Arch Linux', 19 | 'Operating System :: Linux :: Debian', 20 | 'Programming Language :: Python', 21 | 'Programming Language :: Python :: 3', 22 | 'Programming Language :: Python :: 3.10', 23 | 'Programming Language :: Python :: 3.11', 24 | 'Programming Language :: Python :: 3.12', 25 | 'Programming Language :: Python :: 3.13', 26 | 'Security :: Cryptography', 27 | 'System :: Filesystems', 28 | 'System :: Boot :: Init' 29 | ], 30 | author='Simon Brand', 31 | author_email='simon.brand@postadigitale.de', 32 | url='https://github.com/brandsimon/verity-squash-root', 33 | keywords='Secure boot, squashfs, dm-verity', 34 | package_dir={'': 'src'}, 35 | packages=find_packages('src/'), 36 | include_package_data=True, 37 | zip_safe=False, 38 | extras_require={ 39 | 'dev': ['pycodestyle', 'pyflakes'], 40 | }, 41 | install_requires=[], 42 | entry_points={ 43 | 'console_scripts': [ 44 | 'verity-squash-root=verity_squash_root:parse_params_and_run' 45 | ] 46 | }, 47 | ) 48 | -------------------------------------------------------------------------------- /src/verity_squash_root/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | import argparse 3 | import logging 4 | import os 5 | import shutil 6 | import sys 7 | import verity_squash_root.encrypt as encrypt 8 | from configparser import ConfigParser 9 | from verity_squash_root.config import read_config, LOG_FILE, \ 10 | check_config_and_system, config_str_to_stripped_arr, TMPDIR, CONFIG_FILE 11 | from verity_squash_root.decrypt import DecryptKeys 12 | from verity_squash_root.distributions.base import DistributionConfig, \ 13 | calc_kernel_packages_not_unique 14 | from verity_squash_root.distributions.autodetect import autodetect_distribution 15 | from verity_squash_root.initramfs.autodetect import InitramfsBuilder, \ 16 | autodetect_initramfs 17 | from verity_squash_root.file_names import iterate_kernel_variants, \ 18 | kernel_is_ignored 19 | from verity_squash_root.main import create_image_and_sign_kernel, \ 20 | backup_and_sign_extra_files 21 | from verity_squash_root.mount import TmpfsMount 22 | from verity_squash_root.setup import add_kernels_to_uefi, setup_systemd_boot 23 | 24 | 25 | def list_distribution_efi(config: ConfigParser, 26 | distribution: DistributionConfig, 27 | initramfs: InitramfsBuilder) -> None: 28 | ignore_efis = config_str_to_stripped_arr( 29 | config["DEFAULT"]["IGNORE_KERNEL_EFIS"]) 30 | last = ("", "") 31 | 32 | for (kernel, preset, base_name, display) in iterate_kernel_variants( 33 | distribution, initramfs): 34 | ident = (kernel, preset) 35 | if ident != last: 36 | print("{}: kernel: {}, preset: {}".format(display, kernel, preset)) 37 | last = ident 38 | 39 | op = "-" if kernel_is_ignored(base_name, ignore_efis) else "+" 40 | print(" {} {} ({})".format(op, base_name, display)) 41 | print("\n(+ = included, - = excluded") 42 | 43 | 44 | def warn_check_system_config(config: ConfigParser, 45 | distribution: DistributionConfig) -> bool: 46 | warnings = check_config_and_system(config) + \ 47 | calc_kernel_packages_not_unique(distribution) 48 | for line in warnings: 49 | logging.warning(line) 50 | return len(warnings) > 0 51 | 52 | 53 | def configure_logger(verbose: bool) -> None: 54 | loglevel = logging.INFO if not verbose else logging.DEBUG 55 | logging.basicConfig( 56 | level=logging.DEBUG, 57 | format="%(asctime)s [%(levelname)s] %(message)s", 58 | encoding="utf-8", 59 | handlers=[ 60 | logging.FileHandler(LOG_FILE), 61 | logging.StreamHandler(), 62 | ], 63 | ) 64 | logger = logging.getLogger() 65 | logger.handlers[0].setLevel(logging.DEBUG) 66 | logger.handlers[1].setLevel(loglevel) 67 | 68 | 69 | def parse_params_and_run(): 70 | os.umask(0o077) 71 | config = read_config() 72 | distribution = autodetect_distribution() 73 | initramfs = autodetect_initramfs(distribution) 74 | 75 | parser = argparse.ArgumentParser( 76 | description="Create signed efi binaries which mount a verified " 77 | "squashfs (dm-verity) image\nas rootfs on boot " 78 | "(in the initramfs/initrd). " 79 | "On boot you can choose to boot\ninto a tmpfs overlay or " 80 | "a directory (/overlay and /workdir) on your\n" 81 | "root-partition.\n\n" 82 | "The configuration file is located at {}".format( 83 | CONFIG_FILE), 84 | formatter_class=argparse.RawTextHelpFormatter) 85 | parser.add_argument("--verbose", 86 | action="store_true", 87 | help="Show debug messages on the terminal") 88 | parser.add_argument("--ignore-warnings", 89 | action="store_true", 90 | help="Do not exit if there are warnings") 91 | cmd_parser = parser.add_subparsers(dest="command", required=True) 92 | cmd_parser.add_parser("create-keys", 93 | help="Create secure boot keys and store them " 94 | "(age needs to be installed)") 95 | cmd_parser.add_parser("list", 96 | help="List all efi files to build and show " 97 | "which ones are\nbuilt and which ones " 98 | "are excluded via configfile.") 99 | cmd_parser.add_parser("check", 100 | help="Check the system and print warnings if " 101 | "the\nconfiguration does not match the " 102 | "recommendation.") 103 | cmd_parser.add_parser("build", 104 | help="Build squashfs, verity-info, efi binaries " 105 | "and sign\nthem.") 106 | setup_parser = cmd_parser.add_parser("setup", 107 | formatter_class=( 108 | argparse.RawTextHelpFormatter), 109 | help="Setup boot menu entries for " 110 | "all efi binaries which are" 111 | "\nnot excluded. Supported: \n" 112 | " - systemd: systemd-boot\n" 113 | " - uefi: efibootmgr") 114 | boot_parser = setup_parser.add_subparsers(dest="boot_method", 115 | required=True) 116 | boot_parser.add_parser("systemd", 117 | help="Install and configure systemd-boot and " 118 | "register it in the\nuefi as bootable.") 119 | efi_parser = boot_parser.add_parser("uefi", 120 | help="Register all efi binaries in " 121 | "the uefi as bootable via\n" 122 | "efibootmgr.") 123 | efi_parser.add_argument("disk", 124 | help="Parameter for efibootmgr --disk") 125 | efi_parser.add_argument("partition_no", 126 | help="Parameter for efibootmgr --part", 127 | type=int) 128 | cmd_parser.add_parser("sign-extra-files", 129 | help="Sign all files specified in the EXTRA_SIGN " 130 | "section in the config file.") 131 | args = parser.parse_args() 132 | configure_logger(args.verbose) 133 | 134 | logging.debug("Running: {}".format(sys.argv)) 135 | logging.debug("Parsed arguments: {}".format(args)) 136 | warned = warn_check_system_config(config, distribution) 137 | if warned and not args.ignore_warnings: 138 | logging.error("If you want to ignore those warnings, run with " 139 | "--ignore-warnings") 140 | sys.exit(1) 141 | 142 | try: 143 | if args.command == "create-keys": 144 | if shutil.which("age") is None: 145 | raise FileNotFoundError("age is not installed, but needed " 146 | "for encryption") 147 | with TmpfsMount(TMPDIR): 148 | encrypt.check_if_archives_exist() 149 | encrypt.create_and_pack_secure_boot_keys() 150 | elif args.command == "list": 151 | list_distribution_efi(config, distribution, initramfs) 152 | elif args.command == "setup": 153 | with TmpfsMount(TMPDIR): 154 | if args.boot_method == "uefi": 155 | add_kernels_to_uefi(config, distribution, initramfs, 156 | args.disk, args.partition_no) 157 | if args.boot_method == "systemd": 158 | with DecryptKeys(config): 159 | setup_systemd_boot(config, distribution, initramfs) 160 | elif args.command == "build": 161 | with TmpfsMount(TMPDIR): 162 | with DecryptKeys(config): 163 | create_image_and_sign_kernel(config, distribution, 164 | initramfs) 165 | elif args.command == "sign-extra-files": 166 | with TmpfsMount(TMPDIR): 167 | with DecryptKeys(config): 168 | backup_and_sign_extra_files(config) 169 | except BaseException as e: 170 | logging.error("Error: {}".format(e)) 171 | logging.debug(e, exc_info=1) 172 | logging.error("For more info use the --verbose option " 173 | "or look into the log file: {}".format(LOG_FILE)) 174 | 175 | 176 | if __name__ == "__main__": 177 | parse_params_and_run() 178 | -------------------------------------------------------------------------------- /src/verity_squash_root/cmdline.py: -------------------------------------------------------------------------------- 1 | from typing import Optional 2 | from verity_squash_root.config import KERNEL_PARAM_BASE 3 | 4 | 5 | def current_slot(kernel_cmdline: str) -> Optional[str]: 6 | params = kernel_cmdline.split(" ") 7 | for p in params: 8 | if p.startswith("{}_slot=".format(KERNEL_PARAM_BASE)): 9 | return p[24:].lower() 10 | return None 11 | 12 | 13 | def unused_slot(kernel_cmdline: str) -> str: 14 | curr = current_slot(kernel_cmdline) 15 | try: 16 | next_slot = {"a": "b", "b": "a"} 17 | return next_slot[curr or ""] 18 | except KeyError: 19 | return "a" 20 | -------------------------------------------------------------------------------- /src/verity_squash_root/config.py: -------------------------------------------------------------------------------- 1 | from configparser import ConfigParser 2 | from pathlib import Path 3 | from typing import List 4 | from verity_squash_root.exec import exec_binary 5 | 6 | 7 | TMPDIR = Path("/tmp/verity_squash_root") 8 | KEY_DIR = TMPDIR / "keys" 9 | KERNEL_PARAM_BASE = "verity_squash_root" 10 | NAME_DASH = KERNEL_PARAM_BASE.replace("_", "-") 11 | CONFIG_DIR = Path("/etc/{}".format(KERNEL_PARAM_BASE)) 12 | CONFIG_FILE = CONFIG_DIR / "config.ini" 13 | DISTRI_FILE = Path("/usr/share") / KERNEL_PARAM_BASE / "default.ini" 14 | LOG_FILE = Path("/var/log/{}.log".format(KERNEL_PARAM_BASE)) 15 | EFI_PATH = Path("EFI") 16 | EFI_KERNELS = EFI_PATH / KERNEL_PARAM_BASE 17 | 18 | 19 | def config_str_to_stripped_arr(s: str) -> List[str]: 20 | return [i.strip() for i in s.split(",")] 21 | 22 | 23 | def read_config() -> ConfigParser: 24 | # defaults will be visible in EXTRA_SIGN and break 25 | config = ConfigParser(default_section="DO_NOT_USE_DEFAULTS") 26 | directory = Path(__file__).resolve().parent 27 | defconfig = directory / "default_config.ini" 28 | config.read(defconfig) 29 | config.read(DISTRI_FILE) 30 | config.read(CONFIG_FILE) 31 | return config 32 | 33 | 34 | def is_volatile_boot(): 35 | res = exec_binary(["findmnt", "-uno", "OPTIONS", "/"])[0].decode() 36 | parts = res.split(",") 37 | return "upperdir=/verity-squash-root-tmp/tmpfs/overlay" in parts 38 | 39 | 40 | def check_config(config: ConfigParser) -> List[str]: 41 | root_mount = Path(config["DEFAULT"]["ROOT_MOUNT"]) 42 | efi_partition = Path(config["DEFAULT"]["EFI_PARTITION"]) 43 | result = [] 44 | for d in [root_mount, efi_partition]: 45 | if not d.resolve().is_mount(): 46 | result.append("Directory '{}' is not a mount point".format(d)) 47 | return result 48 | 49 | 50 | def check_config_and_system(config: ConfigParser) -> List[str]: 51 | res = check_config(config) 52 | if not is_volatile_boot(): 53 | res.append("System is not booted in volatile mode:") 54 | res.append(" - System could be compromised from previous boots") 55 | res.append(" - It is recommended to enter secure boot key passwords " 56 | "only in volatile mode") 57 | res.append(" - Know what you are doing!") 58 | return res 59 | -------------------------------------------------------------------------------- /src/verity_squash_root/decrypt.py: -------------------------------------------------------------------------------- 1 | import re 2 | import shutil 3 | import tarfile 4 | from configparser import ConfigParser 5 | from pathlib import Path 6 | from typing import List 7 | from verity_squash_root.config import KEY_DIR 8 | from verity_squash_root.exec import exec_binary 9 | from verity_squash_root.efi import DB_CERT_FILE, DB_KEY_FILE 10 | TAR_FILE = KEY_DIR / "keys.tar" 11 | 12 | 13 | def format_cmd(cmd: str, file: Path) -> List[str]: 14 | # split at ' ' or new line into parameters for exec 15 | parts = re.split(" |\n|\t", cmd.strip()) 16 | return list(map(lambda x: x.format(file), parts)) 17 | 18 | 19 | def decrypt_secure_boot_keys(config: ConfigParser) -> None: 20 | cmd = config["DEFAULT"]["DECRYPT_SECURE_BOOT_KEYS_CMD"] 21 | cmd_arr = format_cmd(cmd, TAR_FILE) 22 | KEY_DIR.mkdir() 23 | exec_binary(cmd_arr) 24 | with tarfile.open(TAR_FILE) as t: 25 | # Only extract needed files to avoid python extract/extractall 26 | # vulnerability 27 | t.extract(DB_CERT_FILE, KEY_DIR) 28 | t.extract(DB_KEY_FILE, KEY_DIR) 29 | 30 | 31 | class DecryptKeys: 32 | _config: ConfigParser 33 | 34 | def __init__(self, config: ConfigParser): 35 | self._config = config 36 | 37 | def __enter__(self): 38 | decrypt_secure_boot_keys(self._config) 39 | 40 | def __exit__(self, exc_type, exc_value, exc_tb): 41 | shutil.rmtree(KEY_DIR) 42 | -------------------------------------------------------------------------------- /src/verity_squash_root/default_config.ini: -------------------------------------------------------------------------------- 1 | [DEFAULT] 2 | # If CMDLINE is not configured, use /etc/kernel/cmdline 3 | # CMDLINE = root=LABEL=root 4 | EFI_STUB = /usr/lib/systemd/boot/efi/linuxx64.efi.stub 5 | DECRYPT_SECURE_BOOT_KEYS_CMD = age -d -o {} /etc/verity_squash_root/keys.tar.age 6 | 7 | # DECRYPT_SECURE_BOOT_KEYS_CMD = 8 | # openssl enc -aes-256-cbc -pbkdf2 -d 9 | # -in /etc/verity_squash_root/keys.tar.openssl -out {} 10 | # DECRYPT_SECURE_BOOT_KEYS_CMD = cp /etc/verity_squash_root/keys.tar {} 11 | EXCLUDE_DIRS = /home,/opt,/srv,/var/!(lib|log) 12 | EFI_PARTITION = /boot/efi 13 | ROOT_MOUNT = /mnt/root 14 | IGNORE_KERNEL_EFIS = 15 | 16 | [EXTRA_SIGN] 17 | # These files will be signed when called with sign_extra_files 18 | # The format is: `NAME = SOURCE_PATH => DESTINATION_PATH` 19 | # Be careful to not sign files from untrusted sources, 20 | # e.g. the ESP partition. An attacker could exchange these 21 | # files. 22 | # systemd = /usr/lib/systemd/boot/efi/systemd-bootx64.efi => /boot/efi/EFI/systemd/systemd-bootx64.efi 23 | -------------------------------------------------------------------------------- /src/verity_squash_root/distributions/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brandsimon/verity-squash-root/1a31d0ad8934029b5fd065a9baeb50f5a492e585/src/verity_squash_root/distributions/__init__.py -------------------------------------------------------------------------------- /src/verity_squash_root/distributions/arch.py: -------------------------------------------------------------------------------- 1 | from functools import lru_cache 2 | from pathlib import Path 3 | from typing import List 4 | from verity_squash_root.file_op import read_text_from 5 | from verity_squash_root.distributions.base import DistributionConfig 6 | 7 | 8 | class ArchLinuxConfig(DistributionConfig): 9 | 10 | @lru_cache(maxsize=128) 11 | def kernel_to_name(self, kernel: str) -> str: 12 | pkgbase_file = self._modules_dir / kernel / "pkgbase" 13 | return read_text_from(pkgbase_file).strip() 14 | 15 | def display_name(self) -> str: 16 | return "Arch" 17 | 18 | def microcode_paths(self) -> List[Path]: 19 | return [Path("/boot/amd-ucode.img"), 20 | Path("/boot/intel-ucode.img")] 21 | -------------------------------------------------------------------------------- /src/verity_squash_root/distributions/autodetect.py: -------------------------------------------------------------------------------- 1 | from collections.abc import Mapping 2 | from pathlib import Path 3 | from verity_squash_root.file_op import read_text_from 4 | from verity_squash_root.distributions.arch import ArchLinuxConfig, \ 5 | DistributionConfig 6 | from verity_squash_root.distributions.debian import DebianConfig 7 | import verity_squash_root.parsing as parsing 8 | 9 | ETC_FILE = Path("/etc/os-release") 10 | USR_FILE = Path("/usr/lib/os-release") 11 | 12 | 13 | def load_os_release() -> Mapping[str, str]: 14 | if ETC_FILE.resolve().is_file(): 15 | content = read_text_from(ETC_FILE) 16 | else: 17 | content = read_text_from(USR_FILE) 18 | info = parsing.info_to_dict(content, sep="=") 19 | result = {} 20 | for k, v in info.items(): 21 | result[k] = v.strip('"') 22 | return result 23 | 24 | 25 | def autodetect_distribution() -> DistributionConfig: 26 | info = load_os_release() 27 | os_id = info.get("ID", "") 28 | os_name = info.get("NAME", "") 29 | if os_id == "debian" or info.get("ID_LIKE") == "debian": 30 | return DebianConfig(os_id, os_name) 31 | elif info.get("ID") == "arch": 32 | return ArchLinuxConfig(os_id, os_name) 33 | else: 34 | raise ValueError( 35 | "Distribution {} not yet supported".format(info.get("ID"))) 36 | -------------------------------------------------------------------------------- /src/verity_squash_root/distributions/base.py: -------------------------------------------------------------------------------- 1 | import re 2 | from pathlib import Path 3 | from typing import List, MutableMapping 4 | 5 | 6 | class DistributionConfig: 7 | 8 | _modules_dir: Path = Path("/usr/lib/modules") 9 | __os_id: str = "" 10 | __os_name: str = "" 11 | 12 | def __init__(self, os_id: str, os_name: str): 13 | self.__os_id = os_id 14 | self.__os_name = os_name 15 | 16 | def kernel_to_name(self, kernel: str) -> str: 17 | raise NotImplementedError("Base class") 18 | 19 | def display_name(self) -> str: 20 | return self.__os_name 21 | 22 | def efi_dirname(self) -> str: 23 | return self.__os_id 24 | 25 | def vmlinuz(self, kernel: str) -> Path: 26 | return self._modules_dir / kernel / "vmlinuz" 27 | 28 | def list_kernels(self) -> List[str]: 29 | def int_or_str(e): 30 | try: 31 | return int(e) 32 | except ValueError: 33 | return e 34 | 35 | def human_sort(e): 36 | return [int_or_str(a) 37 | for a in re.split('([0-9]+)', e)] 38 | 39 | return sorted([k.name for k in self._modules_dir.iterdir()], 40 | reverse=True, key=human_sort) 41 | 42 | def microcode_paths(self) -> List[Path]: 43 | raise NotImplementedError("Base class") 44 | 45 | 46 | def calc_kernel_packages_not_unique(distribution: DistributionConfig) \ 47 | -> List[str]: 48 | mapping: MutableMapping[str, str] = {} 49 | result = [] 50 | for kernel in distribution.list_kernels(): 51 | file_name = distribution.kernel_to_name(kernel) 52 | if file_name in mapping: 53 | result.append("Package {} has multiple kernel versions: {}, {}" 54 | .format(file_name, kernel, mapping[file_name])) 55 | mapping[file_name] = kernel 56 | if len(result) > 0: 57 | result.append("This means, that there are probably old files in " 58 | "/usr/lib/modules") 59 | return result 60 | -------------------------------------------------------------------------------- /src/verity_squash_root/distributions/debian.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | from verity_squash_root.distributions.base import DistributionConfig 3 | 4 | 5 | class DebianConfig(DistributionConfig): 6 | 7 | def kernel_to_name(self, kernel: str) -> str: 8 | kernels = self.list_kernels() 9 | pos = kernels.index(kernel) 10 | if pos == 0: 11 | return "current" 12 | elif pos == 1: 13 | return "old" 14 | else: 15 | return "old_x{}".format(pos) 16 | 17 | def vmlinuz(self, kernel: str) -> Path: 18 | return Path("/boot") / "vmlinuz-{}".format(kernel) 19 | -------------------------------------------------------------------------------- /src/verity_squash_root/efi.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import math 3 | from collections import OrderedDict 4 | from configparser import ConfigParser 5 | from pathlib import Path 6 | from verity_squash_root.config import KERNEL_PARAM_BASE, TMPDIR, KEY_DIR 7 | from verity_squash_root.exec import exec_binary, ExecBinaryError 8 | from verity_squash_root.file_op import write_str_to 9 | 10 | DB_CERT_FILE = "db.crt" 11 | DB_KEY_FILE = "db.key" 12 | CMDLINE_FILE = Path("/etc/kernel/cmdline") 13 | 14 | 15 | def file_matches_slot_or_is_broken(file: Path, slot: str): 16 | search_str = " {}_slot={} ".format(KERNEL_PARAM_BASE, slot) 17 | try: 18 | result = exec_binary(["objcopy", "-O", "binary", "--dump-section", 19 | ".cmdline=/dev/fd/1", str(file), "/dev/null"]) 20 | except ExecBinaryError as e: 21 | err = e.stderr() 22 | if err.startswith("objcopy:") and err.endswith(": file truncated\n"): 23 | logging.warning("Old efi file was truncated") 24 | logging.debug(err) 25 | return True 26 | if err.startswith("objcopy: error: the input file '") and ( 27 | err.endswith("' is empty\n")): 28 | logging.warning("Old efi file was empty") 29 | logging.debug(err) 30 | return True 31 | raise e 32 | text = result[0].decode() 33 | return search_str in text 34 | 35 | 36 | def sign(key_dir: Path, in_file: Path, out_file: Path) -> None: 37 | exec_binary([ 38 | "sbsign", 39 | "--key", str(key_dir / DB_KEY_FILE), 40 | "--cert", str(key_dir / DB_CERT_FILE), 41 | "--output", str(out_file), str(in_file)]) 42 | 43 | 44 | def calculate_efi_stub_end(stub: Path) -> int: 45 | result = exec_binary(["objdump", "-h", str(stub)]) 46 | lines = result[0].decode().split("\n") 47 | words = lines[-3].split() 48 | return int(words[2], 16) + int(words[3], 16) 49 | 50 | 51 | def calculate_efi_stub_alignment(stub: Path) -> int: 52 | result = exec_binary(["objdump", "-p", str(stub)]) 53 | lines = result[0].decode().split("\n") 54 | for line in lines: 55 | words = line.split() 56 | if len(words) == 2: 57 | if words[0] == "SectionAlignment": 58 | return int(words[1], 16) 59 | raise ValueError("Efistub SectionAlignment not found") 60 | 61 | 62 | # TODO: use systemd-ukify when Debian 13 is stable 63 | def create_efi_executable(stub: Path, cmdline_file: Path, linux: Path, 64 | initrd: Path, dest: Path): 65 | offset = calculate_efi_stub_end(stub) 66 | alignment = calculate_efi_stub_alignment(stub) 67 | sections = OrderedDict() 68 | sections["osrel"] = Path("/etc/os-release") 69 | sections["cmdline"] = cmdline_file 70 | sections["initrd"] = initrd 71 | # place linux at the end so decompressing it in-place does not 72 | # cause problems: 73 | # https://github.com/systemd/systemd/commit/ 74 | # 0fa2cac4f0cdefaf1addd7f1fe0fd8113db9360b#commitcomment-84868898 75 | sections["linux"] = linux 76 | 77 | cmd = ["objcopy"] 78 | for name, file in sections.items(): 79 | offset = alignment * math.ceil(offset / alignment) 80 | size = file.stat().st_size 81 | cmd += ["--add-section", ".{}={}".format(name, str(file)), 82 | "--change-section-vma", ".{}={}".format(name, hex(offset))] 83 | offset = offset + size 84 | cmd += [str(stub), str(dest)] 85 | exec_binary(cmd) 86 | 87 | 88 | def get_cmdline(config: ConfigParser) -> str: 89 | con_line = config["DEFAULT"].get("CMDLINE") 90 | if con_line is not None: 91 | return con_line 92 | if CMDLINE_FILE.exists(): 93 | return CMDLINE_FILE.read_text().replace("\n", " ") 94 | raise RuntimeError("CMDLINE not configured, either configure it in the " 95 | "config file or in {}".format(CMDLINE_FILE)) 96 | 97 | 98 | def build_and_sign_kernel(config: ConfigParser, vmlinuz: Path, initramfs: Path, 99 | slot: str, root_hash: str, 100 | tmp_efi_file: Path, add_cmdline: str = "") -> None: 101 | # add rw, if root is mounted ro, it cannot be mounted rw later 102 | cmdline = "{} rw {} {p}_slot={} {p}_hash={}".format( 103 | get_cmdline(config), 104 | add_cmdline, 105 | slot, 106 | root_hash, 107 | p=KERNEL_PARAM_BASE) 108 | cmdline_file = TMPDIR / "cmdline" 109 | write_str_to(cmdline_file, cmdline) 110 | create_efi_executable( 111 | Path(config["DEFAULT"]["EFI_STUB"]), 112 | cmdline_file, 113 | vmlinuz, 114 | initramfs, 115 | tmp_efi_file) 116 | sign(KEY_DIR, tmp_efi_file, tmp_efi_file) 117 | -------------------------------------------------------------------------------- /src/verity_squash_root/encrypt.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import os 3 | import tarfile 4 | from pathlib import Path 5 | from typing import List 6 | from verity_squash_root.config import KEY_DIR, CONFIG_DIR 7 | from verity_squash_root.efi import DB_CERT_FILE, DB_KEY_FILE 8 | from verity_squash_root.exec import exec_binary 9 | 10 | SIGNING_FILES = [DB_KEY_FILE, DB_CERT_FILE] 11 | PUBLIC_FILES = [ 12 | "db.auth", "db.cer", "db.esl", "db.hash", 13 | "kek.auth", "kek.cer", "kek.esl", "kek.hash", 14 | "pk.auth", "pk.cer", "pk.esl", "pk.hash"] 15 | ALL_FILES = [DB_KEY_FILE, DB_CERT_FILE, 16 | "pk.key", "kek.key", 17 | "kek.crt", "pk.crt", 18 | "rm_pk.auth", "guid.txt"] + PUBLIC_FILES 19 | PUBLIC_KEY_FILES_TAR = CONFIG_DIR / "public_keys.tar" 20 | SIGNING_FILES_TAR = CONFIG_DIR / "keys.tar.age" 21 | ALL_FILES_TAR = CONFIG_DIR / "all_keys.tar.age" 22 | 23 | 24 | def create_secure_boot_keys() -> None: 25 | exec_binary(["/usr/lib/verity-squash-root/generate_secure_boot_keys"]) 26 | 27 | 28 | def create_tar_file(files: List[str], out: Path) -> None: 29 | with tarfile.open(out, "w") as t: 30 | for file in files: 31 | t.add(file) 32 | 33 | 34 | def create_encrypted_tar_file(files: List[str], out: Path) -> None: 35 | cmd_arr = ["/usr/lib/verity-squash-root/create_encrypted_tar_file", 36 | str(out)] + files 37 | exec_binary(cmd_arr) 38 | 39 | 40 | def create_and_pack_secure_boot_keys() -> None: 41 | prev_cwd = Path.cwd() 42 | try: 43 | KEY_DIR.mkdir() 44 | os.chdir(KEY_DIR) 45 | logging.info("Creating secure boot keys...") 46 | create_secure_boot_keys() 47 | logging.info("Create archive with signing files") 48 | create_encrypted_tar_file(SIGNING_FILES, SIGNING_FILES_TAR) 49 | logging.info("Create archive with all keys") 50 | create_encrypted_tar_file(ALL_FILES, ALL_FILES_TAR) 51 | logging.info("Create archive with public keys") 52 | create_tar_file(PUBLIC_FILES, PUBLIC_KEY_FILES_TAR) 53 | finally: 54 | os.chdir(prev_cwd) 55 | 56 | 57 | def check_if_archives_exist(): 58 | for p in [PUBLIC_KEY_FILES_TAR, SIGNING_FILES_TAR, ALL_FILES_TAR]: 59 | if p.exists(): 60 | raise ValueError("Archive {} already exists, delete it only if " 61 | "you dont need it anymore".format(p)) 62 | -------------------------------------------------------------------------------- /src/verity_squash_root/exec.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import subprocess 3 | from typing import List, Tuple 4 | 5 | 6 | class ExecBinaryError(ChildProcessError): 7 | 8 | def __init__(self, cmd: List[str], stdout: bytes, stderr: bytes): 9 | self.__cmd = cmd 10 | self.__stderr = stderr 11 | super().__init__([cmd, (stdout, stderr)]) 12 | 13 | def stderr(self): 14 | return self.__stderr.decode(errors="ignore") 15 | 16 | def __str__(self): 17 | return "Failed to execute '{}', error: {}".format( 18 | " ".join(self.__cmd), self.stderr()) 19 | 20 | 21 | def exec_binary(cmd: List[str], expect_returncode: int = 0) \ 22 | -> Tuple[bytes, bytes]: 23 | if len(cmd) == 0: 24 | raise ChildProcessError("Cannot execute empty cmd") 25 | try: 26 | logging.debug("Execute {}".format(cmd)) 27 | proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, 28 | stderr=subprocess.PIPE) 29 | except FileNotFoundError: 30 | raise ChildProcessError( 31 | "Binary not found: {}, is it installed?".format(cmd[0])) 32 | result = proc.communicate() 33 | if proc.returncode != expect_returncode: 34 | raise ExecBinaryError(cmd, result[0], result[1]) 35 | return result 36 | -------------------------------------------------------------------------------- /src/verity_squash_root/file_names.py: -------------------------------------------------------------------------------- 1 | from typing import Generator, List, Tuple 2 | from configparser import ConfigParser 3 | from verity_squash_root.config import config_str_to_stripped_arr 4 | from verity_squash_root.distributions.base import DistributionConfig 5 | from verity_squash_root.initramfs.base import InitramfsBuilder, \ 6 | iterate_distribution_efi 7 | 8 | BACKUP_SUFFIX = "_backup" 9 | 10 | 11 | def backup_file(file: str) -> str: 12 | return "{}{}".format(file, BACKUP_SUFFIX) 13 | 14 | 15 | def backup_label(label: str) -> str: 16 | return "{} Backup".format(label) 17 | 18 | 19 | def tmpfs_file(file: str) -> str: 20 | return "{}_tmpfs".format(file) 21 | 22 | 23 | def tmpfs_label(label: str) -> str: 24 | return "{} tmpfs".format(label) 25 | 26 | 27 | def kernel_is_ignored(base_name: str, ignored: List[str]) -> bool: 28 | if base_name in ignored: 29 | return True 30 | if base_name.endswith(BACKUP_SUFFIX): 31 | length = len(BACKUP_SUFFIX) 32 | # Also ignore A_backup, when A is ignored 33 | if base_name[:-length] in ignored: 34 | return True 35 | return False 36 | 37 | 38 | def iterate_kernel_variants(distribution: DistributionConfig, 39 | initramfs: InitramfsBuilder) \ 40 | -> Generator[Tuple[str, str, str, str], None, None]: 41 | for [kernel, preset, base_name] in iterate_distribution_efi(distribution, 42 | initramfs): 43 | display = initramfs.display_name(kernel, preset) 44 | yield (kernel, preset, base_name, display) 45 | yield (kernel, preset, backup_file(base_name), backup_label(display)) 46 | yield (kernel, preset, tmpfs_file(base_name), tmpfs_label(display)) 47 | yield (kernel, preset, 48 | backup_file(tmpfs_file(base_name)), 49 | backup_label(tmpfs_label(display))) 50 | 51 | 52 | def iterate_non_ignored_kernel_variants( 53 | config: ConfigParser, distribution: DistributionConfig, 54 | initramfs: InitramfsBuilder) -> Generator[Tuple[ 55 | str, str, str, str], None, None]: 56 | ignore_efis = config_str_to_stripped_arr( 57 | config["DEFAULT"]["IGNORE_KERNEL_EFIS"]) 58 | 59 | for (kernel, preset, base_name, display) in iterate_kernel_variants( 60 | distribution, initramfs): 61 | if not kernel_is_ignored(base_name, ignore_efis): 62 | yield (kernel, preset, base_name, display) 63 | -------------------------------------------------------------------------------- /src/verity_squash_root/file_op.py: -------------------------------------------------------------------------------- 1 | import shutil 2 | from pathlib import Path 3 | from typing import List 4 | 5 | 6 | def write_str_to(path: Path, content: str) -> None: 7 | path.write_text(content) 8 | 9 | 10 | def read_from(path: Path) -> bytes: 11 | return path.read_bytes() 12 | 13 | 14 | def read_text_from(path: Path) -> str: 15 | return read_from(path).decode() 16 | 17 | 18 | def merge_files(src: List[Path], dest: Path): 19 | with open(dest, "wb") as dest_fd: 20 | for s in src: 21 | with open(s, "rb") as src_fd: 22 | shutil.copyfileobj(src_fd, dest_fd) 23 | -------------------------------------------------------------------------------- /src/verity_squash_root/image.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | from typing import List 3 | import verity_squash_root.parsing as parsing 4 | from verity_squash_root.exec import exec_binary 5 | 6 | 7 | def mksquashfs(exclude_dirs: List[str], image: Path, 8 | root_mount: Path, efi_partition: Path): 9 | include_dirs = ["/"] 10 | all_excluded = [ 11 | "dev", "proc", "run", "sys", "tmp", 12 | str(root_mount), str(efi_partition)] + exclude_dirs 13 | options = ["-reproducible", "-xattrs", "-wildcards", "-noappend", 14 | # prevents overlayfs corruption on updates 15 | "-no-exports", 16 | "-p", "{} d 0700 0 0".format(root_mount), 17 | "-p", "{} d 0700 0 0".format(efi_partition)] 18 | cmd = ["mksquashfs"] + include_dirs + [str(image)] + options + ["-e"] 19 | for d in all_excluded: 20 | sd = d.strip("/") 21 | p = Path(d) 22 | if p.is_file(): 23 | cmd += [sd] 24 | else: 25 | cmd += ["{}/*".format(sd)] 26 | cmd += ["{}/.*".format(sd)] 27 | exec_binary(cmd) 28 | 29 | 30 | def verity_image_path(image: Path) -> Path: 31 | return image.with_suffix("{}.verity".format(image.suffix)) 32 | 33 | 34 | def veritysetup_image(image: Path) -> str: 35 | cmd = ["veritysetup", "format", str(image), str(verity_image_path(image))] 36 | result = exec_binary(cmd) 37 | stdout = result[0].decode() 38 | info = parsing.info_to_dict(stdout) 39 | return info["Root hash"] 40 | -------------------------------------------------------------------------------- /src/verity_squash_root/initramfs/__init__.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | from typing import List 3 | from verity_squash_root.file_op import merge_files 4 | 5 | 6 | def merge_initramfs_images(main_image: Path, microcode_paths: List[Path], 7 | out: Path) -> None: 8 | initramfs_files = [] 9 | for i in microcode_paths: 10 | if i.exists(): 11 | initramfs_files.append(i) 12 | # main image needs to be last! 13 | initramfs_files.append(main_image) 14 | merge_files(initramfs_files, out) 15 | -------------------------------------------------------------------------------- /src/verity_squash_root/initramfs/autodetect.py: -------------------------------------------------------------------------------- 1 | import shutil 2 | from verity_squash_root.distributions.base import DistributionConfig 3 | from verity_squash_root.initramfs.dracut import InitramfsBuilder, \ 4 | Dracut 5 | from verity_squash_root.initramfs.mkinitcpio import Mkinitcpio 6 | 7 | 8 | def autodetect_initramfs(distri: DistributionConfig) -> InitramfsBuilder: 9 | if shutil.which("mkinitcpio") is not None: 10 | return Mkinitcpio(distri) 11 | elif shutil.which("dracut") is not None: 12 | return Dracut(distri) 13 | else: 14 | raise ValueError("No supported initramfs builder found") 15 | -------------------------------------------------------------------------------- /src/verity_squash_root/initramfs/base.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | from typing import Generator, List, Tuple 3 | from verity_squash_root.distributions.base import DistributionConfig 4 | 5 | 6 | class InitramfsBuilder: 7 | 8 | def file_name(self, kernel: str, preset: str) -> str: 9 | raise NotImplementedError("Base class") 10 | 11 | def display_name(self, kernel: str, preset: str) -> str: 12 | raise NotImplementedError("Base class") 13 | 14 | def build_initramfs_with_microcode(self, kernel: str, 15 | preset: str) -> Path: 16 | raise NotImplementedError("Base class") 17 | 18 | def list_kernel_presets(self, kernel: str) -> List[str]: 19 | raise NotImplementedError("Base class") 20 | 21 | 22 | def iterate_distribution_efi(distribution: DistributionConfig, 23 | initramfs: InitramfsBuilder) \ 24 | -> Generator[Tuple[str, str, str], None, None]: 25 | for kernel in distribution.list_kernels(): 26 | for preset in initramfs.list_kernel_presets(kernel): 27 | base_name = initramfs.file_name(kernel, preset) 28 | yield (kernel, preset, base_name) 29 | -------------------------------------------------------------------------------- /src/verity_squash_root/initramfs/dracut.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | from typing import List 3 | from verity_squash_root.config import TMPDIR 4 | from verity_squash_root.exec import exec_binary 5 | from verity_squash_root.distributions.base import DistributionConfig 6 | from verity_squash_root.initramfs.base import InitramfsBuilder 7 | 8 | 9 | class Dracut(InitramfsBuilder): 10 | 11 | def __init__(self, distribution: DistributionConfig): 12 | self._distribution = distribution 13 | 14 | def file_name(self, kernel: str, preset: str) -> str: 15 | return self._distribution.kernel_to_name(kernel) 16 | 17 | def display_name(self, kernel: str, preset: str) -> str: 18 | kernel_name = self._distribution.kernel_to_name( 19 | kernel).replace("-", " ") 20 | return "{} {}".format( 21 | self._distribution.display_name(), 22 | kernel_name.capitalize()) 23 | 24 | def build_initramfs_with_microcode(self, kernel: str, 25 | preset: str) -> Path: 26 | merged_initramfs = TMPDIR / "{}-{}.image".format(kernel, preset) 27 | exec_binary(["dracut", "--kver", kernel, "--no-uefi", 28 | "--early-microcode", "--add", "verity-squash-root", 29 | str(merged_initramfs)]) 30 | return merged_initramfs 31 | 32 | def list_kernel_presets(self, kernel: str) -> List[str]: 33 | return [""] 34 | -------------------------------------------------------------------------------- /src/verity_squash_root/initramfs/mkinitcpio.py: -------------------------------------------------------------------------------- 1 | from collections.abc import Mapping 2 | from pathlib import Path 3 | from typing import List 4 | from verity_squash_root.config import TMPDIR, NAME_DASH 5 | from verity_squash_root.exec import exec_binary 6 | from verity_squash_root.file_op import read_text_from, write_str_to 7 | from verity_squash_root.initramfs import merge_initramfs_images 8 | from verity_squash_root.distributions.base import DistributionConfig 9 | from verity_squash_root.initramfs.base import InitramfsBuilder 10 | 11 | 12 | class Mkinitcpio(InitramfsBuilder): 13 | _preset_map: Mapping[str, str] = {"default": ""} 14 | distribution: DistributionConfig 15 | 16 | def __init__(self, distribution: DistributionConfig): 17 | self._distribution = distribution 18 | 19 | def file_name(self, kernel: str, preset: str) -> str: 20 | preset_name = self._preset_map.get(preset, preset) 21 | kernel_name = self._distribution.kernel_to_name(kernel) 22 | if preset_name == "": 23 | return kernel_name 24 | else: 25 | return "{}_{}".format(kernel_name, preset_name) 26 | 27 | def display_name(self, kernel: str, preset: str) -> str: 28 | preset_name = self._preset_map.get(preset, preset) 29 | kernel_name = self._distribution.kernel_to_name( 30 | kernel).replace("-", " ") 31 | if preset_name != "": 32 | preset_name = " ({})".format(preset_name) 33 | return "{} {}{}".format( 34 | self._distribution.display_name(), 35 | kernel_name.capitalize(), 36 | preset_name) 37 | 38 | def build_initramfs_with_microcode(self, kernel: str, 39 | preset: str) -> Path: 40 | name = self._distribution.kernel_to_name(kernel) 41 | config = read_text_from( 42 | Path("/etc/mkinitcpio.d") / "{}.preset".format(name)) 43 | base_path = TMPDIR / "{}-{}".format(name, preset) 44 | initcpio_image = Path("{}.initcpio".format(base_path)) 45 | preset_path = base_path.with_suffix(".preset") 46 | write_config = ("{}\n" 47 | "PRESETS=('{p}')\n" 48 | "{p}_image={}\n" 49 | "{p}_options=\"${{{p}_options}} -A {}\"\n").format( 50 | config, 51 | initcpio_image, 52 | NAME_DASH, 53 | p=preset) 54 | write_str_to(preset_path, write_config) 55 | exec_binary(["mkinitcpio", "-p", str(preset_path)]) 56 | 57 | merged_initramfs = base_path.with_suffix(".image") 58 | merge_initramfs_images(initcpio_image, 59 | self._distribution.microcode_paths(), 60 | merged_initramfs) 61 | return merged_initramfs 62 | 63 | def list_kernel_presets(self, kernel: str) -> List[str]: 64 | name = self._distribution.kernel_to_name(kernel) 65 | run = "/usr/lib/verity-squash-root/mkinitcpio_list_presets" 66 | presets_str = exec_binary([run, name])[0].decode() 67 | return presets_str.strip().split("\n") 68 | -------------------------------------------------------------------------------- /src/verity_squash_root/main.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import shutil 3 | from pathlib import Path 4 | from configparser import ConfigParser 5 | from typing import List, Union 6 | import verity_squash_root.cmdline as cmdline 7 | import verity_squash_root.efi as efi 8 | from verity_squash_root.config import TMPDIR, KERNEL_PARAM_BASE, KEY_DIR, \ 9 | EFI_KERNELS, config_str_to_stripped_arr 10 | from verity_squash_root.distributions.base import DistributionConfig 11 | from verity_squash_root.initramfs.base import InitramfsBuilder, \ 12 | iterate_distribution_efi 13 | from verity_squash_root.file_names import backup_file, tmpfs_file, tmpfs_label 14 | from verity_squash_root.file_op import read_text_from 15 | from verity_squash_root.image import mksquashfs, veritysetup_image, \ 16 | verity_image_path 17 | 18 | 19 | def move_kernel_to(src: Path, dst: Path, slot: str, 20 | dst_backup: Union[Path, None]) -> None: 21 | if dst.exists(): 22 | overwrite_file = efi.file_matches_slot_or_is_broken(dst, slot) 23 | if overwrite_file or dst_backup is None: 24 | # if backup slot is booted, dont override it 25 | if dst_backup is None: 26 | logging.debug("Backup ignored") 27 | elif overwrite_file: 28 | logging.debug("Backup slot kept as is") 29 | dst.unlink() 30 | else: 31 | logging.info("Moving old efi to backup") 32 | logging.debug("Path: {}".format(dst_backup)) 33 | dst.replace(dst_backup) 34 | shutil.move(src, dst) 35 | 36 | 37 | def create_squashfs_return_verity_hash(config: ConfigParser, image: Path) \ 38 | -> str: 39 | root_mount = Path(config["DEFAULT"]["ROOT_MOUNT"]) 40 | logging.debug("Image path: {}".format(image)) 41 | efi_partition = Path(config["DEFAULT"]["EFI_PARTITION"]) 42 | exclude_dirs = config_str_to_stripped_arr( 43 | config["DEFAULT"]["EXCLUDE_DIRS"]) 44 | logging.info("Creating squashfs...") 45 | mksquashfs(exclude_dirs, image, root_mount, efi_partition) 46 | logging.info("Setup device verity") 47 | root_hash = veritysetup_image(image) 48 | return root_hash 49 | 50 | 51 | def build_and_move_kernel(config: ConfigParser, 52 | vmlinuz: Path, initramfs: Path, 53 | use_slot: str, root_hash: str, cmdline_add: str, 54 | base_name: str, out_dir: Path, 55 | label: str, 56 | ignore_efis: List[str]): 57 | if base_name in ignore_efis: 58 | return 59 | logging.info("Processing {}".format(label)) 60 | out = out_dir / "{}.efi".format(base_name) 61 | backup_out = None 62 | backup_base_name = backup_file(base_name) 63 | if backup_base_name not in ignore_efis: 64 | backup_out = out_dir / "{}.efi".format(backup_base_name) 65 | logging.debug("Write efi to {}".format(out)) 66 | # Store files to sign on trusted tmpfs 67 | tmp_efi_file = TMPDIR / "tmp.efi" 68 | efi.build_and_sign_kernel(config, vmlinuz, initramfs, use_slot, 69 | root_hash, tmp_efi_file, 70 | cmdline_add) 71 | move_kernel_to(tmp_efi_file, out, use_slot, backup_out) 72 | 73 | 74 | def create_directory(path: Path): 75 | path.mkdir(parents=True, exist_ok=True) 76 | 77 | 78 | def create_image_and_sign_kernel(config: ConfigParser, 79 | distribution: DistributionConfig, 80 | initramfs: InitramfsBuilder): 81 | kernel_cmdline = read_text_from(Path("/proc/cmdline")) 82 | use_slot = cmdline.unused_slot(kernel_cmdline) 83 | efi_partition = Path(config["DEFAULT"]["EFI_PARTITION"]) 84 | efi_dirname = distribution.efi_dirname() 85 | out_dir = efi_partition / EFI_KERNELS / efi_dirname 86 | create_directory(out_dir) 87 | logging.info("Using slot {} for new image".format(use_slot)) 88 | root_mount = Path(config["DEFAULT"]["ROOT_MOUNT"]) 89 | image = root_mount / "image_{}.squashfs".format(use_slot) 90 | tmp_image = TMPDIR / "tmp.squashfs" 91 | root_hash = create_squashfs_return_verity_hash(config, tmp_image) 92 | logging.debug("Calculated root hash: {}".format(root_hash)) 93 | ignore_efis = config_str_to_stripped_arr( 94 | config["DEFAULT"]["IGNORE_KERNEL_EFIS"]) 95 | 96 | for [kernel, preset, base_name] in iterate_distribution_efi(distribution, 97 | initramfs): 98 | vmlinuz = distribution.vmlinuz(kernel) 99 | base_name = initramfs.file_name(kernel, preset) 100 | base_name_tmpfs = tmpfs_file(base_name) 101 | display = initramfs.display_name(kernel, preset) 102 | 103 | if base_name in ignore_efis and base_name_tmpfs in ignore_efis: 104 | logging.info("skipping due to ignored kernels") 105 | continue 106 | 107 | logging.info("Create initramfs for {}".format(display)) 108 | initramfs_path = initramfs.build_initramfs_with_microcode( 109 | kernel, preset) 110 | 111 | def build(bn, label, cmdline_add): 112 | build_and_move_kernel(config, vmlinuz, initramfs_path, 113 | use_slot, root_hash, cmdline_add, 114 | bn, out_dir, label, 115 | ignore_efis) 116 | 117 | build(base_name, display, "") 118 | build(base_name_tmpfs, tmpfs_label(display), 119 | "{}_volatile".format(KERNEL_PARAM_BASE)) 120 | 121 | # Only replace old image if initramfs was successfully created 122 | shutil.move(tmp_image, image) 123 | shutil.move(verity_image_path(tmp_image), verity_image_path(image)) 124 | 125 | 126 | def backup_and_sign_efi(source: Path, dest: Path): 127 | if dest.exists(): 128 | parent = dest.parent 129 | backup_name = backup_file(dest.stem) + dest.suffix 130 | backup = parent / backup_name 131 | dest.replace(backup) 132 | efi.sign(KEY_DIR, source, dest) 133 | 134 | 135 | def backup_and_sign_extra_files(config: ConfigParser): 136 | extra = config["EXTRA_SIGN"] 137 | for key in extra.keys(): 138 | logging.info("Signing {}...".format(key)) 139 | files = extra[key].split("=>") 140 | if len(files) != 2: 141 | raise ValueError("extra signing files need to be specified as\n" 142 | "name = SOURCE => DEST") 143 | src = Path(files[0].strip()) 144 | dest = Path(files[1].strip()) 145 | logging.debug("Sign file '{}' to '{}'".format(src, dest)) 146 | dest.resolve().parent.mkdir(parents=True, exist_ok=True) 147 | backup_and_sign_efi(src, dest) 148 | -------------------------------------------------------------------------------- /src/verity_squash_root/mount.py: -------------------------------------------------------------------------------- 1 | import shutil 2 | from pathlib import Path 3 | from verity_squash_root.exec import exec_binary 4 | 5 | 6 | class TmpfsMount(): 7 | _directory: Path 8 | 9 | def __init__(self, directory: Path): 10 | self._directory = directory 11 | 12 | def __enter__(self): 13 | tmp_mount = ["mount", "-t", "tmpfs", "-o", 14 | "mode=0700,uid=0,gid=0", "tmpfs", str(self._directory)] 15 | self._directory.mkdir() 16 | exec_binary(tmp_mount) 17 | 18 | def __exit__(self, exc_type, exc_value, exc_tb): 19 | tmp_umount = ["umount", "-f", "-R", str(self._directory)] 20 | exec_binary(tmp_umount) 21 | shutil.rmtree(self._directory) 22 | -------------------------------------------------------------------------------- /src/verity_squash_root/parsing.py: -------------------------------------------------------------------------------- 1 | from collections.abc import Mapping 2 | 3 | 4 | def info_to_dict(output: str, sep: str = ":") -> Mapping[str, str]: 5 | result = {} 6 | for line in output.split("\n"): 7 | if sep not in line: 8 | continue 9 | s = line.split(sep, 2) 10 | key = s[0].strip() 11 | result[key] = s[1].strip() 12 | return result 13 | -------------------------------------------------------------------------------- /src/verity_squash_root/setup.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from configparser import ConfigParser 3 | from pathlib import Path 4 | from verity_squash_root.distributions.base import DistributionConfig 5 | from verity_squash_root.initramfs.base import InitramfsBuilder 6 | from verity_squash_root.exec import exec_binary 7 | from verity_squash_root.config import KEY_DIR, EFI_KERNELS 8 | import verity_squash_root.efi as efi 9 | from verity_squash_root.file_op import write_str_to 10 | from verity_squash_root.file_names import iterate_non_ignored_kernel_variants 11 | 12 | 13 | def add_uefi_boot_option(disk: str, partition_no: int, label: str, 14 | efi_file: Path): 15 | cmd = ["efibootmgr", "--disk", disk, "--part", str(partition_no), 16 | "--create", "--label", label, "--loader", str(efi_file)] 17 | exec_binary(cmd) 18 | 19 | 20 | def add_kernels_to_uefi(config: ConfigParser, distribution: DistributionConfig, 21 | initramfs: InitramfsBuilder, 22 | disk: str, partition_no: int) -> None: 23 | efi_dirname = distribution.efi_dirname() 24 | out_dir = Path("/") / EFI_KERNELS / efi_dirname 25 | 26 | # Use reversed order, because last added option is sometimes 27 | # the default boot option 28 | kernels = reversed(list(iterate_non_ignored_kernel_variants( 29 | config, distribution, initramfs))) 30 | for (kernel, preset, base_name, label) in kernels: 31 | out = out_dir / "{}.efi".format(base_name) 32 | add_uefi_boot_option(disk, partition_no, label, out) 33 | 34 | 35 | def setup_systemd_boot(config: ConfigParser, 36 | distribution: DistributionConfig, 37 | initramfs: InitramfsBuilder) -> None: 38 | exec_binary(["bootctl", "install"]) 39 | boot_efi = Path("/usr/lib/systemd/boot/efi/systemd-bootx64.efi") 40 | efi_partition = Path(config["DEFAULT"]["EFI_PARTITION"]) 41 | logging.debug("Sign efi files") 42 | efi.sign(KEY_DIR, boot_efi, 43 | efi_partition / "EFI/systemd/systemd-bootx64.efi") 44 | efi.sign(KEY_DIR, boot_efi, efi_partition / "EFI/BOOT/BOOTX64.EFI") 45 | 46 | efi_dirname = distribution.efi_dirname() 47 | out_dir = Path("/") / EFI_KERNELS / efi_dirname 48 | entries_dir = efi_partition / "loader/entries" 49 | 50 | logging.debug("Create configuration files") 51 | for (kernel, preset, base_name, label) in \ 52 | iterate_non_ignored_kernel_variants(config, distribution, 53 | initramfs): 54 | binary = out_dir / "{}.efi".format(base_name) 55 | out = entries_dir / "{}.conf".format(base_name) 56 | text = "title {}\nlinux {}\n".format(label, binary) 57 | write_str_to(out, text) 58 | -------------------------------------------------------------------------------- /tests/unit/cmdline.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from verity_squash_root.cmdline import current_slot, unused_slot 3 | 4 | 5 | class CmdlineTest(unittest.TestCase): 6 | 7 | def test__current_slot(self): 8 | self.assertEqual( 9 | current_slot("root=L=5 rw verity_squash_root_slot=a pti=on"), 10 | "a") 11 | self.assertEqual( 12 | current_slot("crpytroot verity_squash_root_slot=b tsx=off"), 13 | "b") 14 | self.assertEqual( 15 | current_slot("crpytroot pti=on tsx=off"), 16 | None) 17 | 18 | def test__unused_slot(self): 19 | self.assertEqual( 20 | unused_slot("root=L=5 rw verity_squash_root_slot=a pti=on"), 21 | "b") 22 | self.assertEqual( 23 | unused_slot("crpytroot verity_squash_root_slot=b tsx=off"), 24 | "a") 25 | self.assertEqual( 26 | unused_slot("crpytroot verity_squash_root_slot=h tsx=off"), 27 | "a") 28 | self.assertEqual( 29 | unused_slot("crpytroot pti=on tsx=off"), 30 | "a") 31 | -------------------------------------------------------------------------------- /tests/unit/config.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from pathlib import Path 3 | from unittest import mock 4 | from tests.unit.test_helper import PROJECT_ROOT 5 | from verity_squash_root.config import config_str_to_stripped_arr, \ 6 | read_config, check_config, is_volatile_boot, check_config_and_system 7 | 8 | 9 | class ConfigTest(unittest.TestCase): 10 | 11 | def test__config_str_to_stripped_arr(self): 12 | self.assertEqual( 13 | config_str_to_stripped_arr( 14 | " var/!(lib) ,/mnt/test\n,another/dir/ ,testdir"), 15 | ["var/!(lib)", "/mnt/test", "another/dir/", "testdir"]) 16 | 17 | @mock.patch("verity_squash_root.config.ConfigParser") 18 | def test__read_config(self, cp_mock): 19 | result = read_config() 20 | cp_mock.assert_called_once_with(default_section="DO_NOT_USE_DEFAULTS") 21 | self.assertEqual(cp_mock.mock_calls, [ 22 | mock.call(default_section="DO_NOT_USE_DEFAULTS"), 23 | mock.call().read( 24 | PROJECT_ROOT / "src/verity_squash_root/default_config.ini"), 25 | mock.call().read( 26 | Path('/usr/share/verity_squash_root/default.ini')), 27 | mock.call().read(Path('/etc/verity_squash_root/config.ini'))]) 28 | self.assertEqual(result, cp_mock()) 29 | 30 | @mock.patch("verity_squash_root.config.Path") 31 | def test__check_config(self, path_mock): 32 | config = { 33 | "DEFAULT": { 34 | "ROOT_MOUNT": "/opt/root", 35 | "EFI_PARTITION": "/boot/efimnt", 36 | } 37 | } 38 | 39 | def run(mount_result): 40 | root_mock = mock.Mock() 41 | root_mock.is_mount.return_value = mount_result[0] 42 | efi_mock = mock.Mock() 43 | efi_mock.is_mount.return_value = mount_result[1] 44 | 45 | def ret_mock(name): 46 | pm = mock.Mock() 47 | pm.resolve.return_value = \ 48 | root_mock if name == "/opt/root" else efi_mock 49 | pm.__str__ = mock.Mock() 50 | pm.__str__.return_value = name 51 | return pm 52 | 53 | path_mock.side_effect = ret_mock 54 | result = check_config(config) 55 | self.assertEqual( 56 | path_mock.mock_calls, 57 | [mock.call("/opt/root"), 58 | mock.call("/boot/efimnt")]) 59 | path_mock.reset_mock() 60 | return result 61 | 62 | mnt_error = "Directory '/opt/root' is not a mount point" 63 | efi_error = "Directory '/boot/efimnt' is not a mount point" 64 | self.assertEqual(run([True, True]), []) 65 | self.assertEqual(run([True, False]), [efi_error]) 66 | self.assertEqual(run([False, True]), [mnt_error]) 67 | self.assertEqual(run([False, False]), [mnt_error, efi_error]) 68 | 69 | @mock.patch("verity_squash_root.config.exec_binary") 70 | def test__is_volatile_boot(self, exec_mock): 71 | cmdline = ("rw,relatime,lowerdir=/verity-squash-root-tmp/squashroot," 72 | "upperdir={},workdir=/sysroot/workdir," 73 | "index=off,xino=off,metacopy=off") 74 | 75 | def test(upper, result): 76 | exec_mock.reset_mock() 77 | exec_mock.return_value = [cmdline.format(upper).encode(), b""] 78 | self.assertEqual(is_volatile_boot(), result) 79 | exec_mock.assert_called_once_with( 80 | ["findmnt", "-uno", "OPTIONS", "/"]) 81 | 82 | test("/sysroot/overlay", False) 83 | test("/verity-squash-root-tmp/tmpfs/overlay", True) 84 | 85 | def test__check_config_and_system(self): 86 | base = "verity_squash_root.config" 87 | with (mock.patch("{}.is_volatile_boot".format(base)) as is_vol, 88 | mock.patch("{}.check_config".format(base)) as check_config): 89 | check_config.return_value = ["some", "test", "values"] 90 | is_vol.return_value = True 91 | config = mock.Mock() 92 | res = check_config_and_system(config) 93 | is_vol.assert_called_once_with() 94 | check_config.assert_called_once_with(config) 95 | self.assertEqual(res, ["some", "test", "values"]) 96 | 97 | is_vol.return_value = False 98 | res = check_config_and_system(config) 99 | self.assertEqual( 100 | res, 101 | ["some", "test", "values", 102 | "System is not booted in volatile mode:", 103 | " - System could be compromised from previous boots", 104 | (" - It is recommended to enter secure boot key passwords " 105 | "only in volatile mode"), 106 | " - Know what you are doing!"]) 107 | -------------------------------------------------------------------------------- /tests/unit/decrypt.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from unittest import mock 3 | from unittest.mock import call 4 | from verity_squash_root.decrypt import format_cmd, TAR_FILE, KEY_DIR, \ 5 | decrypt_secure_boot_keys, DecryptKeys 6 | from .test_helper import wrap_tempdir, get_test_files_path 7 | 8 | TEST_FILES_DIR = get_test_files_path("decrypt") 9 | 10 | 11 | class DecryptTest(unittest.TestCase): 12 | 13 | def test__format_cmd(self): 14 | self.assertEqual(format_cmd("\n\tage -d\t-o {}\n/etc/keys.tar.age", 15 | "/tmp/keys.tar"), 16 | ["age", "-d", "-o", "/tmp/keys.tar", 17 | "/etc/keys.tar.age"]) 18 | 19 | @wrap_tempdir 20 | def test__decrypt_secure_boot_keys(self, tempdir): 21 | base = "verity_squash_root.decrypt" 22 | key_dir = tempdir / "keys" 23 | all_mocks = mock.MagicMock() 24 | with (mock.patch("{}.exec_binary".format(base), 25 | new=all_mocks.exec_binary), 26 | mock.patch("{}.KEY_DIR".format(base), 27 | new=key_dir), 28 | mock.patch("{}.tarfile".format(base), 29 | new=all_mocks.tarfile)): 30 | config = { 31 | "DEFAULT": { 32 | "DECRYPT_SECURE_BOOT_KEYS_CMD": "cp /root/keys.tar {}", 33 | } 34 | } 35 | self.assertEqual(key_dir.is_dir(), False) 36 | decrypt_secure_boot_keys(config) 37 | self.assertEqual( 38 | all_mocks.mock_calls, 39 | [call.exec_binary(["cp", "/root/keys.tar", str(TAR_FILE)]), 40 | call.tarfile.open(TAR_FILE), 41 | call.tarfile.open().__enter__(), 42 | call.tarfile.open().__enter__().extract("db.crt", key_dir), 43 | call.tarfile.open().__enter__().extract("db.key", key_dir), 44 | call.tarfile.open().__exit__(None, None, None)]) 45 | self.assertEqual(key_dir.is_dir(), True) 46 | 47 | def test__decrypt_keys(self): 48 | base = "verity_squash_root.decrypt" 49 | all_mocks = mock.Mock() 50 | config = mock.Mock() 51 | with (mock.patch("{}.decrypt_secure_boot_keys".format(base), 52 | new=all_mocks.decrypt_secure_boot_keys), 53 | mock.patch("{}.shutil".format(base), 54 | new=all_mocks.shutil)): 55 | with DecryptKeys(config): 56 | all_mocks.inner_func() 57 | self.assertEqual( 58 | all_mocks.mock_calls, 59 | [call.decrypt_secure_boot_keys(config), 60 | call.inner_func(), 61 | call.shutil.rmtree(KEY_DIR)]) 62 | 63 | @wrap_tempdir 64 | def test__decrypt_keys__real_file(self, tempdir): 65 | cmd = "cp {} {{}}".format(TEST_FILES_DIR / "keys.tar") 66 | config = { 67 | "DEFAULT": { 68 | "DECRYPT_SECURE_BOOT_KEYS_CMD": cmd 69 | } 70 | } 71 | key_dir = tempdir / "keys" 72 | tar_file = key_dir / "keys.tar" 73 | base = "verity_squash_root.decrypt" 74 | with (mock.patch("{}.TAR_FILE".format(base), 75 | new=tar_file), 76 | mock.patch("{}.KEY_DIR".format(base), 77 | new=key_dir)): 78 | with DecryptKeys(config): 79 | key_file = key_dir / "db.key" 80 | cert_file = key_dir / "db.crt" 81 | self.assertEqual( 82 | key_file.read_text(), 83 | "DB Key File 842\n") 84 | self.assertEqual( 85 | cert_file.read_text(), 86 | "22 DB Cert File 7\n") 87 | # Test that no other file got extracted to avoid python 88 | # extract / extractall vulnerability 89 | self.assertEqual( 90 | sorted(list(key_dir.iterdir())), 91 | [cert_file, key_file, tar_file]) 92 | -------------------------------------------------------------------------------- /tests/unit/distributions/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brandsimon/verity-squash-root/1a31d0ad8934029b5fd065a9baeb50f5a492e585/tests/unit/distributions/__init__.py -------------------------------------------------------------------------------- /tests/unit/distributions/arch.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from pathlib import Path 3 | from unittest import mock 4 | from verity_squash_root.distributions.arch import ArchLinuxConfig 5 | 6 | 7 | class ArchLinuxConfigTest(unittest.TestCase): 8 | 9 | def test__efi_dirname(self): 10 | arch = ArchLinuxConfig("arch", "Arch") 11 | self.assertEqual(arch.efi_dirname(), "arch") 12 | 13 | @mock.patch("verity_squash_root.distributions.arch.read_text_from") 14 | def test__kernel_to_name(self, mock): 15 | arch = ArchLinuxConfig("arch", "Arch") 16 | mock.return_value = " kernel Nom " 17 | result = arch.kernel_to_name("5.12.2") 18 | mock.assert_called_once_with( 19 | Path("/usr/lib/modules") / "5.12.2" / "pkgbase") 20 | self.assertEqual(result, "kernel Nom") 21 | 22 | def test__vmlinuz(self): 23 | arch = ArchLinuxConfig("arch", "Arch") 24 | self.assertEqual(arch.vmlinuz("5.17.2-arch"), 25 | Path("/usr/lib/modules/5.17.2-arch/vmlinuz")) 26 | 27 | def test__display_name(self): 28 | arch = ArchLinuxConfig("arch", "Arch") 29 | self.assertEqual(arch.display_name(), "Arch") 30 | 31 | @mock.patch("verity_squash_root.distributions." 32 | "base.DistributionConfig._modules_dir") 33 | def test__list_kernels(self, mock): 34 | mock.iterdir.return_value = [Path("/usr/lib/modules/5.16.4"), 35 | Path("/usr/lib/modules/5.5.2")] 36 | arch = ArchLinuxConfig("arch", "Arch") 37 | result = arch.list_kernels() 38 | mock.iterdir.assert_called_once_with() 39 | self.assertEqual(result, ["5.16.4", "5.5.2"]) 40 | 41 | def test__microcode_paths(self): 42 | arch = ArchLinuxConfig("arch", "Arch") 43 | self.assertEqual(arch.microcode_paths(), 44 | [Path("/boot/amd-ucode.img"), 45 | Path("/boot/intel-ucode.img")]) 46 | -------------------------------------------------------------------------------- /tests/unit/distributions/autodetect.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from unittest import mock 3 | from verity_squash_root.distributions.autodetect import load_os_release, \ 4 | autodetect_distribution 5 | from tests.unit.test_helper import get_test_files_path 6 | 7 | 8 | TEST_FILES_DIR = get_test_files_path("distributions") 9 | 10 | 11 | class DistributionDetectTest(unittest.TestCase): 12 | 13 | @mock.patch("verity_squash_root.distributions.autodetect.ETC_FILE", 14 | new=TEST_FILES_DIR / "etc-os-release") 15 | def test__load_os_release__etc(self): 16 | result = load_os_release() 17 | self.assertEqual(result, 18 | {"ID": "arch", 19 | "NAME": "Arch Linux", 20 | "HOME_URL": "arch website", 21 | "LOGO": "archlinux-logo", 22 | }) 23 | 24 | @mock.patch("verity_squash_root.distributions.autodetect.ETC_FILE") 25 | @mock.patch("verity_squash_root.distributions.autodetect.USR_FILE", 26 | new=TEST_FILES_DIR / "usr-os-release") 27 | def test__load_os_release__usr(self, file): 28 | file.resolve.return_value.is_file.return_value = False 29 | result = load_os_release() 30 | self.assertEqual(result, 31 | {"ID": "ubuntu", 32 | "ID_LIKE": "debian", 33 | "NAME": "Debian GNU/Linux", 34 | "HOME_URL": "their site", 35 | }) 36 | 37 | @mock.patch("verity_squash_root.distributions.autodetect.ETC_FILE", 38 | new=TEST_FILES_DIR / "arch") 39 | @mock.patch("verity_squash_root.distributions.autodetect.ArchLinuxConfig") 40 | def test__autodetect_distribution__arch(self, mock): 41 | result = autodetect_distribution() 42 | mock.assert_called_once_with("arch", "My Arch") 43 | self.assertEqual(result, mock()) 44 | 45 | @mock.patch("verity_squash_root.distributions.autodetect.ETC_FILE", 46 | new=TEST_FILES_DIR / "debian") 47 | @mock.patch("verity_squash_root.distributions.autodetect.DebianConfig") 48 | def test__autodetect_distribution__debian(self, mock): 49 | result = autodetect_distribution() 50 | mock.assert_called_once_with("debian", "Debian GNU/Linux") 51 | self.assertEqual(result, mock()) 52 | 53 | @mock.patch("verity_squash_root.distributions.autodetect.ETC_FILE", 54 | new=TEST_FILES_DIR / "debian-like") 55 | @mock.patch("verity_squash_root.distributions.autodetect.DebianConfig") 56 | def test__autodetect_distribution__debian_like(self, mock): 57 | result = autodetect_distribution() 58 | mock.assert_called_once_with("ubuntu", "Ubuntu 22.04") 59 | self.assertEqual(result, mock()) 60 | -------------------------------------------------------------------------------- /tests/unit/distributions/base.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from pathlib import Path 3 | from unittest import mock 4 | from verity_squash_root.distributions.base import \ 5 | calc_kernel_packages_not_unique 6 | 7 | 8 | def distribution_mock(): 9 | obj_kernel = {"5.19": "linux", "5.14": "linux-lts"} 10 | 11 | def vmlinuz(kernel): 12 | return Path("/lib64/modules/{}/vmlinuz".format(kernel)) 13 | 14 | def kernel_to_name(kernel): 15 | return obj_kernel[kernel] 16 | 17 | distri_mock = mock.Mock() 18 | distri_mock.list_kernels.return_value = ["5.19", "5.14"] 19 | distri_mock.efi_dirname.return_value = "ArchEfi" 20 | distri_mock.display_name.return_value = "Arch" 21 | distri_mock.vmlinuz.side_effect = vmlinuz 22 | distri_mock.kernel_to_name.side_effect = kernel_to_name 23 | return distri_mock 24 | 25 | 26 | class BaseDistributionTest(unittest.TestCase): 27 | 28 | def test__calc_kernel_packages_not_unique(self): 29 | distri_mock = distribution_mock() 30 | self.assertEqual( 31 | [], 32 | calc_kernel_packages_not_unique(distri_mock)) 33 | 34 | distri_mock.kernel_to_name.side_effect = None 35 | distri_mock.kernel_to_name.return_value = "linux-h" 36 | self.assertEqual( 37 | ["Package linux-h has multiple kernel versions: 5.14, 5.19", 38 | "This means, that there are probably old files in " 39 | "/usr/lib/modules"], 40 | calc_kernel_packages_not_unique(distri_mock)) 41 | -------------------------------------------------------------------------------- /tests/unit/distributions/debian.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from pathlib import Path 3 | from unittest import mock 4 | from verity_squash_root.distributions.debian import DebianConfig 5 | 6 | 7 | class DebianConfigTest(unittest.TestCase): 8 | 9 | def test__efi_dirname(self): 10 | debian = DebianConfig("ubuntu", "Kali") 11 | self.assertEqual(debian.efi_dirname(), "ubuntu") 12 | 13 | def test__kernel_to_name(self): 14 | debian = DebianConfig("debian", "Debian GNU/Linux") 15 | debian.list_kernels = mock.Mock( 16 | return_value=["6.1.13", "5.15.2", "5.4"]) 17 | self.assertEqual(debian.kernel_to_name("6.1.13"), 18 | "current") 19 | self.assertEqual(debian.kernel_to_name("5.15.2"), 20 | "old") 21 | self.assertEqual(debian.kernel_to_name("5.4"), 22 | "old_x2") 23 | 24 | def test__vmlinuz(self): 25 | debian = DebianConfig("debian", "Debian GNU/Linux") 26 | self.assertEqual(debian.vmlinuz("5.17.2-debian"), 27 | Path("/boot/vmlinuz-5.17.2-debian")) 28 | 29 | def test__display_name(self): 30 | debian = DebianConfig("debian", "Debian GNU/Linux") 31 | self.assertEqual(debian.display_name(), "Debian GNU/Linux") 32 | 33 | @mock.patch("verity_squash_root.distributions." 34 | "base.DistributionConfig._modules_dir") 35 | def test__list_kernels(self, mock): 36 | mock.iterdir.return_value = [Path("/usr/lib/modules/5.16.4-amd64"), 37 | Path("/usr/lib/modules/5.4.2-amd64"), 38 | Path("/usr/lib/modules/6.0.1-amd64"), 39 | Path("/usr/lib/modules/5.13.2-amd64")] 40 | debian = DebianConfig("debian", "Debian GNU/Linux") 41 | result = debian.list_kernels() 42 | mock.iterdir.assert_called_once_with() 43 | self.assertEqual(result, 44 | ["6.0.1-amd64", "5.16.4-amd64", 45 | "5.13.2-amd64", "5.4.2-amd64"]) 46 | 47 | def test__microcode_paths(self): 48 | debian = DebianConfig("debian", "Debian GNU/Linux") 49 | with self.assertRaises(NotImplementedError) as e: 50 | debian.microcode_paths() 51 | self.assertEqual(str(e), "Base class") 52 | -------------------------------------------------------------------------------- /tests/unit/efi.py: -------------------------------------------------------------------------------- 1 | import math 2 | import unittest 3 | from .test_helper import get_test_files_path 4 | from pathlib import Path 5 | from unittest import mock 6 | from verity_squash_root.config import KEY_DIR 7 | from verity_squash_root.file_op import read_from 8 | from verity_squash_root.efi import file_matches_slot_or_is_broken, sign, \ 9 | create_efi_executable, build_and_sign_kernel, get_cmdline, \ 10 | calculate_efi_stub_end, calculate_efi_stub_alignment 11 | 12 | TEST_FILES_DIR = get_test_files_path("efi") 13 | 14 | 15 | class EfiTest(unittest.TestCase): 16 | 17 | def test__file_matches_slot_or_is_broken(self): 18 | 19 | def wrapper(path: str, slot: str): 20 | file = TEST_FILES_DIR / path 21 | content_before = read_from(file) 22 | result = file_matches_slot_or_is_broken(file, slot) 23 | self.assertEqual(content_before, read_from(file), 24 | "objcopy modified file (breaks secure boot)") 25 | return result 26 | 27 | self.assertTrue(wrapper("stub_slot_a.efi", "a")) 28 | self.assertFalse(wrapper("stub_slot_a.efi", "b")) 29 | self.assertTrue(wrapper("stub_slot_b.efi", "b")) 30 | self.assertFalse(wrapper("stub_slot_b.efi", "a")) 31 | 32 | self.assertFalse(wrapper("stub_slot_unkown.efi", "a")) 33 | self.assertFalse(wrapper("stub_slot_unkown.efi", "b")) 34 | 35 | log_trunc = ["WARNING:root:Old efi file was truncated"] 36 | with self.assertLogs() as logs: 37 | self.assertTrue(wrapper("stub_broken.efi", "a")) 38 | self.assertEqual(logs.output, log_trunc) 39 | with self.assertLogs() as logs: 40 | self.assertTrue(wrapper("stub_broken.efi", "b")) 41 | self.assertEqual(logs.output, log_trunc) 42 | 43 | log_empty = ["WARNING:root:Old efi file was empty"] 44 | with self.assertLogs() as logs: 45 | self.assertTrue(wrapper("stub_empty.efi", "a")) 46 | self.assertEqual(logs.output, log_empty) 47 | with self.assertLogs() as logs: 48 | self.assertTrue(wrapper("stub_empty.efi", "b")) 49 | self.assertEqual(logs.output, log_empty) 50 | 51 | @mock.patch("verity_squash_root.efi.exec_binary") 52 | def test__sign(self, mock): 53 | sign(Path("my/key/dir"), Path("my/in/file"), Path("my/out/file")) 54 | mock.assert_called_once_with( 55 | ["sbsign", 56 | "--key", "my/key/dir/db.key", 57 | "--cert", "my/key/dir/db.crt", 58 | "--output", "my/out/file", 59 | "my/in/file"]) 60 | 61 | def test__calculate_efi_stub_end(self): 62 | self.assertEqual( 63 | calculate_efi_stub_end(TEST_FILES_DIR / "stub_slot_a.efi"), 64 | 74064) 65 | 66 | def test__calculate_efi_stub_alignment(self): 67 | self.assertEqual( 68 | calculate_efi_stub_alignment(TEST_FILES_DIR / "stub_slot_a.efi"), 69 | 512) 70 | 71 | def test__calculate_efi_stub_alignment__not_found(self): 72 | with self.assertRaises(ValueError) as e_ctx: 73 | calculate_efi_stub_alignment( 74 | TEST_FILES_DIR / "no_sections_or_info") 75 | self.assertEqual( 76 | str(e_ctx.exception), "Efistub SectionAlignment not found") 77 | 78 | @mock.patch("verity_squash_root.efi.calculate_efi_stub_alignment") 79 | @mock.patch("verity_squash_root.efi.exec_binary") 80 | @mock.patch("verity_squash_root.efi.calculate_efi_stub_end") 81 | def test__create_efi_executable(self, calc_mock, exec_mock, align_mock): 82 | def align(x): 83 | return 2048 * math.ceil(x / 2048) 84 | 85 | align_mock.return_value = 2048 86 | calc_mock.return_value = 74064 87 | create_efi_executable( 88 | TEST_FILES_DIR / "stub_slot_a.efi", 89 | TEST_FILES_DIR / "cmdline", 90 | TEST_FILES_DIR / "vmlinuz", 91 | TEST_FILES_DIR / "initrd", 92 | Path("/tmp/file.efi")) 93 | align_mock.assert_called_once_with(TEST_FILES_DIR / "stub_slot_a.efi") 94 | calc_mock.assert_called_once_with(TEST_FILES_DIR / "stub_slot_a.efi") 95 | osrel_size = Path("/etc/os-release").stat().st_size 96 | 97 | exec_mock.assert_called_once_with([ 98 | 'objcopy', 99 | '--add-section', '.osrel=/etc/os-release', 100 | '--change-section-vma', '.osrel=0x12800', 101 | '--add-section', '.cmdline={}'.format(TEST_FILES_DIR / "cmdline"), 102 | '--change-section-vma', '.cmdline={}'.format( 103 | hex(align(0x12800 + osrel_size))), 104 | '--add-section', '.initrd={}'.format(TEST_FILES_DIR / "initrd"), 105 | '--change-section-vma', '.initrd={}'.format( 106 | hex(align(0x13000 + osrel_size))), 107 | '--add-section', '.linux={}'.format(TEST_FILES_DIR / "vmlinuz"), 108 | '--change-section-vma', '.linux={}'.format( 109 | hex(align(0x13800 + osrel_size))), 110 | str(TEST_FILES_DIR / "stub_slot_a.efi"), 111 | str(Path("/tmp/file.efi"))]) 112 | 113 | def test__get_cmdline__configfile(self): 114 | all_mocks = mock.Mock() 115 | cmdline = "rw encrypt=/dev/sda2 quiet" 116 | config = { 117 | "DEFAULT": { 118 | "CMDLINE": cmdline, 119 | "EFI_STUB": "/usr/lib/systemd/mystub.efi", 120 | } 121 | } 122 | base = "verity_squash_root.efi" 123 | with mock.patch("{}.CMDLINE_FILE".format(base), 124 | new=all_mocks.efi.CMDLINE_FILE): 125 | self.assertEqual(get_cmdline(config), cmdline) 126 | self.assertEqual(all_mocks.mock_calls, []) 127 | 128 | def test__get_cmdline__etc_file(self): 129 | all_mocks = mock.Mock() 130 | config = { 131 | "DEFAULT": { 132 | "EFI_STUB": "/usr/lib/systemd/mystub.efi", 133 | } 134 | } 135 | base = "verity_squash_root.efi" 136 | call = mock.call 137 | with mock.patch("{}.CMDLINE_FILE".format(base), 138 | new=all_mocks.efi.CMDLINE_FILE): 139 | all_mocks.efi.CMDLINE_FILE.exists.return_value = True 140 | all_mocks.efi.CMDLINE_FILE.read_text.return_value = \ 141 | " ro param=val\nroot=UUID=x\n kpti=on debugfs=off" 142 | self.assertEqual( 143 | get_cmdline(config), 144 | " ro param=val root=UUID=x kpti=on debugfs=off") 145 | self.assertEqual( 146 | all_mocks.mock_calls, 147 | [call.efi.CMDLINE_FILE.exists(), 148 | call.efi.CMDLINE_FILE.read_text()]) 149 | 150 | def test__build_and_sign_kernel(self): 151 | all_mocks = mock.Mock() 152 | base = "verity_squash_root.efi" 153 | config = { 154 | "DEFAULT": { 155 | "EFI_STUB": "/usr/lib/systemd/mystub.efi", 156 | } 157 | } 158 | call = mock.call 159 | 160 | with (mock.patch("{}.sign".format(base), 161 | new=all_mocks.efi.sign), 162 | mock.patch("{}.get_cmdline".format(base), 163 | new=all_mocks.get_cmdline), 164 | mock.patch("{}.create_efi_executable".format(base), 165 | new=all_mocks.efi.create_efi_executable), 166 | mock.patch("{}.write_str_to".format(base), 167 | new=all_mocks.write_str_to)): 168 | all_mocks.get_cmdline.return_value = "rw encrypt=/dev/sda2 quiet" 169 | build_and_sign_kernel(config, Path("/boot/vmlinuz"), 170 | Path("/tmp/initramfs.img"), "a", 171 | "567myhash234", Path("/tmp/file.efi"), 172 | "tmpfsparam") 173 | self.assertEqual( 174 | all_mocks.mock_calls, 175 | [call.get_cmdline(config), 176 | call.write_str_to(Path("/tmp/verity_squash_root/cmdline"), 177 | ("rw encrypt=/dev/sda2 quiet rw tmpfsparam " 178 | "verity_squash_root_slot=a " 179 | "verity_squash_root_hash=567myhash234")), 180 | call.efi.create_efi_executable( 181 | Path("/usr/lib/systemd/mystub.efi"), 182 | Path("/tmp/verity_squash_root/cmdline"), 183 | Path("/boot/vmlinuz"), Path("/tmp/initramfs.img"), 184 | Path("/tmp/file.efi")), 185 | call.efi.sign(KEY_DIR, Path("/tmp/file.efi"), 186 | Path("/tmp/file.efi"))]) 187 | 188 | all_mocks.reset_mock() 189 | 190 | all_mocks.get_cmdline.return_value = "encrypt=/dev/sda2 quiet" 191 | build_and_sign_kernel(config, Path("/usr/lib/vmlinuz-lts"), 192 | Path("/boot/initramfs_fallback.img"), "b", 193 | "853anotherhash723", 194 | Path("/tmporary/dir/f.efi"), 195 | "") 196 | self.assertEqual( 197 | all_mocks.mock_calls, 198 | [call.get_cmdline(config), 199 | call.write_str_to( 200 | Path("/tmp/verity_squash_root/cmdline"), 201 | ("encrypt=/dev/sda2 quiet rw verity_squash_root_slot=b " 202 | "verity_squash_root_hash=853anotherhash723")), 203 | call.efi.create_efi_executable( 204 | Path("/usr/lib/systemd/mystub.efi"), 205 | Path("/tmp/verity_squash_root/cmdline"), 206 | Path("/usr/lib/vmlinuz-lts"), 207 | Path("/boot/initramfs_fallback.img"), 208 | Path("/tmporary/dir/f.efi")), 209 | call.efi.sign(KEY_DIR, 210 | Path("/tmporary/dir/f.efi"), 211 | Path("/tmporary/dir/f.efi"))]) 212 | -------------------------------------------------------------------------------- /tests/unit/encrypt.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from pathlib import Path 3 | from unittest import mock 4 | from unittest.mock import call 5 | from verity_squash_root.encrypt import create_secure_boot_keys, \ 6 | create_encrypted_tar_file, create_tar_file, \ 7 | check_if_archives_exist, create_and_pack_secure_boot_keys, \ 8 | PUBLIC_KEY_FILES_TAR, SIGNING_FILES_TAR, ALL_FILES_TAR, \ 9 | SIGNING_FILES, PUBLIC_FILES, ALL_FILES 10 | from .test_helper import wrap_tempdir 11 | 12 | 13 | class EncryptTest(unittest.TestCase): 14 | 15 | @mock.patch("verity_squash_root.encrypt.exec_binary") 16 | def test__create_secure_boot_keys(self, mock): 17 | create_secure_boot_keys() 18 | mock.assert_called_once_with( 19 | ["/usr/lib/verity-squash-root/generate_secure_boot_keys"]) 20 | 21 | @mock.patch("verity_squash_root.encrypt.exec_binary") 22 | def test__create_encrypted_tar_file(self, mock): 23 | create_encrypted_tar_file( 24 | ["db.key", "db.crt", "another_file", "/etc/resolv.conf"], 25 | "/etc/targetfile.tar.age") 26 | mock.assert_called_once_with( 27 | ["/usr/lib/verity-squash-root/create_encrypted_tar_file", 28 | "/etc/targetfile.tar.age", 29 | "db.key", "db.crt", "another_file", "/etc/resolv.conf"]) 30 | 31 | def test__create_tar_file(self): 32 | base = "verity_squash_root.encrypt" 33 | all_mocks = mock.MagicMock() 34 | with mock.patch("{}.tarfile".format(base), 35 | new=all_mocks.tarfile): 36 | target = Path("/etc/target/file.tar") 37 | create_tar_file(["file1", "db.key", "pk.crt", "/dev/null"], 38 | target) 39 | self.assertEqual( 40 | all_mocks.mock_calls, 41 | [call.tarfile.open(target, "w"), 42 | call.tarfile.open().__enter__(), 43 | call.tarfile.open().__enter__().add("file1"), 44 | call.tarfile.open().__enter__().add("db.key"), 45 | call.tarfile.open().__enter__().add("pk.crt"), 46 | call.tarfile.open().__enter__().add("/dev/null"), 47 | call.tarfile.open().__exit__(None, None, None)]) 48 | 49 | def test__check_if_archives_exist(self): 50 | base = "verity_squash_root.encrypt" 51 | all_mocks = mock.Mock() 52 | variants = [ 53 | [False, False, False, None], 54 | [True, False, False, PUBLIC_KEY_FILES_TAR, all_mocks.pubkeytar], 55 | [False, True, False, SIGNING_FILES_TAR, all_mocks.signtar], 56 | [False, False, True, ALL_FILES_TAR, all_mocks.alltar]] 57 | for i in variants: 58 | with (mock.patch("{}.PUBLIC_KEY_FILES_TAR".format(base), 59 | new=all_mocks.pubkeytar), 60 | mock.patch("{}.SIGNING_FILES_TAR".format(base), 61 | new=all_mocks.signtar), 62 | mock.patch("{}.ALL_FILES_TAR".format(base), 63 | new=all_mocks.alltar)): 64 | all_mocks.pubkeytar.exists.return_value = i[0] 65 | all_mocks.signtar.exists.return_value = i[1] 66 | all_mocks.alltar.exists.return_value = i[2] 67 | if i[3] is None: 68 | check_if_archives_exist() 69 | else: 70 | with self.assertRaises(ValueError) as e: 71 | check_if_archives_exist() 72 | self.assertEqual( 73 | str(e.exception), 74 | "Archive {} already exists, delete it only if " 75 | "you dont need it anymore".format(i[4])) 76 | 77 | @wrap_tempdir 78 | def test__create_and_pack_secure_boot_keys(self, tempdir): 79 | base = "verity_squash_root.encrypt" 80 | key_dir = tempdir / "keys" 81 | 82 | all_mocks = mock.Mock() 83 | with (mock.patch("{}.create_secure_boot_keys".format(base), 84 | new=all_mocks.create_secure_boot_keys), 85 | mock.patch("{}.create_encrypted_tar_file".format(base), 86 | new=all_mocks.create_encrypted_tar_file), 87 | mock.patch("{}.create_encrypted_tar_file".format(base), 88 | new=all_mocks.create_encrypted_tar_file), 89 | mock.patch("{}.create_tar_file".format(base), 90 | new=all_mocks.create_tar_file), 91 | mock.patch("{}.os".format(base), 92 | new=all_mocks.os), 93 | mock.patch("{}.KEY_DIR".format(base), 94 | new=key_dir)): 95 | 96 | cwd = Path.cwd() 97 | self.assertEqual(key_dir.is_dir(), False) 98 | create_and_pack_secure_boot_keys() 99 | self.assertEqual( 100 | all_mocks.mock_calls, 101 | [call.os.chdir(key_dir), 102 | call.create_secure_boot_keys(), 103 | call.create_encrypted_tar_file(SIGNING_FILES, 104 | SIGNING_FILES_TAR), 105 | call.create_encrypted_tar_file(ALL_FILES, ALL_FILES_TAR), 106 | call.create_tar_file(PUBLIC_FILES, PUBLIC_KEY_FILES_TAR), 107 | call.os.chdir(cwd)]) 108 | self.assertEqual(key_dir.is_dir(), True) 109 | 110 | @wrap_tempdir 111 | def test__create_and_pack_secure_boot_keys__error_cleanup(self, tempdir): 112 | base = "verity_squash_root.encrypt" 113 | key_dir = tempdir / "keys" 114 | 115 | all_mocks = mock.Mock() 116 | with (mock.patch("{}.create_secure_boot_keys".format(base), 117 | new=all_mocks.create_secure_boot_keys), 118 | mock.patch("{}.os".format(base), 119 | new=all_mocks.os), 120 | mock.patch("{}.KEY_DIR".format(base), 121 | new=key_dir)): 122 | 123 | cwd = Path.cwd() 124 | all_mocks.create_secure_boot_keys.side_effect = ValueError("") 125 | with self.assertRaises(ValueError): 126 | create_and_pack_secure_boot_keys() 127 | self.assertEqual( 128 | all_mocks.mock_calls, 129 | [call.os.chdir(key_dir), 130 | call.create_secure_boot_keys(), 131 | call.os.chdir(cwd)]) 132 | -------------------------------------------------------------------------------- /tests/unit/exec.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from verity_squash_root.exec import exec_binary, ExecBinaryError 3 | 4 | 5 | class ExecTest(unittest.TestCase): 6 | 7 | def test__exec_binary__exit_code(self): 8 | exit_3_cmd = ["dash", "-c", "echo err str >&2 ; exit 3"] 9 | self.assertEqual( 10 | exec_binary(exit_3_cmd, 3), 11 | (b'', b'err str\n')) 12 | with self.assertRaises(ExecBinaryError) as e_ctx: 13 | exec_binary(exit_3_cmd) 14 | exception = [exit_3_cmd, (b'', b'err str\n')], 15 | self.assertEqual(e_ctx.exception.args, exception) 16 | 17 | with self.assertRaises(ExecBinaryError) as e_ctx: 18 | exec_binary(exit_3_cmd, 1) 19 | self.assertEqual(e_ctx.exception.args, exception) 20 | self.assertEqual( 21 | str(e_ctx.exception), 22 | "Failed to execute 'dash -c echo err str >&2 " 23 | "; exit 3', error: err str\n") 24 | self.assertEqual(e_ctx.exception.stderr(), 25 | 'err str\n') 26 | 27 | def test__exec_binary__result(self): 28 | result = exec_binary(["dash", "-c", 29 | 'printf StdOutStr\\\\n123;' 30 | 'printf StdErrStr\\\\n456 >&2']) 31 | self.assertEqual(result, 32 | (b'StdOutStr\n123', b'StdErrStr\n456')) 33 | 34 | def test__exec_binary__empty(self): 35 | with self.assertRaises(ChildProcessError) as e_ctx: 36 | exec_binary([]) 37 | self.assertEqual(e_ctx.exception.args, 38 | ("Cannot execute empty cmd",)) 39 | 40 | def test__exec_binary__not_found(self): 41 | with self.assertRaises(ChildProcessError) as e_ctx: 42 | exec_binary(["notexistingbin", "-c", "param"]) 43 | self.assertEqual( 44 | e_ctx.exception.args, 45 | ("Binary not found: notexistingbin, is it installed?",)) 46 | -------------------------------------------------------------------------------- /tests/unit/file_names.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from tests.unit.distributions.base import distribution_mock 3 | from tests.unit.initramfs import create_initramfs_mock 4 | from verity_squash_root.file_names import iterate_kernel_variants, \ 5 | backup_file, backup_label, tmpfs_file, tmpfs_label, \ 6 | iterate_non_ignored_kernel_variants, kernel_is_ignored 7 | 8 | 9 | class FileNamesTest(unittest.TestCase): 10 | 11 | def test__iterate_kernel_variants(self): 12 | distri_mock = distribution_mock() 13 | initramfs_mock = create_initramfs_mock(distri_mock) 14 | variants = list(iterate_kernel_variants(distri_mock, initramfs_mock)) 15 | self.assertEqual( 16 | variants, 17 | [('5.19', 'default', 'linux_default', 'Display Linux (default)'), 18 | ('5.19', 'default', 'linux_default_backup', 19 | 'Display Linux (default) Backup'), 20 | ('5.19', 'default', 'linux_default_tmpfs', 21 | 'Display Linux (default) tmpfs'), 22 | ('5.19', 'default', 'linux_default_tmpfs_backup', 23 | 'Display Linux (default) tmpfs Backup'), 24 | ('5.19', 'fallback', 'linux_fallback', 25 | 'Display Linux (fallback)'), 26 | ('5.19', 'fallback', 'linux_fallback_backup', 27 | 'Display Linux (fallback) Backup'), 28 | ('5.19', 'fallback', 'linux_fallback_tmpfs', 29 | 'Display Linux (fallback) tmpfs'), 30 | ('5.19', 'fallback', 'linux_fallback_tmpfs_backup', 31 | 'Display Linux (fallback) tmpfs Backup'), 32 | ('5.14', 'default', 'linux-lts_default', 33 | 'Display Linux-lts (default)'), 34 | ('5.14', 'default', 'linux-lts_default_backup', 35 | 'Display Linux-lts (default) Backup'), 36 | ('5.14', 'default', 'linux-lts_default_tmpfs', 37 | 'Display Linux-lts (default) tmpfs'), 38 | ('5.14', 'default', 'linux-lts_default_tmpfs_backup', 39 | 'Display Linux-lts (default) tmpfs Backup')]) 40 | 41 | def test__iterate_non_ignored_kernel_variants(self): 42 | distri_mock = distribution_mock() 43 | initramfs_mock = create_initramfs_mock(distri_mock) 44 | ignore = (" linux-lts_default_backup,linux_default_backup," 45 | "linux-lts_default_tmpfs ,linux_fallback") 46 | config = { 47 | "DEFAULT": { 48 | "IGNORE_KERNEL_EFIS": ignore, 49 | } 50 | } 51 | variants = list(iterate_non_ignored_kernel_variants( 52 | config, distri_mock, initramfs_mock)) 53 | self.assertEqual( 54 | variants, 55 | [('5.19', 'default', 'linux_default', 'Display Linux (default)'), 56 | ('5.19', 'default', 'linux_default_tmpfs', 57 | 'Display Linux (default) tmpfs'), 58 | ('5.19', 'default', 'linux_default_tmpfs_backup', 59 | 'Display Linux (default) tmpfs Backup'), 60 | ('5.19', 'fallback', 'linux_fallback_tmpfs', 61 | 'Display Linux (fallback) tmpfs'), 62 | ('5.19', 'fallback', 'linux_fallback_tmpfs_backup', 63 | 'Display Linux (fallback) tmpfs Backup'), 64 | ('5.14', 'default', 'linux-lts_default', 65 | 'Display Linux-lts (default)')]) 66 | 67 | def test__backup_file(self): 68 | self.assertEqual(backup_file("linux-lts"), "linux-lts_backup") 69 | 70 | def test__backup_label(self): 71 | self.assertEqual(backup_label("Linux LTS"), "Linux LTS Backup") 72 | 73 | def test__tmpfs_file(self): 74 | self.assertEqual(tmpfs_file("linux-lts"), "linux-lts_tmpfs") 75 | 76 | def test__tmpfs_label(self): 77 | self.assertEqual(tmpfs_label("Linux LTS"), "Linux LTS tmpfs") 78 | 79 | def test__kernel_is_ignored(self): 80 | ignored = ["linux_fallback_tmpfs", "linux_default_backup", 81 | "linux-lts_tmpfs"] 82 | self.assertEqual(kernel_is_ignored("linux_default", ignored), 83 | False) 84 | self.assertEqual(kernel_is_ignored("linux_default_backup", ignored), 85 | True) 86 | self.assertEqual(kernel_is_ignored("linux-lts_tmpfs_backup", ignored), 87 | True) 88 | -------------------------------------------------------------------------------- /tests/unit/file_op.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from .test_helper import get_test_files_path, wrap_tempdir 3 | from verity_squash_root.file_op import read_text_from, write_str_to, \ 4 | merge_files, read_from 5 | 6 | TEST_FILES_DIR = get_test_files_path("file_op") 7 | READ_TXT = TEST_FILES_DIR / "read.txt" 8 | READ_TXT_CONTENT = "This is a test str.\n" 9 | 10 | 11 | class FileOPTest(unittest.TestCase): 12 | 13 | def test__read_from(self): 14 | result = read_from(READ_TXT) 15 | self.assertEqual(result, bytes(READ_TXT_CONTENT, "utf-8")) 16 | 17 | def test__read_text_from(self): 18 | result = read_text_from(READ_TXT) 19 | self.assertEqual(result, READ_TXT_CONTENT) 20 | 21 | @wrap_tempdir 22 | def test__write_str_to(self, tempdir): 23 | path = tempdir / "testfile" 24 | text = "Some test string\nnext line" 25 | write_str_to(path, text) 26 | self.assertEqual(read_text_from(path), text) 27 | 28 | @wrap_tempdir 29 | def test__merge_files(self, tempdir): 30 | in1 = tempdir / "in1" 31 | in3 = tempdir / "in2" 32 | in1_text = "First part\n" 33 | in3_text = "Third part\n" 34 | out = tempdir / "my_out_file" 35 | write_str_to(in1, in1_text) 36 | write_str_to(in3, in3_text) 37 | merge_files([in1, READ_TXT, in3, READ_TXT], out) 38 | self.assertEqual(read_text_from(out), 39 | "{}{}{}{}".format( 40 | in1_text, READ_TXT_CONTENT, 41 | in3_text, READ_TXT_CONTENT)) 42 | -------------------------------------------------------------------------------- /tests/unit/files/decrypt/keys.tar: -------------------------------------------------------------------------------- 1 | db.crt0000644000000000000000000000002214335025107010645 0ustar rootroot22 DB Cert File 7 2 | db.key0000644000000000000000000000002014335025062010643 0ustar rootrootDB Key File 842 3 | pk.crt0000644000000000000000000000000014335025337010673 0ustar rootrootpk.key0000644000000000000000000000000014335025335010671 0ustar rootroot -------------------------------------------------------------------------------- /tests/unit/files/distributions/arch: -------------------------------------------------------------------------------- 1 | ID=arch 2 | NAME=My Arch 3 | -------------------------------------------------------------------------------- /tests/unit/files/distributions/debian: -------------------------------------------------------------------------------- 1 | ID=debian 2 | NAME=Debian GNU/Linux 3 | -------------------------------------------------------------------------------- /tests/unit/files/distributions/debian-like: -------------------------------------------------------------------------------- 1 | ID=ubuntu 2 | ID_LIKE=debian 3 | NAME=Ubuntu 22.04 4 | -------------------------------------------------------------------------------- /tests/unit/files/distributions/etc-os-release: -------------------------------------------------------------------------------- 1 | NAME="Arch Linux" 2 | ID=arch 3 | HOME_URL="arch website" 4 | LOGO=archlinux-logo 5 | -------------------------------------------------------------------------------- /tests/unit/files/distributions/usr-os-release: -------------------------------------------------------------------------------- 1 | NAME="Debian GNU/Linux 2 | ID=ubuntu 3 | ID_LIKE=debian 4 | HOME_URL="their site" 5 | -------------------------------------------------------------------------------- /tests/unit/files/efi/cmdline: -------------------------------------------------------------------------------- 1 | root=/dev/mapper/root rw verity_squash_root_volatile verity_squash_root_slot=b verity_squash_root_hash=13f2b44462d811853cd2e1c959f4c985686761eb704ee05affc241ea62ed7f85 2 | -------------------------------------------------------------------------------- /tests/unit/files/efi/initrd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brandsimon/verity-squash-root/1a31d0ad8934029b5fd065a9baeb50f5a492e585/tests/unit/files/efi/initrd -------------------------------------------------------------------------------- /tests/unit/files/efi/no_sections_or_info: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brandsimon/verity-squash-root/1a31d0ad8934029b5fd065a9baeb50f5a492e585/tests/unit/files/efi/no_sections_or_info -------------------------------------------------------------------------------- /tests/unit/files/efi/stub_broken.efi: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brandsimon/verity-squash-root/1a31d0ad8934029b5fd065a9baeb50f5a492e585/tests/unit/files/efi/stub_broken.efi -------------------------------------------------------------------------------- /tests/unit/files/efi/stub_empty.efi: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brandsimon/verity-squash-root/1a31d0ad8934029b5fd065a9baeb50f5a492e585/tests/unit/files/efi/stub_empty.efi -------------------------------------------------------------------------------- /tests/unit/files/efi/stub_slot_a.efi: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brandsimon/verity-squash-root/1a31d0ad8934029b5fd065a9baeb50f5a492e585/tests/unit/files/efi/stub_slot_a.efi -------------------------------------------------------------------------------- /tests/unit/files/efi/stub_slot_b.efi: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brandsimon/verity-squash-root/1a31d0ad8934029b5fd065a9baeb50f5a492e585/tests/unit/files/efi/stub_slot_b.efi -------------------------------------------------------------------------------- /tests/unit/files/efi/stub_slot_unkown.efi: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brandsimon/verity-squash-root/1a31d0ad8934029b5fd065a9baeb50f5a492e585/tests/unit/files/efi/stub_slot_unkown.efi -------------------------------------------------------------------------------- /tests/unit/files/efi/vmlinuz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brandsimon/verity-squash-root/1a31d0ad8934029b5fd065a9baeb50f5a492e585/tests/unit/files/efi/vmlinuz -------------------------------------------------------------------------------- /tests/unit/files/file_op/read.txt: -------------------------------------------------------------------------------- 1 | This is a test str. 2 | -------------------------------------------------------------------------------- /tests/unit/files/initramfs/mkinitcpio/linux_name.preset: -------------------------------------------------------------------------------- 1 | # example config 2 | ALL_config="/etc/mkinitcpio.conf" 3 | PRESETS=('default' 'fallback' 'test') 4 | 5 | default_image="/boot/initramfs-linux-test.img" 6 | fallback_image="/boot/initramfs-linux-test-fallback.img" 7 | fallback_options="-S autodetect" 8 | -------------------------------------------------------------------------------- /tests/unit/image.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from pathlib import Path 3 | from unittest import mock 4 | from verity_squash_root.image import mksquashfs, veritysetup_image, \ 5 | verity_image_path 6 | from .test_helper import PROJECT_ROOT 7 | 8 | 9 | class ImageTest(unittest.TestCase): 10 | 11 | @mock.patch("verity_squash_root.image.exec_binary") 12 | def test__veritysetup_image(self, mock): 13 | mock.return_value = (b'Test: 5\nRoot hash: 0x1256890\nLine', b'') 14 | root_hash = veritysetup_image(Path("/myimage.squashfs")) 15 | self.assertEqual(root_hash, "0x1256890") 16 | mock.assert_called_once_with( 17 | ["veritysetup", "format", "/myimage.squashfs", 18 | "/myimage.squashfs.verity"]) 19 | 20 | def test__verity_image_path(self): 21 | self.assertEqual(verity_image_path(Path("/mnt/root/my_img.iso")), 22 | Path("/mnt/root/my_img.iso.verity")) 23 | 24 | @mock.patch("verity_squash_root.image.exec_binary") 25 | def test__mksquashfs(self, mock): 26 | setup = str(PROJECT_ROOT / "setup.py") 27 | mksquashfs(["var/!(lib)", "/home", setup, "/root"], "/image.squashfs", 28 | "/mnt/root/", "/boot/weird/efi") 29 | mock.assert_called_once_with( 30 | ['mksquashfs', '/', '/image.squashfs', 31 | '-reproducible', '-xattrs', '-wildcards', '-noappend', 32 | '-no-exports', 33 | '-p', '/mnt/root/ d 0700 0 0', 34 | '-p', '/boot/weird/efi d 0700 0 0', 35 | '-e', 'dev/*', 'dev/.*', 36 | 'proc/*', 'proc/.*', 'run/*', 'run/.*', 'sys/*', 'sys/.*', 37 | 'tmp/*', 'tmp/.*', 'mnt/root/*', 'mnt/root/.*', 38 | 'boot/weird/efi/*', 'boot/weird/efi/.*', 39 | 'var/!(lib)/*', 'var/!(lib)/.*', 40 | 'home/*', 'home/.*', 41 | setup[1:], 42 | 'root/*', 'root/.*']) 43 | -------------------------------------------------------------------------------- /tests/unit/initramfs/__init__.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from pathlib import Path 3 | from unittest import mock 4 | from verity_squash_root.file_op import write_str_to, read_text_from 5 | from verity_squash_root.initramfs import merge_initramfs_images 6 | from verity_squash_root.initramfs.base import iterate_distribution_efi 7 | from tests.unit.distributions.base import distribution_mock 8 | from tests.unit.test_helper import wrap_tempdir 9 | 10 | 11 | def create_initramfs_mock(distri): 12 | initramfs_mock = mock.Mock() 13 | 14 | def file_name(kernel, preset): 15 | return "{}_{}".format( 16 | distri.kernel_to_name(kernel), preset) 17 | 18 | def display_name(kernel, preset): 19 | return "Display {} ({})".format( 20 | distri.kernel_to_name(kernel).capitalize(), preset) 21 | 22 | def list_kernel_presets(kernel): 23 | obj = {"5.19": ["default", "fallback"], "5.14": ["default"]} 24 | return obj[kernel] 25 | 26 | initramfs_mock.list_kernel_presets.side_effect = list_kernel_presets 27 | initramfs_mock.file_name.side_effect = file_name 28 | initramfs_mock.display_name.side_effect = display_name 29 | return initramfs_mock 30 | 31 | 32 | class InitramfsTest(unittest.TestCase): 33 | 34 | @wrap_tempdir 35 | def test__merge_initramfs_images(self, tempdir): 36 | in1 = tempdir / "some_file" 37 | in2 = tempdir / "another_file" 38 | in3 = tempdir / "filename" 39 | in1_text = "First\npart" 40 | in2_text = "First \n part\n" 41 | in3_text = "Third part" 42 | for i in [(in1, in1_text), 43 | (in2, in2_text), 44 | (in3, in3_text)]: 45 | write_str_to(i[0], i[1]) 46 | 47 | out = tempdir / "merged_file" 48 | merge_initramfs_images(in1, [Path("/not/existing"), in2, 49 | Path("/no/image"), 50 | Path("/still/no/image"), 51 | in3, 52 | Path("another/non/image")], 53 | out) 54 | self.assertEqual(read_text_from(out), 55 | "{}{}{}".format( 56 | in2_text, in3_text, in1_text)) 57 | 58 | def test__iterate_distribution_efi(self): 59 | distri_mock = distribution_mock() 60 | # create separate mock, since otherwise all_mock history will be full 61 | # with initramfs calls to distribution. 62 | initramfs_mock = create_initramfs_mock(distribution_mock()) 63 | all_mock = mock.Mock() 64 | all_mock.distri = distri_mock 65 | all_mock.initramfs = initramfs_mock 66 | result = list(iterate_distribution_efi(distri_mock, initramfs_mock)) 67 | self.assertEqual(result, 68 | [("5.19", "default", "linux_default"), 69 | ("5.19", "fallback", "linux_fallback"), 70 | ("5.14", "default", "linux-lts_default")]) 71 | self.assertEqual(all_mock.mock_calls, 72 | [mock.call.distri.list_kernels(), 73 | mock.call.initramfs.list_kernel_presets("5.19"), 74 | mock.call.initramfs.file_name("5.19", "default"), 75 | mock.call.initramfs.file_name("5.19", "fallback"), 76 | mock.call.initramfs.list_kernel_presets("5.14"), 77 | mock.call.initramfs.file_name("5.14", "default")]) 78 | -------------------------------------------------------------------------------- /tests/unit/initramfs/autodetect.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from unittest import mock 3 | from verity_squash_root.initramfs.autodetect import autodetect_initramfs 4 | 5 | 6 | class InitramfsDetectTest(unittest.TestCase): 7 | 8 | @mock.patch("verity_squash_root.initramfs.autodetect.shutil") 9 | @mock.patch("verity_squash_root.initramfs.autodetect.Mkinitcpio") 10 | def test__autodetect__mkinitcpio(self, mkinitcpio_mock, shutil_mock): 11 | distri = mock.Mock() 12 | shutil_mock.which.return_value = True 13 | result = autodetect_initramfs(distri) 14 | shutil_mock.which.assert_called_once_with("mkinitcpio") 15 | mkinitcpio_mock.assert_called_once_with(distri) 16 | self.assertEqual(result, mkinitcpio_mock()) 17 | 18 | @mock.patch("verity_squash_root.initramfs.autodetect.shutil") 19 | @mock.patch("verity_squash_root.initramfs.autodetect.Dracut") 20 | def test__autodetect__dracut(self, dracut_mock, shutil_mock): 21 | def which(f): 22 | if f == "dracut": 23 | return f 24 | return None 25 | 26 | distri = mock.Mock() 27 | shutil_mock.which.side_effect = which 28 | result = autodetect_initramfs(distri) 29 | self.assertEqual(shutil_mock.which.mock_calls, 30 | [mock.call("mkinitcpio"), 31 | mock.call("dracut")]) 32 | dracut_mock.assert_called_once_with(distri) 33 | self.assertEqual(result, dracut_mock()) 34 | 35 | @mock.patch("verity_squash_root.initramfs.autodetect.shutil") 36 | def test__autodetect__unkown(self, shutil_mock): 37 | shutil_mock.which.return_value = None 38 | with self.assertRaises(ValueError) as e: 39 | autodetect_initramfs(mock.Mock()) 40 | self.assertEqual(str(e.exception), 41 | "No supported initramfs builder found") 42 | -------------------------------------------------------------------------------- /tests/unit/initramfs/dracut.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from unittest.mock import Mock, call, patch 3 | from verity_squash_root.config import TMPDIR 4 | from verity_squash_root.initramfs.dracut import Dracut 5 | 6 | 7 | class DracutTest(unittest.TestCase): 8 | 9 | def test__file_name(self): 10 | distri = Mock() 11 | dracut = Dracut(distri) 12 | res = dracut.file_name("5.17.2", "not used") 13 | self.assertEqual(distri.mock_calls, [call.kernel_to_name("5.17.2")]) 14 | self.assertEqual(res, distri.kernel_to_name()) 15 | 16 | def test__display_name(self): 17 | distri = Mock() 18 | distri.display_name.return_value = "Debian" 19 | distri.kernel_to_name.return_value = "hardened-linux" 20 | dracut = Dracut(distri) 21 | res = dracut.display_name("5.17.2", "random") 22 | self.assertEqual( 23 | distri.mock_calls, 24 | [call.kernel_to_name("5.17.2"), 25 | call.display_name()]) 26 | self.assertEqual(res, "Debian Hardened linux") 27 | 28 | @patch("verity_squash_root.initramfs.dracut.exec_binary") 29 | def test__build_initramfs_with_microcode(self, exec_mock): 30 | distri = Mock() 31 | dracut = Dracut(distri) 32 | result = dracut.build_initramfs_with_microcode("5.17.2", "rr") 33 | self.assertEqual(result, TMPDIR / "5.17.2-rr.image") 34 | self.assertEqual(distri.mock_calls, []) 35 | self.assertEqual( 36 | exec_mock.mock_calls, 37 | [call(["dracut", 38 | "--kver", "5.17.2", 39 | "--no-uefi", 40 | "--early-microcode", 41 | "--add", 42 | "verity-squash-root", 43 | str(TMPDIR / "5.17.2-rr.image")])]) 44 | 45 | def test__list_kernel_presets(self): 46 | distri = Mock() 47 | dracut = Dracut(distri) 48 | self.assertEqual(dracut.list_kernel_presets("5.17.2"), [""]) 49 | -------------------------------------------------------------------------------- /tests/unit/initramfs/mkinitcpio.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from pathlib import Path 3 | from unittest import mock 4 | from verity_squash_root.initramfs.mkinitcpio import Mkinitcpio 5 | from verity_squash_root.exec import exec_binary 6 | from verity_squash_root.config import TMPDIR 7 | from tests.unit.test_helper import PROJECT_ROOT, get_test_files_path 8 | from tests.unit.distributions.base import distribution_mock 9 | 10 | TEST_FILES_DIR = get_test_files_path("initramfs/mkinitcpio") 11 | 12 | 13 | class MkinitcpioTest(unittest.TestCase): 14 | 15 | def test__file_name(self): 16 | mkinitcpio = Mkinitcpio(distribution_mock()) 17 | self.assertEqual(mkinitcpio.file_name("5.19", "press"), "linux_press") 18 | self.assertEqual(mkinitcpio.file_name("5.14", "nom"), "linux-lts_nom") 19 | 20 | def test__display_name(self): 21 | mkinitcpio = Mkinitcpio(distribution_mock()) 22 | self.assertEqual(mkinitcpio.display_name("5.19", "my_preset"), 23 | "Arch Linux (my_preset)") 24 | self.assertEqual(mkinitcpio.display_name("5.14", "set"), 25 | "Arch Linux lts (set)") 26 | 27 | def test__build_initramfs_with_microcode(self): 28 | preset_info = "some info\npreset_image = /test\nsome more info\nx=y\n" 29 | 30 | def read(file): 31 | if file == Path("/etc/mkinitcpio.d/linux-lts.preset"): 32 | return preset_info 33 | raise ValueError(file) 34 | 35 | base = "verity_squash_root.initramfs.mkinitcpio" 36 | all_mocks = mock.Mock() 37 | all_mocks.read_text_from.side_effect = read 38 | with (mock.patch("{}.merge_initramfs_images".format(base), 39 | new=all_mocks.merge_initramfs_images), 40 | mock.patch("{}.exec_binary".format(base), 41 | new=all_mocks.exec_binary), 42 | mock.patch("{}.read_text_from".format(base), 43 | new=all_mocks.read_text_from), 44 | mock.patch("{}.write_str_to".format(base), 45 | new=all_mocks.write_str_to)): 46 | distri_mock = distribution_mock() 47 | mkinitcpio = Mkinitcpio(distri_mock) 48 | res = mkinitcpio.build_initramfs_with_microcode( 49 | "5.14", "x_preset") 50 | base_name = "linux-lts-x_preset" 51 | call = mock.call 52 | self.assertEqual( 53 | all_mocks.mock_calls, 54 | [call.read_text_from( 55 | Path("/etc/mkinitcpio.d/linux-lts.preset")), 56 | call.write_str_to( 57 | TMPDIR / "linux-lts-x_preset.preset", 58 | ("{}\n" 59 | "PRESETS=('x_preset')\n" 60 | "x_preset_image={}/{}.initcpio\n" 61 | "x_preset_options=\"${{x_preset_options}} " 62 | "-A verity-squash-root\"\n" 63 | ).format( 64 | preset_info, TMPDIR, base_name)), 65 | call.exec_binary(["mkinitcpio", "-p", 66 | "{}/{}.preset".format(TMPDIR, base_name)]), 67 | call.merge_initramfs_images(TMPDIR / "{}.initcpio".format( 68 | base_name), 69 | distri_mock.microcode_paths(), 70 | TMPDIR / "{}.image".format( 71 | base_name))]) 72 | self.assertEqual(res, TMPDIR / "{}.image".format(base_name)) 73 | 74 | @mock.patch("verity_squash_root.initramfs.mkinitcpio.exec_binary") 75 | def test__list_kernel_presets(self, exec_mock): 76 | def change_lib_path(cmd): 77 | cmd[0] = str(PROJECT_ROOT / cmd[0].strip('/')) 78 | cmd[1] = "../..{}".format( 79 | TEST_FILES_DIR / "linux_name") 80 | return exec_binary(cmd) 81 | 82 | exec_mock.side_effect = change_lib_path 83 | mkinitcpio = Mkinitcpio(distribution_mock()) 84 | result = mkinitcpio.list_kernel_presets("5.14") 85 | self.assertEqual(result, ["default", "fallback", "test"]) 86 | exec_mock.assert_called_once_with([ 87 | str(PROJECT_ROOT / 88 | 'usr/lib/verity-squash-root/mkinitcpio_list_presets'), 89 | "../..{}".format( 90 | PROJECT_ROOT / 91 | 'tests/unit/files/initramfs/mkinitcpio/linux_name')]) 92 | -------------------------------------------------------------------------------- /tests/unit/main.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from pathlib import Path 3 | from unittest import mock 4 | from unittest.mock import call 5 | from tests.unit.distributions.base import distribution_mock 6 | from tests.unit.initramfs import create_initramfs_mock 7 | from tests.unit.test_helper import wrap_tempdir 8 | from verity_squash_root.config import KEY_DIR 9 | from verity_squash_root.main import move_kernel_to, \ 10 | create_squashfs_return_verity_hash, build_and_move_kernel, \ 11 | create_image_and_sign_kernel, backup_and_sign_efi, \ 12 | backup_and_sign_extra_files, create_directory 13 | 14 | 15 | class MainTest(unittest.TestCase): 16 | 17 | def test__move_kernel_to(self): 18 | base = "verity_squash_root.main" 19 | all_mocks = mock.Mock() 20 | 21 | with (mock.patch("{}.shutil".format(base), 22 | new=all_mocks.shutil), 23 | mock.patch("{}.efi".format(base), 24 | new=all_mocks.efi)): 25 | # Kernel does not exist yet 26 | all_mocks.dest.exists.return_value = False 27 | move_kernel_to(all_mocks.src, 28 | all_mocks.dest, 29 | "a", 30 | all_mocks.backup) 31 | self.assertEqual( 32 | all_mocks.mock_calls, 33 | [call.dest.exists(), 34 | call.shutil.move(all_mocks.src, 35 | all_mocks.dest)]) 36 | 37 | all_mocks.reset_mock() 38 | # Kernel exist, backup not supplied 39 | all_mocks.dest.exists.return_value = True 40 | move_kernel_to(all_mocks.src, 41 | all_mocks.dest, 42 | "a", 43 | None) 44 | self.assertEqual( 45 | list(all_mocks.mock_calls), 46 | [call.dest.exists(), 47 | call.efi.file_matches_slot_or_is_broken( 48 | all_mocks.dest, "a"), 49 | call.dest.unlink(), 50 | call.shutil.move(all_mocks.src, 51 | all_mocks.dest)]) 52 | 53 | all_mocks.reset_mock() 54 | # Kernel exist, slot does not match 55 | all_mocks.dest.exists.return_value = True 56 | all_mocks.efi.file_matches_slot_or_is_broken.return_value = False 57 | move_kernel_to(all_mocks.src, 58 | all_mocks.dest, 59 | "b", 60 | all_mocks.backup) 61 | self.assertEqual( 62 | all_mocks.mock_calls, 63 | [call.dest.exists(), 64 | call.efi.file_matches_slot_or_is_broken(all_mocks.dest, "b"), 65 | call.dest.replace(all_mocks.backup), 66 | call.shutil.move(all_mocks.src, 67 | all_mocks.dest)]) 68 | 69 | all_mocks.reset_mock() 70 | # Kernel exist, slot matches 71 | all_mocks.dest.exists.return_value = True 72 | all_mocks.efi.file_matches_slot_or_is_broken.return_value = True 73 | move_kernel_to(all_mocks.src, 74 | all_mocks.dest, 75 | "a", 76 | all_mocks.backup) 77 | self.assertEqual( 78 | all_mocks.mock_calls, 79 | [call.dest.exists(), 80 | call.efi.file_matches_slot_or_is_broken(all_mocks.dest, "a"), 81 | call.dest.unlink(), 82 | call.shutil.move(all_mocks.src, 83 | all_mocks.dest)]) 84 | 85 | def test__create_squashfs_return_verity_hash(self): 86 | base = "verity_squash_root.main" 87 | all_mocks = mock.Mock() 88 | 89 | config = { 90 | "DEFAULT": { 91 | "ROOT_MOUNT": "/opt/mnt/root", 92 | "EFI_PARTITION": "/boot/second/efi", 93 | "EXCLUDE_DIRS": "var/lib,opt/var", 94 | } 95 | } 96 | 97 | with (mock.patch("{}.veritysetup_image".format(base), 98 | new=all_mocks.veritysetup_image), 99 | mock.patch("{}.mksquashfs".format(base), 100 | new=all_mocks.mksquashfs)): 101 | result = create_squashfs_return_verity_hash( 102 | config, Path("/tmp/test.img")) 103 | self.assertEqual( 104 | all_mocks.mock_calls, 105 | [call.mksquashfs(['var/lib', 'opt/var'], 106 | Path('/tmp/test.img'), 107 | Path('/opt/mnt/root'), 108 | Path('/boot/second/efi')), 109 | call.veritysetup_image( 110 | Path('/tmp/test.img'))]) 111 | self.assertEqual( 112 | result, 113 | all_mocks.veritysetup_image()) 114 | 115 | def test__create_directory(self): 116 | path = mock.Mock() 117 | create_directory(path) 118 | self.assertEqual( 119 | path.mock_calls, 120 | [call.mkdir(parents=True, exist_ok=True)]) 121 | 122 | def test__build_and_move_kernel(self): 123 | base = "verity_squash_root.main" 124 | all_mocks = mock.Mock() 125 | config = mock.Mock() 126 | vmlinuz = mock.Mock() 127 | initramfs = mock.Mock() 128 | use_slot = mock.Mock() 129 | root_hash = mock.Mock() 130 | cmdline_add = mock.Mock() 131 | 132 | with (mock.patch("{}.efi".format(base), 133 | new=all_mocks.efi), 134 | mock.patch("{}.move_kernel_to".format(base), 135 | new=all_mocks.move_kernel_to)): 136 | # No install 137 | build_and_move_kernel( 138 | config, vmlinuz, initramfs, use_slot, root_hash, 139 | cmdline_add, "linux_fallback", Path("/boot/ef/EFI/Debian"), 140 | "Debian Linux", ["linux", "linux_fallback", "linux-lts"]) 141 | self.assertEqual(all_mocks.mock_calls, []) 142 | 143 | # No backup 144 | build_and_move_kernel( 145 | config, vmlinuz, initramfs, use_slot, root_hash, 146 | cmdline_add, "linux_fallback", Path("/boot/ef/EFI/Debian"), 147 | "Debian Linux", ["linux", "linux_fallback_backup", "lts_lin"]) 148 | 149 | self.assertEqual( 150 | all_mocks.mock_calls, 151 | [call.efi.build_and_sign_kernel( 152 | config, vmlinuz, initramfs, use_slot, root_hash, 153 | Path('/tmp/verity_squash_root/tmp.efi'), cmdline_add), 154 | call.move_kernel_to( 155 | Path('/tmp/verity_squash_root/tmp.efi'), 156 | Path('/boot/ef/EFI/Debian/linux_fallback.efi'), 157 | use_slot, None)]) 158 | 159 | all_mocks.reset_mock() 160 | # Backup 161 | build_and_move_kernel( 162 | config, vmlinuz, initramfs, use_slot, root_hash, 163 | cmdline_add, "linux_tmpfs", Path("/boot/efidir/EFI/Debian"), 164 | "Debian Linux 11", ["linux", "linux_tmp", "lts_lin"]) 165 | self.assertEqual( 166 | all_mocks.mock_calls, 167 | [call.efi.build_and_sign_kernel( 168 | config, vmlinuz, initramfs, use_slot, root_hash, 169 | Path('/tmp/verity_squash_root/tmp.efi'), cmdline_add), 170 | call.move_kernel_to( 171 | Path('/tmp/verity_squash_root/tmp.efi'), 172 | Path('/boot/efidir/EFI/Debian/linux_tmpfs.efi'), 173 | use_slot, 174 | Path('/boot/efidir/EFI/Debian/linux_tmpfs_backup.efi'))]) 175 | 176 | def test__create_image_and_sign_kernel(self): 177 | base = "verity_squash_root.main" 178 | all_mocks = mock.Mock() 179 | cmdline = mock.Mock() 180 | use_slot = mock.Mock() 181 | root_hash = mock.Mock() 182 | 183 | def initrdfunc(kernel, preset): 184 | return Path("/path/initramfs_{}_{}.img".format(kernel, preset)) 185 | 186 | config = { 187 | "DEFAULT": { 188 | "ROOT_MOUNT": "/opt/mnt/root", 189 | "EFI_PARTITION": "/boot/efi", 190 | "IGNORE_KERNEL_EFIS": 191 | " linux_default ,linux_default_tmpfs ,linux_fallback_tmpfs" 192 | ",linux_lts_t", 193 | } 194 | } 195 | ignored_efis = ["linux_default", "linux_default_tmpfs", 196 | "linux_fallback_tmpfs", "linux_lts_t"] 197 | with (mock.patch("{}.read_text_from".format(base), 198 | new=all_mocks.read_text_from), 199 | mock.patch("{}.cmdline".format(base), 200 | new=all_mocks.cmdline), 201 | mock.patch("{}.create_directory".format(base), 202 | new=all_mocks.create_directory), 203 | mock.patch("{}.create_squashfs_return_verity_hash".format(base), 204 | new=all_mocks.create_squashfs_return_verity_hash), 205 | mock.patch("{}.build_and_move_kernel".format(base), 206 | new=all_mocks.build_and_move_kernel), 207 | mock.patch("{}.shutil".format(base), 208 | new=all_mocks.shutil), 209 | mock.patch("{}.move_kernel_to".format(base), 210 | new=all_mocks.move_kernel_to)): 211 | distri_mock = distribution_mock() 212 | # create separate mock, since otherwise all_mock history will be 213 | # full # with initramfs calls to distribution. 214 | initramfs_mock = create_initramfs_mock(distribution_mock()) 215 | distri_initramfs_mock = mock.Mock() 216 | distri_initramfs_mock.distri = distri_mock 217 | distri_initramfs_mock.initramfs = initramfs_mock 218 | initramfs_mock.build_initramfs_with_microcode.side_effect = \ 219 | initrdfunc 220 | all_mocks.cmdline.unused_slot.return_value = use_slot 221 | all_mocks.create_squashfs_return_verity_hash.return_value = \ 222 | root_hash 223 | all_mocks.read_text_from.return_value = cmdline 224 | create_image_and_sign_kernel(config, distri_mock, initramfs_mock) 225 | efi_path = Path('/boot/efi/EFI/verity_squash_root/ArchEfi') 226 | self.assertEqual( 227 | all_mocks.mock_calls, 228 | [call.read_text_from(Path('/proc/cmdline')), 229 | call.cmdline.unused_slot(cmdline), 230 | call.create_directory(Path( 231 | "/boot/efi/EFI/verity_squash_root/ArchEfi")), 232 | call.create_squashfs_return_verity_hash( 233 | config, 234 | Path('/tmp/verity_squash_root/tmp.squashfs')), 235 | call.build_and_move_kernel( 236 | config, 237 | Path('/lib64/modules/5.19/vmlinuz'), 238 | Path('/path/initramfs_5.19_fallback.img'), 239 | use_slot, 240 | root_hash, 241 | '', 242 | 'linux_fallback', 243 | efi_path, 244 | 'Display Linux (fallback)', 245 | ignored_efis), 246 | call.build_and_move_kernel( 247 | config, 248 | Path('/lib64/modules/5.19/vmlinuz'), 249 | Path('/path/initramfs_5.19_fallback.img'), 250 | use_slot, 251 | root_hash, 252 | 'verity_squash_root_volatile', 253 | 'linux_fallback_tmpfs', 254 | efi_path, 255 | 'Display Linux (fallback) tmpfs', 256 | ignored_efis), 257 | call.build_and_move_kernel( 258 | config, 259 | Path('/lib64/modules/5.14/vmlinuz'), 260 | Path('/path/initramfs_5.14_default.img'), 261 | use_slot, 262 | root_hash, 263 | '', 264 | 'linux-lts_default', 265 | efi_path, 266 | 'Display Linux-lts (default)', 267 | ignored_efis), 268 | call.build_and_move_kernel( 269 | config, 270 | Path('/lib64/modules/5.14/vmlinuz'), 271 | Path('/path/initramfs_5.14_default.img'), 272 | use_slot, 273 | root_hash, 274 | 'verity_squash_root_volatile', 275 | 'linux-lts_default_tmpfs', 276 | efi_path, 277 | 'Display Linux-lts (default) tmpfs', 278 | ignored_efis), 279 | call.shutil.move( 280 | Path('/tmp/verity_squash_root/tmp.squashfs'), 281 | Path('/opt/mnt/root/image_{}.squashfs'.format(use_slot))), 282 | call.shutil.move( 283 | Path('/tmp/verity_squash_root/tmp.squashfs.verity'), 284 | Path('/opt/mnt/root/image_{}.squashfs.verity'.format( 285 | use_slot)))]) 286 | self.assertEqual( 287 | distri_initramfs_mock.mock_calls, 288 | [call.distri.efi_dirname(), 289 | call.distri.list_kernels(), 290 | call.initramfs.list_kernel_presets('5.19'), 291 | call.initramfs.file_name('5.19', 'default'), 292 | call.distri.vmlinuz('5.19'), 293 | call.initramfs.file_name('5.19', 'default'), 294 | call.initramfs.display_name('5.19', 'default'), 295 | call.initramfs.file_name('5.19', 'fallback'), 296 | call.distri.vmlinuz('5.19'), 297 | call.initramfs.file_name('5.19', 'fallback'), 298 | call.initramfs.display_name('5.19', 'fallback'), 299 | call.initramfs.build_initramfs_with_microcode( 300 | '5.19', 'fallback'), 301 | call.initramfs.list_kernel_presets('5.14'), 302 | call.initramfs.file_name('5.14', 'default'), 303 | call.distri.vmlinuz('5.14'), 304 | call.initramfs.file_name('5.14', 'default'), 305 | call.initramfs.display_name('5.14', 'default'), 306 | call.initramfs.build_initramfs_with_microcode( 307 | '5.14', 'default')]) 308 | 309 | def test__backup_and_sign_efi(self): 310 | base = "verity_squash_root.main" 311 | all_mocks = mock.Mock() 312 | with mock.patch("{}.efi".format(base), 313 | new=all_mocks.efi): 314 | dest = all_mocks.dest 315 | dest.exists.return_value = False 316 | backup_and_sign_efi(all_mocks.source, dest) 317 | self.assertEqual( 318 | all_mocks.mock_calls, 319 | [call.dest.exists(), 320 | call.efi.sign(KEY_DIR, all_mocks.source, dest)]) 321 | 322 | def test__backup_and_sign_efi__backup(self): 323 | base = "verity_squash_root.main" 324 | all_mocks = mock.Mock() 325 | with mock.patch("{}.efi".format(base), 326 | new=all_mocks.efi): 327 | dest = all_mocks.dest 328 | dest.exists.return_value = True 329 | dest.parent = Path("/parent/dir") 330 | dest.stem = "myfile" 331 | dest.suffix = ".efi" 332 | replace = Path("/parent/dir/myfile_backup.efi") 333 | backup_and_sign_efi(all_mocks.source, dest) 334 | self.assertEqual( 335 | all_mocks.mock_calls, 336 | [call.dest.exists(), 337 | call.dest.replace(replace), 338 | call.efi.sign(KEY_DIR, all_mocks.source, dest)]) 339 | 340 | @wrap_tempdir 341 | def test__backup_and_sign_extra_files(self, tempdir): 342 | base = "verity_squash_root.main" 343 | all_mocks = mock.Mock() 344 | systemd_out = Path("{}/EFI/systemd/systemd-bootx64.efi".format( 345 | tempdir)) 346 | fwupd_out = Path("{}/EFI/systemd/systemd-bootx64.efi".format( 347 | tempdir)) 348 | config = { 349 | "EXTRA_SIGN": { 350 | "systemd": "/usr/lib/systemd/boot/efi/systemd-bootx64.efi => " 351 | "{}".format(systemd_out), 352 | "fwupd": "/usr/lib/fwupd/fwupd.efi => {}".format(fwupd_out), 353 | } 354 | } 355 | with mock.patch("{}.backup_and_sign_efi".format(base), 356 | new=all_mocks.backup_and_sign_efi): 357 | backup_and_sign_extra_files(config) 358 | self.assertTrue(systemd_out.resolve().parent.is_dir()) 359 | self.assertTrue(fwupd_out.resolve().parent.is_dir()) 360 | self.assertEqual( 361 | all_mocks.mock_calls, 362 | [call.backup_and_sign_efi( 363 | Path("/usr/lib/systemd/boot/efi/systemd-bootx64.efi"), 364 | systemd_out), 365 | call.backup_and_sign_efi( 366 | Path("/usr/lib/fwupd/fwupd.efi"), 367 | fwupd_out)]) 368 | 369 | def test__backup_and_sign_extra_files__exception(self): 370 | base = "verity_squash_root.main" 371 | all_mocks = mock.Mock() 372 | config = { 373 | "EXTRA_SIGN": { 374 | "systemd": "/usr/lib/systemd/boot/efi/systemd-bootx64.efi => " 375 | " /systemd-bootx64.efi", 376 | "fwupd": "/usr/lib/fwupd/fwupd.efi", 377 | "test": "/x.efi => /y.efi", 378 | } 379 | } 380 | with mock.patch("{}.backup_and_sign_efi".format(base), 381 | new=all_mocks.backup_and_sign_efi): 382 | with self.assertRaises(ValueError) as e: 383 | backup_and_sign_extra_files(config) 384 | self.assertEqual( 385 | str(e.exception), 386 | "extra signing files need to be specified as\n" 387 | "name = SOURCE => DEST") 388 | self.assertEqual( 389 | all_mocks.mock_calls, 390 | [call.backup_and_sign_efi( 391 | Path("/usr/lib/systemd/boot/efi/systemd-bootx64.efi"), 392 | Path("/systemd-bootx64.efi"))]) 393 | -------------------------------------------------------------------------------- /tests/unit/mount.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from unittest import mock 3 | from verity_squash_root.mount import TmpfsMount 4 | 5 | 6 | class MountTest(unittest.TestCase): 7 | 8 | def test__tmpfs_mount(self): 9 | base = "verity_squash_root.mount" 10 | all_mocks = mock.Mock() 11 | all_mocks.path.__str__ = mock.Mock(return_value="/my_directory") 12 | with (mock.patch("{}.exec_binary".format(base), 13 | new=all_mocks.exec_binary), 14 | mock.patch("{}.shutil".format(base), 15 | new=all_mocks.shutil)): 16 | call = mock.call 17 | with TmpfsMount(all_mocks.path): 18 | self.assertEqual( 19 | all_mocks.mock_calls[1:], 20 | [call.path.mkdir(), 21 | call.exec_binary(['mount', '-t', 'tmpfs', '-o', 22 | 'mode=0700,uid=0,gid=0', 'tmpfs', 23 | '/my_directory'])]) 24 | all_mocks.path.__str__.assert_called_once_with() 25 | all_mocks.reset_mock() 26 | self.assertEqual( 27 | all_mocks.mock_calls[1:], 28 | [call.exec_binary(['umount', '-f', '-R', '/my_directory']), 29 | call.shutil.rmtree(all_mocks.path)]) 30 | all_mocks.path.__str__.assert_called_once_with() 31 | -------------------------------------------------------------------------------- /tests/unit/parsing.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from verity_squash_root.parsing import info_to_dict 3 | 4 | 5 | class ParsingTest(unittest.TestCase): 6 | 7 | def test__info_to_dict(self): 8 | text = ("Somekey = Somevalue\n# comment\nRandom line\n" 9 | "Anotherkey = Anothervalue\nMore random lines\n" 10 | "Var = Val\nTest = 1\nVar = Overwritten\n") 11 | result = info_to_dict(text, "=") 12 | self.assertEqual(result, 13 | {"Somekey": "Somevalue", 14 | "Anotherkey": "Anothervalue", 15 | "Test": "1", 16 | "Var": "Overwritten"}) 17 | 18 | self.assertEqual(info_to_dict("Var: val"), 19 | {"Var": "val"}) 20 | -------------------------------------------------------------------------------- /tests/unit/pep_checker.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import unittest 3 | import pycodestyle 4 | from pyflakes.api import checkRecursive 5 | from pyflakes.reporter import Reporter 6 | 7 | CHECK_FILES = ["src/", "tests/", "setup.py"] 8 | 9 | 10 | class Pep8Test(unittest.TestCase): 11 | 12 | def test_pep8(self): 13 | style = pycodestyle.StyleGuide() 14 | check = style.check_files(CHECK_FILES) 15 | self.assertEqual(check.total_errors, 0, 16 | 'PEP8 errors: %d' % check.total_errors) 17 | 18 | def test_pyflakes(self): 19 | rep = Reporter(sys.stdout, sys.stderr) 20 | error_count = checkRecursive(CHECK_FILES, rep) 21 | self.assertEqual(error_count, 0, 22 | 'PyFlakes errors: %d' % error_count) 23 | -------------------------------------------------------------------------------- /tests/unit/setup.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from pathlib import Path 3 | from unittest import mock 4 | from verity_squash_root.config import KEY_DIR 5 | from verity_squash_root.setup import add_uefi_boot_option, \ 6 | add_kernels_to_uefi, setup_systemd_boot 7 | from tests.unit.distributions.base import distribution_mock 8 | from tests.unit.initramfs import create_initramfs_mock 9 | 10 | 11 | class SetupTest(unittest.TestCase): 12 | 13 | @mock.patch("verity_squash_root.setup.exec_binary") 14 | def test__add_uefi_boot_option(self, mock): 15 | add_uefi_boot_option("/dev/sda", 1, "Arch Linux", 16 | Path("/EFI/Arch/linux.efi")) 17 | mock.assert_called_once_with( 18 | ["efibootmgr", "--disk", "/dev/sda", "--part", "1", 19 | "--create", "--label", "Arch Linux", "--loader", 20 | "/EFI/Arch/linux.efi"]) 21 | 22 | @mock.patch("verity_squash_root.setup.add_uefi_boot_option") 23 | def test__add_kernels_to_uefi(self, boot_mock): 24 | distri_mock = distribution_mock() 25 | initramfs_mock = create_initramfs_mock(distri_mock) 26 | ignore = (" linux-lts_default_backup," 27 | "linux-lts_default_tmpfs ,linux_fallback") 28 | config = { 29 | "DEFAULT": { 30 | "IGNORE_KERNEL_EFIS": ignore, 31 | } 32 | } 33 | add_kernels_to_uefi(config, distri_mock, initramfs_mock, "/dev/vda", 3) 34 | efi_path = Path('/EFI/verity_squash_root/ArchEfi') 35 | self.assertEqual( 36 | boot_mock.mock_calls, 37 | [mock.call('/dev/vda', 3, 'Display Linux-lts (default)', 38 | efi_path / 'linux-lts_default.efi'), 39 | mock.call('/dev/vda', 3, 'Display Linux (fallback) tmpfs Backup', 40 | efi_path / 'linux_fallback_tmpfs_backup.efi'), 41 | mock.call('/dev/vda', 3, 'Display Linux (fallback) tmpfs', 42 | efi_path / 'linux_fallback_tmpfs.efi'), 43 | mock.call('/dev/vda', 3, 'Display Linux (default) tmpfs Backup', 44 | efi_path / 'linux_default_tmpfs_backup.efi'), 45 | mock.call('/dev/vda', 3, 'Display Linux (default) tmpfs', 46 | efi_path / 'linux_default_tmpfs.efi'), 47 | mock.call('/dev/vda', 3, 'Display Linux (default) Backup', 48 | efi_path / 'linux_default_backup.efi'), 49 | mock.call('/dev/vda', 3, 'Display Linux (default)', 50 | efi_path / 'linux_default.efi')]) 51 | 52 | @mock.patch("verity_squash_root.setup.exec_binary") 53 | @mock.patch("verity_squash_root.setup.write_str_to") 54 | @mock.patch("verity_squash_root.setup.efi.sign") 55 | def test__setup_systemd_boot(self, sign_mock, write_to_mock, exec_mock): 56 | distri_mock = distribution_mock() 57 | initramfs_mock = create_initramfs_mock(distri_mock) 58 | ignore = (" linux-lts_default_tmpfs ,linux_fallback, " 59 | "linux_default_backup") 60 | config = { 61 | "DEFAULT": { 62 | "IGNORE_KERNEL_EFIS": ignore, 63 | "SECURE_BOOT_KEYS": "/secure/path", 64 | "EFI_PARTITION": "/boot/efi_dir", 65 | } 66 | } 67 | setup_systemd_boot(config, distri_mock, initramfs_mock) 68 | exec_mock.assert_called_once_with(["bootctl", "install"]) 69 | boot_efi = Path("/usr/lib/systemd/boot/efi/systemd-bootx64.efi") 70 | self.assertEqual( 71 | sign_mock.mock_calls, 72 | [mock.call(KEY_DIR, Path(boot_efi), 73 | Path("/boot/efi_dir/EFI/systemd/systemd-bootx64.efi")), 74 | mock.call(KEY_DIR, boot_efi, 75 | Path("/boot/efi_dir/EFI/BOOT/BOOTX64.EFI"))]) 76 | text = "title Display {}\nlinux /EFI/verity_squash_root/ArchEfi/{}\n" 77 | path = Path("/boot/efi_dir/loader/entries") 78 | self.assertEqual( 79 | write_to_mock.mock_calls, 80 | [mock.call(path / "linux_default.conf", 81 | text.format("Linux (default)", "linux_default.efi")), 82 | mock.call(path / "linux_default_tmpfs.conf", 83 | text.format("Linux (default) tmpfs", 84 | "linux_default_tmpfs.efi")), 85 | mock.call(path / "linux_default_tmpfs_backup.conf", 86 | text.format("Linux (default) tmpfs Backup", 87 | "linux_default_tmpfs_backup.efi")), 88 | mock.call(path / "linux_fallback_tmpfs.conf", 89 | text.format("Linux (fallback) tmpfs", 90 | "linux_fallback_tmpfs.efi")), 91 | mock.call(path / "linux_fallback_tmpfs_backup.conf", 92 | text.format("Linux (fallback) tmpfs Backup", 93 | "linux_fallback_tmpfs_backup.efi")), 94 | mock.call(path / "linux-lts_default.conf", 95 | text.format("Linux-lts (default)", 96 | "linux-lts_default.efi")), 97 | mock.call(path / "linux-lts_default_backup.conf", 98 | text.format("Linux-lts (default) Backup", 99 | "linux-lts_default_backup.efi"))]) 100 | -------------------------------------------------------------------------------- /tests/unit/test_helper.py: -------------------------------------------------------------------------------- 1 | import shutil 2 | import tempfile 3 | from pathlib import Path 4 | 5 | PROJECT_ROOT = Path(__file__).resolve().parent.parent.parent 6 | 7 | 8 | def get_test_files_path(extra: str) -> Path: 9 | return Path(__file__).resolve().parent / "files" / extra 10 | 11 | 12 | def wrap_tempdir(func): 13 | def f(*args, **kwargs): 14 | tempdir = Path(tempfile.mkdtemp()) 15 | try: 16 | return func(*args, **kwargs, tempdir=tempdir) 17 | finally: 18 | shutil.rmtree(tempdir) 19 | 20 | return f 21 | -------------------------------------------------------------------------------- /tests/unit/tests.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from tests.unit.cmdline import CmdlineTest 3 | from tests.unit.config import ConfigTest 4 | from tests.unit.decrypt import DecryptTest 5 | from tests.unit.distributions.arch import ArchLinuxConfigTest 6 | from tests.unit.distributions.autodetect import DistributionDetectTest 7 | from tests.unit.distributions.base import BaseDistributionTest 8 | from tests.unit.distributions.debian import DebianConfigTest 9 | from tests.unit.efi import EfiTest 10 | from tests.unit.encrypt import EncryptTest 11 | from tests.unit.exec import ExecTest 12 | from tests.unit.file_names import FileNamesTest 13 | from tests.unit.file_op import FileOPTest 14 | from tests.unit.image import ImageTest 15 | from tests.unit.initramfs import InitramfsTest 16 | from tests.unit.initramfs.autodetect import InitramfsDetectTest 17 | from tests.unit.initramfs.dracut import DracutTest 18 | from tests.unit.initramfs.mkinitcpio import MkinitcpioTest 19 | from tests.unit.main import MainTest 20 | from tests.unit.mount import MountTest 21 | from tests.unit.parsing import ParsingTest 22 | from tests.unit.pep_checker import Pep8Test 23 | from tests.unit.setup import SetupTest 24 | 25 | 26 | def test_suite(): 27 | suite = unittest.TestSuite([ 28 | unittest.makeSuite(ArchLinuxConfigTest), 29 | unittest.makeSuite(BaseDistributionTest), 30 | unittest.makeSuite(CmdlineTest), 31 | unittest.makeSuite(ConfigTest), 32 | unittest.makeSuite(DebianConfigTest), 33 | unittest.makeSuite(DecryptTest), 34 | unittest.makeSuite(DistributionDetectTest), 35 | unittest.makeSuite(DracutTest), 36 | unittest.makeSuite(EfiTest), 37 | unittest.makeSuite(EncryptTest), 38 | unittest.makeSuite(ExecTest), 39 | unittest.makeSuite(FileNamesTest), 40 | unittest.makeSuite(FileOPTest), 41 | unittest.makeSuite(ImageTest), 42 | unittest.makeSuite(InitramfsDetectTest), 43 | unittest.makeSuite(InitramfsTest), 44 | unittest.makeSuite(MainTest), 45 | unittest.makeSuite(MkinitcpioTest), 46 | unittest.makeSuite(MountTest), 47 | unittest.makeSuite(ParsingTest), 48 | unittest.makeSuite(Pep8Test), 49 | unittest.makeSuite(SetupTest), 50 | ]) 51 | return suite 52 | -------------------------------------------------------------------------------- /usr/lib/dracut/modules.d/99verity-squash-root/cryptsetup_overlay.conf: -------------------------------------------------------------------------------- 1 | [Unit] 2 | After=verity-squash-root-notify.service 3 | Wants=verity-squash-root-notify.service 4 | -------------------------------------------------------------------------------- /usr/lib/dracut/modules.d/99verity-squash-root/dracut_mount_overlay.conf: -------------------------------------------------------------------------------- 1 | [Unit] 2 | # Reset ALL conditions 3 | ConditionDirectoryNotEmpty= 4 | After=verity-squash-root-notify.service 5 | Wants=verity-squash-root-notify.service 6 | 7 | [Service] 8 | StandardInput=tty 9 | StandardOutput=tty 10 | TimeoutStartSec=infinity 11 | ExecStartPre=/usr/bin/sh /usr/lib/verity-squash-root/mount_handler_dracut 12 | -------------------------------------------------------------------------------- /usr/lib/dracut/modules.d/99verity-squash-root/module-setup.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | date_or_override() { 4 | # allow override for reproducible builds 5 | if [ "${DATE_OVERRIDE}" != "" ]; then 6 | printf "%s\\n" "${DATE_OVERRIDE}" 7 | else 8 | date "+%Y-%m-%d %H:%M:%S %Z" 9 | fi 10 | } 11 | 12 | warn_security() { 13 | # shellcheck disable=SC2154 14 | if [ "${hostonly}" = "" ]; then 15 | dwarning "" 16 | dwarning "##################################################" 17 | dwarning "verity-squash-root: not using hostonly will allow" 18 | dwarning " login to the emergency console" 19 | dwarning "##################################################" 20 | dwarning "" 21 | fi 22 | } 23 | 24 | check() { 25 | warn_security 26 | return 255 27 | } 28 | 29 | cmdline() { 30 | echo "verity_squash_root_slot" 31 | echo "verity_squash_root_hash" 32 | echo "verity_squash_root_volatile" 33 | } 34 | 35 | depends() { 36 | echo "dracut-systemd" 37 | echo "systemd-initrd" 38 | return 0 39 | } 40 | 41 | installkernel() { 42 | hostonly="" instmods dm_mod dm_verity loop overlay squashfs 43 | # install all filesystems, since on debian the lower fs is 44 | # not detected 45 | hostonly="" instmods "=fs" 46 | } 47 | 48 | install() { 49 | warn_security 50 | # shellcheck disable=SC2154 51 | date_or_override > "${initdir}/VERITY_SQUASH_ROOT_DATE" 52 | # create mount points 53 | mkdir -p "${initdir}"/moved_root 54 | mkdir -p "${initdir}"/overlayroot 55 | mkdir -p "${initdir}"/verity-squash-root-tmp/squashroot 56 | mkdir -p "${initdir}"/verity-squash-root-tmp/tmpfs 57 | # shellcheck disable=SC2154 58 | local ssud="${systemdsystemunitdir}" 59 | # shellcheck disable=SC2154 60 | local mod="${moddir}" 61 | inst "${mod}/dracut_mount_overlay.conf" \ 62 | "${ssud}/dracut-mount.service.d/verity-squash-root.conf" 63 | inst "${mod}/cryptsetup_overlay.conf" \ 64 | "${ssud}/systemd-cryptsetup@.service.d/verity-squash-root.conf" 65 | inst /usr/lib/systemd/system/verity-squash-root-notify.service 66 | 67 | inst /usr/lib/verity-squash-root/functions 68 | inst /usr/lib/verity-squash-root/mount_handler 69 | inst /usr/lib/verity-squash-root/mount_handler_dracut 70 | inst /usr/lib/verity-squash-root/show_boot_info 71 | DRACUT_RESOLVE_DEPS=1 inst_multiple mount umount sed sleep veritysetup 72 | 73 | # needed for veritysetup 74 | inst_binary dmeventd 75 | inst_binary dmsetup 76 | inst_rules 55-dm.rules 77 | inst_rules 95-dm-notify.rules 78 | } 79 | -------------------------------------------------------------------------------- /usr/lib/initcpio/install/verity-squash-root: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | date_or_override() { 4 | # allow override for reproducible builds 5 | if [ "${DATE_OVERRIDE}" != "" ]; then 6 | printf "%s\\n" "${DATE_OVERRIDE}" 7 | else 8 | date "+%Y-%m-%d %H:%M:%S %Z" 9 | fi 10 | } 11 | 12 | build() { 13 | add_binary sed 14 | add_binary sleep 15 | add_binary mount 16 | add_binary veritysetup 17 | add_dir /overlayroot 18 | add_dir /verity-squash-root-tmp/squashroot 19 | add_dir /verity-squash-root-tmp/tmpfs 20 | add_file "/usr/lib/verity-squash-root/mount_handler" 21 | add_file "/usr/lib/verity-squash-root/functions" 22 | add_file "/usr/lib/verity-squash-root/show_boot_info" 23 | date_or_override > "${BUILDROOT}/VERITY_SQUASH_ROOT_DATE" 24 | 25 | add_module dm_mod 26 | add_module dm_verity 27 | add_module loop 28 | add_module overlay 29 | add_module squashfs 30 | 31 | if type add_systemd_unit &> /dev/null; then 32 | add_systemd_unit verity-squash-root-notify.service 33 | cat < guid.txt 7 | UUID="$(cat guid.txt)" 8 | create_key() { 9 | openssl req -newkey rsa:4096 -nodes -keyout "${1}.key" -new -x509 \ 10 | -sha256 -days 3650 -subj "/CN=${2}/" -out "${1}.crt" 11 | fingerprint "${1}.crt" > "${1}.hash" 12 | openssl x509 -outform DER -in "${1}.crt" -out "${1}.cer" 13 | cert-to-efi-sig-list -g "${UUID}" "${1}.crt" "${1}.esl" 14 | } 15 | 16 | create_key pk "Platform Key" 17 | sign-efi-sig-list -g "${UUID}" -k pk.key -c pk.crt pk pk.esl pk.auth 18 | sign-efi-sig-list -g "${UUID}" -c pk.crt -k pk.key pk /dev/null rm_pk.auth 19 | 20 | create_key kek "Key Exchange Key" 21 | sign-efi-sig-list -g "${UUID}" -k pk.key -c pk.crt kek kek.esl kek.auth 22 | 23 | create_key db "Signature Database Key" 24 | sign-efi-sig-list -g "${UUID}" -k kek.key -c kek.crt db db.esl db.auth 25 | -------------------------------------------------------------------------------- /usr/lib/verity-squash-root/mkinitcpio_list_presets: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | die() { 3 | # shellcheck disable=SC2059 4 | printf "${@}" 5 | exit 1 6 | } 7 | preset_name="${1}" 8 | _d_presets=/etc/mkinitcpio.d 9 | printf -v preset '%s/%s.preset' "${_d_presets}" "${preset_name}" 10 | # Files can be executed, since they will be executed by 11 | # mkinitcpio anyway 12 | # shellcheck disable=SC1090 13 | . "${preset}" || die "Failed to load preset: %s" "${preset}" 14 | (( ! ${#PRESETS[@]} )) && die \ 15 | "Preset file \`%s' is empty or does not contain any presets." "${preset}" 16 | for p in "${PRESETS[@]}"; do 17 | printf "%s\n" "${p}" 18 | done 19 | -------------------------------------------------------------------------------- /usr/lib/verity-squash-root/mount_handler: -------------------------------------------------------------------------------- 1 | #!/usr/bin/sh 2 | set -e 3 | . /usr/lib/verity-squash-root/functions 4 | SLOT="$(get_kparam "${KP_NAME}_slot")" 5 | ROOTHASH="$(get_kparam "${KP_NAME}_hash")" 6 | VOLATILE="$(get_kparam_set "${KP_NAME}_volatile")" 7 | ROOT="${1}" 8 | DEST="${2}" 9 | TMP="/verity-squash-root-tmp" 10 | if [ "${VOLATILE}" = "${KP_NAME}_volatile" ]; then 11 | OLROOT="${TMP}/tmpfs" 12 | mount -t tmpfs tmpfs "${OLROOT}" 13 | else 14 | OLROOT="${ROOT}" 15 | fi 16 | 17 | # workdir needs to be an empty directory, otherwise there can be file corruption 18 | rm -rf "${OLROOT}/workdir" 19 | mkdir -p "${OLROOT}/overlay" "${OLROOT}/workdir" 20 | IMAGE="${ROOT}/image_${SLOT}.squashfs" 21 | veritysetup open "${IMAGE}" rootsq "${IMAGE}.verity" "${ROOTHASH}" 22 | mount -o ro "/dev/mapper/rootsq" "${TMP}/squashroot" 23 | # Disable xino, index and metacopy, so the underlying filesystem can be updated 24 | # Metacopy also has security problems on untrusted upper (See kernel overlayfs) 25 | mount \ 26 | -t overlay overlay \ 27 | -o lowerdir="${TMP}/squashroot" \ 28 | -o upperdir="${OLROOT}/overlay" \ 29 | -o workdir="${OLROOT}/workdir" \ 30 | -o index=off,metacopy=off,xino=off \ 31 | "${DEST}" 32 | -------------------------------------------------------------------------------- /usr/lib/verity-squash-root/mount_handler_dracut: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | mount --bind "${NEWROOT}" "/moved_root" 3 | umount "${NEWROOT}" 4 | sh /usr/lib/verity-squash-root/mount_handler "/moved_root" "${NEWROOT}" 5 | -------------------------------------------------------------------------------- /usr/lib/verity-squash-root/show_boot_info: -------------------------------------------------------------------------------- 1 | #!/usr/bin/sh 2 | . /usr/lib/verity-squash-root/functions 3 | set -e 4 | SLOT="$(get_kparam "${KP_NAME}_slot")" 5 | VOLATILE="$(get_kparam_set "${KP_NAME}_volatile")" 6 | if [ "${VOLATILE}" = "${KP_NAME}_volatile" ]; then 7 | # Give systemd time to print messages, this results in a cleaner output 8 | sleep 1 || true 9 | # Print date so rollback attacks can be detected 10 | printf "You are booting a tmpfs overlay (slot %s) built at " "${SLOT}" 11 | cat "/VERITY_SQUASH_ROOT_DATE" 12 | printf "Press enter to continue\\n" 13 | read -r _ 14 | fi 15 | -------------------------------------------------------------------------------- /usr/share/bash-completion/completions/verity-squash-root: -------------------------------------------------------------------------------- 1 | #!/usr/bin/bash 2 | _VERITY_SQUASH_ROOT_FIRST="create-keys list check build setup 3 | sign-extra-files --verbose --ignore-warnings" 4 | 5 | _verity_sq_root_reply() { 6 | mapfile -t COMPREPLY < <(compgen -W "${1}" -- "${2}") 7 | } 8 | 9 | _verity_squash_root_completion() { 10 | local i cur devices prev start 11 | cur="${COMP_WORDS[COMP_CWORD]}" 12 | prev="${COMP_WORDS[COMP_CWORD - 1]}" 13 | start="$((COMP_CWORD))" 14 | for i in $(seq 1 "${start}"); do 15 | case "${COMP_WORDS[i]}" in 16 | --verbose|--ignore-warnings) 17 | start="$((start - 1))" 18 | ;; 19 | *) 20 | break 21 | ;; 22 | esac 23 | done 24 | 25 | case "${start}" in 26 | "1") 27 | _verity_sq_root_reply "${_VERITY_SQUASH_ROOT_FIRST}" "${cur}" 28 | ;; 29 | "2") 30 | case "${prev}" in 31 | "setup") 32 | _verity_sq_root_reply "systemd uefi" "${cur}" 33 | ;; 34 | esac 35 | ;; 36 | "3") 37 | case "${prev}" in 38 | "uefi") 39 | mapfile -t devices < <(lsblk -pnro name) 40 | _verity_sq_root_reply "${devices[*]%%[0-9]*}" "${cur}" 41 | ;; 42 | esac 43 | ;; 44 | "4") 45 | case "${COMP_WORDS[COMP_CWORD-2]}" in 46 | "uefi") 47 | mapfile -t devices < <( 48 | lsblk -pnro name | grep "^${prev}") 49 | _verity_sq_root_reply \ 50 | "${devices[*]##"${prev}"}" "${cur}" 51 | ;; 52 | esac 53 | esac 54 | } 55 | 56 | complete -F _verity_squash_root_completion verity-squash-root 57 | --------------------------------------------------------------------------------