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
+
+
+
+[](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