Metadata-Version: 2.4
Name: cvmutils
Version: 0.3.0
Summary: Tools for confidential virtual machines
Author: Vitaly Kuznetsov
Author-email: vkuznets@redhat.com
License: LGPL-2.1-or-later
Project-URL: GitLab, https://gitlab.com/vkuznets/cvmutils/
Keywords: CVM
Description-Content-Type: text/markdown
License-File: COPYING
Requires-Dist: setuptools; python_version < "3.13"
Dynamic: license-file

# cvmutils -- Prepare OS Images for Confidential Environments

## Overview

**cvmutils** is a toolkit for preparing Linux OS images to run in confidential virtual machine (CVM) environments. It provides two CLI tools:

*   **`cvm-encrypt-image`** -- Pre-encrypts the root volume and seals the encryption key to a target vTPM using predicted PCR values. Also supports creating immutable, integrity-protected images with dm-verity.
*   **`cvm-reseal`** (experimental) -- Re-seals LUKS volume keys on a running system when PCR measurements change (e.g., after a kernel or UKI update).

Previously named `encrypt-rhel-image`, the toolkit was originally targeted at Fedora/RHEL but now supports multiple other distributions.

### Supported Distributions

*   RHEL 9.4+, RHEL 10.0+
*   Fedora 41+
*   Azure Linux 3
*   CentOS Stream 9, CentOS Stream 10

Other Linux distributions using UKI boot scheme with GPT partitioning should also work.

### Image Formats

`cvm-encrypt-image` accepts image files in the following formats:

*   VHD (`.vhd`)
*   QCOW2 (`.qcow2`)
*   Raw (`.raw`)
*   Block devices directly (e.g., `/dev/nbd0`)

Image files are attached via `qemu-nbd`; block devices are used as-is.

## Installation

### From source (pip)

```bash
pip install .
```

or via the Makefile:

```bash
make install
```

To uninstall:

```bash
make uninstall
```

### RPM packages

Three RPM subpackages are provided:

| Package | Contents |
|---------|----------|
| `python3-cvmutils` | `cvm-encrypt-image` CLI, Python library, man page, utility scripts |
| `cvmutils-reseal` (experimental) | `cvm-reseal` CLI |
| `cvmutils-dracut` (experimental) | Dracut module, `cvmutils-rquote`, systemd service |

To build RPMs locally:

```bash
rm -rf dist
make -f .copr/Makefile srpm outdir=. spec=.distro/python-cvmutils.spec
mock python-cvmutils-0.3.0-1.fc43.src.rpm
```

### Container

See [Running in a Container](#running-in-a-container) below.

## Image Prerequisites

*   **Partition Table:** GUID Partition Table (GPT).
*   **ESP Partition:** Image must contain an ESP partition with GUID `C12A7328-F81F-11D2-BA4B-00A0C93EC93B`.
*   **Root Partition:** Image must contain a "Linux root (x86-64)" partition with GUID `4F68BCE3-E8CD-4DB1-96E7-FBCAF984B709`.
*   **LUKS Conversion:** The root partition must be convertible to LUKS. To accommodate the LUKS header, the tool supports:
    *   Resizing the root partition (requires available space after root).
    *   Shrinking an `ext4` filesystem.
*   **PCR7:** The expected value can be specified explicitly (`--pcr7 <sha256value>`) or predicted (`--pcr7 auto`) using one of the following profiles:
    *   UEFI profile (`--uefi-profile`)
    *   Azure Disk Profile (`--az-disk-profile`)
    *   EFI vars profile (`--efivars-profile`)
        > Example templates are present in `tests/test-data`.
*   **PCR4:** Can optionally be included in the set of PCRs for root volume key sealing. Its value can be specified directly (`--pcr4 <sha256value>`) or predicted (`--pcr4 auto`).

## How It Works

Image preparation is a two-phase workflow:

1.  **Encrypt** -- The root partition is converted to LUKS2 in-place using `cryptsetup reencrypt --encrypt`. A temporary cleartext password (derived from the root partition UUID) is used during this phase.

2.  **Deploy** -- The ESP is mounted and boot chains are discovered from `BOOTX64.CSV` (UTF-16LE file enumerating the bootloader and UKI paths). For each boot chain, the expected PCR4 and/or PCR7 values are predicted by:
    *   Parsing the Secure Boot configuration (PK, KEK, db, dbx) from a UEFI profile, Azure disk profile, or efivars
    *   Computing Authenticode hashes of PE binaries in the boot chain (shim, bootloader, UKI, addons)
    *   Simulating TPM PCR extend operations

    The root volume key is then sealed to the target vTPM via `systemd-cryptenroll` with the predicted PCR policy. The temporary cleartext password is removed.

An optional third mode, **makeverity**, creates a dm-verity partition and a signed UKI addon containing the root hash instead of encrypting.

After deployment, the **reseal** tool (`cvm-reseal`) can be used on a running system to update TPM2 tokens when the boot chain changes.

## TPM2

The SRK (Storage Root Key) public key is required for the deploy phase. It can be obtained from the target vTPM with:

```bash
systemd-analyze srk > public.srk
```

or

```bash
tpm2_readpublic -c 0x81000001 -o public.srk -t primary.handle
```

## Usage: cvm-encrypt-image

### Actions

| Action | Description |
|--------|-------------|
| `encrypt` | Encrypt the root partition (LUKS2 conversion) |
| `encrypt-resume` | Resume an interrupted encryption |
| `deploy` | Seal the encryption key to a target vTPM |
| `makeverity` (experimental) | Create a dm-verity partition and signed UKI addon |

### Basic Example

```bash
cvm-encrypt-image encrypt /path/to/image.vhd
cvm-encrypt-image deploy \
    -s /path/to/public.srk \
    --pcr7 auto \
    --uefi-profile uefi_profile.json \
    /path/to/image.vhd
```

### Common Options

| Option | Description |
|--------|-------------|
| `-n`, `--nbddev N` | NBD device number (default: 0) |
| `-v`, `--verbose` | Print additional info |
| `--encryption-password` | Use the specified temporary password for LUKS encryption instead of a randomly generated one |


### Encrypt Options

| Option | Description |
|--------|-------------|
| `-g`, `--growpart` | Grow root partition to the size of the volume before encryption |
| `--no-cloud-init` | Do not create `/cc_growpart_keydata` and LUKS keyslot for root volume resize |
| `--encrypt-progress-json FILE` | Write encryption progress to a file or `-` for stdout |

### Deploy Options

| Option | Description |
|--------|-------------|
| `-s`, `--srkpub FILE` | SRK public key (required) |
| `--pcr7 VALUE` | Expected PCR7 sha256 value, or `auto` for prediction |
| `--pcr4 VALUE` | Expected PCR4 sha256 value, or `auto` for prediction |
| `--uefi-profile FILE` | UEFI profile JSON for `--pcr7 auto` (mutually exclusive with other profile options) |
| `--az-disk-profile FILE` | Azure disk profile JSON for `--pcr7 auto` (mutually exclusive with other profile options) |
| `--efivars-profile DIR` | Efivars-format directory for `--pcr7 auto` (mutually exclusive with other profile options) |
| `--efivars-profile-no-attrs` | Efivars files lack the 4-byte attribute header (`--efivars-profile` only) |
| `--nosecureboot` | Predict PCRs with Secure Boot disabled |
| `-r`, `--recovery-key FILE` | Add a recovery key passphrase to the root volume |
| `--recovery-key-type {binary,text,both}` | Recovery key format (default: `binary`) |

### Makeverity Options (Experimental)

| Option | Description |
|--------|-------------|
| `--secureboot-cert FILE` | SecureBoot certificate (PEM) to sign UKI addon (required) |
| `--secureboot-key FILE` | SecureBoot key (PEM) to sign UKI addon (required) |
| `--volatile-overlay` | Add `systemd.volatile=overlay` for read-write overlay |

**Example:**

```bash
cvm-encrypt-image makeverity \
    --secureboot-key=custom_db.key \
    --secureboot-cert=custom_db.pem \
    --volatile-overlay vol.raw
```

The signed root hash is placed on the ESP as `/loader/addons/roothash.addon.efi`.

## Usage: cvm-reseal

`cvm-reseal` runs on a live system to update TPM2 tokens when the boot chain or Secure Boot configuration changes (e.g., after a kernel update that installs a new UKI).

It reads the current Secure Boot state from `/sys/firmware/efi/efivars/`, discovers boot chains from the ESP, and compares predicted PCR policies against existing LUKS tokens. New tokens are added and stale tokens can be removed.

### Basic Example

```bash
cvm-reseal
```

Dry-run to see what would change:

```bash
cvm-reseal --dry-run
```

### Options

| Option | Description |
|--------|-------------|
| `-d`, `--dry-run` | List volumes needing re-seal without making changes |
| `-p`, `--pcrs PCRS` | PCRs to seal against, e.g., `4,7` or `4+7` (default: `auto`, reuses existing scheme) |
| `-r`, `--remove {none,matching,all}` | Remove unused tokens: `none` (default), `matching` PCR set, or `all` |
| `-u`, `--unlock {tpm,key-file}` | How to unlock LUKS for re-enrollment (default: `tpm`) |
| `--unlock-keyfile FILE` | Key file for `--unlock key-file` |
| `--tpm-srk-pub FILE` | TPM SRK public key (default: `/run/systemd/tpm2-srk-public-key.tpm2b_public`) |
| `-v`, `--verbose` | Print additional info |

## Running in a Container

Three Containerfiles are provided:

| Containerfile | Base Image | Supported Actions |
|---------------|------------|-------------------|
| `Containerfile.fedora` | Fedora Rawhide | encrypt, deploy, makeverity |
| `Containerfile.c10s` | CentOS Stream 10 | encrypt, deploy, makeverity |
| `Containerfile.ubi10` | UBI 10 | deploy only (no `e2fsprogs`, `veritysetup`, or `ukify`) |

**Example** (assuming data is in `/data` on the host):

```bash
docker build -f Containerfile.fedora -t cvmutils-fedora .
modprobe nbd max_part=8
qemu-nbd -c /dev/nbd0 vol.raw
docker run -it --privileged \
    --mount type=bind,source=/data,target=/data \
    --mount type=bind,source=/run/udev,target=/run/udev \
    --mount type=bind,source=/dev,target=/dev \
    cvmutils-fedora:latest \
    cvm-encrypt-image encrypt -v /dev/nbd0
docker run -it --privileged \
    --mount type=bind,source=/data,target=/data \
    --mount type=bind,source=/run/udev,target=/run/udev \
    --mount type=bind,source=/dev,target=/dev \
    cvmutils-fedora:latest \
    cvm-encrypt-image deploy \
        -s /data/public.srk \
        --pcr7 auto \
        --uefi-profile /data/uefi-profile.json \
        -v /dev/nbd0
qemu-nbd --disconnect /dev/nbd0
```

## Utility Scripts

The `scripts/` directory contains helper tools:

*   **`tpm_eventlog_to_uefi_profile.py`** -- Converts a TPM2 event log (YAML format) to a UEFI profile JSON suitable for `--uefi-profile`. Requires `PyYAML`.

    ```bash
    python3 scripts/tpm_eventlog_to_uefi_profile.py /path/to/eventlog.yaml -o uefi_profile.json
    ```

*   **`print-recovery-key.py`** -- Converts a binary recovery key file to a human-readable dash-separated numeric format.

    ```bash
    python3 scripts/print-recovery-key.py /path/to/recovery.key
    ```

## Dracut Module

The `cvmutils-dracut` RPM package installs a dracut module (`99cvmutils`) for use inside Azure CVM guests during initrd. It includes:

*   **`cvmutils-rquote`** -- A shell script that reads Azure attestation data from TPM2 NV indices, generates a nonce, and obtains a TPM2 quote. The results are stored under `/run/cvmutils/`.
*   **`cvmutils-initrd.service`** -- A systemd oneshot service that runs `cvmutils-rquote` after `tpm2.target` and before `cryptsetup.target`. Only activates when `ConditionSecurity=measured-uki` is met.

The dracut module depends on the `tpm2-tss` dracut module.

## Dependencies

**Basic:**
*   python3.x
*   setuptools (for Python < 3.13)

**Encrypt phase:**
*   qemu-nbd (for image files; not needed for block devices)
*   e2fsprogs (`e2fsck`, `resize2fs`)
*   cryptsetup
*   util-linux (`blkid`, `sfdisk`)

**Deploy phase additionally requires:**
*   openssl
*   systemd-cryptenroll >= 255

**Makeverity phase additionally requires:**
*   veritysetup
*   ukify

**Reseal (`cvm-reseal`) requires:**
*   cryptsetup
*   systemd-cryptenroll
*   bootctl (to locate ESP)
*   lsblk

**Optional (scripts):**
*   PyYAML (for `scripts/tpm_eventlog_to_uefi_profile.py`)

## Testing

The tool comes with unit tests which can be executed with `pytest`. The `test-data` submodule must be checked out first.

```bash
pip install pytest
git submodule update --init
pytest
```

Or via the Makefile:

```bash
make test
```

Out of tree end-to-end tests are available at [https://gitlab.com/vkuznets/cvmutils-tests-e2e](https://gitlab.com/vkuznets/cvmutils-tests-e2e).

## Development

### Prerequisites

Install the development tools as needed:

```bash
pip install pylint build twine pytest
```

Additional tools required by specific targets: `help2man` (man), `pyp2rpm`, `rpmbuild`, and `createrepo` (rpm).

### Makefile Targets

| Target | Description |
|--------|-------------|
| `make lint`, `make pylint` | Run pylint on the `cvmutils` package |
| `make test`, `make check` | Run pytest |
| `make dist`, `make tarball` | Build source distribution and run `twine check` |
| `make rpm` | Build RPM packages via `pyp2rpm` and `rpmbuild` |
| `make man` | Regenerate the man page with `help2man` |
| `make install` | Install with `pip install --user .` |
| `make uninstall` | Uninstall the package |
| `make clean` | Remove build artifacts |

## Troubleshooting

| Problem | Cause | Fix |
|---------|-------|-----|
| `qemu-nbd` fails with "No such file or directory" | The `nbd` kernel module is not loaded | Run `modprobe nbd max_part=8` before using image files |
| Device or mount errors inside a container | Container lacks device access | Run the container with `--privileged` (see [Running in a Container](#running-in-a-container)) |
| "Permission denied reading …" from `cvm-reseal` | Reading `/sys/firmware/efi/efivars/` requires root | Run `cvm-reseal` as root or with `sudo` |
| NBD device is busy | A previous run did not disconnect | Run `qemu-nbd --disconnect /dev/nbd<N>` to release the device |
| Encryption was interrupted | Power loss or process killed during `cryptsetup reencrypt` | Resume with `cvm-encrypt-image encrypt-resume` |
| `udevadm settle` fails in a container | No udev daemon running | Ensure `/run/udev` from the host is bind-mounted into the container |

## Unused Features Queued for Deprecation

ESP may contain an `efivars.json` file in Azure format:

```json
{
    "type": "Microsoft.Compute/disks",
    "properties": {
        "uefiSettings": {
            "Boot0004": {
                "guid": "Yd/ki8qT0hGqDQDgmAMrjA==",
                "attributes": "Bw==",
                "value": "AQAAAGIAUwBoAGkAbQAgAGIAbwBvAHQAIAB0AG8AIAA1AC4AMQA0AC4AMAAtADIAMwA4AF8AdQBrAGkAXwB0AGUAcwB0ADEAOQAuAGUAbAA5AC4AeAA4ADYAXwA2ADQAAAAEASoAAgAAAAAoAAAAAAAAAOAHAAAAAABibF3EAi9J4o1TPhDbQRiuAgIEBDQAXABFAEYASQBcAHIAZQBkAGgAYQB0AFwAcwBoAGkAbQB4ADYANAAuAGUAZgBpAAAAf/8EAFwARQBGAEkAXABMAGkAbgB1AHgAXAB2AG0AbABpAG4AdQB6AC0ANQAuADEANAAuADAALQAyADMAOABfAHUAawBpAF8AdABlAHMAdAAxADkALgBlAGwAOQAuAHgAOAA2AF8ANgA0AC0AdgBpAHIAdAAuAGUAZgBpAAAA"
            }
        }
    }
}
```

In case it does, its size and content will be written to a special 'Linux reserved' GUID `8DA63339-0007-60C0-C436-083AC8230908` partition starting at offset `2048 * 512 = 1048576`.

## License

cvmutils is licensed under the [GNU Lesser General Public License v2.1 or later](COPYING) (LGPL-2.1-or-later).
