From 69ea50251c121ab3a67084b55ee677104e50d747 Mon Sep 17 00:00:00 2001 From: Daniel Trick Date: Thu, 5 Sep 2024 13:26:40 +0200 Subject: [PATCH] Initial commit. --- .github/workflows/cargo-ci-pipeline.yaml | 278 ++++ .github/workflows/on-published.yaml | 11 + .github/workflows/on-push-pull_request.yaml | 10 + .github/workflows/on-schedule.yaml | 9 + .gitignore | 4 + .gitlab-ci.yml | 37 + CHANGELOG.md | 10 + Cargo.lock | 1154 +++++++++++++++ Cargo.toml | 34 + LICENSE | 27 + MAINTAINERS.md | 3 + Makefile | 14 + README.md | 213 +++ build.rs | 138 ++ docs/crates-README.md | 20 + docs/html/redir.html | 11 + docs/images/tss2-fapi-rs-overview.png | Bin 0 -> 30485 bytes docs/images/tss2-fapi-rs-overview.small.png | Bin 0 -> 36387 bytes examples/1_get_random.rs | 69 + examples/2_sign_and_verify.rs | 103 ++ examples/3_auth_callback.rs | 98 ++ examples/data/.gitignore | 2 + examples/data/fapi-config.json | 10 + examples/data/keystore/system/.gitkeep | 0 examples/data/keystore/user/.gitkeep | 0 examples/data/logs/.gitkeep | 0 examples/data/profiles/P_ECCP256SHA256.json | 47 + src/algorithm_id.rs | 46 + src/callback.rs | 362 +++++ src/context.rs | 1413 +++++++++++++++++++ src/error.rs | 452 ++++++ src/fapi_sys/constants.rs | 222 +++ src/fapi_sys/mod.rs | 14 + src/flags.rs | 304 ++++ src/lib.rs | 368 +++++ src/marshal.rs | 26 + src/memory.rs | 355 +++++ src/version.rs | 53 + tests/01_version_test.rs | 45 + tests/02_get_info_test.rs | 127 ++ tests/03_provision_test.rs | 125 ++ tests/04_get_random_test.rs | 50 + tests/05_key_test.rs | 317 +++++ tests/06_encrypt_decrypt_test.rs | 126 ++ tests/07_signature_test.rs | 237 ++++ tests/08_nv_test.rs | 342 +++++ tests/09_policy_test.rs | 409 ++++++ tests/10_sealed_object_test.rs | 116 ++ tests/11_prc_test.rs | 461 ++++++ tests/12_duplicate_test.rs | 155 ++ tests/13_authorize_policy_test.rs | 390 +++++ tests/14_certificate_test.rs | 292 ++++ tests/15_description_test.rs | 182 +++ tests/16_app_data_test.rs | 188 +++ tests/17_change_auth_test.rs | 100 ++ tests/18_get_tcti_test.rs | 53 + tests/19_multithread_test.rs | 155 ++ tests/common/crypto.rs | 172 +++ tests/common/mod.rs | 12 + tests/common/param.rs | 7 + tests/common/random.rs | 47 + tests/common/setup.rs | 227 +++ tests/common/tempfile.rs | 45 + tests/common/utils.rs | 126 ++ tests/data/certificates/x509-cert.pem | 30 + tests/data/fapi-config.json.template | 10 + tests/data/keys/ca_key_ecc.pem | 14 + tests/data/keys/ca_key_rsa.pem | 80 ++ tests/data/keys/key_ecc_256.pem | 5 + tests/data/keys/key_rsa_2048.pem | 28 + tests/data/policies/pol_action.json | 9 + tests/data/policies/pol_duplicate.json | 9 + tests/data/policies/pol_or.json | 33 + tests/data/policies/pol_signed_ecc.json | 11 + tests/data/policies/pol_signed_rsa.json | 11 + tests/data/profiles/P_ECCP256SHA256.json | 47 + tests/data/profiles/P_RSA2048SHA256.json | 51 + tools/codecov/code-coverage.sh | 59 + tools/docker/build/Makefile | 16 + tools/docker/build/build.Dockerfile | 9 + tools/docker/build/docker-compose.yml | 17 + tools/docker/swtpm/Makefile | 14 + tools/docker/swtpm/docker-compose.yml | 19 + tools/docker/swtpm/swtpm.Dockerfile | 11 + tools/docker/tests/Makefile | 16 + tools/docker/tests/bin/PTM_SHUTDOWN | Bin 0 -> 4 bytes tools/docker/tests/bin/test-runner.sh | 37 + tools/docker/tests/docker-compose.yml | 56 + tools/docker/tests/tests.Dockerfile | 8 + tools/libtpms/test-runner.sh | 22 + 90 files changed, 11045 insertions(+) create mode 100644 .github/workflows/cargo-ci-pipeline.yaml create mode 100644 .github/workflows/on-published.yaml create mode 100644 .github/workflows/on-push-pull_request.yaml create mode 100644 .github/workflows/on-schedule.yaml create mode 100644 .gitignore create mode 100644 .gitlab-ci.yml create mode 100644 CHANGELOG.md create mode 100644 Cargo.lock create mode 100644 Cargo.toml create mode 100644 LICENSE create mode 100644 MAINTAINERS.md create mode 100644 Makefile create mode 100644 README.md create mode 100644 build.rs create mode 100644 docs/crates-README.md create mode 100644 docs/html/redir.html create mode 100644 docs/images/tss2-fapi-rs-overview.png create mode 100644 docs/images/tss2-fapi-rs-overview.small.png create mode 100644 examples/1_get_random.rs create mode 100644 examples/2_sign_and_verify.rs create mode 100644 examples/3_auth_callback.rs create mode 100644 examples/data/.gitignore create mode 100644 examples/data/fapi-config.json create mode 100644 examples/data/keystore/system/.gitkeep create mode 100644 examples/data/keystore/user/.gitkeep create mode 100644 examples/data/logs/.gitkeep create mode 100644 examples/data/profiles/P_ECCP256SHA256.json create mode 100644 src/algorithm_id.rs create mode 100644 src/callback.rs create mode 100644 src/context.rs create mode 100644 src/error.rs create mode 100644 src/fapi_sys/constants.rs create mode 100644 src/fapi_sys/mod.rs create mode 100644 src/flags.rs create mode 100644 src/lib.rs create mode 100644 src/marshal.rs create mode 100644 src/memory.rs create mode 100644 src/version.rs create mode 100644 tests/01_version_test.rs create mode 100644 tests/02_get_info_test.rs create mode 100644 tests/03_provision_test.rs create mode 100644 tests/04_get_random_test.rs create mode 100644 tests/05_key_test.rs create mode 100644 tests/06_encrypt_decrypt_test.rs create mode 100644 tests/07_signature_test.rs create mode 100644 tests/08_nv_test.rs create mode 100644 tests/09_policy_test.rs create mode 100644 tests/10_sealed_object_test.rs create mode 100644 tests/11_prc_test.rs create mode 100644 tests/12_duplicate_test.rs create mode 100644 tests/13_authorize_policy_test.rs create mode 100644 tests/14_certificate_test.rs create mode 100644 tests/15_description_test.rs create mode 100644 tests/16_app_data_test.rs create mode 100644 tests/17_change_auth_test.rs create mode 100644 tests/18_get_tcti_test.rs create mode 100644 tests/19_multithread_test.rs create mode 100644 tests/common/crypto.rs create mode 100644 tests/common/mod.rs create mode 100644 tests/common/param.rs create mode 100644 tests/common/random.rs create mode 100644 tests/common/setup.rs create mode 100644 tests/common/tempfile.rs create mode 100644 tests/common/utils.rs create mode 100644 tests/data/certificates/x509-cert.pem create mode 100644 tests/data/fapi-config.json.template create mode 100644 tests/data/keys/ca_key_ecc.pem create mode 100644 tests/data/keys/ca_key_rsa.pem create mode 100644 tests/data/keys/key_ecc_256.pem create mode 100644 tests/data/keys/key_rsa_2048.pem create mode 100644 tests/data/policies/pol_action.json create mode 100644 tests/data/policies/pol_duplicate.json create mode 100644 tests/data/policies/pol_or.json create mode 100644 tests/data/policies/pol_signed_ecc.json create mode 100644 tests/data/policies/pol_signed_rsa.json create mode 100644 tests/data/profiles/P_ECCP256SHA256.json create mode 100644 tests/data/profiles/P_RSA2048SHA256.json create mode 100755 tools/codecov/code-coverage.sh create mode 100644 tools/docker/build/Makefile create mode 100644 tools/docker/build/build.Dockerfile create mode 100644 tools/docker/build/docker-compose.yml create mode 100644 tools/docker/swtpm/Makefile create mode 100644 tools/docker/swtpm/docker-compose.yml create mode 100644 tools/docker/swtpm/swtpm.Dockerfile create mode 100644 tools/docker/tests/Makefile create mode 100644 tools/docker/tests/bin/PTM_SHUTDOWN create mode 100755 tools/docker/tests/bin/test-runner.sh create mode 100644 tools/docker/tests/docker-compose.yml create mode 100644 tools/docker/tests/tests.Dockerfile create mode 100755 tools/libtpms/test-runner.sh diff --git a/.github/workflows/cargo-ci-pipeline.yaml b/.github/workflows/cargo-ci-pipeline.yaml new file mode 100644 index 0000000..fb623d7 --- /dev/null +++ b/.github/workflows/cargo-ci-pipeline.yaml @@ -0,0 +1,278 @@ +name: "Cargo CI/CD pipeline" +on: + workflow_call: + inputs: + MAKE_RELEASE: + required: false + type: string + default: "false" + TOOLCHAIN_MIN: + required: false + type: string + default: "1.79.0" + TOOLCHAIN_MAX: + required: false + type: string + default: "1.80.1" + OS_VERS_MIN: + required: false + type: string + default: "22.04" + OS_VERS_MAX: + required: false + type: string + default: "24.04" + +jobs: + # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + # Code formatting + # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + format: + name: cargo fmt + runs-on: ubuntu-${{ inputs.OS_VERS_MAX }} + steps: + - uses: actions/checkout@v4 + - uses: actions-rust-lang/setup-rust-toolchain@v1 + with: + toolchain: ${{ inputs.TOOLCHAIN_MAX }} + rustflags: "-D warnings" + components: rustfmt + - uses: actions-rust-lang/rustfmt@v1 + + # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + # Code checks + # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + checks: + name: cargo check + strategy: + matrix: + platform: ["${{ inputs.OS_VERS_MIN }}", "${{ inputs.OS_VERS_MAX }}"] + rust: ["${{ inputs.TOOLCHAIN_MIN }}", "${{ inputs.TOOLCHAIN_MAX }}"] + runs-on: ubuntu-${{ matrix.platform }} + steps: + - uses: actions/checkout@v4 + - uses: actions-rust-lang/setup-rust-toolchain@v1 + with: + toolchain: ${{ matrix.rust }} + rustflags: "-D warnings" + - uses: awalsh128/cache-apt-pkgs-action@v1 + with: + packages: libtss2-dev uuid-dev libjson-c-dev libcurl4-openssl-dev + - run: cargo check + + # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + # Integeration tests + # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + tests: + name: cargo test + needs: [format, checks] + strategy: + matrix: + platform: ["${{ inputs.OS_VERS_MIN }}", "${{ inputs.OS_VERS_MAX }}"] + rust: ["${{ inputs.TOOLCHAIN_MIN }}", "${{ inputs.TOOLCHAIN_MAX }}"] + profile: [RSA2048SHA256, ECCP256SHA256] + runs-on: ubuntu-${{ matrix.platform }} + steps: + - uses: actions/checkout@v4 + - uses: actions-rust-lang/setup-rust-toolchain@v1 + with: + toolchain: ${{ matrix.rust }} + rustflags: "-D warnings" + - uses: awalsh128/cache-apt-pkgs-action@v1 + with: + packages: libtss2-dev uuid-dev libjson-c-dev libcurl4-openssl-dev + - run: | + make -C tools/docker/swtpm DETACH=1 + RUST_LOG=info TSS2_LOG=all+none FAPI_RS_TEST_PROF=${{ matrix.profile }} cargo test --tests + + # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + # Code coverage analysis (codecov) + # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + codecov: + name: codecov + needs: [format, checks] + runs-on: ubuntu-${{ inputs.OS_VERS_MAX }} + steps: + - uses: actions/checkout@v4 + - uses: actions-rust-lang/setup-rust-toolchain@v1 + with: + toolchain: ${{ inputs.TOOLCHAIN_MAX }} + rustflags: "-D warnings" + components: llvm-tools-preview + - uses: taiki-e/install-action@cargo-llvm-cov + - uses: awalsh128/cache-apt-pkgs-action@v1 + with: + packages: libtss2-dev uuid-dev libjson-c-dev libcurl4-openssl-dev + - run: | + make -C tools/docker/swtpm DETACH=1 + TSS2_LOG=all+none ./tools/codecov/code-coverage.sh ccov target/fapi-rs-codecov-output.json + - uses: actions/upload-artifact@v4 + with: + name: code-coverage-analysis + path: target/fapi-rs-codecov-output.json + - uses: codecov/codecov-action@v4 + with: + files: target/fapi-rs-codecov-output.json + verbose: true + token: ${{ secrets.CODECOV_TOKEN }} + + # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + # Build binaries + # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + build: + name: cargo build + needs: tests + strategy: + fail-fast: false + matrix: + platform: ["${{ inputs.OS_VERS_MIN }}", "${{ inputs.OS_VERS_MAX }}"] + rust: ["${{ inputs.TOOLCHAIN_MIN }}", "${{ inputs.TOOLCHAIN_MAX }}"] + runs-on: ubuntu-${{ matrix.platform }} + steps: + - uses: actions/checkout@v4 + - uses: actions-rust-lang/setup-rust-toolchain@v1 + with: + toolchain: ${{ matrix.rust }} + rustflags: "-D warnings" + - uses: awalsh128/cache-apt-pkgs-action@v1 + with: + packages: libtss2-dev uuid-dev libjson-c-dev libcurl4-openssl-dev + - run: cargo build --release + - run: cp -vf README.md LICENSE target/release/ + - run: | + printf "%s\n%s\n%s\n%s\n%s\n" "${GITHUB_REF} @ ${GITHUB_SHA:0:12}" \ + "$(date -R)" "$(lsb_release -s -d)" "$(rustc --version)" \ + "$(pkg-config --print-provides tss2-fapi)" > target/release/BUILD_INFO + - uses: actions/upload-artifact@v4 + with: + name: tss2_fapi_rs.ubuntu-${{ matrix.platform }}.rust-${{ matrix.rust }} + path: | + target/release/libtss2_fapi_rs.rlib + target/release/README.md + target/release/LICENSE + target/release/BUILD_INFO + + # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + # Generate documentation + # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + docs: + name: cargo doc + needs: tests + runs-on: ubuntu-${{ inputs.OS_VERS_MAX }} + steps: + - uses: actions/checkout@v4 + - uses: actions-rust-lang/setup-rust-toolchain@v1 + with: + toolchain: ${{ inputs.TOOLCHAIN_MAX }} + rustflags: "-D warnings" + - uses: awalsh128/cache-apt-pkgs-action@v1 + with: + packages: libtss2-dev uuid-dev libjson-c-dev libcurl4-openssl-dev + - run: cargo doc --no-deps + - run: | + cp -f docs/html/redir.html target/doc/index.html + rm -f target/doc/.lock + - uses: actions/upload-pages-artifact@v3 + with: + path: target/doc/ + + # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + # Assemble package into a distributable tarball + # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + package: + name: cargo package + needs: tests + runs-on: ubuntu-${{ inputs.OS_VERS_MAX }} + steps: + - uses: actions/checkout@v4 + - uses: actions-rust-lang/setup-rust-toolchain@v1 + with: + toolchain: ${{ inputs.TOOLCHAIN_MAX }} + rustflags: "-D warnings" + - uses: awalsh128/cache-apt-pkgs-action@v1 + with: + packages: libtss2-dev uuid-dev libjson-c-dev libcurl4-openssl-dev + - run: cargo package + - uses: actions/upload-artifact@v4 + with: + name: tss2_fapi_rs.package + path: target/package/**/*.crate + + # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + # Release build artifacts on GitHub + # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + release-bin: + name: release build + if: ${{ inputs.MAKE_RELEASE == 'true' }} + needs: build + strategy: + fail-fast: false + matrix: + platform: ["${{ inputs.OS_VERS_MIN }}", "${{ inputs.OS_VERS_MAX }}"] + rust: ["${{ inputs.TOOLCHAIN_MIN }}", "${{ inputs.TOOLCHAIN_MAX }}"] + runs-on: ubuntu-latest + steps: + - uses: actions/download-artifact@v4 + with: + name: tss2_fapi_rs.ubuntu-${{ matrix.platform }}.rust-${{ matrix.rust }} + path: artifacts/files + - run: tar -czvf ../tss2-fapi-rs-${{ github.ref_name }}-ubuntu-${{ matrix.platform }}-rust-${{ matrix.rust }}.tar.gz * + working-directory: artifacts/files + - uses: Roang-zero1/github-upload-release-artifacts-action@v2 + with: + args: artifacts/ + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + release-pkg: + name: release package + if: ${{ inputs.MAKE_RELEASE == 'true' }} + needs: package + runs-on: ubuntu-latest + steps: + - uses: actions/download-artifact@v4 + with: + name: tss2_fapi_rs.package + path: artifacts + - uses: Roang-zero1/github-upload-release-artifacts-action@v2 + with: + args: artifacts/ + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + # Publish new release to Creates.io + # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + publish: + name: cargo publish + if: ${{ inputs.MAKE_RELEASE == 'true' }} + needs: build + runs-on: ubuntu-${{ inputs.OS_VERS_MAX }} + steps: + - uses: actions/checkout@v4 + - uses: actions-rust-lang/setup-rust-toolchain@v1 + - uses: awalsh128/cache-apt-pkgs-action@v1 + with: + packages: libtss2-dev uuid-dev libjson-c-dev libcurl4-openssl-dev + - run: cargo publish + env: + CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }} + + # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + # TODO: Re-enabled, once GitHub pages have been activated! + # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + # pages: + # name: deploy pages + # needs: docs + # if: github.ref == 'refs/heads/main' + # runs-on: ubuntu-${{ inputs.OS_VERS_MAX }} + # permissions: + # pages: write + # id-token: write + # environment: + # name: github-pages + # url: ${{ steps.deployment.outputs.page_url }} + # steps: + # - name: Deploy to GitHub Pages + # id: deployment + # uses: actions/deploy-pages@v4 + # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/.github/workflows/on-published.yaml b/.github/workflows/on-published.yaml new file mode 100644 index 0000000..a837353 --- /dev/null +++ b/.github/workflows/on-published.yaml @@ -0,0 +1,11 @@ +name: "Release" +on: + release: + types: [published] + +jobs: + cargo-ci-pipeline: + name: CI + uses: ./.github/workflows/cargo-ci-pipeline.yaml + with: + MAKE_RELEASE: "true" diff --git a/.github/workflows/on-push-pull_request.yaml b/.github/workflows/on-push-pull_request.yaml new file mode 100644 index 0000000..497feca --- /dev/null +++ b/.github/workflows/on-push-pull_request.yaml @@ -0,0 +1,10 @@ +name: "Check" +on: + push: + branches: ['**'] + pull_request: + +jobs: + cargo-ci-pipeline: + name: CI + uses: ./.github/workflows/cargo-ci-pipeline.yaml diff --git a/.github/workflows/on-schedule.yaml b/.github/workflows/on-schedule.yaml new file mode 100644 index 0000000..2cca87b --- /dev/null +++ b/.github/workflows/on-schedule.yaml @@ -0,0 +1,9 @@ +name: "Nightly" +on: + schedule: + - cron: "19 1 * * *" + +jobs: + cargo-ci-pipeline: + name: CI + uses: ./.github/workflows/cargo-ci-pipeline.yaml diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..213c5f6 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +/**/log +/**/out +/**/target/ +/**/tmp diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml new file mode 100644 index 0000000..f483ea0 --- /dev/null +++ b/.gitlab-ci.yml @@ -0,0 +1,37 @@ +stages: + - check + - build + +cargo check: + stage: check + tags: + - linux-docker-x64 + image: rust:latest + script: + - apt-get update && apt-get install -y libclang-dev libtss2-dev + - cargo check + +cargo doc: + stage: build + tags: + - linux-docker-x64 + image: rust:latest + script: + - apt-get update && apt-get install -y libclang-dev libtss2-dev + - cargo doc --no-deps + - cp -f docs/html/redir.html target/doc/index.html + artifacts: + paths: + - "target/doc" + +cargo build: + stage: build + tags: + - linux-docker-x64 + image: rust:latest + script: + - apt-get update && apt-get install -y libclang-dev libtss2-dev + - cargo build --release + artifacts: + paths: + - "target/release/*.rlib" diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..6b71bfd --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,10 @@ +# Change Log + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](http://keepachangelog.com/). + +## [1.0.0] - 2024-??-?? + +### Added +- This is the first public release of this project. diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..0e2c5fb --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,1154 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "aho-corasick" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" +dependencies = [ + "memchr", +] + +[[package]] +name = "anstream" +version = "0.6.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64e15c1ab1f89faffbf04a634d5e1962e9074f2741eef6d97f3c4e322426d526" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bec1de6f59aedf83baf9ff929c98f2ad654b97c9510f4e70cf6f661d49fd5b1" + +[[package]] +name = "anstyle-parse" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb47de1e80c2b463c735db5b217a0ddc39d612e7ac9e2e96a5aed1f57616c1cb" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d36fc52c7f6c869915e99412912f22093507da8d9e942ceaf66fe4b7c14422a" +dependencies = [ + "windows-sys", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5bf74e1b6e971609db8ca7a9ce79fd5768ab6ae46441c572e46cf596f59e57f8" +dependencies = [ + "anstyle", + "windows-sys", +] + +[[package]] +name = "autocfg" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c4b4d0bd25bd0b74681c0ad21497610ce1b7c91b1022cd21c80c6fbdd9476b0" + +[[package]] +name = "base16ct" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c7f02d4ea65f2c1853089ffd8d2787bdbc63de2f0d29dedbcf8ccdfa0ccd4cf" + +[[package]] +name = "base64ct" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b" + +[[package]] +name = "bindgen" +version = "0.70.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f49d8fed880d473ea71efb9bf597651e77201bdd4893efe54c9e5d65ae04ce6f" +dependencies = [ + "bitflags", + "cexpr", + "clang-sys", + "itertools", + "log", + "prettyplease", + "proc-macro2", + "quote", + "regex", + "rustc-hash", + "shlex", + "syn", +] + +[[package]] +name = "bitflags" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "cexpr" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766" +dependencies = [ + "nom", +] + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "clang-sys" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b023947811758c97c59bf9d1c188fd619ad4718dcaa767947df1cadb14f39f4" +dependencies = [ + "glob", + "libc", + "libloading", +] + +[[package]] +name = "colorchoice" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3fd119d74b830634cea2a0f58bbd0d54540518a14397557951e79340abc28c0" + +[[package]] +name = "const-oid" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" + +[[package]] +name = "const-random" +version = "0.1.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87e00182fe74b066627d63b85fd550ac2998d4b0bd86bfed477a0ae4c7c71359" +dependencies = [ + "const-random-macro", +] + +[[package]] +name = "const-random-macro" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9d839f2a20b0aee515dc581a6172f2321f96cab76c1a38a4c584a194955390e" +dependencies = [ + "getrandom", + "once_cell", + "tiny-keccak", +] + +[[package]] +name = "cpufeatures" +version = "0.2.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51e852e6dc9a5bed1fae92dd2375037bf2b768725bf3be87811edee3249d09ad" +dependencies = [ + "libc", +] + +[[package]] +name = "crunchy" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a81dae078cea95a014a339291cec439d2f232ebe854a9d672b796c6afafa9b7" + +[[package]] +name = "crypto-bigint" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0dc92fb57ca44df6db8059111ab3af99a63d5d0f8375d9972e319a379c6bab76" +dependencies = [ + "generic-array", + "rand_core", + "subtle", + "zeroize", +] + +[[package]] +name = "crypto-common" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "der" +version = "0.7.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f55bf8e7b65898637379c1b74eb1551107c8294ed26d855ceb9fd1a09cfc9bc0" +dependencies = [ + "const-oid", + "pem-rfc7468", + "zeroize", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "const-oid", + "crypto-common", + "subtle", +] + +[[package]] +name = "ecdsa" +version = "0.16.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee27f32b5c5292967d2d4a9d7f1e0b0aed2c15daded5a60300e4abb9d8020bca" +dependencies = [ + "der", + "digest", + "elliptic-curve", + "rfc6979", + "signature", + "spki", +] + +[[package]] +name = "either" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0" + +[[package]] +name = "elliptic-curve" +version = "0.13.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5e6043086bf7973472e0c7dff2142ea0b680d30e18d9cc40f267efbf222bd47" +dependencies = [ + "base16ct", + "crypto-bigint", + "digest", + "ff", + "generic-array", + "group", + "pem-rfc7468", + "pkcs8", + "rand_core", + "sec1", + "subtle", + "zeroize", +] + +[[package]] +name = "env_filter" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f2c92ceda6ceec50f43169f9ee8424fe2db276791afde7b2cd8bc084cb376ab" +dependencies = [ + "log", + "regex", +] + +[[package]] +name = "env_logger" +version = "0.11.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13fa619b91fb2381732789fc5de83b45675e882f66623b7d8cb4f643017018d" +dependencies = [ + "anstream", + "anstyle", + "env_filter", + "humantime", + "log", +] + +[[package]] +name = "ff" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ded41244b729663b1e574f1b4fb731469f69f79c17667b5d776b16cda0479449" +dependencies = [ + "rand_core", + "subtle", +] + +[[package]] +name = "function_name" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1ab577a896d09940b5fe12ec5ae71f9d8211fff62c919c03a3750a9901e98a7" +dependencies = [ + "function_name-proc-macro", +] + +[[package]] +name = "function_name-proc-macro" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "673464e1e314dd67a0fd9544abc99e8eb28d0c7e3b69b033bcff9b2d00b87333" + +[[package]] +name = "futures" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "645c6916888f6cb6350d2550b80fb63e734897a8498abe35cfb732b6487804b0" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eac8f7d7865dcb88bd4373ab671c8cf4508703796caa2b1985a9ca867b3fcb78" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfc6580bb841c5a68e9ef15c77ccc837b40a7504914d52e47b8b0e9bbda25a1d" + +[[package]] +name = "futures-executor" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a576fc72ae164fca6b9db127eaa9a9dda0d61316034f33a0a0d4eda41f02b01d" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a44623e20b9681a318efdd71c299b6b222ed6f231972bfe2f224ebad6311f0c1" + +[[package]] +name = "futures-sink" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fb8e00e87438d937621c1c6269e53f536c14d3fbd6a042bb24879e57d474fb5" + +[[package]] +name = "futures-task" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38d84fa142264698cdce1a9f9172cf383a0c82de1bddcf3092901442c4097004" + +[[package]] +name = "futures-util" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d6401deb83407ab3da39eba7e33987a73c3df0c82b4bb5813ee871c19c41d48" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "pin-utils", + "slab", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", + "zeroize", +] + +[[package]] +name = "getrandom" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "glob" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b" + +[[package]] +name = "group" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0f9ef7462f7c099f518d754361858f86d8a07af53ba9af0fe635bbccb151a63" +dependencies = [ + "ff", + "rand_core", + "subtle", +] + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "hmac" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" +dependencies = [ + "digest", +] + +[[package]] +name = "humantime" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" + +[[package]] +name = "is_terminal_polyfill" +version = "1.70.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" + +[[package]] +name = "itertools" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" +dependencies = [ + "either", +] + +[[package]] +name = "json" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "078e285eafdfb6c4b434e0d31e8cfcb5115b651496faca5749b88fafd4f23bfd" + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" +dependencies = [ + "spin", +] + +[[package]] +name = "libc" +version = "0.2.158" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8adc4bb1803a324070e64a98ae98f38934d91957a99cfb3a43dcbc01bc56439" + +[[package]] +name = "libloading" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4979f22fdb869068da03c9f7528f8297c6fd2606bc3a4affe42e6a823fdb8da4" +dependencies = [ + "cfg-if", + "windows-targets", +] + +[[package]] +name = "libm" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ec2a862134d2a7d32d7983ddcdd1c4923530833c9f2ea1a44fc5fa473989058" + +[[package]] +name = "lock_api" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17" +dependencies = [ + "autocfg", + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" + +[[package]] +name = "memchr" +version = "2.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" + +[[package]] +name = "memory-stats" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c73f5c649995a115e1a0220b35e4df0a1294500477f97a91d0660fb5abeb574a" +dependencies = [ + "libc", + "windows-sys", +] + +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + +[[package]] +name = "num-bigint-dig" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc84195820f291c7697304f3cbdadd1cb7199c0efc917ff5eafd71225c136151" +dependencies = [ + "byteorder", + "lazy_static", + "libm", + "num-integer", + "num-iter", + "num-traits", + "rand", + "smallvec", + "zeroize", +] + +[[package]] +name = "num-integer" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-iter" +version = "0.1.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1429034a0490724d0075ebb2bc9e875d6503c3cf69e235a8941aa757d83ef5bf" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", + "libm", +] + +[[package]] +name = "once_cell" +version = "1.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" + +[[package]] +name = "p256" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c9863ad85fa8f4460f9c48cb909d38a0d689dba1f6f6988a5e3e0d31071bcd4b" +dependencies = [ + "ecdsa", + "elliptic-curve", + "primeorder", + "sha2", +] + +[[package]] +name = "parking_lot" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-targets", +] + +[[package]] +name = "pem-rfc7468" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88b39c9bfcfc231068454382784bb460aae594343fb030d46e9f50a645418412" +dependencies = [ + "base64ct", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bda66fc9667c18cb2758a2ac84d1167245054bcf85d5d1aaa6923f45801bdd02" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "pkcs1" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8ffb9f10fa047879315e6625af03c164b16962a5368d724ed16323b68ace47f" +dependencies = [ + "der", + "pkcs8", + "spki", +] + +[[package]] +name = "pkcs8" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" +dependencies = [ + "der", + "spki", +] + +[[package]] +name = "pkg-config" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d231b230927b5e4ad203db57bbcbee2802f6bce620b1e4a9024a07d94e2907ec" + +[[package]] +name = "ppv-lite86" +version = "0.2.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77957b295656769bb8ad2b6a6b09d897d94f05c41b069aede1fcdaa675eaea04" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "prettyplease" +version = "0.2.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "479cf940fbbb3426c32c5d5176f62ad57549a0bb84773423ba8be9d089f5faba" +dependencies = [ + "proc-macro2", + "syn", +] + +[[package]] +name = "primeorder" +version = "0.13.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "353e1ca18966c16d9deb1c69278edbc5f194139612772bd9537af60ac231e1e6" +dependencies = [ + "elliptic-curve", +] + +[[package]] +name = "proc-macro2" +version = "1.0.86" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e719e8df665df0d1c8fbfd238015744736151d4445ec0836b8e628aae103b77" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5b9d34b8991d19d98081b46eacdd8eb58c6f2b201139f7c5f643cc155a633af" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom", +] + +[[package]] +name = "redox_syscall" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a908a6e00f1fdd0dfd9c0eb08ce85126f6d8bbda50017e74bc4a4b7d4a926a4" +dependencies = [ + "bitflags", +] + +[[package]] +name = "regex" +version = "1.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4219d74c6b67a3654a9fbebc4b419e22126d13d2f3c4a07ee0cb61ff79a79619" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38caf58cc5ef2fed281f89292ef23f6365465ed9a41b7a7754eb4e26496c92df" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a66a03ae7c801facd77a29370b4faec201768915ac14a721ba36f20bc9c209b" + +[[package]] +name = "rfc6979" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dd2a808d456c4a54e300a23e9f5a67e122c3024119acbfd73e3bf664491cb2" +dependencies = [ + "hmac", + "subtle", +] + +[[package]] +name = "rsa" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d0e5124fcb30e76a7e79bfee683a2746db83784b86289f6251b54b7950a0dfc" +dependencies = [ + "const-oid", + "digest", + "num-bigint-dig", + "num-integer", + "num-traits", + "pkcs1", + "pkcs8", + "rand_core", + "signature", + "spki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustc-hash" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" + +[[package]] +name = "scc" +version = "2.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aeb7ac86243095b70a7920639507b71d51a63390d1ba26c4f60a552fbb914a37" +dependencies = [ + "sdd", +] + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "sdd" +version = "3.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0495e4577c672de8254beb68d01a9b62d0e8a13c099edecdbedccce3223cd29f" + +[[package]] +name = "sec1" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3e97a565f76233a6003f9f5c54be1d9c5bdfa3eccfb189469f11ec4901c47dc" +dependencies = [ + "base16ct", + "der", + "generic-array", + "pkcs8", + "subtle", + "zeroize", +] + +[[package]] +name = "serial_test" +version = "3.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b4b487fe2acf240a021cf57c6b2b4903b1e78ca0ecd862a71b71d2a51fed77d" +dependencies = [ + "futures", + "log", + "once_cell", + "parking_lot", + "scc", + "serial_test_derive", +] + +[[package]] +name = "serial_test_derive" +version = "3.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82fe9db325bcef1fbcde82e078a5cc4efdf787e96b3b9cf45b50b529f2083d67" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "sha2" +version = "0.10.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "signature" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" +dependencies = [ + "digest", + "rand_core", +] + +[[package]] +name = "slab" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" +dependencies = [ + "autocfg", +] + +[[package]] +name = "smallvec" +version = "1.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" + +[[package]] +name = "spin" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" + +[[package]] +name = "spki" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d" +dependencies = [ + "base64ct", + "der", +] + +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + +[[package]] +name = "syn" +version = "2.0.76" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "578e081a14e0cefc3279b0472138c513f37b41a08d5a3cca9b6e4e8ceb6cd525" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "tiny-keccak" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c9d3793400a45f954c52e73d068316d76b6f4e36977e3fcebb13a2721e80237" +dependencies = [ + "crunchy", +] + +[[package]] +name = "tss2-fapi-rs" +version = "0.5.0" +dependencies = [ + "bindgen", + "const-random", + "digest", + "env_logger", + "function_name", + "hex", + "json", + "libc", + "log", + "memory-stats", + "p256", + "pkg-config", + "rand", + "rand_chacha", + "regex", + "rsa", + "serial_test", + "sha2", + "uuid", +] + +[[package]] +name = "typenum" +version = "1.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" + +[[package]] +name = "unicode-ident" +version = "1.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" + +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + +[[package]] +name = "uuid" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81dfa00651efa65069b0b6b651f4aaa31ba9e3c3ce0137aaad053604ee7e0314" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "wasi" +version = "0.11.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_gnullvm", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "zerocopy" +version = "0.7.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0" +dependencies = [ + "byteorder", + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.7.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "zeroize" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..f7ad812 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,34 @@ +[package] +name = "tss2-fapi-rs" +version = "0.5.0" +edition = "2021" +description = "Provides a Rust interface to the TSS2.0 Feature API (FAPI)" +license = "BSD-3-Clause" +authors = ["Daniel Trick "] +readme = "docs/crates-README.md" +repository = "https://github.com/tpm2-software/rust-tss-fapi" + +[dependencies] +json = "0.12.4" +libc = "0.2.155" +log = "0.4.22" + +[build-dependencies] +bindgen = "0.70.1" +pkg-config = "0.3.30" + +[dev-dependencies] +const-random = "0.1.18" +digest = "0.10.7" +env_logger = "0.11.3" +function_name = "0.3.0" +hex = "0.4.3" +memory-stats = "1.2.0" +p256 = "0.13.2" +rand = "0.8.5" +rand_chacha = "0.3.1" +regex = "1.10.5" +rsa = "0.9.6" +serial_test = "3.1.1" +sha2 = "0.10.8" +uuid = "1.9.1" diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..9950b2a --- /dev/null +++ b/LICENSE @@ -0,0 +1,27 @@ +Copyright 2024, Fraunhofer SIT sponsored by the ELISA research project +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, +this list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, +this list of conditions and the following disclaimer in the documentation +and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its contributors +may be used to endorse or promote products derived from this software without +specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE +LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE +USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/MAINTAINERS.md b/MAINTAINERS.md new file mode 100644 index 0000000..d9420e3 --- /dev/null +++ b/MAINTAINERS.md @@ -0,0 +1,3 @@ +| email | PGP Fingerprint | +| -------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------- | +| Daniel Trick [<daniel.trick@sit.fraunhofer.de>](mailto:daniel.trick@sit.fraunhofer.de) | [872CF0382BF544B6551AE84E2990B15C666AD009](https://keys.openpgp.org/vks/v1/by-fingerprint/872CF0382BF544B6551AE84E2990B15C666AD009) | diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..5aff991 --- /dev/null +++ b/Makefile @@ -0,0 +1,14 @@ +BUILD_TARGETS := build swtpm tests +RESET_TARGETS := $(addsuffix -reset,$(BUILD_TARGETS)) +CLEAN_TARGETS := $(addsuffix -clean,$(BUILD_TARGETS)) + +.PHONY: all clean $(BUILD_TARGETS) $(CLEAN_TARGETS) + +all: + $(error Must specify a target! Choose one of: $(BUILD_TARGETS) clean) + +$(BUILD_TARGETS) $(RESET_TARGETS) $(CLEAN_TARGETS): + $(MAKE) -C tools/docker/$(subst -, ,$@) + +reset: $(RESET_TARGETS) +clean: $(CLEAN_TARGETS) diff --git a/README.md b/README.md new file mode 100644 index 0000000..5c56758 --- /dev/null +++ b/README.md @@ -0,0 +1,213 @@ +[![Rust](https://img.shields.io/badge/Rust-1.79.0+-orange?logo=rust)](https://www.rust-lang.org/) +[![Crates.io](https://img.shields.io/crates/v/tss2-fapi-rs.svg)](https://crates.io/crates/tss2-fapi-rs) +[![Docs.rs](https://img.shields.io/docsrs/tss2-fapi-rs.svg)](https://docs.rs/tss2-fapi-rs/latest/tss2_fapi_rs/) +[![BSD-3-Clause](https://img.shields.io/badge/License-BSD_3--Clause-blue.svg?logo=freebsd)](https://opensource.org/licenses/BSD-3-Clause) +[![Cargo CI/CD](https://github.com/tpm2-software/rust-tss-fapi/actions/workflows/on-push-pull_request.yaml/badge.svg)](https://github.com/tpm2-software/rust-tss-fapi/actions/workflows/on-push-pull_request.yaml) + +# TSS 2.0 FAPI Rust Wrapper + +The **`tss2-fapi-rs`** Rust crate provides an interface to the [**TSS2.0 Feature API (FAPI)**](https://tpm2-tss.readthedocs.io/en/latest/group__fapi.html). + +*Architectural overview:* +![tss2-fapi-rs Overview](docs/images/tss2-fapi-rs-overview.png) + +## Layout + +The `tss2-fapi-rs` project is organized as follows: + +``` +tss2-fapi-rs +├── Cargo.toml The "manifest" file for Cargo +├── build.rs Build script (for pkg-config + bindgen) +├── docs Various bits of documentation +├── examples Usage examples +│ └── data Example FAPI configuration +├── src Source code of the "tss2-fapi-rs" library +│ └── fapi_sys Low-level FFI bindings for FAPI +├── tests Integration tests +│ └── data Test configuration files +└── tools Build tools + ├── codecov Code coverage analysis script (llvm-cov) + ├── docker Docker test/build environment + │ ├── build Build container + │ ├── swtpm Software TPM container + │ └── tests Test container + └── libtpms Test driver script for using libtpms +``` + +## Prerequisites + +The following prerequisites are required for building or using the `tss2-fapi-rs` crate on your system: + +##### *libtss2‑fapi* + +The native **`libtss2‑fapi`** library, version 3.0.3 or later, and its associated header files must be available: + + +On Ubuntu 22.04 (Jammy), Debian 12.0 (Bookworm) or later, it can be installed via the `libtss2-dev` package. + +Furthermore, it may be necessary to additionally install the following *transitive* dependencies: +`uuid-dev`, `libjson-c-dev`, `libcrypt-dev` and `libcurl4-openssl-dev` + +**Note:** By default, the native FAPI library is detected automatically, using the [pkg-config](https://en.wikipedia.org/wiki/Pkg-config) utility. If the required metadata file `tss2-fapi.pc` can *not* be found, please set or update your `PKG_CONFIG_PATH` as needed! Alternatively, the location of the native FAPI library can be specified explicitly via the *environment variables* described below. + +##### *C compiler* + +A working **C compiler** (`cc`) is required for Rust/Cargo to build some of the required dependencies. + +On Ubuntu 22.04 (Jammy), Debian 12.0 (Bookworm) or later, it can be installed via the `build-essential` package. + +##### *libclang* + +Rust's [`bindgen`](https://github.com/rust-lang/rust-bindgen?tab=readme-ov-file) tool requires that **`libclang`** is available on the system. + +On Ubuntu 22.04 (Jammy), Debian 12.0 (Bookworm) or later, it can be installed via the `libclang-dev` package. + +#### Environment + +The following environment variables may specify the location of the native FAPI library during the *build* process: + +* **`TSS2_INCLUDE_PATH`** + Location of the TSS 2.0 header files. If specified, this path shall contain the `tss2_fapi.h` header file. + This variable must be set in conjunction with `TSS2_LIBRARY_PATH` to be effective! + +* **`TSS2_LIBRARY_PATH`** + Location of the TSS 2.0 library files. If specified, this path shall contain the `libtss2-fapi.so` file. + This variable must be set in conjunction with `TSS2_INCLUDE_PATH` to be effective! + +* **`TSS2_LIBRARY_VERS`** + Version of the TSS 2.0 library. If specified, the version string shall have the `"major.minor.patch"` format. + +The following environment variables can be used to control the *runtime* behavior: + +* **`LD_LIBRARY_PATH`** + On Linux/Unix systems, this can be set to control the location to load the native FAPI library from. + +## Build instructions + +In order to build the `tss2-fapi-rs` library, simply run the following command in the project root directory: + +``` +$ cargo build --release +``` + +#### Example code + +Some examples demonstrating how to use the `tss2-fapi-rs` library are provided in the **`examples`** sub-directory. + +In order to execute an example, simply run the following command in the project root directory: + +``` +$ cargo run --example +``` + +## Documentation + +The documentation for `tss2-fapi-rs` is available via [`rustdoc`](https://doc.rust-lang.org/rustdoc/what-is-rustdoc.html), so please run the following command: + +``` +$ cargo doc --open +``` + +#### Online Documentation + +Alternatively, the latest documentation can always be found here: + + +## Docker + +A fully self-contained build/test environment, based on [Docker Compose](https://docs.docker.com/compose/) and [SWTPM](https://github.com/stefanberger/swtpm), is provided. + +#### Prerequisites + +The following prerequisites are required for using the Docker environment: + +* On Ubuntu 22.04 (Jammy) or later, *Docker Compose V2* can be installed via the `docker-compose-v2` package. + +* On Debian 12.0 (Bookworm) or later, please set up the Docker.io repository as described here: + + + Finally, install the packages `docker-ce`, `docker-ce-cli`, `containerd.io` and `docker-compose-plugin`. + +#### Building + +With Docker Compose V2 installed, just run the following commands from the project root directory: +``` +$ cd etc/docker/build +$ docker compose up --build --force-recreate +``` + +Alternatively, the provided `Makefile` in the project root directory can be invoked: +``` +$ make build +``` + +##### Environment + +The following environment variables can be set to control the build: + +* **`CLEAN`** + If set to a non-zero value, force a full re-build. + +* **`RELEASE`** + If set to a non-zero value, build in "release" mode (default). Set to zero to build in "debug" mode. + +##### Output + +Build artifacts can be found at, e.g.: +``` +/var/lib/docker/volumes/tss2-fapi-rs-build_out/_data/target +``` + +#### Testing + +With Docker Compose V2 installed, just run the following commands from the project root directory: +``` +$ cd tools/docker/tests +$ docker compose up --build --force-recreate --abort-on-container-exit +``` + +Alternatively, the provided `Makefile` in the project root directory can be invoked: +``` +$ make tests +``` + +##### Environment + +The following environment variables can be set to tweak the tests: + +* **`TEST_NAME`** + Specify the test name filter. Can be set to run *only* the matching test(s). By default, *all* tests will be run. + +* **`TEST_LOOP`** + Speciy the number of iterations to run for each test. By default, each test is repeated *5* times. + +* **`KEEP_RUNNING`** + If set to a non-zero value, keep the test container running after the tests have finished. + +* **`INCL_IGNORED`** + If set to a non-zero value, include tests that are marked with the `[ignore]` attribute in the test run. + +## Code Coverage + +Code coverage analysis is provided by the [**`cargo-llvm-cov`**](https://github.com/taiki-e/cargo-llvm-cov) utility. + +In order to perform a code coverage analysis, just run the provided helper script: +``` +$ ./tools/llvm-cov/code-coverage.sh +``` + +#### Prerequisites + +The required tool `cargo-llvm-cov` can be installed using the following command: + +``` +$ cargo +stable install cargo-llvm-cov --locked +``` + +## License + +Copyright 2024, Fraunhofer SIT sponsored by the ELISA research project +All rights reserved. + +This work is released under the [**3-Clause BSD License**](https://opensource.org/license/bsd-3-clause) (SPDX short identifier: `BSD-3-Clause`). diff --git a/build.rs b/build.rs new file mode 100644 index 0000000..6b8b7b6 --- /dev/null +++ b/build.rs @@ -0,0 +1,138 @@ +/* SPDX-License-Identifier: BSD-3-Clause */ +/******************************************************************************* + * Copyright 2024, Fraunhofer SIT sponsored by the ELISA research project + * All rights reserved. + ******************************************************************************/ + +use std::{ + env, + fs::File, + io::Write, + path::{Path, PathBuf}, +}; + +/// The name of the native TSS 2.0 FPAI library +const LIBRARY_NAME: &str = "tss2-fapi"; + +/// Minimal required version of the native FAPI library +/// The version specified here is equal to the TSS 2.0 version available in Debian 11 (Bullseye) +const LIBRARY_MIN_VERSION: &str = "3.0.3"; + +/// This build scripts is required to detect and link the "native" FAPI library +fn main() { + // Get the output directory + let out_path = PathBuf::from(env::var("OUT_DIR").unwrap()); + + // Detect the native TSS 2.0 FAPI library location + let tss2_fapi = detect_library(); + + // Assert that library information is complete + assert!(!tss2_fapi.0.is_empty(), "FAPI library name is not defined!"); + assert!( + !tss2_fapi.0.first().unwrap().is_empty(), + "Library name is empty!" + ); + assert!(!tss2_fapi.1.is_empty(), "FAPI link path is not defined!"); + assert!(!tss2_fapi.2.is_empty(), "FAPI include path is not defined!"); + assert!( + !tss2_fapi.3.is_empty(), + "FAPI library version is not defined!" + ); + + // Add the required libraries to be linked + for library_name in tss2_fapi.0 { + println!("cargo:rustc-link-lib={}", library_name); + } + + // Add the required linker search paths + for link_path in tss2_fapi.1 { + println!("cargo:rustc-link-search={}", link_path.to_str().unwrap()); + } + + // Initialize `bindgen` builder with the required include directories + let mut bindgen_builder = bindgen::Builder::default(); + for incl_path in tss2_fapi.2 { + bindgen_builder = bindgen_builder.clang_arg(format!("-I{}", incl_path.to_str().unwrap())); + } + + // Invoke the `bindgen` for TSS 2.0 FAPI library + bindgen_builder + .header_contents("wrapper.h", "#include ") + .layout_tests(false) + .parse_callbacks(Box::new(bindgen::CargoCallbacks::new())) + .generate() + .expect("Unable to generate bindings") + .write_to_file(out_path.join("tss2_fapi_bindings.rs")) + .expect("Failed to write FAPI bindings!"); + + // Persist the detected library version + write_version_string(&out_path.join("tss2_fapi_versinfo.rs"), &tss2_fapi.3); +} + +/// Detect the native TSS2 FAPI library +/// +/// Try to detect the library using **pkg-config**, unless the environment variables `TSS2_INCLUDE_PATH` and `TSS2_LIBRARY_PATH` are defined! +fn detect_library() -> (Vec, Vec, Vec, String) { + let include_path = env::var("TSS2_INCLUDE_PATH") + .map(|value| PathBuf::from(value)) + .ok(); + let library_path = env::var("TSS2_LIBRARY_PATH") + .map(|value| PathBuf::from(value)) + .ok(); + + // Shortcut if `TSS2_INCLUDE_PATH` and `TSS2_LIBRARY_PATH` are defined + if include_path.is_some() && library_path.is_some() { + let library_version = + env::var("TSS2_LIBRARY_VERS").unwrap_or_else(|_| LIBRARY_MIN_VERSION.to_owned()); + return ( + vec![LIBRARY_NAME.to_owned()], + vec![library_path.unwrap()], + vec![include_path.unwrap()], + library_version, + ); + } + + // Try to detect the TSS 2.0 library by invoking the `pkg-config` utility + let tss2_fapi_config = pkg_config::Config::new() + .cargo_metadata(false) + .atleast_version(LIBRARY_MIN_VERSION) + .probe(LIBRARY_NAME) + .expect("pkg_config: Required library \"tss2-fapi\" not found!"); + + ( + tss2_fapi_config.libs, + tss2_fapi_config.link_paths, + tss2_fapi_config.include_paths, + tss2_fapi_config.version, + ) +} + +/// Persist the version string to output file, so that it can be evaluated in the code at runtime +fn write_version_string(path: &Path, version_string: &str) { + // Parse the version string, assuming that is is in the `"major.minor.patch"` format + let mut tokens = version_string.split('.'); + let vers_major = tokens + .next() + .unwrap_or_default() + .parse::() + .expect("Failed to parse version string!"); + let vers_minor = tokens + .next() + .unwrap_or_default() + .parse::() + .expect("Failed to parse version string!"); + let vers_patch = tokens + .next() + .unwrap_or_default() + .parse::() + .expect("Failed to parse version string!"); + + // Try to write the version string to the output file + let mut file = File::create(path).expect("Failed to create output file for version!"); + writeln!( + file, + r#"pub const TSS2_FAPI_VERSION: &str = "{}.{}.{}";"#, + vers_major, vers_minor, vers_patch + ) + .expect("Failed to write version!"); +} diff --git a/docs/crates-README.md b/docs/crates-README.md new file mode 100644 index 0000000..1f1d940 --- /dev/null +++ b/docs/crates-README.md @@ -0,0 +1,20 @@ +# TSS 2.0 FAPI Rust Wrapper + +This library provides an interface to the [TSS2.0 Feature API (FAPI)](https://tpm2-tss.readthedocs.io/en/latest/group__fapi.html). + +#### Architectural overview + +![tss2-fapi-rs Overview](https://raw.githubusercontent.com/tpm2-software/rust-tss-fapi/main/docs/images/tss2-fapi-rs-overview.small.png) + +## See also + +* [**Documentation**](https://docs.rs/tss2-fapi-rs/latest) + +* [**GitHub Repository**](https://github.com/tpm2-software/rust-tss-fapi) + +## License + +Copyright 2024, Fraunhofer SIT sponsored by the ELISA research project +All rights reserved. + +This work is released under the [**3-Clause BSD License**](https://opensource.org/license/bsd-3-clause). diff --git a/docs/html/redir.html b/docs/html/redir.html new file mode 100644 index 0000000..89184b1 --- /dev/null +++ b/docs/html/redir.html @@ -0,0 +1,11 @@ + + + + TSS 2.0 FAPI Rust Wrapper + + + +

TSS 2.0 FAPI Rust Wrapper

+

If your web browser doesn't redirect you automatically, please click here!

+ + \ No newline at end of file diff --git a/docs/images/tss2-fapi-rs-overview.png b/docs/images/tss2-fapi-rs-overview.png new file mode 100644 index 0000000000000000000000000000000000000000..fbf80015b9beb5aa7424566fb6e99c3d0eb21107 GIT binary patch literal 30485 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%0DvK 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 fpai_ctx, + Err(error) => panic!("Failed to create context: {:?}", error), + }; + + // Print result + info!("FAPI context created. ({})", context); + + // Perform the provisioning, if it has not been done yet + info!("Provisioning, please wait..."); + match context.provision(None, None, None) { + Ok(_) => info!("Success."), + Err(ErrorCode::FapiError(BaseErrorCode::AlreadyProvisioned)) => { + warn!("TPM already provisioned -> skipping!") + } + Err(error) => panic!("Provisioning has failed: {:?}", error), + } + + // Generate random + info!("Generating random data..."); + for _i in 0..8 { + match context.get_random(NonZeroUsize::new(32).unwrap()) { + Ok(random) => info!("Random data: {}", hex::encode(&random[..])), + Err(error) => error!("get_random() failed: {:?}", error), + } + } + + // Exit + info!("Shutting down..."); +} diff --git a/examples/2_sign_and_verify.rs b/examples/2_sign_and_verify.rs new file mode 100644 index 0000000..f3c91cf --- /dev/null +++ b/examples/2_sign_and_verify.rs @@ -0,0 +1,103 @@ +/* SPDX-License-Identifier: BSD-3-Clause */ +/******************************************************************************* + * Copyright 2024, Fraunhofer SIT sponsored by the ELISA research project + * All rights reserved. + ******************************************************************************/ + +use log::{debug, info, warn}; +use sha2::{Digest, Sha256}; +use tss2_fapi_rs::{BaseErrorCode, ErrorCode, FapiContext, KeyFlags}; + +const MY_KEYFLAG: &[KeyFlags] = &[KeyFlags::Sign, KeyFlags::NoDA]; +const MY_KEYPATH: &str = "HS/SRK/mySignKey"; +const MY_MESSAGE: &str = "The quick brown fox jumps over the lazy dog."; + +/// # tss2-fapi-rs example #2 — sign() and verify_signature() +/// +/// This example demonstrates how to create a signing key in the TMP and then use that TPM-resident key to sign a message as well as how to verify the generated signature. +/// +/// ### Remarks +/// +/// Please note that this example requires that a valid FAPI configuration has already been set up! +/// +/// The environment variable `TSS2_FAPICONF` can be used to control the path of the FAPI configuration file. Otherwise it defaults to `/etc/tpm2-tss/fapi-config.json`. +/// +/// A template for creating a valid FAPI configuration is provided in the `data` sub-directory. +fn main() { + // Set up Rust logger + env_logger::builder() + .filter_level(log::LevelFilter::Debug) + .init(); + + // Print logo + info!("TSS2 FAPI Wrapper - Example #2"); + + // Create a new FAPI context + info!("Creating FAPI context, please wait..."); + let mut context = match FapiContext::new() { + Ok(fpai_ctx) => fpai_ctx, + Err(error) => panic!("Failed to create context: {:?}", error), + }; + + // Print result + info!("FAPI context created. ({})", context); + + // Perform the provisioning, if it has not been done yet + info!("Provisioning, please wait..."); + match context.provision(None, None, None) { + Ok(_) => info!("Success."), + Err(ErrorCode::FapiError(BaseErrorCode::AlreadyProvisioned)) => { + warn!("TPM already provisioned -> skipping!") + } + Err(error) => panic!("Provisioning has failed: {:?}", error), + } + + // Create the siging key + info!("Creating key, please wait..."); + match context.create_key(MY_KEYPATH, Some(MY_KEYFLAG), None, None) { + Ok(_) => info!("Success."), + Err(ErrorCode::FapiError(BaseErrorCode::PathAlreadyExists)) => { + debug!("Key already created -> skipping!") + } + Err(error) => panic!("Key creation has failed: {:?}", error), + }; + + // Compute digest to be signed (using the "sha2" crate) + let digest = Sha256::digest(MY_MESSAGE); + debug!("Digest to be signed: {}", hex::encode(&digest[..])); + + // Create the signature + info!("Computing signature, please wait..."); + let mut signature = match context.sign(MY_KEYPATH, None, &digest, false, false) { + Ok(value) => value, + Err(error) => panic!("Failed to create the signature: {:?}", error), + }; + + // Print signature value + info!("Signature: {}", hex::encode(&signature.0[..])); + + // Simulate an attack on the siganture value! + if !option_env!("ATTACK").unwrap_or_default().is_empty() { + warn!("Disturbing the digest value !!!"); + *signature.0.last_mut().unwrap() = signature.0.last().unwrap().wrapping_add(1u8); + } + + // Verify the signature + let verify_result = match context.verify_signature(MY_KEYPATH, &digest, &signature.0[..]) { + Ok(value) => value, + Err(error) => panic!("Failed to verify the signature: {:?}", error), + }; + + // Print verification result + info!( + "Result: {}", + if verify_result { + "valid \u{2714}" + } else { + "invalid \u{274C}" + } + ); + + // Exit + info!("Shutting down..."); +} diff --git a/examples/3_auth_callback.rs b/examples/3_auth_callback.rs new file mode 100644 index 0000000..c3d9f0e --- /dev/null +++ b/examples/3_auth_callback.rs @@ -0,0 +1,98 @@ +/* SPDX-License-Identifier: BSD-3-Clause */ +/******************************************************************************* + * Copyright 2024, Fraunhofer SIT sponsored by the ELISA research project + * All rights reserved. + ******************************************************************************/ + +use log::{debug, info, warn}; +use std::borrow::Cow; +use tss2_fapi_rs::{ + AuthCallback, AuthCallbackParam, BaseErrorCode, ErrorCode, FapiContext, KeyFlags, +}; + +const MY_KEYFLAG: &[KeyFlags] = &[KeyFlags::Sign, KeyFlags::NoDA]; +const MY_KEYPATH: &str = "HS/SRK/myTestKey"; +const MY_AUTHVAL: &str = "OrpheanBeholderScryDoubt"; + +/// # tss2-fapi-rs example #3 — set_auth_callback() +/// +/// This example demonstrates how to create a key that is protected by an auth value (password) and how to register the required authorization callback. +/// +/// The callback function will be called, by the FAPI, in order to request the auth value (password) from the application when the key is used. +/// +/// ### Remarks +/// +/// Please note that this example requires that a valid FAPI configuration has already been set up! +/// +/// The environment variable `TSS2_FAPICONF` can be used to control the path of the FAPI configuration file. Otherwise it defaults to `/etc/tpm2-tss/fapi-config.json`. +/// +/// A template for creating a valid FAPI configuration is provided in the `data` sub-directory. +fn main() { + // Set up Rust logger + env_logger::builder() + .filter_level(log::LevelFilter::Debug) + .init(); + + // Print logo + info!("TSS2 FAPI Wrapper - Example #3"); + + // Create a new FAPI context + info!("Creating FAPI context, please wait..."); + let mut context = match FapiContext::new() { + Ok(fpai_ctx) => fpai_ctx, + Err(error) => panic!("Failed to create context: {:?}", error), + }; + + // Print result + info!("FAPI context created. ({})", context); + + // Add authorization callback function + match context.set_auth_callback(AuthCallback::new(my_auth_callback)) { + Ok(_) => info!("Success."), + Err(error) => panic!("Failed to set up AUTH callback function: {:?}", error), + } + + // Perform the provisioning, if it has not been done yet + info!("Provisioning, please wait..."); + match context.provision(None, None, None) { + Ok(_) => info!("Success."), + Err(ErrorCode::FapiError(BaseErrorCode::AlreadyProvisioned)) => { + warn!("TPM already provisioned -> skipping!") + } + Err(error) => panic!("Provisioning has failed: {:?}", error), + } + + // Create the key and set up auth value (password) + info!("Creating key, please wait..."); + match context.create_key(MY_KEYPATH, Some(MY_KEYFLAG), None, Some(MY_AUTHVAL)) { + Ok(_) => info!("Success."), + Err(ErrorCode::FapiError(BaseErrorCode::PathAlreadyExists)) => { + debug!("Key already created -> skipping!") + } + Err(error) => panic!("Key creation has failed: {:?}", error), + }; + + // Create the signature + info!("Computing signature, please wait..."); + let signature = match context.sign(MY_KEYPATH, None, &[0u8; 32], false, false) { + Ok(value) => value, + Err(error) => panic!("Failed to create the signature: {:?}", error), + }; + + // Print signature value + info!("Signature: {}", hex::encode(&signature.0[..])); + + // Exit + info!("Shutting down..."); +} + +/// This function will be called by FAPI in order to request authorization values from the application. +/// +/// *Note:* For simplicity, in this example, the callback function always returns our password, regardless of the requested object path. +fn my_auth_callback(auth_param: AuthCallbackParam) -> Option> { + info!( + "Authorization for object at {:?} has been requested.", + auth_param.object_path + ); + Some(Cow::from(MY_AUTHVAL)) +} diff --git a/examples/data/.gitignore b/examples/data/.gitignore new file mode 100644 index 0000000..244a9fb --- /dev/null +++ b/examples/data/.gitignore @@ -0,0 +1,2 @@ +keystore/**/* +logs/**/* diff --git a/examples/data/fapi-config.json b/examples/data/fapi-config.json new file mode 100644 index 0000000..c8b0483 --- /dev/null +++ b/examples/data/fapi-config.json @@ -0,0 +1,10 @@ +{ + "ek_cert_less": "yes", + "log_dir": "/home/john_doe/tss2-fapi-config/logs", + "profile_dir": "/home/john_doe/tss2-fapi-config/profiles", + "profile_name": "P_ECCP256SHA256", + "system_dir": "/home/john_doe/tss2-fapi-config/keystore/system", + "system_pcrs": [], + "tcti": "swtpm:host=127.0.0.1,port=2321", + "user_dir": "/home/john_doe/tss2-fapi-config/keystore/user" +} diff --git a/examples/data/keystore/system/.gitkeep b/examples/data/keystore/system/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/examples/data/keystore/user/.gitkeep b/examples/data/keystore/user/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/examples/data/logs/.gitkeep b/examples/data/logs/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/examples/data/profiles/P_ECCP256SHA256.json b/examples/data/profiles/P_ECCP256SHA256.json new file mode 100644 index 0000000..135212f --- /dev/null +++ b/examples/data/profiles/P_ECCP256SHA256.json @@ -0,0 +1,47 @@ +{ + "curveID": "TPM2_ECC_NIST_P256", + "ecc_signing_scheme": { + "details": { + "hashAlg": "TPM2_ALG_SHA256" + }, + "scheme": "TPM2_ALG_ECDSA" + }, + "ek_description": "Endorsement key EK", + "ek_policy": { + "description": "Endorsement hierarchy used for policy secret.", + "policy": [ + { + "objectName": "4000000b", + "type": "POLICYSECRET" + } + ] + }, + "ek_template": "system,restricted,decrypt", + "nameAlg": "TPM2_ALG_SHA256", + "pcr_selection": [ + { + "hash": "TPM2_ALG_SHA256", + "pcrSelect": [ + 8, + 9, + 10, + 11, + 12, + 13, + 14, + 15 + ] + } + ], + "srk_description": "Storage root key SRK", + "srk_persistent": 0, + "srk_template": "system,restricted,decrypt,0x81000001", + "sym_block_size": 16, + "sym_mode": "TPM2_ALG_CFB", + "sym_parameters": { + "algorithm": "TPM2_ALG_AES", + "keyBits": "128", + "mode": "TPM2_ALG_CFB" + }, + "type": "TPM2_ALG_ECC" +} diff --git a/src/algorithm_id.rs b/src/algorithm_id.rs new file mode 100644 index 0000000..4dfb55c --- /dev/null +++ b/src/algorithm_id.rs @@ -0,0 +1,46 @@ +/* SPDX-License-Identifier: BSD-3-Clause */ +/******************************************************************************* + * Copyright 2024, Fraunhofer SIT sponsored by the ELISA research project + * All rights reserved. + ******************************************************************************/ + +use crate::fapi_sys::{constants, TPM2_ALG_ID}; + +/// Identifies the hash algorithm to be used, e.g. for signature creation. +#[derive(Debug, PartialEq)] +pub enum HashAlgorithm { + /// Secure Hash Algorithm 1 + Sha1, + /// Secure Hash Algorithm 2 with 256-Bit output (SHA-256) + Sha2_256, + /// Secure Hash Algorithm 2 with 384-Bit output (SHA-384) + Sha2_384, + /// Secure Hash Algorithm 2 with 512-Bit output (SHA-512) + Sha2_512, + /// Secure Hash Algorithm 3 with 256-Bit output (SHA3-256) + Sha3_256, + /// Secure Hash Algorithm 3 with 384-Bit output (SHA3-384) + Sha3_384, + /// Secure Hash Algorithm 3 with 512-Bit output (SHA3-512) + Sha3_512, + /// ShangMi 3 hash function with 256-Bit output + SM3_256, + /// Unknown hash algorithm + UnknownAlgorithm, +} + +impl HashAlgorithm { + pub(crate) fn from_id(algo_id: TPM2_ALG_ID) -> Self { + match algo_id { + constants::TPM2_ALG_SHA1 => Self::Sha1, + constants::TPM2_ALG_SHA256 => Self::Sha2_256, + constants::TPM2_ALG_SHA384 => Self::Sha2_384, + constants::TPM2_ALG_SHA512 => Self::Sha2_512, + constants::TPM2_ALG_SHA3_256 => Self::Sha3_256, + constants::TPM2_ALG_SHA3_384 => Self::Sha3_384, + constants::TPM2_ALG_SHA3_512 => Self::Sha3_512, + constants::TPM2_ALG_SM3_256 => Self::SM3_256, + _ => Self::UnknownAlgorithm, + } + } +} diff --git a/src/callback.rs b/src/callback.rs new file mode 100644 index 0000000..6355648 --- /dev/null +++ b/src/callback.rs @@ -0,0 +1,362 @@ +/* SPDX-License-Identifier: BSD-3-Clause */ +/******************************************************************************* + * Copyright 2024, Fraunhofer SIT sponsored by the ELISA research project + * All rights reserved. + ******************************************************************************/ + +use crate::{fapi_sys::TPM2_ALG_ID, memory::CStringHolder, HashAlgorithm}; +use log::trace; +use std::{any::Any, borrow::Cow, ffi::CStr, fmt::Debug}; + +// ========================================================================== +// AuthCallback +// ========================================================================== + +/// A callback function that allows the FAPI to request authorization values. +/// +/// Regsitered to a FAPI context via the [`set_auth_callback()`](crate::FapiContext::set_auth_callback) function. +pub struct AuthCallback { + auth_fn: Box Option> + Send>, + auth_value: Option, +} + +/// Wraps the parameters to be passed to the [`AuthCallback`] callback's `auth_fn` function. +#[derive(Debug)] +pub struct AuthCallbackParam<'a> { + /// Identifies the TPM object (path) for which an authorization value is requested. + pub object_path: &'a str, + /// User readable description of the authorization value requested (optional). + pub description: Option<&'a str>, +} + +impl AuthCallback { + /// Creates a new callback instance. + /// + /// The supplied `auth_fn` will be called whenever the FAPI requests authorization values from the application. This function receives an [`AuthCallbackParam`] as parameter; it shall return `Some(value)`, if an authorization value for the requested object is provided by the application, or `None`, if **no** authorization value is provided. + pub fn new( + auth_fn: impl Fn(AuthCallbackParam) -> Option> + 'static + Send, + ) -> Self { + Self { + auth_fn: Box::new(auth_fn), + auth_value: None, + } + } + + /// Creates a new callback instance with additional data. + /// + /// The supplied `auth_fn` will be called whenever the FAPI requests authorization values from the application. This function receives an [`AuthCallbackParam`] as parameter; it shall return `Some(value)`, if an authorization value for the requested object is provided by the application, or `None`, if **no** authorization value is provided. + /// + /// The application-defined `extra_data` argument will be passed to each invocation of `auth_fn` as an additional parameter. + pub fn with_data( + sign_fn: impl Fn(AuthCallbackParam, &T) -> Option> + 'static + Send, + extra_data: T, + ) -> Self { + Self::new(move |callback_param| sign_fn(callback_param, &extra_data)) + } + + /// Request authorization value for the specified TPM object (path) from the application. + pub(crate) fn invoke( + &mut self, + object_path: &CStr, + description: Option<&CStr>, + ) -> Option<&CStringHolder> { + let param = AuthCallbackParam::new(object_path, description); + trace!("AuthCallback::invoke({:?})", ¶m); + match (self.auth_fn)(param) { + Some(value) => { + self.auth_value = CStringHolder::try_from(value).ok(); + self.auth_value.as_ref() + } + _ => None, + } + } + + pub(crate) fn clear_buffer(&mut self) { + self.auth_value.take(); + } +} + +impl<'a> AuthCallbackParam<'a> { + fn new(object_path: &'a CStr, description: Option<&'a CStr>) -> Self { + Self { + object_path: object_path.to_str().unwrap_or_default(), + description: description.map(|str| str.to_str().unwrap_or_default()), + } + } +} + +impl Debug for AuthCallback { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("AuthCallback") + .field("auth_fn", &self.auth_fn.type_id()) + .field("auth_value", &self.auth_value) + .finish() + } +} + +// ========================================================================== +// SignCallback +// ========================================================================== + +/// A callback function that allows the FAPI to request signatures for authorizing the use of TPM objects. +/// +/// Registered to a FAPI context via the [`set_sign_callback()`](crate::FapiContext::set_sign_callback) function. +pub struct SignCallback { + sign_fn: Box Option> + Send>, + sign_data: Option>, +} + +/// Wraps the parameters to be passed to the [`SignCallback`] callback's `sign_fn` function. +#[derive(Debug)] +pub struct SignCallbackParam<'a> { + /// Identifies the policy (path) being executed for which a signature is requested. + pub object_path: &'a str, + /// Description as provided in the policy definition (optional). + pub description: Option<&'a str>, + /// The public key that will be used by the TPM to verify the signature, in PEM encoding. + pub public_key: &'a str, + /// Human readable information, regarding the public key to be used (optional). + pub key_hint: Option<&'a str>, + /// The hash algorithm to be used during signing. + pub hash_algo: HashAlgorithm, + /// The data that is to be hashed and signed by the application. + pub challenge: &'a [u8], +} + +impl SignCallback { + /// Creates a new callback instance. + /// + /// The supplied `sign_fn` will be called whenever the FAPI requests a signature from the application. The purpose of this signature is to authorize a policy execution containing a *PolicySigned* element. This function receives a [`SignCallbackParam`] as parameter; it shall return `Some(value)`, if a signature value is provided by the application, or `None`, if **no** signature value is provided. + pub fn new(sign_fn: impl Fn(SignCallbackParam) -> Option> + 'static + Send) -> Self { + Self { + sign_fn: Box::new(sign_fn), + sign_data: None, + } + } + + /// Creates a new callback instance with additional data. + /// + /// The supplied `sign_fn` will be called whenever the FAPI requests a signature from the application. The purpose of this signature is to authorize a policy execution containing a *PolicySigned* element. This function receives a [`SignCallbackParam`] as parameter; it shall return `Some(value)`, if a signature value is provided by the application, or `None`, if **no** signature value is provided. + /// + /// The application-defined `extra_data` argument will be passed to each invocation of `sign_fn` as an additional parameter. + pub fn with_data( + sign_fn: impl Fn(SignCallbackParam, &T) -> Option> + 'static + Send, + extra_data: T, + ) -> Self { + Self::new(move |callback_param| sign_fn(callback_param, &extra_data)) + } + + /// Request a signature for authorizing use of TPM objects from the application. + pub(crate) fn invoke( + &mut self, + object_path: &CStr, + description: Option<&CStr>, + public_key: &CStr, + key_hint: Option<&CStr>, + hash_algo: u32, + challenge: &[u8], + ) -> Option<&[u8]> { + let param = SignCallbackParam::new( + object_path, + description, + public_key, + key_hint, + hash_algo, + challenge, + ); + trace!("SignCallback::invoke({:?})", ¶m); + match (self.sign_fn)(param) { + Some(value) => { + self.sign_data = Some(value); + self.sign_data.as_ref().map(Vec::as_slice) + } + _ => None, + } + } + + pub(crate) fn clear_buffer(&mut self) { + self.sign_data.take(); + } +} + +impl<'a> SignCallbackParam<'a> { + fn new( + object_path: &'a CStr, + description: Option<&'a CStr>, + public_key: &'a CStr, + key_hint: Option<&'a CStr>, + hash_algo: u32, + challenge: &'a [u8], + ) -> Self { + Self { + object_path: object_path.to_str().unwrap_or_default(), + description: description.map(|str| str.to_str().unwrap_or_default()), + public_key: public_key.to_str().unwrap_or_default(), + key_hint: key_hint.map(|str| str.to_str().unwrap_or_default()), + hash_algo: HashAlgorithm::from_id(TPM2_ALG_ID::try_from(hash_algo).unwrap_or_default()), + challenge, + } + } +} + +impl Debug for SignCallback { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("SignCallback") + .field("sign_fn", &self.sign_fn.type_id()) + .field("sign_data", &self.sign_data) + .finish() + } +} + +// ========================================================================== +// BranCallback +// ========================================================================== + +/// A callback function that allows the FAPI to request branch choices during policy evaluation. +/// +/// Registered to a FAPI context via the [`set_branch_callback()`](crate::FapiContext::set_branch_callback) function. +pub struct BranCallback { + bran_fn: Box Option + Send>, +} + +/// Wraps the parameters to be passed to the [`BranCallback`] callback's `bran_fn` function. +#[derive(Debug)] +pub struct BranCallbackParam<'a> { + /// Identifies the policy (path) being executed for which a branch choice is requested. + pub object_path: &'a str, + /// Description as provided in the policy definition (optional). + pub description: Option<&'a str>, + /// A list of human readable names for the branches as from the *PolicyOR* statement. + pub branches: Vec<&'a str>, +} + +impl BranCallback { + /// Creates a new callback instance. + /// + /// The supplied `bran_fn` will be called whenever a branch needs to be chosen during policy evaluation. Such choices take place when a policy contains a *PolicyOR* (with more than one branch), or a *PolicyAuthorize* (which has more than one approved policy). This function receives a [`BranCallbackParam`] as parameter; it shall return `Some(n)`, where ***n*** is the zero-based index of the chosen branch (must be less than or equal to `branches.len()-1`), or `None`, if **no** choice can be made. + pub fn new(bran_fn: impl Fn(BranCallbackParam) -> Option + 'static + Send) -> Self { + Self { + bran_fn: Box::new(bran_fn), + } + } + + /// Creates a new callback instance with additional data. + /// + /// The supplied `bran_fn` will be called whenever a branch needs to be chosen during policy evaluation. Such choices take place when a policy contains a *PolicyOR* (with more than one branch), or a *PolicyAuthorize* (which has more than one approved policy). This function receives a [`BranCallbackParam`] as parameter; it shall return `Some(n)`, where ***n*** is the zero-based index of the chosen branch (must be less than or equal to `branches.len()-1`), or `None`, if **no** choice can be made. + /// + /// The application-defined `extra_data` argument will be passed to each invocation of `bran_fn` as an additional parameter. + pub fn with_data( + bran_fn: impl Fn(BranCallbackParam, &T) -> Option + 'static + Send, + extra_data: T, + ) -> Self { + Self::new(move |callback_param| bran_fn(callback_param, &extra_data)) + } + + /// Request a signature for authorizing use of TPM objects from the application. + pub(crate) fn invoke( + &mut self, + object_path: &CStr, + description: Option<&CStr>, + branches: &[&CStr], + ) -> Option { + let param = BranCallbackParam::new(object_path, description, branches); + trace!("BranCallback::invoke({:?})", ¶m); + (self.bran_fn)(param).inspect(|index| { + if *index >= branches.len() { + panic!( + "The chosen branch index #{} is out of range! (must be in the 0..{} range)", + index, + branches.len() - 1usize + ); + } + }) + } +} + +impl<'a> BranCallbackParam<'a> { + fn new(object_path: &'a CStr, description: Option<&'a CStr>, branches: &'a [&CStr]) -> Self { + Self { + object_path: object_path.to_str().unwrap_or_default(), + description: description.map(|str| str.to_str().unwrap_or_default()), + branches: branches + .into_iter() + .map(|str| str.to_str().unwrap_or_default()) + .collect(), + } + } +} + +impl Debug for BranCallback { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("BranCallback") + .field("bran_fn", &self.bran_fn.type_id()) + .finish() + } +} + +// ========================================================================== +// ActnCallback +// ========================================================================== + +/// A callback function that allows the FAPI to notify the application about policy actions. +/// +/// Registered to a FAPI context via the [`set_policy_action_callback()`](crate::FapiContext::set_policy_action_callback) function. +pub struct ActnCallback { + actn_fn: Box bool + Send>, +} + +/// Wraps the parameters to be passed to the [`ActnCallback`] callback's `actn_fn` function. +#[derive(Debug)] +pub struct ActnCallbackParam<'a> { + /// Identifies the policy (path) being executed for which an action is signaled. + pub object_path: &'a str, + /// The action string as specified in the PolicyAction (optional). + pub action: Option<&'a str>, +} + +impl ActnCallback { + /// Creates a new callback instance. + /// + /// The supplied `actn_fn` will be called whenever a *PolicyAction* element is encountered during policy evaluation. The purpose and reaction to such an event is application dependent. This function receives a [`ActnCallbackParam`] as parameter. + pub fn new(actn_fn: impl Fn(ActnCallbackParam) -> bool + 'static + Send) -> Self { + Self { + actn_fn: Box::new(actn_fn), + } + } + + /// Creates a new callback instance with additional data. + /// + /// The supplied `actn_fn` will be called whenever a *PolicyAction* element is encountered during policy evaluation. The purpose and reaction to such an event is application dependent. This function receives a [`ActnCallbackParam`] as parameter. + /// + /// The application-defined `extra_data` argument will be passed to each invocation of `actn_fn` as an additional parameter. + pub fn with_data( + actn_fn: impl Fn(ActnCallbackParam, &T) -> bool + 'static + Send, + extra_data: T, + ) -> Self { + Self::new(move |callback_param| actn_fn(callback_param, &extra_data)) + } + + /// Request a signature for authorizing use of TPM objects from the application. + pub(crate) fn invoke(&mut self, object_path: &CStr, action: Option<&CStr>) -> bool { + let param = ActnCallbackParam::new(object_path, action); + trace!("ActnCallback::invoke({:?})", ¶m); + (self.actn_fn)(param) + } +} + +impl<'a> ActnCallbackParam<'a> { + fn new(object_path: &'a CStr, action: Option<&'a CStr>) -> Self { + Self { + object_path: object_path.to_str().unwrap_or_default(), + action: action.map(|str| str.to_str().unwrap_or_default()), + } + } +} + +impl Debug for ActnCallback { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("ActnCallback") + .field("actn_fn", &self.actn_fn.type_id()) + .finish() + } +} diff --git a/src/context.rs b/src/context.rs new file mode 100644 index 0000000..0fa5466 --- /dev/null +++ b/src/context.rs @@ -0,0 +1,1413 @@ +/* SPDX-License-Identifier: BSD-3-Clause */ +/******************************************************************************* + * Copyright 2024, Fraunhofer SIT sponsored by the ELISA research project + * All rights reserved. + ******************************************************************************/ + +use core::slice; +use json::{self, JsonValue}; +use std::{ + ffi::{c_char, CStr}, + fmt::Display, + num::NonZeroUsize, + os::raw::c_void, + ptr, +}; + +use crate::callback::{ActnCallback, AuthCallback, BranCallback, SignCallback}; +use crate::fapi_sys::{ + self, + constants::{self, TSS2_RC_SUCCESS}, + FAPI_CONTEXT, TPM2_RC, TSS2_RC, +}; +use crate::marshal::u64_from_be; +use crate::memory::{ + cond_out, cond_ptr, opt_to_len, opt_to_ptr, ptr_to_cstr_vec, ptr_to_opt_cstr, CStringHolder, + FapiMemoryHolder, +}; +use crate::{ + flags::flags_to_string, BaseErrorCode, BlobType, ErrorCode, InternalError, KeyFlags, NvFlags, + PaddingFlags, QuoteFlags, SealFlags, +}; + +/* Const */ +const ERR_NO_RESULT_DATA: ErrorCode = ErrorCode::InternalError(InternalError::NoResultData); +const ERR_INVALID_ARGUMENTS: ErrorCode = ErrorCode::InternalError(InternalError::InvalidArguments); + +/* Types */ +type TctiOpaqueContextBlob = *mut [u8; 0]; + +/// Wraps the native `FAPI_CONTEXT` and exposes the related FAPI functions. +/// +/// Each FAPI context represents a logically independent connection to the TPM. It stores meta data information about object in order to calculate session auths and similar things. +/// +/// A context is allocated and initialized using the [`new()`](FapiContext::new) function, while [dropping](std::ops::Drop) the object finalizes and de-allocates the context. +/// +/// *See also:* [`FAPI_CONTEXT()`](https://tpm2-tss.readthedocs.io/en/latest/struct_f_a_p_i___c_o_n_t_e_x_t.html) +/// +/// #### FAPI Specification +/// +/// Most functions provided by `FapiContext` are direct equivalents of the underlying FAPI library functions. +/// +/// For details, please refer to the TCG Feature API specification: +/// +/// +/// ###### Optional Parameters +/// +/// Some FAPI function have *optional* input parameters. These parameters are represented as [`Option`] on the Rust layer and they may be set to `None`, if they are **not** needed for a certain use-case. Set to `Some(value)`, if the parameter is actually used. +/// +/// ###### Return Values +/// +/// The FAPI functions have a return type of [`Result`] on the Rust layer. If the underlying native FAPI invocation has succeeded, then the variant `Ok(_)` wrapping the actual return value(s) is returned. Otherwise, if an error has occurred, the variant `Err(error)` containing the relevant error code is returned. Please refer to the [ErrorCode](crate::ErrorCode) enumeration for details! +/// +/// Some FAPI functions have *optional* return values (output parameters). On the rust layer, the call flags control whether these optional outputs shall be requested. The corresponding return values have type [`Option`] and will be `None` if unavailable. +/// +/// #### Thread Safety +/// +/// `FapiContext` implements the `Send` trait, so it may be transferred to another thread. However, it does **not** implement the `Sync` trait! Hence, in order to share a `FapiContext` between multiple concurrent threads, wrapping the context in an `Arc>` is required. This effectively ensures that *at most* **one** thread at a time can access the "shared" `FapiContext`. +/// +/// #### FAPI Library +/// +/// The `tss2-fapi-rs` wrapper requires that the *native* TSS2 2.0 FAPI library, e.g. `libtss2-fapi.so`, is available at runtime. +/// +/// Please refer to the [*prerequisites*](./index.html#prerequisites) section for details! +/// +/// #### FAPI Configuration +/// +/// The FAPI context *implicitly* uses the local FAPI configuration (e.g. `fapi-config.json`). +/// +/// Please set the environment variable **`TSS2_FAPICONF`** *before* creating a new FAPI context in order to change the path of the configuration file to be used by the TSS2 2.0 FAPI library. Otherwise, FAPI will fall back to the default path! +/// +/// A template for creating the FAPI configuration is provided in the `tests/data` directory, but a valid configuration is probably already available, if you installed the native TSS2 2.0 FAPI library via the operating system's package manager. +/// +/// #### FAPI Logging +/// +/// FAPI logging can be controlled by the environment variable **`TSS2_LOG`**, which can be useful for debugging purposes. +/// +/// Please set `TSS2_LOG` *before* creating a new FAPI context in order to change the log level of the TSS2 2.0 FAPI library. For example, set this variable to `all+debug` for more verbose outputs. A value of `all+none` will silence all debug outputs. +#[derive(Debug)] +pub struct FapiContext { + native_holder: NativeContextHolder, + callback_auth: Option>, + callback_sign: Option>, + callback_bran: Option>, + callback_actn: Option>, +} + +/// A struct that wraps the native C pointer to the underlying FAPI_CONTEXT instance +#[derive(Debug)] +pub struct NativeContextHolder { + native_context: *mut FAPI_CONTEXT, +} + +// ========================================================================== +// Helper functions +// ========================================================================== + +/// Create FAPI error code from base error code +macro_rules! mk_fapi_rc { + ($error:ident) => { + constants::TSS2_FEATURE_RC_LAYER | constants::$error + }; +} + +/// Assert that the given slice or slices are not empty +macro_rules! fail_if_empty { + ($slice:ident) => { + if <[_]>::is_empty($slice) { + return Err(ERR_INVALID_ARGUMENTS); + } + }; + ($slice:ident, $($next_slice:ident),+) => { + fail_if_empty!($slice); + fail_if_empty!($($next_slice),+) + } +} + +/// Assert that the given optional slice or slices are not empty (they may be None though!) +macro_rules! fail_if_opt_empty { + ($opt_slice:ident) => { + if let Some(slice) = $opt_slice.as_ref() { + fail_if_empty!(slice); + } + }; + ($opt_slice:ident, $($next_opt_slice:ident),+) => { + fail_if_opt_empty!($opt_slice); + fail_if_opt_empty!($($next_opt_slice),+) + } +} + +/// Create result from `TPM2_RC` error code +fn create_result_from_retval(error_code: TPM2_RC) -> Result<(), ErrorCode> { + match error_code { + TSS2_RC_SUCCESS => Ok(()), + _ => Err(ErrorCode::from_raw(error_code)), + } +} + +// ========================================================================== +// Context implementation +// ========================================================================== + +impl FapiContext { + /// Creates and initializes a `FAPI_CONTEXT` that holds all the state and metadata information during an interaction with the TPM. + /// + /// *See also:* [`Fapi_Initialize()`](https://tpm2-tss.readthedocs.io/en/latest/group___fapi___initialize.html) + pub fn new() -> Result { + let mut native_context: *mut FAPI_CONTEXT = ptr::null_mut(); + create_result_from_retval(unsafe { + fapi_sys::Fapi_Initialize(&mut native_context, ptr::null()) + }) + .map(|_| Self { + native_holder: NativeContextHolder::new(native_context), + callback_auth: None, + callback_sign: None, + callback_bran: None, + callback_actn: None, + }) + } + + // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + // Callback setters + // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + /// This function registers an application-defined function as a callback to allow the FAPI to request authorization values from the application. The callback is implemented via the [`AuthCallback`](crate::AuthCallback) struct. + /// + /// *See also:* [`Fapi_SetAuthCB()`](https://tpm2-tss.readthedocs.io/en/stable/group___fapi___set_auth_c_b.html) + pub fn set_auth_callback(&mut self, auth_callback: AuthCallback) -> Result<(), ErrorCode> { + let mut callback_box = Box::new(auth_callback); + self.fapi_call(|context| unsafe { + fapi_sys::Fapi_SetAuthCB(context, Some(auth_callback_entrypoint), callback_box.as_mut() as *mut AuthCallback as *mut c_void) + }).map(|_| { + self.callback_auth = Some(callback_box); /*store AuthCallback in FapiContext to keep it alive!*/ + }) + } + + /// This function registers an application-defined function as a callback to allow the FAPI to request signatures for authorizing the use of TPM objects from the application. The callback is implemented via the [`SignCallback`](crate::SignCallback) struct. + /// + /// *See also:* [`Fapi_SetSignCB()`](https://tpm2-tss.readthedocs.io/en/stable/group___fapi___set_sign_c_b.html) + pub fn set_sign_callback(&mut self, sign_callback: SignCallback) -> Result<(), ErrorCode> { + let mut callback_box = Box::new(sign_callback); + self.fapi_call(|context| unsafe { + fapi_sys::Fapi_SetSignCB(context, Some(sign_callback_entrypoint), callback_box.as_mut() as *mut SignCallback as *mut c_void) + }).map(|_| { + self.callback_sign = Some(callback_box); /*store SignCallback in FapiContext to keep it alive!*/ + }) + } + + /// This function registers an application-defined function as a callback to allow the FAPI to request branch choices from the application during policy evaluation. The callback is implemented via the [`BranCallback`](crate::BranCallback) struct. + /// + /// *See also:* [`Fapi_SetBranchCB()`](https://tpm2-tss.readthedocs.io/en/stable/group___fapi___set_sign_c_b.html) + pub fn set_branch_callback(&mut self, bran_callback: BranCallback) -> Result<(), ErrorCode> { + let mut callback_box = Box::new(bran_callback); + self.fapi_call(|context| unsafe { + fapi_sys::Fapi_SetBranchCB(context, Some(branch_callback_entrypoint), callback_box.as_mut() as *mut BranCallback as *mut c_void) + }).map(|_| { + self.callback_bran = Some(callback_box); /*store BranCallback in FapiContext to keep it alive!*/ + }) + } + + /// This function registers an application-defined function as a callback to allow the FAPI to notify the application about policy actions. The callback is implemented via the [`ActnCallback`](crate::ActnCallback) struct. + /// + /// *See also:* [`Fapi_SetPolicyActionCB()`](https://tpm2-tss.readthedocs.io/en/stable/group___fapi___set_sign_c_b.html) + pub fn set_policy_action_callback( + &mut self, + actn_callback: ActnCallback, + ) -> Result<(), ErrorCode> { + let mut callback_box = Box::new(actn_callback); + self.fapi_call(|context| unsafe { + fapi_sys::Fapi_SetPolicyActionCB(context, Some(action_callback_entrypoint), callback_box.as_mut() as *mut ActnCallback as *mut c_void) + }).map(|_| { + self.callback_actn = Some(callback_box); /*store BranCallback in FapiContext to keep it alive!*/ + }) + } + + // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + // Provisioning + // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + /// Provisions a TSS with its TPM. This includes the setting of important passwords and policy settings as well as the readout of the EK and its certificate and the initialization of the system-wide keystore. + /// + /// *See also:* [`Fapi_Provision()`](https://tpm2-tss.readthedocs.io/en/latest/group___fapi___provision.html) + pub fn provision( + &mut self, + auth_eh: Option<&str>, + auth_sh: Option<&str>, + auth_lo: Option<&str>, + ) -> Result<(), ErrorCode> { + let cstr_eh = CStringHolder::try_from(auth_eh)?; + let cstr_sh = CStringHolder::try_from(auth_sh)?; + let cstr_lo = CStringHolder::try_from(auth_lo)?; + + self.fapi_call(|context| unsafe { + fapi_sys::Fapi_Provision( + context, + cstr_eh.as_ptr(), + cstr_sh.as_ptr(), + cstr_lo.as_ptr(), + ) + }) + } + + // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + // Miscellaneous functions + // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + /// Returns a JSON value that identifies the versions of FAPI, TPM, configurations and other relevant information. + /// + /// *See also:* [`Fapi_GetInfo()`](https://tpm2-tss.readthedocs.io/en/latest/group___fapi___get_info.html) + pub fn get_info(&mut self) -> Result { + let mut tpm_info: *mut c_char = ptr::null_mut(); + self.fapi_call(|context| unsafe { fapi_sys::Fapi_GetInfo(context, &mut tpm_info) }) + .and_then(|_| { + FapiMemoryHolder::from_str(tpm_info) + .to_string() + .ok_or(ERR_NO_RESULT_DATA) + }) + .and_then(|info_data| json::parse(&info_data[..]).map_err(|_error| ERR_NO_RESULT_DATA)) + } + + /// Creates an array with a specified number of bytes. May execute the underlying TPM command multiple times if the requested number of bytes is too big. + /// + /// *See also:* [`Fapi_GetRandom()`](https://tpm2-tss.readthedocs.io/en/latest/group___fapi___get_random.html) + pub fn get_random(&mut self, length: NonZeroUsize) -> Result, ErrorCode> { + let mut data_ptr: *mut u8 = ptr::null_mut(); + self.fapi_call(|context| unsafe { + fapi_sys::Fapi_GetRandom(context, length.into(), &mut data_ptr) + }) + .and_then(|_| { + FapiMemoryHolder::from_raw(data_ptr, length.into()) + .to_vec() + .ok_or(ERR_NO_RESULT_DATA) + }) + } + + // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + // [Key and policy management functions ] + // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + /// Creates a key inside the TPM based on the Key type, using the supplied policy and authValue. The key is then stored either in the FAPI metadata store or the TPM. + /// + /// *See also:* [`Fapi_CreateKey()`](https://tpm2-tss.readthedocs.io/en/latest/group___fapi___create_key.html) + pub fn create_key( + &mut self, + key_path: &str, + key_type: Option<&[KeyFlags]>, + pol_path: Option<&str>, + auth_val: Option<&str>, + ) -> Result<(), ErrorCode> { + fail_if_opt_empty!(key_type); + + let cstr_path = CStringHolder::try_from(key_path)?; + let cstr_type = CStringHolder::try_from(flags_to_string(key_type)?)?; + let cstr_poli = CStringHolder::try_from(pol_path)?; + let cstr_auth = CStringHolder::try_from(auth_val)?; + + self.fapi_call(|context| unsafe { + fapi_sys::Fapi_CreateKey( + context, + cstr_path.as_ptr(), + cstr_type.as_ptr(), + cstr_poli.as_ptr(), + cstr_auth.as_ptr(), + ) + }) + } + + /// Imports a JSON encoded policy, policy template or key and stores it at the given path. + /// + /// *See also:* [`Fapi_Import()`](https://tpm2-tss.readthedocs.io/en/stable/group___fapi___import.html) + pub fn import(&mut self, path: &str, data: &JsonValue) -> Result<(), ErrorCode> { + let cstr_path = CStringHolder::try_from(path)?; + let cstr_data = CStringHolder::try_from(data)?; + + self.fapi_call(|context| unsafe { + fapi_sys::Fapi_Import(context, cstr_path.as_ptr(), cstr_data.as_ptr()) + }) + } + + /// Duplicates the key pointed to by `key_to_duplicate`, inclduing all keys below it, and encrypts it using the public key pointed to by `new_parent_public_key`. The exported data will contain the re-wrapped key and then the JSON encoded policy. + /// + /// If the parameter `new_parent_public_key` is `None`, then the *public key* pointed to by `key_to_duplicate` will be exported. + /// + /// *See also:* [`Fapi_ExportKey()`](https://tpm2-tss.readthedocs.io/en/stable/group___fapi___export_key.html) + pub fn export_key( + &mut self, + key_to_duplicate: &str, + new_parent_public_key: Option<&str>, + ) -> Result { + let cstr_duplicate = CStringHolder::try_from(key_to_duplicate)?; + let cstr_publickey = CStringHolder::try_from(new_parent_public_key)?; + let mut encoded_subtree: *mut c_char = ptr::null_mut(); + + self.fapi_call(|context| unsafe { + fapi_sys::Fapi_ExportKey( + context, + cstr_duplicate.as_ptr(), + cstr_publickey.as_ptr(), + &mut encoded_subtree, + ) + }) + .and_then(|_| { + FapiMemoryHolder::from_str(encoded_subtree) + .to_string() + .ok_or(ERR_NO_RESULT_DATA) + }) + .and_then(|exported_data| { + json::parse(&exported_data[..]).map_err(|_error| ERR_NO_RESULT_DATA) + }) + } + + /// Exports a policy to a JSON encoded byte buffer. + /// + /// *See also:* [`Fapi_ExportPolicy()`](https://tpm2-tss.readthedocs.io/en/stable/group___fapi___export_policy.html) + pub fn export_policy(&mut self, path: &str) -> Result { + let cstr_path = CStringHolder::try_from(path)?; + let mut policy: *mut c_char = ptr::null_mut(); + + self.fapi_call(|context| unsafe { + fapi_sys::Fapi_ExportPolicy(context, cstr_path.as_ptr(), &mut policy) + }) + .and_then(|_| { + FapiMemoryHolder::from_str(policy) + .to_string() + .ok_or(ERR_NO_RESULT_DATA) + }) + .and_then(|exported_data| { + json::parse(&exported_data[..]).map_err(|_error| ERR_NO_RESULT_DATA) + }) + } + + /// Get the public and private BLOBs of a TPM object. They can be loaded with a lower-level API such as the SAPI or the ESAPI. + /// + /// The flags `get_pubkey`, `get_privkey` and `get_policy` control which BLOBs are requested. Even if a BLOB was requested, that BLOB may *not* be available, in which case a `None` value is returned. + /// + /// *See also:* [`Fapi_GetTpmBlobs()`](https://tpm2-tss.readthedocs.io/en/stable/group___fapi___get_tpm_blobs.html) + /// + /// ###### Return Value + /// + /// **`(Option(public_key), Option(private_key), Option(policy))`** + pub fn get_tpm_blobs( + &mut self, + key_path: &str, + get_pubkey: bool, + get_privkey: bool, + get_policy: bool, + ) -> Result<(Option>, Option>, Option), ErrorCode> { + if !(get_pubkey || get_privkey || get_policy) { + return Err(ERR_INVALID_ARGUMENTS); /* must request at least one kind of BLOB! */ + } + + let cstr_path = CStringHolder::try_from(key_path)?; + + let mut blob_pub_data: *mut u8 = ptr::null_mut(); + let mut blob_pub_size: usize = 0; + let mut blob_sec_data: *mut u8 = ptr::null_mut(); + let mut blob_sec_size: usize = 0; + let mut policy_string: *mut c_char = ptr::null_mut(); + + self.fapi_call(|context| unsafe { + fapi_sys::Fapi_GetTpmBlobs( + context, + cstr_path.as_ptr(), + cond_out(&mut blob_pub_data, get_pubkey), + cond_ptr(&mut blob_pub_size, get_pubkey), + cond_out(&mut blob_sec_data, get_privkey), + cond_ptr(&mut blob_sec_size, get_privkey), + cond_out(&mut policy_string, get_policy), + ) + }) + .map(|_| { + ( + FapiMemoryHolder::from_raw(blob_pub_data, blob_pub_size).to_vec(), + FapiMemoryHolder::from_raw(blob_sec_data, blob_sec_size).to_vec(), + FapiMemoryHolder::from_str(policy_string).to_json(), + ) + }) + } + + /// Gets blobs of FAPI objects which can be used to create ESAPI objects. + /// + /// *See also:* [`Fapi_GetEsysBlob()`](https://tpm2-tss.readthedocs.io/en/stable/group___fapi___get_esys_blobs.html) + pub fn get_esys_blob(&mut self, path: &str) -> Result<(BlobType, Vec), ErrorCode> { + let cstr_path = CStringHolder::try_from(path)?; + let mut blob_type: u8 = 0; + let mut blob_data: *mut u8 = ptr::null_mut(); + let mut blob_size: usize = 0; + + self.fapi_call(|context| unsafe { + fapi_sys::Fapi_GetEsysBlob( + context, + cstr_path.as_ptr(), + &mut blob_type, + &mut blob_data, + &mut blob_size, + ) + }) + .and_then(|_| { + FapiMemoryHolder::from_raw(blob_data, blob_size) + .to_vec() + .ok_or(ERR_NO_RESULT_DATA) + }) + .and_then(|esys_blob| { + BlobType::try_from(blob_type) + .map(|type_flag| (type_flag, esys_blob)) + .map_err(|_| ERR_NO_RESULT_DATA) + }) + } + + // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + // [ NV index functions ] + // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + /// This command creates an NV index in the TPM using a given path and type. + /// + /// *See also:* [`Fapi_CreateNv()`](https://tpm2-tss.readthedocs.io/en/stable/group___fapi___create_nv.html) + pub fn create_nv( + &mut self, + nv_path: &str, + nvi_type: Option<&[NvFlags]>, + nvi_size: usize, + pol_path: Option<&str>, + auth_val: Option<&str>, + ) -> Result<(), ErrorCode> { + fail_if_opt_empty!(nvi_type); + + let cstr_path = CStringHolder::try_from(nv_path)?; + let cstr_type = CStringHolder::try_from(flags_to_string(nvi_type)?)?; + let cstr_poli = CStringHolder::try_from(pol_path)?; + let cstr_auth = CStringHolder::try_from(auth_val)?; + + self.fapi_call(|context| unsafe { + fapi_sys::Fapi_CreateNv( + context, + cstr_path.as_ptr(), + cstr_type.as_ptr(), + nvi_size, + cstr_poli.as_ptr(), + cstr_auth.as_ptr(), + ) + }) + } + + /// Reads data from an NV index within the TPM. + /// + /// The flag `request_log` controls whether the log data shall be requested, if the NV index was created as "pcr" type. + /// + /// *See also:* [`Fapi_NvRead()`](https://tpm2-tss.readthedocs.io/en/stable/group___fapi___nv_read.html) + /// + /// ###### Return Value + /// + /// **`(nv_data, Option(log_data))`** + pub fn nv_read( + &mut self, + nv_path: &str, + request_log: bool, + ) -> Result<(Vec, Option), ErrorCode> { + let cstr_path = CStringHolder::try_from(nv_path)?; + let mut data: *mut u8 = ptr::null_mut(); + let mut size: usize = 0; + let mut logs: *mut c_char = ptr::null_mut(); + + self.fapi_call(|context| unsafe { + fapi_sys::Fapi_NvRead( + context, + cstr_path.as_ptr(), + &mut data, + &mut size, + cond_out(&mut logs, request_log), + ) + }) + .and_then(|_| { + FapiMemoryHolder::from_raw(data, size) + .to_vec() + .ok_or(ERR_NO_RESULT_DATA) + }) + .and_then(|result| Ok((result, FapiMemoryHolder::from_str(logs).to_json()))) + } + + /// Convenience function to [`nv_read()`](FapiContext::nv_read) an **`u64`** value. Assumes "big endian" byte order. + /// + /// *See also:* [`Fapi_NvRead()`](https://tpm2-tss.readthedocs.io/en/stable/group___fapi___nv_read.html) + pub fn nv_read_u64(&mut self, nv_path: &str) -> Result { + self.nv_read(nv_path, false) + .map(|data| u64_from_be(&data.0[..])) + } + + /// Writes data to an "ordinary" (i.e *not* pin, extend or counter) NV index. + /// + /// *See also:* [`Fapi_NvWrite()`](https://tpm2-tss.readthedocs.io/en/stable/group___fapi___nv_write.html) + pub fn nv_write(&mut self, nv_path: &str, data: &[u8]) -> Result<(), ErrorCode> { + fail_if_empty!(data); + let cstr_path = CStringHolder::try_from(nv_path)?; + + self.fapi_call(|context| unsafe { + fapi_sys::Fapi_NvWrite(context, cstr_path.as_ptr(), data.as_ptr(), data.len()) + }) + } + + /// Convenience function to [`nv_write()`](FapiContext::nv_write) an **`u64`** value. Assumes "big endian" byte order. + /// + /// *See also:* [`Fapi_NvWrite()`](https://tpm2-tss.readthedocs.io/en/stable/group___fapi___nv_write.html) + pub fn nv_write_u64(&mut self, nv_path: &str, value: u64) -> Result<(), ErrorCode> { + self.nv_write(nv_path, &value.to_be_bytes()[..]) + } + + /// Increments an NV index that is a counter by 1. + /// + /// *See also:* [`Fapi_NvIncrement()`](https://tpm2-tss.readthedocs.io/en/stable/group___fapi___nv_increment.html) + pub fn nv_increment(&mut self, nv_path: &str) -> Result<(), ErrorCode> { + let cstr_path = CStringHolder::try_from(nv_path)?; + + self.fapi_call(|context| unsafe { fapi_sys::Fapi_NvIncrement(context, cstr_path.as_ptr()) }) + } + + /// Sets bits in an NV index that was created as a bit field. Any number of bits from 0 to 64 may be SET. + /// + /// *See also:* [`Fapi_NvSetBits()`](https://tpm2-tss.readthedocs.io/en/stable/group___fapi___nv_set_bits.html) + pub fn nv_set_bits(&mut self, nv_path: &str, bitmap: u64) -> Result<(), ErrorCode> { + let cstr_path = CStringHolder::try_from(nv_path)?; + + self.fapi_call(|context| unsafe { + fapi_sys::Fapi_NvSetBits(context, cstr_path.as_ptr(), bitmap) + }) + } + + /// Performs an extend operation on an NV index that was created as "pcr" type. + /// + /// *See also:* [`Fapi_NvExtend()`](https://tpm2-tss.readthedocs.io/en/latest/group___fapi___nv_extend.html) + pub fn nv_extend( + &mut self, + nv_path: &str, + data: &[u8], + log_data: Option<&JsonValue>, + ) -> Result<(), ErrorCode> { + fail_if_empty!(data); + + let cstr_path = CStringHolder::try_from(nv_path)?; + let cstr_logs = CStringHolder::try_from(log_data)?; + + self.fapi_call(|context| unsafe { + fapi_sys::Fapi_NvExtend( + context, + cstr_path.as_ptr(), + data.as_ptr(), + data.len(), + cstr_logs.as_ptr(), + ) + }) + } + + /// Write the policyDigest of a policy to an NV index so it can be used in policies containing PolicyAuthorizeNV elements. + /// + /// *See also:* [`Fapi_WriteAuthorizeNv()`](https://tpm2-tss.readthedocs.io/en/stable/group___fapi___write_authorize_nv.html) + pub fn write_authorize_nv(&mut self, nv_path: &str, pol_path: &str) -> Result<(), ErrorCode> { + let cstr_path = CStringHolder::try_from(nv_path)?; + let cstr_poli = CStringHolder::try_from(pol_path)?; + + self.fapi_call(|context| unsafe { + fapi_sys::Fapi_WriteAuthorizeNv(context, cstr_path.as_ptr(), cstr_poli.as_ptr()) + }) + } + + // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + // [ PCR functions ] + // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + /// Performs an extend operation on a given PCR. + /// + /// *See also:* [`Fapi_PcrExtend()`](https://tpm2-tss.readthedocs.io/en/stable/group___fapi___pcr_extend.html) + pub fn pcr_extend( + &mut self, + pcr_no: u32, + data: &[u8], + log_data: Option<&str>, + ) -> Result<(), ErrorCode> { + fail_if_empty!(data); + + let cstr_logs = CStringHolder::try_from(log_data)?; + + self.fapi_call(|context| unsafe { + fapi_sys::Fapi_PcrExtend( + context, + pcr_no, + data.as_ptr(), + data.len(), + cstr_logs.as_ptr(), + ) + }) + } + + /// Reads from a given PCR and returns the value and the event log. + /// + /// The flags `request_log` controls whether a PCR log shall be requested too. + /// + /// *See also:* [`Fapi_PcrRead()`](https://tpm2-tss.readthedocs.io/en/stable/group___fapi___pcr_read.html) + /// + /// ###### Return Value + /// + /// **`(pcr_value, Option(prc_log))`** + pub fn pcr_read( + &mut self, + pcr_no: u32, + request_log: bool, + ) -> Result<(Vec, Option), ErrorCode> { + let mut pcr_data: *mut u8 = ptr::null_mut(); + let mut pcr_size: usize = 0usize; + let mut log_data: *mut c_char = ptr::null_mut(); + + self.fapi_call(|context| unsafe { + fapi_sys::Fapi_PcrRead( + context, + pcr_no, + &mut pcr_data, + &mut pcr_size, + cond_out(&mut log_data, request_log), + ) + }) + .and_then(|_| { + FapiMemoryHolder::from_raw(pcr_data, pcr_size) + .to_vec() + .ok_or(ERR_NO_RESULT_DATA) + }) + .and_then(|result| Ok((result, FapiMemoryHolder::from_str(log_data).to_json()))) + } + + /// Given a set of PCRs and a restricted signing key, it will sign those PCRs and return the quote. + /// + /// The optional `qualifying_data` is a nonce provided by the caller to ensure freshness of the signature. + /// + /// The flags `request_log` and `request_cert` control whether to request PCR log and/or certificate. Even if requested, a signer certificate may *not* be available, in which case a `None` value is returned. + /// + /// *See also:* [`Fapi_Quote()`](https://tpm2-tss.readthedocs.io/en/stable/group___fapi___quote.html) + /// + /// ###### Return Value + /// + /// **`(quote_info, signature, Option(prc_log), Option(certificate))`** + pub fn quote( + &mut self, + pcr_no: &[u32], + quote_type: Option<&[QuoteFlags]>, + key_path: &str, + qualifying_data: Option<&[u8]>, + request_log: bool, + request_cert: bool, + ) -> Result<(JsonValue, Vec, Option, Option), ErrorCode> { + fail_if_empty!(pcr_no); + fail_if_opt_empty!(quote_type, qualifying_data); + + let cstr_path = CStringHolder::try_from(key_path)?; + let cstr_type = CStringHolder::try_from(flags_to_string(quote_type)?)?; + + let mut quote_info: *mut c_char = ptr::null_mut(); + let mut signature_data: *mut u8 = ptr::null_mut(); + let mut signature_size = 0usize; + let mut pcr_log: *mut c_char = ptr::null_mut(); + let mut certificate: *mut c_char = ptr::null_mut(); + + self.fapi_call(|context| unsafe { + fapi_sys::Fapi_Quote( + context, + pcr_no.as_ptr() as *mut u32, + pcr_no.len(), + cstr_path.as_ptr(), + cstr_type.as_ptr(), + opt_to_ptr(qualifying_data), + opt_to_len(qualifying_data), + &mut quote_info, + &mut signature_data, + &mut signature_size, + cond_out(&mut pcr_log, request_log), + cond_out(&mut certificate, request_cert), + ) + }) + .and_then(|_| { + FapiMemoryHolder::from_str(quote_info) + .to_json() + .ok_or(ERR_NO_RESULT_DATA) + }) + .and_then(|info| { + FapiMemoryHolder::from_raw(signature_data, signature_size) + .to_vec() + .map(|data| (info, data)) + .ok_or(ERR_NO_RESULT_DATA) + }) + .and_then(|signature| { + Ok(( + signature.0, + signature.1, + FapiMemoryHolder::from_str(pcr_log).to_json(), + FapiMemoryHolder::from_str(certificate).to_string(), + )) + }) + } + + /// Verifies that the data returned by a quote is valid. This includes: + /// - Reconstructing the `quote_info`’s PCR values from the `prc_log` (if a `prc_log` was provided) + /// - Verifying the `quote_info` using the signature and the public key at `key_path` + /// + /// *See also:* [`Fapi_VerifyQuote()`](https://tpm2-tss.readthedocs.io/en/stable/group___fapi___verify_quote.html) + pub fn verify_quote( + &mut self, + key_path: &str, + qualifying_data: Option<&[u8]>, + quote_info: &JsonValue, + signature: &[u8], + prc_log: Option<&JsonValue>, + ) -> Result { + fail_if_empty!(signature); + fail_if_opt_empty!(qualifying_data); + + let cstr_path = CStringHolder::try_from(key_path)?; + let cstr_info = CStringHolder::try_from(quote_info)?; + let cstr_logs = CStringHolder::try_from(prc_log)?; + + self.fapi_call(|context| unsafe { + fapi_sys::Fapi_VerifyQuote( + context, + cstr_path.as_ptr(), + opt_to_ptr(qualifying_data), + opt_to_len(qualifying_data), + cstr_info.as_ptr(), + signature.as_ptr(), + signature.len(), + cstr_logs.as_ptr(), + ) + }) + .map(|_| true) + .or_else(|error| match error { + ErrorCode::FapiError(BaseErrorCode::GeneralFailure) => Ok(false), + ErrorCode::FapiError(BaseErrorCode::SignatureVerificationFailed) => Ok(false), + _ => Err(error), + }) + } + + // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + // [ Encryption/decryption functions ] + // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + /// Encrypt the provided data for the target key using the TPM encryption schemes as specified in the crypto profile. This function does not use the TPM; i.e. works in non-TPM mode. + /// + /// *See also:* [`Fapi_Encrypt()`](https://tpm2-tss.readthedocs.io/en/stable/group___fapi___encrypt.html) + pub fn encrypt(&mut self, key_path: &str, plaintext: &[u8]) -> Result, ErrorCode> { + fail_if_empty!(plaintext); + + let cstr_path = CStringHolder::try_from(key_path)?; + + let mut ciphertext_data: *mut u8 = ptr::null_mut(); + let mut ciphertext_size: usize = 0; + + self.fapi_call(|context| unsafe { + fapi_sys::Fapi_Encrypt( + context, + cstr_path.as_ptr(), + plaintext.as_ptr(), + plaintext.len(), + &mut ciphertext_data, + &mut ciphertext_size, + ) + }) + .and_then(|_| { + FapiMemoryHolder::from_raw(ciphertext_data, ciphertext_size) + .to_vec() + .ok_or(ERR_NO_RESULT_DATA) + }) + } + + /// Decrypts data that was previously encrypted with [`encrypt()`](FapiContext::decrypt). + /// + /// *See also:* [`Fapi_Decrypt()`](https://tpm2-tss.readthedocs.io/en/stable/group___fapi___decrypt.html) + pub fn decrypt(&mut self, key_path: &str, ciphertext: &[u8]) -> Result, ErrorCode> { + fail_if_empty!(ciphertext); + + let cstr_path = CStringHolder::try_from(key_path)?; + + let mut plaintext_data: *mut u8 = ptr::null_mut(); + let mut plaintext_size: usize = 0; + + self.fapi_call(|context| unsafe { + fapi_sys::Fapi_Decrypt( + context, + cstr_path.as_ptr(), + ciphertext.as_ptr(), + ciphertext.len(), + &mut plaintext_data, + &mut plaintext_size, + ) + }) + .and_then(|_| { + FapiMemoryHolder::from_raw(plaintext_data, plaintext_size) + .to_vec() + .ok_or(ERR_NO_RESULT_DATA) + }) + } + + // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + // [ Signature functions ] + // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + /// Uses a key, identified by its path, to sign a digest and puts the result in a TPM2B bytestream. + /// + /// The flags `get_pubkey` and `get_cert` control whether the signer's public key and/or the signer certificate shall be returned. If requested, a signer certificate may *not* be available, in which case a `None` value is returned. + /// + /// Note that this function can **not** sign the "plain" message! Use, e.g., the [`sha2`](https://crates.io/crates/sha2) crate for computing the digest to be signed! + /// + /// *See also:* [`Fapi_Sign()`](https://tpm2-tss.readthedocs.io/en/latest/group___fapi___sign.html) + /// + /// ###### Return Value + /// + /// **`(signature_value, Option(public_key), Option(certificate))`** + pub fn sign( + &mut self, + key_path: &str, + pad_algo: Option<&[PaddingFlags]>, + digest: &[u8], + get_pubkey: bool, + get_cert: bool, + ) -> Result<(Vec, Option, Option), ErrorCode> { + fail_if_empty!(digest); + + let cstr_path = CStringHolder::try_from(key_path)?; + let cstr_algo = CStringHolder::try_from(flags_to_string(pad_algo)?)?; + + let mut signature_data: *mut u8 = ptr::null_mut(); + let mut signature_size: usize = 0; + let mut public_key_pem: *mut c_char = ptr::null_mut(); + let mut signer_crt_pem: *mut c_char = ptr::null_mut(); + + self.fapi_call(|context| unsafe { + fapi_sys::Fapi_Sign( + context, + cstr_path.as_ptr(), + cstr_algo.as_ptr(), + digest.as_ptr(), + digest.len(), + &mut signature_data, + &mut signature_size, + cond_out(&mut public_key_pem, get_pubkey), + cond_out(&mut signer_crt_pem, get_cert), + ) + }) + .and_then(|_| { + FapiMemoryHolder::from_raw(signature_data, signature_size) + .to_vec() + .ok_or(ERR_NO_RESULT_DATA) + }) + .map(|signature| { + ( + signature, + FapiMemoryHolder::from_str(public_key_pem).to_string(), + FapiMemoryHolder::from_str(signer_crt_pem).to_string(), + ) + }) + } + + /// Verifies a signature using a public key found in a keyPath. + /// + /// Returns `Ok(true)` if the signature is *valid*, `Ok(false)` if the signature is *invalid* (or malformed), and `Err(_)` if an error other than the signature value being invalid (or malformed) occurred during the verification process. + /// + /// *See also:* [`Fapi_VerifySignature()`](https://tpm2-tss.readthedocs.io/en/latest/group___fapi___verify_signature.html) + pub fn verify_signature( + &mut self, + key_path: &str, + digest: &[u8], + signature: &[u8], + ) -> Result { + fail_if_empty!(digest, signature); + + let cstr_path = CStringHolder::try_from(key_path)?; + + self.fapi_call(|context| unsafe { + fapi_sys::Fapi_VerifySignature( + context, + cstr_path.as_ptr(), + digest.as_ptr(), + digest.len(), + signature.as_ptr(), + signature.len(), + ) + }) + .map(|_| true) + .or_else(|error| match error { + ErrorCode::FapiError(BaseErrorCode::GeneralFailure) => Ok(false), + ErrorCode::FapiError(BaseErrorCode::SignatureVerificationFailed) => Ok(false), + _ => Err(error), + }) + } + + // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + // [ Sealing functions ] + // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + /// Creates a sealed object and stores it in the FAPI metadata store. If no data is provided, the TPM generates random data to fill the sealed object. + /// + /// *See also:* [`Fapi_CreateSeal()`](https://tpm2-tss.readthedocs.io/en/stable/group___fapi___create_seal.html) + pub fn create_seal( + &mut self, + path: &str, + seal_type: Option<&[SealFlags]>, + seal_size: NonZeroUsize, + pol_path: Option<&str>, + auth_val: Option<&str>, + data: Option<&[u8]>, + ) -> Result<(), ErrorCode> { + fail_if_opt_empty!(seal_type); + fail_if_opt_empty!(data); + + let cstr_path = CStringHolder::try_from(path)?; + let cstr_type = CStringHolder::try_from(flags_to_string(seal_type)?)?; + let cstr_poli = CStringHolder::try_from(pol_path)?; + let cstr_auth = CStringHolder::try_from(auth_val)?; + + self.fapi_call(|context| unsafe { + fapi_sys::Fapi_CreateSeal( + context, + cstr_path.as_ptr(), + cstr_type.as_ptr(), + seal_size.get(), + cstr_poli.as_ptr(), + cstr_auth.as_ptr(), + opt_to_ptr(data), + ) + }) + } + + /// Unseals data from a seal in the FAPI metadata store. + /// + /// *See also:* [`Fapi_Unseal()`](https://tpm2-tss.readthedocs.io/en/stable/group___fapi___unseal.html) + pub fn unseal(&mut self, path: &str) -> Result, ErrorCode> { + let cstr_path = CStringHolder::try_from(path)?; + let mut sealed_size: usize = 0; + let mut sealed_data: *mut u8 = ptr::null_mut(); + + self.fapi_call(|context| unsafe { + fapi_sys::Fapi_Unseal( + context, + cstr_path.as_ptr(), + &mut sealed_data, + &mut sealed_size, + ) + }) + .and_then(|_| { + FapiMemoryHolder::from_raw(sealed_data, sealed_size) + .to_vec() + .ok_or(ERR_NO_RESULT_DATA) + }) + } + + // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + // [ Certificate functions ] + // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + /// Gets an x.509 certificate for the key at a given path. + /// + /// *See also:* [`Fapi_GetCertificate()`](https://tpm2-tss.readthedocs.io/en/stable/group___fapi___get_certificate.html) + pub fn get_certificate(&mut self, path: &str) -> Result, ErrorCode> { + let cstr_path = CStringHolder::try_from(path)?; + let mut cert_data_pem: *mut c_char = ptr::null_mut(); + + self.fapi_call(|context| unsafe { + fapi_sys::Fapi_GetCertificate(context, cstr_path.as_ptr(), &mut cert_data_pem) + }) + .and_then(|_| Ok(FapiMemoryHolder::from_str(cert_data_pem).to_string())) + .or_else(|error| match error { + ErrorCode::FapiError(BaseErrorCode::NoCert) => Ok(None), + _ => Err(error), + }) + } + + /// Sets an x509 cert into the path of a key. + /// + /// If the parameter `cert_data` is `None`, then an existing certificate will be removed from the entity. + /// + /// *See also:* [`Fapi_SetCertificate()`](https://tpm2-tss.readthedocs.io/en/stable/group___fapi___set_certificate.html) + pub fn set_certificate( + &mut self, + path: &str, + cert_data: Option<&str>, + ) -> Result<(), ErrorCode> { + let cstr_path = CStringHolder::try_from(path)?; + let cstr_cert = CStringHolder::try_from(cert_data)?; + + self.fapi_call(|context| unsafe { + fapi_sys::Fapi_SetCertificate(context, cstr_path.as_ptr(), cstr_cert.as_ptr()) + }) + } + + /// Returns the set of Platform certificates concatenated in a continuous buffer, if the platform provides platform certificates. Platform certificates for TPM 2.0 can consist not only of a single certificate but also a series of so-called delta certificates. + /// + /// *See also:* [`Fapi_GetPlatformCertificates()`](https://tpm2-tss.readthedocs.io/en/stable/group___fapi___get_platform_certificates.html) + pub fn get_platform_certificates(&mut self) -> Result>, ErrorCode> { + let mut certificate_size: usize = 0; + let mut certificate_data: *mut u8 = ptr::null_mut(); + + self.fapi_call(|context| unsafe { + fapi_sys::Fapi_GetPlatformCertificates( + context, + &mut certificate_data, + &mut certificate_size, + ) + }) + .and_then(|_| { + FapiMemoryHolder::from_raw(certificate_data, certificate_size) + .to_vec() + .ok_or(ERR_NO_RESULT_DATA) + .map(Some) + }) + .or_else(|error| match error { + ErrorCode::FapiError(BaseErrorCode::NoCert) => Ok(None), + _ => Err(error), + }) + } + + // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + // [ Description and associated data functions ] + // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + /// Returns the description of a previously stored object. + /// + /// *See also:* [`Fapi_GetDescription()`](https://tpm2-tss.readthedocs.io/en/stable/group___fapi___get_description.html) + pub fn get_description(&mut self, path: &str) -> Result, ErrorCode> { + let cstr_path = CStringHolder::try_from(path)?; + let mut description: *mut c_char = ptr::null_mut(); + + self.fapi_call(|context| unsafe { + fapi_sys::Fapi_GetDescription(context, cstr_path.as_ptr(), &mut description) + }) + .and_then(|_| Ok(FapiMemoryHolder::from_str(description).to_string())) + } + + /// Allows an application to assign a human readable description to an object in the metadata store. + /// + /// If the parameter `description` is `None`, then any stored description assigned to the referenced object is deleted. + /// + /// *See also:* [`Fapi_SetDescription()`](https://tpm2-tss.readthedocs.io/en/stable/group___fapi___set_description.html) + pub fn set_description( + &mut self, + path: &str, + description: Option<&str>, + ) -> Result<(), ErrorCode> { + let cstr_path = CStringHolder::try_from(path)?; + let cstr_desc = CStringHolder::try_from(description)?; + + self.fapi_call(|context| unsafe { + fapi_sys::Fapi_SetDescription(context, cstr_path.as_ptr(), cstr_desc.as_ptr()) + }) + } + + /// Returns the previously stored application data for an object. + /// + /// *See also:* [`Fapi_GetAppData()`](https://tpm2-tss.readthedocs.io/en/stable/group___fapi___get_app_data.html) + pub fn get_app_data(&mut self, path: &str) -> Result>, ErrorCode> { + let cstr_path = CStringHolder::try_from(path)?; + let mut app_data_size: usize = 0; + let mut app_data_buff: *mut u8 = ptr::null_mut(); + + self.fapi_call(|context| unsafe { + fapi_sys::Fapi_GetAppData( + context, + cstr_path.as_ptr(), + &mut app_data_buff, + &mut app_data_size, + ) + }) + .and_then(|_| Ok(FapiMemoryHolder::from_raw(app_data_buff, app_data_size).to_vec())) + } + + /// Associates an arbitrary data blob with a given object. + /// + /// If the parameter `app_data` is `None`, then the stored data for the referenced object is erased. + /// + /// *See also:* [`Fapi_SetAppData()`](https://tpm2-tss.readthedocs.io/en/stable/group___fapi___set_app_data.html) + pub fn set_app_data(&mut self, path: &str, app_data: Option<&[u8]>) -> Result<(), ErrorCode> { + fail_if_opt_empty!(app_data); + let cstr_path = CStringHolder::try_from(path)?; + + self.fapi_call(|context| unsafe { + fapi_sys::Fapi_SetAppData( + context, + cstr_path.as_ptr(), + opt_to_ptr(app_data), + opt_to_len(app_data), + ) + }) + } + + // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + // [ Enumeration and deletion functions ] + // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + /// Enumerates all objects in the metadatastore in a given path and returns them in a list of complete paths. + /// + /// *See also:* [`Fapi_List()`](https://tpm2-tss.readthedocs.io/en/stable/group___fapi___list.html) + pub fn list(&mut self, path: &str) -> Result, ErrorCode> { + let cstr_path = CStringHolder::try_from(path)?; + let mut list: *mut c_char = ptr::null_mut(); + + self.fapi_call(|context| unsafe { + fapi_sys::Fapi_List(context, cstr_path.as_ptr(), &mut list) + }) + .and_then(|_| { + FapiMemoryHolder::from_str(list) + .to_string() + .map(|str| str.split(':').map(str::to_owned).collect()) + .ok_or(ERR_NO_RESULT_DATA) + }) + } + + /// Deletes a given key, policy or NV index from the system. + /// + /// *See also:* [`Fapi_Delete()`](https://tpm2-tss.readthedocs.io/en/stable/group___fapi___delete.html) + pub fn delete(&mut self, path: &str) -> Result<(), ErrorCode> { + let cstr_path = CStringHolder::try_from(path)?; + + self.fapi_call(|context| unsafe { fapi_sys::Fapi_Delete(context, cstr_path.as_ptr()) }) + } + + // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + // [ Auth functions ] + // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + /// Changes the authorization data of an entity found at key path. The parameter `auth` is a 0-terminated UTF-8 encoded password. If it is longer than the digest size of the entity's nameAlg, it will be hashed according the the TPM specification part 1, rev 138, section 19.6.4.3. + /// + /// If authValue is NULL then thep assword is set to the empty string. + /// + /// *See also:* [`Fapi_ChangeAuth()`](https://tpm2-tss.readthedocs.io/en/stable/group___fapi___change_auth.html) + pub fn change_auth(&mut self, path: &str, auth: Option<&str>) -> Result<(), ErrorCode> { + let cstr_path = CStringHolder::try_from(path)?; + let cstr_auth = CStringHolder::try_from(auth)?; + + self.fapi_call(|context| unsafe { + fapi_sys::Fapi_ChangeAuth(context, cstr_path.as_ptr(), cstr_auth.as_ptr()) + }) + } + + /// If a current policy happens to be a PolicyAuthorize, then for it to be used, the user must first satisfy a policy authorized by a having been signed (and made into a ticket) by an authorized party. + /// + /// *See also:* [`Fapi_AuthorizePolicy()`](https://tpm2-tss.readthedocs.io/en/stable/group___fapi___authorize_policy.html) + pub fn authorize_policy( + &mut self, + pol_path: &str, + key_path: &str, + ref_data: Option<&[u8]>, + ) -> Result<(), ErrorCode> { + fail_if_opt_empty!(ref_data); + + let cstr_path_pol = CStringHolder::try_from(pol_path)?; + let cstr_path_key = CStringHolder::try_from(key_path)?; + + self.fapi_call(|context| unsafe { + fapi_sys::Fapi_AuthorizePolicy( + context, + cstr_path_pol.as_ptr(), + cstr_path_key.as_ptr(), + opt_to_ptr(ref_data), + opt_to_len(ref_data), + ) + }) + } + + // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + // [ Interoperability functions ] + // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + /// Returns the `TSS2_TCTI_CONTEXT` currently used by this `FAPI_CONTEXT`. The purpose is to enable advanced access to the TPM that is currently being talked to. + /// + /// *See also:* [`Fapi_GetTcti()`](https://tpm2-tss.readthedocs.io/en/latest/group___fapi___get_tcti.html) + pub fn get_tcti(&mut self) -> Result { + let mut tcti: *mut fapi_sys::TSS2_TCTI_CONTEXT = ptr::null_mut(); + + self.fapi_call(|context| unsafe { fapi_sys::Fapi_GetTcti(context, &mut tcti) }) + .and_then(|_| Ok(tcti as TctiOpaqueContextBlob)) + } + + /// Returns an array of handles that can be polled on to get notified when data from the TPM or from a disk operation is available. + /// + /// *See also:* [`Fapi_GetPollHandles()`](https://tpm2-tss.readthedocs.io/en/stable/group___fapi___get_poll_handles.html) + /// + ///
+ /// + /// This functions is a stub. Currently, `Fapi_GetPollHandles()` is **not** implemented in the Rust wrapper library! + /// + ///
+ pub fn get_poll_handles(&mut self) -> Result, ErrorCode> { + todo!("Not implemented yet."); + } + + // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + // [ Internal functions ] + // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + /// Wrapper function that performs the actual FAPI invocation and checks the return value + fn fapi_call(&mut self, caller: F) -> Result<(), ErrorCode> + where + F: FnOnce(*mut FAPI_CONTEXT) -> TSS2_RC, + { + let error_code = caller(self.native_holder.get()); + self.clear_callbacks(); + create_result_from_retval(error_code) + } + + /// Clear temporary buffers of the callback, e.g. after FAPI invocation + fn clear_callbacks(&mut self) { + if let Some(cb) = &mut self.callback_auth { + cb.clear_buffer() + } + if let Some(cb) = &mut self.callback_sign { + cb.clear_buffer() + } + } +} + +impl Display for FapiContext { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "FapiContext({:p})", self.native_holder.get()) + } +} + +// ========================================================================== +// ContextHolder implementation +// ========================================================================== + +impl NativeContextHolder { + pub fn new(native_context: *mut FAPI_CONTEXT) -> Self { + Self { native_context } + } + + pub fn get(&self) -> *mut FAPI_CONTEXT { + assert!( + self.native_context != ptr::null_mut(), + "FAPI_CONTEXT is a NULL pointer!" + ); + self.native_context + } +} + +/// Free the native `FAPI_CONTEXT` as soon as the holder goes out of scope! +impl Drop for NativeContextHolder { + /// Finalizes a context by closing IPC/RPC connections and freeing its consumed memory. + /// + /// *See also:* [`Fapi_Finalize()`](https://tpm2-tss.readthedocs.io/en/latest/group___fapi___finalize.html) + fn drop(&mut self) { + unsafe { + fapi_sys::Fapi_Finalize(&mut self.native_context); + self.native_context = ptr::null_mut(); + } + } +} + +/// This allows `*mut FAPI_CONTEXT` to be moved between threads, but wrapping the `FapiContext` in an `Arc>` is still required! +unsafe impl Send for NativeContextHolder {} + +// ========================================================================== +// Callback functions +// ========================================================================== + +/// Invokes the actual AuthCallback struct +unsafe extern "C" fn auth_callback_entrypoint( + object_path: *const c_char, + description: *const c_char, + auth: *mut *const c_char, + user_data: *mut c_void, +) -> TSS2_RC { + if object_path == ptr::null() || auth == ptr::null_mut() || user_data == ptr::null_mut() { + return mk_fapi_rc!(TSS2_BASE_RC_BAD_VALUE); + } + match (&mut *(user_data as *mut AuthCallback)) + .invoke(CStr::from_ptr(object_path), ptr_to_opt_cstr(description)) + { + Some(auth_value) => { + *auth = auth_value.as_ptr(); + TSS2_RC_SUCCESS + } + _ => mk_fapi_rc!(TSS2_BASE_RC_GENERAL_FAILURE), + } +} + +/// Invokes the actual SignCallback struct +unsafe extern "C" fn sign_callback_entrypoint( + object_path: *const c_char, + description: *const c_char, + public_key: *const c_char, + key_hint: *const c_char, + hash_alg: u32, + challenge_data: *const u8, + challenge_size: usize, + signature_data: *mut *const u8, + signature_size: *mut usize, + user_data: *mut c_void, +) -> TSS2_RC { + if object_path == ptr::null() + || public_key == ptr::null() + || hash_alg == 0u32 + || challenge_data == ptr::null() + || challenge_size == 0usize + || signature_data == ptr::null_mut() + || signature_size == ptr::null_mut() + || user_data == ptr::null_mut() + { + return mk_fapi_rc!(TSS2_BASE_RC_BAD_VALUE); + } + match (&mut *(user_data as *mut SignCallback)).invoke( + CStr::from_ptr(object_path), + ptr_to_opt_cstr(description), + CStr::from_ptr(public_key), + ptr_to_opt_cstr(key_hint), + hash_alg, + slice::from_raw_parts(challenge_data, challenge_size), + ) { + Some(sign_value) => { + *signature_data = sign_value.as_ptr(); + *signature_size = sign_value.len(); + TSS2_RC_SUCCESS + } + _ => mk_fapi_rc!(TSS2_BASE_RC_GENERAL_FAILURE), + } +} + +/// Invokes the actual BranCallback struct +unsafe extern "C" fn branch_callback_entrypoint( + object_path: *const c_char, + description: *const c_char, + branch_names: *mut *const c_char, + num_branches: usize, + selected: *mut usize, + user_data: *mut c_void, +) -> TSS2_RC { + if object_path == ptr::null() + || branch_names == ptr::null_mut() + || num_branches == 0usize + || selected == ptr::null_mut() + || user_data == ptr::null_mut() + { + return mk_fapi_rc!(TSS2_BASE_RC_BAD_VALUE); + } + match (&mut *(user_data as *mut BranCallback)).invoke( + CStr::from_ptr(object_path), + ptr_to_opt_cstr(description), + &ptr_to_cstr_vec(branch_names, num_branches)[..], + ) { + Some(bran_value) => { + *selected = bran_value; + TSS2_RC_SUCCESS + } + _ => mk_fapi_rc!(TSS2_BASE_RC_GENERAL_FAILURE), + } +} + +/// Invokes the actual ActnCallback struct +unsafe extern "C" fn action_callback_entrypoint( + object_path: *const c_char, + action: *const c_char, + user_data: *mut c_void, +) -> TSS2_RC { + if object_path == ptr::null() || user_data == ptr::null_mut() { + return mk_fapi_rc!(TSS2_BASE_RC_BAD_VALUE); + } + match (&mut *(user_data as *mut ActnCallback)) + .invoke(CStr::from_ptr(object_path), ptr_to_opt_cstr(action)) + { + true => TSS2_RC_SUCCESS, + _ => mk_fapi_rc!(TSS2_BASE_RC_GENERAL_FAILURE), + } +} diff --git a/src/error.rs b/src/error.rs new file mode 100644 index 0000000..911ca54 --- /dev/null +++ b/src/error.rs @@ -0,0 +1,452 @@ +/* SPDX-License-Identifier: BSD-3-Clause */ +/******************************************************************************* + * Copyright 2024, Fraunhofer SIT sponsored by the ELISA research project + * All rights reserved. + ******************************************************************************/ + +use crate::fapi_sys::{constants, TSS2_RC}; + +const LAYER_BIT_MASK: u32 = 0x0000FFFF; + +/// The error type for FAPI operations, used by the [`FapiContext`](crate::FapiContext) struct. +#[derive(Debug)] +pub enum ErrorCode { + TpmError(Tpm2ErrorCode), + FapiError(BaseErrorCode), + EsysApiError(BaseErrorCode), + SysApiError(BaseErrorCode), + MuApiError(BaseErrorCode), + TctiError(BaseErrorCode), + ResMgrError(BaseErrorCode), + ResMgrTpmError(BaseErrorCode), + OtherError(BaseErrorCode), + InternalError(InternalError), +} + +/// Generic TSS2 error codes. +#[derive(Debug)] +pub enum BaseErrorCode { + GeneralFailure, + NotImplemented, + BadContext, + AbiMismatch, + BadReference, + InsufficientBuffer, + BadSequence, + NoConnection, + TryAgain, + IoError, + BadValue, + NotPermitted, + InvalidSessions, + NoDecryptParam, + NoEncryptParam, + BadSize, + MalformedResponse, + InsufficientContext, + InsufficientResponse, + IncompatibleTcti, + NotSupported, + BadTctiStructure, + Memory, + BadTr, + MultipleDecryptSessions, + MultipleEncryptSessions, + RspAuthFailed, + NoConfig, + BadPath, + NotDeletable, + PathAlreadyExists, + KeyNotFound, + SignatureVerificationFailed, + HashMismatch, + KeyNotDuplicable, + PathNotFound, + NoCert, + NoPcr, + PcrNotResettable, + BadTemplate, + AuthorizationFailed, + AuthorizationUnknown, + NvNotRadable, + NvTooSmall, + NvNotWriteable, + PolicyUnknown, + NvWrongType, + NameAlreadyExists, + NoTpm, + BadKey, + NoHandle, + NotProvisioned, + AlreadyProvisioned, + CallbackNull, + UnknownError(u32), +} + +/// TPM 2.0 response code wrapper. +#[derive(Debug)] +pub enum Tpm2ErrorCode { + Tpm2ErrFmt0(Tpm2ErrFmt0), + Tpm2ErrFmt1(Tpm2ErrFmt1), + Tpm2Warning(Tpm2Warning), + Other(u32), +} + +/// TPM 2.0 "format zero" response codes. +#[derive(Debug)] +pub enum Tpm2ErrFmt0 { + Initialize, + Failure, + Sequence, + Private, + Hmac, + Disabled, + Exclusive, + AuthType, + AuthMissing, + Policy, + Pcr, + PcrChanged, + Upgrade, + TooManyContexts, + AuthUnavailable, + Reboot, + Unbalanced, + CommandSize, + CommandCode, + AuthSize, + AuthContext, + NvRange, + NvSize, + NvLocked, + NvAuthorization, + NvUninitialized, + NvSpace, + NvDefined, + BadContext, + CpHash, + Parent, + NeedsTest, + NoResult, + Sensitive, + Unknown(u32), +} + +/// TPM 2.0 "format one" response codes. +#[derive(Debug)] +pub enum Tpm2ErrFmt1 { + Asymmetric, + Attributes, + Hash, + Value, + Hierarchy, + KeySize, + Mgf, + Mode, + Type, + Handle, + Kdf, + Range, + AuthFail, + Nonce, + PP, + Scheme, + Size, + Symmetric, + Tag, + Selector, + Insufficient, + Signature, + Key, + PolicyFail, + Integrity, + Ticket, + ReservedBits, + BadAuth, + Expired, + PolicyCC, + Binding, + Curve, + EccPoint, + Unknown(u32), +} + +/// TPM 2.0 "warning" response codes. +#[derive(Debug)] +pub enum Tpm2Warning { + ContextGap, + ObjectMemory, + SessionMemory, + Memory, + SessionHandles, + ObjectHandles, + Locality, + Yielded, + Cancelled, + Testing, + ReferenceH0, + ReferenceH1, + ReferenceH2, + ReferenceH3, + ReferenceH4, + ReferenceH5, + ReferenceH6, + ReferenceS0, + ReferenceS1, + ReferenceS2, + ReferenceS3, + ReferenceS4, + ReferenceS5, + ReferenceS6, + NvRate, + Lockout, + Retry, + NvUnavailable, + Other(u32), +} + +/// Internal error codes. +#[derive(Debug)] +pub enum InternalError { + NoResultData, + InvalidArguments, +} + +impl ErrorCode { + pub(crate) fn from_raw(error_code: TSS2_RC) -> Self { + let layer_code = error_code & !LAYER_BIT_MASK; + match layer_code { + constants::TSS2_TPM_RC_LAYER => { + ErrorCode::TpmError(Tpm2ErrorCode::from_raw(error_code)) + } + constants::TSS2_FEATURE_RC_LAYER => { + ErrorCode::FapiError(BaseErrorCode::from_raw(error_code)) + } + constants::TSS2_ESAPI_RC_LAYER => { + ErrorCode::EsysApiError(BaseErrorCode::from_raw(error_code)) + } + constants::TSS2_SYS_RC_LAYER => { + ErrorCode::SysApiError(BaseErrorCode::from_raw(error_code)) + } + constants::TSS2_MU_RC_LAYER => { + ErrorCode::MuApiError(BaseErrorCode::from_raw(error_code)) + } + constants::TSS2_TCTI_RC_LAYER => { + ErrorCode::TctiError(BaseErrorCode::from_raw(error_code)) + } + constants::TSS2_RESMGR_RC_LAYER => { + ErrorCode::ResMgrError(BaseErrorCode::from_raw(error_code)) + } + constants::TSS2_RESMGR_TPM_RC_LAYER => { + ErrorCode::ResMgrTpmError(BaseErrorCode::from_raw(error_code)) + } + _ => ErrorCode::OtherError(BaseErrorCode::from_raw(error_code)), + } + } +} + +impl BaseErrorCode { + pub(crate) fn from_raw(error_code: TSS2_RC) -> Self { + let base_error = error_code & LAYER_BIT_MASK; + match base_error { + constants::TSS2_BASE_RC_GENERAL_FAILURE => BaseErrorCode::GeneralFailure, + constants::TSS2_BASE_RC_NOT_IMPLEMENTED => BaseErrorCode::NotImplemented, + constants::TSS2_BASE_RC_BAD_CONTEXT => BaseErrorCode::BadContext, + constants::TSS2_BASE_RC_ABI_MISMATCH => BaseErrorCode::AbiMismatch, + constants::TSS2_BASE_RC_BAD_REFERENCE => BaseErrorCode::BadReference, + constants::TSS2_BASE_RC_INSUFFICIENT_BUFFER => BaseErrorCode::InsufficientBuffer, + constants::TSS2_BASE_RC_BAD_SEQUENCE => BaseErrorCode::BadSequence, + constants::TSS2_BASE_RC_NO_CONNECTION => BaseErrorCode::NoConnection, + constants::TSS2_BASE_RC_TRY_AGAIN => BaseErrorCode::TryAgain, + constants::TSS2_BASE_RC_IO_ERROR => BaseErrorCode::IoError, + constants::TSS2_BASE_RC_BAD_VALUE => BaseErrorCode::BadValue, + constants::TSS2_BASE_RC_NOT_PERMITTED => BaseErrorCode::NotPermitted, + constants::TSS2_BASE_RC_INVALID_SESSIONS => BaseErrorCode::InvalidSessions, + constants::TSS2_BASE_RC_NO_DECRYPT_PARAM => BaseErrorCode::NoDecryptParam, + constants::TSS2_BASE_RC_NO_ENCRYPT_PARAM => BaseErrorCode::NoEncryptParam, + constants::TSS2_BASE_RC_BAD_SIZE => BaseErrorCode::BadSize, + constants::TSS2_BASE_RC_MALFORMED_RESPONSE => BaseErrorCode::MalformedResponse, + constants::TSS2_BASE_RC_INSUFFICIENT_CONTEXT => BaseErrorCode::InsufficientContext, + constants::TSS2_BASE_RC_INSUFFICIENT_RESPONSE => BaseErrorCode::InsufficientResponse, + constants::TSS2_BASE_RC_INCOMPATIBLE_TCTI => BaseErrorCode::IncompatibleTcti, + constants::TSS2_BASE_RC_NOT_SUPPORTED => BaseErrorCode::NotSupported, + constants::TSS2_BASE_RC_BAD_TCTI_STRUCTURE => BaseErrorCode::BadTctiStructure, + constants::TSS2_BASE_RC_MEMORY => BaseErrorCode::Memory, + constants::TSS2_BASE_RC_BAD_TR => BaseErrorCode::BadTr, + constants::TSS2_BASE_RC_MULTIPLE_DECRYPT_SESSIONS => { + BaseErrorCode::MultipleDecryptSessions + } + constants::TSS2_BASE_RC_MULTIPLE_ENCRYPT_SESSIONS => { + BaseErrorCode::MultipleEncryptSessions + } + constants::TSS2_BASE_RC_RSP_AUTH_FAILED => BaseErrorCode::RspAuthFailed, + constants::TSS2_BASE_RC_NO_CONFIG => BaseErrorCode::NoConfig, + constants::TSS2_BASE_RC_BAD_PATH => BaseErrorCode::BadPath, + constants::TSS2_BASE_RC_NOT_DELETABLE => BaseErrorCode::NotDeletable, + constants::TSS2_BASE_RC_PATH_ALREADY_EXISTS => BaseErrorCode::PathAlreadyExists, + constants::TSS2_BASE_RC_KEY_NOT_FOUND => BaseErrorCode::KeyNotFound, + constants::TSS2_BASE_RC_SIGNATURE_VERIFICATION_FAILED => { + BaseErrorCode::SignatureVerificationFailed + } + constants::TSS2_BASE_RC_HASH_MISMATCH => BaseErrorCode::HashMismatch, + constants::TSS2_BASE_RC_KEY_NOT_DUPLICABLE => BaseErrorCode::KeyNotDuplicable, + constants::TSS2_BASE_RC_PATH_NOT_FOUND => BaseErrorCode::PathNotFound, + constants::TSS2_BASE_RC_NO_CERT => BaseErrorCode::NoCert, + constants::TSS2_BASE_RC_NO_PCR => BaseErrorCode::NoPcr, + constants::TSS2_BASE_RC_PCR_NOT_RESETTABLE => BaseErrorCode::PcrNotResettable, + constants::TSS2_BASE_RC_BAD_TEMPLATE => BaseErrorCode::BadTemplate, + constants::TSS2_BASE_RC_AUTHORIZATION_FAILED => BaseErrorCode::AuthorizationFailed, + constants::TSS2_BASE_RC_AUTHORIZATION_UNKNOWN => BaseErrorCode::AuthorizationUnknown, + constants::TSS2_BASE_RC_NV_NOT_READABLE => BaseErrorCode::NvNotRadable, + constants::TSS2_BASE_RC_NV_TOO_SMALL => BaseErrorCode::NvTooSmall, + constants::TSS2_BASE_RC_NV_NOT_WRITEABLE => BaseErrorCode::NvNotWriteable, + constants::TSS2_BASE_RC_POLICY_UNKNOWN => BaseErrorCode::PolicyUnknown, + constants::TSS2_BASE_RC_NV_WRONG_TYPE => BaseErrorCode::NvWrongType, + constants::TSS2_BASE_RC_NAME_ALREADY_EXISTS => BaseErrorCode::NameAlreadyExists, + constants::TSS2_BASE_RC_NO_TPM => BaseErrorCode::NoTpm, + constants::TSS2_BASE_RC_BAD_KEY => BaseErrorCode::BadKey, + constants::TSS2_BASE_RC_NO_HANDLE => BaseErrorCode::NoHandle, + constants::TSS2_BASE_RC_NOT_PROVISIONED => BaseErrorCode::NotProvisioned, + constants::TSS2_BASE_RC_ALREADY_PROVISIONED => BaseErrorCode::AlreadyProvisioned, + constants::TSS2_BASE_RC_CALLBACK_NULL => BaseErrorCode::CallbackNull, + _ => BaseErrorCode::UnknownError(base_error), + } + } +} + +impl Tpm2ErrorCode { + pub(crate) fn from_raw(error_code: TSS2_RC) -> Self { + let tpm2_error = error_code & LAYER_BIT_MASK; + if tpm2_error & 0x80 == 0x80 { + Tpm2ErrorCode::Tpm2ErrFmt1(Tpm2ErrFmt1::from_raw(tpm2_error)) /* if bit #7 is set -> "Format-One" error */ + } else if tpm2_error & 0x900 == 0x900 { + Tpm2ErrorCode::Tpm2Warning(Tpm2Warning::from_raw(tpm2_error)) /* ...otherwise, if bits #8 and #11 are set -> "Format-Zero" warning */ + } else if tpm2_error & 0x100 == 0x100 { + Tpm2ErrorCode::Tpm2ErrFmt0(Tpm2ErrFmt0::from_raw(tpm2_error)) /* ...otherwise, if bit #8 is set -> "Format-Zero" error */ + } else { + Tpm2ErrorCode::Other(tpm2_error) + } + } +} + +impl Tpm2ErrFmt0 { + pub(crate) fn from_raw(error_code: TSS2_RC) -> Self { + let tpm2_error = error_code & 0x7F; + match tpm2_error { + constants::TPM_RC_INITIALIZE => Tpm2ErrFmt0::Initialize, + constants::TPM_RC_FAILURE => Tpm2ErrFmt0::Failure, + constants::TPM_RC_SEQUENCE => Tpm2ErrFmt0::Sequence, + constants::TPM_RC_PRIVATE => Tpm2ErrFmt0::Private, + constants::TPM_RC_HMAC => Tpm2ErrFmt0::Hmac, + constants::TPM_RC_DISABLED => Tpm2ErrFmt0::Disabled, + constants::TPM_RC_EXCLUSIVE => Tpm2ErrFmt0::Exclusive, + constants::TPM_RC_AUTH_TYPE => Tpm2ErrFmt0::AuthType, + constants::TPM_RC_AUTH_MISSING => Tpm2ErrFmt0::AuthMissing, + constants::TPM_RC_POLICY => Tpm2ErrFmt0::Policy, + constants::TPM_RC_PCR => Tpm2ErrFmt0::Pcr, + constants::TPM_RC_PCR_CHANGED => Tpm2ErrFmt0::PcrChanged, + constants::TPM_RC_UPGRADE => Tpm2ErrFmt0::Upgrade, + constants::TPM_RC_TOO_MANY_CONTEXTS => Tpm2ErrFmt0::TooManyContexts, + constants::TPM_RC_AUTH_UNAVAILABLE => Tpm2ErrFmt0::AuthUnavailable, + constants::TPM_RC_REBOOT => Tpm2ErrFmt0::Reboot, + constants::TPM_RC_UNBALANCED => Tpm2ErrFmt0::Unbalanced, + constants::TPM_RC_COMMAND_SIZE => Tpm2ErrFmt0::CommandSize, + constants::TPM_RC_COMMAND_CODE => Tpm2ErrFmt0::CommandCode, + constants::TPM_RC_AUTHSIZE => Tpm2ErrFmt0::AuthSize, + constants::TPM_RC_AUTH_CONTEXT => Tpm2ErrFmt0::AuthContext, + constants::TPM_RC_NV_RANGE => Tpm2ErrFmt0::NvRange, + constants::TPM_RC_NV_SIZE => Tpm2ErrFmt0::NvSize, + constants::TPM_RC_NV_LOCKED => Tpm2ErrFmt0::NvLocked, + constants::TPM_RC_NV_AUTHORIZATION => Tpm2ErrFmt0::NvAuthorization, + constants::TPM_RC_NV_UNINITIALIZED => Tpm2ErrFmt0::NvUninitialized, + constants::TPM_RC_NV_SPACE => Tpm2ErrFmt0::NvSpace, + constants::TPM_RC_NV_DEFINED => Tpm2ErrFmt0::NvDefined, + constants::TPM_RC_BAD_CONTEXT => Tpm2ErrFmt0::BadContext, + constants::TPM_RC_CPHASH => Tpm2ErrFmt0::CpHash, + constants::TPM_RC_PARENT => Tpm2ErrFmt0::Parent, + constants::TPM_RC_NEEDS_TEST => Tpm2ErrFmt0::NeedsTest, + constants::TPM_RC_NO_RESULT => Tpm2ErrFmt0::NoResult, + constants::TPM_RC_SENSITIVE => Tpm2ErrFmt0::Sensitive, + _ => Tpm2ErrFmt0::Unknown(tpm2_error), + } + } +} + +impl Tpm2ErrFmt1 { + pub(crate) fn from_raw(error_code: TSS2_RC) -> Self { + let tpm2_error = error_code & 0x3F; + match tpm2_error { + constants::TPM_RC_ASYMMETRIC => Tpm2ErrFmt1::Asymmetric, + constants::TPM_RC_ATTRIBUTES => Tpm2ErrFmt1::Attributes, + constants::TPM_RC_HASH => Tpm2ErrFmt1::Hash, + constants::TPM_RC_VALUE => Tpm2ErrFmt1::Value, + constants::TPM_RC_HIERARCHY => Tpm2ErrFmt1::Hierarchy, + constants::TPM_RC_KEY_SIZE => Tpm2ErrFmt1::KeySize, + constants::TPM_RC_MGF => Tpm2ErrFmt1::Mgf, + constants::TPM_RC_MODE => Tpm2ErrFmt1::Mode, + constants::TPM_RC_TYPE => Tpm2ErrFmt1::Type, + constants::TPM_RC_HANDLE => Tpm2ErrFmt1::Handle, + constants::TPM_RC_KDF => Tpm2ErrFmt1::Kdf, + constants::TPM_RC_RANGE => Tpm2ErrFmt1::Range, + constants::TPM_RC_AUTH_FAIL => Tpm2ErrFmt1::AuthFail, + constants::TPM_RC_NONCE => Tpm2ErrFmt1::Nonce, + constants::TPM_RC_PP => Tpm2ErrFmt1::PP, + constants::TPM_RC_SCHEME => Tpm2ErrFmt1::Scheme, + constants::TPM_RC_SIZE => Tpm2ErrFmt1::Size, + constants::TPM_RC_SYMMETRIC => Tpm2ErrFmt1::Symmetric, + constants::TPM_RC_TAG => Tpm2ErrFmt1::Tag, + constants::TPM_RC_SELECTOR => Tpm2ErrFmt1::Selector, + constants::TPM_RC_INSUFFICIENT => Tpm2ErrFmt1::Insufficient, + constants::TPM_RC_SIGNATURE => Tpm2ErrFmt1::Signature, + constants::TPM_RC_KEY => Tpm2ErrFmt1::Key, + constants::TPM_RC_POLICY_FAIL => Tpm2ErrFmt1::PolicyFail, + constants::TPM_RC_INTEGRITY => Tpm2ErrFmt1::Integrity, + constants::TPM_RC_TICKET => Tpm2ErrFmt1::Ticket, + constants::TPM_RC_RESERVED_BITS => Tpm2ErrFmt1::ReservedBits, + constants::TPM_RC_BAD_AUTH => Tpm2ErrFmt1::BadAuth, + constants::TPM_RC_EXPIRED => Tpm2ErrFmt1::Expired, + constants::TPM_RC_POLICY_CC => Tpm2ErrFmt1::PolicyCC, + constants::TPM_RC_BINDING => Tpm2ErrFmt1::Binding, + constants::TPM_RC_CURVE => Tpm2ErrFmt1::Curve, + constants::TPM_RC_ECC_POINT => Tpm2ErrFmt1::EccPoint, + _ => Tpm2ErrFmt1::Unknown(tpm2_error), + } + } +} + +impl Tpm2Warning { + pub(crate) fn from_raw(error_code: TSS2_RC) -> Self { + let tpm2_error = error_code & 0x7F; + match tpm2_error { + constants::TPM_RC_CONTEXT_GAP => Tpm2Warning::ContextGap, + constants::TPM_RC_OBJECT_MEMORY => Tpm2Warning::ObjectMemory, + constants::TPM_RC_SESSION_MEMORY => Tpm2Warning::SessionMemory, + constants::TPM_RC_MEMORY => Tpm2Warning::Memory, + constants::TPM_RC_SESSION_HANDLES => Tpm2Warning::SessionHandles, + constants::TPM_RC_OBJECT_HANDLES => Tpm2Warning::ObjectHandles, + constants::TPM_RC_LOCALITY => Tpm2Warning::Locality, + constants::TPM_RC_YIELDED => Tpm2Warning::Yielded, + constants::TPM_RC_CANCELED => Tpm2Warning::Cancelled, + constants::TPM_RC_TESTING => Tpm2Warning::Testing, + constants::TPM_RC_REFERENCE_H0 => Tpm2Warning::ReferenceH0, + constants::TPM_RC_REFERENCE_H1 => Tpm2Warning::ReferenceH1, + constants::TPM_RC_REFERENCE_H2 => Tpm2Warning::ReferenceH2, + constants::TPM_RC_REFERENCE_H3 => Tpm2Warning::ReferenceH3, + constants::TPM_RC_REFERENCE_H4 => Tpm2Warning::ReferenceH4, + constants::TPM_RC_REFERENCE_H5 => Tpm2Warning::ReferenceH5, + constants::TPM_RC_REFERENCE_H6 => Tpm2Warning::ReferenceH6, + constants::TPM_RC_REFERENCE_S0 => Tpm2Warning::ReferenceS0, + constants::TPM_RC_REFERENCE_S1 => Tpm2Warning::ReferenceS1, + constants::TPM_RC_REFERENCE_S2 => Tpm2Warning::ReferenceS2, + constants::TPM_RC_REFERENCE_S3 => Tpm2Warning::ReferenceS3, + constants::TPM_RC_REFERENCE_S4 => Tpm2Warning::ReferenceS4, + constants::TPM_RC_REFERENCE_S5 => Tpm2Warning::ReferenceS5, + constants::TPM_RC_REFERENCE_S6 => Tpm2Warning::ReferenceS6, + constants::TPM_RC_NV_RATE => Tpm2Warning::NvRate, + constants::TPM_RC_LOCKOUT => Tpm2Warning::Lockout, + constants::TPM_RC_RETRY => Tpm2Warning::Retry, + constants::TPM_RC_NV_UNAVAILABLE => Tpm2Warning::NvUnavailable, + _ => Tpm2Warning::Other(tpm2_error), + } + } +} diff --git a/src/fapi_sys/constants.rs b/src/fapi_sys/constants.rs new file mode 100644 index 0000000..aae126a --- /dev/null +++ b/src/fapi_sys/constants.rs @@ -0,0 +1,222 @@ +/* SPDX-License-Identifier: BSD-3-Clause */ +/******************************************************************************* + * Copyright 2024, Fraunhofer SIT sponsored by the ELISA research project + * All rights reserved. + ******************************************************************************/ + +#![allow(unused)] + +use crate::fapi_sys::{TPM2_ALG_ID, TSS2_RC}; + +/* General error codes */ +pub const TSS2_RC_SUCCESS: TSS2_RC = 0x00000000; + +/* Layer codes */ +pub const TSS2_TPM_RC_LAYER: TSS2_RC = 0x00000000; +pub const TSS2_FEATURE_RC_LAYER: TSS2_RC = 0x00060000; +pub const TSS2_ESAPI_RC_LAYER: TSS2_RC = 0x00070000; +pub const TSS2_SYS_RC_LAYER: TSS2_RC = 0x00080000; +pub const TSS2_MU_RC_LAYER: TSS2_RC = 0x00090000; +pub const TSS2_TCTI_RC_LAYER: TSS2_RC = 0x00100000; +pub const TSS2_RESMGR_RC_LAYER: TSS2_RC = 0x00110000; +pub const TSS2_RESMGR_TPM_RC_LAYER: TSS2_RC = 0x00120000; + +/* Error codes */ +pub const TSS2_BASE_RC_GENERAL_FAILURE: TSS2_RC = 1; +pub const TSS2_BASE_RC_NOT_IMPLEMENTED: TSS2_RC = 2; +pub const TSS2_BASE_RC_BAD_CONTEXT: TSS2_RC = 3; +pub const TSS2_BASE_RC_ABI_MISMATCH: TSS2_RC = 4; +pub const TSS2_BASE_RC_BAD_REFERENCE: TSS2_RC = 5; +pub const TSS2_BASE_RC_INSUFFICIENT_BUFFER: TSS2_RC = 6; +pub const TSS2_BASE_RC_BAD_SEQUENCE: TSS2_RC = 7; +pub const TSS2_BASE_RC_NO_CONNECTION: TSS2_RC = 8; +pub const TSS2_BASE_RC_TRY_AGAIN: TSS2_RC = 9; +pub const TSS2_BASE_RC_IO_ERROR: TSS2_RC = 10; +pub const TSS2_BASE_RC_BAD_VALUE: TSS2_RC = 11; +pub const TSS2_BASE_RC_NOT_PERMITTED: TSS2_RC = 12; +pub const TSS2_BASE_RC_INVALID_SESSIONS: TSS2_RC = 13; +pub const TSS2_BASE_RC_NO_DECRYPT_PARAM: TSS2_RC = 14; +pub const TSS2_BASE_RC_NO_ENCRYPT_PARAM: TSS2_RC = 15; +pub const TSS2_BASE_RC_BAD_SIZE: TSS2_RC = 16; +pub const TSS2_BASE_RC_MALFORMED_RESPONSE: TSS2_RC = 17; +pub const TSS2_BASE_RC_INSUFFICIENT_CONTEXT: TSS2_RC = 18; +pub const TSS2_BASE_RC_INSUFFICIENT_RESPONSE: TSS2_RC = 19; +pub const TSS2_BASE_RC_INCOMPATIBLE_TCTI: TSS2_RC = 20; +pub const TSS2_BASE_RC_NOT_SUPPORTED: TSS2_RC = 21; +pub const TSS2_BASE_RC_BAD_TCTI_STRUCTURE: TSS2_RC = 22; +pub const TSS2_BASE_RC_MEMORY: TSS2_RC = 23; +pub const TSS2_BASE_RC_BAD_TR: TSS2_RC = 24; +pub const TSS2_BASE_RC_MULTIPLE_DECRYPT_SESSIONS: TSS2_RC = 25; +pub const TSS2_BASE_RC_MULTIPLE_ENCRYPT_SESSIONS: TSS2_RC = 26; +pub const TSS2_BASE_RC_RSP_AUTH_FAILED: TSS2_RC = 27; +pub const TSS2_BASE_RC_NO_CONFIG: TSS2_RC = 28; +pub const TSS2_BASE_RC_BAD_PATH: TSS2_RC = 29; +pub const TSS2_BASE_RC_NOT_DELETABLE: TSS2_RC = 30; +pub const TSS2_BASE_RC_PATH_ALREADY_EXISTS: TSS2_RC = 31; +pub const TSS2_BASE_RC_KEY_NOT_FOUND: TSS2_RC = 32; +pub const TSS2_BASE_RC_SIGNATURE_VERIFICATION_FAILED: TSS2_RC = 33; +pub const TSS2_BASE_RC_HASH_MISMATCH: TSS2_RC = 34; +pub const TSS2_BASE_RC_KEY_NOT_DUPLICABLE: TSS2_RC = 35; +pub const TSS2_BASE_RC_PATH_NOT_FOUND: TSS2_RC = 36; +pub const TSS2_BASE_RC_NO_CERT: TSS2_RC = 37; +pub const TSS2_BASE_RC_NO_PCR: TSS2_RC = 38; +pub const TSS2_BASE_RC_PCR_NOT_RESETTABLE: TSS2_RC = 39; +pub const TSS2_BASE_RC_BAD_TEMPLATE: TSS2_RC = 40; +pub const TSS2_BASE_RC_AUTHORIZATION_FAILED: TSS2_RC = 41; +pub const TSS2_BASE_RC_AUTHORIZATION_UNKNOWN: TSS2_RC = 42; +pub const TSS2_BASE_RC_NV_NOT_READABLE: TSS2_RC = 43; +pub const TSS2_BASE_RC_NV_TOO_SMALL: TSS2_RC = 44; +pub const TSS2_BASE_RC_NV_NOT_WRITEABLE: TSS2_RC = 45; +pub const TSS2_BASE_RC_POLICY_UNKNOWN: TSS2_RC = 46; +pub const TSS2_BASE_RC_NV_WRONG_TYPE: TSS2_RC = 47; +pub const TSS2_BASE_RC_NAME_ALREADY_EXISTS: TSS2_RC = 48; +pub const TSS2_BASE_RC_NO_TPM: TSS2_RC = 49; +pub const TSS2_BASE_RC_BAD_KEY: TSS2_RC = 50; +pub const TSS2_BASE_RC_NO_HANDLE: TSS2_RC = 51; +pub const TSS2_BASE_RC_NOT_PROVISIONED: TSS2_RC = 52; +pub const TSS2_BASE_RC_ALREADY_PROVISIONED: TSS2_RC = 53; +pub const TSS2_BASE_RC_CALLBACK_NULL: TSS2_RC = 54; + +/* TPM 2.0 Format-Zero response codes */ +pub const TPM_RC_INITIALIZE: TSS2_RC = 0x00; +pub const TPM_RC_FAILURE: TSS2_RC = 0x01; +pub const TPM_RC_SEQUENCE: TSS2_RC = 0x03; +pub const TPM_RC_PRIVATE: TSS2_RC = 0x0B; +pub const TPM_RC_HMAC: TSS2_RC = 0x19; +pub const TPM_RC_DISABLED: TSS2_RC = 0x20; +pub const TPM_RC_EXCLUSIVE: TSS2_RC = 0x21; +pub const TPM_RC_AUTH_TYPE: TSS2_RC = 0x24; +pub const TPM_RC_AUTH_MISSING: TSS2_RC = 0x25; +pub const TPM_RC_POLICY: TSS2_RC = 0x26; +pub const TPM_RC_PCR: TSS2_RC = 0x27; +pub const TPM_RC_PCR_CHANGED: TSS2_RC = 0x28; +pub const TPM_RC_UPGRADE: TSS2_RC = 0x2D; +pub const TPM_RC_TOO_MANY_CONTEXTS: TSS2_RC = 0x2E; +pub const TPM_RC_AUTH_UNAVAILABLE: TSS2_RC = 0x2F; +pub const TPM_RC_REBOOT: TSS2_RC = 0x30; +pub const TPM_RC_UNBALANCED: TSS2_RC = 0x31; +pub const TPM_RC_COMMAND_SIZE: TSS2_RC = 0x42; +pub const TPM_RC_COMMAND_CODE: TSS2_RC = 0x43; +pub const TPM_RC_AUTHSIZE: TSS2_RC = 0x44; +pub const TPM_RC_AUTH_CONTEXT: TSS2_RC = 0x45; +pub const TPM_RC_NV_RANGE: TSS2_RC = 0x46; +pub const TPM_RC_NV_SIZE: TSS2_RC = 0x47; +pub const TPM_RC_NV_LOCKED: TSS2_RC = 0x48; +pub const TPM_RC_NV_AUTHORIZATION: TSS2_RC = 0x49; +pub const TPM_RC_NV_UNINITIALIZED: TSS2_RC = 0x4A; +pub const TPM_RC_NV_SPACE: TSS2_RC = 0x4B; +pub const TPM_RC_NV_DEFINED: TSS2_RC = 0x4C; +pub const TPM_RC_BAD_CONTEXT: TSS2_RC = 0x50; +pub const TPM_RC_CPHASH: TSS2_RC = 0x51; +pub const TPM_RC_PARENT: TSS2_RC = 0x52; +pub const TPM_RC_NEEDS_TEST: TSS2_RC = 0x53; +pub const TPM_RC_NO_RESULT: TSS2_RC = 0x54; +pub const TPM_RC_SENSITIVE: TSS2_RC = 0x55; + +/* TPM 2.0 Format-One response codes */ +pub const TPM_RC_ASYMMETRIC: TSS2_RC = 0x01; +pub const TPM_RC_ATTRIBUTES: TSS2_RC = 0x02; +pub const TPM_RC_HASH: TSS2_RC = 0x03; +pub const TPM_RC_VALUE: TSS2_RC = 0x04; +pub const TPM_RC_HIERARCHY: TSS2_RC = 0x05; +pub const TPM_RC_KEY_SIZE: TSS2_RC = 0x07; +pub const TPM_RC_MGF: TSS2_RC = 0x08; +pub const TPM_RC_MODE: TSS2_RC = 0x09; +pub const TPM_RC_TYPE: TSS2_RC = 0x0A; +pub const TPM_RC_HANDLE: TSS2_RC = 0x0B; +pub const TPM_RC_KDF: TSS2_RC = 0x0C; +pub const TPM_RC_RANGE: TSS2_RC = 0x0D; +pub const TPM_RC_AUTH_FAIL: TSS2_RC = 0x0E; +pub const TPM_RC_NONCE: TSS2_RC = 0x0F; +pub const TPM_RC_PP: TSS2_RC = 0x10; +pub const TPM_RC_SCHEME: TSS2_RC = 0x12; +pub const TPM_RC_SIZE: TSS2_RC = 0x15; +pub const TPM_RC_SYMMETRIC: TSS2_RC = 0x16; +pub const TPM_RC_TAG: TSS2_RC = 0x17; +pub const TPM_RC_SELECTOR: TSS2_RC = 0x18; +pub const TPM_RC_INSUFFICIENT: TSS2_RC = 0x1A; +pub const TPM_RC_SIGNATURE: TSS2_RC = 0x1B; +pub const TPM_RC_KEY: TSS2_RC = 0x1C; +pub const TPM_RC_POLICY_FAIL: TSS2_RC = 0x1D; +pub const TPM_RC_INTEGRITY: TSS2_RC = 0x1F; +pub const TPM_RC_TICKET: TSS2_RC = 0x20; +pub const TPM_RC_RESERVED_BITS: TSS2_RC = 0x21; +pub const TPM_RC_BAD_AUTH: TSS2_RC = 0x22; +pub const TPM_RC_EXPIRED: TSS2_RC = 0x23; +pub const TPM_RC_POLICY_CC: TSS2_RC = 0x24; +pub const TPM_RC_BINDING: TSS2_RC = 0x25; +pub const TPM_RC_CURVE: TSS2_RC = 0x26; +pub const TPM_RC_ECC_POINT: TSS2_RC = 0x27; + +/* TPM 2.0 Warning response codes */ +pub const TPM_RC_CONTEXT_GAP: TSS2_RC = 0x01; +pub const TPM_RC_OBJECT_MEMORY: TSS2_RC = 0x02; +pub const TPM_RC_SESSION_MEMORY: TSS2_RC = 0x03; +pub const TPM_RC_MEMORY: TSS2_RC = 0x04; +pub const TPM_RC_SESSION_HANDLES: TSS2_RC = 0x05; +pub const TPM_RC_OBJECT_HANDLES: TSS2_RC = 0x06; +pub const TPM_RC_LOCALITY: TSS2_RC = 0x07; +pub const TPM_RC_YIELDED: TSS2_RC = 0x08; +pub const TPM_RC_CANCELED: TSS2_RC = 0x09; +pub const TPM_RC_TESTING: TSS2_RC = 0x0A; +pub const TPM_RC_REFERENCE_H0: TSS2_RC = 0x10; +pub const TPM_RC_REFERENCE_H1: TSS2_RC = 0x11; +pub const TPM_RC_REFERENCE_H2: TSS2_RC = 0x12; +pub const TPM_RC_REFERENCE_H3: TSS2_RC = 0x13; +pub const TPM_RC_REFERENCE_H4: TSS2_RC = 0x14; +pub const TPM_RC_REFERENCE_H5: TSS2_RC = 0x15; +pub const TPM_RC_REFERENCE_H6: TSS2_RC = 0x16; +pub const TPM_RC_REFERENCE_S0: TSS2_RC = 0x18; +pub const TPM_RC_REFERENCE_S1: TSS2_RC = 0x19; +pub const TPM_RC_REFERENCE_S2: TSS2_RC = 0x1A; +pub const TPM_RC_REFERENCE_S3: TSS2_RC = 0x1B; +pub const TPM_RC_REFERENCE_S4: TSS2_RC = 0x1C; +pub const TPM_RC_REFERENCE_S5: TSS2_RC = 0x1D; +pub const TPM_RC_REFERENCE_S6: TSS2_RC = 0x1E; +pub const TPM_RC_NV_RATE: TSS2_RC = 0x20; +pub const TPM_RC_LOCKOUT: TSS2_RC = 0x21; +pub const TPM_RC_RETRY: TSS2_RC = 0x22; +pub const TPM_RC_NV_UNAVAILABLE: TSS2_RC = 0x23; + +/* TPM 2.0 algorithm identifiers */ +pub const TPM2_ALG_ERROR: TPM2_ALG_ID = 0x00; +pub const TPM2_ALG_RSA: TPM2_ALG_ID = 0x01; +pub const TPM2_ALG_TDES: TPM2_ALG_ID = 0x03; +pub const TPM2_ALG_SHA: TPM2_ALG_ID = 0x04; +pub const TPM2_ALG_SHA1: TPM2_ALG_ID = 0x04; +pub const TPM2_ALG_HMAC: TPM2_ALG_ID = 0x05; +pub const TPM2_ALG_AES: TPM2_ALG_ID = 0x06; +pub const TPM2_ALG_MGF1: TPM2_ALG_ID = 0x07; +pub const TPM2_ALG_KEYEDHASH: TPM2_ALG_ID = 0x08; +pub const TPM2_ALG_XOR: TPM2_ALG_ID = 0x0A; +pub const TPM2_ALG_SHA256: TPM2_ALG_ID = 0x0B; +pub const TPM2_ALG_SHA384: TPM2_ALG_ID = 0x0C; +pub const TPM2_ALG_SHA512: TPM2_ALG_ID = 0x0D; +pub const TPM2_ALG_NULL: TPM2_ALG_ID = 0x10; +pub const TPM2_ALG_SM3_256: TPM2_ALG_ID = 0x12; +pub const TPM2_ALG_SM4: TPM2_ALG_ID = 0x13; +pub const TPM2_ALG_RSASSA: TPM2_ALG_ID = 0x14; +pub const TPM2_ALG_RSAES: TPM2_ALG_ID = 0x15; +pub const TPM2_ALG_RSAPSS: TPM2_ALG_ID = 0x16; +pub const TPM2_ALG_OAEP: TPM2_ALG_ID = 0x17; +pub const TPM2_ALG_ECDSA: TPM2_ALG_ID = 0x18; +pub const TPM2_ALG_ECDH: TPM2_ALG_ID = 0x19; +pub const TPM2_ALG_ECDAA: TPM2_ALG_ID = 0x1A; +pub const TPM2_ALG_SM2: TPM2_ALG_ID = 0x1B; +pub const TPM2_ALG_ECSCHNORR: TPM2_ALG_ID = 0x1C; +pub const TPM2_ALG_ECMQV: TPM2_ALG_ID = 0x1D; +pub const TPM2_ALG_KDF1_SP800_56A: TPM2_ALG_ID = 0x20; +pub const TPM2_ALG_KDF2: TPM2_ALG_ID = 0x21; +pub const TPM2_ALG_KDF1_SP800_108: TPM2_ALG_ID = 0x22; +pub const TPM2_ALG_ECC: TPM2_ALG_ID = 0x23; +pub const TPM2_ALG_SYMCIPHER: TPM2_ALG_ID = 0x25; +pub const TPM2_ALG_CAMELLIA: TPM2_ALG_ID = 0x26; +pub const TPM2_ALG_CMAC: TPM2_ALG_ID = 0x3F; +pub const TPM2_ALG_CTR: TPM2_ALG_ID = 0x40; +pub const TPM2_ALG_SHA3_256: TPM2_ALG_ID = 0x27; +pub const TPM2_ALG_SHA3_384: TPM2_ALG_ID = 0x28; +pub const TPM2_ALG_SHA3_512: TPM2_ALG_ID = 0x29; +pub const TPM2_ALG_OFB: TPM2_ALG_ID = 0x41; +pub const TPM2_ALG_CBC: TPM2_ALG_ID = 0x42; +pub const TPM2_ALG_CFB: TPM2_ALG_ID = 0x43; +pub const TPM2_ALG_ECB: TPM2_ALG_ID = 0x44; diff --git a/src/fapi_sys/mod.rs b/src/fapi_sys/mod.rs new file mode 100644 index 0000000..73ffd94 --- /dev/null +++ b/src/fapi_sys/mod.rs @@ -0,0 +1,14 @@ +/* SPDX-License-Identifier: BSD-3-Clause */ +/******************************************************************************* + * Copyright 2024, Fraunhofer SIT sponsored by the ELISA research project + * All rights reserved. + ******************************************************************************/ + +#![allow(non_upper_case_globals)] +#![allow(non_camel_case_types)] +#![allow(non_snake_case)] +#![allow(unused)] + +pub mod constants; +include!(concat!(env!("OUT_DIR"), "/tss2_fapi_bindings.rs")); +include!(concat!(env!("OUT_DIR"), "/tss2_fapi_versinfo.rs")); diff --git a/src/flags.rs b/src/flags.rs new file mode 100644 index 0000000..e4147b9 --- /dev/null +++ b/src/flags.rs @@ -0,0 +1,304 @@ +/* SPDX-License-Identifier: BSD-3-Clause */ +/******************************************************************************* + * Copyright 2024, Fraunhofer SIT sponsored by the ELISA research project + * All rights reserved. + ******************************************************************************/ + +use crate::{ErrorCode, InternalError}; +use std::{borrow::Cow, collections::BTreeSet, fmt::Debug, num::NonZeroU32, usize}; + +// ========================================================================== +// Flags trait +// ========================================================================== + +pub(crate) trait Flags { + fn as_string(&self) -> Cow<'static, str>; + fn ordinal(&self) -> usize; + fn validate(list: &[T]) -> bool; +} + +// ========================================================================== +// Key creation flags +// ========================================================================== + +/// Key creation flags, as used by the [`create_key()`](crate::FapiContext::create_key) function. +#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)] +pub enum KeyFlags { + /// The private portion of the key may be used to decrypt. + Decrypt, + /// The key may be exported from the TPM. By default, the key is **not** exportable. + Exportable, + /// The key is not subject to dictionary attack protections. + NoDA, + /// Key usage is restricted to manipulate structures of known format. + Restricted, + /// The private portion of the key may be used to sign. + Sign, + /// Store the key in the system-wide directory. + System, + /// Approval of USER role actions with this key may be with an HMAC session or with a password using the authValue of the object or a policy session. + User, + /// Store key persistent in NV RAM. Contains the persistent handle which should be used. + Persistent(NonZeroU32), +} + +impl Flags for KeyFlags { + fn as_string(&self) -> Cow<'static, str> { + match self { + Self::Decrypt => Cow::Borrowed("decrypt"), + Self::Exportable => Cow::Borrowed("exportable"), + Self::NoDA => Cow::Borrowed("noda"), + Self::Restricted => Cow::Borrowed("restricted"), + Self::Sign => Cow::Borrowed("sign"), + Self::System => Cow::Borrowed("system"), + Self::User => Cow::Borrowed("user"), + Self::Persistent(handle) => Cow::Owned(format!("0x{:08X}", handle)), + } + } + + fn ordinal(&self) -> usize { + match self { + Self::Decrypt => 0x01usize, + Self::Exportable => 0x02usize, + Self::NoDA => 0x04usize, + Self::Restricted => 0x08usize, + Self::Sign => 0x10usize, + Self::System => 0x20usize, + Self::User => 0x40usize, + Self::Persistent(_) => 0x80usize, + } + } + + fn validate(_list: &[Self]) -> bool { + true + } +} + +// ========================================================================== +// NV index creation flags +// ========================================================================== + +/// NV index creation flags, as used by the [`create_nv()`](crate::FapiContext::create_nv) function. +/// +/// *Note:* The Flags [`BitField`](NvFlags::BitField), [`Counter`](NvFlags::Counter) and [`PCR`](NvFlags::PCR) are mutually exclusive! If **no** type flag is given, an "ordinary" NV index is created. +#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)] +pub enum NvFlags { + /// NV index contains an 8-octet value to be used as a bit field and can only be modified with `TPM2_NV_SetBits`. + BitField, + /// NV index contains an 8-octet value that is to be used as a counter and can only be modified with `TPM2_NV_Increment`. + Counter, + /// NV index contains a digestsized value used like a PCR. The Index can only be modified using `TPM2_NV_Extend`. + PCR, + /// NV index is not subject to dictionary attack protections. + NoDA, + /// Store the NV index in the system-wide directory. + System, + /// Store the NV index using the contained persistent handle value. + Index(NonZeroU32), +} + +impl Flags for NvFlags { + fn as_string(&self) -> Cow<'static, str> { + match self { + Self::BitField => Cow::Borrowed("bitfield"), + Self::Counter => Cow::Borrowed("counter"), + Self::NoDA => Cow::Borrowed("noda"), + Self::PCR => Cow::Borrowed("pcr"), + Self::System => Cow::Borrowed("system"), + Self::Index(handle) => Cow::Owned(format!("0x{:08X}", handle)), + } + } + + fn ordinal(&self) -> usize { + match self { + Self::BitField => 0x01usize, + Self::Counter => 0x02usize, + Self::NoDA => 0x04usize, + Self::PCR => 0x08usize, + Self::System => 0x10usize, + Self::Index(_) => 0x20usize, + } + } + + fn validate(list: &[Self]) -> bool { + list.into_iter() + .map(|flag| match flag { + Self::BitField => true, + Self::Counter => true, + Self::PCR => true, + _ => false, + }) + .map(usize::from) + .sum::() + < 2usize + } +} + +// ========================================================================== +// Seal creation flags +// ========================================================================== + +/// Sealed object creation flags, as used by the [`create_seal()`](crate::FapiContext::create_seal) function. +#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)] +pub enum SealFlags { + /// Sealed object is not subject to dictionary attack protections. + NoDA, + /// Store the sealed object in the system-wide directory. + System, + /// Store the sealed object using the contained persistent handle value. + Index(NonZeroU32), +} + +impl Flags for SealFlags { + fn as_string(&self) -> Cow<'static, str> { + match self { + Self::NoDA => Cow::Borrowed("noda"), + Self::System => Cow::Borrowed("system"), + Self::Index(handle) => Cow::Owned(format!("0x{:08X}", handle)), + } + } + + fn ordinal(&self) -> usize { + match self { + Self::NoDA => 0x01usize, + Self::System => 0x02usize, + Self::Index(_) => 0x04usize, + } + } + + fn validate(_list: &[Self]) -> bool { + true + } +} + +// ========================================================================== +// Quote creation flags +// ========================================================================== + +/// Quote creation flags, as used by the [`quote()`](crate::FapiContext::quote) function. +#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)] +pub enum QuoteFlags { + /// This is currently the only allowed type of attestation. + TpmQuote, +} + +impl Flags for QuoteFlags { + fn as_string(&self) -> Cow<'static, str> { + match self { + Self::TpmQuote => Cow::Borrowed("TPM-Quote"), + } + } + + fn ordinal(&self) -> usize { + match self { + Self::TpmQuote => 0x01usize, + } + } + + fn validate(_list: &[Self]) -> bool { + true + } +} + +// ========================================================================== +// Padding algorithm +// ========================================================================== + +/// Padding algorithm to be used with [`sign()`](crate::FapiContext::sign) function. +#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)] +pub enum PaddingFlags { + /// RSASSA PKCS1-v1.5 signature scheme + RsaSsa, + /// RSASSA-PSS PKCS#1-v2.1 probabilistic signature scheme + RsaPss, +} + +impl Flags for PaddingFlags { + fn as_string(&self) -> Cow<'static, str> { + match self { + Self::RsaSsa => Cow::Borrowed("RSA_SSA"), + Self::RsaPss => Cow::Borrowed("RSA_PSS"), + } + } + + fn ordinal(&self) -> usize { + match self { + Self::RsaSsa => 0x01usize, + Self::RsaPss => 0x02usize, + } + } + + fn validate(list: &[Self]) -> bool { + list.len() < 2usize + } +} + +// ========================================================================== +// ESYS blob types +// ========================================================================== + +/// Blob type to be used with [`get_esys_blob()`](crate::FapiContext::get_esys_blob) function. +#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)] +pub enum BlobType { + ContextLoad, + Deserialize, +} + +#[derive(Clone, Copy, Debug)] +pub struct UnknownFlagError; + +impl TryFrom for BlobType { + type Error = UnknownFlagError; + + fn try_from(value: u8) -> Result { + match value { + 1u8 => Ok(BlobType::ContextLoad), + 2u8 => Ok(BlobType::Deserialize), + _ => Err(UnknownFlagError), + } + } +} + +// ========================================================================== +// Helper functions +// ========================================================================== + +pub(crate) fn flags_to_string + Ord + Debug + Copy>( + list: Option<&[T]>, +) -> Result, ErrorCode> { + match list { + Some(flags) => { + if !flags.is_empty() { + if contains_duplicates(flags) || (!T::validate(flags)) { + Err(crate::ErrorCode::InternalError( + InternalError::InvalidArguments, + )) + } else { + Ok(Some( + BTreeSet::from_iter(flags) + .into_iter() + .map(T::as_string) + .collect::>>() + .join(","), + )) + } + } else { + Err(crate::ErrorCode::InternalError( + InternalError::InvalidArguments, + )) + } + } + None => Ok(None), /*No flags, but that's okay!*/ + } +} + +fn contains_duplicates + Ord + Copy>(list: &[T]) -> bool { + for i in 0..list.len() { + for j in i + 1..list.len() { + if list[i].ordinal() == list[j].ordinal() { + return true; + } + } + } + false /* no duplicates found! */ +} diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..2c56d46 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,368 @@ +/* SPDX-License-Identifier: BSD-3-Clause */ +/******************************************************************************* + * Copyright 2024, Fraunhofer SIT sponsored by the ELISA research project + * All rights reserved. + ******************************************************************************/ + +//! # TSS 2.0 FAPI Rust Wrapper +//! +//! The `tss2-fapi-rs` Rust crate provides an interface to the [**TCG TSS 2.0 Feature API (FAPI)**](https://trustedcomputinggroup.org/resource/tss-fapi/). +//! +//! ## Getting started +//! +//! The following example illustrates how to use the TSS 2.0 FAPI in Rust: +//! +//! ```rust no_run +//! use std::num::NonZeroUsize; +//! use log::{info, error}; +//! use tss2_fapi_rs::FapiContext; +//! +//! fn main() { +//! // Create a new FAPI context +//! info!("Creating FAPI context, please wait..."); +//! let mut context = match FapiContext::new() { +//! Ok(fpai_ctx) => fpai_ctx, +//! Err(error) => panic!("Failed to create context: {:?}", error) +//! }; +//! +//! // Perform the provisioning, if it has not been done yet +//! info!("Provisioning, please wait..."); +//! match context.provision(None, None, None) { +//! Ok(_) => info!("Success."), +//! Err(error) => panic!("Provisioning has failed: {:?}", error) +//! } +//! +//! // Generate random +//! info!("Generating random data..."); +//! for _i in 0..8 { +//! match context.get_random(NonZeroUsize::new(32).unwrap()) { +//! Ok(random) => info!("Random data: {}", hex::encode(&random[..])), +//! Err(error) => error!("get_random() failed: {:?}", error) +//! } +//! } +//! } +//! ``` +//! +//! Please see the **[`FapiContext`]** documentation for details! +//! +//!
+//! +//! In this example, it is assumed that a valid [FAPI configuration](https://github.com/tpm2-software/tpm2-tss/blob/master/doc/fapi-config.md) has already been set up on the target system. +//! +//! Please see the sub-directory `examples/data` for an example of a FAPI configuration. +//! +//!
+//! +//! #### Callback functions +//! +//! Various use-cases of the FAPI require implementing and installing ***callback functions***, for example: +//! +//! ```rust no_run +//! use std::borrow::Cow; +//! use log::info; +//! use tss2_fapi_rs::{FapiContext, AuthCallback, AuthCallbackParam}; +//! +//! fn main() { +//! // Create a new FAPI context +//! let mut context = FapiContext::new().expect("Failed to create context!"); +//! +//! // Set up "auth" callback function +//! match context.set_auth_callback(AuthCallback::new(my_auth_func)) { +//! Ok(_) => info!("Callback installed."), +//! Err(error) => panic!("Failed to install callback: {:?}", error) +//! } +//! } +//! +//! // Application-defined callback function +//! fn my_auth_func(param: AuthCallbackParam) -> Option> { +//! info!("Auth value for {:?} requested!", param.object_path); +//! Some(Cow::from("my password")) +//! } +//! ``` +//! +//! ## Usage instructions +//! +//! In order to use the FAPI in your own project, simply add the `tss2-fapi-rs` dependency to your `Cargo.toml` file: +//! +//! ```toml +//! [dependencies] +//! tss2-fapi-rs = { version = "^0.3.0", registry = "tss" } +//! ``` +//! +//!
+//! +//! Be aware that, if you add `tss2-fapi-rs` as a dependency, then the [prerequisites](#prerequisites) must be installed on the system too! +//! +//!
+//! +//! ## Build instructions +//! +//! In order to build `tss2-fapi-rs` from the sources, simply run the following command in the project root directory: +//! +//! ```sh +//! $ cargo build --release +//! ``` +//! +//!
+//! +//! Be aware that the above build command requires the [prerequisites](#prerequisites) to be installed on the system! +//! +//!
+//! +//! #### Example code +//! +//! Some examples demonstrating how to use the `tss2-fapi-rs` library are provided in the **`examples`** sub-directory. +//! +//! In order to execute an example, simply run the following command in the project root directory: +//! +//! ```sh +//! $ cargo run --example +//! ``` +//! +//! ## Prerequisites +//! +//! The following prerequisites are required for building or using the `tss2-fapi-rs` crate on your system: +//! +//! ###### *libtss2‑fapi* +//! +//! The native **`libtss2‑fapi`** library (version 3.2.0 or later) and its associated header files must be available: +//! +//! +//! On Ubuntu 22.04 (Jammy), Debian 12.0 (Bookworm) or later, this can be installed via the `libtss2-dev` package. +//! +//! Furthermore, it may be necessary to additionally install the following *transitive* dependencies: +//! `uuid-dev`, `libjson-c-dev`, `libcrypt-dev` and `libcurl4-openssl-dev` +//! +//! **Note:** By default, the location of the native FAPI library is detected automatically via the `pkg-config` utility. Alternatively, it is also possible to manually specify the location of the native FAPI library via the *environment variables* described below. +//! +//! ###### *C compiler* +//! +//! A working **C compiler** (`cc`) is required for Rust/Cargo to build some of the required dependencies. +//! +//! On Ubuntu 22.04 (Jammy), Debian 12.0 (Bookworm) or later, this can be installed, e.g., via the `build-essential` package. +//! +//! ###### *libclang* +//! +//! Rust's [`bindgen`](https://github.com/rust-lang/rust-bindgen?tab=readme-ov-file) tool requires that **`libclang`** is available on the system. +//! +//! On Ubuntu 22.04 (Jammy), Debian 12.0 (Bookworm) or later, this can be installed via the `libclang-dev` package. +//! +//! #### Environment +//! +//! The following environment variables optionally specify the location of the native FAPI library during the *build* process: +//! +//! * **`TSS2_INCLUDE_PATH`** +//! Location of the TSS 2.0 header files. If specified, this path shall contain the `tss2_fapi.h` header file. +//! This variable must be set in conjunction with `TSS2_LIBRARY_PATH` to be effective! +//! +//! * **`TSS2_LIBRARY_PATH`** +//! Location of the TSS 2.0 library files. If specified, this path shall contain the `libtss2-fapi.so` file. +//! This variable must be set in conjunction with `TSS2_INCLUDE_PATH` to be effective! +//! +//! * **`TSS2_LIBRARY_VERS`** +//! Version of the TSS 2.0 library. If specified, the version string shall have the `"major.minor.patch"` format. +//! +//! The following environment variables can be used to control the *runtime* behavior: +//! +//! * **`LD_LIBRARY_PATH`** +//! On Linux/Unix systems, this can be set to control the location to load the native FAPI library from. +//! +//! ## Testing +//! +//! Integration tests for *all* supported FAPI functions are provided in the **`tests`** sub-directory. +//! +//! In order to execute these integration tests, simply run the following command in the project root directory: +//! +//! ```sh +//! $ cargo test --release [] +//! ``` +//! +//! If a test name is **not** specified, then *all* available tests will be executed. +//! +//! #### Test Prerequisites +//! +//! The integration tests require that a working TPM is available. Using a TPM emulator is recommended for testing! +//! +//! ###### *Software TPM Emulator* +//! +//! The *Software TPM Emulator* (swtpm) created by Stefan Berger can be used testing purposes: +//! +//! +//! In order to use the *swtpm*, the TCTI needs to be configured as follows, specifying the proper IP address and port number: +//! ```sh +//! $ export FAPI_RS_TEST_TCTI="swtpm:host=,port=" +//! ``` +//! +//! ###### *Docker* +//! +//! A fully self-contained [Docker Compose](https://docs.docker.com/compose/) setup for building and starting the software TPM is provided at **`etc/docker/swtpm`**. +//! +//! On Ubuntu 22.04 (Jammy) or later, *Docker Compose V2* can be installed via the `docker-compose-v2` package. +//! +//! With Docker Compose V2 installed, just run **`make`** in the `etc/docker/swtpm` directory in order to start up the software TPM. +//! +//! ###### *libtpms* +//! +//! As a more lightweight alternative, the integration tests may also be run using the *libtpms* library: +//! +//! +//! On Ubuntu 24.04 (Noble), Debian 12.0 (Bookworm) or later, *libtpms* can be installed via the `libtpms-dev` package. +//! +//! In order to use the *libtpms* library, the TCTI needs to be configured as follows: +//! ```sh +//! $ export FAPI_RS_TEST_TCTI="libtpms:$(mktemp)" +//! ``` +//! +//! **Note:** It is necessary to save the state to the filesystem by specifying a file name when using *libtpms* for testing! +//! +//! #### Environment +//! +//! The following environment variables can be set as needed: +//! +//! * **`FAPI_RS_TEST_TCTI`** +//! The TCTI connection string to be used for testing. By default, `swtpm:host=127.0.0.1,port=2321` is used. +//! *See also:* +//! +//! * **`FAPI_RS_TEST_PROF`** +//! The FAPI profile to be used for testing. By default, `RSA2048SHA256` is used. May be set to `ECCP256SHA256`. +//! +//! * **`FAPI_RS_TEST_LOOP`** +//! The number of times to repeat each test. Default is **3**. +//! +//! * **`RUST_LOG`** +//! Controls the logging level. Set to, for example, `debug` in order to enable additional logging outputs. +//! +//! * **`TSS2_LOG`** +//! Controls the logging level of the underlying native TSS2 libraries. This can be set to `all+none` in order to silence all outputs from the native TSS2 libraries. It can also be set `all+debug` in order to enable some additional debug outputs. +//! +//! * **`LD_LIBRARY_PATH`** +//! On Linux/Unix systems, this can be set to control the location to load the native `libtss2-fapi.so` library from. This is useful if you want to test with a "custom" build of `tpm2-tss` instead of using the version provided by the operating system. +//! +//! #### Configuration +//! +//! The integration tests automatically create and use a temporary FAPI configuration. It is generated from the template at: +//! **`tests/data/fapi-config.json.template`** +//! +//! The FAPI profiles that are intended to be used by the integration tests are stored at: +//! **`tests/data/profiles/*.json`** +//! +//! ## Troubleshooting +//! +//! * **Error message:** +//! ```txt +//! pkg_config: Required library "tss2-fapi" not found! +//! The system library `tss2-fapi` required by crate `tss2-fapi-rs` was not found. +//! ``` +//! *Solution:* +//! The native TSS 2.0 FAPI library, or one of its dependencies, can not be found by `pkg-config`. Please make sure that the required library and all of its dependencies are installed! If the `tss2-fapi.pc` resides at a non-standard location, set `PKG_CONFIG_PATH` accordingly. Also, the command `pkg-config --libs tss2-fapi` may be useful for debugging. +//! +//! * **Error message:** +//! ```txt +//! error: linker `cc` not found +//! note: No such file or directory (os error 2) +//! ``` +//! *Solution:* +//! The C compiler could not be found. Please install a working C compiler (e.g. `gcc` or `clang`) and then try again. +//! +//! * **Error message:** +//! ```txt +//! error: failed to run custom build command for `tss2-fapi-rs` +//! Unable to generate bindings: "fatal error: 'tss2_fapi.h' file not found" +//! ``` +//! *Solution:* +//! The header file for the native TSS 2.0 FAPI library is missing. Make sure that the native TSS 2.0 FAPI library and its header files are installed. If the `tss2_fapi.h` is installed at a non-standard location, set `TSS2_INCLUDE_PATH` as needed. +//! Be aware that the "include" directory shall contain a sub-directory named `tss2` containing the actual `tss2_fapi.h` file. +//! +//! * **Error message:** +//! ```txt +//! error: failed to run custom build command for `tss2-fapi-rs` +//! Unable to generate bindings: "fatal error: 'stddef.h' file not found" +//! ``` +//! *Solution:* +//! The C Standard Library header files can not be found. This usually indicates an incomplete/broken installation of the Clang compiler, as used by Rust's `bindgen` tool. Can be fixed, e.g., by (re)installing the `clang` or `libclang-dev` package. +//! +//! * **Error message:** +//! ```txt +//! error: linking with `cc` failed: exit status: 1 +//! /usr/bin/ld: cannot find -ltss2-fapi: No such file or directory +//! ``` +//! *Solution:* +//! The native TSS 2.0 FAPI library is missing. Make sure that the native TSS 2.0 FAPI library is actually installed (or has been built from the sources). If the `libtss2-fapi.so` resides at a non-standard location, set `TSS2_LIBRARY_PATH` as needed. +//! +//! * **Error message:** +//! ```txt +//! error: failed to run custom build command for `tss2-fapi-rs` +//! Unable to find libclang: "couldn't find any valid shared libraries matching ..." +//! ``` +//! *Solution:* +//! Indicates that Rust's `bindgen` tool was unable to load the required Clang library. This is usually fixed, e.g., by installing the `libclang-dev` package. If the `libclang.so` resides at a non-standard location, set `LIBCLANG_PATH` as needed. +//! +//! * **Error message:** +//! ```txt +//! error while loading shared libraries: libtss2-fapi.so.1: +//! cannot open shared object file: No such file or directory +//! ``` +//! *Solution:* +//! The native TSS 2.0 FAPI library could not be loaded at runtime. Make sure that the native TSS 2.0 FAPI library is actually installed, and that it is located in one of the directories where the dynamic linker/loader is looking for shared libraries. If the `libtss2-fapi.so.1` resides at a non-standard location, you may update the `LD_LIBRARY_PATH` as needed. +//! +//! * **Error message:** +//! ```txt +//! ERROR:tcti:src/tss2-tcti/tcti-swtpm.c:617:Tss2_Tcti_Swtpm_Init() +//! Cannot connect to swtpm TPM socket +//! Failed to create context: OtherError(IoError) +//! ``` +//! ```txt +//! Failed to connect to the software TPM! Is the software TPM running? +//! Connection refused +//! ``` +//! *Solution:* +//! If this message appears at runtime, e.g. when trying to run the test cases, it indicates that the connection to the software TPM could not be established. Make sure that the software TPM is actually running and ready for incoming connections! One simple way to get a running software TPM is by using the Docker environment provided in the `etc/docker/swtpm` directory. +//! +//! * **Error message:** +//! ```txt +//! socket_connect() Failed to connect to host 10.0.0.1, port 2321: +//! errno 99: Cannot assign requested address +//! ``` +//! *Solution:* +//! The native TSS 2.0 FAPI library was unable to connect to the software TPM. This error may occur, after some time, when too many TPM commands are sent to the software TPM at a high rate. There currently is **no** known solution, but a possible workaround is to simply slow down the sequence of TPM commands, e.g., by adding an artificial delay after each command. Furthermore, adding the `disconnect` option to the SWTPM `--server` parameter seems to attenuate the problem. +//! +//! ## Source Code +//! +//! The `tss2-fapi-rs` source code can be found at the official GitHub repository: +//! +//! +//! #### Contact +//! +//! For bug reports, feature requests, etc., please refer to the issue tracker at: +//! +//! +//! ## License +//! +//! Copyright 2024, [Fraunhofer SIT](https://www.sit.fraunhofer.de/) sponsored by the ELISA research project +//! All rights reserved. +//! +//! This work is released under the [**3-Clause BSD License**](https://opensource.org/license/bsd-3-clause) (SPDX short identifier: `BSD-3-Clause`). + +#![doc(html_no_source)] + +mod algorithm_id; +mod callback; +mod context; +mod error; +mod fapi_sys; +mod flags; +mod marshal; +mod memory; +mod version; + +pub use algorithm_id::HashAlgorithm; +pub use callback::{ + ActnCallback, ActnCallbackParam, AuthCallback, AuthCallbackParam, BranCallback, + BranCallbackParam, SignCallback, SignCallbackParam, +}; +pub use context::FapiContext; +pub use error::{ + BaseErrorCode, ErrorCode, InternalError, Tpm2ErrFmt0, Tpm2ErrFmt1, Tpm2ErrorCode, Tpm2Warning, +}; +pub use flags::{BlobType, KeyFlags, NvFlags, PaddingFlags, QuoteFlags, SealFlags}; +pub use version::{get_version, VersionInfo}; diff --git a/src/marshal.rs b/src/marshal.rs new file mode 100644 index 0000000..fb67733 --- /dev/null +++ b/src/marshal.rs @@ -0,0 +1,26 @@ +/* SPDX-License-Identifier: BSD-3-Clause */ +/******************************************************************************* + * Copyright 2024, Fraunhofer SIT sponsored by the ELISA research project + * All rights reserved. + ******************************************************************************/ + +use std::{borrow::Cow, iter::repeat, mem::size_of}; + +const U64_SIZE: usize = size_of::(); + +pub(crate) fn u64_from_be(bytes: &[u8]) -> u64 { + u64::from_be_bytes(<[u8; U64_SIZE]>::try_from(resize_be(bytes, U64_SIZE).as_ref()).unwrap()) +} + +pub(crate) fn resize_be<'a>(bytes: &'a [u8], new_size: usize) -> Cow<'a, [u8]> { + if bytes.len() >= new_size { + Cow::Borrowed(&bytes[bytes.len() - new_size..]) + } else { + Cow::Owned( + repeat(0u8) + .take(new_size - bytes.len()) + .chain(bytes.into_iter().copied()) + .collect::>(), + ) + } +} diff --git a/src/memory.rs b/src/memory.rs new file mode 100644 index 0000000..921f805 --- /dev/null +++ b/src/memory.rs @@ -0,0 +1,355 @@ +/* SPDX-License-Identifier: BSD-3-Clause */ +/******************************************************************************* + * Copyright 2024, Fraunhofer SIT sponsored by the ELISA research project + * All rights reserved. + ******************************************************************************/ + +use crate::{fapi_sys, ErrorCode}; +use json::JsonValue; +use std::{ + borrow::Cow, + ffi::{c_char, c_void, CStr, CString, NulError}, + ptr, slice, +}; + +#[cfg(unix)] +use libc::explicit_bzero; + +const INVALID_ARGUMENTS: ErrorCode = + ErrorCode::InternalError(crate::InternalError::InvalidArguments); + +// ========================================================================== +// Helper macros +// ========================================================================== + +macro_rules! fail_if_empty { + ($str:ident) => { + if $str.is_empty() { + return Err(INVALID_ARGUMENTS); + } + }; +} + +macro_rules! fail_if_opt_empty { + ($opt_str:ident) => { + if let Some(strval) = $opt_str.as_ref() { + fail_if_empty!(strval) + } + }; +} + +// ========================================================================== +// CStringHolder +// ========================================================================== + +/// Wrapper class to encapsulate a string as `ffi:CString`. +#[derive(Debug)] +pub struct CStringHolder { + str_data: Option, +} + +impl CStringHolder { + /// Returns a pointer to the wrapped NUL-terminated string, which is guaranteed to remain valid until the `CStringHolder` instance is dropped. + pub fn as_ptr(&self) -> *const c_char { + self.str_data + .as_ref() + .map_or(ptr::null(), |str| str.as_ptr()) + } + + #[allow(dead_code)] + pub fn as_c_str(&self) -> Option<&CStr> { + self.str_data.as_ref().map(CString::as_c_str) + } + + fn force_clear(&mut self) { + if let Some(data) = self.str_data.take() { + erase_memory(data.as_ptr(), data.count_bytes()); + } + } +} + +impl Drop for CStringHolder { + fn drop(&mut self) { + self.force_clear(); + } +} + +impl TryFrom<&str> for CStringHolder { + type Error = ErrorCode; + + /// Creates a new `CStringHolder` from a given `&str` slice. + fn try_from(str: &str) -> Result { + fail_if_empty!(str); + Ok(Self { + str_data: Some(CString::new(str).expect("Failed to allocate CString!")), + }) + } +} + +impl TryFrom for CStringHolder { + type Error = ErrorCode; + + /// Creates a new `CStringHolder` from a given `String` object. + fn try_from(str: String) -> Result { + fail_if_empty!(str); + Ok(Self { + str_data: Some(CString::new(str).expect("Failed to allocate CString!")), + }) + } +} + +impl TryFrom> for CStringHolder { + type Error = ErrorCode; + + /// Creates a new `CStringHolder` from a given `Cow<'static, str>` value. + fn try_from(str: Cow<'static, str>) -> Result { + fail_if_empty!(str); + Ok(Self { + str_data: Some(cstring_from_cow(str).expect("Failed to allocate CString!")), + }) + } +} + +impl TryFrom<&JsonValue> for CStringHolder { + type Error = ErrorCode; + + /// Creates a new `CStringHolder` from a given `JsonValue` value. + fn try_from(json: &JsonValue) -> Result { + fail_if_empty!(json); + Ok(Self { + str_data: Some(CString::new(json.to_string()).expect("Failed to allocate CString!")), + }) + } +} + +impl TryFrom> for CStringHolder { + type Error = ErrorCode; + + /// Create sa new `CStringHolder` from a given `Option<&str>`. The new instance will contain a `NULL` pointer, if the given `opt_str` was `None`. + fn try_from(opt_str: Option<&str>) -> Result { + fail_if_opt_empty!(opt_str); + Ok(Self { + str_data: opt_str.map(|str| CString::new(str).expect("Failed to allocate CString!")), + }) + } +} + +impl TryFrom> for CStringHolder { + type Error = ErrorCode; + + /// Create sa new `CStringHolder` from a given `Option<&str>`. The new instance will contain a `NULL` pointer, if the given `opt_str` was `None`. + fn try_from(opt_str: Option) -> Result { + fail_if_opt_empty!(opt_str); + Ok(Self { + str_data: opt_str.map(|str| CString::new(str).expect("Failed to allocate CString!")), + }) + } +} + +impl TryFrom>> for CStringHolder { + type Error = ErrorCode; + + /// Creates a new `CStringHolder` from a given `Option>`. The new instance will contain a `NULL` pointer, if the given `opt_str` was `None`. + fn try_from(opt_str: Option>) -> Result { + fail_if_opt_empty!(opt_str); + Ok(Self { + str_data: opt_str + .map(|str| cstring_from_cow(str).expect("Failed to allocate CString!")), + }) + } +} + +impl TryFrom> for CStringHolder { + type Error = ErrorCode; + + /// Creates a new `CStringHolder` from a given `Option`. The new instance will contain a `NULL` pointer, if the given `opt_json` was `None`. + fn try_from(opt_json: Option<&JsonValue>) -> Result { + fail_if_opt_empty!(opt_json); + Ok(Self { + str_data: opt_json + .map(|json| CString::new(json.to_string()).expect("Failed to allocate CString!")), + }) + } +} + +// ========================================================================== +// MemoryHolder +// ========================================================================== + +/// Wrapper class to hold a data pointer returned by the native FAPI. +/// +/// Assumes that the pointed-to memory was allocated by the FAPI and therefore automatically releases the pointed-to memory, by calling `FAPI_Free()`, when the `FapiMemoryHolder` instance is dropped. +pub(crate) struct FapiMemoryHolder +where + T: Sized + Clone, +{ + data_ptr: *mut T, + length: usize, +} + +impl FapiMemoryHolder +where + T: Sized + Clone, +{ + /// Creates a new `FapiMemoryHolder` from a "raw" pointer and a length. The pointer can be a `NULL` pointer, in which case the `length` is ignored. + pub fn from_raw(data_ptr: *mut T, length: usize) -> Self { + Self { + data_ptr, + length: if data_ptr != ptr::null_mut() { + length + } else { + 0usize + }, + } + } + + /// Copies the wrapped data into a new `Vec`, so that it can remain valid after this `FapiMemoryHolder` instance was dropped; returns `None` if wrapped pointer is `NULL`. + pub fn to_vec(&self) -> Option> { + if (self.data_ptr != ptr::null_mut()) && (self.length > 0usize) { + unsafe { Some(slice::from_raw_parts(self.data_ptr, self.length).to_vec()) } + } else { + None + } + } + + /// Returns a pointer to the wrapped data, which will be valid only until this `FapiMemoryHolder` instance is dropped! + #[allow(dead_code)] + pub fn as_ptr(&self) -> *const T { + self.data_ptr + } +} + +impl FapiMemoryHolder { + /// Creates a new `FapiMemoryHolder` from a NUL-terminated C-string. The pointer can be a `NULL` pointer, in which case a zero-length string is assumed; otherwise the pointed-to data **must** be NUL-terminated! + pub fn from_str(data_ptr: *mut c_char) -> Self { + Self { + data_ptr, + length: if data_ptr != ptr::null_mut() { + unsafe { CStr::from_ptr(data_ptr).count_bytes() } + } else { + 0usize + }, + } + } + + /// Copies the wrapped data into an owned `String` object, so that it can remain valid after this `FapiMemoryHolder` instance was dropped; returns `None` if wrapped pointer is `NULL`. + pub fn to_string(&self) -> Option { + if (self.data_ptr != ptr::null_mut()) && (self.length > 0usize) { + let result = unsafe { CStr::from_ptr(self.data_ptr).to_str() }; + match result { + Ok(cs) => Some(cs.to_owned()), + Err(_) => None, + } + } else { + None + } + } + + /// Parses the wrapped data into an owned `JsonValue` object, so that it can remain valid after this `FapiMemoryHolder` instance was dropped; returns `None` if wrapped pointer is `NULL` or is not pointing to valid JSON data. + pub fn to_json(&self) -> Option { + if (self.data_ptr != ptr::null_mut()) && (self.length > 0usize) { + let result = unsafe { CStr::from_ptr(self.data_ptr).to_str() }; + match result { + Ok(cs) => json::parse(cs).ok(), + Err(_) => None, + } + } else { + None + } + } +} + +impl Drop for FapiMemoryHolder +where + T: Sized + Clone, +{ + fn drop(&mut self) { + if self.data_ptr != ptr::null_mut() { + erase_memory(self.data_ptr, self.length); + unsafe { + fapi_sys::Fapi_Free(self.data_ptr as *mut c_void); + } + self.data_ptr = ptr::null_mut(); + }; + } +} + +// ========================================================================== +// Miscellaneous functions +// ========================================================================== + +/// Create a string from a `c_char` pointer, will be `None` iff the pointer is a `NULL` pointer. +pub fn ptr_to_opt_cstr<'a>(str_ptr: *const c_char) -> Option<&'a CStr> { + if str_ptr != ptr::null() { + unsafe { Some(CStr::from_ptr(str_ptr)) } + } else { + None + } +} + +/// Create a vector of strings from a "raw" array of `c_char` pointers +pub fn ptr_to_cstr_vec<'a>(str_ptr: *mut *const c_char, count: usize) -> Vec<&'a CStr> { + unsafe { + let list: &[*const c_char] = slice::from_raw_parts(str_ptr, count); + list.into_iter().map(|ptr| CStr::from_ptr(*ptr)).collect() + } +} + +/// Get pointer to optional data, will be `NULL` if **no** data is actually available. +pub fn opt_to_ptr(opt_data: Option<&[T]>) -> *const T { + match opt_data { + Some(data) => data.as_ptr(), + None => ptr::null(), + } +} + +/// Get length of optional data, will be *zero* if **no** data is actually available. +pub fn opt_to_len(opt_data: Option<&[T]>) -> usize { + match opt_data { + Some(data) => data.len(), + None => 0usize, + } +} + +/// Get conditional pointer, which will be a `NULL` pointer unless the flags is *true*. +pub fn cond_ptr(cond_ptr: &mut T, enabled: bool) -> *mut T { + if enabled { + cond_ptr + } else { + ptr::null_mut() + } +} + +/// Get conditional output pointer (i.e. pointer to pointer), which will be a `NULL` pointer unless the flags is *true*. +pub fn cond_out(cond_ptr: &mut *mut T, enabled: bool) -> *mut *mut T { + if enabled { + cond_ptr + } else { + ptr::null_mut() + } +} + +// ========================================================================== +// Utilities +// ========================================================================== + +fn erase_memory(address: *const T, length: usize) +where + T: Sized + Clone, +{ + #[cfg(unix)] + unsafe { + explicit_bzero(address as *mut c_void, length); + } + #[cfg(not(unix))] + unsafe { + ptr::write_bytes(address as *mut c_void, 0u8, length); + } +} + +fn cstring_from_cow(str: Cow<'static, str>) -> Result { + match str { + Cow::Borrowed(data) => CString::new(data), + Cow::Owned(data) => CString::new(data), + } +} diff --git a/src/version.rs b/src/version.rs new file mode 100644 index 0000000..cee7a9f --- /dev/null +++ b/src/version.rs @@ -0,0 +1,53 @@ +/* SPDX-License-Identifier: BSD-3-Clause */ +/******************************************************************************* + * Copyright 2024, Fraunhofer SIT sponsored by the ELISA research project + * All rights reserved. + ******************************************************************************/ + +use std::{fmt::Display, sync::OnceLock}; + +static VERSION_INFO_PKG: OnceLock = OnceLock::new(); +static VERSION_INFO_SYS: OnceLock = OnceLock::new(); + +/// Contains version information. +pub struct VersionInfo { + /// The *major* version of the software. + pub major: u16, + /// The *minor* version of the software. + pub minor: u16, + /// The *patch* level of the software. + pub patch: u16, +} + +/// Returns the package version of the **`tss2-fapi-rs`** library as well as the version of the native FAPI library. +/// +/// ###### Return Value +/// +/// **`(package_version, fapi_library_version)`** +pub fn get_version() -> (&'static VersionInfo, &'static VersionInfo) { + ( + VERSION_INFO_PKG.get_or_init(|| { + parse_version(env!("CARGO_PKG_VERSION", "Package version not defined!")) + }), + VERSION_INFO_SYS.get_or_init(|| parse_version(crate::fapi_sys::TSS2_FAPI_VERSION)), + ) +} + +/// Parse a version string that is in the `"major.minor.patch"` format into a [`VersionInfo`] struct. +fn parse_version(version_string: &str) -> VersionInfo { + let mut tokens = version_string + .split('.') + .map(|str| u16::from_str_radix(str, 10).unwrap_or_default()); + VersionInfo { + major: tokens.next().unwrap_or_default(), + minor: tokens.next().unwrap_or_default(), + patch: tokens.next().unwrap_or_default(), + } +} + +/// Convert the `VersionInfo` struct to a string in the `"major.minor.patch"` format +impl Display for VersionInfo { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}.{}.{}", self.major, self.minor, self.patch) + } +} diff --git a/tests/01_version_test.rs b/tests/01_version_test.rs new file mode 100644 index 0000000..472eb40 --- /dev/null +++ b/tests/01_version_test.rs @@ -0,0 +1,45 @@ +/* SPDX-License-Identifier: BSD-3-Clause */ +/******************************************************************************* + * Copyright 2024, Fraunhofer SIT sponsored by the ELISA research project + * All rights reserved. + ******************************************************************************/ + +pub mod common; + +use common::setup::TestConfiguration; +use function_name::named; +use log::debug; +use serial_test::serial; +use tss2_fapi_rs::get_version; + +const CURRENT_PKG_VERSION: &str = env!("CARGO_PKG_VERSION"); + +/// Test the `get_version()` function +#[test] +#[serial] +#[named] +fn test_version() { + let _configuration = TestConfiguration::new(); + + repeat_test!(|_i| { + let (version_pkg, version_sys) = get_version(); + debug!("tss2-fapi-rs package version: {}", version_pkg); + debug!("Native FAPI version: {}", version_sys); + + // Verify the package version + assert_eq!( + CURRENT_PKG_VERSION, + format!( + "{}.{}.{}", + version_pkg.major, version_pkg.minor, version_pkg.patch + ) + ); + + // Verify the FAPI version + assert!( + (version_sys.major > 3u16) + || ((version_sys.major == 3u16) + && ((version_sys.minor > 0u16) || (version_sys.patch >= 3u16))) + ); + }); +} diff --git a/tests/02_get_info_test.rs b/tests/02_get_info_test.rs new file mode 100644 index 0000000..2a30e13 --- /dev/null +++ b/tests/02_get_info_test.rs @@ -0,0 +1,127 @@ +/* SPDX-License-Identifier: BSD-3-Clause */ +/******************************************************************************* + * Copyright 2024, Fraunhofer SIT sponsored by the ELISA research project + * All rights reserved. + ******************************************************************************/ + +pub mod common; + +use common::setup::TestConfiguration; +use function_name::named; +use log::{debug, info, trace, Level}; +use memory_stats::memory_stats; +use serial_test::serial; +use std::{mem, time::Instant}; +use tss2_fapi_rs::FapiContext; + +// ========================================================================== +// Test cases +// ========================================================================== + +/// Test the `get_info()` function +#[test] +#[serial] +#[named] +fn test_get_info() { + let _configuration = TestConfiguration::new(); + + repeat_test!(|_i| { + // Create FAPI context + let mut context = match FapiContext::new() { + Ok(fpai_ctx) => fpai_ctx, + Err(error) => panic!("Failed to create context: {:?}", error), + }; + + // Print the context + let _stringify = format!("{}", context); + + // Fetch TPM info + let info_json = match context.get_info() { + Ok(info) => info, + Err(error) => panic!("Retrieving TPM info has failed: {:?}", error), + }; + + // Verify received TPM info data + trace!("{:?}", info_json); + assert!(!info_json.is_empty()); + }); +} + +/// Test for possible memory leaks by calling `get_info()` *many* times in a row and monitoring memory usage +#[test] +#[serial] +#[named] +#[ignore = "tends to fails with the swtpm (socket error)"] +fn test_to_destruction() { + let _configuration = TestConfiguration::new(); + + repeat_test!(|i| { + // Capture initial memory usage + let initial_memory_usage = memory_stats().expect("Failed to fetch initial memory stats!"); + debug!( + "Initial memory usage: {} bytes", + initial_memory_usage.virtual_mem + ); + + // Create FAPI context + let mut context = match FapiContext::new() { + Ok(fpai_ctx) => fpai_ctx, + Err(error) => panic!("Failed to create context: {:?}", error), + }; + + // Fetch info repeatedly to check for possible memory leaks + const LOOP_COUNT: usize = 99_991_usize; + let mut previous_update: Option = None; + for j in 0..LOOP_COUNT { + if log::log_enabled!(Level::Info) { + if previous_update.map_or(true, |ts| ts.elapsed().as_secs() >= 5u64) { + output_progress(j, LOOP_COUNT); + previous_update = Some(Instant::now()); + } + } + match context.get_info() { + Ok(info) => assert!(!info.is_empty()), + Err(error) => panic!("Retrieving TPM info has failed: {:?}", error), + }; + } + + // Completed + output_progress(LOOP_COUNT, LOOP_COUNT); + + // Explicitely drop the context + mem::drop(context); + + // Capture final memory usage + let final_memory_usage = memory_stats().expect("Failed to fetch final memory stats!"); + debug!( + "Final memory usage: {} bytes", + final_memory_usage.virtual_mem + ); + + // Detect number of "leaked" bytes + let leaked_bytes = final_memory_usage + .virtual_mem + .saturating_sub(initial_memory_usage.virtual_mem); + log::debug!("Leaked bytes: {}", leaked_bytes); + if i > 0_usize { + assert!( + leaked_bytes < 4096_usize, + "Memory leak detected! ({} bytes leaked)", + leaked_bytes + ); + } + }); +} + +// ========================================================================== +// Helper functions +// ========================================================================== + +fn output_progress(current: usize, total: usize) { + info!( + "Progress: {:5} of {:5} completed. ({:5.1}%)", + current, + total, + (current as f64) / (total as f64) * 100_f64 + ); +} diff --git a/tests/03_provision_test.rs b/tests/03_provision_test.rs new file mode 100644 index 0000000..7e94d64 --- /dev/null +++ b/tests/03_provision_test.rs @@ -0,0 +1,125 @@ +/* SPDX-License-Identifier: BSD-3-Clause */ +/******************************************************************************* + * Copyright 2024, Fraunhofer SIT sponsored by the ELISA research project + * All rights reserved. + ******************************************************************************/ + +pub mod common; + +use common::{param::PASSWORD, setup::TestConfiguration}; +use function_name::named; +use log::{debug, info, Level}; +use memory_stats::memory_stats; +use serial_test::serial; +use std::time::Instant; +use tss2_fapi_rs::FapiContext; + +mk_auth_callback!(my_auth_callback, PASSWORD); +mk_tpm_finalizer!(my_tpm_finalizer, my_auth_callback); + +// ========================================================================== +// Test cases +// ========================================================================== + +/// Test the `provision()` function with a password (auth value) for the storage hierarchy +#[test] +#[serial] +#[named] +fn test_provision() { + let _configuration = TestConfiguration::with_finalizer(my_tpm_finalizer); + + repeat_test!(|_i| { + // Create FAPI context + let mut context = match FapiContext::new() { + Ok(fpai_ctx) => fpai_ctx, + Err(error) => panic!("Failed to create context: {:?}", error), + }; + + // Set up auth callback + if let Err(error) = + context.set_auth_callback(tss2_fapi_rs::AuthCallback::new(my_auth_callback)) + { + panic!("Setting up the callback has failed: {:?}", error) + } + + // Initialize TPM, if not already initialized + match context.provision(None, Some(PASSWORD), None) { + Ok(_) => log::debug!("Provisioned."), + Err(tss2_fapi_rs::ErrorCode::FapiError( + tss2_fapi_rs::BaseErrorCode::AlreadyProvisioned, + )) => log::debug!("TPM already provisioned -> skipping."), + Err(error) => panic!("Provisioning has failed: {:?}", error), + } + }); +} + +/// Test for possible memory leaks by calling `get_info()` *many* times in a row and monitoring memory usage +#[test] +#[serial] +#[named] +#[ignore = "tends to fails with the swtpm (socket error)"] +fn test_to_destruction() { + let _configuration = TestConfiguration::new(); + + repeat_test!(|i| { + // Capture initial memory usage + let initial_memory_usage = memory_stats().expect("Failed to fetch initial memory stats!"); + debug!( + "Initial memory usage: {} bytes", + initial_memory_usage.virtual_mem + ); + + // Create context and attempt provision repeatedly to check for possible memory leaks + const LOOP_COUNT: usize = 99_991_usize; + let mut previous_update: Option = None; + for j in 0..LOOP_COUNT { + if log::log_enabled!(Level::Info) { + if previous_update.map_or(true, |ts| ts.elapsed().as_secs() >= 5u64) { + output_progress(j, LOOP_COUNT); + previous_update = Some(Instant::now()); + } + } + let mut context = match FapiContext::new() { + Ok(fpai_ctx) => fpai_ctx, + Err(error) => panic!("Failed to create context: {:?}", error), + }; + tpm_initialize!(context, PASSWORD, my_auth_callback); + } + + // Completed + output_progress(LOOP_COUNT, LOOP_COUNT); + + // Capture final memory usage + let final_memory_usage = memory_stats().expect("Failed to fetch final memory stats!"); + debug!( + "Final memory usage: {} bytes", + final_memory_usage.virtual_mem + ); + + // Detect number of "leaked" bytes + let leaked_bytes = final_memory_usage + .virtual_mem + .saturating_sub(initial_memory_usage.virtual_mem); + log::debug!("Leaked bytes: {}", leaked_bytes); + if i > 0_usize { + assert!( + leaked_bytes < 4096_usize, + "Memory leak detected! ({} bytes leaked)", + leaked_bytes + ); + } + }); +} + +// ========================================================================== +// Helper functions +// ========================================================================== + +fn output_progress(current: usize, total: usize) { + info!( + "Progress: {:5} of {:5} completed. ({:5.1}%)", + current, + total, + (current as f64) / (total as f64) * 100_f64 + ); +} diff --git a/tests/04_get_random_test.rs b/tests/04_get_random_test.rs new file mode 100644 index 0000000..f2b8498 --- /dev/null +++ b/tests/04_get_random_test.rs @@ -0,0 +1,50 @@ +/* SPDX-License-Identifier: BSD-3-Clause */ +/******************************************************************************* + * Copyright 2024, Fraunhofer SIT sponsored by the ELISA research project + * All rights reserved. + ******************************************************************************/ + +pub mod common; + +use common::{param::PASSWORD, setup::TestConfiguration}; +use function_name::named; +use log::debug; +use serial_test::serial; +use std::num::NonZeroUsize; +use tss2_fapi_rs::FapiContext; + +mk_auth_callback!(my_auth_callback, PASSWORD); +mk_tpm_finalizer!(my_tpm_finalizer, my_auth_callback); + +// ========================================================================== +// Test cases +// ========================================================================== + +/// Test the `test_get_random()` function +#[test] +#[serial] +#[named] +fn test_get_random() { + let _configuration = TestConfiguration::with_finalizer(my_tpm_finalizer); + + repeat_test!(|_i| { + // Create FAPI context + let mut context = match FapiContext::new() { + Ok(fpai_ctx) => fpai_ctx, + Err(error) => panic!("Failed to create context: {:?}", error), + }; + + // Initialize TPM, if not already initialized + tpm_initialize!(context, PASSWORD, my_auth_callback); + + // Fetch random data + let random_data = match context.get_random(NonZeroUsize::new(128usize).unwrap()) { + Ok(data) => data, + Err(error) => panic!("Generating random data failed: {:?}", error), + }; + + // Verify random data + assert_eq!(random_data.len(), 128usize); + debug!("random_data: {}", hex::encode(random_data)); + }); +} diff --git a/tests/05_key_test.rs b/tests/05_key_test.rs new file mode 100644 index 0000000..37a0a7a --- /dev/null +++ b/tests/05_key_test.rs @@ -0,0 +1,317 @@ +/* SPDX-License-Identifier: BSD-3-Clause */ +/******************************************************************************* + * Copyright 2024, Fraunhofer SIT sponsored by the ELISA research project + * All rights reserved. + ******************************************************************************/ + +pub mod common; + +use common::{ + crypto::{get_key_type, load_public_key}, + param::PASSWORD, + setup::TestConfiguration, +}; +use function_name::named; +use log::debug; +use serial_test::serial; +use std::collections::HashSet; +use tss2_fapi_rs::{BaseErrorCode, ErrorCode, FapiContext, KeyFlags}; + +mk_auth_callback!(my_auth_callback, PASSWORD); +mk_tpm_finalizer!(my_tpm_finalizer, my_auth_callback); + +const KEY_FLAGS: &[KeyFlags] = &[KeyFlags::NoDA]; + +// ========================================================================== +// Test cases +// ========================================================================== + +/// Test the `create_key()` function +#[test] +#[serial] +#[named] +fn test_create_key() { + let _configuration = TestConfiguration::with_finalizer(my_tpm_finalizer); + + repeat_test!(|i| { + let key_path = &format!("HS/SRK/myTestKey{}", i); + + // Create FAPI context + let mut context = match FapiContext::new() { + Ok(fpai_ctx) => fpai_ctx, + Err(error) => panic!("Failed to create context: {:?}", error), + }; + + // Initialize TPM, if not already initialized + tpm_initialize!(context, PASSWORD, my_auth_callback); + + // Create new key, if not already created + match context.create_key(&key_path, Some(KEY_FLAGS), None, Some(PASSWORD)) { + Ok(_) => debug!("Key created successfully."), + Err(error) => panic!("Key creation has failed: {:?}", error), + } + }); +} + +/// Test the `list()` function after a key has been created via the `create_key()` function +#[test] +#[serial] +#[named] +fn test_list_keys() { + let _configuration = TestConfiguration::with_finalizer(my_tpm_finalizer); + + repeat_test!(|i| { + let key_paths = [ + &format!("HS/SRK/myTestKey{}a", i), + &format!("HS/SRK/myTestKey{}b", i), + ]; + + // Create FAPI context + let mut context = match FapiContext::new() { + Ok(fpai_ctx) => fpai_ctx, + Err(error) => panic!("Failed to create context: {:?}", error), + }; + + // Initialize TPM, if not already initialized + tpm_initialize!(context, PASSWORD, my_auth_callback); + + // Create new key, if not already created + for key_path in key_paths { + match context.create_key(key_path, Some(KEY_FLAGS), None, Some(PASSWORD)) { + Ok(_) => debug!("Key created successfully."), + Err(error) => panic!("Key creation has failed: {:?}", error), + } + } + + // Enumerate keys + let mut key_list = match context.list("HS/SRK") { + Ok(list) => list, + Err(error) => panic!("Failed to enumerate the keys: {:?}", error), + }; + + // Check if all keys are present + let mut pending_keys = HashSet::from(key_paths.map(|elem| format!("/{}", elem))); + for list_entry in key_list.drain(..) { + debug!("KeyList entry: \"{}\"", list_entry); + pending_keys.retain(|elem| !list_entry.ends_with(elem)); + } + + // All found? + assert_eq!(0usize, pending_keys.len()); + }); +} + +/// Test the `export_key()` function after a key has been created via the `create_key()` function +#[test] +#[serial] +#[named] +fn test_export_key() { + let configuration = TestConfiguration::with_finalizer(my_tpm_finalizer); + + repeat_test!(|i| { + let key_path = &format!("HS/SRK/myTestKey{}", i); + + // Create FAPI context + let mut context = match FapiContext::new() { + Ok(fpai_ctx) => fpai_ctx, + Err(error) => panic!("Failed to create context: {:?}", error), + }; + + // Initialize TPM, if not already initialized + tpm_initialize!(context, PASSWORD, my_auth_callback); + + // Create new key, if not already created + match context.create_key(&key_path, Some(KEY_FLAGS), None, Some(PASSWORD)) { + Ok(_) => debug!("Key created successfully."), + Err(error) => panic!("Key creation has failed: {:?}", error), + } + + // Export the key from TPM + let key_data_json = match context.export_key(&key_path, None) { + Ok(data) => data, + Err(error) => panic!("Key export has failed: {:?}", error), + }; + + // Get public key in PEM format from JSON data + assert!(!key_data_json.is_empty()); + let pem_data = key_data_json["pem_ext_public"] + .as_str() + .expect("Failed to extract public key from JSON data!"); + debug!("PEM data: {:?}", pem_data); + + // Load public key from PEM data + let loaded_public_key = get_key_type(&configuration.prof_name()) + .map(|key_type| load_public_key(pem_data, key_type)); + assert!(loaded_public_key.is_some()); + debug!("Public key: {:?}", loaded_public_key); + }); +} + +/// Test the `delete()` function after a key has been created via the `create_key()` function +#[test] +#[serial] +#[named] +fn test_delete() { + let _configuration = TestConfiguration::with_finalizer(my_tpm_finalizer); + + repeat_test!(|i| { + let key_path = &format!("HS/SRK/myTestKey{}", i); + + // Create FAPI context + let mut context = match FapiContext::new() { + Ok(fpai_ctx) => fpai_ctx, + Err(error) => panic!("Failed to create context: {:?}", error), + }; + + // Initialize TPM, if not already initialized + tpm_initialize!(context, PASSWORD, my_auth_callback); + + // Create new key, if not already created + match context.create_key(&key_path, Some(KEY_FLAGS), None, Some(PASSWORD)) { + Ok(_) => debug!("Key created successfully."), + Err(error) => panic!("Key creation has failed: {:?}", error), + } + + // Delete the key + match context.delete(&key_path) { + Ok(_) => debug!("Key deleted successfully."), + Err(error) => panic!("The key could not be deleted: {:?}", error), + }; + + // Try delete the key again (expected to fail!) + match context.delete(&key_path) { + Err(ErrorCode::FapiError(BaseErrorCode::BadPath)) => (), + Ok(_) => panic!("Key was deleted again!"), + Err(error) => panic!("The key could not be deleted: {:?}", error), + }; + }); +} + +/// Test the `get_tpm_blobs()` function after a key has been created via the `create_key()` function +#[test] +#[serial] +#[named] +fn test_get_tpm_blobs() { + let _configuration = TestConfiguration::with_finalizer(my_tpm_finalizer); + + repeat_test!(|i| { + let key_path = &format!("HS/SRK/myTestKey{}", i); + + // Create FAPI context + let mut context = match FapiContext::new() { + Ok(fpai_ctx) => fpai_ctx, + Err(error) => panic!("Failed to create context: {:?}", error), + }; + + // Initialize TPM, if not already initialized + tpm_initialize!(context, PASSWORD, my_auth_callback); + + // Create new key, if not already created + match context.create_key(&key_path, Some(KEY_FLAGS), None, Some(PASSWORD)) { + Ok(_) => debug!("Key created successfully."), + Err(error) => panic!("Key creation has failed: {:?}", error), + } + + // Fetch public/private key data + let blobs = match context.get_tpm_blobs(key_path, true, false, false) { + Ok(value) => value, + Err(error) => panic!("Failed to obtain TPM blobs: {:?}", error), + }; + + // Verify public key + let pub_key = blobs.0.expect("No public key data has been returned!"); + assert!(pub_key.len() >= 32usize); + debug!("Public key: {}", hex::encode(pub_key)); + + // Verify private key + assert!(blobs.1.is_none()); + assert!(blobs.2.is_none()); + }); +} + +/// Test the `get_tpm_blobs()` function after a key has been created via the `create_key()` function +#[test] +#[serial] +#[named] +fn test_get_tpm_blobs_with_private() { + let _configuration = TestConfiguration::with_finalizer(my_tpm_finalizer); + + repeat_test!(|i| { + let key_path = &format!("HS/SRK/myTestKey{}", i); + + // Create FAPI context + let mut context = match FapiContext::new() { + Ok(fpai_ctx) => fpai_ctx, + Err(error) => panic!("Failed to create context: {:?}", error), + }; + + // Initialize TPM, if not already initialized + tpm_initialize!(context, PASSWORD, my_auth_callback); + + // Create new key, if not already created + match context.create_key(&key_path, Some(KEY_FLAGS), None, Some(PASSWORD)) { + Ok(_) => debug!("Key created successfully."), + Err(error) => panic!("Key creation has failed: {:?}", error), + } + + // Fetch public/private key data + let blobs = match context.get_tpm_blobs(key_path, true, true, true) { + Ok(value) => value, + Err(error) => panic!("Failed to obtain TPM blobs: {:?}", error), + }; + + // Verify public key + let pub_key = blobs.0.expect("No public key data has been returned!"); + assert!(pub_key.len() >= 32usize); + debug!("Public key: {}", hex::encode(pub_key)); + + // Verify private key + let sec_key = blobs.1.expect("No private key data has been returned!"); + assert!(sec_key.len() >= 32usize); + debug!("Private key: {}", hex::encode(sec_key)); + + // Print the policy, if any: + assert!(blobs.2.as_ref().map_or(true, |policy| !policy.is_empty())); + debug!("Policy: {:?}", blobs.2) + }); +} + +/// Test the `get_esys_blob()` function after a key has been created via the `create_key()` function +#[test] +#[serial] +#[named] +fn test_get_esys_blob() { + let _configuration = TestConfiguration::with_finalizer(my_tpm_finalizer); + + repeat_test!(|i| { + let key_path = &format!("HS/SRK/myTestKey{}", i); + + // Create FAPI context + let mut context = match FapiContext::new() { + Ok(fpai_ctx) => fpai_ctx, + Err(error) => panic!("Failed to create context: {:?}", error), + }; + + // Initialize TPM, if not already initialized + tpm_initialize!(context, PASSWORD, my_auth_callback); + + // Create new key, if not already created + match context.create_key(&key_path, Some(KEY_FLAGS), None, Some(PASSWORD)) { + Ok(_) => debug!("Key created successfully."), + Err(error) => panic!("Key creation has failed: {:?}", error), + } + + // Fetch ESAPI blob + let esys_blob = match context.get_esys_blob(key_path) { + Ok(value) => value, + Err(error) => panic!("Failed to obtain ESAPI blob: {:?}", error), + }; + + // Verify public key + debug!("ESAPI blob type: {:?}", esys_blob.0); + debug!("ESAPI blob data: {}", hex::encode(&esys_blob.1[..])); + + //Check result + assert!(esys_blob.1.len() >= 32usize); + }); +} diff --git a/tests/06_encrypt_decrypt_test.rs b/tests/06_encrypt_decrypt_test.rs new file mode 100644 index 0000000..59da85d --- /dev/null +++ b/tests/06_encrypt_decrypt_test.rs @@ -0,0 +1,126 @@ +/* SPDX-License-Identifier: BSD-3-Clause */ +/******************************************************************************* + * Copyright 2024, Fraunhofer SIT sponsored by the ELISA research project + * All rights reserved. + ******************************************************************************/ + +pub mod common; + +use common::{ + param::PASSWORD, + random::{create_seed, generate_bytes}, + setup::TestConfiguration, +}; +use function_name::named; +use log::{debug, warn}; +use rand::SeedableRng; +use rand_chacha::ChaChaRng; +use serial_test::serial; +use tss2_fapi_rs::{FapiContext, KeyFlags}; + +mk_auth_callback!(my_auth_callback, PASSWORD); +mk_tpm_finalizer!(my_tpm_finalizer, my_auth_callback); + +const KEY_FLAGS_ENCR: &[KeyFlags] = &[KeyFlags::NoDA, KeyFlags::Decrypt]; + +// ========================================================================== +// Test cases +// ========================================================================== + +/// Test the `encrypt()` function to encrypt a random message with a suitable key +#[test] +#[serial] +#[named] +fn test_encrypt() { + let configuration = TestConfiguration::with_finalizer(my_tpm_finalizer); + skip_test_ifeq!(configuration, "ECC"); + + repeat_test!(|i| { + let key_path = &format!("HS/SRK/myEncKey{}", i); + + // Initialize RNG + let mut rng = ChaChaRng::from_seed(create_seed(i)); + + // Create FAPI context + let mut context = match FapiContext::new() { + Ok(fpai_ctx) => fpai_ctx, + Err(error) => panic!("Failed to create context: {:?}", error), + }; + + // Initialize TPM, if not already initialized + tpm_initialize!(context, PASSWORD, my_auth_callback); + + // Create new key, if not already created + match context.create_key(&key_path, Some(KEY_FLAGS_ENCR), None, Some(PASSWORD)) { + Ok(_) => debug!("Key created."), + Err(error) => panic!("Key creation has failed: {:?}", error), + } + + // Generate plain-text + let plaintext: [u8; 128usize] = generate_bytes(&mut rng); + + // Encrypt the plaintext + let ciphertext = match context.encrypt(key_path, &plaintext) { + Ok(value) => value, + Err(error) => panic!("Failed to encrypt plaintext: {:?}", error), + }; + + // Validate the ciphertext + debug!("Ciphertext: {}", hex::encode(&ciphertext[..])); + assert!(ciphertext.len() >= plaintext.len()); + }); +} + +/// Test the `decrypt()` function on some ciphertext that was created via the `encrypt()` function with a suitable key +#[test] +#[serial] +#[named] +fn test_decrypt() { + let configuration = TestConfiguration::with_finalizer(my_tpm_finalizer); + skip_test_ifeq!(configuration, "ECC"); + + repeat_test!(|i| { + let key_path = &format!("HS/SRK/myEncKey{}", i); + + // Initialize RNG + let mut rng = ChaChaRng::from_seed(create_seed(i)); + + // Create FAPI context + let mut context = match FapiContext::new() { + Ok(fpai_ctx) => fpai_ctx, + Err(error) => panic!("Failed to create context: {:?}", error), + }; + + // Initialize TPM, if not already initialized + tpm_initialize!(context, PASSWORD, my_auth_callback); + + // Create new key, if not already created + match context.create_key(&key_path, Some(KEY_FLAGS_ENCR), None, Some(PASSWORD)) { + Ok(_) => debug!("Key created."), + Err(error) => panic!("Key creation has failed: {:?}", error), + } + + // Generate plain-text + let plaintext: [u8; 128usize] = generate_bytes(&mut rng); + + // Encrypt the plaintext + let ciphertext = match context.encrypt(key_path, &plaintext[..]) { + Ok(value) => value, + Err(error) => panic!("Failed to encrypt plaintext: {:?}", error), + }; + + // Validate ciphertext + debug!("Ciphertext: {}", hex::encode(&ciphertext[..])); + assert!(ciphertext.len() >= plaintext.len()); + + // Decrypt the plaintext + let decrypted = match context.decrypt(key_path, &ciphertext) { + Ok(value) => value, + Err(error) => panic!("Failed to decrypt ciphertext: {:?}", error), + }; + + // Validate the ciphertext + debug!("Decrypted: {}", hex::encode(&decrypted[..])); + assert!((&decrypted[..]).eq(&plaintext[..])); + }); +} diff --git a/tests/07_signature_test.rs b/tests/07_signature_test.rs new file mode 100644 index 0000000..7924e0e --- /dev/null +++ b/tests/07_signature_test.rs @@ -0,0 +1,237 @@ +/* SPDX-License-Identifier: BSD-3-Clause */ +/******************************************************************************* + * Copyright 2024, Fraunhofer SIT sponsored by the ELISA research project + * All rights reserved. + ******************************************************************************/ + +pub mod common; + +use common::{ + crypto::{get_key_type, KeyType}, + param::PASSWORD, + random::{create_seed, generate_bytes}, + setup::TestConfiguration, +}; +use function_name::named; +use log::{debug, trace}; +use rand::SeedableRng; +use rand_chacha::ChaChaRng; +use serial_test::serial; +use sha2::{Digest, Sha256}; +use tss2_fapi_rs::{FapiContext, KeyFlags, PaddingFlags}; + +mk_auth_callback!(my_auth_callback, PASSWORD); +mk_tpm_finalizer!(my_tpm_finalizer, my_auth_callback); + +const KEY_FLAGS_SIGN: &[KeyFlags] = &[KeyFlags::NoDA, KeyFlags::Sign]; +const PADDING_RSAPSS: &[PaddingFlags] = &[PaddingFlags::RsaPss]; + +// ========================================================================== +// Test cases +// ========================================================================== + +/// Test the `sign()` function to sign some random data with a suitable key and the matching padding algorithm +#[test] +#[serial] +#[named] +fn test_sign() { + let configuration = TestConfiguration::with_finalizer(my_tpm_finalizer); + + repeat_test!(|i| { + let key_path = &format!("HS/SRK/mySigKey{}", i); + + // Initialize RNG + let mut rng = ChaChaRng::from_seed(create_seed(i)); + + // Create FAPI context + let mut context = match FapiContext::new() { + Ok(fpai_ctx) => fpai_ctx, + Err(error) => panic!("Failed to create context: {:?}", error), + }; + + // Initialize TPM, if not already initialized + tpm_initialize!(context, PASSWORD, my_auth_callback); + + // Create new key, if not already created + match context.create_key(&key_path, Some(KEY_FLAGS_SIGN), None, Some(PASSWORD)) { + Ok(_) => debug!("Key created."), + Err(error) => panic!("Key creation has failed: {:?}", error), + } + + // Compute digest to be signed + let digest = Sha256::digest(&generate_bytes::<256usize>(&mut rng)[..]); + debug!("Digest to be signed: {}", hex::encode(&digest[..])); + + // Select padding algorithm + let padding_algo = get_padding_algorithm(&configuration); + trace!("Padding algorithm: {:?}", padding_algo); + + // Create the signature + let signature = match context.sign(key_path, padding_algo, &digest, false, false) { + Ok(value) => value, + Err(error) => panic!("Failed to create the signature: {:?}", error), + }; + + // Validate signature data + let signature_data: &[u8] = signature.0.as_ref(); + assert!(signature_data.len() >= 32usize); + debug!("Signature value: {}", hex::encode(signature_data)); + + // Verify absent data + assert!(signature.1.is_none()); + assert!(signature.2.is_none()); + }); +} + +/// Test the `sign()` function to sign some random data with a suitable key and the matching padding algorithm; also request the signer's private key (and certificate). +#[test] +#[serial] +#[named] +fn test_sign_with_pubkey() { + let configuration = TestConfiguration::with_finalizer(my_tpm_finalizer); + + repeat_test!(|i| { + let key_path = &format!("HS/SRK/mySigKey{}", i); + + // Initialize RNG + let mut rng = ChaChaRng::from_seed(create_seed(i)); + + // Create FAPI context + let mut context = match FapiContext::new() { + Ok(fpai_ctx) => fpai_ctx, + Err(error) => panic!("Failed to create context: {:?}", error), + }; + + // Initialize TPM, if not already initialized + tpm_initialize!(context, PASSWORD, my_auth_callback); + + // Create new key, if not already created + match context.create_key(&key_path, Some(KEY_FLAGS_SIGN), None, Some(PASSWORD)) { + Ok(_) => debug!("Key created."), + Err(error) => panic!("Key creation has failed: {:?}", error), + } + + // Compute digest to be signed + let digest = Sha256::digest(&generate_bytes::<256usize>(&mut rng)[..]); + debug!("Digest to be signed: {}", hex::encode(&digest[..])); + + // Select padding algorithm + let padding_algo = get_padding_algorithm(&configuration); + trace!("Padding algorithm: {:?}", padding_algo); + + // Create the signature + let signature = match context.sign(key_path, padding_algo, &digest, true, true) { + Ok(value) => value, + Err(error) => panic!("Failed to create the signature: {:?}", error), + }; + + // Validate signature data + let signature_data: &[u8] = signature.0.as_ref(); + assert!(signature_data.len() >= 32usize); + debug!("Signature value: {}", hex::encode(signature_data)); + + // Print the public key + assert!(signature + .1 + .as_ref() + .map_or(false, |pem_data| !pem_data.is_empty())); + debug!("Public key: \"{}\"", signature.1.unwrap_or_default()); + + // Print the certificate, if any: + debug!( + "Certificate: \"{}\"", + signature.2.unwrap_or_else(String::new).trim() + ); + }); +} + +/// Test the `verify_signature()` function with a signature that was created via the `sign()` function +#[test] +#[serial] +#[named] +fn test_verify_signature() { + let configuration = TestConfiguration::with_finalizer(my_tpm_finalizer); + + repeat_test!(|i| { + let key_path = &format!("HS/SRK/mySigKey{}", i); + + // Initialize RNG + let mut rng = ChaChaRng::from_seed(create_seed(i)); + + // Create FAPI context + let mut context = match FapiContext::new() { + Ok(fpai_ctx) => fpai_ctx, + Err(error) => panic!("Failed to create context: {:?}", error), + }; + + // Initialize TPM, if not already initialized + tpm_initialize!(context, PASSWORD, my_auth_callback); + + // Create new key, if not already created + match context.create_key(&key_path, Some(KEY_FLAGS_SIGN), None, Some(PASSWORD)) { + Ok(_) => debug!("Key created successfully."), + Err(error) => panic!("Key creation has failed: {:?}", error), + } + + // Compute digest to be signed + let digest = Sha256::digest(&generate_bytes::<256usize>(&mut rng)[..]); + debug!("Digest to be signed: {}", hex::encode(&digest[..])); + + // Select padding algorithm + let padding_algo = get_padding_algorithm(&configuration); + trace!("Padding algorithm: {:?}", padding_algo); + + // Create the signature + let signature = match context.sign(key_path, padding_algo, &digest, false, false) { + Ok(value) => value, + Err(error) => panic!("Failed to create the signature: {:?}", error), + }; + + // Print signature data + debug!("Signature value: {}", hex::encode(&signature.0[..])); + + // Verify the signature + let verify_result = match context.verify_signature(key_path, &digest, &signature.0[..]) { + Ok(value) => value, + Err(error) => panic!("Failed to verify the signature: {:?}", error), + }; + + // Check result + debug!("Signature valid: {:?}", verify_result); + assert!(verify_result); + + // Modify signature value + let signature_mod = increment(&signature.0[..]); + + // Verify the signature (again) + let verify_result = match context.verify_signature(key_path, &digest, &signature_mod[..]) { + Ok(value) => value, + Err(error) => panic!("Failed to verify the signature: {:?}", error), + }; + + // Check result + debug!("Signature valid: {:?}", verify_result); + assert!(!verify_result); + }); +} + +// ========================================================================== +// Helper functions +// ========================================================================== + +fn increment(data: &[u8]) -> Vec { + let mut mut_data: Vec = data.into(); + _increment(mut_data.last_mut().expect("Data must not be empty!")); + mut_data +} + +fn _increment(data: &mut u8) { + *data = data.wrapping_add(1u8); +} + +fn get_padding_algorithm(config: &TestConfiguration) -> Option<&'static [PaddingFlags]> { + match get_key_type(config.prof_name()) { + Some(KeyType::RsaKey) => Some(PADDING_RSAPSS), + _ => None, + } +} diff --git a/tests/08_nv_test.rs b/tests/08_nv_test.rs new file mode 100644 index 0000000..329cee4 --- /dev/null +++ b/tests/08_nv_test.rs @@ -0,0 +1,342 @@ +/* SPDX-License-Identifier: BSD-3-Clause */ +/******************************************************************************* + * Copyright 2024, Fraunhofer SIT sponsored by the ELISA research project + * All rights reserved. + ******************************************************************************/ + +pub mod common; + +use common::{ + param::PASSWORD, + random::{create_seed, generate_bytes}, + setup::TestConfiguration, +}; +use function_name::named; +use log::{debug, trace}; +use rand::{thread_rng, RngCore, SeedableRng}; +use rand_chacha::ChaChaRng; +use serial_test::serial; +use tss2_fapi_rs::{FapiContext, NvFlags}; + +mk_auth_callback!(my_auth_callback, PASSWORD); +mk_tpm_finalizer!(my_tpm_finalizer, my_auth_callback); + +const NV_ORDINARY_FLAGS: &[NvFlags] = &[NvFlags::NoDA]; +const NV_COUNTER_FLAGS: &[NvFlags] = &[NvFlags::Counter, NvFlags::NoDA]; +const NV_BITFIELD_FLAGS: &[NvFlags] = &[NvFlags::BitField, NvFlags::NoDA]; +const NV_PCR_FLAGS: &[NvFlags] = &[NvFlags::PCR, NvFlags::NoDA]; + +// ========================================================================== +// Test cases +// ========================================================================== + +/// Test the `nv_write()` function on some newly created NV index +#[test] +#[serial] +#[named] +fn test_nv_write() { + let _configuration = TestConfiguration::with_finalizer(my_tpm_finalizer); + + repeat_test!(|i| { + let nv_path = &format!("nv/Owner/myNv{}", i); + let mut data = [0u8; 128usize]; + + // Create FAPI context + let mut context = match FapiContext::new() { + Ok(fpai_ctx) => fpai_ctx, + Err(error) => panic!("Failed to create context: {:?}", error), + }; + + // Initialize TPM, if not already initialized + tpm_initialize!(context, PASSWORD, my_auth_callback); + + // Create NV index, if not already created + match context.create_nv(&nv_path, Some(NV_ORDINARY_FLAGS), data.len(), None, None) { + Ok(_) => debug!("NV index created."), + Err(error) => panic!("NV index creation has failed: {:?}", error), + } + + // Generate random data + thread_rng().fill_bytes(&mut data[..]); + + // Write data to NV index + match context.nv_write(&nv_path, &data[..]) { + Ok(_) => debug!("Data written."), + Err(error) => panic!("Writing data to NV index has failed: {:?}", error), + } + + // Generate random number + let number = thread_rng().next_u64(); + + // Write number to NV index + match context.nv_write_u64(&nv_path, number) { + Ok(_) => debug!("Number written."), + Err(error) => panic!("Writing data to NV index has failed: {:?}", error), + } + }); +} + +/// Test the `nv_read()` function to read back data that was written to a NV index via the `nv_write()` function +#[test] +#[serial] +#[named] +fn test_nv_read() { + let _configuration = TestConfiguration::with_finalizer(my_tpm_finalizer); + + repeat_test!(|i| { + let nv_path = &format!("nv/Owner/myNv{}", i); + let mut data = [0u8; 128usize]; + + // Create FAPI context + let mut context = match FapiContext::new() { + Ok(fpai_ctx) => fpai_ctx, + Err(error) => panic!("Failed to create context: {:?}", error), + }; + + // Initialize TPM, if not already initialized + tpm_initialize!(context, PASSWORD, my_auth_callback); + + // Create NV index, if not already created + match context.create_nv(&nv_path, Some(NV_ORDINARY_FLAGS), data.len(), None, None) { + Ok(_) => debug!("NV index created."), + Err(error) => panic!("NV index creation has failed: {:?}", error), + } + + // Generate random data + thread_rng().fill_bytes(&mut data[..]); + + // Write data to NV index + match context.nv_write(&nv_path, &data[..]) { + Ok(_) => debug!("Data written."), + Err(error) => panic!("Writing data to NV index has failed: {:?}", error), + } + + // Read data from NV index + let recovered_data = match context.nv_read(&nv_path, false) { + Ok(data) => data, + Err(error) => panic!("Reading data from NV index has failed: {:?}", error), + }; + + // Validate the result + assert!((&recovered_data.0[..]).eq(&data[..])); + assert!(recovered_data.1.is_none()); + }); +} + +/// Test the `nv_increment()` function to increment (multiple times) a NV index that was created in "counter" mode, verify updated value via the `nv_read_u64()` function +#[test] +#[serial] +#[named] +fn test_nv_counter() { + let _configuration = TestConfiguration::with_finalizer(my_tpm_finalizer); + + repeat_test!(|i| { + let nv_path = &format!("nv/Owner/myNvCtr{}", i); + + // Create FAPI context + let mut context = match FapiContext::new() { + Ok(fpai_ctx) => fpai_ctx, + Err(error) => panic!("Failed to create context: {:?}", error), + }; + + // Initialize TPM, if not already initialized + tpm_initialize!(context, PASSWORD, my_auth_callback); + + // Create NV index, if not already created + match context.create_nv(&nv_path, Some(NV_COUNTER_FLAGS), 0usize, None, None) { + Ok(_) => debug!("NV index created."), + Err(error) => panic!("NV index creation has failed: {:?}", error), + } + + // Increment the counter + match context.nv_increment(&nv_path) { + Ok(_) => debug!("Incremented."), + Err(error) => panic!("Incrementing the NV index has failed: {:?}", error), + }; + + // Read data from NV index + let counter_1 = match context.nv_read_u64(&nv_path) { + Ok(value) => value, + Err(error) => panic!("Reading data from NV index has failed: {:?}", error), + }; + + // Increment the counter + match context.nv_increment(&nv_path) { + Ok(_) => debug!("Incremented."), + Err(error) => panic!("Incrementing the NV index has failed: {:?}", error), + }; + + // Read data from NV index + let counter_2 = match context.nv_read_u64(&nv_path) { + Ok(value) => value, + Err(error) => panic!("Reading data from NV index has failed: {:?}", error), + }; + + // Validate the result + debug!("Counter value #1: 0x{:016X}", counter_1); + debug!("Counter value #2: 0x{:016X}", counter_2); + + // Verify + assert_eq!(counter_1 + 1u64, counter_2); + }); +} + +/// Test the `nv_set_bits()` function to set some bits in a NV index that was created in "BitField" mode, verify updated value via the `nv_read_u64()` function +#[test] +#[serial] +#[named] +fn test_nv_bitset() { + let _configuration = TestConfiguration::with_finalizer(my_tpm_finalizer); + + repeat_test!(|i| { + let nv_path = &format!("nv/Owner/myNvBits{}", i); + + // Create FAPI context + let mut context = match FapiContext::new() { + Ok(fpai_ctx) => fpai_ctx, + Err(error) => panic!("Failed to create context: {:?}", error), + }; + + // Initialize TPM, if not already initialized + tpm_initialize!(context, PASSWORD, my_auth_callback); + + // Create NV index, if not already created + match context.create_nv(&nv_path, Some(NV_BITFIELD_FLAGS), 0usize, None, None) { + Ok(_) => debug!("NV index created."), + Err(error) => panic!("NV index creation has failed: {:?}", error), + } + + // Initialize bits in NV index + match context.nv_set_bits(&nv_path, 0u64) { + Ok(_) => debug!("Bits set."), + Err(error) => panic!("Setting the NV index bits has failed: {:?}", error), + }; + + // Read data from NV index + let bits_0 = match context.nv_read_u64(&nv_path) { + Ok(value) => value, + Err(error) => panic!("Reading data from NV index has failed: {:?}", error), + }; + + // Set bits in NV index + match context.nv_set_bits(&nv_path, 0x5555555555555555u64) { + Ok(_) => debug!("Bits set."), + Err(error) => panic!("Setting the NV index bits has failed: {:?}", error), + }; + + // Read data from NV index + let bits_1 = match context.nv_read_u64(&nv_path) { + Ok(value) => value, + Err(error) => panic!("Reading data from NV index has failed: {:?}", error), + }; + + // Set bits in NV index + match context.nv_set_bits(&nv_path, 0xAAAAAAAAAAAAAAAAu64) { + Ok(_) => debug!("Bits set."), + Err(error) => panic!("Setting the NV index bits has failed: {:?}", error), + }; + + // Read data from NV index + let bits_2 = match context.nv_read_u64(&nv_path) { + Ok(value) => value, + Err(error) => panic!("Reading data from NV index has failed: {:?}", error), + }; + + // Validate the result + debug!("Counter value #0: 0x{:016X}", bits_0); + debug!("Counter value #1: 0x{:016X}", bits_1); + debug!("Counter value #2: 0x{:016X}", bits_2); + + // Verify + assert_eq!(bits_0 ^ bits_1, 0x5555555555555555u64); + assert_eq!(bits_1 ^ bits_2, 0xAAAAAAAAAAAAAAAAu64); + }); +} + +/// Test the `nv_extend()` function to update a NV index that was created in "PCR" mode, verify updated value via the `nv_read()` function +#[test] +#[serial] +#[named] +fn test_nv_pcr() { + let _configuration = TestConfiguration::with_finalizer(my_tpm_finalizer); + + repeat_test!(|i| { + let nv_path = &format!("nv/Owner/myNvPcr{}", i); + + // Initialize RNG + let mut rng = ChaChaRng::from_seed(create_seed(i)); + + // Create FAPI context + let mut context = match FapiContext::new() { + Ok(fpai_ctx) => fpai_ctx, + Err(error) => panic!("Failed to create context: {:?}", error), + }; + + // Initialize TPM, if not already initialized + tpm_initialize!(context, PASSWORD, my_auth_callback); + + // Create NV index, if not already created + match context.create_nv(&nv_path, Some(NV_PCR_FLAGS), 0usize, None, None) { + Ok(_) => debug!("NV index created."), + Err(error) => panic!("NV index creation has failed: {:?}", error), + } + + // Prepare log data + let log_data = [ + json::parse("{ \"test\": \"1st value\" }").unwrap(), + json::parse("{ \"test\": \"2nd value\" }").unwrap(), + ]; + + // Extend PCR at NV index + match context.nv_extend( + &nv_path, + &generate_bytes::<128usize>(&mut rng)[..], + Some(&log_data[0]), + ) { + Ok(_) => debug!("Extended."), + Err(error) => panic!("Incrementing NV index has failed: {:?}", error), + }; + + // Read data from NV index + let pcr_value_1 = match context.nv_read(&nv_path, true) { + Ok(value) => value, + Err(error) => panic!("Reading data from NV index has failed: {:?}", error), + }; + + // Print PCR data #1 + assert!(pcr_value_1.0.len() >= 20usize); + debug!("PCR value #1: 0x{}", hex::encode(&pcr_value_1.0[..])); + if let Some(log_data) = pcr_value_1.1 { + trace!("PCR log data: {:?}", log_data); + assert!(log_data.is_array()); + } + + // Extend PCR at NV index + match context.nv_extend( + &nv_path, + &generate_bytes::<128usize>(&mut rng)[..], + Some(&log_data[1]), + ) { + Ok(_) => debug!("Extended."), + Err(error) => panic!("Incrementing NV index has failed: {:?}", error), + }; + + // Read data from NV index + let pcr_value_2 = match context.nv_read(&nv_path, true) { + Ok(value) => value, + Err(error) => panic!("Reading data from NV index has failed: {:?}", error), + }; + + // Print PCR data #2 + assert!(pcr_value_2.0.len() >= 20usize); + debug!("PCR value #2: 0x{}", hex::encode(&pcr_value_2.0[..])); + if let Some(log_data) = pcr_value_2.1 { + trace!("PCR log data: {:?}", log_data); + assert!(log_data.is_array()); + } + + // Verify + assert_eq!(pcr_value_1.0.len(), pcr_value_2.0.len()); + assert!((&pcr_value_1.0[..]).ne(&pcr_value_2.0[..])); + }); +} diff --git a/tests/09_policy_test.rs b/tests/09_policy_test.rs new file mode 100644 index 0000000..a5d28d7 --- /dev/null +++ b/tests/09_policy_test.rs @@ -0,0 +1,409 @@ +/* SPDX-License-Identifier: BSD-3-Clause */ +/******************************************************************************* + * Copyright 2024, Fraunhofer SIT sponsored by the ELISA research project + * All rights reserved. + ******************************************************************************/ + +pub mod common; + +use common::{ + crypto::{create_signature, load_private_key, KeyType, PrivateKey}, + param::PASSWORD, + random::{create_seed, generate_bytes}, + setup::TestConfiguration, +}; +use function_name::named; +use json::JsonValue; +use log::{debug, trace, warn}; +use rand::SeedableRng; +use rand_chacha::ChaChaRng; +use serial_test::serial; +use sha2::{Digest, Sha256}; +use std::{ + fs, + path::Path, + sync::{Arc, Mutex}, +}; +use tss2_fapi_rs::{ + ActnCallback, ActnCallbackParam, BranCallback, BranCallbackParam, FapiContext, KeyFlags, + SignCallback, SignCallbackParam, +}; + +mk_auth_callback!(my_auth_callback, PASSWORD); +mk_tpm_finalizer!(my_tpm_finalizer, my_auth_callback); + +const KEY_FLAGS_SIGN: &[KeyFlags] = &[KeyFlags::NoDA, KeyFlags::Sign]; + +// ========================================================================== +// Test cases +// ========================================================================== + +/// Test the `import()` and `set_sign_callback()` functions to import and use a `POLICYSIGNED` policy containing an RSA key +#[test] +#[serial] +#[named] +fn test_policy_signed_rsa() { + let configuration = TestConfiguration::with_finalizer(my_tpm_finalizer); + + repeat_test!(|i| { + let key_path = &format!("HS/SRK/myPolKeyRsa{}", i); + let pol_name = &format!("/policy/pol_signed_rsa{}", i); + + // Initialize RNG + let mut rng = ChaChaRng::from_seed(create_seed(i)); + + // Create FAPI context + + let mut context = match FapiContext::new() { + Ok(fpai_ctx) => fpai_ctx, + Err(error) => panic!("Failed to create context: {:?}", error), + }; + + // Initialize TPM, if not already initialized + tpm_initialize!(context, PASSWORD, my_auth_callback); + + // Read policy from file + let policy_json = read_policy(configuration.data_path(), "pol_signed_rsa") + .expect("Failed to read policy!"); + + // Read the private key + let private_key = + read_private_key(configuration.data_path(), KeyType::RsaKey, "key_rsa_2048") + .expect("Failed to load RSA private key!"); + + // Set up callback + match context.set_sign_callback(SignCallback::with_data(my_sign_callback, private_key)) { + Ok(_) => debug!("Sign callback installed."), + Err(error) => panic!("Failed to set up sign callback: {:?}", error), + } + + // Import policy + match context.import(pol_name, &policy_json) { + Ok(_) => debug!("Policy imported."), + Err(error) => panic!("Failed to import policy: {:?}", error), + }; + + // Create new key, if not already created + match context.create_key( + &key_path, + Some(KEY_FLAGS_SIGN), + Some(pol_name), + Some(PASSWORD), + ) { + Ok(_) => debug!("Key created with policy."), + Err(error) => panic!("Key creation has failed: {:?}", error), + } + + // Compute digest to be signed + let digest = Sha256::digest(&generate_bytes::<256usize>(&mut rng)[..]); + debug!("Digest to be signed: {}", hex::encode(&digest[..])); + + // Create the signature + let _signature = match context.sign(key_path, None, &digest, false, false) { + Ok(value) => value, + Err(error) => panic!("Failed to create the signature: {:?}", error), + }; + }); +} + +/// Test the `import()` and `set_sign_callback()` functions to import and use a `POLICYSIGNED` policy containing an ECC key +#[test] +#[serial] +#[named] +fn test_policy_signed_ecc() { + let configuration = TestConfiguration::with_finalizer(my_tpm_finalizer); + + repeat_test!(|i| { + let key_path = &format!("HS/SRK/myPolKeyEcc{}", i); + let pol_name = &format!("/policy/pol_signed_ecc{}", i); + + // Initialize RNG + let mut rng = ChaChaRng::from_seed(create_seed(i)); + + // Create FAPI context + let mut context = match FapiContext::new() { + Ok(fpai_ctx) => fpai_ctx, + Err(error) => panic!("Failed to create context: {:?}", error), + }; + + // Initialize TPM, if not already initialized + tpm_initialize!(context, PASSWORD, my_auth_callback); + + // Read policy from file + let policy_json = read_policy(configuration.data_path(), "pol_signed_ecc") + .expect("Failed to read policy!"); + + // Read the private key + let private_key = + read_private_key(configuration.data_path(), KeyType::EccKey, "key_ecc_256") + .expect("Failed to load ECC private key!"); + + // Set up callback + match context.set_sign_callback(SignCallback::with_data(my_sign_callback, private_key)) { + Ok(_) => debug!("Sign callback installed."), + Err(error) => panic!("Failed to set up sign callback: {:?}", error), + } + + // Import policy + match context.import(pol_name, &policy_json) { + Ok(_) => debug!("Policy imported."), + Err(error) => panic!("Failed to import policy: {:?}", error), + }; + + // Create new key, if not already created + match context.create_key( + &key_path, + Some(KEY_FLAGS_SIGN), + Some(pol_name), + Some(PASSWORD), + ) { + Ok(_) => debug!("Key created with policy."), + Err(error) => panic!("Key creation has failed: {:?}", error), + } + + // Compute digest to be signed + let digest = Sha256::digest(&generate_bytes::<256usize>(&mut rng)[..]); + debug!("Digest to be signed: {}", hex::encode(&digest[..])); + + // Create the signature + let _signature = match context.sign(key_path, None, &digest, false, false) { + Ok(value) => value, + Err(error) => panic!("Failed to create the signature: {:?}", error), + }; + }); +} + +/// Test the `import()` and `set_branch_callback()` functions to import and use a `POLICYOR` policy +#[test] +#[serial] +#[named] +fn test_policy_or() { + let configuration = TestConfiguration::with_finalizer(my_tpm_finalizer); + + repeat_test!(|i| { + let key_path = &format!("HS/SRK/myPolKeyOr{}", i); + let pol_name = &format!("/policy/pol_or{}", i); + + // Initialize RNG + let mut rng = ChaChaRng::from_seed(create_seed(i)); + + // Create FAPI context + let mut context = match FapiContext::new() { + Ok(fpai_ctx) => fpai_ctx, + Err(error) => panic!("Failed to create context: {:?}", error), + }; + + // Initialize TPM, if not already initialized + tpm_initialize!(context, PASSWORD, my_auth_callback); + + // Read policy from file + let policy_json = + read_policy(configuration.data_path(), "pol_or").expect("Failed to read policy!"); + + // Read the private key + let private_key = + read_private_key(configuration.data_path(), KeyType::RsaKey, "key_rsa_2048") + .expect("Failed to load RSA private key!"); + + // Set up callback + let branches: Arc>> = Arc::new(Mutex::new(Vec::new())); + match context + .set_branch_callback(BranCallback::with_data(my_bran_callback, branches.clone())) + { + Ok(_) => debug!("Branch callback installed."), + Err(error) => panic!("Failed to set up sign callback: {:?}", error), + } + + // Set up callback + match context.set_sign_callback(SignCallback::with_data(my_sign_callback, private_key)) { + Ok(_) => debug!("Sign callback installed."), + Err(error) => panic!("Failed to set up sign callback: {:?}", error), + } + + // Import policy + match context.import(pol_name, &policy_json) { + Ok(_) => debug!("Policy imported."), + Err(error) => panic!("Failed to import policy: {:?}", error), + }; + + // Create new key, if not already created + match context.create_key( + &key_path, + Some(KEY_FLAGS_SIGN), + Some(pol_name), + Some(PASSWORD), + ) { + Ok(_) => debug!("Key created with policy."), + Err(error) => panic!("Key creation has failed: {:?}", error), + } + + // Compute digest to be signed + let digest = Sha256::digest(&generate_bytes::<256usize>(&mut rng)[..]); + debug!("Digest to be signed: {}", hex::encode(&digest[..])); + + // Create the signature + let _signature = match context.sign(key_path, None, &digest, false, false) { + Ok(value) => value, + Err(error) => panic!("Failed to create the signature: {:?}", error), + }; + + // Check retrieved branches + let branches_ref = branches + .try_lock() + .expect("Failed to borrow the result value!"); + assert_eq!(branches_ref.len(), 2); + assert!(branches_ref[0].eq_ignore_ascii_case("#0,PolicySignedRSA")); + assert!(branches_ref[1].eq_ignore_ascii_case("#1,PolicySignedECC")); + }); +} + +/// Test the `import()` and `set_policy_action_callback()` functions to import and use a `POLICYACTION` policy +#[test] +#[serial] +#[named] +fn test_policy_action() { + let configuration = TestConfiguration::with_finalizer(my_tpm_finalizer); + + repeat_test!(|i| { + let key_path = &format!("HS/SRK/myPolKeyAction{}", i); + let pol_name = &format!("/policy/pol_action{}", i); + + // Initialize RNG + let mut rng = ChaChaRng::from_seed(create_seed(i)); + + // Create FAPI context + let mut context = match FapiContext::new() { + Ok(fpai_ctx) => fpai_ctx, + Err(error) => panic!("Failed to create context: {:?}", error), + }; + + // Initialize TPM, if not already initialized + tpm_initialize!(context, PASSWORD, my_auth_callback); + + // Read policy from file + let policy_json = + read_policy(configuration.data_path(), "pol_action").expect("Failed to read policy!"); + + // Set up callback + let actions: Arc>> = Arc::new(Mutex::new(Vec::new())); + match context + .set_policy_action_callback(ActnCallback::with_data(my_actn_callback, actions.clone())) + { + Ok(_) => debug!("Action callback installed."), + Err(error) => panic!("Failed to set up sign callback: {:?}", error), + } + + // Import policy + match context.import(pol_name, &policy_json) { + Ok(_) => debug!("Policy imported."), + Err(error) => panic!("Failed to import policy: {:?}", error), + }; + + // Create new key, if not already created + match context.create_key( + &key_path, + Some(KEY_FLAGS_SIGN), + Some(pol_name), + Some(PASSWORD), + ) { + Ok(_) => debug!("Key created with policy."), + Err(error) => panic!("Key creation has failed: {:?}", error), + } + + // Compute digest to be signed + let digest = Sha256::digest(&generate_bytes::<256usize>(&mut rng)[..]); + debug!("Digest to be signed: {}", hex::encode(&digest[..])); + + // Create the signature + let _signature = match context.sign(key_path, None, &digest, false, false) { + Ok(value) => value, + Err(error) => panic!("Failed to create the signature: {:?}", error), + }; + + // Check retrieved branches + let actions_ref = actions + .try_lock() + .expect("Failed to borrow the result value!"); + assert!(!actions_ref.is_empty()); + assert!(actions_ref[0].eq_ignore_ascii_case("myaction")); + }); +} + +// ========================================================================== +// Callback functions +// ========================================================================== + +/// Read policy from file +fn read_policy(data_path: &Path, policy_name: &str) -> Option { + let policy_file = data_path + .join("policies") + .join(format!("{}.json", policy_name)); + fs::read_to_string(policy_file) + .ok() + .and_then(|policy_data| json::parse(&policy_data[..]).ok()) +} + +/// Read private key from file +fn read_private_key(data_path: &Path, key_type: KeyType, key_name: &str) -> Option { + let pem_file = data_path.join("keys").join(format!("{}.pem", key_name)); + fs::read_to_string(pem_file) + .ok() + .and_then(|pem_data| load_private_key(&pem_data[..], key_type)) +} + +/// The "sign" callback implementation used for testing +fn my_sign_callback(param: SignCallbackParam, private_key: &PrivateKey) -> Option> { + debug!( + "(SIGN_CB) Signature for {:?} has been requested!", + param.object_path + ); + trace!("(SIGN_CB) Parameters: {:?}", param); + + let signature_value = create_signature(private_key, ¶m.hash_algo, param.challenge); + + if let Some(signature_bytes) = signature_value.as_ref() { + debug!( + "(SIGN_CB) Computed signature value: {}", + hex::encode(&signature_bytes[..]) + ); + } else { + warn!("(SIGN_CB) Failed to generate the signature!"); + } + + signature_value +} + +/// The "branch" callback implementation used for testing +fn my_bran_callback(param: BranCallbackParam, branches: &Arc>>) -> Option { + debug!( + "(BRAN_CB) Branch for {:?} has been requested!", + param.object_path + ); + trace!("(BRAN_CB) Parameters: {:?}", param); + + if let Ok(mut branches) = branches.try_lock() { + branches.clear(); + for name in param.branches.iter().enumerate() { + branches.push(format!("#{},{}", name.0, name.1.trim())); + } + } + + Some(0usize) +} + +/// The "action" callback implementation used for testing +fn my_actn_callback(param: ActnCallbackParam, actions: &Arc>>) -> bool { + debug!( + "(ACTN_CB) Action for {:?} has been requested!", + param.object_path + ); + trace!("(ACTN_CB) Parameters: {:?}", param); + + if let Ok(mut actions) = actions.try_lock() { + if let Some(action) = param.action { + actions.push(action.trim().to_owned()); + } + } + + param.action.is_some() +} diff --git a/tests/10_sealed_object_test.rs b/tests/10_sealed_object_test.rs new file mode 100644 index 0000000..96c28fa --- /dev/null +++ b/tests/10_sealed_object_test.rs @@ -0,0 +1,116 @@ +/* SPDX-License-Identifier: BSD-3-Clause */ +/******************************************************************************* + * Copyright 2024, Fraunhofer SIT sponsored by the ELISA research project + * All rights reserved. + ******************************************************************************/ + +pub mod common; + +use common::{ + param::PASSWORD, + random::{create_seed, generate_bytes}, + setup::TestConfiguration, +}; +use function_name::named; +use log::debug; +use rand::SeedableRng; +use rand_chacha::ChaChaRng; +use serial_test::serial; +use std::num::NonZeroUsize; +use tss2_fapi_rs::{FapiContext, SealFlags}; + +mk_auth_callback!(my_auth_callback, PASSWORD); +mk_tpm_finalizer!(my_tpm_finalizer, my_auth_callback); + +const SEAL_TYPE_FLAGS: &[SealFlags] = &[SealFlags::NoDA]; + +// ========================================================================== +// Test cases +// ========================================================================== + +/// Test the `create_seal()` function to create a new sealed object +#[test] +#[serial] +#[named] +fn test_create_seal() { + let _configuration = TestConfiguration::with_finalizer(my_tpm_finalizer); + + repeat_test!(|i| { + let key_path = &format!("/HS/SRK/mySealObj{}", i); + + // Create FAPI context + let mut context = match FapiContext::new() { + Ok(fpai_ctx) => fpai_ctx, + Err(error) => panic!("Failed to create context: {:?}", error), + }; + + // Initialize TPM, if not already initialized + tpm_initialize!(context, PASSWORD, my_auth_callback); + + // Create new seal, if not already created + match context.create_seal( + &key_path, + Some(SEAL_TYPE_FLAGS), + NonZeroUsize::new(32usize).unwrap(), + None, + None, + None, + ) { + Ok(_) => debug!("Seal created."), + Err(error) => panic!("Seal creation has failed: {:?}", error), + } + }); +} + +/// Test the `unseal()` function to unseal an object that was sealed via the `create_seal()` function +#[test] +#[serial] +#[named] +fn test_unseal() { + let _configuration = TestConfiguration::with_finalizer(my_tpm_finalizer); + + repeat_test!(|i| { + let key_path = &format!("/HS/SRK/mySealData{}", i); + + // Initialize RNG + let mut rng = ChaChaRng::from_seed(create_seed(i)); + + // Create FAPI context + let mut context = match FapiContext::new() { + Ok(fpai_ctx) => fpai_ctx, + Err(error) => panic!("Failed to create context: {:?}", error), + }; + + // Initialize TPM, if not already initialized + tpm_initialize!(context, PASSWORD, my_auth_callback); + + // Generate plain-text + let original_data: [u8; 128usize] = generate_bytes(&mut rng); + + // Create new seal, if not already created + match context.create_seal( + &key_path, + Some(SEAL_TYPE_FLAGS), + NonZeroUsize::new(original_data.len()).unwrap(), + None, + None, + Some(&original_data[..]), + ) { + Ok(_) => debug!("Seal created."), + Err(error) => panic!("Seal creation has failed: {:?}", error), + } + + // Unseal data + let unsealed_data = match context.unseal(&key_path) { + Ok(data) => data, + Err(error) => panic!("Unseal operation has failed: {:?}", error), + }; + + // Validate the result + debug!("Original data: {}", hex::encode(&original_data[..])); + debug!("Unsealed data: {}", hex::encode(&unsealed_data[..])); + + // Verify + assert!((&unsealed_data[..]).eq(&original_data[..])); + }); +} diff --git a/tests/11_prc_test.rs b/tests/11_prc_test.rs new file mode 100644 index 0000000..bc5ba66 --- /dev/null +++ b/tests/11_prc_test.rs @@ -0,0 +1,461 @@ +/* SPDX-License-Identifier: BSD-3-Clause */ +/******************************************************************************* + * Copyright 2024, Fraunhofer SIT sponsored by the ELISA research project + * All rights reserved. + ******************************************************************************/ + +pub mod common; + +use common::{ + param::PASSWORD, + random::{create_seed, generate_bytes}, + setup::TestConfiguration, +}; +use function_name::named; +use log::debug; +use rand::SeedableRng; +use rand_chacha::ChaChaRng; +use serial_test::serial; +use tss2_fapi_rs::{FapiContext, KeyFlags}; + +const KEY_FLAGS_RESTRICTED: &[KeyFlags] = &[KeyFlags::NoDA, KeyFlags::Restricted, KeyFlags::Sign]; +const PCR_NO: [u32; 8usize] = [8, 9, 10, 11, 12, 13, 14, 15]; + +mk_auth_callback!(my_auth_callback, PASSWORD); +mk_tpm_finalizer!(my_tpm_finalizer, my_auth_callback); + +// ========================================================================== +// Test cases +// ========================================================================== + +/// Test the `pcr_extend()` to update a PCR with some random data +#[test] +#[serial] +#[named] +fn test_pcr_extend() { + let _configuration = TestConfiguration::with_finalizer(my_tpm_finalizer); + + repeat_test!(|i| { + // Initialize RNG + let mut rng = ChaChaRng::from_seed(create_seed(i)); + + // Create FAPI context + let mut context = match FapiContext::new() { + Ok(fpai_ctx) => fpai_ctx, + Err(error) => panic!("Failed to create context: {:?}", error), + }; + + // Initialize TPM, if not already initialized + tpm_initialize!(context, PASSWORD, my_auth_callback); + + // Extend PCR with data + match context.pcr_extend( + PCR_NO[i % PCR_NO.len()], + &generate_bytes::<128usize>(&mut rng)[..], + None, + ) { + Ok(_) => debug!("PCR extended."), + Err(error) => panic!("PCR extension has failed: {:?}", error), + } + }); +} + +/// Test the `pcr_read()` function to read back the value of a PCR after it was updated via the `pcr_extend()` function +#[test] +#[serial] +#[named] +fn test_pcr_read() { + let _configuration = TestConfiguration::with_finalizer(my_tpm_finalizer); + + repeat_test!(|i| { + // Initialize RNG + let mut rng = ChaChaRng::from_seed(create_seed(i)); + + // Create FAPI context + let mut context = match FapiContext::new() { + Ok(fpai_ctx) => fpai_ctx, + Err(error) => panic!("Failed to create context: {:?}", error), + }; + + // Initialize TPM, if not already initialized + tpm_initialize!(context, PASSWORD, my_auth_callback); + + // Read current PCR value + let pcr_value_0 = match context.pcr_read(PCR_NO[i % PCR_NO.len()], false) { + Ok(data) => data, + Err(error) => panic!("PCR read has failed: {:?}", error), + }; + + // Print PCR data #0 + assert!(pcr_value_0.0.len() >= 20usize); + assert!(pcr_value_0.1.is_none()); + debug!("PCR value #0: 0x{}", hex::encode(&pcr_value_0.0[..])); + + // Extend PCR with data + match context.pcr_extend( + PCR_NO[i % PCR_NO.len()], + &generate_bytes::<128usize>(&mut rng)[..], + Some("{ \"test\": \"1st value\" }"), + ) { + Ok(_) => debug!("PCR extended."), + Err(error) => panic!("PCR extension has failed: {:?}", error), + } + + // Read current PCR value + let pcr_value_1 = match context.pcr_read(PCR_NO[i % PCR_NO.len()], false) { + Ok(data) => data, + Err(error) => panic!("PCR read has failed: {:?}", error), + }; + + // Print PCR data #1 + assert!(pcr_value_1.0.len() >= 20usize); + assert!(pcr_value_1.1.is_none()); + debug!("PCR value #1: 0x{}", hex::encode(&pcr_value_1.0[..])); + + // Extend PCR with data + match context.pcr_extend( + PCR_NO[i % PCR_NO.len()], + &generate_bytes::<128usize>(&mut rng)[..], + Some("{ \"test\": \"2nd value\" }"), + ) { + Ok(_) => debug!("PCR extended."), + Err(error) => panic!("PCR extension has failed: {:?}", error), + } + + // Read current PCR value + let pcr_value_2 = match context.pcr_read(PCR_NO[i % PCR_NO.len()], false) { + Ok(data) => data, + Err(error) => panic!("PCR read has failed: {:?}", error), + }; + + // Print PCR data #2 + assert!(pcr_value_2.0.len() >= 20usize); + assert!(pcr_value_2.1.is_none()); + debug!("PCR value #2: 0x{}", hex::encode(&pcr_value_2.0[..])); + + // Verify + assert_eq!(pcr_value_0.0.len(), pcr_value_1.0.len()); + assert_eq!(pcr_value_1.0.len(), pcr_value_2.0.len()); + assert!((&pcr_value_0.0[..]).ne(&pcr_value_1.0[..])); + assert!((&pcr_value_1.0[..]).ne(&pcr_value_2.0[..])); + assert!((&pcr_value_2.0[..]).ne(&pcr_value_0.0[..])); + }); +} + +/// Test the `pcr_read()` function to read back the value of a PCR after it was updated via the `pcr_extend()` function *and* request a PCR log +#[test] +#[serial] +#[named] +fn test_pcr_read_with_quote() { + let _configuration = TestConfiguration::with_finalizer(my_tpm_finalizer); + + repeat_test!(|i| { + // Initialize RNG + let mut rng = ChaChaRng::from_seed(create_seed(i)); + + // Create FAPI context + let mut context = match FapiContext::new() { + Ok(fpai_ctx) => fpai_ctx, + Err(error) => panic!("Failed to create context: {:?}", error), + }; + + // Initialize TPM, if not already initialized + tpm_initialize!(context, PASSWORD, my_auth_callback); + + // Extend PCR with data + match context.pcr_extend( + PCR_NO[i % PCR_NO.len()], + &generate_bytes::<128usize>(&mut rng)[..], + Some("{ \"test\": \"1st value\" }"), + ) { + Ok(_) => debug!("PCR extended."), + Err(error) => panic!("PCR extension has failed: {:?}", error), + } + + // Read current PCR value + let pcr_value = match context.pcr_read(PCR_NO[i % PCR_NO.len()], true) { + Ok(data) => data, + Err(error) => panic!("PCR read has failed: {:?}", error), + }; + + // Verify + assert!(pcr_value.0.len() >= 20usize); + assert!(pcr_value + .1 + .as_ref() + .map_or(false, |log_data| !log_data.is_empty())); + debug!("PCR value: 0x{}", hex::encode(&pcr_value.0[..])); + debug!("PCR log: {:?}", pcr_value.1.unwrap().to_string()); + }); +} + +/// Test the `quote()` function to create a quote of a set of several PCRs after they have been updated via the `pcr_extend()` function +#[test] +#[serial] +#[named] +fn test_pcr_quote() { + let _configuration = TestConfiguration::with_finalizer(my_tpm_finalizer); + + repeat_test!(|i| { + let key_path = &format!("HS/SRK/myTestAK{}", i); + + // Initialize RNG + let mut rng = ChaChaRng::from_seed(create_seed(i)); + + // Create FAPI context + let mut context = match FapiContext::new() { + Ok(fpai_ctx) => fpai_ctx, + Err(error) => panic!("Failed to create context: {:?}", error), + }; + + // Initialize TPM, if not already initialized + tpm_initialize!(context, PASSWORD, my_auth_callback); + + // Create attestation key, if not already created + match context.create_key(&key_path, Some(KEY_FLAGS_RESTRICTED), None, Some(PASSWORD)) { + Ok(_) => debug!("Key created successfully."), + Err(error) => panic!("Key creation has failed: {:?}", error), + } + + // Extend all PCR's with some data + for pcr_no in PCR_NO { + match context.pcr_extend(pcr_no, &generate_bytes::<128usize>(&mut rng)[..], None) { + Ok(_) => debug!("PCR extended."), + Err(error) => panic!("PCR extension has failed: {:?}", error), + } + } + + // Prepare inputs + let pcr_list = [ + PCR_NO[i % PCR_NO.len()], + PCR_NO[(i + 1usize) % PCR_NO.len()], + PCR_NO[(i + 2usize) % PCR_NO.len()], + ]; + let qualifying_data = generate_bytes::<32usize>(&mut rng); + + // Create attestation + let attestation = match context.quote( + &pcr_list, + None, + &key_path, + Some(&qualifying_data[..]), + false, + false, + ) { + Ok(data) => data, + Err(error) => panic!("Quote operation has failed: {:?}", error), + }; + + // Print attestation + debug!("QuoteInfo: {:?}", attestation.0); + debug!("Signature: {:?}", hex::encode(&attestation.1[..])); + + // Verify + assert!(!attestation.0.is_empty()); + assert!(attestation.1.len() >= 20usize); + assert!(attestation.2.is_none()); + assert!(attestation.3.is_none()); + }); +} + +/// Test the `quote()` function to create a quote of a set of several PCRs after they have been updated via the `pcr_extend()` function *and* also request the PCR log +#[test] +#[serial] +#[named] +fn test_pcr_quote_with_log() { + let _configuration = TestConfiguration::with_finalizer(my_tpm_finalizer); + + repeat_test!(|i| { + let key_path = &format!("HS/SRK/myTestAK{}", i); + + // Initialize RNG + let mut rng = ChaChaRng::from_seed(create_seed(i)); + + // Create FAPI context + let mut context = match FapiContext::new() { + Ok(fpai_ctx) => fpai_ctx, + Err(error) => panic!("Failed to create context: {:?}", error), + }; + + // Initialize TPM, if not already initialized + tpm_initialize!(context, PASSWORD, my_auth_callback); + + // Create attestation key, if not already created + match context.create_key(&key_path, Some(KEY_FLAGS_RESTRICTED), None, Some(PASSWORD)) { + Ok(_) => debug!("Key created successfully."), + Err(error) => panic!("Key creation has failed: {:?}", error), + } + + // Extend all PCR's with some data + for pcr_no in PCR_NO { + match context.pcr_extend(pcr_no, &generate_bytes::<128usize>(&mut rng)[..], None) { + Ok(_) => debug!("PCR extended."), + Err(error) => panic!("PCR extension has failed: {:?}", error), + } + } + + // Prepare inputs + let pcr_list = [ + PCR_NO[i % PCR_NO.len()], + PCR_NO[(i + 1usize) % PCR_NO.len()], + PCR_NO[(i + 2usize) % PCR_NO.len()], + ]; + let qualifying_data = generate_bytes::<32usize>(&mut rng); + + // Create attestation + let attestation = match context.quote( + &pcr_list, + None, + &key_path, + Some(&qualifying_data[..]), + true, + true, + ) { + Ok(data) => data, + Err(error) => panic!("Quote operation has failed: {:?}", error), + }; + + // Print attestation + debug!("QuoteInfo: {:?}", attestation.0); + debug!("Signature: {:?}", hex::encode(&attestation.1[..])); + debug!("PCRLogOut: {:?}", attestation.2); + debug!("CertAsPem: {:?}", attestation.3); + + // Verify + assert!(!attestation.0.is_empty()); + assert!(attestation.1.len() >= 20usize); + assert!(attestation.2.map_or(false, |log_data| !log_data.is_empty())); + assert!(attestation.3.map_or(true, |cert| !cert.is_empty())); + }); +} + +/// Test the `verify_quote()` function to verify a quote that was created via the `quote()` function +#[test] +#[serial] +#[named] +fn test_pcr_verify_quote() { + let _configuration = TestConfiguration::with_finalizer(my_tpm_finalizer); + + repeat_test!(|i| { + let key_path = &format!("HS/SRK/myTestAK{}", i); + + // Initialize RNG + let mut rng = ChaChaRng::from_seed(create_seed(i)); + + // Create FAPI context + let mut context = match FapiContext::new() { + Ok(fpai_ctx) => fpai_ctx, + Err(error) => panic!("Failed to create context: {:?}", error), + }; + + // Initialize TPM, if not already initialized + tpm_initialize!(context, PASSWORD, my_auth_callback); + + // Create attestation key, if not already created + match context.create_key(&key_path, Some(KEY_FLAGS_RESTRICTED), None, Some(PASSWORD)) { + Ok(_) => debug!("Key created successfully."), + Err(error) => panic!("Key creation has failed: {:?}", error), + } + + // Extend all PCR's with some data + for pcr_no in PCR_NO { + match context.pcr_extend(pcr_no, &generate_bytes::<128usize>(&mut rng)[..], None) { + Ok(_) => debug!("PCR extended."), + Err(error) => panic!("PCR extension has failed: {:?}", error), + } + } + + // Prepare inputs + let pcr_list = [ + PCR_NO[i % PCR_NO.len()], + PCR_NO[(i + 1usize) % PCR_NO.len()], + PCR_NO[(i + 2usize) % PCR_NO.len()], + ]; + let qualifying_data = generate_bytes::<32usize>(&mut rng); + + // Create attestation + let attestation = match context.quote( + &pcr_list, + None, + &key_path, + Some(&qualifying_data[..]), + false, + false, + ) { + Ok(data) => data, + Err(error) => panic!("Quote operation has failed: {:?}", error), + }; + + // Print attestation + debug!("QuoteInfo: {:?}", attestation.0); + debug!("Signature: {:?}", hex::encode(&attestation.1[..])); + + // Verify attestation + let verify_result = match context.verify_quote( + &key_path, + Some(&qualifying_data[..]), + &attestation.0, + &attestation.1[..], + None, + ) { + Ok(data) => data, + Err(error) => panic!("Verification of quote has failed: {:?}", error), + }; + + // Check result + debug!("Quote valid: {:?}", verify_result); + assert!(verify_result); + + // Modify signature value + let signature_mod = increment(&attestation.1[..]); + + // Verify attestation (again) + let verify_result = match context.verify_quote( + &key_path, + Some(&qualifying_data[..]), + &attestation.0, + &signature_mod[..], + None, + ) { + Ok(data) => data, + Err(error) => panic!("Verification of quote has failed: {:?}", error), + }; + + // Check result + debug!("Quote valid: {:?}", verify_result); + assert!(!verify_result); + + // Modify qualifying data + let qualifying_mod = increment(&qualifying_data[..]); + + // Verify attestation (again) + let verify_result = match context.verify_quote( + &key_path, + Some(&qualifying_mod[..]), + &attestation.0, + &attestation.1[..], + None, + ) { + Ok(data) => data, + Err(error) => panic!("Verification of quote has failed: {:?}", error), + }; + + // Check result + debug!("Quote valid: {:?}", verify_result); + assert!(!verify_result); + }); +} + +// ========================================================================== +// Helper functions +// ========================================================================== + +fn increment(data: &[u8]) -> Vec { + let mut mut_data: Vec = data.into(); + _increment(mut_data.last_mut().expect("Data must not be empty!")); + mut_data +} + +fn _increment(data: &mut u8) { + *data = data.wrapping_add(1u8); +} diff --git a/tests/12_duplicate_test.rs b/tests/12_duplicate_test.rs new file mode 100644 index 0000000..78857f1 --- /dev/null +++ b/tests/12_duplicate_test.rs @@ -0,0 +1,155 @@ +/* SPDX-License-Identifier: BSD-3-Clause */ +/******************************************************************************* + * Copyright 2024, Fraunhofer SIT sponsored by the ELISA research project + * All rights reserved. + ******************************************************************************/ + +pub mod common; + +use common::{param::PASSWORD, setup::TestConfiguration}; +use function_name::named; +use json::{Error as JsonError, JsonValue}; +use log::debug; +use serial_test::serial; +use tss2_fapi_rs::{FapiContext, KeyFlags}; + +mk_auth_callback!(my_auth_callback, PASSWORD); +mk_tpm_finalizer!(my_tpm_finalizer, my_auth_callback); + +const PARENT_KEY_FLAGS: &[KeyFlags] = &[KeyFlags::Restricted, KeyFlags::Decrypt, KeyFlags::NoDA]; +const EXPORT_KEY_FLAGS: &[KeyFlags] = &[KeyFlags::Exportable, KeyFlags::Decrypt, KeyFlags::NoDA]; + +// ========================================================================== +// Test cases +// ========================================================================== + +/// Test the `export_key()` and `import()` functions to duplicate an extsing key, by using a `POLICYDUPLICATIONSELECT` policy +#[test] +#[serial] +#[named] +fn test_duplicate_key() { + let _configuration = TestConfiguration::with_finalizer(my_tpm_finalizer); + + repeat_test!(|i| { + let offset = 2usize.checked_mul(i).unwrap(); + + let crypt_key1 = &format!("HS/SRK/myCryptKey{}", offset); + let crypt_key2 = &format!("HS/SRK/myCryptKey{}", offset.checked_add(1usize).unwrap()); + let new_parent = &format!("ext/myNewParentKey{}", offset); + let child_key1 = &format!("{}/myChildKey{}", crypt_key1, offset); + let child_key2 = &format!("myImportedKey{}", offset); + let dup_policy = &format!("/policy/pol_duplicate{}", offset); + + // Create FAPI context + let mut context = match FapiContext::new() { + Ok(fpai_ctx) => fpai_ctx, + Err(error) => panic!("Failed to create context: {:?}", error), + }; + + // Initialize TPM, if not already initialized + tpm_initialize!(context, PASSWORD, my_auth_callback); + + // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + // Step #1: Create the parent keys + // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + // Create parent key #1, if not already created + match context.create_key(&crypt_key1, Some(PARENT_KEY_FLAGS), None, Some(PASSWORD)) { + Ok(_) => debug!("Key created successfully."), + Err(error) => panic!("Key creation has failed: {:?}", error), + } + + // Create parent key #2, if not already created + match context.create_key(&crypt_key2, Some(PARENT_KEY_FLAGS), None, Some(PASSWORD)) { + Ok(_) => debug!("Key created successfully."), + Err(error) => panic!("Key creation has failed: {:?}", error), + } + + // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + // Step #2: Get public key of new parent + // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + // Export public key + let json_public_key = match context.export_key(&crypt_key2, None) { + Ok(exported) => exported, + Err(error) => panic!("Key export (public) has failed: {:?}", error), + }; + + // Verify + debug!("Exported public key: {:?}", json_public_key); + assert!(!json_public_key.is_empty()); + + // Import public key + match context.import(&new_parent, &json_public_key) { + Ok(_) => debug!("Key imported."), + Err(error) => panic!("Failed to import the key: {:?}", error), + }; + + // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + // Step #3: Create the exportable key + // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + // Build the duplication policy + let policy_json = create_duplication_policy(new_parent).expect("Failed to create policy!"); + + // Import policy + match context.import(dup_policy, &policy_json) { + Ok(_) => debug!("Policy imported."), + Err(error) => panic!("Failed to import policy: {:?}", error), + }; + + // Create the exportable key, if not already created + match context.create_key(&child_key1, Some(EXPORT_KEY_FLAGS), Some(dup_policy), None) { + Ok(_) => debug!("Key created successfully."), + Err(error) => panic!("Key creation has failed: {:?}", error), + } + + // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + // Step #4: Export the private key (wrapped) + // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + // Export private key + let json_wrapped_key = match context.export_key(&child_key1, Some(&new_parent)) { + Ok(exported) => exported, + Err(error) => panic!("Key export (private) has failed: {:?}", error), + }; + + // Verify + debug!("Exported wrapped key: {:?}", json_wrapped_key); + assert!(!json_wrapped_key.is_empty()); + + // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + // Step #5: Import the wrapped key + // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + // Import wrapped key + match context.import(&child_key2, &json_wrapped_key) { + Ok(_) => debug!("Key imported."), + Err(error) => panic!("Failed to import the key: {:?}", error), + }; + + // Enumerate keys + match context.list(&crypt_key2) { + Ok(mut list) => { + let duplicated_key = format!("{}/{}", &crypt_key2, child_key2).to_ascii_lowercase(); + assert!(list + .drain(..) + .any(|entry| entry.to_ascii_lowercase().ends_with(&duplicated_key[..]))); + } + Err(error) => panic!("Failed to enumerate the keys: {:?}", error), + }; + }); +} + +// ========================================================================== +// Helper functions +// ========================================================================== + +const POLICY_TEMPLATE: &str = "{\"description\":\"Description pol_duplicate\",\"policy\":[{\"type\":\"POLICYDUPLICATIONSELECT\",\"newParentPath\":\"\"}]}"; + +/// Create new POLICYDUPLICATIONSELECT policy +fn create_duplication_policy(new_parent_path: &str) -> Result { + let mut policy_json = json::parse(POLICY_TEMPLATE)?; + policy_json["policy"][0usize]["newParentPath"] = JsonValue::String(new_parent_path.to_owned()); + Ok(policy_json) +} diff --git a/tests/13_authorize_policy_test.rs b/tests/13_authorize_policy_test.rs new file mode 100644 index 0000000..888a4b2 --- /dev/null +++ b/tests/13_authorize_policy_test.rs @@ -0,0 +1,390 @@ +/* SPDX-License-Identifier: BSD-3-Clause */ +/******************************************************************************* + * Copyright 2024, Fraunhofer SIT sponsored by the ELISA research project + * All rights reserved. + ******************************************************************************/ + +pub mod common; + +use common::{ + param::PASSWORD, + random::{create_seed, generate_bytes}, + setup::TestConfiguration, +}; +use function_name::named; +use json::{number::Number, Error as JsonError, JsonValue}; +use log::{debug, trace}; +use rand::SeedableRng; +use rand_chacha::ChaChaRng; +use serial_test::serial; +use sha2::{Digest, Sha256}; +use std::sync::{ + atomic::{AtomicI64, Ordering}, + Arc, +}; +use tss2_fapi_rs::{ActnCallback, ActnCallbackParam, FapiContext, KeyFlags}; + +mk_auth_callback!(my_auth_callback, PASSWORD); +mk_tpm_finalizer!(my_tpm_finalizer, my_auth_callback); + +const KEY_FLAGS_SIGN: &[KeyFlags] = &[KeyFlags::NoDA, KeyFlags::Sign]; + +// ========================================================================== +// Test cases +// ========================================================================== + +/// Test `authorize_policy()` with a `POLICYAUTHORIZE` policy that contains a public key (PEM) +#[test] +#[serial] +#[named] +fn test_policy_authorize_pubkey() { + let _configuration = TestConfiguration::with_finalizer(my_tpm_finalizer); + + repeat_test!(|i| { + let key_path = [ + &format!("/HS/SRK/myPolicySignKey{}", i), + &format!("HS/SRK/myTestKey{}", i), + ]; + let pol_name = [ + &format!("/policy/pol_authorize{}", i), + &format!("/policy/pol_name_hash{}", i), + ]; + + // Initialize RNG + let mut rng = ChaChaRng::from_seed(create_seed(i)); + + // Create FAPI context + let mut context = match FapiContext::new() { + Ok(fpai_ctx) => fpai_ctx, + Err(error) => panic!("Failed to create context: {:?}", error), + }; + + // Initialize TPM, if not already initialized + tpm_initialize!(context, PASSWORD, my_auth_callback); + + // Set up "action" callback + let action_triggered = Arc::new(AtomicI64::new(0i64)); + match context.set_policy_action_callback(ActnCallback::with_data( + my_actn_callback, + action_triggered.clone(), + )) { + Ok(_) => debug!("Action callback installed."), + Err(error) => panic!("Failed to set up sign callback: {:?}", error), + } + + // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + // Step #1: Create the policy siging key [backend] + // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + // Create policy signing key, if not already created + match context.create_key(&key_path[0], Some(KEY_FLAGS_SIGN), None, Some(PASSWORD)) { + Ok(_) => debug!("Key created."), + Err(error) => panic!("Key creation has failed: {:?}", error), + } + + // Export the public key from TPM + let policy_siging_public_key = match context.export_key(&key_path[0], None) { + Ok(data) => data["pem_ext_public"] + .as_str() + .expect("Failed to find the public key!") + .to_owned(), + Err(error) => panic!("Key export has failed: {:?}", error), + }; + + // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + // Step #2: Create the user key [frontend] + // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + // Create the POLICYAUTHORIZE policy with public key + let policy_authorize_json = + create_authorize_policy_with_public_key(&policy_siging_public_key[..], None) + .expect("Failed to create policy!"); + + // Import policy + match context.import(&pol_name[0], &policy_authorize_json) { + Ok(_) => debug!("Policy imported."), + Err(error) => panic!("Failed to import policy: {:?}", error), + }; + + // Create the key, if not already created + match context.create_key(&key_path[1], Some(KEY_FLAGS_SIGN), Some(&pol_name[0]), None) { + Ok(_) => debug!("Key created with policy."), + Err(error) => panic!("Key creation has failed: {:?}", error), + } + + // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + // Step #3: Authorize the new policy [backend] + // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + // Create the POLICYNAMEHASH policy + let policy_name_hash_json = + create_name_hash_policy(key_path[1]).expect("Failed to create policy!"); + + // Import new policy + match context.import(&pol_name[1], &policy_name_hash_json) { + Ok(_) => debug!("Policy imported."), + Err(error) => panic!("Failed to import policy: {:?}", error), + }; + + // Authorize new policy + match context.authorize_policy(&pol_name[1], key_path[0], None) { + Ok(_) => debug!("Policy authorized."), + Err(error) => panic!("Failed to authorize policy: {:?}", error), + } + + // Delete policy signing key from TPM (no longer required!) + match context.delete(&key_path[0]) { + Ok(_) => debug!("Policy siging key deleted."), + Err(error) => panic!("Failed to delete authorized policy: {:?}", error), + }; + + // Export the authorized policy + let authorized_policy_json = match context.export_policy(&pol_name[1]) { + Ok(policy) => policy, + Err(error) => panic!("Failed to export authorized policy: {:?}", error), + }; + + // Print authroized policy + trace!("{:?}", authorized_policy_json); + + // Delete policy from TPM (for testing purposes!) + match context.delete(&pol_name[1]) { + Ok(_) => debug!("Authorized policy deleted."), + Err(error) => panic!("Failed to delete authorized policy: {:?}", error), + }; + + // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + // Step #4: Import new policy and use key [frontend] + // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + // Compute digest to be signed + let digest_tbs = Sha256::digest(&generate_bytes::<256usize>(&mut rng)[..]); + debug!("Digest to be signed: {}", hex::encode(&digest_tbs[..])); + + // Create the signature (expected to fail!) + match context.sign(&key_path[1], None, &digest_tbs, false, false) { + Ok(_) => panic!("Signature was created succesfully *without* an authorized policy!"), + Err(error) => debug!("Failed to create the signature (expected): {:?}", error), + }; + + // Import authorized policy + match context.import(&pol_name[1], &authorized_policy_json) { + Ok(_) => debug!("Authorized policy imported."), + Err(error) => panic!("Failed to import authorized policy: {:?}", error), + }; + + // Create the signature (this time it is supposed to work!) + let signature = match context.sign(&key_path[1], None, &digest_tbs, false, false) { + Ok(value) => value, + Err(error) => panic!("Failed to create the signature: {:?}", error), + }; + + // Validate signature data + let signature_data: &[u8] = signature.0.as_ref(); + assert!(signature_data.len() >= 32usize); + debug!("Signature value: {}", hex::encode(signature_data)); + }); +} + +/// Test `authorize_policy()` with a `POLICYAUTHORIZE` policy that contains a key path +#[test] +#[serial] +#[named] +fn test_policy_authorize_path() { + let _configuration = TestConfiguration::with_finalizer(my_tpm_finalizer); + + repeat_test!(|i| { + let key_path = [ + &format!("/HS/SRK/myPolicySignKey{}", i), + &format!("HS/SRK/myTestKey{}", i), + ]; + let pol_name = [ + &format!("/policy/pol_authorize{}", i), + &format!("/policy/pol_name_hash{}", i), + ]; + + // Initialize RNG + let mut rng = ChaChaRng::from_seed(create_seed(i)); + + // Create FAPI context + let mut context = match FapiContext::new() { + Ok(fpai_ctx) => fpai_ctx, + Err(error) => panic!("Failed to create context: {:?}", error), + }; + + // Initialize TPM, if not already initialized + tpm_initialize!(context, PASSWORD, my_auth_callback); + + // Set up "action" callback + let action_triggered = Arc::new(AtomicI64::new(0i64)); + match context.set_policy_action_callback(ActnCallback::with_data( + my_actn_callback, + action_triggered.clone(), + )) { + Ok(_) => debug!("Action callback installed."), + Err(error) => panic!("Failed to set up sign callback: {:?}", error), + } + + // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + // Step #1: Create the policy siging key [backend] + // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + // Create policy signing key, if not already created + match context.create_key(&key_path[0], Some(KEY_FLAGS_SIGN), None, Some(PASSWORD)) { + Ok(_) => debug!("Key created."), + Err(error) => panic!("Key creation has failed: {:?}", error), + } + + // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + // Step #2: Create the user key [frontend] + // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + // Create the POLICYAUTHORIZE policy with key path + let policy_authorize_json = create_authorize_policy_with_key_path(&key_path[0], None) + .expect("Failed to create policy!"); + + // Import policy + match context.import(&pol_name[0], &policy_authorize_json) { + Ok(_) => debug!("Policy imported."), + Err(error) => panic!("Failed to import policy: {:?}", error), + }; + + // Create the key, if not already created + match context.create_key(&key_path[1], Some(KEY_FLAGS_SIGN), Some(&pol_name[0]), None) { + Ok(_) => debug!("Key created with policy."), + Err(error) => panic!("Key creation has failed: {:?}", error), + } + + // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + // Step #3: Authorize the new policy [backend] + // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + // Create the POLICYNAMEHASH policy + let policy_name_hash_json = + create_name_hash_policy(key_path[1]).expect("Failed to create policy!"); + + // Import new policy + match context.import(&pol_name[1], &policy_name_hash_json) { + Ok(_) => debug!("Policy imported."), + Err(error) => panic!("Failed to import policy: {:?}", error), + }; + + // Authorize new policy + match context.authorize_policy(&pol_name[1], key_path[0], None) { + Ok(_) => debug!("Policy authorized."), + Err(error) => panic!("Failed to authorize policy: {:?}", error), + } + + // Delete policy signing key from TPM (no longer required!) + match context.delete(&key_path[0]) { + Ok(_) => debug!("Policy siging key deleted."), + Err(error) => panic!("Failed to delete authorized policy: {:?}", error), + }; + + // Export the authorized policy + let authorized_policy_json = match context.export_policy(&pol_name[1]) { + Ok(policy) => policy, + Err(error) => panic!("Failed to export authorized policy: {:?}", error), + }; + + // Print authroized policy + trace!("{:?}", authorized_policy_json); + + // Delete policy from TPM (for testing purposes!) + match context.delete(&pol_name[1]) { + Ok(_) => debug!("Authorized policy deleted."), + Err(error) => panic!("Failed to delete authorized policy: {:?}", error), + }; + + // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + // Step #4: Import new policy and use key [frontend] + // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + // Compute digest to be signed + let digest_tbs = Sha256::digest(&generate_bytes::<256usize>(&mut rng)[..]); + debug!("Digest to be signed: {}", hex::encode(&digest_tbs[..])); + + // Create the signature (expected to fail!) + match context.sign(&key_path[1], None, &digest_tbs, false, false) { + Ok(_) => panic!("Signature was created succesfully *without* an authorized policy!"), + Err(error) => debug!("Failed to create the signature (expected): {:?}", error), + }; + + // Import authorized policy + match context.import(&pol_name[1], &authorized_policy_json) { + Ok(_) => debug!("Authorized policy imported."), + Err(error) => panic!("Failed to import authorized policy: {:?}", error), + }; + + // Create the signature (this time it is supposed to work!) + let signature = match context.sign(&key_path[1], None, &digest_tbs, false, false) { + Ok(value) => value, + Err(error) => panic!("Failed to create the signature: {:?}", error), + }; + + // Validate signature data + let signature_data: &[u8] = signature.0.as_ref(); + assert!(signature_data.len() >= 32usize); + debug!("Signature value: {}", hex::encode(signature_data)); + }); +} + +// ========================================================================== +// Helper functions +// ========================================================================== + +const POLICY_AUTHORIZE_TEMPLATE: &str = + "{\"description\":\"Description pol_authorize\",\"policy\":[{\"type\":\"POLICYAUTHORIZE\"}]}"; +const POLICY_NAME_HASH_TEMPLATE: &str = "{\"description\":\"Description pol_name_hash\",\"policy\":[{\"type\":\"POLICYNAMEHASH\",\"namePaths\":[]},{\"type\":\"POLICYACTION\",\"action\":\"myaction\"}]}"; + +/// Create new POLICYAUTHORIZE policy with "keyPEM" +fn create_authorize_policy_with_public_key( + public_key_pem: &str, + policy_ref: Option<&[u8]>, +) -> Result { + _create_authorize_policy("keyPEM", public_key_pem, policy_ref) +} + +/// Create new POLICYAUTHORIZE policy with "keyPath" +fn create_authorize_policy_with_key_path( + key_path: &str, + policy_ref: Option<&[u8]>, +) -> Result { + _create_authorize_policy("keyPath", key_path, policy_ref) +} + +/// Create new POLICYAUTHORIZE policy +fn _create_authorize_policy( + policy_entry: &str, + policy_value: &str, + policy_ref: Option<&[u8]>, +) -> Result { + let mut policy_json = json::parse(POLICY_AUTHORIZE_TEMPLATE)?; + policy_json["policy"][0usize] + .insert(policy_entry, JsonValue::String(policy_value.to_owned()))?; + if let Some(data) = policy_ref { + let values = data + .into_iter() + .map(|val| JsonValue::Number(Number::from(*val))) + .collect::>(); + policy_json["policy"][0usize].insert("policyRef", JsonValue::Array(values))?; + } + Ok(policy_json) +} + +/// Create new POLICYNAMEHASH policy +fn create_name_hash_policy(key_path: &str) -> Result { + let mut policy_json = json::parse(POLICY_NAME_HASH_TEMPLATE)?; + policy_json["policy"][0usize]["namePaths"].push(JsonValue::String(key_path.to_owned()))?; + Ok(policy_json) +} + +/// The "action" callback implementation to be used for testing +fn my_actn_callback(param: ActnCallbackParam, triggered: &Arc) -> bool { + debug!( + "(ACTN_CB) Action for {:?} has been requested!", + param.object_path + ); + trace!("(ACTN_CB) Parameters: {:?}", param); + triggered.fetch_add(1i64, Ordering::SeqCst) >= 0i64 +} diff --git a/tests/14_certificate_test.rs b/tests/14_certificate_test.rs new file mode 100644 index 0000000..af943b7 --- /dev/null +++ b/tests/14_certificate_test.rs @@ -0,0 +1,292 @@ +/* SPDX-License-Identifier: BSD-3-Clause */ +/******************************************************************************* + * Copyright 2024, Fraunhofer SIT sponsored by the ELISA research project + * All rights reserved. + ******************************************************************************/ + +pub mod common; + +use common::{ + crypto::{get_key_type, KeyType}, + param::PASSWORD, + setup::TestConfiguration, + tempfile::TempFile, +}; +use function_name::named; +use json::JsonValue; +use log::debug; +use serial_test::serial; +use std::{ + fs, + process::{Command, Stdio}, +}; +use tss2_fapi_rs::{BaseErrorCode, ErrorCode, FapiContext, KeyFlags}; + +mk_auth_callback!(my_auth_callback, PASSWORD); +mk_tpm_finalizer!(my_tpm_finalizer, my_auth_callback); + +const KEY_FLAGS: &[KeyFlags] = &[KeyFlags::NoDA, KeyFlags::Decrypt]; + +// ========================================================================== +// Test cases +// ========================================================================== + +/// Test the `export_key()` and `set_certificate()` functions to generate and import a certificate for an existing key +#[test] +#[serial] +#[named] +fn test_set_certificate() { + let configuration = TestConfiguration::with_finalizer(my_tpm_finalizer); + + repeat_test!(|i| { + let key_path = &format!("HS/SRK/myTestKey{}", i); + + // Create FAPI context + let mut context = match FapiContext::new() { + Ok(fpai_ctx) => fpai_ctx, + Err(error) => panic!("Failed to create context: {:?}", error), + }; + + // Initialize TPM, if not already initialized + tpm_initialize!(context, PASSWORD, my_auth_callback); + + // Create the key, if not already created + match context.create_key(&key_path, Some(KEY_FLAGS), None, Some(PASSWORD)) { + Ok(_) => debug!("Key created successfully."), + Err(error) => panic!("Key creation has failed: {:?}", error), + } + + // Export public key + let json_public_key = match context.export_key(&key_path, None) { + Ok(exported) => exported, + Err(error) => panic!("Key export (public) has failed: {:?}", error), + }; + + // Create the certificate + let pem_data = + generate_certificate(&configuration, json_public_key, &format!("User {}", i)) + .expect("Failed to generate certificate!"); + debug!("Certificate: {:?}", pem_data); + + // Set the certificate + match context.set_certificate(&key_path, Some(&pem_data[..])) { + Ok(_) => debug!("Certificate set successfully."), + Err(error) => panic!("Setting certificate has failed: {:?}", error), + } + }); +} + +/// Test the `get_certificate()` function to read back the certificate that was previosuly set via the `set_certificate()` function +#[test] +#[serial] +#[named] +fn test_get_certificate() { + let configuration = TestConfiguration::with_finalizer(my_tpm_finalizer); + + repeat_test!(|i| { + let key_path = &format!("HS/SRK/myTestKey{}", i); + + // Create FAPI context + let mut context = match FapiContext::new() { + Ok(fpai_ctx) => fpai_ctx, + Err(error) => panic!("Failed to create context: {:?}", error), + }; + + // Initialize TPM, if not already initialized + tpm_initialize!(context, PASSWORD, my_auth_callback); + + // Create the key, if not already created + match context.create_key(&key_path, Some(KEY_FLAGS), None, Some(PASSWORD)) { + Ok(_) => debug!("Key created successfully."), + Err(error) => panic!("Key creation has failed: {:?}", error), + } + + // Export public key + let json_public_key = match context.export_key(&key_path, None) { + Ok(exported) => exported, + Err(error) => panic!("Key export (public) has failed: {:?}", error), + }; + + // Create the certificate + let pem_data = + generate_certificate(&configuration, json_public_key, &format!("User {}", i)) + .expect("Failed to generate certificate!"); + debug!("Certificate: {:?}", pem_data); + + // Set the certificate + match context.set_certificate(&key_path, Some(&pem_data[..])) { + Ok(_) => debug!("Certificate set successfully."), + Err(error) => panic!("Setting certificate has failed: {:?}", error), + } + + // Get the certificate + let recovered_cert = match context.get_certificate(&key_path) { + Ok(cert_data) => cert_data, + Err(error) => panic!("Getting certificate has failed: {:?}", error), + }; + + // Verify + debug!("Certificate: {:?}", recovered_cert); + assert!(recovered_cert + .expect("No certificate data avialble!") + .trim() + .eq_ignore_ascii_case(pem_data.trim())) + }); +} + +/// Test the `set_certificate()` function to remove the certificate that was previosuly set via the `set_certificate()` function +#[test] +#[serial] +#[named] +fn test_remove_certificate() { + let configuration = TestConfiguration::with_finalizer(my_tpm_finalizer); + + repeat_test!(|i| { + let key_path = &format!("HS/SRK/myTestKey{}", i); + + // Create FAPI context + let mut context = match FapiContext::new() { + Ok(fpai_ctx) => fpai_ctx, + Err(error) => panic!("Failed to create context: {:?}", error), + }; + + // Initialize TPM, if not already initialized + tpm_initialize!(context, PASSWORD, my_auth_callback); + + // Create the key, if not already created + match context.create_key(&key_path, Some(KEY_FLAGS), None, Some(PASSWORD)) { + Ok(_) => debug!("Key created successfully."), + Err(error) => panic!("Key creation has failed: {:?}", error), + } + + // Export public key + let json_public_key = match context.export_key(&key_path, None) { + Ok(exported) => exported, + Err(error) => panic!("Key export (public) has failed: {:?}", error), + }; + + // Create the certificate + let pem_data = + generate_certificate(&configuration, json_public_key, &format!("User {}", i)) + .expect("Failed to generate certificate!"); + debug!("Certificate: {:?}", pem_data); + + // Set the certificate + match context.set_certificate(&key_path, Some(&pem_data[..])) { + Ok(_) => debug!("Certificate set successfully."), + Err(error) => panic!("Setting certificate has failed: {:?}", error), + } + + // Get the certificate + let recovered_cert = match context.get_certificate(&key_path) { + Ok(cert_data) => cert_data, + Err(error) => panic!("Getting certificate has failed: {:?}", error), + }; + + // Verify + assert!(recovered_cert.is_some()); + + // Remove the certificate + match context.set_certificate(&key_path, None) { + Ok(_) => debug!("Certificate removed."), + Err(error) => panic!("Removing certificate has failed: {:?}", error), + } + + // Get the certificate (again) + let recovered_cert = match context.get_certificate(&key_path) { + Ok(cert_data) => cert_data, + Err(ErrorCode::FapiError(BaseErrorCode::NoCert)) => None, + Err(error) => panic!("Getting certificate has failed: {:?}", error), + }; + + // Verify + assert!(recovered_cert.is_none()); + }); +} + +/// Test the `get_platform_certificates()` function to retrieve the platform certificates +#[test] +#[serial] +#[named] +fn test_get_platform_certificates() { + let _configuration = TestConfiguration::with_finalizer(my_tpm_finalizer); + + repeat_test!(|_i| { + // Create FAPI context + let mut context = match FapiContext::new() { + Ok(fpai_ctx) => fpai_ctx, + Err(error) => panic!("Failed to create context: {:?}", error), + }; + + // Initialize TPM, if not already initialized + tpm_initialize!(context, PASSWORD, my_auth_callback); + + // Get the certificate + let platform_certs = match context.get_platform_certificates() { + Ok(cert_data) => cert_data, + Err(error) => panic!("Getting certificate has failed: {:?}", error), + }; + + // Verify + match platform_certs { + Some(cert_data) => debug!("Certificate: {}", hex::encode(&cert_data[..])), + None => debug!("No platform certificates available."), + } + }); +} + +// ========================================================================== +// Helper functions +// ========================================================================== + +fn generate_certificate( + config: &TestConfiguration, + public_key: JsonValue, + common_name: &str, +) -> Option { + let common_name = common_name.trim(); + assert!( + !common_name.is_empty() + && common_name + .chars() + .all(|c| char::is_ascii_alphanumeric(&c) || c == '\x20') + ); + + let key_suffix = get_key_suffix(config).expect("Failed to determine key type!"); + let public_key = public_key["pem_ext_public"].as_str()?; + let tmp_pubkey = TempFile::with_suffix(config.work_path(), "pem")?; + + fs::write(&tmp_pubkey.path(), public_key).ok()?; + + let cert_data = Command::new("openssl") + .arg("x509") + .arg("-new") + .arg("-CA") + .arg( + config + .data_path() + .join("keys") + .join(format!("ca_key_{}.pem", key_suffix)), + ) + .arg("-force_pubkey") + .arg(&tmp_pubkey.path()) + .arg("-subj") + .arg(format!("/CN={}", common_name)) + .stderr(Stdio::null()) + .stdout(Stdio::piped()) + .output() + .ok()?; + + match cert_data.status.success() { + true => String::from_utf8(cert_data.stdout).ok(), + _ => None, + } +} + +fn get_key_suffix(config: &TestConfiguration) -> Option<&'static str> { + match get_key_type(config.prof_name()) { + Some(KeyType::RsaKey) => Some("rsa"), + Some(KeyType::EccKey) => Some("ecc"), + _ => None, + } +} diff --git a/tests/15_description_test.rs b/tests/15_description_test.rs new file mode 100644 index 0000000..aca4154 --- /dev/null +++ b/tests/15_description_test.rs @@ -0,0 +1,182 @@ +/* SPDX-License-Identifier: BSD-3-Clause */ +/******************************************************************************* + * Copyright 2024, Fraunhofer SIT sponsored by the ELISA research project + * All rights reserved. + ******************************************************************************/ + +pub mod common; + +use common::{ + param::PASSWORD, + random::{create_seed, generate_string}, + setup::TestConfiguration, +}; +use function_name::named; +use log::debug; +use rand::SeedableRng; +use rand_chacha::ChaChaRng; +use serial_test::serial; +use tss2_fapi_rs::{FapiContext, KeyFlags}; + +mk_auth_callback!(my_auth_callback, PASSWORD); +mk_tpm_finalizer!(my_tpm_finalizer, my_auth_callback); + +const KEY_FLAGS: &[KeyFlags] = &[KeyFlags::NoDA, KeyFlags::Decrypt]; + +// ========================================================================== +// Test cases +// ========================================================================== + +/// Test the `set_description()` function to set up a description for an existing key +#[test] +#[serial] +#[named] +fn test_set_description() { + let _configuration = TestConfiguration::with_finalizer(my_tpm_finalizer); + + repeat_test!(|i| { + let key_path = &format!("HS/SRK/myTestKey{}", i); + + // Initialize RNG + let mut rng = ChaChaRng::from_seed(create_seed(i)); + + // Create FAPI context + let mut context = match FapiContext::new() { + Ok(fpai_ctx) => fpai_ctx, + Err(error) => panic!("Failed to create context: {:?}", error), + }; + + // Initialize TPM, if not already initialized + tpm_initialize!(context, PASSWORD, my_auth_callback); + + // Create the key, if not already created + match context.create_key(&key_path, Some(KEY_FLAGS), None, Some(PASSWORD)) { + Ok(_) => debug!("Key created successfully."), + Err(error) => panic!("Key creation has failed: {:?}", error), + } + + // Generate description + let desciption = generate_string::<64usize>(&mut rng); + + // Set description + match context.set_description(&key_path, Some(&desciption)) { + Ok(_) => debug!("Description set successfully."), + Err(error) => panic!("Setting description data has failed: {:?}", error), + } + }); +} + +/// Test the `get_description()` function to read back the description that was previosuly set via the `set_description()` function +#[test] +#[serial] +#[named] +fn test_get_description() { + let _configuration = TestConfiguration::with_finalizer(my_tpm_finalizer); + + repeat_test!(|i| { + let key_path = &format!("HS/SRK/myTestKey{}", i); + + // Initialize RNG + let mut rng = ChaChaRng::from_seed(create_seed(i)); + + // Create FAPI context + let mut context = match FapiContext::new() { + Ok(fpai_ctx) => fpai_ctx, + Err(error) => panic!("Failed to create context: {:?}", error), + }; + + // Initialize TPM, if not already initialized + tpm_initialize!(context, PASSWORD, my_auth_callback); + + // Create the key, if not already created + match context.create_key(&key_path, Some(KEY_FLAGS), None, Some(PASSWORD)) { + Ok(_) => debug!("Key created successfully."), + Err(error) => panic!("Key creation has failed: {:?}", error), + } + + // Generate description + let desciption = generate_string::<64usize>(&mut rng); + + // Set description + match context.set_description(&key_path, Some(&desciption)) { + Ok(_) => debug!("Description set successfully."), + Err(error) => panic!("Setting description data has failed: {:?}", error), + } + + // Get the description data + let recovered_descr = match context.get_description(&key_path) { + Ok(cert_data) => cert_data, + Err(error) => panic!("Getting description has failed: {:?}", error), + }; + + // Verify + debug!("Description: {:?}", &recovered_descr); + assert!( + (&recovered_descr.expect("No description avialble!").trim()[..]) + .eq(&desciption.trim()[..]) + ) + }); +} + +/// Test the `set_description()` function to remove the description that was previosuly set via the `set_description()` function +#[test] +#[serial] +#[named] +fn test_remove_description() { + let _configuration = TestConfiguration::with_finalizer(my_tpm_finalizer); + + repeat_test!(|i| { + let key_path = &format!("HS/SRK/myTestKey{}", i); + + // Initialize RNG + let mut rng = ChaChaRng::from_seed(create_seed(i)); + + // Create FAPI context + let mut context = match FapiContext::new() { + Ok(fpai_ctx) => fpai_ctx, + Err(error) => panic!("Failed to create context: {:?}", error), + }; + + // Initialize TPM, if not already initialized + tpm_initialize!(context, PASSWORD, my_auth_callback); + + // Create the key, if not already created + match context.create_key(&key_path, Some(KEY_FLAGS), None, Some(PASSWORD)) { + Ok(_) => debug!("Key created successfully."), + Err(error) => panic!("Key creation has failed: {:?}", error), + } + + // Generate description + let desciption = generate_string::<64usize>(&mut rng); + + // Set description + match context.set_description(&key_path, Some(&desciption)) { + Ok(_) => debug!("Description set successfully."), + Err(error) => panic!("Setting description data has failed: {:?}", error), + } + + // Get the description data + let recovered_descr = match context.get_description(&key_path) { + Ok(cert_data) => cert_data, + Err(error) => panic!("Getting description has failed: {:?}", error), + }; + + // Verify + assert!(recovered_descr.is_some()); + + // Erase the description + match context.set_description(&key_path, None) { + Ok(_) => debug!("Description erased."), + Err(error) => panic!("Setting application-specific data has failed: {:?}", error), + } + + // Get the description data + let recovered_descr = match context.get_description(&key_path) { + Ok(cert_data) => cert_data, + Err(error) => panic!("Getting description has failed: {:?}", error), + }; + + // Verify + assert!(recovered_descr.is_none()); + }); +} diff --git a/tests/16_app_data_test.rs b/tests/16_app_data_test.rs new file mode 100644 index 0000000..6bc2eb8 --- /dev/null +++ b/tests/16_app_data_test.rs @@ -0,0 +1,188 @@ +/* SPDX-License-Identifier: BSD-3-Clause */ +/******************************************************************************* + * Copyright 2024, Fraunhofer SIT sponsored by the ELISA research project + * All rights reserved. + ******************************************************************************/ + +pub mod common; + +use common::{ + param::PASSWORD, + random::{create_seed, generate_bytes}, + setup::TestConfiguration, +}; +use function_name::named; +use log::debug; +use rand::SeedableRng; +use rand_chacha::ChaChaRng; +use serial_test::serial; +use tss2_fapi_rs::{FapiContext, KeyFlags}; + +mk_auth_callback!(my_auth_callback, PASSWORD); +mk_tpm_finalizer!(my_tpm_finalizer, my_auth_callback); + +const KEY_FLAGS: &[KeyFlags] = &[KeyFlags::NoDA, KeyFlags::Decrypt]; + +// ========================================================================== +// Test cases +// ========================================================================== + +/// Test the `set_app_data()` function to associate some application-specific data with an existing key +#[test] +#[serial] +#[named] +fn test_set_appdata() { + let _configuration = TestConfiguration::with_finalizer(my_tpm_finalizer); + + repeat_test!(|i| { + let key_path = &format!("HS/SRK/myTestKey{}", i); + + // Initialize RNG + let mut rng = ChaChaRng::from_seed(create_seed(i)); + + // Create FAPI context + let mut context = match FapiContext::new() { + Ok(fpai_ctx) => fpai_ctx, + Err(error) => panic!("Failed to create context: {:?}", error), + }; + + // Initialize TPM, if not already initialized + tpm_initialize!(context, PASSWORD, my_auth_callback); + + // Create the key, if not already created + match context.create_key(&key_path, Some(KEY_FLAGS), None, Some(PASSWORD)) { + Ok(_) => debug!("Key created successfully."), + Err(error) => panic!("Key creation has failed: {:?}", error), + } + + // Generate data + let app_data = generate_bytes::<128usize>(&mut rng); + + // Set application data + match context.set_app_data(&key_path, Some(&app_data[..])) { + Ok(_) => debug!("Data set successfully."), + Err(error) => panic!("Setting application-specific data has failed: {:?}", error), + } + }); +} + +/// Test the `get_app_data()` function to read back the application-specific data that was previously set via the `set_app_data()` function +#[test] +#[serial] +#[named] +fn test_get_appdata() { + let _configuration = TestConfiguration::with_finalizer(my_tpm_finalizer); + + repeat_test!(|i| { + let key_path = &format!("HS/SRK/myTestKey{}", i); + + // Initialize RNG + let mut rng = ChaChaRng::from_seed(create_seed(i)); + + // Create FAPI context + let mut context = match FapiContext::new() { + Ok(fpai_ctx) => fpai_ctx, + Err(error) => panic!("Failed to create context: {:?}", error), + }; + + // Initialize TPM, if not already initialized + tpm_initialize!(context, PASSWORD, my_auth_callback); + + // Create the key, if not already created + match context.create_key(&key_path, Some(KEY_FLAGS), None, Some(PASSWORD)) { + Ok(_) => debug!("Key created successfully."), + Err(error) => panic!("Key creation has failed: {:?}", error), + } + + // Generate data + let app_data = generate_bytes::<128usize>(&mut rng); + + // Set application data + match context.set_app_data(&key_path, Some(&app_data[..])) { + Ok(_) => debug!("Data set successfully."), + Err(error) => panic!("Setting application-specific data has failed: {:?}", error), + } + + // Get the application data + let recovered_data = match context.get_app_data(&key_path) { + Ok(cert_data) => cert_data, + Err(error) => panic!("Getting application-specific data has failed: {:?}", error), + }; + + // Verify + debug!( + "Application data: {}", + recovered_data + .as_ref() + .map(|data| hex::encode(&data[..])) + .unwrap_or_else(|| "(None)".to_owned()) + ); + assert!( + (&recovered_data.expect("No application-specific data avialble!")[..]) + .eq_ignore_ascii_case(&app_data[..]) + ) + }); +} + +/// Test the `set_app_data()` function to erase the application-specific data that was previously set via the `set_app_data()` function +#[test] +#[serial] +#[named] +fn test_remove_appdata() { + let _configuration = TestConfiguration::with_finalizer(my_tpm_finalizer); + + repeat_test!(|i| { + let key_path = &format!("HS/SRK/myTestKey{}", i); + + // Initialize RNG + let mut rng = ChaChaRng::from_seed(create_seed(i)); + + // Create FAPI context + let mut context = match FapiContext::new() { + Ok(fpai_ctx) => fpai_ctx, + Err(error) => panic!("Failed to create context: {:?}", error), + }; + + // Initialize TPM, if not already initialized + tpm_initialize!(context, PASSWORD, my_auth_callback); + + // Create the key, if not already created + match context.create_key(&key_path, Some(KEY_FLAGS), None, Some(PASSWORD)) { + Ok(_) => debug!("Key created successfully."), + Err(error) => panic!("Key creation has failed: {:?}", error), + } + + // Generate data + let app_data = generate_bytes::<128usize>(&mut rng); + + // Set application data + match context.set_app_data(&key_path, Some(&app_data[..])) { + Ok(_) => debug!("Data set successfully."), + Err(error) => panic!("Setting application-specific data has failed: {:?}", error), + } + + // Get the application data + let recovered_data = match context.get_app_data(&key_path) { + Ok(cert_data) => cert_data, + Err(error) => panic!("Getting application-specific data has failed: {:?}", error), + }; + + // Verify + assert!(recovered_data.is_some()); + + // Erase application data + match context.set_app_data(&key_path, None) { + Ok(_) => debug!("Data erased."), + Err(error) => panic!("Setting application-specific data has failed: {:?}", error), + } + + // Get the application data (again) + let recovered_data = match context.get_app_data(&key_path) { + Ok(cert_data) => cert_data, + Err(error) => panic!("Getting application-specific data has failed: {:?}", error), + }; + + // Verify + assert!(recovered_data.is_none()); + }); +} diff --git a/tests/17_change_auth_test.rs b/tests/17_change_auth_test.rs new file mode 100644 index 0000000..07a2eab --- /dev/null +++ b/tests/17_change_auth_test.rs @@ -0,0 +1,100 @@ +/* SPDX-License-Identifier: BSD-3-Clause */ +/******************************************************************************* + * Copyright 2024, Fraunhofer SIT sponsored by the ELISA research project + * All rights reserved. + ******************************************************************************/ + +pub mod common; + +use common::{ + param::PASSWORD, + random::{create_seed, generate_string}, + setup::TestConfiguration, +}; +use function_name::named; +use log::debug; +use rand::SeedableRng; +use rand_chacha::ChaChaRng; +use serial_test::serial; +use tss2_fapi_rs::{FapiContext, KeyFlags}; + +mk_auth_callback!(my_auth_callback, PASSWORD); +mk_tpm_finalizer!(my_tpm_finalizer, my_auth_callback); + +const KEY_FLAGS: &[KeyFlags] = &[KeyFlags::NoDA, KeyFlags::Decrypt]; + +// ========================================================================== +// Test cases +// ========================================================================== + +/// Test the `change_auth()` function to change the auth value (password) for an existing key +#[test] +#[serial] +#[named] +fn test_change_auth() { + let _configuration = TestConfiguration::with_finalizer(my_tpm_finalizer); + + repeat_test!(|i| { + let key_path = &format!("HS/SRK/myTestKey{}", i); + + // Initialize RNG + let mut rng = ChaChaRng::from_seed(create_seed(i)); + + // Create FAPI context + let mut context = match FapiContext::new() { + Ok(fpai_ctx) => fpai_ctx, + Err(error) => panic!("Failed to create context: {:?}", error), + }; + + // Initialize TPM, if not already initialized + tpm_initialize!(context, PASSWORD, my_auth_callback); + + // Create the key, if not already created + match context.create_key(&key_path, Some(KEY_FLAGS), None, Some(PASSWORD)) { + Ok(_) => debug!("Key created successfully."), + Err(error) => panic!("Key creation has failed: {:?}", error), + } + + // Generate password + let new_password = generate_string::<12usize>(&mut rng); + + // Change auth value + match context.change_auth(&key_path, Some(&new_password[..])) { + Ok(_) => debug!("Auth value changed successfully."), + Err(error) => panic!("Changing the auth value has failed: {:?}", error), + } + }); +} + +/// Test the `change_auth()` function to remove the auth value (password) from an existing key +#[test] +#[serial] +#[named] +fn test_remove_auth() { + let _configuration = TestConfiguration::with_finalizer(my_tpm_finalizer); + + repeat_test!(|i| { + let key_path = &format!("HS/SRK/myTestKey{}", i); + + // Create FAPI context + let mut context = match FapiContext::new() { + Ok(fpai_ctx) => fpai_ctx, + Err(error) => panic!("Failed to create context: {:?}", error), + }; + + // Initialize TPM, if not already initialized + tpm_initialize!(context, PASSWORD, my_auth_callback); + + // Create the key, if not already created + match context.create_key(&key_path, Some(KEY_FLAGS), None, Some(PASSWORD)) { + Ok(_) => debug!("Key created successfully."), + Err(error) => panic!("Key creation has failed: {:?}", error), + } + + // Change auth value + match context.change_auth(&key_path, None) { + Ok(_) => debug!("Auth value removed."), + Err(error) => panic!("Removing the auth value has failed: {:?}", error), + } + }); +} diff --git a/tests/18_get_tcti_test.rs b/tests/18_get_tcti_test.rs new file mode 100644 index 0000000..c9d17d1 --- /dev/null +++ b/tests/18_get_tcti_test.rs @@ -0,0 +1,53 @@ +/* SPDX-License-Identifier: BSD-3-Clause */ +/******************************************************************************* + * Copyright 2024, Fraunhofer SIT sponsored by the ELISA research project + * All rights reserved. + ******************************************************************************/ + +pub mod common; + +use std::ptr; + +use common::{param::PASSWORD, setup::TestConfiguration}; +use function_name::named; +use log::debug; +use serial_test::serial; +use tss2_fapi_rs::FapiContext; + +mk_auth_callback!(my_auth_callback, PASSWORD); +mk_tpm_finalizer!(my_tpm_finalizer, my_auth_callback); + +// ========================================================================== +// Test cases +// ========================================================================== + +/// Test the `get_tcti()` function to retrieve the TCTI context +#[test] +#[serial] +#[named] +fn test_get_tcti() { + let _configuration = TestConfiguration::with_finalizer(my_tpm_finalizer); + + repeat_test!(|_i| { + // Create FAPI context + let mut context = match FapiContext::new() { + Ok(fpai_ctx) => fpai_ctx, + Err(error) => panic!("Failed to create context: {:?}", error), + }; + + // Initialize TPM, if not already initialized + tpm_initialize!(context, PASSWORD, my_auth_callback); + + // Get TCTI context + let tcti_context = match context.get_tcti() { + Ok(tcti_ctx) => tcti_ctx, + Err(error) => panic!("Failed to obtain TCTI context: {:?}", error), + }; + + // Print value + debug!("TCTI: {:?}", tcti_context); + + // Verify + assert!(tcti_context != ptr::null_mut()) + }); +} diff --git a/tests/19_multithread_test.rs b/tests/19_multithread_test.rs new file mode 100644 index 0000000..27910a2 --- /dev/null +++ b/tests/19_multithread_test.rs @@ -0,0 +1,155 @@ +/* SPDX-License-Identifier: BSD-3-Clause */ +/******************************************************************************* + * Copyright 2024, Fraunhofer SIT sponsored by the ELISA research project + * All rights reserved. + ******************************************************************************/ + +pub mod common; + +use common::{ + crypto::{get_key_type, KeyType}, + param::PASSWORD, + random::create_seed, + setup::TestConfiguration, +}; +use function_name::named; +use log::{debug, trace}; +use rand::{RngCore, SeedableRng}; +use rand_chacha::ChaChaRng; +use serial_test::serial; +use sha2::{Digest, Sha256}; +use std::{ + sync::{Arc, Barrier, Mutex}, + thread, +}; +use tss2_fapi_rs::{FapiContext, KeyFlags, PaddingFlags}; + +mk_auth_callback!(my_auth_callback, PASSWORD); +mk_tpm_finalizer!(my_tpm_finalizer, my_auth_callback); + +const KEY_FLAGS_SIGN: &[KeyFlags] = &[KeyFlags::NoDA, KeyFlags::Sign]; +const PADDING_RSAPSS: &[PaddingFlags] = &[PaddingFlags::RsaPss]; + +const THREAD_COUNT: usize = 8usize; + +// ========================================================================== +// Test cases +// ========================================================================== + +/// Test access to the same "shared" FAPI context from multiple concurrent threads to sign some random data +#[test] +#[serial] +#[named] +fn test_multiple_threads() { + let configuration = TestConfiguration::with_finalizer(my_tpm_finalizer); + + repeat_test!(|i| { + let key_path = &format!("HS/SRK/mySigKey{}", i); + + // Initialize RNG + let rng = ChaChaRng::from_seed(create_seed(i)); + + // Create FAPI context + let mut context = match FapiContext::new() { + Ok(fpai_ctx) => fpai_ctx, + Err(error) => panic!("Failed to create context: {:?}", error), + }; + + // Initialize TPM, if not already initialized + tpm_initialize!(context, PASSWORD, my_auth_callback); + + // Create new key, if not already created + match context.create_key(&key_path, Some(KEY_FLAGS_SIGN), None, Some(PASSWORD)) { + Ok(_) => debug!("Key created."), + Err(error) => panic!("Key creation has failed: {:?}", error), + } + + // Select padding algorithm + let padding_algo = get_padding_algorithm(&configuration); + trace!("Padding algorithm: {:?}", padding_algo); + + // Wrap the context in a Arc/Mutex so that it can be shared between threads + let shared_context = Arc::new(Mutex::new(context)); + + // Create a list and the barrier for the thread that will be spawned + let mut thread_list = Vec::with_capacity(THREAD_COUNT); + let barrier = Arc::new(Barrier::new(THREAD_COUNT)); + + // Start multiple threads to perform signing operations in parallel + for _n in 0..THREAD_COUNT { + let thread_keypath = key_path.clone(); + let mut thread_rng = rng.clone(); + let thread_context = shared_context.clone(); + let thread_barrier = barrier.clone(); + + // Spawn a new thread + thread_list.push(thread::spawn(move || { + // Get unique thread id + let tid = thread::current().id(); + + // Repeat the following operations n times (per thread) + for _k in 0..THREAD_COUNT { + // Make sure that all threads have reached this point, before we go on! + thread_barrier.wait(); + + // Compute digest to be signed + let digest = Sha256::digest( + format!("{:?}\\{:08X}", tid, thread_rng.next_u64()).as_bytes(), + ); + debug!("Digest to be signed: {}", hex::encode(&digest[..])); + + // Lock the mutex and create the signature + let signature = { + let mut mutex_guard = thread_context.lock().unwrap(); + match mutex_guard.sign(&thread_keypath, padding_algo, &digest, false, false) + { + Ok(value) => value, + Err(error) => panic!("Failed to create the signature: {:?}", error), + } + }; + + // Print the signature + let signature_data: &[u8] = signature.0.as_ref(); + assert!(signature_data.len() >= 32usize); + debug!("Signature value: {}", hex::encode(signature_data)); + + // Lock the mutex (again) and verify the signature + let verify_result = { + let mut mutex_guard = thread_context.lock().unwrap(); + match mutex_guard.verify_signature( + &thread_keypath, + &digest, + &signature.0[..], + ) { + Ok(value) => value, + Err(error) => panic!("Failed to verify the signature: {:?}", error), + } + }; + + // Check result + debug!("Signature valid: {:?}", verify_result); + assert!(verify_result); + } + })); + } + + // Wait for all threads + for thread in thread_list.drain(..) { + thread.join().unwrap(); + } + + // Finished + debug!("All threads have finished!"); + }); +} + +// ========================================================================== +// Helper functions +// ========================================================================== + +fn get_padding_algorithm(config: &TestConfiguration) -> Option<&'static [PaddingFlags]> { + match get_key_type(config.prof_name()) { + Some(KeyType::RsaKey) => Some(PADDING_RSAPSS), + _ => None, + } +} diff --git a/tests/common/crypto.rs b/tests/common/crypto.rs new file mode 100644 index 0000000..eb261e7 --- /dev/null +++ b/tests/common/crypto.rs @@ -0,0 +1,172 @@ +/* SPDX-License-Identifier: BSD-3-Clause */ +/******************************************************************************* + * Copyright 2024, Fraunhofer SIT sponsored by the ELISA research project + * All rights reserved. + ******************************************************************************/ + +use digest::{Digest, FixedOutputReset}; +use p256::{ + ecdsa::{ + signature::RandomizedDigestSigner as EccRandomizedDigestSigner, Signature as EccSignature, + SigningKey as EccSigningKey, + }, + pkcs8::DecodePrivateKey, + PublicKey as EccPublicKey, SecretKey as EccPrivateKey, +}; +use rand::thread_rng; +use rsa::{ + pkcs8::DecodePublicKey, + pss::{Signature as RsaSignature, SigningKey as RsaSigningKey}, + signature::{RandomizedDigestSigner as RsaRandomizedDigestSigner, SignatureEncoding}, + RsaPrivateKey, RsaPublicKey, +}; +use sha2::{Sha256, Sha384, Sha512}; +use tss2_fapi_rs::HashAlgorithm; + +// ========================================================================== +// Key types +// ========================================================================== + +/// Identifies the type of the private key +#[derive(Debug)] +pub enum KeyType { + RsaKey, + EccKey, +} + +macro_rules! key_type { + ($name:ident, $id:literal, $type:path) => { + if $name + .get(..$id.len()) + .map_or(false, |id| id.eq_ignore_ascii_case($id)) + { + return Some($type); + } + }; +} + +/// Get key type from profile name +pub fn get_key_type(profile_name: &str) -> Option { + key_type!(profile_name, "RSA", KeyType::RsaKey); + key_type!(profile_name, "ECC", KeyType::EccKey); + None +} + +// ========================================================================== +// Key import functions +// ========================================================================== + +/// Wrapper for the public key data +#[derive(Debug)] +pub enum PublicKey { + RsaKey(RsaPublicKey), + EccKey(EccPublicKey), +} + +/// Wrapper for the private key data +#[derive(Debug)] +pub enum PrivateKey { + RsaKey(RsaPrivateKey), + EccKey(EccPrivateKey), +} + +/// Load public key from PEM-encoded data +pub fn load_public_key(pem_data: &str, key_type: KeyType) -> Option { + match key_type { + KeyType::RsaKey => RsaPublicKey::from_public_key_pem(pem_data) + .map(|rsa_key| PublicKey::RsaKey(rsa_key)) + .ok(), + KeyType::EccKey => EccPublicKey::from_public_key_pem(pem_data) + .map(|ecc_key| PublicKey::EccKey(ecc_key)) + .ok(), + } +} + +/// Load private key from PEM-encoded data +pub fn load_private_key(pem_data: &str, key_type: KeyType) -> Option { + match key_type { + KeyType::RsaKey => RsaPrivateKey::from_pkcs8_pem(pem_data) + .map(|rsa_key| PrivateKey::RsaKey(rsa_key)) + .ok(), + KeyType::EccKey => EccPrivateKey::from_pkcs8_pem(pem_data) + .map(|ecc_key| PrivateKey::EccKey(ecc_key)) + .ok(), + } +} + +// ========================================================================== +// Signature computation +// ========================================================================== + +/// Compute signature using the given private key +pub fn create_signature( + private_key: &PrivateKey, + hash_algo: &HashAlgorithm, + message: &[u8], +) -> Option> { + match private_key { + PrivateKey::RsaKey(rsa_key) => create_signature_rsa(rsa_key, hash_algo, message), + PrivateKey::EccKey(ecc_key) => create_signature_ecc(ecc_key, hash_algo, message), + } +} + +/// Compute signature using the RSA-SSA scheme +fn create_signature_rsa( + private_key: &RsaPrivateKey, + hash_algo: &HashAlgorithm, + message: &[u8], +) -> Option> { + match hash_algo { + HashAlgorithm::Sha2_256 => Some(_create_signature_rsa( + private_key, + Sha256::new_with_prefix(message), + )), + HashAlgorithm::Sha2_384 => Some(_create_signature_rsa( + private_key, + Sha384::new_with_prefix(message), + )), + HashAlgorithm::Sha2_512 => Some(_create_signature_rsa( + private_key, + Sha512::new_with_prefix(message), + )), + _ => None, + } +} + +/// Compute signature using the RSA-SSA scheme +fn _create_signature_rsa(private_key: &RsaPrivateKey, digest: D) -> Vec +where + D: Digest + FixedOutputReset, +{ + let sign_key = RsaSigningKey::::from(private_key.to_owned()); + RsaRandomizedDigestSigner::::sign_digest_with_rng( + &sign_key, + &mut thread_rng(), + digest, + ) + .to_vec() +} + +/// Compute signature using the ECDSA-scheme on NIST P-256 curve +fn create_signature_ecc( + private_key: &EccPrivateKey, + hash_algo: &HashAlgorithm, + message: &[u8], +) -> Option> { + match hash_algo { + HashAlgorithm::Sha2_256 => Some(_create_signature_ecc(private_key, message)), + _ => None, + } +} + +/// Compute signature using the ECDSA-scheme on NIST P-256 curve +fn _create_signature_ecc(private_key: &EccPrivateKey, message: &[u8]) -> Vec { + let sign_key = EccSigningKey::from(private_key); + EccRandomizedDigestSigner::::sign_digest_with_rng( + &sign_key, + &mut thread_rng(), + Sha256::new_with_prefix(message), + ) + .to_der() + .to_vec() +} diff --git a/tests/common/mod.rs b/tests/common/mod.rs new file mode 100644 index 0000000..d41bc65 --- /dev/null +++ b/tests/common/mod.rs @@ -0,0 +1,12 @@ +/* SPDX-License-Identifier: BSD-3-Clause */ +/******************************************************************************* + * Copyright 2024, Fraunhofer SIT sponsored by the ELISA research project + * All rights reserved. + ******************************************************************************/ + +pub mod crypto; +pub mod param; +pub mod random; +pub mod setup; +pub mod tempfile; +pub mod utils; diff --git a/tests/common/param.rs b/tests/common/param.rs new file mode 100644 index 0000000..f107622 --- /dev/null +++ b/tests/common/param.rs @@ -0,0 +1,7 @@ +/* SPDX-License-Identifier: BSD-3-Clause */ +/******************************************************************************* + * Copyright 2024, Fraunhofer SIT sponsored by the ELISA research project + * All rights reserved. + ******************************************************************************/ + +pub const PASSWORD: &str = "uTtPndy3"; diff --git a/tests/common/random.rs b/tests/common/random.rs new file mode 100644 index 0000000..ebd66c9 --- /dev/null +++ b/tests/common/random.rs @@ -0,0 +1,47 @@ +/* SPDX-License-Identifier: BSD-3-Clause */ +/******************************************************************************* + * Copyright 2024, Fraunhofer SIT sponsored by the ELISA research project + * All rights reserved. + ******************************************************************************/ + +use rand::{Rng, RngCore}; +use std::{fmt::Debug, usize}; + +/// Create seed value from index +pub fn create_seed + Debug>(value: T) -> [u8; N] +where + >::Error: std::fmt::Debug, +{ + let mut seed_data = [0u8; N]; + let value_bytes = value.try_into().unwrap().to_be_bytes(); + if N > value_bytes.len() { + seed_data[N - value_bytes.len()..].copy_from_slice(&value_bytes[..]); + } else { + seed_data[..].copy_from_slice(&value_bytes[value_bytes.len() - N..]); + } + seed_data +} + +/// Generate pseudo-random bytes +pub fn generate_bytes(rand_gen: &mut impl RngCore) -> [u8; N] { + let mut rand_data = [0u8; N]; + rand_gen.fill_bytes(&mut rand_data); + rand_data +} + +/// Generate pseudo-random bytes +pub fn generate_string(rand_gen: &mut impl RngCore) -> String { + const ASCII_PRINTABLE: [char; 94] = [ + '!', '"', '#', '$', '%', '&', '\'', '(', ')', '*', '+', ',', '-', '.', '/', '0', '1', '2', + '3', '4', '5', '6', '7', '8', '9', ':', ';', '<', '=', '>', '?', '@', 'A', 'B', 'C', 'D', + 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', + 'W', 'X', 'Y', 'Z', '[', '\\', ']', '^', ' ', '`', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', + 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', + '{', '|', '}', '~', + ]; + let mut rand_str = ['\0'; N]; + for i in 0..N { + rand_str[i] = ASCII_PRINTABLE[rand_gen.gen_range(0..ASCII_PRINTABLE.len())]; + } + String::from_iter(rand_str) +} diff --git a/tests/common/setup.rs b/tests/common/setup.rs new file mode 100644 index 0000000..75d3afb --- /dev/null +++ b/tests/common/setup.rs @@ -0,0 +1,227 @@ +/* SPDX-License-Identifier: BSD-3-Clause */ +/******************************************************************************* + * Copyright 2024, Fraunhofer SIT sponsored by the ELISA research project + * All rights reserved. + ******************************************************************************/ + +use const_random::const_random; +use log::{debug, info, trace}; +use regex::{Regex, RegexBuilder}; +use std::{ + env, fs, + net::{IpAddr, SocketAddr, TcpStream}, + path::{Path, PathBuf}, + str::FromStr, + sync::{Mutex, MutexGuard, Once, OnceLock}, + time::{Duration, Instant}, +}; +use uuid::Uuid; + +/* Defaults */ +const TCTI_DEFAULT_VALUE: &str = "swtpm:host=127.0.0.1,port=2321"; +const PROF_DEFAULT_VALUE: &str = "RSA2048SHA256"; + +/* One-time initialization */ +static ENV_LOGGER_INIT: Once = Once::new(); + +/* Lazy initialization of Regex */ +static REGEX_SWTPM: OnceLock = OnceLock::new(); + +/* Randomize (at compile-time!) */ +const RANDOM_UUID_PREFIX: Uuid = Uuid::from_u64_pair(const_random!(u64), const_random!(u64)); + +/* Finalizer type alias */ +type Finalizer = Box; + +/* The mutex lock */ +static MUTEX: Mutex = Mutex::new(false); + +/* Accquire the lock */ +macro_rules! accquire_lock { + ($mutex:ident, $lock:ident) => { + let mut $lock = $mutex + .lock() + .or_else(|err| Ok::<_, ()>(err.into_inner())) + .unwrap(); + assert_eq!(*$lock, false); + *$lock = true; + }; +} + +pub struct TestConfiguration<'a> { + uniq_lock: MutexGuard<'a, bool>, + prof_name: &'a str, + data_path: PathBuf, + work_path: PathBuf, + finalizer: Option, +} + +impl<'a> TestConfiguration<'a> { + pub fn new() -> Self { + accquire_lock!(MUTEX, uniq_lock); + ENV_LOGGER_INIT.call_once(env_logger::init); + Self::initialize(uniq_lock, None) + } + + pub fn with_finalizer(finalizer: impl FnOnce() + 'static) -> Self { + accquire_lock!(MUTEX, uniq_lock); + ENV_LOGGER_INIT.call_once(env_logger::init); + Self::initialize(uniq_lock, Some(Box::new(finalizer))) + } + + fn initialize(uniq_lock: MutexGuard<'a, bool>, finalizer: Option) -> Self { + info!("Setting up the FAPI configuration, please wait..."); + + if let Some(path) = option_env!("FAPI_RS_TEST_DIR") { + env::set_current_dir(Path::new(path)).expect("Failed to set the working directory!"); + } + + let tcti_conf = option_env!("FAPI_RS_TEST_TCTI").unwrap_or(TCTI_DEFAULT_VALUE); + let prof_name = option_env!("FAPI_RS_TEST_PROF").unwrap_or(PROF_DEFAULT_VALUE); + + let regex_swtpm = REGEX_SWTPM.get_or_init(|| { + RegexBuilder::new(r"^\s*swtpm\s*:\s*host\s*=\s*([^\s,]+)\s*,\s*port\s*=\s*([^\s,]+)") + .case_insensitive(true) + .build() + .unwrap() + }); + + if let Some(capture) = regex_swtpm.captures(tcti_conf) { + let (host, port) = ( + capture.get(1).unwrap().as_str(), + capture.get(2).unwrap().as_str().parse::().unwrap(), + ); + Self::check_tpm_connection(&host, port); + } + + let base_path = Path::new(env!("CARGO_MANIFEST_DIR")); + debug!("Base directory: \"{}\"", base_path.to_str().unwrap()); + + let data_path = base_path.join("tests").join("data"); + debug!("Data directory: \"{}\"", data_path.to_str().unwrap()); + + let temp_path = Path::new(env!("CARGO_TARGET_TMPDIR")); + debug!("Temp directory: \"{}\"", temp_path.to_str().unwrap()); + + let work_path = temp_path.join(format!("fapi-{}", &RANDOM_UUID_PREFIX)); + debug!("Work directory: \"{}\"", work_path.to_str().unwrap()); + + let conf_file = work_path.join("config.json"); + debug!("FAPI conf file: \"{}\"", conf_file.to_str().unwrap()); + + if fs::metadata(&conf_file).map_or(false, |file_info| file_info.is_file()) { + debug!("Re-using the existing FAPI configuration!"); + } else { + Self::write_fapi_config(&conf_file, &data_path, &work_path, prof_name, tcti_conf); + } + + env::set_var("TSS2_FAPICONF", conf_file.to_str().unwrap()); + + Self { + uniq_lock, + prof_name, + data_path, + work_path, + finalizer, + } + } + + fn write_fapi_config( + conf_file: &Path, + data_path: &Path, + work_path: &Path, + prof_name: &str, + tcti_conf: &str, + ) { + if Path::try_exists(&work_path).unwrap_or(true) { + fs::remove_dir_all(&work_path).expect("Failed to remove existing directory!"); + } + + fs::create_dir_all(&work_path).expect("Failed to create subdirectories!"); + + let prof_path = work_path.join("profiles"); + debug!("Prof directory: \"{}\"", prof_path.to_str().unwrap()); + fs::create_dir_all(&prof_path).expect("Failed to create subdirectories!"); + + let keys_path = work_path.join("keystore"); + debug!("Keys directory: \"{}\"", keys_path.to_str().unwrap()); + fs::create_dir_all(&keys_path).expect("Failed to create subdirectories!"); + + let user_path = keys_path.join("user"); + debug!("User key-store: \"{}\"", user_path.to_str().unwrap()); + fs::create_dir_all(user_path.join("policy")).expect("Failed to create subdirectories!"); + + let syst_path = keys_path.join("system"); + debug!("Syst key-store: \"{}\"", syst_path.to_str().unwrap()); + fs::create_dir_all(syst_path.join("policy")).expect("Failed to create subdirectories!"); + + let logs_path = work_path.join("eventlog"); + debug!("Logs directory: \"{}\"", logs_path.to_str().unwrap()); + fs::create_dir_all(&logs_path).expect("Failed to create subdirectories!"); + + let mut content = fs::read_to_string(data_path.join("fapi-config.json.template")) + .expect("Failed to read input file!"); + content = content.replace("{{TCTI_CFG}}", tcti_conf); + content = content.replace("{{PROF_CFG}}", prof_name); + content = content.replace("{{PROF_DIR}}", prof_path.to_str().unwrap()); + content = content.replace("{{USER_DIR}}", user_path.to_str().unwrap()); + content = content.replace("{{SYST_DIR}}", syst_path.to_str().unwrap()); + content = content.replace("{{LOGS_DIR}}", logs_path.to_str().unwrap()); + + trace!("FAPI conf data: {}", content.trim()); + fs::write(&conf_file, &content).expect("Failed to write configuration to output file!"); + + for entry in fs::read_dir(data_path.join("profiles")).unwrap().flatten() { + let fname = entry.file_name(); + if fname.to_str().map_or(false, |str| str.starts_with("P_")) { + let (path_src, path_dst) = (entry.path(), prof_path.join(fname)); + debug!("Copy: {:?} -> {:?}", path_src, path_dst); + fs::copy(path_src, path_dst).expect("Failed to copy file!"); + } + } + } + + fn check_tpm_connection(host: &str, port: u16) { + debug!("Connecting to SWTPM, please wait... [{}:{}]", host, port); + let start_time = Instant::now(); + loop { + match TcpStream::connect_timeout( + &SocketAddr::new(IpAddr::from_str(host).unwrap(), port), + Duration::from_secs(10), + ) { + Ok(conn) => { + return conn.shutdown(std::net::Shutdown::Both).unwrap(); + } + Err(_) => { + if start_time.elapsed().as_secs() > 100_u64 { + panic!("Failed to connect to the SWTPM. Is the SWTPM running?"); + } + } + } + } + } + + #[allow(dead_code)] + pub fn prof_name(&self) -> &str { + &self.prof_name + } + + #[allow(dead_code)] + pub fn data_path(&self) -> &Path { + &self.data_path + } + + #[allow(dead_code)] + pub fn work_path(&self) -> &Path { + &self.work_path + } +} + +impl<'a> Drop for TestConfiguration<'a> { + fn drop(&mut self) { + if let Some(finalizer) = self.finalizer.take() { + finalizer(); + } + *self.uniq_lock = false; + } +} diff --git a/tests/common/tempfile.rs b/tests/common/tempfile.rs new file mode 100644 index 0000000..1e3f8d9 --- /dev/null +++ b/tests/common/tempfile.rs @@ -0,0 +1,45 @@ +/* SPDX-License-Identifier: BSD-3-Clause */ +/******************************************************************************* + * Copyright 2024, Fraunhofer SIT sponsored by the ELISA research project + * All rights reserved. + ******************************************************************************/ + +use rand::RngCore; +use std::{ + fs::{self, File}, + path::{Path, PathBuf}, +}; + +pub struct TempFile { + file_path: PathBuf, +} + +impl TempFile { + pub fn new(base_dir: &Path) -> Option { + Self::with_suffix(base_dir, "tmp") + } + + pub fn with_suffix(base_dir: &Path, suffix: &str) -> Option { + assert!(!suffix.is_empty() && suffix.chars().all(|c| char::is_ascii_alphanumeric(&c))); + let mut rng = rand::thread_rng(); + + for _i in 0..99 { + let file_path = base_dir.join(format!("temp-{:16X}.{}", rng.next_u64(), suffix)); + if File::create_new(&file_path).is_ok() { + return Some(TempFile { file_path }); + } + } + + None /* failed to generate unique temp file name*/ + } + + pub fn path(&self) -> &Path { + &self.file_path + } +} + +impl Drop for TempFile { + fn drop(&mut self) { + let _success = fs::remove_file(&self.file_path); + } +} diff --git a/tests/common/utils.rs b/tests/common/utils.rs new file mode 100644 index 0000000..1aac67c --- /dev/null +++ b/tests/common/utils.rs @@ -0,0 +1,126 @@ +/* SPDX-License-Identifier: BSD-3-Clause */ +/******************************************************************************* + * Copyright 2024, Fraunhofer SIT sponsored by the ELISA research project + * All rights reserved. + ******************************************************************************/ + +use log::info; +use std::{sync::OnceLock, usize}; + +/* Defaults */ +const LOOPS_DEFAULT_VALUE: usize = 3; + +/* One-time initialization */ +static TEST_LOOPS: OnceLock = OnceLock::new(); + +/// Repeat the given function (e.g. test) `n` times. Should be invoked by using the `repeat_test!()` macro! +pub fn _repeat_test(name: &str, test_fn: F) { + let loops = *TEST_LOOPS.get_or_init(|| { + option_env!("FAPI_RS_TEST_LOOP") + .and_then(|str| str.parse::().ok()) + .unwrap_or(LOOPS_DEFAULT_VALUE) + }); + for i in 0..loops { + info!( + "\u{25B6} {}, execution {} of {} \u{25C0}", + name, + i + 1, + loops + ); + test_fn(i); + } +} + +/// Repeat the given function (e.g. test) `n` times. +#[macro_export] +macro_rules! repeat_test { + ($func:expr) => { + crate::common::utils::_repeat_test(function_name!(), $func) + }; +} + +/// Skip test conditionally on profile name. +#[macro_export] +macro_rules! skip_test_ifeq { + ($conf:ident, $name:literal) => { + if $conf + .prof_name() + .get(..$name.len()) + .map_or(false, |name| name.eq_ignore_ascii_case($name)) + { + warn!("Skipping this test for \"{}\" profile!", $name); + return; /* skip! */ + } + }; +} + +/// Set up the auth callback and then perform the initial provisioning. +/// +/// This function ignores a possible "AlreadyProvisioned" error, because that kind of error is expected for all but the very first test! +#[macro_export] +macro_rules! tpm_initialize { + ($context:ident, $password:ident, $auth_cb:ident) => { + if let Err(error) = $context.set_auth_callback(tss2_fapi_rs::AuthCallback::new($auth_cb)) { + panic!("Setting up the callback has failed: {:?}", error) + } + + match $context.provision(None, Some($password), None) { + Ok(_) => log::debug!("Provisioned."), + Err(tss2_fapi_rs::ErrorCode::FapiError( + tss2_fapi_rs::BaseErrorCode::AlreadyProvisioned, + )) => log::debug!("TPM already provisioned."), + Err(error) => panic!("Provisioning has failed: {:?}", error), + } + }; +} + +/// The "auth" callback implementation used for testing. +#[macro_export] +macro_rules! mk_auth_callback { + ($fn_name:ident, $password:expr) => { + fn $fn_name( + param: tss2_fapi_rs::AuthCallbackParam, + ) -> Option> { + log::debug!( + "(AUTH_CB) Auth value for path {:?} has been requested!", + param.object_path + ); + log::trace!("(AUTH_CB) Parameters: {:?}", param); + + let object_path = param + .object_path + .find('/') + .map(|pos| ¶m.object_path[pos + 1usize..]) + .unwrap_or(param.object_path); + if !object_path.eq("HS") && !object_path.starts_with("HS/SRK/my") { + log::warn!( + "(AUTH_CB) The requested object path {:?} is not recognized!", + object_path + ); + return None; + } + + Some(std::borrow::Cow::Borrowed($password)) + } + }; +} + +/// Perform final clean-up of all objects created during test run(s). +#[macro_export] +macro_rules! mk_tpm_finalizer { + ($fn_name:ident, $auth_cb:ident) => { + fn $fn_name() { + log::debug!("Cleaning up the TPM now!"); + if FapiContext::new() + .and_then(|mut fpai_ctx| { + fpai_ctx + .set_auth_callback(tss2_fapi_rs::AuthCallback::new($auth_cb)) + .and_then(|_| fpai_ctx.delete("/")) + }) + .is_err() + { + log::warn!("Failed to clean-up test objects, take care!"); + } + } + }; +} diff --git a/tests/data/certificates/x509-cert.pem b/tests/data/certificates/x509-cert.pem new file mode 100644 index 0000000..f740cfc --- /dev/null +++ b/tests/data/certificates/x509-cert.pem @@ -0,0 +1,30 @@ +-----BEGIN CERTIFICATE----- +MIIFEDCCBLagAwIBAgIQeKYt0kDKd720iqA1OSlqNDAKBggqhkjOPQQDAjBEMQsw +CQYDVQQGEwJOTDEZMBcGA1UEChMQR0VBTlQgVmVyZW5pZ2luZzEaMBgGA1UEAxMR +R0VBTlQgT1YgRUNDIENBIDQwHhcNMjMxMjI5MDAwMDAwWhcNMjQxMjI4MjM1OTU5 +WjBTMQswCQYDVQQGEwJERTEPMA0GA1UECBMGQmF5ZXJuMRMwEQYDVQQKEwpGcmF1 +bmhvZmVyMR4wHAYDVQQDExV3d3cuc2l0LmZyYXVuaG9mZXIuZGUwWTATBgcqhkjO +PQIBBggqhkjOPQMBBwNCAAQ94kB1cVZlnT7yBgWqSPinsrSq0Z/mQTiNPGgmCRlQ +PDoEkLQRchPheNJPNqusP9ZaYhejXKnCLFEiSVwRtxFTo4IDeTCCA3UwHwYDVR0j +BBgwFoAU7bSgM2obCJG2vfpBkr2aq6tj9FMwHQYDVR0OBBYEFKIYC6LXto02W8GA +uIYIo/F033IjMA4GA1UdDwEB/wQEAwIHgDAMBgNVHRMBAf8EAjAAMB0GA1UdJQQW +MBQGCCsGAQUFBwMBBggrBgEFBQcDAjBJBgNVHSAEQjBAMDQGCysGAQQBsjEBAgJP +MCUwIwYIKwYBBQUHAgEWF2h0dHBzOi8vc2VjdGlnby5jb20vQ1BTMAgGBmeBDAEC +AjA/BgNVHR8EODA2MDSgMqAwhi5odHRwOi8vR0VBTlQuY3JsLnNlY3RpZ28uY29t +L0dFQU5UT1ZFQ0NDQTQuY3JsMHUGCCsGAQUFBwEBBGkwZzA6BggrBgEFBQcwAoYu +aHR0cDovL0dFQU5ULmNydC5zZWN0aWdvLmNvbS9HRUFOVE9WRUNDQ0E0LmNydDAp +BggrBgEFBQcwAYYdaHR0cDovL0dFQU5ULm9jc3Auc2VjdGlnby5jb20wggF+Bgor +BgEEAdZ5AgQCBIIBbgSCAWoBaAB1AHb/iD8KtvuVUcJhzPWHujS0pM27KdxoQgqf +5mdMWjp0AAABjLTSlhIAAAQDAEYwRAIgJhC+7HhMg/MqBYE1RAqSL+mqfkfbTXun +ZrBRoUOMhnYCIEr7ySEdjgcbrEJFRkGwwvqsY8KMf0bkyOfZsmI32LiXAHYAPxdL +T9ciR1iUHWUchL4NEu2QN38fhWrrwb8ohez4ZG4AAAGMtNKV9AAABAMARzBFAiAz +bgiGayqRQSzEJ8t+NXhOo4d4fXzaM/mCw00td6uFRAIhAIbtlgBKGIk6MdxpNX+c +nC61wBr6Jf3DHakih1LYGOAwAHcA7s3QZNXbGs7FXLedtM0TojKHRny87N7DUUhZ +RnEftZsAAAGMtNKV8wAABAMASDBGAiEAkyVy86pXWeVPOoNdg80xjAK6lY4Qfybz ++6vbq16Zoc8CIQD7sWps4zRak5tahCSGGheUJidwjAlTJj0a8tVWP0mVazBxBgNV +HREEajBoghV3d3cuc2l0LmZyYXVuaG9mZXIuZGWCG2xlaHJzdHVobC5zaXQuZnJh +dW5ob2Zlci5kZYIRc2l0LmZyYXVuaG9mZXIuZGWCH3d3dy5sZWhyc3R1aGwuc2l0 +LmZyYXVuaG9mZXIuZGUwCgYIKoZIzj0EAwIDSAAwRQIgbF1BXUBAmwjusFL5K84P +KkpLjZXRWm+9sMsyskHVPEQCIQD4cL3TiiFDgb8s8mX/cZlYw1QZdTd/c1v6B0T/ +98m5JQ== +-----END CERTIFICATE----- diff --git a/tests/data/fapi-config.json.template b/tests/data/fapi-config.json.template new file mode 100644 index 0000000..ac9a0e6 --- /dev/null +++ b/tests/data/fapi-config.json.template @@ -0,0 +1,10 @@ +{ + "profile_name": "P_{{PROF_CFG}}", + "profile_dir": "{{PROF_DIR}}", + "user_dir": "{{USER_DIR}}", + "system_dir": "{{SYST_DIR}}", + "tcti": "{{TCTI_CFG}}", + "system_pcrs": [], + "log_dir": "{{LOGS_DIR}}", + "ek_cert_less": "yes" +} diff --git a/tests/data/keys/ca_key_ecc.pem b/tests/data/keys/ca_key_ecc.pem new file mode 100644 index 0000000..ae620e4 --- /dev/null +++ b/tests/data/keys/ca_key_ecc.pem @@ -0,0 +1,14 @@ +-----BEGIN CERTIFICATE----- +MIIBRzCB7aADAgECAhQ+ZOzpVwCUeHv6Phgpwg62fuEpADAKBggqhkjOPQQDAjAS +MRAwDgYDVQQDDAdUZXN0IENBMB4XDTI0MDgwODExNDEwOFoXDTI0MDkwNzExNDEw +OFowEjEQMA4GA1UEAwwHVGVzdCBDQTBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IA +BNEz9Ew1g23s/4mNpC+h1paGzPwUjtKJ2bJLHxrhP4IwBc+/P5M+XN/jsrHCRsWa +p/Yv0Hi9gFnm2sxMyzY8DfCjITAfMB0GA1UdDgQWBBT1UWBwtPSe9jp920WvY+Z+ +kT5A7TAKBggqhkjOPQQDAgNJADBGAiEA+tyq7HGg/KDtwXKYpgW7Ia07OONLF8xI +SvKD83rE+9sCIQCJ5JCSp11qe7Y3fpY23IavH+Nrrbu8jvRFkc7JPvVOtg== +-----END CERTIFICATE----- +-----BEGIN PRIVATE KEY----- +MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQg9oCE8/6LEH/7vXjh +37ASgffBvKUfLBXeewevd/9ctoGhRANCAATRM/RMNYNt7P+JjaQvodaWhsz8FI7S +idmySx8a4T+CMAXPvz+TPlzf47KxwkbFmqf2L9B4vYBZ5trMTMs2PA3w +-----END PRIVATE KEY----- diff --git a/tests/data/keys/ca_key_rsa.pem b/tests/data/keys/ca_key_rsa.pem new file mode 100644 index 0000000..c3656ee --- /dev/null +++ b/tests/data/keys/ca_key_rsa.pem @@ -0,0 +1,80 @@ +-----BEGIN CERTIFICATE----- +MIIE0zCCArugAwIBAgIUSQ7vz3KryejaQoJfJB17oDftL40wDQYJKoZIhvcNAQEL +BQAwEjEQMA4GA1UEAwwHVGVzdCBDQTAeFw0yNDA4MDgxMTQwNTlaFw0yNDA5MDcx +MTQwNTlaMBIxEDAOBgNVBAMMB1Rlc3QgQ0EwggIiMA0GCSqGSIb3DQEBAQUAA4IC +DwAwggIKAoICAQCtcPWUbyQReOwQy79qdpimToDEhuHi5ApP/JhIULUkfzZmpMLx +cIT4Rv56YAJ7tJt8XvQX5emrue4VpaQqxCkp+f8TfFBnVma9SmV1YgP5K2+Jircz +RNuNolTbuffbCbPJOhI9csZ1XzP2nLrK3E530YExuEWM+fQ+N9x/ovnxlQeBXygX +ee2yhC2z5eAH39NxOysMFuoTHj9QL910sZ5XX4unIZsPf05JA1hyfnhFYZU/jYTP +4hcDpK6f3NcwbyJUw5qFVN/gPlxf4LGJQMzGbCmf37LoZ89uLqxv8Bc8PEz4uQ1X +X3D4MiWVgdsDg/CM/nby6vM07nKbpV4vqiPxigKFXN1kZQQOCucrNetMZ4eZRHbn +iOhzZZFf1pmvl/slMKaymMYKeGMNQmxc/W3JqdBw+YKM68nbZ+XA4VpIShrLABXh +If/3SUWpqgggJFTF6goD+M90lLVgHA0n4My2IycsF+AqkILIqwCJqZHLeosptVzl +p8+haaG++66t6lBbW+mw4KfalaBeb/udGjOr/imlbJ70yeB+p2yzfynDCKd1Eli9 +9pbqy6nICx5QEzugAC2O/4UZ1anOBMHakhTaUZc5p7WDnklJ4iHs6KLBovQCuMv3 +ylewxK4qBriT6sz8d6vSxM2wdZl47OhPXvlKR21R30Uh0MqVE0kAEBEyCwIDAQAB +oyEwHzAdBgNVHQ4EFgQUjCfMXQZUOYr0eMQf058WFfjk10EwDQYJKoZIhvcNAQEL +BQADggIBAE1N3zS5OngmwO9DTDtppRJ8Sz0iyEbjFw/2xGlu1qQqA30Uc+LKc57E +Rz5FeZEuvyUh2mYmXCZX0fVWtl7f9BmlUR7U0sr7oH8q7n3xcDkywATXb8gWYALx +HQ/BAvkAJhdGx+4G13TaKn5DXzhonzFb+9xVCvvlt01i1N+oIjAOVp1NxV5uCvRq +0ou6ZrZdUDEL6mWgpb/QzIjJ8YS8vyl7QT2UrG/jJI6d1gPlfV9f1yn5xBh/gadU +C9An6qtYCPO7r80fBvjcZYTjoGoowb8YTKg/lARZYKJg2LvRjLXSPNm9Eye1wBJz +BO9ac0BoBv/SdzW+XL3CEoaCyi/hEO/NOYdeiNIlQTmzK+hr8qgyhVqTkR10lXqx +AFvN+h5cDUnkuL1ZTcUcFXN0E8fuIrQhCk9fV/oX81E2yavMnlL0CmL+RnsWIoTx +raYcCfsl4WnwaimhZEWJno8Ut2IHdcdG9EIYg9v38Qmf0e1poZCtC21AGsWNYOlP +D8cnLd3NNxEGjNNTNB8yesnBp3uZUCvIjDPwZIkED0yohA7eGiIN+Ss4jWt5re6A +/7FXogwk1OqPj2eD7uCjYRBwtCjWIszsLL+UOtssnDjBgP6zAtVBVg3dtfEPoexz +u7h8vH50EUXsamWPOEh9GHdgGc/cWl9yNLtqvE9qUuxm7cpGFwFE +-----END CERTIFICATE----- +-----BEGIN PRIVATE KEY----- +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 +z25aItMpEFssr0GhUD4voJe+NHf8TgI= +-----END PRIVATE KEY----- diff --git a/tests/data/keys/key_ecc_256.pem b/tests/data/keys/key_ecc_256.pem new file mode 100644 index 0000000..a1a82ef --- /dev/null +++ b/tests/data/keys/key_ecc_256.pem @@ -0,0 +1,5 @@ +-----BEGIN PRIVATE KEY----- +MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgsTyOJz/rD+UqgVNV +uWQ1DguzkhiJtA1DgOTkH1HT042hRANCAASglNrfN+10DMcL3oiOmpD92cub6nuk +SIsyLTcl3Sg/RpiedcWOiXMgH90Fn+34EuHQ/AszloAz+O6B4uQeCkjJ +-----END PRIVATE KEY----- diff --git a/tests/data/keys/key_rsa_2048.pem b/tests/data/keys/key_rsa_2048.pem new file mode 100644 index 0000000..992d1d2 --- /dev/null +++ b/tests/data/keys/key_rsa_2048.pem @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQCgYvoisJIDOeYg +jMF6ywiZbu085TLvy5ZMhq5vfYqdgefvwpemutnfKSnpYOs5B4yO/gAD7XiYluDv +tqlVhfISeQV04xrWGzNImenpwm/HsgueAu8VTHNtWSL96G+BLedGTrs2NqX6cxN7 +yGl7dQpB5X8iP4XSvpjP3Vb7gs+adwCJR6xFkt60jYFmwrAdhEzOeakhimi5rU21 +LxCkRdEyaxS57X15L9dEA+aYJ+dvkFfZOfTqIKmTrA75F8yj161xflwtIC4hgRBg +K9Xb/RdN8TDrTu+20E3RjngutU4qejW9Fd3mzHJGV8HRYvjYXhUblN9wmjm7Veru +T2b0rnvzAgMBAAECggEBAIwHvoJ5DRJ6A50Zp3dROxHTEphfOEi6xF/OGxBGWLbK +C7l+eS9d5gj8BJa5QsXI/IR/6X2EYQ1AdeV04oVD7CUKuqPiALU8jFrv3pV0aGm+ +3nu37gv3crPe5jkvLeNoM4tkA/oCXom63SDuyoG6nxkHiSdatLlaJUse4em3vRAL +QivziZIMyswcleMe0xAoMi7LO+nUFFxBS8/xGya0vsU0dsMQEl1SRITv1VCXmPQD +T4dEI4+1cufv6Ax0EDbFKmnjyiGTjOeQKrGIqETUSQolbg5PgL1XZehaaxM822OY +Qpnp5T0XhUEmVrOb2Wrboj+dC/2tgAN/fWXjAAxnm2ECgYEA02UTZuZ+QnD6tqo3 +/y3n5kaM9uA2mdOIqgECI9psGF1IBIC/iP2diKyuvmQL8hzymComb5YzZl3TOAga +WHQYbIeU3JhnYTG75/Dv5Zh32H4NjkIJHT2/8LUM25Ove9u6QAniVgIQpBZ47LjX +9jHjTYCW5n79qNSfu0egYJUvypECgYEAwjqWzzEINqnX/xIVCoB4XpuDuSdkM0JW +MZDIH9xHjZPp07/5XYEoITylk6Zwbh+djvWDNP4gzPtuK26VsqrNxoWMsFZeXn6U +xSOYL2UNCZiOgchdZCOr+6r8LRUuo8xHjbawVoJVK1+tZ2WsR3ilt3Gw34O8Z5ep +f4v7GOXw+EMCgYAUHjFrgJIRhqkFi0uK+HZyXtJ5iDsKBqyh6Tin6tiQtQfujcYs +pl5ArJZwvhq47vJTcud3hSbdHh7E3ViMhHfylDChkct833vPhgl+ozT8oHpvyG8P +nlnO8ZwIpZR0yCOAhrBImSe2RgE6HhlHb9X/ATbbNsizMZEGBLoJlwkWUQKBgQCy +4U7fh2LvJUF+82JZh7RUPZn1Pmg0JVZI0/TcEv37UEy77kR1b2xMIBTGhTVq1sc/ +ULIEbkA7SR1P9sr7//8AZSMLjJ/hG2dcoMmabNCzE8O7l5MblRbh87nIs4d+57bG +t4h0RBi4l6eWYLdoI59L8fNaB3PPXIiIpZ0eczeZDQKBgQC2vuFYpUZqDb9CaJsn +Luee6P6n5v3ZBTAT4E+GG1kWS28BiebcCuLKNAY4ZtLo08ozaTWcMxooOTeka2ux +fQDE4M/LTNpam8QOJ2hqECF5a0uBYNcbmaGtfA9KwIgwCZZYuwb5IDq/DRPuR690 +i8Kp6jR2wY0suObmZHKvbCB1Dw== +-----END PRIVATE KEY----- diff --git a/tests/data/policies/pol_action.json b/tests/data/policies/pol_action.json new file mode 100644 index 0000000..7cb8a08 --- /dev/null +++ b/tests/data/policies/pol_action.json @@ -0,0 +1,9 @@ +{ + "description": "The description", + "policy": [ + { + "type": "POLICYACTION", + "action": "myaction" + } + ] +} diff --git a/tests/data/policies/pol_duplicate.json b/tests/data/policies/pol_duplicate.json new file mode 100644 index 0000000..f7df6bf --- /dev/null +++ b/tests/data/policies/pol_duplicate.json @@ -0,0 +1,9 @@ +{ + "description": "Description pol_duplicate", + "policy": [ + { + "type": "POLICYDUPLICATIONSELECT", + "newParentPath": "ext/myNewParent" + } + ] +} diff --git a/tests/data/policies/pol_or.json b/tests/data/policies/pol_or.json new file mode 100644 index 0000000..fa1de19 --- /dev/null +++ b/tests/data/policies/pol_or.json @@ -0,0 +1,33 @@ +{ + "description": "Description pol_or", + "policy": [ + { + "type": "POLICYOR", + "branches": [ + { + "name": "PolicySignedRSA", + "description": "Description pol_signed #1", + "policy": [ + { + "type": "POLICYSIGNED", + "publicKeyHint": "Test key hint", + "keyPEM": "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAoGL6IrCSAznmIIzBessI\nmW7tPOUy78uWTIaub32KnYHn78KXprrZ3ykp6WDrOQeMjv4AA+14mJbg77apVYXy\nEnkFdOMa1hszSJnp6cJvx7ILngLvFUxzbVki/ehvgS3nRk67Njal+nMTe8hpe3UK\nQeV/Ij+F0r6Yz91W+4LPmncAiUesRZLetI2BZsKwHYRMznmpIYpoua1NtS8QpEXR\nMmsUue19eS/XRAPmmCfnb5BX2Tn06iCpk6wO+RfMo9etcX5cLSAuIYEQYCvV2/0X\nTfEw607vttBN0Y54LrVOKno1vRXd5sxyRlfB0WL42F4VG5TfcJo5u1Xq7k9m9K57\n8wIDAQAB\n-----END PUBLIC KEY-----\n", + "keyPEMhashAlg": "SHA256" + } + ] + }, + { + "name": "PolicySignedECC", + "description": "Description pol_signed #2", + "policy": [ + { + "type": "POLICYSIGNED", + "keyPEM": "-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEoJTa3zftdAzHC96IjpqQ/dnLm+p7\npEiLMi03Jd0oP0aYnnXFjolzIB/dBZ/t+BLh0PwLM5aAM/jugeLkHgpIyQ==\n-----END PUBLIC KEY-----\n", + "keyPEMhashAlg": "SHA256" + } + ] + } + ] + } + ] +} diff --git a/tests/data/policies/pol_signed_ecc.json b/tests/data/policies/pol_signed_ecc.json new file mode 100644 index 0000000..527124e --- /dev/null +++ b/tests/data/policies/pol_signed_ecc.json @@ -0,0 +1,11 @@ +{ + "description": "Description pol_signed", + "policy": [ + { + "type": "POLICYSIGNED", + "publicKeyHint": "Test key hint (ecc)", + "keyPEM": "-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEoJTa3zftdAzHC96IjpqQ/dnLm+p7\npEiLMi03Jd0oP0aYnnXFjolzIB/dBZ/t+BLh0PwLM5aAM/jugeLkHgpIyQ==\n-----END PUBLIC KEY-----\n", + "keyPEMhashAlg": "SHA256" + } + ] +} diff --git a/tests/data/policies/pol_signed_rsa.json b/tests/data/policies/pol_signed_rsa.json new file mode 100644 index 0000000..af172a1 --- /dev/null +++ b/tests/data/policies/pol_signed_rsa.json @@ -0,0 +1,11 @@ +{ + "description": "Description pol_signed", + "policy": [ + { + "type": "POLICYSIGNED", + "publicKeyHint": "Test key hint (rsa)", + "keyPEM": "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAoGL6IrCSAznmIIzBessI\nmW7tPOUy78uWTIaub32KnYHn78KXprrZ3ykp6WDrOQeMjv4AA+14mJbg77apVYXy\nEnkFdOMa1hszSJnp6cJvx7ILngLvFUxzbVki/ehvgS3nRk67Njal+nMTe8hpe3UK\nQeV/Ij+F0r6Yz91W+4LPmncAiUesRZLetI2BZsKwHYRMznmpIYpoua1NtS8QpEXR\nMmsUue19eS/XRAPmmCfnb5BX2Tn06iCpk6wO+RfMo9etcX5cLSAuIYEQYCvV2/0X\nTfEw607vttBN0Y54LrVOKno1vRXd5sxyRlfB0WL42F4VG5TfcJo5u1Xq7k9m9K57\n8wIDAQAB\n-----END PUBLIC KEY-----\n", + "keyPEMhashAlg": "SHA256" + } + ] +} diff --git a/tests/data/profiles/P_ECCP256SHA256.json b/tests/data/profiles/P_ECCP256SHA256.json new file mode 100644 index 0000000..a9eb99f --- /dev/null +++ b/tests/data/profiles/P_ECCP256SHA256.json @@ -0,0 +1,47 @@ +{ + "type": "TPM2_ALG_ECC", + "nameAlg": "TPM2_ALG_SHA256", + "srk_template": "system,restricted,decrypt,0x81000001", + "srk_description": "Storage root key SRK", + "srk_persistent": 0, + "ek_template": "system,restricted,decrypt", + "ek_description": "Endorsement key EK", + "ecc_signing_scheme": { + "scheme": "TPM2_ALG_ECDSA", + "details": { + "hashAlg": "TPM2_ALG_SHA256" + } + }, + "sym_mode": "TPM2_ALG_CFB", + "sym_parameters": { + "algorithm": "TPM2_ALG_AES", + "keyBits": "128", + "mode": "TPM2_ALG_CFB" + }, + "sym_block_size": 16, + "pcr_selection": [ + { + "hash": "TPM2_ALG_SHA256", + "pcrSelect": [ + 8, + 9, + 10, + 11, + 12, + 13, + 14, + 15 + ] + } + ], + "curveID": "TPM2_ECC_NIST_P256", + "ek_policy": { + "description": "Endorsement hierarchy used for policy secret.", + "policy": [ + { + "type": "POLICYSECRET", + "objectName": "4000000b" + } + ] + } +} diff --git a/tests/data/profiles/P_RSA2048SHA256.json b/tests/data/profiles/P_RSA2048SHA256.json new file mode 100644 index 0000000..66d9ef5 --- /dev/null +++ b/tests/data/profiles/P_RSA2048SHA256.json @@ -0,0 +1,51 @@ +{ + "type": "TPM2_ALG_RSA", + "nameAlg": "TPM2_ALG_SHA256", + "srk_template": "system,restricted,decrypt", + "srk_description": "Storage root key SRK", + "srk_persistent": 0, + "ek_template": "system,restricted,decrypt", + "ek_description": "Endorsement key EK", + "ecc_signing_scheme": { + "scheme": "TPM2_ALG_ECDSA", + "details": { + "hashAlg": "TPM2_ALG_SHA256" + } + }, + "rsa_signing_scheme": { + "scheme": "TPM2_ALG_RSAPSS", + "details": { + "hashAlg": "TPM2_ALG_SHA256" + } + }, + "rsa_decrypt_scheme": { + "scheme": "TPM2_ALG_OAEP", + "details": { + "hashAlg": "TPM2_ALG_SHA256" + } + }, + "sym_mode": "TPM2_ALG_CFB", + "sym_parameters": { + "algorithm": "TPM2_ALG_AES", + "keyBits": "128", + "mode": "TPM2_ALG_CFB" + }, + "sym_block_size": 16, + "pcr_selection": [ + { + "hash": "TPM2_ALG_SHA256", + "pcrSelect": [ + 8, + 9, + 10, + 11, + 12, + 13, + 14, + 15 + ] + } + ], + "exponent": 0, + "keyBits": 2048 +} diff --git a/tools/codecov/code-coverage.sh b/tools/codecov/code-coverage.sh new file mode 100755 index 0000000..cd7a556 --- /dev/null +++ b/tools/codecov/code-coverage.sh @@ -0,0 +1,59 @@ +#!/bin/bash +set -eo pipefail +cd -- "$(dirname -- "${BASH_SOURCE[0]}")/../.." + +# ---------------------------------------------------------------------------- +# Check parameters +# ---------------------------------------------------------------------------- + +readonly DEST_FORMAT="${1:-html}" +readonly OUTPUT_FILE="${2:-target/llvm-cov/report.out}" + +case "${DEST_FORMAT}" in + html|json|text|lcov|ccov) + true;; + *) + echo "ERROR: The specified target format \"${1}\" is *not* supported!" + exit 1 +esac + +# ---------------------------------------------------------------------------- +# Check prerequisites +# ---------------------------------------------------------------------------- + +if ! which cargo >/dev/null 2>&1; then + echo "ERROR: Cargo program could not be found. Please make sure that Rust/Cargo is installed!" + exit 1 +fi + +if [[ -z "$(cargo --list | grep 'llvm-cov')" ]]; then + echo "ERROR: It appears that the required command 'cargo-llvm-cov' is *not* currently installed." + echo "Please run \"cargo +stable install cargo-llvm-cov --locked\" to install!" + exit 1 +fi + +# ---------------------------------------------------------------------------- +# Instrumentation and profiling +# ---------------------------------------------------------------------------- + +cargo llvm-cov clean --workspace + +case "${DEST_FORMAT}" in + html) + true;; + *) + mkdir -p "$(dirname -- ${OUTPUT_FILE})" +esac + +case "${DEST_FORMAT}" in + html) + cargo llvm-cov --release --html;; + json) + cargo llvm-cov --release --json --output-path "${OUTPUT_FILE}";; + text) + cargo llvm-cov --release --text --output-path "${OUTPUT_FILE}";; + lcov) + cargo llvm-cov --release --lcov --output-path "${OUTPUT_FILE}";; + ccov) + cargo llvm-cov --release --codecov --output-path "${OUTPUT_FILE}";; +esac diff --git a/tools/docker/build/Makefile b/tools/docker/build/Makefile new file mode 100644 index 0000000..a654bbe --- /dev/null +++ b/tools/docker/build/Makefile @@ -0,0 +1,16 @@ +SERVICE_NAME := build + +.PHONY: all start reset clean + +all: start reset + +start: + docker compose run --build --rm $(SERVICE_NAME) + +reset: + docker compose down --remove-orphans + +clean: + docker ps --all --quiet | xargs -r docker rm --force + docker system prune --all --force + docker volume prune --force --filter all=1 diff --git a/tools/docker/build/build.Dockerfile b/tools/docker/build/build.Dockerfile new file mode 100644 index 0000000..7f30584 --- /dev/null +++ b/tools/docker/build/build.Dockerfile @@ -0,0 +1,9 @@ +# Docker file for build-env +FROM danieltrick/rust-tss2-docker:r4 + +# Create the "rebuild" command +RUN printf '#!/bin/bash -ex\ncargo clean\ncargo build "${@:2}"' > /usr/local/bin/cargo-rebuild \ + && chmod 555 /usr/local/bin/cargo-rebuild + +# Default command +CMD ["rebuild", "--release"] diff --git a/tools/docker/build/docker-compose.yml b/tools/docker/build/docker-compose.yml new file mode 100644 index 0000000..d48d583 --- /dev/null +++ b/tools/docker/build/docker-compose.yml @@ -0,0 +1,17 @@ +name: "tss2-fapi-rs-build" + +services: + build: + build: + context: ./ + dockerfile: build.Dockerfile + volumes: + - ../../../:/var/opt/rust/src:ro + - out:/var/tmp/rust:rw + - reg:/opt/rust/cargo/registry:rw + command: + ${CARGO_CMD:-rebuild --release} + +volumes: + out: + reg: diff --git a/tools/docker/swtpm/Makefile b/tools/docker/swtpm/Makefile new file mode 100644 index 0000000..c2a2c55 --- /dev/null +++ b/tools/docker/swtpm/Makefile @@ -0,0 +1,14 @@ +.PHONY: all start reset clean + +all: reset start + +start: + docker compose up --build --force-recreate $(if $(strip $(DETACH)),--detach,--abort-on-container-exit) + +reset: + docker compose down --remove-orphans + +clean: + docker ps --all --quiet | xargs -r docker rm --force + docker system prune --all --force + docker volume prune --force --filter all=1 diff --git a/tools/docker/swtpm/docker-compose.yml b/tools/docker/swtpm/docker-compose.yml new file mode 100644 index 0000000..0c2f06c --- /dev/null +++ b/tools/docker/swtpm/docker-compose.yml @@ -0,0 +1,19 @@ +name: "tss2-fapi-rs-swtpm" + +services: + swtpm: + build: + context: ./ + dockerfile: swtpm.Dockerfile + volumes: + - log:/var/log/swtpm:rw + tmpfs: + - /var/lib/swtpm + ports: + - "2321:2321" + - "2322:2322" + restart: unless-stopped + stop_signal: SIGKILL + +volumes: + log: diff --git a/tools/docker/swtpm/swtpm.Dockerfile b/tools/docker/swtpm/swtpm.Dockerfile new file mode 100644 index 0000000..8e139af --- /dev/null +++ b/tools/docker/swtpm/swtpm.Dockerfile @@ -0,0 +1,11 @@ +# Docker file for SWTPM +FROM danieltrick/swtpm-docker:r5 + +# Start SWTPM Server +ENTRYPOINT ["/usr/bin/swtpm"] +CMD ["socket", "--tpm2", \ + "--server", "port=2321,bindaddr=0.0.0.0,disconnect", \ + "--ctrl", "type=tcp,port=2322,bindaddr=0.0.0.0", \ + "--flags", "not-need-init", \ + "--tpmstate", "dir=/var/lib/swtpm", \ + "--log", "file=/var/log/swtpm/swtpm.log,level=20,truncate"] diff --git a/tools/docker/tests/Makefile b/tools/docker/tests/Makefile new file mode 100644 index 0000000..520efb0 --- /dev/null +++ b/tools/docker/tests/Makefile @@ -0,0 +1,16 @@ +SERVICE_NAME := tests + +.PHONY: all start reset clean + +all: start reset + +start: + docker compose run --build --rm $(SERVICE_NAME) + +reset: + docker compose down --remove-orphans + +clean: + docker ps --all --quiet | xargs -r docker rm --force + docker system prune --all --force + docker volume prune --force --filter all=1 diff --git a/tools/docker/tests/bin/PTM_SHUTDOWN b/tools/docker/tests/bin/PTM_SHUTDOWN new file mode 100644 index 0000000000000000000000000000000000000000..fde1ac19d2b083530bcab4cb4fd2dcaa285234ab GIT binary patch literal 4 LcmZQzU| /dev/tcp/${SWTPM_CTRL_ADDR:-127.0.0.1}/${SWTPM_CTRL_PORT:-2322} +} + +. /opt/rust/cargo/env + +for profile_name in "${@:-RSA2048SHA256}"; do + test_profile "${profile_name}" 2>&1 | tee "${logfile}.${profile_name}.log" + result=$(cat "${logfile}.${profile_name}.log" | grep -F 'test result:' | grep -Po '\d+ passed' | awk '{s+=$1}END{print s}') + printf "SUMMARY: %d tests completed successfully.\n\n" "${result}" +done diff --git a/tools/docker/tests/docker-compose.yml b/tools/docker/tests/docker-compose.yml new file mode 100644 index 0000000..e17953f --- /dev/null +++ b/tools/docker/tests/docker-compose.yml @@ -0,0 +1,56 @@ +name: "tss2-fapi-rs-tests" + +services: + tests: + networks: + tss2-fapi-rs-test-net: + ipv4_address: 10.0.0.10 + build: + context: ./ + dockerfile: tests.Dockerfile + volumes: + - ../../../:/var/opt/rust/src:ro + - log:/var/log/tss2-fapi-rs:rw + - reg:/opt/rust/cargo/registry:rw + tmpfs: + - /var/tmp/rust:exec + environment: + FAPI_RS_TEST_TCTI: "swtpm:host=10.0.0.20,port=2321" + FAPI_RS_TEST_NAME: "${TEST_NAME-}" + FAPI_RS_TEST_LOOP: "${TEST_LOOP:-5}" + RUST_LOG: "${RUST_LOG:-info}" + TSS2_LOG: "${TSS2_LOG:-all+none}" + SWTPM_CTRL_ADDR: "10.0.0.20" + SWTPM_CTRL_PORT: "2322" + TEST_KEEP_RUNNING: "${KEEP_RUNNING:-0}" + TEST_INCL_IGNORED: "${INCL_IGNORED:-0}" + command: + - "RSA2048SHA256" + - "ECCP256SHA256" + depends_on: + - swtpm + + swtpm: + networks: + tss2-fapi-rs-test-net: + ipv4_address: 10.0.0.20 + build: + context: ../swtpm + dockerfile: swtpm.Dockerfile + volumes: + - log:/var/log/swtpm:rw + tmpfs: + - /var/lib/swtpm + restart: unless-stopped + stop_signal: SIGKILL + +networks: + tss2-fapi-rs-test-net: + ipam: + driver: default + config: + - subnet: "10.0.0.0/24" + +volumes: + log: + reg: diff --git a/tools/docker/tests/tests.Dockerfile b/tools/docker/tests/tests.Dockerfile new file mode 100644 index 0000000..66b47a8 --- /dev/null +++ b/tools/docker/tests/tests.Dockerfile @@ -0,0 +1,8 @@ +# Docker file for build-env +FROM danieltrick/rust-tss2-docker:r4 + +# Copy script file +COPY bin/test-runner.sh bin/PTM_SHUTDOWN /opt/ + +# Entry point +ENTRYPOINT ["/opt/test-runner.sh"] diff --git a/tools/libtpms/test-runner.sh b/tools/libtpms/test-runner.sh new file mode 100755 index 0000000..92f2114 --- /dev/null +++ b/tools/libtpms/test-runner.sh @@ -0,0 +1,22 @@ +#/bin/bash +set -eo pipefail +cd -- "$(dirname -- "${BASH_SOURCE[0]}")/../.." + +# ---------------------------------------------------------------------------- +# Check prerequisites +# ---------------------------------------------------------------------------- + +pkg-config --cflags --libs libtpms + +pkg-config --cflags --libs tss2-tcti-libtpms + +# ---------------------------------------------------------------------------- +# Run tests using libtpms +# ---------------------------------------------------------------------------- + +readonly WORKING_DIR="$(mktemp -d)" +trap "rm -rf \"${WORKING_DIR}\"" EXIT ERR + +cargo clean + +RUST_LOG=info TSS2_LOG="all+none" FAPI_RS_TEST_DIR="${WORKING_DIR}" FAPI_RS_TEST_TCTI="libtpms:libtpms_state.dat" cargo test --release -- "${@}"