Skip to content

keylimectl: A replacement for keylime_tenant in rust#1068

Draft
ansasaki wants to merge 46 commits intokeylime:masterfrom
ansasaki:keylimectl
Draft

keylimectl: A replacement for keylime_tenant in rust#1068
ansasaki wants to merge 46 commits intokeylime:masterfrom
ansasaki:keylimectl

Conversation

@ansasaki
Copy link
Copy Markdown
Contributor

@ansasaki ansasaki commented Aug 4, 2025

Disclaimer: this is an AI generated rewrite. We should be careful reviewing it.

Adds a modern Rust replacement for keylime_tenant with full API compatibility and improved usability.

Features

  • Agent Management: add, remove, update, status, reactivate commands
  • Policy Management: runtime and measured boot policy CRUD operations
  • Resource Listing: agents, policies with detailed/basic views
  • Multi-format Output: JSON, table, YAML with configurable verbosity
  • Robust Error Handling: typed errors with context and retry logic
  • TLS Support: mutual authentication with certificate validation
  • Configuration: file-based config with CLI overrides

Implementation

  • 8,512 lines of documented Rust code
  • 158 comprehensive unit tests (100% pass rate)
  • 0 clippy warnings, full type safety
  • Modular architecture with proper abstractions
  • IPv6 support and exponential backoff retry

Usage

keylimectl agent add <uuid> --ip 192.168.1.100 --port 9002
keylimectl policy create web-policy --file policy.json
keylimectl list agents --detailed

Replaces Python keylime_tenant while maintaining backward compatibility.

@ansasaki ansasaki marked this pull request as draft August 4, 2025 12:04
@ansasaki ansasaki force-pushed the keylimectl branch 2 times, most recently from b55ee8e to da44cbc Compare August 4, 2025 15:43
@codecov
Copy link
Copy Markdown

codecov bot commented Aug 5, 2025

Codecov Report

❌ Patch coverage is 0% with 1 line in your changes missing coverage. Please review.
✅ Project coverage is 42.18%. Comparing base (bec5d94) to head (d1ff80a).

Files with missing lines Patch % Lines
keylime-push-model-agent/src/attestation.rs 0.00% 1 Missing ⚠️
Additional details and impacted files
Flag Coverage Δ
e2e-testsuite 42.18% <0.00%> (-16.10%) ⬇️
upstream-unit-tests 42.18% <0.00%> (-16.10%) ⬇️

Flags with carried forward coverage won't be shown. Click here to find out more.

Files with missing lines Coverage Δ
keylime-push-model-agent/src/struct_filler.rs 0.00% <ø> (-25.44%) ⬇️
keylime-push-model-agent/src/attestation.rs 0.00% <0.00%> (-44.76%) ⬇️

... and 53 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@ansasaki ansasaki force-pushed the keylimectl branch 3 times, most recently from 7cd11bf to 9609be7 Compare August 7, 2025 10:19
@ansasaki ansasaki mentioned this pull request Aug 27, 2025
30 tasks
@ansasaki ansasaki mentioned this pull request Sep 24, 2025
30 tasks
@ansasaki ansasaki force-pushed the keylimectl branch 6 times, most recently from f15c57b to d1ff80a Compare October 6, 2025 17:19
@sarroutbi sarroutbi mentioned this pull request Nov 26, 2025
36 tasks
@ansasaki ansasaki force-pushed the keylimectl branch 9 times, most recently from 9b7d3d7 to f1bf332 Compare February 23, 2026 16:49
ansasaki and others added 29 commits March 9, 2026 15:23
Add IMA measurement list parsing, flat-text and JSON allowlist parsing,
exclude list parsing, and file digest calculation for local runtime
policy generation via `keylimectl policy generate runtime`.

- ima_parser: parse IMA logs (ima, ima-ng, ima-sig, ima-buf templates)
- ima_parser: parse flat-text and JSON allowlists, exclude lists
- digest: calculate file digests using OpenSSL (sha1/256/384/512/sm3)
- generate: wire Runtime subcommand to parse inputs and build policy

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: Anderson Toshiyuki Sasaki <ansasaki@redhat.com>
Add filesystem tree scanning for rootfs digest calculation and policy
merge utilities. Wire --rootfs and --skip-path CLI args to the runtime
policy generate command using tokio::spawn_blocking for CPU-bound work.

- filesystem: recursive directory walk with skip paths and symlink exclusion
- merge: union of digests, excludes, keyrings, and ima-buf entries
- runtime_policy: add deduplication to add_digest/add_keyring/add_ima_buf

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: Anderson Toshiyuki Sasaki <ansasaki@redhat.com>
Add Dead Simple Signing Envelope (DSSE) support for policy signing and
verification with ECDSA P-256 and X.509 certificate backends. Implements
PAE (Pre-Authentication Encoding), envelope sign/verify protocol, key
generation/loading, and self-signed certificate creation.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: Anderson Toshiyuki Sasaki <ansasaki@redhat.com>
Add structural and content validation for all three policy types with
auto-detection. Validates digest formats, required fields, PCR mask
consistency, and schema compatibility. Supports optional DSSE signature
verification during validation via --signature-key.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: Anderson Toshiyuki Sasaki <ansasaki@redhat.com>
Add conversion from JSON and flat-text allowlists to v1 runtime policy
format. Supports auto-detection of input format, exclude list merging,
and verification key injection. Adds in-memory parsing helpers for JSON
and flat-text allowlists to ima_parser.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: Anderson Toshiyuki Sasaki <ansasaki@redhat.com>
Add verify evidence command that posts TPM or TEE attestation evidence
to the verifier's /verify/evidence endpoint. Reads quote, AK, EK files
as base64, sends with nonce and policies, parses verification results
including failure details.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: Anderson Toshiyuki Sasaki <ansasaki@redhat.com>
Add TPM policy generation from PCR values file with index filtering and
mask calculation. Add measured boot policy generation from UEFI event
logs using the shared crate's UefiLogHandler, extracting S-CRTM,
platform firmware, and Secure Boot variable measurements.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: Anderson Toshiyuki Sasaki <ansasaki@redhat.com>
Add integration tests in tests/policy_tools.rs covering:
- Help output for all new subcommands (generate, sign, validate, convert)
- Runtime policy generation from IMA logs, allowlists, with excludes
- TPM policy generation from PCR values files
- Policy validation for runtime and TPM policy types
- DSSE signing (ECDSA and X.509 backends) and verification
- Legacy allowlist conversion (flat-text and JSON formats)
- End-to-end pipeline: generate -> validate -> sign -> verify

Fix bugs found during integration testing:
- Remove duplicate stdout output (commands called output.success()
  AND main.rs dispatcher also called it, producing double JSON)
- Remove default_value on --ima-measurement-list to make it truly
  optional (previously always read /sys/kernel/security/ima even
  when only --allowlist was specified)
- Add PolicyAction::is_local_only() and match arm in main() so
  local-only policy commands (generate, sign, verify-signature,
  validate, convert) bypass strict TLS config validation

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: Anderson Toshiyuki Sasaki <ansasaki@redhat.com>
Add policy_tools/privilege module with helpers for detecting permission
errors and suggesting sudo retries. Add PrivilegeRequired error variant
to PolicyGenerationError for privileged operations like TPM access,
initramfs reading, and boot event log parsing.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: Anderson Toshiyuki Sasaki <ansasaki@redhat.com>
… log

Add EV_EFI_VARIABLE_AUTHORITY and EV_EFI_PLATFORM_FIRMWARE_BLOB2 event
types to the shared UEFI log handler. Create uefi_event_data module for
parsing UEFI_VARIABLE_DATA and EV_IPL event data structures.

Extract boot chain entries (shim/grub/kernel from PCR 4), kernel command
line (PCR 8), initrd/vmlinuz digests (PCR 9), MOK digests
(MokList/MokListX), and vendor_db from EV_EFI_VARIABLE_AUTHORITY events.
Add vmlinuz_plain_sha256 field to KernelEntry for non-SecureBoot
systems.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: Anderson Toshiyuki Sasaki <ansasaki@redhat.com>
Add tpm-local feature flag (aliases dep:tss-esapi). Implement
generate_from_tpm() that opens the local TPM via TCTI, reads PCR values
for requested indices and hash algorithm, and builds a TpmPolicy.

On permission errors accessing /dev/tpmrm0, suggest running with sudo.
Pass hash_alg through from CLI to TPM generation.

Without the feature flag, --from-tpm produces a clear error naming the
required feature.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: Anderson Toshiyuki Sasaki <ansasaki@redhat.com>
Add ability to extract and hash files from initramfs/initrd images for
runtime policy generation. Supports gzip, zstd, xz, and bzip2
compression formats with CPIO new-ASCII archive parsing.

Key components:
- Compression detection via magic bytes with automatic decompression
- Early microcode CPIO archive detection and skipping
- In-memory CPIO parsing that hashes files without extracting to disk
- Privilege detection for /boot directory access with sudo suggestion

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: Anderson Toshiyuki Sasaki <ansasaki@redhat.com>
Add --local-rpm-repo and --remote-rpm-repo options for runtime policy
generation.

Local repos are scanned for RPM files and their headers are parsed for
file digests. Remote repos use filelists-ext.xml as a fast path, falling
back to downloading individual RPMs.

Key components:
- RPM header parsing via pure-Rust rpm crate (no librpm-devel needed)
- repomd.xml and filelists-ext.xml parsing via quick-xml
- Automatic decompression of metadata files (gzip, xz, zstd, bzip2)
- Feature-gated: rebuild with --features rpm-repo to enable

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: Anderson Toshiyuki Sasaki <ansasaki@redhat.com>
Add new integration tests:
- Runtime help shows --ramdisk-dir, --local-rpm-repo, --remote-rpm-repo
- TPM help shows --from-tpm
- Nonexistent ramdisk dir fails with error
- Empty ramdisk dir succeeds with no initrd digests

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: Anderson Toshiyuki Sasaki <ansasaki@redhat.com>
Simplify the CLI flag name for querying the registrar directly.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: Anderson Toshiyuki Sasaki <ansasaki@redhat.com>
…olling

The previous implementation matched operational_state against string
values, but the verifier returns it as an integer. Instead, use the
attestation_status field ("PENDING", "PASS", "FAIL") which the verifier
computes for both push and pull mode agents.

On failure, report the operational state, severity level, and last event
ID so the user understands why attestation failed. On timeout, use the
correct error type to avoid the misleading "Failed to list verifier"
message.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: Anderson Toshiyuki Sasaki <ansasaki@redhat.com>
The accept_tpm_signing_algs field was set to ["rsa", "ecdsa"], which
are encryption algorithm names, not signing algorithm names. The
verifier rejected quotes signed with rsassa because it was not in
the accepted list. Use the correct signing algorithm names matching
the Python tenant defaults: ["ecschnorr", "rsassa"].

Also align accept_tpm_hash_algs with tenant defaults by including
sha512 and sha384, and dropping sha1.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: Anderson Toshiyuki Sasaki <ansasaki@redhat.com>
The agent expects the payload as base64-encoded AES-256-GCM ciphertext
(base64(iv || ciphertext || tag), encrypted with key K). Previously, the
raw file content was sent without encryption or encoding, causing
"Invalid base64 encoding in payload" errors.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: Anderson Toshiyuki Sasaki <ansasaki@redhat.com>
Two issues caused --verify to always fail:

1. HMAC encoding mismatch: the agent returns HMAC as hex (matching
   Python's do_hmac hexdigest), but we compared against base64-encoded
   HMAC. Changed to hex::encode.

2. No retry: the agent needs V from the verifier before it can compute
   K = U XOR V. The Python tenant retries in a loop; we did a single
   attempt. Added exponential backoff retry (up to 12 attempts).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: Anderson Toshiyuki Sasaki <ansasaki@redhat.com>
Add the agent's self-signed mTLS certificate (from the registrar
database) as a trusted root CA when connecting to agents in pull mode.
This allows verifying the agent's TLS certificate without disabling
certificate verification globally, matching the Python tenant behavior.

Also change accept_invalid_hostnames default from true to false, since
certificates should have proper SANs set.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: Anderson Toshiyuki Sasaki <ansasaki@redhat.com>
Add animated progress spinners using indicatif for long-running
operations (attestation polling, key derivation retry) and optional
color output via console. Spinners auto-detect TTY on stderr and fall
back to plain text when piped. Colors apply to stderr only, keeping
stdout clean for machine consumption.

New --color flag (auto|always|never) controls color output. The
OutputHandler now supports start_wait() which returns an RAII WaitHandle
for polling loops with live status updates.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: Anderson Toshiyuki Sasaki <ansasaki@redhat.com>
Rename --verifier-only to --verifier and --from-registrar to --registrar
for consistency with the existing --registrar flag on agent list/status.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: Anderson Toshiyuki Sasaki <ansasaki@redhat.com>
Add --interactive (-I) flag to 'policy generate runtime' that launches a
step-by-step wizard using dialoguer prompts. The wizard guides users
through selecting input sources, configuring paths, setting IMA options,
choosing a hash algorithm, and specifying output — then delegates to the
existing generate_runtime() function.

The wizard is gated behind the 'wizard' feature flag, matching the
existing pattern used by the configure command.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: Anderson Toshiyuki Sasaki <ansasaki@redhat.com>
Add --interactive (-I) flag to 'policy generate measured-boot' that
launches a step-by-step wizard. The wizard prompts for the UEFI event
log path, whether to include Secure Boot variables, shows a preview of
event log statistics (total events, S-CRTM entries, algorithms), asks
for the output file, and confirms before generating.

Uses the existing get_eventlog_stats() function for the preview step,
replacing #[allow(dead_code)] with a conditional cfg_attr gate.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: Anderson Toshiyuki Sasaki <ansasaki@redhat.com>
Add --interactive (-I) flag to 'policy generate tpm' that launches a
step-by-step wizard. The wizard prompts for the PCR source (file or
local TPM), lets the user select PCR indices from a labeled list with
descriptions (S-CRTM, Secure Boot, IMA, etc.), chooses the hash
algorithm, asks for the output file, and confirms with a summary before
generating.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: Anderson Toshiyuki Sasaki <ansasaki@redhat.com>
Add --interactive (-I) flag to 'verify evidence' that launches a
step-by-step wizard. The wizard prompts for evidence type (TPM/TEE),
required files (nonce, quote, AK, EK), hash algorithm, policy files (at
least one required), measurement logs (conditional on selected
policies), and confirms with a summary before sending to the verifier.

The nonce, quote, tpm-ak, and tpm-ek fields are now optional in the CLI
definition (using required_unless_present = "interactive") so the wizard
can prompt for them instead. Non-interactive mode validates their
presence explicitly.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: Anderson Toshiyuki Sasaki <ansasaki@redhat.com>
Add BASE_EXCLUDE_DIRS matching Python keylime-policy defaults:
/sys, /run, /proc, /lost+found, /dev, /media, /snap, /mnt, /var, /tmp.
These directories contain volatile or virtual data with no meaningful
integrity to verify.

The default excluded paths are automatically merged with user-provided
--skip-path values. When a user path is already covered by a default
(e.g. --skip-path /var/log is under /var), a note is printed to
inform the user it has no additional effect. Default paths are resolved
relative to --rootfs so scanning /mnt/image correctly skips
/mnt/image/sys, etc.

Refactor filesystem scanning to use Rayon for parallel digest
calculation: file discovery remains sequential (I/O-bound directory
walk), but hash computation runs across all available CPU cores.
Directory permission errors are now non-fatal (logged and skipped).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: Anderson Toshiyuki Sasaki <ansasaki@redhat.com>
Match the Python tenant's process_policy() behavior by automatically
enabling PCR bits in the TPM policy mask when attestation policies are
attached:
- runtime policy → enables IMA PCR 10
- measured boot policy → enables measured boot PCRs (0-9, 11-15)

Without this, keylimectl sent {"mask":"0x0"} regardless of attached
policies, causing the verifier to skip TPM challenge generation and
reject attestations with "challenges expired at None" (403).

Also add a hard error when no attestation policy (--runtime-policy,
--mb-policy, or --tpm-policy) is provided, since the verifier cannot
attest an agent without at least one policy.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: Anderson Toshiyuki Sasaki <ansasaki@redhat.com>
The measured boot policy generator was using
PolicyGenerationError::Output (which formats as "Failed to write output
to ...") when the UEFI event log could not be read or parsed. This
produced misleading error messages like "Failed to write output to
/sys/kernel/.../binary_bios_measurements" for what is actually a
read/parse error.

Add a dedicated EventLogParse variant and use it in
generate_from_eventlog() and get_eventlog_stats(), producing clear
messages like "Failed to parse event log
/sys/.../binary_bios_measurements: IO error: No such file".

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: Anderson Toshiyuki Sasaki <ansasaki@redhat.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants