diff --git a/.github/workflows/rust-test.yml b/.github/workflows/rust-test.yml new file mode 100644 index 000000000..b1bfa3e92 --- /dev/null +++ b/.github/workflows/rust-test.yml @@ -0,0 +1,140 @@ +name: WolfTPM Rust Wrapper Tests + +on: + push: + branches: [ 'master', 'main', 'release/**' ] + pull_request: + branches: [ '**' ] + types: [opened, synchronize, reopened, ready_for_review] + repository_dispatch: + types: [nightly-trigger] + +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + +permissions: + contents: read + +jobs: + # Resolve the latest wolfSSL -stable tag so we also test the shipped release. + discover: + if: github.event_name != 'pull_request' || github.event.pull_request.draft == false + uses: ./.github/workflows/_resolve-wolfssl.yml + + rust: + if: github.event_name != 'pull_request' || github.event.pull_request.draft == false + needs: discover + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + include: + - name: master + wolfssl_ref: master + - name: latest-stable + wolfssl_ref: ${{ needs.discover.outputs.latest_stable }} + steps: + - name: Checkout wolfTPM + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + + # Build + install wolfSSL with the crypto-callback support the wrapper + # (and the TPM-backed TLS bridge) needs; installs to /usr/local so + # pkg-config finds it for both wolfTPM configure and the Rust build.rs. + - name: Setup wolfSSL + uses: ./.github/actions/setup-wolfssl + with: + ref: ${{ matrix.wolfssl_ref || 'master' }} + # fwTPM RSA object creation needs WOLFSSL_KEY_GEN and the raw-RSA + # padding path, or TPM2_Create returns TPM_RC_COMMAND_CODE for RSA + # keys (see src/fwtpm/README.md). + configure-flags: --enable-wolftpm --enable-pkcallbacks --enable-keygen + cflags: -DWC_RSA_NO_PADDING + + # Use the runner's preinstalled rustup rather than a mutable third-party + # action, so no external action code runs with the job token. + - name: Install Rust + run: | + rustup toolchain install stable --profile minimal --component clippy --component rustfmt + rustup default stable + + - name: Generate TPM port + run: | + MATRIX_HASH=$(echo -n "rust-${{ matrix.name }}" | cksum | cut -d' ' -f1) + TPM_PORT=$((40000 + (MATRIX_HASH % 1000) * 2)) + echo "TPM_PORT=$TPM_PORT" >> $GITHUB_ENV + echo "TPM2_SWTPM_PORT=$TPM_PORT" >> $GITHUB_ENV + echo "TPM2_SWTPM_HOST=localhost" >> $GITHUB_ENV + + # Build wolfTPM with our in-tree firmware TPM (fwtpm_server) as the + # software TPM the Rust tests run against — no external simulator. + - name: Build wolfTPM (swtpm + fwtpm) + run: | + ./autogen.sh + ./configure --enable-swtpm --enable-fwtpm --with-swtpm-port=$TPM_PORT + make -j"$(nproc)" + + - name: Start fwTPM server + run: | + ./src/fwtpm/fwtpm_server --clear --port "$TPM_PORT" \ + --platform-port "$((TPM_PORT + 1))" & + sleep 1 + + - name: Build Rust wrapper + working-directory: ./wrapper/rust/wolftpm + run: cargo build --all-targets --features swtpm-tests + + # Style gates are advisory on the first runs (annotate, don't fail the + # pipeline); flip continue-on-error off once a `cargo fmt` pass is landed. + - name: Clippy + working-directory: ./wrapper/rust/wolftpm + continue-on-error: true + run: cargo clippy --all-targets --features swtpm-tests -- -D warnings + + - name: Rustfmt check + working-directory: ./wrapper/rust/wolftpm + continue-on-error: true + run: cargo fmt --check + + - name: Test against fwTPM + working-directory: ./wrapper/rust/wolftpm + run: cargo test --features swtpm-tests -- --test-threads=1 + + - name: Docs + working-directory: ./wrapper/rust/wolftpm + run: cargo doc --no-deps + + # Compile-only against the Linux kernel-device transport, so the non-swtpm + # cfg paths and the callback-free open() build are exercised too. + rust-devtpm-compile: + if: github.event_name != 'pull_request' || github.event.pull_request.draft == false + runs-on: ubuntu-latest + steps: + - name: Checkout wolfTPM + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + - name: Setup wolfSSL + uses: ./.github/actions/setup-wolfssl + with: + ref: master + configure-flags: --enable-wolftpm --enable-pkcallbacks + - name: Install Rust + run: | + rustup toolchain install stable --profile minimal + rustup default stable + - name: Build wolfTPM (devtpm) + run: | + ./autogen.sh + ./configure --enable-devtpm + make -j"$(nproc)" + - name: Compile Rust wrapper (no swtpm) + working-directory: ./wrapper/rust/wolftpm + run: cargo build --all-targets + + - name: Upload failure logs + if: failure() + uses: actions/upload-artifact@v4 + with: + name: wolftpm-rust-devtpm-logs + path: | + wrapper/rust/wolftpm/target/debug/build/*/output + retention-days: 5 diff --git a/wrapper/include.am b/wrapper/include.am index 991a32acd..2975f6f76 100644 --- a/wrapper/include.am +++ b/wrapper/include.am @@ -2,6 +2,7 @@ # All paths should be given relative to the root include wrapper/CSharp/include.am +include wrapper/rust/include.am wrapperdir = $(docdir)/wrapper dist_wrapper_DATA= wrapper/wolfTPM-csharp.sln diff --git a/wrapper/rust/README.md b/wrapper/rust/README.md new file mode 100644 index 000000000..3227e1f00 --- /dev/null +++ b/wrapper/rust/README.md @@ -0,0 +1,17 @@ +# wolfTPM Rust wrapper + +Official Rust bindings for wolfTPM. The crate lives in [`wolftpm/`](wolftpm/); +see its [README](wolftpm/README.md) for the full API, build, and test details. + +```sh +# 1. build the C library first (with a software TPM for testing) +cd ../.. # wolfTPM repo root +./autogen.sh && ./configure --enable-swtpm --enable-fwtpm && make + +# 2. build the Rust crate against it +cd wrapper/rust/wolftpm +cargo build + +# 3. test (needs a running software TPM on localhost:2321) +cargo test --features swtpm-tests -- --test-threads=1 +``` diff --git a/wrapper/rust/include.am b/wrapper/rust/include.am new file mode 100644 index 000000000..320d8d1ff --- /dev/null +++ b/wrapper/rust/include.am @@ -0,0 +1,48 @@ +# vim:ft=automake +# included from wrapper/include.am +# All paths should be given relative to the root + +EXTRA_DIST += wrapper/rust/README.md +EXTRA_DIST += wrapper/rust/wolftpm/Cargo.toml +EXTRA_DIST += wrapper/rust/wolftpm/README.md +EXTRA_DIST += wrapper/rust/wolftpm/build.rs +EXTRA_DIST += wrapper/rust/wolftpm/headers.h +EXTRA_DIST += wrapper/rust/wolftpm/src/lib.rs +EXTRA_DIST += wrapper/rust/wolftpm/src/sys.rs +EXTRA_DIST += wrapper/rust/wolftpm/src/device.rs +EXTRA_DIST += wrapper/rust/wolftpm/src/key.rs +EXTRA_DIST += wrapper/rust/wolftpm/src/sign.rs +EXTRA_DIST += wrapper/rust/wolftpm/src/seal.rs +EXTRA_DIST += wrapper/rust/wolftpm/src/nv.rs +EXTRA_DIST += wrapper/rust/wolftpm/src/pcr.rs +EXTRA_DIST += wrapper/rust/wolftpm/src/certify.rs +EXTRA_DIST += wrapper/rust/wolftpm/src/rsa.rs +EXTRA_DIST += wrapper/rust/wolftpm/src/persist.rs +EXTRA_DIST += wrapper/rust/wolftpm/src/hmac.rs +EXTRA_DIST += wrapper/rust/wolftpm/src/session.rs +EXTRA_DIST += wrapper/rust/wolftpm/src/caps.rs +EXTRA_DIST += wrapper/rust/wolftpm/src/symmetric.rs +EXTRA_DIST += wrapper/rust/wolftpm/src/ecdh.rs +EXTRA_DIST += wrapper/rust/wolftpm/src/credential.rs +EXTRA_DIST += wrapper/rust/wolftpm/examples/create_primary.rs +EXTRA_DIST += wrapper/rust/wolftpm/examples/full_flow.rs +EXTRA_DIST += wrapper/rust/wolftpm/tests/smoke.rs +EXTRA_DIST += wrapper/rust/wolftpm/tests/common/mod.rs +EXTRA_DIST += wrapper/rust/wolftpm/tests/keys.rs +EXTRA_DIST += wrapper/rust/wolftpm/tests/sign.rs +EXTRA_DIST += wrapper/rust/wolftpm/tests/seal.rs +EXTRA_DIST += wrapper/rust/wolftpm/tests/nv.rs +EXTRA_DIST += wrapper/rust/wolftpm/tests/pcr.rs +EXTRA_DIST += wrapper/rust/wolftpm/tests/certify.rs +EXTRA_DIST += wrapper/rust/wolftpm/tests/ek.rs +EXTRA_DIST += wrapper/rust/wolftpm/tests/persist.rs +EXTRA_DIST += wrapper/rust/wolftpm/tests/rsa.rs +EXTRA_DIST += wrapper/rust/wolftpm/tests/hmac.rs +EXTRA_DIST += wrapper/rust/wolftpm/tests/seal_pcr.rs +EXTRA_DIST += wrapper/rust/wolftpm/tests/session.rs +EXTRA_DIST += wrapper/rust/wolftpm/tests/caps.rs +EXTRA_DIST += wrapper/rust/wolftpm/tests/quote.rs +EXTRA_DIST += wrapper/rust/wolftpm/tests/credential.rs +EXTRA_DIST += wrapper/rust/wolftpm/tests/ecdh.rs +EXTRA_DIST += wrapper/rust/wolftpm/tests/symmetric.rs +EXTRA_DIST += wrapper/rust/wolftpm/tests/import.rs diff --git a/wrapper/rust/wolftpm/Cargo.toml b/wrapper/rust/wolftpm/Cargo.toml new file mode 100644 index 000000000..8fed17528 --- /dev/null +++ b/wrapper/rust/wolftpm/Cargo.toml @@ -0,0 +1,20 @@ +[package] +name = "wolftpm" +version = "0.4.0" +edition = "2021" +description = "Rust wrapper for the wolfTPM TPM 2.0 library" +license = "GPL-3.0-or-later" +repository = "https://github.com/wolfSSL/wolfTPM" +documentation = "https://github.com/wolfSSL/wolfTPM/tree/master/wrapper/rust" +keywords = ["wolftpm", "tpm", "tpm2", "security", "api-bindings"] +categories = ["cryptography", "hardware-support", "api-bindings"] +readme = "README.md" + +[features] +default = [] +# Gate the socket integration tests that need a running swtpm/fwtpm server. +swtpm-tests = [] + +[build-dependencies] +bindgen = "0.72" +regex = "1.5" diff --git a/wrapper/rust/wolftpm/README.md b/wrapper/rust/wolftpm/README.md new file mode 100644 index 000000000..d05bfa32d --- /dev/null +++ b/wrapper/rust/wolftpm/README.md @@ -0,0 +1,257 @@ +# wolftpm + +Safe Rust bindings for wolfTPM, the portable TPM 2.0 library. + +The raw FFI is generated with bindgen and kept in the `sys` module. The rest of +the crate is a safe API: functions return `Result`, TPM handles are released when +they go out of scope, and all `unsafe` stays inside the crate. + +## Requirements + +- Rust and Cargo, stable, rustc 1.80 or newer. +- A built wolfTPM C library (`libwolftpm`) and its wolfSSL dependency + (`libwolfssl`). The crate links a prebuilt library. It does not build the C + code itself. +- For the tests, a software TPM. wolfTPM's own `fwtpm_server` works or any other + TPM emulator/software TPM. + +## Step 1: build the C library + +Run these from the wolfTPM repository root: + + ./autogen.sh + ./configure --enable-swtpm --enable-fwtpm + make + +This produces `src/.libs/libwolftpm`. With `--enable-fwtpm` it also builds the +software TPM server at `src/fwtpm/fwtpm_server`. + +## Step 2: build the crate + + cd wrapper/rust/wolftpm + cargo build + +The build finds the libraries in this order: + +1. If `WOLFTPM_PREFIX` or `WOLFSSL_PREFIX` are set, it uses `$PREFIX/include` and + `$PREFIX/lib` of an installed copy. +2. Otherwise it uses the in-tree build: headers from the repo root, libraries + from `src/.libs`. wolfSSL is located with `pkg-config`, then a local + `./wolfssl` or a sibling `../wolfssl` checkout. + +Shared libraries are preferred. Static is used when no shared library is present. + +## Step 3: run the tests + +The integration tests talk to a software TPM over a socket. They are behind the +`swtpm-tests` feature, so a plain `cargo test` does not need a running server. + +Start a server on port 2321 (from the repo root): + + ./src/fwtpm/fwtpm_server --clear --port 2321 --platform-port 2322 & + +Run the tests against it: + + cd wrapper/rust/wolftpm + TPM2_SWTPM_HOST=localhost TPM2_SWTPM_PORT=2321 \ + cargo test --features swtpm-tests -- --test-threads=1 + +Use `--test-threads=1` because the software TPM serves one client at a time. + +Run a single test file, for example the signing tests: + + cargo test --features swtpm-tests --test sign -- --test-threads=1 + +The default host and port are `localhost:2321`, so the environment variables can +be omitted when the server is on that address. + +## What the tests check + +Each file under `tests/` exercises one area against the software TPM: + +- `tests/smoke.rs`: random bytes differ across draws, and a primary key loads. +- `tests/keys.rs`: create and load a child key, blob round-trip, short-auth blob + round-trip, and an auth-protected parent. +- `tests/sign.rs`: sign a digest and verify it, and reject a tampered signature. +- `tests/seal.rs`: seal a secret and unseal it, and fail with the wrong auth. +- `tests/seal_pcr.rs`: PCR-bound seal/unseal, unseal fails after a PCR changes, + and an invalid PCR selection is rejected. +- `tests/nv.rs`: define an NV index, write and read it, then delete it. +- `tests/pcr.rs`: read a PCR, extend it, and confirm the value changed. +- `tests/certify.rs`: an attestation key (ECC and RSA) certifies another key. +- `tests/quote.rs`: quote PCRs with an ECC and RSA AIK, and reject a bad + PCR selection. +- `tests/credential.rs`: MakeCredential then ActivateCredential round-trip. +- `tests/ek.rs`: create the endorsement key and export its public part. +- `tests/persist.rs`: persist a key, read it back, and evict it. +- `tests/rsa.rs`: RSA-OAEP encrypt/decrypt, and an explicit OAEP-SHA1 round-trip. +- `tests/hmac.rs`: raw-key HMAC and a TPM-resident keyed-hash key HMAC. +- `tests/caps.rs`: self-test and capability query. +- `tests/ecdh.rs`: ECDH generate then recover the same shared secret. +- `tests/symmetric.rs`: AES-CFB encrypt/decrypt round-trip. +- `tests/import.rs`: import an external RSA and ECC private key. + +## Example test output + + running 2 tests + test certify_with_ecc_aik ... ok + test certify_with_rsa_aik ... ok + test result: ok. 2 passed; 0 failed; 0 ignored + + running 4 tests + test create_and_load_child ... ok + test key_blob_roundtrip_then_load ... ok + test key_blob_roundtrip_preserves_short_auth ... ok + test auth_protected_parent_loads_child ... ok + test result: ok. 4 passed; 0 failed; 0 ignored + + running 2 tests + test sign_then_verify ... ok + test verify_rejects_tampered_signature ... ok + test result: ok. 2 passed; 0 failed; 0 ignored + + running 2 tests + test rsa_oaep_roundtrip ... ok + test rsa_oaep_sha1_roundtrip ... ok + test result: ok. 2 passed; 0 failed; 0 ignored + + running 1 test + test make_and_activate_credential ... ok + test result: ok. 1 passed; 0 failed; 0 ignored + +## Step 4: run the examples + +There are two examples. The first is minimal: + + cargo run --example create_primary + +It opens the software TPM, reads random bytes, and creates an RSA and an ECC +storage root key. + +The second runs the whole API in one pass: + + cargo run --example full_flow + +Expected output. The random bytes and handle values differ between runs: + + device connected to software TPM + get_random a91b37b1838d002453f1632ba2c0adbc + create_primary ECC SRK handle 0x80000000 + create_and_load signing key handle 0x80000001 + sign/verify 64 byte signature, verified + seal/unseal recovered "my secret" + key blob 257 bytes, reloaded as handle 0x80000002 + pcr read/extend PCR16 000000000000.. -> debb3e7acfff.. + nv define/rw 32 bytes at index 0x01500100 + certify 157 byte attestation + done all operations succeeded + +If the wolfTPM C library was built with debug output, you will also see verbose +TPM2_* traces from the library. A normal build prints only the lines above. + +## Using the wrapper + +```rust +use wolftpm::{Device, HashAlg, Hierarchy, KeyAlg, KeyBlob, Template}; + +fn main() -> Result<(), wolftpm::TpmError> { + // Connect to a software TPM. Use Device::open() for the platform default. + let dev = Device::open_swtpm()?; + + // Random bytes from the TPM. + let mut nonce = [0u8; 32]; + dev.get_random(&mut nonce)?; + + // Storage root key under the owner hierarchy. + let srk = dev.create_primary(Hierarchy::Owner, KeyAlg::EccP256, None)?; + + // Signing key under the SRK, then sign and verify a digest. + let signer = dev.create_and_load(&srk, &Template::signing(KeyAlg::EccP256)?, None)?; + let digest = [0x11u8; 32]; + let sig = signer.sign_hash(&digest)?; + signer.verify_hash(&digest, &sig)?; + + // Seal a secret to the TPM and read it back. + let sealed = dev.seal(&srk, b"my secret", None)?; + let _secret = dev.unseal(sealed, &srk, None)?; + + // Persist a key as bytes, then load it again. Scope the reloaded key so it + // releases its TPM handle before more transient objects are created below + // (many TPMs allow only three transient objects at once). + let bytes = { + let blob = dev.create_key(&srk, &Template::signing(KeyAlg::EccP256)?, None)?; + blob.to_bytes()? + }; + { + let restored = KeyBlob::from_bytes(&dev, &bytes)?; + let _loaded = restored.load(&srk, None)?; + } + + // Read and extend a PCR. + let _value = dev.pcr_read(16, HashAlg::Sha256)?; + dev.pcr_extend(16, HashAlg::Sha256, &[0xAB; 32])?; + + // Define, write, read, and delete an NV index. + let mut slot = dev.nv_create(0x0150_0100, 32, None)?; + dev.nv_write(&mut slot, b"metadata", 0)?; + let mut buf = [0u8; 32]; + dev.nv_read(&mut slot, &mut buf, 0)?; + dev.nv_delete(0x0150_0100)?; + + // Attest that a key lives in this TPM, signed by an attestation key. + let aik = dev.create_and_load(&srk, &Template::attestation(KeyAlg::EccP256)?, None)?; + let _attestation = dev.certify(&signer, &aik, &nonce)?; + + Ok(()) +} +``` + +Keys and the device release their TPM handles automatically when they drop. + +## What the crate covers + +- Device open and cleanup, TPM random numbers, self-test, and capability query. +- Primary and child keys. Templates for storage, signing, attestation, EK, RSA + decrypt, keyed-hash HMAC, symmetric AES, and ECDH keys. +- Key blob serialize and load for persistence; external RSA and ECC key import. +- Persistent key handles: store, read back, and evict. +- Sign and verify. +- RSA-OAEP encrypt and decrypt, including an explicit label hash (SHA-1 for + Microsoft enrollment interop). +- Symmetric AES-CFB encrypt and decrypt. +- ECDH key agreement. +- HMAC, both raw-key and with a TPM-resident keyed-hash key. +- Seal and unseal, plain and bound to a PCR policy. +- PCR read and extend. +- NV define, write, read, delete, and certificate read (EK certificate). +- Attestation: certify a key, and quote PCRs. +- Credential activation: MakeCredential and ActivateCredential. + +Recovered secrets (unseal, RSA decrypt, ECDH, AES decrypt, credential +activation) are returned in a `Secret` that zeroizes its buffer on drop. + +## Transport security + +For confidentiality on the TPM transport, start a parameter-encryption session +with [`Device::start_encrypted_session`] before the secret-bearing operations: + +```rust +let srk = dev.create_primary(Hierarchy::Owner, KeyAlg::EccP256, None)?; +let _session = dev.start_encrypted_session(&srk)?; // salted HMAC + AES-CFB +let sealed = dev.seal(&srk, b"secret", None)?; // command param encrypted +let plain = dev.unseal(sealed, &srk, None)?; // response param encrypted +``` + +While the session is alive its salted HMAC session occupies auth slot 1, so +wolfTPM encrypts the sensitive command and response parameters of seal/unseal, +RSA and AES encrypt/decrypt, HMAC, NV, ECDH, and key create/load. Only one +session is allowed at a time. The attestation commands (certify, quote, +activate_credential) need the same auth slot and are refused while a session is +active; drop the session before calling them. Without a session, parameters +cross the transport in the clear, so either use a session or run over a trusted +local transport (the Linux kernel device or a local socket) rather than a remote +`TPM2_SWTPM_HOST` or an observable physical bus. + +## License + +GPLv3, or a commercial wolfSSL license, matching wolfTPM. diff --git a/wrapper/rust/wolftpm/build.rs b/wrapper/rust/wolftpm/build.rs new file mode 100644 index 000000000..909fcecc7 --- /dev/null +++ b/wrapper/rust/wolftpm/build.rs @@ -0,0 +1,372 @@ +//! Build script for the `wolftpm` crate. +//! +//! Mirrors the official `wolfssl-wolfcrypt` build.rs: it does NOT build the C +//! library, it links a pre-built one. Order of operations: +//! 1. bindgen over `headers.h` -> $OUT_DIR/bindings.rs +//! 2. link libwolftpm (+ its libwolfssl dependency) +//! 3. scan_cfg: emit cfgs for what the C library was actually built with + +extern crate bindgen; + +use regex::Regex; +use std::env; +use std::fs; +use std::io::{self, Read, Result}; +use std::path::{Path, PathBuf}; + +fn main() { + if let Err(e) = run_build() { + eprintln!("Build failed: {}", e); + std::process::exit(1); + } +} + +fn run_build() -> Result<()> { + println!("cargo:rerun-if-env-changed=WOLFTPM_PREFIX"); + println!("cargo:rerun-if-env-changed=WOLFSSL_PREFIX"); + generate_bindings()?; + setup_link()?; + scan_cfg()?; + scan_options()?; + Ok(()) +} + +fn crate_dir() -> Result { + Ok(env::current_dir()?.display().to_string()) +} + +/// wolfTPM repo root, assuming this crate lives at `wrapper/rust/wolftpm`. +fn wolftpm_repo_base_dir() -> Result { + Ok(format!("{}/../../..", crate_dir()?)) +} + +fn wolftpm_repo_lib_dir() -> Result { + Ok(format!("{}/src/.libs", wolftpm_repo_base_dir()?)) +} + +/// Read and validate a `*_PREFIX` env var. +fn user_prefix(var: &str) -> Option { + match env::var(var) { + Ok(prefix) if !prefix.is_empty() && !prefix.contains('\n') => Some(prefix), + Ok(_) => { + println!("cargo:warning=ignoring {}", var); + None + } + Err(_) => None, + } +} + +/// Include dir holding `wolftpm/options.h`. `WOLFTPM_PREFIX/include`, else the +/// in-tree repo root when configured. +fn wolftpm_include_dir() -> Result> { + if let Some(prefix) = user_prefix("WOLFTPM_PREFIX") { + let inc = format!("{}/include", prefix); + if Path::new(&inc).join("wolftpm").is_dir() { + return Ok(Some(inc)); + } + return Err(io::Error::other(format!( + "WOLFTPM_PREFIX is set but {}/wolftpm is missing", + inc + ))); + } + let base = wolftpm_repo_base_dir()?; + if Path::new(&base).join("wolftpm/options.h").is_file() { + Ok(Some(base)) + } else { + Ok(None) + } +} + +/// Include dir holding `wolfssl/options.h`. `WOLFSSL_PREFIX/include`, else a +/// sibling `../wolfssl` source checkout when present. +fn wolfssl_include_dir() -> Result> { + if let Some(prefix) = user_prefix("WOLFSSL_PREFIX") { + let inc = format!("{}/include", prefix); + if Path::new(&inc).join("wolfssl").is_dir() { + return Ok(Some(inc)); + } + return Err(io::Error::other(format!( + "WOLFSSL_PREFIX is set but {}/wolfssl is missing", + inc + ))); + } + if let Some(inc) = pkg_config_var("--variable=includedir") { + if Path::new(&inc).join("wolfssl").is_dir() { + return Ok(Some(inc)); + } + } + // in-repo `./wolfssl` (the wolfTPM CI layout) then a sibling checkout + let base = wolftpm_repo_base_dir()?; + for cand in [format!("{}/wolfssl", base), format!("{}/../wolfssl", base)] { + if Path::new(&cand).join("wolfssl/options.h").is_file() { + return Ok(Some(cand)); + } + } + Ok(None) +} + +/// Library dir for libwolfssl. `WOLFSSL_PREFIX/lib`, else pkg-config's libdir, +/// else a sibling `../wolfssl` build output. Prefer the installed copy that +/// libwolftpm was actually linked against over a feature-reduced checkout. +fn wolfssl_lib_dir() -> Result> { + if let Some(prefix) = user_prefix("WOLFSSL_PREFIX") { + let dir = format!("{}/lib", prefix); + if Path::new(&dir).is_dir() { + return Ok(Some(dir)); + } + return Err(io::Error::other(format!( + "WOLFSSL_PREFIX is set but {} is missing", + dir + ))); + } + if let Some(dir) = pkg_config_var("--variable=libdir") { + if Path::new(&dir).exists() { + return Ok(Some(dir)); + } + } + let base = wolftpm_repo_base_dir()?; + for cand in [ + format!("{}/wolfssl/src/.libs", base), + format!("{}/../wolfssl/src/.libs", base), + ] { + if Path::new(&cand).exists() { + return Ok(Some(cand)); + } + } + Ok(None) +} + +/// Query a pkg-config variable for wolfssl, without a build dependency. +fn pkg_config_var(flag: &str) -> Option { + let out = std::process::Command::new("pkg-config") + .args([flag, "wolfssl"]) + .output() + .ok()?; + if !out.status.success() { + return None; + } + let s = String::from_utf8_lossy(&out.stdout).trim().to_string(); + if s.is_empty() { + None + } else { + Some(s) + } +} + +fn lib_dir(prefix_var: &str, in_tree: Option) -> Result> { + if let Some(prefix) = user_prefix(prefix_var) { + let dir = format!("{}/lib", prefix); + if Path::new(&dir).is_dir() { + return Ok(Some(dir)); + } + return Err(io::Error::other(format!( + "{} is set but {} is missing", + prefix_var, dir + ))); + } + match in_tree { + Some(dir) if Path::new(&dir).exists() => Ok(Some(dir)), + _ => Ok(None), + } +} + +fn bindings_path() -> String { + PathBuf::from(env::var("OUT_DIR").unwrap()) + .join("bindings.rs") + .display() + .to_string() +} + +fn generate_bindings() -> Result<()> { + let mut builder = bindgen::Builder::default() + .header("headers.h") + // The raw FFI layer needs neither libc decls nor the C doxygen text; + // both only produce warnings (libc memcpy/etc. trip the runtime-symbol + // lint, and doxygen [in,out]/[0] markers become broken rustdoc links). + .blocklist_function("memcpy") + .blocklist_function("memmove") + .blocklist_function("memset") + .blocklist_function("memcmp") + .blocklist_function("bcmp") + .blocklist_function("strlen") + .generate_comments(false) + .parse_callbacks(Box::new(bindgen::CargoCallbacks::new())); + + // Preprocess for the actual target, not the build host: wolfTPM headers + // select struct fields and transports on __linux__/_WIN32/arch macros, so a + // host/target mismatch would generate the wrong FFI layout when cross-compiling. + if let Ok(target) = env::var("TARGET") { + builder = builder.clang_arg(format!("--target={}", target)); + } + + if let Some(inc) = wolftpm_include_dir()? { + builder = builder.clang_arg(format!("-I{}", inc)); + } + if let Some(inc) = wolfssl_include_dir()? { + builder = builder.clang_arg(format!("-I{}", inc)); + } + + let bindings = builder + .generate() + .map_err(|_| io::Error::other("Failed to generate bindings"))?; + bindings + .write_to_file(bindings_path()) + .map_err(|e| io::Error::other(format!("Couldn't write bindings: {}", e))) +} + +/// Emit link directives for one library, preferring shared then static. +fn link_one(name: &str, dir: &Option) { + let target = env::var("TARGET").unwrap(); + let is_windows = target.contains("windows"); + if let Some(dir) = dir { + println!("cargo:rustc-link-search={}", dir); + let p = Path::new(dir); + let shared = p.join(format!("lib{}.so", name)).exists() + || p.join(format!("lib{}.dylib", name)).exists() + || p.join(format!("{}.dll", name)).exists() + || p.join(format!("{}.lib", name)).exists(); + if shared { + println!("cargo:rustc-link-lib={}", name); + // rpath is a GNU ld concept; skip it for MSVC and bare-metal targets. + if !is_windows && !target.ends_with("-none-elf") { + println!("cargo:rustc-link-arg=-Wl,-rpath,{}", dir); + } + } else { + println!("cargo:rustc-link-lib=static={}", name); + } + } else { + println!("cargo:rustc-link-lib={}", name); + } +} + +/// Whether a directory actually holds a `wolftpm` library to link, across the +/// Unix (`lib` prefix) and Windows (MSVC/MinGW) naming conventions. +fn has_wolftpm_lib(dir: &str) -> bool { + let p = Path::new(dir); + [ + "libwolftpm.so", + "libwolftpm.a", + "libwolftpm.dylib", + "wolftpm.lib", // MSVC static / import library + "wolftpm.dll", // MSVC shared + "libwolftpm.dll.a", // MinGW import library + ] + .iter() + .any(|name| p.join(name).exists()) +} + +fn setup_link() -> Result<()> { + let wolftpm_libs = lib_dir("WOLFTPM_PREFIX", Some(wolftpm_repo_lib_dir()?))?; + // Fail closed: the bindings were generated from a specific wolftpm/options.h, + // and wolfTPM struct layouts are configuration dependent (e.g. WOLFTPM_SPDM + // grows WOLFTPM2_DEV). Require an actual libwolftpm in the resolved directory + // rather than falling back to a bare `-l wolftpm` that could pull in a + // system library built with a different configuration than the headers. + match &wolftpm_libs { + Some(dir) if has_wolftpm_lib(dir) => {} + _ => { + return Err(io::Error::other(format!( + "no libwolftpm to link ({}); build wolfTPM in-tree (src/.libs) or set \ + WOLFTPM_PREFIX to an install whose headers match the library", + wolftpm_libs.as_deref().unwrap_or("in-tree src/.libs missing"), + ))); + } + } + let wolfssl_libs = wolfssl_lib_dir()?; + link_one("wolftpm", &wolftpm_libs); + link_one("wolfssl", &wolfssl_libs); + Ok(()) +} + +fn read_file(path: String) -> Result { + let mut file = fs::File::open(path)?; + let mut content = String::new(); + file.read_to_string(&mut content)?; + Ok(content) +} + +fn check_cfg(binding: &str, symbol: &str, cfg_name: &str) -> bool { + let re = Regex::new(&format!(r"\b{}\b", regex::escape(symbol))).unwrap(); + println!("cargo::rustc-check-cfg=cfg({})", cfg_name); + if re.is_match(binding) { + println!("cargo:rustc-cfg={}", cfg_name); + true + } else { + false + } +} + +/// Emit cfgs describing how the linked libwolftpm was actually built, so the +/// safe modules can gate on real availability rather than Cargo features. +fn scan_cfg() -> Result<()> { + let binding = read_file(bindings_path())?; + + // high-level wolfTPM2 wrapper present (i.e. not WOLFTPM2_NO_WRAPPER) + check_cfg(&binding, "wolfTPM2_Init", "wrapper"); + // human-readable return-code strings + check_cfg(&binding, "TPM2_GetRCString", "rc_string"); + // optional capabilities + check_cfg(&binding, "wolfTPM2_SetCryptoDevCb", "crypto_cb"); + check_cfg(&binding, "wolfTPM2_CreateKeySeal", "seal"); + check_cfg(&binding, "wolfTPM2_NVCreateAuth", "nv"); + check_cfg(&binding, "wolfTPM2_ReadPCR", "pcr"); + check_cfg(&binding, "wolfTPM2_GetRandom", "rng"); + check_cfg(&binding, "wolfTPM2_RsaEncrypt", "rsa"); + check_cfg(&binding, "wolfTPM2_NVStoreKey", "persist"); + check_cfg(&binding, "wolfTPM2_ExportPublicKeyBuffer", "pubexport"); + check_cfg(&binding, "wolfTPM2_HmacStart", "hmac"); + // self-test + capability query + check_cfg(&binding, "wolfTPM2_GetCapabilities", "caps"); + // TPM-resident keyed-hash HMAC key (create/load-once) + check_cfg(&binding, "wolfTPM2_GetKeyTemplate_KeyedHash", "keyedhash"); + // symmetric AES encrypt/decrypt + check_cfg(&binding, "wolfTPM2_EncryptDecrypt", "symmetric"); + // ECDH key agreement + check_cfg(&binding, "wolfTPM2_ECDHGen", "ecdh"); + // external RSA/ECC private-key import + check_cfg(&binding, "wolfTPM2_ImportRsaPrivateKey", "import"); + // certificate read from an NV index (EK cert) + check_cfg(&binding, "wolfTPM2_NVReadCert", "nvcert"); + // EK policy session for credential activation + check_cfg(&binding, "wolfTPM2_CreateAuthSession_EkPolicy", "ek_policy"); + + Ok(()) +} + +/// Emit cfgs for build-flag macros that are `#define`-only (no bindable +/// symbol), read straight from the linked library's `wolftpm/options.h`. +fn scan_options() -> Result<()> { + let inc = match wolftpm_include_dir()? { + Some(d) => d, + None => return Ok(()), + }; + let text = fs::read_to_string(format!("{}/wolftpm/options.h", inc)).unwrap_or_default(); + + let flag = |macro_name: &str, cfg_name: &str| { + println!("cargo::rustc-check-cfg=cfg({})", cfg_name); + let re = + Regex::new(&format!(r"(?m)^\s*#\s*define\s+{}\b", regex::escape(macro_name))).unwrap(); + if re.is_match(&text) { + println!("cargo:rustc-cfg={}", cfg_name); + } + }; + flag("WOLFTPM_SWTPM", "swtpm"); + flag("WOLFTPM_LINUX_DEV", "devtpm"); + flag("WOLFTPM_MMIO", "mmio"); + flag("WOLFTPM_FWTPM", "fwtpm"); + // Callback-free transports: Windows TBS and Linux kernel-device autodetect. + flag("WOLFTPM_WINAPI", "winapi"); + flag("WOLFTPM_LINUX_DEV_AUTODETECT", "linux_autodetect"); + // Autoconf's --enable-autodetect records WOLFTPM_AUTODETECT; on a Linux + // target the headers derive WOLFTPM_LINUX_DEV_AUTODETECT (a callback-free + // kernel-device transport), so treat that as linux_autodetect too. On + // non-Linux targets WOLFTPM_AUTODETECT is the SPI/I2C HAL autodetect, which + // needs a callback and must not enable the callback-free open(). + if env::var("TARGET").unwrap_or_default().contains("linux") { + let re = Regex::new(r"(?m)^\s*#\s*define\s+WOLFTPM_AUTODETECT\b").unwrap(); + if re.is_match(&text) { + println!("cargo:rustc-cfg=linux_autodetect"); + } + } + Ok(()) +} diff --git a/wrapper/rust/wolftpm/examples/create_primary.rs b/wrapper/rust/wolftpm/examples/create_primary.rs new file mode 100644 index 000000000..e60a6bc27 --- /dev/null +++ b/wrapper/rust/wolftpm/examples/create_primary.rs @@ -0,0 +1,25 @@ +//! Rust analog of wolfTPM's `examples/keygen/create_primary` against a software +//! TPM. Start a server first, e.g. `ibmswtpm2/src/tpm_server` on :2321. + +use wolftpm::{Device, Hierarchy, KeyAlg}; + +fn hex(b: &[u8]) -> String { + b.iter().map(|x| format!("{:02x}", x)).collect() +} + +fn main() -> Result<(), Box> { + let dev = Device::open()?; + println!("wolfTPM initialized"); + + let mut r = [0u8; 16]; + dev.get_random(&mut r)?; + println!("GetRandom: {}", hex(&r)); + + let ecc = dev.create_primary(Hierarchy::Owner, KeyAlg::EccP256, None)?; + println!("ECC SRK handle: 0x{:08x}", ecc.handle()); + + let rsa = dev.create_primary(Hierarchy::Owner, KeyAlg::Rsa, None)?; + println!("RSA SRK handle: 0x{:08x}", rsa.handle()); + + Ok(()) +} diff --git a/wrapper/rust/wolftpm/examples/full_flow.rs b/wrapper/rust/wolftpm/examples/full_flow.rs new file mode 100644 index 000000000..3a6cd2de4 --- /dev/null +++ b/wrapper/rust/wolftpm/examples/full_flow.rs @@ -0,0 +1,79 @@ +//! Full walkthrough of the wolftpm safe API against a software TPM. +//! +//! Start a server first, for example from the repo root: +//! ./src/fwtpm/fwtpm_server --clear --port 2321 --platform-port 2322 & +//! then: +//! cargo run --example full_flow + +use wolftpm::{Device, HashAlg, Hierarchy, KeyAlg, KeyBlob, Template}; + +fn hex(b: &[u8]) -> String { + b.iter().map(|x| format!("{:02x}", x)).collect() +} + +fn main() -> Result<(), wolftpm::TpmError> { + let dev = Device::open()?; + println!("device connected to TPM"); + + let mut nonce = [0u8; 16]; + dev.get_random(&mut nonce)?; + println!("get_random {}", hex(&nonce)); + + let srk = dev.create_primary(Hierarchy::Owner, KeyAlg::EccP256, None)?; + println!("create_primary ECC SRK handle 0x{:08x}", srk.handle()); + + let signer = dev.create_and_load(&srk, &Template::signing(KeyAlg::EccP256)?, None)?; + println!("create_and_load signing key handle 0x{:08x}", signer.handle()); + + let digest = [0x11u8; 32]; + let sig = signer.sign_hash(&digest)?; + signer.verify_hash(&digest, &sig)?; + println!("sign/verify {} byte signature, verified", sig.len()); + + let sealed = dev.seal(&srk, b"my secret", None)?; + let secret = dev.unseal(sealed, &srk, None)?; + println!( + "seal/unseal recovered \"{}\"", + core::str::from_utf8(&secret).unwrap_or("") + ); + + // Scope the reloaded key so its handle is freed before the attestation + // step below. A TPM only holds a few transient objects at once (often 3), + // so keep the number of simultaneously-loaded keys small. + { + let blob = dev.create_key(&srk, &Template::signing(KeyAlg::EccP256)?, None)?; + let bytes = blob.to_bytes()?; + let restored = KeyBlob::from_bytes(&dev, &bytes)?; + let loaded = restored.load(&srk, None)?; + println!( + "key blob {} bytes, reloaded as handle 0x{:08x}", + bytes.len(), + loaded.handle() + ); + } + + let before = dev.pcr_read(16, HashAlg::Sha256)?; + dev.pcr_extend(16, HashAlg::Sha256, &[0xAB; 32])?; + let after = dev.pcr_read(16, HashAlg::Sha256)?; + println!( + "pcr read/extend PCR16 {}.. -> {}..", + hex(&before[..6]), + hex(&after[..6]) + ); + + let index = 0x0150_0100; + let _ = dev.nv_delete(index); + let mut slot = dev.nv_create(index, 32, None)?; + dev.nv_write(&mut slot, b"metadata", 0)?; + let mut buf = [0u8; 32]; + let n = dev.nv_read(&mut slot, &mut buf, 0)?; + dev.nv_delete(index)?; + println!("nv define/rw {} bytes at index 0x{:08x}", n, index); + + let aik = dev.create_and_load(&srk, &Template::attestation(KeyAlg::EccP256)?, None)?; + let att = dev.certify(&signer, &aik, &nonce)?; + println!("certify {} byte attestation", att.attest.len()); + + println!("done all operations succeeded"); + Ok(()) +} diff --git a/wrapper/rust/wolftpm/headers.h b/wrapper/rust/wolftpm/headers.h new file mode 100644 index 000000000..2340758b0 --- /dev/null +++ b/wrapper/rust/wolftpm/headers.h @@ -0,0 +1,4 @@ +/* bindgen shim: the single translation unit fed to bindgen. + * tpm2_wrap.h pulls in tpm2.h -> tpm2_types.h, which is the type-heavy header. */ +#include +#include diff --git a/wrapper/rust/wolftpm/src/caps.rs b/wrapper/rust/wolftpm/src/caps.rs new file mode 100644 index 000000000..2ec365fe3 --- /dev/null +++ b/wrapper/rust/wolftpm/src/caps.rs @@ -0,0 +1,62 @@ +//! TPM self-test and capability discovery. + +use crate::device::Device; +use crate::{check_rc, sys, Result}; + +/// A subset of the TPM's reported capabilities. +#[derive(Clone, Debug, Default)] +pub struct Caps { + /// Manufacturer id string (e.g. `"IBM"`, `"STM"`, `"NTC"`). + pub manufacturer: String, + /// Vendor detail string. + pub vendor: String, + /// Firmware version, major and minor. + pub fw_version_major: u16, + pub fw_version_minor: u16, + /// TPM operating in a FIPS 140-2 validated mode. + pub fips_140_2: bool, + /// TPM operating in a FIPS 140-3 validated mode. + pub fips_140_3: bool, + /// Common Criteria EAL4+ certified. + pub cc_eal4: bool, +} + +/// Read a fixed-size C `char` array up to its first NUL into a `String`. +fn c_str(arr: &[core::ffi::c_char]) -> String { + let bytes: Vec = arr + .iter() + .take_while(|&&c| c != 0) + .map(|&c| c as u8) + .collect(); + String::from_utf8_lossy(&bytes).into_owned() +} + +impl Device { + /// Run the TPM's self-test of all its algorithms. A common first call to + /// confirm the TPM is healthy before use. + #[cfg(caps)] + pub fn self_test(&self) -> Result<()> { + // SAFETY: self.ptr() is the pinned dev pointer for the life of this Device. + let rc = unsafe { sys::wolfTPM2_SelfTest(self.ptr()) }; + check_rc(rc) + } + + /// Query the TPM's manufacturer, firmware version, and certification flags. + #[cfg(caps)] + pub fn capabilities(&self) -> Result { + // SAFETY: WOLFTPM2_CAPS is a C POD struct; all-zero is a valid starting state for GetCapabilities to fill. + let mut caps: sys::WOLFTPM2_CAPS = unsafe { core::mem::zeroed() }; + // SAFETY: self.ptr() is live and &mut caps is a valid, exclusively-borrowed out-param. + let rc = unsafe { sys::wolfTPM2_GetCapabilities(self.ptr(), &mut caps) }; + check_rc(rc)?; + Ok(Caps { + manufacturer: c_str(&caps.mfgStr), + vendor: c_str(&caps.vendorStr), + fw_version_major: caps.fwVerMajor, + fw_version_minor: caps.fwVerMinor, + fips_140_2: caps.fips140_2() != 0, + fips_140_3: caps.fips140_3() != 0, + cc_eal4: caps.cc_eal4() != 0, + }) + } +} diff --git a/wrapper/rust/wolftpm/src/certify.rs b/wrapper/rust/wolftpm/src/certify.rs new file mode 100644 index 000000000..7f21944e4 --- /dev/null +++ b/wrapper/rust/wolftpm/src/certify.rs @@ -0,0 +1,194 @@ +//! Attestation: have an AIK certify that an object resides in this TPM. + +use crate::device::Device; +use crate::key::{HashAlg, Key}; +use crate::{check_rc, sys, Result, TpmError}; + +/// Number of PCRs a TPM 2.0 implementation exposes; valid indices are `0..24`. +const PCR_COUNT: u32 = 24; + +/// A TPM attestation: the signed `certifyInfo` (a `TPMS_ATTEST`) plus the +/// signature over it, which a verifier checks against the AIK's public key. +pub struct Attestation { + /// The raw attestation structure that was signed. + pub attest: Vec, + /// Signature algorithm (`TPM_ALG_ECDSA` or `TPM_ALG_RSASSA`). + pub sig_alg: u16, + /// The signature bytes: `R || S` for ECDSA, or the RSA signature. + pub signature: Vec, +} + +impl Device { + /// Have `signer` (an attestation key, see [`Template::attestation`](crate::Template::attestation)) + /// certify `object`, proving `object` lives in this TPM. `qualifying` is an + /// optional verifier-supplied nonce (freshness); it must fit the TPM's + /// `TPM2B_DATA` buffer. + pub fn certify( + &self, + object: &Key<'_>, + signer: &Key<'_>, + qualifying: &[u8], + ) -> Result { + // An encryption session holds auth slot 1, which certify needs for its + // signing key; refuse rather than silently disable the session. + if crate::session::is_active() { + return Err(TpmError(crate::E_SESSION_IN_USE)); + } + // `is_ecc` only selects which signature union field to read back; the + // command itself uses the signer's own configured scheme (below). + let is_ecc = signer.alg() == sys::TPM_ALG_ID_T_TPM_ALG_ECC as sys::TPM_ALG_ID; + + // SAFETY: Certify_In is a C POD struct; all-zero is a valid starting state, and the length check below bounds the copy into cin.qualifyingData.buffer. + let mut cin: sys::Certify_In = unsafe { core::mem::zeroed() }; + if qualifying.len() > cin.qualifyingData.buffer.len() { + return Err(TpmError(crate::BUFFER_E)); + } + + // Certify needs two auth slots; a one-session build (MAX_SESSION_NUM=1) + // rejects slot 1, so check both rather than submitting a half-authorized + // command. + // SAFETY: self.ptr() is live and object.handle_ptr()/signer.handle_ptr() address each Key's own pinned handle. + let (s0, s1) = unsafe { + ( + sys::wolfTPM2_SetAuthHandle(self.ptr(), 0, object.handle_ptr()), + sys::wolfTPM2_SetAuthHandle(self.ptr(), 1, signer.handle_ptr()), + ) + }; + if s0 != 0 || s1 != 0 { + // SAFETY: self.ptr() is live; unwind whichever slots were set. + unsafe { + sys::wolfTPM2_UnsetAuth(self.ptr(), 0); + sys::wolfTPM2_UnsetAuth(self.ptr(), 1); + } + return Err(TpmError(if s0 != 0 { s0 } else { s1 })); + } + cin.objectHandle = object.handle(); + cin.signHandle = signer.handle(); + // TPM_ALG_NULL: sign with the AIK's own fixed scheme and hash, which need + // not be SHA-256 for a non-default-curve key. + cin.inScheme.scheme = sys::TPM_ALG_ID_T_TPM_ALG_NULL as sys::TPMI_ALG_SIG_SCHEME; + cin.qualifyingData.size = qualifying.len() as u16; + cin.qualifyingData.buffer[..qualifying.len()].copy_from_slice(qualifying); + + // SAFETY: Certify_Out is a C POD struct; all-zero is a valid starting state for TPM2_Certify to fill. + let mut cout: sys::Certify_Out = unsafe { core::mem::zeroed() }; + // SAFETY: &mut cin/&mut cout are valid, exclusively-borrowed in/out-params for TPM2_Certify. + let rc = unsafe { sys::TPM2_Certify(&mut cin, &mut cout) }; + // SAFETY: self.ptr() is live; this clears both auth slots set above regardless of the certify outcome. + unsafe { + sys::wolfTPM2_UnsetAuth(self.ptr(), 0); + sys::wolfTPM2_UnsetAuth(self.ptr(), 1); + } + check_rc(rc)?; + + let asz = cout.certifyInfo.size as usize; + let attest = cout.certifyInfo.attestationData[..asz].to_vec(); + let signature = extract_signature(is_ecc, &cout.signature); + + Ok(Attestation { + attest, + sig_alg: cout.signature.sigAlg, + signature, + }) + } + + /// Have `signer` (an attestation key) sign a quote over the current values + /// of the PCRs in `pcr_indices` (in the `hash` bank), proving the machine's + /// measured-boot state. `qualifying` is an optional verifier nonce. This is + /// the standard remote-attestation primitive. + pub fn quote( + &self, + signer: &Key<'_>, + pcr_indices: &[u32], + hash: HashAlg, + qualifying: &[u8], + ) -> Result { + // An encryption session holds auth slot 1, which quote needs for its + // signing key; refuse rather than silently disable the session. + if crate::session::is_active() { + return Err(TpmError(crate::E_SESSION_IN_USE)); + } + // `is_ecc` only selects which signature union field to read back; the + // command uses the signer's own scheme and `hash` selects the PCR bank. + let is_ecc = signer.alg() == sys::TPM_ALG_ID_T_TPM_ALG_ECC as sys::TPM_ALG_ID; + + // Reject empty or out-of-range PCRs: TPM2_SetupPCRSel silently drops + // indices outside the implemented range, which would otherwise let the + // TPM sign a quote bound to fewer (or zero) PCRs than requested. + if pcr_indices.is_empty() || pcr_indices.iter().any(|&i| i >= PCR_COUNT) { + return Err(TpmError(crate::BUFFER_E)); + } + // SAFETY: Quote_In is a C POD struct; all-zero is a valid starting state, and the length check below bounds the copy into qin.qualifyingData.buffer. + let mut qin: sys::Quote_In = unsafe { core::mem::zeroed() }; + if qualifying.len() > qin.qualifyingData.buffer.len() { + return Err(TpmError(crate::BUFFER_E)); + } + for &idx in pcr_indices { + // SAFETY: &mut qin.PCRselect is a valid, exclusively-borrowed field of the live qin struct. + unsafe { sys::TPM2_SetupPCRSel(&mut qin.PCRselect, hash.alg_id(), idx as core::ffi::c_int) }; + } + + // SAFETY: self.ptr() is live and signer.handle_ptr() addresses that Key's own pinned handle. + unsafe { sys::wolfTPM2_SetAuthHandle(self.ptr(), 0, signer.handle_ptr()) }; + qin.signHandle = signer.handle(); + // TPM_ALG_NULL: sign with the AIK's own scheme/hash. The signature hash + // is independent of the PCR-bank hash selected above. + qin.inScheme.scheme = sys::TPM_ALG_ID_T_TPM_ALG_NULL as sys::TPMI_ALG_SIG_SCHEME; + qin.qualifyingData.size = qualifying.len() as u16; + qin.qualifyingData.buffer[..qualifying.len()].copy_from_slice(qualifying); + + // SAFETY: Quote_Out is a C POD struct; all-zero is a valid starting state for TPM2_Quote to fill. + let mut qout: sys::Quote_Out = unsafe { core::mem::zeroed() }; + // SAFETY: &mut qin/&mut qout are valid, exclusively-borrowed in/out-params for TPM2_Quote. + let rc = unsafe { sys::TPM2_Quote(&mut qin, &mut qout) }; + // SAFETY: self.ptr() is live; this clears the auth slot set above regardless of the quote outcome. + unsafe { sys::wolfTPM2_UnsetAuth(self.ptr(), 0) }; + check_rc(rc)?; + + let asz = qout.quoted.size as usize; + let attest = qout.quoted.attestationData[..asz].to_vec(); + let signature = extract_signature(is_ecc, &qout.signature); + + Ok(Attestation { + attest, + sig_alg: qout.signature.sigAlg, + signature, + }) + } +} + +/// Field size (bytes) of NIST P-256, the ECC curve this crate's `KeyAlg` +/// exposes; used as the minimum ECDSA coordinate width. +const ECC_P256_COORD: usize = 32; + +/// Marshal a TPM signature out of its alg-specific union: fixed-width +/// `R || S` for ECDSA (each component left-padded to the same width so the +/// split point is unambiguous — half the result each), or the raw signature +/// buffer for RSASSA. The width is the larger of the two returned components +/// and the P-256 field size, so P-256 always yields 32-byte halves and a +/// larger curve (in a P-256-disabled build) is never truncated. +fn extract_signature(is_ecc: bool, sig: &sys::TPMT_SIGNATURE) -> Vec { + // SAFETY: is_ecc, derived from the signing key's own algorithm, selects the union field the TPM actually populated. + unsafe { + if is_ecc { + let e = &sig.signature.ecdsa; + let coord = (e.signatureR.size as usize) + .max(e.signatureS.size as usize) + .max(ECC_P256_COORD); + let mut v = vec![0u8; coord * 2]; + put_left_padded(&mut v[..coord], &e.signatureR.buffer, e.signatureR.size); + put_left_padded(&mut v[coord..], &e.signatureS.buffer, e.signatureS.size); + v + } else { + let r = &sig.signature.rsassa; + r.sig.buffer[..r.sig.size as usize].to_vec() + } + } +} + +/// Right-align `src[..len]` into `dst` (left-padded with the existing zeros). +fn put_left_padded(dst: &mut [u8], src: &[u8], len: u16) { + let n = (len as usize).min(dst.len()); + let start = dst.len() - n; + dst[start..].copy_from_slice(&src[..n]); +} diff --git a/wrapper/rust/wolftpm/src/credential.rs b/wrapper/rust/wolftpm/src/credential.rs new file mode 100644 index 000000000..c393e58f8 --- /dev/null +++ b/wrapper/rust/wolftpm/src/credential.rs @@ -0,0 +1,161 @@ +//! EK-based credential activation (`TPM2_MakeCredential` / +//! `TPM2_ActivateCredential`): the handshake that binds an attestation identity +//! key to a specific TPM's endorsement key, proving both live in the same TPM +//! without exposing the EK private key. + +use crate::device::Device; +use crate::key::Key; +use crate::{check_rc, sys, Result, Secret, TpmError}; + +/// A credential protected to a TPM's EK: the encrypted `credential_blob` and +/// the wrapped `secret`, both produced by [`Device::make_credential`] and +/// consumed by [`Device::activate_credential`]. +pub struct Credential { + pub credential_blob: Vec, + pub secret: Vec, +} + +impl Device { + /// Encrypt `secret` so only the TPM holding `ek` can recover it, bound to + /// the object named `object_name` (an AIK's [`Key::name`]). This is the + /// verifier-side step; it needs only the EK's public part, no auth. + pub fn make_credential( + &self, + ek: &Key<'_>, + object_name: &[u8], + secret: &[u8], + ) -> Result { + // SAFETY: MakeCredential_In is a C POD struct; all-zero is a valid starting state, and the length checks below bound the copies into its buffers. + let mut cin: sys::MakeCredential_In = unsafe { core::mem::zeroed() }; + if secret.len() > cin.credential.buffer.len() + || object_name.len() > cin.objectName.name.len() + { + return Err(TpmError(crate::BUFFER_E)); + } + cin.handle = ek.handle(); + cin.credential.size = secret.len() as u16; + cin.credential.buffer[..secret.len()].copy_from_slice(secret); + cin.objectName.size = object_name.len() as u16; + cin.objectName.name[..object_name.len()].copy_from_slice(object_name); + + // SAFETY: MakeCredential_Out is a C POD struct; all-zero is a valid starting state for TPM2_MakeCredential to fill. + let mut cout: sys::MakeCredential_Out = unsafe { core::mem::zeroed() }; + // SAFETY: &mut cin/&mut cout are valid, exclusively-borrowed in/out-params for TPM2_MakeCredential. + let rc = unsafe { sys::TPM2_MakeCredential(&mut cin, &mut cout) }; + // Scrub the plaintext credential copy left in the input on every path. + // SAFETY: cin is a live, fully-owned local being zeroized after the call. + unsafe { crate::zeroize_raw(&mut cin) }; + check_rc(rc)?; + + let bsz = cout.credentialBlob.size as usize; + let ssz = cout.secret.size as usize; + Ok(Credential { + credential_blob: cout.credentialBlob.buffer[..bsz].to_vec(), + secret: cout.secret.secret[..ssz].to_vec(), + }) + } + + /// Recover the secret from `cred` using `aik` (the activating key, auth in + /// slot 0) and `ek` (the decrypting endorsement key, satisfied by an EK + /// policy session in slot 1). Succeeds only on the TPM whose EK the + /// credential was made for. + #[cfg(ek_policy)] + pub fn activate_credential( + &self, + aik: &Key<'_>, + ek: &Key<'_>, + cred: &Credential, + ) -> Result { + // An encryption session holds auth slot 1, which the EK policy session + // needs here; refuse rather than silently disable the session. + if crate::session::is_active() { + return Err(TpmError(crate::E_SESSION_IN_USE)); + } + // EK auth is by policy (PolicySecret over the endorsement hierarchy), + // not a password. Save the prior bit and restore it on every exit so the + // caller's Key is not left mutated. + // SAFETY: ek.handle_ptr() addresses the live, pinned handle bit-field for this Key. + let prev_policy = unsafe { (*ek.handle_ptr()).policyAuth() }; + unsafe { (*ek.handle_ptr()).set_policyAuth(1) }; + // SAFETY: WOLFTPM2_SESSION is a C POD struct; all-zero is a valid starting state for CreateAuthSession_EkPolicy to fill. + let mut session: sys::WOLFTPM2_SESSION = unsafe { core::mem::zeroed() }; + // SAFETY: self.ptr() is live and &mut session is a valid, exclusively-borrowed out-param. + let rc = unsafe { sys::wolfTPM2_CreateAuthSession_EkPolicy(self.ptr(), &mut session) }; + if rc != 0 { + // The helper may have started the session before PolicySecret failed + // (e.g. a protected endorsement hierarchy); unload it so repeated + // attempts don't exhaust the TPM's session slots. + if session.handle.hndl != 0 { + // SAFETY: self.ptr() is live and &mut session.handle addresses the partially-created session. + unsafe { sys::wolfTPM2_UnloadHandle(self.ptr(), &mut session.handle) }; + } + // SAFETY: session is a live, fully-owned local being scrubbed before return. + unsafe { crate::zeroize_raw(&mut session) }; + // SAFETY: ek.handle_ptr() addresses the live handle bit-field. + unsafe { (*ek.handle_ptr()).set_policyAuth(prev_policy) }; + return Err(TpmError(rc)); + } + + // SAFETY: ActivateCredential_In is a C POD struct; all-zero is a valid starting state before the checked field copies below. + let mut cin: sys::ActivateCredential_In = unsafe { core::mem::zeroed() }; + if cred.credential_blob.len() > cin.credentialBlob.buffer.len() + || cred.secret.len() > cin.secret.secret.len() + { + // SAFETY: self.ptr() is live; the just-opened session is torn down before returning on this error path. + unsafe { sys::wolfTPM2_UnloadHandle(self.ptr(), &mut session.handle) }; + // SAFETY: ek.handle_ptr() addresses the live handle bit-field. + unsafe { (*ek.handle_ptr()).set_policyAuth(prev_policy) }; + return Err(TpmError(crate::BUFFER_E)); + } + + // Slot 1: EK policy session, bound to the EK's Name. + // SAFETY: &mut session still refers to the live session created above. + let set_rc = unsafe { + sys::wolfTPM2_SetAuthSession(self.ptr(), 1, &mut session, 0) + }; + // SAFETY: self.ptr() is live, and ek.handle_ptr()/aik.handle_ptr() address each Key's own pinned handle. + unsafe { + sys::wolfTPM2_SetAuthHandleName(self.ptr(), 1, ek.handle_ptr()); + // Slot 0: the AIK's own (password) auth. + sys::wolfTPM2_SetAuthHandle(self.ptr(), 0, aik.handle_ptr()); + } + + cin.activateHandle = aik.handle(); + cin.keyHandle = ek.handle(); + cin.credentialBlob.size = cred.credential_blob.len() as u16; + cin.credentialBlob.buffer[..cred.credential_blob.len()] + .copy_from_slice(&cred.credential_blob); + cin.secret.size = cred.secret.len() as u16; + cin.secret.secret[..cred.secret.len()].copy_from_slice(&cred.secret); + + // SAFETY: ActivateCredential_Out is a C POD struct; all-zero is a valid starting state for TPM2_ActivateCredential to fill. + let mut cout: sys::ActivateCredential_Out = unsafe { core::mem::zeroed() }; + let rc = if set_rc != 0 { + set_rc + } else { + // SAFETY: &mut cin/&mut cout are valid, exclusively-borrowed in/out-params, reached only once both auth slots were set successfully. + unsafe { sys::TPM2_ActivateCredential(&mut cin, &mut cout) } + }; + // SAFETY: self.ptr() is live; this clears both auth slots set above regardless of the activation outcome. + unsafe { + sys::wolfTPM2_UnsetAuth(self.ptr(), 0); + sys::wolfTPM2_UnsetAuth(self.ptr(), 1); + } + // The TPM flushes the policy session on a successful use; only unload it + // if the command did not consume it. + if rc != 0 { + // SAFETY: self.ptr() is live and &mut session.handle addresses the still-loaded policy session. + unsafe { sys::wolfTPM2_UnloadHandle(self.ptr(), &mut session.handle) }; + } + // SAFETY: ek.handle_ptr() addresses the live handle bit-field; restore the + // caller's EK auth mode now that the command is done. + unsafe { (*ek.handle_ptr()).set_policyAuth(prev_policy) }; + check_rc(rc)?; + + let n = cout.certInfo.size as usize; + let out = cout.certInfo.buffer[..n].to_vec(); + // SAFETY: cout is a live, fully-owned local; zeroizing it after copying out scrubs the recovered secret. + unsafe { crate::zeroize_raw(&mut cout) }; + Ok(Secret::new(out)) + } +} diff --git a/wrapper/rust/wolftpm/src/device.rs b/wrapper/rust/wolftpm/src/device.rs new file mode 100644 index 000000000..3aaee6fdb --- /dev/null +++ b/wrapper/rust/wolftpm/src/device.rs @@ -0,0 +1,134 @@ +//! The TPM connection: [`Device`] owns a `WOLFTPM2_DEV` and the transport to +//! the TPM (real hardware, the Linux kernel driver, or a software TPM over the +//! swtpm socket). + +use crate::key::{Hierarchy, Key, KeyAlg}; +use crate::{check_rc, sys, Result, TpmError, E_DEVICE_IN_USE}; +use core::cell::UnsafeCell; +use core::sync::atomic::{AtomicBool, Ordering}; + +/// wolfTPM routes every command through a single active context that +/// `wolfTPM2_Init` overwrites, so only one live `Device` is supported at a time. +static DEVICE_ACTIVE: AtomicBool = AtomicBool::new(false); + +/// An initialized wolfTPM device. +/// +/// `WOLFTPM2_DEV` holds a self-referential pointer (`spdmCtx -> spdmCtxData`), +/// so it must never move once initialized: it lives boxed on the heap. The +/// `UnsafeCell` reflects that every TPM command mutates the device through the +/// C pointer, which lets key handles borrow the device immutably and coexist. +pub struct Device { + dev: Box>, +} + +impl Device { + /// Open using a transport that needs no HAL callback: the swtpm socket, the + /// Linux kernel device (including its autodetect variant), MMIO, or the + /// Windows TBS, as selected when the C library was built. Hardware SPI/I2C + /// HAL builds (including `WOLFTPM_AUTODETECT` over SPI/I2C) require a + /// caller-provided callback and are not opened by this method. + #[cfg(any(swtpm, devtpm, mmio, winapi, linux_autodetect))] + pub fn open() -> Result { + Self::init_with(None) + } + + /// Open using a caller-supplied HAL I/O callback, for the SPI/I2C (and other + /// callback-based) transports that [`open`](Device::open) does not cover. + /// + /// # Safety + /// + /// `io_cb` must be a valid wolfTPM HAL callback for the linked transport, + /// and anything it dereferences (its user context) must remain valid for the + /// whole lifetime of the returned `Device`. + pub unsafe fn open_with_io_cb(io_cb: sys::TPM2HalIoCb) -> Result { + Self::init_with(io_cb) + } + + /// Open a software TPM over the swtpm/mssim socket. + /// + /// The endpoint comes from the `TPM2_SWTPM_HOST` / `TPM2_SWTPM_PORT` + /// environment the C backend reads with `getenv` (default `localhost:2321`). + /// Because that is process-global, set it once at startup before opening, + /// rather than per connection. + #[cfg(swtpm)] + pub fn open_swtpm() -> Result { + Self::init_with(None) + } + + /// Acquire the single-live-`Device` lease, or fail if one is already held. + fn acquire() -> Result<()> { + if DEVICE_ACTIVE + .compare_exchange(false, true, Ordering::AcqRel, Ordering::Acquire) + .is_err() + { + return Err(TpmError(E_DEVICE_IN_USE)); + } + Ok(()) + } + + fn init_with(io_cb: sys::TPM2HalIoCb) -> Result { + Self::acquire()?; + Self::init_held(io_cb) + } + + /// Initialize with the lease already held; releases it on failure. + fn init_held(io_cb: sys::TPM2HalIoCb) -> Result { + // SAFETY: WOLFTPM2_DEV is a C POD struct; all-zero is a valid state for wolfTPM2_Init to fill in. + let dev: Box> = + Box::new(UnsafeCell::new(unsafe { core::mem::zeroed() })); + // SAFETY: dev is heap-boxed and not yet moved again, so dev.get() is a stable pointer for Init to populate. + let rc = unsafe { sys::wolfTPM2_Init(dev.get(), io_cb, core::ptr::null_mut()) }; + if rc != 0 { + // Tear down any active context wolfTPM installed (for example on + // TPM_RC_UPGRADE) before the box is freed, then release the slot. + // SAFETY: dev.get() still points at the live, boxed WOLFTPM2_DEV Init partially set up. + unsafe { sys::wolfTPM2_Cleanup(dev.get()) }; + DEVICE_ACTIVE.store(false, Ordering::Release); + return Err(TpmError(rc)); + } + Ok(Device { dev }) + } + + /// The raw device pointer for FFI. Stable for the life of the `Device`. + pub(crate) fn ptr(&self) -> *mut sys::WOLFTPM2_DEV { + self.dev.get() + } + + /// Fill `buf` with TPM-generated random bytes. + #[cfg(rng)] + pub fn get_random(&self, buf: &mut [u8]) -> Result<()> { + if buf.is_empty() { + return Ok(()); + } + let n = crate::checked_u32(buf.len())?; + // SAFETY: self.ptr() is the pinned dev pointer and buf.as_mut_ptr()/len describe a live, in-bounds slice. + let rc = unsafe { sys::wolfTPM2_GetRandom(self.ptr(), buf.as_mut_ptr(), n) }; + check_rc(rc) + } + + /// Create a primary key (a storage root key by default) under `hierarchy`. + /// `auth` sets the new object's own authorization value, not the + /// hierarchy's. + /// + /// The hierarchy itself is assumed to be unauthenticated (empty owner / + /// endorsement / platform auth), which is the device-identity case and + /// matches the underlying `wolfTPM2_CreatePrimaryKey`, which issues the + /// command with a blank hierarchy authorization. Provisioned TPMs that have + /// set a non-empty hierarchy authorization are not supported by this call. + pub fn create_primary( + &self, + hierarchy: Hierarchy, + alg: KeyAlg, + auth: Option<&[u8]>, + ) -> Result> { + Key::create_primary(self, hierarchy, alg, auth) + } +} + +impl Drop for Device { + fn drop(&mut self) { + // SAFETY: the Device is being dropped, so self.dev.get() is still the valid, uniquely-owned dev pointer. + unsafe { sys::wolfTPM2_Cleanup(self.dev.get()) }; + DEVICE_ACTIVE.store(false, Ordering::Release); + } +} diff --git a/wrapper/rust/wolftpm/src/ecdh.rs b/wrapper/rust/wolftpm/src/ecdh.rs new file mode 100644 index 000000000..e048579c4 --- /dev/null +++ b/wrapper/rust/wolftpm/src/ecdh.rs @@ -0,0 +1,94 @@ +//! ECDH key agreement on a TPM-resident ECC key (NIST P-256). + +use crate::key::Key; +use crate::{check_rc, sys, Result, Secret, TpmError}; +use core::ffi::c_int; + +/// An ephemeral ECDH public point (`x || y`, each the curve's field size) and +/// the derived shared secret Z, from [`Key::ecdh_gen`]. +pub struct EcdhResult { + /// The ephemeral public point as `x || y` bytes; feed to a peer, or back to + /// [`Key::ecdh_z`] to recover the same secret. + pub point: Vec, + /// The derived shared secret Z. + pub secret: Secret, +} + +impl<'d> Key<'d> { + /// One-shot ephemeral ECDH: the TPM generates an ephemeral key pair, derives + /// Z against this key's private part, and returns the ephemeral public point + /// plus Z. A peer holding this key's private part can recover the same Z from + /// the point via [`ecdh_z`](Key::ecdh_z). + pub fn ecdh_gen(&self) -> Result { + // SAFETY: TPM2B_ECC_POINT is a C POD struct; all-zero is a valid starting state for ECDHGen to fill. + let mut pt: sys::TPM2B_ECC_POINT = unsafe { core::mem::zeroed() }; + let mut secret = vec![0u8; 128]; + let mut secret_sz = secret.len() as c_int; + // SAFETY: self.dev()/self.kptr() are live, &mut pt is a valid out-param, and secret.as_mut_ptr()/&mut secret_sz describe the full secret capacity. + let rc = unsafe { + sys::wolfTPM2_ECDHGen( + self.dev(), + self.kptr(), + &mut pt, + secret.as_mut_ptr(), + &mut secret_sz, + ) + }; + check_rc(rc)?; + secret.truncate(secret_sz as usize); + Ok(EcdhResult { + point: point_bytes(&pt), + secret: Secret::new(secret), + }) + } + + /// Recompute the shared secret Z from a peer's public `point` (`x || y`, the + /// output of [`ecdh_gen`](Key::ecdh_gen)) and this key's private part. + pub fn ecdh_z(&self, point: &[u8]) -> Result { + let pt = point_from_bytes(point)?; + let mut secret = vec![0u8; 128]; + let mut secret_sz = secret.len() as c_int; + // SAFETY: self.dev()/self.kptr() are live, &pt is the caller-built valid peer point, and secret.as_mut_ptr()/&mut secret_sz describe the full secret capacity. + let rc = unsafe { + sys::wolfTPM2_ECDHGenZ( + self.dev(), + self.kptr(), + &pt, + secret.as_mut_ptr(), + &mut secret_sz, + ) + }; + check_rc(rc)?; + secret.truncate(secret_sz as usize); + Ok(Secret::new(secret)) + } +} + +/// Serialize a `TPM2B_ECC_POINT` to `x || y` bytes. +fn point_bytes(pt: &sys::TPM2B_ECC_POINT) -> Vec { + let x = &pt.point.x; + let y = &pt.point.y; + let (xn, yn) = (x.size as usize, y.size as usize); + let mut v = Vec::with_capacity(xn + yn); + v.extend_from_slice(&x.buffer[..xn]); + v.extend_from_slice(&y.buffer[..yn]); + v +} + +/// Build a `TPM2B_ECC_POINT` from an `x || y` byte string (split in half). +fn point_from_bytes(point: &[u8]) -> Result { + if point.is_empty() || point.len() % 2 != 0 { + return Err(TpmError(crate::BUFFER_E)); + } + let half = point.len() / 2; + // SAFETY: TPM2B_ECC_POINT is a C POD struct; all-zero is a valid starting state before the checked field copies below. + let mut pt: sys::TPM2B_ECC_POINT = unsafe { core::mem::zeroed() }; + if half > pt.point.x.buffer.len() { + return Err(TpmError(crate::BUFFER_E)); + } + pt.point.x.size = half as u16; + pt.point.x.buffer[..half].copy_from_slice(&point[..half]); + pt.point.y.size = half as u16; + pt.point.y.buffer[..half].copy_from_slice(&point[half..]); + Ok(pt) +} diff --git a/wrapper/rust/wolftpm/src/hmac.rs b/wrapper/rust/wolftpm/src/hmac.rs new file mode 100644 index 000000000..ef7572414 --- /dev/null +++ b/wrapper/rust/wolftpm/src/hmac.rs @@ -0,0 +1,96 @@ +//! Keyed-hash HMAC computed inside the TPM. + +use crate::device::Device; +use crate::key::{HashAlg, Key}; +use crate::{check_rc, sys, Result, TpmError}; + +impl<'d> Key<'d> { + /// Compute HMAC over `data` using this loaded keyed-hash key, whose secret + /// stays inside the TPM. `hash` must match the algorithm the key was + /// created with (see [`Template::hmac`](crate::Template::hmac)). + /// + /// This is the one-shot `TPM2_HMAC`, so `data` must fit the TPM's max + /// command buffer; larger inputs are rejected rather than truncated. + #[cfg(keyedhash)] + pub fn hmac(&self, data: &[u8], hash: HashAlg) -> Result> { + // SAFETY: HMAC_In is a C POD struct; all-zero is a valid starting state, and the length check above bounds the copy into cin.buffer.buffer. + let mut cin: sys::HMAC_In = unsafe { core::mem::zeroed() }; + if data.len() > cin.buffer.buffer.len() { + return Err(TpmError(crate::BUFFER_E)); + } + // SAFETY: self.dev() is live and self.handle_ptr() addresses this Key's own pinned handle. + unsafe { sys::wolfTPM2_SetAuthHandle(self.dev(), 0, self.handle_ptr()) }; + cin.handle = self.handle(); + cin.hashAlg = hash.alg_id() as sys::TPMI_ALG_HASH; + cin.buffer.size = data.len() as u16; + cin.buffer.buffer[..data.len()].copy_from_slice(data); + + // SAFETY: HMAC_Out is a C POD struct; all-zero is a valid starting state for TPM2_HMAC to fill. + let mut cout: sys::HMAC_Out = unsafe { core::mem::zeroed() }; + // SAFETY: &mut cin/&mut cout are valid, exclusively-borrowed in/out-params for TPM2_HMAC. + let rc = unsafe { sys::TPM2_HMAC(&mut cin, &mut cout) }; + // SAFETY: self.dev() is live; clear the auth slot and scrub the message + // copy left in cin, regardless of the HMAC outcome. + unsafe { + sys::wolfTPM2_UnsetAuth(self.dev(), 0); + crate::zeroize_raw(&mut cin); + } + check_rc(rc)?; + let n = cout.outHMAC.size as usize; + Ok(cout.outHMAC.buffer[..n].to_vec()) + } +} + +impl Device { + /// Compute an HMAC over `data` using `key` as the HMAC key. The keyed-hash + /// object is created under `parent` (a loaded storage key such as the SRK) + /// and freed before returning. + pub fn hmac( + &self, + parent: &Key<'_>, + key: &[u8], + data: &[u8], + hash: HashAlg, + ) -> Result> { + let key_len = crate::checked_u32(key.len())?; + let data_len = crate::checked_u32(data.len())?; + // SAFETY: WOLFTPM2_HMAC is a C POD struct; all-zero is a valid starting state for HmacStart to fill. + let mut ctx: sys::WOLFTPM2_HMAC = unsafe { core::mem::zeroed() }; + // SAFETY: self.ptr()/parent.handle_ptr() are live, &mut ctx is a valid out-param, and key ptr+len bound its slice (data label is null/0, unused here). + let rc = unsafe { + sys::wolfTPM2_HmacStart( + self.ptr(), + &mut ctx, + parent.handle_ptr(), + hash.alg_id(), + key.as_ptr(), + key_len, + core::ptr::null(), + 0, + ) + }; + check_rc(rc)?; + + // SAFETY: ctx was just initialized by HmacStart above, and data ptr+len bound its slice. + let rc_update = unsafe { + sys::wolfTPM2_HmacUpdate(self.ptr(), &mut ctx, data.as_ptr(), data_len) + }; + + let mut out = vec![0u8; 64]; + let mut out_sz = out.len() as sys::word32; + // SAFETY: ctx is still the live HMAC context, and out.as_mut_ptr()/&mut out_sz describe the full out capacity. + let rc_finish = + unsafe { sys::wolfTPM2_HmacFinish(self.ptr(), &mut ctx, out.as_mut_ptr(), &mut out_sz) }; + + // Free the transient keyed-hash key if the finish left it loaded. + if ctx.key.handle.hndl != 0 { + // SAFETY: self.ptr() is live and &mut ctx.key.handle addresses the transient key HmacFinish left loaded. + unsafe { sys::wolfTPM2_UnloadHandle(self.ptr(), &mut ctx.key.handle) }; + } + + check_rc(rc_update)?; + check_rc(rc_finish)?; + out.truncate(out_sz as usize); + Ok(out) + } +} diff --git a/wrapper/rust/wolftpm/src/key.rs b/wrapper/rust/wolftpm/src/key.rs new file mode 100644 index 000000000..09e3515c0 --- /dev/null +++ b/wrapper/rust/wolftpm/src/key.rs @@ -0,0 +1,686 @@ +//! TPM key objects, templates, and key blobs. + +use crate::device::Device; +use crate::{check_rc, sys, Result, TpmError}; +use core::cell::UnsafeCell; +use core::ffi::c_int; +use core::marker::PhantomData; + +/// TPM authorization hierarchy a primary key is created under. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum Hierarchy { + Owner, + Endorsement, + Platform, + Null, +} + +impl Hierarchy { + pub(crate) fn handle(self) -> sys::TPM_HANDLE { + let h = match self { + Hierarchy::Owner => sys::TPM_RH_T_TPM_RH_OWNER, + Hierarchy::Endorsement => sys::TPM_RH_T_TPM_RH_ENDORSEMENT, + Hierarchy::Platform => sys::TPM_RH_T_TPM_RH_PLATFORM, + Hierarchy::Null => sys::TPM_RH_T_TPM_RH_NULL, + }; + h as sys::TPM_HANDLE + } +} + +/// Asymmetric algorithm for a key template. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum KeyAlg { + Rsa, + EccP256, +} + +impl KeyAlg { + pub(crate) fn alg_id(self) -> sys::TPM_ALG_ID { + let a = match self { + KeyAlg::Rsa => sys::TPM_ALG_ID_T_TPM_ALG_RSA, + KeyAlg::EccP256 => sys::TPM_ALG_ID_T_TPM_ALG_ECC, + }; + a as sys::TPM_ALG_ID + } +} + +/// Hash algorithm selector (PCR banks, schemes, OAEP label hash). +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum HashAlg { + /// SHA-1. Legacy; the one place it is still needed is RSA-OAEP with the + /// Microsoft enrollment scheme, which wraps its session key with OAEP-SHA1. + Sha1, + Sha256, + Sha384, + Sha512, +} + +impl HashAlg { + pub(crate) fn alg_id(self) -> sys::TPM_ALG_ID { + let a = match self { + HashAlg::Sha1 => sys::TPM_ALG_ID_T_TPM_ALG_SHA1, + HashAlg::Sha256 => sys::TPM_ALG_ID_T_TPM_ALG_SHA256, + HashAlg::Sha384 => sys::TPM_ALG_ID_T_TPM_ALG_SHA384, + HashAlg::Sha512 => sys::TPM_ALG_ID_T_TPM_ALG_SHA512, + }; + a as sys::TPM_ALG_ID + } + + pub(crate) fn as_c_int(self) -> c_int { + self.alg_id() as c_int + } + + /// Digest length in bytes for this hash. + pub fn digest_size(self) -> usize { + match self { + HashAlg::Sha1 => 20, + HashAlg::Sha256 => 32, + HashAlg::Sha384 => 48, + HashAlg::Sha512 => 64, + } + } +} + +/// A TPM public-area key template. +pub struct Template(pub(crate) sys::TPMT_PUBLIC); + +impl Template { + /// Storage root key template (restricted decryption parent). + pub fn srk(alg: KeyAlg) -> Result { + // SAFETY: TPMT_PUBLIC is a C POD struct; all-zero is a valid starting state for the template helper to fill. + let mut t: sys::TPMT_PUBLIC = unsafe { core::mem::zeroed() }; + // SAFETY: &mut t is a valid, exclusively-borrowed out-param for the selected SRK template helper. + let rc = unsafe { + match alg { + KeyAlg::Rsa => sys::wolfTPM2_GetKeyTemplate_RSA_SRK(&mut t), + KeyAlg::EccP256 => sys::wolfTPM2_GetKeyTemplate_ECC_SRK(&mut t), + } + }; + check_rc(rc)?; + check_p256(&t, alg)?; + Ok(Template(t)) + } + + /// Attestation identity key (AIK) template — a restricted signing key used + /// to certify other objects / quote PCRs. + pub fn attestation(alg: KeyAlg) -> Result { + // SAFETY: TPMT_PUBLIC is a C POD struct; all-zero is a valid starting state for the AIK template helper. + let mut t: sys::TPMT_PUBLIC = unsafe { core::mem::zeroed() }; + // SAFETY: &mut t is a valid, exclusively-borrowed out-param for the selected AIK template helper. + let rc = unsafe { + match alg { + KeyAlg::EccP256 => sys::wolfTPM2_GetKeyTemplate_ECC_AIK(&mut t), + KeyAlg::Rsa => sys::wolfTPM2_GetKeyTemplate_RSA_AIK(&mut t), + } + }; + check_rc(rc)?; + check_p256(&t, alg)?; + Ok(Template(t)) + } + + /// Endorsement key (EK) template, created under the endorsement hierarchy. + pub fn ek(alg: KeyAlg) -> Result { + // SAFETY: TPMT_PUBLIC is a C POD struct; all-zero is a valid starting state for the EK template helper. + let mut t: sys::TPMT_PUBLIC = unsafe { core::mem::zeroed() }; + // SAFETY: &mut t is a valid, exclusively-borrowed out-param for the selected EK template helper. + let rc = unsafe { + match alg { + KeyAlg::EccP256 => sys::wolfTPM2_GetKeyTemplate_ECC_EK(&mut t), + KeyAlg::Rsa => sys::wolfTPM2_GetKeyTemplate_RSA_EK(&mut t), + } + }; + check_rc(rc)?; + Ok(Template(t)) + } + + /// RSA decryption-key template (non-restricted), for RSA-OAEP encrypt and + /// decrypt. + pub fn rsa_decrypt() -> Result { + let attrs = (sys::TPMA_OBJECT_mask_TPMA_OBJECT_decrypt + | sys::TPMA_OBJECT_mask_TPMA_OBJECT_fixedTPM + | sys::TPMA_OBJECT_mask_TPMA_OBJECT_fixedParent + | sys::TPMA_OBJECT_mask_TPMA_OBJECT_sensitiveDataOrigin + | sys::TPMA_OBJECT_mask_TPMA_OBJECT_userWithAuth + | sys::TPMA_OBJECT_mask_TPMA_OBJECT_noDA) as sys::TPMA_OBJECT; + // SAFETY: TPMT_PUBLIC is a C POD struct; all-zero is a valid starting state for the template helper to fill. + let mut t: sys::TPMT_PUBLIC = unsafe { core::mem::zeroed() }; + // SAFETY: &mut t is a valid, exclusively-borrowed out-param for wolfTPM2_GetKeyTemplate_RSA. + let rc = unsafe { sys::wolfTPM2_GetKeyTemplate_RSA(&mut t, attrs) }; + check_rc(rc)?; + Ok(Template(t)) + } + + /// General signing-key template (non-restricted, ECDSA/RSASSA). + pub fn signing(alg: KeyAlg) -> Result { + let attrs = (sys::TPMA_OBJECT_mask_TPMA_OBJECT_sign + | sys::TPMA_OBJECT_mask_TPMA_OBJECT_fixedTPM + | sys::TPMA_OBJECT_mask_TPMA_OBJECT_fixedParent + | sys::TPMA_OBJECT_mask_TPMA_OBJECT_sensitiveDataOrigin + | sys::TPMA_OBJECT_mask_TPMA_OBJECT_userWithAuth + | sys::TPMA_OBJECT_mask_TPMA_OBJECT_noDA) as sys::TPMA_OBJECT; + // SAFETY: TPMT_PUBLIC is a C POD struct; all-zero is a valid starting state for the signing template helper. + let mut t: sys::TPMT_PUBLIC = unsafe { core::mem::zeroed() }; + // SAFETY: &mut t is a valid, exclusively-borrowed out-param for the selected signing template helper. + let rc = unsafe { + match alg { + KeyAlg::EccP256 => sys::wolfTPM2_GetKeyTemplate_ECC( + &mut t, + attrs, + sys::TPM_ECC_CURVE_T_TPM_ECC_NIST_P256 as sys::TPM_ECC_CURVE, + sys::TPM_ALG_ID_T_TPM_ALG_ECDSA as sys::TPM_ALG_ID, + ), + // Set an explicit RSASSA-SHA256 scheme; the plain template + // leaves the scheme NULL, which sign_hash cannot use for RSA. + KeyAlg::Rsa => sys::wolfTPM2_GetKeyTemplate_RSA_ex( + &mut t, + sys::TPM_ALG_ID_T_TPM_ALG_SHA256 as sys::TPM_ALG_ID, + attrs, + 2048, + 0, + sys::TPM_ALG_ID_T_TPM_ALG_RSASSA as sys::TPM_ALG_ID, + sys::TPM_ALG_ID_T_TPM_ALG_SHA256 as sys::TPM_ALG_ID, + ), + } + }; + check_rc(rc)?; + check_p256(&t, alg)?; + Ok(Template(t)) + } + + /// TPM-generated keyed-hash HMAC key template, bound to this TPM. + /// + /// The key material originates in the TPM (`sensitiveDataOrigin`) and is + /// non-duplicable (`fixedTPM`/`fixedParent`), so the HMAC secret never + /// leaves the TPM: create it once, keep only the wrapped blob, reload it, + /// and compute with [`Key::hmac`](crate::Key::hmac). + #[cfg(keyedhash)] + pub fn hmac(hash: HashAlg) -> Result { + // SAFETY: TPMT_PUBLIC is a C POD struct; all-zero is a valid starting state for the template helper to fill. + let mut t: sys::TPMT_PUBLIC = unsafe { core::mem::zeroed() }; + // SAFETY: &mut t is a valid, exclusively-borrowed out-param for wolfTPM2_GetKeyTemplate_KeyedHash. + let rc = unsafe { sys::wolfTPM2_GetKeyTemplate_KeyedHash(&mut t, hash.alg_id(), 1, 0) }; + check_rc(rc)?; + // The helper leaves sensitiveDataOrigin clear (caller-supplied key); + // set it plus the fixed bits so the TPM generates a bound secret. + t.objectAttributes |= (sys::TPMA_OBJECT_mask_TPMA_OBJECT_sensitiveDataOrigin + | sys::TPMA_OBJECT_mask_TPMA_OBJECT_fixedTPM + | sys::TPMA_OBJECT_mask_TPMA_OBJECT_fixedParent) + as sys::TPMA_OBJECT; + Ok(Template(t)) + } + + /// AES symmetric-cipher key template (CFB mode) for TPM-backed bulk + /// encrypt/decrypt via [`Key::aes_encrypt`](crate::Key::aes_encrypt) / + /// [`Key::aes_decrypt`](crate::Key::aes_decrypt). `bits` is 128 or 256. + #[cfg(symmetric)] + pub fn symmetric(bits: u16) -> Result { + // SAFETY: TPMT_PUBLIC is a C POD struct; all-zero is a valid starting state for the template helper to fill. + let mut t: sys::TPMT_PUBLIC = unsafe { core::mem::zeroed() }; + // SAFETY: &mut t is a valid, exclusively-borrowed out-param for wolfTPM2_GetKeyTemplate_Symmetric. + let rc = unsafe { + sys::wolfTPM2_GetKeyTemplate_Symmetric( + &mut t, + bits as c_int, + sys::TPM_ALG_ID_T_TPM_ALG_CFB as sys::TPM_ALG_ID, + 0, + 1, + ) + }; + check_rc(rc)?; + t.objectAttributes |= (sys::TPMA_OBJECT_mask_TPMA_OBJECT_fixedTPM + | sys::TPMA_OBJECT_mask_TPMA_OBJECT_fixedParent) + as sys::TPMA_OBJECT; + Ok(Template(t)) + } + + /// ECDH key-agreement key template (NIST P-256, restricted-decrypt with the + /// ECDH scheme). Use with [`Key::ecdh_gen`](crate::Key::ecdh_gen). + #[cfg(ecdh)] + pub fn ecdh() -> Result { + // fixedTPM/fixedParent keep this child key non-duplicable (TPM-bound); + // it is loaded under a storage parent, not an ephemeral primary. + let attrs = (sys::TPMA_OBJECT_mask_TPMA_OBJECT_decrypt + | sys::TPMA_OBJECT_mask_TPMA_OBJECT_fixedTPM + | sys::TPMA_OBJECT_mask_TPMA_OBJECT_fixedParent + | sys::TPMA_OBJECT_mask_TPMA_OBJECT_sensitiveDataOrigin + | sys::TPMA_OBJECT_mask_TPMA_OBJECT_userWithAuth + | sys::TPMA_OBJECT_mask_TPMA_OBJECT_noDA) as sys::TPMA_OBJECT; + // SAFETY: TPMT_PUBLIC is a C POD struct; all-zero is a valid starting state for the ECDH template helper. + let mut t: sys::TPMT_PUBLIC = unsafe { core::mem::zeroed() }; + // SAFETY: &mut t is a valid, exclusively-borrowed out-param for wolfTPM2_GetKeyTemplate_ECC. + let rc = unsafe { + sys::wolfTPM2_GetKeyTemplate_ECC( + &mut t, + attrs, + sys::TPM_ECC_CURVE_T_TPM_ECC_NIST_P256 as sys::TPM_ECC_CURVE, + sys::TPM_ALG_ID_T_TPM_ALG_ECDH as sys::TPM_ALG_ID, + ) + }; + check_rc(rc)?; + t.nameAlg = sys::TPM_ALG_ID_T_TPM_ALG_SHA256 as sys::TPMI_ALG_HASH; + check_p256(&t, KeyAlg::EccP256)?; + Ok(Template(t)) + } +} + +/// A loaded TPM key. Its handle is unloaded from the TPM when dropped. +/// +/// Borrows the [`Device`] it lives on, so it cannot outlive the connection. +pub struct Key<'d> { + key: UnsafeCell, + dev: *mut sys::WOLFTPM2_DEV, + _marker: PhantomData<&'d Device>, +} + +impl<'d> Key<'d> { + pub(crate) fn from_raw(dev: *mut sys::WOLFTPM2_DEV, key: sys::WOLFTPM2_KEY) -> Self { + Key { + key: UnsafeCell::new(key), + dev, + _marker: PhantomData, + } + } + + pub(crate) fn kptr(&self) -> *mut sys::WOLFTPM2_KEY { + self.key.get() + } + + pub(crate) fn dev(&self) -> *mut sys::WOLFTPM2_DEV { + self.dev + } + + pub(crate) fn handle_ptr(&self) -> *mut sys::WOLFTPM2_HANDLE { + // SAFETY: self.key.get() points into the live, pinned WOLFTPM2_KEY cell for this Key's lifetime. + unsafe { &mut (*self.key.get()).handle } + } + + /// The TPM handle value for this key. + pub fn handle(&self) -> u32 { + // SAFETY: self.key.get() points into the live, pinned WOLFTPM2_KEY cell for this Key's lifetime. + unsafe { (*self.key.get()).handle.hndl } + } + + /// The object's cryptographic Name (the hash of its public area), as + /// computed by the TPM when the key was loaded. Needed to address the key + /// in credential activation (see [`Device::make_credential`](crate::Device::make_credential)). + pub fn name(&self) -> Vec { + // SAFETY: self.key.get() is live and n.size, set by the TPM, never exceeds the fixed n.name buffer capacity. + unsafe { + let n = &(*self.key.get()).handle.name; + n.name[..n.size as usize].to_vec() + } + } + + /// Set the object's authorization value (used by unseal / authorized ops). + /// Rejects values larger than the TPM auth buffer rather than truncating. + pub(crate) fn set_auth(&self, auth: &[u8]) -> Result<()> { + // SAFETY: self.key.get() is live, and the length check above bounds the copy to h.auth.buffer's fixed size. + unsafe { + let h = &mut (*self.key.get()).handle; + if auth.len() > h.auth.buffer.len() { + return Err(TpmError(crate::BUFFER_E)); + } + h.auth.size = auth.len() as u16; + h.auth.buffer[..auth.len()].copy_from_slice(auth); + } + Ok(()) + } + + /// Set the object's auth, zero-padded up to the key's nameAlg digest size + /// the way `wolfTPM2_CreateKey`/`CreatePrimaryKey` store it. Restoring a + /// short auth verbatim would not match the padded value the TPM holds. + pub(crate) fn set_auth_padded(&self, auth: &[u8]) -> Result<()> { + // SAFETY: self.key.get() points into the live, pinned WOLFTPM2_KEY cell for this Key's lifetime. + let name_alg = unsafe { (*self.key.get()).pub_.publicArea.nameAlg }; + // SAFETY: name_alg is a valid TPM_ALG_ID read from the loaded key's own public area. + let dsz = unsafe { sys::TPM2_GetHashDigestSize(name_alg) }; + if dsz > 0 && auth.len() < dsz as usize { + let mut padded = vec![0u8; dsz as usize]; + padded[..auth.len()].copy_from_slice(auth); + let r = self.set_auth(&padded); + for b in padded.iter_mut() { + // SAFETY: b is a valid &mut u8 into the live padded Vec; the volatile write scrubs it from the compiler's view. + unsafe { core::ptr::write_volatile(b, 0) }; + } + r + } else { + self.set_auth(auth) + } + } + + /// The public key's algorithm id (`TPM_ALG_RSA` or `TPM_ALG_ECC`). + pub(crate) fn alg(&self) -> sys::TPM_ALG_ID { + // SAFETY: self.key.get() points into the live, pinned WOLFTPM2_KEY cell for this Key's lifetime. + unsafe { (*self.key.get()).pub_.publicArea.type_ } + } + + /// Export this key's public part. `pem` selects PEM, otherwise DER (ASN.1). + #[cfg(pubexport)] + pub fn export_public(&self, pem: bool) -> Result> { + let mut out = vec![0u8; 2048]; + let mut out_sz = out.len() as sys::word32; + let enc = if pem { + sys::ENCODING_TYPE_PEM + } else { + sys::ENCODING_TYPE_ASN1 + } as core::ffi::c_int; + // SAFETY: self.dev/self.kptr() are live pointers, and out.as_mut_ptr()/out_sz describe the full out Vec capacity. + let rc = unsafe { + sys::wolfTPM2_ExportPublicKeyBuffer(self.dev, self.kptr(), enc, out.as_mut_ptr(), &mut out_sz) + }; + check_rc(rc)?; + out.truncate(out_sz as usize); + Ok(out) + } + + pub(crate) fn create_primary( + dev: &'d Device, + hierarchy: Hierarchy, + alg: KeyAlg, + auth: Option<&[u8]>, + ) -> Result { + let devp = dev.ptr(); + let mut tmpl = Template::srk(alg)?; + // SAFETY: WOLFTPM2_KEY is a C POD struct; all-zero is a valid starting state for CreatePrimaryKey to fill. + let mut key: sys::WOLFTPM2_KEY = unsafe { core::mem::zeroed() }; + let (authp, authsz) = auth_ptr(auth)?; + // SAFETY: devp is the pinned dev pointer, &mut key/&mut tmpl.0 are valid exclusive out-params, and authp/authsz match (auth's ptr, len) or (null, 0). + let rc = unsafe { + sys::wolfTPM2_CreatePrimaryKey( + devp, + &mut key, + hierarchy.handle(), + &mut tmpl.0, + authp, + authsz, + ) + }; + if rc != 0 { + // The C call can copy the padded auth into `key` before failing; + // scrub the stack copy before discarding it. + unsafe { crate::zeroize_raw(&mut key) }; + return Err(TpmError(rc)); + } + Ok(Key::from_raw(devp, key)) + } +} + +impl<'d> Drop for Key<'d> { + fn drop(&mut self) { + // SAFETY: self.dev/self.key.get() are still valid on drop; the key is unloaded before its storage is zeroized. + unsafe { + sys::wolfTPM2_UnloadHandle(self.dev, &mut (*self.key.get()).handle); + crate::zeroize_raw(&mut *self.key.get()); + } + } +} + +/// A created-but-not-loaded key: the wrapped `pub`/`priv` blob that can be +/// serialized for persistence and later [`load`](KeyBlob::load)ed. +pub struct KeyBlob<'d> { + blob: UnsafeCell, + dev: *mut sys::WOLFTPM2_DEV, + _marker: PhantomData<&'d Device>, +} + +impl<'d> KeyBlob<'d> { + pub(crate) fn from_parts(dev: *mut sys::WOLFTPM2_DEV, blob: sys::WOLFTPM2_KEYBLOB) -> Self { + KeyBlob { + blob: UnsafeCell::new(blob), + dev, + _marker: PhantomData, + } + } + + /// Serialize the (public + encrypted-private) blob to bytes for storage. + pub fn to_bytes(&self) -> Result> { + let mut buf = vec![0u8; core::mem::size_of::() + 32]; + // SAFETY: buf.as_mut_ptr()/buf.len() describe the full buf Vec capacity, and self.blob.get() is the live blob cell. + let n = unsafe { + sys::wolfTPM2_GetKeyBlobAsBuffer(buf.as_mut_ptr(), buf.len() as sys::word32, self.blob.get()) + }; + if n < 0 { + return Err(TpmError(n)); + } + buf.truncate(n as usize); + Ok(buf) + } + + /// Rebuild a blob from [`to_bytes`](KeyBlob::to_bytes) output. + pub fn from_bytes(dev: &'d Device, bytes: &[u8]) -> Result { + // SAFETY: WOLFTPM2_KEYBLOB is a C POD struct; all-zero is a valid starting state for SetKeyBlobFromBuffer to fill. + let mut blob: sys::WOLFTPM2_KEYBLOB = unsafe { core::mem::zeroed() }; + let mut tmp = bytes.to_vec(); + // SAFETY: &mut blob is a valid out-param, and tmp.as_mut_ptr()/tmp.len() describe the live tmp Vec. + let rc = unsafe { + sys::wolfTPM2_SetKeyBlobFromBuffer(&mut blob, tmp.as_mut_ptr(), tmp.len() as sys::word32) + }; + // Scrub the temporary copy on every path. + for b in tmp.iter_mut() { + // SAFETY: b is a valid &mut u8 into the live tmp Vec; the volatile write scrubs it from the compiler's view. + unsafe { core::ptr::write_volatile(b, 0) }; + } + check_rc(rc)?; + Ok(KeyBlob { + blob: UnsafeCell::new(blob), + dev: dev.ptr(), + _marker: PhantomData, + }) + } + + /// Load the blob into the TPM under `parent`, yielding a live [`Key`]. + /// + /// The object's authorization value is **not** part of the serialized blob, + /// so pass the original `auth` (or `None`) to restore it; without it, an + /// auth-protected key would load but fail authorization on first use. + pub fn load(self, parent: &Key<'d>, auth: Option<&[u8]>) -> Result> { + // SAFETY: self.dev/self.blob.get() are live, and parent.handle_ptr() points at parent's own pinned handle. + let rc = unsafe { + sys::wolfTPM2_LoadKey(self.dev, self.blob.get(), parent.handle_ptr()) + }; + check_rc(rc)?; + // The handle now lives in the blob; move handle+public into a Key and + // hand ownership over so only the Key unloads it. + // SAFETY: self.blob.get() is the live blob cell LoadKey just populated. + let blob = unsafe { &*self.blob.get() }; + // SAFETY: WOLFTPM2_KEY is a C POD struct; all-zero is a valid starting state before copying fields from blob. + let mut key: sys::WOLFTPM2_KEY = unsafe { core::mem::zeroed() }; + key.handle = blob.handle; + key.pub_ = blob.pub_; + let out = Key::from_raw(self.dev, key); + if let Some(a) = auth { + out.set_auth_padded(a)?; + } + Ok(out) + } +} + +impl<'d> Drop for KeyBlob<'d> { + fn drop(&mut self) { + // SAFETY: self.blob.get() is still valid on drop, the sole reference to it. + unsafe { crate::zeroize_raw(&mut *self.blob.get()) }; + } +} + +impl Device { + /// Create the endorsement key (EK) under the endorsement hierarchy. + pub fn create_ek(&self, alg: KeyAlg) -> Result> { + // SAFETY: WOLFTPM2_KEY is a C POD struct; all-zero is a valid starting state for CreateEK to fill. + let mut key: sys::WOLFTPM2_KEY = unsafe { core::mem::zeroed() }; + // SAFETY: self.ptr() is the pinned dev pointer and &mut key is a valid exclusive out-param. + let rc = unsafe { sys::wolfTPM2_CreateEK(self.ptr(), &mut key, alg.alg_id()) }; + check_rc(rc)?; + Ok(Key::from_raw(self.ptr(), key)) + } + + /// Create a child key under `parent` from `template`, returning its blob + /// (not yet loaded). Persist with [`KeyBlob::to_bytes`]. + pub fn create_key( + &self, + parent: &Key<'_>, + template: &Template, + auth: Option<&[u8]>, + ) -> Result> { + // SAFETY: WOLFTPM2_KEYBLOB is a C POD struct; all-zero is a valid starting state for CreateKey to fill. + let mut blob: sys::WOLFTPM2_KEYBLOB = unsafe { core::mem::zeroed() }; + let mut tmpl = template.0; + let (authp, authsz) = auth_ptr(auth)?; + // SAFETY: self.ptr()/parent.handle_ptr() are live, &mut blob/&mut tmpl are valid out-params, authp/authsz match (ptr, len) or (null, 0). + let rc = unsafe { + sys::wolfTPM2_CreateKey( + self.ptr(), + &mut blob, + parent.handle_ptr(), + &mut tmpl, + authp, + authsz, + ) + }; + if rc != 0 { + unsafe { crate::zeroize_raw(&mut blob) }; + return Err(TpmError(rc)); + } + Ok(KeyBlob { + blob: UnsafeCell::new(blob), + dev: self.ptr(), + _marker: PhantomData, + }) + } + + /// Create a child key under `parent` and load it in one step. + pub fn create_and_load( + &self, + parent: &Key<'_>, + template: &Template, + auth: Option<&[u8]>, + ) -> Result> { + // SAFETY: WOLFTPM2_KEY is a C POD struct; all-zero is a valid starting state for CreateAndLoadKey to fill. + let mut key: sys::WOLFTPM2_KEY = unsafe { core::mem::zeroed() }; + let mut tmpl = template.0; + let (authp, authsz) = auth_ptr(auth)?; + // SAFETY: self.ptr()/parent.handle_ptr() are live, &mut key/&mut tmpl are valid out-params, authp/authsz match (ptr, len) or (null, 0). + let rc = unsafe { + sys::wolfTPM2_CreateAndLoadKey( + self.ptr(), + &mut key, + parent.handle_ptr(), + &mut tmpl, + authp, + authsz, + ) + }; + if rc != 0 { + unsafe { crate::zeroize_raw(&mut key) }; + return Err(TpmError(rc)); + } + Ok(Key::from_raw(self.ptr(), key)) + } + + /// Import an externally generated RSA private key under `parent`, wrapping + /// it as a TPM key blob. `modulus` is the public modulus, `exponent` the + /// public exponent (e.g. `0x10001`), and `prime` one private prime — the + /// TPM derives the rest of the sensitive area. Persist with + /// [`KeyBlob::to_bytes`], load with [`KeyBlob::load`]. + #[cfg(import)] + pub fn import_rsa_key( + &self, + parent: &Key<'_>, + modulus: &[u8], + exponent: u32, + prime: &[u8], + ) -> Result> { + // SAFETY: WOLFTPM2_KEYBLOB is a C POD struct; all-zero is a valid starting state for ImportRsaPrivateKey to fill. + let mut blob: sys::WOLFTPM2_KEYBLOB = unsafe { core::mem::zeroed() }; + // SAFETY: self.ptr()/parent.kptr() are live, &mut blob is a valid out-param, and modulus/prime ptr+len each describe their live slice. + let rc = unsafe { + sys::wolfTPM2_ImportRsaPrivateKey( + self.ptr(), + parent.kptr() as *const sys::WOLFTPM2_KEY, + &mut blob, + modulus.as_ptr(), + modulus.len() as sys::word32, + exponent as sys::word32, + prime.as_ptr(), + prime.len() as sys::word32, + sys::TPM_ALG_ID_T_TPM_ALG_NULL as sys::TPMI_ALG_RSA_SCHEME, + sys::TPM_ALG_ID_T_TPM_ALG_NULL as sys::TPMI_ALG_HASH, + ) + }; + if rc != 0 { + // The imported private material may be partly copied into `blob` + // before a failure; scrub the stack copy before discarding it. + unsafe { crate::zeroize_raw(&mut blob) }; + return Err(TpmError(rc)); + } + Ok(KeyBlob::from_parts(self.ptr(), blob)) + } + + /// Import an externally generated NIST P-256 ECC private key under `parent`. + /// `x`/`y` are the public point coordinates and `d` the private scalar. + #[cfg(import)] + pub fn import_ecc_key( + &self, + parent: &Key<'_>, + x: &[u8], + y: &[u8], + d: &[u8], + ) -> Result> { + // SAFETY: WOLFTPM2_KEYBLOB is a C POD struct; all-zero is a valid starting state for ImportEccPrivateKey to fill. + let mut blob: sys::WOLFTPM2_KEYBLOB = unsafe { core::mem::zeroed() }; + // SAFETY: self.ptr()/parent.kptr() are live, &mut blob is a valid out-param, and x/y/d ptr+len each describe their live slice. + let rc = unsafe { + sys::wolfTPM2_ImportEccPrivateKey( + self.ptr(), + parent.kptr() as *const sys::WOLFTPM2_KEY, + &mut blob, + sys::TPM_ECC_CURVE_T_TPM_ECC_NIST_P256 as c_int, + x.as_ptr(), + x.len() as sys::word32, + y.as_ptr(), + y.len() as sys::word32, + d.as_ptr(), + d.len() as sys::word32, + ) + }; + if rc != 0 { + // The imported private material may be partly copied into `blob` + // before a failure; scrub the stack copy before discarding it. + unsafe { crate::zeroize_raw(&mut blob) }; + return Err(TpmError(rc)); + } + Ok(KeyBlob::from_parts(self.ptr(), blob)) + } +} + +/// Map an optional auth slice to a `(ptr, len)` pair for the C API. +/// Confirm an ECC template actually resolved to NIST P-256. The C helpers use +/// the build's default ECC curve, which a `NO_ECC256` build makes P-384/P-521 +/// (updating name alg, scheme, and coordinate sizes together). Rather than +/// partially rewrite that into an inconsistent template, fail cleanly so +/// `KeyAlg::EccP256` never silently produces another curve. +fn check_p256(t: &sys::TPMT_PUBLIC, alg: KeyAlg) -> Result<()> { + if alg == KeyAlg::EccP256 { + // SAFETY: the caller ran an ECC template helper (type = ECC), so + // eccDetail is the active variant of the parameters union. + let curve = unsafe { t.parameters.eccDetail.curveID }; + if curve != sys::TPM_ECC_CURVE_T_TPM_ECC_NIST_P256 as sys::TPM_ECC_CURVE { + return Err(TpmError(crate::BUFFER_E)); + } + } + Ok(()) +} + +pub(crate) fn auth_ptr(auth: Option<&[u8]>) -> Result<(*const sys::byte, c_int)> { + match auth { + // A TPM authorization value is at most the largest hash digest (64 bytes + // for SHA-512). Reject anything longer up front so the length can never + // wrap when narrowed to c_int and be misread by the C layer as small or + // negative. + Some(a) if a.len() > MAX_AUTH_LEN => Err(TpmError(crate::BUFFER_E)), + Some(a) => Ok((a.as_ptr(), a.len() as c_int)), + None => Ok((core::ptr::null(), 0)), + } +} + +/// Maximum TPM authorization value length (the SHA-512 digest size). +pub(crate) const MAX_AUTH_LEN: usize = 64; diff --git a/wrapper/rust/wolftpm/src/lib.rs b/wrapper/rust/wolftpm/src/lib.rs new file mode 100644 index 000000000..b7aaed89c --- /dev/null +++ b/wrapper/rust/wolftpm/src/lib.rs @@ -0,0 +1,222 @@ +//! Safe Rust bindings for [wolfTPM](https://github.com/wolfSSL/wolfTPM), the +//! portable TPM 2.0 library. +//! +//! The raw FFI lives in [`sys`]; everything else wraps it in a safe API that +//! turns TPM return codes into [`Result`], manages device and key handle +//! lifetimes with RAII, and keeps all `unsafe` confined to this crate. +//! +//! Backend availability (swtpm socket, fwTPM HAL, sealing, NV, RNG, …) is +//! detected from the linked C library at build time via `cfg` flags emitted by +//! `build.rs`, so the surface reflects how libwolftpm was actually configured. +//! +//! # Safety +//! +//! `Device` boxes its `WOLFTPM2_DEV` in an `UnsafeCell` so it is heap-pinned +//! and self-referential-safe; `Device::ptr()` is therefore a stable pointer +//! for the whole `Device` lifetime, and every wolfTPM2 C call in this crate +//! takes it. `Key`/`KeyBlob` borrow `&Device` and hold their own +//! `UnsafeCell`-wrapped, pinned C struct plus the dev pointer, so their +//! `kptr()`/`handle_ptr()` accessors are likewise stable for as long as the +//! borrow lives. C structs passed to FFI are zero-initialized with +//! `core::mem::zeroed()` first — valid for these C plain-old-data types — and +//! then filled by the callee; buffer copies into fixed C arrays are bounds- +//! checked beforehand so they cannot overflow. Union fields are read only +//! after the code that set the matching selector (an `is_ecc`/scheme flag) ran +//! immediately before. `zeroize_raw` and volatile-write scrubbing always +//! operate on a live, correctly-sized, exclusively-owned local. + +pub mod sys; + +#[cfg(wrapper)] +mod device; +#[cfg(wrapper)] +mod key; +#[cfg(wrapper)] +mod sign; +#[cfg(all(wrapper, seal))] +mod seal; +#[cfg(all(wrapper, nv))] +mod nv; +#[cfg(all(wrapper, pcr))] +mod pcr; +#[cfg(wrapper)] +mod certify; +#[cfg(all(wrapper, rsa))] +mod rsa; +#[cfg(all(wrapper, persist))] +mod persist; +#[cfg(all(wrapper, hmac))] +mod hmac; +#[cfg(wrapper)] +mod session; +#[cfg(all(wrapper, caps))] +mod caps; +#[cfg(all(wrapper, symmetric))] +mod symmetric; +#[cfg(all(wrapper, ecdh))] +mod ecdh; +#[cfg(all(wrapper, ek_policy))] +mod credential; + +#[cfg(wrapper)] +pub use device::Device; +#[cfg(wrapper)] +pub use session::Session; +#[cfg(wrapper)] +pub use key::{HashAlg, Hierarchy, Key, KeyAlg, KeyBlob, Template}; +#[cfg(all(wrapper, nv))] +pub use nv::NvSlot; +#[cfg(wrapper)] +pub use certify::Attestation; +#[cfg(all(wrapper, caps))] +pub use caps::Caps; +#[cfg(all(wrapper, ecdh))] +pub use ecdh::EcdhResult; +#[cfg(all(wrapper, ek_policy))] +pub use credential::Credential; + +use core::fmt; +use std::os::raw::c_int; + +/// A wolfTPM operation that failed, carrying the raw TPM return code. +#[derive(Clone, Copy, PartialEq, Eq)] +pub struct TpmError(pub c_int); + +impl TpmError { + /// The raw TPM/wolfTPM return code (`TPM_RC_*` / wolfCrypt error). + pub fn code(&self) -> c_int { + self.0 + } +} + +impl fmt::Display for TpmError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self.0 { + E_DEVICE_IN_USE => return write!(f, "another wolfTPM Device is already open"), + E_SESSION_IN_USE => { + return write!(f, "an encrypted session is active and conflicts with this operation") + } + BUFFER_E => return write!(f, "buffer size error (BUFFER_E)"), + _ => {} + } + #[cfg(rc_string)] + { + // TPM2_GetRCString returns a static, NUL-terminated string. + // SAFETY: self.0 is a plain integer return code; TPM2_GetRCString has no pointer preconditions. + let p = unsafe { sys::TPM2_GetRCString(self.0) }; + if !p.is_null() { + // SAFETY: p was just checked non-null and points at TPM2_GetRCString's static NUL-terminated string. + let s = unsafe { std::ffi::CStr::from_ptr(p) }; + return write!(f, "{} (0x{:x})", s.to_string_lossy(), self.0); + } + } + write!(f, "TPM error 0x{:x}", self.0) + } +} + +impl fmt::Debug for TpmError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "TpmError(0x{:x})", self.0) + } +} + +impl std::error::Error for TpmError {} + +/// Result of a wolfTPM operation. +pub type Result = core::result::Result; + +/// An owned secret byte buffer that scrubs its heap allocation when dropped. +/// +/// Returned by operations that recover plaintext or key material (unseal, RSA +/// decrypt, ECDH, credential activation) so the secret does not linger in +/// reusable process memory. Deref gives read-only slice access. +pub struct Secret(Vec); + +impl Secret { + pub(crate) fn new(v: Vec) -> Self { + Secret(v) + } + + /// The secret bytes. + pub fn as_bytes(&self) -> &[u8] { + &self.0 + } + + pub fn len(&self) -> usize { + self.0.len() + } + + pub fn is_empty(&self) -> bool { + self.0.is_empty() + } +} + +impl core::ops::Deref for Secret { + type Target = [u8]; + fn deref(&self) -> &[u8] { + &self.0 + } +} + +impl Drop for Secret { + fn drop(&mut self) { + for b in self.0.iter_mut() { + // SAFETY: b is a valid &mut u8 into the live Vec; the volatile write scrubs the secret byte from the compiler's view. + unsafe { core::ptr::write_volatile(b, 0u8) }; + } + core::sync::atomic::compiler_fence(core::sync::atomic::Ordering::SeqCst); + } +} + +/// Turn a wolfTPM C return code into a [`Result`]. `TPM_RC_SUCCESS` is 0. +#[inline] +pub(crate) fn check_rc(rc: c_int) -> Result<()> { + if rc == 0 { + Ok(()) + } else { + Err(TpmError(rc)) + } +} + +/// Narrow a slice length to the C `word32` a wolfTPM API expects, rejecting a +/// value too large to represent rather than letting it wrap to a small count. +#[inline] +pub(crate) fn checked_u32(n: usize) -> Result { + u32::try_from(n).map_err(|_| TpmError(BUFFER_E)) +} + +/// Narrow a slice length to the C `int` a wolfTPM API expects, rejecting a +/// value too large to represent (which would wrap to a negative count). +#[inline] +pub(crate) fn checked_c_int(n: usize) -> Result { + i32::try_from(n).map_err(|_| TpmError(BUFFER_E)) +} + +/// wolfCrypt `BUFFER_E`, used when a caller-supplied buffer is the wrong size. +pub(crate) const BUFFER_E: c_int = -132; + +/// Sentinel: another `Device` is already open. wolfTPM routes commands through a +/// single active context, so only one live `Device` is supported at a time. +pub(crate) const E_DEVICE_IN_USE: c_int = -900; + +/// Sentinel: an encrypted [`Session`] is active. Only one is allowed at a time, +/// and the attestation commands are refused while one holds the auth slot. +pub(crate) const E_SESSION_IN_USE: c_int = -901; + +/// Zeroize an arbitrary FFI struct by raw bytes. +/// +/// `Drop` impls that hold secret material scrub the backing bytes with volatile +/// writes so the compiler cannot elide them (the Rust equivalent of wolfSSL's +/// `ForceZero`). +#[inline] +#[allow(dead_code)] /* used by the sealed-secret modules landing next */ +pub(crate) unsafe fn zeroize_raw(v: &mut T) { + let p = v as *mut T as *mut u8; + let n = core::mem::size_of::(); + let mut i = 0; + while i < n { + core::ptr::write_volatile(p.add(i), 0u8); + i += 1; + } + core::sync::atomic::compiler_fence(core::sync::atomic::Ordering::SeqCst); +} diff --git a/wrapper/rust/wolftpm/src/nv.rs b/wrapper/rust/wolftpm/src/nv.rs new file mode 100644 index 000000000..79903e8b6 --- /dev/null +++ b/wrapper/rust/wolftpm/src/nv.rs @@ -0,0 +1,134 @@ +//! Non-volatile (NV) storage: define an index, write, read, delete. Useful for +//! persisting device-identity metadata / a machine-key handle. +//! +//! These operate under the owner hierarchy assuming it is unauthenticated +//! (empty owner auth), the device-identity case; the per-index `auth` protects +//! the index itself. TPMs with a non-empty owner-hierarchy authorization are +//! not supported here. + +use crate::device::Device; +use crate::key::auth_ptr; +use crate::{check_rc, sys, Result}; + +/// A defined NV index. NV state persists in the TPM until [`Device::nv_delete`]. +pub struct NvSlot<'d> { + nv: sys::WOLFTPM2_NV, + index: u32, + _marker: core::marker::PhantomData<&'d Device>, +} + +impl<'d> NvSlot<'d> { + /// The NV index handle (in the `0x0100_0000`–`0x01FF_FFFF` range). + pub fn index(&self) -> u32 { + self.index + } +} + +fn owner_handle() -> sys::WOLFTPM2_HANDLE { + // SAFETY: WOLFTPM2_HANDLE is a C POD struct; all-zero is a valid state before setting hndl below. + let mut h: sys::WOLFTPM2_HANDLE = unsafe { core::mem::zeroed() }; + h.hndl = sys::TPM_RH_T_TPM_RH_OWNER as sys::TPM_HANDLE; + h +} + +impl Device { + /// Define an NV index of `size` bytes under the owner hierarchy, + /// auth-protected (auth/owner read+write). + pub fn nv_create(&self, index: u32, size: u32, auth: Option<&[u8]>) -> Result> { + let attrs = sys::TPMA_NV_AUTHREAD | sys::TPMA_NV_AUTHWRITE; + // SAFETY: WOLFTPM2_NV is a C POD struct; all-zero is a valid starting state for NVCreateAuth to fill. + let mut nv: sys::WOLFTPM2_NV = unsafe { core::mem::zeroed() }; + let mut parent = owner_handle(); + let (authp, authsz) = auth_ptr(auth)?; + // SAFETY: self.ptr() is live, &mut parent/&mut nv are valid out-params, and authp/authsz match (auth's ptr, len) or (null, 0). + let rc = unsafe { + sys::wolfTPM2_NVCreateAuth( + self.ptr(), + &mut parent, + &mut nv, + index, + attrs, + size, + authp, + authsz, + ) + }; + check_rc(rc)?; + Ok(NvSlot { + nv, + index, + _marker: core::marker::PhantomData, + }) + } + + /// Write `data` to the NV index at `offset`. + pub fn nv_write(&self, slot: &mut NvSlot<'_>, data: &[u8], offset: u32) -> Result<()> { + let mut buf = data.to_vec(); + let n = crate::checked_u32(buf.len())?; + // SAFETY: self.ptr() is live, &mut slot.nv is this slot's own state, and buf.as_mut_ptr()/len describe the live buf Vec. + let rc = unsafe { + sys::wolfTPM2_NVWriteAuth( + self.ptr(), + &mut slot.nv, + slot.index, + buf.as_mut_ptr(), + n, + offset, + ) + }; + for b in buf.iter_mut() { + // SAFETY: b is a valid &mut u8 into the live buf Vec; the volatile write scrubs the temporary copy. + unsafe { core::ptr::write_volatile(b, 0) }; + } + check_rc(rc) + } + + /// Read up to `buf.len()` bytes from the NV index at `offset`, returning the + /// number of bytes read. + pub fn nv_read(&self, slot: &mut NvSlot<'_>, buf: &mut [u8], offset: u32) -> Result { + let mut sz = crate::checked_u32(buf.len())?; + // SAFETY: self.ptr() is live, &mut slot.nv is this slot's own state, and buf.as_mut_ptr()/&mut sz describe the full buf capacity. + let rc = unsafe { + sys::wolfTPM2_NVReadAuth( + self.ptr(), + &mut slot.nv, + slot.index, + buf.as_mut_ptr(), + &mut sz, + offset, + ) + }; + check_rc(rc)?; + Ok(sz as usize) + } + + /// Undefine (delete) an NV index. + pub fn nv_delete(&self, index: u32) -> Result<()> { + let mut parent = owner_handle(); + // SAFETY: self.ptr() is live and &mut parent is a valid, exclusively-borrowed handle for NVDeleteAuth. + let rc = unsafe { sys::wolfTPM2_NVDeleteAuth(self.ptr(), &mut parent, index) }; + check_rc(rc) + } + + /// Read a certificate stored in an NV index, such as the manufacturer's EK + /// certificate (RSA EK cert at `0x01C00002`, ECC at `0x01C0000A`). Returns + /// the raw certificate (DER) bytes. + #[cfg(nvcert)] + pub fn read_cert(&self, nv_handle: u32) -> Result> { + let mut buf = vec![0u8; 2048]; + let mut len = buf.len() as u32; + // SAFETY: self.ptr() is live and buf.as_mut_ptr()/&mut len describe the full buf Vec capacity. + let rc = + unsafe { sys::wolfTPM2_NVReadCert(self.ptr(), nv_handle, buf.as_mut_ptr(), &mut len) }; + check_rc(rc)?; + buf.truncate(len as usize); + Ok(buf) + } +} + +impl<'d> Drop for NvSlot<'d> { + fn drop(&mut self) { + // SAFETY: self.nv is a live, uniquely-owned field being scrubbed as the slot is dropped. + unsafe { crate::zeroize_raw(&mut self.nv) }; + } +} diff --git a/wrapper/rust/wolftpm/src/pcr.rs b/wrapper/rust/wolftpm/src/pcr.rs new file mode 100644 index 000000000..bbc1b5193 --- /dev/null +++ b/wrapper/rust/wolftpm/src/pcr.rs @@ -0,0 +1,47 @@ +//! PCR (Platform Configuration Register) read and extend — the measured-boot / +//! attestation primitives. + +use crate::device::Device; +use crate::key::HashAlg; +use crate::{check_rc, sys, Result, TpmError}; +use core::ffi::c_int; + +impl Device { + /// Read the current value of PCR `index` in the given hash bank. + pub fn pcr_read(&self, index: u32, hash: HashAlg) -> Result> { + let mut digest = vec![0u8; 64]; + let mut len = digest.len() as c_int; + // SAFETY: self.ptr() is live and digest.as_mut_ptr()/&mut len describe the full digest Vec capacity. + let rc = unsafe { + sys::wolfTPM2_ReadPCR( + self.ptr(), + index as c_int, + hash.as_c_int(), + digest.as_mut_ptr(), + &mut len, + ) + }; + check_rc(rc)?; + digest.truncate(len as usize); + Ok(digest) + } + + /// Extend PCR `index` in the given hash bank with `digest`, which must be + /// exactly the bank's digest length (32/48/64 bytes for SHA-256/384/512). + pub fn pcr_extend(&self, index: u32, hash: HashAlg, digest: &[u8]) -> Result<()> { + if digest.len() != hash.digest_size() { + return Err(TpmError(crate::BUFFER_E)); + } + // SAFETY: self.ptr() is live and digest ptr+len bound the slice, already checked above to match the bank's digest size. + let rc = unsafe { + sys::wolfTPM2_ExtendPCR( + self.ptr(), + index as c_int, + hash.as_c_int(), + digest.as_ptr(), + digest.len() as c_int, + ) + }; + check_rc(rc) + } +} diff --git a/wrapper/rust/wolftpm/src/persist.rs b/wrapper/rust/wolftpm/src/persist.rs new file mode 100644 index 000000000..b09d23555 --- /dev/null +++ b/wrapper/rust/wolftpm/src/persist.rs @@ -0,0 +1,60 @@ +//! Persistent key handles: move a key into the TPM's non-volatile store so it +//! survives a reboot, read it back, and evict it. This is how a machine key or +//! root storage key is kept across boots. +//! +//! Persisting and evicting run under the given hierarchy assuming it is +//! unauthenticated (empty owner / platform auth), the device-identity case. +//! TPMs with a non-empty hierarchy authorization are not supported here. + +use crate::device::Device; +use crate::key::{Hierarchy, Key}; +use crate::{check_rc, sys, Result}; + +impl Device { + /// Store `key` at `persistent_handle` (a value in the + /// `0x8100_0000`..`0x81FF_FFFF` range) under `hierarchy`. After this the key + /// survives a reboot. `key`'s live handle becomes the persistent handle. + pub fn persist_key( + &self, + key: &Key<'_>, + hierarchy: Hierarchy, + persistent_handle: u32, + ) -> Result<()> { + // SAFETY: self.ptr() is live and key.kptr() addresses the live, loaded key being persisted. + let rc = unsafe { + sys::wolfTPM2_NVStoreKey(self.ptr(), hierarchy.handle(), key.kptr(), persistent_handle) + }; + check_rc(rc) + } + + /// Read a key already persisted at `persistent_handle`. `auth` restores the + /// object's authorization value (the same one it was created with) so the + /// returned key can perform private operations; pass `None` for an + /// unauthenticated key. Reading recovers only the public area, so the auth + /// must be supplied here to sign or decrypt with a protected key. + pub fn read_persistent(&self, persistent_handle: u32, auth: Option<&[u8]>) -> Result> { + // SAFETY: WOLFTPM2_KEY is a C POD struct; all-zero is a valid starting state for ReadPublicKey to fill. + let mut key: sys::WOLFTPM2_KEY = unsafe { core::mem::zeroed() }; + // SAFETY: self.ptr() is live and &mut key is a valid, exclusively-borrowed out-param. + let rc = unsafe { sys::wolfTPM2_ReadPublicKey(self.ptr(), &mut key, persistent_handle) }; + check_rc(rc)?; + let k = Key::from_raw(self.ptr(), key); + if let Some(a) = auth { + k.set_auth_padded(a)?; + } + Ok(k) + } + + /// Evict (remove) a persistent key from the TPM's non-volatile store. `key` + /// must refer to the persistent handle (for example from [`persist_key`] or + /// [`read_persistent`]). + /// + /// [`persist_key`]: Device::persist_key + /// [`read_persistent`]: Device::read_persistent + pub fn evict_key(&self, key: &Key<'_>, hierarchy: Hierarchy) -> Result<()> { + // SAFETY: self.ptr() is live and key.kptr() addresses the persistent key handle being evicted. + let rc = + unsafe { sys::wolfTPM2_NVDeleteKey(self.ptr(), hierarchy.handle(), key.kptr()) }; + check_rc(rc) + } +} diff --git a/wrapper/rust/wolftpm/src/rsa.rs b/wrapper/rust/wolftpm/src/rsa.rs new file mode 100644 index 000000000..92e59f062 --- /dev/null +++ b/wrapper/rust/wolftpm/src/rsa.rs @@ -0,0 +1,124 @@ +//! RSA-OAEP encrypt and decrypt on a loaded RSA decryption key. This is the +//! primitive the Microsoft device-enrollment path uses to wrap and unwrap +//! session keys. + +use crate::key::{HashAlg, Key}; +use crate::{check_rc, sys, Result, Secret, TpmError}; +use core::ffi::c_int; + +impl<'d> Key<'d> { + /// Encrypt `msg` to this key's public part with RSA-OAEP. + pub fn rsa_encrypt(&self, msg: &[u8]) -> Result> { + let msg_len = crate::checked_c_int(msg.len())?; + let mut out = vec![0u8; sys::MAX_RSA_KEY_BYTES as usize]; + let mut out_sz = out.len() as c_int; + // SAFETY: self.dev()/self.kptr() are live, msg ptr+len bound its slice, and out.as_mut_ptr()/out_sz describe the full out capacity. + let rc = unsafe { + sys::wolfTPM2_RsaEncrypt( + self.dev(), + self.kptr(), + sys::TPM_ALG_ID_T_TPM_ALG_OAEP as sys::TPM_ALG_ID, + msg.as_ptr(), + msg_len, + out.as_mut_ptr(), + &mut out_sz, + ) + }; + check_rc(rc)?; + out.truncate(out_sz as usize); + Ok(out) + } + + /// Decrypt an RSA-OAEP ciphertext with this key's private part. + pub fn rsa_decrypt(&self, ciphertext: &[u8]) -> Result { + let ct_len = crate::checked_c_int(ciphertext.len())?; + let mut out = vec![0u8; sys::MAX_RSA_KEY_BYTES as usize]; + let mut out_sz = out.len() as c_int; + // SAFETY: self.dev()/self.kptr() are live, ciphertext ptr+len bound its slice, and out.as_mut_ptr()/out_sz describe the full out capacity. + let rc = unsafe { + sys::wolfTPM2_RsaDecrypt( + self.dev(), + self.kptr(), + sys::TPM_ALG_ID_T_TPM_ALG_OAEP as sys::TPM_ALG_ID, + ciphertext.as_ptr(), + ct_len, + out.as_mut_ptr(), + &mut out_sz, + ) + }; + check_rc(rc)?; + out.truncate(out_sz as usize); + Ok(Secret::new(out)) + } + + /// Encrypt `msg` with RSA-OAEP using an explicit label-hash algorithm. + /// + /// The plain [`rsa_encrypt`](Key::rsa_encrypt) uses the TPM's default OAEP + /// hash (SHA-256). This variant issues the low-level command directly so any + /// hash the TPM supports can be selected, including `HashAlg::Sha1`, which + /// Microsoft device enrollment (MS-OAPXBC) requires for its session-key + /// wrap. SHA-1 is legacy and cryptographically weak; select it only for that + /// interop, not for new designs. + pub fn rsa_encrypt_with_hash(&self, msg: &[u8], hash: HashAlg) -> Result> { + // SAFETY: RSA_Encrypt_In is a C POD struct; all-zero is a valid starting state, and the length check above bounds the copy into cin.message.buffer. + let mut cin: sys::RSA_Encrypt_In = unsafe { core::mem::zeroed() }; + if msg.len() > cin.message.buffer.len() { + return Err(TpmError(crate::BUFFER_E)); + } + // SAFETY: self.dev() is live and self.handle_ptr() addresses this Key's own pinned handle. + unsafe { sys::wolfTPM2_SetAuthHandle(self.dev(), 0, self.handle_ptr()) }; + cin.keyHandle = self.handle(); + cin.message.size = msg.len() as u16; + cin.message.buffer[..msg.len()].copy_from_slice(msg); + cin.inScheme.scheme = sys::TPM_ALG_ID_T_TPM_ALG_OAEP as sys::TPMI_ALG_RSA_DECRYPT; + cin.inScheme.details.anySig.hashAlg = hash.alg_id() as sys::TPMI_ALG_HASH; + + // SAFETY: RSA_Encrypt_Out is a C POD struct; all-zero is a valid starting state for TPM2_RSA_Encrypt to fill. + let mut cout: sys::RSA_Encrypt_Out = unsafe { core::mem::zeroed() }; + // SAFETY: &mut cin/&mut cout are valid, exclusively-borrowed in/out-params; cin.inScheme.details.anySig was just set to match the OAEP scheme above. + let rc = unsafe { sys::TPM2_RSA_Encrypt(&mut cin, &mut cout) }; + // SAFETY: self.dev() is live; clear the auth slot and scrub the plaintext + // copy left in cin, on both the success and error paths. + unsafe { + sys::wolfTPM2_UnsetAuth(self.dev(), 0); + crate::zeroize_raw(&mut cin); + } + check_rc(rc)?; + let n = cout.outData.size as usize; + Ok(cout.outData.buffer[..n].to_vec()) + } + + /// Decrypt an RSA-OAEP ciphertext using an explicit label-hash algorithm + /// (see [`rsa_encrypt_with_hash`](Key::rsa_encrypt_with_hash)). Supports + /// `HashAlg::Sha1` for Microsoft enrollment interop. + pub fn rsa_decrypt_with_hash(&self, ciphertext: &[u8], hash: HashAlg) -> Result { + // SAFETY: RSA_Decrypt_In is a C POD struct; all-zero is a valid starting state, and the length check above bounds the copy into cin.cipherText.buffer. + let mut cin: sys::RSA_Decrypt_In = unsafe { core::mem::zeroed() }; + if ciphertext.len() > cin.cipherText.buffer.len() { + return Err(TpmError(crate::BUFFER_E)); + } + // SAFETY: self.dev() is live and self.handle_ptr() addresses this Key's own pinned handle. + unsafe { sys::wolfTPM2_SetAuthHandle(self.dev(), 0, self.handle_ptr()) }; + cin.keyHandle = self.handle(); + cin.cipherText.size = ciphertext.len() as u16; + cin.cipherText.buffer[..ciphertext.len()].copy_from_slice(ciphertext); + cin.inScheme.scheme = sys::TPM_ALG_ID_T_TPM_ALG_OAEP as sys::TPMI_ALG_RSA_DECRYPT; + cin.inScheme.details.anySig.hashAlg = hash.alg_id() as sys::TPMI_ALG_HASH; + + // SAFETY: RSA_Decrypt_Out is a C POD struct; all-zero is a valid starting state for TPM2_RSA_Decrypt to fill. + let mut cout: sys::RSA_Decrypt_Out = unsafe { core::mem::zeroed() }; + // SAFETY: &mut cin/&mut cout are valid, exclusively-borrowed in/out-params; cin.inScheme.details.anySig was just set to match the OAEP scheme above. + let rc = unsafe { sys::TPM2_RSA_Decrypt(&mut cin, &mut cout) }; + // SAFETY: self.dev() is live; this clears the auth slot set above regardless of the decrypt outcome. + unsafe { sys::wolfTPM2_UnsetAuth(self.dev(), 0) }; + let out = if rc == 0 { + cout.message.buffer[..cout.message.size as usize].to_vec() + } else { + Vec::new() + }; + // SAFETY: cout is a live, fully-owned local; zeroizing it after copying out scrubs the recovered plaintext. + unsafe { crate::zeroize_raw(&mut cout) }; + check_rc(rc)?; + Ok(Secret::new(out)) + } +} diff --git a/wrapper/rust/wolftpm/src/seal.rs b/wrapper/rust/wolftpm/src/seal.rs new file mode 100644 index 000000000..5a09026a6 --- /dev/null +++ b/wrapper/rust/wolftpm/src/seal.rs @@ -0,0 +1,279 @@ +//! Seal a secret to the TPM and unseal it back (keyed-hash sealed objects). + +use crate::device::Device; +use crate::key::{auth_ptr, Key, KeyBlob}; +use crate::{check_rc, sys, Result, Secret, TpmError}; +use core::ffi::c_int; + +/// Number of PCRs a TPM 2.0 implementation exposes; valid indices are `0..24`. +const PCR_COUNT: u8 = 24; + +/// Reject an empty or out-of-range PCR selection, which would otherwise bind a +/// sealed object to fewer PCRs than the caller asked for (silently dropped by +/// the TPM's selection builder). +fn check_pcr_indices(pcr_indices: &[u8]) -> Result<()> { + if pcr_indices.is_empty() || pcr_indices.iter().any(|&i| i >= PCR_COUNT) { + return Err(TpmError(crate::BUFFER_E)); + } + Ok(()) +} + +impl Device { + /// Seal `data` under `parent`, optionally protected by `auth`. Returns the + /// sealed blob (persist it with [`KeyBlob::to_bytes`](crate::KeyBlob::to_bytes)). + pub fn seal(&self, parent: &Key<'_>, data: &[u8], auth: Option<&[u8]>) -> Result> { + // SAFETY: TPMT_PUBLIC is a C POD struct; all-zero is a valid starting state for the seal template helper. + let mut tmpl: sys::TPMT_PUBLIC = unsafe { core::mem::zeroed() }; + // SAFETY: &mut tmpl is a valid, exclusively-borrowed out-param for wolfTPM2_GetKeyTemplate_KeySeal. + let rc = unsafe { + sys::wolfTPM2_GetKeyTemplate_KeySeal( + &mut tmpl, + sys::TPM_ALG_ID_T_TPM_ALG_SHA256 as sys::TPM_ALG_ID, + ) + }; + check_rc(rc)?; + + // SAFETY: WOLFTPM2_KEYBLOB is a C POD struct; all-zero is a valid starting state for CreateKeySeal to fill. + let data_len = crate::checked_c_int(data.len())?; + let mut blob: sys::WOLFTPM2_KEYBLOB = unsafe { core::mem::zeroed() }; + let (authp, authsz) = auth_ptr(auth)?; + // SAFETY: self.ptr()/parent.handle_ptr() are live, &mut blob/&mut tmpl are valid out-params, authp/authsz match (ptr, len) or (null, 0), and data ptr+len bound its slice. + let rc = unsafe { + sys::wolfTPM2_CreateKeySeal( + self.ptr(), + &mut blob, + parent.handle_ptr(), + &mut tmpl, + authp, + authsz, + data.as_ptr(), + data_len, + ) + }; + check_rc(rc)?; + + // Keep only the persistable pub/priv blob; drop any transient handle so + // unseal re-loads it fresh under the parent. + if blob.handle.hndl != 0 { + // SAFETY: self.ptr() is live and &mut blob.handle addresses the transient handle CreateKeySeal returned. + unsafe { sys::wolfTPM2_UnloadHandle(self.ptr(), &mut blob.handle) }; + blob.handle.hndl = 0; + } + Ok(KeyBlob::from_parts(self.ptr(), blob)) + } + + /// Load a `sealed` blob under `parent` and release its secret. Requires the + /// same `auth` the blob was sealed with. + pub fn unseal( + &self, + sealed: KeyBlob<'_>, + parent: &Key<'_>, + auth: Option<&[u8]>, + ) -> Result { + let key = sealed.load(parent, None)?; + if let Some(a) = auth { + key.set_auth(a)?; + } + // SAFETY: self.ptr() is the pinned dev pointer and key.handle_ptr() addresses the just-loaded key's live handle. + unsafe { sys::wolfTPM2_SetAuthHandle(self.ptr(), 0, key.handle_ptr()) }; + + // SAFETY: Unseal_In/Unseal_Out are C POD structs; all-zero is a valid starting state for TPM2_Unseal to fill. + let mut cmd_in: sys::Unseal_In = unsafe { core::mem::zeroed() }; + cmd_in.itemHandle = key.handle(); + // SAFETY: Unseal_Out is a C POD struct; all-zero is a valid starting state for TPM2_Unseal to fill. + let mut cmd_out: sys::Unseal_Out = unsafe { core::mem::zeroed() }; + // SAFETY: &mut cmd_in/&mut cmd_out are valid, exclusively-borrowed in/out-params for TPM2_Unseal. + let rc = unsafe { sys::TPM2_Unseal(&mut cmd_in, &mut cmd_out) }; + // SAFETY: self.ptr() is live; this clears the auth slot set above regardless of the unseal outcome. + unsafe { sys::wolfTPM2_UnsetAuth(self.ptr(), 0) }; + + let out = if rc == 0 { + let n = cmd_out.outData.size as usize; + cmd_out.outData.buffer[..n].to_vec() + } else { + Vec::new() + }; + // SAFETY: cmd_out is a live, fully-owned local; zeroizing it after copying out scrubs the returned secret bytes. + unsafe { crate::zeroize_raw(&mut cmd_out) }; + check_rc(rc)?; + Ok(Secret::new(out)) + } + + /// Seal `data` under `parent`, bound to the current values of the PCRs in + /// `pcr_indices` (SHA-256 bank). Unsealing later requires those PCRs to + /// still hold the same values (measured-boot binding). + /// + /// Access is gated solely by the PCR policy: there is deliberately no auth + /// value, since a PCR-policy object clears `userWithAuth` and any auth would + /// not be enforced on unseal. + pub fn seal_pcr(&self, parent: &Key<'_>, data: &[u8], pcr_indices: &[u8]) -> Result> { + check_pcr_indices(pcr_indices)?; + let sha256 = sys::TPM_ALG_ID_T_TPM_ALG_SHA256 as sys::TPM_ALG_ID; + let mut pcrs = pcr_indices.to_vec(); + + // Compute the PCR policy digest exactly as unseal will, using a trial + // session, so the sealed authPolicy matches what PolicyPCR produces. + // SAFETY: WOLFTPM2_SESSION is a C POD struct; all-zero is a valid starting state for StartSession to fill. + let mut trial: sys::WOLFTPM2_SESSION = unsafe { core::mem::zeroed() }; + // SAFETY: self.ptr() is live and &mut trial is a valid exclusive out-param. + let rc = unsafe { + sys::wolfTPM2_StartSession( + self.ptr(), + &mut trial, + core::ptr::null_mut(), + core::ptr::null_mut(), + sys::TPM_SE_T_TPM_SE_TRIAL as sys::TPM_SE, + sys::TPM_ALG_ID_T_TPM_ALG_NULL as c_int, + ) + }; + check_rc(rc)?; + // SAFETY: trial.handle.hndl is the session StartSession just opened, and pcrs ptr+len bound the live Vec. + let policy_rc = unsafe { + sys::wolfTPM2_PolicyPCR( + self.ptr(), + trial.handle.hndl, + sha256, + pcrs.as_mut_ptr(), + pcrs.len() as sys::word32, + ) + }; + let mut policy = vec![0u8; 64]; + let mut policy_sz = policy.len() as sys::word32; + // SAFETY: policy.as_mut_ptr()/policy_sz describe the full policy Vec capacity, and trial.handle.hndl is still live. + let digest_rc = unsafe { + sys::wolfTPM2_GetPolicyDigest( + self.ptr(), + trial.handle.hndl, + policy.as_mut_ptr(), + &mut policy_sz, + ) + }; + // SAFETY: the trial session's policy digest has been read; this unloads it regardless of the calls' outcomes. + unsafe { sys::wolfTPM2_UnloadHandle(self.ptr(), &mut trial.handle) }; + check_rc(policy_rc)?; + check_rc(digest_rc)?; + policy.truncate(policy_sz as usize); + + // SAFETY: TPMT_PUBLIC is a C POD struct; all-zero is a valid starting state for the seal template helper. + let mut tmpl: sys::TPMT_PUBLIC = unsafe { core::mem::zeroed() }; + // SAFETY: &mut tmpl is a valid, exclusively-borrowed out-param for wolfTPM2_GetKeyTemplate_KeySeal. + let rc = unsafe { sys::wolfTPM2_GetKeyTemplate_KeySeal(&mut tmpl, sha256) }; + check_rc(rc)?; + // Policy-only access: clear userWithAuth so the PCR policy is required. + tmpl.objectAttributes &= + !(sys::TPMA_OBJECT_mask_TPMA_OBJECT_userWithAuth as sys::TPMA_OBJECT); + tmpl.authPolicy.size = policy.len() as u16; + tmpl.authPolicy.buffer[..policy.len()].copy_from_slice(&policy); + + // SAFETY: WOLFTPM2_KEYBLOB is a C POD struct; all-zero is a valid starting state for CreateKeySeal_ex to fill. + let data_len = crate::checked_c_int(data.len())?; + let mut blob: sys::WOLFTPM2_KEYBLOB = unsafe { core::mem::zeroed() }; + let (authp, authsz) = auth_ptr(None)?; + // SAFETY: self.ptr()/parent.handle_ptr() are live, &mut blob/&mut tmpl are valid out-params, authp/authsz is (null, 0), and data ptr+len bound its slice. + let rc = unsafe { + sys::wolfTPM2_CreateKeySeal_ex( + self.ptr(), + &mut blob, + parent.handle_ptr(), + &mut tmpl, + authp, + authsz, + sha256, + core::ptr::null_mut(), + 0, + data.as_ptr(), + data_len, + ) + }; + check_rc(rc)?; + if blob.handle.hndl != 0 { + // SAFETY: self.ptr() is live and &mut blob.handle addresses the transient handle CreateKeySeal_ex returned. + unsafe { sys::wolfTPM2_UnloadHandle(self.ptr(), &mut blob.handle) }; + blob.handle.hndl = 0; + } + Ok(KeyBlob::from_parts(self.ptr(), blob)) + } + + /// Unseal a PCR-bound blob. Succeeds only if the PCRs in `pcr_indices` still + /// hold the values they had when [`seal_pcr`](Device::seal_pcr) ran. + pub fn unseal_pcr( + &self, + sealed: KeyBlob<'_>, + parent: &Key<'_>, + pcr_indices: &[u8], + ) -> Result { + check_pcr_indices(pcr_indices)?; + let key = sealed.load(parent, None)?; + + // Policy session, satisfy the PCR policy, register it, then bind the + // sealed object's name for the session HMAC before unsealing. + // SAFETY: WOLFTPM2_SESSION is a C POD struct; all-zero is a valid starting state for StartSession to fill. + let mut session: sys::WOLFTPM2_SESSION = unsafe { core::mem::zeroed() }; + // SAFETY: self.ptr() is live and &mut session is a valid exclusive out-param. + let rc = unsafe { + sys::wolfTPM2_StartSession( + self.ptr(), + &mut session, + core::ptr::null_mut(), + core::ptr::null_mut(), + sys::TPM_SE_T_TPM_SE_POLICY as sys::TPM_SE, + sys::TPM_ALG_ID_T_TPM_ALG_NULL as c_int, + ) + }; + check_rc(rc)?; + + let mut pcrs = pcr_indices.to_vec(); + // SAFETY: session.handle.hndl is the session StartSession just opened, and pcrs ptr+len bound the live Vec. + let policy_rc = unsafe { + sys::wolfTPM2_PolicyPCR( + self.ptr(), + session.handle.hndl, + sys::TPM_ALG_ID_T_TPM_ALG_SHA256 as sys::TPM_ALG_ID, + pcrs.as_mut_ptr(), + pcrs.len() as sys::word32, + ) + }; + // SAFETY: &mut session still refers to the live session opened above. + let set_rc = unsafe { + sys::wolfTPM2_SetAuthSession( + self.ptr(), + 0, + &mut session, + sys::TPMA_SESSION_mask_TPMA_SESSION_continueSession as sys::TPMA_SESSION, + ) + }; + // SAFETY: self.ptr() is live and key.handle_ptr() addresses the just-loaded key's own handle. + unsafe { sys::wolfTPM2_SetAuthHandleName(self.ptr(), 0, key.handle_ptr()) }; + + // SAFETY: Unseal_In/Unseal_Out are C POD structs; all-zero is a valid starting state for TPM2_Unseal to fill. + let mut cmd_in: sys::Unseal_In = unsafe { core::mem::zeroed() }; + cmd_in.itemHandle = key.handle(); + // SAFETY: Unseal_Out is a C POD struct; all-zero is a valid starting state for TPM2_Unseal to fill. + let mut cmd_out: sys::Unseal_Out = unsafe { core::mem::zeroed() }; + let unseal_rc = if policy_rc != 0 { + policy_rc + } else if set_rc != 0 { + set_rc + } else { + // SAFETY: &mut cmd_in/&mut cmd_out are valid, exclusively-borrowed in/out-params, reached only once policy/session setup succeeded. + unsafe { sys::TPM2_Unseal(&mut cmd_in, &mut cmd_out) } + }; + + // SAFETY: self.ptr() is live; this clears the auth slot and unloads the policy session regardless of the unseal outcome. + unsafe { + sys::wolfTPM2_UnsetAuth(self.ptr(), 0); + sys::wolfTPM2_UnloadHandle(self.ptr(), &mut session.handle); + } + + let out = if unseal_rc == 0 { + let n = cmd_out.outData.size as usize; + cmd_out.outData.buffer[..n].to_vec() + } else { + Vec::new() + }; + // SAFETY: cmd_out is a live, fully-owned local; zeroizing it after copying out scrubs the returned secret bytes. + unsafe { crate::zeroize_raw(&mut cmd_out) }; + check_rc(unseal_rc)?; + Ok(Secret::new(out)) + } +} diff --git a/wrapper/rust/wolftpm/src/session.rs b/wrapper/rust/wolftpm/src/session.rs new file mode 100644 index 000000000..5bccf63e7 --- /dev/null +++ b/wrapper/rust/wolftpm/src/session.rs @@ -0,0 +1,113 @@ +//! Parameter-encryption sessions. While a [`Session`] is alive, command and +//! response parameters of the secret-bearing operations (seal/unseal, RSA and +//! AES encrypt/decrypt, HMAC, NV, ECDH, key create/load) travel encrypted over +//! the TPM transport instead of in the clear. + +use crate::device::Device; +use crate::key::Key; +use crate::{sys, Result, TpmError, E_SESSION_IN_USE}; +use core::ffi::c_int; +use core::marker::PhantomData; +use core::sync::atomic::{AtomicBool, Ordering}; + +/// Auth slot the encryption session occupies. It sits at slot 1, immediately +/// after a command's own object authorization at slot 0. wolfTPM's command +/// builder (`TPM2_GetCmdAuthCount`) then includes it as an encrypt/decrypt +/// session for every parameter-encryption-capable command, with no gap that +/// would drop it. The 2-auth attestation commands (certify, quote, activate) +/// need slot 1 for their second handle, so they are refused while a session is +/// live rather than silently displacing it. +const SESSION_SLOT: c_int = 1; + +/// Only one encryption session may be live at a time; it holds slot 1 for its +/// whole lifetime so wolfTPM can roll its nonce across commands. +static SESSION_ACTIVE: AtomicBool = AtomicBool::new(false); + +/// Whether an encryption session currently holds the auth slot. Used by the +/// attestation commands to refuse rather than clobber it. +pub(crate) fn is_active() -> bool { + SESSION_ACTIVE.load(Ordering::Acquire) +} + +/// A salted HMAC session with AES-CFB parameter encryption, registered at auth +/// slot 1 for its whole lifetime. Once started, the secret-bearing operations +/// have their sensitive command/response parameters encrypted automatically. +/// Only one is allowed at a time; it is closed and its slot released on drop. +pub struct Session<'d> { + session: sys::WOLFTPM2_SESSION, + dev: *mut sys::WOLFTPM2_DEV, + _marker: PhantomData<&'d Device>, +} + +impl Device { + /// Start a salted HMAC session with AES-CFB parameter encryption, keyed by + /// `salt` (typically the SRK), and register it so the following + /// secret-bearing commands are encrypted. Returns an error if a session is + /// already active. + /// + /// While it is alive, the attestation commands [`certify`](Device::certify), + /// [`quote`](Device::quote), and + /// [`activate_credential`](Device::activate_credential) are refused, since + /// they need the same auth slot; drop the session before calling them. + pub fn start_encrypted_session(&self, salt: &Key<'_>) -> Result> { + if SESSION_ACTIVE + .compare_exchange(false, true, Ordering::AcqRel, Ordering::Acquire) + .is_err() + { + return Err(TpmError(E_SESSION_IN_USE)); + } + // SAFETY: WOLFTPM2_SESSION is a C POD struct; all-zero is a valid starting state for StartSession to fill. + let mut session: sys::WOLFTPM2_SESSION = unsafe { core::mem::zeroed() }; + // SAFETY: self.ptr() is live, &mut session is a valid out-param, and salt.kptr() addresses the live salt key. + let rc = unsafe { + sys::wolfTPM2_StartSession( + self.ptr(), + &mut session, + salt.kptr(), + core::ptr::null_mut(), + sys::TPM_SE_T_TPM_SE_HMAC as sys::TPM_SE, + sys::TPM_ALG_ID_T_TPM_ALG_CFB as c_int, + ) + }; + if rc != 0 { + SESSION_ACTIVE.store(false, Ordering::Release); + return Err(TpmError(rc)); + } + + let attrs = (sys::TPMA_SESSION_mask_TPMA_SESSION_continueSession + | sys::TPMA_SESSION_mask_TPMA_SESSION_decrypt + | sys::TPMA_SESSION_mask_TPMA_SESSION_encrypt) as sys::TPMA_SESSION; + // SAFETY: &mut session still refers to the live session opened above. + let rc = + unsafe { sys::wolfTPM2_SetAuthSession(self.ptr(), SESSION_SLOT, &mut session, attrs) }; + if rc != 0 { + // SAFETY: self.ptr() is live; unload the session and scrub its key + // material before releasing the slot. + unsafe { + sys::wolfTPM2_UnloadHandle(self.ptr(), &mut session.handle); + crate::zeroize_raw(&mut session); + } + SESSION_ACTIVE.store(false, Ordering::Release); + return Err(TpmError(rc)); + } + + Ok(Session { + session, + dev: self.ptr(), + _marker: PhantomData, + }) + } +} + +impl<'d> Drop for Session<'d> { + fn drop(&mut self) { + // SAFETY: self.dev is still valid on drop; clear the auth slot, flush the + // TPM session, then scrub the derived key/nonces left in the struct. + unsafe { + sys::wolfTPM2_UnsetAuth(self.dev, SESSION_SLOT); + sys::wolfTPM2_UnloadHandle(self.dev, &mut self.session.handle); + crate::zeroize_raw(&mut self.session); + } + SESSION_ACTIVE.store(false, Ordering::Release); + } +} diff --git a/wrapper/rust/wolftpm/src/sign.rs b/wrapper/rust/wolftpm/src/sign.rs new file mode 100644 index 000000000..f8bc11b05 --- /dev/null +++ b/wrapper/rust/wolftpm/src/sign.rs @@ -0,0 +1,53 @@ +//! Signing and verification on a loaded [`Key`]. + +use crate::key::Key; +use crate::{check_rc, sys, Result}; +use core::ffi::c_int; + +impl<'d> Key<'d> { + /// Sign a pre-computed `digest` with this key, returning the raw signature. + /// + /// The scheme/hash come from the key's template (e.g. ECDSA-SHA256 for a + /// P-256 signing key), so `digest` must be the matching hash length. + pub fn sign_hash(&self, digest: &[u8]) -> Result> { + let digest_len = crate::checked_c_int(digest.len())?; + let mut sig = vec![0u8; sys::MAX_RSA_KEY_BYTES as usize]; + let mut sig_sz = sig.len() as c_int; + // SAFETY: self.dev()/self.kptr() are live, digest ptr+len bound its slice, and sig.as_mut_ptr()/sig_sz describe the full sig capacity. + let rc = unsafe { + sys::wolfTPM2_SignHash( + self.dev(), + self.kptr(), + digest.as_ptr(), + digest_len, + sig.as_mut_ptr(), + &mut sig_sz, + ) + }; + // wolfTPM2_SignHash caches the key auth in the device's slot 0; clear it + // so it does not linger in the live Device after this returns. + // SAFETY: self.dev() is the live pinned device pointer. + unsafe { sys::wolfTPM2_UnsetAuth(self.dev(), 0) }; + check_rc(rc)?; + sig.truncate(sig_sz as usize); + Ok(sig) + } + + /// Verify `sig` over `digest` with this key. + pub fn verify_hash(&self, digest: &[u8], sig: &[u8]) -> Result<()> { + let sig_len = crate::checked_c_int(sig.len())?; + let digest_len = crate::checked_c_int(digest.len())?; + // SAFETY: self.dev()/self.kptr() are live, and sig/digest ptr+len each bound their own slice. + let rc = unsafe { + sys::wolfTPM2_VerifyHash( + self.dev(), + self.kptr(), + sig.as_ptr(), + sig_len, + digest.as_ptr(), + digest_len, + ) + }; + check_rc(rc) + } +} diff --git a/wrapper/rust/wolftpm/src/symmetric.rs b/wrapper/rust/wolftpm/src/symmetric.rs new file mode 100644 index 000000000..a2a1ea178 --- /dev/null +++ b/wrapper/rust/wolftpm/src/symmetric.rs @@ -0,0 +1,54 @@ +//! Symmetric (AES-CFB) encrypt/decrypt with a TPM-resident key. + +use crate::key::Key; +use crate::{check_rc, sys, Result, Secret}; + +impl<'d> Key<'d> { + /// Encrypt `data` with this loaded AES key (see + /// [`Template::symmetric`](crate::Template::symmetric)). `iv` is the CFB + /// initialization vector (16 bytes for AES). + pub fn aes_encrypt(&self, data: &[u8], iv: &[u8]) -> Result> { + self.aes(data, iv, 0) + } + + /// Decrypt `data` produced by [`aes_encrypt`](Key::aes_encrypt) with the + /// same key and `iv`. The recovered plaintext is returned in a zeroizing + /// [`Secret`](crate::Secret). + pub fn aes_decrypt(&self, data: &[u8], iv: &[u8]) -> Result { + Ok(Secret::new(self.aes(data, iv, 1)?)) + } + + fn aes(&self, data: &[u8], iv: &[u8], is_decrypt: core::ffi::c_int) -> Result> { + // AES-CFB uses a full 16-byte block as the IV; reject any other length + // rather than forwarding it to the C layer. + if iv.len() != 16 { + return Err(crate::TpmError(crate::BUFFER_E)); + } + let data_len = crate::checked_u32(data.len())?; + let mut out = vec![0u8; data.len()]; + let mut iv_buf = iv.to_vec(); + let iv_len = crate::checked_u32(iv_buf.len())?; + // SAFETY: self.dev()/self.kptr() are live, data/out and iv_buf ptr+len each describe their own live buffer, out sized to data.len(). + let rc = unsafe { + sys::wolfTPM2_EncryptDecrypt( + self.dev(), + self.kptr(), + data.as_ptr(), + out.as_mut_ptr(), + data_len, + iv_buf.as_mut_ptr(), + iv_len, + is_decrypt, + ) + }; + if rc != 0 { + // The C side decrypts in chunks, so a mid-stream failure can leave + // partial plaintext in `out`; scrub it before discarding. + for b in out.iter_mut() { + unsafe { core::ptr::write_volatile(b, 0) }; + } + check_rc(rc)?; + } + Ok(out) + } +} diff --git a/wrapper/rust/wolftpm/src/sys.rs b/wrapper/rust/wolftpm/src/sys.rs new file mode 100644 index 000000000..4b1128c6c --- /dev/null +++ b/wrapper/rust/wolftpm/src/sys.rs @@ -0,0 +1,13 @@ +//! Raw FFI layer: bindgen output for wolfTPM's public headers. +//! +//! Generated at build time into `$OUT_DIR/bindings.rs`; never hand-edited. +//! Everything here is `unsafe` to call — the safe API lives in the sibling +//! modules. + +#![allow(non_camel_case_types)] +#![allow(non_snake_case)] +#![allow(non_upper_case_globals)] +#![allow(improper_ctypes)] +#![allow(dead_code)] + +include!(concat!(env!("OUT_DIR"), "/bindings.rs")); diff --git a/wrapper/rust/wolftpm/tests/caps.rs b/wrapper/rust/wolftpm/tests/caps.rs new file mode 100644 index 000000000..4606d7f9b --- /dev/null +++ b/wrapper/rust/wolftpm/tests/caps.rs @@ -0,0 +1,16 @@ +//! TPM self-test and capability query. +#![cfg(feature = "swtpm-tests")] + +mod common; + +#[test] +fn self_test_and_capabilities() { + let dev = common::open(); + dev.self_test().unwrap(); + + let caps = dev.capabilities().unwrap(); + assert!( + !caps.manufacturer.is_empty(), + "the TPM should report a manufacturer id" + ); +} diff --git a/wrapper/rust/wolftpm/tests/certify.rs b/wrapper/rust/wolftpm/tests/certify.rs new file mode 100644 index 000000000..de5ea1726 --- /dev/null +++ b/wrapper/rust/wolftpm/tests/certify.rs @@ -0,0 +1,35 @@ +//! Attestation: an AIK certifies a key object. +#![cfg(feature = "swtpm-tests")] + +mod common; + +use wolftpm::{Hierarchy, KeyAlg, Template}; + +fn certify_with(aik_alg: KeyAlg) { + let dev = common::open(); + let srk = dev + .create_primary(Hierarchy::Owner, KeyAlg::EccP256, None) + .unwrap(); + + let object = dev + .create_and_load(&srk, &Template::signing(KeyAlg::EccP256).unwrap(), None) + .unwrap(); + let aik = dev + .create_and_load(&srk, &Template::attestation(aik_alg).unwrap(), None) + .unwrap(); + + let att = dev.certify(&object, &aik, b"verifier-nonce-1234").unwrap(); + assert!(!att.attest.is_empty(), "attestation data must be non-empty"); + assert!(!att.signature.is_empty(), "signature must be returned"); + assert_ne!(att.sig_alg, 0, "signature algorithm must be set"); +} + +#[test] +fn certify_with_ecc_aik() { + certify_with(KeyAlg::EccP256); +} + +#[test] +fn certify_with_rsa_aik() { + certify_with(KeyAlg::Rsa); +} diff --git a/wrapper/rust/wolftpm/tests/common/mod.rs b/wrapper/rust/wolftpm/tests/common/mod.rs new file mode 100644 index 000000000..3d1789350 --- /dev/null +++ b/wrapper/rust/wolftpm/tests/common/mod.rs @@ -0,0 +1,10 @@ +//! Shared test harness. Assumes a software TPM (fwtpm_server / ibmswtpm2) is +//! listening on `TPM2_SWTPM_HOST:TPM2_SWTPM_PORT` (default localhost:2321) — +//! the run script / CI starts one; tests connect to it. + +use wolftpm::Device; + +/// Open a connection to the software TPM, or fail with a clear message. +pub fn open() -> Device { + Device::open_swtpm().expect("open swtpm — is fwtpm_server (or ibmswtpm2) on :2321?") +} diff --git a/wrapper/rust/wolftpm/tests/credential.rs b/wrapper/rust/wolftpm/tests/credential.rs new file mode 100644 index 000000000..dd92497b9 --- /dev/null +++ b/wrapper/rust/wolftpm/tests/credential.rs @@ -0,0 +1,46 @@ +//! EK-based credential activation (MakeCredential / ActivateCredential). +#![cfg(feature = "swtpm-tests")] + +mod common; + +use wolftpm::{Hierarchy, KeyAlg, Template}; + +#[test] +fn make_and_activate_credential() { + let dev = common::open(); + + // Attestation key under an SRK, then free the SRK so only the EK and AIK + // occupy transient object slots for the activation. + let aik_name; + let aik; + { + let srk = dev + .create_primary(Hierarchy::Owner, KeyAlg::EccP256, None) + .unwrap(); + aik = dev + .create_and_load( + &srk, + &Template::attestation(KeyAlg::EccP256).unwrap(), + Some(b"aik-auth"), + ) + .unwrap(); + aik_name = aik.name(); + } + assert!(!aik_name.is_empty(), "AIK must have a computed Name"); + + let ek = dev.create_ek(KeyAlg::Rsa).unwrap(); + + // Verifier side: seal a secret to this TPM's EK, bound to the AIK's Name. + let secret = b"credential-secret-0123456789abcd"; + let cred = dev.make_credential(&ek, &aik_name, secret).unwrap(); + assert!(!cred.credential_blob.is_empty()); + assert!(!cred.secret.is_empty()); + + // TPM side: recover it, proving the AIK and EK share this TPM. + let recovered = dev.activate_credential(&aik, &ek, &cred).unwrap(); + assert_eq!( + recovered.as_bytes(), + &secret[..], + "activated credential must recover the original secret" + ); +} diff --git a/wrapper/rust/wolftpm/tests/ecdh.rs b/wrapper/rust/wolftpm/tests/ecdh.rs new file mode 100644 index 000000000..17972d780 --- /dev/null +++ b/wrapper/rust/wolftpm/tests/ecdh.rs @@ -0,0 +1,30 @@ +//! ECDH key agreement. +#![cfg(feature = "swtpm-tests")] + +mod common; + +use wolftpm::{Hierarchy, KeyAlg, Template}; + +#[test] +fn ecdh_gen_then_z_agree() { + let dev = common::open(); + let srk = dev + .create_primary(Hierarchy::Owner, KeyAlg::EccP256, None) + .unwrap(); + let key = dev + .create_and_load(&srk, &Template::ecdh().unwrap(), None) + .unwrap(); + + // One-shot ephemeral ECDH yields an ephemeral point and secret Z; feeding + // that point back through this key's private part must recover the same Z. + let gen = key.ecdh_gen().unwrap(); + assert!(!gen.point.is_empty()); + assert!(!gen.secret.is_empty()); + + let z = key.ecdh_z(&gen.point).unwrap(); + assert_eq!( + gen.secret.as_bytes(), + z.as_bytes(), + "ECDHGenZ must recover the same shared secret as ECDHGen" + ); +} diff --git a/wrapper/rust/wolftpm/tests/ek.rs b/wrapper/rust/wolftpm/tests/ek.rs new file mode 100644 index 000000000..3995d3415 --- /dev/null +++ b/wrapper/rust/wolftpm/tests/ek.rs @@ -0,0 +1,16 @@ +//! Endorsement key creation and public-key export. +#![cfg(feature = "swtpm-tests")] + +mod common; + +use wolftpm::KeyAlg; + +#[test] +fn create_ek_and_export_public() { + let dev = common::open(); + let ek = dev.create_ek(KeyAlg::EccP256).unwrap(); + assert_ne!(ek.handle(), 0, "EK should have a live handle"); + + let der = ek.export_public(false).unwrap(); + assert!(!der.is_empty(), "exported DER public key should be non-empty"); +} diff --git a/wrapper/rust/wolftpm/tests/hmac.rs b/wrapper/rust/wolftpm/tests/hmac.rs new file mode 100644 index 000000000..76cb343ca --- /dev/null +++ b/wrapper/rust/wolftpm/tests/hmac.rs @@ -0,0 +1,42 @@ +//! TPM keyed-hash HMAC. +#![cfg(feature = "swtpm-tests")] + +mod common; + +use wolftpm::{HashAlg, Hierarchy, KeyAlg, Template}; + +#[test] +fn hmac_deterministic_and_keyed() { + let dev = common::open(); + let srk = dev + .create_primary(Hierarchy::Owner, KeyAlg::EccP256, None) + .unwrap(); + + let a = dev.hmac(&srk, b"secret-key", b"message", HashAlg::Sha256).unwrap(); + let b = dev.hmac(&srk, b"secret-key", b"message", HashAlg::Sha256).unwrap(); + assert_eq!(a, b, "same key and data must give the same HMAC"); + assert_eq!(a.len(), 32); + + let c = dev.hmac(&srk, b"other-key", b"message", HashAlg::Sha256).unwrap(); + assert_ne!(a, c, "a different key must give a different HMAC"); +} + +#[test] +fn loaded_hmac_key_compute() { + let dev = common::open(); + let srk = dev + .create_primary(Hierarchy::Owner, KeyAlg::EccP256, None) + .unwrap(); + + // A TPM-generated keyed-hash key: the secret never leaves the TPM, we + // compute HMAC on the loaded handle. + let key = dev + .create_and_load(&srk, &Template::hmac(HashAlg::Sha256).unwrap(), None) + .unwrap(); + let m1 = key.hmac(b"message", HashAlg::Sha256).unwrap(); + let m2 = key.hmac(b"message", HashAlg::Sha256).unwrap(); + assert_eq!(m1, m2, "same loaded key and data must give the same HMAC"); + assert_eq!(m1.len(), 32); + let m3 = key.hmac(b"different", HashAlg::Sha256).unwrap(); + assert_ne!(m1, m3, "different data must give a different HMAC"); +} diff --git a/wrapper/rust/wolftpm/tests/import.rs b/wrapper/rust/wolftpm/tests/import.rs new file mode 100644 index 000000000..326886aa9 --- /dev/null +++ b/wrapper/rust/wolftpm/tests/import.rs @@ -0,0 +1,87 @@ +//! External RSA and ECC private-key import (fixtures from wolfSSL test certs). +#![cfg(feature = "swtpm-tests")] + +mod common; + +use wolftpm::{Hierarchy, KeyAlg}; + +const RSA_MODULUS: &[u8] = &[ + 0xc3, 0x03, 0xd1, 0x2b, 0xfe, 0x39, 0xa4, 0x32, 0x45, 0x3b, 0x53, 0xc8, + 0x84, 0x2b, 0x2a, 0x7c, 0x74, 0x9a, 0xbd, 0xaa, 0x2a, 0x52, 0x07, 0x47, + 0xd6, 0xa6, 0x36, 0xb2, 0x07, 0x32, 0x8e, 0xd0, 0xba, 0x69, 0x7b, 0xc6, + 0xc3, 0x44, 0x9e, 0xd4, 0x81, 0x48, 0xfd, 0x2d, 0x68, 0xa2, 0x8b, 0x67, + 0xbb, 0xa1, 0x75, 0xc8, 0x36, 0x2c, 0x4a, 0xd2, 0x1b, 0xf7, 0x8b, 0xba, + 0xcf, 0x0d, 0xf9, 0xef, 0xec, 0xf1, 0x81, 0x1e, 0x7b, 0x9b, 0x03, 0x47, + 0x9a, 0xbf, 0x65, 0xcc, 0x7f, 0x65, 0x24, 0x69, 0xa6, 0xe8, 0x14, 0x89, + 0x5b, 0xe4, 0x34, 0xf7, 0xc5, 0xb0, 0x14, 0x93, 0xf5, 0x67, 0x7b, 0x3a, + 0x7a, 0x78, 0xe1, 0x01, 0x56, 0x56, 0x91, 0xa6, 0x13, 0x42, 0x8d, 0xd2, + 0x3c, 0x40, 0x9c, 0x4c, 0xef, 0xd1, 0x86, 0xdf, 0x37, 0x51, 0x1b, 0x0c, + 0xa1, 0x3b, 0xf5, 0xf1, 0xa3, 0x4a, 0x35, 0xe4, 0xe1, 0xce, 0x96, 0xdf, + 0x1b, 0x7e, 0xbf, 0x4e, 0x97, 0xd0, 0x10, 0xe8, 0xa8, 0x08, 0x30, 0x81, + 0xaf, 0x20, 0x0b, 0x43, 0x14, 0xc5, 0x74, 0x67, 0xb4, 0x32, 0x82, 0x6f, + 0x8d, 0x86, 0xc2, 0x88, 0x40, 0x99, 0x36, 0x83, 0xba, 0x1e, 0x40, 0x72, + 0x22, 0x17, 0xd7, 0x52, 0x65, 0x24, 0x73, 0xb0, 0xce, 0xef, 0x19, 0xcd, + 0xae, 0xff, 0x78, 0x6c, 0x7b, 0xc0, 0x12, 0x03, 0xd4, 0x4e, 0x72, 0x0d, + 0x50, 0x6d, 0x3b, 0xa3, 0x3b, 0xa3, 0x99, 0x5e, 0x9d, 0xc8, 0xd9, 0x0c, + 0x85, 0xb3, 0xd9, 0x8a, 0xd9, 0x54, 0x26, 0xdb, 0x6d, 0xfa, 0xac, 0xbb, + 0xff, 0x25, 0x4c, 0xc4, 0xd1, 0x79, 0xf4, 0x71, 0xd3, 0x86, 0x40, 0x18, + 0x13, 0xb0, 0x63, 0xb5, 0x72, 0x4e, 0x30, 0xc4, 0x97, 0x84, 0x86, 0x2d, + 0x56, 0x2f, 0xd7, 0x15, 0xf7, 0x7f, 0xc0, 0xae, 0xf5, 0xfc, 0x5b, 0xe5, + 0xfb, 0xa1, 0xba, 0xd3, +]; +const RSA_EXPONENT: u32 = 0x0001_0001; +const RSA_PRIME: &[u8] = &[ + 0xd5, 0x38, 0x1b, 0xc3, 0x8f, 0xc5, 0x93, 0x0c, 0x47, 0x0b, 0x6f, 0x35, + 0x92, 0xc5, 0xb0, 0x8d, 0x46, 0xc8, 0x92, 0x18, 0x8f, 0xf5, 0x80, 0x0a, + 0xf7, 0xef, 0xa1, 0xfe, 0x80, 0xb9, 0xb5, 0x2a, 0xba, 0xca, 0x18, 0xb0, + 0x5d, 0xa5, 0x07, 0xd0, 0x93, 0x8d, 0xd8, 0x9c, 0x04, 0x1c, 0xd4, 0x62, + 0x8e, 0xa6, 0x26, 0x81, 0x01, 0xff, 0xce, 0x8a, 0x2a, 0x63, 0x34, 0x35, + 0x40, 0xaa, 0x6d, 0x80, 0xde, 0x89, 0x23, 0x6a, 0x57, 0x4d, 0x9e, 0x6e, + 0xad, 0x93, 0x4e, 0x56, 0x90, 0x0b, 0x6d, 0x9d, 0x73, 0x8b, 0x0c, 0xae, + 0x27, 0x3d, 0xde, 0x4e, 0xf0, 0xaa, 0xc5, 0x6c, 0x78, 0x67, 0x6c, 0x94, + 0x52, 0x9c, 0x37, 0x67, 0x6c, 0x2d, 0xef, 0xbb, 0xaf, 0xdf, 0xa6, 0x90, + 0x3c, 0xc4, 0x47, 0xcf, 0x8d, 0x96, 0x9e, 0x98, 0xa9, 0xb4, 0x9f, 0xc5, + 0xa6, 0x50, 0xdc, 0xb3, 0xf0, 0xfb, 0x74, 0x17, +]; + +const ECC_PUB_X: &[u8] = &[ + 0xbb, 0x33, 0xac, 0x4c, 0x27, 0x50, 0x4a, 0xc6, 0x4a, 0xa5, 0x04, 0xc3, + 0x3c, 0xde, 0x9f, 0x36, 0xdb, 0x72, 0x2d, 0xce, 0x94, 0xea, 0x2b, 0xfa, + 0xcb, 0x20, 0x09, 0x39, 0x2c, 0x16, 0xe8, 0x61, +]; +const ECC_PUB_Y: &[u8] = &[ + 0x02, 0xe9, 0xaf, 0x4d, 0xd3, 0x02, 0x93, 0x9a, 0x31, 0x5b, 0x97, 0x92, + 0x21, 0x7f, 0xf0, 0xcf, 0x18, 0xda, 0x91, 0x11, 0x02, 0x34, 0x86, 0xe8, + 0x20, 0x58, 0x33, 0x0b, 0x80, 0x34, 0x89, 0xd8, +]; +const ECC_PRIV_D: &[u8] = &[ + 0x45, 0xb6, 0x69, 0x02, 0x73, 0x9c, 0x6c, 0x85, 0xa1, 0x38, 0x5b, 0x72, + 0xe8, 0xe8, 0xc7, 0xac, 0xc4, 0x03, 0x8d, 0x53, 0x35, 0x04, 0xfa, 0x6c, + 0x28, 0xdc, 0x34, 0x8d, 0xe1, 0xa8, 0x09, 0x8c, +]; + +#[test] +fn import_rsa_private_key() { + let dev = common::open(); + let srk = dev + .create_primary(Hierarchy::Owner, KeyAlg::Rsa, None) + .unwrap(); + let blob = dev + .import_rsa_key(&srk, RSA_MODULUS, RSA_EXPONENT, RSA_PRIME) + .unwrap(); + let key = blob.load(&srk, None).unwrap(); + assert_ne!(key.handle(), 0, "imported RSA key must load to a live handle"); +} + +#[test] +fn import_ecc_private_key() { + let dev = common::open(); + let srk = dev + .create_primary(Hierarchy::Owner, KeyAlg::EccP256, None) + .unwrap(); + let blob = dev + .import_ecc_key(&srk, ECC_PUB_X, ECC_PUB_Y, ECC_PRIV_D) + .unwrap(); + let key = blob.load(&srk, None).unwrap(); + assert_ne!(key.handle(), 0, "imported ECC key must load to a live handle"); +} diff --git a/wrapper/rust/wolftpm/tests/keys.rs b/wrapper/rust/wolftpm/tests/keys.rs new file mode 100644 index 000000000..71ced1fa1 --- /dev/null +++ b/wrapper/rust/wolftpm/tests/keys.rs @@ -0,0 +1,69 @@ +//! Child key creation, load, and blob serialization round-trips. +#![cfg(feature = "swtpm-tests")] + +mod common; + +use wolftpm::{Hierarchy, KeyAlg, KeyBlob, Template}; + +#[test] +fn create_and_load_child() { + let dev = common::open(); + let srk = dev + .create_primary(Hierarchy::Owner, KeyAlg::EccP256, None) + .unwrap(); + let key = dev + .create_and_load(&srk, &Template::signing(KeyAlg::EccP256).unwrap(), None) + .unwrap(); + assert_ne!(key.handle(), 0); +} + +#[test] +fn key_blob_roundtrip_then_load() { + let dev = common::open(); + let srk = dev + .create_primary(Hierarchy::Owner, KeyAlg::EccP256, None) + .unwrap(); + + let blob = dev + .create_key(&srk, &Template::signing(KeyAlg::EccP256).unwrap(), None) + .unwrap(); + let bytes = blob.to_bytes().unwrap(); + assert!(!bytes.is_empty(), "serialized blob should be non-empty"); + + let restored = KeyBlob::from_bytes(&dev, &bytes).unwrap(); + let key = restored.load(&srk, None).unwrap(); + assert_ne!(key.handle(), 0, "reloaded key should have a live handle"); +} + +#[test] +fn key_blob_roundtrip_preserves_short_auth() { + let dev = common::open(); + let srk = dev + .create_primary(Hierarchy::Owner, KeyAlg::EccP256, None) + .unwrap(); + + // A short auth is zero-padded to the nameAlg digest size when the TPM + // stores it, so load must restore it the same way for an authorized + // operation on the reloaded key to succeed. + let blob = dev + .create_key(&srk, &Template::signing(KeyAlg::EccP256).unwrap(), Some(b"pw")) + .unwrap(); + let bytes = blob.to_bytes().unwrap(); + let restored = KeyBlob::from_bytes(&dev, &bytes).unwrap(); + let key = restored.load(&srk, Some(b"pw")).unwrap(); + let sig = key.sign_hash(&[0x44u8; 32]).unwrap(); + assert!(!sig.is_empty(), "authorized sign on a reloaded short-auth key must succeed"); +} + +#[test] +fn auth_protected_parent_loads_child() { + let dev = common::open(); + let auth: &[u8; 32] = b"0123456789abcdef0123456789abcdef"; + let srk = dev + .create_primary(Hierarchy::Owner, KeyAlg::EccP256, Some(auth)) + .unwrap(); + let child = dev + .create_and_load(&srk, &Template::signing(KeyAlg::EccP256).unwrap(), None) + .unwrap(); + assert_ne!(child.handle(), 0, "child under an auth-protected parent must load"); +} diff --git a/wrapper/rust/wolftpm/tests/nv.rs b/wrapper/rust/wolftpm/tests/nv.rs new file mode 100644 index 000000000..57bf5a81d --- /dev/null +++ b/wrapper/rust/wolftpm/tests/nv.rs @@ -0,0 +1,26 @@ +//! NV storage write/read round-trip and delete. +#![cfg(feature = "swtpm-tests")] + +mod common; + +#[test] +fn nv_write_read_delete() { + let dev = common::open(); + let index: u32 = 0x0150_0100; + + // Clean any leftover from a prior run so the define succeeds. + let _ = dev.nv_delete(index); + + let mut slot = dev.nv_create(index, 32, None).unwrap(); + assert_eq!(slot.index(), index); + + let data = b"root-of-trust-metadata"; + dev.nv_write(&mut slot, data, 0).unwrap(); + + let mut buf = [0u8; 32]; + let n = dev.nv_read(&mut slot, &mut buf, 0).unwrap(); + assert_eq!(n, 32); + assert_eq!(&buf[..data.len()], data, "NV read must return what was written"); + + dev.nv_delete(index).unwrap(); +} diff --git a/wrapper/rust/wolftpm/tests/pcr.rs b/wrapper/rust/wolftpm/tests/pcr.rs new file mode 100644 index 000000000..5cc965691 --- /dev/null +++ b/wrapper/rust/wolftpm/tests/pcr.rs @@ -0,0 +1,21 @@ +//! PCR read and extend. +#![cfg(feature = "swtpm-tests")] + +mod common; + +use wolftpm::HashAlg; + +#[test] +fn pcr_extend_changes_digest() { + let dev = common::open(); + let idx = 16; // debug PCR, safe to extend + + let before = dev.pcr_read(idx, HashAlg::Sha256).unwrap(); + assert_eq!(before.len(), 32); + + dev.pcr_extend(idx, HashAlg::Sha256, &[0xABu8; 32]).unwrap(); + + let after = dev.pcr_read(idx, HashAlg::Sha256).unwrap(); + assert_eq!(after.len(), 32); + assert_ne!(before, after, "extend must change the PCR value"); +} diff --git a/wrapper/rust/wolftpm/tests/persist.rs b/wrapper/rust/wolftpm/tests/persist.rs new file mode 100644 index 000000000..7cc359df2 --- /dev/null +++ b/wrapper/rust/wolftpm/tests/persist.rs @@ -0,0 +1,31 @@ +//! Persistent key handles: store, read back, and evict. +#![cfg(feature = "swtpm-tests")] + +mod common; + +use wolftpm::{Hierarchy, KeyAlg}; + +#[test] +fn persist_read_evict() { + let dev = common::open(); + let handle: u32 = 0x8100_0200; + + // Clean up any leftover from a prior run. + if let Ok(old) = dev.read_persistent(handle, None) { + let _ = dev.evict_key(&old, Hierarchy::Owner); + } + + let srk = dev + .create_primary(Hierarchy::Owner, KeyAlg::EccP256, None) + .unwrap(); + dev.persist_key(&srk, Hierarchy::Owner, handle).unwrap(); + + let readback = dev.read_persistent(handle, None).unwrap(); + assert_eq!(readback.handle(), handle, "key should live at the persistent handle"); + + dev.evict_key(&readback, Hierarchy::Owner).unwrap(); + assert!( + dev.read_persistent(handle, None).is_err(), + "evicted key should no longer be readable" + ); +} diff --git a/wrapper/rust/wolftpm/tests/quote.rs b/wrapper/rust/wolftpm/tests/quote.rs new file mode 100644 index 000000000..ea2540e4d --- /dev/null +++ b/wrapper/rust/wolftpm/tests/quote.rs @@ -0,0 +1,60 @@ +//! PCR quote attestation. +#![cfg(feature = "swtpm-tests")] + +mod common; + +use wolftpm::{HashAlg, Hierarchy, KeyAlg, Template}; + +#[test] +fn quote_ecc_aik() { + let dev = common::open(); + let srk = dev + .create_primary(Hierarchy::Owner, KeyAlg::EccP256, None) + .unwrap(); + let aik = dev + .create_and_load(&srk, &Template::attestation(KeyAlg::EccP256).unwrap(), None) + .unwrap(); + + let quote = dev.quote(&aik, &[16], HashAlg::Sha256, b"verifier-nonce").unwrap(); + assert!(!quote.attest.is_empty(), "quote must carry the signed PCR digest"); + assert_ne!(quote.sig_alg, 0, "signature algorithm must be set"); + assert_eq!( + quote.signature.len(), + 64, + "an ECDSA P-256 signature must be a fixed 64-byte R||S" + ); +} + +#[test] +fn quote_rsa_aik() { + let dev = common::open(); + let srk = dev + .create_primary(Hierarchy::Owner, KeyAlg::EccP256, None) + .unwrap(); + let aik = dev + .create_and_load(&srk, &Template::attestation(KeyAlg::Rsa).unwrap(), None) + .unwrap(); + + let quote = dev.quote(&aik, &[16], HashAlg::Sha256, b"verifier-nonce").unwrap(); + assert!(!quote.attest.is_empty()); + assert!(!quote.signature.is_empty(), "RSA signature must be returned"); +} + +#[test] +fn quote_rejects_invalid_pcr_selection() { + let dev = common::open(); + let srk = dev + .create_primary(Hierarchy::Owner, KeyAlg::EccP256, None) + .unwrap(); + let aik = dev + .create_and_load(&srk, &Template::attestation(KeyAlg::EccP256).unwrap(), None) + .unwrap(); + assert!( + dev.quote(&aik, &[], HashAlg::Sha256, b"n").is_err(), + "empty PCR selection must be rejected" + ); + assert!( + dev.quote(&aik, &[99], HashAlg::Sha256, b"n").is_err(), + "out-of-range PCR must be rejected" + ); +} diff --git a/wrapper/rust/wolftpm/tests/rsa.rs b/wrapper/rust/wolftpm/tests/rsa.rs new file mode 100644 index 000000000..b5b55939f --- /dev/null +++ b/wrapper/rust/wolftpm/tests/rsa.rs @@ -0,0 +1,42 @@ +//! RSA-OAEP encrypt and decrypt round-trip. +#![cfg(feature = "swtpm-tests")] + +mod common; + +use wolftpm::{HashAlg, Hierarchy, KeyAlg, Template}; + +#[test] +fn rsa_oaep_roundtrip() { + let dev = common::open(); + let srk = dev + .create_primary(Hierarchy::Owner, KeyAlg::EccP256, None) + .unwrap(); + let key = dev + .create_and_load(&srk, &Template::rsa_decrypt().unwrap(), None) + .unwrap(); + + let msg = b"session key material"; + let ciphertext = key.rsa_encrypt(msg).unwrap(); + assert_ne!(ciphertext.as_slice(), &msg[..], "ciphertext must differ from plaintext"); + + let recovered = key.rsa_decrypt(&ciphertext).unwrap(); + assert_eq!(recovered.as_bytes(), &msg[..], "decrypt must recover the plaintext"); +} + +#[test] +fn rsa_oaep_sha1_roundtrip() { + // Microsoft device enrollment (MS-OAPXBC) wraps its session key with + // OAEP-SHA1, so the explicit-hash variant must round-trip with SHA-1. + let dev = common::open(); + let srk = dev + .create_primary(Hierarchy::Owner, KeyAlg::EccP256, None) + .unwrap(); + let key = dev + .create_and_load(&srk, &Template::rsa_decrypt().unwrap(), None) + .unwrap(); + + let msg = b"ms enrollment session key"; + let ciphertext = key.rsa_encrypt_with_hash(msg, HashAlg::Sha1).unwrap(); + let recovered = key.rsa_decrypt_with_hash(&ciphertext, HashAlg::Sha1).unwrap(); + assert_eq!(recovered.as_bytes(), &msg[..], "OAEP-SHA1 decrypt must recover the plaintext"); +} diff --git a/wrapper/rust/wolftpm/tests/seal.rs b/wrapper/rust/wolftpm/tests/seal.rs new file mode 100644 index 000000000..a5908cba2 --- /dev/null +++ b/wrapper/rust/wolftpm/tests/seal.rs @@ -0,0 +1,31 @@ +//! Seal/unseal round-trip and a wrong-auth negative test. +#![cfg(all(feature = "swtpm-tests"))] + +mod common; + +use wolftpm::{Hierarchy, KeyAlg}; + +#[test] +fn seal_unseal_roundtrip() { + let dev = common::open(); + let srk = dev + .create_primary(Hierarchy::Owner, KeyAlg::EccP256, None) + .unwrap(); + let secret = b"himmelblau-device-secret"; + let sealed = dev.seal(&srk, secret, None).unwrap(); + let out = dev.unseal(sealed, &srk, None).unwrap(); + assert_eq!(out.as_bytes(), &secret[..], "unsealed data must match the sealed secret"); +} + +#[test] +fn unseal_wrong_auth_fails() { + let dev = common::open(); + let srk = dev + .create_primary(Hierarchy::Owner, KeyAlg::EccP256, None) + .unwrap(); + let sealed = dev.seal(&srk, b"top secret", Some(b"correcthorse")).unwrap(); + assert!( + dev.unseal(sealed, &srk, Some(b"wrongpass")).is_err(), + "unseal with the wrong auth must fail" + ); +} diff --git a/wrapper/rust/wolftpm/tests/seal_pcr.rs b/wrapper/rust/wolftpm/tests/seal_pcr.rs new file mode 100644 index 000000000..de86ad6ca --- /dev/null +++ b/wrapper/rust/wolftpm/tests/seal_pcr.rs @@ -0,0 +1,52 @@ +//! PCR-policy sealing: measured-boot-bound seal and unseal. +#![cfg(feature = "swtpm-tests")] + +mod common; + +use wolftpm::{HashAlg, Hierarchy, KeyAlg}; + +#[test] +fn pcr_bound_seal_roundtrip() { + let dev = common::open(); + let srk = dev + .create_primary(Hierarchy::Owner, KeyAlg::EccP256, None) + .unwrap(); + let pcrs = [16u8]; + + let sealed = dev.seal_pcr(&srk, b"pcr-bound secret", &pcrs).unwrap(); + let out = dev.unseal_pcr(sealed, &srk, &pcrs).unwrap(); + assert_eq!(out.as_bytes(), &b"pcr-bound secret"[..], "unseal must recover the secret"); +} + +#[test] +fn pcr_change_breaks_unseal() { + let dev = common::open(); + let srk = dev + .create_primary(Hierarchy::Owner, KeyAlg::EccP256, None) + .unwrap(); + let pcrs = [23u8]; // a debug PCR distinct from other tests + + let sealed = dev.seal_pcr(&srk, b"bound to pcr23", &pcrs).unwrap(); + dev.pcr_extend(23, HashAlg::Sha256, &[0xFFu8; 32]).unwrap(); + + assert!( + dev.unseal_pcr(sealed, &srk, &pcrs).is_err(), + "unseal must fail once the bound PCR has changed" + ); +} + +#[test] +fn seal_pcr_rejects_invalid_selection() { + let dev = common::open(); + let srk = dev + .create_primary(Hierarchy::Owner, KeyAlg::EccP256, None) + .unwrap(); + assert!( + dev.seal_pcr(&srk, b"x", &[]).is_err(), + "an empty PCR selection must be rejected, not bound to nothing" + ); + assert!( + dev.seal_pcr(&srk, b"x", &[255]).is_err(), + "an out-of-range PCR index must be rejected" + ); +} diff --git a/wrapper/rust/wolftpm/tests/session.rs b/wrapper/rust/wolftpm/tests/session.rs new file mode 100644 index 000000000..639b884de --- /dev/null +++ b/wrapper/rust/wolftpm/tests/session.rs @@ -0,0 +1,56 @@ +//! Parameter-encryption session. +#![cfg(feature = "swtpm-tests")] + +mod common; + +use wolftpm::{Hierarchy, KeyAlg}; + +#[test] +fn secret_ops_under_encrypted_session() { + let dev = common::open(); + let srk = dev + .create_primary(Hierarchy::Owner, KeyAlg::EccP256, None) + .unwrap(); + + // With the session at slot 1, seal (command param) and unseal (response + // param) travel encrypted; both must still round-trip correctly. + let session = dev.start_encrypted_session(&srk).unwrap(); + let sealed = dev.seal(&srk, b"secret-under-session", None).unwrap(); + let out = dev.unseal(sealed, &srk, None).unwrap(); + assert_eq!(out.as_bytes(), &b"secret-under-session"[..]); + + // GetRandom's response is also encrypted while the session is live. + let mut buf = [0u8; 32]; + dev.get_random(&mut buf).unwrap(); + assert_ne!(buf, [0u8; 32]); + drop(session); +} + +#[test] +fn second_session_rejected() { + let dev = common::open(); + let srk = dev + .create_primary(Hierarchy::Owner, KeyAlg::EccP256, None) + .unwrap(); + let _s1 = dev.start_encrypted_session(&srk).unwrap(); + assert!( + dev.start_encrypted_session(&srk).is_err(), + "a second concurrent encrypted session must be rejected" + ); +} + +#[test] +fn attestation_refused_during_session() { + let dev = common::open(); + let srk = dev + .create_primary(Hierarchy::Owner, KeyAlg::EccP256, None) + .unwrap(); + let _session = dev.start_encrypted_session(&srk).unwrap(); + // certify needs the auth slot the session holds, so it is refused rather + // than silently displacing the session (which would leave later commands + // unencrypted). srk stands in for both handles; the guard fires first. + assert!( + dev.certify(&srk, &srk, b"nonce").is_err(), + "certify must be refused while an encrypted session is active" + ); +} diff --git a/wrapper/rust/wolftpm/tests/sign.rs b/wrapper/rust/wolftpm/tests/sign.rs new file mode 100644 index 000000000..33f261d80 --- /dev/null +++ b/wrapper/rust/wolftpm/tests/sign.rs @@ -0,0 +1,39 @@ +//! Sign / verify round-trip and a tamper-rejection negative test. +#![cfg(feature = "swtpm-tests")] + +mod common; + +use wolftpm::{Hierarchy, KeyAlg, Template}; + +fn ecc_signing_key(dev: &wolftpm::Device) -> wolftpm::Key<'_> { + let srk = dev + .create_primary(Hierarchy::Owner, KeyAlg::EccP256, None) + .unwrap(); + // Once loaded, the child is independent of the parent, so the SRK is free + // to be flushed when it drops at the end of this function. + dev.create_and_load(&srk, &Template::signing(KeyAlg::EccP256).unwrap(), None) + .unwrap() +} + +#[test] +fn sign_then_verify() { + let dev = common::open(); + let key = ecc_signing_key(&dev); + let digest = [0x11u8; 32]; + let sig = key.sign_hash(&digest).unwrap(); + assert!(!sig.is_empty()); + key.verify_hash(&digest, &sig).expect("valid signature verifies"); +} + +#[test] +fn verify_rejects_tampered_signature() { + let dev = common::open(); + let key = ecc_signing_key(&dev); + let digest = [0x22u8; 32]; + let mut sig = key.sign_hash(&digest).unwrap(); + sig[0] ^= 0xFF; + assert!( + key.verify_hash(&digest, &sig).is_err(), + "tampered signature must not verify" + ); +} diff --git a/wrapper/rust/wolftpm/tests/smoke.rs b/wrapper/rust/wolftpm/tests/smoke.rs new file mode 100644 index 000000000..04b5181a0 --- /dev/null +++ b/wrapper/rust/wolftpm/tests/smoke.rs @@ -0,0 +1,28 @@ +//! Integration smoke test against a software TPM (swtpm/fwtpm/ibmswtpm2). +//! +//! Needs a server on `TPM2_SWTPM_HOST:TPM2_SWTPM_PORT` (default localhost:2321), +//! so it is gated behind the `swtpm-tests` feature. +#![cfg(feature = "swtpm-tests")] + +use wolftpm::{Device, Hierarchy, KeyAlg}; + +#[test] +fn random_and_primary_keys() { + let dev = Device::open_swtpm().expect("open swtpm (is a TPM server on :2321?)"); + + let mut a = [0u8; 32]; + let mut b = [0u8; 32]; + dev.get_random(&mut a).expect("get_random a"); + dev.get_random(&mut b).expect("get_random b"); + assert_ne!(a, b, "two random draws should differ"); + + let ecc = dev + .create_primary(Hierarchy::Owner, KeyAlg::EccP256, None) + .expect("create ECC SRK"); + assert_ne!(ecc.handle(), 0, "SRK should have a live handle"); + + let rsa = dev + .create_primary(Hierarchy::Owner, KeyAlg::Rsa, None) + .expect("create RSA SRK"); + assert_ne!(rsa.handle(), 0); +} diff --git a/wrapper/rust/wolftpm/tests/symmetric.rs b/wrapper/rust/wolftpm/tests/symmetric.rs new file mode 100644 index 000000000..d939e1075 --- /dev/null +++ b/wrapper/rust/wolftpm/tests/symmetric.rs @@ -0,0 +1,29 @@ +//! Symmetric AES-CFB encrypt/decrypt with a TPM key. +#![cfg(feature = "swtpm-tests")] + +mod common; + +use wolftpm::{Hierarchy, KeyAlg, Template}; + +#[test] +fn aes_cfb_roundtrip() { + let dev = common::open(); + let srk = dev + .create_primary(Hierarchy::Owner, KeyAlg::EccP256, None) + .unwrap(); + let key = dev + .create_and_load(&srk, &Template::symmetric(256).unwrap(), None) + .unwrap(); + + let iv = [0u8; 16]; + let plaintext = b"symmetric plaintext, two blocks!"; + let ciphertext = key.aes_encrypt(plaintext, &iv).unwrap(); + assert_ne!(ciphertext.as_slice(), &plaintext[..], "ciphertext must differ"); + + let recovered = key.aes_decrypt(&ciphertext, &iv).unwrap(); + assert_eq!( + recovered.as_bytes(), + &plaintext[..], + "AES-CFB decrypt must recover the plaintext" + ); +}