keylimectl: A replacement for keylime_tenant in rust - #1068
Draft
ansasaki wants to merge 61 commits into
Draft
Conversation
ansasaki
marked this pull request as draft
August 4, 2025 12:04
ansasaki
force-pushed
the
keylimectl
branch
2 times, most recently
from
August 4, 2025 15:43
b55ee8e to
da44cbc
Compare
ansasaki
force-pushed
the
keylimectl
branch
3 times, most recently
from
August 7, 2025 10:19
7cd11bf to
9609be7
Compare
30 tasks
30 tasks
ansasaki
force-pushed
the
keylimectl
branch
6 times, most recently
from
October 6, 2025 17:19
f15c57b to
d1ff80a
Compare
36 tasks
ansasaki
force-pushed
the
keylimectl
branch
9 times, most recently
from
February 23, 2026 16:49
9b7d3d7 to
f1bf332
Compare
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>
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>
Add PGP detached signature verification for repomd.xml when generating policy from RPM repositories, matching the behaviour of the Python keylime implementation in keylime/policy/rpm_repo.py. When repomd.xml.asc is present the signature is verified against the public key supplied via --gpg-key or, if absent, the bundled repomd.xml.key file. If the signature file is absent a warning is emitted and processing continues; if verification fails the command aborts with an error. The implementation uses sequoia-openpgp (crypto-openssl backend) so no GPG binary or temporary keyring is required. A new gpg_verify module provides the core verify_detached_signature() function with unit tests covering valid, tampered, wrong-key, and bad-data scenarios. Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com> Signed-off-by: Anderson Toshiyuki Sasaki <ansasaki@redhat.com>
Add the keylimectl binary to the GNUmakefile programs list so it is installed alongside the other binaries. Add a keylimectl subpackage to both the Fedora and CentOS RPM specs with its own %files section. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> Signed-off-by: Anderson Toshiyuki Sasaki <ansasaki@redhat.com>
Hide CLI arguments from help output when their corresponding feature is disabled at compile time. Previously these args were always visible and produced runtime errors when used without the feature. Now they are omitted from the binary entirely: - --interactive / -I: gated behind "wizard" feature - --local-rpm-repo, --remote-rpm-repo, --gpg-key: gated behind "rpm-repo" - --from-tpm: gated behind "tpm-local" or "tpm-quote-validation" Update destructuring patterns in generate.rs and evidence.rs to match, passing None/false defaults when features are disabled. Gate the corresponding integration tests behind the same feature flags. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> Signed-off-by: Anderson Toshiyuki Sasaki <ansasaki@redhat.com>
Return the HTTP status code (200 or 202) in the JSON value from delete_agent() and delete_agent_v3(). This allows callers to distinguish between immediate deletion (200 OK) and asynchronous deletion (202 Accepted), where the verifier is still processing the removal because an in-flight attestation cycle has not completed yet. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> Signed-off-by: Anderson Toshiyuki Sasaki <ansasaki@redhat.com>
The update operation removes an agent from the verifier and re-adds it. If the verifier responds to DELETE with 202 Accepted (meaning deletion is still in progress because an in-flight attestation cycle has not completed), sending POST immediately causes a 409 Conflict. Fix by moving the polling logic into remove_agent(): after DELETE, if the response is 202, poll the verifier with exponential backoff until the agent returns 404 before returning to the caller. This gives remove_agent() a clean post-condition — when it returns successfully, the agent is fully gone — regardless of whether the caller is agent remove or agent update. The polling uses the retry configuration from keylimectl.conf (retry_interval, exponential_backoff, max_retries) and shows a spinner with progress information. This mirrors the behavior of the upstream Python tenant (do_cvdelete / do_cvadd), hardened against this race condition by upstream PRs keylime/keylime#1874 and keylime/keylime#1902. Also fix the misleading error message in add_agent() that suggested "keylimectl agent add" when enrollment fails with a conflict; it now directs the user to "keylimectl agent update" or remove-then-add. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> Signed-off-by: Anderson Toshiyuki Sasaki <ansasaki@redhat.com>
Parsing the API version string as f32 is incorrect: "2.10" would parse to 2.1 (less than 2.9), and even well-formed versions like "2.6" suffer from floating-point representation errors that leaked into the JSON output (2.5999999046325684). Add parse_version() to api_versions.rs, which splits "major.minor" into a (u32, u32) tuple. Replace all f32-based version comparisons with this function: - is_v3() in api_versions.rs - add_agent() in commands/agent/add.rs (model detection + JSON output) - update_agent() in commands/agent/update.rs (model detection) Also update the test_supported_versions_ascending_order test and the test_model_auto_detection_logic test to use integer tuples. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> Signed-off-by: Anderson Toshiyuki Sasaki <ansasaki@redhat.com>
The v3 POST /policies/ima and POST /refstates/uefi endpoints require a name attribute in the JSON:API request body. add_runtime_policy_v3() ignored its policy_name parameter (prefixed with _) and add_mb_policy() also omitted name from the v3 request body, causing 422 Unprocessable Entity from the verifier. Inject name into the policy_data attributes before wrapping in json_api_resource() for both IMA and measured boot policy methods. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> Signed-off-by: Anderson Toshiyuki Sasaki <ansasaki@redhat.com>
Remove validation functions (validate(), is_valid_tpm_*, is_valid_api_version) from AddAgentRequest which validated hardcoded values or duplicated server-side checks, along with all their tests. Remove is_pull_model() and UNKNOWN_API_VERSION from AgentClient since model detection uses the verifier's API version, not the agent's. Remove the entire config/error.rs module (ConfigError, LoadError, ValidationError) which was empty scaffolding never referenced outside the file itself. Remove EvidenceError enum and its CommandError::Evidence variant since evidence verification uses KeylimectlError directly and these types were never constructed. Remove PolicyGenerationError::Merge since merge_policies() is infallible and there was no producer for this variant. Remove TpmPolicy::calculate_mask() which duplicated the inline mask calculation in from_pcrs(), along with its tests. Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com> Signed-off-by: Anderson Toshiyuki Sasaki <ansasaki@redhat.com>
…tools Both ima_parser and rpm_repo had their own private copy of DigestMap and merge_digest_maps with identical semantics. Extract the canonical definition to policy_tools/mod.rs so both modules share a single implementation. - policy_tools/mod.rs: add pub DigestMap type alias and merge_digest_maps - ima_parser.rs: replace local DigestMap with pub use super::DigestMap, remove dead merge_digest_maps duplicate - rpm_repo.rs: replace private DigestMap and merge_digest_maps with imports from the parent module - filesystem.rs: update DigestMap import to super::DigestMap Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com> Signed-off-by: Anderson Toshiyuki Sasaki <ansasaki@redhat.com>
Use tuple destructuring to initialize error, message, and response variables directly from the match expression, satisfying the clippy::needless_late_init lint enforced in Rust 1.98.0+. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> Signed-off-by: Anderson Toshiyuki Sasaki <ansasaki@redhat.com>
The accepted TPM algorithms (hash, encryption, signing) were hardcoded in add.rs, causing the verifier to reject agents using non-default algorithms like rsa3072. Add an [agent] config section with broad defaults that accept all strong algorithm variants, excluding weak ones (sha1, rsa1024, ecc192, ecc224). Values can be restricted via config file or KEYLIME_AGENT__ACCEPT_TPM_* environment variables. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> Signed-off-by: Anderson Toshiyuki Sasaki <ansasaki@redhat.com>
Commands like `policy validate`, `policy verify-signature`, and `verify evidence` previously returned exit code 0 even when the validation result was negative (valid: false in the JSON output). This broke shell scripting and CI pipelines that rely on exit codes. Add a `ValidationFailed` error variant to `KeylimectlError` that carries structured validation details. Commands now return `Err` when validation/verification fails, producing exit code 10 with the validation details preserved in the JSON error output. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> Signed-off-by: Anderson Toshiyuki Sasaki <ansasaki@redhat.com>
Previously, `agent status` only returned exit code 1 when all queried services returned errors. Now it returns exit code 10 when any service reports not_found, error, connection_failed, or unreachable, so scripts can detect partial failures. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> Signed-off-by: Anderson Toshiyuki Sasaki <ansasaki@redhat.com>
… failures Client initialization errors (e.g., TLS configuration failures) were wrapped with ResourceError::ListingFailed, producing misleading messages like "Failed to list verifier: TLS error". Add a ConnectionFailed variant to ResourceError and a connection_error constructor to CommandError. All factory::get_*() error wrappings now use connection_error, producing "Failed to connect to verifier: ..." instead. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> Signed-off-by: Anderson Toshiyuki Sasaki <ansasaki@redhat.com>
reqwest::Identity::from_pkcs8_pem only accepts PKCS#8 PEM keys (BEGIN PRIVATE KEY). ECDSA keys in SEC1 format (BEGIN EC PRIVATE KEY) and traditional RSA keys (BEGIN RSA PRIVATE KEY) caused a "builder error" when configuring mTLS. Use OpenSSL's PKey::private_key_from_pem to parse any PEM key format, then re-encode as PKCS#8 before passing to reqwest. This fixes the TLS configuration error with ECDSA certificates. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> Signed-off-by: Anderson Toshiyuki Sasaki <ansasaki@redhat.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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
Implementation
Usage