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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 9 additions & 16 deletions docs/docs.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ We created a modular proof‑of‑concept based on NixOS that fulfills most of t
* **aarch64 support** could be added if needed. Only `x86_64` with `UEFI` is implemented at the moment.
* **artifact uploads**: build artifacts are currently not automatically uploaded anywhere, but stay on the build machine.
Integration of a Trusted Platform Module (TPM) could be useful here, to ease authentication to private repositories as well as destinations for artifact upload.
* **measured boot & attestation**: PCR 11 (UKI) is pre-calculated at build time and verified at runtime. Firmware PCRs (0–3, 7) and PCR 11 are automatically reported to the attestation server via `report-pcrs` for full-policy auto-enrollment. The keylime agent is enabled by default (using TPM EK-derived identity). Remaining work: an attestation-gated step in the unattended pipeline, network policy for attestation traffic, and replacing the `accept-all` measured boot policy with real UEFI event log validation.
* **measured boot & attestation**: Firmware PCRs (0–3, 7) and PCR 11 (UKI measurement) are automatically reported to the attestation server via `report-pcrs` for full-policy auto-enrollment. The keylime agent is enabled by default (using TPM EK-derived identity). Remaining work: an attestation-gated step in the unattended pipeline, network policy for attestation traffic, and replacing the `accept-all` measured boot policy with real UEFI event log validation.
* **credential storage**: TPM-encrypted credentials (via `systemd-creds`) are currently bound to PCR 7 (Secure Boot policy) only, not PCR 11 (UKI). Since the Secure Boot signing keys are created specifically for this project, only images signed with our key can produce a matching PCR 7 — so the practical risk is low. Binding to PCR 11 as well would prevent a *different* image signed with the same key from decrypting credentials, at the cost of invalidating all stored credentials on every image update. A `systemd-measure sign` based approach could provide PCR 11 binding without this drawback.
* **higher-level configuration**: Adapting the build environment to the needs of custom AOSP distributions might need extra work. Depending on the nature of those
customizations, a good understanding of `nix` might be needed. We will ease those as far as possible, as we learn more about users customization needs.
Expand Down Expand Up @@ -261,13 +261,13 @@ The daemon runs on the attestation server alongside the registrar and verifier.
2. Periodically polls the registrar for registered agent UUIDs.
3. When an agent is both registered AND has submitted its PCR report, enrolls it with the verifier using the full policy.

On the agent side, `report-pcrs` runs as a oneshot systemd service after the keylime agent registers. It reads all PCR values from the TPM, verifies PCR 11 against the expected value on the ESP, and POSTs the policy to the daemon.
On the agent side, `report-pcrs` runs as a oneshot systemd service after the keylime agent registers. It reads all PCR values (firmware PCRs 0–3, 7 and PCR 11) from the TPM and POSTs the policy to the daemon.

#### Trust Model

Firmware PCRs are accepted on a **trust-on-first-use (TOFU)** basis: the agent self-reports its PCR values before the first attestation. This is acceptable because:

- PCR 11 is verified locally against the build-time expected value before reporting — the agent must be running the correct image.
- PCR 11 (UKI measurement) is included in the reported policy, ensuring the verifier attests the specific image booted on the agent.
- After enrollment, the verifier validates all PCR values against the TPM quote on every attestation cycle — any false report is caught immediately.
- Once enrolled with the full policy, the agent cannot downgrade the policy — only an admin with verifier mTLS credentials can modify it.

Expand All @@ -282,13 +282,12 @@ Attestation verifies the following Platform Configuration Registers (PCRs) by de
- **PCR 7** – Secure Boot state (keys, policy, boot variables)
- **PCR 11** – UKI components and boot phases

PCRs 0–3 and 7 are firmware-dependent and can only be read from the live TPM. PCR 11 is pre-calculated at build time from the UKI using `systemd-measure` and written to the ESP as `/boot/expected-pcr11` by `configure-disk-image set-pcr11`.
PCRs 0–3 and 7 are firmware-dependent and can only be read from the live TPM. PCR 11 is measured by systemd at boot from the UKI components and boot phase strings.

Two tools are included for PCR management:

- `report-pcrs` – reads firmware PCR values (0–3, 7) and PCR 11 from the TPM sysfs, verifies PCR 11 against the expected value on the ESP, and enrolls on the keylime server by sending the PCRs to the auto-enrollment service. Runs automatically as a oneshot service after the keylime agent registers.
- `read-firmware-pcrs` – reads PCR values from the TPM sysfs and outputs a keylime `tpm_policy` JSON. Supports `--verify-pcr11` to include PCR 11 after verifying it against the expected value, `--save` to persist a baseline, and `--diff` to compare against a previously saved baseline. Useful for debugging and manual inspection.
- `calculate-pcr11` – offline tool for use on a local workstation that computes the expected PCR 11 value from a UKI file by extracting its PE sections and running `systemd-measure calculate` with the `sysinit:ready` phase.
- `report-pcrs` – reads firmware PCR values (0–3, 7) and PCR 11 from the TPM sysfs and sends them to the auto-enrollment service on the attestation server. Runs automatically as a oneshot service after the keylime agent registers.
- `read-firmware-pcrs` – reads PCR values (0–3, 7, 11) from the TPM sysfs and outputs a keylime `tpm_policy` JSON. Supports `--save` to persist a baseline and `--diff` to compare against a previously saved baseline. Useful for debugging and manual inspection.

## Credential Storage {#credential-storage}

Expand Down Expand Up @@ -338,14 +337,8 @@ flowchart TB
copy-auth["<b>(7)</b> Copy Secure Boot update bundles"]
end

signing-script --> set-pcr11
subgraph set-pcr11["configure-disk-image set-pcr11"]
direction TB
inject-pcr11["<b>(8)</b> Write expected PCR 11 hash to ESP"]
end

set-pcr11 --> signed
signed["<b>(9)</b> Image is signed & ready to boot"]
signing-script --> signed
signed["<b>(8)</b> Image is signed & ready to boot"]

~~~

Expand Down Expand Up @@ -378,7 +371,7 @@ Usage is documented in [user-guide.pdf](user-guide.pdf). `configure-disk-image`

- **(6)** The `UKI` is copied to a temporary file, signed, and copied back into the `esp` again.
- **(7)** Secure Boot update bundles (`*.auth` files) are copied to the `esp` to ensure that `ensure-secure-boot-enrollment.service` can find them during boot.
- **(8)** The expected PCR 11 value is pre-calculated from the signed UKI and written to the ESP as `/boot/expected-pcr11`. At runtime, `report-pcrs` verifies the live TPM PCR 11 against this value before reporting the full policy to the auto-enrollment server.

- **(9)** We finally have a signed image, ready to flash & boot on a target machine.


Expand Down
36 changes: 7 additions & 29 deletions docs/user-guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -155,20 +155,6 @@ Copying keystore files for auto-enrollment...
✓ Keystore files copied to ESP
```

To write the expected PCR 11 hash (UKI measurement) to the image, run:

```shell-session
$ nix run .#configure-disk-image -- set-pcr11 --device android-builder_25.11pre-git.raw
```

``` text
Extracting UKI from image...
Computing expected PCR 11...
✓ Expected PCR 11 written to ESP: 00e9c94ef58cd0c569e2872b451fee0e30b322dffb38cf79415c9f478807dddf
```

This step must be run **after** signing, since signing changes the UKI and therefore its PCR 11 value. If omitted, `report-pcrs` will fail at runtime because it cannot verify PCR 11.

## Configure Attestation Server

If you are running a keylime attestation server (registrar & verifier), the builder image needs to know how to reach it. `configure-disk-image set-attestation-server` writes the server address and CA certificate to the ESP so the keylime agent can connect on boot.
Expand Down Expand Up @@ -206,7 +192,7 @@ sequenceDiagram
participant S as Attestation Server
participant A as Agent

B->>B: nix build, sign, set-pcr11,<br/>set-attestation-server
B->>B: nix build, sign,<br/>set-attestation-server
B->>A: Flash image to disk

A->>A: Boot — agent starts
Expand Down Expand Up @@ -461,14 +447,14 @@ $ cat /sys/class/tpm/tpm0/pcr-sha256/7
abc123...
```

On boot, the `report-pcrs` service automatically reads firmware PCRs (0–3, 7) and PCR 11, verifies PCR 11 against the expected value on the ESP, and enrolls on the keylime server by sending the PCRs to the auto-enrollment service. No manual PCR inspection is normally needed.
On boot, the `report-pcrs` service automatically reads firmware PCRs (0–3, 7) and PCR 11 from the TPM, then sends them to the auto-enrollment service on the attestation server. No manual PCR inspection is normally needed.

For debugging, `read-firmware-pcrs` is also available to inspect PCR values interactively:

```shell-session
$ read-firmware-pcrs
{"0": ["abc123..."], "1": ["def456..."], "2": ["..."], "3": ["..."], "7": ["..."]}
$ read-firmware-pcrs --verify-pcr11
$ read-firmware-pcrs
{"0": ["..."], "1": ["..."], "2": ["..."], "3": ["..."], "7": ["..."], "11": ["..."]}
```

Expand Down Expand Up @@ -603,9 +589,6 @@ Installation target:
Storage target:
Interactive menu (user will select artifact storage)

PCR 11 (UKI measurement):
✓ Expected hash: 00e9c94ef58cd0c569e2872b451fee0e30b322dffb38cf79415c9f478807dddf

Attestation server:
✓ Server: 10.0.0.1 (registrar:8891, verifier:8881)
✓ CA cert: present
Expand Down Expand Up @@ -714,12 +697,11 @@ This will:

1. Create a writable copy of the read-only disk image (e.g. `android-builder_25.11pre-git.raw`) in the current directory.
2. Sign the UKI with a pair of auto-generated test keys (stored in the nix store, so they persist across runs).
3. Inject the expected PCR 11 hash into the ESP.
4. Pre-configure artifact storage to use `/dev/vdb` (a second virtual disk is created automatically when `nixosAndroidBuilder.artifactStorage.enable` is set).
5. If an `attestation-server.json` file exists in the current directory, configure the keylime agent to use it. The file uses the same format as `/boot/attestation-server.json` (see [Configure Attestation Server](#configure-attestation-server) above).
6. Start a QEMU VM with Secure Boot, a TPM, and a graphical window.
3. Pre-configure artifact storage to use `/dev/vdb` (a second virtual disk is created automatically when `nixosAndroidBuilder.artifactStorage.enable` is set).
4. If an `attestation-server.json` file exists in the current directory, configure the keylime agent to use it. The file uses the same format as `/boot/attestation-server.json` (see [Configure Attestation Server](#configure-attestation-server) above).
5. Start a QEMU VM with Secure Boot, a TPM, and a graphical window.

If the writable disk image already exists from a previous run, steps 1–5 are skipped and the existing image is reused. Delete the `.raw` file to force a fresh image.
If the writable disk image already exists from a previous run, steps 1–4 are skipped and the existing image is reused. Delete the `.raw` file to force a fresh image.

Use `systemctl poweroff` from within the VM, or close the QEMU window, to stop it.

Expand Down Expand Up @@ -810,10 +792,6 @@ Check the daemon logs (`journalctl -u keylime-auto-enroll`) for enrollment error
- mTLS certificate issues (expired, wrong CA).
- Port 8893 not reachable from the agent.

**Problem: `report-pcrs` fails with "PCR 11 mismatch"**

The agent's live PCR 11 does not match the expected value on the ESP. Ensure `set-pcr11` was run after signing the image.

**Problem: Agent is enrolled but fails attestation**

The TPM quote does not match the enrolled policy. This can happen after a firmware update or BIOS settings change that alters firmware PCRs. Delete the enrollment and let the agent re-enroll:
Expand Down
3 changes: 1 addition & 2 deletions flake.nix
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,6 @@
packages = with secureBootScripts; [
create-signing-keys
diskInstaller.configure
pcrPolicy.calculate-pcr11
docs.build-docs
docs.watch-docs
pkgs.pam_u2f
Expand All @@ -112,7 +111,7 @@
keylime-agent
;
inherit (secureBootScripts) create-signing-keys;
inherit (pcrPolicy) calculate-pcr11 report-pcrs read-firmware-pcrs;
inherit (pcrPolicy) report-pcrs read-firmware-pcrs;
configure-disk-image = diskInstaller.configure;
default = image;
};
Expand Down
21 changes: 0 additions & 21 deletions modules/secure-boot.nix
Original file line number Diff line number Diff line change
Expand Up @@ -7,16 +7,6 @@
let
pcrPolicy = pkgs.callPackage ../packages/pcr-policy { };

# Pre-calculate the expected PCR 11 value from the UKI at build time.
# Placed on the ESP (not in /nix/store) to avoid a circular dependency:
# /etc → store partition → UKI → expectedPcr11 → /etc would be infinite.
# The ESP already contains the UKI so referencing it here is safe.
expectedPcr11 = pkgs.runCommand "expected-pcr11" { } ''
${lib.getExe pcrPolicy.calculate-pcr11} \
${config.system.build.uki}/${config.system.build.uki.name} \
> $out
'';

enroll-secure-boot = pkgs.writeShellScriptBin "enroll-secure-boot" ''
set -xeu
# Allow modification of efivars
Expand Down Expand Up @@ -67,17 +57,6 @@ in
pcrPolicy.read-firmware-pcrs
];

# Expose the expected PCR 11 hash as a build output.
#
# It can't be baked into the store partition or ESP at image build
# time because that would create a circular dependency
# (store → UKI → expectedPcr11 → store). Instead it is written to
# the ESP by configure-disk-image set-pcr11 (post-build).
#
# report-pcrs reads it from /boot/expected-pcr11
# at runtime to compare against the running TPM state.
system.build.expectedPcr11 = expectedPcr11;

# Enable PCR phase measurements (systemd-pcrextend extends PCR 11 with boot
# phase strings so that the final value is only reachable after a full,
# successful boot of this exact UKI).
Expand Down
6 changes: 0 additions & 6 deletions modules/vm.nix
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ let

secureBootScripts = hostPkgs.callPackage ../packages/secure-boot-scripts { };
disk-installer = hostPkgs.callPackage ../packages/disk-installer { };
pcrPolicy = hostPkgs.callPackage ../packages/pcr-policy { };
in
{
config = {
Expand Down Expand Up @@ -84,11 +83,6 @@ in
--target "/dev/vdb" \
--device "${cfg.diskImage}"

echo >&2 "Injecting expected PCR 11 hash onto ESP"
${lib.getExe disk-installer.configure} set-pcr11 \
--expected-pcr11 "${config.system.build.expectedPcr11}" \
--device "${cfg.diskImage}"

# Configure attestation server from local attestation-server.json
# (same format as /boot/attestation-server.json).
if [ -f attestation-server.json ]; then
Expand Down
92 changes: 0 additions & 92 deletions packages/disk-installer/configure-disk-image.py
Original file line number Diff line number Diff line change
Expand Up @@ -227,19 +227,6 @@ def show_storage_target(img_spec):
print()


def show_pcr11_status(img_spec):
result = subprocess.run(
["mtype", "-i", img_spec, "::/expected-pcr11"],
capture_output=True, text=True, check=False,
)
print("PCR 11 (UKI measurement):")
if result.returncode == 0:
pcr11_hash = result.stdout.strip().replace('\r', '').replace('\n', '')
print(f" ✓ Expected hash: {pcr11_hash}")
else:
print(f" ✗ Not configured (run: configure-disk-image set-pcr11 --device <image>)")
print()


def cmd_status(args):
"""Check status of installer image."""
Expand Down Expand Up @@ -268,7 +255,6 @@ def cmd_status(args):
raise InstallerError("Cannot access EFI partition (invalid FAT filesystem)")

show_storage_target(esp_img_spec)
show_pcr11_status(esp_img_spec)
show_attestation_server_status(esp_img_spec)
extract_and_verify_uki(esp_img_spec, cert_path, "Payload")
else:
Expand All @@ -289,7 +275,6 @@ def cmd_status(args):

show_install_target(installer_img_spec)
show_storage_target(installer_img_spec)
show_pcr11_status(payload_img_spec)
show_attestation_server_status(payload_img_spec)

extract_and_verify_uki(installer_img_spec, cert_path, "Installer")
Expand Down Expand Up @@ -377,78 +362,6 @@ def cmd_set_target(args):
temp_path.unlink(missing_ok=True)


def cmd_set_pcr11(args):
"""Compute expected PCR 11 from the UKI inside the image and write it to the ESP."""
device = Path(args.device)
if not device.exists():
raise InstallerError(
f"Device or image file not found: {device}")

payload_only = is_payload_only(device)
if payload_only:
esp_offset = get_partition_offset(
device, UUID_EFI_SYSTEM)
else:
esp_offset = get_payload_esp_offset(device)

esp_img_spec = f"{device}@@{esp_offset}"
if not verify_mtools_access(esp_img_spec):
raise InstallerError(
"Cannot access EFI partition")

if args.expected_pcr11:
# Use a pre-computed hash file if provided
expected_pcr11 = Path(args.expected_pcr11)
if not expected_pcr11.is_file():
raise InstallerError(
f"Expected PCR 11 file not found:"
f" {expected_pcr11}")
pcr11_hash = expected_pcr11.read_text().strip()
else:
# Extract the UKI from the image and compute PCR 11
with tempfile.NamedTemporaryFile(suffix=".efi", delete=False) as temp_efi:
temp_uki = Path(temp_efi.name)
try:
print("Extracting UKI from image...")
if subprocess.run(
["mcopy", "-n", "-i", esp_img_spec,
"::/EFI/BOOT/BOOTX64.EFI", str(temp_uki)],
check=False, capture_output=True
).returncode != 0:
raise InstallerError(
"Failed to extract UKI from image")

print("Computing expected PCR 11...")
result = subprocess.run(
["calculate-pcr11", str(temp_uki)],
capture_output=True, text=True
)
if result.returncode != 0:
raise InstallerError(
f"Failed to compute PCR 11: {result.stderr.strip()}")
pcr11_hash = result.stdout.strip()
finally:
temp_uki.unlink(missing_ok=True)

with tempfile.NamedTemporaryFile(mode='w', delete=False) as temp_hash:
temp_hash.write(pcr11_hash)
temp_hash_path = Path(temp_hash.name)

try:
if subprocess.run(
["mcopy", "-n", "-o", "-i", esp_img_spec,
str(temp_hash_path), "::/expected-pcr11"],
check=False, capture_output=True
).returncode != 0:
raise InstallerError(
"Failed to write expected-pcr11 to ESP")
finally:
temp_hash_path.unlink(missing_ok=True)

print(f"✓ Expected PCR 11 written to ESP:"
f" {pcr11_hash}")


def cmd_set_storage(args):
"""Configure target for artifact storage."""
device = Path(args.device)
Expand Down Expand Up @@ -615,10 +528,6 @@ def main():
storage_parser.add_argument('--target', required=True, help='Target device (e.g., /dev/sda) or "select" for interactive')
storage_parser.add_argument('--device', required=True, help='Block device or disk image file')

pcr11_parser = subparsers.add_parser('set-pcr11', help='Compute expected PCR 11 from UKI in image and write to ESP')
pcr11_parser.add_argument('--expected-pcr11', help='File containing pre-computed PCR 11 hash (default: compute from UKI in image)')
pcr11_parser.add_argument('--device', required=True, help='Block device or disk image file')

registrar_parser = subparsers.add_parser('set-attestation-server', help='Configure keylime registrar/verifier connection on ESP')
registrar_parser.add_argument('--ip', required=True, help='Registrar/verifier server IP address')
registrar_parser.add_argument('--ca-cert', required=True, help='Path to CA certificate PEM file')
Expand All @@ -634,7 +543,6 @@ def main():
'sign': cmd_sign,
'set-target': cmd_set_target,
'set-storage': cmd_set_storage,
'set-pcr11': cmd_set_pcr11,
'set-attestation-server': cmd_set_attestation_server,
}[args.command](args)
except InstallerError as e:
Expand Down
Loading
Loading