diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..5a0d5e4 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,2 @@ +# Auto detect text files and perform LF normalization +* text=auto eol=lf diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 0000000..0ebaabf --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,62 @@ +name: Build + +on: + pull_request: + paths: + - '**/*.rs' + - '**/Cargo.toml' + - '**/Cargo.lock' + - '.github/workflows/build.yml' + push: + paths: + - '**/*.rs' + - '**/Cargo.toml' + - '**/Cargo.lock' + - '.github/workflows/build.yml' + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + build: + runs-on: ${{ matrix.runner }} + strategy: + fail-fast: false + matrix: + toolchain: + - "1.87.0" + - stable + os-arch: + - ubuntu-24.04-x86_64 + - ubuntu-24.04-aarch64 + - macos-x86_64 + - macos-aarch64 + - windows-x86_64 + include: + - os-arch: ubuntu-24.04-x86_64 + runner: ubuntu-24.04 + target: x86_64-unknown-linux-gnu + - os-arch: ubuntu-24.04-aarch64 + runner: ubuntu-24.04-arm + target: aarch64-unknown-linux-gnu + - os-arch: macos-x86_64 + runner: macos-26-intel + target: x86_64-apple-darwin + - os-arch: macos-aarch64 + runner: macos-26 + target: aarch64-apple-darwin + - os-arch: windows-x86_64 + runner: windows-2025 + target: x86_64-pc-windows-gnu + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - uses: actions-rust-lang/setup-rust-toolchain@150fca883cd4034361b621bd4e6a9d34e5143606 # v1.15.4 + with: + toolchain: ${{ matrix.toolchain }} + target: ${{ matrix.target }} + - run: cargo build --locked --workspace --all-targets --all-features --target ${{ matrix.target }} diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml new file mode 100644 index 0000000..aa4587c --- /dev/null +++ b/.github/workflows/docs.yml @@ -0,0 +1,44 @@ +name: Docs + +on: + pull_request: + paths: + - '**/*.rs' + - '**/Cargo.toml' + - '**/Cargo.lock' + - '.github/workflows/docs.yml' + push: + paths: + - '**/*.rs' + - '**/Cargo.toml' + - '**/Cargo.lock' + - '.github/workflows/docs.yml' + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + rdme: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - uses: actions-rust-lang/setup-rust-toolchain@150fca883cd4034361b621bd4e6a9d34e5143606 # v1.15.4 + with: + toolchain: stable + - uses: taiki-e/install-action@58e862542551f667fa44c8a2a4a1d64ad477c96a # v2.75.17 + with: + tool: cargo-rdme + - run: cargo rdme --check + doc: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - uses: actions-rust-lang/setup-rust-toolchain@150fca883cd4034361b621bd4e6a9d34e5143606 # v1.15.4 + with: + toolchain: stable + - run: cargo doc --locked --no-deps --lib --examples diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml new file mode 100644 index 0000000..e17bb50 --- /dev/null +++ b/.github/workflows/lint.yml @@ -0,0 +1,44 @@ +name: Lint + +on: + pull_request: + paths: + - '**/*.rs' + - '**/Cargo.toml' + - '**/Cargo.lock' + - '.github/workflows/lint.yml' + push: + paths: + - '**/*.rs' + - '**/Cargo.toml' + - '**/Cargo.lock' + - '.github/workflows/lint.yml' + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + rustfmt: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - uses: actions-rust-lang/setup-rust-toolchain@150fca883cd4034361b621bd4e6a9d34e5143606 # v1.15.4 + with: + toolchain: stable + components: rustfmt + - run: cargo fmt --all -- --check + + clippy: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - uses: actions-rust-lang/setup-rust-toolchain@150fca883cd4034361b621bd4e6a9d34e5143606 # v1.15.4 + with: + toolchain: stable + components: clippy + - run: cargo clippy --locked --workspace --all-targets --all-features -- -D warnings diff --git a/.github/workflows/spdx.yml b/.github/workflows/spdx.yml new file mode 100644 index 0000000..a2c9bdf --- /dev/null +++ b/.github/workflows/spdx.yml @@ -0,0 +1,28 @@ +name: SPDX License Check + +on: + pull_request: + paths: + - '**/*.rs' + - ".github/workflows/spdx.yml" + push: + paths: + - '**/*.rs' + - ".github/workflows/spdx.yml" + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + spdx: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - uses: enarx/spdx@d4020ee98e3101dd487c5184f27c6a6fb4f88709 + with: + licenses: Apache-2.0 diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..3403fb4 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,66 @@ +name: Test + +on: + push: + paths: + - '**/*.rs' + - '**/Cargo.toml' + - '**/Cargo.lock' + - '.github/workflows/test.yml' + pull_request: + paths: + - '**/*.rs' + - '**/Cargo.toml' + - '**/Cargo.lock' + - '.github/workflows/test.yml' + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +env: + CARGO_TERM_COLOR: always + +jobs: + test: + runs-on: ${{ matrix.runner }} + strategy: + fail-fast: false + matrix: + toolchain: + - "1.87.0" + - stable + os-arch: + - ubuntu-24.04-x86_64 + - ubuntu-24.04-aarch64 + - macos-x86_64 + - macos-aarch64 + - windows-x86_64 + include: + - os-arch: ubuntu-24.04-x86_64 + runner: ubuntu-24.04 + target: x86_64-unknown-linux-gnu + - os-arch: ubuntu-24.04-aarch64 + runner: ubuntu-24.04-arm + target: aarch64-unknown-linux-gnu + - os-arch: macos-x86_64 + runner: macos-26-intel + target: x86_64-apple-darwin + - os-arch: macos-aarch64 + runner: macos-26 + target: aarch64-apple-darwin + - os-arch: windows-x86_64 + runner: windows-2025 + target: x86_64-pc-windows-gnu + + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - uses: actions-rust-lang/setup-rust-toolchain@150fca883cd4034361b621bd4e6a9d34e5143606 # v1.15.4 + with: + toolchain: ${{ matrix.toolchain }} + target: ${{ matrix.target }} + - run: cargo test --locked --all-targets --target ${{ matrix.target }} diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..6384e44 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,302 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "anstream" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000" + +[[package]] +name = "anstyle-parse" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" +dependencies = [ + "windows-sys", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" +dependencies = [ + "anstyle", + "once_cell_polyfill", + "windows-sys", +] + +[[package]] +name = "block-buffer" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdd35008169921d80bc60d3d0ab416eecb028c4cd653352907921d95084790be" +dependencies = [ + "hybrid-array", +] + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "clap" +version = "4.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ddb117e43bbf7dacf0a4190fef4d345b9bad68dfc649cb349e7d17d28428e51" +dependencies = [ + "clap_builder", + "clap_derive", +] + +[[package]] +name = "clap_builder" +version = "4.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "714a53001bf66416adb0e2ef5ac857140e7dc3a0c48fb28b2f10762fc4b5069f" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_derive" +version = "4.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2ce8604710f6733aa641a2b3731eaa1e8b3d9973d5e3565da11800813f997a9" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "clap_lex" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9" + +[[package]] +name = "colorchoice" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570" + +[[package]] +name = "const-oid" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6ef517f0926dd24a1582492c791b6a4818a4d94e789a334894aa15b0d12f55c" + +[[package]] +name = "cpufeatures" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b2a41393f66f16b0823bb79094d54ac5fbd34ab292ddafb9a0456ac9f87d201" +dependencies = [ + "libc", +] + +[[package]] +name = "crypto-common" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77727bb15fa921304124b128af125e7e3b968275d1b108b379190264f4423710" +dependencies = [ + "hybrid-array", +] + +[[package]] +name = "digest" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4850db49bf08e663084f7fb5c87d202ef91a3907271aff24a94eb97ff039153c" +dependencies = [ + "block-buffer", + "const-oid", + "crypto-common", +] + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "hybrid-array" +version = "0.4.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3944cf8cf766b40e2a1a333ee5e9b563f854d5fa49d6a8ca2764e97c6eddb214" +dependencies = [ + "typenum", +] + +[[package]] +name = "ima-parser" +version = "0.1.0" +dependencies = [ + "clap", + "sha1", + "sha2", + "thiserror", +] + +[[package]] +name = "is_terminal_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" + +[[package]] +name = "libc" +version = "0.2.185" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52ff2c0fe9bc6cb6b14a0592c2ff4fa9ceb83eea9db979b0487cd054946a2b8f" + +[[package]] +name = "once_cell_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" + +[[package]] +name = "proc-macro2" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "sha1" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aacc4cc499359472b4abe1bf11d0b12e688af9a805fa5e3016f9a386dc2d0214" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "sha2" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "446ba717509524cb3f22f17ecc096f10f4822d76ab5c0b9822c5f9c284e825f4" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "syn" +version = "2.0.117" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "thiserror" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "typenum" +version = "1.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..54ff3be --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,34 @@ +[package] +name = "ima-parser" +version = "0.1.0" +edition = "2024" +rust-version = "1.87.0" +license = "Apache-2.0" +readme = "README.md" +exclude = [".gitignore", ".gitattributes", ".github/*", "testdata/*"] + +[features] +default = ["hash"] +hash = ["dep:sha1", "dep:sha2"] + +[dependencies] +thiserror = "2.0.18" +sha1 = { version = "0.11.0", optional = true } +sha2 = { version = "0.11.0", optional = true } + +[dev-dependencies] +sha1 = "0.11.0" +sha2 = "0.11.0" +clap = { version = "4.6.1", features = ["derive"] } + +[[example]] +name = "parse_binary_log" +path = "examples/parse_binary_log.rs" + +[[example]] +name = "parse_ascii_log" +path = "examples/parse_ascii_log.rs" + +[[example]] +name = "parse_policy" +path = "examples/parse_policy.rs" diff --git a/LICENSE b/LICENSE index 261eeb9..56aa9c8 100644 --- a/LICENSE +++ b/LICENSE @@ -186,7 +186,7 @@ same "printed page" as the copyright notice for easier identification within third-party archives. - Copyright [yyyy] [name of copyright owner] + Copyright 2026 Takuma IMAMURA Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/README.md b/README.md index 66f6d6b..c3d34fd 100644 --- a/README.md +++ b/README.md @@ -1 +1,112 @@ -# ima-parser \ No newline at end of file +# ima-parser + +![SemVer: pre-release](https://img.shields.io/badge/ima--parser-pre--release-blue) +![MSRV: 1.87.0](https://img.shields.io/badge/MSRV-1.87.0-brown.svg) +[![License: Apache-2.0](https://img.shields.io/badge/License-Apache--2.0-red.svg)](https://www.apache.org/licenses/LICENSE-2.0) + + + +Type definitions and parsers for the Linux **Integrity Measurement +Architecture** (IMA), based on the upstream specification at +. + +Two artefacts produced by IMA are supported: + +* The **event log** (measurement list / integrity log) exposed by the + kernel via `securityfs`: + * `/sys/kernel/security/ima/binary_runtime_measurements` + * `/sys/kernel/security/ima/ascii_runtime_measurements` + + Both *binary* and *ASCII* representations can be parsed, and the + `template_hash` of every event can be recomputed and verified using the + built-in [`Hasher`](https://docs.rs/ima-parser/latest/ima_parser/hash/trait.Hasher.html) implementations. + +* The **policy** file (`/etc/ima/ima-policy`, + `/sys/kernel/security/ima/policy`, …): an ordered list of rules + composed of an action, conditions and options. + +### Quick tour + +#### Parsing an IMA policy + +```rust +use ima_parser::policy::{parse_policy, Action}; + +let text = concat!( + "# measure all executables run\n", + "measure func=BPRM_CHECK\n", + "dont_measure fsmagic=0x9fa0\n", +); +let policy = parse_policy(text).unwrap(); +assert_eq!(policy.rules.len(), 2); +assert_eq!(policy.rules[0].action, Action::Measure); +``` + +#### Parsing the binary log + +```rust +use ima_parser::log::{EventLogParser, ParseOptions}; +use ima_parser::hash::HashAlgorithm; + +let bytes = std::fs::read("/sys/kernel/security/ima/binary_runtime_measurements")?; +// The default file is hashed with SHA-1; for the SHA-256 variant use +// `with_template_hash_algorithm`. +let opts = ParseOptions::default() + .with_template_hash_algorithm(HashAlgorithm::Sha1); +for event in EventLogParser::new(bytes.as_slice(), opts) { + let event = event?; + println!("PCR {} {}", event.pcr_index, event.template_name); +} +``` + +#### Parsing the ASCII log + +```rust +use ima_parser::log::parse_ascii_log; + +let line = "10 91f34b5c671d73504b274a919661cf80dab1e127 ima-ng sha1:1801e1be3e65ef1eaa5c16617bec8f1274eaf6b3 boot_aggregate\n"; +let events = parse_ascii_log(line).unwrap(); +assert_eq!(events.len(), 1); +assert_eq!(events[0].template_name, "ima-ng"); +``` + +#### Recomputing a template hash + +```rust +use ima_parser::hash::HashAlgorithm; +use ima_parser::log::parse_ascii_log; + +// Build a self-consistent synthetic event by first computing the +// template hash for a known (digest, filename) pair, then feeding the +// result back through the ASCII parser. +use sha1::{Digest, Sha1}; +let filedata_hex = "cd".repeat(20); +let filename = "/etc/hosts"; +let mut d_ng = Vec::new(); +d_ng.extend_from_slice(b"sha1"); +d_ng.push(b':'); +d_ng.push(0); +d_ng.extend_from_slice(&[0xcd; 20]); +let mut n_ng = Vec::new(); +n_ng.extend_from_slice(filename.as_bytes()); +n_ng.push(0); +let mut h = Sha1::new(); +h.update((d_ng.len() as u32).to_le_bytes()); +h.update(&d_ng); +h.update((n_ng.len() as u32).to_le_bytes()); +h.update(&n_ng); +let th_hex: String = h.finalize().iter().map(|b| format!("{:02x}", b)).collect(); + +let line = format!("10 {} ima-ng sha1:{} {}\n", th_hex, filedata_hex, filename); +let events = parse_ascii_log(&line).unwrap(); +assert_eq!(events[0].verify_template_hash(HashAlgorithm::Sha1), Some(true)); +``` + +### Cargo features + +* `hash` *(default)* — enables built-in template-hash computation via the + `sha1` and `sha2` crates from RustCrypto. Disabling it removes both + dependencies; you can still implement the [`Hasher`](https://docs.rs/ima-parser/latest/ima_parser/hash/trait.Hasher.html) + trait against your own crypto stack. + + diff --git a/examples/parse_ascii_log.rs b/examples/parse_ascii_log.rs new file mode 100644 index 0000000..9ebe641 --- /dev/null +++ b/examples/parse_ascii_log.rs @@ -0,0 +1,118 @@ +// SPDX-License-Identifier: Apache-2.0 + +//! Read an ASCII IMA event log and print a summary of each event. +//! +//! Usage: +//! +//! ```sh +//! cargo run --example parse_ascii_log -- [--algo sha256] [--verify] +//! ``` + +use std::fs; +use std::path::PathBuf; +use std::process::ExitCode; + +use clap::{Parser, ValueEnum}; +use ima_parser::hash::HashAlgorithm; +use ima_parser::log::{TemplateData, parse_ascii_log}; + +/// Parse an ASCII IMA event log +/// (`/sys/kernel/security/ima/ascii_runtime_measurements[_]`). +#[derive(Debug, Parser)] +#[command(name = "parse_ascii_log", about, version)] +struct Args { + /// Path to the ASCII log. Use `-` to read from standard input. + path: PathBuf, + + /// Algorithm assumed when re-computing the per-event template hash with + /// `--verify`. Defaults to SHA-1, the algorithm of the legacy + /// `ascii_runtime_measurements` file. + #[arg(long, value_enum, default_value_t = AlgoArg::Sha1)] + algo: AlgoArg, + + /// Recompute every event's template hash and print whether the stored + /// digest matches the recomputation. + #[arg(long)] + verify: bool, +} + +#[derive(Debug, Clone, Copy, ValueEnum)] +enum AlgoArg { + Sha1, + Sha224, + Sha256, + Sha384, + Sha512, +} + +impl From for HashAlgorithm { + fn from(a: AlgoArg) -> Self { + match a { + AlgoArg::Sha1 => HashAlgorithm::Sha1, + AlgoArg::Sha224 => HashAlgorithm::Sha224, + AlgoArg::Sha256 => HashAlgorithm::Sha256, + AlgoArg::Sha384 => HashAlgorithm::Sha384, + AlgoArg::Sha512 => HashAlgorithm::Sha512, + } + } +} + +fn read_input(path: &PathBuf) -> std::io::Result { + if path.as_os_str() == "-" { + let mut s = String::new(); + std::io::Read::read_to_string(&mut std::io::stdin().lock(), &mut s)?; + Ok(s) + } else { + fs::read_to_string(path) + } +} + +fn main() -> ExitCode { + let args = Args::parse(); + let algo: HashAlgorithm = args.algo.into(); + + let text = match read_input(&args.path) { + Ok(t) => t, + Err(e) => { + eprintln!("cannot read {}: {e}", args.path.display()); + return ExitCode::from(1); + } + }; + + let events = match parse_ascii_log(&text) { + Ok(e) => e, + Err(e) => { + eprintln!("parse error: {e}"); + return ExitCode::from(1); + } + }; + + for ev in &events { + let hint = match &ev.template_data { + TemplateData::Ima(e) => format!("{} (legacy ima)", e.filename), + TemplateData::ImaNg(e) => format!("{} [{}]", e.filename, e.digest), + TemplateData::ImaSig(e) => { + format!("{} [{}] sig={}B", e.filename, e.digest, e.signature.len()) + } + TemplateData::ImaBuf(e) => format!("{} [{}] buf={}B", e.name, e.digest, e.buf.len()), + TemplateData::Unknown(fields) => format!("{} unknown-field(s)", fields.len()), + }; + if args.verify { + #[cfg(feature = "hash")] + let ok = match ev.verify_template_hash(algo) { + Some(true) => "true", + Some(false) => "false", + None => "unsupported-algo", + }; + #[cfg(not(feature = "hash"))] + let ok = "no-hash-feature"; + println!( + "PCR={:>2} {:<8} hash-ok={} {hint}", + ev.pcr_index, ev.template_name, ok + ); + } else { + println!("PCR={:>2} {:<8} {hint}", ev.pcr_index, ev.template_name); + } + } + ExitCode::SUCCESS +} diff --git a/examples/parse_binary_log.rs b/examples/parse_binary_log.rs new file mode 100644 index 0000000..bfbafc0 --- /dev/null +++ b/examples/parse_binary_log.rs @@ -0,0 +1,129 @@ +// SPDX-License-Identifier: Apache-2.0 + +//! Read a binary IMA event log and print a summary of each event. +//! +//! Usage: +//! +//! ```sh +//! cargo run --example parse_binary_log -- [--algo sha256] [--endian little|big] [--verify] +//! ``` + +use std::fs::File; +use std::io::BufReader; +use std::path::PathBuf; +use std::process::ExitCode; + +use clap::{Parser, ValueEnum}; +use ima_parser::hash::HashAlgorithm; +use ima_parser::log::{Endianness, EventLogParser, ParseOptions, TemplateData}; + +/// Parse a binary IMA event log and print a one-line summary per event. +#[derive(Debug, Parser)] +#[command(name = "parse_binary_log", about, version)] +struct Args { + /// Path to the binary log + /// (e.g. `/sys/kernel/security/ima/binary_runtime_measurements`). + path: PathBuf, + + /// Hash algorithm used to recompute / verify each event's template hash. + #[arg(long, value_enum, default_value_t = AlgoArg::Sha1)] + algo: AlgoArg, + + /// Byte order of the integer fields in the log. + #[arg(long, value_enum, default_value_t = EndianArg::Little)] + endian: EndianArg, + + /// Recompute every event's template hash and print whether it matches. + #[arg(long)] + verify: bool, +} + +#[derive(Debug, Clone, Copy, ValueEnum)] +enum AlgoArg { + Sha1, + Sha224, + Sha256, + Sha384, + Sha512, +} + +impl From for HashAlgorithm { + fn from(a: AlgoArg) -> Self { + match a { + AlgoArg::Sha1 => HashAlgorithm::Sha1, + AlgoArg::Sha224 => HashAlgorithm::Sha224, + AlgoArg::Sha256 => HashAlgorithm::Sha256, + AlgoArg::Sha384 => HashAlgorithm::Sha384, + AlgoArg::Sha512 => HashAlgorithm::Sha512, + } + } +} + +#[derive(Debug, Clone, Copy, ValueEnum)] +enum EndianArg { + Little, + Big, +} + +impl From for Endianness { + fn from(e: EndianArg) -> Self { + match e { + EndianArg::Little => Endianness::Little, + EndianArg::Big => Endianness::Big, + } + } +} + +fn main() -> ExitCode { + let args = Args::parse(); + let algo: HashAlgorithm = args.algo.into(); + + let file = match File::open(&args.path) { + Ok(f) => f, + Err(e) => { + eprintln!("cannot open {}: {e}", args.path.display()); + return ExitCode::from(1); + } + }; + + let opts = ParseOptions::default() + .with_endianness(args.endian.into()) + .with_template_hash_algorithm(algo); + let parser = EventLogParser::new(BufReader::new(file), opts); + + for (i, ev) in parser.enumerate() { + let ev = match ev { + Ok(ev) => ev, + Err(e) => { + eprintln!("event {i}: parse error: {e}"); + return ExitCode::from(1); + } + }; + let hint = match &ev.template_data { + TemplateData::Ima(e) => format!("{} (legacy ima)", e.filename), + TemplateData::ImaNg(e) => format!("{} [{}]", e.filename, e.digest), + TemplateData::ImaSig(e) => { + format!("{} [{}] sig={}B", e.filename, e.digest, e.signature.len()) + } + TemplateData::ImaBuf(e) => format!("{} [{}] buf={}B", e.name, e.digest, e.buf.len()), + TemplateData::Unknown(fields) => format!("{} unknown-field(s)", fields.len()), + }; + if args.verify { + #[cfg(feature = "hash")] + let ok = match ev.verify_template_hash(algo) { + Some(true) => "true", + Some(false) => "false", + None => "unsupported-algo", + }; + #[cfg(not(feature = "hash"))] + let ok = "no-hash-feature"; + println!( + "PCR={:>2} {:<8} hash-ok={} {hint}", + ev.pcr_index, ev.template_name, ok + ); + } else { + println!("PCR={:>2} {:<8} {hint}", ev.pcr_index, ev.template_name); + } + } + ExitCode::SUCCESS +} diff --git a/examples/parse_policy.rs b/examples/parse_policy.rs new file mode 100644 index 0000000..626be0e --- /dev/null +++ b/examples/parse_policy.rs @@ -0,0 +1,47 @@ +// SPDX-License-Identifier: Apache-2.0 + +//! Read an IMA policy file and pretty-print the parsed structure. +//! +//! Usage: +//! +//! ```sh +//! cargo run --example parse_policy -- +//! ``` + +use std::fs; +use std::path::PathBuf; +use std::process::ExitCode; + +use clap::Parser; +use ima_parser::policy::parse_policy; + +/// Parse an IMA policy file and dump every rule. +#[derive(Debug, Parser)] +#[command(name = "parse_policy", about, version)] +struct Args { + /// Path to the IMA policy file (e.g. `/etc/ima/ima-policy`). + path: PathBuf, +} + +fn main() -> ExitCode { + let args = Args::parse(); + + let text = match fs::read_to_string(&args.path) { + Ok(t) => t, + Err(e) => { + eprintln!("cannot read {}: {e}", args.path.display()); + return ExitCode::from(1); + } + }; + let policy = match parse_policy(&text) { + Ok(p) => p, + Err(e) => { + eprintln!("parse error: {e}"); + return ExitCode::from(1); + } + }; + for (i, rule) in policy.rules.iter().enumerate() { + println!("rule {i:03}: {rule:#?}"); + } + ExitCode::SUCCESS +} diff --git a/src/error.rs b/src/error.rs new file mode 100644 index 0000000..dcab993 --- /dev/null +++ b/src/error.rs @@ -0,0 +1,85 @@ +// SPDX-License-Identifier: Apache-2.0 + +//! Error type used throughout this crate. + +use std::io; + +use thiserror::Error; + +/// Convenient `Result` alias used across the crate. +pub type Result = std::result::Result; + +/// All errors that can occur while parsing IMA artefacts. +#[derive(Debug, Error)] +#[non_exhaustive] +pub enum Error { + /// Underlying I/O error from the [`std::io::Read`] adapter. + #[error("I/O error: {0}")] + Io(#[from] io::Error), + + /// The byte stream ended in the middle of a record. + /// + /// `expected` is the number of bytes that were still required. + #[error("unexpected end of input (need {expected} more byte(s) for {context})")] + UnexpectedEof { + /// Number of bytes that were still required. + expected: usize, + /// Free-form description of what was being decoded. + context: &'static str, + }, + + /// A length field announced more bytes than the spec permits, so the + /// parser refused to allocate them. + #[error("invalid length {value} for {context} (limit {limit})")] + InvalidLength { + /// Length read from the stream. + value: u64, + /// Maximum length tolerated by the parser. + limit: u64, + /// Free-form description of which field overflowed. + context: &'static str, + }, + + /// A field that should be UTF-8 (template name, file name, hash algorithm + /// name, …) failed to decode. + #[error("invalid UTF-8 in {context}")] + InvalidUtf8 { + /// Free-form description of which field is invalid. + context: &'static str, + }, + + /// The hash-algorithm prefix in a `d-ng` style digest (e.g. `sha256:…`) + /// did not match any known algorithm name. + #[error("unknown hash algorithm `{0}`")] + UnknownHashAlgorithm(String), + + /// The decoded `Template Data` length disagrees with the sum of the + /// individual field lengths. + #[error("template data length mismatch (header says {header}, fields total {fields})")] + TemplateDataLengthMismatch { + /// Length declared in the event header. + header: usize, + /// Sum of the per-field lengths. + fields: usize, + }, + + /// Generic syntax error in the textual representation. + #[error("parse error: {0}")] + Parse(String), + + /// The text of an ASCII line/policy line had too few tokens. + #[error("malformed line: {0}")] + MalformedLine(String), +} + +impl Error { + /// Convenience constructor for [`Error::Parse`]. + pub fn parse>(s: S) -> Self { + Self::Parse(s.into()) + } + + /// Convenience constructor for [`Error::MalformedLine`]. + pub fn malformed>(s: S) -> Self { + Self::MalformedLine(s.into()) + } +} diff --git a/src/hash.rs b/src/hash.rs new file mode 100644 index 0000000..71b2c32 --- /dev/null +++ b/src/hash.rs @@ -0,0 +1,268 @@ +// SPDX-License-Identifier: Apache-2.0 + +//! Hash algorithms used by IMA. +//! +//! IMA templates store and reference several different hash algorithms; the +//! same algorithm identifiers appear in the `d-ng` / `d-ngv2` digest fields +//! of the event log, in the `hash_algo_name[]` table of the kernel, and in +//! `security.ima` extended attributes. +//! +//! This module provides: +//! +//! * [`HashAlgorithm`], an enum covering every algorithm currently known to +//! the IMA kernel code, along with its byte-length digest size and +//! canonical lower-case name. +//! * A small [`Hasher`] trait so template-hash computation can be plugged +//! into any crypto stack. When the `hash` feature is enabled (the default), +//! built-in implementations back [`HashAlgorithm::hasher`] with +//! `sha1`/`sha2` from RustCrypto. + +use core::fmt; + +use crate::error::Error; + +/// Hash algorithm identifiers used by IMA templates and digests. +/// +/// The names match the strings emitted by the kernel (for example the +/// `sha256:` prefix of an ASCII log's `filedata-hash` column, or the +/// `\0` fragment inside a `d-ng` field). +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +#[non_exhaustive] +pub enum HashAlgorithm { + /// MD4 – 16 byte digest. Legacy. + Md4, + /// MD5 – 16 byte digest. Legacy. + Md5, + /// SHA-1 – 20 byte digest. IMA's historical default. + Sha1, + /// RIPEMD-160 – 20 byte digest. + RmdRipeMd160, + /// SHA-224 – 28 byte digest. + Sha224, + /// RIPEMD-128 – 16 byte digest. + RmdRipeMd128, + /// RIPEMD-256 – 32 byte digest. + RmdRipeMd256, + /// RIPEMD-320 – 40 byte digest. + RmdRipeMd320, + /// Whirlpool-256 – 32 byte digest. + Wp256, + /// Whirlpool-384 – 48 byte digest. + Wp384, + /// Whirlpool-512 – 64 byte digest. + Wp512, + /// SHA-256 – 32 byte digest. Current IMA default on most distros. + Sha256, + /// SHA-384 – 48 byte digest. + Sha384, + /// SHA-512 – 64 byte digest. + Sha512, + /// SM3-256 – 32 byte digest (Chinese national standard). + Sm3_256, + /// Streebog-256 – 32 byte digest (Russian national standard). + Streebog256, + /// Streebog-512 – 64 byte digest. + Streebog512, + /// SHA3-256 – 32 byte digest. + Sha3_256, + /// SHA3-384 – 48 byte digest. + Sha3_384, + /// SHA3-512 – 64 byte digest. + Sha3_512, +} + +impl HashAlgorithm { + /// Returns the size, in bytes, of a raw digest produced by this + /// algorithm. + #[must_use] + pub const fn digest_size(&self) -> usize { + match self { + Self::Md4 | Self::Md5 | Self::RmdRipeMd128 => 16, + Self::Sha1 | Self::RmdRipeMd160 => 20, + Self::Sha224 => 28, + Self::Sha256 + | Self::RmdRipeMd256 + | Self::Wp256 + | Self::Sm3_256 + | Self::Streebog256 + | Self::Sha3_256 => 32, + Self::RmdRipeMd320 => 40, + Self::Sha384 | Self::Wp384 | Self::Sha3_384 => 48, + Self::Sha512 | Self::Wp512 | Self::Streebog512 | Self::Sha3_512 => 64, + } + } + + /// Canonical lower-case name as emitted by the kernel's `hash_algo_name` + /// table – this is exactly the string that appears before `:` in a + /// `d-ng` ASCII digest. + #[must_use] + pub const fn name(&self) -> &'static str { + match self { + Self::Md4 => "md4", + Self::Md5 => "md5", + Self::Sha1 => "sha1", + Self::RmdRipeMd160 => "rmd160", + Self::Sha224 => "sha224", + Self::RmdRipeMd128 => "rmd128", + Self::RmdRipeMd256 => "rmd256", + Self::RmdRipeMd320 => "rmd320", + Self::Wp256 => "wp256", + Self::Wp384 => "wp384", + Self::Wp512 => "wp512", + Self::Sha256 => "sha256", + Self::Sha384 => "sha384", + Self::Sha512 => "sha512", + Self::Sm3_256 => "sm3-256", + Self::Streebog256 => "streebog256", + Self::Streebog512 => "streebog512", + Self::Sha3_256 => "sha3-256", + Self::Sha3_384 => "sha3-384", + Self::Sha3_512 => "sha3-512", + } + } + + /// Parse an algorithm identifier from the case-insensitive kernel name. + /// + /// Accepts both the hyphenated form (`sm3-256`, `sha3-256`) and the + /// underscore form (`sm3_256`, `sha3_256`). + pub fn from_name(name: &str) -> Result { + let lower = name.trim().to_ascii_lowercase(); + let norm = lower.replace('_', "-"); + Ok(match norm.as_str() { + "md4" => Self::Md4, + "md5" => Self::Md5, + "sha1" => Self::Sha1, + "rmd160" | "ripemd-160" | "ripemd160" => Self::RmdRipeMd160, + "sha224" => Self::Sha224, + "rmd128" => Self::RmdRipeMd128, + "rmd256" => Self::RmdRipeMd256, + "rmd320" => Self::RmdRipeMd320, + "wp256" => Self::Wp256, + "wp384" => Self::Wp384, + "wp512" => Self::Wp512, + "sha256" => Self::Sha256, + "sha384" => Self::Sha384, + "sha512" => Self::Sha512, + "sm3-256" | "sm3" => Self::Sm3_256, + "streebog256" | "streebog-256" => Self::Streebog256, + "streebog512" | "streebog-512" => Self::Streebog512, + "sha3-256" => Self::Sha3_256, + "sha3-384" => Self::Sha3_384, + "sha3-512" => Self::Sha3_512, + _ => return Err(Error::UnknownHashAlgorithm(name.to_owned())), + }) + } + + /// Build a boxed streaming hasher for this algorithm. + /// + /// This is only available when the `hash` feature is enabled (it is by + /// default). Algorithms not backed by RustCrypto's `sha1`/`sha2` + /// (MD4, MD5, RIPEMD, Whirlpool, SM3, Streebog, SHA3, …) return `None`. + #[cfg(feature = "hash")] + #[must_use] + pub fn hasher(&self) -> Option> { + use crate::hash::backends::{ + Sha1Backend, Sha224Backend, Sha256Backend, Sha384Backend, Sha512Backend, + }; + match self { + Self::Sha1 => Some(Box::::default()), + Self::Sha224 => Some(Box::::default()), + Self::Sha256 => Some(Box::::default()), + Self::Sha384 => Some(Box::::default()), + Self::Sha512 => Some(Box::::default()), + _ => None, + } + } +} + +impl fmt::Display for HashAlgorithm { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str(self.name()) + } +} + +/// Minimal streaming hash interface used by the template-hash calculator. +/// +/// Intentionally tiny: IMA only ever needs to feed a handful of chunks and +/// then read out a finalized digest. Implementing it against your own +/// crypto stack (OpenSSL, ring, etc.) requires only two methods. +pub trait Hasher { + /// Feed more bytes into the digest. + fn update(&mut self, data: &[u8]); + + /// Finalize and return the digest as an owned byte vector. + /// + /// Calling `finalize` consumes the hasher; callers that want a repeated + /// computation must construct a fresh one. + fn finalize(self: Box) -> Vec; +} + +#[cfg(feature = "hash")] +pub(crate) mod backends { + //! Internal RustCrypto-backed implementations of [`Hasher`]. + + use super::Hasher; + + macro_rules! backend { + ($name:ident, $algo:ty) => { + #[derive(Default)] + pub(crate) struct $name(pub(crate) $algo); + + impl Hasher for $name { + fn update(&mut self, data: &[u8]) { + use ::sha2::Digest as _; + self.0.update(data); + } + fn finalize(self: Box) -> Vec { + use ::sha2::Digest as _; + self.0.finalize().to_vec() + } + } + }; + } + + backend!(Sha1Backend, ::sha1::Sha1); + backend!(Sha224Backend, ::sha2::Sha224); + backend!(Sha256Backend, ::sha2::Sha256); + backend!(Sha384Backend, ::sha2::Sha384); + backend!(Sha512Backend, ::sha2::Sha512); +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn digest_sizes_are_consistent() { + assert_eq!(HashAlgorithm::Sha1.digest_size(), 20); + assert_eq!(HashAlgorithm::Sha256.digest_size(), 32); + assert_eq!(HashAlgorithm::Sha512.digest_size(), 64); + } + + #[test] + fn roundtrip_names() { + for algo in [ + HashAlgorithm::Sha1, + HashAlgorithm::Sha256, + HashAlgorithm::Sha384, + HashAlgorithm::Sha512, + HashAlgorithm::Md5, + HashAlgorithm::Sm3_256, + ] { + let name = algo.name(); + assert_eq!(HashAlgorithm::from_name(name).unwrap(), algo); + } + } + + #[test] + fn from_name_normalizes_underscores() { + assert_eq!( + HashAlgorithm::from_name("sha3_256").unwrap(), + HashAlgorithm::Sha3_256 + ); + assert_eq!( + HashAlgorithm::from_name("SHA256").unwrap(), + HashAlgorithm::Sha256 + ); + } +} diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..fcf3204 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,118 @@ +// SPDX-License-Identifier: Apache-2.0 + +//! Type definitions and parsers for the Linux **Integrity Measurement +//! Architecture** (IMA), based on the upstream specification at +//! . +//! +//! Two artefacts produced by IMA are supported: +//! +//! * The **event log** (measurement list / integrity log) exposed by the +//! kernel via `securityfs`: +//! * `/sys/kernel/security/ima/binary_runtime_measurements` +//! * `/sys/kernel/security/ima/ascii_runtime_measurements` +//! +//! Both *binary* and *ASCII* representations can be parsed, and the +//! `template_hash` of every event can be recomputed and verified using the +//! built-in [`Hasher`](crate::hash::Hasher) implementations. +//! +//! * The **policy** file (`/etc/ima/ima-policy`, +//! `/sys/kernel/security/ima/policy`, …): an ordered list of rules +//! composed of an action, conditions and options. +//! +//! ## Quick tour +//! +//! ### Parsing an IMA policy +//! +//! ``` +//! use ima_parser::policy::{parse_policy, Action}; +//! +//! let text = concat!( +//! "# measure all executables run\n", +//! "measure func=BPRM_CHECK\n", +//! "dont_measure fsmagic=0x9fa0\n", +//! ); +//! let policy = parse_policy(text).unwrap(); +//! assert_eq!(policy.rules.len(), 2); +//! assert_eq!(policy.rules[0].action, Action::Measure); +//! ``` +//! +//! ### Parsing the binary log +//! +//! ```no_run +//! use ima_parser::log::{EventLogParser, ParseOptions}; +//! use ima_parser::hash::HashAlgorithm; +//! +//! let bytes = std::fs::read("/sys/kernel/security/ima/binary_runtime_measurements")?; +//! // The default file is hashed with SHA-1; for the SHA-256 variant use +//! // `with_template_hash_algorithm`. +//! let opts = ParseOptions::default() +//! .with_template_hash_algorithm(HashAlgorithm::Sha1); +//! for event in EventLogParser::new(bytes.as_slice(), opts) { +//! let event = event?; +//! println!("PCR {} {}", event.pcr_index, event.template_name); +//! } +//! # Ok::<(), Box>(()) +//! ``` +//! +//! ### Parsing the ASCII log +//! +//! ``` +//! use ima_parser::log::parse_ascii_log; +//! +//! let line = "10 91f34b5c671d73504b274a919661cf80dab1e127 ima-ng sha1:1801e1be3e65ef1eaa5c16617bec8f1274eaf6b3 boot_aggregate\n"; +//! let events = parse_ascii_log(line).unwrap(); +//! assert_eq!(events.len(), 1); +//! assert_eq!(events[0].template_name, "ima-ng"); +//! ``` +//! +//! ### Recomputing a template hash +//! +//! ``` +//! # #[cfg(feature = "hash")] { +//! use ima_parser::hash::HashAlgorithm; +//! use ima_parser::log::parse_ascii_log; +//! +//! // Build a self-consistent synthetic event by first computing the +//! // template hash for a known (digest, filename) pair, then feeding the +//! // result back through the ASCII parser. +//! use sha1::{Digest, Sha1}; +//! let filedata_hex = "cd".repeat(20); +//! let filename = "/etc/hosts"; +//! let mut d_ng = Vec::new(); +//! d_ng.extend_from_slice(b"sha1"); +//! d_ng.push(b':'); +//! d_ng.push(0); +//! d_ng.extend_from_slice(&[0xcd; 20]); +//! let mut n_ng = Vec::new(); +//! n_ng.extend_from_slice(filename.as_bytes()); +//! n_ng.push(0); +//! let mut h = Sha1::new(); +//! h.update((d_ng.len() as u32).to_le_bytes()); +//! h.update(&d_ng); +//! h.update((n_ng.len() as u32).to_le_bytes()); +//! h.update(&n_ng); +//! let th_hex: String = h.finalize().iter().map(|b| format!("{:02x}", b)).collect(); +//! +//! let line = format!("10 {} ima-ng sha1:{} {}\n", th_hex, filedata_hex, filename); +//! let events = parse_ascii_log(&line).unwrap(); +//! assert_eq!(events[0].verify_template_hash(HashAlgorithm::Sha1), Some(true)); +//! # } +//! ``` +//! +//! ## Cargo features +//! +//! * `hash` *(default)* — enables built-in template-hash computation via the +//! `sha1` and `sha2` crates from RustCrypto. Disabling it removes both +//! dependencies; you can still implement the [`Hasher`](crate::hash::Hasher) +//! trait against your own crypto stack. + +#![deny(missing_docs)] +#![warn(unreachable_pub)] +#![warn(rust_2018_idioms)] + +pub mod error; +pub mod hash; +pub mod log; +pub mod policy; + +pub use crate::error::{Error, Result}; diff --git a/src/log.rs b/src/log.rs new file mode 100644 index 0000000..a7eb505 --- /dev/null +++ b/src/log.rs @@ -0,0 +1,47 @@ +// SPDX-License-Identifier: Apache-2.0 + +//! IMA event log (a.k.a. *measurement list* / *integrity log*). +//! +//! The Linux kernel exposes the event log through `securityfs`: +//! +//! * `/sys/kernel/security/ima/binary_runtime_measurements[_]` – the +//! packed binary representation parsed by [`EventLogParser`]. +//! * `/sys/kernel/security/ima/ascii_runtime_measurements[_]` – a +//! human-readable, space-separated rendering parsed by [`parse_ascii_log`]. +//! +//! Both parsers produce the same [`Event`] type, so applications can mix and +//! match representations freely. The [`template_hash`] of every event can be +//! recomputed with [`Event::calculate_template_hash`] to verify the log's +//! self-consistency, independently of any TPM replay. +//! +//! [`template_hash`]: Event::template_hash + +mod ascii; +mod event; +mod parser; +mod template; +mod template_hash; + +pub use self::ascii::{parse_ascii_line, parse_ascii_log}; +pub use self::event::Event; +pub use self::parser::{Endianness, EventLogParser, ParseOptions}; +pub use self::template::{ + Digest, ImaBufEntry, ImaEntry, ImaNgEntry, ImaSigEntry, TemplateData, TemplateField, +}; + +/// Default PCR index used by IMA (`CONFIG_IMA_MEASURE_PCR_IDX`). +pub const DEFAULT_IMA_PCR: u32 = 10; + +/// Maximum length of an `n` (legacy) filename field, excluding the nul byte. +/// The legacy `ima` template pads the `n` field to this size plus one when +/// computing the template hash. +pub const IMA_EVENT_NAME_LEN_MAX: usize = 255; + +/// Template name of the legacy fixed-format template. +pub const IMA_TEMPLATE_NAME: &str = "ima"; +/// Template name of the `ima-ng` (next-generation) template. +pub const IMA_NG_TEMPLATE_NAME: &str = "ima-ng"; +/// Template name of the `ima-sig` template. +pub const IMA_SIG_TEMPLATE_NAME: &str = "ima-sig"; +/// Template name of the `ima-buf` template. +pub const IMA_BUF_TEMPLATE_NAME: &str = "ima-buf"; diff --git a/src/log/ascii.rs b/src/log/ascii.rs new file mode 100644 index 0000000..1faaf5e --- /dev/null +++ b/src/log/ascii.rs @@ -0,0 +1,390 @@ +// SPDX-License-Identifier: Apache-2.0 + +//! ASCII IMA event-log parser. +//! +//! The format of `/sys/kernel/security/ima/ascii_runtime_measurements_*` is: +//! +//! ```text +//! +//! ``` +//! +//! with SP (`' '`) as the token separator and LF (`'\n'`) as the record +//! separator. Template-specific fields are: +//! +//! * `ima`: ` ` +//! * `ima-ng`: `: ` +//! * `ima-sig`: `: []` +//! * `ima-buf`: `: ` +//! +//! The `filename` may contain spaces; the kernel escapes those as `\x20`. + +use crate::error::{Error, Result}; +use crate::hash::HashAlgorithm; + +use super::event::Event; +use super::template::{ + Digest, ImaBufEntry, ImaEntry, ImaNgEntry, ImaSigEntry, TemplateData, TemplateField, +}; +use super::{ + IMA_BUF_TEMPLATE_NAME, IMA_NG_TEMPLATE_NAME, IMA_SIG_TEMPLATE_NAME, IMA_TEMPLATE_NAME, +}; + +/// Parse an entire ASCII measurement log (one event per line). +/// +/// Lines that are blank or begin with `#` are skipped so the helper can be +/// reused with annotated files. +pub fn parse_ascii_log(input: &str) -> Result> { + let mut out = Vec::new(); + for (i, line) in input.lines().enumerate() { + let trimmed = line.trim(); + if trimmed.is_empty() || trimmed.starts_with('#') { + continue; + } + out.push( + parse_ascii_line(trimmed) + .map_err(|e| Error::parse(format!("line {}: {}", i + 1, e)))?, + ); + } + Ok(out) +} + +/// Parse exactly one ASCII event-log line. +pub fn parse_ascii_line(line: &str) -> Result { + let mut toks = line.split_whitespace(); + + let pcr = toks + .next() + .ok_or_else(|| Error::malformed("missing PCR index"))?; + let pcr_index: u32 = pcr + .parse() + .map_err(|_| Error::malformed(format!("invalid PCR `{pcr}`")))?; + + let template_hash_hex = toks + .next() + .ok_or_else(|| Error::malformed("missing template hash"))?; + let template_hash = decode_hex(template_hash_hex)?; + + let template_name = toks + .next() + .ok_or_else(|| Error::malformed("missing template name"))? + .to_owned(); + + let rest: Vec<&str> = toks.collect(); + let (template_data, template_data_raw) = parse_template_payload(&template_name, &rest)?; + + Ok(Event { + pcr_index, + template_hash, + template_name, + template_data, + template_data_raw, + }) +} + +fn parse_template_payload(template_name: &str, fields: &[&str]) -> Result<(TemplateData, Vec)> { + match template_name { + n if n == IMA_NG_TEMPLATE_NAME => parse_ima_ng(fields), + n if n == IMA_SIG_TEMPLATE_NAME => parse_ima_sig(fields), + n if n == IMA_BUF_TEMPLATE_NAME => parse_ima_buf(fields), + n if n == IMA_TEMPLATE_NAME => parse_legacy_ima(fields), + _ => { + // Unknown template: preserve every whitespace-separated token as + // its own opaque field so callers can still inspect the payload. + // We have no way to know the kernel's wire framing for this + // template, so `template_data_raw` stays empty and the template + // hash recomputation will hash ` || token` per field + // — close to the kernel's framing for most real templates, but + // not guaranteed to match for templates whose ASCII rendering + // re-encodes binary payloads (e.g. hex-encoded blobs). + let unknown = fields + .iter() + .map(|tok| TemplateField { + data: tok.as_bytes().to_vec(), + }) + .collect(); + Ok((TemplateData::Unknown(unknown), Vec::new())) + } + } +} + +fn parse_ima_ng(fields: &[&str]) -> Result<(TemplateData, Vec)> { + if fields.len() < 2 { + return Err(Error::malformed("ima-ng expects ")); + } + let digest = parse_prefixed_digest(fields[0])?; + let filename = unescape_filename(&fields[1..].join(" ")); + + // Rebuild the wire bytes so template-hash computation can run. + let raw = rebuild_ima_ng(&digest, &filename); + Ok((TemplateData::ImaNg(ImaNgEntry { digest, filename }), raw)) +} + +fn parse_ima_sig(fields: &[&str]) -> Result<(TemplateData, Vec)> { + if fields.len() < 2 { + return Err(Error::malformed( + "ima-sig expects []", + )); + } + let digest = parse_prefixed_digest(fields[0])?; + // The last token is the signature hex if it looks purely hex AND is + // non-empty AND we have more than 2 tokens. + let (filename, signature) = if fields.len() >= 3 && looks_like_hex(fields[fields.len() - 1]) { + let sig = decode_hex(fields[fields.len() - 1])?; + let name = unescape_filename(&fields[1..fields.len() - 1].join(" ")); + (name, sig) + } else { + (unescape_filename(&fields[1..].join(" ")), Vec::new()) + }; + let raw = rebuild_ima_sig(&digest, &filename, &signature); + Ok(( + TemplateData::ImaSig(ImaSigEntry { + digest, + filename, + signature, + }), + raw, + )) +} + +fn parse_ima_buf(fields: &[&str]) -> Result<(TemplateData, Vec)> { + if fields.len() < 3 { + return Err(Error::malformed( + "ima-buf expects ", + )); + } + let digest = parse_prefixed_digest(fields[0])?; + let buf = decode_hex(fields[fields.len() - 1])?; + let name = unescape_filename(&fields[1..fields.len() - 1].join(" ")); + let raw = rebuild_ima_buf(&digest, &name, &buf); + Ok((TemplateData::ImaBuf(ImaBufEntry { digest, name, buf }), raw)) +} + +fn parse_legacy_ima(fields: &[&str]) -> Result<(TemplateData, Vec)> { + if fields.len() < 2 { + return Err(Error::malformed("ima expects ")); + } + let digest_bytes = decode_hex(fields[0])?; + if digest_bytes.len() != 20 { + return Err(Error::InvalidLength { + value: digest_bytes.len() as u64, + limit: 20, + context: "legacy ima digest", + }); + } + let mut digest = [0u8; 20]; + digest.copy_from_slice(&digest_bytes); + let filename = unescape_filename(&fields[1..].join(" ")); + + // Reconstruct the 276-byte wire payload. + let mut raw = Vec::with_capacity(276); + raw.extend_from_slice(&digest); + let mut padded = [0u8; super::IMA_EVENT_NAME_LEN_MAX + 1]; + let name_bytes = filename.as_bytes(); + let take = name_bytes.len().min(padded.len() - 1); + padded[..take].copy_from_slice(&name_bytes[..take]); + raw.extend_from_slice(&padded); + + Ok((TemplateData::Ima(ImaEntry { digest, filename }), raw)) +} + +// --------------------------------------------------------------------- +// helpers +// --------------------------------------------------------------------- + +fn parse_prefixed_digest(s: &str) -> Result { + let (algo_name, hex) = s + .split_once(':') + .ok_or_else(|| Error::malformed(format!("expected `:`, got `{s}`")))?; + let algo = HashAlgorithm::from_name(algo_name)?; + let bytes = decode_hex(hex)?; + if bytes.len() != algo.digest_size() { + return Err(Error::InvalidLength { + value: bytes.len() as u64, + limit: algo.digest_size() as u64, + context: "digest", + }); + } + Ok(Digest::new(algo, bytes)) +} + +fn decode_hex(s: &str) -> Result> { + if !s.len().is_multiple_of(2) { + return Err(Error::parse(format!("odd-length hex string `{s}`"))); + } + let mut out = Vec::with_capacity(s.len() / 2); + let bytes = s.as_bytes(); + for chunk in bytes.chunks(2) { + let hi = hex_nibble(chunk[0])?; + let lo = hex_nibble(chunk[1])?; + out.push((hi << 4) | lo); + } + Ok(out) +} + +fn hex_nibble(b: u8) -> Result { + match b { + b'0'..=b'9' => Ok(b - b'0'), + b'a'..=b'f' => Ok(b - b'a' + 10), + b'A'..=b'F' => Ok(b - b'A' + 10), + _ => Err(Error::parse(format!("invalid hex nibble `{}`", b as char))), + } +} + +fn looks_like_hex(s: &str) -> bool { + !s.is_empty() && s.len().is_multiple_of(2) && s.bytes().all(|b| b.is_ascii_hexdigit()) +} + +/// Undo the kernel's `\x20`, `\\n`, `\\r`, `\\t`, `\\\\` escaping. +fn unescape_filename(s: &str) -> String { + let mut out = Vec::with_capacity(s.len()); + let bytes = s.as_bytes(); + let mut i = 0; + while i < bytes.len() { + let b = bytes[i]; + if b == b'\\' && i + 1 < bytes.len() { + match bytes[i + 1] { + b'n' => { + out.push(b'\n'); + i += 2; + continue; + } + b'r' => { + out.push(b'\r'); + i += 2; + continue; + } + b't' => { + out.push(b'\t'); + i += 2; + continue; + } + b'\\' => { + out.push(b'\\'); + i += 2; + continue; + } + b'x' | b'X' if i + 3 < bytes.len() => { + if let (Ok(hi), Ok(lo)) = (hex_nibble(bytes[i + 2]), hex_nibble(bytes[i + 3])) { + out.push((hi << 4) | lo); + i += 4; + continue; + } + } + _ => {} + } + } + out.push(b); + i += 1; + } + String::from_utf8_lossy(&out).into_owned() +} + +fn rebuild_ima_ng(digest: &Digest, filename: &str) -> Vec { + let d = super::template_hash::encode_d_ng(digest); + let n = super::template_hash::encode_n_ng(filename); + let mut out = Vec::with_capacity(8 + d.len() + n.len()); + out.extend_from_slice(&(d.len() as u32).to_le_bytes()); + out.extend_from_slice(&d); + out.extend_from_slice(&(n.len() as u32).to_le_bytes()); + out.extend_from_slice(&n); + out +} + +fn rebuild_ima_sig(digest: &Digest, filename: &str, sig: &[u8]) -> Vec { + let d = super::template_hash::encode_d_ng(digest); + let n = super::template_hash::encode_n_ng(filename); + let mut out = Vec::with_capacity(12 + d.len() + n.len() + sig.len()); + out.extend_from_slice(&(d.len() as u32).to_le_bytes()); + out.extend_from_slice(&d); + out.extend_from_slice(&(n.len() as u32).to_le_bytes()); + out.extend_from_slice(&n); + out.extend_from_slice(&(sig.len() as u32).to_le_bytes()); + out.extend_from_slice(sig); + out +} + +fn rebuild_ima_buf(digest: &Digest, name: &str, buf: &[u8]) -> Vec { + let d = super::template_hash::encode_d_ng(digest); + let n = super::template_hash::encode_n_ng(name); + let mut out = Vec::with_capacity(12 + d.len() + n.len() + buf.len()); + out.extend_from_slice(&(d.len() as u32).to_le_bytes()); + out.extend_from_slice(&d); + out.extend_from_slice(&(n.len() as u32).to_le_bytes()); + out.extend_from_slice(&n); + out.extend_from_slice(&(buf.len() as u32).to_le_bytes()); + out.extend_from_slice(buf); + out +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn parse_boot_aggregate_line() { + let line = "10 91f34b5c671d73504b274a919661cf80dab1e127 ima-ng \ + sha1:1801e1be3e65ef1eaa5c16617bec8f1274eaf6b3 boot_aggregate"; + let ev = parse_ascii_line(line).unwrap(); + assert_eq!(ev.pcr_index, 10); + assert_eq!(ev.template_name, "ima-ng"); + match &ev.template_data { + TemplateData::ImaNg(e) => { + assert_eq!(e.digest.algorithm, HashAlgorithm::Sha1); + assert_eq!(e.filename, "boot_aggregate"); + } + other => panic!("{:?}", other), + } + } + + #[test] + fn parse_comment_and_blank_lines() { + let input = "\ +# comment\n\ +\n\ +10 91f34b5c671d73504b274a919661cf80dab1e127 ima-ng \ +sha1:1801e1be3e65ef1eaa5c16617bec8f1274eaf6b3 /init\n\ +"; + let events = parse_ascii_log(input).unwrap(); + assert_eq!(events.len(), 1); + } + + #[test] + fn unescape_paths() { + assert_eq!(unescape_filename(r"/tmp/a\x20b"), "/tmp/a b"); + assert_eq!(unescape_filename(r"a\\b"), "a\\b"); + } + + #[test] + fn unknown_template_preserves_tokens() { + // A made-up template name that the parser doesn't recognise: the + // remaining tokens must come back as opaque fields so callers can + // still see them. + let line = "10 \ + deadbeefdeadbeefdeadbeefdeadbeefdeadbeef \ + weird-template foo bar baz"; + let ev = parse_ascii_line(line).unwrap(); + assert_eq!(ev.template_name, "weird-template"); + match &ev.template_data { + TemplateData::Unknown(fields) => { + let strs: Vec<&[u8]> = fields.iter().map(|f| f.data.as_slice()).collect(); + assert_eq!(strs, vec![&b"foo"[..], &b"bar"[..], &b"baz"[..]]); + } + other => panic!("{:?}", other), + } + } + + #[test] + fn sig_hex_recognised_as_last_field() { + let line = "10 f63c10947347c71ff205ebfde5971009af27b0ba ima-sig \ + sha256:6c118980083bccd259f069c2b3c3f3a2f5302d17a685409786564f4cf05b3939 \ + /usr/lib64/libgspell-1.so.1.0.0 0302046e6c10460100aa"; + let ev = parse_ascii_line(line).unwrap(); + match &ev.template_data { + TemplateData::ImaSig(e) => { + assert_eq!(e.filename, "/usr/lib64/libgspell-1.so.1.0.0"); + assert_eq!(e.signature.len(), 10); + } + other => panic!("{:?}", other), + } + } +} diff --git a/src/log/event.rs b/src/log/event.rs new file mode 100644 index 0000000..11990e2 --- /dev/null +++ b/src/log/event.rs @@ -0,0 +1,86 @@ +// SPDX-License-Identifier: Apache-2.0 + +//! The [`Event`] type, representing a single entry of an IMA event log. + +#[cfg(feature = "hash")] +use crate::hash::HashAlgorithm; + +use super::template::TemplateData; +use super::template_hash; + +/// A single IMA measurement event. +/// +/// Fields mirror the wire format described in the IMA specification: +/// +/// 1. `pcr_index` — PCR that `template_hash` was extended into (conventional +/// default: `10`). +/// 2. `template_hash` — digest covering the template's data bytes, computed +/// with the algorithm recorded in +/// [`ParseOptions::template_hash_algorithm`](super::parser::ParseOptions::with_template_hash_algorithm). +/// 3. `template_name` — ASCII identifier of the template (`"ima"`, +/// `"ima-ng"`, `"ima-sig"`, `"ima-buf"`, …). +/// 4. `template_data` — decoded payload; see [`TemplateData`]. +/// 5. `template_data_raw` — unparsed template-data bytes, retained so the +/// template hash can be recomputed even when the payload is +/// template-specific or we don't recognise the template. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct Event { + /// PCR index. Conventional default: `10`. + pub pcr_index: u32, + + /// Template hash as read from the log. + pub template_hash: Vec, + + /// Template name (e.g. `"ima-ng"`). + pub template_name: String, + + /// Decoded template payload. + pub template_data: TemplateData, + + /// Raw bytes that make up `template_data` on the wire, stored so + /// [`calculate_template_hash`](Event::calculate_template_hash) can run + /// without any re-encoding ambiguity. + pub template_data_raw: Vec, +} + +impl Event { + /// Recompute the `template_hash` with the given hash algorithm, using the + /// same rules as the Linux kernel. + /// + /// For the legacy `"ima"` template the hash covers `<20 bytes digest> || + /// <256 bytes zero-padded name>` with no length prefixes. For every + /// other template it covers the concatenation of ` || + /// ` for each field. + /// + /// Returns the freshly computed digest, or `None` when `algo` has no + /// built-in backend (MD4, MD5, RIPEMD, Whirlpool, SM3, Streebog, SHA-3, + /// …). Callers that want to plug in their own hash implementation for + /// such algorithms should use + /// [`calculate_template_hash_with`](Event::calculate_template_hash_with). + #[cfg(feature = "hash")] + #[must_use] + pub fn calculate_template_hash(&self, algo: HashAlgorithm) -> Option> { + template_hash::calculate_with(self, algo) + } + + /// Same as [`Event::calculate_template_hash`] but letting the caller + /// supply a custom [`Hasher`](crate::hash::Hasher) instead of relying on + /// the built-in RustCrypto backends. + pub fn calculate_template_hash_with(&self, hasher: H) -> Vec + where + H: crate::hash::Hasher + 'static, + { + template_hash::calculate_with_hasher(self, Box::new(hasher)) + } + + /// Returns `Some(true)` when the stored `template_hash` matches what a + /// fresh computation with the given algorithm yields, `Some(false)` when + /// they differ, and `None` when `algo` has no built-in backend so the + /// recomputation could not run. + #[cfg(feature = "hash")] + #[must_use] + pub fn verify_template_hash(&self, algo: HashAlgorithm) -> Option { + let computed = self.calculate_template_hash(algo)?; + Some(computed == self.template_hash) + } +} diff --git a/src/log/parser.rs b/src/log/parser.rs new file mode 100644 index 0000000..db15aa1 --- /dev/null +++ b/src/log/parser.rs @@ -0,0 +1,530 @@ +// SPDX-License-Identifier: Apache-2.0 + +//! Binary IMA event-log parser. +//! +//! Wire format (one record): +//! +//! ```text +//! +----------------------------------------------------------+ +//! | u32 PCR Index | +//! | [N]byte Template Data Hash (N = template_hash_size) | +//! | u32 Template Name Length | +//! | [L]byte Template Name (not NUL-terminated) | +//! | u32 Template Data Length (ABSENT for "ima") | +//! | [D]byte Template Data | +//! +----------------------------------------------------------+ +//! ``` +//! +//! Integers are host-endian by default. When the kernel was booted with +//! `ima_canonical_fmt` they are little-endian; use +//! [`ParseOptions::with_endianness`] to match. + +use std::io::Read; + +use crate::error::{Error, Result}; +use crate::hash::HashAlgorithm; + +use super::event::Event; +use super::template::{ + Digest, ImaBufEntry, ImaEntry, ImaNgEntry, ImaSigEntry, TemplateData, TemplateField, +}; +use super::{ + IMA_BUF_TEMPLATE_NAME, IMA_EVENT_NAME_LEN_MAX, IMA_NG_TEMPLATE_NAME, IMA_SIG_TEMPLATE_NAME, + IMA_TEMPLATE_NAME, +}; + +/// Byte order of the integer fields in the binary log. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] +pub enum Endianness { + /// Little-endian. What you get from `ima_canonical_fmt` and from every + /// little-endian host (virtually everything today). + #[default] + Little, + /// Big-endian. Host-native on big-endian machines that did not set the + /// canonical flag. + Big, + /// Whatever byte order the host running the parser uses. + Native, +} + +impl Endianness { + fn u32_from(self, bytes: [u8; 4]) -> u32 { + match self { + Self::Little => u32::from_le_bytes(bytes), + Self::Big => u32::from_be_bytes(bytes), + Self::Native => u32::from_ne_bytes(bytes), + } + } +} + +/// Tunables for the binary parser. +#[derive(Debug, Clone)] +pub struct ParseOptions { + endianness: Endianness, + template_hash_algorithm: HashAlgorithm, + max_field_len: usize, +} + +impl Default for ParseOptions { + fn default() -> Self { + Self { + endianness: Endianness::Little, + template_hash_algorithm: HashAlgorithm::Sha1, + max_field_len: 16 * 1024 * 1024, + } + } +} + +impl ParseOptions { + /// Override the byte order used to decode 32-bit length/count fields. + #[must_use] + pub fn with_endianness(mut self, endianness: Endianness) -> Self { + self.endianness = endianness; + self + } + + /// Override the hash algorithm used to size the fixed-width + /// `template_hash` field. The kernel writes one binary log per + /// configured PCR bank, so this will typically match the suffix of the + /// securityfs file you're parsing. + #[must_use] + pub fn with_template_hash_algorithm(mut self, algo: HashAlgorithm) -> Self { + self.template_hash_algorithm = algo; + self + } + + /// Cap on the size of any variable-length field the parser will try to + /// allocate. Default is 16 MiB, which comfortably accommodates every + /// real-world buffer but prevents a corrupted log from triggering a + /// pathological allocation. + #[must_use] + pub fn with_max_field_len(mut self, max: usize) -> Self { + self.max_field_len = max; + self + } + + /// Returns the configured endianness. + #[must_use] + pub fn endianness(&self) -> Endianness { + self.endianness + } + + /// Returns the configured template hash algorithm. + #[must_use] + pub fn template_hash_algorithm(&self) -> HashAlgorithm { + self.template_hash_algorithm + } +} + +/// Streaming parser that yields one [`Event`] per `next()` call. +/// +/// The parser reads lazily from a [`Read`]er, so it is safe to point at an +/// enormous measurement list without buffering all of it in memory. +pub struct EventLogParser { + reader: R, + opts: ParseOptions, + eof: bool, +} + +impl EventLogParser { + /// Construct a parser over `reader` with the given options. + pub fn new(reader: R, opts: ParseOptions) -> Self { + Self { + reader, + opts, + eof: false, + } + } + + /// Access the options this parser was constructed with. + #[must_use] + pub fn options(&self) -> &ParseOptions { + &self.opts + } + + fn read_exact(&mut self, buf: &mut [u8]) -> Result { + // Returns Ok(true) on success, Ok(false) on clean EOF at the first + // byte, or an error when EOF happens mid-record. + let mut filled = 0; + while filled < buf.len() { + match self.reader.read(&mut buf[filled..])? { + 0 => { + if filled == 0 { + return Ok(false); + } + return Err(Error::UnexpectedEof { + expected: buf.len() - filled, + context: "record body", + }); + } + n => filled += n, + } + } + Ok(true) + } + + fn read_u32(&mut self, context: &'static str) -> Result> { + let mut buf = [0u8; 4]; + let mut filled = 0; + while filled < 4 { + match self.reader.read(&mut buf[filled..])? { + 0 => { + if filled == 0 { + return Ok(None); + } + return Err(Error::UnexpectedEof { + expected: 4 - filled, + context, + }); + } + n => filled += n, + } + } + Ok(Some(self.opts.endianness.u32_from(buf))) + } + + fn read_vec(&mut self, len: usize, context: &'static str) -> Result> { + if len > self.opts.max_field_len { + return Err(Error::InvalidLength { + value: len as u64, + limit: self.opts.max_field_len as u64, + context, + }); + } + let mut buf = vec![0u8; len]; + if !self.read_exact(&mut buf)? { + return Err(Error::UnexpectedEof { + expected: len, + context, + }); + } + Ok(buf) + } + + fn read_event(&mut self) -> Result> { + // 1) PCR index – also serves as our EOF probe. + let pcr_index = match self.read_u32("PCR index")? { + Some(v) => v, + None => return Ok(None), + }; + + // 2) Template hash – fixed-width, no length prefix. + let hash_size = self.opts.template_hash_algorithm.digest_size(); + let template_hash = self.read_vec(hash_size, "template hash")?; + + // 3) Template name. + let name_len = self + .read_u32("template name length")? + .ok_or(Error::UnexpectedEof { + expected: 4, + context: "template name length", + })? as usize; + let name_bytes = self.read_vec(name_len, "template name")?; + let template_name = String::from_utf8(name_bytes).map_err(|_| Error::InvalidUtf8 { + context: "template name", + })?; + + // 4) Template data length (suppressed for the legacy "ima" template). + let (template_data_raw, template_data) = if template_name == IMA_TEMPLATE_NAME { + // Legacy `ima` template: 20-byte digest + 256-byte padded name. + let raw = self.read_vec(20 + IMA_EVENT_NAME_LEN_MAX + 1, "ima template data")?; + let entry = decode_legacy_ima(&raw)?; + (raw, TemplateData::Ima(entry)) + } else { + let data_len = self + .read_u32("template data length")? + .ok_or(Error::UnexpectedEof { + expected: 4, + context: "template data length", + })? as usize; + let raw = self.read_vec(data_len, "template data")?; + let decoded = decode_generic(&template_name, &raw)?; + (raw, decoded) + }; + + Ok(Some(Event { + pcr_index, + template_hash, + template_name, + template_data, + template_data_raw, + })) + } +} + +impl Iterator for EventLogParser { + type Item = Result; + + fn next(&mut self) -> Option { + if self.eof { + return None; + } + match self.read_event() { + Ok(Some(ev)) => Some(Ok(ev)), + Ok(None) => { + self.eof = true; + None + } + Err(e) => { + self.eof = true; + Some(Err(e)) + } + } + } +} + +// --------------------------------------------------------------------- +// Template decoders +// --------------------------------------------------------------------- + +fn decode_legacy_ima(raw: &[u8]) -> Result { + if raw.len() != 20 + IMA_EVENT_NAME_LEN_MAX + 1 { + return Err(Error::InvalidLength { + value: raw.len() as u64, + limit: (20 + IMA_EVENT_NAME_LEN_MAX + 1) as u64, + context: "legacy ima template data", + }); + } + let mut digest = [0u8; 20]; + digest.copy_from_slice(&raw[..20]); + + let name_slice = &raw[20..]; + let end = name_slice + .iter() + .position(|b| *b == 0) + .unwrap_or(name_slice.len()); + let filename = std::str::from_utf8(&name_slice[..end]) + .map_err(|_| Error::InvalidUtf8 { + context: "legacy ima filename", + })? + .to_owned(); + + Ok(ImaEntry { digest, filename }) +} + +fn decode_generic(template_name: &str, raw: &[u8]) -> Result { + let fields = split_fields(raw)?; + let decoded = match template_name { + n if n == IMA_NG_TEMPLATE_NAME => decode_ima_ng(&fields)?, + n if n == IMA_SIG_TEMPLATE_NAME => decode_ima_sig(&fields)?, + n if n == IMA_BUF_TEMPLATE_NAME => decode_ima_buf(&fields)?, + _ => TemplateData::Unknown( + fields + .into_iter() + .map(|data| TemplateField { data }) + .collect(), + ), + }; + Ok(decoded) +} + +/// Decode the framed `template_data` into a plain list of field payloads. +/// +/// Each field is ` || `. We always use little-endian +/// for these inner lengths – that's what the kernel writes, regardless of +/// `ima_canonical_fmt`, because the template-hash computation code uses +/// exactly those bytes. +fn split_fields(raw: &[u8]) -> Result>> { + let mut fields = Vec::new(); + let mut i = 0; + while i < raw.len() { + if i + 4 > raw.len() { + return Err(Error::UnexpectedEof { + expected: 4, + context: "template field length", + }); + } + let len = u32::from_le_bytes([raw[i], raw[i + 1], raw[i + 2], raw[i + 3]]) as usize; + i += 4; + let end = i.checked_add(len).ok_or(Error::InvalidLength { + value: len as u64, + limit: raw.len() as u64, + context: "template field", + })?; + if end > raw.len() { + return Err(Error::UnexpectedEof { + expected: end - raw.len(), + context: "template field body", + }); + } + fields.push(raw[i..end].to_vec()); + i = end; + } + Ok(fields) +} + +fn decode_ima_ng(fields: &[Vec]) -> Result { + if fields.len() != 2 { + return Err(Error::parse(format!( + "ima-ng expects 2 fields, got {}", + fields.len() + ))); + } + let digest = decode_d_ng(&fields[0])?; + let filename = decode_n_ng(&fields[1])?; + Ok(TemplateData::ImaNg(ImaNgEntry { digest, filename })) +} + +fn decode_ima_sig(fields: &[Vec]) -> Result { + if fields.len() != 3 { + return Err(Error::parse(format!( + "ima-sig expects 3 fields, got {}", + fields.len() + ))); + } + let digest = decode_d_ng(&fields[0])?; + let filename = decode_n_ng(&fields[1])?; + let signature = fields[2].clone(); + Ok(TemplateData::ImaSig(ImaSigEntry { + digest, + filename, + signature, + })) +} + +fn decode_ima_buf(fields: &[Vec]) -> Result { + if fields.len() != 3 { + return Err(Error::parse(format!( + "ima-buf expects 3 fields, got {}", + fields.len() + ))); + } + let digest = decode_d_ng(&fields[0])?; + let name = decode_n_ng(&fields[1])?; + let buf = fields[2].clone(); + Ok(TemplateData::ImaBuf(ImaBufEntry { digest, name, buf })) +} + +/// Decode a `d-ng` field: ` ":" "\0" `. +pub(crate) fn decode_d_ng(raw: &[u8]) -> Result { + // Minimum length: "x:\0" + 1 byte of digest + if raw.len() < 4 { + return Err(Error::parse("d-ng field too short")); + } + let sep = raw + .iter() + .position(|b| *b == 0) + .ok_or_else(|| Error::parse("d-ng field missing NUL separator"))?; + if sep < 2 || raw[sep - 1] != b':' { + return Err(Error::parse("d-ng field missing ':\\0' separator")); + } + let algo_name = std::str::from_utf8(&raw[..sep - 1]).map_err(|_| Error::InvalidUtf8 { + context: "d-ng algorithm name", + })?; + let algo = HashAlgorithm::from_name(algo_name)?; + let digest_bytes = raw[sep + 1..].to_vec(); + if digest_bytes.len() != algo.digest_size() { + return Err(Error::InvalidLength { + value: digest_bytes.len() as u64, + limit: algo.digest_size() as u64, + context: "d-ng digest", + }); + } + Ok(Digest::new(algo, digest_bytes)) +} + +/// Decode an `n-ng` field: ` \0`. +pub(crate) fn decode_n_ng(raw: &[u8]) -> Result { + // Drop the trailing nul if any. + let end = raw.iter().position(|b| *b == 0).unwrap_or(raw.len()); + let s = std::str::from_utf8(&raw[..end]).map_err(|_| Error::InvalidUtf8 { + context: "n-ng filename", + })?; + Ok(s.to_owned()) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn decode_d_ng_sha1() { + // "sha1" + ":" + "\0" + 20 bytes + let mut raw = Vec::new(); + raw.extend_from_slice(b"sha1"); + raw.push(b':'); + raw.push(0); + raw.extend_from_slice(&[0xAB; 20]); + let d = decode_d_ng(&raw).unwrap(); + assert_eq!(d.algorithm, HashAlgorithm::Sha1); + assert_eq!(d.bytes, vec![0xAB; 20]); + } + + #[test] + fn decode_n_ng_strips_nul() { + let raw = b"/usr/bin/ls\0"; + assert_eq!(decode_n_ng(raw).unwrap(), "/usr/bin/ls"); + } + + #[test] + fn parse_ima_ng_roundtrip() { + // Construct a single ima-ng event by hand. + let digest = [0xCDu8; 20]; + let mut d_ng = Vec::new(); + d_ng.extend_from_slice(b"sha1"); + d_ng.push(b':'); + d_ng.push(0); + d_ng.extend_from_slice(&digest); + + let mut n_ng = Vec::new(); + n_ng.extend_from_slice(b"/etc/hosts"); + n_ng.push(0); + + let mut td = Vec::new(); + td.extend_from_slice(&(d_ng.len() as u32).to_le_bytes()); + td.extend_from_slice(&d_ng); + td.extend_from_slice(&(n_ng.len() as u32).to_le_bytes()); + td.extend_from_slice(&n_ng); + + let mut event = Vec::new(); + event.extend_from_slice(&10u32.to_le_bytes()); // pcr + event.extend_from_slice(&[0xEE; 20]); // template hash (sha1 sized) + event.extend_from_slice(&(b"ima-ng".len() as u32).to_le_bytes()); + event.extend_from_slice(b"ima-ng"); + event.extend_from_slice(&(td.len() as u32).to_le_bytes()); + event.extend_from_slice(&td); + + let parser = EventLogParser::new(event.as_slice(), ParseOptions::default()); + let events: Vec<_> = parser.collect::>>().unwrap(); + assert_eq!(events.len(), 1); + let ev = &events[0]; + assert_eq!(ev.pcr_index, 10); + assert_eq!(ev.template_name, "ima-ng"); + match &ev.template_data { + TemplateData::ImaNg(e) => { + assert_eq!(e.filename, "/etc/hosts"); + assert_eq!(e.digest.algorithm, HashAlgorithm::Sha1); + assert_eq!(e.digest.bytes, digest); + } + other => panic!("expected ImaNg, got {:?}", other), + } + } + + #[test] + fn parse_legacy_ima() { + let mut td = Vec::new(); + td.extend_from_slice(&[0x11; 20]); + let mut name = [0u8; IMA_EVENT_NAME_LEN_MAX + 1]; + name[..5].copy_from_slice(b"/init"); + td.extend_from_slice(&name); + + let mut event = Vec::new(); + event.extend_from_slice(&10u32.to_le_bytes()); + event.extend_from_slice(&[0x22; 20]); + event.extend_from_slice(&(b"ima".len() as u32).to_le_bytes()); + event.extend_from_slice(b"ima"); + event.extend_from_slice(&td); + + let events: Vec<_> = EventLogParser::new(event.as_slice(), ParseOptions::default()) + .collect::>>() + .unwrap(); + assert_eq!(events.len(), 1); + match &events[0].template_data { + TemplateData::Ima(e) => { + assert_eq!(e.filename, "/init"); + assert_eq!(e.digest, [0x11; 20]); + } + other => panic!("expected Ima, got {:?}", other), + } + } +} diff --git a/src/log/template.rs b/src/log/template.rs new file mode 100644 index 0000000..916c636 --- /dev/null +++ b/src/log/template.rs @@ -0,0 +1,122 @@ +// SPDX-License-Identifier: Apache-2.0 + +//! Decoded IMA template data. +//! +//! Each [`Event`](crate::log::Event) carries a [`TemplateData`] value that +//! captures the semantics of the `template_data` bytes for the well-known +//! built-in templates. For anything we don't recognise, the event falls back +//! to [`TemplateData::Unknown`], which preserves the raw field layout so +//! callers can still access the bytes without losing information. + +use crate::hash::HashAlgorithm; + +/// A decoded `d-ng` / `d-ngv2` style digest: a named hash algorithm plus the +/// raw digest bytes. +/// +/// The length of `bytes` is always equal to `algorithm.digest_size()`. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct Digest { + /// Hash algorithm that produced the digest. + pub algorithm: HashAlgorithm, + /// Raw digest bytes. Length matches [`HashAlgorithm::digest_size`]. + pub bytes: Vec, +} + +impl Digest { + /// Convenience constructor. + pub fn new(algorithm: HashAlgorithm, bytes: Vec) -> Self { + Self { algorithm, bytes } + } +} + +impl core::fmt::Display for Digest { + /// Formats as `:`, matching the kernel's ASCII output. + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + write!(f, "{}:", self.algorithm)?; + for b in &self.bytes { + write!(f, "{:02x}", b)?; + } + Ok(()) + } +} + +/// Single `{length, bytes}` field inside an unknown template's +/// `template_data`. +/// +/// Used by [`TemplateData::Unknown`] to preserve the raw wire layout when we +/// see a template we don't know how to interpret. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct TemplateField { + /// Raw field bytes (value, without the 4-byte length header). + pub data: Vec, +} + +/// Legacy `ima` template payload. +/// +/// This template has a **fixed** layout on the wire: a 20-byte digest +/// (SHA-1/MD5, always 20 bytes including zero-padding of smaller hashes) +/// followed by a 256-byte NUL-terminated file name (the `n` field is always +/// zero-padded to `IMA_EVENT_NAME_LEN_MAX + 1` bytes). +/// +/// Note that, unique to this template, the enclosing event has **no** +/// `template_data_length` header – parsers consume exactly 276 bytes. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct ImaEntry { + /// 20-byte file-data digest (`d` field). + pub digest: [u8; 20], + /// File name (`n` field), NUL-terminated and padded to 256 bytes on the + /// wire; only the leading string portion is retained here. + pub filename: String, +} + +/// `ima-ng` template payload: `d-ng | n-ng`. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct ImaNgEntry { + /// Hash of the file contents (`d-ng` field). + pub digest: Digest, + /// File name (`n-ng` field). + pub filename: String, +} + +/// `ima-sig` template payload: `d-ng | n-ng | sig`. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct ImaSigEntry { + /// Hash of the file contents (`d-ng` field). + pub digest: Digest, + /// File name (`n-ng` field). + pub filename: String, + /// Raw contents of `security.ima` (may be empty when no signature was + /// attached). + pub signature: Vec, +} + +/// `ima-buf` template payload: `d-ng | n-ng | buf`. +/// +/// Used for `func=KEY_CHECK`, `func=CRITICAL_DATA`, `func=KEXEC_CMDLINE`… +/// rules, where the `buf` field is an arbitrary byte buffer (DER-encoded +/// X.509 certificate, kernel command line, …). +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct ImaBufEntry { + /// Hash of the buffer. + pub digest: Digest, + /// Logical name of the buffer (e.g. a keyring name). + pub name: String, + /// Raw buffer bytes. + pub buf: Vec, +} + +/// Decoded `template_data` for the built-in IMA templates. +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum TemplateData { + /// Legacy `ima` template. + Ima(ImaEntry), + /// `ima-ng` (next-generation) template — the modern default. + ImaNg(ImaNgEntry), + /// `ima-sig` template. + ImaSig(ImaSigEntry), + /// `ima-buf` template. + ImaBuf(ImaBufEntry), + /// Any other template: we preserve the per-field raw bytes as they were + /// framed by `u32 length | data` records on the wire. + Unknown(Vec), +} diff --git a/src/log/template_hash.rs b/src/log/template_hash.rs new file mode 100644 index 0000000..3cc4dbd --- /dev/null +++ b/src/log/template_hash.rs @@ -0,0 +1,156 @@ +// SPDX-License-Identifier: Apache-2.0 + +//! Template-hash calculator, kept strictly separate from the parser so the +//! same logic is shared between the binary and ASCII paths. + +#[cfg(feature = "hash")] +use crate::hash::HashAlgorithm; +use crate::hash::Hasher; + +use super::event::Event; +use super::template::{TemplateData, TemplateField}; +use super::{IMA_EVENT_NAME_LEN_MAX, IMA_TEMPLATE_NAME}; + +/// Internal: reconstruct the per-field framing that appears on the wire so +/// we can feed it into a hash. +/// +/// For the legacy `"ima"` template the framing rule in +/// `ima_calc_field_array_hash_tfm()` is: +/// +/// * `d` field – 20 bytes raw, **no** length prefix. +/// * `n` field – 256 bytes zero-padded, **no** length prefix. +/// +/// For every other template each field becomes ` || `. +pub(super) fn feed_event_into(hasher: &mut H, event: &Event) { + if event.template_name == IMA_TEMPLATE_NAME { + feed_legacy_ima(hasher, &event.template_data); + } else { + feed_generic(hasher, &event.template_data, &event.template_data_raw); + } +} + +fn feed_legacy_ima(hasher: &mut H, data: &TemplateData) { + match data { + TemplateData::Ima(entry) => { + // digest: 20 bytes, no length prefix + hasher.update(&entry.digest); + + // name: padded to 256 bytes (IMA_EVENT_NAME_LEN_MAX + 1) + let mut padded = [0u8; IMA_EVENT_NAME_LEN_MAX + 1]; + let name = entry.filename.as_bytes(); + let take = name.len().min(padded.len() - 1); // always keep nul + padded[..take].copy_from_slice(&name[..take]); + hasher.update(&padded); + } + // If for some reason we ended up with a non-Ima payload wearing the + // "ima" name, fall back to generic framing so we don't panic. + other => feed_generic(hasher, other, &[]), + } +} + +fn feed_generic(hasher: &mut H, data: &TemplateData, raw: &[u8]) { + // When we have the raw bytes we parsed out of the log, we can simply + // re-emit them field by field; the format on disk is already the hash + // input. Otherwise we re-serialise from the decoded structure. + if !raw.is_empty() { + feed_generic_from_raw(hasher, raw); + } else { + feed_generic_from_decoded(hasher, data); + } +} + +/// Walk a raw `template_data` blob, emitting ` || ` +/// for each framed field into the hasher. This matches exactly what the +/// kernel does in `ima_calc_field_array_hash_tfm()` for non-`"ima"` +/// templates. +fn feed_generic_from_raw(hasher: &mut H, raw: &[u8]) { + let mut i = 0; + while i + 4 <= raw.len() { + let len = u32::from_le_bytes([raw[i], raw[i + 1], raw[i + 2], raw[i + 3]]); + i += 4; + let end = i.saturating_add(len as usize); + if end > raw.len() { + // Malformed framing: stop rather than panic. The caller has + // already been told the data length header by the parser, so + // this should never happen in practice. + return; + } + hasher.update(&len.to_le_bytes()); + hasher.update(&raw[i..end]); + i = end; + } +} + +fn feed_generic_from_decoded(hasher: &mut H, data: &TemplateData) { + let fields = collect_fields(data); + for f in fields { + hasher.update(&(f.data.len() as u32).to_le_bytes()); + hasher.update(&f.data); + } +} + +fn collect_fields(data: &TemplateData) -> Vec { + match data { + TemplateData::Ima(_) => Vec::new(), + TemplateData::ImaNg(e) => vec![ + TemplateField { + data: encode_d_ng(&e.digest), + }, + TemplateField { + data: encode_n_ng(&e.filename), + }, + ], + TemplateData::ImaSig(e) => vec![ + TemplateField { + data: encode_d_ng(&e.digest), + }, + TemplateField { + data: encode_n_ng(&e.filename), + }, + TemplateField { + data: e.signature.clone(), + }, + ], + TemplateData::ImaBuf(e) => vec![ + TemplateField { + data: encode_d_ng(&e.digest), + }, + TemplateField { + data: encode_n_ng(&e.name), + }, + TemplateField { + data: e.buf.clone(), + }, + ], + TemplateData::Unknown(fields) => fields.clone(), + } +} + +pub(crate) fn encode_d_ng(digest: &crate::log::Digest) -> Vec { + let name = digest.algorithm.name().as_bytes(); + let mut out = Vec::with_capacity(name.len() + 2 + digest.bytes.len()); + out.extend_from_slice(name); + out.push(b':'); + out.push(0); + out.extend_from_slice(&digest.bytes); + out +} + +pub(crate) fn encode_n_ng(name: &str) -> Vec { + let mut out = Vec::with_capacity(name.len() + 1); + out.extend_from_slice(name.as_bytes()); + out.push(0); + out +} + +#[cfg(feature = "hash")] +pub(super) fn calculate_with(event: &Event, algo: HashAlgorithm) -> Option> { + let mut hasher = algo.hasher()?; + feed_event_into(hasher.as_mut(), event); + Some(hasher.finalize()) +} + +pub(super) fn calculate_with_hasher(event: &Event, mut hasher: Box) -> Vec { + feed_event_into(hasher.as_mut(), event); + hasher.finalize() +} diff --git a/src/policy.rs b/src/policy.rs new file mode 100644 index 0000000..c4854b8 --- /dev/null +++ b/src/policy.rs @@ -0,0 +1,40 @@ +// SPDX-License-Identifier: Apache-2.0 + +//! IMA policy definitions and parser. +//! +//! An IMA policy is an ordered list of rules that the kernel evaluates +//! (first-match wins per file access). Each rule has an *action*, zero or +//! more *conditions*, and zero or more *options*. The grammar is described +//! by `Documentation/ABI/testing/ima_policy` in the kernel: +//! +//! ```bnf +//! rule ::= action [condition ...] [option ...] +//! action ::= measure | dont_measure | appraise | dont_appraise | +//! audit | dont_audit | hash | dont_hash +//! condition ::= func= | mask=[^] | fsmagic= +//! | fsuuid= | fsname= | fs_subtype= +//! | uid | euid | gid | egid +//! | fowner | fgroup ; OP is one of `= < >` +//! | subj_user=|subj_role=|subj_type= +//! | obj_user= |obj_role= |obj_type= +//! option ::= digest_type= | template= +//! | permit_directio | appraise_type= +//! | appraise_flag= | appraise_algos= +//! | keyrings= | label= +//! | pcr= +//! ``` +//! +//! Lines that are blank or begin with `#` are ignored. +//! +//! Every documented keyword and value maps to a strongly-typed enum variant; +//! values not currently recognised by the parser are preserved through +//! `Other(String)` fallback variants for round-tripping new kernel additions. + +mod parser; +mod rule; + +pub use self::parser::{parse_policy, parse_policy_line}; +pub use self::rule::{ + Action, AppraiseAlgo, AppraiseFlag, AppraiseType, Condition, DigestType, Func, IdOp, + LabelEntry, Mask, MaskBit, Opt, Policy, Rule, Template, +}; diff --git a/src/policy/parser.rs b/src/policy/parser.rs new file mode 100644 index 0000000..12a044f --- /dev/null +++ b/src/policy/parser.rs @@ -0,0 +1,391 @@ +// SPDX-License-Identifier: Apache-2.0 + +//! Parser for the IMA policy syntax. +//! +//! The grammar follows `Documentation/ABI/testing/ima_policy` from the +//! Linux kernel. Numeric identity conditions (`uid`, `euid`, `gid`, `egid`, +//! `fowner`, `fgroup`) accept the three operator forms `=`, `<` and `>` as +//! the kernel's `ima_policy.c` does. + +use crate::error::{Error, Result}; + +use super::rule::{ + Action, AppraiseAlgo, AppraiseFlag, AppraiseType, Condition, DigestType, Func, IdOp, + LabelEntry, Mask, MaskBit, Opt, Policy, Rule, Template, +}; + +/// Parse a complete IMA policy. +/// +/// Lines that are blank or begin with `#` are skipped. All other lines must +/// be valid rules; any parse error aborts and is reported with the +/// offending line number. +pub fn parse_policy(input: &str) -> Result { + let mut rules = Vec::new(); + for (i, raw) in input.lines().enumerate() { + let line = strip_comment(raw).trim(); + if line.is_empty() { + continue; + } + rules.push( + parse_policy_line(line).map_err(|e| Error::parse(format!("line {}: {}", i + 1, e)))?, + ); + } + Ok(Policy { rules }) +} + +/// Parse a single policy rule from a single line. +pub fn parse_policy_line(line: &str) -> Result { + let mut tokens = line.split_whitespace(); + let action_str = tokens + .next() + .ok_or_else(|| Error::malformed("empty rule"))?; + let action = Action::parse(action_str) + .ok_or_else(|| Error::malformed(format!("unknown action `{action_str}`")))?; + + let mut conditions = Vec::new(); + let mut options = Vec::new(); + + for tok in tokens { + if let Some((key, op, value)) = split_key_op_value(tok) { + match parse_keyed(key, op, value)? { + Parsed::Condition(c) => conditions.push(c), + Parsed::Option(o) => options.push(o), + } + } else { + // Bare flags are always options in the IMA grammar. + options.push(match tok { + "permit_directio" => Opt::PermitDirectio, + other => Opt::Flag(other.to_owned()), + }); + } + } + + Ok(Rule { + action, + conditions, + options, + }) +} + +enum Parsed { + Condition(Condition), + Option(Opt), +} + +/// Split a token like `uid=1000` / `uid<1000` / `uid>1000` into +/// `(key, op_char, value)`. Returns `None` when the token contains no +/// recognised operator at all (a bare flag). +fn split_key_op_value(tok: &str) -> Option<(&str, char, &str)> { + // The kernel only recognises the operators on numeric identity + // conditions, but lexically every keyed token uses one of `= < >`. + // Find the first occurrence of any of them. + tok.char_indices() + .find(|(_, c)| matches!(c, '=' | '<' | '>')) + .map(|(i, c)| (&tok[..i], c, &tok[i + c.len_utf8()..])) +} + +fn parse_keyed(key: &str, op: char, value: &str) -> Result { + // Identity-style conditions accept `=`, `<`, `>`. Everything else + // must use plain `=`. + let id_op = match op { + '=' => IdOp::Eq, + '<' => IdOp::Lt, + '>' => IdOp::Gt, + _ => unreachable!(), + }; + + if matches!(key, "uid" | "euid" | "gid" | "egid" | "fowner" | "fgroup") { + let value = parse_dec_u32(value)?; + let cond = match key { + "uid" => Condition::Uid { op: id_op, value }, + "euid" => Condition::Euid { op: id_op, value }, + "gid" => Condition::Gid { op: id_op, value }, + "egid" => Condition::Egid { op: id_op, value }, + "fowner" => Condition::Fowner { op: id_op, value }, + "fgroup" => Condition::Fgroup { op: id_op, value }, + _ => unreachable!(), + }; + return Ok(Parsed::Condition(cond)); + } + + // For every other keyword the only legal operator is `=`. + if op != '=' { + return Err(Error::malformed(format!( + "operator `{op}` is only valid on uid/euid/gid/egid/fowner/fgroup, not `{key}`" + ))); + } + + Ok(match key { + // --- Conditions ------------------------------------------------- + "func" => Parsed::Condition(Condition::Func(Func::parse(value))), + "mask" => Parsed::Condition(Condition::Mask(parse_mask(value)?)), + "fsmagic" => Parsed::Condition(Condition::Fsmagic(parse_hex_u64(value)?)), + "fsuuid" => Parsed::Condition(Condition::Fsuuid(value.to_owned())), + "fsname" => Parsed::Condition(Condition::Fsname(value.to_owned())), + "fs_subtype" => Parsed::Condition(Condition::FsSubtype(value.to_owned())), + "subj_user" => Parsed::Condition(Condition::SubjUser(value.to_owned())), + "subj_role" => Parsed::Condition(Condition::SubjRole(value.to_owned())), + "subj_type" => Parsed::Condition(Condition::SubjType(value.to_owned())), + "obj_user" => Parsed::Condition(Condition::ObjUser(value.to_owned())), + "obj_role" => Parsed::Condition(Condition::ObjRole(value.to_owned())), + "obj_type" => Parsed::Condition(Condition::ObjType(value.to_owned())), + + // --- Options --------------------------------------------------- + "digest_type" => Parsed::Option(Opt::DigestType(DigestType::parse(value))), + "template" => Parsed::Option(Opt::Template(Template::parse(value))), + "appraise_type" => Parsed::Option(Opt::AppraiseType(AppraiseType::parse(value))), + "appraise_flag" => Parsed::Option(Opt::AppraiseFlag(AppraiseFlag::parse(value))), + "appraise_algos" => Parsed::Option(Opt::AppraiseAlgos(parse_appraise_algos(value)?)), + "keyrings" => Parsed::Option(Opt::Keyrings(parse_pipe_list(value, "keyrings")?)), + "label" => { + let entries = parse_pipe_list(value, "label")? + .into_iter() + .map(|s| LabelEntry::parse(&s)) + .collect(); + Parsed::Option(Opt::Label(entries)) + } + "pcr" => Parsed::Option(Opt::Pcr(parse_dec_u32(value)?)), + + other => Parsed::Option(Opt::Other { + key: other.to_owned(), + value: value.to_owned(), + }), + }) +} + +fn parse_mask(value: &str) -> Result { + let (negated, name) = match value.strip_prefix('^') { + Some(rest) => (true, rest), + None => (false, value), + }; + let bit = MaskBit::parse(name) + .ok_or_else(|| Error::malformed(format!("unknown mask bit `{name}`")))?; + Ok(Mask { negated, bit }) +} + +fn parse_appraise_algos(value: &str) -> Result> { + if value.is_empty() { + return Err(Error::malformed("empty appraise_algos list")); + } + let parts: Vec<&str> = value.split(',').collect(); + if parts.iter().any(|p| p.is_empty()) { + return Err(Error::malformed(format!( + "empty entry in appraise_algos `{value}`" + ))); + } + Ok(parts.into_iter().map(AppraiseAlgo::parse).collect()) +} + +fn parse_pipe_list(value: &str, what: &'static str) -> Result> { + if value.is_empty() { + return Err(Error::malformed(format!("empty {what} list"))); + } + let parts: Vec<&str> = value.split('|').collect(); + if parts.iter().any(|p| p.is_empty()) { + return Err(Error::malformed(format!("empty entry in {what} `{value}`"))); + } + Ok(parts.into_iter().map(str::to_owned).collect()) +} + +fn parse_hex_u64(s: &str) -> Result { + let clean = s + .strip_prefix("0x") + .or_else(|| s.strip_prefix("0X")) + .unwrap_or(s); + u64::from_str_radix(clean, 16) + .map_err(|_| Error::malformed(format!("invalid fsmagic value `{s}`"))) +} + +fn parse_dec_u32(s: &str) -> Result { + s.parse::() + .map_err(|_| Error::malformed(format!("invalid integer `{s}`"))) +} + +fn strip_comment(line: &str) -> &str { + match line.find('#') { + Some(i) => &line[..i], + None => line, + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::hash::HashAlgorithm; + + #[test] + fn parses_simple_measure_rule() { + let r = parse_policy_line("measure func=BPRM_CHECK").unwrap(); + assert_eq!(r.action, Action::Measure); + assert_eq!(r.conditions, vec![Condition::Func(Func::BprmCheck)]); + } + + #[test] + fn parses_mask_with_negation() { + let r = parse_policy_line("measure func=FILE_CHECK mask=^MAY_READ").unwrap(); + match &r.conditions[1] { + Condition::Mask(m) => { + assert!(m.negated); + assert_eq!(m.bit, MaskBit::MayRead); + } + other => panic!("{other:?}"), + } + } + + #[test] + fn rejects_unknown_mask_bit() { + let err = parse_policy_line("measure func=FILE_CHECK mask=hogehoge").unwrap_err(); + assert!(err.to_string().contains("unknown mask bit")); + } + + #[test] + fn parses_fsmagic_and_uid() { + let r = parse_policy_line("dont_measure fsmagic=0x9fa0 uid=1000").unwrap(); + assert_eq!(r.action, Action::DontMeasure); + assert!(r.conditions.contains(&Condition::Fsmagic(0x9fa0))); + assert!(r.conditions.contains(&Condition::Uid { + op: IdOp::Eq, + value: 1000, + })); + } + + #[test] + fn parses_id_operator_lt_gt() { + let r = parse_policy_line("measure uid<1000 fowner>0").unwrap(); + assert!(r.conditions.contains(&Condition::Uid { + op: IdOp::Lt, + value: 1000, + })); + assert!(r.conditions.contains(&Condition::Fowner { + op: IdOp::Gt, + value: 0, + })); + } + + #[test] + fn rejects_lt_gt_on_non_id_keys() { + let err = parse_policy_line("measure func assert_eq!(*t, AppraiseType::ImasigModsig), + other => panic!("{other:?}"), + } + } + + #[test] + fn parses_sigv3_appraise_type() { + let r = parse_policy_line("appraise func=BPRM_CHECK appraise_type=sigv3").unwrap(); + assert!(r.options.contains(&Opt::AppraiseType(AppraiseType::Sigv3))); + } + + #[test] + fn parses_digest_type_verity_and_template() { + let r = parse_policy_line("measure func=FILE_CHECK digest_type=verity template=ima-ngv2") + .unwrap(); + assert!(r.options.contains(&Opt::DigestType(DigestType::Verity))); + assert!(r.options.contains(&Opt::Template(Template::ImaNgv2))); + } + + #[test] + fn parses_permit_directio_flag() { + let r = parse_policy_line("measure func=FILE_CHECK permit_directio").unwrap(); + assert!(r.options.contains(&Opt::PermitDirectio)); + } + + #[test] + fn parses_appraise_algos_list() { + let r = + parse_policy_line("appraise func=SETXATTR_CHECK appraise_algos=sha256,sha384,sha512") + .unwrap(); + match r.options.last().unwrap() { + Opt::AppraiseAlgos(algos) => { + assert_eq!(algos.len(), 3); + assert_eq!(algos[0], AppraiseAlgo::Algorithm(HashAlgorithm::Sha256)); + assert_eq!(algos[1], AppraiseAlgo::Algorithm(HashAlgorithm::Sha384)); + assert_eq!(algos[2], AppraiseAlgo::Algorithm(HashAlgorithm::Sha512)); + } + other => panic!("{other:?}"), + } + } + + #[test] + fn parses_keyrings_pipe_list() { + let r = parse_policy_line("measure func=KEY_CHECK keyrings=.builtin_trusted_keys|.ima") + .unwrap(); + match r.options.last().unwrap() { + Opt::Keyrings(k) => { + assert_eq!( + k, + &vec![".builtin_trusted_keys".to_owned(), ".ima".to_owned()] + ); + } + other => panic!("{other:?}"), + } + } + + #[test] + fn parses_label_pipe_list_with_known_and_custom() { + let r = parse_policy_line("measure func=CRITICAL_DATA label=selinux|kernel_info|my_label") + .unwrap(); + match r.options.last().unwrap() { + Opt::Label(entries) => { + assert_eq!( + entries, + &vec![ + LabelEntry::Selinux, + LabelEntry::KernelInfo, + LabelEntry::Custom("my_label".to_owned()), + ] + ); + } + other => panic!("{other:?}"), + } + } + + #[test] + fn parses_pcr_as_option() { + let r = parse_policy_line("measure func=KEXEC_KERNEL_CHECK pcr=4").unwrap(); + assert!(r.options.contains(&Opt::Pcr(4))); + } + + #[test] + fn parses_file_mmap_alias_to_mmap_check() { + let r = parse_policy_line("measure func=FILE_MMAP mask=MAY_EXEC").unwrap(); + assert_eq!(r.conditions[0], Condition::Func(Func::MmapCheck)); + } + + #[test] + fn parses_path_check_alias_to_file_check() { + let r = parse_policy_line("measure func=PATH_CHECK").unwrap(); + assert_eq!(r.conditions[0], Condition::Func(Func::FileCheck)); + } + + #[test] + fn unknown_func_falls_back_to_other() { + let r = parse_policy_line("measure func=FUTURE_CHECK").unwrap(); + match &r.conditions[0] { + Condition::Func(Func::Other(s)) => assert_eq!(s, "FUTURE_CHECK"), + other => panic!("{other:?}"), + } + } + + #[test] + fn ignores_comments_and_blanks() { + let text = "\ +# ignore me\n\ +\n\ +measure func=BPRM_CHECK\n\ +dont_measure fsmagic=0x9fa0 uid=0 # trailing comment\n\ +"; + let p = parse_policy(text).unwrap(); + assert_eq!(p.rules.len(), 2); + } +} diff --git a/src/policy/rule.rs b/src/policy/rule.rs new file mode 100644 index 0000000..e111859 --- /dev/null +++ b/src/policy/rule.rs @@ -0,0 +1,707 @@ +// SPDX-License-Identifier: Apache-2.0 + +//! Data types describing a parsed IMA policy. +//! +//! The grammar follows the Linux kernel's +//! `Documentation/ABI/testing/ima_policy` and the parser in +//! `security/integrity/ima/ima_policy.c`. Every keyword and value documented +//! by the kernel maps to a dedicated enum variant; values the kernel would +//! reject (unknown `func=`, unknown `template=`, …) are still preserved +//! through `Other(String)` fallback variants so the parser can round-trip +//! future kernel additions without losing data. + +use core::fmt; + +use crate::hash::HashAlgorithm; + +/// A full IMA policy: a sequence of [`Rule`]s preserved in source order. +#[derive(Debug, Default, Clone, PartialEq, Eq)] +pub struct Policy { + /// Rules in the order they were written in the policy file. + pub rules: Vec, +} + +/// A single rule. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct Rule { + /// Action keyword (`measure`, `dont_measure`, …). + pub action: Action, + /// Conditions that must match for the rule to apply. + pub conditions: Vec, + /// Trailing options (`template=…`, `permit_directio`, `appraise_type=…`, …). + pub options: Vec, +} + +impl Rule { + /// Construct a rule with only an action and no conditions or options. + #[must_use] + pub fn new(action: Action) -> Self { + Self { + action, + conditions: Vec::new(), + options: Vec::new(), + } + } +} + +// --------------------------------------------------------------------------- +// Action +// --------------------------------------------------------------------------- + +/// Top-level action keyword of a rule. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub enum Action { + /// `measure` – append a measurement to the event log. + Measure, + /// `dont_measure` – suppress measurement. + DontMeasure, + /// `appraise` – enforce a stored good value. + Appraise, + /// `dont_appraise` – suppress appraisal. + DontAppraise, + /// `audit` – write a line to the audit subsystem. + Audit, + /// `dont_audit` – suppress audit output. + DontAudit, + /// `hash` – compute and store a digest in `security.ima`. + Hash, + /// `dont_hash` – suppress storing the digest. + DontHash, +} + +impl Action { + /// Parse an action keyword, returning `None` for unknown strings. + #[must_use] + pub fn parse(s: &str) -> Option { + Some(match s { + "measure" => Self::Measure, + "dont_measure" => Self::DontMeasure, + "appraise" => Self::Appraise, + "dont_appraise" => Self::DontAppraise, + "audit" => Self::Audit, + "dont_audit" => Self::DontAudit, + "hash" => Self::Hash, + "dont_hash" => Self::DontHash, + _ => return None, + }) + } + + /// Canonical keyword for this action. + #[must_use] + pub const fn as_str(&self) -> &'static str { + match self { + Self::Measure => "measure", + Self::DontMeasure => "dont_measure", + Self::Appraise => "appraise", + Self::DontAppraise => "dont_appraise", + Self::Audit => "audit", + Self::DontAudit => "dont_audit", + Self::Hash => "hash", + Self::DontHash => "dont_hash", + } + } +} + +impl core::str::FromStr for Action { + type Err = crate::error::Error; + + fn from_str(s: &str) -> Result { + Self::parse(s) + .ok_or_else(|| crate::error::Error::malformed(format!("unknown action `{s}`"))) + } +} + +impl fmt::Display for Action { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str(self.as_str()) + } +} + +// --------------------------------------------------------------------------- +// Func +// --------------------------------------------------------------------------- + +/// IMA `func=` target. Unknown values fall back to [`Func::Other`] so the +/// parser can still round-trip new kernel additions. +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub enum Func { + /// `BPRM_CHECK` – program execution via `execve`. + BprmCheck, + /// `MMAP_CHECK` – `mmap` with `PROT_EXEC`. Historical alias `FILE_MMAP`. + MmapCheck, + /// `MMAP_CHECK_REQPROT` – same as `MMAP_CHECK` for requested protection. + MmapCheckReqprot, + /// `CREDS_CHECK` – credential changes (`commit_creds`). + CredsCheck, + /// `FILE_CHECK` – open/read of a regular file. Historical alias `PATH_CHECK`. + FileCheck, + /// `MODULE_CHECK` – kernel-module load. + ModuleCheck, + /// `FIRMWARE_CHECK` – firmware blob load. + FirmwareCheck, + /// `KEXEC_KERNEL_CHECK` – kexec-load of a new kernel image. + KexecKernelCheck, + /// `KEXEC_INITRAMFS_CHECK` – kexec-load of the associated initramfs. + KexecInitramfsCheck, + /// `KEXEC_CMDLINE` – command-line string used for kexec. + KexecCmdline, + /// `POLICY_CHECK` – measure/appraise the IMA policy itself. + PolicyCheck, + /// `KEY_CHECK` – addition of a key to a keyring. + KeyCheck, + /// `CRITICAL_DATA` – ad-hoc critical-data buffers. + CriticalData, + /// `SETXATTR_CHECK` – `security.ima` xattr writes. + SetxattrCheck, + /// An unrecognised func name; preserved verbatim. + Other(String), +} + +impl Func { + /// Parse a `func=` value. Unknown names are returned as + /// [`Func::Other`] so round-tripping always succeeds. + #[must_use] + pub fn parse(s: &str) -> Self { + match s { + "BPRM_CHECK" => Self::BprmCheck, + // FILE_MMAP is the historical name accepted by the kernel. + "MMAP_CHECK" | "FILE_MMAP" => Self::MmapCheck, + "MMAP_CHECK_REQPROT" => Self::MmapCheckReqprot, + "CREDS_CHECK" => Self::CredsCheck, + // PATH_CHECK is a historical alias. + "FILE_CHECK" | "PATH_CHECK" => Self::FileCheck, + "MODULE_CHECK" => Self::ModuleCheck, + "FIRMWARE_CHECK" => Self::FirmwareCheck, + "KEXEC_KERNEL_CHECK" => Self::KexecKernelCheck, + "KEXEC_INITRAMFS_CHECK" => Self::KexecInitramfsCheck, + "KEXEC_CMDLINE" => Self::KexecCmdline, + "POLICY_CHECK" => Self::PolicyCheck, + "KEY_CHECK" => Self::KeyCheck, + "CRITICAL_DATA" => Self::CriticalData, + "SETXATTR_CHECK" => Self::SetxattrCheck, + other => Self::Other(other.to_owned()), + } + } + + /// Canonical keyword form. For unknown variants returns the preserved + /// original string. + #[must_use] + pub fn as_str(&self) -> &str { + match self { + Self::BprmCheck => "BPRM_CHECK", + Self::MmapCheck => "MMAP_CHECK", + Self::MmapCheckReqprot => "MMAP_CHECK_REQPROT", + Self::CredsCheck => "CREDS_CHECK", + Self::FileCheck => "FILE_CHECK", + Self::ModuleCheck => "MODULE_CHECK", + Self::FirmwareCheck => "FIRMWARE_CHECK", + Self::KexecKernelCheck => "KEXEC_KERNEL_CHECK", + Self::KexecInitramfsCheck => "KEXEC_INITRAMFS_CHECK", + Self::KexecCmdline => "KEXEC_CMDLINE", + Self::PolicyCheck => "POLICY_CHECK", + Self::KeyCheck => "KEY_CHECK", + Self::CriticalData => "CRITICAL_DATA", + Self::SetxattrCheck => "SETXATTR_CHECK", + Self::Other(s) => s, + } + } +} + +impl core::str::FromStr for Func { + type Err = core::convert::Infallible; + + fn from_str(s: &str) -> Result { + Ok(Self::parse(s)) + } +} + +impl fmt::Display for Func { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str(self.as_str()) + } +} + +// --------------------------------------------------------------------------- +// Mask +// --------------------------------------------------------------------------- + +/// One of the four `mask=` access bits documented in +/// `Documentation/ABI/testing/ima_policy`. +/// +/// The kernel parser accepts only one bit per `mask=` clause (it does not +/// split on `|`), so this is a single value rather than a set. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub enum MaskBit { + /// `MAY_EXEC` + MayExec, + /// `MAY_READ` + MayRead, + /// `MAY_WRITE` + MayWrite, + /// `MAY_APPEND` + MayAppend, +} + +impl MaskBit { + /// Parse a bare mask name (without the leading `^`). + #[must_use] + pub fn parse(s: &str) -> Option { + Some(match s { + "MAY_EXEC" => Self::MayExec, + "MAY_READ" => Self::MayRead, + "MAY_WRITE" => Self::MayWrite, + "MAY_APPEND" => Self::MayAppend, + _ => return None, + }) + } + + /// Canonical kernel keyword. + #[must_use] + pub const fn as_str(&self) -> &'static str { + match self { + Self::MayExec => "MAY_EXEC", + Self::MayRead => "MAY_READ", + Self::MayWrite => "MAY_WRITE", + Self::MayAppend => "MAY_APPEND", + } + } +} + +impl fmt::Display for MaskBit { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str(self.as_str()) + } +} + +/// Decoded `mask=` value: a single bit with optional `^` (NOT) prefix. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub struct Mask { + /// Whether the value was prefixed with `^` (meaning "not"). + pub negated: bool, + /// The selected mask bit. + pub bit: MaskBit, +} + +impl fmt::Display for Mask { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + if self.negated { + f.write_str("^")?; + } + fmt::Display::fmt(&self.bit, f) + } +} + +// --------------------------------------------------------------------------- +// Numeric comparison operator (for uid/euid/gid/egid/fowner/fgroup) +// --------------------------------------------------------------------------- + +/// Comparison operator for numeric identity conditions. +/// +/// The kernel accepts `key=value`, `keyvalue` for the six +/// identity conditions (`uid`, `euid`, `gid`, `egid`, `fowner`, `fgroup`). +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub enum IdOp { + /// `=` – exact match. + Eq, + /// `<` – less than. + Lt, + /// `>` – greater than. + Gt, +} + +impl IdOp { + /// Single-character form used in the policy syntax. + #[must_use] + pub const fn as_str(&self) -> &'static str { + match self { + Self::Eq => "=", + Self::Lt => "<", + Self::Gt => ">", + } + } +} + +impl fmt::Display for IdOp { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str(self.as_str()) + } +} + +// --------------------------------------------------------------------------- +// Condition +// --------------------------------------------------------------------------- + +/// A single condition inside a rule. +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum Condition { + /// `func=` + Func(Func), + /// `mask[=^]` + Mask(Mask), + /// `fsmagic=` – filesystem magic number. + Fsmagic(u64), + /// `fsuuid=` + Fsuuid(String), + /// `fsname=` + Fsname(String), + /// `fs_subtype=` + FsSubtype(String), + /// `uid` + Uid { + /// Comparison operator. + op: IdOp, + /// Numeric UID. + value: u32, + }, + /// `euid` + Euid { + /// Comparison operator. + op: IdOp, + /// Numeric EUID. + value: u32, + }, + /// `gid` + Gid { + /// Comparison operator. + op: IdOp, + /// Numeric GID. + value: u32, + }, + /// `egid` + Egid { + /// Comparison operator. + op: IdOp, + /// Numeric EGID. + value: u32, + }, + /// `fowner` + Fowner { + /// Comparison operator. + op: IdOp, + /// File-owner UID. + value: u32, + }, + /// `fgroup` + Fgroup { + /// Comparison operator. + op: IdOp, + /// File-group GID. + value: u32, + }, + /// `subj_user=` – LSM subject user. + SubjUser(String), + /// `subj_role=` + SubjRole(String), + /// `subj_type=` + SubjType(String), + /// `obj_user=` – LSM object user. + ObjUser(String), + /// `obj_role=` + ObjRole(String), + /// `obj_type=` + ObjType(String), +} + +// --------------------------------------------------------------------------- +// Option values +// --------------------------------------------------------------------------- + +/// `digest_type=` value. +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub enum DigestType { + /// `verity` – use fs-verity's file digest instead of the regular IMA hash. + Verity, + /// An unrecognised value; preserved verbatim. + Other(String), +} + +impl DigestType { + /// Parse a `digest_type=` value. + #[must_use] + pub fn parse(s: &str) -> Self { + match s { + "verity" => Self::Verity, + other => Self::Other(other.to_owned()), + } + } + + /// Canonical string form. + #[must_use] + pub fn as_str(&self) -> &str { + match self { + Self::Verity => "verity", + Self::Other(s) => s, + } + } +} + +impl fmt::Display for DigestType { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str(self.as_str()) + } +} + +/// `appraise_type=` value. +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub enum AppraiseType { + /// `imasig` – original / v2 IMA signature in `security.ima`. + Imasig, + /// `imasig|modsig` – `imasig` with appended kernel-module style signature + /// allowed. + ImasigModsig, + /// `sigv3` – signature format version 3. + Sigv3, + /// An unrecognised value; preserved verbatim. + Other(String), +} + +impl AppraiseType { + /// Parse an `appraise_type=` value. + #[must_use] + pub fn parse(s: &str) -> Self { + match s { + "imasig" => Self::Imasig, + "imasig|modsig" => Self::ImasigModsig, + "sigv3" => Self::Sigv3, + other => Self::Other(other.to_owned()), + } + } + + /// Canonical string form. + #[must_use] + pub fn as_str(&self) -> &str { + match self { + Self::Imasig => "imasig", + Self::ImasigModsig => "imasig|modsig", + Self::Sigv3 => "sigv3", + Self::Other(s) => s, + } + } +} + +impl fmt::Display for AppraiseType { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str(self.as_str()) + } +} + +/// `appraise_flag=` value. The kernel currently logs but ignores this option; +/// the only documented value is `check_blacklist`, kept here for round-trip. +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub enum AppraiseFlag { + /// `check_blacklist` – deprecated; appraisal already checks the + /// blacklist by default. + CheckBlacklist, + /// An unrecognised value; preserved verbatim. + Other(String), +} + +impl AppraiseFlag { + /// Parse an `appraise_flag=` value. + #[must_use] + pub fn parse(s: &str) -> Self { + match s { + "check_blacklist" => Self::CheckBlacklist, + other => Self::Other(other.to_owned()), + } + } + + /// Canonical string form. + #[must_use] + pub fn as_str(&self) -> &str { + match self { + Self::CheckBlacklist => "check_blacklist", + Self::Other(s) => s, + } + } +} + +impl fmt::Display for AppraiseFlag { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str(self.as_str()) + } +} + +/// One entry of an `appraise_algos=` comma-separated list. Known algorithm +/// names map to [`HashAlgorithm`]; unknown ones are preserved verbatim. +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub enum AppraiseAlgo { + /// A recognised hash algorithm. + Algorithm(HashAlgorithm), + /// An unrecognised algorithm name; preserved verbatim. + Other(String), +} + +impl AppraiseAlgo { + /// Parse a single algorithm name. + #[must_use] + pub fn parse(s: &str) -> Self { + match HashAlgorithm::from_name(s) { + Ok(a) => Self::Algorithm(a), + Err(_) => Self::Other(s.to_owned()), + } + } + + /// Canonical string form. + #[must_use] + pub fn as_str(&self) -> &str { + match self { + Self::Algorithm(a) => a.name(), + Self::Other(s) => s, + } + } +} + +impl fmt::Display for AppraiseAlgo { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str(self.as_str()) + } +} + +/// `template=` value. Lists every template documented by +/// `Documentation/security/IMA-templates.rst`; unknown names fall back to +/// [`Template::Other`]. +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub enum Template { + /// `ima` – legacy 20-byte digest + 256-byte zero-padded name. + Ima, + /// `ima-ng` – `d-ng | n-ng`. + ImaNg, + /// `ima-ngv2` – `d-ngv2 | n-ng`. Adds digest type prefix. + ImaNgv2, + /// `ima-sig` – `d-ng | n-ng | sig`. + ImaSig, + /// `ima-sigv2` – `d-ngv2 | n-ng | sig`. + ImaSigv2, + /// `ima-sigv3` – signature format v3 variant referenced in + /// `Documentation/ABI/testing/ima_policy`. + ImaSigv3, + /// `ima-buf` – `d-ng | n-ng | buf`. + ImaBuf, + /// `ima-modsig` – `d-ng | n-ng | sig | d-modsig | modsig`. + ImaModsig, + /// `evm-sig` – the EVM portable signature template. + EvmSig, + /// An unrecognised template name; preserved verbatim. + Other(String), +} + +impl Template { + /// Parse a `template=` value. + #[must_use] + pub fn parse(s: &str) -> Self { + match s { + "ima" => Self::Ima, + "ima-ng" => Self::ImaNg, + "ima-ngv2" => Self::ImaNgv2, + "ima-sig" => Self::ImaSig, + "ima-sigv2" => Self::ImaSigv2, + "ima-sigv3" => Self::ImaSigv3, + "ima-buf" => Self::ImaBuf, + "ima-modsig" => Self::ImaModsig, + "evm-sig" => Self::EvmSig, + other => Self::Other(other.to_owned()), + } + } + + /// Canonical string form. + #[must_use] + pub fn as_str(&self) -> &str { + match self { + Self::Ima => "ima", + Self::ImaNg => "ima-ng", + Self::ImaNgv2 => "ima-ngv2", + Self::ImaSig => "ima-sig", + Self::ImaSigv2 => "ima-sigv2", + Self::ImaSigv3 => "ima-sigv3", + Self::ImaBuf => "ima-buf", + Self::ImaModsig => "ima-modsig", + Self::EvmSig => "evm-sig", + Self::Other(s) => s, + } + } +} + +impl fmt::Display for Template { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str(self.as_str()) + } +} + +/// One entry of a `label=` pipe-separated list (used with +/// `func=CRITICAL_DATA`). +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub enum LabelEntry { + /// `selinux` – critical data emitted by SELinux. + Selinux, + /// `kernel_info` – critical data describing the kernel itself. + KernelInfo, + /// Any other unique grouping/limiting label. + Custom(String), +} + +impl LabelEntry { + /// Parse a single label entry. + #[must_use] + pub fn parse(s: &str) -> Self { + match s { + "selinux" => Self::Selinux, + "kernel_info" => Self::KernelInfo, + other => Self::Custom(other.to_owned()), + } + } + + /// Canonical string form. + #[must_use] + pub fn as_str(&self) -> &str { + match self { + Self::Selinux => "selinux", + Self::KernelInfo => "kernel_info", + Self::Custom(s) => s, + } + } +} + +impl fmt::Display for LabelEntry { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str(self.as_str()) + } +} + +// --------------------------------------------------------------------------- +// Option (renamed to `Opt` to avoid clashing with `core::option::Option`) +// --------------------------------------------------------------------------- + +/// A single option inside a rule. +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum Opt { + /// `digest_type=` + DigestType(DigestType), + /// `template=` – e.g. `ima-ng`, `ima-sig`, `ima-buf`. Only valid + /// when `action == measure`, but the parser does not enforce that here. + Template(Template), + /// `permit_directio` – flag, no value. + PermitDirectio, + /// `appraise_type=` + AppraiseType(AppraiseType), + /// `appraise_flag=` + AppraiseFlag(AppraiseFlag), + /// `appraise_algos=` – allowed algorithms for + /// `security.ima`. + AppraiseAlgos(Vec), + /// `keyrings=` – e.g. `.builtin_trusted_keys|.ima`. + Keyrings(Vec), + /// `label=` – grouping for `CRITICAL_DATA` rules. + Label(Vec), + /// `pcr=` – override destination PCR for measurements. + Pcr(u32), + /// Any other `key=value` pair we don't model as a dedicated variant. + Other { + /// Left-hand side of the `=`. + key: String, + /// Right-hand side of the `=`. + value: String, + }, + /// A bare flag (no `=`) that the parser didn't recognise. + Flag(String), +} diff --git a/testdata/logs/ascii_runtime_measurements_sha1 b/testdata/logs/ascii_runtime_measurements_sha1 new file mode 100644 index 0000000..05009ae --- /dev/null +++ b/testdata/logs/ascii_runtime_measurements_sha1 @@ -0,0 +1,43 @@ +10 c55f4e74c99597b51a5194a7f9b1b10bf6896351 ima-ng sha256:21390c748be864c56f9dcd5c47b4b32bc8f73f37124602deddcb02c40fea6ef9 boot_aggregate +10 c20aa15ce5a81da6c5b66869d6bd730f5d48d21e ima-ng sha256:c9c69ff607ea889ab7b9d6b7a40ec5220638818f26d3b740bee0e93addd7954a /usr/lib/modules/6.17.0-1012-gcp/kernel/fs/autofs/autofs4.ko.zst +10 3c70e42625745ee5fb322f4b75ae1f40b6be7993 ima-ng sha256:5034a95854cc6e36c9a34d0da6ea6ed68796a962438e4966415dc2f3f4df2410 /usr/lib/modules/6.17.0-1012-gcp/kernel/net/netfilter/x_tables.ko.zst +10 a980e1b8076b3776771108a313055d7e1ca4a097 ima-ng sha256:a96740f306375fac45631688322b8f5dc896bdbbdbd6d3faab01b7c1ed3d534b /usr/lib/modules/6.17.0-1012-gcp/kernel/net/ipv4/netfilter/ip_tables.ko.zst +10 72adf114e9ee68a94e4a4e7ce0a19fee9928e3e1 ima-ng sha256:99fbb924c0f7248bde358f3b9a0ac5f3d59aa8d682ffa71bf938701b609fbf37 /usr/lib/modules/6.17.0-1012-gcp/kernel/drivers/char/hw_random/virtio-rng.ko.zst +10 e467beeaa78c2dd433908efd2343d134a9e795bf ima-ng sha256:b0969949ff520d3a80f16e8387b0263e2a901159c49b1c6e65e1c9ef16f2ca44 /usr/lib/modules/6.17.0-1012-gcp/kernel/drivers/firmware/dmi-sysfs.ko.zst +10 c20897fd9261a7e44a4b3bc940848a927ff4677d ima-ng sha256:bc76e11c93f8dbb40ca91721481075c7b1dbe32dcfd42898fb044663febdc4ad /usr/lib/modules/6.17.0-1012-gcp/kernel/drivers/misc/vmw_vmci/vmw_vmci.ko.zst +10 2ae5e7128e49ce747c09fb3fca2d82175038cb3a ima-ng sha256:774832b0096fb586cb9786a00ec7c7425a0d49f7381fbf4c4414a484d3f3843d /usr/lib/modules/6.17.0-1012-gcp/kernel/net/vmw_vsock/vsock.ko.zst +10 37aed2ddbf3406a1749c58e927a0f2b9d57eca74 ima-ng sha256:85e28ed288b2f88361c7f736bef9c10ca6a108a092cb3fac2b97fd52b33c9b2b /usr/lib/modules/6.17.0-1012-gcp/kernel/net/vmw_vsock/vmw_vsock_vmci_transport.ko.zst +10 9311a667258b6a02f5db5c214b41bc8f8f267ef6 ima-ng sha256:b0c69d6f4b7cae2aca99b881fe73036ade35273af1f49d0c0a064013e9d8c987 /usr/lib/modules/6.17.0-1012-gcp/kernel/drivers/hv/hv_vmbus.ko.zst +10 a196a400dd398420632b06bc75059f7c3e433c10 ima-ng sha256:0c386274770800835b261ab110f5cdaf3d3ed314676ea2e0e13dc3d17aaa6d65 /usr/lib/modules/6.17.0-1012-gcp/kernel/net/vmw_vsock/vmw_vsock_virtio_transport_common.ko.zst +10 072304fe0040090908f5b441a4df332ef210a44c ima-ng sha256:3d967329e1dba41f8d1e5c6b7c68ed31b387b4c5167a24f53661326e34a13e65 /usr/lib/modules/6.17.0-1012-gcp/kernel/net/vmw_vsock/vsock_loopback.ko.zst +10 22162e01f1547061378dd7929f0ed28caded28ac ima-ng sha256:f6ee38cdaff81419653ad76e7d3958f1763c0f3592fd2a787b183eec1d8092d8 /usr/lib/modules/6.17.0-1012-gcp/kernel/drivers/firmware/efi/efi-pstore.ko.zst +10 7e277d47ba66538018b48d95d104c42c61e7f19e ima-ng sha256:e1df46e4cabb8b9a8c7d7334eb16cbb0c675879d9a713d74ab3ffa1bd34cab5d /usr/lib/modules/6.17.0-1012-gcp/kernel/drivers/nvme/host/nvme-fabrics.ko.zst +10 ef1fd23f61e0a2c10d77b92b8b2311048f84e6cf ima-ng sha256:42b888a1190f0190e45c745fa458b6e11b6414cde58e398149f4eb3438c93a2f /usr/lib/modules/6.17.0-1012-gcp/kernel/net/sched/sch_fq_codel.ko.zst +10 dd65ae912a0088e8345ffd3778d605aced4556c8 ima-ng sha256:d84c70a84d318c86add8a2eba815a21af47b1f9dee92b7813b76fb0b8e39a825 /usr/lib/modules/6.17.0-1012-gcp/kernel/drivers/input/serio/serio_raw.ko.zst +10 749229e5d24c75d9cfa8409ed28d9d7e0c7fc7a0 ima-ng sha256:203e4605ad16cf95d73a917c56886266a0806c183c96e317819e3ee53bfb5dbf /usr/lib/modules/6.17.0-1012-gcp/kernel/drivers/macintosh/mac_hid.ko.zst +10 0680512495ed5b6a9fbfd07a4ede8fa04029ebbc ima-ng sha256:462e52cb2a7adfb0deb9d0ca647aea52d3b47f237a7a1550292752aa83df509e /usr/lib/modules/6.17.0-1012-gcp/kernel/drivers/input/input-leds.ko.zst +10 f3a2f541721a1b9dab65b1c03c9bea571514d3d1 ima-ng sha256:5d95df03b9bc2be59144361b9b3be696fe170c5d61e45d487530562f0d7150e6 /usr/lib/modules/6.17.0-1012-gcp/kernel/drivers/input/mouse/psmouse.ko.zst +10 10ecbd713b5d66f0ee0762d643f0ca2cdd570e02 ima-ng sha256:6e96367a39d08305d565b84ea1efcf3f820e5b14aa460ac7362f7aa088693e01 /usr/lib/modules/6.17.0-1012-gcp/kernel/drivers/i2c/i2c-smbus.ko.zst +10 d70ee5345e9c530fb1a24eba3847a60a60730e0d ima-ng sha256:a0678538605debcdb76bb7493feeced960e54e3ad6016eacd4488b198b0cb034 /usr/lib/modules/6.17.0-1012-gcp/kernel/drivers/virtio/virtio_balloon.ko.zst +10 5d104241664906580a890ed6967f0a7db453f55b ima-ng sha256:ea14a54b7937fa721165b97a5b76598c18a0817533d3272bfbfd6a9790d0b6e9 /usr/lib/modules/6.17.0-1012-gcp/kernel/drivers/i2c/busses/i2c-piix4.ko.zst +10 d7790d797e8aa3271fecfc56ef7a07b11a63e49f ima-ng sha256:2c77a0ab2e89692f4fbe9dbd7902b6b436100e03022c76e6c7d3996bb3ce0af3 /usr/lib/modules/6.17.0-1012-gcp/kernel/drivers/misc/pvpanic/pvpanic.ko.zst +10 b500024e0f393a2251e094938feb6bfc1ce53054 ima-ng sha256:5609e30521ba3812eb94cdfed8d533058344c1ad82d2ff7688980aabbccd3c04 /usr/lib/modules/6.17.0-1012-gcp/kernel/drivers/misc/pvpanic/pvpanic-mmio.ko.zst +10 7759619e109148aac1a5cf037c7b50dac606834a ima-ng sha256:0b4f6cf4470aa40c197b119d51ce082e1bceb5b2e5c7b64df3ed316103ac72c0 /usr/lib/modules/6.17.0-1012-gcp/kernel/arch/x86/events/intel/intel-cstate.ko.zst +10 a8fc525e2b719a6a70ef7ed826d3e40dad2f7a3b ima-ng sha256:e79805f1b9cbebc4f968e06348d45341451afc83b02f32fcdc07d7be4f77fc83 /usr/lib/modules/6.17.0-1012-gcp/kernel/arch/x86/events/rapl.ko.zst +10 f4dc053cb3a1accc66529de64c90e1a248e5e2c3 ima-ng sha256:54e8d97db79cb23534741880fd8c838fac570503091fa13e7f19aa6e82d2c2d8 /usr/lib/modules/6.17.0-1012-gcp/kernel/fs/nls/nls_iso8859-1.ko.zst +10 a9f7702ff71be3aa955af8a9c6e089425e3215d4 ima-ng sha256:bfe3b1a305dab0da87b0d18f1df5da214db9a0090d8eafb0492ccc650bb804db /usr/lib/modules/6.17.0-1012-gcp/kernel/arch/x86/crypto/aesni-intel.ko.zst +10 2732794df7d2948897c8eb039e575b623cca1b97 ima-ng sha256:f823b5d774466fbcd560459355b185854351de7e733c445243800f6d90f239a0 /usr/lib/modules/6.17.0-1012-gcp/kernel/arch/x86/crypto/ghash-clmulni-intel.ko.zst +10 29d6869810f5d7417b874c1e5a659dea68cc9851 ima-ng sha256:8a755d28938963d72b88df0722eed2d654b8ed6948760d604ae7f7c69e10c44a /usr/lib/modules/6.17.0-1012-gcp/kernel/arch/x86/crypto/polyval-clmulni.ko.zst +10 50e71822a9738eecf8384a9af94cb2158bd4d4b3 ima-ng sha256:64245414258abc234c8d1b7bea8acf955136efa091798a2481d697b82a7b062b /usr/lib/modules/6.17.0-1012-gcp/kernel/drivers/edac/sb_edac.ko.zst +10 5abb753e504700eac0071d5f58bc15380fcf3399 ima-ng sha256:2357443839181634b0aa3da3e080bacedfde1ca9fbb3d911daecaff4ffa6486e /usr/lib/modules/6.17.0-1012-gcp/kernel/drivers/platform/x86/intel/uncore-frequency/intel-uncore-frequency-common.ko.zst +10 442eb6dfae2413fca40e93af50ac80d53624499c ima-ng sha256:02e08eea6fb65a7678361f3770fb40892922967c8c3b824fcf6390916d4787a2 /usr/lib/modules/6.17.0-1012-gcp/kernel/drivers/platform/x86/intel/uncore-frequency/intel-uncore-frequency.ko.zst +10 b0310e7922e0b32447b7f248e5fa6b2342f57345 ima-ng sha256:92dc6e3a86fe6f2af7cd514f9b478735053dd289fd5a3abd07bc1ea91f724543 /usr/lib/modules/6.17.0-1012-gcp/kernel/drivers/powercap/intel_rapl_common.ko.zst +10 4e8a04f46f74fdc5bc8d3ad29bdccb99fa99aa1c ima-ng sha256:7ed788e50a85511f7af8a3ba30422b621e1cd8e3811367cf8d5a7df0bb8c2648 /usr/lib/modules/6.17.0-1012-gcp/kernel/drivers/powercap/intel_rapl_msr.ko.zst +10 6f8a31f449b7a7af6530b7328752964749e0d20c ima-ng sha256:ff05d791a9341cb7f2273568105f05baacb5725414a52527b61532633c8e15b7 /usr/lib/modules/6.17.0-1012-gcp/kernel/fs/binfmt_misc.ko.zst +10 6bd51841a02952a3007841dc7478feb7f561daaf ima-ng sha256:bc6d9a4279d463014bf6fb3afbd9e40e87d6b117a09f7eb861582b9448405d8e /usr/lib/modules/6.17.0-1012-gcp/kernel/net/llc/llc.ko.zst +10 4957b8df9aed551c6bcd465712c635eb79d8d154 ima-ng sha256:6f5211ba742fd1e5730d63316cd690025081353ef0ff95f47b0c41114f11ac7f /usr/lib/modules/6.17.0-1012-gcp/kernel/net/802/stp.ko.zst +10 390bcbe8e2f8b7cc15de692b0f9e74af8716bc16 ima-ng sha256:1ef22911fa4b57de4245de337254cc863969db5d5c01512e74145c15a532750c /usr/lib/modules/6.17.0-1012-gcp/kernel/net/802/mrp.ko.zst +10 f2a66c53d1b4ca57cb7c63426af1ac05f6aa3135 ima-ng sha256:3b2989b3fb14a37495c2f145d774c002b2e0ee2166bdd5b542081b3bbe0e7b83 /usr/lib/modules/6.17.0-1012-gcp/kernel/net/802/garp.ko.zst +10 9abf03995a779f377cd6b96917839a4d9554dbc8 ima-ng sha256:6abef56fad6f3f3e50ba7c821100300658c02d2177423d552372f22b103c6c63 /usr/lib/modules/6.17.0-1012-gcp/kernel/net/8021q/8021q.ko.zst +10 d0b4c2df9cb84363cfea71ba36251eb671f24594 ima-ng sha256:d7abd96f7d954cf28e44cca828a11753627439753f844db48cfcfb8d9a1b0ef0 /usr/lib/modules/6.17.0-1012-gcp/kernel/net/wireless/cfg80211.ko.zst +10 211ac0c918ae28b74faefa69943f107e95fb9e4c ima-ng sha256:f80a72c5d71964494177548cc13ada22d6caa7b4e29f1a23512b33758224cc11 /usr/lib/modules/6.17.0-1012-gcp/kernel/net/tls/tls.ko.zst diff --git a/testdata/logs/ascii_runtime_measurements_sha256 b/testdata/logs/ascii_runtime_measurements_sha256 new file mode 100644 index 0000000..314e35b --- /dev/null +++ b/testdata/logs/ascii_runtime_measurements_sha256 @@ -0,0 +1,43 @@ +10 7af7a6938ed565c238870e0ca7944a5a0cfe872dcba9b6d67261234185717f53 ima-ng sha256:21390c748be864c56f9dcd5c47b4b32bc8f73f37124602deddcb02c40fea6ef9 boot_aggregate +10 d11343cba7dd9cbd7ad6ac991715eadcfb09f30b5150a8b7309e99920e3b320f ima-ng sha256:c9c69ff607ea889ab7b9d6b7a40ec5220638818f26d3b740bee0e93addd7954a /usr/lib/modules/6.17.0-1012-gcp/kernel/fs/autofs/autofs4.ko.zst +10 b0bb691b4bd5844e8315ce61faf9f82e3ee7523bdae302d4b8176b5b8a87123a ima-ng sha256:5034a95854cc6e36c9a34d0da6ea6ed68796a962438e4966415dc2f3f4df2410 /usr/lib/modules/6.17.0-1012-gcp/kernel/net/netfilter/x_tables.ko.zst +10 f8a208137a6f0127e829a84eb2d426bc8e6226af3defbd16601b7b1448304f0b ima-ng sha256:a96740f306375fac45631688322b8f5dc896bdbbdbd6d3faab01b7c1ed3d534b /usr/lib/modules/6.17.0-1012-gcp/kernel/net/ipv4/netfilter/ip_tables.ko.zst +10 15f38074e581de279f9ee5772cf3e1444df7b23932b6765ddaca3c5912f4fe76 ima-ng sha256:99fbb924c0f7248bde358f3b9a0ac5f3d59aa8d682ffa71bf938701b609fbf37 /usr/lib/modules/6.17.0-1012-gcp/kernel/drivers/char/hw_random/virtio-rng.ko.zst +10 59ff18bdd2a78639c2e71391c1d8c4fe3fabbd9c846dbd0b3eb75dc9e36ae670 ima-ng sha256:b0969949ff520d3a80f16e8387b0263e2a901159c49b1c6e65e1c9ef16f2ca44 /usr/lib/modules/6.17.0-1012-gcp/kernel/drivers/firmware/dmi-sysfs.ko.zst +10 dbcc5106a944be9949e74ed9628ea9e864f576b39e4f8621b6cfc66adc272090 ima-ng sha256:bc76e11c93f8dbb40ca91721481075c7b1dbe32dcfd42898fb044663febdc4ad /usr/lib/modules/6.17.0-1012-gcp/kernel/drivers/misc/vmw_vmci/vmw_vmci.ko.zst +10 d6b29912b692f6ecfc811df636e53e3b07dcef6fc83e79de5ba0dc35a48a492d ima-ng sha256:774832b0096fb586cb9786a00ec7c7425a0d49f7381fbf4c4414a484d3f3843d /usr/lib/modules/6.17.0-1012-gcp/kernel/net/vmw_vsock/vsock.ko.zst +10 4e5d684f95e14b4710b48a710f04d1a3ca7782bef99ba442414e5e26485b347a ima-ng sha256:85e28ed288b2f88361c7f736bef9c10ca6a108a092cb3fac2b97fd52b33c9b2b /usr/lib/modules/6.17.0-1012-gcp/kernel/net/vmw_vsock/vmw_vsock_vmci_transport.ko.zst +10 10fc827acf108c5a2b896b70fdb2d79b67974f3406239d2961154e8e55124b97 ima-ng sha256:b0c69d6f4b7cae2aca99b881fe73036ade35273af1f49d0c0a064013e9d8c987 /usr/lib/modules/6.17.0-1012-gcp/kernel/drivers/hv/hv_vmbus.ko.zst +10 459a36f6a995499a9e042e182b0eee6e212b7b57a7250e5ff9564b41e82d02fb ima-ng sha256:0c386274770800835b261ab110f5cdaf3d3ed314676ea2e0e13dc3d17aaa6d65 /usr/lib/modules/6.17.0-1012-gcp/kernel/net/vmw_vsock/vmw_vsock_virtio_transport_common.ko.zst +10 0064d57cbca4be03fdf1c099a2bdc28e7f063347fa9eed642fec8d0789402417 ima-ng sha256:3d967329e1dba41f8d1e5c6b7c68ed31b387b4c5167a24f53661326e34a13e65 /usr/lib/modules/6.17.0-1012-gcp/kernel/net/vmw_vsock/vsock_loopback.ko.zst +10 ea9b647703763553aa449023a3cdc9399e1586db0fe4313d6e1b4295b3634b0d ima-ng sha256:f6ee38cdaff81419653ad76e7d3958f1763c0f3592fd2a787b183eec1d8092d8 /usr/lib/modules/6.17.0-1012-gcp/kernel/drivers/firmware/efi/efi-pstore.ko.zst +10 48b3f7d4bcd0f035ea5883c90ddfc8f5f12583229648f5d2679bf8de5155c993 ima-ng sha256:e1df46e4cabb8b9a8c7d7334eb16cbb0c675879d9a713d74ab3ffa1bd34cab5d /usr/lib/modules/6.17.0-1012-gcp/kernel/drivers/nvme/host/nvme-fabrics.ko.zst +10 f35aee1f09eac4dea006eb1ecaa9341d8426ed1c3a3eedec4dd89c256f189c02 ima-ng sha256:42b888a1190f0190e45c745fa458b6e11b6414cde58e398149f4eb3438c93a2f /usr/lib/modules/6.17.0-1012-gcp/kernel/net/sched/sch_fq_codel.ko.zst +10 8ccf35aea9c72e2978a290a6521a478ec9cbe5cd3116607b21df4703e9d95391 ima-ng sha256:d84c70a84d318c86add8a2eba815a21af47b1f9dee92b7813b76fb0b8e39a825 /usr/lib/modules/6.17.0-1012-gcp/kernel/drivers/input/serio/serio_raw.ko.zst +10 484cecb549c209701c2f0995e089e9185ae8cd9591ed8237bfc903603d10ed14 ima-ng sha256:203e4605ad16cf95d73a917c56886266a0806c183c96e317819e3ee53bfb5dbf /usr/lib/modules/6.17.0-1012-gcp/kernel/drivers/macintosh/mac_hid.ko.zst +10 c024d5e353d45c57c83eafe328d2faebb50e9d35a7f9ca96eafe1c07dc71b468 ima-ng sha256:462e52cb2a7adfb0deb9d0ca647aea52d3b47f237a7a1550292752aa83df509e /usr/lib/modules/6.17.0-1012-gcp/kernel/drivers/input/input-leds.ko.zst +10 cd9b2b71fb6fc1974a7dfa9212fc030e1a3b821033a56f2a1c9d79258f9d2a17 ima-ng sha256:5d95df03b9bc2be59144361b9b3be696fe170c5d61e45d487530562f0d7150e6 /usr/lib/modules/6.17.0-1012-gcp/kernel/drivers/input/mouse/psmouse.ko.zst +10 28b77e45b56218ec0bdc4fcd80265f30b4c144af56be575f6d30a26802ab6c70 ima-ng sha256:6e96367a39d08305d565b84ea1efcf3f820e5b14aa460ac7362f7aa088693e01 /usr/lib/modules/6.17.0-1012-gcp/kernel/drivers/i2c/i2c-smbus.ko.zst +10 5fe355937af2f71d091de0279278c2498cda8a18485d3b9caa4086a29d3e06b1 ima-ng sha256:a0678538605debcdb76bb7493feeced960e54e3ad6016eacd4488b198b0cb034 /usr/lib/modules/6.17.0-1012-gcp/kernel/drivers/virtio/virtio_balloon.ko.zst +10 33cd3404e64d8d62194b3ee95e590d9846a4aa0d10363caa531b7c7e35512f2a ima-ng sha256:ea14a54b7937fa721165b97a5b76598c18a0817533d3272bfbfd6a9790d0b6e9 /usr/lib/modules/6.17.0-1012-gcp/kernel/drivers/i2c/busses/i2c-piix4.ko.zst +10 c619d2b8d4e92436b6604d8dce80464352763f72349f02d76ddfe396a3dd9b33 ima-ng sha256:2c77a0ab2e89692f4fbe9dbd7902b6b436100e03022c76e6c7d3996bb3ce0af3 /usr/lib/modules/6.17.0-1012-gcp/kernel/drivers/misc/pvpanic/pvpanic.ko.zst +10 9eaacf1516652742bbe0474d130487417aced99ff49911c67290ecaaa090d597 ima-ng sha256:5609e30521ba3812eb94cdfed8d533058344c1ad82d2ff7688980aabbccd3c04 /usr/lib/modules/6.17.0-1012-gcp/kernel/drivers/misc/pvpanic/pvpanic-mmio.ko.zst +10 800f9e22d091ce3d3972579bc23c89cef0e60765a7e52d5701b8f93c012ac861 ima-ng sha256:0b4f6cf4470aa40c197b119d51ce082e1bceb5b2e5c7b64df3ed316103ac72c0 /usr/lib/modules/6.17.0-1012-gcp/kernel/arch/x86/events/intel/intel-cstate.ko.zst +10 c3a9d0ec2a37b05a13caba8bc1bc173c1bcb715b10575534407f2d6cc49a45d6 ima-ng sha256:e79805f1b9cbebc4f968e06348d45341451afc83b02f32fcdc07d7be4f77fc83 /usr/lib/modules/6.17.0-1012-gcp/kernel/arch/x86/events/rapl.ko.zst +10 a95f35df07f22df59fa8729d0357f338e28d5c68d27afbb45a722cdf60b2212a ima-ng sha256:54e8d97db79cb23534741880fd8c838fac570503091fa13e7f19aa6e82d2c2d8 /usr/lib/modules/6.17.0-1012-gcp/kernel/fs/nls/nls_iso8859-1.ko.zst +10 bde5b7a23e096fed78b27b5d60bd2f439f6bc53e76d01d1bb7286d320a438960 ima-ng sha256:bfe3b1a305dab0da87b0d18f1df5da214db9a0090d8eafb0492ccc650bb804db /usr/lib/modules/6.17.0-1012-gcp/kernel/arch/x86/crypto/aesni-intel.ko.zst +10 6be86dfc4f09aca235ac71c5ad8c2f9d2d9d390c82bb3c75862cb7210e6108ac ima-ng sha256:f823b5d774466fbcd560459355b185854351de7e733c445243800f6d90f239a0 /usr/lib/modules/6.17.0-1012-gcp/kernel/arch/x86/crypto/ghash-clmulni-intel.ko.zst +10 8a30cb79b38c5202ac17cb0e7291224fe46b96268cf6d757a04328f083c0dc6e ima-ng sha256:8a755d28938963d72b88df0722eed2d654b8ed6948760d604ae7f7c69e10c44a /usr/lib/modules/6.17.0-1012-gcp/kernel/arch/x86/crypto/polyval-clmulni.ko.zst +10 ac12fa84ed30f9e84981e7828639714b2b3cd6f98dfc7936db6bbe32a38744ce ima-ng sha256:64245414258abc234c8d1b7bea8acf955136efa091798a2481d697b82a7b062b /usr/lib/modules/6.17.0-1012-gcp/kernel/drivers/edac/sb_edac.ko.zst +10 f1675228d725a28cfa911e3c8d5680b1d6d2ba51fe7a5ae88173acd331ebaeb4 ima-ng sha256:2357443839181634b0aa3da3e080bacedfde1ca9fbb3d911daecaff4ffa6486e /usr/lib/modules/6.17.0-1012-gcp/kernel/drivers/platform/x86/intel/uncore-frequency/intel-uncore-frequency-common.ko.zst +10 89679185afbdb5eb7ed9f265b08392e58dd10d0652792813be2e502ed3403066 ima-ng sha256:02e08eea6fb65a7678361f3770fb40892922967c8c3b824fcf6390916d4787a2 /usr/lib/modules/6.17.0-1012-gcp/kernel/drivers/platform/x86/intel/uncore-frequency/intel-uncore-frequency.ko.zst +10 559adbdd90e09dcf55782176f4305e26e124bd8c596c781ed3df2be4879a923e ima-ng sha256:92dc6e3a86fe6f2af7cd514f9b478735053dd289fd5a3abd07bc1ea91f724543 /usr/lib/modules/6.17.0-1012-gcp/kernel/drivers/powercap/intel_rapl_common.ko.zst +10 5aa26d4a76de7cfe7db79c4a3e517e109df06525d146c0eea57eaf301d489a0b ima-ng sha256:7ed788e50a85511f7af8a3ba30422b621e1cd8e3811367cf8d5a7df0bb8c2648 /usr/lib/modules/6.17.0-1012-gcp/kernel/drivers/powercap/intel_rapl_msr.ko.zst +10 3b14892a172fae689d6ac7e3c0de24ce83469bb0edb7e3cdeede39fbe094c27b ima-ng sha256:ff05d791a9341cb7f2273568105f05baacb5725414a52527b61532633c8e15b7 /usr/lib/modules/6.17.0-1012-gcp/kernel/fs/binfmt_misc.ko.zst +10 99953f1753bcb15ad96c191d952c9e46e139231aa978eef258b55863c0e2a565 ima-ng sha256:bc6d9a4279d463014bf6fb3afbd9e40e87d6b117a09f7eb861582b9448405d8e /usr/lib/modules/6.17.0-1012-gcp/kernel/net/llc/llc.ko.zst +10 b1fa1ccec09b0ea5453b03aaf4286576b59347f0dd7633a85fbef6722cef2403 ima-ng sha256:6f5211ba742fd1e5730d63316cd690025081353ef0ff95f47b0c41114f11ac7f /usr/lib/modules/6.17.0-1012-gcp/kernel/net/802/stp.ko.zst +10 15db7d2e4505065e7cb5d152823a77688c0c6ecb28e161ffd419ad460c6feaf7 ima-ng sha256:1ef22911fa4b57de4245de337254cc863969db5d5c01512e74145c15a532750c /usr/lib/modules/6.17.0-1012-gcp/kernel/net/802/mrp.ko.zst +10 b285d3099f5d83021dd5f54832146c7840113eec40aa05989ab057191e357cd3 ima-ng sha256:3b2989b3fb14a37495c2f145d774c002b2e0ee2166bdd5b542081b3bbe0e7b83 /usr/lib/modules/6.17.0-1012-gcp/kernel/net/802/garp.ko.zst +10 a5b3062f635f0e6cd568f8d2071eab370a65c98815316129b4895182d3e35f35 ima-ng sha256:6abef56fad6f3f3e50ba7c821100300658c02d2177423d552372f22b103c6c63 /usr/lib/modules/6.17.0-1012-gcp/kernel/net/8021q/8021q.ko.zst +10 e070863ad1452f5b8f107959672c3ab0b888ee62cd1d7f27e183a77f84dfb6e7 ima-ng sha256:d7abd96f7d954cf28e44cca828a11753627439753f844db48cfcfb8d9a1b0ef0 /usr/lib/modules/6.17.0-1012-gcp/kernel/net/wireless/cfg80211.ko.zst +10 0e3accc305c0074ce68a3a6a9042d12e99ff9cd6952094d14ba87caa6922dbe0 ima-ng sha256:f80a72c5d71964494177548cc13ada22d6caa7b4e29f1a23512b33758224cc11 /usr/lib/modules/6.17.0-1012-gcp/kernel/net/tls/tls.ko.zst diff --git a/testdata/logs/ascii_runtime_measurements_sha384 b/testdata/logs/ascii_runtime_measurements_sha384 new file mode 100644 index 0000000..322dbda --- /dev/null +++ b/testdata/logs/ascii_runtime_measurements_sha384 @@ -0,0 +1,43 @@ +10 886c10c0dbd1a115e4af843ce689c91344ae30b27a4fd70cfbb1cfe300b6cdbc855fc377cd4a80a2de0a613c9258bc41 ima-ng sha256:21390c748be864c56f9dcd5c47b4b32bc8f73f37124602deddcb02c40fea6ef9 boot_aggregate +10 d259a5027fb4bff96055bec7c4a3ad9661e39d050014af981c6dd51a8cbb8ac44db5363fde1105238be19cd26087349d ima-ng sha256:c9c69ff607ea889ab7b9d6b7a40ec5220638818f26d3b740bee0e93addd7954a /usr/lib/modules/6.17.0-1012-gcp/kernel/fs/autofs/autofs4.ko.zst +10 1a8864819816769b762e66d555a7472c1b08e127cda20a1226e59cc26d9074b64b119b2736e60f54ab368fefab440a79 ima-ng sha256:5034a95854cc6e36c9a34d0da6ea6ed68796a962438e4966415dc2f3f4df2410 /usr/lib/modules/6.17.0-1012-gcp/kernel/net/netfilter/x_tables.ko.zst +10 fbf96a773ef9b4a20cd3c79a97ac094e4fb8576d55fb9df7fd53358ed9202532edce55411034e442a124d0a64138ffb3 ima-ng sha256:a96740f306375fac45631688322b8f5dc896bdbbdbd6d3faab01b7c1ed3d534b /usr/lib/modules/6.17.0-1012-gcp/kernel/net/ipv4/netfilter/ip_tables.ko.zst +10 ff42040878e7227c0403fbea0d5d4bc4a19eedb71254f4c9b8779147e2b01975d40f91f47e32800a3206d269d4492a31 ima-ng sha256:99fbb924c0f7248bde358f3b9a0ac5f3d59aa8d682ffa71bf938701b609fbf37 /usr/lib/modules/6.17.0-1012-gcp/kernel/drivers/char/hw_random/virtio-rng.ko.zst +10 2a9991f6a0d34594027d531984fd06415a35e0af06c5215819b4433de650871b900eef1f937ee1e7605869d9f3645940 ima-ng sha256:b0969949ff520d3a80f16e8387b0263e2a901159c49b1c6e65e1c9ef16f2ca44 /usr/lib/modules/6.17.0-1012-gcp/kernel/drivers/firmware/dmi-sysfs.ko.zst +10 6aab243b150e83b2d1fb21c9171fbe59a503cfc73b1f90c2f82463233f6f5f0c5a0218bb2a002e3a31d7c60f17dab678 ima-ng sha256:bc76e11c93f8dbb40ca91721481075c7b1dbe32dcfd42898fb044663febdc4ad /usr/lib/modules/6.17.0-1012-gcp/kernel/drivers/misc/vmw_vmci/vmw_vmci.ko.zst +10 e6fbef614c0ed498df4b24eb97b0839489af5510cb5ef4c59c8a44baa0934fc2e7df9af71c32c3a7e8afc0d17e792ea1 ima-ng sha256:774832b0096fb586cb9786a00ec7c7425a0d49f7381fbf4c4414a484d3f3843d /usr/lib/modules/6.17.0-1012-gcp/kernel/net/vmw_vsock/vsock.ko.zst +10 6a9a9285133140a776071688aa978001820a53310b5523ed20661cf9c75754a3f515fe4c8b568e8de0e279fe204310ac ima-ng sha256:85e28ed288b2f88361c7f736bef9c10ca6a108a092cb3fac2b97fd52b33c9b2b /usr/lib/modules/6.17.0-1012-gcp/kernel/net/vmw_vsock/vmw_vsock_vmci_transport.ko.zst +10 b3ea9964f66edc5c6e361d5117039fb36e95aa91ecb371bcd02b9e60f2a41486fb0a4cbfe078e210fec6c4b5cb783847 ima-ng sha256:b0c69d6f4b7cae2aca99b881fe73036ade35273af1f49d0c0a064013e9d8c987 /usr/lib/modules/6.17.0-1012-gcp/kernel/drivers/hv/hv_vmbus.ko.zst +10 0eca5aa6a4f228524e4cf94a882f9d00703615eb489ef700c7f63eb43ef5f9a3a15c94f3d904fe645996f9daa2e60d3d ima-ng sha256:0c386274770800835b261ab110f5cdaf3d3ed314676ea2e0e13dc3d17aaa6d65 /usr/lib/modules/6.17.0-1012-gcp/kernel/net/vmw_vsock/vmw_vsock_virtio_transport_common.ko.zst +10 f121bd57957560c200eb1e44c60df1d2325a39dd67ad19d70c38c5edc711b91c265ff61b9089e0f8482bc6474dc46d20 ima-ng sha256:3d967329e1dba41f8d1e5c6b7c68ed31b387b4c5167a24f53661326e34a13e65 /usr/lib/modules/6.17.0-1012-gcp/kernel/net/vmw_vsock/vsock_loopback.ko.zst +10 93b9700a65fb48927d7d7af8dfd5b80b94e536855267da2892c048097c78516833b21adf12a14e235d904d432b221504 ima-ng sha256:f6ee38cdaff81419653ad76e7d3958f1763c0f3592fd2a787b183eec1d8092d8 /usr/lib/modules/6.17.0-1012-gcp/kernel/drivers/firmware/efi/efi-pstore.ko.zst +10 862567766427fff33dd741d0a776e98c167f4765493db85e73cd592a231588447b547e599a4f1106c2fedd1c2b601432 ima-ng sha256:e1df46e4cabb8b9a8c7d7334eb16cbb0c675879d9a713d74ab3ffa1bd34cab5d /usr/lib/modules/6.17.0-1012-gcp/kernel/drivers/nvme/host/nvme-fabrics.ko.zst +10 cd62420b42ce00a6fb343b3f57efa0a405f13438b63f48971133a18105e4dd23f5831667572813d48059be7fefc157f2 ima-ng sha256:42b888a1190f0190e45c745fa458b6e11b6414cde58e398149f4eb3438c93a2f /usr/lib/modules/6.17.0-1012-gcp/kernel/net/sched/sch_fq_codel.ko.zst +10 4c1a34b532cbb7ccba471430a35c74f6352adb29c9ee3a3783c8209758f17bdc89ca829ce2bff071d69b8d074406b42f ima-ng sha256:d84c70a84d318c86add8a2eba815a21af47b1f9dee92b7813b76fb0b8e39a825 /usr/lib/modules/6.17.0-1012-gcp/kernel/drivers/input/serio/serio_raw.ko.zst +10 807c45bc27474c5c4f29fdd8d0f4dbea98571a176b59841fe794efc16737b7ea9be02016b0cbf74514f6ae3714f0dd3a ima-ng sha256:203e4605ad16cf95d73a917c56886266a0806c183c96e317819e3ee53bfb5dbf /usr/lib/modules/6.17.0-1012-gcp/kernel/drivers/macintosh/mac_hid.ko.zst +10 3eb55ddf5135965915962893db19f552ff704bb3fb123d11a5796756eefd1e4b90798842dca28f30692406e138fbc671 ima-ng sha256:462e52cb2a7adfb0deb9d0ca647aea52d3b47f237a7a1550292752aa83df509e /usr/lib/modules/6.17.0-1012-gcp/kernel/drivers/input/input-leds.ko.zst +10 e4138b19aad6cba05b89f64a88d1bef31bdace1a848c0aa42f181f30c9ab1d77a21553cf56cd7031cdfd2bd11f6b1e58 ima-ng sha256:5d95df03b9bc2be59144361b9b3be696fe170c5d61e45d487530562f0d7150e6 /usr/lib/modules/6.17.0-1012-gcp/kernel/drivers/input/mouse/psmouse.ko.zst +10 8f0633d9dfdd66dbaca5dea0b4a14fefc33dc0ef0e5ea9e86597bdf4eb7baa86572cd04fdf8f418235a0cf264f836a29 ima-ng sha256:6e96367a39d08305d565b84ea1efcf3f820e5b14aa460ac7362f7aa088693e01 /usr/lib/modules/6.17.0-1012-gcp/kernel/drivers/i2c/i2c-smbus.ko.zst +10 bc5a8993c71c620672277a934e6c2d583f4378626d344c733128ab93df3a62e9d0a99ccd67daf045b748e25e4ffb9f5c ima-ng sha256:a0678538605debcdb76bb7493feeced960e54e3ad6016eacd4488b198b0cb034 /usr/lib/modules/6.17.0-1012-gcp/kernel/drivers/virtio/virtio_balloon.ko.zst +10 ff71b1b9d8ce8aaa7e1621c8f7f49191f35dd75694ccf51ac743bff34e355821ca4276e3aeaeee3a73131d15fb26cc47 ima-ng sha256:ea14a54b7937fa721165b97a5b76598c18a0817533d3272bfbfd6a9790d0b6e9 /usr/lib/modules/6.17.0-1012-gcp/kernel/drivers/i2c/busses/i2c-piix4.ko.zst +10 383e15c5d787ff15950216b69d0ef3b108b4d302c5117c9e4d5b683f176b279023d02cbfcea2da5a8e3f56ec2d7c202a ima-ng sha256:2c77a0ab2e89692f4fbe9dbd7902b6b436100e03022c76e6c7d3996bb3ce0af3 /usr/lib/modules/6.17.0-1012-gcp/kernel/drivers/misc/pvpanic/pvpanic.ko.zst +10 f3d09d4cd94c590f62fc309dee77601103f3d950d7b07ae2746dfeb4ebb9a0535c1f0b779b05ddcdabbcc50d6cbeb7e9 ima-ng sha256:5609e30521ba3812eb94cdfed8d533058344c1ad82d2ff7688980aabbccd3c04 /usr/lib/modules/6.17.0-1012-gcp/kernel/drivers/misc/pvpanic/pvpanic-mmio.ko.zst +10 9a1e0e9e845c78b4950a7e186e61bc1eccb829b29c8a907920c563fd15ee7fa315a03dbb6cbbd7a387f80e92d4919d3a ima-ng sha256:0b4f6cf4470aa40c197b119d51ce082e1bceb5b2e5c7b64df3ed316103ac72c0 /usr/lib/modules/6.17.0-1012-gcp/kernel/arch/x86/events/intel/intel-cstate.ko.zst +10 320afb4d8d5575f44343b12a003e968ccf9b595af5b2e9a9181e7da414220eb8d9914e304eb126ba1794625e1e58810c ima-ng sha256:e79805f1b9cbebc4f968e06348d45341451afc83b02f32fcdc07d7be4f77fc83 /usr/lib/modules/6.17.0-1012-gcp/kernel/arch/x86/events/rapl.ko.zst +10 4fa9e8a6d11f3a5ff05bb5bb334f79925aa4711f1a1a78877a978108fc8dbd3d67c02967454f93d5eec6e30e2603d1aa ima-ng sha256:54e8d97db79cb23534741880fd8c838fac570503091fa13e7f19aa6e82d2c2d8 /usr/lib/modules/6.17.0-1012-gcp/kernel/fs/nls/nls_iso8859-1.ko.zst +10 6f4429399b22561db0e3ca2af9d08d597f07226327d424fdb77fce735024a58baeff073f98181042fb066b24c9255308 ima-ng sha256:bfe3b1a305dab0da87b0d18f1df5da214db9a0090d8eafb0492ccc650bb804db /usr/lib/modules/6.17.0-1012-gcp/kernel/arch/x86/crypto/aesni-intel.ko.zst +10 e5844a8ade9a884e310cbf6b3ffc69b2cd90070756926a16da66713409069758d0554d1332e100646a263b6f94efd6c4 ima-ng sha256:f823b5d774466fbcd560459355b185854351de7e733c445243800f6d90f239a0 /usr/lib/modules/6.17.0-1012-gcp/kernel/arch/x86/crypto/ghash-clmulni-intel.ko.zst +10 7d407642ace7a945f3707bf1b3533dcea3294ec5dab9851cc4dee823d871d1042842102b05aa0d71ac3d8a6f6a86ada2 ima-ng sha256:8a755d28938963d72b88df0722eed2d654b8ed6948760d604ae7f7c69e10c44a /usr/lib/modules/6.17.0-1012-gcp/kernel/arch/x86/crypto/polyval-clmulni.ko.zst +10 2da9373f01ac246cb4aa18c0cd25fc03c180694ca34c47ab875170a3ed24c883daa2caff07720e45a5713e1502b61bf6 ima-ng sha256:64245414258abc234c8d1b7bea8acf955136efa091798a2481d697b82a7b062b /usr/lib/modules/6.17.0-1012-gcp/kernel/drivers/edac/sb_edac.ko.zst +10 33e1cfa73e818f4a84e66836839fa6b55cad2a6bf25272541579279d2ccf94b8cd132e7d3d239fd81bbbb0627e8faea0 ima-ng sha256:2357443839181634b0aa3da3e080bacedfde1ca9fbb3d911daecaff4ffa6486e /usr/lib/modules/6.17.0-1012-gcp/kernel/drivers/platform/x86/intel/uncore-frequency/intel-uncore-frequency-common.ko.zst +10 3e013c0f61311432ec0fe17e5ad54cec8e5af38745171ab1bf54075893ec7df3ace0bb8b0ffbd47fcd570db2f1dbf3a4 ima-ng sha256:02e08eea6fb65a7678361f3770fb40892922967c8c3b824fcf6390916d4787a2 /usr/lib/modules/6.17.0-1012-gcp/kernel/drivers/platform/x86/intel/uncore-frequency/intel-uncore-frequency.ko.zst +10 18184bf65444763aee3af17bdb0cc8c1ee0e5f57d7c115a2075b753f37276c7ec2d46e9441b56fe557273b228a2c411d ima-ng sha256:92dc6e3a86fe6f2af7cd514f9b478735053dd289fd5a3abd07bc1ea91f724543 /usr/lib/modules/6.17.0-1012-gcp/kernel/drivers/powercap/intel_rapl_common.ko.zst +10 690dce2f20dd376bbf9f83cb734b6d53eb7dd399592bba6bbc76025f8a78c96837be88b49c4a56b3a52ec6aa4ef4fa06 ima-ng sha256:7ed788e50a85511f7af8a3ba30422b621e1cd8e3811367cf8d5a7df0bb8c2648 /usr/lib/modules/6.17.0-1012-gcp/kernel/drivers/powercap/intel_rapl_msr.ko.zst +10 581e65a156f8c4f33df6ef521406bb8472691abe99efe099a8b9e66a7f1293acd3c0a360ad87253f54dfa79e309b0998 ima-ng sha256:ff05d791a9341cb7f2273568105f05baacb5725414a52527b61532633c8e15b7 /usr/lib/modules/6.17.0-1012-gcp/kernel/fs/binfmt_misc.ko.zst +10 5040e3dbb43fbb51e628f48df8457cfe29aa6581dee2c4d19e52048e0a0958cf3d007e6706800ff5c44c90796d655a32 ima-ng sha256:bc6d9a4279d463014bf6fb3afbd9e40e87d6b117a09f7eb861582b9448405d8e /usr/lib/modules/6.17.0-1012-gcp/kernel/net/llc/llc.ko.zst +10 d23bffec6b021b02496923b447ce4be6bcc4257ef66e04ef76ad1de89c6895e46237638df2eb0a150456a84b5b3df614 ima-ng sha256:6f5211ba742fd1e5730d63316cd690025081353ef0ff95f47b0c41114f11ac7f /usr/lib/modules/6.17.0-1012-gcp/kernel/net/802/stp.ko.zst +10 8ffd6abb109cf0c2f9326b6fe0da85036536b364f100b81ce607f514f467db6943d6373a56d03e9c3a148b7bd7f3d000 ima-ng sha256:1ef22911fa4b57de4245de337254cc863969db5d5c01512e74145c15a532750c /usr/lib/modules/6.17.0-1012-gcp/kernel/net/802/mrp.ko.zst +10 f3cf12a11bd9ae5df80fc3ef303dc09757b74c0661a5da681520f57af5a3743feba28d5ecffabba8108ab9985381cb28 ima-ng sha256:3b2989b3fb14a37495c2f145d774c002b2e0ee2166bdd5b542081b3bbe0e7b83 /usr/lib/modules/6.17.0-1012-gcp/kernel/net/802/garp.ko.zst +10 7a1dd07b0f3503d04814192ad4fc3a5d43d6a931b866575edd8cbc99734e93d95bfa97771275c5b0581913567cd7daf8 ima-ng sha256:6abef56fad6f3f3e50ba7c821100300658c02d2177423d552372f22b103c6c63 /usr/lib/modules/6.17.0-1012-gcp/kernel/net/8021q/8021q.ko.zst +10 60f737d0e12b55047bbb9dced94d9838242820d1c9f87b15b23ad7614b9d7b003a541372ad5dd5f197c96b31ab17b34c ima-ng sha256:d7abd96f7d954cf28e44cca828a11753627439753f844db48cfcfb8d9a1b0ef0 /usr/lib/modules/6.17.0-1012-gcp/kernel/net/wireless/cfg80211.ko.zst +10 a503644b54de29a85c5dc1168a49e95b3df56a96ee16ca3ecff6df99b1603d66d52ac9747137c0f576137f6570d0fe1b ima-ng sha256:f80a72c5d71964494177548cc13ada22d6caa7b4e29f1a23512b33758224cc11 /usr/lib/modules/6.17.0-1012-gcp/kernel/net/tls/tls.ko.zst diff --git a/testdata/logs/binary_runtime_measurements_sha1 b/testdata/logs/binary_runtime_measurements_sha1 new file mode 100644 index 0000000..8397c06 Binary files /dev/null and b/testdata/logs/binary_runtime_measurements_sha1 differ diff --git a/testdata/logs/binary_runtime_measurements_sha256 b/testdata/logs/binary_runtime_measurements_sha256 new file mode 100644 index 0000000..38ef315 Binary files /dev/null and b/testdata/logs/binary_runtime_measurements_sha256 differ diff --git a/testdata/logs/binary_runtime_measurements_sha384 b/testdata/logs/binary_runtime_measurements_sha384 new file mode 100644 index 0000000..c5454cb Binary files /dev/null and b/testdata/logs/binary_runtime_measurements_sha384 differ diff --git a/testdata/logs/pcr10s-serialized.dat b/testdata/logs/pcr10s-serialized.dat new file mode 100644 index 0000000..aa2070d Binary files /dev/null and b/testdata/logs/pcr10s-serialized.dat differ diff --git a/testdata/policies/builtin-tcb-appraise-policy b/testdata/policies/builtin-tcb-appraise-policy new file mode 100644 index 0000000..2dffa14 --- /dev/null +++ b/testdata/policies/builtin-tcb-appraise-policy @@ -0,0 +1,14 @@ +dont_appraise fsmagic=0x9fa0 +dont_appraise fsmagic=0x62656572 +dont_appraise fsmagic=0x64626720 +dont_appraise fsmagic=0x1021994 +dont_appraise fsmagic=0x858458f6 +dont_appraise fsmagic=0x1cd1 +dont_appraise fsmagic=0x42494e4d +dont_appraise fsmagic=0x73636673 +dont_appraise fsmagic=0xf97cff8c +dont_appraise fsmagic=0x43415d53 +dont_appraise fsmagic=0x6e736673 +dont_appraise fsmagic=0x27e0eb +dont_appraise fsmagic=0x63677270 +appraise fowner=0 diff --git a/testdata/policies/builtin-tcb-policy b/testdata/policies/builtin-tcb-policy new file mode 100644 index 0000000..b377fbc --- /dev/null +++ b/testdata/policies/builtin-tcb-policy @@ -0,0 +1,17 @@ +dont_measure fsmagic=0x9fa0 +dont_measure fsmagic=0x62656572 +dont_measure fsmagic=0x64626720 +dont_measure fsmagic=0x1021994 +dont_measure fsmagic=0x1cd1 +dont_measure fsmagic=0x42494e4d +dont_measure fsmagic=0x73636673 +dont_measure fsmagic=0xf97cff8c +dont_measure fsmagic=0x43415d53 +dont_measure fsmagic=0x27e0eb +dont_measure fsmagic=0x63677270 +dont_measure fsmagic=0x6e736673 +measure func=MMAP_CHECK mask=MAY_EXEC +measure func=BPRM_CHECK mask=MAY_EXEC +measure func=FILE_CHECK mask=MAY_READ uid=0 +measure func=MODULE_CHECK +measure func=FIRMWARE_CHECK diff --git a/testdata/policies/custom-ima-policy b/testdata/policies/custom-ima-policy new file mode 100644 index 0000000..696208d --- /dev/null +++ b/testdata/policies/custom-ima-policy @@ -0,0 +1,18 @@ +dont_measure fsmagic=0x9fa0 +dont_measure fsmagic=0x62656572 +dont_measure fsmagic=0x64626720 +dont_measure fsmagic=0x1021994 +dont_measure fsmagic=0x1cd1 +dont_measure fsmagic=0x42494e4d +dont_measure fsmagic=0x73636673 +dont_measure fsmagic=0xf97cff8c +dont_measure fsmagic=0x43415d53 +dont_measure fsmagic=0x27e0eb +dont_measure fsmagic=0x63677270 +dont_measure fsmagic=0x6e736673 +measure func=BPRM_CHECK mask=MAY_EXEC +measure func=CRITICAL_DATA mask=MAY_WRITE +measure func=FILE_CHECK mask=MAY_READ uid=0 +measure func=FIRMWARE_CHECK +measure func=MMAP_CHECK mask=MAY_EXEC +measure func=MODULE_CHECK diff --git a/tests/integration.rs b/tests/integration.rs new file mode 100644 index 0000000..862f697 --- /dev/null +++ b/tests/integration.rs @@ -0,0 +1,164 @@ +// SPDX-License-Identifier: Apache-2.0 + +//! End-to-end tests: build a log in memory, parse it back, and verify +//! template-hash recomputation. + +use ima_parser::hash::HashAlgorithm; +use ima_parser::log::{ + Digest, Endianness, EventLogParser, ParseOptions, TemplateData, parse_ascii_log, +}; + +/// Helper: encode one `ima-ng` event into the binary log format. +fn build_ima_ng_event(pcr: u32, template_hash: &[u8], digest: &Digest, filename: &str) -> Vec { + let mut d_ng = Vec::new(); + d_ng.extend_from_slice(digest.algorithm.name().as_bytes()); + d_ng.push(b':'); + d_ng.push(0); + d_ng.extend_from_slice(&digest.bytes); + + let mut n_ng = Vec::new(); + n_ng.extend_from_slice(filename.as_bytes()); + n_ng.push(0); + + let mut td = Vec::new(); + td.extend_from_slice(&(d_ng.len() as u32).to_le_bytes()); + td.extend_from_slice(&d_ng); + td.extend_from_slice(&(n_ng.len() as u32).to_le_bytes()); + td.extend_from_slice(&n_ng); + + let mut event = Vec::new(); + event.extend_from_slice(&pcr.to_le_bytes()); + event.extend_from_slice(template_hash); + event.extend_from_slice(&(b"ima-ng".len() as u32).to_le_bytes()); + event.extend_from_slice(b"ima-ng"); + event.extend_from_slice(&(td.len() as u32).to_le_bytes()); + event.extend_from_slice(&td); + event +} + +#[test] +fn binary_parser_yields_multiple_events() { + let d = Digest::new(HashAlgorithm::Sha256, vec![0xAA; 32]); + let mut log = Vec::new(); + log.extend_from_slice(&build_ima_ng_event(10, &[0x01; 20], &d, "/usr/bin/ls")); + log.extend_from_slice(&build_ima_ng_event(10, &[0x02; 20], &d, "/etc/passwd")); + + let opts = ParseOptions::default() + .with_endianness(Endianness::Little) + .with_template_hash_algorithm(HashAlgorithm::Sha1); + let events: Vec<_> = EventLogParser::new(log.as_slice(), opts) + .collect::, _>>() + .unwrap(); + assert_eq!(events.len(), 2); + assert_eq!(events[0].template_name, "ima-ng"); +} + +#[cfg(feature = "hash")] +#[test] +fn template_hash_recomputation_matches() { + // Build an ima-ng event with a known payload, let the parser compute + // the template hash, then verify the stored hash equals the recomputed + // one. + let d = Digest::new(HashAlgorithm::Sha256, vec![0x42; 32]); + + // Reconstruct the exact bytes that the template hash must cover. + let mut d_ng = Vec::new(); + d_ng.extend_from_slice(b"sha256"); + d_ng.push(b':'); + d_ng.push(0); + d_ng.extend_from_slice(&d.bytes); + + let n_ng = { + let mut v = Vec::new(); + v.extend_from_slice(b"/bin/sh"); + v.push(0); + v + }; + + // Compute the expected SHA-1 template hash manually. + use sha1::{Digest as _, Sha1}; + let mut h = Sha1::new(); + h.update((d_ng.len() as u32).to_le_bytes()); + h.update(&d_ng); + h.update((n_ng.len() as u32).to_le_bytes()); + h.update(&n_ng); + let expected: Vec = h.finalize().to_vec(); + + let event_bytes = build_ima_ng_event(10, &expected, &d, "/bin/sh"); + let opts = ParseOptions::default().with_template_hash_algorithm(HashAlgorithm::Sha1); + let events: Vec<_> = EventLogParser::new(event_bytes.as_slice(), opts) + .collect::, _>>() + .unwrap(); + assert_eq!(events.len(), 1); + let recomputed = events[0] + .calculate_template_hash(HashAlgorithm::Sha1) + .unwrap(); + assert_eq!(recomputed, expected); + assert_eq!( + events[0].verify_template_hash(HashAlgorithm::Sha1), + Some(true) + ); +} + +#[cfg(feature = "hash")] +#[test] +fn legacy_ima_template_hash() { + // Legacy "ima" template: hash(20-byte digest || 256-byte zero-padded name). + use ima_parser::log::{EventLogParser, IMA_EVENT_NAME_LEN_MAX}; + use sha1::{Digest as _, Sha1}; + + let digest = [0xEFu8; 20]; + let name = "/init"; + + let mut expected = Sha1::new(); + expected.update(digest); + let mut padded = [0u8; IMA_EVENT_NAME_LEN_MAX + 1]; + padded[..name.len()].copy_from_slice(name.as_bytes()); + expected.update(padded); + let expected: Vec = expected.finalize().to_vec(); + + // Build a log with that template hash. + let mut event = Vec::new(); + event.extend_from_slice(&10u32.to_le_bytes()); + event.extend_from_slice(&expected); + event.extend_from_slice(&(b"ima".len() as u32).to_le_bytes()); + event.extend_from_slice(b"ima"); + event.extend_from_slice(&digest); + event.extend_from_slice(&padded); + + let opts = ParseOptions::default().with_template_hash_algorithm(HashAlgorithm::Sha1); + let events: Vec<_> = EventLogParser::new(event.as_slice(), opts) + .collect::, _>>() + .unwrap(); + assert_eq!( + events[0].verify_template_hash(HashAlgorithm::Sha1), + Some(true) + ); +} + +#[test] +fn ascii_and_binary_agree_on_decoded_form() { + let d = Digest::new(HashAlgorithm::Sha1, vec![0xCC; 20]); + let binary = build_ima_ng_event(10, &[0x00; 20], &d, "/etc/hosts"); + let opts = ParseOptions::default().with_template_hash_algorithm(HashAlgorithm::Sha1); + let from_binary = EventLogParser::new(binary.as_slice(), opts) + .next() + .unwrap() + .unwrap(); + + let ascii_line = format!( + "10 {} ima-ng sha1:{} /etc/hosts", + "00".repeat(20), + "cc".repeat(20), + ); + let from_ascii = parse_ascii_log(&ascii_line).unwrap().remove(0); + + assert_eq!(from_binary.pcr_index, from_ascii.pcr_index); + assert_eq!(from_binary.template_name, from_ascii.template_name); + match (&from_binary.template_data, &from_ascii.template_data) { + (TemplateData::ImaNg(a), TemplateData::ImaNg(b)) => { + assert_eq!(a, b); + } + other => panic!("{:?}", other), + } +} diff --git a/tests/testdata.rs b/tests/testdata.rs new file mode 100644 index 0000000..4cdbf51 --- /dev/null +++ b/tests/testdata.rs @@ -0,0 +1,226 @@ +// SPDX-License-Identifier: Apache-2.0 + +//! Integration tests that exercise the parsers against real-world fixtures +//! captured from a Linux VM (see `testdata/`). + +use std::fs; +use std::io::BufReader; +use std::path::{Path, PathBuf}; + +use ima_parser::hash::HashAlgorithm; +use ima_parser::log::{Endianness, EventLogParser, ParseOptions, TemplateData, parse_ascii_log}; +use ima_parser::policy::parse_policy; + +fn testdata(rel: &str) -> PathBuf { + Path::new(env!("CARGO_MANIFEST_DIR")) + .join("testdata") + .join(rel) +} + +const TEMPLATE_HASH_ALGOS: &[(HashAlgorithm, &str)] = &[ + (HashAlgorithm::Sha1, "sha1"), + (HashAlgorithm::Sha256, "sha256"), + (HashAlgorithm::Sha384, "sha384"), +]; + +#[test] +fn parses_builtin_tcb_policy() { + let text = fs::read_to_string(testdata("policies/builtin-tcb-policy")).unwrap(); + let policy = parse_policy(&text).unwrap(); + // The kernel's built-in `ima_tcb` policy expands to roughly a dozen rules: + // a handful of `dont_measure fsmagic=…` lines plus the trailing + // `measure func=…` rules. We don't pin the exact count (it has wandered + // across kernel versions), only that every non-comment line parsed. + let non_blank = text + .lines() + .filter(|l| !l.trim().is_empty() && !l.trim_start().starts_with('#')) + .count(); + assert_eq!(policy.rules.len(), non_blank); + assert!(!policy.rules.is_empty()); +} + +#[test] +fn parses_builtin_tcb_appraise_policy() { + let text = fs::read_to_string(testdata("policies/builtin-tcb-appraise-policy")).unwrap(); + let policy = parse_policy(&text).unwrap(); + let non_blank = text + .lines() + .filter(|l| !l.trim().is_empty() && !l.trim_start().starts_with('#')) + .count(); + assert_eq!(policy.rules.len(), non_blank); + assert!(!policy.rules.is_empty()); +} + +#[test] +fn parses_custom_ima_policy() { + let text = fs::read_to_string(testdata("policies/custom-ima-policy")).unwrap(); + let policy = parse_policy(&text).unwrap(); + let non_blank = text + .lines() + .filter(|l| !l.trim().is_empty() && !l.trim_start().starts_with('#')) + .count(); + assert_eq!(policy.rules.len(), non_blank); +} + +#[test] +fn parses_all_ascii_logs() { + for (algo, name) in TEMPLATE_HASH_ALGOS { + let path = testdata(&format!("logs/ascii_runtime_measurements_{name}")); + let text = + fs::read_to_string(&path).unwrap_or_else(|e| panic!("read {}: {e}", path.display())); + let events = + parse_ascii_log(&text).unwrap_or_else(|e| panic!("parse {}: {e}", path.display())); + assert!(!events.is_empty(), "{}: no events parsed", path.display()); + + for (i, ev) in events.iter().enumerate() { + assert_eq!( + ev.template_hash.len(), + algo.digest_size(), + "{}: event {i} has wrong template_hash size", + path.display(), + ); + assert_eq!( + ev.pcr_index, + 10, + "{}: event {i} unexpected PCR", + path.display() + ); + } + } +} + +#[cfg(feature = "hash")] +#[test] +fn ascii_logs_template_hashes_verify() { + for (algo, name) in TEMPLATE_HASH_ALGOS { + let path = testdata(&format!("logs/ascii_runtime_measurements_{name}")); + let text = fs::read_to_string(&path).unwrap(); + let events = parse_ascii_log(&text).unwrap(); + + // The first event of any IMA log is `boot_aggregate`, whose + // template_hash field in the ASCII rendering is computed by the + // kernel from the PCR bank (typically PCR[0]..PCR[9]), is not + // reproducible from the template data alone (it deliberately + // differs between ascii_runtime_measurements files for different + // banks). Skip it. + for (i, ev) in events.iter().enumerate().skip(1) { + assert_eq!( + ev.verify_template_hash(*algo), + Some(true), + "{}: event {i} ({}) failed template-hash verification", + path.display(), + ev.template_name, + ); + } + } +} + +#[test] +fn parses_all_binary_logs() { + for (algo, name) in TEMPLATE_HASH_ALGOS { + let path = testdata(&format!("logs/binary_runtime_measurements_{name}")); + let file = fs::File::open(&path).unwrap_or_else(|e| panic!("open {}: {e}", path.display())); + let opts = ParseOptions::default() + .with_endianness(Endianness::Little) + .with_template_hash_algorithm(*algo); + let events: Vec<_> = EventLogParser::new(BufReader::new(file), opts) + .collect::>() + .unwrap_or_else(|e| panic!("parse {}: {e}", path.display())); + + assert!(!events.is_empty(), "{}: no events parsed", path.display()); + for (i, ev) in events.iter().enumerate() { + assert_eq!( + ev.template_hash.len(), + algo.digest_size(), + "{}: event {i} has wrong template_hash size", + path.display(), + ); + } + } +} + +#[cfg(feature = "hash")] +#[test] +fn binary_logs_template_hashes_verify() { + for (algo, name) in TEMPLATE_HASH_ALGOS { + let path = testdata(&format!("logs/binary_runtime_measurements_{name}")); + let file = fs::File::open(&path).unwrap(); + let opts = ParseOptions::default() + .with_endianness(Endianness::Little) + .with_template_hash_algorithm(*algo); + let events: Vec<_> = EventLogParser::new(BufReader::new(file), opts) + .collect::>() + .unwrap(); + + for (i, ev) in events.iter().enumerate().skip(1) { + assert_eq!( + ev.verify_template_hash(*algo), + Some(true), + "{}: event {i} ({}) failed template-hash verification", + path.display(), + ev.template_name, + ); + } + } +} + +#[test] +fn ascii_and_binary_logs_agree() { + for (algo, name) in TEMPLATE_HASH_ALGOS { + let ascii_path = testdata(&format!("logs/ascii_runtime_measurements_{name}")); + let binary_path = testdata(&format!("logs/binary_runtime_measurements_{name}")); + + let ascii_text = fs::read_to_string(&ascii_path).unwrap(); + let ascii_events = parse_ascii_log(&ascii_text).unwrap(); + + let binary_file = fs::File::open(&binary_path).unwrap(); + let opts = ParseOptions::default() + .with_endianness(Endianness::Little) + .with_template_hash_algorithm(*algo); + let binary_events: Vec<_> = EventLogParser::new(BufReader::new(binary_file), opts) + .collect::>() + .unwrap(); + + assert_eq!( + ascii_events.len(), + binary_events.len(), + "{name}: event count differs between ASCII and binary logs", + ); + + for (i, (a, b)) in ascii_events.iter().zip(binary_events.iter()).enumerate() { + assert_eq!(a.pcr_index, b.pcr_index, "event {i}: pcr differs"); + assert_eq!( + a.template_name, b.template_name, + "event {i}: template name differs", + ); + // The first event is `boot_aggregate`; its template_hash differs + // between banks but the decoded template_data should still match. + assert!( + template_payload_eq(&a.template_data, &b.template_data), + "event {i} ({}): template_data differs between ASCII and binary", + a.template_name, + ); + if i > 0 { + assert_eq!( + a.template_hash, b.template_hash, + "event {i} ({}): template_hash differs between ASCII and binary", + a.template_name, + ); + } + } + } +} + +fn template_payload_eq(a: &TemplateData, b: &TemplateData) -> bool { + match (a, b) { + (TemplateData::Ima(x), TemplateData::Ima(y)) => x == y, + (TemplateData::ImaNg(x), TemplateData::ImaNg(y)) => x == y, + (TemplateData::ImaSig(x), TemplateData::ImaSig(y)) => { + x.filename == y.filename && x.digest == y.digest + } + (TemplateData::ImaBuf(x), TemplateData::ImaBuf(y)) => { + x.name == y.name && x.digest == y.digest + } + _ => false, + } +}