├── .fmf
└── version
├── .github
├── dependabot.yml
└── workflows
│ ├── cargo-audit.yml
│ ├── container.yml
│ ├── keylime-bot.yml
│ ├── mockoon.yaml
│ ├── rust.yml
│ ├── submit-HEAD-coverage.yaml
│ └── submit-PR-coverage.yaml
├── .gitignore
├── .gitleaks.toml
├── .packit.yaml
├── .rustfmt.toml
├── CODEOWNERS
├── Cargo.lock
├── Cargo.toml
├── GNUmakefile
├── LICENSE
├── MAINTAINERS
├── README.md
├── codecov.yml
├── debian
└── keylime_agent.postinst
├── dist
└── systemd
│ └── system
│ ├── keylime_agent.service
│ └── var-lib-keylime-secure.mount
├── docker-compose.yml
├── docker
├── fedora
│ ├── dbus-policy.conf
│ ├── keylime_py.Dockerfile
│ ├── keylime_rust.Dockerfile
│ └── wait.sh
└── release
│ ├── Dockerfile.distroless
│ ├── Dockerfile.fedora
│ ├── Dockerfile.wolfi
│ └── build_locally.sh
├── ima-policy
├── README
└── ima-policy
├── keylime-agent.conf
├── keylime-agent
├── Cargo.toml
├── src
│ ├── agent_handler.rs
│ ├── api.rs
│ ├── common.rs
│ ├── config.rs
│ ├── errors_handler.rs
│ ├── keys_handler.rs
│ ├── main.rs
│ ├── notifications_handler.rs
│ ├── payloads.rs
│ ├── permissions.rs
│ ├── quotes_handler.rs
│ ├── revocation.rs
│ └── secure_mount.rs
├── test-data
│ ├── ima
│ │ └── ascii_runtime_measurements
│ ├── payload.zip
│ ├── revocation.sig
│ ├── test-cert.pem
│ ├── test-rsa.pem
│ ├── test.json
│ ├── test_input.txt
│ ├── test_ok.json
│ └── tpmdata_test.json
└── tests
│ ├── actions
│ ├── local_action_hello.py
│ ├── local_action_hello_shell.sh
│ ├── local_action_stand_alone.py
│ └── shim.py
│ └── unzipped
│ ├── action_list
│ ├── local_action_payload.py
│ ├── local_action_payload_shell.sh
│ ├── local_action_rev_script1.py
│ ├── local_action_rev_script2.py
│ ├── test_err.json
│ └── test_ok.json
├── keylime-ima-emulator
├── Cargo.toml
└── src
│ └── main.rs
├── keylime-push-model-agent
├── Cargo.toml
├── src
│ ├── json_dump.rs
│ ├── main.rs
│ ├── registration.rs
│ ├── struct_filler.rs
│ └── url_selector.rs
├── test-data
│ ├── evidence_handling_request.json
│ ├── evidence_supported_attestation_request.json
│ ├── session_request.json
│ └── verifier.json
└── tests
│ └── test-keylime-push-model-agent.rs
├── keylime
├── Cargo.toml
├── src
│ ├── agent_data.rs
│ ├── agent_identity.rs
│ ├── agent_registration.rs
│ ├── algorithms.rs
│ ├── cert.rs
│ ├── config
│ │ ├── mod.rs
│ │ └── push_model_config.rs
│ ├── context_info.rs
│ ├── crypto.rs
│ ├── crypto
│ │ ├── auth_tag.rs
│ │ ├── encrypted_data.rs
│ │ ├── symmkey.rs
│ │ └── x509.rs
│ ├── device_id.rs
│ ├── file_ops.rs
│ ├── global_config.rs
│ ├── hash_ek.rs
│ ├── hostname.pest
│ ├── hostname_parser.rs
│ ├── https_client.rs
│ ├── ima
│ │ ├── entry.rs
│ │ ├── measurement_list.rs
│ │ └── mod.rs
│ ├── ip.pest
│ ├── ip_parser.rs
│ ├── keylime_error.rs
│ ├── lib.rs
│ ├── list.pest
│ ├── list_parser.rs
│ ├── quote.rs
│ ├── registrar_client.rs
│ ├── serialization.rs
│ ├── structures
│ │ ├── capabilities_negotiation.rs
│ │ ├── evidence_handling.rs
│ │ ├── mod.rs
│ │ └── sessions.rs
│ ├── tpm.rs
│ └── version.rs
└── test-data
│ ├── prime256v1.cert.pem
│ ├── prime256v1.pem
│ ├── test-cert.pem
│ ├── test-quote.txt
│ ├── test-rsa.pem
│ └── test-rsa.sig
├── license-header.tpl
├── packit-ci.fmf
├── rpm
├── README.md
├── centos
│ └── keylime-agent-rust.spec
└── fedora
│ ├── keylime-agent-rust.spec
│ └── rust-keylime-metadata.patch
├── scripts
└── download_packit_coverage.sh
└── tests
├── actions
└── shim.py
├── ca.conf
├── common_tests.sh
├── generate-iak-idevid-certs.sh
├── mockoon_tests.sh
├── nopanic.ci
├── run.sh
├── seccomp-profile.json
└── setup_swtpm.sh
/.fmf/version:
--------------------------------------------------------------------------------
1 | 1
2 |
--------------------------------------------------------------------------------
/.github/dependabot.yml:
--------------------------------------------------------------------------------
1 | # To get started with Dependabot version updates, you'll need to specify which
2 | # package ecosystems to update and where the package manifests are located.
3 | # Please see the documentation for all configuration options:
4 | # https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates
5 |
6 | version: 2
7 | updates:
8 | - package-ecosystem: "cargo"
9 | directory: "/" # Location of package manifests
10 | schedule:
11 | interval: "daily"
12 | versioning-strategy: lockfile-only
13 | - package-ecosystem: "github-actions"
14 | directory: "/"
15 | schedule:
16 | interval: "daily"
17 |
--------------------------------------------------------------------------------
/.github/workflows/cargo-audit.yml:
--------------------------------------------------------------------------------
1 | name: Security audit
2 | on:
3 | push:
4 | branches:
5 | - master
6 | tags:
7 | - "v*"
8 | paths:
9 | - '**/Cargo.toml'
10 | - '**/Cargo.lock'
11 | pull_request:
12 | paths:
13 | - '**/Cargo.toml'
14 | - '**/Cargo.lock'
15 | workflow_dispatch:
16 | branches:
17 | - master
18 |
19 | jobs:
20 | security_audit:
21 | runs-on: ubuntu-latest
22 | steps:
23 | - uses: actions/checkout@v4
24 | - uses: actions-rs/audit-check@v1
25 | with:
26 | token: ${{ secrets.GITHUB_TOKEN }}
27 |
--------------------------------------------------------------------------------
/.github/workflows/container.yml:
--------------------------------------------------------------------------------
1 | name: Build Image
2 |
3 | on:
4 | push:
5 | branches:
6 | - master
7 | tags:
8 | - "v*"
9 | workflow_dispatch:
10 | branches:
11 | - master
12 |
13 | env:
14 | REGISTRY: quay.io
15 | IMAGE_BASE: quay.io/keylime
16 |
17 | jobs:
18 | build-images:
19 | runs-on: ubuntu-latest
20 | steps:
21 | - name: Checkout
22 | uses: actions/checkout@v4
23 |
24 | - name: Log in to the Container registry
25 | uses: docker/login-action@v3
26 | with:
27 | registry: ${{ env.REGISTRY }}
28 | username: ${{ secrets.QUAY_USER }}
29 | password: ${{ secrets.QUAY_TOKEN }}
30 |
31 | - name: Generate docker metadata for keylime_agent
32 | id: meta_agent
33 | uses: docker/metadata-action@v5
34 | with:
35 | images: |
36 | ${{ env.IMAGE_BASE }}/keylime_agent
37 | tags: |
38 | type=ref,enable=true,priority=600,prefix=,suffix=,event=branch
39 | type=ref,enable=true,priority=600,prefix=,suffix=,event=tag
40 | type=sha,prefix=sha-
41 |
42 | - name: Build and push agent image
43 | id: build_agent
44 | uses: docker/build-push-action@v6
45 | env:
46 | VERSION: "${{ steps.meta_base.outputs.version }}"
47 | with:
48 | context: .
49 | file: docker/release/Dockerfile.fedora
50 | push: true
51 | tags: ${{ steps.meta_agent.outputs.tags }}
52 | labels: ${{ steps.meta_agent.outputs.labels }}
53 |
--------------------------------------------------------------------------------
/.github/workflows/keylime-bot.yml:
--------------------------------------------------------------------------------
1 | name: keylime-bot
2 |
3 | on:
4 | check_run:
5 | check_suite:
6 | create:
7 | delete:
8 | deployment:
9 | deployment_status:
10 | fork:
11 | gollum:
12 | issue_comment:
13 | issues:
14 | label:
15 | milestone:
16 | page_build:
17 | project:
18 | project_card:
19 | project_column:
20 | public:
21 | pull_request_target:
22 | types:
23 | - assigned
24 | - unassigned
25 | - labeled
26 | - unlabeled
27 | - opened
28 | - edited
29 | - closed
30 | - reopened
31 | - synchronize
32 | - ready_for_review
33 | - locked
34 | - unlocked
35 | - review_requested
36 | - review_request_removed
37 | push:
38 | registry_package:
39 | release:
40 | status:
41 | watch:
42 | schedule:
43 | - cron: '*/15 * * * *'
44 | workflow_dispatch:
45 |
46 | jobs:
47 | pull-request-responsibility:
48 | runs-on: ubuntu-latest
49 | name: pull-request-responsibility
50 | steps:
51 | - uses: actions-automation/pull-request-responsibility@main
52 | with:
53 | token: ${{ secrets.BOT_TOKEN }}
54 | actions: "request,assign,copy-labels-linked"
55 | reviewers: "rust-team"
56 | num_to_request: 3
57 |
--------------------------------------------------------------------------------
/.github/workflows/mockoon.yaml:
--------------------------------------------------------------------------------
1 | ---
2 | name: "Mockoon Tests"
3 |
4 | "on":
5 | push:
6 | branches: [master]
7 | pull_request:
8 | branches: [master]
9 |
10 | jobs:
11 | mockoon-tests:
12 | runs-on: ubuntu-latest
13 | container:
14 | image: quay.io/keylime/keylime-ci:latest
15 | steps:
16 | - uses: actions/checkout@v4
17 | - name: NPM installation
18 | run: dnf install -y npm
19 | - name: Run Mockoon CLI
20 | uses: mockoon/cli-action@v2
21 | with:
22 | version: latest
23 | data-file: keylime-push-model-agent/test-data/verifier.json
24 | port: 3000
25 | - name: Set git safe.directory for the working directory
26 | run: git config --system --add safe.directory "$PWD"
27 | - name: Mockoon tests custom script execution
28 | run: bash tests/mockoon_tests.sh
29 |
--------------------------------------------------------------------------------
/.github/workflows/rust.yml:
--------------------------------------------------------------------------------
1 | name: Rust
2 |
3 | on:
4 | push:
5 | branches: [ master ]
6 | pull_request:
7 | branches: [ master ]
8 | workflow_dispatch:
9 |
10 | env:
11 | CARGO_TERM_COLOR: always
12 |
13 | jobs:
14 | static:
15 | name: Static code checks
16 | runs-on: ubuntu-latest
17 | steps:
18 | - uses: actions/checkout@v4
19 | - name: Check formatting
20 | run: cargo fmt --all -- --check
21 | - name: Check for panics
22 | run: ./tests/nopanic.ci
23 |
24 | tests:
25 | name: Fedora tests
26 | runs-on: ubuntu-latest
27 | container:
28 | image: quay.io/keylime/keylime-ci:latest
29 | steps:
30 | - uses: actions/checkout@v4
31 | - name: Set git safe.directory for the working directory
32 | run : git config --system --add safe.directory "$PWD"
33 | - name: Run tests
34 | run: bash tests/run.sh
35 | - uses: actions/upload-artifact@v4
36 | with:
37 | name: tarpaulin-report
38 | path: |
39 | tarpaulin-report.json
40 | tarpaulin-report.html
41 |
--------------------------------------------------------------------------------
/.github/workflows/submit-HEAD-coverage.yaml:
--------------------------------------------------------------------------------
1 | name: Upload code coverage for a merged PR to codecov.io
2 |
3 | on:
4 | push:
5 | branches:
6 | - master
7 |
8 | jobs:
9 | build:
10 | runs-on: ubuntu-22.04
11 | name: Submit code coverage from merged PR
12 | steps:
13 | - uses: actions/checkout@v4
14 | - name: Install testing-farm script
15 | run: pip3 -v install tft-cli
16 | - name: Run tests on Testing Farm
17 | run: testing-farm request --context distro=fedora-41 --arch x86_64 --compose Fedora-41 --plan '/e2e' -e UPLOAD_COVERAGE=1 2>&1 | tee tt_output
18 | env:
19 | TESTING_FARM_API_TOKEN: ${{ secrets.TESTING_FARM_API_TOKEN }}
20 | - name: Find PR Packit tests to finish and download e2e_coverage.txt and upstream_coverage.xml coverage files.
21 | run: grep -q 'tests passed' tt_output && sleep 20 && scripts/download_packit_coverage.sh --testing-farm-log tt_output
22 | env:
23 | MAX_DURATION: 120
24 | SLEEP_DELAY: 20
25 | - name: List downloaded files.
26 | run: ls
27 | - name: Upload e2e_coverage report to Codecov with GitHub Action.
28 | uses: codecov/codecov-action@v5
29 | env:
30 | CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
31 | with:
32 | files: e2e_coverage.txt
33 | flags: e2e-testsuite
34 | - name: Upload upstream_coverage report to Codecov with GitHub Action.
35 | uses: codecov/codecov-action@v5
36 | env:
37 | CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
38 | with:
39 | files: upstream_coverage.xml
40 | flags: upstream-unit-tests
41 |
--------------------------------------------------------------------------------
/.github/workflows/submit-PR-coverage.yaml:
--------------------------------------------------------------------------------
1 | name: PR code coverage measurement on codecov.io
2 |
3 | on: [pull_request]
4 |
5 | jobs:
6 | build:
7 | runs-on: ubuntu-latest
8 | continue-on-error: true
9 | name: Submit code coverage from Packit tests
10 | defaults:
11 | run:
12 | working-directory: scripts
13 | steps:
14 | - uses: actions/checkout@v4
15 | - name: Wait for Packit tests to finish and download e2e_coverage.txt and upstream_coverage.xml files.
16 | run: ./download_packit_coverage.sh
17 | env:
18 | MAX_DURATION: 5400
19 | SLEEP_DELAY: 120
20 | - name: List downloaded files.
21 | run: ls
22 | - name: Upload e2e_coverage.txt report to Codecov with GitHub Action.
23 | uses: codecov/codecov-action@v5
24 | env:
25 | CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
26 | with:
27 | files: scripts/e2e_coverage.txt
28 | flags: e2e-testsuite
29 | - name: Upload upstream_coverage.xml report to Codecov with GitHub Action.
30 | uses: codecov/codecov-action@v5
31 | env:
32 | CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
33 | with:
34 | files: scripts/upstream_coverage.xml
35 | flags: upstream-unit-tests
36 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | /target
2 | **/*.rs.bk
3 | .auditing-0
4 | tpmdata.json
5 | tests/.tmp*
6 | tests/actions/__pycache__
7 | tests/unzipped/__pycache__
8 | **/tarpaulin-report.*
9 | **/__pycache__
10 | *.swp
11 | *.orig
12 |
--------------------------------------------------------------------------------
/.gitleaks.toml:
--------------------------------------------------------------------------------
1 | #
2 | # GitLeaks Repo Specific Configuration
3 | #
4 | # This allowlist is used to help Red Hat ignore false positives during its code
5 | # scans.
6 |
7 | [allowlist]
8 | paths = [
9 | '''test-data/test-rsa.pem''',
10 | '''keylime/test-data/test-rsa.pem''',
11 | '''keylime/test-data/prime256v1.pem''',
12 | ]
13 |
--------------------------------------------------------------------------------
/.packit.yaml:
--------------------------------------------------------------------------------
1 | downstream_package_name: keylime-agent-rust
2 | upstream_project_url: https://github.com/keylime/rust-keylime
3 | specfile_path: rpm/fedora/keylime-agent-rust.spec
4 | actions:
5 | get-current-version:
6 | - bash -c "git describe --tags --abbrev=0 | sed 's/v\(.*\)/\1/g'"
7 |
8 | srpm_build_deps:
9 | - cargo
10 | - rust
11 | - git
12 | - openssl-devel
13 |
14 | jobs:
15 | - job: tests
16 | trigger: pull_request
17 | metadata:
18 | targets:
19 | - fedora-branched
20 | - centos-stream-10-x86_64
21 | skip_build: true
22 |
23 | - job: copr_build
24 | trigger: commit
25 | branch: master
26 | identifier: fedora
27 | specfile_path: rpm/fedora/keylime-agent-rust.spec
28 | files_to_sync:
29 | - rpm/fedora/*
30 | actions:
31 | get-current-version:
32 | bash -c "git describe --tags --abbrev=0 | sed 's/v\(.*\)/\1/g'"
33 | post-upstream-clone:
34 | bash -c 'if [[ ! -d /var/tmp/cargo-vendor-filterer ]]; then git clone https://github.com/cgwalters/cargo-vendor-filterer.git /var/tmp/cargo-vendor-filterer; fi &&
35 | cd /var/tmp/cargo-vendor-filterer &&
36 | cargo build &&
37 | cd - &&
38 | cp /var/tmp/cargo-vendor-filterer/target/debug/cargo-vendor-filterer . &&
39 | ./cargo-vendor-filterer --all-features
40 | --platform x86_64-unknown-linux-gnu
41 | --platform powerpc64le-unknown-linux-gnu
42 | --platform aarch64-unknown-linux-gnu
43 | --platform i686-unknown-linux-gnu
44 | --platform s390x-unknown-linux-gnu
45 | --exclude-crate-path "libloading#tests" &&
46 | tar jcf rpm/fedora/rust-keylime-vendor.tar.xz vendor'
47 | targets:
48 | - fedora-all
49 | preserve_project: True
50 |
51 | - job: copr_build
52 | trigger: commit
53 | branch: master
54 | identifier: centos
55 | specfile_path: rpm/centos/keylime-agent-rust.spec
56 | files_to_sync:
57 | - rpm/centos/*
58 | actions:
59 | get-current-version:
60 | bash -c "git describe --tags --abbrev=0 | sed 's/v\(.*\)/\1/g'"
61 | post-upstream-clone:
62 | bash -c 'if [[ ! -d /var/tmp/cargo-vendor-filterer ]]; then git clone https://github.com/cgwalters/cargo-vendor-filterer.git /var/tmp/cargo-vendor-filterer; fi &&
63 | cd /var/tmp/cargo-vendor-filterer &&
64 | cargo build &&
65 | cd - &&
66 | cp /var/tmp/cargo-vendor-filterer/target/debug/cargo-vendor-filterer . &&
67 | ./cargo-vendor-filterer --all-features
68 | --platform x86_64-unknown-linux-gnu
69 | --platform powerpc64le-unknown-linux-gnu
70 | --platform aarch64-unknown-linux-gnu
71 | --platform i686-unknown-linux-gnu
72 | --platform s390x-unknown-linux-gnu
73 | --exclude-crate-path "libloading#tests" &&
74 | tar jcf rpm/centos/rust-keylime-vendor.tar.xz vendor'
75 | targets:
76 | - centos-stream-9-x86_64
77 | - centos-stream-10-x86_64
78 | preserve_project: True
79 |
--------------------------------------------------------------------------------
/.rustfmt.toml:
--------------------------------------------------------------------------------
1 | max_width = 78
2 | # Enable when available: https://github.com/rust-lang/rustfmt/issues/3352
3 | #license_template_path = "license-header.tpl"
4 |
--------------------------------------------------------------------------------
/CODEOWNERS:
--------------------------------------------------------------------------------
1 | @keylime/rust-team @lkatalin
2 |
--------------------------------------------------------------------------------
/Cargo.toml:
--------------------------------------------------------------------------------
1 | [workspace]
2 | members = [
3 | "keylime",
4 | "keylime-agent",
5 | "keylime-ima-emulator",
6 | "keylime-push-model-agent",
7 | ]
8 | resolver = "2"
9 |
10 | [workspace.package]
11 | authors = ["Keylime Authors"]
12 | edition = "2021"
13 | license = "Apache-2.0"
14 | repository = "https://github.com/keylime/rust-keylime"
15 | version = "0.2.7"
16 |
17 | [workspace.dependencies]
18 | actix-rt = "2"
19 | actix-web = { version = "4", default-features = false, features = ["macros", "openssl"] }
20 | anyhow = { version = "1.0", features = ["backtrace"] }
21 | assert_cmd = { version = "2.0.16" }
22 | base64 = "0.22"
23 | cfg-if = "1"
24 | chrono = { version = "0.4.40", features = ["serde"] }
25 | clap = { version = "4.5", features = ["derive"] }
26 | config = { version = "0.13", default-features = false, features = ["toml"] }
27 | futures = "0.3.6"
28 | glob = "0.3"
29 | hex = "0.4"
30 | keylime = { version = "=0.2.7", path = "keylime" }
31 | libc = "0.2.43"
32 | log = "0.4"
33 | once_cell = "1.19.0"
34 | openssl = "0.10.15"
35 | pest = "2"
36 | pest_derive = "2"
37 | picky-asn1-der = "0.4"
38 | picky-asn1-x509 = "0.12"
39 | predicates = { version = "3.1.3" }
40 | pretty_env_logger = "0.5"
41 | reqwest = {version = "0.12", default-features = false, features = ["json", "native-tls"]}
42 | serde = "1.0.80"
43 | serde_derive = "1.0.80"
44 | serde_json = { version = "1.0", features = ["raw_value"] }
45 | signal-hook = "0.3"
46 | static_assertions = "1"
47 | tempfile = "3.4.0"
48 | thiserror = "2.0"
49 | tokio = {version = "1", features = ["rt", "sync", "macros"]}
50 | tss-esapi = {version = "7.6.0", features = ["generate-bindings"]}
51 | uuid = {version = "1.3", features = ["v4"]}
52 | zip = {version = "0.6", default-features = false, features= ["deflate"]}
53 |
--------------------------------------------------------------------------------
/GNUmakefile:
--------------------------------------------------------------------------------
1 | # SPDX-License-Identifier: Apache-2.0
2 | # Copyright 2022 Keylime Authors
3 |
4 | RELEASE ?= 0
5 | TARGETDIR ?= target
6 | CONFFILE ?= ./keylime-agent.conf
7 |
8 | ifeq ($(RELEASE),1)
9 | PROFILE ?= release
10 | CARGO_ARGS = --release
11 | else
12 | PROFILE ?= debug
13 | CARGO_ARGS =
14 | endif
15 |
16 | systemdsystemunitdir := $(shell pkg-config systemd --variable=systemdsystemunitdir)
17 |
18 | programs = \
19 | ${TARGETDIR}/${PROFILE}/keylime_agent \
20 | ${TARGETDIR}/${PROFILE}/keylime_ima_emulator
21 |
22 | .PHONY: all
23 | all: $(programs)
24 |
25 | $(programs):
26 | cargo build --target-dir="${TARGETDIR}" ${CARGO_ARGS}
27 |
28 | .PHONY: clean
29 | clean::
30 | ${RM} -rf target
31 |
32 | .PHONY: install
33 | install: all
34 | mkdir -p ${DESTDIR}/etc/keylime/
35 | mkdir -p ${DESTDIR}/etc/keylime/agent.conf.d
36 | cp ${CONFFILE} ${DESTDIR}/etc/keylime/agent.conf
37 | for f in $(programs); do \
38 | install -D -t ${DESTDIR}/usr/bin "$$f"; \
39 | done
40 | install -D -m 644 -t ${DESTDIR}$(systemdsystemunitdir) dist/systemd/system/keylime_agent.service
41 | install -D -m 644 -t ${DESTDIR}$(systemdsystemunitdir) dist/systemd/system/var-lib-keylime-secure.mount
42 | # Remove when https://github.com/keylime/rust-keylime/issues/325 is fixed
43 | install -D -t ${DESTDIR}/usr/libexec/keylime keylime-agent/tests/actions/shim.py
44 |
45 | # This only runs tests without TPM access. See tests/run.sh for
46 | # running full testsuite with swtpm.
47 | .PHONY: check
48 | check: all
49 | cargo test --target-dir="${TARGETDIR}"
50 |
--------------------------------------------------------------------------------
/MAINTAINERS:
--------------------------------------------------------------------------------
1 | @ashcrow
2 | @mbestavros
3 | @lukehinds
4 | @lkatalin
5 | @puiterwijk
6 | @mpeters
7 | @ueno
8 | @ansasaki
9 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Keylime
2 |
3 | [](https://www.apache.org/licenses/LICENSE-2.0)
4 |
5 | ## Overview
6 |
7 | This is a Rust implementation of
8 | [keylime](https://github.com/keylime/keylime) agent. Keylime is system
9 | integrity monitoring system that has the following features:
10 |
11 | * Exposes TPM trust chain for higher-level use
12 | * Provides an end-to-end solution for bootstrapping node cryptographic
13 | identities
14 | * Securely monitors system integrity
15 |
16 | For more information, visit the [keylime website](https://keylime.dev)
17 |
18 | For now, this project is focusing on the keylime agent component, which is a
19 | HTTP server running on the machine that executes keylime operations.
20 | Most keylime operations rely on TPM co-processor; therefore, the server needs
21 | a physical TPM chip (or a TPM emulator) to perform keylime operations. The
22 | TPM emulator is a program that runs in the daemon to mimic TPM commands.
23 |
24 | The rust-keylime agent is the official agent (starting with version 0.1.0) and
25 | replaces the Python implementation.
26 |
27 | ## Prerequisites
28 |
29 | ### Required Packages
30 |
31 | #### Fedora
32 |
33 | The following packages are required for building:
34 |
35 | * `clang`
36 | * `openssl-devel`
37 | * `tpm2-tss-devel`
38 | * (optional for the `with-zmq` feature): `zeromq-devel`
39 |
40 | To install, use the following command:
41 | ```
42 | $ dnf install clang openssl-devel tpm2-tss-devel zeromq-devel
43 | ```
44 |
45 | For runtime, the following packages are required:
46 |
47 | * `openssl`
48 | * `tpm2-tss`
49 | * `systemd` (to run as systemd service)
50 | * `util-linux-core` (for the `mount` command)
51 | * (optional for the `with-zmq` feature): `zeromq`
52 |
53 | #### Debian and Ubuntu
54 |
55 | For Debian and Ubuntu, use the following packages are required:
56 |
57 | * `libclang-dev`
58 | * `libssl-dev`
59 | * `libtss2-dev`
60 | * `pkg-config`
61 | * (optional for the `with-zmq` feature): `libzmq3-dev`
62 |
63 | To install, use the following command:
64 |
65 | ```
66 | $ apt-get install libclang-dev libssl-dev libtss2-dev libzmq3-dev pkg-config
67 | ```
68 |
69 | For runtime, the following packages are required:
70 |
71 | * `coreutils` (for the `mount` command)
72 | * `libssl`
73 | * `libtss2-esys-3.0.2-0`
74 | * (optional for the `with-zmq` feature): `libzmq3`
75 | * `systemd` (to run as systemd service)
76 |
77 | ### Rust
78 |
79 | Make sure Rust is installed before running Keylime. Installation
80 | instructions can be found [here](https://www.rust-lang.org/en-US/install.html).
81 |
82 | ## Logging env
83 |
84 | To run with `pretty-env-logger` trace logging active, set cargo run
85 | within `RUST_LOG`, as follows:
86 |
87 | $ RUST_LOG=keylime_agent=trace cargo run --bin keylime_agent
88 |
89 | ## Testing
90 |
91 | Unit tests are gating in CI for new code submission. To run them:
92 |
93 | ```
94 | $ cargo test
95 | ```
96 | In case you want to execute [Mockoon](https://mockoon.com/) based tests, you need to follow two steps:
97 |
98 | 1. Start Mockoon with [appropriate configuration file](https://github.com/keylime/rust-keylime/blob/master/keylime-push-model-agent/test-data/verifier.json) on port 3000
99 | 2. Execute tests through MOCKOON environment variable:
100 |
101 | ```
102 | $ MOCKOON=1 cargo test
103 | ```
104 |
105 | ## Running agent as a systemd-managed service
106 |
107 | To make deployment and management of the service easier, this crate
108 | comes with a Makefile and systemd unit file.
109 |
110 | To install the executables and the unit file, do:
111 |
112 | ```console
113 | $ make
114 | $ sudo make install
115 | ```
116 |
117 | Then you should be able to start the service with:
118 |
119 | ```console
120 | $ sudo systemctl start keylime_agent
121 | ```
122 |
123 | ## Building Debian package with cargo-deb
124 |
125 | Cargo deb requires Rust 1.60, so on Debian you need to install it first from rustup.rs.
126 |
127 | ```shell
128 | # Install cargo-deb
129 | rustup update
130 | cargo install cargo-deb
131 |
132 | # Build Debian package
133 | cargo deb -p keylime_agent
134 | ```
135 |
--------------------------------------------------------------------------------
/codecov.yml:
--------------------------------------------------------------------------------
1 | codecov:
2 | notify:
3 | after_n_builds: 2
4 |
5 | coverage:
6 | status:
7 | project:
8 | default:
9 | informational: true
10 |
11 | ignore:
12 | - '**/lib.rs'
13 |
14 | comment:
15 | layout: "flags,files"
16 | behavior: default
17 | require_changes: false
18 | require_base: no
19 | after_n_builds: 2
20 |
21 | flags:
22 | e2e-testsuite:
23 | carryforward: false
24 | upstream-unit-tests:
25 | carryforward: false
26 |
--------------------------------------------------------------------------------
/debian/keylime_agent.postinst:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 |
3 | set -e
4 |
5 |
6 | keylime_setup_group_user ()
7 | {
8 | # creating tss group if he isn't already there
9 | if ! getent group tss >/dev/null; then
10 | addgroup --system tss
11 | fi
12 |
13 | # creating tss user if he isn't already there
14 | if ! getent passwd tss >/dev/null; then
15 | adduser --system --ingroup tss --shell /bin/false \
16 | --home /var/lib/tpm --no-create-home \
17 | --gecos "TPM software stack" \
18 | tss
19 | fi
20 |
21 | # creating keylime user if he isn't already there
22 | if ! getent passwd keylime >/dev/null; then
23 | adduser --system --ingroup tss --shell /bin/false \
24 | --home /var/lib/keylime --no-create-home \
25 | --gecos "Keylime remote attestation" \
26 | keylime
27 | fi
28 | }
29 |
30 | case "$1" in
31 | configure)
32 | # Setup the keylime user and the tss group
33 | keylime_setup_group_user
34 |
35 | mkdir -p /var/lib/keylime
36 |
37 | # Setting owner
38 | if [ -d /var/lib/keylime ] && ! dpkg-statoverride --list /var/lib/keylime >/dev/null && getent passwd keylime >/dev/null; then
39 | chown -R keylime:tss /var/lib/keylime
40 | fi
41 |
42 | ;;
43 |
44 | abort-upgrade|abort-remove|abort-deconfigure)
45 | ;;
46 |
47 | *)
48 | echo "postinst called with unknown argument \`$1'" >&2
49 | exit 1
50 | ;;
51 | esac
52 |
53 | #DEBHELPER#
54 |
--------------------------------------------------------------------------------
/dist/systemd/system/keylime_agent.service:
--------------------------------------------------------------------------------
1 | [Unit]
2 | Description=The Keylime compute agent
3 | StartLimitInterval=10s
4 | StartLimitBurst=5
5 | Requires=var-lib-keylime-secure.mount
6 | After=var-lib-keylime-secure.mount
7 | After=network-online.target
8 | Wants=network-online.target
9 | Wants=tpm2-abrmd.service
10 | After=tpm2-abrmd.service
11 | # If the service should start only when hardware TPMs are available, uncomment the below lines
12 | #ConditionPathExistsGlob=/dev/tpm[0-9]*
13 | #ConditionPathExistsGlob=/dev/tpmrm[0-9]*
14 |
15 | [Service]
16 | ExecStart=/usr/bin/keylime_agent
17 | TimeoutSec=60s
18 | Restart=on-failure
19 | RestartSec=120s
20 | Environment="RUST_LOG=keylime_agent=info,keylime=info"
21 | # If using swtpm with tpm2-abrmd service, uncomment the line below to set TCTI
22 | # variable on the service environment
23 | #Environment="TCTI=tabrmd:"
24 |
25 | [Install]
26 | WantedBy=default.target
27 |
--------------------------------------------------------------------------------
/dist/systemd/system/var-lib-keylime-secure.mount:
--------------------------------------------------------------------------------
1 | [Unit]
2 | Description=Keylime configuration filesystem
3 | Before=keylime_agent.service
4 |
5 | [Mount]
6 | What=tmpfs
7 | Where=/var/lib/keylime/secure
8 | Type=tmpfs
9 | Options=mode=0700,size=1m,uid=keylime,gid=tss
10 |
11 | [Install]
12 | WantedBy=multi-user.target
13 |
--------------------------------------------------------------------------------
/docker-compose.yml:
--------------------------------------------------------------------------------
1 | version: '3.6'
2 | services:
3 | keylime-verifier:
4 | build:
5 | context: ./docker/fedora/
6 | dockerfile: keylime_py.Dockerfile
7 | image: keylime_py
8 | hostname: 'keylime-verifier'
9 | user: root
10 | volumes:
11 | - secure-volume:/var/lib/keylime
12 | ports:
13 | - "8892:8892"
14 | command: [
15 | "/usr/local/bin/keylime_verifier"
16 | ]
17 | keylime-registrar:
18 | build:
19 | context: ./docker/fedora/
20 | dockerfile: keylime_py.Dockerfile
21 | image: keylime_py
22 | hostname: 'keylime-verifier' # this is a bit of poor workaround, will fix in python code
23 | user: root
24 | volumes:
25 | - secure-volume:/var/lib/keylime
26 | ports:
27 | - "8891:8891"
28 | - "8890:8890"
29 | command: ["/root/wait.sh", "/var/lib/keylime/cv_ca/client-cert.crt", "keylime_registrar"]
30 | keylime_agent:
31 | build:
32 | context: ./docker/fedora/
33 | dockerfile: keylime_rust.Dockerfile
34 | image: keylime_rust
35 | hostname: 'keylime-agent'
36 | user: root
37 | volumes:
38 | - ./target/debug/:/rust-keylime
39 | network_mode: host
40 | environment:
41 | - TCTI=tabrmd:bus_type=system
42 | command:
43 | - /bin/bash
44 | - -c
45 | - |
46 | mkdir /tmp/tpmdir
47 | rm -rf /var/run/dbus
48 | mkdir /var/run/dbus
49 | dbus-daemon --system
50 | ls /etc/dbus-1/system.d/
51 | swtpm_setup --tpm2 \
52 | --tpmstate /tmp/tpmdir \
53 | --createek --decryption --create-ek-cert \
54 | --create-platform-cert \
55 | --display
56 | swtpm socket --tpm2 \
57 | --tpmstate dir=/tmp/tpmdir \
58 | --flags startup-clear \
59 | --ctrl type=tcp,port=2322 \
60 | --server type=tcp,port=2321 \
61 | --daemon
62 | tpm2-abrmd \
63 | --logger=stdout \
64 | --tcti=swtpm: \
65 | --allow-root \
66 | --flush-all &
67 | RUST_LOG=keylime_agent=trace /rust-keylime/keylime_agent
68 | volumes:
69 | secure-volume:
70 |
71 |
--------------------------------------------------------------------------------
/docker/fedora/dbus-policy.conf:
--------------------------------------------------------------------------------
1 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/docker/fedora/keylime_py.Dockerfile:
--------------------------------------------------------------------------------
1 | ##############################################################################
2 | # keylime TPM 2.0 Python Dockerfile
3 | #
4 | # This file is for automatic test running of Keylime and rust-keylime.
5 | # It is not recommended for use beyond testing scenarios.
6 | ##############################################################################
7 |
8 | FROM fedora:latest
9 | LABEL version="2.0.1" description="Keylime - Python Devel Env"
10 |
11 | # environment variables
12 | ARG BRANCH=master
13 | ENV container docker
14 | ENV HOME /root
15 | ENV KEYLIME_HOME ${HOME}/keylime
16 | ENV TPM_HOME ${HOME}/swtpm2
17 | COPY dbus-policy.conf /etc/dbus-1/system.d/
18 | COPY wait.sh /root/
19 | RUN ["chmod", "+x", "/root/wait.sh"]
20 |
21 |
22 | # Install dev tools and libraries (includes openssl-devel)
23 | RUN dnf groupinstall -y \
24 | "Development Tools" \
25 | "Development Libraries"
26 |
27 | # Install additional packages
28 | RUN dnf install -y \
29 | clang-devel \
30 | kmod \
31 | llvm llvm-devel \
32 | pkg-config \
33 | automake \
34 | cargo \
35 | clang-devel \
36 | dbus \
37 | dbus-daemon \
38 | dbus-devel \
39 | dnf-plugins-core \
40 | efivar-devel \
41 | gcc \
42 | git \
43 | glib2-devel \
44 | glib2-static \
45 | gnulib \
46 | kmod \
47 | libselinux-python3 \
48 | libtool \
49 | libtpms \
50 | make \
51 | openssl-devel \
52 | procps \
53 | python3-cryptography \
54 | python3-dbus \
55 | python3-devel \
56 | python3-m2crypto \
57 | python3-pip \
58 | python3-requests \
59 | python3-setuptools \
60 | python3-sqlalchemy \
61 | python3-simplejson \
62 | python3-tornado \
63 | python3-virtualenv \
64 | python3-yaml \
65 | python3-zmq \
66 | python3-pyasn1 \
67 | redhat-rpm-config \
68 | tpm2-abrmd \
69 | tpm2-tools \
70 | tpm2-tss \
71 | tpm2-tss-devel \
72 | uthash-devel \
73 | wget \
74 | which
75 |
76 | WORKDIR ${HOME}
77 | RUN git clone https://github.com/keylime/keylime.git && \
78 | cd keylime && \
79 | sed -e 's/127.0.0.1/0.0.0.0/g' keylime-agent.conf > tmp_keylime-agent.conf && \
80 | mv tmp_keylime-agent.conf keylime-agent.conf && \
81 | python3 ${KEYLIME_HOME}/setup.py install && \
82 | pip3 install -r $KEYLIME_HOME/requirements.txt && \
83 | ${KEYLIME_HOME}/services/installer.sh
84 |
85 | RUN dnf makecache && \
86 | dnf clean all && \
87 | rm -rf /var/cache/dnf/*
88 |
--------------------------------------------------------------------------------
/docker/fedora/keylime_rust.Dockerfile:
--------------------------------------------------------------------------------
1 | ##############################################################################
2 | # keylime TPM 2.0 Rust Dockerfile
3 | #
4 | # This file is for automatic test running of Keylime and rust-keylime.
5 | # It is not recommended for use beyond testing scenarios.
6 | ##############################################################################
7 |
8 | FROM fedora:latest
9 | LABEL version="2.0.1" description="Keylime - Bootstrapping and Maintaining Trust in the Cloud"
10 |
11 | # environment variables
12 | ARG BRANCH=master
13 | ENV RUST_KEYLIME_HOME ${HOME}/rust-keylime
14 | ENV container docker
15 | COPY dbus-policy.conf /etc/dbus-1/system.d/
16 |
17 | # Install dev tools and libraries (includes openssl-devel)
18 | RUN dnf groupinstall -y \
19 | "Development Tools" \
20 | "Development Libraries"
21 |
22 | # Packaged dependencies
23 | ENV PKGS_DEPS="automake \
24 | cargo \
25 | clang-devel \
26 | dbus \
27 | dbus-daemon \
28 | dbus-devel \
29 | dnf-plugins-core \
30 | efivar-devel \
31 | gcc \
32 | git \
33 | glib2-devel \
34 | glib2-static \
35 | gnulib \
36 | kmod \
37 | llvm llvm-devel \
38 | libselinux-python3 \
39 | libtool \
40 | libtpms \
41 | make \
42 | openssl-devel \
43 | redhat-rpm-config \
44 | rust clippy cargo \
45 | pkg-config \
46 | swtpm \
47 | swtpm-tools \
48 | tpm2-abrmd \
49 | tpm2-tools \
50 | tpm2-tss \
51 | tpm2-tss-devel \
52 | uthash-devel \
53 | czmq-devel"
54 |
55 | RUN dnf makecache && \
56 | dnf -y install $PKGS_DEPS && \
57 | dnf clean all && \
58 | rm -rf /var/cache/dnf/*
59 |
60 | # clone and build rust-keylime
61 | WORKDIR ${RUST_KEYLIME_HOME}
62 | RUN git clone https://github.com/keylime/rust-keylime.git && \
63 | cd rust-keylime && \
64 | make && \
65 | make install && \
66 | cargo clean
67 |
--------------------------------------------------------------------------------
/docker/fedora/wait.sh:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 | # SPDX-License-Identifier: Apache-2.0
3 | # Copyright 2021 Keylime Authors
4 | #
5 | # wait.sh
6 |
7 | set -e
8 |
9 | file="$1"
10 | shift
11 | cmd="$@"
12 |
13 | # while ! nc -z $host 8881;
14 | while ! test -f $file;
15 | do
16 | >&2 echo "Verifier certificate unavailable - sleeping"
17 | sleep 2
18 | done
19 |
20 | >&2 echo "Verifier certificate available - executing registrar"
21 | exec $cmd
--------------------------------------------------------------------------------
/docker/release/Dockerfile.distroless:
--------------------------------------------------------------------------------
1 | # SPDX-License-Identifier: Apache-2.0
2 | # the builder stage which uses the official rust base image (based on Debian 11) to build the keylime agent
3 | FROM rust:1.70-bullseye AS builder
4 |
5 | # we are using the "generate-bindings" feature for the tss-esapi crate which requires clang/llvm
6 | RUN apt-get update && apt-get install -y --no-install-recommends clang llvm
7 |
8 | # Install tpm2-tss (dependency for the tss-esapi crate)
9 | WORKDIR /src
10 | RUN wget https://github.com/tpm2-software/tpm2-tss/releases/download/4.0.1/tpm2-tss-4.0.1.tar.gz
11 | RUN tar xf tpm2-tss-4.0.1.tar.gz
12 | WORKDIR /src/tpm2-tss-4.0.1
13 | RUN ./configure \
14 | --prefix=/usr \
15 | --disable-static \
16 | --disable-fapi \
17 | --disable-policy \
18 | --disable-doxygen-doc \
19 | --disable-defaultflags
20 | RUN make
21 | RUN make install
22 |
23 | # build rust-keylime
24 | COPY . /src/rust-keylime/
25 | WORKDIR /src/rust-keylime
26 | RUN make RELEASE=1 TARGETDIR=target target/release/keylime_agent
27 |
28 | # truly just for debugging purposes for the assembly stage
29 | RUN readelf -W \
30 | --file-header --program-headers --sections --dynamic --notes --version-info --arch-specific --unwind --section-groups --histogram \
31 | /src/rust-keylime/target/release/keylime_agent
32 | RUN ldd /src/rust-keylime/target/release/keylime_agent
33 |
34 | # now assemble a release docker image using a minimal docker image
35 | FROM gcr.io/distroless/cc-debian11:latest
36 | ARG VERSION=latest
37 | LABEL org.opencontainers.image.authors="Keylime Team "
38 | LABEL org.opencontainers.image.version="$VERSION"
39 | LABEL org.opencontainers.image.title="Keylime Agent"
40 | LABEL org.opencontainers.image.description="Keylime Agent - Bootstrapping and Maintaining Trust in the Cloud"
41 | LABEL org.opencontainers.image.url="https://keylime.dev/"
42 | LABEL org.opencontainers.image.source="https://github.com/keylime/rust-keylime/"
43 | LABEL org.opencontainers.image.licenses="Apache-2.0"
44 | LABEL org.opencontainers.image.vendor="The Keylime Authors"
45 |
46 | # Copy all agent dependencies from the builder image
47 | # NOTE: the cc base image comes with all C runtime dependencies (libc, libm, libgcc, etc.), so no need to copy those
48 | # TODO: Unfortunately the COPY directive is following links and not preserving the link file. This slightly bloats the image.
49 |
50 | # libz is a direct dependency for the zip crate
51 | COPY --from=builder \
52 | /lib/x86_64-linux-gnu/libz.so* \
53 | /usr/lib/x86_64-linux-gnu/
54 | # tpm2-tss libraries are a dependency (probably not all of them, but we just copy all)
55 | # because we are using the tss-esapi crate which is essentially just a wrapper around those (unfortunately)
56 | COPY --from=builder \
57 | /usr/lib/libtss2*.so* \
58 | /usr/lib/x86_64-linux-gnu/
59 |
60 | # now copy the agent from the builder
61 | COPY --from=builder /src/rust-keylime/target/release/keylime_agent /bin/keylime_agent
62 | COPY --from=builder /src/rust-keylime/keylime-agent.conf /etc/keylime/agent.conf
63 | ENTRYPOINT ["/bin/keylime_agent"]
64 |
65 | # we default the log level to info if not overwritten
66 | ENV RUST_LOG=keylime_agent=info
67 |
68 | # the agent currently listens on this port by default
69 | # it's good practice to declare this in the Dockerfile
70 | EXPOSE 9002/tcp
71 |
72 | # these are all podman labels that work with the 'podman container runlabel' command, and are standardized at least in RHEL (install, uninstall, run)
73 | LABEL install="podman volume create keylime-agent"
74 | LABEL uninstall="podman volume rm keylime-agent"
75 | LABEL run="podman run --read-only --name keylime-agent --rm --device /dev/tpm0 --device /dev/tpmrm0 -v keylime-agent:/var/lib/keylime -v /etc/keylime:/etc/keylime:ro --tmpfs /var/lib/keylime/secure:rw,size=1m,mode=0700 -dt IMAGE"
76 |
77 | # run as root by default
78 | USER 0:0
79 |
--------------------------------------------------------------------------------
/docker/release/Dockerfile.fedora:
--------------------------------------------------------------------------------
1 | # SPDX-License-Identifier: Apache-2.0
2 | # the builder stage which uses the latest Fedora minimal image to build the keylime agent - we know this works well
3 | FROM registry.fedoraproject.org/fedora-minimal AS builder
4 |
5 | # Packaged dependencies
6 | RUN microdnf install -y \
7 | cargo \
8 | clang \
9 | clang-devel \
10 | dnf-plugins-core \
11 | git \
12 | make \
13 | openssl-devel \
14 | rust \
15 | systemd \
16 | tpm2-tss \
17 | tpm2-tss-devel
18 |
19 | # build rust-keylime
20 | COPY . /src/rust-keylime/
21 | WORKDIR /src/rust-keylime
22 | RUN make RELEASE=1 TARGETDIR=target target/release/keylime_agent
23 |
24 | # now assemble a release docker image using a fedora minimal base image
25 | FROM registry.fedoraproject.org/fedora-minimal
26 | ARG VERSION=latest
27 | LABEL org.opencontainers.image.authors="Keylime Team "
28 | LABEL org.opencontainers.image.version="$VERSION"
29 | LABEL org.opencontainers.image.title="Keylime Agent"
30 | LABEL org.opencontainers.image.description="Keylime Agent - Bootstrapping and Maintaining Trust in the Cloud"
31 | LABEL org.opencontainers.image.url="https://keylime.dev/"
32 | LABEL org.opencontainers.image.source="https://github.com/keylime/rust-keylime/"
33 | LABEL org.opencontainers.image.licenses="Apache-2.0"
34 | LABEL org.opencontainers.image.vendor="The Keylime Authors"
35 |
36 | # these labels are set in the fedora base image and should be overwritten
37 | LABEL name="Keylime Agent"
38 | LABEL version="$VERSION"
39 | LABEL license="Apache-2.0"
40 | LABEL vendor="The Keylime Authors"
41 |
42 | # Install all agent runtime dependencies from the builder image
43 | # NOTE: the fedora base image is "fat" and comes with basically all dependencies that we need out of the box with a few exceptions
44 | RUN microdnf makecache && \
45 | microdnf -y install tpm2-tss openssl util-linux-core && \
46 | microdnf clean all && \
47 | rm -rf /var/cache/dnf/*
48 |
49 | # now copy the agent from the builder
50 | COPY --from=builder /src/rust-keylime/target/release/keylime_agent /bin/keylime_agent
51 | COPY --from=builder /src/rust-keylime/keylime-agent.conf /etc/keylime/agent.conf
52 | ENTRYPOINT ["/bin/keylime_agent"]
53 |
54 | # we default the log level to info if not overwritten
55 | ENV RUST_LOG=keylime_agent=info
56 |
57 | # the agent currently listens on this port by default
58 | # it's good practice to declare this in the Dockerfile
59 | EXPOSE 9002
60 |
61 | # these are all podman labels that work with the 'podman container runlabel' command, and are standardized at least in RHEL (install, uninstall, run)
62 | LABEL install="podman volume create keylime-agent"
63 | LABEL uninstall="podman volume rm keylime-agent"
64 | LABEL run="podman run --read-only --name keylime-agent --rm --device /dev/tpm0 --device /dev/tpmrm0 -v keylime-agent:/var/lib/keylime -v /etc/keylime:/etc/keylime:ro --tmpfs /var/lib/keylime/secure:rw,size=1m,mode=0700 -dt IMAGE"
65 |
66 | # Create a system user 'keylime' to allow dropping privileges
67 | RUN useradd -s /sbin/nologin -r -G tss keylime
68 |
69 | # run as root by default
70 | USER 0:0
71 |
--------------------------------------------------------------------------------
/docker/release/Dockerfile.wolfi:
--------------------------------------------------------------------------------
1 | # SPDX-License-Identifier: Apache-2.0
2 | # the builder stage which uses the wolfi-base image to build the keylime agent
3 | # NOTE: even though there is a rust wolfi base image, it does not ship apk which we need to add some dependencies
4 | FROM cgr.dev/chainguard/wolfi-base AS builder
5 |
6 | # update apk
7 | RUN apk update
8 |
9 | # we are installing build dependencies, describe them per line as they are added:
10 | # - install rust
11 | # - install gcc and others to compile tpm2-tss
12 | # - we are using the "generate-bindings" feature for the tss-esapi crate which requires clang/llvm
13 | RUN apk add --no-cache --update-cache \
14 | rust \
15 | make pkgconf gcc glibc glibc-dev openssl openssl-dev posix-libc-utils \
16 | clang-17 clang-17-dev wget
17 |
18 | # Install tpm2-tss (dependency for the tss-esapi crate)
19 | WORKDIR /src
20 | RUN wget https://github.com/tpm2-software/tpm2-tss/releases/download/4.0.1/tpm2-tss-4.0.1.tar.gz
21 | RUN tar xf tpm2-tss-4.0.1.tar.gz
22 | WORKDIR /src/tpm2-tss-4.0.1
23 | RUN ./configure \
24 | --prefix=/usr \
25 | --disable-static \
26 | --disable-fapi \
27 | --disable-policy \
28 | --disable-doxygen-doc \
29 | --disable-defaultflags \
30 | --disable-dependency-tracking
31 | RUN make
32 | RUN make install
33 |
34 | # build rust-keylime
35 | COPY . /src/rust-keylime/
36 | WORKDIR /src/rust-keylime
37 | RUN make RELEASE=1 TARGETDIR=target target/release/keylime_agent
38 |
39 | # truly just for debugging purposes for the assembly stage
40 | RUN readelf -W \
41 | --file-header --program-headers --sections --dynamic --notes --version-info --arch-specific --unwind --section-groups --histogram \
42 | /src/rust-keylime/target/release/keylime_agent
43 | RUN ldd /src/rust-keylime/target/release/keylime_agent
44 |
45 | # now assemble a release docker image using a minimal docker image
46 | FROM cgr.dev/chainguard/cc-dynamic:latest
47 | ARG VERSION=latest
48 | LABEL org.opencontainers.image.authors="Keylime Team "
49 | LABEL org.opencontainers.image.version="$VERSION"
50 | LABEL org.opencontainers.image.title="Keylime Agent"
51 | LABEL org.opencontainers.image.description="Keylime Agent - Bootstrapping and Maintaining Trust in the Cloud"
52 | LABEL org.opencontainers.image.url="https://keylime.dev/"
53 | LABEL org.opencontainers.image.source="https://github.com/keylime/rust-keylime/"
54 | LABEL org.opencontainers.image.licenses="Apache-2.0"
55 | LABEL org.opencontainers.image.vendor="The Keylime Authors"
56 |
57 | # Copy all agent dependencies from the builder image
58 | # NOTE: the cc base image comes with all C runtime dependencies (libc, libm, libgcc_s, etc.), so no need to copy those
59 | # TODO: Unfortunately the COPY directive is following links and not preserving the link file. This slightly bloats the image.
60 |
61 | # openssl (both crypto and ssl libraries) are direct dependencies
62 | COPY --from=builder \
63 | /usr/lib/libcrypto.so* \
64 | /usr/lib/libssl.so* \
65 | /usr/lib/
66 |
67 | # libz is a direct dependency for the zip crate
68 | COPY --from=builder \
69 | /lib/libz.so* \
70 | /lib/
71 |
72 | # tpm2-tss libraries are a dependency (probably not all of them, but we just copy all)
73 | # because we are using the tss-esapi crate which is essentially just a wrapper around those (unfortunately)
74 | COPY --from=builder \
75 | /usr/lib/libtss2*.so* \
76 | /usr/lib/
77 |
78 | # now copy the agent from the builder
79 | COPY --from=builder /src/rust-keylime/target/release/keylime_agent /bin/keylime_agent
80 | COPY --from=builder /src/rust-keylime/keylime-agent.conf /etc/keylime/agent.conf
81 | ENTRYPOINT ["/bin/keylime_agent"]
82 |
83 | # we default the log level to info if not overwritten
84 | ENV RUST_LOG=keylime_agent=info
85 |
86 | # the agent currently listens on this port by default
87 | # it's good practice to declare this in the Dockerfile
88 | EXPOSE 9002/tcp
89 |
90 | # these are all podman labels that work with the 'podman container runlabel' command, and are standardized at least in RHEL (install, uninstall, run)
91 | LABEL install="podman volume create keylime-agent"
92 | LABEL uninstall="podman volume rm keylime-agent"
93 | LABEL run="podman run --read-only --name keylime-agent --rm --device /dev/tpm0 --device /dev/tpmrm0 -v keylime-agent:/var/lib/keylime -v /etc/keylime:/etc/keylime:ro --tmpfs /var/lib/keylime/secure:rw,size=1m,mode=0700 -dt IMAGE"
94 |
95 | # run as root by default
96 | USER 0:0
97 |
--------------------------------------------------------------------------------
/docker/release/build_locally.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | # SPDX-License-Identifier: Apache-2.0
3 | # Copyright 2023 Keylime Authors
4 |
5 | # Build Docker container locally
6 |
7 | SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )
8 |
9 | if [ "$1" = "--help" -o "$1" = "-h" ] ; then
10 | echo "USAGE: $0 [VERSION] [KEYLIME_DIR] [DOCKER_BUILDX_FLAGS...]" 1>&2
11 | echo 1>&2
12 | echo "Examples:" 1>&2
13 | echo "$0" 1>&2
14 | echo "$0 15.1.2" 1>&2
15 | echo "$0 15.1.2 /different/source/folder" 1>&2
16 | echo "$0 15.1.2 /source/folder --pull --push" 1>&2
17 | echo "DOCKERFILE_TYPE=fedora $0" 1>&2
18 | echo "DOCKERFILE_TYPE=wolfi $0" 1>&2
19 | echo 1>&2
20 | exit 0
21 | fi
22 |
23 | VERSION=${1:-latest}
24 | $( cd -- "${SCRIPT_DIR}/../dev/images" &> /dev/null && pwd )
25 | KEYLIME_DIR=${2:-$( cd -- "${SCRIPT_DIR}/../../" &>/dev/null && pwd )}
26 | # TODO: why is shift N not working?
27 | shift
28 | shift
29 | DOCKER_BUILDX_FLAGS=${@:-"--load"}
30 |
31 | # overwrite this with one of the following:
32 | # - distroless (default)
33 | # - fedora
34 | # - wolfi
35 | DOCKERFILE_TYPE="${DOCKERFILE_TYPE:-distroless}"
36 |
37 | LOG_DIR=${LOG_DIR:-/tmp}
38 | DOCKERFILE="${SCRIPT_DIR}/Dockerfile.${DOCKERFILE_TYPE}"
39 |
40 | docker buildx build \
41 | -f $DOCKERFILE \
42 | -t keylime_agent:${VERSION}-${DOCKERFILE_TYPE} \
43 | --progress=plain \
44 | --platform=linux/amd64 \
45 | --build-arg VERSION="$VERSION" \
46 | $DOCKER_BUILDX_FLAGS $KEYLIME_DIR 2>&1 | tee $LOG_DIR/docker-keylime-agent-build.log
47 | docker tag keylime_agent:${VERSION}-${DOCKERFILE_TYPE} keylime_agent:${VERSION}
48 |
--------------------------------------------------------------------------------
/ima-policy/README:
--------------------------------------------------------------------------------
1 | # Notes about the IMA policy
2 |
3 | This IMA policy is provided as an example that can be later adapted to
4 | more specific usage.
5 |
6 | This was generated from a default tcb IMA policy from a 6.1.12 Linux
7 | kernel, and extended with SELinux file types to filter out the part of
8 | the system that we usually do not want to measure.
9 |
10 | To use this policy, we need to copy it in "/etc/ima/ima-policy" and
11 | systemd will load it after the SELinux policy has been loaded.
12 |
13 | For this example, we used the initial set of SELinux attributes, that
14 | group the file types under categories. From that list we selected
15 | some of those attribute to deep more into the types that can be relevant for the IMA policy:
16 |
17 | seinfo -a
18 |
19 | The current selection covers fully or partially the types under those
20 | attributes:
21 |
22 | base_file_type
23 | base_ro_file_type
24 | configfile
25 | file_type
26 | files_unconfined_type
27 | init_script_file_type
28 | init_sock_file_type
29 | lockfile
30 | logfile
31 | non_auth_file_type
32 | non_security_file_type
33 | openshift_file_type
34 | pidfile
35 | pulseaudio_tmpfsfile
36 | security_file_type
37 | setfiles_domain
38 | spoolfile
39 | svirt_file_type
40 | systemd_unit_file_type
41 | tmpfile
42 | tmpfsfile
43 |
44 | Special mention to non_auth_file_type and non_security_file_type
45 | (among others like logfile or tmpfile), that should cover the most
46 | relevant types of the dynamic part of the system.
47 |
48 | The list should also include types from other attributes like
49 | virt_image_type and others (see the policy file comments from a
50 | complete list).
51 |
52 | Sometimes is important to see what files are labeled under a specific
53 | type, and for that we can use this:
54 |
55 | semanage fcontext -l | grep $TYPE
56 |
--------------------------------------------------------------------------------
/keylime-agent/Cargo.toml:
--------------------------------------------------------------------------------
1 | [package]
2 | name = "keylime_agent"
3 | description = "Rust agent for Keylime"
4 | authors.workspace = true
5 | edition.workspace = true
6 | license.workspace = true
7 | repository.workspace = true
8 | version.workspace = true
9 |
10 | [dependencies]
11 | actix-web.workspace = true
12 | base64.workspace = true
13 | cfg-if.workspace = true
14 | clap.workspace = true
15 | config.workspace = true
16 | futures.workspace = true
17 | glob.workspace = true
18 | hex.workspace = true
19 | keylime.workspace = true
20 | libc.workspace = true
21 | log.workspace = true
22 | openssl.workspace = true
23 | pretty_env_logger.workspace = true
24 | reqwest.workspace = true
25 | serde.workspace = true
26 | serde_derive.workspace = true
27 | serde_json.workspace = true
28 | static_assertions.workspace = true
29 | tempfile.workspace = true
30 | tokio.workspace = true
31 | tss-esapi.workspace = true
32 | thiserror.workspace = true
33 | uuid.workspace = true
34 | zip.workspace = true
35 | zmq = {version = "0.9.2", optional = true}
36 |
37 | [dev-dependencies]
38 | actix-rt.workspace = true
39 |
40 | [features]
41 | # The features enabled by default
42 | default = []
43 | testing = []
44 | # Whether the agent should be compiled with support to listen for notification
45 | # messages on ZeroMQ
46 | #
47 | # This feature is deprecated and will be removed on next major release
48 | with-zmq = ["zmq"]
49 | # Whether the agent should be compiled with support for python revocation
50 | # actions loaded as modules, which is the only kind supported by the python
51 | # agent (unless the enhancement-55 is implemented). See:
52 | # https://github.com/keylime/enhancements/blob/master/55_revocation_actions_without_python.md
53 | #
54 | # This feature is deprecated and will be removed on next major release
55 | legacy-python-actions = []
56 |
57 | [package.metadata.deb]
58 | section = "net"
59 | assets = [
60 | ["target/release/keylime_agent", "usr/bin/", "755"],
61 | ["../README.md", "usr/share/doc/keylime-agent/README", "644"],
62 | ["../keylime-agent.conf", "/etc/keylime/agent.conf", "640"],
63 | ["../dist/systemd/system/var-lib-keylime-secure.mount", "lib/systemd/system/var-lib-keylime-secure.mount", "644"],
64 | ["../tests/actions/shim.py", "usr/libexec/keylime/shim.py", "755"],
65 | ]
66 | maintainer-scripts = "../debian/"
67 | systemd-units = { unit-scripts = "../dist/systemd/system/", enable = true }
68 |
--------------------------------------------------------------------------------
/keylime-agent/src/agent_handler.rs:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: Apache-2.0
2 | // Copyright 2023 Keylime Authors
3 |
4 | use crate::common::JsonWrapper;
5 | use crate::{tpm, Error as KeylimeError, QuoteData};
6 | use actix_web::{http, web, HttpRequest, HttpResponse, Responder};
7 | use base64::{engine::general_purpose, Engine as _};
8 | use log::*;
9 | use serde::{Deserialize, Serialize};
10 |
11 | #[derive(Serialize, Deserialize, Debug)]
12 | pub(crate) struct AgentInfo {
13 | pub agent_uuid: String,
14 | pub tpm_hash_alg: String,
15 | pub tpm_enc_alg: String,
16 | pub tpm_sign_alg: String,
17 | pub ak_handle: u32,
18 | }
19 |
20 | // This is an Info request which gets some information about this keylime agent
21 | // It should return a AgentInfo object as JSON
22 | async fn info(
23 | req: HttpRequest,
24 | data: web::Data>,
25 | ) -> impl Responder {
26 | debug!("Returning agent information");
27 |
28 | let mut info = AgentInfo {
29 | agent_uuid: data.agent_uuid.clone(),
30 | tpm_hash_alg: data.hash_alg.to_string(),
31 | tpm_enc_alg: data.enc_alg.to_string(),
32 | tpm_sign_alg: data.sign_alg.to_string(),
33 | ak_handle: data.ak_handle.value(),
34 | };
35 |
36 | let response = JsonWrapper::success(info);
37 | info!("GET info returning 200 response");
38 | HttpResponse::Ok().json(response)
39 | }
40 |
41 | /// Configure the endpoints for the /agent scope
42 | async fn agent_default(req: HttpRequest) -> impl Responder {
43 | let error;
44 | let response;
45 | let message;
46 |
47 | match req.head().method {
48 | http::Method::GET => {
49 | error = 400;
50 | message = "URI not supported, only /info is supported for GET in /agent interface";
51 | response = HttpResponse::BadRequest()
52 | .json(JsonWrapper::error(error, message));
53 | }
54 | _ => {
55 | error = 405;
56 | message = "Method is not supported in /agent interface";
57 | response = HttpResponse::MethodNotAllowed()
58 | .insert_header(http::header::Allow(vec![http::Method::GET]))
59 | .json(JsonWrapper::error(error, message));
60 | }
61 | };
62 |
63 | warn!(
64 | "{} returning {} response. {}",
65 | req.head().method,
66 | error,
67 | message
68 | );
69 |
70 | response
71 | }
72 |
73 | /// Configure the endpoints for the /agents scope
74 | pub(crate) fn configure_agent_endpoints(cfg: &mut web::ServiceConfig) {
75 | _ = cfg
76 | .service(web::resource("/info").route(web::get().to(info)))
77 | .default_service(web::to(agent_default));
78 | }
79 |
80 | #[cfg(test)]
81 | #[cfg(feature = "testing")]
82 | mod tests {
83 | use super::*;
84 | use actix_web::{test, web, App};
85 | use serde_json::{json, Value};
86 |
87 | #[actix_rt::test]
88 | async fn test_agent_info() {
89 | let (mut quotedata, mutex) = QuoteData::fixture().await.unwrap(); //#[allow_ci]
90 | quotedata.hash_alg = keylime::algorithms::HashAlgorithm::Sha256;
91 | quotedata.enc_alg = keylime::algorithms::EncryptionAlgorithm::Rsa2048;
92 | quotedata.sign_alg = keylime::algorithms::SignAlgorithm::RsaSsa;
93 | quotedata.agent_uuid = "DEADBEEF".to_string();
94 | let data = web::Data::new(quotedata);
95 | let mut app = test::init_service(
96 | App::new()
97 | .app_data(data.clone())
98 | .route("/vX.Y/agent/info", web::get().to(info)),
99 | )
100 | .await;
101 |
102 | let req = test::TestRequest::get()
103 | .uri("/vX.Y/agent/info")
104 | .to_request();
105 |
106 | let resp = test::call_service(&app, req).await;
107 | assert!(resp.status().is_success());
108 |
109 | let result: JsonWrapper = test::read_body_json(resp).await;
110 | assert_eq!(result.results.agent_uuid.as_str(), "DEADBEEF");
111 | assert_eq!(result.results.tpm_hash_alg.as_str(), "sha256");
112 | assert_eq!(result.results.tpm_enc_alg.as_str(), "rsa");
113 | assert_eq!(result.results.tpm_sign_alg.as_str(), "rsassa");
114 |
115 | // Explicitly drop QuoteData to cleanup keys
116 | drop(data);
117 | }
118 |
119 | #[actix_rt::test]
120 | async fn test_agents_default() {
121 | let mut app = test::init_service(
122 | App::new().service(web::resource("/").to(agent_default)),
123 | )
124 | .await;
125 |
126 | let req = test::TestRequest::get().uri("/").to_request();
127 |
128 | let resp = test::call_service(&app, req).await;
129 | assert!(resp.status().is_client_error());
130 |
131 | let result: JsonWrapper = test::read_body_json(resp).await;
132 |
133 | assert_eq!(result.results, json!({}));
134 | assert_eq!(result.code, 400);
135 |
136 | let req = test::TestRequest::delete().uri("/").to_request();
137 |
138 | let resp = test::call_service(&app, req).await;
139 | assert!(resp.status().is_client_error());
140 |
141 | let headers = resp.headers();
142 |
143 | assert!(headers.contains_key("allow"));
144 | assert_eq!(headers.get("allow").unwrap().to_str().unwrap(), "GET"); //#[allow_ci]
145 |
146 | let result: JsonWrapper = test::read_body_json(resp).await;
147 |
148 | assert_eq!(result.results, json!({}));
149 | assert_eq!(result.code, 405);
150 | }
151 | }
152 |
--------------------------------------------------------------------------------
/keylime-agent/src/common.rs:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: Apache-2.0
2 | // Copyright 2021 Keylime Authors
3 |
4 | use keylime::keylime_error::{Error, Result};
5 |
6 | use crate::permissions;
7 |
8 | use keylime::algorithms::{
9 | EncryptionAlgorithm, HashAlgorithm, SignAlgorithm,
10 | };
11 | use keylime::{
12 | crypto::{hash, tss_pubkey_to_pem},
13 | hash_ek, tpm,
14 | };
15 | use log::*;
16 | use openssl::hash::MessageDigest;
17 | use serde::{Deserialize, Serialize};
18 | use serde_json::{json, Value};
19 | use std::{
20 | convert::{Into, TryFrom, TryInto},
21 | env,
22 | ffi::CString,
23 | fmt::{self, Debug, Display},
24 | fs::File,
25 | path::{Path, PathBuf},
26 | str::FromStr,
27 | };
28 | use tss_esapi::structures::{Private, Public};
29 | use tss_esapi::traits::Marshall;
30 | use tss_esapi::utils::PublicKey;
31 | use tss_esapi::{
32 | structures::PcrSlot, traits::UnMarshall, utils::TpmsContext,
33 | };
34 |
35 | #[derive(Serialize, Deserialize, Debug)]
36 | pub(crate) struct APIVersion {
37 | major: u32,
38 | minor: u32,
39 | }
40 |
41 | impl Display for APIVersion {
42 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
43 | write!(f, "v{}.{}", self.major, self.minor)
44 | }
45 | }
46 |
47 | #[derive(Serialize, Deserialize, Debug)]
48 | pub(crate) struct JsonWrapper {
49 | pub code: u16,
50 | pub status: String,
51 | pub results: A,
52 | }
53 |
54 | impl JsonWrapper {
55 | pub(crate) fn error(
56 | code: u16,
57 | status: impl ToString,
58 | ) -> JsonWrapper {
59 | JsonWrapper {
60 | code,
61 | status: status.to_string(),
62 | results: json!({}),
63 | }
64 | }
65 | }
66 |
67 | impl<'de, A> JsonWrapper
68 | where
69 | A: Deserialize<'de> + Serialize + Debug,
70 | {
71 | pub(crate) fn success(results: A) -> JsonWrapper {
72 | JsonWrapper {
73 | code: 200,
74 | status: String::from("Success"),
75 | results,
76 | }
77 | }
78 | }
79 |
80 | #[cfg(test)]
81 | mod tests {
82 | use super::*;
83 | use crate::config::KeylimeConfig;
84 | use keylime::agent_data::AgentData;
85 | use keylime::algorithms::{
86 | EncryptionAlgorithm, HashAlgorithm, SignAlgorithm,
87 | };
88 | use keylime::hash_ek;
89 | use std::convert::TryFrom;
90 | use tss_esapi::{
91 | handles::KeyHandle,
92 | interface_types::algorithm::AsymmetricAlgorithm,
93 | interface_types::resource_handles::Hierarchy,
94 | structures::{Auth, PublicBuffer},
95 | traits::Marshall,
96 | Context,
97 | };
98 |
99 | #[tokio::test]
100 | #[cfg(feature = "testing")]
101 | async fn test_agent_data() -> Result<()> {
102 | let _mutex = tpm::testing::lock_tests().await;
103 | let mut config = KeylimeConfig::default();
104 |
105 | let mut ctx = tpm::Context::new()?;
106 |
107 | let tpm_encryption_alg = EncryptionAlgorithm::try_from(
108 | config.agent.tpm_encryption_alg.as_str(),
109 | )?;
110 |
111 | let tpm_hash_alg =
112 | HashAlgorithm::try_from(config.agent.tpm_hash_alg.as_str())
113 | .expect("Failed to get hash algorithm");
114 |
115 | let tpm_signing_alg =
116 | SignAlgorithm::try_from(config.agent.tpm_signing_alg.as_str())
117 | .expect("Failed to get signing algorithm");
118 |
119 | let ek_result = ctx
120 | .create_ek(tpm_encryption_alg, None)
121 | .expect("Failed to create EK");
122 |
123 | let ek_hash = hash_ek::hash_ek_pubkey(ek_result.public)
124 | .expect("Failed to get pubkey");
125 |
126 | let ak = ctx.create_ak(
127 | ek_result.key_handle,
128 | tpm_hash_alg,
129 | tpm_encryption_alg,
130 | tpm_signing_alg,
131 | )?;
132 |
133 | let agent_data_test = AgentData::create(
134 | tpm_hash_alg,
135 | tpm_signing_alg,
136 | &ak,
137 | ek_hash.as_bytes(),
138 | )?;
139 |
140 | let valid = AgentData::valid(
141 | &agent_data_test,
142 | tpm_hash_alg,
143 | tpm_signing_alg,
144 | ek_hash.as_bytes(),
145 | );
146 |
147 | assert!(valid);
148 |
149 | // Cleanup created keys
150 | let ak_handle = ctx.load_ak(ek_result.key_handle, &ak)?;
151 | ctx.flush_context(ak_handle.into());
152 | ctx.flush_context(ek_result.key_handle.into());
153 |
154 | Ok(())
155 | }
156 |
157 | #[tokio::test]
158 | #[cfg(feature = "testing")]
159 | async fn test_hash() -> Result<()> {
160 | use keylime::agent_data;
161 | use keylime::hash_ek;
162 |
163 | let _mutex = tpm::testing::lock_tests().await;
164 | let mut config = KeylimeConfig::default();
165 |
166 | let mut ctx = tpm::Context::new()?;
167 |
168 | let tpm_encryption_alg = EncryptionAlgorithm::try_from(
169 | config.agent.tpm_encryption_alg.as_str(),
170 | )
171 | .expect("Failed to get encryption algorithm");
172 |
173 | let ek_result = ctx
174 | .create_ek(tpm_encryption_alg, None)
175 | .expect("Failed to create EK");
176 |
177 | let result = hash_ek::hash_ek_pubkey(ek_result.public);
178 |
179 | assert!(result.is_ok());
180 |
181 | // Cleanup created keys
182 | ctx.flush_context(ek_result.key_handle.into());
183 |
184 | Ok(())
185 | }
186 | }
187 |
--------------------------------------------------------------------------------
/keylime-agent/src/notifications_handler.rs:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: Apache-2.0
2 | // Copyright 2021 Keylime Authors
3 |
4 | use crate::{
5 | common::JsonWrapper,
6 | revocation::{Revocation, RevocationMessage},
7 | Error, QuoteData, Result,
8 | };
9 | use actix_web::{http, web, HttpRequest, HttpResponse, Responder};
10 | use log::*;
11 | use serde::{Deserialize, Serialize};
12 | use std::path::{Path, PathBuf};
13 |
14 | // This is Revocation request from the cloud verifier via REST API
15 | async fn revocation(
16 | body: web::Json,
17 | req: HttpRequest,
18 | data: web::Data>,
19 | ) -> impl Responder {
20 | info!("Received revocation");
21 |
22 | match data
23 | .revocation_tx
24 | .send(RevocationMessage::Revocation(body.into_inner()))
25 | .await
26 | {
27 | Err(e) => {
28 | HttpResponse::InternalServerError().json(JsonWrapper::error(
29 | 500,
30 | "Fail to send Revocation message to revocation worker"
31 | .to_string(),
32 | ))
33 | }
34 | Ok(_) => HttpResponse::Ok().json(JsonWrapper::success(())),
35 | }
36 | }
37 |
38 | async fn notifications_default(req: HttpRequest) -> impl Responder {
39 | let error;
40 | let response;
41 | let message;
42 |
43 | match req.head().method {
44 | http::Method::POST => {
45 | error = 400;
46 | message = "URI not supported, only /revocation is supported for POST in /notifications/ interface";
47 | response = HttpResponse::BadRequest()
48 | .json(JsonWrapper::error(error, message));
49 | }
50 | _ => {
51 | error = 405;
52 | message = "Method is not supported in /notifications/ interface";
53 | response = HttpResponse::MethodNotAllowed()
54 | .insert_header(http::header::Allow(vec![http::Method::POST]))
55 | .json(JsonWrapper::error(error, message));
56 | }
57 | };
58 |
59 | warn!(
60 | "{} returning {} response. {}",
61 | req.head().method,
62 | error,
63 | message
64 | );
65 |
66 | response
67 | }
68 |
69 | /// Configure the endpoints for the /notifications scope
70 | pub(crate) fn configure_notifications_endpoints(
71 | cfg: &mut web::ServiceConfig,
72 | ) {
73 | _ = cfg
74 | .service(
75 | web::resource("/revocation").route(web::post().to(revocation)),
76 | )
77 | .default_service(web::to(notifications_default));
78 | }
79 |
80 | #[cfg(test)]
81 | mod tests {
82 | use super::*;
83 | use actix_rt::Arbiter;
84 | use actix_web::{test, web, App};
85 | use serde_json::{json, Value};
86 | use std::{fs, path::Path};
87 | use tokio::sync::mpsc;
88 |
89 | #[actix_rt::test]
90 | async fn test_notifications_default() {
91 | let mut app = test::init_service(
92 | App::new().service(web::resource("/").to(notifications_default)),
93 | )
94 | .await;
95 |
96 | let req = test::TestRequest::post()
97 | .uri("/")
98 | .data("some data")
99 | .to_request();
100 |
101 | let resp = test::call_service(&app, req).await;
102 | assert!(resp.status().is_client_error());
103 |
104 | let result: JsonWrapper = test::read_body_json(resp).await;
105 |
106 | assert_eq!(result.results, json!({}));
107 | assert_eq!(result.code, 400);
108 |
109 | let req = test::TestRequest::delete().uri("/").to_request();
110 |
111 | let resp = test::call_service(&app, req).await;
112 | assert!(resp.status().is_client_error());
113 |
114 | let headers = resp.headers();
115 |
116 | assert!(headers.contains_key("allow"));
117 | assert_eq!(
118 | headers.get("allow").unwrap().to_str().unwrap(), //#[allow_ci]
119 | "POST"
120 | );
121 |
122 | let result: JsonWrapper = test::read_body_json(resp).await;
123 |
124 | assert_eq!(result.results, json!({}));
125 | assert_eq!(result.code, 405);
126 | }
127 |
128 | #[cfg(feature = "testing")]
129 | #[actix_rt::test]
130 | async fn test_revocation() {
131 | let revocation_cert = Some(
132 | PathBuf::from(env!("CARGO_MANIFEST_DIR"))
133 | .join("test-data/test-cert.pem"),
134 | );
135 |
136 | let revocation_actions_dir = Some(
137 | PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests/actions"),
138 | );
139 |
140 | let (mut fixture, mutex) = QuoteData::fixture().await.unwrap(); //#[allow_ci]
141 |
142 | // Replace the channels on the fixture with some local ones
143 | let (mut revocation_tx, mut revocation_rx) =
144 | mpsc::channel::(1);
145 | fixture.revocation_tx = revocation_tx;
146 |
147 | let quotedata = web::Data::new(fixture);
148 |
149 | let mut app =
150 | test::init_service(App::new().app_data(quotedata.clone()).route(
151 | "/vX.Y/notifications/revocation",
152 | web::post().to(revocation),
153 | ))
154 | .await;
155 |
156 | let sig_path = Path::new(env!("CARGO_MANIFEST_DIR"))
157 | .join("test-data/revocation.sig");
158 | let signature = fs::read_to_string(sig_path).unwrap(); //#[allow_ci]
159 |
160 | let message_path = Path::new(env!("CARGO_MANIFEST_DIR"))
161 | .join("test-data/test_ok.json");
162 | let message = fs::read_to_string(message_path).unwrap(); //#[allow_ci]
163 |
164 | let arbiter = Arbiter::new();
165 |
166 | // Create the message body with the payload and signature
167 | let revocation = Revocation {
168 | msg: message.clone(),
169 | signature: signature.clone(),
170 | };
171 |
172 | // Run fake revocation worker
173 | assert!(arbiter.spawn(Box::pin(async move {
174 | let m = revocation_rx.recv().await;
175 | assert!(
176 | m == Some(RevocationMessage::Revocation(Revocation {
177 | msg: message,
178 | signature,
179 | }))
180 | )
181 | })));
182 |
183 | let req = test::TestRequest::post()
184 | .uri("/vX.Y/notifications/revocation")
185 | .set_json(&revocation)
186 | .to_request();
187 |
188 | let resp = test::call_service(&app, req).await;
189 | assert!(resp.status().is_success());
190 |
191 | // Explicitly drop QuoteData to cleanup keys
192 | drop(quotedata);
193 | }
194 | }
195 |
--------------------------------------------------------------------------------
/keylime-agent/src/permissions.rs:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: Apache-2.0
2 | // Copyright 2021 Keylime Authors
3 |
4 | use keylime::keylime_error::{Error, Result};
5 | use libc::{c_char, c_int, gid_t, uid_t};
6 | use log::*;
7 | use std::os::unix::ffi::OsStrExt;
8 | use std::{
9 | convert::{TryFrom, TryInto},
10 | ffi::CString,
11 | io,
12 | path::Path,
13 | ptr,
14 | };
15 |
16 | pub(crate) struct UserIds {
17 | passwd: libc::passwd,
18 | group: libc::group,
19 | }
20 |
21 | pub(crate) fn get_gid() -> gid_t {
22 | unsafe { libc::getgid() }
23 | }
24 |
25 | pub(crate) fn get_uid() -> uid_t {
26 | unsafe { libc::getuid() }
27 | }
28 |
29 | pub(crate) fn get_euid() -> uid_t {
30 | unsafe { libc::geteuid() }
31 | }
32 |
33 | impl TryFrom<&str> for UserIds {
34 | type Error = Error;
35 |
36 | fn try_from(value: &str) -> Result {
37 | let parts = value.split(':').collect::>();
38 |
39 | if parts.len() != 2 {
40 | let e = format!("Invalid parameter format: {value} cannot be parsed as 'user:group'");
41 | error!("{}", e);
42 | return Err(Error::Conversion(e));
43 | }
44 |
45 | let user = parts[0];
46 | let group = parts[1];
47 |
48 | // Get gid from group name
49 | let grnam = if let Ok(g_cstr) = CString::new(group.as_bytes()) {
50 | let p = unsafe { libc::getgrnam(g_cstr.as_ptr()) };
51 | if p.is_null() {
52 | let e = io::Error::last_os_error();
53 | error!("Could not get group {}: {}", group, e);
54 | return Err(Error::Conversion(e.to_string()));
55 | }
56 | unsafe { (*p) }
57 | } else {
58 | return Err(Error::Conversion(format!(
59 | "Failed to convert {group} to CString"
60 | )));
61 | };
62 |
63 | // Get uid from user name
64 | let passwd = if let Ok(u_cstr) = CString::new(user.as_bytes()) {
65 | let p = unsafe { libc::getpwnam(u_cstr.as_ptr()) };
66 | if p.is_null() {
67 | let e = io::Error::last_os_error();
68 | error!("Could not get user {}: {}", user, e);
69 | return Err(Error::Conversion(e.to_string()));
70 | }
71 | unsafe { (*p) }
72 | } else {
73 | return Err(Error::Conversion(format!(
74 | "Failed to convert {user} to CString"
75 | )));
76 | };
77 |
78 | Ok(UserIds {
79 | passwd,
80 | group: grnam,
81 | })
82 | }
83 | }
84 |
85 | // Drop the process privileges and run under the provided user and group. The correct order of
86 | // operations are: drop supplementary groups, set gid, then set uid.
87 | // See: POS36-C and CWE-696
88 | pub(crate) fn run_as(user_group: &str) -> Result<()> {
89 | let ids: UserIds = user_group.try_into()?;
90 |
91 | // Set gid
92 | if unsafe { libc::setgid(ids.group.gr_gid) } != 0 {
93 | let e = io::Error::last_os_error();
94 | error!("Could not set group id: {}", e);
95 | return Err(Error::Permission);
96 | }
97 |
98 | // Get list of supplementary groups
99 | let mut sup_groups: [gid_t; 32] = [0u32; 32];
100 | let mut ngroups: c_int = 32;
101 | if unsafe {
102 | libc::getgrouplist(
103 | ids.passwd.pw_name,
104 | ids.group.gr_gid,
105 | sup_groups.as_mut_ptr(),
106 | &mut ngroups,
107 | )
108 | } < 0
109 | {
110 | // Allocate a Vec and try again
111 | let mut sup_groups: Vec = Vec::with_capacity(ngroups as usize);
112 | if unsafe {
113 | libc::getgrouplist(
114 | ids.passwd.pw_name,
115 | ids.group.gr_gid,
116 | sup_groups.as_mut_ptr(),
117 | &mut ngroups,
118 | )
119 | } < 0
120 | {
121 | error!("Could not get list of supplementary groups");
122 | return Err(Error::Permission);
123 | }
124 | }
125 |
126 | // Set supplementary groups
127 | if unsafe { libc::setgroups(ngroups as usize, sup_groups.as_ptr()) } != 0
128 | {
129 | let e = io::Error::last_os_error();
130 | error!("Could not set supplementary groups: {}", e);
131 | return Err(Error::Permission);
132 | }
133 |
134 | // Set uid
135 | if unsafe { libc::setuid(ids.passwd.pw_uid) } != 0 {
136 | let e = io::Error::last_os_error();
137 | error!("Could not set user id: {}", e);
138 | return Err(Error::Permission);
139 | }
140 |
141 | info!("Dropped privileges to run as {}", user_group);
142 |
143 | Ok(())
144 | }
145 |
146 | pub(crate) fn chown(user_group: &str, path: &Path) -> Result<()> {
147 | let ids: UserIds = user_group.try_into()?;
148 |
149 | // check privilege
150 | if get_euid() != 0 {
151 | error!(
152 | "Privilege level unable to change file {} ownership",
153 | path.display()
154 | );
155 | return Err(Error::Permission);
156 | }
157 |
158 | // change directory owner
159 | let c_path = CString::new(path.as_os_str().as_bytes())?;
160 | if unsafe {
161 | libc::chown(c_path.as_ptr(), ids.passwd.pw_uid, ids.group.gr_gid)
162 | } != 0
163 | {
164 | error!("Failed to change file {} owner.", path.display());
165 | return Err(Error::Permission);
166 | }
167 |
168 | info!("Changed file {} owner to {}.", path.display(), user_group);
169 | Ok(())
170 | }
171 |
--------------------------------------------------------------------------------
/keylime-agent/src/secure_mount.rs:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: Apache-2.0
2 | // Copyright 2021 Keylime Authors
3 |
4 | use super::*;
5 |
6 | use keylime::keylime_error::{Error, Result};
7 | use std::fs;
8 | use std::io::BufRead;
9 | use std::os::unix::fs::PermissionsExt;
10 | use std::path::PathBuf;
11 | use std::process::Command;
12 |
13 | pub static MOUNTINFO: &str = "/proc/self/mountinfo";
14 |
15 | /*
16 | * Check the mount status of the secure mount directory by parsing /proc/self/mountinfo content.
17 | *
18 | * /proc/[pid]/mountinfo have 10+ elements separated with spaces (check proc (5) for a complete
19 | * description)
20 | *
21 | * The elements of interest are the mount point (5th element), and the file system type (1st
22 | * element after the '-' separator).
23 | *
24 | * Input: secure mount directory path
25 | * Return: Result wrap boolean with error message
26 | * - true if directory is mounted
27 | * - false if not mounted
28 | *
29 | */
30 | fn check_mount(secure_dir: &Path) -> Result {
31 | let f = fs::File::open(MOUNTINFO)?;
32 | let f = BufReader::new(f);
33 | let lines = f.lines();
34 |
35 | for line in lines.map_while(std::result::Result::ok) {
36 | let mut iter = line.split(' ');
37 | if let Some(mount_point) = &iter.nth(4) {
38 | if Path::new(mount_point) == secure_dir {
39 | // Skip all fields up to the separator
40 | let mut iter = iter.skip_while(|&x| x != "-");
41 |
42 | if let Some(separator) = iter.next() {
43 | // The file system type is the first element after the separator
44 | if let Some(fs_type) = iter.next() {
45 | if fs_type == "tmpfs" {
46 | debug!("Secure store location {} already mounted on tmpfs", secure_dir.display());
47 | return Ok(true);
48 | } else {
49 | let message = format!("Secure storage location {} already mounted on wrong file system type: {}. Unmount to continue.", secure_dir.display(), fs_type);
50 | error!("Secure mount error: {}", message);
51 | return Err(Error::SecureMount(message));
52 | }
53 | } else {
54 | let message = "Mount information parsing error: missing file system type".to_string();
55 | error!("Secure mount error: {}", &message);
56 | return Err(Error::SecureMount(message));
57 | }
58 | } else {
59 | let message = "Separator field not found. Information line cannot be parsed".to_string();
60 | error!("Secure mount error: {}", &message);
61 | return Err(Error::SecureMount(message));
62 | }
63 | }
64 | } else {
65 | let message =
66 | "Mount information parsing error: not enough elements"
67 | .to_string();
68 | error!("Secure mount error: {}", message);
69 | return Err(Error::SecureMount(message));
70 | }
71 | }
72 | debug!("Secure store location {} not mounted", secure_dir.display());
73 | Ok(false)
74 | }
75 |
76 | /*
77 | * Return: Result wrap secure mount directory or error code
78 | *
79 | * Mounted the work directory as tmpfs, which is owned by root. Same
80 | * implementation as the original python version, but the chown/geteuid
81 | * functions are unsafe function in Rust to use.
82 | */
83 | pub(crate) fn mount(work_dir: &Path, secure_size: &str) -> Result {
84 | // Mount the directory to file system
85 | let secure_dir_path = Path::new(work_dir).join("secure");
86 |
87 | // If the directory is not mount to file system, mount the directory to
88 | // file system.
89 | if !check_mount(&secure_dir_path)? {
90 | // Create directory if the directory is not exist. The
91 | // directory permission is set to 448.
92 | if !secure_dir_path.exists() {
93 | fs::create_dir(&secure_dir_path).map_err(|e| {
94 | Error::SecureMount(format!(
95 | "unable to create secure dir path: {e:?}"
96 | ))
97 | })?;
98 |
99 | info!("Directory {:?} created.", secure_dir_path);
100 | let metadata = fs::metadata(&secure_dir_path).map_err(|e| {
101 | Error::SecureMount(format!(
102 | "unable to get metadata for secure dir path: {e:?}"
103 | ))
104 | })?;
105 | metadata.permissions().set_mode(0o750); // decimal 488
106 | }
107 |
108 | info!(
109 | "Mounting secure storage location {:?} on tmpfs.",
110 | &secure_dir_path
111 | );
112 |
113 | // mount tmpfs with secure directory
114 | match Command::new("mount")
115 | .args([
116 | "-t",
117 | "tmpfs",
118 | "-o",
119 | format!("size={secure_size},mode=0700").as_str(),
120 | "tmpfs",
121 | secure_dir_path.to_str().unwrap(), //#[allow_ci]
122 | ])
123 | .output()
124 | {
125 | Ok(output) => {
126 | if !output.status.success() {
127 | return Err(Error::SecureMount(format!(
128 | "unable to mount tmpfs with secure dir: exit status code {}",
129 | output.status
130 | )));
131 | }
132 | }
133 | Err(e) => {
134 | return Err(Error::SecureMount(format!(
135 | "unable to mount tmpfs with secure dir: {e}"
136 | )));
137 | }
138 | }
139 | }
140 |
141 | Ok(secure_dir_path)
142 | }
143 | #[cfg(test)]
144 | mod tests {
145 | use super::*;
146 |
147 | #[test]
148 | fn test_secure_mount() {
149 | let temp_workdir = tempfile::tempdir().unwrap(); //#[allow_ci]
150 | let secure_size = "1m";
151 | let test_mount = mount(temp_workdir.path(), secure_size);
152 | assert!(check_mount(temp_workdir.path()).is_ok());
153 | }
154 | }
155 |
--------------------------------------------------------------------------------
/keylime-agent/test-data/payload.zip:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/keylime/rust-keylime/07167a0def687d6b8d4bf31b4822ea97545d48d4/keylime-agent/test-data/payload.zip
--------------------------------------------------------------------------------
/keylime-agent/test-data/revocation.sig:
--------------------------------------------------------------------------------
1 | RPRVARPrI+kvxnKye77IwlslokadrrOk//+Urn0xP5fjK6SOTu2pSZi5u1tSEc1o/C6RyawBOPyE+q3qxzbJxQamCKMjomYAsuHOoSnOKpN7L2GEGNA9FjhyDlDCGo5k4ZM6QcUvqeLqDKan7yBBINQyfogEVe+DcohevXYh6e01aBLT7zVASz1Qps5QzkuTt72K02TwbkBJoo/ZRyVMknBgP09xrXPMc1AkKKIquA1CAJ7Es6dMHWHb17L31gJsFWIaClmi5BiaQeMOTRzZ9ZJOKCZwi9xt38xUJGbhhY7MrVPvWN4bNuiePkU6/zAok5Pc/+dR0SLWnL3cxsocLA==
--------------------------------------------------------------------------------
/keylime-agent/test-data/test-cert.pem:
--------------------------------------------------------------------------------
1 | -----BEGIN CERTIFICATE-----
2 | MIIDBTCCAe2gAwIBAgIUMdvVLurhVJw+zgZU+tDkLs5Gzo0wDQYJKoZIhvcNAQEL
3 | BQAwEjEQMA4GA1UEAwwHa2V5bGltZTAeFw0yMjAxMjYxNjM3NTRaFw0yMzAxMjYx
4 | NjM3NTRaMBIxEDAOBgNVBAMMB2tleWxpbWUwggEiMA0GCSqGSIb3DQEBAQUAA4IB
5 | DwAwggEKAoIBAQDtx7FvpgAvcF46+UwVoETA+KmWrzPtpzai8BTmF4oOOX3GXMtp
6 | vtjDFCYVvmbUWeQN8LqMBKoJ0O9mzB82FtXZAggSMoIy8Gimcq0TqSNCWFRs61Ho
7 | KlkeJk5gcmgG1DiMzQ6Cp+A71aKrgheaxe4t44KkP6YldF6UAWduzUL3oJQ7QsQj
8 | IWA5i0fZu+ZyTqImo9NzN20KqMCawtvCXjwUmA4qVPGgne6S0GggCnTdd7LAb15/
9 | XPexmu+OWMH8pcfzp4wTlqar/cfJpKnb5aaemOzwwIhEMfp4gTfXyVKMP+3qCp77
10 | KwbUyXDIMXBWssig85z7aGwVUmA00rQz5REfAgMBAAGjUzBRMB0GA1UdDgQWBBTw
11 | nW9LUu8SeBOQJcL0MHkxxiMhejAfBgNVHSMEGDAWgBTwnW9LUu8SeBOQJcL0MHkx
12 | xiMhejAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEBCwUAA4IBAQB7DSJa3I75
13 | e4Zgdmvzt9CUqvgxTUb2gzevXBk3QZclBogNXDoQNYYm1eifZ8PGNj14kDBwPWQi
14 | rt0hB99O0eety5qUj7ro8lRzd7uZ/TrDGyt/mUJt05DU4zeH9mLLspQFfQqq18sO
15 | 5ytnqfrLANV+a8WUgqj/e12pkIvPfzlm8UUKW8qniEdiyVvh1MW8lmnJnlGk0AJn
16 | fpdJO1jc+1c+MTngHN/K81e8Irn+Z9pR6xOmGpZdypnQfLJpHzCyE5vpLQEVxd28
17 | 3kts+VSvxSz1kaKI15mZHykWZ+L1DGYRG9Oopz49uPb9VjqUrSiWjc2lviLbEPeb
18 | pJmGJUTwt5ea
19 | -----END CERTIFICATE-----
20 |
--------------------------------------------------------------------------------
/keylime-agent/test-data/test-rsa.pem:
--------------------------------------------------------------------------------
1 | -----BEGIN RSA PRIVATE KEY-----
2 | MIIEpgIBAAKCAQEA7cexb6YAL3BeOvlMFaBEwPiplq8z7ac2ovAU5heKDjl9xlzL
3 | ab7YwxQmFb5m1FnkDfC6jASqCdDvZswfNhbV2QIIEjKCMvBopnKtE6kjQlhUbOtR
4 | 6CpZHiZOYHJoBtQ4jM0OgqfgO9Wiq4IXmsXuLeOCpD+mJXRelAFnbs1C96CUO0LE
5 | IyFgOYtH2bvmck6iJqPTczdtCqjAmsLbwl48FJgOKlTxoJ3uktBoIAp03XeywG9e
6 | f1z3sZrvjljB/KXH86eME5amq/3HyaSp2+Wmnpjs8MCIRDH6eIE318lSjD/t6gqe
7 | +ysG1MlwyDFwVrLIoPOc+2hsFVJgNNK0M+URHwIDAQABAoIBAQCspZ8XAwAFceBp
8 | j5OH7Eufla2FRIc+2neYTRvPiW3rMCE7wyrLCBBZbKrOhOYi73XgDVdVzRktcXAy
9 | Qqmy21fAbnIvzE6u79H8cS1sJhX82SfLwf1BxmXYt1WXP9p6guLgkQ8lHQF6UH8B
10 | ar762RY8aYH1AmX/sgPuESrpz838/0v4Tq52yJeSiGOS3MG6iRWQ6/K0CHT3olbH
11 | RDN/48YrCSRP3RkAXoGcbev59+qOFJazC2av+qQM+59FwM2SrzrKa7RJLSQ1+wEz
12 | gZEmmgJu6tOVUnAd7mg5VS+N7cXu6+Xl1jCg1N7XaQWh3nRCI1bysJ9UJQQZHl0B
13 | gnIDXAU5AoGBAPex+wA1oaf4y22PIrVoLhD1HksdIxCOhmMYkojaIo3qZCVi8bS8
14 | JnUiHhvo25xYuUtB2Qw0VstzBaq6RDUnVgO3yVBYABc9MSmO+up/2ljNb9S5uWrm
15 | +FzqjCRIWwABTWVyI7zvgVKSuSbvVLAQkgwAEBibp26V5E5SmuB4uKcNAoGBAPXA
16 | m8n9Ajo+2Fzage0JZuW4uELbCqUwCkAlwJoSP42y0SIpB5fITkwRIwOat1MEhQ0I
17 | yh68ZxGvqfE2fO78ZbE2bSlKkkadrZjPFAG2svd57MdHnR5c4iCm6ui7D0snCgcg
18 | pUt2qlXYesABWb7o3tKOezDlbjFtv9JDJPOmnI3bAoGBAJw/RINsUW5BDiotWYqv
19 | jieaSCK/3YerMHDAZmc3mwaErem7kZcd/PB0tiOK70Wf3jrv7be6KGosQ43f8/jH
20 | uIWd4Lry2BPQwPtjOzrDrfvIk9vP0Hvz+QW72u1kSyskpyrwJkUfnCd3cJ5z6Ksr
21 | uMUjIQQ05BhpK1yQ1Sv2WxzdAoGBAKkLwd5SzOp1+mz83azI7+ALjaxnck4o2pQ/
22 | o9oXvWHiZFuEL7Xn0nweuaAsF/jiPge2SRqVbKzM0jCb05qtQeKB1ts1caNjqVtY
23 | 7qEzJK55TzfRejG9oMrnJuXKbv26L/qxKSLc0NTWYbGb/DkHhOb/nZwH5iHYJcAj
24 | 8dIshLpLAoGBAK9RBWaP3KdDheUDOVpSSnOjVOtzq0T2qHPLklpdZzz2BHGKTNly
25 | qek7gOTiy/DvON0F/gFhVi0+7RnCG+CnMXe2Ac/vn3QqZLAL5pSc+AUhmfE72hn2
26 | HNz0GFbYmFqr8QpzXG7b4aCF/yxcPrHGN3LdBKZvPPv7OmcCPGVEOKVk
27 | -----END RSA PRIVATE KEY-----
28 |
--------------------------------------------------------------------------------
/keylime-agent/test-data/test.json:
--------------------------------------------------------------------------------
1 | {
2 | "colors": [
3 | {
4 | "color": "black",
5 | "category": "hue",
6 | "type": "primary",
7 | "code": {
8 | "rgba": [255,255,255,1],
9 | "hex": "#000"
10 | }
11 | },
12 | {
13 | "color": "white",
14 | "category": "value",
15 | "code": {
16 | "rgba": [0,0,0,1],
17 | "hex": "#FFF"
18 | }
19 | },
20 | {
21 | "color": "red",
22 | "category": "hue",
23 | "type": "primary",
24 | "code": {
25 | "rgba": [255,0,0,1],
26 | "hex": "#FF0"
27 | }
28 | },
29 | {
30 | "color": "blue",
31 | "category": "hue",
32 | "type": "primary",
33 | "code": {
34 | "rgba": [0,0,255,1],
35 | "hex": "#00F"
36 | }
37 | },
38 | {
39 | "color": "yellow",
40 | "category": "hue",
41 | "type": "primary",
42 | "code": {
43 | "rgba": [255,255,0,1],
44 | "hex": "#FF0"
45 | }
46 | },
47 | {
48 | "color": "green",
49 | "category": "hue",
50 | "type": "secondary",
51 | "code": {
52 | "rgba": [0,255,0,1],
53 | "hex": "#0F0"
54 | }
55 | },
56 | ]
57 | }
--------------------------------------------------------------------------------
/keylime-agent/test-data/test_input.txt:
--------------------------------------------------------------------------------
1 | Hello World!
2 |
--------------------------------------------------------------------------------
/keylime-agent/test-data/test_ok.json:
--------------------------------------------------------------------------------
1 | {
2 | "hello": "there"
3 | }
4 |
--------------------------------------------------------------------------------
/keylime-agent/test-data/tpmdata_test.json:
--------------------------------------------------------------------------------
1 | {
2 | "aik": "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAwWnAON9h+AYu4Ee+D5LY\nhqKYiQpbFqboE4ohGYjy/eIAOJ//TKysXmdraiq9XYqRWXC/+K5kFn+irRc9k1sn\nhX2HIsIxdqJOZvzzbWXlZ4VyPsc18DQvNiabelBSWxX3rcpB01liooclPB+WsDks\no0Ay/jwgiJvuEbfACzf4z+noQep4x4ysnPd/lcPnN6+TD7TMaaNrMUxp5o8/cmve\nIor6BuFf9bgXH58UrA0V2Bejh83eoyQQ1V/TkDBbnov9/FGaMTURGonapIh9jy9K\nSkmsjFp6LQ/YySIArk6gqRS0fSCp//vX/hTBknpcu6u0CqgEzkBhbGnNa7tMOyo9\npQIDAQAB\n-----END PUBLIC KEY-----\n",
3 | "aik_handle": "FB1F19E0",
4 | "aik_pw": "QXgMbqSxSuB05MVHO0Ox",
5 | "aikmod": "wWnAON9h+AYu4Ee+D5LYhqKYiQpbFqboE4ohGYjy/eIAOJ//TKysXmdraiq9XYqRWXC/+K5kFn+irRc9k1snhX2HIsIxdqJOZvzzbWXlZ4VyPsc18DQvNiabelBSWxX3rcpB01liooclPB+WsDkso0Ay/jwgiJvuEbfACzf4z+noQep4x4ysnPd/lcPnN6+TD7TMaaNrMUxp5o8/cmveIor6BuFf9bgXH58UrA0V2Bejh83eoyQQ1V/TkDBbnov9/FGaMTURGonapIh9jy9KSkmsjFp6LQ/YySIArk6gqRS0fSCp//vX/hTBknpcu6u0CqgEzkBhbGnNa7tMOyo9pQ==",
6 | "aikpriv": "AQEAAAASAAAAAAEAAAABAAEAAgAAAAwAAAgAAAAAAgAAAAAAAAAAAAABAMFpwDjfYfgGLuBHvg+S2IaimIkKWxam6BOKIRmI8v3iADif/0ysrF5na2oqvV2KkVlwv/iuZBZ/oq0XPZNbJ4V9hyLCMXaiTmb8821l5WeFcj7HNfA0LzYmm3pQUlsV963KQdNZYqKHJTwflrA5LKNAMv48IIib7hG3wAs3+M/p6EHqeMeMrJz3f5XD5zevkw+0zGmjazFMaeaPP3Jr3iKK+gbhX/W4Fx+fFKwNFdgXo4fN3qMkENVf05AwW56L/fxRmjE1ERqJ2qSIfY8vSkpJrIxaei0P2MkiAK5OoKkUtH0gqf/71/4UwZJ6XLurtAqoBM5AYWxpzWu7TDsqPaUAAAEAI/83lAiEz8a8RmgennBJNUELDu+zTARQxjcaWR95cEIMqYP/2V4IEbpp78o9l3BBYyZWnWEUPvzV8z2XpQJ60zp07mjTKTFwH84nl+NAh0wu/CwCfTaLxS0kty2drLHcr/YRC+3g7vvztpmRz3NfgUYvUsozgakfC3IZYXPY0vPFH8uYWFwSnwG7xPgoamcHct0x48N4ejAWx8492QYNyex07A/wdOTETCO7oMTRXlrK4MijgGJ+odZ7owBWrLXHKTl6A6mqmGCbaLR49xgVNTyjdRyEFitpkRMfm59lwab0Ljin2p6YyzN1i0oONpPL8PHtLz9liubQymK73i1KWg==",
7 | "ek": "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAzLJC3n/KpLjUn3DIRkhw\nUO9igtnCugQ92sAM/hJt7wj8uRDtz5O0mv7aHLmrlKA2XVmPgnInD3WOEVyfYpM3\nRXOftLSDdouB/C5iwqCJRiH+gr012SMDdqJsnWb4ofdPNBgJXRTs6+ztXH8GNO0c\nfqoMJBIBizE3P8kRPcqVGMJSXgkS1p9Otp0w431ckXZwdkoNxuM2GF6ktVfOEdCZ\nr8Idm+qbQaK+Ur57VCB02U49ekGdd1qA76BjmSVyHYPEkce7Jf2ipWK3d/yP0exs\neLKL9rO+Aab2X4/2v2NQPwEj0bz6oEAhvgpaoV0eakN2WiXxTVUG30OvCsRFxvUu\nvwIDAQAB\n-----END PUBLIC KEY-----\n",
8 | "owner_pw": "keylime"
9 | }
--------------------------------------------------------------------------------
/keylime-agent/tests/actions/local_action_hello.py:
--------------------------------------------------------------------------------
1 | '''
2 | SPDX-License-Identifier: Apache-2.0
3 | Copyright 2021 Keylime Authors
4 | '''
5 |
6 |
7 | async def execute(revocation):
8 | try:
9 | value = revocation['hello']
10 | print(value)
11 | except Exception as e:
12 | raise Exception(
13 | "The provided dictionary does not contain the 'hello' key")
14 |
--------------------------------------------------------------------------------
/keylime-agent/tests/actions/local_action_hello_shell.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | #SPDX-License-Identifier: Apache-2.0
4 | #Copyright 2021 Keylime Authors
5 |
6 | echo "Hello from non-python local action!"
7 |
--------------------------------------------------------------------------------
/keylime-agent/tests/actions/local_action_stand_alone.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/python3
2 | '''
3 | SPDX-License-Identifier: Apache-2.0
4 | Copyright 2021 Keylime Authors
5 | '''
6 |
7 | import argparse
8 | import json
9 | import sys
10 |
11 |
12 | def main():
13 | parser = argparse.ArgumentParser()
14 | parser.add_argument('json_file')
15 | args = parser.parse_args()
16 |
17 | with open(args.json_file, 'r') as f:
18 | input_json = json.load(f)
19 | value = input_json['hello']
20 |
21 | print(value)
22 |
23 |
24 | if __name__ == "__main__":
25 | main()
26 |
27 |
--------------------------------------------------------------------------------
/keylime-agent/tests/actions/shim.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/python3
2 | '''
3 | SPDX-License-Identifier: Apache-2.0
4 | Copyright 2021 Keylime Authors
5 | '''
6 |
7 | import argparse
8 | import asyncio
9 | import importlib
10 | import json
11 | import os
12 | import sys
13 |
14 |
15 | def main():
16 | # Parse arguments to get action name and input json file path
17 | parser = argparse.ArgumentParser()
18 | parser.add_argument('action', type=str, help='The revocation action to be'
19 | ' executed. The module must provide an \'execute()\''
20 | ' method which receives a JSON as argument')
21 | parser.add_argument('json_file', type=str, help='Input file')
22 |
23 | args = parser.parse_args()
24 |
25 | with open(args.json_file, 'r') as f:
26 | input_json = json.load(f)
27 |
28 | try:
29 | module = importlib.import_module(args.action)
30 | execute = getattr(module, 'execute')
31 | asyncio.run(execute(input_json))
32 | except Exception as e:
33 | print("Exception during execution of revocation action {}: {}".format(
34 | args.action, e), file=sys.stderr)
35 |
36 |
37 | if __name__ == "__main__":
38 | main()
39 |
--------------------------------------------------------------------------------
/keylime-agent/tests/unzipped/action_list:
--------------------------------------------------------------------------------
1 | local_action_rev_script1.py
2 | local_action_rev_script2.py
3 |
--------------------------------------------------------------------------------
/keylime-agent/tests/unzipped/local_action_payload.py:
--------------------------------------------------------------------------------
1 | '''
2 | SPDX-License-Identifier: Apache-2.0
3 | Copyright 2021 Keylime Authors
4 | '''
5 |
6 |
7 | async def execute(revocation):
8 | try:
9 | value = revocation['hello']
10 | print(value)
11 | except Exception as e:
12 | raise Exception(
13 | "The provided dictionary does not contain the 'hello' key")
14 |
--------------------------------------------------------------------------------
/keylime-agent/tests/unzipped/local_action_payload_shell.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | #SPDX-License-Identifier: Apache-2.0
4 | #Copyright 2021 Keylime Authors
5 |
6 | echo "Hello from non-python payload action!"
7 |
--------------------------------------------------------------------------------
/keylime-agent/tests/unzipped/local_action_rev_script1.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/python3
2 | '''
3 | SPDX-License-Identifier: Apache-2.0
4 | Copyright 2021 Keylime Authors
5 | '''
6 |
7 | import argparse
8 | import json
9 | import sys
10 |
11 |
12 | def main():
13 | parser = argparse.ArgumentParser()
14 | parser.add_argument('json_file')
15 | args = parser.parse_args()
16 |
17 | with open(args.json_file, 'r') as f:
18 | input_json = json.load(f)
19 | value = input_json['hello']
20 |
21 | print(value)
22 |
23 |
24 | if __name__ == "__main__":
25 | main()
26 |
--------------------------------------------------------------------------------
/keylime-agent/tests/unzipped/local_action_rev_script2.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/python3
2 | '''
3 | SPDX-License-Identifier: Apache-2.0
4 | Copyright 2021 Keylime Authors
5 | '''
6 |
7 | import argparse
8 | import json
9 | import sys
10 |
11 |
12 | def main():
13 | parser = argparse.ArgumentParser()
14 | parser.add_argument('json_file')
15 | args = parser.parse_args()
16 |
17 | with open(args.json_file, 'r') as f:
18 | input_json = json.load(f)
19 | value = input_json['hello']
20 |
21 | print(value)
22 |
23 |
24 | if __name__ == "__main__":
25 | main()
26 |
--------------------------------------------------------------------------------
/keylime-agent/tests/unzipped/test_err.json:
--------------------------------------------------------------------------------
1 | {
2 | "goodbye" : "there"
3 | }
4 |
--------------------------------------------------------------------------------
/keylime-agent/tests/unzipped/test_ok.json:
--------------------------------------------------------------------------------
1 | {
2 | "hello": "there"
3 | }
4 |
--------------------------------------------------------------------------------
/keylime-ima-emulator/Cargo.toml:
--------------------------------------------------------------------------------
1 | [package]
2 | name = "keylime_ima_emulator"
3 | description = "IMA emulator for Keylime"
4 | authors.workspace = true
5 | edition.workspace = true
6 | license.workspace = true
7 | repository.workspace = true
8 | version.workspace = true
9 |
10 | [dependencies]
11 | clap.workspace = true
12 | hex.workspace = true
13 | keylime.workspace = true
14 | log.workspace = true
15 | openssl.workspace = true
16 | signal-hook.workspace = true
17 | thiserror.workspace = true
18 | tss-esapi.workspace = true
19 |
--------------------------------------------------------------------------------
/keylime-push-model-agent/Cargo.toml:
--------------------------------------------------------------------------------
1 | [package]
2 | name = "keylime_push_model_agent"
3 | description = "Rust agent for Keylime (Push Model)"
4 | authors.workspace = true
5 | edition.workspace = true
6 | license.workspace = true
7 | repository.workspace = true
8 | version.workspace = true
9 |
10 | [dependencies]
11 | actix-web.workspace = true
12 | anyhow.workspace = true
13 | assert_cmd.workspace = true
14 | chrono.workspace = true
15 | clap.workspace = true
16 | keylime.workspace = true
17 | log.workspace = true
18 | predicates.workspace = true
19 | pretty_env_logger.workspace = true
20 | reqwest.workspace = true
21 | serde.workspace = true
22 | serde_derive.workspace = true
23 | serde_json.workspace = true
24 | static_assertions.workspace = true
25 | tokio.workspace = true
26 |
27 | [dev-dependencies]
28 | actix-rt.workspace = true
29 | tempfile.workspace = true
30 |
31 | [features]
32 | # The features enabled by default
33 | default = []
34 | testing = []
35 | legacy-python-actions = []
36 |
37 | [package.metadata.deb]
38 | section = "net"
39 | assets = [
40 | ["target/release/keylime_push_model_agent", "usr/bin/", "755"],
41 | ["../README.md", "usr/share/doc/keylime-agent/README", "644"],
42 | ["../keylime-agent.conf", "/etc/keylime/agent.conf", "640"],
43 | ]
44 | maintainer-scripts = "../debian/"
45 | systemd-units = { unit-scripts = "../dist/systemd/system/", enable = true }
46 |
--------------------------------------------------------------------------------
/keylime-push-model-agent/src/json_dump.rs:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: Apache-2.0
2 | // Copyright 2025 Keylime Authors
3 | use keylime::structures;
4 | use serde_json::{Error, Value};
5 |
6 | pub fn dump_attestation_request_to_value(
7 | request: &structures::AttestationRequest,
8 | ) -> Result {
9 | serde_json::to_value(request)
10 | }
11 |
12 | pub fn dump_session_request_to_value(
13 | request: &structures::SessionRequest,
14 | ) -> Result {
15 | serde_json::to_value(request)
16 | }
17 |
18 | pub fn dump_evidence_handling_request_to_value(
19 | request: &structures::EvidenceHandlingRequest,
20 | ) -> Result {
21 | serde_json::to_value(request)
22 | }
23 |
--------------------------------------------------------------------------------
/keylime-push-model-agent/src/registration.rs:
--------------------------------------------------------------------------------
1 | use keylime::agent_registration::{
2 | AgentRegistration, AgentRegistrationConfig,
3 | };
4 | use keylime::config::PushModelConfigTrait;
5 | use keylime::keylime_error::Result;
6 | use keylime::{cert, context_info};
7 |
8 | pub async fn check_registration(
9 | context_info: Option,
10 | ) -> Result<()> {
11 | let reg_config = keylime::config::PushModelConfig::default();
12 | if context_info.is_some() {
13 | crate::registration::register_agent(
14 | ®_config,
15 | &mut context_info.unwrap(),
16 | )
17 | .await?;
18 | }
19 | Ok(())
20 | }
21 |
22 | pub async fn register_agent(
23 | config: &T,
24 | context_info: &mut context_info::ContextInfo,
25 | ) -> Result<()> {
26 | let ac = AgentRegistrationConfig {
27 | contact_ip: config.get_contact_ip(),
28 | contact_port: config.get_contact_port(),
29 | registrar_ip: config.get_registrar_ip(),
30 | registrar_port: config.get_registrar_port(),
31 | enable_iak_idevid: config.get_enable_iak_idevid(),
32 | ek_handle: config.get_ek_handle(),
33 | };
34 |
35 | let cert_config = cert::CertificateConfig {
36 | agent_uuid: config.get_uuid(),
37 | contact_ip: config.get_contact_ip(),
38 | contact_port: config.get_contact_port(),
39 | server_cert: config.get_server_cert(),
40 | server_key: config.get_server_key(),
41 | server_key_password: config.get_server_key_password(),
42 | };
43 |
44 | let server_cert_key = cert::cert_from_server_key(&cert_config)?;
45 |
46 | let aa = AgentRegistration {
47 | ak: context_info.ak.clone(),
48 | ek_result: context_info.ek_result.clone(),
49 | api_versions: config.get_registrar_api_versions(),
50 | agent_registration_config: ac,
51 | agent_uuid: config.get_uuid(),
52 | mtls_cert: Some(server_cert_key.0),
53 | device_id: None, // TODO: Check how to proceed with device ID
54 | attest: None, // TODO: Check how to proceed with attestation, normally, no device ID means no attest
55 | signature: None, // TODO: Normally, no device ID means no signature
56 | ak_handle: context_info.ak_handle,
57 | };
58 | let ctx = context_info.get_mutable_tpm_context();
59 | match keylime::agent_registration::register_agent(aa, ctx).await {
60 | Ok(_) => Ok(()),
61 | Err(e) => Err(e),
62 | }
63 | } // register_agent
64 |
65 | #[cfg(test)]
66 | mod tests {
67 | use super::*;
68 |
69 | #[cfg(feature = "testing")]
70 | use keylime::context_info::{AlgorithmConfigurationString, ContextInfo};
71 |
72 | #[actix_rt::test]
73 | async fn test_avoid_registration() {
74 | let result = check_registration(None).await;
75 | assert!(result.is_ok());
76 | }
77 |
78 | #[tokio::test]
79 | #[cfg(feature = "testing")]
80 | async fn test_register_agent() {
81 | use keylime::tpm::testing;
82 | let _mutex = testing::lock_tests().await;
83 | let config = keylime::config::PushModelConfig::default();
84 | let alg_config = AlgorithmConfigurationString {
85 | tpm_encryption_alg: "rsa".to_string(),
86 | tpm_hash_alg: "sha256".to_string(),
87 | tpm_signing_alg: "rsassa".to_string(),
88 | };
89 | let mut context_info = ContextInfo::new_from_str(alg_config);
90 | let result = register_agent(&config, &mut context_info).await;
91 | assert!(result.is_err());
92 | assert!(context_info.flush_context().is_ok());
93 | }
94 | }
95 |
--------------------------------------------------------------------------------
/keylime-push-model-agent/test-data/evidence_handling_request.json:
--------------------------------------------------------------------------------
1 | {
2 | "data": {
3 | "type": "attestation",
4 | "attributes": {
5 | "evidence_collected": [
6 | {
7 | "evidence_class": "certification",
8 | "evidence_type": "tpm_quote",
9 | "data": {
10 | "subject_data": "subject_data_deserialized",
11 | "message": "message_deserialized",
12 | "signature": "signature_deserialized"
13 | }
14 | },
15 | {
16 | "evidence_class": "log",
17 | "evidence_type": "uefi_log",
18 | "data": {
19 | "entries": "uefi_log_entries_deserialized"
20 | }
21 | },
22 | {
23 | "evidence_class": "log",
24 | "evidence_type": "ima_log",
25 | "data": {
26 | "entries": "ima_log_entries_deserialized",
27 | "entry_count": 96
28 | }
29 | }
30 | ]
31 | }
32 | }
33 | }
--------------------------------------------------------------------------------
/keylime-push-model-agent/test-data/evidence_supported_attestation_request.json:
--------------------------------------------------------------------------------
1 | {
2 | "data": {
3 | "type": "attestation",
4 | "attributes": {
5 | "evidence_supported": [
6 | {
7 | "evidence_class": "certification",
8 | "evidence_type": "tpm_quote",
9 | "capabilities": {
10 | "component_version": "2.0",
11 | "hash_algorithms": [
12 | "sha3_512"
13 | ],
14 | "signature_schemes": [
15 | "rsassa"
16 | ],
17 | "available_subjects": {
18 | "sha1": [
19 | 1,
20 | 2,
21 | 3
22 | ],
23 | "sha256": [
24 | 4,
25 | 5,
26 | 6
27 | ]
28 | },
29 | "certification_keys": [
30 | {
31 | "key_class": "asymmetric",
32 | "local_identifier": "att_local_identifier",
33 | "key_algorithm": "rsa",
34 | "key_size": 2048,
35 | "server_identifier": "ak",
36 | "public": "VGhpcyBpcyBhIHRlc3QgZm9yIGEgYmFzZTY0IGVuY29kZWQgZm9ybWF0IHN0cmluZw=="
37 | }
38 | ]
39 | }
40 | },
41 | {
42 | "evidence_class": "log",
43 | "evidence_type": "uefi_log",
44 | "capabilities": {
45 | "evidence_version": "2.1",
46 | "entry_count": 20,
47 | "supports_partial_access": false,
48 | "appendable": false,
49 | "formats": [
50 | "application/octet-stream"
51 | ]
52 | }
53 | },
54 | {
55 | "evidence_class": "log",
56 | "evidence_type": "ima_log",
57 | "capabilities": {
58 | "entry_count": 20,
59 | "supports_partial_access": true,
60 | "appendable": true,
61 | "formats": [
62 | "text/plain"
63 | ]
64 | }
65 | }
66 | ],
67 | "system_info": {
68 | "boot_time": "2025-04-02T12:12:51Z"
69 | }
70 | }
71 | }
72 | }
73 |
--------------------------------------------------------------------------------
/keylime-push-model-agent/test-data/session_request.json:
--------------------------------------------------------------------------------
1 | {
2 | "data": {
3 | "type": "session",
4 | "attributes": {
5 | "agent_id": "example-agent",
6 | "authentication_supported": [
7 | {
8 | "authentication_class": "pop",
9 | "authentication_type": "tpm_pop"
10 | }
11 | ]
12 | }
13 | }
14 | }
--------------------------------------------------------------------------------
/keylime-push-model-agent/tests/test-keylime-push-model-agent.rs:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: Apache-2.0
2 | // Copyright 2025 Keylime Authors
3 | use assert_cmd::Command;
4 | use predicates::prelude::*;
5 |
6 | const KEYLIME_PUSH_MODEL_AGENT_BINARY: &str = "keylime_push_model_agent";
7 | const ERROR_SENDING_REQUEST: &str =
8 | "error sending request for url (http://1.2.3.4:5678/v3.0/agents/12345678/attestations)";
9 | const ERROR_SENDING_LOCALHOST_REQUEST: &str =
10 | "error sending request for url (http://localhost:3000/v3.0/agents/12345678/attestations)";
11 |
12 | #[cfg(test)]
13 | mod tests {
14 |
15 | use super::*;
16 |
17 | #[test]
18 | fn print_help_test() -> Result<(), Box> {
19 | let mut cmd = Command::cargo_bin(KEYLIME_PUSH_MODEL_AGENT_BINARY)?;
20 | cmd.arg("-h");
21 | cmd.assert().success().stdout(predicate::str::contains(
22 | KEYLIME_PUSH_MODEL_AGENT_BINARY,
23 | ));
24 | Ok(())
25 | }
26 |
27 | #[test]
28 | fn connection_error_test() -> Result<(), Box> {
29 | let mut cmd = Command::cargo_bin(KEYLIME_PUSH_MODEL_AGENT_BINARY)?;
30 | cmd.arg("-v")
31 | .arg("http://1.2.3.4:5678")
32 | .arg("--timeout")
33 | .arg("10")
34 | .arg("--agent-identifier")
35 | .arg("12345678")
36 | .arg("--message-type")
37 | .arg("attestation")
38 | .arg("--method")
39 | .arg("--avoid-tpm")
40 | .arg("true")
41 | .arg("POST");
42 | cmd.assert()
43 | .success()
44 | .stderr(predicate::str::contains(ERROR_SENDING_REQUEST));
45 | Ok(())
46 | }
47 | #[test]
48 | fn mockoon_based_test() -> Result<(), Box> {
49 | let mut cmd = Command::cargo_bin(KEYLIME_PUSH_MODEL_AGENT_BINARY)?;
50 | cmd.arg("-v")
51 | .arg("http://localhost:3000")
52 | .arg("--avoid-tpm")
53 | .arg("true")
54 | .arg("--agent-identifier")
55 | .arg("12345678")
56 | .arg("--timeout")
57 | .arg("1000");
58 | // Check if MOCKOON environment variable is set and, if so, assume
59 | // that the mock server is running in port 3000 and response will be received
60 | // in less than 1 second.
61 | if std::env::var("MOCKOON").is_ok() {
62 | cmd.assert().success();
63 | } else {
64 | // If MOCKOON is not set, assume that the server is not running
65 | // and we expect an error.
66 | cmd.assert().success().stderr(predicate::str::contains(
67 | ERROR_SENDING_LOCALHOST_REQUEST,
68 | ));
69 | }
70 | Ok(())
71 | }
72 | }
73 |
--------------------------------------------------------------------------------
/keylime/Cargo.toml:
--------------------------------------------------------------------------------
1 | [package]
2 | name = "keylime"
3 | description = "Shared utilities for Keylime crates"
4 | authors.workspace = true
5 | edition.workspace = true
6 | license.workspace = true
7 | repository.workspace = true
8 | version.workspace = true
9 |
10 | [dependencies]
11 | actix-web.workspace = true
12 | anyhow.workspace = true
13 | base64.workspace = true
14 | chrono.workspace = true
15 | config.workspace = true
16 | glob.workspace = true
17 | hex.workspace = true
18 | log.workspace = true
19 | once_cell.workspace = true
20 | openssl.workspace = true
21 | pest.workspace = true
22 | pest_derive.workspace = true
23 | reqwest.workspace = true
24 | serde.workspace = true
25 | serde_derive.workspace = true
26 | serde_json.workspace = true
27 | static_assertions.workspace = true
28 | tempfile.workspace = true
29 | thiserror.workspace = true
30 | tss-esapi.workspace = true
31 | picky-asn1-der.workspace = true
32 | picky-asn1-x509.workspace = true
33 | tokio.workspace = true
34 | # wiremock was moved to be a regular dependency because optional
35 | # dev-dependencies are not supported
36 | # see: https://github.com/rust-lang/cargo/issues/1596
37 | wiremock = {version = "0.6", optional = true}
38 | uuid.workspace = true
39 | zip.workspace = true
40 | zmq = {version = "0.9.2", optional = true}
41 |
42 | [dev-dependencies]
43 | tempfile.workspace = true
44 | actix-rt.workspace = true
45 |
46 | [features]
47 | # This feature enables tests that require a TPM and the TCTI environment
48 | # variable properly configured
49 | # This should change to dev-dependencies when we have integration testing
50 | testing = ["wiremock"]
51 | # This feature is deprecated and will be removed on next major release
52 | with-zmq = ["zmq"]
53 |
--------------------------------------------------------------------------------
/keylime/src/agent_data.rs:
--------------------------------------------------------------------------------
1 | use crate::algorithms::{HashAlgorithm, SignAlgorithm};
2 | use crate::keylime_error::Result;
3 | use crate::tpm;
4 | use serde::{Deserialize, Serialize};
5 | use std::fs::File;
6 | use std::path::Path;
7 | use tss_esapi::structures::{Private, Public};
8 | use tss_esapi::traits::{Marshall, UnMarshall};
9 |
10 | // TPM data and agent related that can be persisted and loaded on agent startup.
11 | #[derive(Debug, Clone, Serialize, Deserialize)]
12 | pub struct AgentData {
13 | pub ak_hash_alg: HashAlgorithm,
14 | pub ak_sign_alg: SignAlgorithm,
15 | ak_public: Vec,
16 | ak_private: Vec,
17 | ek_hash: Vec,
18 | }
19 |
20 | impl AgentData {
21 | pub fn create(
22 | ak_hash_alg: HashAlgorithm,
23 | ak_sign_alg: SignAlgorithm,
24 | ak: &tpm::AKResult,
25 | ek_hash: &[u8],
26 | ) -> Result {
27 | let ak_public = ak.public.marshall()?;
28 | let ak_private: Vec = ak.private.to_vec();
29 | let ek_hash: Vec = ek_hash.to_vec();
30 | Ok(Self {
31 | ak_hash_alg,
32 | ak_sign_alg,
33 | ak_public,
34 | ak_private,
35 | ek_hash,
36 | })
37 | }
38 |
39 | pub fn load(path: &Path) -> Result {
40 | let file = File::open(path)?;
41 | let data: Self = serde_json::from_reader(file)?;
42 | Ok(data)
43 | }
44 |
45 | pub fn store(&self, path: &Path) -> Result<()> {
46 | let file = File::create(path)?;
47 | serde_json::to_writer_pretty(file, self)?;
48 | Ok(())
49 | }
50 |
51 | pub fn get_ak(&self) -> Result {
52 | let public = Public::unmarshall(&self.ak_public)?;
53 | let private = Private::try_from(self.ak_private.clone())?;
54 |
55 | Ok(tpm::AKResult { public, private })
56 | }
57 |
58 | pub fn valid(
59 | &self,
60 | hash_alg: HashAlgorithm,
61 | sign_alg: SignAlgorithm,
62 | ek_hash: &[u8],
63 | ) -> bool {
64 | hash_alg == self.ak_hash_alg
65 | && sign_alg == self.ak_sign_alg
66 | && ek_hash.to_vec() == self.ek_hash
67 | }
68 | }
69 |
--------------------------------------------------------------------------------
/keylime/src/agent_registration.rs:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: Apache-2.0
2 | // Copyright 2025 Keylime Authors
3 | use crate::keylime_error::{Error, Result};
4 | use crate::{
5 | agent_identity::AgentIdentityBuilder,
6 | crypto::{self},
7 | device_id,
8 | registrar_client::RegistrarClientBuilder,
9 | tpm::{self},
10 | };
11 | use base64::{engine::general_purpose, Engine as _};
12 | use log::{error, info};
13 | use openssl::x509::X509;
14 | use tss_esapi::{
15 | handles::KeyHandle, structures::PublicBuffer, traits::Marshall,
16 | };
17 |
18 | #[derive(Debug)]
19 | pub struct AgentRegistrationConfig {
20 | pub contact_ip: String,
21 | pub contact_port: u32,
22 | pub ek_handle: String,
23 | pub enable_iak_idevid: bool,
24 | pub registrar_ip: String,
25 | pub registrar_port: u32,
26 | }
27 |
28 | #[derive(Debug)]
29 | pub struct AgentRegistration {
30 | pub ak: tpm::AKResult,
31 | pub ek_result: tpm::EKResult,
32 | pub api_versions: Vec,
33 | pub agent_registration_config: AgentRegistrationConfig,
34 | pub agent_uuid: String,
35 | pub mtls_cert: Option,
36 | pub device_id: Option,
37 | pub attest: Option,
38 | pub signature: Option,
39 | pub ak_handle: KeyHandle,
40 | }
41 |
42 | pub async fn register_agent(
43 | aa: AgentRegistration,
44 | ctx: &mut tpm::Context<'_>,
45 | ) -> Result<()> {
46 | let iak_pub;
47 | let idevid_pub;
48 | let ak_pub = &PublicBuffer::try_from(aa.ak.public)?.marshall()?;
49 | let ek_pub =
50 | &PublicBuffer::try_from(aa.ek_result.public.clone())?.marshall()?;
51 |
52 | let mut ai_builder = AgentIdentityBuilder::new()
53 | .ak_pub(ak_pub)
54 | .ek_pub(ek_pub)
55 | .enabled_api_versions(
56 | aa.api_versions.iter().map(|ver| ver.as_ref()).collect(),
57 | )
58 | .uuid(&aa.agent_uuid)
59 | .ip(aa.agent_registration_config.contact_ip.clone())
60 | .port(aa.agent_registration_config.contact_port);
61 |
62 | if let Some(mtls_cert) = aa.mtls_cert {
63 | ai_builder = ai_builder.mtls_cert(mtls_cert);
64 | }
65 |
66 | // If the certificate is not None add it to the builder
67 | if let Some(ekchain) = aa.ek_result.to_pem() {
68 | ai_builder = ai_builder.ek_cert(ekchain);
69 | }
70 |
71 | // Set the IAK/IDevID related fields, if enabled
72 | if aa.agent_registration_config.enable_iak_idevid {
73 | let (Some(dev_id), Some(attest), Some(signature)) =
74 | (&aa.device_id, aa.attest, aa.signature)
75 | else {
76 | error!("IDevID and IAK are enabled but could not be generated");
77 | return Err(Error::ConfigurationGenericError(
78 | "IDevID and IAK are enabled but could not be generated"
79 | .to_string(),
80 | ));
81 | };
82 |
83 | iak_pub =
84 | PublicBuffer::try_from(dev_id.iak_pubkey.clone())?.marshall()?;
85 | idevid_pub = PublicBuffer::try_from(dev_id.idevid_pubkey.clone())?
86 | .marshall()?;
87 | ai_builder = ai_builder
88 | .iak_attest(attest.marshall()?)
89 | .iak_sign(signature.marshall()?)
90 | .iak_pub(&iak_pub)
91 | .idevid_pub(&idevid_pub);
92 |
93 | // If the IAK certificate was provided, set it
94 | if let Some(iak_cert) = dev_id.iak_cert.clone() {
95 | ai_builder = ai_builder.iak_cert(iak_cert);
96 | }
97 |
98 | // If the IDevID certificate was provided, set it
99 | if let Some(idevid_cert) = dev_id.idevid_cert.clone() {
100 | ai_builder = ai_builder.idevid_cert(idevid_cert);
101 | }
102 | }
103 |
104 | // Build the Agent Identity
105 | let ai = ai_builder.build().await?;
106 |
107 | let ac = &aa.agent_registration_config;
108 | // Build the registrar client
109 | // Create a RegistrarClientBuilder and set the parameters
110 | let mut registrar_client = RegistrarClientBuilder::new()
111 | .registrar_address(ac.registrar_ip.clone())
112 | .registrar_port(ac.registrar_port)
113 | .build()
114 | .await?;
115 |
116 | // Request keyblob material
117 | let keyblob = registrar_client.register_agent(&ai).await?;
118 |
119 | info!("SUCCESS: Agent {} registered", &aa.agent_uuid);
120 |
121 | let key = ctx.activate_credential(
122 | keyblob,
123 | aa.ak_handle,
124 | aa.ek_result.key_handle,
125 | )?;
126 |
127 | // Flush EK if we created it
128 | if aa.agent_registration_config.ek_handle.is_empty() {
129 | ctx.flush_context(aa.ek_result.key_handle.into())?;
130 | }
131 |
132 | let mackey = general_purpose::STANDARD.encode(key.value());
133 | let auth_tag =
134 | crypto::compute_hmac(mackey.as_bytes(), aa.agent_uuid.as_bytes())?;
135 | let auth_tag = hex::encode(&auth_tag);
136 |
137 | registrar_client.activate_agent(&ai, &auth_tag).await?;
138 |
139 | info!("SUCCESS: Agent {} activated", &aa.agent_uuid);
140 | Ok(())
141 | }
142 |
--------------------------------------------------------------------------------
/keylime/src/cert.rs:
--------------------------------------------------------------------------------
1 | use crate::crypto;
2 | use crate::keylime_error::Result;
3 | use log::debug;
4 | use openssl::pkey::{PKey, Public};
5 | use openssl::x509::X509;
6 | use std::path::Path;
7 |
8 | pub struct CertificateConfig {
9 | pub agent_uuid: String,
10 | pub contact_ip: String,
11 | pub contact_port: u32,
12 | pub server_cert: String,
13 | pub server_key: String,
14 | pub server_key_password: String,
15 | }
16 |
17 | pub fn cert_from_server_key(
18 | config: &CertificateConfig,
19 | ) -> Result<(X509, PKey)> {
20 | let cert: X509;
21 | let (nk_pub, nk_priv) = match config.server_key.as_ref() {
22 | "" => {
23 | debug!(
24 | "The server_key option was not set in the configuration file"
25 | );
26 | debug!("Generating new key pair");
27 | crypto::rsa_generate_pair(2048)?
28 | }
29 | path => {
30 | let key_path = Path::new(&path);
31 | if key_path.exists() {
32 | debug!(
33 | "Loading existing key pair from {}",
34 | key_path.display()
35 | );
36 | crypto::load_key_pair(
37 | key_path,
38 | Some(&config.server_key_password),
39 | )?
40 | } else {
41 | debug!("Generating new key pair");
42 | let (public, private) = crypto::rsa_generate_pair(2048)?;
43 | // Write the generated key to the file
44 | crypto::write_key_pair(
45 | &private,
46 | key_path,
47 | Some(&config.server_key_password),
48 | )?;
49 | (public, private)
50 | }
51 | }
52 | };
53 |
54 | let contact_ips = vec![config.contact_ip.as_str()];
55 | cert = match config.server_cert.as_ref() {
56 | "" => {
57 | debug!("The server_cert option was not set in the configuration file");
58 |
59 | crypto::x509::CertificateBuilder::new()
60 | .private_key(&nk_priv)
61 | .common_name(&config.agent_uuid)
62 | .add_ips(contact_ips)
63 | .build()?
64 | }
65 | path => {
66 | let cert_path = Path::new(&path);
67 | if cert_path.exists() {
68 | debug!(
69 | "Loading existing mTLS certificate from {}",
70 | cert_path.display()
71 | );
72 | crypto::load_x509_pem(cert_path)?
73 | } else {
74 | debug!("Generating new mTLS certificate");
75 | let cert = crypto::x509::CertificateBuilder::new()
76 | .private_key(&nk_priv)
77 | .common_name(&config.agent_uuid)
78 | .add_ips(contact_ips)
79 | .build()?;
80 | crypto::write_x509(&cert, cert_path)?;
81 | cert
82 | }
83 | }
84 | };
85 | Ok((cert, nk_pub))
86 | }
87 |
88 | #[cfg(test)]
89 | mod tests {
90 | use super::*;
91 | use crate::cert::CertificateConfig;
92 |
93 | #[test]
94 | fn test_cert_from_server_key() {
95 | const TEST_CERT_PATH: &str = "test_cert.pem";
96 | const TEST_KEY_PATH: &str = "test_key.pem";
97 | // Ensure the test cert file does not exist before the test
98 | let _ = std::fs::remove_file(TEST_CERT_PATH);
99 | let _ = std::fs::remove_file(TEST_KEY_PATH);
100 | let config = CertificateConfig {
101 | agent_uuid: "test-uuid".to_string(),
102 | contact_ip: "1.2.3.4".to_string(),
103 | contact_port: 8080,
104 | server_cert: TEST_CERT_PATH.to_string(),
105 | server_key: TEST_KEY_PATH.to_string(),
106 | server_key_password: "test_password".to_string(),
107 | };
108 | let result = cert_from_server_key(&config);
109 | assert!(result.is_ok());
110 | // Clean up the test cert file after the test
111 | let _ = std::fs::remove_file(TEST_CERT_PATH);
112 | let _ = std::fs::remove_file(TEST_KEY_PATH);
113 | } // test_cert_from_server_key
114 |
115 | #[test]
116 | fn test_cert_no_server_key() {
117 | const TEST_CERT_PATH: &str = "test_cert2.pem";
118 | // Ensure the test cert file does not exist before the test
119 | let _ = std::fs::remove_file(TEST_CERT_PATH);
120 | let config = CertificateConfig {
121 | agent_uuid: "test-uuid".to_string(),
122 | contact_ip: "1.2.3.4".to_string(),
123 | contact_port: 8080,
124 | server_cert: TEST_CERT_PATH.to_string(),
125 | server_key: "".to_string(),
126 | server_key_password: "test_password2".to_string(),
127 | };
128 | let result = cert_from_server_key(&config);
129 | assert!(result.is_ok());
130 | // Clean up the test cert file after the test
131 | let _ = std::fs::remove_file(TEST_CERT_PATH);
132 | } // test_cert_no_server_key
133 |
134 | #[test]
135 | fn test_cert_no_server_cert() {
136 | // Ensure the test cert file does not exist before the test
137 | const TEST_KEY_PATH: &str = "test_key.pem";
138 | let config = CertificateConfig {
139 | agent_uuid: "test-uuid".to_string(),
140 | contact_ip: "1.2.3.4".to_string(),
141 | contact_port: 8080,
142 | server_cert: "".to_string(),
143 | server_key: TEST_KEY_PATH.to_string(),
144 | server_key_password: "test_password".to_string(),
145 | };
146 | let result = cert_from_server_key(&config);
147 | assert!(result.is_ok());
148 | // Clean up the test key file after the test
149 | let _ = std::fs::remove_file(TEST_KEY_PATH);
150 | } // test_cert_no_server_cert
151 |
152 | #[test]
153 | fn test_cert_wrong_server_key_path() {
154 | let config = CertificateConfig {
155 | agent_uuid: "test-uuid".to_string(),
156 | contact_ip: "1.2.3.4".to_string(),
157 | contact_port: 8080,
158 | server_cert: "test_cert3.pem".to_string(),
159 | server_key: "/server/key/can/not/be/created/here".to_string(),
160 | server_key_password: "test_password3".to_string(),
161 | };
162 | let result = cert_from_server_key(&config);
163 | assert!(result.is_err());
164 | } // test_cert_wrong_server_key_path
165 |
166 | #[test]
167 | fn test_cert_correct_server_key_path() {
168 | const TEST_KEY_PATH: &str = "test_key2.pem";
169 | const TEST_CERT_PATH: &str = "test_cert4.pem";
170 | // Ensure the test key file does not exist before the test
171 | let _ = std::fs::remove_file(TEST_KEY_PATH);
172 | let _ = std::fs::remove_file(TEST_CERT_PATH);
173 | let config = CertificateConfig {
174 | agent_uuid: "test-uuid".to_string(),
175 | contact_ip: "1.2.3.4".to_string(),
176 | contact_port: 8080,
177 | server_cert: TEST_CERT_PATH.to_string(),
178 | server_key: TEST_KEY_PATH.to_string(),
179 | server_key_password: "test_password4".to_string(),
180 | };
181 | let result = cert_from_server_key(&config);
182 | assert!(result.is_ok());
183 | // Clean up files after the test
184 | let _ = std::fs::remove_file(TEST_KEY_PATH);
185 | let _ = std::fs::remove_file(TEST_CERT_PATH);
186 | } // test_cert_correct_server_key_path
187 | }
188 |
--------------------------------------------------------------------------------
/keylime/src/config/mod.rs:
--------------------------------------------------------------------------------
1 | mod push_model_config;
2 |
3 | pub use push_model_config::*;
4 |
--------------------------------------------------------------------------------
/keylime/src/crypto/auth_tag.rs:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: Apache-2.0
2 | // Copyright 2025 Keylime Authors
3 |
4 | use crate::crypto::AUTH_TAG_LEN;
5 | use serde::{Deserialize, Serialize};
6 | use thiserror::Error;
7 |
8 | #[derive(Debug, Error)]
9 | pub enum AuthTagError {
10 | // Invalid authentication tag size
11 | #[error("auth tag length {0} does not correspond to valid SHA-384 HMAC")]
12 | InvalidAuthTagSize(usize),
13 | }
14 |
15 | #[derive(Debug, Clone, Serialize, Deserialize)]
16 | pub struct AuthTag {
17 | bytes: Vec,
18 | }
19 |
20 | impl AsRef<[u8]> for AuthTag {
21 | fn as_ref(&self) -> &[u8] {
22 | self.bytes.as_slice()
23 | }
24 | }
25 |
26 | impl TryFrom<&[u8]> for AuthTag {
27 | type Error = AuthTagError;
28 |
29 | fn try_from(v: &[u8]) -> std::result::Result {
30 | match v.len() {
31 | AUTH_TAG_LEN => Ok(AuthTag { bytes: v.to_vec() }),
32 | _ => Err(AuthTagError::InvalidAuthTagSize(v.len())),
33 | }
34 | }
35 | }
36 |
37 | #[cfg(test)]
38 | mod tests {
39 | use super::*;
40 |
41 | #[test]
42 | fn test_convert() {
43 | let a: [u8; AUTH_TAG_LEN] = [0xAA; AUTH_TAG_LEN];
44 | let invalid: [u8; 32] = [0xBB; 32];
45 |
46 | let r = AuthTag::try_from(a.as_ref());
47 | assert!(r.is_ok());
48 |
49 | let r = AuthTag::try_from(invalid.as_ref());
50 | assert!(r.is_err());
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/keylime/src/crypto/encrypted_data.rs:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: Apache-2.0
2 | // Copyright 2025 Keylime Authors
3 |
4 | use serde::{Deserialize, Serialize};
5 |
6 | #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
7 | pub struct EncryptedData {
8 | bytes: Vec,
9 | }
10 |
11 | impl AsRef<[u8]> for EncryptedData {
12 | fn as_ref(&self) -> &[u8] {
13 | self.bytes.as_slice()
14 | }
15 | }
16 |
17 | impl From<&[u8]> for EncryptedData {
18 | fn from(v: &[u8]) -> Self {
19 | EncryptedData { bytes: v.to_vec() }
20 | }
21 | }
22 |
23 | impl From> for EncryptedData {
24 | fn from(v: Vec) -> Self {
25 | EncryptedData { bytes: v }
26 | }
27 | }
28 |
29 | #[cfg(test)]
30 | mod test {
31 | use super::*;
32 |
33 | #[test]
34 | fn test_encrypted_data_as_ref() {
35 | let a = EncryptedData {
36 | bytes: vec![0x0A, 16],
37 | };
38 |
39 | let r = a.as_ref();
40 | assert_eq!(r, vec![0x0A, 16]);
41 | }
42 |
43 | #[test]
44 | fn test_encrypted_data_from_slice() {
45 | let a: [u8; 16] = [0x0B; 16];
46 | let expected = EncryptedData {
47 | bytes: vec![0x0B; 16],
48 | };
49 |
50 | let r = EncryptedData::from(a.as_ref());
51 |
52 | assert_eq!(r, expected);
53 | }
54 |
55 | #[test]
56 | fn test_encrypted_data_from_vec() {
57 | let a: Vec = vec![0x0C; 16];
58 | let expected = EncryptedData {
59 | bytes: vec![0x0C; 16],
60 | };
61 |
62 | let r = EncryptedData::from(a);
63 |
64 | assert_eq!(r, expected);
65 | }
66 | }
67 |
--------------------------------------------------------------------------------
/keylime/src/crypto/symmkey.rs:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: Apache-2.0
2 | // Copyright 2025 Keylime Authors
3 |
4 | use crate::crypto::{AES_128_KEY_LEN, AES_256_KEY_LEN};
5 | use serde::{Deserialize, Serialize};
6 | use thiserror::Error;
7 |
8 | #[derive(Debug, Error)]
9 | pub enum SymmKeyError {
10 | // Invalid key size for AES
11 | #[error("invalid AES key size: {0}")]
12 | InvalidKeySize(usize),
13 |
14 | // Incompatible sizes for XOR
15 | #[error("cannot XOR slices of different sizes")]
16 | XorIncompatibleSizes,
17 | }
18 |
19 | // a vector holding keys
20 | pub type KeySet = Vec;
21 |
22 | // a key of len AES_128_KEY_LEN or AES_256_KEY_LEN
23 | #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
24 | pub struct SymmKey {
25 | bytes: Vec,
26 | }
27 |
28 | impl SymmKey {
29 | pub fn xor(&self, other: &Self) -> Result {
30 | let my_bytes = self.as_ref();
31 | let other_bytes = other.as_ref();
32 | if my_bytes.len() != other_bytes.len() {
33 | return Err(SymmKeyError::XorIncompatibleSizes);
34 | }
35 | let mut outbuf = vec![0u8; my_bytes.len()];
36 | for (out, (x, y)) in
37 | outbuf.iter_mut().zip(my_bytes.iter().zip(other_bytes))
38 | {
39 | *out = x ^ y;
40 | }
41 | Ok(Self { bytes: outbuf })
42 | }
43 | }
44 |
45 | impl AsRef<[u8]> for SymmKey {
46 | fn as_ref(&self) -> &[u8] {
47 | self.bytes.as_slice()
48 | }
49 | }
50 |
51 | impl TryFrom<&[u8]> for SymmKey {
52 | type Error = SymmKeyError;
53 |
54 | fn try_from(v: &[u8]) -> std::result::Result {
55 | match v.len() {
56 | AES_128_KEY_LEN | AES_256_KEY_LEN => {
57 | Ok(SymmKey { bytes: v.to_vec() })
58 | }
59 | other => Err(SymmKeyError::InvalidKeySize(other)),
60 | }
61 | }
62 | }
63 |
64 | #[cfg(test)]
65 | mod tests {
66 | use super::*;
67 |
68 | #[test]
69 | fn test_convert() {
70 | let a_128: [u8; AES_128_KEY_LEN] = [0; AES_128_KEY_LEN];
71 | let a_256: [u8; AES_256_KEY_LEN] = [0; AES_256_KEY_LEN];
72 | let a_unknown: [u8; 127] = [0; 127];
73 |
74 | let r_128 = SymmKey::try_from(a_128.as_ref());
75 | assert!(r_128.is_ok());
76 |
77 | let r_256 = SymmKey::try_from(a_256.as_ref());
78 | assert!(r_256.is_ok());
79 |
80 | let r_unknown = SymmKey::try_from(a_unknown.as_ref());
81 | assert!(r_unknown.is_err());
82 | }
83 |
84 | #[test]
85 | fn test_xor() {
86 | // Input for 128 bits keys
87 | let a: [u8; AES_128_KEY_LEN] = [0xA0; AES_128_KEY_LEN];
88 | let b: [u8; AES_128_KEY_LEN] = [0x0A; AES_128_KEY_LEN];
89 | let axb: [u8; AES_128_KEY_LEN] = [0xAA; AES_128_KEY_LEN];
90 | let r_128 =
91 | SymmKey::try_from(axb.as_ref()).expect("failed to convert");
92 |
93 | // Input for 256 bits keys
94 | let c: [u8; AES_256_KEY_LEN] = [0xA0; AES_256_KEY_LEN];
95 | let d: [u8; AES_256_KEY_LEN] = [0x0A; AES_256_KEY_LEN];
96 | let cxd: [u8; AES_256_KEY_LEN] = [0xAA; AES_256_KEY_LEN];
97 | let r_256 =
98 | SymmKey::try_from(cxd.as_ref()).expect("failed to convert");
99 |
100 | // Test for each set of inputs
101 | for (i, j, expected) in [
102 | (a.as_ref(), b.as_ref(), &r_128),
103 | (c.as_ref(), d.as_ref(), &r_256),
104 | ] {
105 | let k_i =
106 | SymmKey::try_from(i).expect("failed to get key from slice");
107 | let k_j =
108 | SymmKey::try_from(j).expect("failed to get key from slice");
109 | let result = k_i.xor(&k_j);
110 | assert!(result.is_ok());
111 |
112 | let out = result.expect("xor failed");
113 | assert_eq!(&out, expected);
114 | }
115 | }
116 | }
117 |
--------------------------------------------------------------------------------
/keylime/src/file_ops.rs:
--------------------------------------------------------------------------------
1 | use std::fs;
2 |
3 | pub fn read_file(path: &str) -> Result {
4 | fs::read_to_string(path)
5 | }
6 |
7 | #[cfg(test)]
8 | mod tests {
9 | use super::*;
10 |
11 | #[test]
12 | fn test_read_file() {
13 | let path = "test.txt";
14 | fs::write(path, "Hello, world!").unwrap(); //#[allow_ci]
15 | let content = read_file(path).unwrap(); //#[allow_ci]
16 | assert_eq!(content, "Hello, world!");
17 | fs::remove_file(path).unwrap(); //#[allow_ci]
18 | }
19 |
20 | #[test]
21 | fn test_read_nonexistent_file() {
22 | let result = read_file("nonexistent.txt");
23 | assert!(result.is_err());
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/keylime/src/global_config.rs:
--------------------------------------------------------------------------------
1 | use crate::{
2 | hostname_parser::HostnameParsingError, ip_parser::IpParsingError,
3 | list_parser::ListParsingError,
4 | };
5 | use config::ConfigError;
6 | use thiserror::Error;
7 |
8 | #[derive(Error, Debug)]
9 | pub enum KeylimeConfigError {
10 | // Error from config crate
11 | #[error("Error from the config crate")]
12 | Config(#[from] ConfigError),
13 |
14 | // Generic configuration error
15 | #[error("Configuration error: {0}")]
16 | Generic(String),
17 |
18 | // Glob error
19 | #[error("Glob pattern error")]
20 | GlobPattern(#[from] glob::PatternError),
21 |
22 | // Host name parsing error
23 | #[error("Host name parsing error")]
24 | HostnameParsing(#[from] HostnameParsingError),
25 |
26 | // Incompatible options error
27 | #[error("Incompatible configuration options '{option_a}' set as '{value_a}', but '{option_b}' is set as '{value_b}'")]
28 | IncompatibleOptions {
29 | option_a: String,
30 | value_a: String,
31 | option_b: String,
32 | value_b: String,
33 | },
34 |
35 | // Infallible
36 | #[error("Infallible")]
37 | Infallible(#[from] std::convert::Infallible),
38 |
39 | // IP parsing error
40 | #[error("IP parsing error")]
41 | IpParsing(#[from] IpParsingError),
42 |
43 | // Unsupported type in configuration
44 | #[error(
45 | "Unsupported type conversion from serde_json::Value to config::Value"
46 | )]
47 | JsonConversion,
48 |
49 | // List parsing error
50 | #[error("List parsing error")]
51 | ListParsing(#[from] ListParsingError),
52 |
53 | // Missing configuration file set in KEYLIME_AGENT_CONFIG
54 | #[error("Missing file {file} set in 'KEYLIME_AGENT_CONFIG' environment variable")]
55 | MissingEnvConfigFile { file: String },
56 |
57 | // Missing directory set in keylime_dir configuration option
58 | #[error(
59 | "Missing directory {path} set in 'keylime_dir' configuration option"
60 | )]
61 | MissingKeylimeDir {
62 | path: String,
63 | source: std::io::Error,
64 | },
65 |
66 | #[error("Required option {0} not set in configuration")]
67 | RequiredOption(String),
68 |
69 | // Error from serde crate
70 | #[error("Serde error")]
71 | Serde(#[from] serde_json::Error),
72 | }
73 |
--------------------------------------------------------------------------------
/keylime/src/hash_ek.rs:
--------------------------------------------------------------------------------
1 | use crate::keylime_error::Result;
2 | use openssl::hash::MessageDigest;
3 | use tss_esapi::structures::Public;
4 |
5 | use crate::crypto::{hash, tss_pubkey_to_pem};
6 |
7 | /// Calculate the SHA-256 hash of the TPM public key in PEM format
8 | ///
9 | /// This is used as the agent UUID when the configuration option 'uuid' is set as 'hash_ek'
10 | pub fn hash_ek_pubkey(ek_pub: Public) -> Result {
11 | // Calculate the SHA-256 hash of the public key in PEM format
12 | let pem = tss_pubkey_to_pem(ek_pub)?;
13 | let hash = hash(&pem, MessageDigest::sha256())?;
14 | Ok(hex::encode(hash))
15 | }
16 |
--------------------------------------------------------------------------------
/keylime/src/hostname.pest:
--------------------------------------------------------------------------------
1 | hostname = {SOI ~ label ~ ("." ~ label)* ~ EOI}
2 | label = { ASCII_ALPHANUMERIC+ ~ ("-"+ ~ ASCII_ALPHANUMERIC+)*}
3 |
--------------------------------------------------------------------------------
/keylime/src/hostname_parser.rs:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: Apache-2.0
2 | // Copyright 2024 Keylime Authors
3 |
4 | use pest::Parser;
5 | use pest_derive::Parser;
6 | use thiserror::Error;
7 |
8 | #[derive(Parser)]
9 | #[grammar = "hostname.pest"]
10 | pub struct HostnameParser;
11 |
12 | #[derive(Error, Debug)]
13 | pub enum HostnameParsingError {
14 | #[error("Invalid input {0}")]
15 | InvalidInput(String),
16 |
17 | #[error("failed to parse the input {input}")]
18 | ParseError {
19 | input: String,
20 | source: Box>,
21 | },
22 | }
23 |
24 | /// Parses a hostname from a string slice following RFC-1123
25 | ///
26 | /// Valid hostnames are formed by labels separated by dots ('.').
27 | ///
28 | /// The labels can only contain alphanumeric characters ('a'..'z' | 'A'..'Z' | '0'..'9') and the
29 | /// hyphen ('-'). The labels cannot begin or end with an hyphen.
30 | ///
31 | /// # Arguments
32 | ///
33 | /// * `hostname` the string to be parsed
34 | ///
35 | /// # Returns
36 | ///
37 | /// The obtained hostname as a &str if it is a valid hostname
38 | ///
39 | /// # Examples
40 | ///
41 | /// Valid hostnames:
42 | ///
43 | /// * `hostname`
44 | /// * `host-name`
45 | /// * `a.b.c`
46 | /// * `a-b.c-d.e-f`
47 | ///
48 | /// Invalid hostnames:
49 | ///
50 | /// * `a_b.c`
51 | /// * `a.b-.c`
52 | /// * `a.-b.c`
53 | pub fn parse_hostname(hostname: &str) -> Result<&str, HostnameParsingError> {
54 | let Some(pair) = HostnameParser::parse(Rule::hostname, hostname)
55 | .map_err(|e| HostnameParsingError::ParseError {
56 | input: hostname.to_string(),
57 | source: Box::new(e),
58 | })?
59 | .next()
60 | else {
61 | return Err(HostnameParsingError::InvalidInput(hostname.to_string()));
62 | };
63 | Ok(pair.as_str())
64 | }
65 |
66 | // Unit Testing
67 | #[cfg(test)]
68 | mod tests {
69 | use super::*;
70 |
71 | #[test]
72 | fn test_parse_hostname() {
73 | // Sanity: most common case
74 | assert_eq!(parse_hostname("hostname").unwrap(), "hostname"); //#[allow_ci]
75 | assert_eq!(parse_hostname("ab.cd.ef").unwrap(), "ab.cd.ef"); //#[allow_ci]
76 | assert_eq!(parse_hostname("ab-cd-ef").unwrap(), "ab-cd-ef"); //#[allow_ci]
77 |
78 | // More advanced cases
79 | assert_eq!(
80 | parse_hostname("hostname-123.test").unwrap(), //#[allow_ci]
81 | "hostname-123.test"
82 | );
83 | assert_eq!(parse_hostname("123-456.789").unwrap(), "123-456.789"); //#[allow_ci]
84 | assert_eq!(parse_hostname("1----9").unwrap(), "1----9"); //#[allow_ci]
85 |
86 | // Invalid input
87 | assert!(parse_hostname("-host-na.me").is_err());
88 | assert!(parse_hostname("host-na.me-").is_err());
89 | assert!(parse_hostname(".host-na.me").is_err());
90 | assert!(parse_hostname("host-na.me.").is_err());
91 | assert!(parse_hostname("host_name").is_err());
92 | assert!(parse_hostname("host..name").is_err());
93 | }
94 | }
95 |
--------------------------------------------------------------------------------
/keylime/src/https_client.rs:
--------------------------------------------------------------------------------
1 | use anyhow::{Context, Result};
2 | use std::{
3 | fs::{self, File},
4 | io::Read,
5 | time::Duration,
6 | };
7 |
8 | pub struct ClientArgs {
9 | pub ca_certificate: String,
10 | pub certificate: String,
11 | pub key: String,
12 | pub insecure: Option,
13 | pub timeout: u64,
14 | }
15 |
16 | pub fn get_https_client(args: &ClientArgs) -> Result {
17 | let mut builder = reqwest::Client::builder()
18 | .connection_verbose(true)
19 | .timeout(Duration::from_millis(args.timeout));
20 |
21 | if args.insecure.unwrap_or(false) {
22 | builder = builder.danger_accept_invalid_certs(true);
23 | } else {
24 | // Get CA certificate from file
25 | let mut buf = Vec::new();
26 | File::open(args.ca_certificate.clone())
27 | .context(format!(
28 | "Failed to open '{}' file",
29 | args.ca_certificate
30 | ))?
31 | .read_to_end(&mut buf)
32 | .context(format!(
33 | "Failed to read '{}' to the end",
34 | args.ca_certificate
35 | ))?;
36 | let ca_cert =
37 | reqwest::Certificate::from_pem(&buf).context(format!(
38 | "Failed to parse certificate from PEM file '{}'",
39 | args.ca_certificate
40 | ))?;
41 |
42 | // Get client key and certificate from files
43 | let cert = fs::read(args.certificate.clone()).context(format!(
44 | "Failed to read client certificate from file '{}'",
45 | args.certificate
46 | ))?;
47 | let key = fs::read(args.key.clone()).context(format!(
48 | "Failed to read key from file '{}'",
49 | args.key
50 | ))?;
51 | let identity = reqwest::Identity::from_pkcs8_pem(&cert, &key)
52 | .context(format!(
53 | "Failed to add client identity from certificate '{}' and key '{}'",
54 | args.certificate,
55 | args.key
56 | ))?;
57 |
58 | builder = builder
59 | .add_root_certificate(ca_cert)
60 | .identity(identity)
61 | // Here we disable hostname verification because the certificates generated by the
62 | // verifier don't have the Subject Alternative Name (SAN) properly set, failing
63 | // hostname verification
64 | .danger_accept_invalid_hostnames(true)
65 | }
66 | builder.build().context("Failed to create HTTPS client")
67 | }
68 |
--------------------------------------------------------------------------------
/keylime/src/ima/measurement_list.rs:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: Apache-2.0
2 | // Copyright 2021 Keylime Authors
3 |
4 | use std::{
5 | collections::HashSet,
6 | fs::File,
7 | io::{prelude::*, Error, SeekFrom},
8 | };
9 |
10 | /// MeasurementList models the IMA measurement lists's last two known
11 | /// numbers of entries in the log and filesizes at that point
12 | #[derive(Debug)]
13 | pub struct MeasurementList {
14 | entries: HashSet<(u64, u64)>,
15 | }
16 |
17 | impl MeasurementList {
18 | pub fn new() -> Self {
19 | Self {
20 | entries: HashSet::new(),
21 | }
22 | }
23 |
24 | pub fn reset(&mut self) {
25 | self.entries = HashSet::new();
26 | }
27 |
28 | fn update(&mut self, num_entries: u64, filesize: u64) -> Option {
29 | if self.entries.len() > 32 {
30 | let e = *self.entries.iter().next()?;
31 | let _ = self.entries.remove(&e);
32 | }
33 | Some(self.entries.insert((num_entries, filesize)))
34 | }
35 |
36 | fn find(&self, nth_entry: u64) -> (u64, u64) {
37 | self.entries.iter().fold((0, 0), |best, entry| {
38 | if entry.0 > best.0 && entry.0 < nth_entry {
39 | *entry
40 | } else {
41 | best
42 | }
43 | })
44 | }
45 |
46 | /// Read the IMA measurement list starting from a given entry.
47 | /// The entry may be of any value 0 <= entry <= entries_in_log where
48 | /// entries_in_log + 1 indicates that the client wants to read the next entry
49 | /// once available. If the entry is outside this range, the function will
50 | /// automatically read from the 0-th entry.
51 | /// This function returns the measurement list and the entry from where it
52 | /// was read and the current number of entries in the file.
53 | pub fn read(
54 | &mut self,
55 | ima_file: &mut File,
56 | nth_entry: u64,
57 | ) -> Result<(String, u64, u64), Error> {
58 | // Try to find the closest entry to the nth_entry
59 | let (mut num_entries, filesize) = self.find(nth_entry);
60 |
61 | let mut ml = None;
62 | let mut filedata = String::new();
63 | let _ = ima_file.seek(SeekFrom::Start(filesize))?;
64 | let _ = ima_file.read_to_string(&mut filedata)?;
65 | let mut offset: usize = 0;
66 |
67 | loop {
68 | if nth_entry == num_entries {
69 | ml = Some(&filedata[offset..]);
70 | }
71 | let s = &filedata[offset..];
72 | let idx = match s.find('\n') {
73 | None => break,
74 | Some(i) => i,
75 | };
76 | offset = offset + idx + 1;
77 | num_entries += 1;
78 | }
79 |
80 | let _ = self.update(num_entries, filesize + offset as u64);
81 |
82 | match ml {
83 | None => self.read(ima_file, 0),
84 | Some(slice) => Ok((String::from(slice), nth_entry, num_entries)),
85 | }
86 | }
87 | }
88 |
89 | impl Default for MeasurementList {
90 | fn default() -> Self {
91 | Self::new()
92 | }
93 | }
94 |
95 | #[cfg(test)]
96 | mod tests {
97 | use super::*;
98 | use tempfile::NamedTempFile;
99 |
100 | #[test]
101 | fn read_measurement_list_test() {
102 | let mut ima_ml = MeasurementList::new();
103 |
104 | let filedata = "0-entry\n1-entry\n2-entry\n";
105 | let mut tf = NamedTempFile::new().unwrap(); //#[allow_ci]
106 | tf.write_all(filedata.as_bytes()).unwrap(); //#[allow_ci]
107 | tf.flush().unwrap(); //#[allow_ci]
108 |
109 | let mut ima_file = File::open(tf.path()).unwrap(); //#[allow_ci]
110 |
111 | // Request the 2nd entry, which is available
112 | let (ml, nth_entry, num_entries) =
113 | ima_ml.read(&mut ima_file, 2).unwrap(); //#[allow_ci]
114 | assert_eq!(num_entries, 3);
115 | assert_eq!(nth_entry, 2);
116 | assert_eq!(ml.find("2-entry").unwrap(), 0); //#[allow_ci]
117 |
118 | // Request the 3rd entry, which is not available yet, thus we get an empty list
119 | let (ml, nth_entry, num_entries) =
120 | ima_ml.read(&mut ima_file, 3).unwrap(); //#[allow_ci]
121 | assert_eq!(num_entries, 3);
122 | assert_eq!(nth_entry, 3);
123 | assert_eq!(ml.len(), 0); //#[allow_ci]
124 |
125 | // Request the 4th entry, which is beyond the next entry; since this is wrong,
126 | // we expect the entire list now.
127 | let (ml, nth_entry, num_entries) =
128 | ima_ml.read(&mut ima_file, 4).unwrap(); //#[allow_ci]
129 | assert_eq!(num_entries, 3);
130 | assert_eq!(nth_entry, 0);
131 | assert_eq!(ml.find("0-entry").unwrap(), 0); //#[allow_ci]
132 | }
133 | }
134 |
--------------------------------------------------------------------------------
/keylime/src/ima/mod.rs:
--------------------------------------------------------------------------------
1 | mod entry;
2 | mod measurement_list;
3 |
4 | pub use entry::*;
5 | pub use measurement_list::*;
6 |
--------------------------------------------------------------------------------
/keylime/src/ip.pest:
--------------------------------------------------------------------------------
1 | WHITESPACE = _{ " " | NEWLINE | "\t" }
2 |
3 | ipv4 = { (ASCII_DIGIT+ ~ ".") ~ (ASCII_DIGIT+ ~ ".") ~ (ASCII_DIGIT+ ~ ".") ~ (ASCII_DIGIT)+ }
4 |
5 | ipv6 = { (ASCII_HEX_DIGIT* ~ ":")? ~ (ASCII_HEX_DIGIT+ ~ ":")* ~
6 | (ASCII_HEX_DIGIT+)? ~ (":" ~ ASCII_HEX_DIGIT+)* ~ (":" ~ ASCII_HEX_DIGIT*)?}
7 |
8 | unbracketed = { ipv4 | ipv6 }
9 | bracketed = { "[" ~ unbracketed ~ "]" }
10 |
11 | ip = { SOI ~ (bracketed | unbracketed) ~ EOI }
12 |
--------------------------------------------------------------------------------
/keylime/src/ip_parser.rs:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: Apache-2.0
2 | // Copyright 2022 Keylime Authors
3 |
4 | use pest::{iterators::Pair, Parser};
5 | use pest_derive::Parser;
6 | use std::net::{AddrParseError, Ipv4Addr, Ipv6Addr};
7 | use thiserror::Error;
8 |
9 | #[derive(Parser)]
10 | #[grammar = "ip.pest"]
11 | pub struct IpParser;
12 |
13 | #[derive(Error, Debug)]
14 | pub enum IpParsingError {
15 | #[error("Invalid input {0}")]
16 | InvalidInput(String),
17 |
18 | #[error("Invalid IP")]
19 | InvalidIP(#[from] AddrParseError),
20 |
21 | #[error("failed to parse the input {input}")]
22 | ParseError {
23 | input: String,
24 | source: Box>,
25 | },
26 |
27 | #[error("Unexpected end of input")]
28 | UnexpectedEOI,
29 | }
30 |
31 | fn get_inner_ip(pair: Pair) -> Result<&str, IpParsingError> {
32 | let Some(item) = pair.into_inner().next() else {
33 | unreachable!()
34 | };
35 |
36 | match item.as_rule() {
37 | Rule::ip | Rule::bracketed | Rule::unbracketed => get_inner_ip(item),
38 | Rule::ipv4 => {
39 | // Validate the IP using the standard parser
40 | let _parsed_ipv4 = item.as_str().parse::()?;
41 | Ok(item.as_str())
42 | }
43 | Rule::ipv6 => {
44 | // Validate the IP using the standard parser
45 | let _parsed_ipv6 = item.as_str().parse::()?;
46 | Ok(item.as_str())
47 | }
48 | Rule::EOI => Err(IpParsingError::UnexpectedEOI),
49 | _ => {
50 | unreachable!()
51 | }
52 | }
53 | }
54 |
55 | /// Parses an ip address from a string slice removing eventual brackets.
56 | /// This is mostly to remove brackets when using IPv6
57 | ///
58 | /// Both IPv4 and IPv6 are supported:
59 | ///
60 | /// * The IPv4 and IPv6 can be inside square brackets ("[]") or not
61 | ///
62 | /// # Arguments
63 | ///
64 | /// * `ip` the string to be parsed
65 | ///
66 | /// # Returns
67 | ///
68 | /// The obtained ip as a &str without brackets, if they were present
69 | ///
70 | /// # Examples
71 | ///
72 | /// Valid input lists, and respective result:
73 | ///
74 | /// * `127.0.0.1` => `127.0.0.1`
75 | /// * `::1` => `::1`
76 | /// * `[127.0.0.1]` => `127.0.0.1
77 | /// * `[::1]` => `::1`
78 | pub fn parse_ip(ip: &str) -> Result<&str, IpParsingError> {
79 | let Some(pair) = IpParser::parse(Rule::ip, ip)
80 | .map_err(|e| IpParsingError::ParseError {
81 | input: ip.to_string(),
82 | source: Box::new(e),
83 | })?
84 | .next()
85 | else {
86 | return Err(IpParsingError::InvalidInput(ip.to_string()));
87 | };
88 | get_inner_ip(pair)
89 | }
90 |
91 | // Unit Testing
92 | #[cfg(test)]
93 | mod tests {
94 | use super::*;
95 |
96 | #[test]
97 | fn test_parse_ip() {
98 | // Sanity: most common case
99 | assert_eq!(parse_ip("127.0.0.1").unwrap(), "127.0.0.1"); //#[allow_ci]
100 | assert_eq!(parse_ip("[127.0.0.1]").unwrap(), "127.0.0.1"); //#[allow_ci]
101 | assert_eq!(parse_ip("::1").unwrap(), "::1"); //#[allow_ci]
102 | assert_eq!(parse_ip("[::1]").unwrap(), "::1"); //#[allow_ci]
103 |
104 | // More advanced cases
105 | assert_eq!(parse_ip("::").unwrap(), "::"); //#[allow_ci]
106 | assert_eq!(parse_ip("::1").unwrap(), "::1"); //#[allow_ci]
107 | assert_eq!(parse_ip("1::").unwrap(), "1::"); //#[allow_ci]
108 | assert_eq!(parse_ip("1::1").unwrap(), "1::1"); //#[allow_ci]
109 | assert_eq!(parse_ip("[1::1]").unwrap(), "1::1"); //#[allow_ci]
110 | assert_eq!(parse_ip("1::2:3:4").unwrap(), "1::2:3:4"); //#[allow_ci]
111 | assert_eq!(parse_ip("1:2::3:4").unwrap(), "1:2::3:4"); //#[allow_ci]
112 | assert_eq!(parse_ip("1:2:3::4").unwrap(), "1:2:3::4"); //#[allow_ci]
113 | assert_eq!(parse_ip("1:2:3:4:5:6:7:8").unwrap(), "1:2:3:4:5:6:7:8"); //#[allow_ci]
114 |
115 | // Invalid input
116 | assert!(parse_ip("1").is_err());
117 | assert!(parse_ip("1.2").is_err());
118 | assert!(parse_ip("1.2.3.4.5").is_err());
119 | assert!(parse_ip("1:2:3").is_err());
120 | assert!(parse_ip("1::2::3").is_err());
121 | assert!(parse_ip("1:2::3:4::5:6").is_err());
122 | }
123 | }
124 |
--------------------------------------------------------------------------------
/keylime/src/lib.rs:
--------------------------------------------------------------------------------
1 | pub mod agent_data;
2 | pub mod agent_identity;
3 | pub mod agent_registration;
4 | pub mod algorithms;
5 | pub mod cert;
6 | pub mod config;
7 | pub mod context_info;
8 | pub mod crypto;
9 | pub mod device_id;
10 | pub mod file_ops;
11 | pub mod global_config;
12 | pub mod hash_ek;
13 | pub mod hostname_parser;
14 | pub mod https_client;
15 | pub mod ima;
16 | pub mod ip_parser;
17 | pub mod keylime_error;
18 | pub mod list_parser;
19 | pub mod quote;
20 | pub mod registrar_client;
21 | pub mod serialization;
22 | pub mod structures;
23 | pub mod tpm;
24 | pub mod version;
25 |
26 | #[macro_use]
27 | extern crate static_assertions;
28 |
--------------------------------------------------------------------------------
/keylime/src/list.pest:
--------------------------------------------------------------------------------
1 | WHITESPACE = _{ " " | NEWLINE | "\t" }
2 | separator = { "," | NEWLINE | " " | "\t" }
3 |
4 | list = { SOI ~ (bracketed | unbracketed) ~ EOI }
5 |
6 | bracketed = { "[" ~ (bracketed_name)* ~ ("," ~ bracketed_name)* ~ ","? ~ "]" }
7 | unbracketed = { (name)* ~ ("," ~ name)* ~ ","? }
8 |
9 | single_content = @{ (!"'" ~ ANY)* }
10 | single = ${ "'" ~ single_content ~ "'" }
11 |
12 | double_content = @{ (!"\"" ~ ANY)* }
13 | double = ${ "\"" ~ double_content ~ "\"" }
14 |
15 | unquoted = @{ (!separator ~ ANY)+ }
16 |
17 | not_closer = @{ !("]" | separator) ~ ANY }
18 | bracketed_unquoted = @{ (!separator ~ !"]" ~ ANY)+ }
19 |
20 | name = { single | double | unquoted }
21 | bracketed_name = { single | double | bracketed_unquoted }
22 |
--------------------------------------------------------------------------------
/keylime/src/list_parser.rs:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: Apache-2.0
2 | // Copyright 2022 Keylime Authors
3 |
4 | use pest::{iterators::Pair, Parser};
5 | use pest_derive::Parser;
6 | use thiserror::Error;
7 |
8 | #[derive(Parser)]
9 | #[grammar = "list.pest"]
10 | pub struct ListParser;
11 |
12 | #[derive(Error, Debug)]
13 | pub enum ListParsingError {
14 | #[error("failed to parse list")]
15 | ParseError(#[source] Box>),
16 | }
17 |
18 | fn get_inner_str(pair: Pair) -> Vec<&str> {
19 | let mut l = Vec::new();
20 | for item in pair.into_inner() {
21 | match item.as_rule() {
22 | Rule::list
23 | | Rule::bracketed
24 | | Rule::unbracketed
25 | | Rule::name
26 | | Rule::bracketed_name => {
27 | l.extend(get_inner_str(item));
28 | }
29 | Rule::single | Rule::double => {
30 | let s = item.as_str();
31 | let c = get_inner_str(item);
32 | if !c.is_empty() {
33 | l.push(s);
34 | }
35 | }
36 | Rule::single_content
37 | | Rule::double_content
38 | | Rule::unquoted
39 | | Rule::bracketed_unquoted => {
40 | let i = item.as_str();
41 | if !i.is_empty() {
42 | l.push(i);
43 | }
44 | }
45 | Rule::EOI => {
46 | break;
47 | }
48 | _ => {
49 | unreachable!()
50 | }
51 | }
52 | }
53 | l
54 | }
55 |
56 | /// Parses a list from a string slice and return a Vec<&str>
57 | ///
58 | /// The list in the input string:
59 | ///
60 | /// * can contain single-quoted, double-quoted, or unquoted strings
61 | /// * can be inside square brackets ("[]") or not
62 | /// * can have string separated by commas, white spaces, or newlines
63 | ///
64 | /// If the string is single-quoted or double-quoted, the quotes are kept in the
65 | /// output Vec<&str>.
66 | ///
67 | /// # Arguments
68 | ///
69 | /// * `list` the string to be parsed
70 | ///
71 | /// # Returns
72 | ///
73 | /// The obtained list as a Vec<&str>
74 | ///
75 | /// # Examples
76 | ///
77 | /// Valid input lists, and respective result:
78 | ///
79 | /// * `a b c` => `["a", "b", "c"]`
80 | /// * `'a' "b" c` => `["'a'", "\"b\"", "c"]`
81 | /// * `[a b c]` => `["a", "b", "c"]`
82 | /// * `['a', "b", c]` => `["'a'", "\"b\"", "c"]`
83 | ///
84 | pub fn parse_list(list: &str) -> Result, ListParsingError> {
85 | if let Some(pair) = ListParser::parse(Rule::list, list)
86 | .map_err(|e| ListParsingError::ParseError(Box::new(e)))?
87 | .next()
88 | {
89 | return Ok(get_inner_str(pair));
90 | }
91 | Ok(Vec::new())
92 | }
93 |
94 | // Unit Testing
95 | #[cfg(test)]
96 | mod tests {
97 | use super::*;
98 |
99 | #[test]
100 | fn test_parse_list() {
101 | // Sanity: most common case
102 | assert_eq!(
103 | parse_list("[\"aa\", \"bb\", \"cc\"]").unwrap(), //#[allow_ci]
104 | ["\"aa\"", "\"bb\"", "\"cc\""]
105 | );
106 |
107 | // Unquoted
108 | assert_eq!(parse_list("default").unwrap(), ["default"]); //#[allow_ci]
109 |
110 | // Double-quoted
111 | assert_eq!(parse_list("\"[default]\"").unwrap(), ["\"[default]\""]); //#[allow_ci]
112 |
113 | // Single-quoted
114 | assert_eq!(parse_list("'[default]'").unwrap(), ["'[default]'"]); //#[allow_ci]
115 |
116 | // Brackets, no quotes
117 | assert_eq!(parse_list("[default]").unwrap(), ["default"]); //#[allow_ci]
118 | assert_eq!(parse_list("[aa, bb]").unwrap(), ["aa", "bb"]); //#[allow_ci]
119 |
120 | // Brackets, double-quotes
121 | assert_eq!(parse_list("[\"default\"]").unwrap(), ["\"default\""]); //#[allow_ci]
122 | assert_eq!(
123 | parse_list("[\"aa\", \"bb\"]").unwrap(), //#[allow_ci]
124 | ["\"aa\"", "\"bb\""]
125 | );
126 |
127 | // Brackets, single-quotes
128 | assert_eq!(parse_list("['default']").unwrap(), ["'default'"]); //#[allow_ci]
129 | assert_eq!(parse_list("['aa', 'bb']").unwrap(), ["'aa'", "'bb'"]); //#[allow_ci]
130 |
131 | // Comma-separated
132 | assert_eq!(parse_list("aa,bb,cc").unwrap(), ["aa", "bb", "cc"]); //#[allow_ci]
133 | assert_eq!(
134 | parse_list("'aa','bb','cc'").unwrap(), //#[allow_ci]
135 | ["'aa'", "'bb'", "'cc'"]
136 | );
137 | assert_eq!(
138 | parse_list("'aa',\"bb\",'cc'").unwrap(), //#[allow_ci]
139 | ["'aa'", "\"bb\"", "'cc'"]
140 | );
141 |
142 | // Spaces-separated
143 | assert_eq!(parse_list("aa bb cc").unwrap(), ["aa", "bb", "cc"]); //#[allow_ci]
144 | assert_eq!(
145 | parse_list("'aa' 'bb' 'cc'").unwrap(), //#[allow_ci]
146 | ["'aa'", "'bb'", "'cc'"]
147 | );
148 | assert_eq!(
149 | parse_list("\"aa\" \"bb\" \"cc\"").unwrap(), //#[allow_ci]
150 | ["\"aa\"", "\"bb\"", "\"cc\""]
151 | );
152 | assert_eq!(
153 | parse_list("aa 'bb' \"cc\"").unwrap(), //#[allow_ci]
154 | ["aa", "'bb'", "\"cc\""]
155 | );
156 |
157 | // New-line-separated
158 | assert_eq!(parse_list("aa\nbb\ncc").unwrap(), ["aa", "bb", "cc"]); //#[allow_ci]
159 |
160 | // Tab-separated
161 | assert_eq!(parse_list("aa\tbb\tcc").unwrap(), ["aa", "bb", "cc"]); //#[allow_ci]
162 |
163 | // Carriage-return-separated
164 | assert_eq!(parse_list("aa\rbb\rcc").unwrap(), ["aa", "bb", "cc"]); //#[allow_ci]
165 |
166 | // Corner cases
167 |
168 | // Entry named "["
169 | assert_eq!(parse_list("\"[\"").unwrap(), ["\"[\""]); //#[allow_ci]
170 | assert_eq!(parse_list("'['").unwrap(), ["'['"]); //#[allow_ci]
171 |
172 | // Entry named "]"
173 | assert_eq!(parse_list("\"]\"").unwrap(), ["\"]\""]); //#[allow_ci]
174 | assert_eq!(parse_list("']'").unwrap(), ["']'"]); //#[allow_ci]
175 | assert_eq!(parse_list("]").unwrap(), ["]"]); //#[allow_ci]
176 |
177 | // Entry named "\'"
178 | assert_eq!(parse_list("\"'\"").unwrap(), ["\"'\""]); //#[allow_ci]
179 | assert_eq!(parse_list("[']").unwrap(), ["'"]); //#[allow_ci]
180 |
181 | // Entry named "\""
182 | assert_eq!(parse_list("'\"'").unwrap(), ["'\"'"]); //#[allow_ci]
183 | assert_eq!(parse_list("[\"]").unwrap(), ["\""]); //#[allow_ci]
184 |
185 | // Entry named ","
186 | assert_eq!(parse_list("','").unwrap(), ["','"]); //#[allow_ci]
187 | assert_eq!(parse_list("\",\"").unwrap(), ["\",\""]); //#[allow_ci]
188 |
189 | // Entry named " "
190 | assert_eq!(parse_list("' '").unwrap(), ["' '"]); //#[allow_ci]
191 | assert_eq!(parse_list("\" \"").unwrap(), ["\" \""]); //#[allow_ci]
192 |
193 | // Empty lists
194 | assert_eq!(parse_list("[]").unwrap(), [] as [&str; 0]); //#[allow_ci]
195 | assert_eq!(parse_list("").unwrap(), [] as [&str; 0]); //#[allow_ci]
196 | assert_eq!(parse_list("\"\"").unwrap(), [] as [&str; 0]); //#[allow_ci]
197 | assert_eq!(parse_list("'', ''").unwrap(), [] as [&str; 0]); //#[allow_ci]
198 | assert_eq!(parse_list(" \n \t \r").unwrap(), [] as [&str; 0]); //#[allow_ci]
199 | assert_eq!(parse_list("[ \n \t \r]").unwrap(), [] as [&str; 0]); //#[allow_ci]
200 |
201 | // Uncommon cases
202 | assert_eq!(parse_list("[a,").unwrap(), ["[a"]); //#[allow_ci]
203 | assert_eq!(parse_list("[a b").unwrap(), ["[a", "b"]); //#[allow_ci]
204 | assert_eq!(parse_list("'[ ]").unwrap(), ["'[", "]"]); //#[allow_ci]
205 |
206 | // Error cases
207 | assert!(parse_list(",,").is_err());
208 | }
209 | }
210 |
--------------------------------------------------------------------------------
/keylime/src/quote.rs:
--------------------------------------------------------------------------------
1 | use serde::{Deserialize, Serialize};
2 |
3 | #[derive(Deserialize)]
4 | pub struct Integ {
5 | pub nonce: String,
6 | pub mask: String,
7 | pub partial: String,
8 | pub ima_ml_entry: Option,
9 | }
10 |
11 | #[derive(Serialize, Deserialize, Debug, Default)]
12 | pub struct KeylimeQuote {
13 | pub quote: String, // 'r' + quote + sig + pcrblob
14 | pub hash_alg: String,
15 | pub enc_alg: String,
16 | pub sign_alg: String,
17 | #[serde(skip_serializing_if = "Option::is_none")]
18 | pub pubkey: Option,
19 | #[serde(skip_serializing_if = "Option::is_none")]
20 | pub ima_measurement_list: Option,
21 | #[serde(skip_serializing_if = "Option::is_none")]
22 | pub mb_measurement_list: Option,
23 | #[serde(skip_serializing_if = "Option::is_none")]
24 | pub ima_measurement_list_entry: Option,
25 | }
26 |
27 | #[cfg(test)]
28 | mod tests {
29 | use super::*;
30 |
31 | #[test]
32 | fn test_keylime_quote_serialization() {
33 | let quote = KeylimeQuote {
34 | quote: "example_quote".to_string(),
35 | hash_alg: "SHA256".to_string(),
36 | enc_alg: "AES".to_string(),
37 | sign_alg: "RSASSA-PSS".to_string(),
38 | pubkey: Some("example_pubkey".to_string()),
39 | ima_measurement_list: Some("example_ima_ml".to_string()),
40 | mb_measurement_list: None,
41 | ima_measurement_list_entry: Some(12345),
42 | };
43 |
44 | let serialized = serde_json::to_string("e).unwrap(); //#[allow_ci]
45 | assert!(serialized.contains("example_quote"));
46 | assert!(serialized.contains("SHA256"));
47 | assert!(serialized.contains("AES"));
48 | assert!(serialized.contains("RSASSA-PSS"));
49 | assert!(serialized.contains("example_pubkey"));
50 | assert!(serialized.contains("example_ima_ml"));
51 | assert!(serialized.contains("12345"));
52 |
53 | let pretty_serialized = serde_json::to_string_pretty("e).unwrap(); //#[allow_ci]
54 | assert_eq!(
55 | pretty_serialized,
56 | r#"{
57 | "quote": "example_quote",
58 | "hash_alg": "SHA256",
59 | "enc_alg": "AES",
60 | "sign_alg": "RSASSA-PSS",
61 | "pubkey": "example_pubkey",
62 | "ima_measurement_list": "example_ima_ml",
63 | "ima_measurement_list_entry": 12345
64 | }"#
65 | );
66 | }
67 | }
68 |
--------------------------------------------------------------------------------
/keylime/src/serialization.rs:
--------------------------------------------------------------------------------
1 | use base64::{engine::general_purpose, Engine as _};
2 | use serde::Deserialize;
3 |
4 | #[derive(Debug, Deserialize)]
5 | struct WrappedBase64Encoded(
6 | #[serde(deserialize_with = "deserialize_as_base64")] Vec,
7 | );
8 |
9 | pub fn is_empty(buf: &[u8]) -> bool {
10 | buf.is_empty()
11 | }
12 |
13 | pub fn serialize_as_base64(
14 | bytes: &[u8],
15 | serializer: S,
16 | ) -> Result
17 | where
18 | S: serde::Serializer,
19 | {
20 | serializer.serialize_str(&general_purpose::STANDARD.encode(bytes))
21 | }
22 |
23 | pub fn deserialize_as_base64<'de, D>(
24 | deserializer: D,
25 | ) -> Result, D::Error>
26 | where
27 | D: serde::Deserializer<'de>,
28 | {
29 | String::deserialize(deserializer).and_then(|string| {
30 | general_purpose::STANDARD
31 | .decode(string)
32 | .map_err(serde::de::Error::custom)
33 | })
34 | }
35 |
36 | pub fn serialize_maybe_base64(
37 | value: &Option>,
38 | serializer: S,
39 | ) -> Result
40 | where
41 | S: serde::Serializer,
42 | {
43 | match *value {
44 | Some(ref value) => {
45 | serializer.serialize_str(&general_purpose::STANDARD.encode(value))
46 | }
47 | None => serializer.serialize_none(),
48 | }
49 | }
50 |
51 | pub fn serialize_option_base64(
52 | value: &Option<&[u8]>,
53 | serializer: S,
54 | ) -> Result
55 | where
56 | S: serde::Serializer,
57 | {
58 | match *value {
59 | Some(value) => {
60 | serializer.serialize_str(&general_purpose::STANDARD.encode(value))
61 | }
62 | None => serializer.serialize_none(),
63 | }
64 | }
65 |
66 | pub fn deserialize_maybe_base64<'de, D>(
67 | deserializer: D,
68 | ) -> Result