diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..0b64bba --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,63 @@ +name: Lint, Build and Test + +on: + push: + branches: [ "main" ] + pull_request: + types: [opened, synchronize, reopened, ready_for_review] + +env: + CARGO_TERM_COLOR: always + +jobs: + lint: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Install Rust (stable) with components + uses: dtolnay/rust-toolchain@stable + with: + components: clippy, rustfmt + - name: Cache cargo registry and build + uses: Swatinem/rust-cache@v2 + - name: rustfmt check + run: cargo fmt --all -- --check + - name: clippy (deny warnings) + run: | + cargo clippy --all-targets --all-features --workspace -- -D warnings + + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Install Rust (stable) with components + uses: dtolnay/rust-toolchain@stable + - name: Cache cargo registry and build + uses: Swatinem/rust-cache@v2 + - name: Cargo Build + run: cargo build --all-targets --workspace --verbose + + unit-tests: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Install Rust (stable) with components + uses: dtolnay/rust-toolchain@stable + - name: Cache cargo registry and build + uses: Swatinem/rust-cache@v2 + - name: Run tests + run: cargo test --all-features --verbose --workspace + roundtrip-test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Cache cargo registry and build + uses: Swatinem/rust-cache@v2 + - name: Build examples + run: cargo build --examples + - name: Run tests + run: | + cargo run --example spdm_responder -- --port 2323 & + (sleep 1; cargo run --example spdm_requester -- --port 2323 ) + + diff --git a/.github/workflows/license.yml b/.github/workflows/license.yml new file mode 100644 index 0000000..b79837e --- /dev/null +++ b/.github/workflows/license.yml @@ -0,0 +1,11 @@ +name: License Check + +on: pull_request + +jobs: + license: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - name: License Eye Header + uses: apache/skywalking-eyes@v0.8.0 diff --git a/.github/workflows/verification.yml b/.github/workflows/verification.yml new file mode 100644 index 0000000..3232dda --- /dev/null +++ b/.github/workflows/verification.yml @@ -0,0 +1,70 @@ +name: Verification with SPDM Emulator + +on: + push: + branches: [ "main" ] + pull_request: + types: [opened, synchronize, reopened, ready_for_review] + +env: + CARGO_TERM_COLOR: always + SPDM_EMU_REF: fe4cdc53b3f0e8300d16519467588001525e84f3 # spdm-emu main (27.02.2026) + CACHE_INVALIDATOR: 20ba74fb3b2bc121 # change to invalidate caches + +jobs: + requester-verification: + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + spdm_version: ["1.1", "1.2", "1.3"] + transport: [NONE, MCTP] + steps: + - name: Cache spdm-emu build output + id: cache-spdm-emu + uses: actions/cache@v5 + with: + path: ${{ github.workspace }}/spdm-emu/build + key: spdm_emu-${{ env.SPDM_EMU_REF }}-${{ runner.os }}-${{ env.CACHE_INVALIDATOR }} + + - name: Checkout DMTF spdm-emu + if: ${{ steps.cache-spdm-emu.outputs.cache-hit != 'true' }} + uses: actions/checkout@v4 + with: + repository: DMTF/spdm-emu + ref: ${{ env.SPDM_EMU_REF }} + submodules: recursive + path: spdm-emu + + - name: Install build dependecies + if: ${{ steps.cache-spdm-emu.outputs.cache-hit != 'true' }} + run: | + sudo apt install -y build-essential + + - name: Build spdm-emu + if: ${{ steps.cache-spdm-emu.outputs.cache-hit != 'true' }} + run: | + cd "$GITHUB_WORKSPACE"/spdm-emu + git submodule update + mkdir build -p && cd build + cmake -DARCH=x64 -DTOOLCHAIN=GCC -DTARGET=Debug -DCRYPTO=openssl .. + make copy_sample_key + make -j + + - uses: actions/checkout@v4 + with: + path: spdm-lib + - name: Cache cargo registry and build + uses: Swatinem/rust-cache@v2 + with: + workspaces: "spdm-lib" + - name: Cargo Build + run: | + cd spdm-lib + cargo build --example spdm_requester + - name: Run verification flow + run: | + cd spdm-lib + (cd "$GITHUB_WORKSPACE"/spdm-emu/build/bin/; ./spdm_responder_emu --trans ${{ matrix.transport }} --ver ${{ matrix.spdm_version }} --slot_id 0 --slot_count 1 --req_slot_id 0) & + (sleep 1; cargo run --example spdm_requester -- --transport-type ${{ matrix.transport }} --port 2323 --verbose) + diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..2c96eb1 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +target/ +Cargo.lock diff --git a/.licenserc.yaml b/.licenserc.yaml new file mode 100644 index 0000000..298395d --- /dev/null +++ b/.licenserc.yaml @@ -0,0 +1,12 @@ +header: + license: + spdx-id: Apache-2.0 + # No copyright owner until this has been clarified + # copyright-owner: OpenPRoT a Series of LF Projects, LLC + copyright-year: 2025 + software-name: spdm-lib + + paths: + - '**/*.rs' + + comment: on-failure diff --git a/COMPILATION_README.md b/COMPILATION_README.md index 2f0999f..48bf08f 100644 --- a/COMPILATION_README.md +++ b/COMPILATION_README.md @@ -52,30 +52,25 @@ spdm-lib/ ### Build the Library ```bash -cargo build --features std,crypto +cargo build ``` ### Build Examples Build the main SPDM responder: ```bash -cargo build --example spdm_responder --features std,crypto -``` - -Build the certificate test: -```bash -cargo build --example test_static_certs --features std +cargo build --example spdm_responder ``` Build all examples: ```bash -cargo build --examples --features std,crypto +cargo build --examples ``` ### Release Build (Optimized) ```bash -cargo build --release --example spdm_responder --features std,crypto +cargo build --release --example spdm_responder ``` ## Running Tests @@ -84,36 +79,7 @@ cargo build --release --example spdm_responder --features std,crypto Run all library unit tests: ```bash -cargo test --features std,crypto -``` - -### Static Certificate Verification - -Test that the static certificates are properly formatted: -```bash -cargo run --example test_static_certs --features std -``` - -Expected output: -``` -Static Certificate Test -======================= -Root CA Certificate: 419 bytes -Attestation Certificate: 453 bytes -Certificate Chain: 872 bytes -✓ Certificate chain length matches individual certificates -✓ Certificate chain starts with root CA -✓ Certificate chain ends with attestation certificate -✓ Both certificates have proper X.509 DER format (SEQUENCE tag) - -Static certificates are ready for use! -``` - -### Integration Tests - -Run integration tests: -```bash -cargo test --test integration --features std,crypto +cargo test ``` ## Running the SPDM Responder @@ -122,25 +88,25 @@ cargo test --test integration --features std,crypto Start the SPDM responder on default port 2323: ```bash -cargo run --example spdm_responder --features std,crypto +cargo run --example spdm_responder ``` ### With Custom Port ```bash -cargo run --example spdm_responder --features std,crypto -- --port 8080 +cargo run --example spdm_responder -- --port 8080 ``` ### With Verbose Logging ```bash -cargo run --example spdm_responder --features std,crypto -- --verbose +cargo run --example spdm_responder -- --verbose ``` ### All Options ```bash -cargo run --example spdm_responder --features std,crypto -- \ +cargo run --example spdm_responder -- \ --port 2323 \ --cert device_cert.pem \ --key device_key.pem \ @@ -165,7 +131,7 @@ The responder is compatible with the DMTF SPDM device validator: 1. **Start the responder:** ```bash - cargo run --example spdm_responder --features std,crypto -- --verbose + cargo run --example spdm_responder -- --verbose ``` 2. **In another terminal, test with nc (netcat):** @@ -200,7 +166,7 @@ openssl verify -CAfile root_ca.pem attestation.pem 1. Create a new file in `examples/` 2. Add necessary dependencies to `Cargo.toml` if needed -3. Build with: `cargo build --example your_example --features std,crypto` +3. Build with: `cargo build --example your_example` ### Modifying Certificates @@ -210,7 +176,7 @@ The static certificates are in `examples/platform/certs.rs`. They were generated Enable verbose logging to see detailed SPDM message processing: ```bash -RUST_LOG=debug cargo run --example spdm_responder --features std,crypto -- --verbose +RUST_LOG=debug cargo run --example spdm_responder -- --verbose ``` ## Troubleshooting @@ -221,7 +187,6 @@ If you encounter build errors: 1. **Update Rust**: `rustup update` 2. **Clean build**: `cargo clean && cargo build` -3. **Check features**: Ensure you're using `--features std,crypto` ### Connection Issues @@ -235,7 +200,7 @@ If the responder doesn't accept connections: If certificate-related errors occur: -1. **Run certificate test**: `cargo run --example test_static_certs --features std` +1. **Run certificate test**: `cargo run --example test_static_certs` 2. **Check certificate format**: Certificates are in DER format, not PEM 3. **Static certificates**: The responder uses hardcoded certificates, not files @@ -249,7 +214,7 @@ Licensed under the Apache-2.0 license. See LICENSE file for details. 2. Create a feature branch 3. Make your changes 4. Add tests if applicable -5. Run `cargo test --features std,crypto` +5. Run `cargo test` 6. Submit a pull request ## Support @@ -259,4 +224,4 @@ For issues and questions: 1. Check the troubleshooting section above 2. Run tests to verify your setup 3. Enable verbose logging for debugging -4. Check that certificates pass verification tests \ No newline at end of file +4. Check that certificates pass verification tests diff --git a/Cargo.lock b/Cargo.lock deleted file mode 100644 index 6cd218d..0000000 --- a/Cargo.lock +++ /dev/null @@ -1,504 +0,0 @@ -# This file is automatically @generated by Cargo. -# It is not intended for manual editing. -version = 4 - -[[package]] -name = "base16ct" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c7f02d4ea65f2c1853089ffd8d2787bdbc63de2f0d29dedbcf8ccdfa0ccd4cf" - -[[package]] -name = "base64ct" -version = "1.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "55248b47b0caf0546f7988906588779981c43bb1bc9d0c44087278f80cdb44ba" - -[[package]] -name = "bitfield" -version = "0.14.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d7e60934ceec538daadb9d8432424ed043a904d8e0243f3c6446bce549a46ac" - -[[package]] -name = "block-buffer" -version = "0.10.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" -dependencies = [ - "generic-array", -] - -[[package]] -name = "cfg-if" -version = "1.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2fd1289c04a9ea8cb22300a459a72a385d7c73d3259e2ed7dcb2af674838cfa9" - -[[package]] -name = "const-oid" -version = "0.9.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" - -[[package]] -name = "cpufeatures" -version = "0.2.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" -dependencies = [ - "libc", -] - -[[package]] -name = "crypto-bigint" -version = "0.5.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0dc92fb57ca44df6db8059111ab3af99a63d5d0f8375d9972e319a379c6bab76" -dependencies = [ - "generic-array", - "rand_core", - "subtle", - "zeroize", -] - -[[package]] -name = "crypto-common" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" -dependencies = [ - "generic-array", - "typenum", -] - -[[package]] -name = "der" -version = "0.7.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb" -dependencies = [ - "const-oid", - "der_derive", - "flagset", - "pem-rfc7468", - "zeroize", -] - -[[package]] -name = "der_derive" -version = "0.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8034092389675178f570469e6c3b0465d3d30b4505c294a6550db47f3c17ad18" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "digest" -version = "0.10.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" -dependencies = [ - "block-buffer", - "const-oid", - "crypto-common", - "subtle", -] - -[[package]] -name = "ecdsa" -version = "0.16.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee27f32b5c5292967d2d4a9d7f1e0b0aed2c15daded5a60300e4abb9d8020bca" -dependencies = [ - "der", - "digest", - "elliptic-curve", - "rfc6979", - "signature", - "spki", -] - -[[package]] -name = "elliptic-curve" -version = "0.13.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5e6043086bf7973472e0c7dff2142ea0b680d30e18d9cc40f267efbf222bd47" -dependencies = [ - "base16ct", - "crypto-bigint", - "digest", - "ff", - "generic-array", - "group", - "hkdf", - "pem-rfc7468", - "pkcs8", - "rand_core", - "sec1", - "subtle", - "zeroize", -] - -[[package]] -name = "ff" -version = "0.13.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0b50bfb653653f9ca9095b427bed08ab8d75a137839d9ad64eb11810d5b6393" -dependencies = [ - "rand_core", - "subtle", -] - -[[package]] -name = "flagset" -version = "0.4.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7ac824320a75a52197e8f2d787f6a38b6718bb6897a35142d749af3c0e8f4fe" - -[[package]] -name = "generic-array" -version = "0.14.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" -dependencies = [ - "typenum", - "version_check", - "zeroize", -] - -[[package]] -name = "getrandom" -version = "0.2.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" -dependencies = [ - "cfg-if", - "libc", - "wasi", -] - -[[package]] -name = "group" -version = "0.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0f9ef7462f7c099f518d754361858f86d8a07af53ba9af0fe635bbccb151a63" -dependencies = [ - "ff", - "rand_core", - "subtle", -] - -[[package]] -name = "hex" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" - -[[package]] -name = "hkdf" -version = "0.12.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b5f8eb2ad728638ea2c7d47a21db23b7b58a72ed6a38256b8a1849f15fbbdf7" -dependencies = [ - "hmac", -] - -[[package]] -name = "hmac" -version = "0.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" -dependencies = [ - "digest", -] - -[[package]] -name = "libc" -version = "0.2.176" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "58f929b4d672ea937a23a1ab494143d968337a5f47e56d0815df1e0890ddf174" - -[[package]] -name = "p384" -version = "0.13.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fe42f1670a52a47d448f14b6a5c61dd78fce51856e68edaa38f7ae3a46b8d6b6" -dependencies = [ - "ecdsa", - "elliptic-curve", - "primeorder", - "sha2", -] - -[[package]] -name = "pem-rfc7468" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "88b39c9bfcfc231068454382784bb460aae594343fb030d46e9f50a645418412" -dependencies = [ - "base64ct", -] - -[[package]] -name = "pkcs8" -version = "0.10.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" -dependencies = [ - "der", - "spki", -] - -[[package]] -name = "ppv-lite86" -version = "0.2.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" -dependencies = [ - "zerocopy", -] - -[[package]] -name = "primeorder" -version = "0.13.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "353e1ca18966c16d9deb1c69278edbc5f194139612772bd9537af60ac231e1e6" -dependencies = [ - "elliptic-curve", -] - -[[package]] -name = "proc-macro2" -version = "1.0.101" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89ae43fd86e4158d6db51ad8e2b80f313af9cc74f5c0e03ccb87de09998732de" -dependencies = [ - "unicode-ident", -] - -[[package]] -name = "quote" -version = "1.0.40" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" -dependencies = [ - "proc-macro2", -] - -[[package]] -name = "rand" -version = "0.8.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" -dependencies = [ - "libc", - "rand_chacha", - "rand_core", -] - -[[package]] -name = "rand_chacha" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" -dependencies = [ - "ppv-lite86", - "rand_core", -] - -[[package]] -name = "rand_core" -version = "0.6.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" -dependencies = [ - "getrandom", -] - -[[package]] -name = "rfc6979" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8dd2a808d456c4a54e300a23e9f5a67e122c3024119acbfd73e3bf664491cb2" -dependencies = [ - "hmac", - "subtle", -] - -[[package]] -name = "sec1" -version = "0.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3e97a565f76233a6003f9f5c54be1d9c5bdfa3eccfb189469f11ec4901c47dc" -dependencies = [ - "base16ct", - "der", - "generic-array", - "pkcs8", - "subtle", - "zeroize", -] - -[[package]] -name = "sha2" -version = "0.10.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" -dependencies = [ - "cfg-if", - "cpufeatures", - "digest", -] - -[[package]] -name = "signature" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" -dependencies = [ - "digest", - "rand_core", -] - -[[package]] -name = "spdm-lib" -version = "0.1.0" -dependencies = [ - "bitfield", - "der", - "digest", - "hex", - "p384", - "rand", - "sha2", - "x509-cert", - "zerocopy", -] - -[[package]] -name = "spki" -version = "0.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d" -dependencies = [ - "base64ct", - "der", -] - -[[package]] -name = "subtle" -version = "2.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" - -[[package]] -name = "syn" -version = "2.0.106" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ede7c438028d4436d71104916910f5bb611972c5cfd7f89b8300a8186e6fada6" -dependencies = [ - "proc-macro2", - "quote", - "unicode-ident", -] - -[[package]] -name = "tls_codec" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0de2e01245e2bb89d6f05801c564fa27624dbd7b1846859876c7dad82e90bf6b" -dependencies = [ - "tls_codec_derive", - "zeroize", -] - -[[package]] -name = "tls_codec_derive" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d2e76690929402faae40aebdda620a2c0e25dd6d3b9afe48867dfd95991f4bd" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "typenum" -version = "1.18.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1dccffe3ce07af9386bfd29e80c0ab1a8205a2fc34e4bcd40364df902cfa8f3f" - -[[package]] -name = "unicode-ident" -version = "1.0.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f63a545481291138910575129486daeaf8ac54aee4387fe7906919f7830c7d9d" - -[[package]] -name = "version_check" -version = "0.9.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" - -[[package]] -name = "wasi" -version = "0.11.1+wasi-snapshot-preview1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" - -[[package]] -name = "x509-cert" -version = "0.2.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1301e935010a701ae5f8655edc0ad17c44bad3ac5ce8c39185f75453b720ae94" -dependencies = [ - "const-oid", - "der", - "spki", - "tls_codec", -] - -[[package]] -name = "zerocopy" -version = "0.8.27" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0894878a5fa3edfd6da3f88c4805f4c8558e2b996227a3d864f47fe11e38282c" -dependencies = [ - "zerocopy-derive", -] - -[[package]] -name = "zerocopy-derive" -version = "0.8.27" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "88d2b8d9c68ad2b9e4340d7832716a4d21a22a1154777ad56ea55c51a9cf3831" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "zeroize" -version = "1.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" -dependencies = [ - "zeroize_derive", -] - -[[package]] -name = "zeroize_derive" -version = "1.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] diff --git a/Cargo.toml b/Cargo.toml index 7c92306..8883491 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,40 +8,28 @@ edition = "2021" [features] default = [] -# Enable to use std-dependent transports/utilities -std = [] -# TCP transport (requires std); opt-in so embedded/no_std users unaffected. -tcp-transport = ["std"] - # Rand-based RNG implementation (requires std for OS entropy via getrandom) - rand-rng = ["std", "rand"] - # Linux evidence mock implementation - linux-evidence = ["std"] - # Linux placeholder certificate store implementation - linux-certs = ["std"] - # Cryptographic implementations - crypto = ["std", "sha2", "p384", "digest", "rand"] [dependencies] bitfield = "0.14.0" zerocopy = { version = "0.8.17", features = ["derive"] } -rand = { version = "0.8.5", optional = true } -# Cryptographic dependencies -sha2 = { version = "0.10", optional = true } -p384 = { version = "0.13", features = ["ecdsa", "pem"], optional = true } -digest = { version = "0.10", optional = true } -hex = "0.4.3" -x509-cert = "0.2.5" -der = "0.7.10" [dev-dependencies] +rand = "0.8.5" +# Cryptographic dependencies +sha2 = { version = "0.10"} +p384 = { version = "0.13", features = ["ecdsa", "pem"] } +digest = { version = "0.10"} +hex = "0.4.3" +der = "0.8" +x509-cert = "0.3.0-rc.4" +signature = "2" +clap = { version = "4", features = ["derive"] } # Examples [[example]] name = "spdm_responder" path = "examples/spdm_responder.rs" -required-features = ["std", "crypto"] [[example]] -name = "test_static_certs" -path = "examples/test_static_certs.rs" -required-features = ["std"] +name = "spdm_requester" +path = "examples/spdm_requester.rs" diff --git a/examples/README.md b/examples/README.md index 5569f66..e4cb946 100644 --- a/examples/README.md +++ b/examples/README.md @@ -13,7 +13,7 @@ A demonstration SPDM responder application that shows how to: ### Usage ```bash -# Build the example +# Build the example responder cargo build --example spdm_responder --features std,crypto # Run with default settings diff --git a/examples/certs/ca.cert.der b/examples/certs/ca.cert.der new file mode 100644 index 0000000..8c6f525 Binary files /dev/null and b/examples/certs/ca.cert.der differ diff --git a/examples/certs/end_responder.cert.der b/examples/certs/end_responder.cert.der new file mode 100644 index 0000000..5be1acc Binary files /dev/null and b/examples/certs/end_responder.cert.der differ diff --git a/examples/certs/end_responder.key.der b/examples/certs/end_responder.key.der new file mode 100644 index 0000000..8dcaa6c Binary files /dev/null and b/examples/certs/end_responder.key.der differ diff --git a/examples/certs/inter.cert.der b/examples/certs/inter.cert.der new file mode 100644 index 0000000..b819986 Binary files /dev/null and b/examples/certs/inter.cert.der differ diff --git a/examples/platform/cert_store.rs b/examples/platform/cert_store.rs index 635117f..20e514f 100644 --- a/examples/platform/cert_store.rs +++ b/examples/platform/cert_store.rs @@ -1,76 +1,81 @@ -// Licensed under the Apache-2.0 license +// Copyright 2025 +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. //! Certificate Store Platform Implementation -//! +//! //! Provides certificate management using static certificates with ECDSA signing use std::sync::Mutex; -#[cfg(feature = "crypto")] use p384::{ - ecdsa::{SigningKey, Signature, signature::hazmat::PrehashSigner}, - SecretKey + ecdsa::{signature::hazmat::PrehashSigner, Signature, SigningKey}, + SecretKey, }; +use zerocopy::FromBytes; - - -use spdm_lib::cert_store::{SpdmCertStore, CertStoreResult, CertStoreError}; -use spdm_lib::protocol::algorithms::{AsymAlgo, ECC_P384_SIGNATURE_SIZE, SHA384_HASH_SIZE}; -use spdm_lib::protocol::certs::{CertificateInfo, KeyUsageMask}; -use super::certs::{STATIC_ROOT_CA_CERT, STATIC_ATTESTATION_CERT, ATTESTATION_PRIVATE_KEY}; +use super::certs::{ + STATIC_END_CERT, STATIC_END_RESPONDER_KEY_DER, STATIC_INTER_CERT, STATIC_ROOT_CA_CERT, +}; +use spdm_lib::commands::challenge::MeasurementSummaryHashType; +use spdm_lib::protocol::{ + algorithms::{AsymAlgo, ECC_P384_SIGNATURE_SIZE, SHA384_HASH_SIZE}, + SpdmCertChainHeader, +}; +use spdm_lib::protocol::{ + certs::{CertificateInfo, KeyUsageMask}, + BaseHashAlgoType, +}; +use spdm_lib::{ + cert_store::{CertStoreError, CertStoreResult, PeerCertStore, ReassemblyStatus, SpdmCertStore}, + error::PlatformError, +}; /// Certificate store with proper ECDSA signing pub struct DemoCertStore { cert_chain: Vec, - #[cfg(feature = "crypto")] signing_key: Mutex>, } impl DemoCertStore { pub fn new() -> Self { - #[cfg(feature = "crypto")] - { - println!("Loading static certificate chain..."); - let (cert_chain, signing_key) = Self::generate_certificate_chain(); - println!("Static certificate chain loaded successfully"); - - Self { - cert_chain, - signing_key: Mutex::new(Some(signing_key)), - } - } - - #[cfg(not(feature = "crypto"))] - { - // Fallback for when crypto feature is not enabled - let cert_chain = b"DEMO_CERTIFICATE_CHAIN_DATA".to_vec(); - Self { cert_chain } + println!("Loading static certificate chain..."); + let (cert_chain, signing_key) = Self::generate_certificate_chain(); + println!("Static certificate chain loaded successfully"); + + Self { + cert_chain, + signing_key: Mutex::new(Some(signing_key)), } } - #[cfg(feature = "crypto")] fn generate_certificate_chain() -> (Vec, SigningKey) { - println!("🔧 DIRECT CERTIFICATE CHAIN - RAW CONCATENATION"); - - // SIMPLE APPROACH: Just concatenate Root CA + Attestation certificates - // Let the SPDM library handle its own formatting + // Concatenate Root CA + Intermediate + End-entity certificates let mut cert_chain = Vec::new(); cert_chain.extend_from_slice(STATIC_ROOT_CA_CERT); - cert_chain.extend_from_slice(STATIC_ATTESTATION_CERT); - - println!(" ✅ Raw certificates: Root({}) + Attestation({})", STATIC_ROOT_CA_CERT.len(), STATIC_ATTESTATION_CERT.len()); - println!(" Total length: {} bytes", cert_chain.len()); - println!(" Root starts: {:02x?}", &cert_chain[..4]); - println!(" Attestation starts: {:02x?}", &cert_chain[STATIC_ROOT_CA_CERT.len()..STATIC_ROOT_CA_CERT.len()+4]); + cert_chain.extend_from_slice(STATIC_INTER_CERT); + cert_chain.extend_from_slice(STATIC_END_CERT); - let secret_key = SecretKey::from_bytes(ATTESTATION_PRIVATE_KEY.into()) - .expect("Failed to parse secret key from static data"); - - let attestation_key = SigningKey::from(secret_key); + // Parse the P-384 private key from SEC1 DER format. + // SEC1 ECPrivateKey: SEQUENCE { version INTEGER, privateKey OCTET STRING(48), ... } + // For a P-384 key with 2-byte length header: skip 8 bytes to reach the raw 48-byte scalar. + let raw_key: &[u8; 48] = STATIC_END_RESPONDER_KEY_DER[8..56] + .try_into() + .expect("key DER too short"); + let secret_key = + SecretKey::from_bytes(raw_key.into()).expect("Failed to parse end-entity private key"); - println!("🔑 Static attestation signing key loaded"); - - (cert_chain, attestation_key) + (cert_chain, SigningKey::from(secret_key)) } /// Extract the first certificate from a DER-encoded certificate chain @@ -79,15 +84,15 @@ impl DemoCertStore { if cert_chain.len() < 2 { return None; } - + let mut offset = 0; - + // Check for SEQUENCE tag (0x30) if cert_chain[offset] != 0x30 { return None; } offset += 1; - + // Parse length and calculate total certificate size let (content_length, header_size) = if cert_chain[offset] & 0x80 == 0 { // Short form length (0-127) @@ -101,26 +106,26 @@ impl DemoCertStore { return None; } offset += 1; - + if offset + length_octets > cert_chain.len() { return None; } - + let mut content_len = 0; for i in 0..length_octets { content_len = (content_len << 8) | cert_chain[offset + i] as usize; } - + let header_len = 2 + length_octets; // tag + length indicator + length bytes (content_len, header_len) }; - + let total_cert_size = header_size + content_length; - + if total_cert_size > cert_chain.len() { return None; } - + Some(&cert_chain[0..total_cert_size]) } } @@ -138,7 +143,7 @@ impl SpdmCertStore for DemoCertStore { if slot_id == 0 { Ok(self.cert_chain.len()) } else { - Err(CertStoreError::InvalidSlotId) + Err(CertStoreError::InvalidSlotId(slot_id)) } } @@ -150,7 +155,7 @@ impl SpdmCertStore for DemoCertStore { cert_portion: &'a mut [u8], ) -> CertStoreResult { if slot_id != 0 { - return Err(CertStoreError::InvalidSlotId); + return Err(CertStoreError::InvalidSlotId(slot_id)); } if offset >= self.cert_chain.len() { @@ -161,7 +166,7 @@ impl SpdmCertStore for DemoCertStore { let copy_len = remaining.min(cert_portion.len()); cert_portion[..copy_len].copy_from_slice(&self.cert_chain[offset..offset + copy_len]); - // println!(" Cert Chain Copy: {:02x?}", &cert_portion[..copy_len]); + // println!(" Cert Chain Copy: {:02x?}", &cert_portion[..copy_len]); Ok(copy_len) } @@ -172,27 +177,16 @@ impl SpdmCertStore for DemoCertStore { cert_hash: &'a mut [u8; SHA384_HASH_SIZE], ) -> CertStoreResult<()> { if slot_id != 0 { - return Err(CertStoreError::InvalidSlotId); + return Err(CertStoreError::InvalidSlotId(slot_id)); } - #[cfg(feature = "crypto")] - { - use sha2::{Sha384, Digest}; - // Calculate proper SHA-384 hash of the root certificate - let mut hasher = Sha384::new(); - hasher.update(STATIC_ROOT_CA_CERT); - let hash_result = hasher.finalize(); - cert_hash.copy_from_slice(&hash_result); - // println!(" Fabrizio Root Hash starts: {:02x?}", &cert_hash[..4]); - } - - #[cfg(not(feature = "crypto"))] - { - // Fallback for when crypto feature is not enabled - for (i, byte) in cert_hash.iter_mut().enumerate() { - *byte = STATIC_ROOT_CA_CERT.get(i % STATIC_ROOT_CA_CERT.len()).copied().unwrap_or(0); - } - } + use sha2::{Digest, Sha384}; + // Calculate proper SHA-384 hash of the root certificate + let mut hasher = Sha384::new(); + hasher.update(STATIC_ROOT_CA_CERT); + let hash_result = hasher.finalize(); + cert_hash.copy_from_slice(&hash_result); + // println!(" Fabrizio Root Hash starts: {:02x?}", &cert_hash[..4]); Ok(()) } @@ -204,39 +198,30 @@ impl SpdmCertStore for DemoCertStore { signature: &'a mut [u8; ECC_P384_SIGNATURE_SIZE], ) -> CertStoreResult<()> { if slot_id != 0 { - return Err(CertStoreError::InvalidSlotId); + return Err(CertStoreError::InvalidSlotId(slot_id)); } - #[cfg(feature = "crypto")] - { - if let Ok(signing_key_guard) = self.signing_key.lock() { - if let Some(ref signing_key) = *signing_key_guard { - - let sig: Signature = signing_key.sign_prehash(hash).unwrap(); - - let sig_bytes = sig.to_bytes(); - if sig_bytes.len() <= ECC_P384_SIGNATURE_SIZE { - signature[..sig_bytes.len()].copy_from_slice(&sig_bytes); - return Ok(()); - } - return Err(CertStoreError::PlatformError); + if let Ok(signing_key_guard) = self.signing_key.lock() { + if let Some(ref signing_key) = *signing_key_guard { + let sig: Signature = signing_key.sign_prehash(hash).unwrap(); + + let sig_bytes = sig.to_bytes(); + if sig_bytes.len() <= ECC_P384_SIGNATURE_SIZE { + signature[..sig_bytes.len()].copy_from_slice(&sig_bytes); + return Ok(()); } + return Err(CertStoreError::PlatformError); } - Err(CertStoreError::PlatformError) - } - - #[cfg(not(feature = "crypto"))] - { - // Fallback for demo without crypto - for (i, byte) in signature.iter_mut().enumerate() { - *byte = hash[i % SHA384_HASH_SIZE] ^ ((i as u8).wrapping_mul(73)); - } - Ok(()) } + Err(CertStoreError::PlatformError) } fn key_pair_id(&self, slot_id: u8) -> Option { - if slot_id == 0 { Some(1) } else { None } + if slot_id == 0 { + Some(1) + } else { + None + } } fn cert_info(&self, slot_id: u8) -> Option { @@ -261,26 +246,25 @@ impl SpdmCertStore for DemoCertStore { } } -#[cfg(all(test, feature = "crypto"))] #[test] fn test_signing() { use p384::ecdsa::signature::SignatureEncoding; - - // Create a known private key - let private_key_bytes = ATTESTATION_PRIVATE_KEY.to_vec(); - let secret_key = SecretKey::from_bytes((&private_key_bytes[..]).into()).unwrap(); + + // Load private key from SEC1 DER (raw 48-byte scalar at offset 8) + let raw_key: &[u8; 48] = STATIC_END_RESPONDER_KEY_DER[8..56].try_into().unwrap(); + let secret_key = SecretKey::from_bytes(raw_key.into()).unwrap(); let signing_key = SigningKey::from(secret_key); - + // Your input let input = hex::decode("32ac91a55d17db5e537448789486c633ecba4cd49185d0933f3d6561573fb68931f88bef4dc6ef20602df7dbeb51086b").unwrap(); - + // Test 1: Sign directly let sig_direct: Signature = signing_key.sign(&input); println!("Direct signature:"); let sig_bytes = sig_direct.to_bytes(); println!(" R: {}", hex::encode(&sig_bytes[..48])); println!(" S: {}", hex::encode(&sig_bytes[48..])); - + // Test 2: Hash then sign let digest = Sha384::digest(&input); let sig_hashed: Signature = signing_key.sign(&digest[..]); @@ -290,26 +274,25 @@ fn test_signing() { println!(" S: {}", hex::encode(&sig_bytes_hashed[48..])); } -#[cfg(all(test, feature = "crypto"))] #[test] fn debug_signing_verification() { - use p384::ecdsa::signature::SignatureEncoding; use hex; - + use p384::ecdsa::signature::SignatureEncoding; + // Your test data let input_hex = "32ac91a55d17db5e537448789486c633ecba4cd49185d0933f3d6561573fb68931f88bef4dc6ef20602df7dbeb51086b"; let input = hex::decode(input_hex).unwrap(); - - // Create signing key - let private_key_bytes = ATTESTATION_PRIVATE_KEY.to_vec(); - let secret_key = SecretKey::from_bytes((&private_key_bytes[..]).into()).unwrap(); + + // Load private key from SEC1 DER (raw 48-byte scalar at offset 8) + let raw_key: &[u8; 48] = STATIC_END_RESPONDER_KEY_DER[8..56].try_into().unwrap(); + let secret_key = SecretKey::from_bytes(raw_key.into()).unwrap(); let signing_key = SigningKey::from(secret_key); - + // Get public key let verifying_key = signing_key.verifying_key(); let public_point = verifying_key.to_encoded_point(false); println!("Public key: {}", hex::encode(public_point.as_bytes())); - + // Test 1: Sign directly println!("\n=== Test 1: Direct signing ==="); let sig1: Signature = signing_key.sign(&input); @@ -317,11 +300,11 @@ fn debug_signing_verification() { println!("Input: {}", input_hex); println!("Signature R: {}", hex::encode(&sig1_bytes[..48])); println!("Signature S: {}", hex::encode(&sig1_bytes[48..])); - + // Verify with Rust let verify_result = verifying_key.verify(&input, &sig1); println!("Rust verification: {:?}", verify_result); - + // Test 2: Hash then sign println!("\n=== Test 2: Hash then sign ==="); let hashed = Sha384::digest(&input); @@ -330,11 +313,11 @@ fn debug_signing_verification() { let sig2_bytes = sig2.to_bytes(); println!("Signature R: {}", hex::encode(&sig2_bytes[..48])); println!("Signature S: {}", hex::encode(&sig2_bytes[48..])); - + // Verify with Rust let verify_result2 = verifying_key.verify(&hashed, &sig2); println!("Rust verification of hashed: {:?}", verify_result2); - + // Test 3: What Python expects println!("\n=== For Python Testing ==="); println!("# Test direct signature"); @@ -344,7 +327,10 @@ fn debug_signing_verification() { println!("import binascii"); println!(); println!(r#"data = binascii.unhexlify("{}")"#, input_hex); - println!(r#"pubkey = binascii.unhexlify("{}")"#, hex::encode(public_point.as_bytes())); + println!( + r#"pubkey = binascii.unhexlify("{}")"#, + hex::encode(public_point.as_bytes()) + ); println!(r#"r1 = int("{}", 16)"#, hex::encode(&sig1_bytes[..48])); println!(r#"s1 = int("{}", 16)"#, hex::encode(&sig1_bytes[48..])); println!(); @@ -361,4 +347,314 @@ fn debug_signing_verification() { println!(" public_key.verify(sig1, data, ec.ECDSA(hashes.SHA384()))"); println!(" print('✓ Sig1 valid with SHA384')"); println!("except: print('✗ Sig1 invalid with SHA384')"); -} \ No newline at end of file +} + +#[derive(Debug, Default)] +pub struct PeerSlot { + /// CertChain[K], retrieved in `CERTIFICATE` response. + pub cert_chain: Vec, + + /// Digest[K], retrieved in `DIGESTS` response. + pub digest: Vec, + + /// `KeyPairID[K]`, retrieved in `DIGESTS` response if the corresponding `MULTI_KEY_CONN_REQ` or `MULTI_KEY_CONN_RSP` is true. + pub keypair_id: Option, + + /// `CertificateInfo[K]`, retrieved in `DIGESTS` response if the corresponding `MULTI_KEY_CONN_REQ` or `MULTI_KEY_CONN_RSP` is true. pub cert_info: Option + pub certificate_info: Option, + + /// KeyUsageMask[K], retrieved in `DIGESTS` response if the corresponding `MULTI_KEY_CONN_REQ` or `MULTI_KEY_CONN_RSP` is true. + pub key_usage_mask: Option, + + pub requested_msh_type: Option, +} + +impl PeerSlot { + /// Get the digest for the root certificate of the chain + /// + /// # Arguments + /// * `hash_algo` - The hash algorithm negotiated with the peer. + fn get_root_hash(&self, hash_algo: BaseHashAlgoType) -> Option<&[u8]> { + let (length, rest) = SpdmCertChainHeader::ref_from_prefix(&self.cert_chain).ok()?; + if length.get_length() != self.cert_chain.len() as u32 { + println!( + "[Error] cert chain length mismatch (expected {}, got {})", + length.get_length(), + self.cert_chain.len() + ); + return None; + } + Some(&rest[..hash_algo.hash_byte_size()]) + } + /// Get the DER x509 certificate chain + /// + /// # Arguments + /// * `hash_algo` - The hash algorithm negotiated with the peer. + fn get_cert_chain(&self, hash_algo: BaseHashAlgoType) -> Option<&[u8]> { + let (length, rest) = SpdmCertChainHeader::ref_from_prefix(&self.cert_chain).ok()?; + if length.get_length() != self.cert_chain.len() as u32 { + println!( + "[Error] cert chain length mismatch (expected {}, got {})", + length.get_length(), + self.cert_chain.len() + ); + return None; + } + Some(&rest[hash_algo.hash_byte_size()..]) + } +} + +/// Concrete implementation of `PeerCertStore` for demonstration purposes. +/// This example store manages a single certificate slot (slot 0) and allows +/// setting and retrieving the certificate chain, digest, key pair ID, certificate info, +/// and key usage mask for that slot. In a real implementation, you would likely +/// want to support multiple slots and have more robust error handling and storage mechanisms. +#[derive(Debug)] +pub struct ExamplePeerCertStore { + /// Retrieved from `DIGESTS` response, indicates which certificate slots are supported by the peer. + supported_slots_mask: u8, + + /// Retrieved from `DIGESTS` response, indicates which certificate slots are provisioned with valid certificate chains. + provisioned_slots_mask: u8, + + // Since not all existing slots may hold eligible certificate chains, keep the PeerSlot values optional. + pub peer_slots: Vec>, +} + +impl Default for ExamplePeerCertStore { + fn default() -> Self { + ExamplePeerCertStore { + supported_slots_mask: 0, + provisioned_slots_mask: 0, + peer_slots: vec![None], + } + } +} + +impl PeerCertStore for ExamplePeerCertStore { + fn slot_count(&self) -> u8 { + self.peer_slots.len() as u8 + } + + fn assemble( + &mut self, + slot_id: u8, + portion: &[u8], + ) -> Result { + let slot = self + .peer_slots + .get_mut(slot_id as usize) + .ok_or(CertStoreError::InvalidSlotId(slot_id))? + .as_mut() + .ok_or(CertStoreError::PlatformError)?; + + slot.cert_chain.extend_from_slice(portion); + + Ok(spdm_lib::cert_store::ReassemblyStatus::InProgress) + } + + fn reset(&mut self, slot_id: u8) { + if let Some(Some(slot)) = self.peer_slots.get_mut(slot_id as usize) { + *slot = PeerSlot::default(); + } + } + + fn get_raw_chain(&self, slot_id: u8) -> CertStoreResult<&[u8]> { + let slot = self + .peer_slots + .get(slot_id as usize) + .ok_or(CertStoreError::InvalidSlotId(slot_id))? + .as_ref() + .ok_or(CertStoreError::PlatformError)?; + Ok(&slot.cert_chain) + } + + fn get_cert_chain(&self, slot_id: u8, hash_algo: BaseHashAlgoType) -> CertStoreResult<&[u8]> { + let slot = self + .peer_slots + .get(slot_id as usize) + .ok_or(CertStoreError::InvalidSlotId(slot_id))? + .as_ref() + .ok_or(CertStoreError::PlatformError)?; + slot.get_cert_chain(hash_algo) + .ok_or(CertStoreError::CertReadError) + } + + /// Set the supported slots bit mask and initialize PeerSlot entries for any newly supported slots. + fn set_supported_slots(&mut self, slot_mask: u8) -> CertStoreResult<()> { + for b in 0..8 { + if slot_mask & (1 << b) == 1 { + if let Some(slot) = self.peer_slots.get_mut(b as usize) { + if slot.is_none() { + *slot = Some(PeerSlot::default()); + } + } + } + } + + Ok(()) + } + + fn get_supported_slots(&self) -> CertStoreResult { + Ok(self.supported_slots_mask) + } + + fn set_provisioned_slots(&mut self, provisioned_slot_mask: u8) -> CertStoreResult<()> { + self.provisioned_slots_mask = provisioned_slot_mask; + Ok(()) + } + + fn get_provisioned_slots(&self) -> CertStoreResult { + Ok(self.provisioned_slots_mask) + } + + /// Set the certificate chain for a given slot. This would typically be called + /// after successfully reassembling the certificate chain from received portions. + /// + /// # Returns + /// - `Ok(())` if the certificate chain was set successfully + /// - `Err(CertStoreError)` if there was an error (e.g., invalid slot ID) + fn set_cert_chain(&mut self, slot_id: u8, cert_chain: &[u8]) -> CertStoreResult<()> { + let slot = self + .peer_slots + .get_mut(slot_id as usize) + .ok_or(CertStoreError::InvalidSlotId(slot_id))? + .as_mut() + .ok_or(CertStoreError::PlatformError)?; + + slot.cert_chain = cert_chain.to_vec(); + Ok(()) + } + + fn get_digest(&self, slot_id: u8) -> CertStoreResult<&[u8]> { + let slot = self + .peer_slots + .get(slot_id as usize) + .ok_or(CertStoreError::InvalidSlotId(slot_id))? + .as_ref() + .ok_or(CertStoreError::PlatformError)?; + Ok(&slot.digest) + } + + /// Set the digest for a given slot, provided by the `DIGESTS` response. + /// + /// # Parameters + /// - `slot_id`: The slot ID to set the digest for + /// - `digest`: The digest value to set + fn set_digest(&mut self, slot_id: u8, digest: &[u8]) -> CertStoreResult<()> { + let slot: &mut PeerSlot = self + .peer_slots + .get_mut(slot_id as usize) + .ok_or(CertStoreError::InvalidSlotId(slot_id))? + .as_mut() + .ok_or(CertStoreError::PlatformError)?; + slot.digest = digest.to_vec(); + Ok(()) + } + + fn get_cert_info(&self, slot_id: u8) -> CertStoreResult { + let slot = self + .peer_slots + .get(slot_id as usize) + .ok_or(CertStoreError::InvalidSlotId(slot_id))? + .as_ref() + .ok_or(CertStoreError::PlatformError)?; + slot.certificate_info + .ok_or(CertStoreError::InvalidSlotId(slot_id)) + } + fn set_cert_info(&mut self, slot_id: u8, cert_info: CertificateInfo) -> CertStoreResult<()> { + let slot = self + .peer_slots + .get_mut(slot_id as usize) + .ok_or(CertStoreError::InvalidSlotId(slot_id))? + .as_mut() + .ok_or(CertStoreError::PlatformError)?; + slot.certificate_info = Some(cert_info); + Ok(()) + } + fn get_key_usage_mask(&self, slot_id: u8) -> CertStoreResult { + let slot = self + .peer_slots + .get(slot_id as usize) + .ok_or(CertStoreError::InvalidSlotId(slot_id))? + .as_ref() + .ok_or(CertStoreError::PlatformError)?; + slot.key_usage_mask + .ok_or(CertStoreError::InvalidSlotId(slot_id)) + } + + fn set_key_usage_mask( + &mut self, + slot_id: u8, + key_usage_mask: KeyUsageMask, + ) -> CertStoreResult<()> { + let slot = self + .peer_slots + .get_mut(slot_id as usize) + .ok_or(CertStoreError::InvalidSlotId(slot_id))? + .as_mut() + .ok_or(CertStoreError::PlatformError)?; + slot.key_usage_mask = Some(key_usage_mask); + Ok(()) + } + + fn get_keypair(&self, slot_id: u8) -> CertStoreResult { + let slot = self + .peer_slots + .get(slot_id as usize) + .ok_or(CertStoreError::InvalidSlotId(slot_id))? + .as_ref() + .ok_or(CertStoreError::PlatformError)?; + slot.keypair_id + .ok_or(CertStoreError::InvalidSlotId(slot_id)) + } + + fn set_keypair(&mut self, slot_id: u8, keypair: u8) -> CertStoreResult<()> { + let slot = self + .peer_slots + .get_mut(slot_id as usize) + .ok_or(CertStoreError::InvalidSlotId(slot_id))? + .as_mut() + .ok_or(CertStoreError::PlatformError)?; + slot.keypair_id = Some(keypair); + Ok(()) + } + + fn get_root_hash(&self, slot_id: u8, hash_algo: BaseHashAlgoType) -> CertStoreResult<&[u8]> { + let slot = self + .peer_slots + .get(slot_id as usize) + .ok_or(CertStoreError::InvalidSlotId(slot_id))? + .as_ref() + .ok_or(CertStoreError::PlatformError)?; + slot.get_root_hash(hash_algo) + .ok_or(CertStoreError::CertReadError) + } + + fn get_requested_msh_type(&self, slot_id: u8) -> CertStoreResult { + self.peer_slots + .get(slot_id as usize) + .ok_or(CertStoreError::InvalidSlotId(slot_id))? + .as_ref() + .ok_or(CertStoreError::PlatformError)? + .requested_msh_type + .clone() + .ok_or(CertStoreError::Undefined) + } + + fn set_requested_msh_type( + &mut self, + slot_id: u8, + msh_type: MeasurementSummaryHashType, + ) -> CertStoreResult<()> { + let slot = self + .peer_slots + .get_mut(slot_id as usize) + .ok_or(CertStoreError::InvalidSlotId(slot_id))? + .as_mut() + .ok_or(CertStoreError::PlatformError)?; + slot.requested_msh_type = Some(msh_type); + + Ok(()) + } +} diff --git a/examples/platform/certs.rs b/examples/platform/certs.rs index f01318f..5212443 100644 --- a/examples/platform/certs.rs +++ b/examples/platform/certs.rs @@ -1,129 +1,34 @@ -// Licensed under the Apache-2.0 license +// Copyright 2025 +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. -//! Static X.509 certificates for SPDM platform implementations -//! -//! These certificates are generated by OpenSSL and known to work correctly -//! with certificate verification. Generated on: September 25, 2025 -//! -//! -pub const ATTESTATION_PRIVATE_KEY: &[u8] = &[ - 0xd5, 0x76, 0x17, 0x2e, 0xe3, 0x5f, 0x3e, 0x62, 0xb2, 0xbd, 0x0c, 0x5e, - 0x7e, 0x6d, 0x8c, 0xe3, 0xa6, 0x05, 0xfe, 0x27, 0x80, 0x17, 0x37, 0x85, - 0x8b, 0x76, 0xa7, 0xd7, 0xfd, 0x8c, 0x0d, 0x26, 0x28, 0x41, 0x7a, 0x8b, - 0xb6, 0xbc, 0x17, 0x18, 0xc6, 0x9a, 0x10, 0x5c, 0x1e, 0xc8, 0x11, 0x70 -]; +//! DMTF libspdm ECP384 test certificate chain for SPDM platform implementations. +//! +//! Certificates sourced from: spdm-emu/libspdm/unit_test/sample_key/ecp384/ +//! - ca.cert.der: root CA (matches examples/cert/ecp384_ca.cert.der used by the requester) +//! - inter.cert.der: intermediate CA signed by the root +//! - end_responder.cert.der: end-entity cert signed by the intermediate CA +//! - end_responder.key.der: SEC1 DER-encoded P-384 private key for the end-entity cert -pub const STATIC_ROOT_CA_CERT: &[u8] = &[ - 0x30, 0x82, 0x02, 0x5d, 0x30, 0x82, 0x01, 0xe3, 0xa0, 0x03, 0x02, 0x01, - 0x02, 0x02, 0x14, 0x66, 0xd4, 0x94, 0x26, 0x31, 0x59, 0xc1, 0x0e, 0xfe, - 0xe2, 0xf1, 0x74, 0x8a, 0x4f, 0xb3, 0xd4, 0x13, 0x66, 0x7f, 0x93, 0x30, - 0x0a, 0x06, 0x08, 0x2a, 0x86, 0x48, 0xce, 0x3d, 0x04, 0x03, 0x03, 0x30, - 0x6e, 0x31, 0x0b, 0x30, 0x09, 0x06, 0x03, 0x55, 0x04, 0x06, 0x13, 0x02, - 0x55, 0x53, 0x31, 0x13, 0x30, 0x11, 0x06, 0x03, 0x55, 0x04, 0x08, 0x0c, - 0x0a, 0x43, 0x61, 0x6c, 0x69, 0x66, 0x6f, 0x72, 0x6e, 0x69, 0x61, 0x31, - 0x16, 0x30, 0x14, 0x06, 0x03, 0x55, 0x04, 0x07, 0x0c, 0x0d, 0x53, 0x61, - 0x6e, 0x20, 0x46, 0x72, 0x61, 0x6e, 0x63, 0x69, 0x73, 0x63, 0x6f, 0x31, - 0x18, 0x30, 0x16, 0x06, 0x03, 0x55, 0x04, 0x0a, 0x0c, 0x0f, 0x45, 0x78, - 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x20, 0x52, 0x6f, 0x6f, 0x74, 0x20, 0x43, - 0x41, 0x31, 0x18, 0x30, 0x16, 0x06, 0x03, 0x55, 0x04, 0x03, 0x0c, 0x0f, - 0x45, 0x78, 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x20, 0x52, 0x6f, 0x6f, 0x74, - 0x20, 0x43, 0x41, 0x30, 0x1e, 0x17, 0x0d, 0x32, 0x35, 0x30, 0x39, 0x32, - 0x37, 0x30, 0x31, 0x32, 0x36, 0x34, 0x31, 0x5a, 0x17, 0x0d, 0x33, 0x35, - 0x30, 0x39, 0x32, 0x35, 0x30, 0x31, 0x32, 0x36, 0x34, 0x31, 0x5a, 0x30, - 0x6e, 0x31, 0x0b, 0x30, 0x09, 0x06, 0x03, 0x55, 0x04, 0x06, 0x13, 0x02, - 0x55, 0x53, 0x31, 0x13, 0x30, 0x11, 0x06, 0x03, 0x55, 0x04, 0x08, 0x0c, - 0x0a, 0x43, 0x61, 0x6c, 0x69, 0x66, 0x6f, 0x72, 0x6e, 0x69, 0x61, 0x31, - 0x16, 0x30, 0x14, 0x06, 0x03, 0x55, 0x04, 0x07, 0x0c, 0x0d, 0x53, 0x61, - 0x6e, 0x20, 0x46, 0x72, 0x61, 0x6e, 0x63, 0x69, 0x73, 0x63, 0x6f, 0x31, - 0x18, 0x30, 0x16, 0x06, 0x03, 0x55, 0x04, 0x0a, 0x0c, 0x0f, 0x45, 0x78, - 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x20, 0x52, 0x6f, 0x6f, 0x74, 0x20, 0x43, - 0x41, 0x31, 0x18, 0x30, 0x16, 0x06, 0x03, 0x55, 0x04, 0x03, 0x0c, 0x0f, - 0x45, 0x78, 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x20, 0x52, 0x6f, 0x6f, 0x74, - 0x20, 0x43, 0x41, 0x30, 0x76, 0x30, 0x10, 0x06, 0x07, 0x2a, 0x86, 0x48, - 0xce, 0x3d, 0x02, 0x01, 0x06, 0x05, 0x2b, 0x81, 0x04, 0x00, 0x22, 0x03, - 0x62, 0x00, 0x04, 0xbd, 0x63, 0x0d, 0x67, 0x2e, 0x64, 0xa3, 0x55, 0x11, - 0x19, 0x9a, 0x1f, 0x66, 0x7c, 0xc5, 0x9c, 0x61, 0x62, 0x3d, 0x40, 0xb6, - 0xe1, 0xff, 0x43, 0x7c, 0x39, 0x27, 0xc8, 0xec, 0xe9, 0x12, 0x3a, 0xa8, - 0xce, 0x53, 0x19, 0x06, 0xd1, 0xab, 0x4d, 0xd8, 0x04, 0x36, 0xc2, 0xb8, - 0x8d, 0xc1, 0x20, 0xe5, 0x0c, 0x34, 0x5e, 0x51, 0x8c, 0x73, 0x1f, 0x67, - 0xe9, 0xad, 0x64, 0x72, 0x58, 0x9c, 0x01, 0xb1, 0x38, 0xd6, 0x7b, 0xd0, - 0xad, 0xf6, 0x44, 0xa9, 0xa8, 0x16, 0xb6, 0x36, 0x10, 0xa8, 0xdc, 0x03, - 0x35, 0x8f, 0x4f, 0xa7, 0x5d, 0x00, 0x0e, 0x78, 0xd8, 0xee, 0x73, 0x8b, - 0xfc, 0x07, 0x0a, 0xa3, 0x42, 0x30, 0x40, 0x30, 0x0f, 0x06, 0x03, 0x55, - 0x1d, 0x13, 0x01, 0x01, 0xff, 0x04, 0x05, 0x30, 0x03, 0x01, 0x01, 0xff, - 0x30, 0x0e, 0x06, 0x03, 0x55, 0x1d, 0x0f, 0x01, 0x01, 0xff, 0x04, 0x04, - 0x03, 0x02, 0x01, 0x86, 0x30, 0x1d, 0x06, 0x03, 0x55, 0x1d, 0x0e, 0x04, - 0x16, 0x04, 0x14, 0x5a, 0xc3, 0x33, 0x0a, 0x3d, 0x7f, 0xe8, 0xc1, 0x4b, - 0x3d, 0xb8, 0x28, 0x80, 0xae, 0xb0, 0xfd, 0xab, 0x9d, 0x60, 0xaa, 0x30, - 0x0a, 0x06, 0x08, 0x2a, 0x86, 0x48, 0xce, 0x3d, 0x04, 0x03, 0x03, 0x03, - 0x68, 0x00, 0x30, 0x65, 0x02, 0x31, 0x00, 0x92, 0x51, 0x80, 0x7b, 0x10, - 0xef, 0xb7, 0x03, 0x5a, 0xdb, 0x94, 0xca, 0x2f, 0x10, 0x32, 0xac, 0xa5, - 0xba, 0xcc, 0x9b, 0x24, 0xe0, 0xfc, 0xed, 0x95, 0xfb, 0x6d, 0x2e, 0x8e, - 0xef, 0xfb, 0x52, 0xdf, 0xf0, 0x29, 0x6f, 0xcf, 0x46, 0x49, 0xaf, 0xdf, - 0x53, 0x2f, 0xc7, 0xc5, 0x5d, 0x71, 0xb1, 0x02, 0x30, 0x68, 0x76, 0x6f, - 0x7e, 0x02, 0xf7, 0x6f, 0xbf, 0x50, 0xfd, 0x50, 0xae, 0xc0, 0x48, 0xa9, - 0xd6, 0x97, 0x9e, 0x32, 0x69, 0x2d, 0x00, 0x94, 0xa4, 0x53, 0x6f, 0x9c, - 0x24, 0x23, 0x6c, 0xa6, 0x98, 0x16, 0x80, 0xe1, 0xe6, 0xc9, 0x39, 0x1a, - 0x53, 0xd6, 0xb1, 0x54, 0x74, 0xad, 0x8d, 0x89, 0xde -]; +/// DMTF libspdm ECP384 root CA certificate (DER-encoded). +/// This is the same cert as examples/cert/ecp384_ca.cert.der used by the requester. +pub const STATIC_ROOT_CA_CERT: &[u8] = include_bytes!("../certs/ca.cert.der"); -pub const STATIC_ATTESTATION_CERT: &[u8] = &[ - 0x30, 0x82, 0x02, 0xab, 0x30, 0x82, 0x02, 0x31, 0xa0, 0x03, 0x02, 0x01, - 0x02, 0x02, 0x14, 0x45, 0x7b, 0xc1, 0xae, 0x0b, 0xb9, 0x70, 0x17, 0x22, - 0x64, 0xfb, 0xf2, 0xb7, 0xac, 0xf4, 0x3a, 0xaa, 0xc0, 0x7c, 0x31, 0x30, - 0x0a, 0x06, 0x08, 0x2a, 0x86, 0x48, 0xce, 0x3d, 0x04, 0x03, 0x03, 0x30, - 0x6e, 0x31, 0x0b, 0x30, 0x09, 0x06, 0x03, 0x55, 0x04, 0x06, 0x13, 0x02, - 0x55, 0x53, 0x31, 0x13, 0x30, 0x11, 0x06, 0x03, 0x55, 0x04, 0x08, 0x0c, - 0x0a, 0x43, 0x61, 0x6c, 0x69, 0x66, 0x6f, 0x72, 0x6e, 0x69, 0x61, 0x31, - 0x16, 0x30, 0x14, 0x06, 0x03, 0x55, 0x04, 0x07, 0x0c, 0x0d, 0x53, 0x61, - 0x6e, 0x20, 0x46, 0x72, 0x61, 0x6e, 0x63, 0x69, 0x73, 0x63, 0x6f, 0x31, - 0x18, 0x30, 0x16, 0x06, 0x03, 0x55, 0x04, 0x0a, 0x0c, 0x0f, 0x45, 0x78, - 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x20, 0x52, 0x6f, 0x6f, 0x74, 0x20, 0x43, - 0x41, 0x31, 0x18, 0x30, 0x16, 0x06, 0x03, 0x55, 0x04, 0x03, 0x0c, 0x0f, - 0x45, 0x78, 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x20, 0x52, 0x6f, 0x6f, 0x74, - 0x20, 0x43, 0x41, 0x30, 0x1e, 0x17, 0x0d, 0x32, 0x35, 0x30, 0x39, 0x32, - 0x37, 0x30, 0x31, 0x32, 0x36, 0x34, 0x31, 0x5a, 0x17, 0x0d, 0x33, 0x35, - 0x30, 0x39, 0x32, 0x35, 0x30, 0x31, 0x32, 0x36, 0x34, 0x31, 0x5a, 0x30, - 0x7f, 0x31, 0x0b, 0x30, 0x09, 0x06, 0x03, 0x55, 0x04, 0x06, 0x13, 0x02, - 0x55, 0x53, 0x31, 0x13, 0x30, 0x11, 0x06, 0x03, 0x55, 0x04, 0x08, 0x0c, - 0x0a, 0x43, 0x61, 0x6c, 0x69, 0x66, 0x6f, 0x72, 0x6e, 0x69, 0x61, 0x31, - 0x16, 0x30, 0x14, 0x06, 0x03, 0x55, 0x04, 0x07, 0x0c, 0x0d, 0x53, 0x61, - 0x6e, 0x20, 0x46, 0x72, 0x61, 0x6e, 0x63, 0x69, 0x73, 0x63, 0x6f, 0x31, - 0x1d, 0x30, 0x1b, 0x06, 0x03, 0x55, 0x04, 0x0a, 0x0c, 0x14, 0x45, 0x78, - 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x20, 0x4f, 0x72, 0x67, 0x61, 0x6e, 0x69, - 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x31, 0x24, 0x30, 0x22, 0x06, 0x03, - 0x55, 0x04, 0x03, 0x0c, 0x1b, 0x41, 0x74, 0x74, 0x65, 0x73, 0x74, 0x61, - 0x74, 0x69, 0x6f, 0x6e, 0x20, 0x4b, 0x65, 0x79, 0x20, 0x43, 0x65, 0x72, - 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x65, 0x30, 0x76, 0x30, 0x10, - 0x06, 0x07, 0x2a, 0x86, 0x48, 0xce, 0x3d, 0x02, 0x01, 0x06, 0x05, 0x2b, - 0x81, 0x04, 0x00, 0x22, 0x03, 0x62, 0x00, 0x04, 0xd6, 0x09, 0x5e, 0x5f, - 0xc4, 0x2d, 0xa7, 0xf6, 0x9f, 0x8d, 0xdf, 0xc1, 0x9f, 0xd1, 0x20, 0xb8, - 0x25, 0x1a, 0x9c, 0xbf, 0xf7, 0x61, 0x5e, 0xce, 0xfd, 0x67, 0xff, 0x72, - 0x3e, 0xbf, 0xe8, 0x21, 0xca, 0x2b, 0xc2, 0xba, 0x7b, 0x81, 0x17, 0x29, - 0xb3, 0x33, 0x13, 0xbc, 0x07, 0xaa, 0xe7, 0x45, 0x4e, 0xb5, 0xe2, 0x2f, - 0x9f, 0xcf, 0x7b, 0x06, 0x5e, 0x27, 0x3f, 0x15, 0x42, 0x1e, 0xd0, 0x16, - 0xdb, 0x83, 0x1b, 0x9c, 0xef, 0xff, 0xf4, 0xe5, 0x9a, 0xf1, 0x16, 0x58, - 0x55, 0x3d, 0x14, 0x34, 0x76, 0x1a, 0x59, 0x01, 0x23, 0x11, 0x7b, 0x9f, - 0xc0, 0xa5, 0x4f, 0x96, 0x07, 0x73, 0xfc, 0x9b, 0xa3, 0x7f, 0x30, 0x7d, - 0x30, 0x0c, 0x06, 0x03, 0x55, 0x1d, 0x13, 0x01, 0x01, 0xff, 0x04, 0x02, - 0x30, 0x00, 0x30, 0x0e, 0x06, 0x03, 0x55, 0x1d, 0x0f, 0x01, 0x01, 0xff, - 0x04, 0x04, 0x03, 0x02, 0x06, 0xc0, 0x30, 0x1d, 0x06, 0x03, 0x55, 0x1d, - 0x25, 0x04, 0x16, 0x30, 0x14, 0x06, 0x08, 0x2b, 0x06, 0x01, 0x05, 0x05, - 0x07, 0x03, 0x03, 0x06, 0x08, 0x2b, 0x06, 0x01, 0x05, 0x05, 0x07, 0x03, - 0x04, 0x30, 0x1d, 0x06, 0x03, 0x55, 0x1d, 0x0e, 0x04, 0x16, 0x04, 0x14, - 0xc9, 0x9a, 0xce, 0x7f, 0x7d, 0xc4, 0xa1, 0xb4, 0xac, 0x8d, 0x56, 0x39, - 0xdd, 0x44, 0x7f, 0x19, 0x50, 0x8a, 0xb3, 0x88, 0x30, 0x1f, 0x06, 0x03, - 0x55, 0x1d, 0x23, 0x04, 0x18, 0x30, 0x16, 0x80, 0x14, 0x5a, 0xc3, 0x33, - 0x0a, 0x3d, 0x7f, 0xe8, 0xc1, 0x4b, 0x3d, 0xb8, 0x28, 0x80, 0xae, 0xb0, - 0xfd, 0xab, 0x9d, 0x60, 0xaa, 0x30, 0x0a, 0x06, 0x08, 0x2a, 0x86, 0x48, - 0xce, 0x3d, 0x04, 0x03, 0x03, 0x03, 0x68, 0x00, 0x30, 0x65, 0x02, 0x30, - 0x4d, 0x81, 0x2b, 0xf4, 0xc1, 0x7a, 0x60, 0x37, 0xd7, 0x94, 0x3e, 0xdd, - 0x86, 0x3e, 0xab, 0xd6, 0xfd, 0x59, 0x45, 0xee, 0x2a, 0x4e, 0xa3, 0xa0, - 0x38, 0xb0, 0x59, 0x5a, 0x75, 0xbf, 0x29, 0x35, 0x00, 0xa1, 0x44, 0x0d, - 0x00, 0xd0, 0x6b, 0xec, 0x54, 0x8d, 0xab, 0x60, 0x75, 0xdf, 0xa9, 0x68, - 0x02, 0x31, 0x00, 0xa2, 0x14, 0xe4, 0x0c, 0x4b, 0x27, 0xb5, 0xd4, 0xad, - 0xa0, 0x53, 0x67, 0x24, 0x1b, 0x19, 0x3f, 0x00, 0x88, 0x69, 0x0c, 0x30, - 0xe8, 0x24, 0xd9, 0x11, 0xd3, 0x16, 0xb6, 0x0d, 0x54, 0x1d, 0x2a, 0x52, - 0xb8, 0xd7, 0x33, 0x57, 0x3f, 0xaf, 0xf9, 0x6a, 0x42, 0x8f, 0xe5, 0xd9, - 0x15, 0x23, 0xbc -]; +/// DMTF libspdm ECP384 intermediate CA certificate (DER-encoded). +pub const STATIC_INTER_CERT: &[u8] = include_bytes!("../certs/inter.cert.der"); + +/// DMTF libspdm ECP384 end-entity (responder) certificate (DER-encoded). +pub const STATIC_END_CERT: &[u8] = include_bytes!("../certs/end_responder.cert.der"); + +/// SEC1 DER-encoded P-384 private key for the end-entity responder certificate. +pub const STATIC_END_RESPONDER_KEY_DER: &[u8] = include_bytes!("../certs/end_responder.key.der"); diff --git a/examples/platform/crypto.rs b/examples/platform/crypto.rs index 20dd4d0..a3d73c2 100644 --- a/examples/platform/crypto.rs +++ b/examples/platform/crypto.rs @@ -1,19 +1,29 @@ -// Licensed under the Apache-2.0 license +// Copyright 2025 +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. //! Cryptographic Platform Implementation -//! +//! //! Provides SHA-384 hash and system RNG implementations -#[cfg(feature = "crypto")] -use sha2::{Sha384, Digest}; +use sha2::{Digest, Sha384}; -use spdm_lib::platform::hash::{SpdmHash, SpdmHashAlgoType, SpdmHashResult, SpdmHashError}; +use spdm_lib::platform::hash::{SpdmHash, SpdmHashAlgoType, SpdmHashError, SpdmHashResult}; use spdm_lib::platform::rng::{SpdmRng, SpdmRngResult}; /// SHA-384 hash implementation using proper cryptography pub struct Sha384Hash { current_algo: SpdmHashAlgoType, - #[cfg(feature = "crypto")] hasher: Option, } @@ -21,39 +31,31 @@ impl Sha384Hash { pub fn new() -> Self { Self { current_algo: SpdmHashAlgoType::SHA384, - #[cfg(feature = "crypto")] hasher: None, } } } impl SpdmHash for Sha384Hash { - fn hash(&mut self, hash_algo: SpdmHashAlgoType, data: &[u8], hash: &mut [u8]) -> SpdmHashResult<()> { + fn hash( + &mut self, + hash_algo: SpdmHashAlgoType, + data: &[u8], + hash: &mut [u8], + ) -> SpdmHashResult<()> { if hash_algo != SpdmHashAlgoType::SHA384 { return Err(SpdmHashError::InvalidAlgorithm); } - + if hash.len() < 48 { return Err(SpdmHashError::BufferTooSmall); } - - #[cfg(feature = "crypto")] - { - let mut hasher = Sha384::new(); - hasher.update(data); - let result = hasher.finalize(); - hash[..48].copy_from_slice(&result[..]); - Ok(()) - } - - #[cfg(not(feature = "crypto"))] - { - // Fallback for demo purposes when crypto feature is not enabled - for (i, &byte) in data.iter().enumerate() { - hash[i % 48] ^= byte; - } - Ok(()) - } + + let mut hasher = Sha384::new(); + hasher.update(data); + let result = hasher.finalize(); + hash[..48].copy_from_slice(&result[..]); + Ok(()) } fn init(&mut self, hash_algo: SpdmHashAlgoType, data: Option<&[u8]>) -> SpdmHashResult<()> { @@ -61,29 +63,23 @@ impl SpdmHash for Sha384Hash { return Err(SpdmHashError::InvalidAlgorithm); } self.current_algo = hash_algo; - - #[cfg(feature = "crypto")] - { - let mut hasher = Sha384::new(); - if let Some(initial_data) = data { - hasher.update(initial_data); - } - self.hasher = Some(hasher); + + let mut hasher = Sha384::new(); + if let Some(initial_data) = data { + hasher.update(initial_data); } - + self.hasher = Some(hasher); + Ok(()) } fn update(&mut self, data: &[u8]) -> SpdmHashResult<()> { - #[cfg(feature = "crypto")] - { - if let Some(ref mut hasher) = self.hasher { - hasher.update(data); - } else { - return Err(SpdmHashError::PlatformError); - } + if let Some(ref mut hasher) = self.hasher { + hasher.update(data); + } else { + return Err(SpdmHashError::PlatformError); } - + Ok(()) } @@ -91,32 +87,20 @@ impl SpdmHash for Sha384Hash { if hash.len() < 48 { return Err(SpdmHashError::BufferTooSmall); } - - #[cfg(feature = "crypto")] - { - if let Some(hasher) = self.hasher.take() { - let result = hasher.finalize(); - hash[..48].copy_from_slice(&result[..]); - } else { - return Err(SpdmHashError::PlatformError); - } - } - - #[cfg(not(feature = "crypto"))] - { - // Fallback for demo - hash[..48].fill(0x42); + + if let Some(hasher) = self.hasher.take() { + let result = hasher.finalize(); + hash[..48].copy_from_slice(&result[..]); + } else { + return Err(SpdmHashError::PlatformError); } - + Ok(()) } fn reset(&mut self) { - #[cfg(feature = "crypto")] - { - if self.current_algo == SpdmHashAlgoType::SHA384 { - self.hasher = Some(Sha384::new()); - } + if self.current_algo == SpdmHashAlgoType::SHA384 { + self.hasher = Some(Sha384::new()); } } @@ -151,4 +135,4 @@ impl SpdmRng for SystemRng { } Ok(()) } -} \ No newline at end of file +} diff --git a/examples/platform/evidence.rs b/examples/platform/evidence.rs index 3699e2e..c89efc0 100644 --- a/examples/platform/evidence.rs +++ b/examples/platform/evidence.rs @@ -1,7 +1,19 @@ -// Licensed under the Apache-2.0 license +// Copyright 2025 +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. //! Evidence Platform Implementation -//! +//! //! Provides device measurements and evidence functionality use spdm_lib::platform::evidence::{SpdmEvidence, SpdmEvidenceResult}; @@ -27,4 +39,4 @@ impl SpdmEvidence for DemoEvidence { fn pcr_quote_size(&self, _with_pqc_sig: bool) -> SpdmEvidenceResult { Ok(b"DEMO_PCR_QUOTE_DATA_FOR_MEASUREMENTS".len()) } -} \ No newline at end of file +} diff --git a/examples/platform/mod.rs b/examples/platform/mod.rs index a183ff6..85e8a0b 100644 --- a/examples/platform/mod.rs +++ b/examples/platform/mod.rs @@ -1,17 +1,35 @@ -// Licensed under the Apache-2.0 license +// Copyright 2025 +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. //! Platform implementations for SPDM examples -//! +//! //! This module provides working platform implementations that can be easily //! swapped out for production implementations. -pub mod socket_transport; -pub mod crypto; +#![allow(unused_imports)] +#![allow(dead_code)] + pub mod cert_store; -pub mod evidence; pub mod certs; +pub mod crypto; +pub mod evidence; +pub mod socket_transport; -pub use socket_transport::SpdmSocketTransport; -pub use crypto::{Sha384Hash, SystemRng}; pub use cert_store::DemoCertStore; +pub use crypto::{Sha384Hash, SystemRng}; pub use evidence::DemoEvidence; +pub use socket_transport::SpdmSocketTransport; +// Certificate constants available for examples that need them +#[allow(unused_imports)] +pub use certs::{STATIC_END_CERT, STATIC_END_RESPONDER_KEY_DER, STATIC_INTER_CERT, STATIC_ROOT_CA_CERT}; diff --git a/examples/platform/socket_transport.rs b/examples/platform/socket_transport.rs index 1f2b682..fca72a9 100644 --- a/examples/platform/socket_transport.rs +++ b/examples/platform/socket_transport.rs @@ -1,23 +1,55 @@ -// Licensed under the Apache-2.0 license +// Copyright 2025 +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. //! Socket Transport Platform Implementation -//! -//! Provides TCP socket transport compatible with DMTF SPDM validator/emulator +//! +//! Provides TCP socket transport compatible with DMTF SPDM validator/emulator of type SOCKET_TRANSPORT_TYPE_NONE. +// Defined in DMTF Spec [DSP0287](https://www.dmtf.org/sites/default/files/standards/documents/DSP0287_1.0.0.pdf) "SPDM over TCP Binding Specification". +use std::io::{Read, Result as IoResult, Write}; use std::net::TcpStream; -use std::io::{Read, Write, Result as IoResult}; -use spdm_lib::platform::transport::{SpdmTransport, TransportResult, TransportError}; -use spdm_lib::codec::MessageBuf; +use clap::{Parser, ValueEnum}; +use spdm_lib::codec::{Codec, CodecError, CommonCodec, MessageBuf}; +use spdm_lib::platform::transport::{SpdmTransport, TransportError, TransportResult}; +use zerocopy::byteorder::{BigEndian, U32}; +use zerocopy::{FromBytes, Immutable, IntoBytes}; + +use crate::platform; /// Socket platform command types (from DMTF emulator) -#[derive(Debug, Clone, Copy, PartialEq)] +/// This is **NOT** part of the official DMTF spec, but is necessary to implement +/// [SocketTransportType::None]. +/// +/// # Protocol Flow +/// 1. Requester: Send (SOCKET_SPDM_COMMAND_TEST, b'Client Hello') to Responder +/// 2. Responder: Send (SOCKET_SPDM_COMMAND_TEST, b'Server Hello') to Requester + #[repr(u32)] +#[allow(unused)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +/// Socket Command definitions. +/// +/// See [spdm-emu/spdm_emu_common/command.h](https://github.com/DMTF/spdm-emu/blob/main/spdm_emu/spdm_emu_common/command.h). pub enum SocketSpdmCommand { Normal = 0x00000001, ClientHello = 0x00000003, - Shutdown = 0x00000004, - Unknown = 0x00000000, + // Shutdown = 0x00000004, + Shutdown = 0xFFFE, + // Unknown = 0x00000000, + Unknown = 0xFFFF, + Test = 0xDEAD, } impl From for SocketSpdmCommand { @@ -26,109 +58,438 @@ impl From for SocketSpdmCommand { 0x00000001 => SocketSpdmCommand::Normal, 0x00000003 => SocketSpdmCommand::ClientHello, 0x00000004 => SocketSpdmCommand::Shutdown, + 0xdead => SocketSpdmCommand::Test, _ => SocketSpdmCommand::Unknown, } } } +#[derive(Debug, Clone, Copy, PartialEq, ValueEnum)] +#[repr(u32)] +#[allow(non_camel_case_types, unused)] +#[clap(rename_all = "UPPER")] +pub enum SocketTransportType { + /// SOCKET_TRANSPORT_TYPE_NONE + None = 0x00, + MCTP = 0x01, + PCI_DOE = 0x02, + TCP = 0x03, +} + +impl From for SocketTransportType { + fn from(value: u32) -> Self { + match value { + 0x00 => SocketTransportType::None, + 0x01 => SocketTransportType::MCTP, + 0x02 => SocketTransportType::PCI_DOE, + 0x03 => SocketTransportType::TCP, + _ => SocketTransportType::None, + } + } +} + +/// # MCTP +/// Header = `IC | MessageType`, where +/// - `IC` is the integrity check byte, which is set to 0 for SPDM messages. +/// - `MessageType` is set to 0x5 for SPDM messages. +impl SocketTransportType { + pub fn transport_header(&self) -> TransportResult<&[u8]> { + match self { + SocketTransportType::None => Ok(&[]), + SocketTransportType::MCTP => Ok(&[0x5]), + SocketTransportType::PCI_DOE | SocketTransportType::TCP => { + Err(TransportError::UnsupportedTransportType) + } + } + } +} + +pub enum SocketMessageHeaderError { + Reserved, +} + /// Socket transport implementation for SPDM over TCP pub struct SpdmSocketTransport { stream: TcpStream, - raw: bool, - verbose: bool, + transport_type: SocketTransportType, } -impl SpdmSocketTransport { - const TCP_BINDING_VERSION: u8 = 0x01; - const TCP_MESSAGE_TYPE_OUT_OF_SESSION: u8 = 0x05; - const TCP_MESSAGE_TYPE_IN_SESSION: u8 = 0x06; +type BeU32 = U32; - /// Create a new socket transport - pub fn new(stream: TcpStream, raw: bool, verbose: bool) -> Self { - Self { stream, raw, verbose } +/// Socket Command Header that is used when using transport `SOCKET_TRANSPORT_TYPE_NONE`. +/// The payload of the according SPM message is appended to this. +#[repr(C)] +pub struct SocketSpdmCommandHdr { + /// SPDM-EMU custom socket command. + command: SocketSpdmCommand, + transport_type: SocketTransportType, + + /// Size of the appended SPDM message payload + payload_size: BeU32, +} + +impl From<&[u8; 12]> for SocketSpdmCommandHdr { + fn from(value: &[u8; 12]) -> Self { + let command_bytes: [u8; 4] = [value[0], value[1], value[2], value[3]]; + let transport_bytes: [u8; 4] = [value[4], value[5], value[6], value[7]]; + let payload_bytes: [u8; 4] = [value[8], value[9], value[10], value[11]]; + + SocketSpdmCommandHdr { + command: SocketSpdmCommand::from(u32::from_be_bytes(command_bytes)), + transport_type: SocketTransportType::from(u32::from_be_bytes(transport_bytes)), + payload_size: BeU32::new(u32::from_be_bytes(payload_bytes)), + } } +} + +impl Into<[u8; 12]> for SocketSpdmCommandHdr { + fn into(self) -> [u8; 12] { + let mut result = [0u8; 12]; + result[0..4].copy_from_slice(&(self.command as u32).to_be_bytes()); + result[4..8].copy_from_slice(&(self.transport_type as u32).to_be_bytes()); + result[8..12].copy_from_slice(&self.payload_size.get().to_be_bytes()); + result + } +} + +#[repr(u8)] +pub enum SpdmSocketTransportError { + /// The PayloadLen in the last received message is too large to be processed by the endpoint. + PayloadLenTooLong = 0xC0, - fn log_bytes(&self, prefix: &str, data: &[u8]) { - if !self.verbose { - return; - } - print!("{prefix}"); - for b in data { - print!("{:02x} ", b); - } - println!(); - } + /// The BindingVer in the last received message is not supported by the endpoint. + /// The binding version supported by the endpoint is indicated in the BindingVer + /// field of this message with MessageType of 0xC1. + BindVerNotSupported = 0xC1, - fn log_frame(&self, prefix: &str, header: &[u8], data: &[u8]) { - if !self.verbose { - return; + /// In the reach out model, the listener receives a Role-Inquiry Message from + /// the initiator. If the listener cannot operate as a Requester, then the listener + /// should send a message with MessageType of 0xC2 to the initiator. + CannotBeRequester = 0xC2, + + /// In the reach down model, if the listener receives an SPDM request message + /// from the initiator but cannot operate as a Responder, then the listener should + /// send a message with MessageType of 0xC3 to the initiator. + CannotBeResponder = 0xC3, + //0xC4 - 0xFF: Reserved. +} + +type SpdmSocketTransportResult = Result; + +impl TryFrom for SpdmSocketTransportError { + type Error = SocketMessageHeaderError; + + fn try_from(value: u8) -> Result { + match value { + 0xC0 => Ok(SpdmSocketTransportError::PayloadLenTooLong), + 0xC1 => Ok(SpdmSocketTransportError::BindVerNotSupported), + 0xC2 => Ok(SpdmSocketTransportError::CannotBeRequester), + 0xC3 => Ok(SpdmSocketTransportError::CannotBeResponder), + _ => Err(SocketMessageHeaderError::Reserved), + } + } +} + +impl Into for SpdmSocketTransportError { + fn into(self) -> u8 { + match self { + SpdmSocketTransportError::PayloadLenTooLong => 0xC0, + SpdmSocketTransportError::BindVerNotSupported => 0xC1, + SpdmSocketTransportError::CannotBeRequester => 0xC2, + SpdmSocketTransportError::CannotBeResponder => 0xC3, + } + } +} + +#[repr(u8)] +pub enum MessageType { + /// Out-of-Session Message. An SPDM message follows the header. + OutOfSession = 0x05, + + /// In-Session Message. An SPDM message follows the header. + InSession = 0x06, + + /// Role-Inquiry Message. **No** SPDM message follows the header. + RoleInquiry = 0xBF, + + /// Error messages. No SPDM message follows the header. + Error(SpdmSocketTransportError), + // Other values: reserved. +} + +impl TryFrom for MessageType { + type Error = SocketMessageHeaderError; + + fn try_from(value: u8) -> Result { + match value { + 0x05 => Ok(MessageType::OutOfSession), + 0x06 => Ok(MessageType::InSession), + 0xBF => Ok(MessageType::RoleInquiry), + 0xC0..=u8::MAX => Ok(MessageType::Error(SpdmSocketTransportError::try_from( + value, + )?)), + _ => Err(SocketMessageHeaderError::Reserved), + } + } +} + +impl Into for MessageType { + fn into(self) -> u8 { + match self { + Self::OutOfSession => 0x05, + Self::InSession => 0x06, + Self::RoleInquiry => 0xBf, + Self::Error(ste) => ste.into(), + } + } +} + +#[repr(C)] +#[derive(FromBytes, IntoBytes, Immutable)] +pub struct TcpSpdmBindingHeader { + /// Shall be the length of the SPDM message that follows the header. + payload_len: u16, + + /// Shall be 0x01 for this version of the binding specification. + binding_ver: u8, + + /// Shall indicate the message type. + message_type: u8, +} + +impl TcpSpdmBindingHeader { + pub fn new(payload_len: u16, message_type: MessageType) -> TcpSpdmBindingHeader { + Self { + payload_len, + binding_ver: 0x01, + message_type: message_type.into(), + } + } +} + +impl TryFrom<&[u8; 4]> for TcpSpdmBindingHeader { + type Error = SocketMessageHeaderError; + + fn try_from(value: &[u8; 4]) -> Result { + let payload_len = u16::from_le_bytes([value[0], value[1]]); + let binding_ver = value[2]; + let message_type = MessageType::try_from(value[3])?; + + Ok(TcpSpdmBindingHeader { + payload_len, + binding_ver, + message_type: match message_type { + MessageType::OutOfSession => 0x05, + MessageType::InSession => 0x06, + MessageType::RoleInquiry => 0xBF, + MessageType::Error(code) => code as u8, + }, + }) + } +} + +impl Codec for TcpSpdmBindingHeader { + fn encode(&self, buffer: &mut MessageBuf) -> spdm_lib::codec::CodecResult { + let len = core::mem::size_of::(); + buffer.push_data(len)?; + + let header = buffer.data_mut(len)?; + self.write_to(header).map_err(|_| CodecError::WriteError)?; + buffer.push_head(len)?; + + Ok(len) + } + + fn decode(buffer: &mut MessageBuf) -> spdm_lib::codec::CodecResult + where + Self: Sized, + { + let len = core::mem::size_of::(); + if buffer.data_len() < len { + Err(CodecError::BufferTooSmall)?; + } + let data = buffer.data(len)?; + let data = Self::read_from_bytes(data).map_err(|_| CodecError::ReadError)?; + buffer.pull_data(len)?; + + // if Self::DATA_KIND == DataKind::Header { + buffer.pull_head(len)?; + // } + Ok(data) + } +} + +/// For now, we ignore any TCP binding. This is a minimal example and the same +/// as the already implemented responder, we do use `SOCKET_TRANSPORT_TYPE_NONE`. +impl SpdmSocketTransport { + /// Create a new socket transport + pub fn new(stream: TcpStream, transport_type: SocketTransportType) -> Self { + Self { + stream, + transport_type, } - let mut buf = Vec::with_capacity(header.len() + data.len()); - buf.extend_from_slice(header); - buf.extend_from_slice(data); - self.log_bytes(prefix, &buf); } /// Receive platform data with socket message header - fn receive_platform_data(&mut self) -> IoResult<(SocketSpdmCommand, Vec)> { + /// + /// The transport specific headers such as MCTP header (see DSP0275) are encoded in the payload. + /// Note, that the payload size needs to be adjusted accordingly when sending/ receiving messages with transport specific headers. + pub(crate) fn receive_platform_data(&mut self) -> IoResult<(SocketSpdmCommand, Vec)> { // Read socket message header let mut header_bytes = [0u8; 12]; // sizeof(SocketMessageHeader) self.stream.read_exact(&mut header_bytes)?; - - let command = u32::from_be_bytes([header_bytes[0], header_bytes[1], header_bytes[2], header_bytes[3]]); - let _transport_type = u32::from_be_bytes([header_bytes[4], header_bytes[5], header_bytes[6], header_bytes[7]]); - let data_size = u32::from_be_bytes([header_bytes[8], header_bytes[9], header_bytes[10], header_bytes[11]]); - - let socket_command = SocketSpdmCommand::from(command); - - if data_size > 0 { - let mut data = vec![0u8; data_size as usize]; + let header = SocketSpdmCommandHdr::from(&header_bytes); + let payload_size = header.payload_size.get(); + + if payload_size > 0 { + let mut data = vec![0u8; payload_size as usize]; self.stream.read_exact(&mut data)?; - self.log_frame("RX frame: ", &header_bytes, &data); - Ok((socket_command, data)) - } else { - self.log_frame("RX frame: ", &header_bytes, &[]); - Ok((socket_command, Vec::new())) - } - } + + // Parse and remove transport specific headers from the payload. + match self.transport_type { + SocketTransportType::None => {} + SocketTransportType::MCTP => { + let mctp_header_got = data[0]; + let mctp_header_want = self.transport_type.transport_header().map_err(|e| { + std::io::Error::new(std::io::ErrorKind::Other, format!("{:?}", e)) + })?[0]; + + if mctp_header_got != mctp_header_want { + return Err(std::io::Error::new( + std::io::ErrorKind::Other, + "Invalid MCTP header", + )); + } + data.remove(0); + } + _ => { + return Err(std::io::Error::new( + std::io::ErrorKind::Other, + "Unsupported transport type", + )); + } + } + + Ok((header.command, data)) + } else { + Ok((header.command, Vec::new())) + } + } /// Send platform data with socket message header + /// + /// Depending on the [SocketTransportType], this may prepend additional transport-specific headers to the data. fn send_platform_data(&mut self, command: SocketSpdmCommand, data: &[u8]) -> IoResult<()> { - // Send header in big-endian format to match validator expectations - let command_bytes = (command as u32).to_be_bytes(); - let transport_bytes = if self.raw { 0u32 } else { 3u32 }.to_be_bytes(); // NONE when raw, TCP=3 otherwise - let size_bytes = (data.len() as u32).to_be_bytes(); - - self.stream.write_all(&command_bytes)?; - self.stream.write_all(&transport_bytes)?; - self.stream.write_all(&size_bytes)?; - - // Send data if any + let mut platform_header: &[u8] = &[]; + match self.transport_type { + SocketTransportType::None => {} + + SocketTransportType::MCTP => { + platform_header = &[0x5]; + } + _ => { + return Err(std::io::Error::new( + std::io::ErrorKind::Other, + "Unsupported transport type", + )); + } + } + + let header_bytes: [u8; 12] = SocketSpdmCommandHdr { + command: SocketSpdmCommand::from(command as u32), + transport_type: self.transport_type, + payload_size: BeU32::new((data.len() + platform_header.len()) as u32), + } + .into(); + + self.stream.write_all(&header_bytes)?; + if !data.is_empty() { + self.stream.write_all(platform_header)?; self.stream.write_all(data)?; } - // Log full frame (header + payload) - let mut header = Vec::with_capacity(12); - header.extend_from_slice(&command_bytes); - header.extend_from_slice(&transport_bytes); - header.extend_from_slice(&size_bytes); - self.log_frame("TX frame: ", &header, data); + self.stream.flush()?; + Ok(()) + } + + pub fn send_client_hello(&mut self) -> TransportResult<()> { + let message_data = b"Client Hello!\x00".as_bytes(); - self.stream.flush()?; - Ok(()) - } + self.send_platform_data(SocketSpdmCommand::Test, message_data) + .map_err(|_| TransportError::SendError)?; + Ok(()) + } } impl SpdmTransport for SpdmSocketTransport { - fn send_request<'a>(&mut self, _dest_eid: u8, _req: &mut MessageBuf<'a>) -> TransportResult<()> { - // Not used in responder mode - Err(TransportError::DriverError) + /// This function is only relevant for the SPDM Requester. + /// Send the SPDM Request encoded into [req] (header|payload]) via the platform transport + /// to and SPDM endpoint. + fn send_request<'a>(&mut self, _dest_eid: u8, req: &mut MessageBuf<'a>) -> TransportResult<()> { + let message_data = req + .message_data() + .map_err(|_| TransportError::BufferTooSmall)?; + + self.send_platform_data(SocketSpdmCommand::Normal, message_data) + .map_err(|_| TransportError::SendError)?; + Ok(()) } - fn receive_response<'a>(&mut self, _rsp: &mut MessageBuf<'a>) -> TransportResult<()> { - // Not used in responder mode - Err(TransportError::DriverError) + /// Initialize any transport-specific sequence state. + /// For SOCKET_TRANSPORT_TYPE_NONE, this means performing the handshake. + /// This function is only valid for the SPDM Requester. + fn init_sequence(&mut self) -> TransportResult<()> { + self.send_client_hello()?; + + match self.receive_platform_data() { + Ok((command, data)) => { + if command != SocketSpdmCommand::Test || data != b"Server Hello!\x00" { + return Err(TransportError::HandshakeNoneError); + } + Ok(()) + } + Err(_) => Err(TransportError::HandshakeNoneError), + } + } + + fn receive_response<'a>(&mut self, rsp: &mut MessageBuf<'a>) -> TransportResult<()> { + loop { + match self.receive_platform_data() { + Ok((command, data)) => { + if !data.is_empty() { + match command { + SocketSpdmCommand::Normal => { + if !data.is_empty() { + rsp.reset(); + rsp.put_data(data.len()) + .map_err(|_| TransportError::BufferTooSmall)?; + + rsp.data_mut(data.len()) + .map_err(|_| TransportError::BufferTooSmall)? + .copy_from_slice(&data); + + return Ok(()); + } + } + + SocketSpdmCommand::ClientHello => {} + SocketSpdmCommand::Shutdown => {} + SocketSpdmCommand::Unknown => {} + SocketSpdmCommand::Test => { + if data != b"Server Hello!" { + return Err(TransportError::HandshakeNoneError); + } + } + } + } + } + + Err(_) => { + return Err(TransportError::ReceiveError); + } + } + } } fn receive_request<'a>(&mut self, req: &mut MessageBuf<'a>) -> TransportResult<()> { @@ -138,72 +499,56 @@ impl SpdmTransport for SpdmSocketTransport { Ok((command, data)) => { match command { SocketSpdmCommand::Normal => { - if self.raw { - if data.is_empty() { - self.send_platform_data(SocketSpdmCommand::Unknown, &[]).map_err(|_| TransportError::SendError)?; - continue; - } + if !data.is_empty() { + // This is an SPDM message req.reset(); - req.put_data(data.len()).map_err(|_| TransportError::BufferTooSmall)?; - let buf = req.data_mut(data.len()).map_err(|_| TransportError::BufferTooSmall)?; + let data_len = data.len(); + req.put_data(data_len) + .map_err(|_| TransportError::BufferTooSmall)?; + let buf = req + .data_mut(data_len) + .map_err(|_| TransportError::BufferTooSmall)?; buf.copy_from_slice(&data); return Ok(()); - } - // Expect SPDM-over-TCP binding header: payload_length (LE, includes version+type+payload), version, type - if data.len() < 4 { - self.send_platform_data(SocketSpdmCommand::Unknown, &[]).map_err(|_| TransportError::SendError)?; - continue; - } - let payload_len = u16::from_le_bytes([data[0], data[1]]) as usize; - let binding_version = data[2]; - let message_type = data[3]; - - // payload_length = 2 (version+type) + SPDM_payload - let expected_total = payload_len + 2; - if binding_version != Self::TCP_BINDING_VERSION - || payload_len < 2 - || data.len() != expected_total - { - self.send_platform_data(SocketSpdmCommand::Unknown, &[]).map_err(|_| TransportError::SendError)?; + } else { + // Empty data - send empty response + self.send_platform_data(SocketSpdmCommand::Unknown, &[]) + .map_err(|_| TransportError::SendError)?; continue; } - - // Allow both in-session and out-of-session; in-session must carry at least session_id (4 bytes) - let spdm_payload = &data[4..]; - let spdm_payload_len = spdm_payload.len(); - let valid_type = match message_type { - Self::TCP_MESSAGE_TYPE_OUT_OF_SESSION => true, - Self::TCP_MESSAGE_TYPE_IN_SESSION => spdm_payload_len >= 4, - _ => false, - }; - if !valid_type || spdm_payload_len + 2 != payload_len { - self.send_platform_data(SocketSpdmCommand::Unknown, &[]).map_err(|_| TransportError::SendError)?; - continue; - } - - req.reset(); - req.put_data(spdm_payload_len).map_err(|_| TransportError::BufferTooSmall)?; - let buf = req.data_mut(spdm_payload_len).map_err(|_| TransportError::BufferTooSmall)?; - buf.copy_from_slice(spdm_payload); - return Ok(()); - }, + } SocketSpdmCommand::ClientHello => { // Handle client hello let response = b"Server Hello!"; - self.send_platform_data(SocketSpdmCommand::ClientHello, response).map_err(|_| TransportError::SendError)?; + self.send_platform_data(SocketSpdmCommand::ClientHello, response) + .map_err(|_| TransportError::SendError)?; continue; - }, + } SocketSpdmCommand::Shutdown => { - // Send shutdown response first - let _ = self.send_platform_data(SocketSpdmCommand::Shutdown, &[]); - return Err(TransportError::ReceiveError); - }, + // Send shutdown response first + let _ = self.send_platform_data(SocketSpdmCommand::Shutdown, &[]); + return Err(TransportError::ReceiveError); + } SocketSpdmCommand::Unknown => { - self.send_platform_data(SocketSpdmCommand::Unknown, &[]).map_err(|_| TransportError::SendError)?; - continue; + self.send_platform_data(SocketSpdmCommand::Unknown, &[]) + .map_err(|_| TransportError::SendError)?; + continue; + } + + // In a correct flow, this can only happen for the responder + SocketSpdmCommand::Test => { + if data == b"Client Hello!\x00" { + self.send_platform_data( + SocketSpdmCommand::Test, + b"Server Hello!\x00", + ) + .map_err(|_| TransportError::SendError)?; + } else { + return Err(TransportError::HandshakeNoneError); + } } } - }, + } Err(_) => { return Err(TransportError::ReceiveError); } @@ -213,30 +558,11 @@ impl SpdmTransport for SpdmSocketTransport { fn send_response<'a>(&mut self, resp: &mut MessageBuf<'a>) -> TransportResult<()> { // Extract response data and send with socket protocol - let message_data = resp.message_data().map_err(|_| TransportError::BufferTooSmall)?; - - if self.raw { - self.send_platform_data(SocketSpdmCommand::Normal, message_data).map_err(|_| TransportError::SendError)?; - return Ok(()); - } - - // Heuristic: SPDM header has version in high nibble == 0x1; otherwise assume in-session (session_id-prefixed) - let msg_type = if message_data.first().map(|b| (b >> 4) == 0x1).unwrap_or(false) { - Self::TCP_MESSAGE_TYPE_OUT_OF_SESSION - } else if message_data.len() >= 4 { - Self::TCP_MESSAGE_TYPE_IN_SESSION - } else { - return Err(TransportError::SendError); - }; - - let payload_len = (message_data.len() + 2) as u16; // version + type + payload - let mut framed = Vec::with_capacity(4 + message_data.len()); - framed.extend_from_slice(&payload_len.to_le_bytes()); - framed.push(Self::TCP_BINDING_VERSION); - framed.push(msg_type); - framed.extend_from_slice(message_data); - - self.send_platform_data(SocketSpdmCommand::Normal, &framed).map_err(|_| TransportError::SendError)?; + let message_data = resp + .message_data() + .map_err(|_| TransportError::BufferTooSmall)?; + self.send_platform_data(SocketSpdmCommand::Normal, message_data) + .map_err(|_| TransportError::SendError)?; Ok(()) } @@ -247,4 +573,4 @@ impl SpdmTransport for SpdmSocketTransport { fn header_size(&self) -> usize { 0 // No additional header for SPDM messages } -} \ No newline at end of file +} diff --git a/examples/spdm_requester.rs b/examples/spdm_requester.rs new file mode 100644 index 0000000..96871e8 --- /dev/null +++ b/examples/spdm_requester.rs @@ -0,0 +1,819 @@ +// Copyright 2025 +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! SPDM Example Responder utilizing the requester library. + +use std::fmt::Display; +use std::io::{Error, Result as IoResult}; +use std::net::TcpStream; + +use clap::Parser; +use der::{Decode, Encode}; +use p384::ecdsa::{Signature, VerifyingKey}; +use spdm_lib::codec::MessageBuf; +use spdm_lib::commands::certificate::request::generate_get_certificate; +use spdm_lib::commands::challenge::{ + request::generate_challenge_request, MeasurementSummaryHashType, +}; +use spdm_lib::commands::measurements::request::{ + generate_get_measurements, parse_measurements_response, +}; +use spdm_lib::commands::measurements::MeasurementOperation; +use spdm_lib::context::SpdmContext; +use spdm_lib::error::SpdmError; +use spdm_lib::protocol::algorithms::{ + AeadCipherSuite, AlgorithmPriorityTable, BaseAsymAlgo, BaseHashAlgo, DeviceAlgorithms, + DheNamedGroup, KeySchedule, LocalDeviceAlgorithms, MeasurementHashAlgo, + MeasurementSpecification, MelSpecification, OtherParamSupport, ReqBaseAsymAlg, +}; +use spdm_lib::protocol::signature::NONCE_LEN; +use spdm_lib::protocol::{self, version, BaseHashAlgoType, SpdmVersion}; +use spdm_lib::protocol::{CapabilityFlags, DeviceCapabilities}; + +// Import platform implementations - no duplicates! +mod platform; +use platform::{DemoCertStore, DemoEvidence, Sha384Hash, SpdmSocketTransport, SystemRng}; + +use spdm_lib::commands::algorithms::{ + request::generate_negotiate_algorithms_request, AlgStructure, AlgType, ExtendedAlgo, RegistryId, +}; +use spdm_lib::commands::capabilities::request::generate_capabilities_request_local; +use spdm_lib::commands::digests::request::generate_digest_request; +use spdm_lib::commands::version::{request::generate_get_version, VersionReqPayload}; + +use crate::platform::cert_store::ExamplePeerCertStore; +use spdm_lib::transcript::TranscriptContext; + +use x509_cert::Certificate; + +/// SPDM Example Requester +#[derive(Debug, Clone, Parser)] +#[command(about = "Real SPDM Library Integrated DMTF Compatible Requester")] +struct RequesterConfig { + /// TCP TCP port to connect to. + /// This needs to be supplied for both type NONE and MCTP. + #[arg(short, long, default_value_t = 2323)] + port: u16, + + /// Path to certificate file + #[arg(short, long, default_value = "device_cert.pem")] + cert_path: String, + + /// Path to private key file + #[arg(short = 'k', long, default_value = "device_key.pem")] + key_path: String, + + /// Path to measurements file + #[arg(short, long)] + measurements_path: Option, + + /// Enable verbose logging + #[arg(short, long)] + verbose: bool, + + /// Transport type to use for the connection + #[arg(short, long, default_value_t = platform::socket_transport::SocketTransportType::None, value_enum)] + transport_type: platform::socket_transport::SocketTransportType, +} + +/// Create SPDM device capabilities +fn create_device_capabilities() -> DeviceCapabilities { + let mut flags_value = 0u32; + flags_value |= 1 << 1; // cert_cap + flags_value |= 1 << 2; // chal_cap + flags_value |= 2 << 3; // meas_cap (with signature) + flags_value |= 1 << 5; // meas_fresh_cap + flags_value |= 1 << 17; // chunk_cap + + let flags = CapabilityFlags::new(flags_value); + + DeviceCapabilities { + ct_exponent: 0, + flags, + data_transfer_size: 1024, + max_spdm_msg_size: 4096, + include_supported_algorithms: false, + } +} + +/// Create local device algorithms +/// +/// Default values are: +/// - Measurement Specification: DMTF (1) +/// - Measurement Hash Algorithm: TPM_ALG_SHA_384 (1) +/// - Base Asymmetric Algorithm: TPM_ALG_ECDSA_ECC_NIST_P384 +/// - Base Hash Algorithm: TPM_ALG_SHA_384 (1) +/// - MEL Specification: 0 +fn create_local_algorithms<'a>() -> LocalDeviceAlgorithms<'a> { + // Configure supported algorithms with proper bitfield construction + let mut measurement_spec = MeasurementSpecification::default(); + measurement_spec.set_dmtf_measurement_spec(1); + + let mut measurement_hash_algo = MeasurementHashAlgo::default(); + measurement_hash_algo.set_tpm_alg_sha_384(1); + + let mut base_asym_algo = BaseAsymAlgo::default(); + base_asym_algo.set_tpm_alg_ecdsa_ecc_nist_p384(1); + + let mut base_hash_algo = BaseHashAlgo::default(); + base_hash_algo.set_tpm_alg_sha_384(1); + + let device_algorithms = DeviceAlgorithms { + measurement_spec, + other_param_support: OtherParamSupport::default(), + measurement_hash_algo, + base_asym_algo, + base_hash_algo, + mel_specification: MelSpecification(1), + dhe_group: DheNamedGroup(1), // FFDHE2048 + aead_cipher_suite: AeadCipherSuite(1), // AES_128_GCM + req_base_asym_algo: ReqBaseAsymAlg(1), + key_schedule: KeySchedule::default(), + }; + + // Keep this empty for now, since we need to wait for responders answer. + let algorithm_priority_table = AlgorithmPriorityTable::default(); + + LocalDeviceAlgorithms { + device_algorithms, + algorithm_priority_table, + } +} + +// Perform a VCS flow (Version, Capabilities, Algorithms) +// using the real SPDM library processing with platform implementations. +fn full_flow(stream: TcpStream, config: &RequesterConfig) -> IoResult<()> { + let mut transport = SpdmSocketTransport::new(stream, config.transport_type); + const EID: u8 = 0; + + // Create platform implementations - all from platform module! + let mut hash = Sha384Hash::new(); + let mut m1_hash = Sha384Hash::new(); + let mut l1_hash = Sha384Hash::new(); + let mut rng = SystemRng::new(); + let mut cert_store = DemoCertStore::new(); + let evidence = DemoEvidence::new(); + + // Create SPDM context + let supported_versions = [ + version::SpdmVersion::V13, + version::SpdmVersion::V12, + version::SpdmVersion::V11, + ]; + let capabilities = create_device_capabilities(); + let algorithms = create_local_algorithms(); + + let mut peer_cert_store = ExamplePeerCertStore::default(); + + if config.verbose { + println!( + "Client connected with transport type: {:?}", + config.transport_type + ); + } + + // TODO: The SpdmContext has to be adjusted (best in a generic way) to be requester compatible + // For now, keep the context the same and ignore the internal state tracking. + // Imho the the Responder is implemented wrong, since it tracks it's own state instead of the + // other parties state. + // So for now, we will re-use the state tracking and keep it in sync with the other parties state. + let mut spdm_context = match SpdmContext::new( + &supported_versions, + &mut transport, + capabilities, + algorithms, + &mut cert_store, + Some(&mut peer_cert_store), + &mut hash, + &mut m1_hash, + &mut l1_hash, + &mut rng, + &evidence, + ) { + Ok(ctx) => ctx, + Err(e) => { + eprintln!("Failed to create SPDM context: {:?}", e); + return Err(Error::other("SPDM context creation failed")); + } + }; + + if config.verbose { + println!("SPDM context created successfully"); + } + + // Before we can start, we need to do the inofficial handshake for SOCKET_TRANSPORT_TYPE_NONE + // 1. Send SOCKET_SPDM_COMMAND_TEST with payload b'Client Hello!' + // 2. Receive SOCKET_SPDM_COMMAND_TEST with payload b'Server Hello!' + if config.transport_type == platform::socket_transport::SocketTransportType::None { + spdm_context.transport_init_sequence().map_err(|e| { + eprintln!("Handshake failed: {:?}", e); + Error::other("SPDM handshake failed") + })?; + } + + if config.verbose + && config.transport_type == platform::socket_transport::SocketTransportType::None + { + println!("Initial handshake completed successfully"); + } + + // Process SPDM messages using the context + let mut buffer = [0u8; 4096]; + let mut message_buffer = MessageBuf::new(&mut buffer); + // For now, we just want to show, that the VCA (Version, Capability, Auth) flow works as expected + // For that, we need to do the following: + // 1.1 Send GET_VERSION + // 1.2 Receive and verify VERSION + // 1.3 Update tracking of remote party + // 2.1 Send GET_CAPABILITIES + // 2.2 Receive and verify CAPABILITIES + // 2.3 Update tracking of remote party + // 3.1 Send GET_AUTH + + // 1.1 Send GET_VERSION + generate_get_version( + &mut spdm_context, + &mut message_buffer, + VersionReqPayload::new(1, 1), + ) + .map_err(|(_send_response, cmd_err)| SpdmError::Command(cmd_err)) + .unwrap(); + + if config.verbose { + println!("GET_VERSION: {:?}", &message_buffer.message_data()); + } + + spdm_context + .requester_send_request(&mut message_buffer, EID) + .unwrap(); + + // 1.2 Receive and verify VERSION + // 1.3 is done by the requester_process_message call + spdm_context + .requester_process_message(&mut message_buffer) + .unwrap(); + + if config.verbose { + println!("Sent GET_VERSION: {:?}", &message_buffer.message_data()); + } + + // 2.1 Send GET_CAPABILITIES + message_buffer.reset(); + generate_capabilities_request_local(&mut spdm_context, &mut message_buffer).unwrap(); + + if config.verbose { + println!("GET_CAPABILITIES: {:?}", &message_buffer.message_data()); + } + + spdm_context + .requester_send_request(&mut message_buffer, EID) + .unwrap(); + + if config.verbose { + println!( + "Sent GET_CAPABILITIES: {:?}", + &message_buffer.message_data() + ); + } + + // 2.2 Receive and verify CAPABILITIES + // 2.3 is done by the requester_process_message call + spdm_context + .requester_process_message(&mut message_buffer) + .unwrap(); + + if config.verbose { + println!( + "Processed CAPABILITIES: {:?}", + &message_buffer.message_data() + ); + } + + let ext_asym = [ExtendedAlgo::new(RegistryId::DMTF, 1)]; + let ext_hash = [ExtendedAlgo::new(RegistryId::DMTF, 1)]; + let alg_external = [ExtendedAlgo::new(RegistryId::DMTF, 1)]; + // TODO: since we re-generate them there is the potential issue of TOCTOU. + let mut local_algorithms = create_local_algorithms(); + local_algorithms + .device_algorithms + .base_asym_algo + .set_tpm_alg_rsapss_2048(1); + local_algorithms + .device_algorithms + .base_hash_algo + .set_tpm_alg_sha_256(1); + + let mut alg_structure = AlgStructure::new(&AlgType::Dhe, &local_algorithms); + alg_structure.set_ext_alg_count(1); + + // 3.1 Send GET_ALGORITHMS + message_buffer.reset(); + generate_negotiate_algorithms_request( + &mut spdm_context, + &mut message_buffer, + Some(&ext_asym), + Some(&ext_hash), + Some(alg_structure), + Some(&alg_external), + ) + .unwrap(); + + spdm_context + .requester_send_request(&mut message_buffer, EID) + .unwrap(); + + if config.verbose { + println!( + "NEGOTIATE_ALGORITHMS: {:x?}", + &message_buffer.message_data() + ); + } + + spdm_context + .requester_process_message(&mut message_buffer) + .unwrap(); + + if config.verbose { + println!("ALGORITHMS: {:x?}", &message_buffer.message_data()); + } + + println!("SPDM VCA flow completed successfully"); + + message_buffer.reset(); + generate_digest_request(&mut spdm_context, &mut message_buffer).unwrap(); + spdm_context + .requester_send_request(&mut message_buffer, EID) + .unwrap(); + + if config.verbose { + println!("GET_DIGESTS: {:x?}", &message_buffer.message_data()); + } + + spdm_context + .requester_process_message(&mut message_buffer) + .unwrap(); + + if config.verbose { + println!("DIGESTS: {:x?}", &message_buffer.message_data()); + } + println!("Successfully retrieved cert chain digests"); + + // Get peer certificate chain + loop { + message_buffer.reset(); + generate_get_certificate(&mut spdm_context, &mut message_buffer, 0, 0, 0x200, false) + .unwrap(); + spdm_context + .requester_send_request(&mut message_buffer, EID) + .unwrap(); + if config.verbose { + println!("requested GET_CERTIFICATE"); + println!("state: {:?}", spdm_context.connection_info().state()); + } + + spdm_context + .requester_process_message(&mut message_buffer) + .unwrap(); + if config.verbose { + println!("CERTIFICATE: Ok ({} bytes)", &message_buffer.msg_len(),); + } + if !matches!( + spdm_context.connection_info().state(), + spdm_lib::state::ConnectionState::DuringCertificate(_) + ) { + break; + } + } + println!("sucessfully retrieved peer cert chain"); + let mut peer_leaf_cert = None; + if let Some(store) = spdm_context.peer_cert_store() { + let hash_algo: BaseHashAlgoType = spdm_context + .connection_info() + .peer_algorithms() + .base_hash_algo + .try_into() + .unwrap(); + let root_hash = store.get_root_hash(0, hash_algo).unwrap(); + println!( + "slot 0: Root hash ({hash_algo:?}, {} bytes): {}", + root_hash.len(), + HexString(root_hash) + ); + let cert_chain = store.get_cert_chain(0, hash_algo).unwrap(); + + println!("slot 0: Parsing {} bytes cert chain:", cert_chain.len()); + let mut certs = Vec::new(); + let mut rest = cert_chain; + loop { + let (cert, r) = Certificate::from_der_partial(rest).unwrap(); + rest = r; + println!("Cert with subject {}", cert.tbs_certificate().subject()); + println!(" signature alg. id: {}", cert.signature_algorithm().oid); + certs.push(cert); + if rest.is_empty() { + break; + } + } + + if !certs.is_empty() { + let ca_cert = Certificate::from_der(platform::STATIC_ROOT_CA_CERT).unwrap(); + let ca_cert_sig = ca_cert.signature().as_bytes().unwrap(); + assert_eq!(certs[0].signature().as_bytes().unwrap(), ca_cert_sig); + println!("CA cert signature matches expected CA signature"); + assert!(verify_cert_chain(&certs)); + println!("Cert chain signatures successfully verified!"); + } + peer_leaf_cert = certs.last().cloned(); + } + + let mut nonce = [0u8; NONCE_LEN]; + spdm_context.get_random_bytes(&mut nonce).unwrap(); + + if config.verbose { + println!("CHALLENGE: Nonce = {:x?}", nonce); + } + + // GET_CHALLENGE + message_buffer.reset(); + generate_challenge_request( + &mut spdm_context, + &mut message_buffer, + 0, + MeasurementSummaryHashType::All, + nonce, + None, + ) + .unwrap(); + + spdm_context + .requester_send_request(&mut message_buffer, EID) + .unwrap(); + + if config.verbose { + println!("CHALLENGE: {:?}", &message_buffer.message_data()); + } + + // CHALLENGE_AUTH + spdm_context + .requester_process_message(&mut message_buffer) + .unwrap(); + + if config.verbose { + println!("CHALLENGE_AUTH: {:x?}", &message_buffer.message_data()); + } + + if let Some(cert) = &peer_leaf_cert { + let pub_key = VerifyingKey::from_sec1_bytes( + cert.tbs_certificate() + .subject_public_key_info() + .subject_public_key + .as_bytes() + .unwrap(), + ) + .unwrap(); + + // get all the remaining bytes from the message buffer as the signature + let sig_raw = message_buffer.data(96).unwrap(); + let sig = Signature::from_slice(sig_raw).unwrap(); + if config.verbose { + println!("signature: {sig}"); + } + + if !verify_challenge_auth_signature(&mut spdm_context, pub_key, sig, config) { + eprintln!("CHALLENGE_AUTH signature verification failed"); + return Err(std::io::Error::other( + "CHALLENGE_AUTH signature verification failed", + )); + } + spdm_context.set_authenticated(); + println!("CHALLENGE_AUTH signature verification successfull"); + } + + // GET_MEASUREMENTS + message_buffer.reset(); + generate_get_measurements( + &mut spdm_context, + &mut message_buffer, + false, + false, + MeasurementOperation::RequestAllMeasBlocks, + Some(0), + None, + ) + .unwrap(); + spdm_context + .requester_send_request(&mut message_buffer, EID) + .unwrap(); + + if config.verbose { + println!("GET_MEASUREMENTS: {:x?}", &message_buffer.message_data()); + } + + spdm_context + .requester_process_message(&mut message_buffer) + .unwrap(); + + if config.verbose { + println!("MEASUREMENTS: {:x?}", &message_buffer.message_data()); + } + + let measurements = parse_measurements_response(message_buffer.message_data().unwrap()) + .expect("Failed to parse measurement response"); + + if config.verbose { + println!( + "Measurements block count: {}", + measurements.total_measurement_blocks() + ); + println!("Measurements Nonce: {}", HexString(measurements.nonce)); + if let Some(sig) = measurements.signature { + println!( + "Measurements Signature ({} bytes): {}", + sig.len(), + HexString(sig) + ); + } + println!( + "Measurement content change status: {:?}", + measurements.content_changed() + ); + } + + if let Some(sig_raw) = measurements.signature { + if let Some(cert) = &peer_leaf_cert { + let pub_key = VerifyingKey::from_sec1_bytes( + cert.tbs_certificate() + .subject_public_key_info() + .subject_public_key + .as_bytes() + .unwrap(), + ) + .unwrap(); + + let sig = Signature::from_slice(sig_raw).unwrap(); + if !verify_measurements_signature(&mut spdm_context, pub_key, sig, config) { + eprintln!("MEASUREMENTS signature verification failed"); + return Err(std::io::Error::other( + "MEASUREMENTS signature verification failed", + )); + } + println!("L1/L2 log verification successfull"); + } + } + + let mut meas_count = 0; + for measurement in measurements.iter() { + meas_count += 1; + if config.verbose { + println!( + "Parsed {:?} measurement with index {}", + measurement.measurement_spec, measurement.index + ); + } + } + + if meas_count != measurements.total_measurement_blocks() { + println!("[WARNING] measurement block count and parsed measurment count mismatch ({} expected, {} parsed)", measurements.total_measurement_blocks(), meas_count); + } else { + println!("Measurements retrieved successfully") + } + + Ok(()) +} + +/// Display configuration information +fn display_info(config: &RequesterConfig) { + println!("SPDM Library DMTF Compatible Requester Example Flow"); + println!("==================================================="); + println!("Configuration:"); + println!(" Port: {}", config.port); + println!(" Certificate: {}", config.cert_path); + println!(" Private Key: {}", config.key_path); + if let Some(ref measurements) = config.measurements_path { + println!(" Measurements: {}", measurements); + } + println!(" Verbose: {}", config.verbose); + println!(); + + let capabilities = create_device_capabilities(); + println!("SPDM Device Capabilities:"); + println!( + " Certificate capability: {}", + capabilities.flags.cert_cap() + ); + println!(" Challenge capability: {}", capabilities.flags.chal_cap()); + println!( + " Measurements capability: {}", + capabilities.flags.meas_cap() + ); + println!( + " Fresh measurements: {}", + capabilities.flags.meas_fresh_cap() + ); + println!(" Chunk capability: {}", capabilities.flags.chunk_cap()); + println!( + " Data transfer size: {} bytes", + capabilities.data_transfer_size + ); + println!( + " Max SPDM message size: {} bytes", + capabilities.max_spdm_msg_size + ); + println!(); + + println!("Requester Features:"); + println!(" SPDM Versions: 1.0, 1.1, 1.2, 1.3"); + println!(" Hash Algorithm: SHA-384 (platform module)"); + println!(" Signature Algorithm: ECDSA P-384 (platform module)"); + println!(" Transport: TCP socket with DMTF NONE or MCTP protocol (platform module)"); + println!(); +} + +/// Main function +fn main() -> Result<(), Box> { + let config = RequesterConfig::parse(); + display_info(&config); + + let remote_addr = format!("0.0.0.0:{}", config.port); + let stream = TcpStream::connect(&remote_addr)?; + + println!( + "Clean SPDM library requester connecting to {}", + &remote_addr + ); + + if let Ok(peer_addr) = stream.peer_addr() { + println!("Connection from: {}", peer_addr); + } + + println!("Starting requester command flow..."); + full_flow(stream, &config)?; + + println!("Request flow finished successfully."); + + Ok(()) +} + +/// Verifies the provided certificate chain +/// +/// Assumes that the fist certificate in the chain is +/// an already verified trusted certificate (e.g. the root CA cert). +/// Only checks the validity of the signatures (does not check CRL, validity period, ...). +fn verify_cert_chain(chain: &[Certificate]) -> bool { + use p384::ecdsa::{Signature, VerifyingKey}; + use signature::Verifier; + let mut pub_key = VerifyingKey::from_sec1_bytes( + chain + .first() + .unwrap() + .tbs_certificate() + .subject_public_key_info() + .subject_public_key + .as_bytes() + .unwrap(), + ) + .unwrap(); + for cert in chain.iter() { + let sig = Signature::from_der(cert.signature().as_bytes().unwrap()).unwrap(); + if pub_key + .verify(&cert.tbs_certificate().to_der().unwrap(), &sig) + .is_err() + { + return false; + } + + println!("Verified {}", cert.tbs_certificate().subject()); + pub_key = VerifyingKey::from_sec1_bytes( + cert.tbs_certificate() + .subject_public_key_info() + .subject_public_key + .as_bytes() + .unwrap(), + ) + .unwrap(); + } + true +} + +/// Currently only p384 support required +/// Here we verify that the responder and we created the same m2 transcript and +/// that the signature is correct. +/// +/// The transcript hash will be retrieved from the context. +/// The signature will be verified using the public key from the responder's certificate chain (which we already verified). +fn verify_challenge_auth_signature( + ctx: &mut SpdmContext, + pubkey: VerifyingKey, + signature: Signature, + config: &RequesterConfig, +) -> bool { + use p384::ecdsa::signature::hazmat::PrehashVerifier; + use signature::Verifier; + + let mut sig_combined_context = Vec::new(); + if ctx.connection_info().version_number() >= SpdmVersion::V12 { + // since we verify the responder-generated signature, we have to use the same "responder-" context constant. + let sig_ctx = protocol::signature::create_responder_signing_context( + ctx.connection_info().version_number(), + protocol::ReqRespCode::ChallengeAuth, + ) + .unwrap(); + sig_combined_context.extend_from_slice(&sig_ctx); + if config.verbose { + println!( + "comb_ctx string: '{}'", + String::from_utf8_lossy(&sig_combined_context) + ); + } + } + + // Get the M1 transcript hash (which is the hash of messages A, B, C) and verify the signature over it. + let mut transcript_hash = [0u8; 48]; + ctx.transcript_hash(TranscriptContext::M1, &mut transcript_hash) + .unwrap(); + if config.verbose { + println!("M1/2 hash: {}", HexString(&transcript_hash)); + } + + // M denotes the message that is signed. M shall be the concatenation of the combined_spdm_prefix and unverified_message_hash. + let m = [sig_combined_context.as_slice(), &transcript_hash].concat(); + + if ctx.connection_info().version_number() >= SpdmVersion::V12 { + pubkey.verify(&m, &signature).is_ok() + } else { + pubkey.verify_prehash(&m, &signature).is_ok() + } +} + +/// Currently only p384 support required +/// Here we verify that the responder and we created the same L1/L2 transcript and +/// that the signature is correct. +/// +/// The transcript hash will be retrieved from the context. +/// The signature will be verified using the public key from the responder's certificate chain (which we already verified). +fn verify_measurements_signature( + ctx: &mut SpdmContext, + pubkey: VerifyingKey, + signature: Signature, + config: &RequesterConfig, +) -> bool { + use p384::ecdsa::signature::hazmat::PrehashVerifier; + use signature::Verifier; + + let mut sig_combined_context = Vec::new(); + if ctx.connection_info().version_number() >= SpdmVersion::V12 { + // since we verify the responder-generated signature, we have to use the same "responder-" context constant. + let sig_ctx = protocol::signature::create_responder_signing_context( + ctx.connection_info().version_number(), + protocol::ReqRespCode::Measurements, + ) + .unwrap(); + sig_combined_context.extend_from_slice(&sig_ctx); + if config.verbose { + println!( + "comb_ctx string: '{}'", + String::from_utf8_lossy(&sig_combined_context) + ); + } + } + + // Get the L1 transcript hash and verify the signature over it. + let mut transcript_hash = [0u8; 48]; + ctx.transcript_hash(TranscriptContext::L1, &mut transcript_hash) + .unwrap(); + if config.verbose { + println!("L1/2 hash: {}", HexString(&transcript_hash)); + } + + // M denotes the message that is signed. M shall be the concatenation of the combined_spdm_prefix and unverified_message_hash. + let m = [sig_combined_context.as_slice(), &transcript_hash].concat(); + + if ctx.connection_info().version_number() >= SpdmVersion::V12 { + pubkey.verify(&m, &signature).is_ok() + } else { + pubkey.verify_prehash(&m, &signature).is_ok() + } +} + +#[derive(Debug)] +struct HexString<'a>(&'a [u8]); + +impl Display for HexString<'_> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + for x in self.0 { + write!(f, "{:02X}", x)?; + } + Ok(()) + } +} diff --git a/examples/spdm_responder.rs b/examples/spdm_responder.rs index ceeb658..697e13f 100644 --- a/examples/spdm_responder.rs +++ b/examples/spdm_responder.rs @@ -1,31 +1,42 @@ -// Licensed under the Apache-2.0 license +// Copyright 2025 +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. //! Real SPDM Library Integrated DMTF Compatible Responder -//! +//! //! This responder integrates the actual SPDM library for real protocol processing //! while maintaining compatibility with the DMTF SPDM emulator protocol. //! //! This version uses platform implementations with no duplicated code. use std::env; -use std::process; +use std::io::{Error, ErrorKind, Result as IoResult}; use std::net::{TcpListener, TcpStream}; -use std::io::{Result as IoResult, Error, ErrorKind}; +use std::process; -use spdm_lib::context::SpdmContext; use spdm_lib::codec::MessageBuf; -use spdm_lib::protocol::{DeviceCapabilities, CapabilityFlags}; -use spdm_lib::protocol::version::SpdmVersion; +use spdm_lib::context::SpdmContext; use spdm_lib::protocol::algorithms::{ - LocalDeviceAlgorithms, AlgorithmPriorityTable, DeviceAlgorithms, - MeasurementSpecification, MeasurementHashAlgo, BaseAsymAlgo, BaseHashAlgo, - DheNamedGroup, AeadCipherSuite, KeySchedule, OtherParamSupport, MelSpecification, - ReqBaseAsymAlg + AeadCipherSuite, AlgorithmPriorityTable, BaseAsymAlgo, BaseHashAlgo, DeviceAlgorithms, + DheNamedGroup, KeySchedule, LocalDeviceAlgorithms, MeasurementHashAlgo, + MeasurementSpecification, MelSpecification, OtherParamSupport, ReqBaseAsymAlg, }; +use spdm_lib::protocol::version::SpdmVersion; +use spdm_lib::protocol::{CapabilityFlags, DeviceCapabilities}; // Import platform implementations - no duplicates! mod platform; -use platform::{SpdmSocketTransport, Sha384Hash, SystemRng, DemoCertStore, DemoEvidence}; +use platform::{DemoCertStore, DemoEvidence, Sha384Hash, SpdmSocketTransport, SystemRng}; /// Responder configuration #[derive(Debug, Clone)] @@ -54,19 +65,20 @@ impl Default for ResponderConfig { /// Create SPDM device capabilities fn create_device_capabilities() -> DeviceCapabilities { let mut flags_value = 0u32; - flags_value |= 1 << 1; // cert_cap - flags_value |= 1 << 2; // chal_cap - flags_value |= 2 << 3; // meas_cap (with signature) - flags_value |= 1 << 5; // meas_fresh_cap + flags_value |= 1 << 1; // cert_cap + flags_value |= 1 << 2; // chal_cap + flags_value |= 2 << 3; // meas_cap (with signature) + flags_value |= 1 << 5; // meas_fresh_cap flags_value |= 1 << 17; // chunk_cap - + let flags = CapabilityFlags::new(flags_value); - + DeviceCapabilities { ct_exponent: 0, flags, data_transfer_size: 1024, max_spdm_msg_size: 4096, + include_supported_algorithms: true, } } @@ -75,16 +87,16 @@ fn create_local_algorithms<'a>() -> LocalDeviceAlgorithms<'a> { // Configure supported algorithms with proper bitfield construction let mut measurement_spec = MeasurementSpecification::default(); measurement_spec.set_dmtf_measurement_spec(1); - + let mut measurement_hash_algo = MeasurementHashAlgo::default(); measurement_hash_algo.set_tpm_alg_sha_384(1); - + let mut base_asym_algo = BaseAsymAlgo::default(); base_asym_algo.set_tpm_alg_ecdsa_ecc_nist_p384(1); - + let mut base_hash_algo = BaseHashAlgo::default(); base_hash_algo.set_tpm_alg_sha_384(1); - + let device_algorithms = DeviceAlgorithms { measurement_spec, other_param_support: OtherParamSupport::default(), @@ -118,8 +130,11 @@ fn create_local_algorithms<'a>() -> LocalDeviceAlgorithms<'a> { /// Handle client connection with real SPDM processing fn handle_spdm_client(stream: TcpStream, config: &ResponderConfig) -> IoResult<()> { - let mut transport = SpdmSocketTransport::new(stream, config.raw, config.verbose); - + let mut transport = SpdmSocketTransport::new( + stream, + platform::socket_transport::SocketTransportType::None, + ); + // Create platform implementations - all from platform module! let mut hash = Sha384Hash::new(); let mut m1_hash = Sha384Hash::new(); @@ -127,25 +142,26 @@ fn handle_spdm_client(stream: TcpStream, config: &ResponderConfig) -> IoResult<( let mut rng = SystemRng::new(); let mut cert_store = DemoCertStore::new(); let evidence = DemoEvidence::new(); - + // Create SPDM context let supported_versions = [SpdmVersion::V12, SpdmVersion::V11]; let capabilities = create_device_capabilities(); let algorithms = create_local_algorithms(); - + if config.verbose { println!("Client connected - initializing SPDM context"); } - + let mut spdm_context = match SpdmContext::new( &supported_versions, &mut transport, capabilities, algorithms, &mut cert_store, + None, &mut hash, &mut m1_hash, - &mut l1_hash, + &mut l1_hash, &mut rng, &evidence, ) { @@ -155,16 +171,16 @@ fn handle_spdm_client(stream: TcpStream, config: &ResponderConfig) -> IoResult<( return Err(Error::new(ErrorKind::Other, "SPDM context creation failed")); } }; - + if config.verbose { println!("SPDM context created successfully"); } - + // Process SPDM messages using the context let mut buffer = [0u8; 4096]; let mut message_buffer = MessageBuf::new(&mut buffer); loop { - match spdm_context.process_message(&mut message_buffer) { + match spdm_context.responder_process_message(&mut message_buffer) { Ok(()) => { if config.verbose { println!("Successfully processed SPDM message"); @@ -190,11 +206,11 @@ fn handle_spdm_client(stream: TcpStream, config: &ResponderConfig) -> IoResult<( } } } - + if config.verbose { println!("Client connection closed"); } - + Ok(()) } @@ -202,7 +218,7 @@ fn handle_spdm_client(stream: TcpStream, config: &ResponderConfig) -> IoResult<( fn parse_args() -> ResponderConfig { let mut config = ResponderConfig::default(); let args: Vec = env::args().collect(); - + let mut i = 1; while i < args.len() { match args[i].as_str() { @@ -217,7 +233,7 @@ fn parse_args() -> ResponderConfig { eprintln!("Port number required after {}", args[i]); process::exit(1); } - }, + } "-c" | "--cert" => { if i + 1 < args.len() { config.cert_path = args[i + 1].clone(); @@ -226,7 +242,7 @@ fn parse_args() -> ResponderConfig { eprintln!("Certificate file path required after {}", args[i]); process::exit(1); } - }, + } "-k" | "--key" => { if i + 1 < args.len() { config.key_path = args[i + 1].clone(); @@ -235,7 +251,7 @@ fn parse_args() -> ResponderConfig { eprintln!("Private key file path required after {}", args[i]); process::exit(1); } - }, + } "-m" | "--measurements" => { if i + 1 < args.len() { config.measurements_path = Some(args[i + 1].clone()); @@ -244,19 +260,15 @@ fn parse_args() -> ResponderConfig { eprintln!("Measurements file path required after {}", args[i]); process::exit(1); } - }, + } "-v" | "--verbose" => { config.verbose = true; i += 1; - }, - "--raw" => { - config.raw = true; - i += 1; - }, + } "-h" | "--help" => { print_help(); process::exit(0); - }, + } _ => { eprintln!("Unknown argument: {}", args[i]); print_help(); @@ -264,7 +276,7 @@ fn parse_args() -> ResponderConfig { } } } - + config } @@ -274,15 +286,23 @@ fn print_help() { println!(" spdm-responder-clean [OPTIONS]\n"); println!("OPTIONS:"); println!(" -p, --port TCP port to listen on [default: 2323]"); - println!(" -c, --cert Path to certificate file [default: device_cert.pem]"); - println!(" -k, --key Path to private key file [default: device_key.pem]"); - println!(" -m, --measurements Path to measurements file [default: measurements.json]"); + println!( + " -c, --cert Path to certificate file [default: device_cert.pem]" + ); + println!( + " -k, --key Path to private key file [default: device_key.pem]" + ); + println!( + " -m, --measurements Path to measurements file [default: measurements.json]" + ); println!(" -v, --verbose Enable verbose logging"); println!(" -h, --help Print this help message\n"); println!("EXAMPLES:"); println!(" spdm-responder-clean --port 8080 --verbose"); println!(" spdm-responder-clean --cert my_cert.pem --key my_key.pem"); - println!("\nIntegrates real SPDM library with clean platform implementations - no code duplication!"); + println!( + "\nIntegrates real SPDM library with clean platform implementations - no code duplication!" + ); } /// Display configuration information @@ -299,18 +319,33 @@ fn display_info(config: &ResponderConfig) { println!(" Verbose: {}", config.verbose); println!(" Raw (no TCP binding): {}", config.raw); println!(); - + let capabilities = create_device_capabilities(); println!("SPDM Device Capabilities:"); - println!(" Certificate capability: {}", capabilities.flags.cert_cap()); + println!( + " Certificate capability: {}", + capabilities.flags.cert_cap() + ); println!(" Challenge capability: {}", capabilities.flags.chal_cap()); - println!(" Measurements capability: {}", capabilities.flags.meas_cap()); - println!(" Fresh measurements: {}", capabilities.flags.meas_fresh_cap()); + println!( + " Measurements capability: {}", + capabilities.flags.meas_cap() + ); + println!( + " Fresh measurements: {}", + capabilities.flags.meas_fresh_cap() + ); println!(" Chunk capability: {}", capabilities.flags.chunk_cap()); - println!(" Data transfer size: {} bytes", capabilities.data_transfer_size); - println!(" Max SPDM message size: {} bytes", capabilities.max_spdm_msg_size); + println!( + " Data transfer size: {} bytes", + capabilities.data_transfer_size + ); + println!( + " Max SPDM message size: {} bytes", + capabilities.max_spdm_msg_size + ); println!(); - + println!("Clean Platform Implementation Features:"); println!(" SPDM Versions: 1.2, 1.1"); println!(" Protocol Processing: Real SPDM library integration"); @@ -326,19 +361,19 @@ fn display_info(config: &ResponderConfig) { /// Main function fn main() -> Result<(), Box> { let config = parse_args(); - + display_info(&config); - + // Create TCP listener let bind_addr = format!("0.0.0.0:{}", config.port); let listener = TcpListener::bind(&bind_addr)?; - + println!("Clean SPDM library responder listening on {}", bind_addr); println!("Compatible with DMTF SPDM device validator and emulator clients"); println!("Uses unified platform implementations with no code duplication"); println!("Waiting for connections... (Press Ctrl+C to exit)"); println!(); - + // Accept connections for stream in listener.incoming() { match stream { @@ -346,7 +381,7 @@ fn main() -> Result<(), Box> { if let Ok(peer_addr) = stream.peer_addr() { println!("Connection from: {}", peer_addr); } - + // Handle client with real SPDM processing using platform implementations if let Err(e) = handle_spdm_client(stream, &config) { eprintln!("Client handling error: {}", e); @@ -357,6 +392,6 @@ fn main() -> Result<(), Box> { } } } - + Ok(()) -} \ No newline at end of file +} diff --git a/examples/test_static_certs.rs b/examples/test_static_certs.rs deleted file mode 100644 index f571bbd..0000000 --- a/examples/test_static_certs.rs +++ /dev/null @@ -1,46 +0,0 @@ -// Test program to verify static certificates work correctly - -// Import platform implementations -mod platform; -use platform::{STATIC_ROOT_CA_CERT, STATIC_ATTESTATION_CERT, STATIC_CERTIFICATE_CHAIN}; - -fn main() { - println!("Static Certificate Test"); - println!("======================="); - - println!("Root CA Certificate: {} bytes", STATIC_ROOT_CA_CERT.len()); - println!("Attestation Certificate: {} bytes", STATIC_ATTESTATION_CERT.len()); - println!("Certificate Chain: {} bytes", STATIC_CERTIFICATE_CHAIN.len()); - - // Verify the chain is the concatenation of the individual certs - let expected_len = STATIC_ROOT_CA_CERT.len() + STATIC_ATTESTATION_CERT.len(); - if STATIC_CERTIFICATE_CHAIN.len() == expected_len { - println!("✓ Certificate chain length matches individual certificates"); - } else { - println!("✗ Certificate chain length mismatch: expected {}, got {}", - expected_len, STATIC_CERTIFICATE_CHAIN.len()); - } - - // Verify the chain starts with the root CA - if STATIC_CERTIFICATE_CHAIN.starts_with(STATIC_ROOT_CA_CERT) { - println!("✓ Certificate chain starts with root CA"); - } else { - println!("✗ Certificate chain does not start with root CA"); - } - - // Verify the chain ends with the attestation cert - if STATIC_CERTIFICATE_CHAIN.ends_with(STATIC_ATTESTATION_CERT) { - println!("✓ Certificate chain ends with attestation certificate"); - } else { - println!("✗ Certificate chain does not end with attestation certificate"); - } - - // Check X.509 structure (should start with SEQUENCE tag 0x30) - if STATIC_ROOT_CA_CERT[0] == 0x30 && STATIC_ATTESTATION_CERT[0] == 0x30 { - println!("✓ Both certificates have proper X.509 DER format (SEQUENCE tag)"); - } else { - println!("✗ Certificates do not have proper X.509 DER format"); - } - - println!("\nStatic certificates are ready for use!"); -} \ No newline at end of file diff --git a/src/cert_store.rs b/src/cert_store.rs index 1471696..582b82c 100644 --- a/src/cert_store.rs +++ b/src/cert_store.rs @@ -1,9 +1,23 @@ -// Licensed under the Apache-2.0 license +// Copyright 2025 +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. use crate::error::{SpdmError, SpdmResult}; use crate::protocol::algorithms::{AsymAlgo, ECC_P384_SIGNATURE_SIZE, SHA384_HASH_SIZE}; use crate::protocol::certs::{CertificateInfo, KeyUsageMask}; -use crate::protocol::SpdmCertChainHeader; +use crate::protocol::{BaseHashAlgoType, SpdmCertChainHeader}; + +use crate::commands::challenge::MeasurementSummaryHashType; pub const MAX_CERT_SLOTS_SUPPORTED: u8 = 2; pub const SPDM_CERT_CHAIN_METADATA_LEN: u16 = @@ -12,13 +26,15 @@ pub const SPDM_CERT_CHAIN_METADATA_LEN: u16 = #[derive(Debug, PartialEq)] pub enum CertStoreError { InitFailed, - InvalidSlotId, + InvalidSlotId(u8), UnsupportedHashAlgo, BufferTooSmall, InvalidOffset, CertReadError, PlatformError, + Undefined, } + pub type CertStoreResult = Result; pub trait SpdmCertStore { @@ -154,3 +170,276 @@ pub(crate) fn cert_slot_mask(cert_store: &dyn SpdmCertStore) -> (u8, u8) { (supported_slot_mask, provisioned_slot_mask) } + +pub trait PeerCertStore { + /// Get supported certificate slot count. + /// The supported slots are consecutive from 0 to slot_count - 1. + /// If certificate slot X exists in the responding SPDM endpoint, then all + /// slots with ID < X must also exist. + /// + /// For example, if slot 2 is supported, then slots 0 and 1 must also be supported. + /// + /// # Returns + /// * `u8` - The number of supported certificate slots. + fn slot_count(&self) -> u8; + + /// Set the number of supported certificate slots. + /// This function is typically called during SPDM connection setup. + /// + /// # Arguments + /// * `slot_count` - The number of supported certificate slots. + /// + /// # Returns + /// * `CertStoreResult<()>` - Ok if the operation was successful, Err otherwise. + fn set_supported_slots(&mut self, supported_slot_mask: u8) -> CertStoreResult<()>; + + /// Get the bitmask of supported certificate slots. + /// + /// # Returns + /// * `Ok(u8)` - Bitmask where each set bit indicates a supported slot. + /// + /// # Errors + /// * `CertStoreError::PlatformError` - If there was a platform-specific error. + fn get_supported_slots(&self) -> CertStoreResult; + + /// Set the bitmask of provisioned certificate slots. + /// + /// # Arguments + /// * `provisioned_slot_mask` - Bitmask where each set bit indicates a provisioned slot. + /// + /// # Returns + /// * `Ok(())` - If the provisioned slot mask was stored successfully. + /// + /// # Errors + /// * `CertStoreError::PlatformError` - If there was a platform-specific error. + fn set_provisioned_slots(&mut self, provisioned_slot_mask: u8) -> CertStoreResult<()>; + + /// Get the bitmask of provisioned certificate slots. + /// + /// # Returns + /// * `Ok(u8)` - Bitmask where each set bit indicates a provisioned slot. + /// + /// # Errors + /// * `CertStoreError::PlatformError` - If there was a platform-specific error. + fn get_provisioned_slots(&self) -> CertStoreResult; + + /// Get the stored certificate chain for the given slot, + /// consisting of one or more ASN.1 DER-encoded X.509 v3 certificates. + /// + /// # Arguments + /// * `slot_id` - The slot ID of the certificate chain. + /// * `hash_algo` - The hash algorithm that was negotiated with the peer. + /// + /// # Returns + /// * `Ok(&[u8])` - The certificate chain bytes, not including the length and root hash header. + /// + /// # Errors + /// * `CertStoreError::InvalidSlotId` - If the slot ID is out of range. + /// * `CertStoreError::PlatformError` - If there was a platform-specific error. + fn get_cert_chain(&self, slot_id: u8, hash_algo: BaseHashAlgoType) -> CertStoreResult<&[u8]>; + + /// Store a certificate chain in the given slot. + /// + /// # Arguments + /// * `slot_id` - The slot ID to store the certificate chain in. + /// * `cert_chain` - The certificate chain bytes to store. + /// + /// # Returns + /// * `Ok(())` - If the certificate chain was stored successfully. + /// + /// # Errors + /// * `CertStoreError::InvalidSlotId` - If the slot ID is out of range. + /// * `CertStoreError::BufferTooSmall` - If the internal buffer is too small for the certificate chain. + /// * `CertStoreError::PlatformError` - If there was a platform-specific error. + fn set_cert_chain(&mut self, slot_id: u8, cert_chain: &[u8]) -> CertStoreResult<()>; + + /// Get the digest of the certificate chain for the given slot. + /// + /// # Arguments + /// * `slot_id` - The slot ID of the certificate chain. + /// + /// # Returns + /// * `Ok(&[u8])` - The digest bytes. + /// + /// # Errors + /// * `CertStoreError::InvalidSlotId` - If the slot ID is out of range. + /// * `CertStoreError::PlatformError` - If there was a platform-specific error. + fn get_digest(&self, slot_id: u8) -> CertStoreResult<&[u8]>; + + /// Store the digest of the certificate chain for the given slot. + /// + /// # Arguments + /// * `slot_id` - The slot ID of the certificate chain. + /// * `digest` - The digest bytes to store. + /// + /// # Returns + /// * `Ok(())` - If the digest was stored successfully. + /// + /// # Errors + /// * `CertStoreError::InvalidSlotId` - If the slot ID is out of range. + /// * `CertStoreError::BufferTooSmall` - If the internal buffer is too small for the digest. + /// * `CertStoreError::PlatformError` - If there was a platform-specific error. + fn set_digest(&mut self, slot_id: u8, digest: &[u8]) -> CertStoreResult<()>; + + /// Get the KeyPairID associated with the certificate chain for the given slot. + /// + /// # Arguments + /// * `slot_id` - The slot ID of the certificate chain. + /// + /// # Returns + /// * `Ok(u8)` - The KeyPairID. + /// + /// # Errors + /// * `CertStoreError::InvalidSlotId` - If the slot ID is out of range. + /// * `CertStoreError::PlatformError` - If there was a platform-specific error. + fn get_keypair(&self, slot_id: u8) -> CertStoreResult; + + /// Set the KeyPairID associated with the certificate chain for the given slot. + /// + /// # Arguments + /// * `slot_id` - The slot ID of the certificate chain. + /// * `keypair` - The KeyPairID to associate with the slot. + /// + /// # Returns + /// * `Ok(())` - If the KeyPairID was stored successfully. + /// + /// # Errors + /// * `CertStoreError::InvalidSlotId` - If the slot ID is out of range. + /// * `CertStoreError::PlatformError` - If there was a platform-specific error. + fn set_keypair(&mut self, slot_id: u8, keypair: u8) -> CertStoreResult<()>; + + /// Get the `CertificateInfo` for the given slot. + /// The `CertificateInfo` specifies the certificate model (such as DeviceID, Alias, or General). + /// + /// # Arguments + /// * `slot_id` - The slot ID of the certificate chain. + /// + /// # Returns + /// * `Ok(CertificateInfo)` - The certificate info for the slot. + /// + /// # Errors + /// * `CertStoreError::InvalidSlotId` - If the slot ID is out of range. + /// * `CertStoreError::PlatformError` - If there was a platform-specific error. + fn get_cert_info(&self, slot_id: u8) -> CertStoreResult; + + /// Set the `CertificateInfo` for the given slot. + /// + /// # Arguments + /// * `slot_id` - The slot ID of the certificate chain. + /// * `cert_info` - The `CertificateInfo` to store. + /// + /// # Returns + /// * `Ok(())` - If the `CertificateInfo` was stored successfully. + /// + /// # Errors + /// * `CertStoreError::InvalidSlotId` - If the slot ID is out of range. + /// * `CertStoreError::PlatformError` - If there was a platform-specific error. + fn set_cert_info(&mut self, slot_id: u8, cert_info: CertificateInfo) -> CertStoreResult<()>; + + /// Get the `KeyUsageMask` associated with the certificate chain for the given slot. + /// The `KeyUsageMask` indicates the permitted key usages for the certificate's public key. + /// + /// # Arguments + /// * `slot_id` - The slot ID of the certificate chain. + /// + /// # Returns + /// * `Ok(KeyUsageMask)` - The key usage mask for the slot. + /// + /// # Errors + /// * `CertStoreError::InvalidSlotId` - If the slot ID is out of range. + /// * `CertStoreError::PlatformError` - If there was a platform-specific error. + fn get_key_usage_mask(&self, slot_id: u8) -> CertStoreResult; + + /// Set the `KeyUsageMask` associated with the certificate chain for the given slot. + /// + /// # Arguments + /// * `slot_id` - The slot ID of the certificate chain. + /// * `key_usage_mask` - The `KeyUsageMask` to store. + /// + /// # Returns + /// * `Ok(())` - If the `KeyUsageMask` was stored successfully. + /// + /// # Errors + /// * `CertStoreError::InvalidSlotId` - If the slot ID is out of range. + /// * `CertStoreError::PlatformError` - If there was a platform-specific error. + fn set_key_usage_mask( + &mut self, + slot_id: u8, + key_usage_mask: KeyUsageMask, + ) -> CertStoreResult<()>; + + /// Add a portion of a certificate chain to the given slot + /// + /// # Returns + /// - `Ok(ReassemblyStatus)` when the portion was added successfully + /// - `Err(ReassemblyError)` when the portion could not be added + fn assemble(&mut self, slot_id: u8, portion: &[u8]) + -> Result; + + /// Reset a slot + /// + /// Removes all certificate data from the given slot. + fn reset(&mut self, slot_id: u8); + + /// Get the root hash of a peer certificate + /// + /// # Arguments + /// * `slot_id` - The Slot ID of the certificate chain + /// * `hash_algo` - The hash algorithm that was negotiated with the peer. + /// + /// # Returns + /// * The digest of the Root Certificate if available + fn get_root_hash(&self, slot_id: u8, hash_algo: BaseHashAlgoType) -> CertStoreResult<&[u8]>; + + /// Get a complete certificate chain consisting of one or more ASN.1 DER-encoded X.509 v3 certificates. + fn get_raw_chain(&self, slot_id: u8) -> CertStoreResult<&[u8]>; + + /// In protocol message `CHALLENGE`, the requester can specify the `MeasurementSummaryHashType` + /// to indicate the type of measurement summary hash it wants the responder + /// to include in the `CHALLENGE_AUTH` response. + /// This is done by encoding the `MeasurementSummaryHashType` value in the `Param2` + /// field of the `CHALLENGE` request. + /// + /// The responder can then retrieve this value from the request and use it to determine + /// which type of measurement summary hash to include in its response. + /// + /// The requester can then retrieve it to parse the measurement summary hash + /// in the response correctly. + /// + /// # Arguments + /// * `slot_id` - The slot ID of the certificate chain. + /// + /// # Returns + /// * `Ok(MeasurementSummaryHashType)` - The `MeasurementSummaryHashType` value requested + /// by the peer in the `CHALLENGE` request, or `None` if not set. + /// - Ok(None) if the requester did not specify a `MeasurementSummaryHashType`, + /// + /// # Errors + /// * `CertStoreError::InvalidSlotId` - If the slot ID is out of range. + /// * `CertStoreError::PlatformError` - If there was a platform-specific error. + /// * `CertStoreError::Undefined` - If the `MeasurementSummaryHashType` has not been set by a previous `CHALLENGE` request. + fn get_requested_msh_type(&self, slot_id: u8) -> CertStoreResult; + + /// Set the `MeasurementSummaryHashType` requested by the peer in the `CHALLENGE` request. + /// + /// # Arguments + /// * `msh_type` - The `MeasurementSummaryHashType` value to set. + /// + /// # Returns + /// * `Ok(())` - If the value was stored successfully. + /// * `Err(CertStoreError)` - If there was an error storing the value. + fn set_requested_msh_type( + &mut self, + slot_id: u8, + msh_type: MeasurementSummaryHashType, + ) -> CertStoreResult<()>; +} + +pub enum ReassemblyStatus { + /// Slot is empty + NotStarted, + /// Reassembly still in progress + InProgress, + /// Reassembly finished + Done, +} diff --git a/src/chunk_ctx.rs b/src/chunk_ctx.rs index 9c083d4..fcfa2d2 100644 --- a/src/chunk_ctx.rs +++ b/src/chunk_ctx.rs @@ -1,6 +1,18 @@ -// Licensed under the Apache-2.0 license - -use crate::commands::measurements_rsp::MeasurementsResponse; +// Copyright 2025 +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use crate::commands::measurements::response::MeasurementsResponse; #[derive(Debug, PartialEq)] pub enum ChunkError { diff --git a/src/codec.rs b/src/codec.rs index 3c7981e..daeb9a0 100644 --- a/src/codec.rs +++ b/src/codec.rs @@ -1,4 +1,16 @@ -// Licensed under the Apache-2.0 license +// Copyright 2025 +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. use zerocopy::{FromBytes, Immutable, IntoBytes}; diff --git a/src/commands/algorithms/mod.rs b/src/commands/algorithms/mod.rs new file mode 100644 index 0000000..1a3e601 --- /dev/null +++ b/src/commands/algorithms/mod.rs @@ -0,0 +1,520 @@ +// Copyright 2025 +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! Commands related to SPDM Algorithms negotiation +//! See DMTF 0274 - SPDM Base Specification v1.3, Section 10.4 ff. +//! +//! The Algorithms negotiation is performed after the Capabilities exchange +//! and before any other commands that depend on the negotiated algorithms. +//! +//! This module contains the request (`NEGOTIATE_ALGORITHMS`) and response +//! (`ALGORITHMS`) handling and generation logic. + +pub mod request; +pub mod response; + +pub(crate) use request::*; +pub(crate) use response::*; + +use crate::codec::CommonCodec; +use crate::protocol::LocalDeviceAlgorithms; +use crate::protocol::{SpdmMsgHdr, SpdmVersion}; +use bitfield::bitfield; +use core::mem::size_of; + +use crate::protocol::algorithms::{ + BaseAsymAlgo, BaseHashAlgo, MeasurementHashAlgo, MeasurementSpecification, MelSpecification, + OtherParamSupport, +}; + +use zerocopy::{FromBytes, Immutable, IntoBytes}; + +use crate::error::{SpdmError, SpdmResult}; + +// Max request length shall be 128 bytes (SPDM1.3 Table 10.4) +const MAX_SPDM_REQUEST_LENGTH: u16 = 128; +const MAX_SPDM_EXT_ALG_COUNT_V10: u8 = 8; +const MAX_SPDM_EXT_ALG_COUNT_V11: u8 = 20; +const MAX_SPDM_EXT_ALG_COUNT_V13: u8 = 20; + +#[derive(IntoBytes, FromBytes, Immutable, Default, Debug)] +#[repr(C, packed)] +/// This request message shall negotiate cryptographic algorithms. A Requester shall not issue a NEGOTIATE_ALGORITHMS +/// request message until it receives a successful CAPABILITIES response message. +/// +/// A Requester shall not issue any other SPDM requests, with the exception of GET_VERSION until it receives a successful +/// ALGORITHMS response message. +/// +/// This structure represents the NEGOTIATE_ALGORITHMS request message, **WITHOUT** the variable-length +/// algorithm structure tables and extended algorithm structures that follow this header, +/// namely +/// - ExtAsym (4 * A), see [ExtendedAlgo]. +/// - ExtHash (4 * E), see [ExtendedAlgo]. +/// - ReqAlgStruct (AlgStructSize), see [AlgStructure]. +struct NegotiateAlgorithmsReq { + /// The number of algorithm structure tables in this request using `ReqAlgStruct`. + num_alg_struct_tables: u8, // param 1 + + /// Reserved. + param2: u8, + + /// The length of the entire request message, in bytes. Length shall be less + /// than or equal to 128 bytes. + length: u16, + + /// For each defined measurement specification a Requester supports, the + /// Requester can set the appropriate bits. + /// + /// See [MeasurementSpecification] for details. + measurement_specification: MeasurementSpecification, + + /// Bit mask listing other parameters supported by the Requester. + /// Introduced in v1.2. + /// + /// See [OtherParamSupport] for details. + other_param_support: OtherParamSupport, + + /// Bit mask listing Requester-supported SPDM-enumerated asymmetric key signature + /// algorithms for the purpose of signature verification. If the Requester does + /// not support any request/ response pair that requires signature verification, + /// this value shall be set to zero. If the Requester will not send any requests + /// that require a signature, this value should be set to zero. + /// Let SigLen be the size of the signature in bytes. + /// + /// See [BaseAsymAlgo] for details. + base_asym_algo: BaseAsymAlgo, + + /// Bit mask listing Requester-supported SPDM-enumerated cryptographic hashing + /// algorithms. If the Requester does not support any request/response pair + /// that requires hashing operations, this value shall be set to zero. + /// + /// See [BaseHashAlgo] for details. + base_hash_algo: BaseHashAlgo, + + /// Reserved. + reserved_1: [u8; 12], + + /// The number of Requester-supported extended asymmetric key signature algorithms + /// (=A) for the purpose of signature verification. + /// A + E + ExtAlgCount2 + ExtAlgCount3 + ExtAlgCount4 + ExtAlgCount5 shall be + /// less than or equal to `20`. If the Requester does not support any request/response + /// pair that requires signature verification, this value shall be set to zero. + /// + /// See [MAX_SPDM_EXT_ALG_COUNT_V11], [MAX_SPDM_EXT_ALG_COUNT_V13]; + ext_asym_count: u8, + + /// Shall be the number of Requester-supported extended hashing algorithms (=E). + /// A + E + ExtAlgCount2 + ExtAlgCount3 + ExtAlgCount4 + ExtAlgCount5 shall be + /// less than or equal to `20`. If the Requester does not support any request/response + /// pair that requires hashing operations, this value shall be set to zero. + /// + /// See [MAX_SPDM_EXT_ALG_COUNT_V11], [MAX_SPDM_EXT_ALG_COUNT_V13]; + ext_hash_count: u8, + + /// Reserved. + reserved_2: u8, + + /// The Requester shall set the corresponding bit for each supported measurement + /// extension log (MEL) specification. + /// Introduced in v1.3. + /// + /// See [MelSpecification] for details. + mel_specification: MelSpecification, +} + +impl NegotiateAlgorithmsReq { + /// Returns a new `NegotiateAlgorithmsReq` with the provided parameters. + /// IT does **NOT** include the variable-length algorithm structure tables and + /// extended algorithm structures that follow this header. + /// Although, the length field is calculated and set as if they were included, + /// to make it easier to later generate the full request and be compatible with + /// the SPDM specification description of the fields provided. + /// + /// It does *NOT* validate the total extended algorithm count against the SPDM version. + #[allow(clippy::too_many_arguments)] + pub fn new( + num_alg_struct_tables: u8, + param2: u8, + measurement_specification: MeasurementSpecification, + other_param_support: OtherParamSupport, + base_asym_algo: BaseAsymAlgo, + base_hash_algo: BaseHashAlgo, + ext_asym_count: u8, + ext_hash_count: u8, + mel_specification: MelSpecification, + alg_ext_count: u8, + ) -> SpdmResult { + let mut req = NegotiateAlgorithmsReq { + num_alg_struct_tables, + param2, + length: 0, + measurement_specification, + other_param_support, + base_asym_algo, + base_hash_algo, + reserved_1: [0u8; 12], + ext_asym_count, + ext_hash_count, + reserved_2: 0, + mel_specification, + }; + + req.length = req.min_req_len() + + (size_of::() * alg_ext_count as usize) as u16; + + if req.length > MAX_SPDM_REQUEST_LENGTH { + return Err(SpdmError::InvalidParam); + } + + Ok(req) + } + + /// Calculate the minimum required length of the request based on the number of + /// algorithm structure tables and extended algorithm structures in bytes. + fn min_req_len(&self) -> u16 { + let total_alg_struct_len = size_of::() * self.num_alg_struct_tables as usize; + let total_ext_asym_len = size_of::() * self.ext_asym_count as usize; + let total_ext_hash_len = size_of::() * self.ext_hash_count as usize; + (size_of::() + + size_of::() + + total_alg_struct_len + + total_ext_asym_len + + total_ext_hash_len) as u16 + } + + /// Calculate the size of the extended algorithm structures in bytes. + /// This includes both extended asymmetric and extended hash algorithms. + pub fn ext_algo_size(&self) -> usize { + let ext_algo_count = self.ext_asym_count as usize + self.ext_hash_count as usize; + size_of::() * ext_algo_count + } + + /// Validate that the total number of extended algorithms does not exceed + /// the maximum allowed for the given SPDM version. + /// + /// # Arguments + /// - `version`: The SPDM version in use. + /// - `total_ext_alg_count`: The total number of extended algorithms (A + E). + /// + /// # Returns + /// - `Ok(())` if the count is valid. + /// - `Err(SpdmError::InvalidParam)` if the count exceeds the maximum + pub fn validate_total_ext_alg_count( + &self, + version: SpdmVersion, + total_ext_alg_count: u8, + ) -> SpdmResult<()> { + if total_ext_alg_count + > match version { + SpdmVersion::V10 => MAX_SPDM_EXT_ALG_COUNT_V10, + SpdmVersion::V11 => MAX_SPDM_EXT_ALG_COUNT_V11, + SpdmVersion::V13 => MAX_SPDM_EXT_ALG_COUNT_V13, + _ => MAX_SPDM_EXT_ALG_COUNT_V11, + } + { + Err(SpdmError::InvalidParam) + } else { + Ok(()) + } + } +} + +impl CommonCodec for NegotiateAlgorithmsReq {} + +#[derive(IntoBytes, FromBytes, Immutable, Default)] +#[repr(C, packed)] +#[allow(dead_code)] +/// # NOTE +/// After this response we expect to be present when sent: +/// - ExtAsymSel +/// - ExtHashSel +/// - RespAlgStruct +pub struct AlgorithmsResp { + /// Shall be the number of algorithm structure tables in this request using RespAlgStruct. + num_alg_struct_tables: u8, + reserved_1: u8, + + /// Shall be the length of the response message, in bytes. + length: u16, + + /// The Responder shall select one of the measurement specifications supported by the + /// Requester and Responder. Thus, no more than one bit shall be set + measurement_specification_sel: MeasurementSpecification, + + /// Shall be the selected Parameter Bit Mask. The Responder shall select one + /// of the opaque data formats supported by the Requester. Thus, no more + /// than one bit shall be set for the opaque data format. + other_params_selection: OtherParamSupport, + + measurement_hash_algo: MeasurementHashAlgo, + base_asym_sel: BaseAsymAlgo, + base_hash_sel: BaseHashAlgo, + reserved_2: [u8; 11], + mel_specification_sel: MelSpecification, + ext_asym_sel_count: u8, + ext_hash_sel_count: u8, + reserved_3: [u8; 2], + // - ExtAsymSel + // - ExtHashSel + // - RespAlgStruct +} + +impl CommonCodec for AlgorithmsResp {} + +#[derive(IntoBytes, FromBytes, Immutable, Default)] +#[repr(C)] +/// See [DSP0274 v1.3.0, p, 86](https://www.dmtf.org/sites/default/files/standards/documents/DSP0274_1.3.0.pdf) +pub struct ExtendedAlgo { + /// Shall represent the registry or standards body. + /// + /// See [RegistryId] for details. + registry_id: u8, + + /// Reserved. + reserved: u8, + + /// Shall indicate the desired algorithm. The registry or standards body owns + /// the value of this field. See [RegistryId]. At present, DMTF does not define + /// any algorithms for use in extended algorithms fields. + algorithm_id: u16, +} + +impl CommonCodec for ExtendedAlgo {} + +impl ExtendedAlgo { + pub fn new(registry_id: RegistryId, algorithm_id: u16) -> Self { + ExtendedAlgo { + registry_id: registry_id as u8, + reserved: 0, + algorithm_id, + } + } +} + +/// Registry or standards body ID for algorithm encoding in extended algorithm fields. +/// Consult the respective registry or standards body unless otherwise specified. +#[repr(u8)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum RegistryId { + /// DMTF does not have a Vendor ID registry. + DMTF = 0x0, + + /// VendorID is identified by using TCG Vendor ID Registry. + /// For extended algorithms, see TCG Algorithm Registry. + TCG = 0x1, + + /// VendorID is identified by using the vendor ID assigned by USB. + USB = 0x2, + + /// VendorID is identified using PCI-SIG Vendor ID. + PCISIG = 0x3, + + /// The Private Enterprise Number (PEN) assigned by the Internet Assigned + /// Numbers Authority (IANA) identifies the vendor. + IANA = 0x4, + + /// VendorID is identified by using HDBaseT HDCD entity. + HDBASET = 0x5, + + /// The Manufacturer ID assigned by MIPI identifies the vendor. + MIPI = 0x6, + + /// VendorID is identified by using CXL vendor ID. + CXL = 0x7, + + /// VendorID is identified by using JEDEC vendor ID. + JEDEC = 0x8, + + /// For fields and formats defined by the VESA standards body, + /// there is no Vendor ID registry. + VESA = 0x9, + + /// The CBOR Tag Registry that identifies the format of the element, + /// as assigned by the Internet Assigned Numbers Authority (IANA). + /// The encoding of the CBOR tag indicates the length of the tag. + /// When a CBOR Tag is used with a standards body or vendor-defined header, + /// the VendorIDLen field shall be set to the length of the encoded CBOR tag, + /// followed by the data payload, which starts with an encoded CBOR tag. + IANACBOR = 0xA, +} + +impl RegistryId { + /// Returns the vendor ID length in bytes for this registry. + /// Returns None for variable-length registries (IANA CBOR). + pub const fn vendor_id_length(&self) -> Option { + match self { + Self::DMTF => Some(0), + Self::TCG => Some(2), + Self::USB => Some(2), + Self::PCISIG => Some(2), + Self::IANA => Some(4), + Self::HDBASET => Some(4), + Self::MIPI => Some(2), + Self::CXL => Some(2), + Self::JEDEC => Some(2), + Self::VESA => Some(0), + Self::IANACBOR => None, // Variable length + } + } +} + +#[derive(Debug, Clone, Copy)] +pub enum AlgType { + // 0x00 and 0x01. Reserved. + Dhe = 2, + AeadCipherSuite = 3, + ReqBaseAsymAlg = 4, + KeySchedule = 5, +} + +impl TryFrom for AlgType { + type Error = SpdmError; + + fn try_from(value: u8) -> Result { + match value { + 2 => Ok(AlgType::Dhe), + 3 => Ok(AlgType::AeadCipherSuite), + 4 => Ok(AlgType::ReqBaseAsymAlg), + 5 => Ok(AlgType::KeySchedule), + _ => Err(SpdmError::InvalidParam), + } + } +} + +impl TryFrom for AlgType { + type Error = SpdmError; + + fn try_from(value: u16) -> Result { + match value { + 2 => Ok(AlgType::Dhe), + 3 => Ok(AlgType::AeadCipherSuite), + 4 => Ok(AlgType::ReqBaseAsymAlg), + 5 => Ok(AlgType::KeySchedule), + _ => Err(SpdmError::InvalidParam), + } + } +} + +#[derive(Debug, Clone, Copy, IntoBytes, FromBytes, Immutable, Default)] +#[repr(C, packed)] +/// Shall be the Requester-supported fixed algorithms. +pub struct AlgCount(u8); + +impl AlgCount { + pub fn get(&self) -> u8 { + self.0 + } + + pub fn set(&mut self, count: u8) { + self.0 = count; + } + + /// Number of bytes required to describe Requester-supported SPDM-enumerated + /// fixed algorithms (=FixedAlgCount). FixedAlgCount + 2 shall be a multiple of 4. + pub fn num_(&self) -> u8 { + self.0 & 0b11111000 + } + + /// Number of Requester-supported extended algorithms (= ExtAlgCount ). + pub fn rum_req_supported_algos(&self) -> u8 { + self.0 & 0b00000111 + } +} + +impl From for AlgCount { + fn from(value: u8) -> Self { + Self(value) + } +} + +impl CommonCodec for AlgCount {} + +bitfield! { + #[derive(FromBytes, IntoBytes, Immutable, Default, Clone, Copy)] + #[repr(C)] + /// This structure describes an algorithm structure table. + /// It does **NOT** include the variable-length `AlgExternal` fields that follow this header for the Request. + /// + /// The `AlgExternal` fields are of type [ExtendedAlgo] and their number is defined + /// by the `ExtAlgCount` field. + /// The existence of `AlgExternal` is optional. + // TODO: make this a structure with Option? + pub struct AlgStructure(u32); + impl Debug; + u8; + /// Shall be the type of algorithm. + /// + /// See [AlgType] for details. + pub alg_type, set_alg_type: 7, 0; + + /// Shall be the bit mask listing Responder-supported fixed algorithm requested by the Requester. + /// This value shall be either 0 or 1. + /// That means, that there is either 1 or None external algorithm structure following this header. + pub ext_alg_count, set_ext_alg_count: 11, 8; + pub fixed_alg_count, set_fixed_alg_count: 15, 12; + u16; + // TODO: somehow we can just assume this will fit? and why do we + pub alg_supported, set_alg_supported: 31, 16; + // AlgExternal +} + +impl AlgStructure { + // FixedAlgCount + 2 shall be a multiple of 4 + pub fn is_multiple(&self) -> bool { + ((self.fixed_alg_count() as usize) + 2).is_multiple_of(4) + } + + /// Create a new [AlgStructure] for the given algorithm type as specified in + // Tables 17, 18, 19, 20 of DSP0274 v1.3.0 + pub fn new(alg_type: &AlgType, local_algos: &LocalDeviceAlgorithms) -> AlgStructure { + let mut res = AlgStructure::default(); + res.set_alg_type(*alg_type as u8); + + // Bit [7:4]. Shall be a value of 2. + res.set_fixed_alg_count(2); + + match alg_type { + AlgType::Dhe => { + res.set_alg_supported(local_algos.device_algorithms.dhe_group.0); + } + + AlgType::AeadCipherSuite => { + res.set_alg_supported(local_algos.device_algorithms.aead_cipher_suite.0); + } + + AlgType::ReqBaseAsymAlg => { + res.set_alg_supported(local_algos.device_algorithms.req_base_asym_algo.0); + } + + AlgType::KeySchedule => { + res.set_alg_supported(local_algos.device_algorithms.key_schedule.0); + } + } + res.set_ext_alg_count(res.alg_supported().count_ones() as u8); + res + } +} + +impl CommonCodec for AlgStructure {} + +#[cfg(test)] +mod tests { + // use super::*; + + #[ignore] + #[test] + fn test_min_req_len() { + todo!(); + } +} diff --git a/src/commands/algorithms/request.rs b/src/commands/algorithms/request.rs new file mode 100644 index 0000000..7f8d2c1 --- /dev/null +++ b/src/commands/algorithms/request.rs @@ -0,0 +1,294 @@ +// Copyright 2025 +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use crate::{ + codec::{Codec, MessageBuf}, + commands::algorithms::{AlgStructure, AlgorithmsResp, ExtendedAlgo, NegotiateAlgorithmsReq}, + commands::error_rsp::ErrorCode, + context::SpdmContext, + error::{CommandError, CommandResult}, + protocol::{DeviceAlgorithms, SpdmMsgHdr}, +}; + +/// Parse and handle the NEGOTIATE_ALGORITHMS response from the Responder. +pub(crate) fn handle_algorithms_response<'a>( + ctx: &mut SpdmContext<'a>, + resp_header: SpdmMsgHdr, + resp: &mut MessageBuf<'a>, +) -> CommandResult<()> { + let version = resp_header + .version() + .map_err(|_| (true, CommandError::UnsupportedRequest))?; + + let req_resp_code = resp_header + .req_resp_code() + .map_err(|_| (true, CommandError::UnsupportedRequest))?; + + if version != ctx.state.connection_info.version_number() { + return Err((true, CommandError::UnsupportedRequest)); + } + if req_resp_code != crate::protocol::ReqRespCode::Algorithms { + return Err((true, CommandError::UnsupportedRequest)); + } + + let algo_resp: AlgorithmsResp = + AlgorithmsResp::decode(resp).map_err(|e| (true, CommandError::Codec(e)))?; + + // Responder MUST set all algorithm fields to non-zero values, otherwise the requester MUST return an error with code SPDM_ERROR_CODE_REQUEST_RESYNCH. + if algo_resp.measurement_hash_algo.0 == 0 + || algo_resp.base_asym_sel.0 == 0 + || algo_resp.base_hash_sel.0 == 0 + { + return Err((true, CommandError::ErrorCode(ErrorCode::RequestResynch))); + } + + // Thus, no more than one bit shall be set + if algo_resp.measurement_specification_sel.0.count_ones() > 1 { + return Err((true, CommandError::ErrorCode(ErrorCode::InvalidPolicy))); + } + + // Thus, no more than one bit shall be set for the opaque data format. + if algo_resp.other_params_selection.opaque_data_fmt0() == 1 + && algo_resp.other_params_selection.opaque_data_fmt1() == 1 + { + return Err((true, CommandError::ErrorCode(ErrorCode::InvalidPolicy))); + } + + let cap = ctx + .state + .connection_info + .peer_capabilities() + .flags + .meas_cap(); + + // If the Responder supports measurements ( MEAS_CAP=01b or MEAS_CAP=10b in its + // CAPABILITIES response) and if MeasurementSpecificationSel is non-zero, + // then exactly one bit in this bit field shall be set. Otherwise, the Responder + // shall set this field to 0. + if cap != 0 + && algo_resp.measurement_specification_sel.0 != 0 + && algo_resp.measurement_specification_sel.0.count_ones() > 1 + { + return Err((true, CommandError::ErrorCode(ErrorCode::InvalidPolicy))); + } + + // If the Responder supports measurements in its CAPABILITIES response) and if + // MeasurementSpecificationSel is non-zero, then exactly one bit in this bit + // field shall be set. Otherwise, the Responder shall set this field to 0 + if (cap == 0b01 || cap == 0b10) && algo_resp.measurement_specification_sel.0 != 0 { + if algo_resp.measurement_specification_sel.0.count_ones() > 1 { + return Err((true, CommandError::ErrorCode(ErrorCode::InvalidPolicy))); + } + } else if algo_resp.measurement_specification_sel.0 != 0 { + return Err((true, CommandError::ErrorCode(ErrorCode::InvalidPolicy))); + } + + // TODO: If the Responder does not support any request/response pair that + // requires hashing operations, this value shall be set to zero. The Responder + // shall set no more than one bit. + + let peer_device_algorithms = DeviceAlgorithms { + measurement_spec: algo_resp.measurement_specification_sel, + other_param_support: algo_resp.other_params_selection, + base_asym_algo: algo_resp.base_asym_sel, + base_hash_algo: algo_resp.base_hash_sel, + mel_specification: algo_resp.mel_specification_sel, + ..Default::default() + }; + + ctx.state + .connection_info + .set_peer_algorithms(peer_device_algorithms); + + // The spec defines this is A' elem of {0, 1} + // TODO: add them to state? + let _ext_asym_alog = if algo_resp.ext_asym_sel_count == 1 { + Some(ExtendedAlgo::decode(resp).map_err(|e| (true, CommandError::Codec(e)))?) + } else { + None + }; + + // The spec defines this is E' elem of {0, 1} + // TODO: add them to state? + let _ext_hash_algo = if algo_resp.ext_hash_sel_count == 1 { + Some(ExtendedAlgo::decode(resp).map_err(|e| (true, CommandError::Codec(e)))?) + } else { + None + }; + + for _ in 0..algo_resp.num_alg_struct_tables { + let alg_struct = AlgStructure::decode(resp).map_err(|e| (true, CommandError::Codec(e)))?; + + // For each struct table, we need to decode the variable length fields. + for _ in 0..alg_struct.ext_alg_count() { + let _ext_algo = + ExtendedAlgo::decode(resp).map_err(|e| (true, CommandError::Codec(e)))?; + } + } + + ctx.state + .connection_info + .set_state(crate::state::ConnectionState::AlgorithmsNegotiated); + + ctx.append_message_to_transcript(resp, crate::transcript::TranscriptContext::Vca) +} + +/// Generate the NEGOTIATE_ALGORITHMS request with all the contexts local information. +/// +/// # Arguments +/// +/// - `ctx` - The SPDM context containing local algorithm information. +/// - `req_buf` - The message buffer to encode the request into. +/// - `ext_asym` - Optional slice of extended asymmetric algorithm types. +/// - `ext_hash` - Optional slice of extended hash algorithm types. +/// - `req_alg_struct` - The AlgStructure variable fields. +/// - `alg_external` - Optional extended algorithm structure. +/// +/// # Returns +/// +/// - `Ok(())` on success. +/// - [CommandError] on failure. +/// +/// # References +/// - See libspdm/library/spdm_requester_lib/libspdm_req_negotiate_algorithms.c for reference implementation. +/// - Note: the `spdm_message_header_t` has param1 and param2 fields used for various purposes. +/// - Note: see spdm_responder_test_3_algorithms.c +pub fn generate_negotiate_algorithms_request<'a>( + ctx: &mut SpdmContext<'a>, + req_buf: &mut MessageBuf<'a>, + ext_asym: Option<&'a [ExtendedAlgo]>, + ext_hash: Option<&'a [ExtendedAlgo]>, + req_alg_struct: Option, + alg_external: Option<&'a [ExtendedAlgo]>, // req_alg_struct.AlgCount.ExtAlgCount many +) -> CommandResult<()> { + let local_algorithms = &ctx.local_algorithms.device_algorithms; + let local_state = &ctx.state.connection_info; + + let ext_asym_count = match ext_asym { + Some(ext) => ext.len() as u8, + None => 0, + }; + let ext_hash_count = match ext_hash { + Some(ext) => ext.len() as u8, + None => 0, + }; + + let num_alg_struct_tables = req_alg_struct.is_some() as u8; + let alg_ext_count = req_alg_struct.map_or(0, |s| s.ext_alg_count()); + + // Generate base structure **without** the variable length structures + let negotiate_algorithms_req = NegotiateAlgorithmsReq::new( + num_alg_struct_tables, + 0, // param2 + local_algorithms.measurement_spec, + local_algorithms.other_param_support, + local_algorithms.base_asym_algo, + local_algorithms.base_hash_algo, + ext_asym_count, + ext_hash_count, + local_algorithms.mel_specification, + alg_ext_count, + ) + .map_err(|_| (false, CommandError::UnsupportedRequest))?; + + // Verify that the extended algorithm counts are valid + negotiate_algorithms_req + .validate_total_ext_alg_count( + local_state.version_number(), + ext_asym_count + ext_hash_count, + ) + .map_err(|_| (false, CommandError::UnsupportedRequest))?; + + if (negotiate_algorithms_req.min_req_len() as usize) > req_buf.capacity() { + return Err((false, CommandError::BufferTooSmall)); + } + + // Message Assembly + // 1. Create Header + // 2. Encode base NegotiateAlgorithmsReq + // 3. Encode Variable length fields + // 3.1 Encode ExtAsym (if present) + // 3.2 Encode ExtHash (if present) + // 3.3 Encode ExtAlgo (if present) + // 3.4 Encode ExtendedAlgorithms (if present) + + // 1. + SpdmMsgHdr::new( + ctx.state.connection_info.version_number(), + crate::protocol::ReqRespCode::NegotiateAlgorithms, + ) + .encode(req_buf) + .map_err(|e| (false, CommandError::Codec(e)))?; + + // 2. + // This encoding does *NOT* yet contain the variable fields. + negotiate_algorithms_req + .encode(req_buf) + .map_err(|e| (false, CommandError::Codec(e)))?; + + // Add variable fields if any. As defined by the size of the struct and the + // PLMD spec, we know that the offset starts at 32 bytes. + // The constructor of NegotiateAlgorithmsReq sets the structs length correctly. + + // 3.1 + if let Some(ext_asym_algos) = ext_asym { + for ext in ext_asym_algos { + ext.encode(req_buf) + .map_err(|e| (false, CommandError::Codec(e)))?; + } + } + + // 3.2 + if let Some(ext_hash_algos) = ext_hash { + for ext in ext_hash_algos { + ext.encode(req_buf) + .map_err(|e| (false, CommandError::Codec(e)))?; + } + } + + // 3.3 + 3.4: encode AlgStructure and its extended algorithms if present + if let Some(alg_struct) = req_alg_struct { + if alg_struct.fixed_alg_count() != 0 && !alg_struct.is_multiple() { + return Err((false, CommandError::UnsupportedRequest)); + } + + alg_struct + .encode(req_buf) + .map_err(|e| (false, CommandError::Codec(e)))?; + + // If ext_alg_count > 0, we must have the extended algorithm structures. + if alg_struct.ext_alg_count() > 0 { + let extended_algos = alg_external.ok_or((false, CommandError::UnsupportedRequest))?; + for ext in extended_algos { + ext.encode(req_buf) + .map_err(|e| (false, CommandError::Codec(e)))?; + } + } + } + + ctx.append_message_to_transcript(req_buf, crate::transcript::TranscriptContext::Vca) +} + +#[cfg(test)] +mod tests { + // use crate::test::MockResources; + + // use super::*; + + #[ignore] + #[test] + pub fn test_parse_negotiate_algorithms() { + todo!(); + } +} diff --git a/src/commands/algorithms_rsp.rs b/src/commands/algorithms/response.rs similarity index 78% rename from src/commands/algorithms_rsp.rs rename to src/commands/algorithms/response.rs index fe4891b..3ab020b 100644 --- a/src/commands/algorithms_rsp.rs +++ b/src/commands/algorithms/response.rs @@ -1,140 +1,31 @@ -// Licensed under the Apache-2.0 license +// Copyright 2025 +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use crate::{ + codec::{Codec, MessageBuf}, + commands::algorithms::*, + context::SpdmContext, + error::CommandResult, + protocol::{SpdmMsgHdr, SpdmVersion}, + state::ConnectionState, + transcript::TranscriptContext, +}; -use crate::codec::{Codec, CommonCodec, MessageBuf}; use crate::commands::error_rsp::ErrorCode; -use crate::context::SpdmContext; -use crate::error::{CommandResult, SpdmError}; use crate::protocol::*; -use crate::state::ConnectionState; -use crate::transcript::TranscriptContext; -use bitfield::bitfield; -use core::mem::size_of; -use zerocopy::{FromBytes, Immutable, IntoBytes}; - -// Max request length shall be 128 bytes (SPDM1.3 Table 10.4) -const MAX_SPDM_REQUEST_LENGTH: u16 = 128; -const MAX_SPDM_EXT_ALG_COUNT_V10: u8 = 8; -const MAX_SPDM_EXT_ALG_COUNT_V11: u8 = 20; - -#[derive(IntoBytes, FromBytes, Immutable, Default, Debug)] -#[repr(C, packed)] -struct NegotiateAlgorithmsReq { - num_alg_struct_tables: u8, - param2: u8, - length: u16, - measurement_specification: MeasurementSpecification, - other_param_support: OtherParamSupport, - base_asym_algo: BaseAsymAlgo, - base_hash_algo: BaseHashAlgo, - reserved_1: [u8; 12], - ext_asyn_count: u8, - ext_hash_count: u8, - reserved_2: u8, - mel_specification: MelSpecification, -} - -impl NegotiateAlgorithmsReq { - fn min_req_len(&self) -> u16 { - let total_alg_struct_len = size_of::() * self.num_alg_struct_tables as usize; - let total_ext_asym_len = size_of::() * self.ext_asyn_count as usize; - let total_ext_hash_len = size_of::() * self.ext_hash_count as usize; - (size_of::() - + total_alg_struct_len - + total_ext_asym_len - + total_ext_hash_len) as u16 - } - - fn ext_algo_size(&self) -> usize { - let ext_algo_count = self.ext_asyn_count as usize + self.ext_hash_count as usize; - size_of::() * ext_algo_count - } - - fn validate_total_ext_alg_count( - &self, - version: SpdmVersion, - total_ext_alg_count: u8, - ) -> Result<(), SpdmError> { - let max_count = match version { - SpdmVersion::V10 => MAX_SPDM_EXT_ALG_COUNT_V10, - _ => MAX_SPDM_EXT_ALG_COUNT_V11, - }; - - if total_ext_alg_count > max_count { - Err(SpdmError::InvalidParam) - } else { - Ok(()) - } - } -} - -impl CommonCodec for NegotiateAlgorithmsReq {} - -#[derive(IntoBytes, FromBytes, Immutable, Default)] -#[repr(C, packed)] -#[allow(dead_code)] -struct AlgorithmsResp { - num_alg_struct_tables: u8, - reserved_1: u8, - length: u16, - measurement_specification_sel: MeasurementSpecification, - other_params_selection: OtherParamSupport, - measurement_hash_algo: MeasurementHashAlgo, - base_asym_sel: BaseAsymAlgo, - base_hash_sel: BaseHashAlgo, - reserved_2: [u8; 11], - mel_specification_sel: MelSpecification, - ext_asym_sel_count: u8, - ext_hash_sel_count: u8, - reserved_3: [u8; 2], -} - -impl CommonCodec for AlgorithmsResp {} -#[derive(IntoBytes, FromBytes, Immutable, Default)] -#[repr(C)] -struct ExtendedAlgo { - registry_id: u8, - reserved: u8, - algorithm_id: u16, -} - -impl CommonCodec for ExtendedAlgo {} - -#[derive(Debug, Clone, Copy)] -enum AlgType { - Dhe = 2, - AeadCipherSuite = 3, - ReqBaseAsymAlg = 4, - KeySchedule = 5, -} -impl TryFrom for AlgType { - type Error = SpdmError; - - fn try_from(value: u8) -> Result { - match value { - 2 => Ok(AlgType::Dhe), - 3 => Ok(AlgType::AeadCipherSuite), - 4 => Ok(AlgType::ReqBaseAsymAlg), - 5 => Ok(AlgType::KeySchedule), - _ => Err(SpdmError::InvalidParam), - } - } -} - -bitfield! { - #[derive(FromBytes, IntoBytes, Immutable, Default, Clone, Copy)] - #[repr(C)] - pub struct AlgStructure(u32); - impl Debug; - u8; - pub alg_type, set_alg_type: 7, 0; - pub ext_alg_count, set_ext_alg_count: 11, 8; - pub fixed_alg_count, set_fixed_alg_count: 15, 12; - u16; - pub alg_supported, set_alg_supported: 31, 16; -} - -impl CommonCodec for AlgStructure {} +use core::mem::size_of; pub(crate) fn selected_measurement_specification(ctx: &SpdmContext) -> MeasurementSpecification { let local_cap_flags = &ctx.local_capabilities.flags; @@ -243,6 +134,13 @@ fn process_negotiate_algorithms_request<'a>( if fixed_alg_count != 2 { Err(ctx.generate_error_response(req_payload, ErrorCode::InvalidRequest, 0, None))?; } + + // Skip extended algorithm structures embedded in this AlgStructure + for _ in 0..ext_alg_count { + ExtendedAlgo::decode(req_payload).map_err(|_| { + ctx.generate_error_response(req_payload, ErrorCode::InvalidRequest, 0, None) + })?; + } } // Total number of extended algorithms check diff --git a/src/commands/capabilities/mod.rs b/src/commands/capabilities/mod.rs new file mode 100644 index 0000000..13641df --- /dev/null +++ b/src/commands/capabilities/mod.rs @@ -0,0 +1,130 @@ +// Copyright 2025 +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +pub mod request; +pub mod response; + +pub(crate) use request::*; +pub(crate) use response::*; + +use zerocopy::{FromBytes, Immutable, IntoBytes}; + +use crate::{codec::CommonCodec, protocol::CapabilityFlags}; + +use crate::protocol::capabilities::DeviceCapabilities; + +#[derive(IntoBytes, FromBytes, Immutable, Default)] +#[repr(C)] +pub struct GetCapabilitiesBase { + param1: u8, + param2: u8, +} +/// CAPABILITIES response base +/// +/// v1.0 CAPABILITIES response is constructed by `CapabilitiesBase`+`Capabilities`. +pub type CapabilitiesBase = GetCapabilitiesBase; + +impl CommonCodec for GetCapabilitiesBase {} + +#[derive(IntoBytes, FromBytes, Immutable, Default)] +#[repr(C, packed)] +#[allow(dead_code)] +pub struct GetCapabilitiesV11 { + /// Reserved. + reserved: u8, + + /// Shall be exponent of base 2, which is used to calculate CT . + /// The equation for CT shall be 2^{CTExponent} microseconds (μs). + /// # Example + /// CT=10 -> 2^10 = 1024 μs = 1.024 ms + pub ct_exponent: u8, + + /// Reserved. + /// + /// _TODO_: Part of the 16-bit extended flags field added in v1.4.0 + reserved2: u8, + + /// Reserved. + /// + /// _TODO_: Part of the 16-bit extended flags field added in v1.4.0 + reserved3: u8, + + /// Capability flags. + flags: CapabilityFlags, +} +/// CAPABILITIES response +pub type Capabilities = GetCapabilitiesV11; + +impl GetCapabilitiesV11 { + pub fn new(ct_exponent: u8, flags: CapabilityFlags) -> Self { + Self { + reserved: 0, + ct_exponent, + reserved2: 0, + reserved3: 0, + flags, + } + } +} + +impl CommonCodec for GetCapabilitiesV11 {} + +/// DSP0274, Table 11 +#[derive(IntoBytes, FromBytes, Immutable)] +#[repr(C, packed)] +pub struct GetCapabilitiesV12 { + /// This field shall indicate the maximum buffer size, in + /// bytes, of the Requester for receiving a single and + /// complete SPDM message whose message size is less + /// than or equal to the value in this field. + data_transfer_size: u32, + + /// If the Requester supports the Large SPDM message + /// transfer mechanism, this field shall indicate the + /// maximum size, in bytes, of the internal buffer of a + /// Requester used to reassemble a single and complete + /// Large SPDM message. + max_spdm_msg_size: u32, +} +/// CAPABILITIES response v1.2 additions +pub type CapabilitiesV12 = GetCapabilitiesV12; + +impl CommonCodec for GetCapabilitiesV12 {} + +/// Although [GetCapabilitiesBase], [GetCapabilitiesV11] and [GetCapabilitiesV12] +/// are more generic, the context currently uses [crate::protocol::capabilities::DeviceCapabilities]. +/// Until we refactor the context, this function translates from one to the other. +impl From<&DeviceCapabilities> for GetCapabilitiesV11 { + fn from(dev_cap: &DeviceCapabilities) -> Self { + Self::new(dev_cap.ct_exponent, dev_cap.flags) + } +} + +impl From<&DeviceCapabilities> for GetCapabilitiesV12 { + fn from(dev_cap: &DeviceCapabilities) -> Self { + Self { + data_transfer_size: dev_cap.data_transfer_size, + max_spdm_msg_size: dev_cap.max_spdm_msg_size, + } + } +} + +impl Default for GetCapabilitiesV12 { + fn default() -> Self { + GetCapabilitiesV12 { + data_transfer_size: crate::protocol::MIN_DATA_TRANSFER_SIZE_V12, + max_spdm_msg_size: crate::protocol::MIN_DATA_TRANSFER_SIZE_V12, + } + } +} diff --git a/src/commands/capabilities/request.rs b/src/commands/capabilities/request.rs new file mode 100644 index 0000000..e95076e --- /dev/null +++ b/src/commands/capabilities/request.rs @@ -0,0 +1,567 @@ +// Copyright 2025 +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use crate::commands::error_rsp::ErrorCode; +use crate::protocol::CapabilityFlags; +use crate::{codec::MessageBuf, context::SpdmContext, error::CommandResult, protocol::SpdmMsgHdr}; + +use crate::commands::capabilities::{ + Capabilities, CapabilitiesBase, CapabilitiesV12, GetCapabilitiesBase, GetCapabilitiesV11, + GetCapabilitiesV12, +}; +use crate::protocol::{capabilities::DeviceCapabilities, ReqRespCode, SpdmVersion}; + +use crate::error::CommandError; +use crate::transcript::TranscriptContext; + +use crate::codec::Codec; + +/// Requester function handling the parsing of the CAPABILITIES response sent by the Responder. +/// +/// # Returns +/// - () on success +/// +/// #TODO +/// - [ ] A Responder can report that it needs to transmit the response in smaller +/// transfers by sending an ERROR message of ErrorCode=LargeResponse +pub(crate) fn handle_capabilities_response<'a>( + ctx: &mut SpdmContext<'a>, + resp_header: SpdmMsgHdr, + resp: &mut MessageBuf<'a>, +) -> CommandResult<()> { + // TODO: I don't think we should call _generate_error_response_ here for every error. + // Instead just returning proper error codes is probably better. + + let version_hdr = match resp_header.version() { + Ok(v) => v, + Err(_) => Err(ctx.generate_error_response(resp, ErrorCode::VersionMismatch, 0, None))?, + }; + + // Verify that the version is supported by both parties + // TODO: Should responses that don't match the negotiated version be silently accepted? + let version = match ctx.supported_versions.iter().find(|&&v| v == version_hdr) { + Some(&v) => v, + None => Err(ctx.generate_error_response(resp, ErrorCode::VersionMismatch, 0, None))?, + }; + + let _base_resp = CapabilitiesBase::decode(resp) + .map_err(|_| ctx.generate_error_response(resp, ErrorCode::OperationFailed, 0, None))?; + + // Based on the negotiated version, try to decode the rest of the response. + // If the response misses expected fields, return an error. + // See src/commands/capabilities/response.rs for more details. + + let mut peer_capabilities = DeviceCapabilities::default(); + + let resp_11 = Capabilities::decode(resp) + .map_err(|_| ctx.generate_error_response(resp, ErrorCode::InvalidRequest, 0, None))?; + peer_capabilities.ct_exponent = resp_11.ct_exponent; + + let flags = resp_11.flags; + if !resp_flags_compatible(version, &flags) { + Err(ctx.generate_error_response(resp, ErrorCode::InvalidPolicy, 0, None))?; + } + peer_capabilities.flags = resp_11.flags; + + if version >= SpdmVersion::V12 { + let resp_12 = CapabilitiesV12::decode(resp) + .map_err(|_| ctx.generate_error_response(resp, ErrorCode::InvalidRequest, 0, None))?; + + // _DataTransferSize_ shall be equal to or greater than _MinDataTransferSize_ + if resp_12.data_transfer_size < crate::protocol::MIN_DATA_TRANSFER_SIZE_V12 { + return Err((false, CommandError::InvalidResponse)); + } + + // _MaxSPDMmsgSize_ should be greater than or equal to _DataTransferSize_ + if resp_12.max_spdm_msg_size < resp_12.data_transfer_size { + return Err((false, CommandError::InvalidResponse)); + } + + peer_capabilities.data_transfer_size = resp_12.data_transfer_size; + peer_capabilities.max_spdm_msg_size = resp_12.max_spdm_msg_size; + } + // TODO: Since v1.3 an additional optional Supported Algorithms block was added. + + ctx.state + .connection_info + .set_peer_capabilities(peer_capabilities); + + ctx.state + .connection_info + .set_state(crate::state::ConnectionState::AfterCapabilities); + + ctx.append_message_to_transcript(resp, TranscriptContext::Vca) +} + +/// Generate the GET_CAPABILITIES command with all the contexts information. +/// +/// # Arguments +/// - `ctx`: The SPDM context +/// - `req_buf`: The buffer to write the request into +/// - `capabilities`: The base capabilities +/// - `capv11`: The V1.1 capabilities (if applicable) +/// - `capv12`: The V1.2 capabilities (if applicable) +/// +/// # Returns +/// - () on success +/// - [CommandError::BufferTooSmall] when the provided buffer is too small +/// +fn generate_capabilities_request<'a>( + ctx: &mut SpdmContext<'a>, + req_buf: &mut MessageBuf<'a>, + capabilities: GetCapabilitiesBase, + capv11: Option, + capv12: Option, +) -> CommandResult<()> { + // Fill SpdmHeader first + let ctx_version = ctx.state.connection_info.version_number(); + let spdm_req_hdr = SpdmMsgHdr::new(ctx_version, ReqRespCode::GetCapabilities); + let mut payload_len = spdm_req_hdr + .encode(req_buf) + .map_err(|_| (false, CommandError::BufferTooSmall))?; + + let req_common = capabilities; + payload_len += req_common + .encode(req_buf) + .map_err(|_| (false, CommandError::BufferTooSmall))?; + + // Ensure that only the appropriate capability fields based on version are included. + if ctx_version >= SpdmVersion::V11 { + if let Some(capv1) = &capv11 { + payload_len += capv1 + .encode(req_buf) + .map_err(|_| (false, CommandError::BufferTooSmall))?; + } + } + + if ctx_version >= SpdmVersion::V12 { + // Versions 1.2 and higher include GetCapabilitiesV12 + if let Some(capv2) = &capv12 { + payload_len += capv2 + .encode(req_buf) + .map_err(|_| (false, CommandError::BufferTooSmall))?; + } + } + + // Push data offset up by total payload length + req_buf + .push_data(payload_len) + .map_err(|_| (false, CommandError::BufferTooSmall))?; + + // Append request to VCA transcript + ctx.append_message_to_transcript(req_buf, TranscriptContext::Vca) +} + +/// Generate the GET_CAPABILITIES command using the local capabilities from the context. +/// # Arguments +/// - `ctx`: The SPDM context +/// - `req_buf`: The buffer to write the request into +/// # Returns +/// - () on success +/// - [CommandError::BufferTooSmall] when the provided buffer is too small +pub fn generate_capabilities_request_local<'a>( + ctx: &mut SpdmContext<'a>, + req_buf: &mut MessageBuf<'a>, +) -> CommandResult<()> { + let local_capabilities = ctx.local_capabilities; + let mut capabilities = GetCapabilitiesBase::default(); + + let capv11 = Some(GetCapabilitiesV11::new( + local_capabilities.ct_exponent, + local_capabilities.flags, + )); + + let capv12 = if ctx.state.connection_info.version_number() >= SpdmVersion::V12 { + Some(GetCapabilitiesV12 { + data_transfer_size: local_capabilities.data_transfer_size, + max_spdm_msg_size: local_capabilities.max_spdm_msg_size, + }) + } else { + None + }; + + if ctx.state.connection_info.version_number() >= SpdmVersion::V13 { + capabilities.param1 |= (local_capabilities.include_supported_algorithms as u8) << 2; + } + + generate_capabilities_request(ctx, req_buf, capabilities, capv11, capv12) +} + +/// Checks that the flags in a capabilites response are compatible with the provided version +/// +/// Checks for reserved values and consistency of flags as far as required. +fn resp_flags_compatible(version: SpdmVersion, flags: &CapabilityFlags) -> bool { + // Most checks are the same but its a bit of a mess with some exceptions, + // so we just do a complete check for every version. + match version { + SpdmVersion::V10 => check_flags_v10(flags), + SpdmVersion::V11 => check_flags_v11(flags), + SpdmVersion::V12 => check_flags_v12(flags), + SpdmVersion::V13 => check_flags_v13(flags), + } +} + +/// Check flags to be compatible with version 1.0 +/// +/// Checks that all flags known to v1.0 have valid values. +/// Reserved fields are ignored. +fn check_flags_v10(flags: &CapabilityFlags) -> bool { + // Check for reserved values + !(flags.meas_cap() == 0b11) +} + +/// Check flags to be compatible with version 1.1 +/// +/// Checks that all flags known to v1.1 have valid values. +/// Reserved fields are ignored. +fn check_flags_v11(flags: &CapabilityFlags) -> bool { + // Check for reserved values + if flags.meas_cap() == 0b11 { + return false; + } + if flags.psk_cap() == 0b11 { + return false; + } + // Check for conditionally needed flags + if flags.encrypt_cap() == 1 { + // One or more of MAC_CAP or KEY_EX_CAP must be set + if flags.mac_cap() == 0 && flags.key_ex_cap() == 0 { + return false; + } + } + if flags.mac_cap() == 1 { + // One or more of PSK_CAP or KEY_EX_CAP must be set + if flags.psk_cap() == 0 && flags.key_ex_cap() == 0 { + return false; + } + } + if flags.key_ex_cap() == 1 { + // One or more of MAC_CAP or ENCRYPT_CAP must be set + if flags.mac_cap() == 0 && flags.encrypt_cap() == 0 { + return false; + } + } + if flags.psk_cap() == 1 { + // One or more of MAC_CAP or ENCRYPT_CAP must be set + if flags.mac_cap() == 0 && flags.encrypt_cap() == 0 { + return false; + } + } + if flags.mut_auth_cap() == 1 { + if flags.encap_cap() == 0 { + return false; + } + } + if flags.handshake_in_the_clear_cap() == 1 { + if flags.key_ex_cap() == 0 { + return false; + } + } + if flags.pub_key_id_cap() == 1 { + if flags.cert_cap() == 1 { + return false; + } + } + true +} + +/// Check flags to be compatible with version 1.2 +/// +/// Checks that all flags known to v1.2 have valid values. +/// Reserved fields are ignored. +fn check_flags_v12(flags: &CapabilityFlags) -> bool { + // Check for reserved values + if flags.meas_cap() == 0b11 { + return false; + } + if flags.psk_cap() == 0b11 { + return false; + } + // Check for conditionally needed flags + if flags.encrypt_cap() == 1 { + // One or more of MAC_CAP or KEY_EX_CAP must be set + if flags.mac_cap() == 0 && flags.key_ex_cap() == 0 { + return false; + } + } + if flags.mac_cap() == 1 { + // One or more of PSK_CAP or KEY_EX_CAP must be set + if flags.psk_cap() == 0 && flags.key_ex_cap() == 0 { + return false; + } + } + if flags.key_ex_cap() == 1 { + // One or more of MAC_CAP or ENCRYPT_CAP must be set + if flags.mac_cap() == 0 && flags.encrypt_cap() == 0 { + return false; + } + } + if flags.psk_cap() == 1 { + // One or more of MAC_CAP or ENCRYPT_CAP must be set + if flags.mac_cap() == 0 && flags.encrypt_cap() == 0 { + return false; + } + } + if flags.mut_auth_cap() == 1 { + if flags.encap_cap() == 0 { + return false; + } + } + if flags.handshake_in_the_clear_cap() == 1 { + if flags.key_ex_cap() == 0 { + return false; + } + } + if flags.pub_key_id_cap() == 1 { + // In this case, CERT_CAP and ALIAS_CERT_CAP of the responder + // shall be 0. + if flags.cert_cap() == 1 || flags.alias_cert_cap() == 1 { + return false; + } + } + if flags.csr_cap() == 1 { + if flags.set_certificate_cap() == 0 { + return false; + } + } + if flags.cert_install_reset_cap() == 1 { + // If this bit is set, CSR_CAP and/or SET_CERT_CAP shall be set. + if flags.csr_cap() == 0 && flags.set_certificate_cap() == 0 { + return false; + } + } + true +} + +/// Check flags to be compatible with version 1.3 +/// +/// Checks that all flags known to v1.3 have valid values. +/// Reserved fields are ignored. +fn check_flags_v13(flags: &CapabilityFlags) -> bool { + // Check for reserved values + if flags.meas_cap() == 0b11 { + return false; + } + if flags.psk_cap() == 0b11 { + return false; + } + if flags.ep_info_cap() == 0b11 { + return false; + } + // Check for conditionally needed flags + if flags.encrypt_cap() == 1 { + // One or more of MAC_CAP or KEY_EX_CAP shall be set + if flags.mac_cap() == 0 && flags.key_ex_cap() == 0 { + return false; + } + } + if flags.mac_cap() == 1 { + // One or more of PSK_CAP or KEY_EX_CAP shall be set + if flags.psk_cap() == 0 && flags.key_ex_cap() == 0 { + return false; + } + } + if flags.key_ex_cap() == 1 { + // One or more of MAC_CAP or ENCRYPT_CAP shall be set + if flags.mac_cap() == 0 && flags.encrypt_cap() == 0 { + return false; + } + } + if flags.psk_cap() == 1 { + // One or more of MAC_CAP or ENCRYPT_CAP shall be set + if flags.mac_cap() == 0 && flags.encrypt_cap() == 0 { + return false; + } + } + if flags.mut_auth_cap() == 1 { + if flags.encap_cap() == 0 { + return false; + } + } + if flags.handshake_in_the_clear_cap() == 1 { + if flags.key_ex_cap() == 0 { + return false; + } + } + if flags.pub_key_id_cap() == 1 { + // In this case, CERT_CAP and ALIAS_CERT_CAP and MULTI_KEY_CAP of the responder + // shall be 0. + if flags.cert_cap() == 1 || flags.alias_cert_cap() == 1 || flags.multi_key_cap() == 1 { + return false; + } + } + if flags.csr_cap() == 1 { + if flags.set_certificate_cap() == 0 { + return false; + } + } + if flags.cert_install_reset_cap() == 1 { + // If this bit is set, SET_CERT_CAP shall be set and CSR_CAP can be set. + // Note: This was changed. In v1.2 one of both was required + if flags.set_certificate_cap() == 0 { + return false; + } + } + if flags.multi_key_cap() == 1 { + if flags.get_key_pair_info_cap() == 0 { + return false; + } + } + true +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::{ + protocol::{CapabilityFlags, MAX_MCTP_SPDM_MSG_SIZE}, + test::*, + }; + + #[test] + fn test_handle_capabilities_response_happy_path() { + let versions = versions_default(); + let mut stack = MockResources::new(); + let algorithms = crate::protocol::LocalDeviceAlgorithms::default(); + let mut context = create_context(&mut stack, &versions, algorithms); + + context + .state + .connection_info + .set_version_number(SpdmVersion::V12); + context + .state + .connection_info + .set_state(crate::state::ConnectionState::AfterVersion); + + let header = SpdmMsgHdr::new(SpdmVersion::V12, crate::protocol::ReqRespCode::Capabilities); + + let mut msg_buf = [0; MAX_MCTP_SPDM_MSG_SIZE]; + let mut msg = MessageBuf::new(&mut msg_buf); + let mut len = 0; + let cap_base = CapabilitiesBase::default(); + len += cap_base.encode(&mut msg).unwrap(); + let cap_10 = Capabilities::new(10, CapabilityFlags::default()); + len += cap_10.encode(&mut msg).unwrap(); + let cap_12 = GetCapabilitiesV12 { + data_transfer_size: crate::protocol::MIN_DATA_TRANSFER_SIZE_V12, + max_spdm_msg_size: crate::protocol::MIN_DATA_TRANSFER_SIZE_V12, + }; + len += cap_12.encode(&mut msg).unwrap(); + msg.push_data(len).unwrap(); + + handle_capabilities_response(&mut context, header, &mut msg) + .expect("Failed to handle capabilities response"); + } + + #[test] + fn test_handle_capabilities_response_error_cases() { + let versions = versions_default(); + let mut stack = MockResources::new(); + let algorithms = crate::protocol::LocalDeviceAlgorithms::default(); + let mut context = create_context(&mut stack, &versions, algorithms); + + context + .state + .connection_info + .set_version_number(SpdmVersion::V13); + context + .state + .connection_info + .set_state(crate::state::ConnectionState::AfterVersion); + + let header = SpdmMsgHdr::new(SpdmVersion::V13, crate::protocol::ReqRespCode::Capabilities); + + // Encode invalid MEAS_CAP flag + let mut msg_buf = [0; MAX_MCTP_SPDM_MSG_SIZE]; + let cap_base = CapabilitiesBase::default(); + let mut cap_flags = CapabilityFlags::default(); + cap_flags.set_meas_cap(0b11); // 0x11 is reserved + let cap_12 = CapabilitiesV12::default(); + let mut msg = prepare_response(&mut msg_buf, cap_base, cap_flags, 10, cap_12); + + let res = handle_capabilities_response(&mut context, header.clone(), &mut msg); + if let Err((_, e)) = res { + assert_eq!( + e, + CommandError::ErrorCode(ErrorCode::InvalidPolicy), + "Expected invalid policy error, got {e:?}" + ); + } else { + panic!("Expected invalid policy error, got OK(())") + } + + // Test invalid v1.2 fields + let mut msg_buf = [0; MAX_MCTP_SPDM_MSG_SIZE]; + let cap_base = CapabilitiesBase::default(); + let cap_flags = CapabilityFlags::default(); + let cap_12 = CapabilitiesV12 { + data_transfer_size: crate::protocol::MIN_DATA_TRANSFER_SIZE_V12 - 1, + max_spdm_msg_size: crate::protocol::MIN_DATA_TRANSFER_SIZE_V12, + }; + let mut msg = prepare_response(&mut msg_buf, cap_base, cap_flags, 10, cap_12); + + let res = handle_capabilities_response(&mut context, header.clone(), &mut msg); + if let Err((_, e)) = res { + assert_eq!( + e, + CommandError::InvalidResponse, + "Expected invalid response error, got {e:?}" + ); + } else { + panic!("Expected invalid response error, got OK(())") + } + + let mut msg_buf = [0; MAX_MCTP_SPDM_MSG_SIZE]; + let cap_base = CapabilitiesBase::default(); + let cap_flags = CapabilityFlags::default(); + let cap_12 = CapabilitiesV12 { + data_transfer_size: crate::protocol::MIN_DATA_TRANSFER_SIZE_V12, + max_spdm_msg_size: crate::protocol::MIN_DATA_TRANSFER_SIZE_V12 - 1, + }; + let mut msg = prepare_response(&mut msg_buf, cap_base, cap_flags, 10, cap_12); + + let res = handle_capabilities_response(&mut context, header, &mut msg); + if let Err((_, e)) = res { + assert_eq!( + e, + CommandError::InvalidResponse, + "Expected invalid response error, got {e:?}" + ); + } else { + panic!("Expected invalid response error, got OK(())") + } + } + + fn prepare_response<'a>( + buf: &'a mut [u8], + cap_base: CapabilitiesBase, + cap_flags: CapabilityFlags, + ct_exp: u8, + cap_12: CapabilitiesV12, + ) -> MessageBuf<'a> { + let mut msg = MessageBuf::new(buf); + let mut len = 0; + + len += cap_base.encode(&mut msg).unwrap(); + len += Capabilities::new(ct_exp, cap_flags) + .encode(&mut msg) + .unwrap(); + len += cap_12.encode(&mut msg).unwrap(); + + msg.push_data(len).unwrap(); + + msg + } +} diff --git a/src/commands/capabilities_rsp.rs b/src/commands/capabilities/response.rs similarity index 84% rename from src/commands/capabilities_rsp.rs rename to src/commands/capabilities/response.rs index 662bec3..8089a2c 100644 --- a/src/commands/capabilities_rsp.rs +++ b/src/commands/capabilities/response.rs @@ -1,145 +1,24 @@ -// Licensed under the Apache-2.0 license -use crate::codec::{Codec, CommonCodec, MessageBuf}; +// Copyright 2025 +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +use super::*; +use crate::codec::{Codec, MessageBuf}; use crate::commands::error_rsp::ErrorCode; use crate::context::SpdmContext; use crate::error::{CommandError, CommandResult}; use crate::protocol::*; use crate::state::ConnectionState; use crate::transcript::TranscriptContext; -use zerocopy::{FromBytes, Immutable, IntoBytes}; - -#[derive(IntoBytes, FromBytes, Immutable, Default)] -#[repr(C)] -pub(crate) struct GetCapabilitiesBase { - param1: u8, - param2: u8, -} - -impl CommonCodec for GetCapabilitiesBase {} - -#[derive(IntoBytes, FromBytes, Immutable, Default)] -#[repr(C, packed)] -#[allow(dead_code)] -pub(crate) struct GetCapabilitiesV11 { - reserved: u8, - ct_exponent: u8, - reserved2: u8, - reserved3: u8, - flags: CapabilityFlags, -} - -impl GetCapabilitiesV11 { - pub fn new(ct_exponent: u8, flags: CapabilityFlags) -> Self { - Self { - reserved: 0, - ct_exponent, - reserved2: 0, - reserved3: 0, - flags, - } - } -} - -impl CommonCodec for GetCapabilitiesV11 {} - -#[derive(IntoBytes, FromBytes, Immutable)] -#[repr(C, packed)] -pub(crate) struct GetCapabilitiesV12 { - data_transfer_size: u32, - max_spdm_msg_size: u32, -} - -impl CommonCodec for GetCapabilitiesV12 {} - -fn req_flag_compatible(version: SpdmVersion, flags: &CapabilityFlags) -> bool { - // Checks common to 1.1 and higher - if version >= SpdmVersion::V11 { - // Illegal to return reserved values (2 and 3) - if flags.psk_cap() >= PskCapability::PskWithContext as u8 { - return false; - } - - // Checks that originate from key exchange capabilities - if flags.key_ex_cap() == 1 || flags.psk_cap() != PskCapability::NoPsk as u8 { - if flags.mac_cap() == 0 && flags.encrypt_cap() == 0 { - return false; - } - } else { - if flags.mac_cap() == 1 - || flags.encrypt_cap() == 1 - || flags.handshake_in_the_clear_cap() == 1 - || flags.hbeat_cap() == 1 - || flags.key_upd_cap() == 1 - { - return false; - } - - if version >= SpdmVersion::V13 && flags.event_cap() == 1 { - return false; - } - } - - if flags.key_ex_cap() == 0 - && flags.psk_cap() == PskCapability::PskWithNoContext as u8 - && flags.handshake_in_the_clear_cap() == 1 - { - return false; - } - - // Checks that originate from certificate or public key capabilities - if flags.cert_cap() == 1 || flags.pub_key_id_cap() == 1 { - // Certificate capabilities and public key capabilities can not both be set - if flags.cert_cap() == 1 && flags.pub_key_id_cap() == 1 { - return false; - } - - if flags.chal_cap() == 0 && flags.pub_key_id_cap() == 1 { - return false; - } - } else { - // If certificates or public keys are not enabled then these capabilities are not allowed - if flags.chal_cap() == 1 || flags.mut_auth_cap() == 1 { - return false; - } - - if version >= SpdmVersion::V13 - && flags.ep_info_cap() == EpInfoCapability::EpInfoWithSignature as u8 - { - return false; - } - } - - // Checks that originate from mutual authentication capabilities - if flags.mut_auth_cap() == 1 { - // Mutual authentication with asymmetric keys can only occur through the basic mutual - // authentication flow (CHAL_CAP == 1) or the session-based mutual authentication flow - // (KEY_EX_CAP == 1) - if flags.cert_cap() == 0 && flags.pub_key_id_cap() == 0 { - return false; - } - } - } - - // Checks specific to 1.1 - if version == SpdmVersion::V11 && flags.mut_auth_cap() == 1 && flags.encap_cap() == 0 { - return false; - } - - // Checks specific to 1.3 and higher - if version >= SpdmVersion::V13 { - // Illegal to return reserved values - if flags.ep_info_cap() == EpInfoCapability::Reserved as u8 || flags.multi_key_cap() == 3 { - return false; - } - - // Check multi_key_cap and pub_key_id_cap - if flags.multi_key_cap() != 0 && flags.pub_key_id_cap() == 1 { - return false; - } - } - - true -} fn process_get_capabilities<'a>( ctx: &mut SpdmContext<'a>, @@ -180,7 +59,7 @@ fn process_get_capabilities<'a>( })?; let flags = req_11.flags; - if !req_flag_compatible(version, &flags) { + if !req_flags_compatible(version, &flags) { Err(ctx.generate_error_response(req_payload, ErrorCode::InvalidRequest, 0, None))?; } @@ -208,6 +87,13 @@ fn process_get_capabilities<'a>( if flags.chunk_cap() == 0 && data_transfer_size != max_spdm_msg_size { Err(ctx.generate_error_response(req_payload, ErrorCode::InvalidRequest, 0, None))?; } + + // If the GET_CAPABILITIES request sets Bit 0 of Param1 to a value of 1 and the + // Responder does not support the Large SPDM message transfer mechanism ( CHUNK_CAP=0 ), + // the Responder shall send an ERROR message of ErrorCode=InvalidRequest + if base_req.param1 & 0b00000001 != 0 && flags.chunk_cap() == 0 { + Err(ctx.generate_error_response(req_payload, ErrorCode::InvalidRequest, 0, None))?; + } } if version >= SpdmVersion::V11 { @@ -223,6 +109,7 @@ fn process_get_capabilities<'a>( flags: req_11.flags, data_transfer_size, max_spdm_msg_size, + include_supported_algorithms: (base_req.param1 & 0b00000100) != 0, }; ctx.state @@ -306,3 +193,107 @@ pub(crate) fn handle_get_capabilities<'a>( .set_state(ConnectionState::AfterCapabilities); Ok(()) } + +/// Checks if the request capability flags are compatible with the SPDM version +///# Arguments +/// - `version`: SPDM version +/// - `flags`: Capability flags from the request +/// +/// # Returns +/// - true if compatible +/// - false if incompatible +fn req_flags_compatible(version: SpdmVersion, flags: &CapabilityFlags) -> bool { + // Checks specific to 1.1 + if version == SpdmVersion::V11 && flags.mut_auth_cap() == 1 && flags.encap_cap() == 0 { + return false; + } + + // Check if MEAS_CAP is valid + // 0b11 is reserved + if flags.meas_cap() == 0b11 { + return false; + } + + // Checks common to 1.1 and higher + if version >= SpdmVersion::V11 { + // Illegal to return reserved values (2 and 3) + if flags.psk_cap() >= PskCapability::PskWithContext as u8 { + return false; + } + + // Checks that originate from key exchange capabilities + if flags.key_ex_cap() == 1 || flags.psk_cap() != PskCapability::NoPsk as u8 { + if flags.mac_cap() == 0 && flags.encrypt_cap() == 0 { + return false; + } + } else { + if flags.mac_cap() == 1 + || flags.encrypt_cap() == 1 + || flags.handshake_in_the_clear_cap() == 1 + || flags.hbeat_cap() == 1 + || flags.key_upd_cap() == 1 + { + return false; + } + + if version >= SpdmVersion::V13 && flags.event_cap() == 1 { + return false; + } + } + + if flags.key_ex_cap() == 0 + && flags.psk_cap() == PskCapability::PskWithNoContext as u8 + && flags.handshake_in_the_clear_cap() == 1 + { + return false; + } + + // Checks that originate from certificate or public key capabilities + if flags.cert_cap() == 1 || flags.pub_key_id_cap() == 1 { + // Certificate capabilities and public key capabilities can not both be set + if flags.cert_cap() == 1 && flags.pub_key_id_cap() == 1 { + return false; + } + + if flags.chal_cap() == 0 && flags.pub_key_id_cap() == 1 { + return false; + } + } else { + // If certificates or public keys are not enabled then these capabilities are not allowed + if flags.chal_cap() == 1 || flags.mut_auth_cap() == 1 { + return false; + } + + if version >= SpdmVersion::V13 + && flags.ep_info_cap() == EpInfoCapability::EpInfoWithSignature as u8 + { + return false; + } + } + + // Checks that originate from mutual authentication capabilities + if flags.mut_auth_cap() == 1 { + // Mutual authentication with asymmetric keys can only occur through the basic mutual + // authentication flow (CHAL_CAP == 1) or the session-based mutual authentication flow + // (KEY_EX_CAP == 1) + if flags.cert_cap() == 0 && flags.pub_key_id_cap() == 0 { + return false; + } + } + } + + // Checks specific to 1.3 and higher + if version >= SpdmVersion::V13 { + // Illegal to return reserved values + if flags.ep_info_cap() == EpInfoCapability::Reserved as u8 || flags.multi_key_cap() == 3 { + return false; + } + + // Check multi_key_cap and pub_key_id_cap + if flags.multi_key_cap() != 0 && flags.pub_key_id_cap() == 1 { + return false; + } + } + + true +} diff --git a/src/commands/certificate/mod.rs b/src/commands/certificate/mod.rs new file mode 100644 index 0000000..cd2a076 --- /dev/null +++ b/src/commands/certificate/mod.rs @@ -0,0 +1,138 @@ +// Copyright 2025 +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +pub mod request; +pub mod response; + +pub(crate) use response::*; + +use crate::cert_store::SpdmCertStore; +use crate::codec::{CommonCodec, MessageBuf}; +use crate::error::{CommandError, CommandResult}; +use crate::protocol::*; +use bitfield::bitfield; +use zerocopy::{FromBytes, Immutable, IntoBytes}; + +#[derive(FromBytes, IntoBytes, Immutable)] +#[repr(C)] +pub struct GetCertificateReq { + pub slot_id: SlotId, + pub param2: CertificateReqAttributes, + pub offset: u16, + pub length: u16, + // TODO: v1.4.0 has two additional fields, LargeOffset and LargeLength, and a new SlotId field. + // Strangely they are attributed to v1.3.0 in the Changelog though... +} + +bitfield! { + #[derive(FromBytes, IntoBytes, Immutable)] + #[repr(C)] + pub struct SlotId(u8); + impl Debug; + u8; + pub slot_id, set_slot_id: 3,0; + reserved, _: 7,4; +} + +bitfield! { + #[derive(FromBytes, IntoBytes, Immutable)] + #[repr(C)] + pub struct CertificateReqAttributes(u8); + impl Debug; + u8; + pub slot_size_requested, set_slot_size_requested: 0,0; + reserved, _: 7,1; +} + +impl CommonCodec for GetCertificateReq {} + +#[derive(IntoBytes, FromBytes, Immutable)] +#[repr(C, packed)] +pub struct CertificateRespCommon { + pub slot_id: SlotId, + pub param2: CertificateRespAttributes, + pub portion_length: u16, + pub remainder_length: u16, +} + +impl CommonCodec for CertificateRespCommon {} + +impl CertificateRespCommon { + pub fn new( + slot_id: SlotId, + param2: CertificateRespAttributes, + portion_length: u16, + remainder_length: u16, + ) -> Self { + Self { + slot_id, + param2, + portion_length, + remainder_length, + } + } +} + +bitfield! { + #[derive(FromBytes, IntoBytes, Immutable, Default)] + #[repr(C)] + pub struct CertificateRespAttributes(u8); + impl Debug; + u8; + pub certificate_info, set_certificate_info: 2,0; + reserved, _: 7,3; +} + +pub(crate) fn encode_certchain_metadata( + cert_store: &mut dyn SpdmCertStore, + total_certchain_len: u16, + slot_id: u8, + asym_algo: AsymAlgo, + offset: usize, + length: usize, + rsp: &mut MessageBuf<'_>, +) -> CommandResult { + let mut certchain_metadata = [0u8; SPDM_CERT_CHAIN_METADATA_LEN as usize]; + + // Read the cert chain header first + // Currently only cert chains with length <= `u16::MAX` are supported. + // (So this should never fail.) + let cert_chain_hdr = SpdmCertChainHeader::new(total_certchain_len as u32, SpdmVersion::V12) + .map_err(|_| (false, CommandError::InternalError))?; + + let cert_chain_hdr_bytes = cert_chain_hdr.as_bytes(); + certchain_metadata[..cert_chain_hdr_bytes.len()].copy_from_slice(cert_chain_hdr_bytes); + + // Read the root cert hash next + let mut root_hash_buf = [0u8; SHA384_HASH_SIZE]; + cert_store + .root_cert_hash(slot_id, asym_algo, &mut root_hash_buf) + .map_err(|e| (false, CommandError::CertStore(e)))?; + certchain_metadata[cert_chain_hdr_bytes.len()..].copy_from_slice(&root_hash_buf[..]); + + let write_len = (SPDM_CERT_CHAIN_METADATA_LEN - offset as u16).min(length as u16) as usize; + + rsp.put_data(write_len) + .map_err(|e| (false, CommandError::Codec(e)))?; + + let cert_portion = rsp + .data_mut(write_len) + .map_err(|e| (false, CommandError::Codec(e)))?; + + cert_portion[..write_len].copy_from_slice(&certchain_metadata[offset..offset + write_len]); + rsp.pull_data(write_len) + .map_err(|e| (false, CommandError::Codec(e)))?; + + Ok(write_len) +} diff --git a/src/commands/certificate/request.rs b/src/commands/certificate/request.rs new file mode 100644 index 0000000..17b8263 --- /dev/null +++ b/src/commands/certificate/request.rs @@ -0,0 +1,250 @@ +// Copyright 2025 +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use crate::codec::{Codec, MessageBuf}; +use crate::commands::certificate::{ + CertificateReqAttributes, CertificateRespCommon, GetCertificateReq, SlotId, +}; +use crate::context::SpdmContext; +use crate::error::{CommandError, CommandResult}; +use crate::protocol::{CertModel, ReqRespCode, SpdmMsgHdr, SpdmVersion}; +use crate::state::{ConnectionState, GetCertificateState}; +use crate::transcript::TranscriptContext; + +/// Generate a GET_CERTIFICATE request +/// +/// If the state is `DuringCertificate` the following parameters will be ignored +/// and instead be calculated from the state: +/// `slot_id`, `offset`. +/// +/// # Arguments +/// * `ctx`: The SPDM context +/// * `req_buf`: Buffer to write the request into +/// * `slot_id`: Certificate slot identifier (0-7) +/// * `offset`: Byte offset into the certificate chain +/// * `length`: Number of bytes to request +/// * `slot_size_requested`: If true, request the slot size instead of certificate data (SPDM v1.3+) +/// +/// # Returns +/// - () on success +/// - [CommandError] on failure +/// +/// # Connection State Requirements +/// - Connection state must be >= AlgorithmsNegotiated +/// - Updates the state to `DuringCertificate` (only if `slot_size_requested` == `false`) +/// +/// # Transcript +/// - Appends request to the transcript context +pub fn generate_get_certificate<'a>( + ctx: &mut SpdmContext<'a>, + req_buf: &mut MessageBuf<'a>, + slot_id: u8, + offset: u16, + length: u16, + slot_size_requested: bool, +) -> CommandResult<()> { + // Validate connection state - algorithms must be negotiated first + if ctx.state.connection_info.state() < ConnectionState::AlgorithmsNegotiated { + return Err((false, CommandError::UnsupportedRequest)); + } + + let state = match ctx.state.connection_info.state() { + ConnectionState::DuringCertificate(s) => s, + _ => GetCertificateState { + current_slot_id: slot_id, + offset, + ..Default::default() + }, + }; + if !slot_size_requested { + ctx.state + .connection_info + .set_state(ConnectionState::DuringCertificate(state)); + } + + // Get connection version + let connection_version = ctx.state.connection_info.version_number(); + + // Create and encode SPDM message header + let spdm_hdr = SpdmMsgHdr::new(connection_version, ReqRespCode::GetCertificate); + let mut payload_len = spdm_hdr + .encode(req_buf) + .map_err(|e| (false, CommandError::Codec(e)))?; + + // Create SlotId bitfield + let mut slot_id_field = SlotId(0); + slot_id_field.set_slot_id(state.current_slot_id); + + // Create CertificateReqAttributes bitfield + let mut req_attributes = CertificateReqAttributes(0); + if slot_size_requested { + req_attributes.set_slot_size_requested(1); + } + + // Create GET_CERTIFICATE request payload + let get_cert_req = GetCertificateReq { + slot_id: slot_id_field, + param2: req_attributes, + offset: state.offset, + length, + }; + + // Encode request payload + payload_len += get_cert_req + .encode(req_buf) + .map_err(|e| (false, CommandError::Codec(e)))?; + + // Finalize message by pushing total payload length + req_buf + .push_data(payload_len) + .map_err(|_| (false, CommandError::BufferTooSmall))?; + + // Message M1.B = Concatenate(GET_DIGESTS, DIGESTS, GET_CERTIFICATE, CERTIFICATE) + ctx.append_message_to_transcript(req_buf, TranscriptContext::M1) +} + +/// Process CERTIFICATE response payload (private helper) +/// +/// # Arguments +/// * `ctx`: The SPDM context +/// * `spdm_hdr`: The SPDM message header from the response +/// * `resp_payload`: Buffer containing the response payload +/// +/// # Returns +/// - () on success +/// - [CommandError] on failure +/// +/// # Current Implementation +/// - Validates version matches connection version +/// - Decodes CertificateRespCommon structure +/// - Validates returned slot_id against expected slot_id +/// - Reads certificate portion data (validates buffer size) +/// - Stores certificate portion in peer cert. store +/// - Updates state +/// +/// # Future Extensions +/// - TODO: Add support for SlotSizeRequested responses +fn process_certificate<'a>( + ctx: &mut SpdmContext<'a>, + spdm_hdr: SpdmMsgHdr, + resp_payload: &mut MessageBuf<'a>, +) -> CommandResult<()> { + // Validate version matches connection version + let connection_version = ctx.state.connection_info.version_number(); + if spdm_hdr.version().ok() != Some(connection_version) { + return Err((true, CommandError::InvalidResponse)); + } + + let ConnectionState::DuringCertificate(mut state) = ctx.state.connection_info.state() else { + // TODO: Add support for SlotSizeRequested + return Err((false, CommandError::InvalidState)); + }; + + // Decode CertificateRespCommon structure + let cert_resp = + CertificateRespCommon::decode(resp_payload).map_err(|e| (true, CommandError::Codec(e)))?; + + // Check slot_id + let slot_id = cert_resp.slot_id.slot_id(); + if slot_id != state.current_slot_id { + return Err((true, CommandError::InvalidResponse)); + } + + // Decode CertModel from param2 + // Seems to be available since v1.3 + if connection_version >= SpdmVersion::V13 { + // Fails if a unknown CertModel is send + // (so we consider this to be a bug on the responder side). + let _cert_info: CertModel = cert_resp + .param2 + .certificate_info() + .try_into() + .map_err(|_| (true, CommandError::InvalidResponse))?; + } + + let portion_len = cert_resp.portion_length; + + // Read the certificate portion from the payload (if any) + if portion_len > 0 { + // Validate that the buffer contains the expected certificate data + let cert_data = resp_payload + .data(portion_len as usize) + .map_err(|e| (true, CommandError::Codec(e)))?; + + let Some(cert_store) = ctx.state.peer_cert_store.as_deref_mut() else { + return Err((true, CommandError::InvalidState)); + }; + + match cert_store.assemble(state.current_slot_id, cert_data) { + Ok(_s) => { + // TODO: match s against remainder length + } + Err(e) => return Err((true, CommandError::CertStore(e))), + } + + // Advance the buffer pointer past the certificate data + resp_payload + .pull_data(portion_len as usize) + .map_err(|e| (true, CommandError::Codec(e)))?; + } + + state.offset += portion_len; + state.remainder_length = Some(cert_resp.remainder_length); + + if cert_resp.remainder_length > 0 { + ctx.state + .connection_info + .set_state(ConnectionState::DuringCertificate(state)); + } else { + ctx.state + .connection_info + .set_state(ConnectionState::AfterCertificate); + } + + Ok(()) +} + +/// Requester function handling the parsing of the CERTIFICATE response sent by the Responder. +/// +/// # Arguments +/// * `ctx`: The SPDM context +/// * `resp_header`: The SPDM message header from the response +/// * `resp`: Buffer containing the complete response message +/// +/// # Returns +/// - () on success +/// - [CommandError] on failure +/// +/// # Connection State +/// - Requires: ConnectionState >= AlgorithmsNegotiated +/// - Updates: ConnectionState to DuringCertificate or AfterCertificate +/// +/// # Transcript +/// - Appends response to TranscriptContext::M1 +pub(crate) fn handle_certificate_response<'a>( + ctx: &mut SpdmContext<'a>, + resp_header: SpdmMsgHdr, + resp: &mut MessageBuf<'a>, +) -> CommandResult<()> { + // Validate connection state - algorithms must be negotiated + if ctx.state.connection_info.state() < ConnectionState::AlgorithmsNegotiated { + return Err((true, CommandError::UnsupportedResponse)); + } + + // Process the certificate response payload + process_certificate(ctx, resp_header, resp)?; + + // Append response to transcript (M1 context for certificate exchange) + ctx.append_message_to_transcript(resp, TranscriptContext::M1) +} diff --git a/src/commands/certificate_rsp.rs b/src/commands/certificate/response.rs similarity index 69% rename from src/commands/certificate_rsp.rs rename to src/commands/certificate/response.rs index a14f39e..f0bcc0f 100644 --- a/src/commands/certificate_rsp.rs +++ b/src/commands/certificate/response.rs @@ -1,125 +1,29 @@ -// Licensed under the Apache-2.0 license - -use crate::cert_store::{cert_slot_mask, SpdmCertStore, MAX_CERT_SLOTS_SUPPORTED}; -use crate::codec::{Codec, CommonCodec, MessageBuf}; +// Copyright 2025 +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use crate::cert_store::{cert_slot_mask, MAX_CERT_SLOTS_SUPPORTED}; +use crate::codec::{Codec, MessageBuf}; +use crate::commands::certificate::{ + encode_certchain_metadata, CertificateRespAttributes, CertificateRespCommon, GetCertificateReq, + SlotId, +}; use crate::commands::error_rsp::ErrorCode; use crate::context::SpdmContext; use crate::error::{CommandError, CommandResult}; use crate::protocol::*; use crate::state::ConnectionState; use crate::transcript::TranscriptContext; -use bitfield::bitfield; -use zerocopy::{FromBytes, Immutable, IntoBytes}; - -#[derive(FromBytes, IntoBytes, Immutable)] -#[repr(C)] -pub struct GetCertificateReq { - pub slot_id: SlotId, - pub param2: CertificateReqAttributes, - pub offset: u16, - pub length: u16, -} - -bitfield! { - #[derive(FromBytes, IntoBytes, Immutable)] - #[repr(C)] - pub struct SlotId(u8); - impl Debug; - u8; - pub slot_id, set_slot_id: 3,0; - reserved, _: 7,4; -} - -bitfield! { - #[derive(FromBytes, IntoBytes, Immutable)] - #[repr(C)] - pub struct CertificateReqAttributes(u8); - impl Debug; - u8; - pub slot_size_requested, set_slot_size_requested: 0,0; - reserved, _: 7,1; -} - -impl CommonCodec for GetCertificateReq {} - -#[derive(IntoBytes, FromBytes, Immutable)] -#[repr(C, packed)] -pub struct CertificateRespCommon { - pub slot_id: SlotId, - pub param2: CertificateRespAttributes, - pub portion_length: u16, - pub remainder_length: u16, -} - -impl CommonCodec for CertificateRespCommon {} - -impl CertificateRespCommon { - pub fn new( - slot_id: SlotId, - param2: CertificateRespAttributes, - portion_length: u16, - remainder_length: u16, - ) -> Self { - Self { - slot_id, - param2, - portion_length, - remainder_length, - } - } -} - -bitfield! { - #[derive(FromBytes, IntoBytes, Immutable, Default)] - #[repr(C)] - pub struct CertificateRespAttributes(u8); - impl Debug; - u8; - pub certificate_info, set_certificate_info: 2,0; - reserved, _: 7,3; -} - -fn encode_certchain_metadata( - cert_store: &mut dyn SpdmCertStore, - total_certchain_len: u16, - slot_id: u8, - asym_algo: AsymAlgo, - offset: usize, - length: usize, - rsp: &mut MessageBuf<'_>, -) -> CommandResult { - let mut certchain_metadata = [0u8; SPDM_CERT_CHAIN_METADATA_LEN as usize]; - - // Read the cert chain header first - let cert_chain_hdr = SpdmCertChainHeader { - length: total_certchain_len, - reserved: 0, - }; - let cert_chain_hdr_bytes = cert_chain_hdr.as_bytes(); - certchain_metadata[..cert_chain_hdr_bytes.len()].copy_from_slice(cert_chain_hdr_bytes); - - // Read the root cert hash next - let mut root_hash_buf = [0u8; SHA384_HASH_SIZE]; - cert_store - .root_cert_hash(slot_id, asym_algo, &mut root_hash_buf) - .map_err(|e| (false, CommandError::CertStore(e)))?; - certchain_metadata[cert_chain_hdr_bytes.len()..].copy_from_slice(&root_hash_buf[..]); - - let write_len = (SPDM_CERT_CHAIN_METADATA_LEN - offset as u16).min(length as u16) as usize; - - rsp.put_data(write_len) - .map_err(|e| (false, CommandError::Codec(e)))?; - - let cert_portion = rsp - .data_mut(write_len) - .map_err(|e| (false, CommandError::Codec(e)))?; - - cert_portion[..write_len].copy_from_slice(&certchain_metadata[offset..offset + write_len]); - rsp.pull_data(write_len) - .map_err(|e| (false, CommandError::Codec(e)))?; - - Ok(write_len) -} fn generate_certificate_response<'a>( ctx: &mut SpdmContext<'a>, diff --git a/src/commands/challenge/mod.rs b/src/commands/challenge/mod.rs new file mode 100644 index 0000000..ff8a7b7 --- /dev/null +++ b/src/commands/challenge/mod.rs @@ -0,0 +1,161 @@ +// Copyright 2025 +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use crate::codec::CommonCodec; +use crate::protocol::SHA384_HASH_SIZE; +use bitfield::bitfield; +use zerocopy::{FromBytes, Immutable, IntoBytes}; + +pub mod request; +pub mod response; + +pub(crate) use request::*; +pub(crate) use response::*; + +const NONCE_LEN: usize = 32; +const CONTEXT_LEN: usize = 8; +const OPAQUE_DATA_MAX: usize = 1024; + +/// 0x02..0xFE are reserved +#[derive(Debug, PartialEq, Clone)] +#[repr(u8)] +pub enum MeasurementSummaryHashType { + None = 0x00, + /// # Trusted Computing Base + /// + /// Set of all hardware, firmware, and/or software components that are critical + /// to its security, in the sense that bugs or vulnerabilities occurring inside + /// the TCB might jeopardize the security properties of the entire system. + Tcb = 0x01, + All = 0xFF, +} + +impl TryFrom for MeasurementSummaryHashType { + type Error = (); + fn try_from(value: u8) -> Result { + match value { + 0x00 => Ok(Self::None), + 0x01 => Ok(Self::Tcb), + 0xFF => Ok(Self::All), + _ => Err(()), + } + } +} + +#[derive(FromBytes, IntoBytes, Immutable)] +#[repr(C)] +/// CHALLENGE request message base +/// +/// # Version specific fields for CHALLENGE: +/// Following fields have to be appended, depending on the SPDM version. +/// ## >= v1.3 +/// - `Context`: 8-byte application specific context. +/// Should be all zeros if no context is provided. +struct ChallengeReq { + /// `Param1`: `SlotID` + /// + /// Slot number of the Responder certificate chain that shall be used for authentication. + /// If the public key of the Responder was provisioned to the Requester in a + /// trusted environment, the value in this field shall be 0xFF ; otherwise it + /// shall be between 0 and 7 inclusive. + slot_id: u8, + + /// `Param2`: Requested measurement summary hash + /// + /// Shall be the type of measurement summary hash requested. + measurement_hash_type: u8, + + /// The Requester should choose a random value. + nonce: [u8; NONCE_LEN], +} +impl CommonCodec for ChallengeReq {} + +impl ChallengeReq { + /// Creates a new `CHALLENGE` request message. + /// + /// # Arguments + /// + /// * `slot_id` - Slot number (0..=7) of the Responder certificate chain to use for + /// authentication, or `0xFF` if the public key was provisioned in a trusted environment. + /// Stored as a bitmask with the corresponding bit set. + /// * `measurement_hash_type` - The type of measurement summary hash requested from the + /// Responder. + /// * `nonce` - A random 32-byte value chosen by the Requester for freshness. + pub fn new( + slot_id: u8, + measurement_hash_type: MeasurementSummaryHashType, + nonce: [u8; NONCE_LEN], + ) -> Self { + Self { + slot_id, + measurement_hash_type: measurement_hash_type as u8, + nonce, + } + } +} + +#[derive(FromBytes, IntoBytes, Immutable)] +#[repr(C)] +struct ChallengeAuthRspBase { + challenge_auth_attr: ChallengeAuthAttr, + slot_mask: u8, + + /// Shall be either the hash of the certificate chain if the public key of the + /// Responder was provisioned to the Requester in a trusted environment, the + /// public key used for authentication. + /// + /// The Requester can use this value to check that the certificate chain or + /// public key matches the one requested. + cert_chain_hash: [u8; SHA384_HASH_SIZE], + + /// Shall be the Responder-selected random value + nonce: [u8; NONCE_LEN], + // Followed by: + // - MeasurementSummaryHash + // - OpaqueDataLength + // - OpaqueData + // - RequesterContext + // - Signature +} + +impl CommonCodec for ChallengeAuthRspBase {} + +impl ChallengeAuthRspBase { + /// Creates a new `ChallengeAuthRspBase` with the specified slot ID. + /// + /// # Arguments + /// + /// * `slot_id` - The slot ID Bit to be set in the response. + fn new(slot_id: u8) -> Self { + Self { + challenge_auth_attr: ChallengeAuthAttr(slot_id), + slot_mask: 1 << slot_id, + cert_chain_hash: [0; SHA384_HASH_SIZE], + nonce: [0; NONCE_LEN], + } + } +} + +bitfield! { + #[derive(FromBytes, IntoBytes, Immutable)] + #[repr(C)] + struct ChallengeAuthAttr(u8); + impl Debug; + u8; + /// Shall contain the `SlotID` in the Param1 `field` of the corresponding `CHALLENGE` request. + /// If the Responder's public key was provisioned to the Requester previously, this field shall + /// be 0xF. The Requester can use this value to check that the certificate matched what was requested. + pub slot_id, set_slot_id: 3, 0; + reserved, _: 7, 4; +} diff --git a/src/commands/challenge/request.rs b/src/commands/challenge/request.rs new file mode 100644 index 0000000..e12a892 --- /dev/null +++ b/src/commands/challenge/request.rs @@ -0,0 +1,215 @@ +// Copyright 2025 +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +use crate::codec::{encode_u8_slice, Codec, MessageBuf}; +use crate::commands::challenge::{ + ChallengeAuthRspBase, ChallengeReq, MeasurementSummaryHashType, CONTEXT_LEN, NONCE_LEN, + OPAQUE_DATA_MAX, +}; +use crate::context::SpdmContext; +use crate::error::{CommandError, CommandResult}; +use crate::protocol::*; +use crate::state::ConnectionState; +use crate::transcript::TranscriptContext; + +/// Generates an SPDM `CHALLENGE` request message. +/// +/// Constructs a [`ChallengeReq`] with the given parameters and encodes it into `buf` +/// as an SPDM message, using the negotiated version from the connection state. +/// +/// # Arguments +/// +/// * `ctx` - The SPDM context holding connection state (used to obtain the negotiated version). +/// * `buf` - The output buffer into which the encoded request message is written. +/// * `slot_id` - Slot number (`0..=7`) of the Responder certificate chain to use for +/// authentication, or `0xFF` if the public key was provisioned in a trusted environment. +/// * `measurement_hash_type` - The type of measurement summary hash requested from the +/// Responder (`None`, `Tcb`, or `All`). +/// * `nonce` - A 32-byte random value chosen by the Requester for freshness. +/// * `context` - Optional 8-byte application-specific context. Defaults to all zeros when +/// `None`, ignored for spdm versions < v1.3. +/// +/// # Errors +/// +/// Returns a [`CommandError`] if encoding the header or request body into the buffer fails. +pub fn generate_challenge_request<'a>( + ctx: &mut SpdmContext<'a>, + message_buffer: &mut MessageBuf<'a>, + slot_id: u8, + measurement_hash_type: MeasurementSummaryHashType, + nonce: [u8; NONCE_LEN], + context: Option<[u8; CONTEXT_LEN]>, +) -> CommandResult<()> { + SpdmMsgHdr::new( + ctx.state.connection_info.version_number(), + ReqRespCode::Challenge, + ) + .encode(message_buffer) + .map_err(|e| (false, CommandError::Codec(e)))?; + + ChallengeReq::new(slot_id, measurement_hash_type.clone(), nonce) + .encode(message_buffer) + .map_err(|e| (false, CommandError::Codec(e)))?; + + // Encode 8-byte context string if version >= v1.3 + if ctx.connection_info().version_number() >= SpdmVersion::V13 { + if let Some(ctx_str) = context { + encode_u8_slice(&ctx_str, message_buffer) + .map_err(|e| (true, CommandError::Codec(e)))?; + } else { + encode_u8_slice(&[0; 8], message_buffer).map_err(|e| (true, CommandError::Codec(e)))?; + } + } + + ctx.state + .peer_cert_store + .as_mut() + .unwrap() + .set_requested_msh_type(slot_id, measurement_hash_type.clone()) + .map_err(|e| (false, CommandError::CertStore(e)))?; + + // Message M1.C = Concatenate(CHALLENGE, CHALLENGE_AUTH without signature) + ctx.append_message_to_transcript(message_buffer, TranscriptContext::M1) +} + +/// Handle the challenge response and apppend the message to the transcript context. +/// See [crate::transcript::TranscriptContext::M1] for details on the transcript context used. +/// +/// # Warning +/// Contrary to the other messages, `CHALLENGE_AUTH` is **NOT** entirely parsed here. +/// The variable-length field `Signature` has to be parsed in the application. This has two reasons: +/// 1. The generate the transcript hash, the entire message, **except the signature!** +/// has to be appended to the transcript context before signature verification, as required by SPDM 1.2 and later. +/// 2. The signature verification has to be done in the application, as it requires +/// access to the public key from the responder's certificate chain (which we already verified) and the transcript hash. +pub(crate) fn handle_challenge_auth_response<'a>( + ctx: &mut SpdmContext<'a>, + spdm_hdr: SpdmMsgHdr, + resp_payload: &mut MessageBuf<'a>, +) -> CommandResult<()> { + if ctx.state.connection_info.state() < ConnectionState::AlgorithmsNegotiated { + return Err((true, CommandError::UnsupportedRequest)); + } + + if spdm_hdr.version().unwrap() != ctx.connection_info().version_number() { + return Err((true, CommandError::InvalidState)); + } + + let challenge_auth_resp_base: ChallengeAuthRspBase = + ChallengeAuthRspBase::decode(resp_payload).map_err(|e| (true, CommandError::Codec(e)))?; + + // Parse the variable length fields: + // - MeasurementSummaryHash + // - OpaqueDataLength + // - OpaqueData + // - RequesterContext + // - Signature + + // - MeasurementSummaryHash + // If the Responder does not support measurements ( MEAS_CAP=00b in its CAPABILITIES response) + // or if the requested Param2 = 0x0 , this field shall be absent. + + let param2 = ctx + .state + .peer_cert_store + .as_mut() + .unwrap() + .get_requested_msh_type(0) + .map_err(|e| (true, CommandError::CertStore(e)))?; + + let hash_size_bytes = ctx.hash.algo().hash_size(); + let mut hash = [0u8; SHA384_HASH_SIZE]; + + if challenge_auth_resp_base.slot_mask != 0 + && ctx.connection_info().peer_capabilities().flags.meas_cap() != 0 + { + // If the Responder supports both raw bit stream and digest representations + // for a given measurement index, the Responder shall use the digest form. + match param2 { + MeasurementSummaryHashType::None => {} + + // The combined hash of measurements of all measurable components + // considered to be in the TCB required to generate this response + MeasurementSummaryHashType::Tcb | MeasurementSummaryHashType::All => { + hash[..hash_size_bytes].copy_from_slice( + resp_payload + .data(hash_size_bytes) + .map_err(|e| (true, CommandError::Codec(e)))?, + ); + + resp_payload + .pull_data(hash_size_bytes) + .map_err(|e| (true, CommandError::Codec(e)))?; + } + } + } + + let opaque_data_size = { + let opaque_data_slice = resp_payload + .data(2) + .map_err(|e| (true, CommandError::Codec(e)))?; + u16::from_le_bytes([opaque_data_slice[0], opaque_data_slice[1]]) + }; + + resp_payload + .pull_data(2) + .map_err(|e| (true, CommandError::Codec(e)))?; + + // The value should not be greater than 1024 bytes + // Opaque data size 64939 exceeds maximum allowed 1024 + if opaque_data_size > OPAQUE_DATA_MAX as u16 { + return Err((true, CommandError::BufferTooSmall)); + } + + // The Responder can include Responder-specific information and/or information + // that its transport defines. If present, this field shall conform to the selected + // opaque data format in [OtherParamsSelection]. + if opaque_data_size > 0 { + let _opaque_data = resp_payload + .data(opaque_data_size as usize) + .map_err(|e| (true, CommandError::Codec(e)))?; + resp_payload + .pull_data(opaque_data_size as usize) + .map_err(|e| (true, CommandError::Codec(e)))?; + } + + // In v1.3 a 8-byte request context was added before the signature field + if ctx.connection_info().version_number() >= SpdmVersion::V13 { + // This field shall be identical to the Context field of the corresponding request message. + // TODO: compare it to the context we sent. + // See: src/protocol/common.rs [RequesterContext] + let _requester_context = resp_payload + .data(8) + .map_err(|e| (true, CommandError::Codec(e)))?; + + resp_payload + .pull_data(8) + .map_err(|e| (true, CommandError::Codec(e)))?; + } + + // We have to use this ugly hack to bring the message buffer into the right form to exclude the signature. + // This message buffer thing is totally fucked up... + // Come on, why do you have to call multiple badly named functions to remove data? + // And then there `message_data`, `data`, `total_message`, ... are you kidding me? + let tail = resp_payload.data_len(); + resp_payload + .trim(0) + .map_err(|e| (true, CommandError::Codec(e)))?; + // Append the entire message (excluding the signature) to the transcript before signature verification, as required by SPDM 1.2 and later. + ctx.append_message_to_transcript(resp_payload, TranscriptContext::M1)?; + resp_payload + .put_data(tail) + .map_err(|e| (true, CommandError::Codec(e)))?; + + Ok(()) +} diff --git a/src/commands/challenge_auth_rsp.rs b/src/commands/challenge/response.rs similarity index 84% rename from src/commands/challenge_auth_rsp.rs rename to src/commands/challenge/response.rs index 5013a0c..6de852c 100644 --- a/src/commands/challenge_auth_rsp.rs +++ b/src/commands/challenge/response.rs @@ -1,8 +1,21 @@ -// Licensed under the Apache-2.0 license +// Copyright 2025 +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. use crate::cert_store::MAX_CERT_SLOTS_SUPPORTED; -use crate::codec::{Codec, CommonCodec, MessageBuf}; -use crate::commands::algorithms_rsp::selected_measurement_specification; -use crate::commands::digests_rsp::compute_cert_chain_hash; +use crate::codec::{Codec, MessageBuf}; +use crate::commands::algorithms::selected_measurement_specification; +use crate::commands::challenge::{ChallengeAuthRspBase, ChallengeReq, MeasurementSummaryHashType}; +use crate::commands::digests::compute_cert_chain_hash; use crate::commands::error_rsp::ErrorCode; use crate::context::SpdmContext; use crate::error::{CommandError, CommandResult, PlatformError}; @@ -10,54 +23,12 @@ use crate::platform::hash::SpdmHashAlgoType; use crate::protocol::*; use crate::state::ConnectionState; use crate::transcript::TranscriptContext; -use bitfield::bitfield; -use zerocopy::{FromBytes, Immutable, IntoBytes}; - -#[derive(FromBytes, IntoBytes, Immutable)] -#[repr(C)] -struct ChallengeReqBase { - slot_id: u8, - measurement_hash_type: u8, - nonce: [u8; NONCE_LEN], -} -impl CommonCodec for ChallengeReqBase {} - -#[derive(FromBytes, IntoBytes, Immutable)] -#[repr(C)] -struct ChallengeAuthRspBase { - challenge_auth_attr: ChallengeAuthAttr, - slot_mask: u8, - cert_chain_hash: [u8; SHA384_HASH_SIZE], - nonce: [u8; NONCE_LEN], -} -impl CommonCodec for ChallengeAuthRspBase {} - -impl ChallengeAuthRspBase { - fn new(slot_id: u8) -> Self { - Self { - challenge_auth_attr: ChallengeAuthAttr(slot_id), - slot_mask: 1 << slot_id, - cert_chain_hash: [0; SHA384_HASH_SIZE], - nonce: [0; NONCE_LEN], - } - } -} - -bitfield! { - #[derive(FromBytes, IntoBytes, Immutable)] - #[repr(C)] - struct ChallengeAuthAttr(u8); - impl Debug; - u8; - pub slot_id, set_slot_id: 3, 0; - reserved, _: 7, 4; -} fn process_challenge<'a>( ctx: &mut SpdmContext<'a>, spdm_hdr: SpdmMsgHdr, req_payload: &mut MessageBuf<'a>, -) -> CommandResult<(u8, u8, Option)> { +) -> CommandResult<(u8, MeasurementSummaryHashType, Option)> { // Validate the version let connection_version = ctx.state.connection_info.version_number(); if spdm_hdr.version().ok() != Some(connection_version) { @@ -69,7 +40,7 @@ fn process_challenge<'a>( .map_err(|_| ctx.generate_error_response(req_payload, ErrorCode::Unspecified, 0, None))?; // Decode the CHALLENGE request payload - let challenge_req = ChallengeReqBase::decode(req_payload).map_err(|_| { + let challenge_req = ChallengeReq::decode(req_payload).map_err(|_| { ctx.generate_error_response(req_payload, ErrorCode::InvalidRequest, 0, None) })?; @@ -104,11 +75,12 @@ fn process_challenge<'a>( // Append the CHALLENGE request to the M1 transcript ctx.append_message_to_transcript(req_payload, TranscriptContext::M1)?; - Ok(( - challenge_req.slot_id, - challenge_req.measurement_hash_type, - requester_context, - )) + let meas_hash_type = MeasurementSummaryHashType::try_from(challenge_req.measurement_hash_type) + .map_err(|_| { + ctx.generate_error_response(req_payload, ErrorCode::InvalidRequest, 0, None) + })?; + + Ok((challenge_req.slot_id, meas_hash_type, requester_context)) } fn encode_m1_signature<'a>( @@ -205,12 +177,18 @@ fn encode_challenge_auth_rsp_base<'a>( fn encode_measurement_summary_hash<'a>( ctx: &mut SpdmContext<'a>, asym_algo: AsymAlgo, - meas_summary_hash_type: u8, + meas_summary_hash_type: MeasurementSummaryHashType, rsp: &mut MessageBuf<'a>, ) -> CommandResult { let mut meas_summary_hash = [0u8; SHA384_HASH_SIZE]; ctx.measurements - .measurement_summary_hash(ctx.evidence, ctx.hash, asym_algo, meas_summary_hash_type, &mut meas_summary_hash) + .measurement_summary_hash( + ctx.evidence, + ctx.hash, + asym_algo, + meas_summary_hash_type, + &mut meas_summary_hash, + ) .map_err(|e| (false, CommandError::Measurement(e)))?; let hash_len = meas_summary_hash.len(); @@ -239,7 +217,7 @@ fn encode_opaque_data(rsp: &mut MessageBuf<'_>) -> CommandResult { fn generate_challenge_auth_response<'a>( ctx: &mut SpdmContext<'a>, slot_id: u8, - meas_summary_hash_type: u8, + meas_summary_hash_type: MeasurementSummaryHashType, requester_context: Option, rsp: &mut MessageBuf<'a>, ) -> CommandResult<()> { @@ -260,7 +238,7 @@ fn generate_challenge_auth_response<'a>( payload_len += encode_challenge_auth_rsp_base(ctx, slot_id, asym_algo, rsp)?; // Get the measurement summary hash - if meas_summary_hash_type != 0 { + if meas_summary_hash_type != MeasurementSummaryHashType::None { payload_len += encode_measurement_summary_hash(ctx, asym_algo, meas_summary_hash_type, rsp)?; } diff --git a/src/commands/chunk_get_rsp.rs b/src/commands/chunk_get_rsp.rs index 7e3fa6c..02d5cd2 100644 --- a/src/commands/chunk_get_rsp.rs +++ b/src/commands/chunk_get_rsp.rs @@ -1,4 +1,16 @@ -// Licensed under the Apache-2.0 license +// Copyright 2025 +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. use crate::chunk_ctx::ChunkError; use crate::chunk_ctx::LargeResponse; use crate::codec::{Codec, CommonCodec, MessageBuf}; @@ -160,17 +172,16 @@ fn encode_chunk_data( match response { LargeResponse::Measurements(meas_rsp) => { // Get the chunk data from the measurements response - meas_rsp - .get_chunk( - ctx.hash, - ctx.rng, - ctx.evidence, - &mut ctx.measurements, - &mut ctx.transcript_mgr, - ctx.device_certs_store, - offset, - chunk_buf, - )?; + meas_rsp.get_chunk( + ctx.hash, + ctx.rng, + ctx.evidence, + &mut ctx.measurements, + &mut ctx.transcript_mgr, + ctx.device_certs_store, + offset, + chunk_buf, + )?; } } } else { diff --git a/src/commands/digests_rsp.rs b/src/commands/digests/mod.rs similarity index 51% rename from src/commands/digests_rsp.rs rename to src/commands/digests/mod.rs index d95365a..fc3fe07 100644 --- a/src/commands/digests_rsp.rs +++ b/src/commands/digests/mod.rs @@ -1,17 +1,33 @@ -// Licensed under the Apache-2.0 license - -use crate::cert_store::{cert_slot_mask, SpdmCertStore}; -use crate::codec::{Codec, CommonCodec, MessageBuf}; -use crate::commands::error_rsp::ErrorCode; +// Copyright 2025 +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use crate::cert_store::SpdmCertStore; +use crate::codec::{CommonCodec, MessageBuf}; use crate::context::SpdmContext; -use crate::error::{PlatformError, CommandError, CommandResult}; -use crate::protocol::*; -use crate::state::ConnectionState; -use crate::transcript::TranscriptContext; +use crate::error::{CommandError, CommandResult, PlatformError}; use crate::platform::hash::{SpdmHash, SpdmHashAlgoType}; -use core::mem::size_of; + +use crate::protocol::*; + use zerocopy::{FromBytes, Immutable, IntoBytes}; +pub mod request; +pub mod response; + +pub(crate) use request::*; +pub(crate) use response::*; + #[derive(IntoBytes, FromBytes, Immutable, Default)] #[repr(C)] pub struct GetDigestsReq { @@ -46,14 +62,12 @@ pub(crate) fn compute_cert_chain_hash( .map_err(|e| (false, CommandError::CertStore(e)))?; let cert_chain_format_len = crt_chain_len + SPDM_CERT_CHAIN_METADATA_LEN as usize; - let header = SpdmCertChainHeader { - length: cert_chain_format_len as u16, - reserved: 0, - }; + let header = SpdmCertChainHeader::new(cert_chain_format_len as u32, SpdmVersion::V13) + .map_err(|_| (false, CommandError::InternalError))?; // Length and reserved fields let header_bytes = header.as_bytes(); - + digest_fn .init(SpdmHashAlgoType::SHA384, Some(header_bytes)) .map_err(|e| (false, CommandError::Platform(PlatformError::HashError(e))))?; @@ -107,7 +121,13 @@ fn encode_cert_chain_digest( .data_mut(SHA384_HASH_SIZE) .map_err(|_| (false, CommandError::BufferTooSmall))?; - compute_cert_chain_hash(digest_fn, slot_id, cert_store, asym_algo, cert_chain_digest_buf)?; + compute_cert_chain_hash( + digest_fn, + slot_id, + cert_store, + asym_algo, + cert_chain_digest_buf, + )?; rsp.pull_data(SHA384_HASH_SIZE) .map_err(|_| (false, CommandError::BufferTooSmall))?; @@ -115,69 +135,6 @@ fn encode_cert_chain_digest( Ok(SHA384_HASH_SIZE) } -fn generate_digests_response<'a>( - ctx: &mut SpdmContext<'a>, - rsp: &mut MessageBuf<'a>, -) -> CommandResult<()> { - // Ensure the selected hash algorithm is SHA384 and retrieve the asymmetric algorithm (currently only ECC-P384 is supported) - ctx.verify_selected_hash_algo() - .map_err(|_| ctx.generate_error_response(rsp, ErrorCode::Unspecified, 0, None))?; - let asym_algo = ctx - .selected_base_asym_algo() - .map_err(|_| ctx.generate_error_response(rsp, ErrorCode::Unspecified, 0, None))?; - - // Get the supported and provisioned slot masks. - let (supported_slot_mask, provisioned_slot_mask) = cert_slot_mask(ctx.device_certs_store); - - // No slots provisioned with certificates - let slot_cnt = provisioned_slot_mask.count_ones() as usize; - if slot_cnt == 0 { - Err(ctx.generate_error_response(rsp, ErrorCode::Unspecified, 0, None))?; - } - - let connection_version = ctx.state.connection_info.version_number(); - - // Start filling the response payload - let spdm_resp_hdr = SpdmMsgHdr::new(connection_version, ReqRespCode::Digests); - let mut payload_len = spdm_resp_hdr - .encode(rsp) - .map_err(|_| (false, CommandError::BufferTooSmall))?; - - // Fill the response header with param1 and param2 - let dgst_rsp_common = GetDigestsRespCommon { - supported_slot_mask, - provisioned_slot_mask, - }; - - payload_len += dgst_rsp_common - .encode(rsp) - .map_err(|_| (false, CommandError::BufferTooSmall))?; - - // Encode the certificate chain digests for each provisioned slot - for slot_id in 0..slot_cnt { - payload_len += encode_cert_chain_digest( - ctx.hash, - slot_id as u8, - ctx.device_certs_store, - asym_algo, - rsp, - ) - .map_err(|_| ctx.generate_error_response(rsp, ErrorCode::Unspecified, 0, None))?; - } - - // Fill the multi-key connection response data if applicable - if connection_version >= SpdmVersion::V13 && ctx.state.connection_info.multi_key_conn_rsp() { - payload_len += encode_multi_key_conn_rsp_data(ctx, provisioned_slot_mask, rsp)?; - } - - // Push data offset up by total payload length - rsp.push_data(payload_len) - .map_err(|_| (false, CommandError::BufferTooSmall))?; - - // Append the response message to the M1 transcript - ctx.append_message_to_transcript(rsp, TranscriptContext::M1) -} - fn encode_multi_key_conn_rsp_data( ctx: &mut SpdmContext, provisioned_slot_mask: u8, @@ -238,62 +195,3 @@ fn encode_multi_key_conn_rsp_data( Ok(total_size) } - -fn process_get_digests<'a>( - ctx: &mut SpdmContext<'a>, - spdm_hdr: SpdmMsgHdr, - req_payload: &mut MessageBuf<'a>, -) -> CommandResult<()> { - // Validate the version - let connection_version = ctx.state.connection_info.version_number(); - match spdm_hdr.version() { - Ok(version) if version == connection_version => {} - _ => Err(ctx.generate_error_response(req_payload, ErrorCode::VersionMismatch, 0, None))?, - } - - let req = GetDigestsReq::decode(req_payload).map_err(|_| { - ctx.generate_error_response(req_payload, ErrorCode::InvalidRequest, 0, None) - })?; - - // Reserved fields must be zero - or unexpected request error - if req.param1 != 0 || req.param2 != 0 { - Err(ctx.generate_error_response(req_payload, ErrorCode::UnexpectedRequest, 0, None))?; - } - - // Reset the transcript manager - ctx.reset_transcript_via_req_code(ReqRespCode::GetDigests); - - // Append the request message to the M1 transcript - ctx.append_message_to_transcript(req_payload, TranscriptContext::M1) -} - -pub(crate) fn handle_get_digests<'a>( - ctx: &mut SpdmContext<'a>, - spdm_hdr: SpdmMsgHdr, - req_payload: &mut MessageBuf<'a>, -) -> CommandResult<()> { - // Validate the connection state - if ctx.state.connection_info.state() < ConnectionState::AlgorithmsNegotiated { - Err(ctx.generate_error_response(req_payload, ErrorCode::UnexpectedRequest, 0, None))?; - } - - // Check if the certificate capability is supported - if ctx.local_capabilities.flags.cert_cap() == 0 { - Err(ctx.generate_error_response(req_payload, ErrorCode::UnsupportedRequest, 0, None))?; - } - - // Process GET_DIGESTS request - process_get_digests(ctx, spdm_hdr, req_payload)?; - - // Generate DIGESTS response - ctx.prepare_response_buffer(req_payload)?; - generate_digests_response(ctx, req_payload)?; - - if ctx.state.connection_info.state() < ConnectionState::AfterDigest { - ctx.state - .connection_info - .set_state(ConnectionState::AfterDigest); - } - - Ok(()) -} diff --git a/src/commands/digests/request.rs b/src/commands/digests/request.rs new file mode 100644 index 0000000..b5de799 --- /dev/null +++ b/src/commands/digests/request.rs @@ -0,0 +1,198 @@ +// Copyright 2025 +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use crate::codec::Codec; +use crate::context::SpdmContext; +use crate::error::{CommandError, CommandResult}; +use crate::protocol::certs::{CertificateInfo, KeyUsageMask}; +use crate::protocol::{SpdmMsgHdr, SpdmVersion, SHA384_HASH_SIZE}; +use crate::state::ConnectionState; +use crate::transcript::TranscriptContext; +use zerocopy::FromBytes; + +use super::*; + +/// Generate a GET_DIGESTS request and append it to the transcript context. +/// See [crate::transcript::TranscriptContext::M1] for details on the transcript context used. +pub fn generate_digest_request( + ctx: &mut SpdmContext, + message_buffer: &mut MessageBuf, +) -> CommandResult<()> { + SpdmMsgHdr::new( + ctx.state.connection_info.version_number(), + crate::protocol::ReqRespCode::GetDigests, + ) + .encode(message_buffer) + .map_err(|e| (false, CommandError::Codec(e)))?; + + let payload = GetDigestsReq::default(); + payload + .encode(message_buffer) + .map_err(|e| (false, CommandError::Codec(e)))?; + + // Message M1.B = Concatenate(GET_DIGESTS, DIGESTS, GET_CERTIFICATE, CERTIFICATE) + ctx.append_message_to_transcript(message_buffer, crate::transcript::TranscriptContext::M1) +} + +/// Process a DIGESTS response, updating the peer certificate store and transcript context accordingly. +/// See [crate::transcript::TranscriptContext::M1] for details on the transcript context used. +pub(crate) fn handle_digests_response<'a>( + ctx: &mut SpdmContext<'a>, + spdm_hdr: SpdmMsgHdr, + resp_payload: &mut MessageBuf<'a>, +) -> CommandResult<()> { + if ctx.state.connection_info.state() < ConnectionState::AlgorithmsNegotiated { + return Err((true, CommandError::UnsupportedRequest)); + } + + let con_version = ctx.state.connection_info.version_number(); + let version = match spdm_hdr.version() { + Ok(version) if version == con_version => version, + _ => return Err((false, CommandError::InvalidResponse)), + }; + + let digests_resp_common = + GetDigestsRespCommon::decode(resp_payload).map_err(|e| (true, CommandError::Codec(e)))?; + + let peer_cert_store = ctx + .state + .peer_cert_store + .as_mut() + .ok_or((true, CommandError::InvalidResponse))?; + + if version >= SpdmVersion::V13 { + peer_cert_store + .set_supported_slots(digests_resp_common.supported_slot_mask) + .map_err(|e| (true, CommandError::CertStore(e)))?; + } else { + // Set all slots as supported, if supported_slot_mask isn't supported (v1.2 and prior) + peer_cert_store + .set_supported_slots(0xFF) + .map_err(|e| (true, CommandError::CertStore(e)))?; + } + + // TODO: Was this intended to do something? + for b in 0..digests_resp_common.supported_slot_mask.count_ones() { + if (digests_resp_common.supported_slot_mask & (1 << b)) == 1 {} + } + + peer_cert_store + .set_provisioned_slots(digests_resp_common.provisioned_slot_mask) + .map_err(|e| (true, CommandError::CertStore(e)))?; + + let slot_n = digests_resp_common.provisioned_slot_mask.count_ones() as usize; + if slot_n == 0 { + return Err((true, CommandError::InvalidResponse)); + } + + // For now that should only be '0'. + for slot_id in 0..slot_n { + if resp_payload.data_len() < SHA384_HASH_SIZE { + return Err((true, CommandError::BufferTooSmall)); + } + + let digest = resp_payload + .data(SHA384_HASH_SIZE) + .map_err(|e| (true, CommandError::Codec(e)))?; + + peer_cert_store + .set_digest(slot_id as u8, digest) + .map_err(|e| (true, CommandError::CertStore(e)))?; + + resp_payload + .pull_data(SHA384_HASH_SIZE) + .map_err(|e| (true, CommandError::Codec(e)))?; + } + + if version >= SpdmVersion::V13 && ctx.state.connection_info.multi_key_conn_rsp() { + for slot_id in 0..slot_n { + if resp_payload.data_len() < size_of::() { + return Err((true, CommandError::InvalidResponse)); + } + + let key_pair_id = resp_payload + .data(size_of::()) + .map_err(|e| (true, CommandError::Codec(e)))?[0]; + + peer_cert_store + .set_keypair(slot_id as u8, key_pair_id) + .map_err(|e| (true, CommandError::CertStore(e)))?; + + resp_payload + .pull_data(size_of::()) + .map_err(|e| (true, CommandError::Codec(e)))?; + } + + for slot_id in 0..slot_n { + if resp_payload.data_len() < size_of::() { + return Err((true, CommandError::InvalidResponse)); + } + + let data = resp_payload + .data(size_of::()) + .map_err(|e| (true, CommandError::Codec(e)))?; + + let cert_info = CertificateInfo::read_from_bytes(data) + .map_err(|_| (true, CommandError::InvalidResponse))?; + + peer_cert_store + .set_cert_info(slot_id as u8, cert_info) + .map_err(|e| (true, CommandError::CertStore(e)))?; + + resp_payload + .pull_data(size_of::()) + .map_err(|e| (true, CommandError::Codec(e)))?; + } + + for slot_id in 0..slot_n { + if resp_payload.data_len() < size_of::() { + return Err((true, CommandError::InvalidResponse)); + } + + let data = resp_payload + .data(size_of::()) + .map_err(|e| (true, CommandError::Codec(e)))?; + + let key_usage_mask = KeyUsageMask::read_from_bytes(data) + .map_err(|_| (true, CommandError::InvalidResponse))?; + + peer_cert_store + .set_key_usage_mask(slot_id as u8, key_usage_mask) + .map_err(|e| (true, CommandError::CertStore(e)))?; + + resp_payload + .pull_data(size_of::()) + .map_err(|e| (true, CommandError::Codec(e)))?; + } + } + + if ctx.state.connection_info.state() < ConnectionState::AfterDigest { + ctx.state + .connection_info + .set_state(ConnectionState::AfterDigest); + } + + // Message M1.B = Concatenate(GET_DIGESTS, DIGESTS, GET_CERTIFICATE, CERTIFICATE) + ctx.append_message_to_transcript(resp_payload, TranscriptContext::M1) +} + +#[cfg(test)] +pub mod tests { + + #[test] + #[ignore] + fn test_generate_digest_request() { + todo!(); + } +} diff --git a/src/commands/digests/response.rs b/src/commands/digests/response.rs new file mode 100644 index 0000000..658ea15 --- /dev/null +++ b/src/commands/digests/response.rs @@ -0,0 +1,145 @@ +// Copyright 2025 +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use crate::cert_store::cert_slot_mask; +use crate::codec::{Codec, MessageBuf}; +use crate::commands::error_rsp::ErrorCode; +use crate::context::SpdmContext; +use crate::error::{CommandError, CommandResult}; +use crate::state::ConnectionState; +use crate::transcript::TranscriptContext; + +use super::*; + +pub(crate) fn generate_digests_response<'a>( + ctx: &mut SpdmContext<'a>, + rsp: &mut MessageBuf<'a>, +) -> CommandResult<()> { + // Ensure the selected hash algorithm is SHA384 and retrieve the asymmetric algorithm (currently only ECC-P384 is supported) + ctx.verify_selected_hash_algo() + .map_err(|_| ctx.generate_error_response(rsp, ErrorCode::Unspecified, 0, None))?; + let asym_algo = ctx + .selected_base_asym_algo() + .map_err(|_| ctx.generate_error_response(rsp, ErrorCode::Unspecified, 0, None))?; + + // Get the supported and provisioned slot masks. + let (supported_slot_mask, provisioned_slot_mask) = cert_slot_mask(ctx.device_certs_store); + + // No slots provisioned with certificates + let slot_cnt = provisioned_slot_mask.count_ones() as usize; + if slot_cnt == 0 { + Err(ctx.generate_error_response(rsp, ErrorCode::Unspecified, 0, None))?; + } + + let connection_version = ctx.state.connection_info.version_number(); + + // Start filling the response payload + let spdm_resp_hdr = SpdmMsgHdr::new(connection_version, ReqRespCode::Digests); + let mut payload_len = spdm_resp_hdr + .encode(rsp) + .map_err(|_| (false, CommandError::BufferTooSmall))?; + + // Fill the response header with param1 and param2 + let dgst_rsp_common = GetDigestsRespCommon { + supported_slot_mask, + provisioned_slot_mask, + }; + + payload_len += dgst_rsp_common + .encode(rsp) + .map_err(|_| (false, CommandError::BufferTooSmall))?; + + // Encode the certificate chain digests for each provisioned slot + for slot_id in 0..slot_cnt { + payload_len += encode_cert_chain_digest( + ctx.hash, + slot_id as u8, + ctx.device_certs_store, + asym_algo, + rsp, + ) + .map_err(|_| ctx.generate_error_response(rsp, ErrorCode::Unspecified, 0, None))?; + } + + // Fill the multi-key connection response data if applicable + if connection_version >= SpdmVersion::V13 && ctx.state.connection_info.multi_key_conn_rsp() { + payload_len += encode_multi_key_conn_rsp_data(ctx, provisioned_slot_mask, rsp)?; + } + + // Push data offset up by total payload length + rsp.push_data(payload_len) + .map_err(|_| (false, CommandError::BufferTooSmall))?; + + // Append the response message to the M1 transcript + ctx.append_message_to_transcript(rsp, TranscriptContext::M1) +} + +fn process_get_digests<'a>( + ctx: &mut SpdmContext<'a>, + spdm_hdr: SpdmMsgHdr, + req_payload: &mut MessageBuf<'a>, +) -> CommandResult<()> { + // Validate the version + let connection_version = ctx.state.connection_info.version_number(); + match spdm_hdr.version() { + Ok(version) if version == connection_version => {} + _ => Err(ctx.generate_error_response(req_payload, ErrorCode::VersionMismatch, 0, None))?, + } + + let req = GetDigestsReq::decode(req_payload).map_err(|_| { + ctx.generate_error_response(req_payload, ErrorCode::InvalidRequest, 0, None) + })?; + + // Reserved fields must be zero - or unexpected request error + if req.param1 != 0 || req.param2 != 0 { + Err(ctx.generate_error_response(req_payload, ErrorCode::UnexpectedRequest, 0, None))?; + } + + // Reset the transcript manager + ctx.reset_transcript_via_req_code(ReqRespCode::GetDigests); + + // Append the request message to the M1 transcript + ctx.append_message_to_transcript(req_payload, TranscriptContext::M1) +} + +pub(crate) fn handle_get_digests<'a>( + ctx: &mut SpdmContext<'a>, + spdm_hdr: SpdmMsgHdr, + req_payload: &mut MessageBuf<'a>, +) -> CommandResult<()> { + // Validate the connection state + if ctx.state.connection_info.state() < ConnectionState::AlgorithmsNegotiated { + Err(ctx.generate_error_response(req_payload, ErrorCode::UnexpectedRequest, 0, None))?; + } + + // Check if the certificate capability is supported + if ctx.local_capabilities.flags.cert_cap() == 0 { + Err(ctx.generate_error_response(req_payload, ErrorCode::UnsupportedRequest, 0, None))?; + } + + // Process GET_DIGESTS request + process_get_digests(ctx, spdm_hdr, req_payload)?; + + // Generate DIGESTS response + ctx.prepare_response_buffer(req_payload)?; + generate_digests_response(ctx, req_payload)?; + + if ctx.state.connection_info.state() < ConnectionState::AfterDigest { + ctx.state + .connection_info + .set_state(ConnectionState::AfterDigest); + } + + Ok(()) +} diff --git a/src/commands/error_rsp.rs b/src/commands/error_rsp.rs index 1de6c26..cf490c5 100644 --- a/src/commands/error_rsp.rs +++ b/src/commands/error_rsp.rs @@ -1,4 +1,16 @@ -// Licensed under the Apache-2.0 license +// Copyright 2025 +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. use crate::codec::{Codec, CommonCodec, MessageBuf}; use crate::error::CommandError; diff --git a/src/commands/measurements/mod.rs b/src/commands/measurements/mod.rs new file mode 100644 index 0000000..5a49208 --- /dev/null +++ b/src/commands/measurements/mod.rs @@ -0,0 +1,160 @@ +// Copyright 2025 +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! GET_MEASUREMENTS and MEASURMENTS command types and logic + +/// Requester logic for GET_MEASUREMENTS and MEASURMENTS +pub mod request; +/// Responder logic for GET_MEASUREMENTS and MEASURMENTS +pub mod response; + +use crate::protocol::*; +use crate::{codec::CommonCodec, error::CommandError}; +use bitfield::bitfield; +use zerocopy::{FromBytes, Immutable, IntoBytes, KnownLayout, Unaligned}; + +const RESPONSE_FIXED_FIELDS_SIZE: usize = 8; +const MAX_RESPONSE_VARIABLE_FIELDS_SIZE: usize = + NONCE_LEN + size_of::() + size_of::(); + +#[derive(FromBytes, IntoBytes, Immutable)] +#[repr(C)] +struct GetMeasurementsReqCommon { + req_attr: GetMeasurementsReqAttr, + meas_op: u8, +} +impl CommonCodec for GetMeasurementsReqCommon {} + +#[derive(FromBytes, IntoBytes, Immutable)] +#[repr(C)] +struct GetMeasurementsReqSignature { + requester_nonce: [u8; NONCE_LEN], + slot_id: u8, +} +impl CommonCodec for GetMeasurementsReqSignature {} + +bitfield! { + #[derive(FromBytes, IntoBytes, Immutable)] + #[repr(C)] + struct GetMeasurementsReqAttr(u8); + impl Debug; + u8; + pub signature_requested, set_signature_requested: 0, 0; + pub raw_bitstream_requested, set_raw_bitstream_requested: 1, 1; + pub new_measurement_requested, set_new_measurement_requested: 2, 2; + reserved, _: 7, 3; +} + +bitfield! { + #[derive(FromBytes, IntoBytes, Immutable, KnownLayout)] + #[repr(C)] + struct MeasurementsRspFixed([u8]); + impl Debug; + u8; + pub spdm_version, set_spdm_version: 7, 0; + pub req_resp_code, set_req_resp_code: 15, 8; + pub total_measurement_indices, set_total_measurement_indices: 23, 16; + pub slot_id, set_slot_id: 27, 24; + pub content_changed, set_content_changed: 29, 28; + reserved, _: 31, 30; + pub num_blocks, set_num_blocks: 39, 32; + pub measurement_record_len_byte0, set_measurement_record_len_byte0: 47, 40; + pub measurement_record_len_byte1, set_measurement_record_len_byte1: 55, 48; + pub measurement_record_len_byte2, set_measurement_record_len_byte2: 63, 56; +} + +impl MeasurementsRspFixed<[u8; RESPONSE_FIXED_FIELDS_SIZE]> { + pub fn set_measurement_record_len(&mut self, len: u32) { + self.set_measurement_record_len_byte0((len & 0xFF) as u8); + self.set_measurement_record_len_byte1(((len >> 8) & 0xFF) as u8); + self.set_measurement_record_len_byte2(((len >> 16) & 0xFF) as u8); + } +} + +impl Default for MeasurementsRspFixed<[u8; RESPONSE_FIXED_FIELDS_SIZE]> { + fn default() -> Self { + Self([0; RESPONSE_FIXED_FIELDS_SIZE]) + } +} + +impl CommonCodec for MeasurementsRspFixed<[u8; RESPONSE_FIXED_FIELDS_SIZE]> {} + +/// Measurement Operation request field +pub enum MeasurementOperation { + /// Query the Responder for the total number of measurement blocks available + ReportMeasBlockCount, + /// Request the measurement block at a specific index + /// + /// Index has to be between `0x01` and `0xFE`, inclusively. + RequestSingleMeasBlock(u8), + /// Request all measurement blocks + RequestAllMeasBlocks, +} + +impl TryInto for MeasurementOperation { + type Error = CommandError; + + fn try_into(self) -> Result { + match self { + MeasurementOperation::ReportMeasBlockCount => Ok(0x01), + MeasurementOperation::RequestSingleMeasBlock(x) => { + if matches!(x, 0x00 | 0xFF) { + Err(CommandError::UnsupportedRequest) + } else { + Ok(x) + } + } + MeasurementOperation::RequestAllMeasBlocks => Ok(0xFF), + } + } +} + +#[derive(Debug, FromBytes, IntoBytes, Immutable, Unaligned, KnownLayout)] +#[repr(C, packed)] +struct MeasurementBlockHeader { + index: u8, + measurement_spec: MeasurementSpecification, + measurement_size: zerocopy::little_endian::U16, +} + +impl CommonCodec for MeasurementBlockHeader {} + +/// Content changed indicators for MEASUREMENT responses +#[derive(Debug, Clone, Copy)] +pub enum ContentChanged { + /// The Responder does not detect changes of MeasurementRecord fields + /// of previous MEASUREMENTS responses in the same measurement log, + /// or this message does not contain a signature. + NoDetection = 0x00, + /// The Responder detected that one or more MeasurementRecord fields + /// of previous MEASUREMENTS responses in the measurement log being + /// signed have changed. The Requester might consider issuing + /// GET_MEASUREMENTS again to acquire latest measurements. + ChangeDetected = 0x01, + /// The Responder detected no change in MeasurementRecord fields of + /// previous MEASUREMENTS responses in the measurement log being signed. + NoChangeDetected = 0x10, + Reserved = 0x11, +} + +impl From for ContentChanged { + fn from(value: u8) -> Self { + match value { + 0b00 => Self::NoDetection, + 0b01 => Self::ChangeDetected, + 0b10 => Self::NoChangeDetected, + _ => Self::Reserved, + } + } +} diff --git a/src/commands/measurements/request.rs b/src/commands/measurements/request.rs new file mode 100644 index 0000000..ef5c5ff --- /dev/null +++ b/src/commands/measurements/request.rs @@ -0,0 +1,416 @@ +// Copyright 2025 +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use zerocopy::FromBytes; + +use crate::{ + codec::{Codec, MessageBuf}, + commands::measurements::{ + ContentChanged, GetMeasurementsReqAttr, GetMeasurementsReqCommon, + GetMeasurementsReqSignature, MeasurementBlockHeader, MeasurementOperation, + MeasurementsRspFixed, RESPONSE_FIXED_FIELDS_SIZE, + }, + context::SpdmContext, + error::{CommandError, CommandResult, PlatformError}, + protocol::{MeasurementSpecificationType, ReqRespCode, SpdmMsgHdr, SpdmVersion, NONCE_LEN}, + state::ConnectionState, + transcript::TranscriptContext, +}; + +#[derive(Debug)] +pub struct Measurement<'a> { + pub index: u8, + pub measurement_spec: MeasurementSpecificationType, + pub measurement: &'a [u8], +} + +/// Parsed MEASUREMENTS response +#[derive(Debug)] +pub struct Measurements<'a> { + /// Fixed fields + fixed_fields: &'a MeasurementsRspFixed<[u8; RESPONSE_FIXED_FIELDS_SIZE]>, + /// Measurement blocks + measurements: &'a [u8], + /// 32-byte Nonce + pub nonce: &'a [u8], + /// Opaque data + pub opaque_data: &'a [u8], + /// Requester context + pub requester_ctx: Option<&'a [u8]>, + /// Optional Signature + /// + /// _Note_: The length of the signature isn't checked for validity. + pub signature: Option<&'a [u8]>, +} + +/// Generate a GET_MEASUREMENTS request +/// +/// # Arguments +/// * `ctx`: The SPDM context +/// * `req_buf`: Buffer to write the request into +/// * `raw_bitstream_requested`: Request a raw bit stream (if supported) (SPDM v1.2+) +/// * `new_measurement_requested`: Request new measurement if the responder has pending updates to blocks (SPDM v1.3+) +/// * `meas_op`: Measurement operation +/// * `slot_id`: Request a signed measurement if provided, signed with certificate slot identifier (0-7) +/// * `context`: Append optional 8 byte context (SPDM v1.3+) +/// +/// ## Note +/// For SPDM version 1.0 this implementation does **not** supporte signed measurements. +/// +/// # Returns +/// - () on success +/// - [CommandError] on failure +/// +/// # Connection State Requirements +/// - Connection state must be >= AlgorithmsNegotiated +/// +/// # Transcript +/// - Resets M1/M2 message transcript +/// - Appends request to the L1 transcript context +pub fn generate_get_measurements<'a>( + ctx: &mut SpdmContext<'a>, + req_buf: &mut MessageBuf<'a>, + raw_bitstream_requested: bool, + new_measurement_requested: bool, + meas_op: MeasurementOperation, + slot_id: Option, + context: Option<&[u8; 8]>, +) -> CommandResult<()> { + // Validate connection state - algorithms must be negotiated first + if ctx.state.connection_info.state() < ConnectionState::AlgorithmsNegotiated { + return Err((true, CommandError::UnsupportedRequest)); + } + + // TODO: maybe add a check if measuements are supported by the responder + + // Get connection version + let connection_version = ctx.state.connection_info.version_number(); + + // Create and encode SPDM message header + let spdm_hdr = SpdmMsgHdr::new(connection_version, ReqRespCode::GetMeasurements); + let mut payload_len = spdm_hdr + .encode(req_buf) + .map_err(|e| (false, CommandError::Codec(e)))?; + + let mut req_attr = GetMeasurementsReqAttr(0); + + // signature requested is available in all versions + // (v1.0 doesn't support slot-ids) + if slot_id.is_some() { + if connection_version == SpdmVersion::V10 { + return Err((true, CommandError::UnsupportedRequest)); + } + // Error if the responder doesn't support this + if !responder_supports_signed_measurements(ctx) { + return Err((true, CommandError::UnsupportedRequest)); + } + req_attr.set_signature_requested(1); + } + + if raw_bitstream_requested { + if connection_version < SpdmVersion::V12 { + return Err((true, CommandError::UnsupportedRequest)); + } + // TODO Check measuement block spec + req_attr.set_raw_bitstream_requested(1); + } + + if new_measurement_requested { + if connection_version < SpdmVersion::V13 { + return Err((true, CommandError::UnsupportedRequest)); + } + req_attr.set_new_measurement_requested(1); + } + + // Encode request attributes and `Measurement` operation + let get_meas_common = GetMeasurementsReqCommon { + req_attr, + meas_op: meas_op.try_into().map_err(|e| (true, e))?, + }; + payload_len += get_meas_common + .encode(req_buf) + .map_err(|e| (false, CommandError::Codec(e)))?; + + // Generate Nonce if signature was requested + if let Some(id) = slot_id { + let mut nonce = [0; NONCE_LEN]; + ctx.rng + .get_random_bytes(&mut nonce) + .map_err(|e| (true, PlatformError::from(e).into()))?; + + if connection_version < SpdmVersion::V11 { + todo!("Implement encoding of nonce only for v1.0"); + } else { + let get_meas_sig = GetMeasurementsReqSignature { + requester_nonce: nonce, + slot_id: id, + }; + payload_len += get_meas_sig + .encode(req_buf) + .map_err(|e| (false, CommandError::Codec(e)))?; + } + } + + // encode context data if spdm version is >= v1.3 + if connection_version >= SpdmVersion::V13 { + if let Some(context) = context { + payload_len += + crate::codec::encode_u8_slice(context, req_buf).map_err(|e| (true, e.into()))?; + } else { + payload_len += + crate::codec::encode_u8_slice(&[0; 8], req_buf).map_err(|e| (true, e.into()))?; + } + } + + // Finalize message by pushing total payload length + req_buf + .push_data(payload_len) + .map_err(|_| (false, CommandError::BufferTooSmall))?; + + ctx.transcript_mgr.reset_context(TranscriptContext::M1); + ctx.append_message_to_transcript(req_buf, TranscriptContext::L1) +} + +/// Check if the responder supports signing its measurements +/// +/// Currently only the `MEAS_CAP` is checked. +/// Checking `BaseAsymSel` and `ExtAsymSelCount` might need to checked, +/// to determine if a signed measurement can be requested. +/// (The Spec is a bit fuzzy about that.) +fn responder_supports_signed_measurements(ctx: &SpdmContext<'_>) -> bool { + let flags = &ctx.state.connection_info.peer_capabilities().flags; + // 0b00 no measurements support + // 0b01 measurements support without signing + // 0b10 measurements with signing supported + // 0b11 reserved + flags.meas_cap() == 0b10 +} + +/// Handle an incoming MEASUREMENTS response +pub(crate) fn handle_measurements_response<'a>( + ctx: &mut SpdmContext<'a>, + resp_header: SpdmMsgHdr, + resp: &mut MessageBuf<'a>, +) -> CommandResult<()> { + // Validate connection state - algorithms must be negotiated + if ctx.state.connection_info.state() < ConnectionState::AlgorithmsNegotiated { + return Err((true, CommandError::UnsupportedResponse)); + } + + // Validate version matches connection version + let connection_version = ctx.state.connection_info.version_number(); + if resp_header.version().ok() != Some(connection_version) { + return Err((true, CommandError::InvalidResponse)); + } + + // Include the already parsed header again to parse `MeasurementsRspFixed` + resp.push_data(size_of::()) + .map_err(|e| (false, e.into()))?; + + let fixed_fields = MeasurementsRspFixed::decode(resp).map_err(|e| (true, e.into()))?; + + // Convert 3-byte measurement record length to u32 + let _meas_record_length = u32::from_le_bytes([ + fixed_fields.measurement_record_len_byte0(), + fixed_fields.measurement_record_len_byte1(), + fixed_fields.measurement_record_len_byte2(), + 0, + ]); + + // Decode all measurement blocks + for _ in 0..fixed_fields.num_blocks() { + let block_header = MeasurementBlockHeader::decode(resp).map_err(|e| (true, e.into()))?; + resp.pull_data(block_header.measurement_size.get() as usize) + .map_err(|e| (true, e.into()))?; + } + + // Decode Nonce + let _nonce = resp.data(32).map_err(|e| (true, e.into()))?; + resp.pull_data(32).map_err(|e| (true, e.into()))?; + + // Decode opaque data + let mut len_bytes = [0; 2]; + len_bytes.copy_from_slice(resp.data(2).map_err(|e| (true, e.into()))?); + let opaque_data_len = u16::from_le_bytes(len_bytes); + resp.pull_data(2).map_err(|e| (true, e.into()))?; + resp.pull_data(opaque_data_len as usize) + .map_err(|e| (true, e.into()))?; + + if connection_version >= SpdmVersion::V13 { + // Decode requester context + let _requester_ctx = resp.data(8).map_err(|e| (true, e.into()))?; + resp.pull_data(8).map_err(|e| (true, e.into()))?; + } + + // Remaining is the signature, if requested by the GET_MEASUREMENTS request + + // Append response to transcript (L1 context for measurements) + // We have to use this ugly hack to bring the message buffer into the right form to exclude the signature. + let tail = resp.data_len(); + resp.trim(0).map_err(|e| (true, CommandError::Codec(e)))?; + // Append the entire message (excluding the signature) to the transcript before signature verification, as required by SPDM 1.2 and later. + ctx.append_message_to_transcript(resp, TranscriptContext::L1)?; + resp.put_data(tail) + .map_err(|e| (true, CommandError::Codec(e)))?; + + Ok(()) +} + +/// Parse a successfull measurements response +/// +/// _Note:_ The caller has to ensure that `resp` is a valid MEASUREMENTS response. +/// Returns `None` if parsing fails. +pub fn parse_measurements_response<'a>(resp: &'a [u8]) -> Option> { + let (fixed_fields, rest) = + MeasurementsRspFixed::<[u8; RESPONSE_FIXED_FIELDS_SIZE]>::ref_from_prefix(resp).ok()?; + + let connection_version: SpdmVersion = fixed_fields.spdm_version().try_into().ok()?; + // Convert 3-byte measurement record length to u32 + let meas_record_length = u32::from_le_bytes([ + fixed_fields.measurement_record_len_byte0(), + fixed_fields.measurement_record_len_byte1(), + fixed_fields.measurement_record_len_byte2(), + 0, + ]) as usize; + + // Get measurement blocks + if meas_record_length > rest.len() { + return None; + } + let (measurements, rest) = rest.split_at(meas_record_length); + + // Decode Nonce + if rest.len() < 32 { + return None; + } + let (nonce, rest) = rest.split_at(32); + + // Decode opaque data + let (opaque_data, rest) = decode_opaque_data(rest)?; + + let (requester_ctx, rest) = if connection_version >= SpdmVersion::V13 { + // Decode requester context + let requester_ctx = rest.get(..8)?; + let rest = rest.get(8..)?; + (Some(requester_ctx), rest) + } else { + (None, rest) + }; + + // Remaining is the signature, if requested by the GET_MEASUREMENTS request + let signature = if !rest.is_empty() { Some(rest) } else { None }; + + Some(Measurements { + fixed_fields, + measurements, + nonce, + opaque_data, + requester_ctx, + signature, + }) +} + +/// Decode the opaque data +/// +/// # Arguments +/// - `buf`: buffer containing 2-byte length and following opaque data +/// +/// # Returns +/// - `Some((opaque_data, rest))` on success +/// - `None` if `buf` is to small to hold the opaque data +fn decode_opaque_data(buf: &[u8]) -> Option<(&[u8], &[u8])> { + if buf.len() < 2 { + return None; + } + let mut opaque_data_len = [0; 2]; + opaque_data_len.copy_from_slice(&buf[..2]); + let opaque_data_len = u16::from_le_bytes(opaque_data_len) as usize; + + let rest = buf.get(2..)?; + + let data = rest.get(..opaque_data_len)?; + let rest = rest.get(opaque_data_len..)?; + + Some((data, rest)) +} + +impl<'a> Measurements<'a> { + /// The certificate slot used to sign the measurements + /// + /// Returns the slot number of the certificate chain + /// specified in the GET_MEASUREMENTS request, + /// or 0xF if the Responder's public key was provisioned + /// to the Requester previously. + pub fn slot_id(&self) -> u8 { + self.fixed_fields.slot_id() + } + + /// The total number of measurment blocks in this response + /// + /// _Note:_ This returns the number of blocks reported by the response header, + /// it is not guranteed at this point, that the response actually contains them. + pub fn total_measurement_blocks(&self) -> u8 { + self.fixed_fields.num_blocks() + } + + /// If this message contains a signature, this field shall indicate + /// if one or more MeasurementRecord fields of previous MEASUREMENTS + /// responses in the same measurement log have changed. + pub fn content_changed(&self) -> ContentChanged { + self.fixed_fields.content_changed().into() + } + + /// Returns an iterator over the measurements. + pub fn iter(&self) -> MeasurementIterator<'a> { + MeasurementIterator { + measurements: self.measurements, + pos: 0, + } + } +} + +impl<'a> IntoIterator for Measurements<'a> { + type Item = Measurement<'a>; + + type IntoIter = MeasurementIterator<'a>; + + fn into_iter(self) -> Self::IntoIter { + MeasurementIterator { + measurements: self.measurements, + pos: 0, + } + } +} + +pub struct MeasurementIterator<'a> { + measurements: &'a [u8], + pos: usize, +} + +impl<'a> Iterator for MeasurementIterator<'a> { + type Item = Measurement<'a>; + + fn next(&mut self) -> Option { + let (header, rest) = + MeasurementBlockHeader::ref_from_prefix(self.measurements.get(self.pos..)?).ok()?; + + let measurement = rest.get(..header.measurement_size.get() as usize)?; + self.pos += size_of::() + header.measurement_size.get() as usize; + Some(Measurement { + index: header.index, + measurement_spec: header.measurement_spec.try_into().ok()?, + measurement, + }) + } +} diff --git a/src/commands/measurements_rsp.rs b/src/commands/measurements/response.rs similarity index 81% rename from src/commands/measurements_rsp.rs rename to src/commands/measurements/response.rs index a59f15a..73bfeca 100644 --- a/src/commands/measurements_rsp.rs +++ b/src/commands/measurements/response.rs @@ -1,89 +1,178 @@ -// Licensed under the Apache-2.0 license +// Copyright 2025 +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. use crate::cert_store::SpdmCertStore; use crate::chunk_ctx::{ChunkError, LargeResponse}; -use crate::codec::{encode_u8_slice, Codec, CommonCodec, MessageBuf}; -use crate::commands::algorithms_rsp::selected_measurement_specification; -use crate::commands::error_rsp::ErrorCode; -use crate::context::SpdmContext; -use crate::error::{CommandError, CommandResult, PlatformError}; -use crate::measurements::common::{ - MeasurementChangeStatus, MeasurementsError, SpdmMeasurements, SPDM_MAX_MEASUREMENT_RECORD_SIZE, -}; +use crate::commands::algorithms::selected_measurement_specification; +use crate::commands::measurements::*; +use crate::measurements::common::*; +use crate::platform::{evidence::SpdmEvidence, hash::SpdmHash, rng::SpdmRng}; use crate::protocol::*; use crate::state::ConnectionState; use crate::transcript::{TranscriptContext, TranscriptManager}; -use crate::platform::hash::SpdmHash; -use crate::platform::rng::SpdmRng; -use crate::platform::evidence::SpdmEvidence; -use bitfield::bitfield; -use zerocopy::{FromBytes, Immutable, IntoBytes}; - -const RESPONSE_FIXED_FIELDS_SIZE: usize = 8; -const MAX_RESPONSE_VARIABLE_FIELDS_SIZE: usize = - NONCE_LEN + size_of::() + size_of::(); - -#[derive(FromBytes, IntoBytes, Immutable)] -#[repr(C)] -struct GetMeasurementsReqCommon { - req_attr: GetMeasurementsReqAttr, - meas_op: u8, -} -impl CommonCodec for GetMeasurementsReqCommon {} +use crate::{ + codec::{encode_u8_slice, Codec, MessageBuf}, + commands::error_rsp::ErrorCode, + context::SpdmContext, + error::{CommandError, CommandResult, PlatformError}, +}; -#[derive(FromBytes, IntoBytes, Immutable)] -#[repr(C)] -struct GetMeasurementsReqSignature { - requester_nonce: [u8; NONCE_LEN], - slot_id: u8, -} -impl CommonCodec for GetMeasurementsReqSignature {} - -bitfield! { - #[derive(FromBytes, IntoBytes, Immutable)] - #[repr(C)] - struct GetMeasurementsReqAttr(u8); - impl Debug; - u8; - pub signature_requested, _: 0, 0; - pub raw_bitstream_requested, _: 1, 1; - pub new_measurement_requested, _: 2, 2; - reserved, _: 7, 3; -} +fn process_get_measurements<'a>( + ctx: &mut SpdmContext<'a>, + spdm_hdr: SpdmMsgHdr, + req_payload: &mut MessageBuf<'a>, +) -> CommandResult { + // Validate the version + let connection_version = ctx.state.connection_info.version_number(); + if spdm_hdr.version().ok() != Some(connection_version) { + Err(ctx.generate_error_response(req_payload, ErrorCode::VersionMismatch, 0, None))?; + } + + // Decode the request + let req_common = GetMeasurementsReqCommon::decode(req_payload).map_err(|_| { + ctx.generate_error_response(req_payload, ErrorCode::InvalidRequest, 0, None) + })?; + + let slot_id = if req_common.req_attr.signature_requested() == 0 { + None + } else { + // check if responder capabilities support signature + if ctx.local_capabilities.flags.meas_cap() + != MeasCapability::MeasurementsWithSignature as u8 + { + Err(ctx.generate_error_response(req_payload, ErrorCode::UnsupportedRequest, 0, None))?; + } -bitfield! { - #[derive(FromBytes, IntoBytes, Immutable)] - #[repr(C)] - struct MeasurementsRspFixed([u8]); - impl Debug; - u8; - pub spdm_version, set_spdm_version: 7, 0; - pub req_resp_code, set_req_resp_code: 15, 8; - pub total_measurement_indices, set_total_measurement_indices: 23, 16; - pub slot_id, set_slot_id: 27, 24; - pub content_changed, set_content_changed: 29, 28; - reserved, _: 31, 30; - pub num_blocks, set_num_blocks: 39, 32; - pub measurement_record_len_byte0, set_measurement_record_len_byte0: 47, 40; - pub measurement_record_len_byte1, set_measurement_record_len_byte1: 55, 48; - pub measurement_record_len_byte2, set_measurement_record_len_byte2: 63, 56; + // Decode the requester nonce and slot ID + let req_signature_fields = + GetMeasurementsReqSignature::decode(req_payload).map_err(|_| { + ctx.generate_error_response(req_payload, ErrorCode::InvalidRequest, 0, None) + })?; + Some(req_signature_fields.slot_id) + }; + + // Decode the requester context if version is >= 1.3 + let requester_context = if connection_version >= SpdmVersion::V13 { + Some(RequesterContext::decode(req_payload).map_err(|_| { + ctx.generate_error_response(req_payload, ErrorCode::InvalidRequest, 0, None) + })?) + } else { + None + }; + + // Reset the transcript for the GET_MEASUREMENTS request + ctx.reset_transcript_via_req_code(ReqRespCode::GetMeasurements); + + // Append the request to the transcript + ctx.append_message_to_transcript(req_payload, TranscriptContext::L1)?; + + let asym_algo = ctx.selected_base_asym_algo().map_err(|_| { + ctx.generate_error_response(req_payload, ErrorCode::InvalidRequest, 0, None) + })?; + + let get_meas_req_context = MeasurementsResponse { + spdm_version: connection_version, + req_attr: req_common.req_attr, + meas_op: req_common.meas_op, + slot_id, + requester_context, + asym_algo, + }; + + Ok(get_meas_req_context) } -impl MeasurementsRspFixed<[u8; RESPONSE_FIXED_FIELDS_SIZE]> { - pub fn set_measurement_record_len(&mut self, len: u32) { - self.set_measurement_record_len_byte0((len & 0xFF) as u8); - self.set_measurement_record_len_byte1(((len >> 8) & 0xFF) as u8); - self.set_measurement_record_len_byte2(((len >> 16) & 0xFF) as u8); +pub(crate) fn generate_measurements_response<'a>( + ctx: &mut SpdmContext<'a>, + rsp_ctx: MeasurementsResponse, + rsp: &mut MessageBuf<'a>, +) -> CommandResult<()> { + let rsp_len = rsp_ctx.response_size(ctx.evidence, &mut ctx.measurements)?; + + if rsp_len > ctx.min_data_transfer_size() { + // If the response is larger than the minimum data transfer size, use chunked response + let large_rsp = LargeResponse::Measurements(rsp_ctx); + let handle = ctx.large_resp_context.init(large_rsp, rsp_len); + Err(ctx.generate_error_response(rsp, ErrorCode::LargeResponse, handle, None))? + } else { + // If the response fits in a single message, prepare it directly + ctx.prepare_response_buffer(rsp)?; + + // Encode the response fixed fields + rsp.put_data(rsp_len) + .map_err(|e| (false, CommandError::Codec(e)))?; + let rsp_buf = rsp + .data_mut(rsp_len) + .map_err(|e| (false, CommandError::Codec(e)))?; + let payload_len = rsp_ctx.get_chunk( + ctx.hash, + ctx.rng, + ctx.evidence, + &mut ctx.measurements, + &mut ctx.transcript_mgr, + ctx.device_certs_store, + 0, + rsp_buf, + )?; + if rsp_len != payload_len { + Err(( + false, + CommandError::Measurement(MeasurementsError::InvalidBuffer), + ))?; + } + rsp.pull_data(payload_len) + .map_err(|e| (false, CommandError::Codec(e)))?; + + rsp.push_data(payload_len) + .map_err(|e| (false, CommandError::Codec(e))) } } -impl Default for MeasurementsRspFixed<[u8; RESPONSE_FIXED_FIELDS_SIZE]> { - fn default() -> Self { - Self([0; RESPONSE_FIXED_FIELDS_SIZE]) +pub(crate) fn handle_get_measurements<'a>( + ctx: &mut SpdmContext<'a>, + spdm_hdr: SpdmMsgHdr, + req_payload: &mut MessageBuf<'a>, +) -> CommandResult<()> { + // Check that the connection state is Negotiated + if ctx.state.connection_info.state() < ConnectionState::AlgorithmsNegotiated { + Err(ctx.generate_error_response(req_payload, ErrorCode::UnexpectedRequest, 0, None))?; + } + + // Check if the measurement capability is supported + if ctx.local_capabilities.flags.meas_cap() == MeasCapability::NoMeasurement as u8 { + return Err(ctx.generate_error_response( + req_payload, + ErrorCode::UnsupportedRequest, + 0, + None, + )); } -} -impl CommonCodec for MeasurementsRspFixed<[u8; RESPONSE_FIXED_FIELDS_SIZE]> {} + // Verify that the DMTF measurement spec is selected and the measurement hash algorithm is SHA384 + let meas_spec_sel = selected_measurement_specification(ctx); + if meas_spec_sel.dmtf_measurement_spec() == 0 || ctx.verify_selected_hash_algo().is_err() { + Err(ctx.generate_error_response(req_payload, ErrorCode::UnexpectedRequest, 0, None))?; + } + + // Process GET_MEASUREMENTS request + let rsp_ctx = process_get_measurements(ctx, spdm_hdr, req_payload)?; + + // Generate MEASUREMENTS response + ctx.prepare_response_buffer(req_payload)?; + generate_measurements_response(ctx, rsp_ctx, req_payload)?; + Ok(()) +} #[derive(Debug)] pub(crate) struct MeasurementsResponse { @@ -121,7 +210,12 @@ impl MeasurementsResponse { let raw_bitstream_requested = self.req_attr.raw_bitstream_requested() == 1; let measurement_record_len = measurements - .measurement_block_size(evidence, self.asym_algo, self.meas_op, raw_bitstream_requested) + .measurement_block_size( + evidence, + self.asym_algo, + self.meas_op, + raw_bitstream_requested, + ) .map_err(|e| (false, CommandError::Measurement(e)))?; // Fill the chunk buffer with the appropriate response sections // Instead of a while loop, use a single-pass approach for clarity and efficiency. @@ -201,8 +295,7 @@ impl MeasurementsResponse { ) -> CommandResult<[u8; RESPONSE_FIXED_FIELDS_SIZE]> { let mut fixed_rsp_fields = [0u8; RESPONSE_FIXED_FIELDS_SIZE]; let mut fixed_rsp_buf = MessageBuf::new(&mut fixed_rsp_fields); - _ = self - .encode_response_fixed_fields(evidence, &mut fixed_rsp_buf, measurements)?; + _ = self.encode_response_fixed_fields(evidence, &mut fixed_rsp_buf, measurements)?; Ok(fixed_rsp_fields) } @@ -261,12 +354,11 @@ impl MeasurementsResponse { fn response_variable_fields( &self, - rng: &mut dyn SpdmRng, - ) -> CommandResult<([u8; MAX_RESPONSE_VARIABLE_FIELDS_SIZE], usize)> { + rng: &mut dyn SpdmRng, + ) -> CommandResult<([u8; MAX_RESPONSE_VARIABLE_FIELDS_SIZE], usize)> { let mut trailer_rsp = [0u8; MAX_RESPONSE_VARIABLE_FIELDS_SIZE]; let mut trailer_buf = MessageBuf::new(&mut trailer_rsp); - let len = self - .encode_response_variable_fields(rng, &mut trailer_buf)?; + let len = self.encode_response_variable_fields(rng, &mut trailer_buf)?; Ok((trailer_rsp, len)) } @@ -305,8 +397,7 @@ impl MeasurementsResponse { ) -> CommandResult<[u8; ECC_P384_SIGNATURE_SIZE]> { let mut signature = [0u8; ECC_P384_SIGNATURE_SIZE]; let mut signature_buf = MessageBuf::new(&mut signature); - let _ = self - .encode_l1_signature_ecc(hash, transcript, cert_store, &mut signature_buf)?; + let _ = self.encode_l1_signature_ecc(hash, transcript, cert_store, &mut signature_buf)?; Ok(signature) } @@ -356,7 +447,11 @@ impl MeasurementsResponse { Ok(signature.len()) } - fn response_size(&self, evidence: &dyn SpdmEvidence, measurements: &mut SpdmMeasurements) -> CommandResult { + fn response_size( + &self, + evidence: &dyn SpdmEvidence, + measurements: &mut SpdmMeasurements, + ) -> CommandResult { // Calculate the size of the response based on the request attributes let mut rsp_size = RESPONSE_FIXED_FIELDS_SIZE; @@ -384,150 +479,3 @@ impl MeasurementsResponse { Ok(rsp_size) } } - -fn process_get_measurements<'a>( - ctx: &mut SpdmContext<'a>, - spdm_hdr: SpdmMsgHdr, - req_payload: &mut MessageBuf<'a>, -) -> CommandResult { - // Validate the version - let connection_version = ctx.state.connection_info.version_number(); - if spdm_hdr.version().ok() != Some(connection_version) { - Err(ctx.generate_error_response(req_payload, ErrorCode::VersionMismatch, 0, None))?; - } - - // Decode the request - let req_common = GetMeasurementsReqCommon::decode(req_payload).map_err(|_| { - ctx.generate_error_response(req_payload, ErrorCode::InvalidRequest, 0, None) - })?; - - let slot_id = if req_common.req_attr.signature_requested() == 0 { - None - } else { - // check if responder capabilities support signature - if ctx.local_capabilities.flags.meas_cap() - != MeasCapability::MeasurementsWithSignature as u8 - { - Err(ctx.generate_error_response(req_payload, ErrorCode::UnsupportedRequest, 0, None))?; - } - - // Decode the requester nonce and slot ID - let req_signature_fields = - GetMeasurementsReqSignature::decode(req_payload).map_err(|_| { - ctx.generate_error_response(req_payload, ErrorCode::InvalidRequest, 0, None) - })?; - Some(req_signature_fields.slot_id) - }; - - // Decode the requester context if version is >= 1.3 - let requester_context = if connection_version >= SpdmVersion::V13 { - Some(RequesterContext::decode(req_payload).map_err(|_| { - ctx.generate_error_response(req_payload, ErrorCode::InvalidRequest, 0, None) - })?) - } else { - None - }; - - // Reset the transcript for the GET_MEASUREMENTS request - ctx.reset_transcript_via_req_code(ReqRespCode::GetMeasurements); - - // Append the request to the transcript - ctx.append_message_to_transcript(req_payload, TranscriptContext::L1)?; - - let asym_algo = ctx.selected_base_asym_algo().map_err(|_| { - ctx.generate_error_response(req_payload, ErrorCode::InvalidRequest, 0, None) - })?; - - let get_meas_req_context = MeasurementsResponse { - spdm_version: connection_version, - req_attr: req_common.req_attr, - meas_op: req_common.meas_op, - slot_id, - requester_context, - asym_algo, - }; - - Ok(get_meas_req_context) -} - -pub(crate) fn generate_measurements_response<'a>( - ctx: &mut SpdmContext<'a>, - rsp_ctx: MeasurementsResponse, - rsp: &mut MessageBuf<'a>, -) -> CommandResult<()> { - let rsp_len = rsp_ctx.response_size(ctx.evidence, &mut ctx.measurements)?; - - if rsp_len > ctx.min_data_transfer_size() { - // If the response is larger than the minimum data transfer size, use chunked response - let large_rsp = LargeResponse::Measurements(rsp_ctx); - let handle = ctx.large_resp_context.init(large_rsp, rsp_len); - Err(ctx.generate_error_response(rsp, ErrorCode::LargeResponse, handle, None))? - } else { - // If the response fits in a single message, prepare it directly - ctx.prepare_response_buffer(rsp)?; - - // Encode the response fixed fields - rsp.put_data(rsp_len) - .map_err(|e| (false, CommandError::Codec(e)))?; - let rsp_buf = rsp - .data_mut(rsp_len) - .map_err(|e| (false, CommandError::Codec(e)))?; - let payload_len = rsp_ctx - .get_chunk( - ctx.hash, - ctx.rng, - ctx.evidence, - &mut ctx.measurements, - &mut ctx.transcript_mgr, - ctx.device_certs_store, - 0, - rsp_buf, - )?; - if rsp_len != payload_len { - Err(( - false, - CommandError::Measurement(MeasurementsError::InvalidBuffer), - ))?; - } - rsp.pull_data(payload_len) - .map_err(|e| (false, CommandError::Codec(e)))?; - - rsp.push_data(payload_len) - .map_err(|e| (false, CommandError::Codec(e))) - } -} - -pub(crate) fn handle_get_measurements<'a>( - ctx: &mut SpdmContext<'a>, - spdm_hdr: SpdmMsgHdr, - req_payload: &mut MessageBuf<'a>, -) -> CommandResult<()> { - // Check that the connection state is Negotiated - if ctx.state.connection_info.state() < ConnectionState::AlgorithmsNegotiated { - Err(ctx.generate_error_response(req_payload, ErrorCode::UnexpectedRequest, 0, None))?; - } - - // Check if the measurement capability is supported - if ctx.local_capabilities.flags.meas_cap() == MeasCapability::NoMeasurement as u8 { - return Err(ctx.generate_error_response( - req_payload, - ErrorCode::UnsupportedRequest, - 0, - None, - )); - } - - // Verify that the DMTF measurement spec is selected and the measurement hash algorithm is SHA384 - let meas_spec_sel = selected_measurement_specification(ctx); - if meas_spec_sel.dmtf_measurement_spec() == 0 || ctx.verify_selected_hash_algo().is_err() { - Err(ctx.generate_error_response(req_payload, ErrorCode::UnexpectedRequest, 0, None))?; - } - - // Process GET_MEASUREMENTS request - let rsp_ctx = process_get_measurements(ctx, spdm_hdr, req_payload)?; - - // Generate MEASUREMENTS response - ctx.prepare_response_buffer(req_payload)?; - generate_measurements_response(ctx, rsp_ctx, req_payload)?; - Ok(()) -} diff --git a/src/commands/mod.rs b/src/commands/mod.rs index 8d9da12..a1dc610 100644 --- a/src/commands/mod.rs +++ b/src/commands/mod.rs @@ -1,11 +1,23 @@ -// Licensed under the Apache-2.0 license +// Copyright 2025 +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. -pub mod algorithms_rsp; -pub mod capabilities_rsp; -pub mod certificate_rsp; -pub mod challenge_auth_rsp; +pub mod algorithms; +pub mod capabilities; +pub mod certificate; +pub mod challenge; pub mod chunk_get_rsp; -pub mod digests_rsp; +pub mod digests; pub mod error_rsp; -pub mod measurements_rsp; -pub mod version_rsp; +pub mod measurements; +pub mod version; diff --git a/src/commands/version/mod.rs b/src/commands/version/mod.rs new file mode 100644 index 0000000..792ced4 --- /dev/null +++ b/src/commands/version/mod.rs @@ -0,0 +1,124 @@ +// Copyright 2025 +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +pub mod request; +pub mod response; + +pub(crate) use request::*; +pub(crate) use response::*; + +use crate::{codec::CommonCodec, protocol::SpdmVersion}; +use bitfield::bitfield; +use zerocopy::{FromBytes, Immutable, IntoBytes}; + +const VERSION_ENTRY_SIZE: usize = 2; + +#[allow(dead_code)] +#[derive(FromBytes, IntoBytes, Immutable)] +pub struct VersionReqPayload { + param1: u8, + param2: u8, +} + +impl VersionReqPayload { + pub fn new(param1: u8, param2: u8) -> Self { + Self { param1, param2 } + } +} + +#[allow(dead_code)] +#[derive(FromBytes, IntoBytes, Immutable)] +struct VersionRespCommon { + param1: u8, + param2: u8, + reserved: u8, + version_num_entry_count: u8, +} + +impl CommonCodec for VersionReqPayload {} + +impl Default for VersionRespCommon { + fn default() -> Self { + VersionRespCommon::new(0) + } +} + +impl VersionRespCommon { + pub fn new(entry_count: u8) -> Self { + VersionRespCommon { + param1: 0, + param2: 0, + reserved: 0, + version_num_entry_count: entry_count, + } + } +} + +impl CommonCodec for VersionRespCommon {} + +bitfield! { +#[repr(C)] +#[derive(FromBytes, IntoBytes, Immutable)] +pub struct VersionNumberEntry(MSB0 [u8]); +impl Debug; +u8; + pub update_ver, set_update_ver: 3, 0; + pub alpha, set_alpha: 7, 4; + pub major, set_major: 11, 8; + pub minor, set_minor: 15, 12; +} + +impl Default for VersionNumberEntry<[u8; VERSION_ENTRY_SIZE]> { + fn default() -> Self { + VersionNumberEntry::new(SpdmVersion::default()) + } +} + +impl VersionNumberEntry<[u8; VERSION_ENTRY_SIZE]> { + pub fn new(version: SpdmVersion) -> Self { + let mut entry = VersionNumberEntry([0u8; VERSION_ENTRY_SIZE]); + entry.set_major(version.major()); + entry.set_minor(version.minor()); + entry + } +} + +impl CommonCodec for VersionNumberEntry<[u8; VERSION_ENTRY_SIZE]> {} + +pub struct FromVersionNumberEntryError; + +impl TryFrom> for SpdmVersion { + type Error = FromVersionNumberEntryError; + + fn try_from(value: VersionNumberEntry<[u8; VERSION_ENTRY_SIZE]>) -> Result { + match (value.major(), value.minor()) { + (1, 0) => Ok(SpdmVersion::V10), + (1, 1) => Ok(SpdmVersion::V11), + (1, 2) => Ok(SpdmVersion::V12), + (1, 3) => Ok(SpdmVersion::V13), + (_, _) => Err(FromVersionNumberEntryError), + } + } +} + +#[cfg(test)] +mod tests { + use crate::commands::version::VersionNumberEntry; + + #[test] + fn version_number_entry_roundtrip() { + let entry = VersionNumberEntry::new(crate::protocol::SpdmVersion::V13); + assert_eq!(entry.major(), 1); + assert_eq!(entry.minor(), 3); + } +} diff --git a/src/commands/version/request.rs b/src/commands/version/request.rs new file mode 100644 index 0000000..59fb7be --- /dev/null +++ b/src/commands/version/request.rs @@ -0,0 +1,243 @@ +// Copyright 2025 +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use crate::{ + codec::{Codec, MessageBuf}, + context::SpdmContext, + error::{CommandError, CommandResult}, + protocol::SpdmMsgHdr, + state::ConnectionState, + transcript::TranscriptContext, +}; + +use crate::commands::error_rsp::ErrorCode; +use crate::commands::version::{VersionNumberEntry, VersionReqPayload, VersionRespCommon}; + +use crate::protocol::SpdmVersion; + +/// Generate the GET_VERSION command with Header and payload and append it to the transcript context +/// See [crate::transcript::TranscriptContext::Vca] and [crate::transcript::TranscriptContext::M1] for details on the transcript context used. +pub fn generate_get_version<'a>( + ctx: &mut SpdmContext<'a>, + req_buf: &mut MessageBuf<'a>, + payload: VersionReqPayload, +) -> CommandResult<()> { + SpdmMsgHdr::new( + ctx.state.connection_info.version_number(), + crate::protocol::ReqRespCode::GetVersion, + ) + .encode(req_buf) + .map_err(|e| (false, CommandError::Codec(e)))?; + + let len = payload + .encode(req_buf) + .map_err(|e| (false, CommandError::Codec(e)))?; + + req_buf + .push_data(len) + .map_err(|_| (false, CommandError::BufferTooSmall))?; + + ctx.append_message_to_transcript(req_buf, TranscriptContext::Vca) +} + +/// Requester function for processing a VERSION response +/// +/// Updates the state of the context to match the selected version. +/// +/// # Returns +/// - The selected latest common supported version on success +/// - [CommandError::UnsupportedResponse] when no common version is found +/// - [CommandError::Codec] when decoding fails +fn process_version<'a>( + ctx: &mut SpdmContext<'a>, + spdm_hdr: SpdmMsgHdr, + resp_payload: &mut MessageBuf<'a>, +) -> CommandResult { + // VERSION response must use version 1.0 per spec + match spdm_hdr.version() { + Ok(SpdmVersion::V10) => {} + _ => { + Err((false, CommandError::UnsupportedResponse))?; + } + } + + // Decode the VERSION response common header + let resp = + VersionRespCommon::decode(resp_payload).map_err(|e| (false, CommandError::Codec(e)))?; + + let entry_count = resp.version_num_entry_count as usize; + + // Validate entry count + if entry_count == 0 { + Err((false, CommandError::UnsupportedResponse))?; + } + + // Decode all version entries from the response + let mut latest_version = None; + for _ in 0..entry_count { + let ver = VersionNumberEntry::decode(resp_payload) + .map_err(|e| (false, CommandError::Codec(e)))?; + if let Ok(ver) = SpdmVersion::try_from(ver) { + if let Some(lv) = latest_version.as_mut() { + if *lv < ver { + *lv = ver; + } + } else { + latest_version = Some(ver); + } + } + } + + if let Some(ver) = latest_version { + ctx.state.connection_info.set_version_number(ver); + ctx.transcript_mgr.set_spdm_version(ver); + Ok(ver) + } else { + Err((false, CommandError::UnsupportedResponse)) + } +} + +/// Requester function handling the parsing of the VERSION response sent by the Responder. +/// Updates the context with the selected version and appends the response to the transcript context. +/// See [crate::transcript::TranscriptContext::Vca] for details on the transcript context used. +pub(crate) fn handle_version_response<'a>( + ctx: &mut SpdmContext<'a>, + resp_header: SpdmMsgHdr, + resp: &mut MessageBuf<'a>, +) -> CommandResult<()> { + // Verify state is correct for VERSION response + if ctx.state.connection_info.state() != ConnectionState::NotStarted { + Err((false, CommandError::UnsupportedResponse))?; + // TODO: is there a better error for this? + Err(ctx.generate_error_response(resp, ErrorCode::InvalidResponseCode, 0, None))?; + } + + process_version(ctx, resp_header, resp)?; + + ctx.state + .connection_info + .set_state(ConnectionState::AfterVersion); + + ctx.append_message_to_transcript(resp, TranscriptContext::Vca) +} + +// tests +#[cfg(test)] +mod tests { + use super::*; + use crate::{protocol::MAX_MCTP_SPDM_MSG_SIZE, test::*}; + + #[test] + fn test_process_version_happy_path() { + let versions = versions_default(); + let mut stack = MockResources::new(); + let algorithms = crate::protocol::LocalDeviceAlgorithms::default(); + let mut context = create_context(&mut stack, &versions, algorithms); + + let header = SpdmMsgHdr::new(SpdmVersion::V10, crate::protocol::ReqRespCode::Version); + + let mut msg_buf = [0; MAX_MCTP_SPDM_MSG_SIZE]; + let mut msg = MessageBuf::new(&mut msg_buf); + let version_response = VersionRespCommon::new(2); + let resp_common_size = version_response.encode(&mut msg).unwrap(); + let entr_1 = VersionNumberEntry::new(SpdmVersion::V11); + let e1_size = entr_1.encode(&mut msg).unwrap(); + let entr_2 = VersionNumberEntry::new(SpdmVersion::V12); + let e2_size = entr_2.encode(&mut msg).unwrap(); + msg.push_data(resp_common_size + e1_size + e2_size).unwrap(); // This has to be done at the end of encoding for some reason + assert_eq!(msg.data_len(), (resp_common_size + e1_size + e2_size)); + + let rsp = process_version(&mut context, header, &mut msg); + assert!( + rsp.is_ok(), + "process_version returned error: {:?}", + rsp.unwrap_err() + ); + + let rsp = rsp.unwrap(); + + assert_eq!(rsp, SpdmVersion::V12); + } + + #[test] + fn test_process_version_malformed_response() { + let versions = versions_default(); + let mut stack = MockResources::new(); + let algorithms = crate::protocol::LocalDeviceAlgorithms::default(); + let mut context = create_context(&mut stack, &versions, algorithms); + + // Test wrong header + let header = SpdmMsgHdr::new(SpdmVersion::V12, crate::protocol::ReqRespCode::Version); + + let mut msg_buf = [0; MAX_MCTP_SPDM_MSG_SIZE]; + let mut msg = MessageBuf::new(&mut msg_buf); + let version_response = VersionRespCommon::new(2); + let resp_common_size = version_response.encode(&mut msg).unwrap(); + let entr_1 = VersionNumberEntry::new(SpdmVersion::V11); + let e1_size = entr_1.encode(&mut msg).unwrap(); + msg.push_data(resp_common_size + e1_size).unwrap(); + assert_eq!(msg.data_len(), (resp_common_size + e1_size)); + + let rsp = process_version(&mut context, header, &mut msg); + assert!(rsp.is_err_and(|e| e.1 == CommandError::UnsupportedResponse)); + + // Test response without entries + let header = SpdmMsgHdr::new(SpdmVersion::V12, crate::protocol::ReqRespCode::Version); + + let mut msg_buf = [0; MAX_MCTP_SPDM_MSG_SIZE]; + let mut msg = MessageBuf::new(&mut msg_buf); + let version_response = VersionRespCommon::new(0); + let resp_common_size = version_response.encode(&mut msg).unwrap(); + msg.push_data(resp_common_size).unwrap(); + + let rsp = process_version(&mut context, header, &mut msg); + assert!(rsp.is_err_and(|e| e.1 == CommandError::UnsupportedResponse)); + + // Test unsupported version + let header = SpdmMsgHdr::new(SpdmVersion::V12, crate::protocol::ReqRespCode::Version); + + let mut msg_buf = [0; MAX_MCTP_SPDM_MSG_SIZE]; + let mut msg = MessageBuf::new(&mut msg_buf); + let version_response = VersionRespCommon::new(1); + let resp_common_size = version_response.encode(&mut msg).unwrap(); + let mut entr_1 = VersionNumberEntry::new(SpdmVersion::V10); + entr_1.set_major(9); // unsupported version + let e1_size = entr_1.encode(&mut msg).unwrap(); + msg.push_data(resp_common_size + e1_size).unwrap(); + assert_eq!(msg.data_len(), (resp_common_size + e1_size)); + + let rsp = process_version(&mut context, header, &mut msg); + assert!(rsp.is_err_and(|e| e.1 == CommandError::UnsupportedResponse)); + } + + #[test] + fn validate_get_version_request() { + let versions = versions_default(); + let mut stack = MockResources::new(); + let algorithms = crate::protocol::LocalDeviceAlgorithms::default(); + let mut context = create_context(&mut stack, &versions, algorithms); + + let mut msg_buf = [0; MAX_MCTP_SPDM_MSG_SIZE]; + let mut msg = MessageBuf::new(&mut msg_buf); + + assert!(generate_get_version(&mut context, &mut msg, VersionReqPayload::new(0, 0)).is_ok()); + + let data = msg.total_message(); + assert_eq!(data.len(), 4, "GET_VERSION command length mismatch"); + let req_version: SpdmVersion = data[0].try_into().unwrap(); + assert_eq!(req_version, SpdmVersion::V10); + + assert_eq!(data[1], 0x84, "Command code doesn't match GET_VERSION"); + } +} diff --git a/src/commands/version_rsp.rs b/src/commands/version/response.rs similarity index 60% rename from src/commands/version_rsp.rs rename to src/commands/version/response.rs index a518edc..cee08ba 100644 --- a/src/commands/version_rsp.rs +++ b/src/commands/version/response.rs @@ -1,82 +1,25 @@ -// Licensed under the Apache-2.0 license - -use crate::codec::{Codec, CommonCodec, MessageBuf}; +// Copyright 2025 +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use crate::codec::{Codec, MessageBuf}; use crate::commands::error_rsp::ErrorCode; +use crate::commands::version::{VersionNumberEntry, VersionReqPayload, VersionRespCommon}; use crate::context::SpdmContext; use crate::error::{CommandError, CommandResult}; use crate::protocol::{ReqRespCode, SpdmMsgHdr, SpdmVersion}; use crate::state::ConnectionState; use crate::transcript::TranscriptContext; -use bitfield::bitfield; -use zerocopy::{FromBytes, Immutable, IntoBytes}; - -const VERSION_ENTRY_SIZE: usize = 2; - -#[allow(dead_code)] -#[derive(FromBytes, IntoBytes, Immutable)] -struct VersionReqPayload { - param1: u8, - param2: u8, -} - -#[allow(dead_code)] -#[derive(FromBytes, IntoBytes, Immutable)] -struct VersionRespCommon { - param1: u8, - param2: u8, - reserved: u8, - version_num_entry_count: u8, -} - -impl CommonCodec for VersionReqPayload {} - -impl Default for VersionRespCommon { - fn default() -> Self { - VersionRespCommon::new(0) - } -} - -impl VersionRespCommon { - pub fn new(entry_count: u8) -> Self { - VersionRespCommon { - param1: 0, - param2: 0, - reserved: 0, - version_num_entry_count: entry_count, - } - } -} - -impl CommonCodec for VersionRespCommon {} - -bitfield! { -#[repr(C)] -#[derive(FromBytes, IntoBytes, Immutable)] -pub struct VersionNumberEntry(MSB0 [u8]); -impl Debug; -u8; - pub update_ver, set_update_ver: 3, 0; - pub alpha, set_alpha: 7, 4; - pub major, set_major: 11, 8; - pub minor, set_minor: 15, 12; -} - -impl Default for VersionNumberEntry<[u8; VERSION_ENTRY_SIZE]> { - fn default() -> Self { - VersionNumberEntry::new(SpdmVersion::default()) - } -} - -impl VersionNumberEntry<[u8; VERSION_ENTRY_SIZE]> { - pub fn new(version: SpdmVersion) -> Self { - let mut entry = VersionNumberEntry([0u8; VERSION_ENTRY_SIZE]); - entry.set_major(version.major()); - entry.set_minor(version.minor()); - entry - } -} - -impl CommonCodec for VersionNumberEntry<[u8; VERSION_ENTRY_SIZE]> {} fn generate_version_response<'a>( ctx: &mut SpdmContext<'a>, diff --git a/src/context.rs b/src/context.rs index 17046d7..bbd5f4f 100644 --- a/src/context.rs +++ b/src/context.rs @@ -1,32 +1,49 @@ -// Licensed under the Apache-2.0 license +// Copyright 2025 +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. // use crate::cert_mgr::DeviceCertsManager; use crate::cert_store::*; use crate::chunk_ctx::LargeResponseCtx; use crate::codec::{Codec, MessageBuf}; +use crate::commands::capabilities::handle_capabilities_response; +use crate::commands::challenge::handle_challenge_auth_response; +use crate::commands::digests::{handle_digests_response, handle_get_digests}; use crate::commands::error_rsp::{encode_error_response, ErrorCode}; +use crate::commands::measurements::request::handle_measurements_response; +use crate::commands::version::handle_version_response; use crate::commands::{ - algorithms_rsp, capabilities_rsp, certificate_rsp, challenge_auth_rsp, chunk_get_rsp, - digests_rsp, measurements_rsp, version_rsp, + algorithms, capabilities, certificate, challenge, chunk_get_rsp, measurements, version, }; + use crate::error::*; use crate::measurements::common::SpdmMeasurements; use crate::platform::evidence::SpdmEvidence; +use crate::platform::hash::SpdmHash; +use crate::platform::rng::{SpdmRng, SpdmRngResult}; +use crate::platform::transport::SpdmTransport; use crate::protocol::algorithms::*; use crate::protocol::common::{ReqRespCode, SpdmMsgHdr}; use crate::protocol::version::*; use crate::protocol::DeviceCapabilities; -use crate::state::{ConnectionState, State}; +use crate::state::{ConnectionInfo, ConnectionState, State}; use crate::transcript::{TranscriptContext, TranscriptManager}; -use crate::platform::transport::SpdmTransport; -use crate::platform::hash::SpdmHash; -use crate::platform::rng::SpdmRng; pub struct SpdmContext<'a> { transport: &'a mut dyn SpdmTransport, pub(crate) hash: &'a mut dyn SpdmHash, pub(crate) supported_versions: &'a [SpdmVersion], - pub(crate) state: State, + pub(crate) state: State<'a>, pub(crate) transcript_mgr: TranscriptManager<'a>, pub(crate) rng: &'a mut dyn SpdmRng, pub(crate) local_capabilities: DeviceCapabilities, @@ -44,6 +61,7 @@ impl<'a> SpdmContext<'a> { local_capabilities: DeviceCapabilities, local_algorithms: LocalDeviceAlgorithms<'a>, device_certs_store: &'a mut dyn SpdmCertStore, + peer_cert_store: Option<&'a mut dyn PeerCertStore>, hash: &'a mut dyn SpdmHash, m1: &'a mut dyn SpdmHash, l1: &'a mut dyn SpdmHash, @@ -51,33 +69,42 @@ impl<'a> SpdmContext<'a> { evidence: &'a dyn SpdmEvidence, ) -> SpdmResult { validate_supported_versions(supported_versions)?; - validate_device_algorithms(&local_algorithms)?; - validate_cert_store(device_certs_store)?; Ok(Self { supported_versions, transport: spdm_transport, - state: State::new(), + state: State::new(peer_cert_store), transcript_mgr: TranscriptManager::new(m1, l1), local_capabilities, local_algorithms, device_certs_store, measurements: SpdmMeasurements::default(), large_resp_context: LargeResponseCtx::default(), - hash: hash, - rng: rng, - evidence: evidence, + hash, + rng, + evidence, }) } - pub fn process_message(&mut self, msg_buf: &mut MessageBuf<'a>) -> SpdmResult<()> { + pub fn connection_info(&self) -> &ConnectionInfo { + &self.state.connection_info + } + + pub fn transport_init_sequence(&mut self) -> SpdmResult<()> { + self.transport + .init_sequence() + .map_err(|e| SpdmError::Transport(e)) + } + + /// The Responder receives a request message sent by the Requester and processes it accordingly. + pub fn responder_process_message(&mut self, msg_buf: &mut MessageBuf<'a>) -> SpdmResult<()> { self.transport .receive_request(msg_buf) .map_err(SpdmError::Transport)?; - match self.handle_request(msg_buf) { + match self.responder_handle_request(msg_buf) { Ok(()) => { self.send_response(msg_buf)?; } @@ -91,7 +118,55 @@ impl<'a> SpdmContext<'a> { Ok(()) } - fn handle_request(&mut self, buf: &mut MessageBuf<'a>) -> CommandResult<()> { + /// The Requester receives a response message sent by the Responder and processes it accordingly. + /// + /// # Arguments + /// * `resp_buffer`: buffer the message is received into from the transport medium. + /// + /// # Warning + /// This function resets all data initially stored in then resp_buffer. + pub fn requester_process_message( + &mut self, + resp_buffer: &mut MessageBuf<'a>, + ) -> SpdmResult<()> { + resp_buffer.reset(); + self.transport + .receive_response(resp_buffer) + .map_err(SpdmError::Transport)?; + + match self + .requester_handle_response(resp_buffer) + .map_err(|(rsp, cmd_err)| { + if rsp { + SpdmError::Command(cmd_err) + } else { + SpdmError::InvalidParam + } + }) { + Ok(()) => {} + Err(e) => { + return Err(e); + } + } + Ok(()) + } + + // Use ReqRespCode as command issuer for now, until the correct state machine is in place + // TODO: implement in transport + pub fn requester_send_request( + &mut self, + req_buf: &mut MessageBuf<'a>, + dst_eid: u8, + ) -> SpdmResult<()> { + self.transport + .send_request(dst_eid, req_buf) + .map_err(|_| SpdmError::InvalidParam)?; + + Ok(()) + } + + /// The responder handles incoming requests and responds to them accordingly. + fn responder_handle_request(&mut self, buf: &mut MessageBuf<'a>) -> CommandResult<()> { let req = buf; let req_msg_header: SpdmMsgHdr = @@ -107,13 +182,21 @@ impl<'a> SpdmContext<'a> { } match req_code { - ReqRespCode::GetVersion => version_rsp::handle_get_version(self, req_msg_header, req)?, - ReqRespCode::GetCapabilities => capabilities_rsp::handle_get_capabilities(self, req_msg_header, req)?, - ReqRespCode::NegotiateAlgorithms => algorithms_rsp::handle_negotiate_algorithms(self, req_msg_header, req)?, - ReqRespCode::GetDigests => digests_rsp::handle_get_digests(self, req_msg_header, req)?, - ReqRespCode::GetCertificate => certificate_rsp::handle_get_certificate(self, req_msg_header, req)?, - ReqRespCode::Challenge => challenge_auth_rsp::handle_challenge(self, req_msg_header, req)?, - ReqRespCode::GetMeasurements => measurements_rsp::handle_get_measurements(self, req_msg_header, req)?, + ReqRespCode::GetVersion => version::handle_get_version(self, req_msg_header, req)?, + ReqRespCode::GetCapabilities => { + capabilities::handle_get_capabilities(self, req_msg_header, req)? + } + ReqRespCode::NegotiateAlgorithms => { + algorithms::handle_negotiate_algorithms(self, req_msg_header, req)? + } + ReqRespCode::GetDigests => handle_get_digests(self, req_msg_header, req)?, + ReqRespCode::GetCertificate => { + certificate::handle_get_certificate(self, req_msg_header, req)? + } + ReqRespCode::Challenge => challenge::handle_challenge(self, req_msg_header, req)?, + ReqRespCode::GetMeasurements => { + measurements::response::handle_get_measurements(self, req_msg_header, req)? + } ReqRespCode::ChunkGet => chunk_get_rsp::handle_chunk_get(self, req_msg_header, req)?, _ => Err((false, CommandError::UnsupportedRequest))?, @@ -121,8 +204,52 @@ impl<'a> SpdmContext<'a> { Ok(()) } + /// Requester function parsing and processing messages provided in `buf`. + /// + /// # Arguments + /// * `buf`: Message buffer containing a raw response. + fn requester_handle_response(&mut self, buf: &mut MessageBuf<'a>) -> CommandResult<()> { + let resp = buf; + let resp_msg_header: SpdmMsgHdr = + SpdmMsgHdr::decode(resp).map_err(|e| (false, CommandError::Codec(e)))?; + + let resp_code = resp_msg_header + .req_resp_code() + .map_err(|_| (false, CommandError::UnsupportedRequest))?; + + if resp_code.is_request() { + Err((false, CommandError::UnsupportedRequest))? + } + + // if req_code != ReqRespCode::ChunkGet && self.large_resp_context.in_progress() { + // // Reset large response context if the request is not a CHUNK_GET + // self.large_resp_context.reset(); + // } + + match resp_code { + ReqRespCode::Version => handle_version_response(self, resp_msg_header, resp)?, + ReqRespCode::Capabilities => handle_capabilities_response(self, resp_msg_header, resp)?, + ReqRespCode::Algorithms => { + algorithms::handle_algorithms_response(self, resp_msg_header, resp)? + } + ReqRespCode::Digests => handle_digests_response(self, resp_msg_header, resp)?, + ReqRespCode::Certificate => { + certificate::request::handle_certificate_response(self, resp_msg_header, resp)? + } + ReqRespCode::ChallengeAuth => { + handle_challenge_auth_response(self, resp_msg_header, resp)? + } + ReqRespCode::Measurements => handle_measurements_response(self, resp_msg_header, resp)?, + _ => Err((false, CommandError::UnsupportedResponse))?, + } + + Ok(()) + } + fn send_response(&mut self, resp: &mut MessageBuf<'a>) -> SpdmResult<()> { - self.transport.send_response(resp).map_err(SpdmError::Transport) + self.transport + .send_response(resp) + .map_err(SpdmError::Transport) } pub(crate) fn prepare_response_buffer(&self, rsp_buf: &mut MessageBuf) -> CommandResult<()> { @@ -231,4 +358,66 @@ impl<'a> SpdmContext<'a> { .append(transcript_context, msg) .map_err(|e| (false, CommandError::Transcript(e))) } + + pub fn peer_cert_store(&self) -> Option<&dyn PeerCertStore> { + self.state.peer_cert_store.as_deref() + } + + /// To safeguard the user-facing API, we prohibit the retrieval of hashes unless the context is in a valid state. + /// These states are: + /// - [`ConnectionState::AfterCertificate`] for the M1 transcript context + /// - [`ConnectionState::Authenticated`] for the L2 transcript context + /// + /// # Arguments + /// - `transcript_context`: The transcript context for which the hash is being requested. + pub fn transcript_hash( + &mut self, + transcript_context: TranscriptContext, + hash: &mut [u8], + ) -> CommandResult<()> { + match transcript_context { + TranscriptContext::M1 => { + if self.state.connection_info.state() < ConnectionState::AfterCertificate { + return Err((false, CommandError::InvalidState)); + } + } + TranscriptContext::L1 => { + if self.state.connection_info.state() < ConnectionState::Authenticated { + return Err((false, CommandError::InvalidState)); + } + } + TranscriptContext::Vca => { + return Err((false, CommandError::InvalidState)); + } + } + + let mut hash_out_max = [0u8; 48]; + self.transcript_mgr + .hash(transcript_context, &mut hash_out_max) + .map_err(|e| (false, CommandError::Transcript(e)))?; + + if hash.len() > hash_out_max.len() { + return Err((false, CommandError::BufferTooSmall)); + } + + hash.copy_from_slice(&hash_out_max[..hash.len()]); + Ok(()) + } + + /// Expose the `SystemRng` function `get_random_bytes` to context. + /// + /// # Arguments + /// - `dest`: Destination buffer that holds the random bytes. + pub fn get_random_bytes(&mut self, dest: &mut [u8]) -> SpdmRngResult<()> { + self.rng.get_random_bytes(dest) + } + + /// Set the connection state to authenticated + /// + /// Should be called after after the signature of a CHALLENGE response has been verified. + pub fn set_authenticated(&mut self) { + self.state + .connection_info + .set_state(ConnectionState::Authenticated); + } } diff --git a/src/error.rs b/src/error.rs index 6ee0f47..d2b5ba1 100644 --- a/src/error.rs +++ b/src/error.rs @@ -1,4 +1,16 @@ -// Licensed under the Apache-2.0 license +// Copyright 2025 +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. // use crate::cert_mgr::DeviceCertsMgrError; use crate::cert_store::CertStoreError; @@ -6,12 +18,12 @@ use crate::chunk_ctx::ChunkError; use crate::codec::CodecError; use crate::commands::error_rsp::ErrorCode; use crate::measurements::common::MeasurementsError; +use crate::platform::evidence::SpdmEvidenceError; +use crate::platform::hash::SpdmHashError; use crate::platform::rng::SpdmRngError; +use crate::platform::transport::TransportError; use crate::protocol::SignCtxError; use crate::transcript::TranscriptError; -use crate::platform::transport::TransportError; -use crate::platform::hash::SpdmHashError; -use crate::platform::evidence::SpdmEvidenceError; #[derive(Debug)] pub enum SpdmError { @@ -29,6 +41,7 @@ pub type SpdmResult = Result; pub type CommandResult = Result; +#[non_exhaustive] #[derive(Debug, PartialEq)] pub enum PlatformError { HashError(SpdmHashError), @@ -36,12 +49,20 @@ pub enum PlatformError { EvidenceError(SpdmEvidenceError), } +impl From for PlatformError { + fn from(value: SpdmRngError) -> Self { + PlatformError::RngError(value) + } +} + +#[non_exhaustive] #[derive(Debug, PartialEq)] pub enum CommandError { BufferTooSmall, Codec(CodecError), ErrorCode(ErrorCode), UnsupportedRequest, + UnsupportedResponse, SignCtx(SignCtxError), InvalidChunkContext, Chunk(ChunkError), @@ -49,4 +70,25 @@ pub enum CommandError { Platform(PlatformError), Transcript(TranscriptError), Measurement(MeasurementsError), + InvalidResponse, + /// An invalid state was encountered (this is likely a bug) + InvalidState, + /// This is a Bug + /// + /// Used in spots which should be infallible. + /// This can either be a bug in this crate, + /// in a dependency, or a uncaught platform misbehavior. + InternalError, +} + +impl From for CommandError { + fn from(value: PlatformError) -> Self { + CommandError::Platform(value) + } +} + +impl From for CommandError { + fn from(value: CodecError) -> Self { + CommandError::Codec(value) + } } diff --git a/src/lib.rs b/src/lib.rs index 1d5942b..549de3c 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,34 +1,50 @@ -// Licensed under the Apache-2.0 license - -#![cfg_attr(not(feature = "std"), no_std)] - -// Common errors +// Copyright 2025 +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#![cfg_attr(not(test), no_std)] + +/// Common errors pub mod error; -// Codec and protocol buffer +/// Codec and protocol buffer pub mod codec; -// Spdm common message protocol handling +/// Spdm common message protocol handling pub mod protocol; -// Context and request handling +/// Context and request handling pub mod commands; pub mod context; -// Spdm responder state +/// Spdm responder state pub mod state; -// Device certificate management +/// Device certificate management pub mod cert_store; -// Transcript management +/// Transcript management pub mod transcript; -// Spdm measurements management +/// Spdm measurements management pub mod measurements; -// Chunking context for large messages +/// Chunking context for large messages pub mod chunk_ctx; -// Platform-specific traits +/// Platform-specific traits pub mod platform; + +/// Mock implementations for unit tests +#[cfg(test)] +pub(crate) mod test; diff --git a/src/measurements/common.rs b/src/measurements/common.rs index 14f06e1..a1487e5 100644 --- a/src/measurements/common.rs +++ b/src/measurements/common.rs @@ -1,8 +1,21 @@ -// Licensed under the Apache-2.0 license +// Copyright 2025 +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +use crate::commands::challenge::MeasurementSummaryHashType; use crate::measurements::freeform_manifest::FreeformManifest; -use crate::protocol::{algorithms::AsymAlgo, MeasurementSpecification, SHA384_HASH_SIZE}; -use crate::platform::hash::{SpdmHash, SpdmHashError}; use crate::platform::evidence::{SpdmEvidence, SpdmEvidenceError}; +use crate::platform::hash::{SpdmHash, SpdmHashError}; +use crate::protocol::{algorithms::AsymAlgo, MeasurementSpecification, SHA384_HASH_SIZE}; use bitfield::bitfield; use zerocopy::{FromBytes, Immutable, IntoBytes}; @@ -102,9 +115,14 @@ impl SpdmMeasurements { measurement_chunk: &mut [u8], ) -> MeasurementsResult { match self { - SpdmMeasurements::FreeformManifest(manifest) => { - manifest.measurement_block(evidence, asym_algo, index, raw_bit_stream, offset, measurement_chunk) - } + SpdmMeasurements::FreeformManifest(manifest) => manifest.measurement_block( + evidence, + asym_algo, + index, + raw_bit_stream, + offset, + measurement_chunk, + ), } } @@ -125,13 +143,17 @@ impl SpdmMeasurements { evidence: &dyn SpdmEvidence, hash_ctx: &mut dyn SpdmHash, asym_algo: AsymAlgo, - measurement_summary_hash_type: u8, + measurement_summary_hash_type: MeasurementSummaryHashType, hash: &mut [u8; SHA384_HASH_SIZE], ) -> MeasurementsResult<()> { match self { - SpdmMeasurements::FreeformManifest(manifest) => { - manifest.measurement_summary_hash(evidence, hash_ctx, asym_algo, measurement_summary_hash_type, hash) - } + SpdmMeasurements::FreeformManifest(manifest) => manifest.measurement_summary_hash( + evidence, + hash_ctx, + asym_algo, + measurement_summary_hash_type, + hash, + ), } } } diff --git a/src/measurements/freeform_manifest.rs b/src/measurements/freeform_manifest.rs index e2e7cff..9455800 100644 --- a/src/measurements/freeform_manifest.rs +++ b/src/measurements/freeform_manifest.rs @@ -1,12 +1,25 @@ -// Licensed under the Apache-2.0 license - +// Copyright 2025 +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use crate::commands::challenge::MeasurementSummaryHashType; use crate::measurements::common::{ DmtfMeasurementBlockMetadata, MeasurementValueType, MeasurementsError, MeasurementsResult, SPDM_MEASUREMENT_MANIFEST_INDEX, }; -use crate::protocol::{algorithms::AsymAlgo, SHA384_HASH_SIZE}; -use crate::platform::hash::SpdmHash; use crate::platform::evidence::{SpdmEvidence, PCR_QUOTE_BUFFER_SIZE}; +use crate::platform::hash::SpdmHash; +use crate::protocol::{algorithms::AsymAlgo, SHA384_HASH_SIZE}; use zerocopy::IntoBytes; const MAX_MEASUREMENT_RECORD_SIZE: usize = @@ -91,7 +104,7 @@ impl FreeformManifest { evidence: &dyn SpdmEvidence, hash_ctx: &mut dyn SpdmHash, asym_algo: AsymAlgo, - _measurement_summary_hash_type: u8, + _measurement_summary_hash_type: MeasurementSummaryHashType, hash: &mut [u8; SHA384_HASH_SIZE], ) -> MeasurementsResult<()> { self.refresh_measurement_record(evidence, asym_algo)?; @@ -103,23 +116,35 @@ impl FreeformManifest { if offset == 0 { hash_ctx - .init(hash_ctx.algo(), Some(&self.measurement_record[..chunk_size])) + .init( + hash_ctx.algo(), + Some(&self.measurement_record[..chunk_size]), + ) .map_err(|e| MeasurementsError::Hash(e))?; } else { let chunk = &self.measurement_record[offset..offset + chunk_size]; - hash_ctx.update(chunk).map_err(|e| MeasurementsError::Hash(e))?; + hash_ctx + .update(chunk) + .map_err(|e| MeasurementsError::Hash(e))?; } offset += chunk_size; } - hash_ctx.finalize(hash).map_err(|e| MeasurementsError::Hash(e)) + hash_ctx + .finalize(hash) + .map_err(|e| MeasurementsError::Hash(e)) } - fn refresh_measurement_record(&mut self, evidence: &dyn SpdmEvidence, asym_algo: AsymAlgo) -> MeasurementsResult<()> { + fn refresh_measurement_record( + &mut self, + evidence: &dyn SpdmEvidence, + asym_algo: AsymAlgo, + ) -> MeasurementsResult<()> { let with_pqc_sig = asym_algo != AsymAlgo::EccP384; let measurement_record = &mut self.measurement_record; - let measurement_value_size = evidence.pcr_quote_size(with_pqc_sig) + let measurement_value_size = evidence + .pcr_quote_size(with_pqc_sig) .map_err(|e| MeasurementsError::Evidence(e))?; measurement_record.fill(0); let metadata = DmtfMeasurementBlockMetadata::new( @@ -136,8 +161,9 @@ impl FreeformManifest { let quote_slice = &mut measurement_record[METADATA_SIZE..METADATA_SIZE + PCR_QUOTE_BUFFER_SIZE]; - let copied_len = evidence.pcr_quote(quote_slice, with_pqc_sig) - .map_err(|e| MeasurementsError::Evidence(e))?; + let copied_len = evidence + .pcr_quote(quote_slice, with_pqc_sig) + .map_err(MeasurementsError::Evidence)?; if copied_len != measurement_value_size { return Err(MeasurementsError::MeasurementSizeMismatch); } diff --git a/src/measurements/mod.rs b/src/measurements/mod.rs index 09d1cff..bab3658 100644 --- a/src/measurements/mod.rs +++ b/src/measurements/mod.rs @@ -1,4 +1,16 @@ -// Licensed under the Apache-2.0 license +// Copyright 2025 +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. pub mod common; pub mod freeform_manifest; diff --git a/src/platform/evidence.rs b/src/platform/evidence.rs index 8710329..fd9ff61 100644 --- a/src/platform/evidence.rs +++ b/src/platform/evidence.rs @@ -1,3 +1,17 @@ +// Copyright 2025 +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + pub const PCR_QUOTE_BUFFER_SIZE: usize = 0x1984; pub type SpdmEvidenceResult = Result; diff --git a/src/platform/hash.rs b/src/platform/hash.rs index ed8d0c0..12269da 100644 --- a/src/platform/hash.rs +++ b/src/platform/hash.rs @@ -1,13 +1,32 @@ +// Copyright 2025 +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + pub type SpdmHashResult = Result; pub trait SpdmHash { - fn hash(&mut self, hash_algo: SpdmHashAlgoType, data: &[u8], hash: &mut [u8]) -> SpdmHashResult<()>; + fn hash( + &mut self, + hash_algo: SpdmHashAlgoType, + data: &[u8], + hash: &mut [u8], + ) -> SpdmHashResult<()>; fn init(&mut self, hash_algo: SpdmHashAlgoType, data: Option<&[u8]>) -> SpdmHashResult<()>; fn update(&mut self, data: &[u8]) -> SpdmHashResult<()>; fn finalize(&mut self, hash: &mut [u8]) -> SpdmHashResult<()>; fn reset(&mut self); - fn algo(&self) -> SpdmHashAlgoType; + fn algo(&self) -> SpdmHashAlgoType; } #[derive(Debug, PartialEq)] @@ -39,4 +58,4 @@ impl SpdmHashAlgoType { SpdmHashAlgoType::SHA512 => 64, } } -} \ No newline at end of file +} diff --git a/src/platform/mod.rs b/src/platform/mod.rs index b5ec485..db8bf60 100644 --- a/src/platform/mod.rs +++ b/src/platform/mod.rs @@ -1,4 +1,18 @@ +// Copyright 2025 +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +pub mod evidence; pub mod hash; pub mod rng; -pub mod evidence; -pub mod transport; \ No newline at end of file +pub mod transport; diff --git a/src/platform/rng.rs b/src/platform/rng.rs index a7ca9a2..5ead220 100644 --- a/src/platform/rng.rs +++ b/src/platform/rng.rs @@ -1,3 +1,17 @@ +// Copyright 2025 +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + pub type SpdmRngResult = Result; #[derive(Debug, PartialEq)] @@ -8,4 +22,4 @@ pub enum SpdmRngError { pub trait SpdmRng { fn get_random_bytes(&mut self, buf: &mut [u8]) -> SpdmRngResult<()>; fn generate_random_number(&mut self, random_number: &mut [u8]) -> SpdmRngResult<()>; -} \ No newline at end of file +} diff --git a/src/platform/transport.rs b/src/platform/transport.rs index cf67aad..e9c7588 100644 --- a/src/platform/transport.rs +++ b/src/platform/transport.rs @@ -1,14 +1,30 @@ +// Copyright 2025 +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. -use crate::codec::{MessageBuf, CodecError}; +use crate::codec::{CodecError, MessageBuf}; pub type TransportResult = Result; pub trait SpdmTransport { - fn send_request<'a>( - &mut self, - dest_eid: u8, - req: &mut MessageBuf<'a>, - ) -> TransportResult<()>; + /// Initialize any transport-specific sequence state. + /// + /// # Note + /// It is expected that this function may perform multiple I/O operations, + /// such as sending and receiving messages, to establish the transport session. + fn init_sequence(&mut self) -> TransportResult<()>; + + fn send_request<'a>(&mut self, dest_eid: u8, req: &mut MessageBuf<'a>) -> TransportResult<()>; fn receive_response<'a>(&mut self, rsp: &mut MessageBuf<'a>) -> TransportResult<()>; fn receive_request<'a>(&mut self, req: &mut MessageBuf<'a>) -> TransportResult<()>; fn send_response<'a>(&mut self, resp: &mut MessageBuf<'a>) -> TransportResult<()>; @@ -26,4 +42,8 @@ pub enum TransportError { SendError, ResponseNotExpected, NoRequestInFlight, -} \ No newline at end of file + UnsupportedTransportType, + + /// Error specific to SOCKET_TRANSPORT_TYPE_NONE handshake + HandshakeNoneError, +} diff --git a/src/protocol/algorithms.rs b/src/protocol/algorithms.rs index 1532559..3cb9271 100644 --- a/src/protocol/algorithms.rs +++ b/src/protocol/algorithms.rs @@ -1,8 +1,20 @@ -// Licensed under the Apache-2.0 license +// Copyright 2025 +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. use crate::error::{SpdmError, SpdmResult}; use bitfield::bitfield; -use zerocopy::{FromBytes, Immutable, IntoBytes}; +use zerocopy::{FromBytes, Immutable, IntoBytes, Unaligned}; pub const SHA384_HASH_SIZE: usize = 48; pub const ECC_P384_SIGNATURE_SIZE: usize = 96; @@ -108,7 +120,7 @@ where // Measurement Specification field bitfield! { -#[derive(FromBytes, IntoBytes, Immutable, Default, Clone, Copy)] +#[derive(FromBytes, IntoBytes, Immutable, Default, Clone, Copy, Unaligned)] #[repr(C)] pub struct MeasurementSpecification(u8); impl Debug; @@ -129,6 +141,16 @@ impl From for u8 { } } } +impl TryFrom for MeasurementSpecificationType { + type Error = (); + + fn try_from(value: MeasurementSpecification) -> Result { + match value.0 { + 1 => Ok(Self::DmtfMeasurementSpec), + _ => Err(()), + } + } +} // Other Param Support Field for request and response bitfield! { @@ -289,15 +311,16 @@ impl Prioritize for BaseHashAlgo { } } +/// BaseHashAlgo variants #[derive(Debug, Clone, Copy)] pub enum BaseHashAlgoType { - TpmAlgSha256, - TpmAlgSha384, - TpmAlgSha512, - TpmAlgSha3_256, - TpmAlgSha3_384, - TpmAlgSha3_512, - TpmAlgSm3_256, + TpmAlgSha256 = 1, + TpmAlgSha384 = 2, + TpmAlgSha512 = 4, + TpmAlgSha3_256 = 8, + TpmAlgSha3_384 = 16, + TpmAlgSha3_512 = 32, + TpmAlgSm3_256 = 64, } impl TryFrom for BaseHashAlgoType { @@ -305,18 +328,47 @@ impl TryFrom for BaseHashAlgoType { fn try_from(value: u8) -> Result { match value { - 0 => Ok(BaseHashAlgoType::TpmAlgSha256), - 1 => Ok(BaseHashAlgoType::TpmAlgSha384), - 2 => Ok(BaseHashAlgoType::TpmAlgSha512), - 3 => Ok(BaseHashAlgoType::TpmAlgSha3_256), - 4 => Ok(BaseHashAlgoType::TpmAlgSha3_384), - 5 => Ok(BaseHashAlgoType::TpmAlgSha3_512), - 6 => Ok(BaseHashAlgoType::TpmAlgSm3_256), + 1 => Ok(BaseHashAlgoType::TpmAlgSha256), + 2 => Ok(BaseHashAlgoType::TpmAlgSha384), + 4 => Ok(BaseHashAlgoType::TpmAlgSha512), + 8 => Ok(BaseHashAlgoType::TpmAlgSha3_256), + 16 => Ok(BaseHashAlgoType::TpmAlgSha3_384), + 32 => Ok(BaseHashAlgoType::TpmAlgSha3_512), + 64 => Ok(BaseHashAlgoType::TpmAlgSm3_256), _ => Err(SpdmError::InvalidParam), } } } +impl BaseHashAlgoType { + /// The size of the digest in bytes + pub fn hash_byte_size(&self) -> usize { + match self { + BaseHashAlgoType::TpmAlgSha256 => 32, + BaseHashAlgoType::TpmAlgSha384 => 48, + BaseHashAlgoType::TpmAlgSha512 => 64, + BaseHashAlgoType::TpmAlgSha3_256 => 32, + BaseHashAlgoType::TpmAlgSha3_384 => 48, + BaseHashAlgoType::TpmAlgSha3_512 => 64, + BaseHashAlgoType::TpmAlgSm3_256 => 32, + } + } +} + +#[derive(Debug)] +pub struct InvalidBashHashAlgo; + +impl TryFrom for BaseHashAlgoType { + type Error = InvalidBashHashAlgo; + + fn try_from(value: BaseHashAlgo) -> Result { + if value.0 > u8::MAX as u32 { + return Err(InvalidBashHashAlgo); + } + Self::try_from(value.0 as u8).map_err(|_| InvalidBashHashAlgo) + } +} + // Measurement Extension Log Specification field bitfield! { #[derive(FromBytes, IntoBytes, Immutable, Default, Clone, Copy)] @@ -559,6 +611,7 @@ impl DeviceAlgorithms { // Algorithm Priority Table set by the responder // to indicate the priority of the selected algorithms +#[derive(Default)] pub struct AlgorithmPriorityTable<'a> { pub measurement_specification: Option<&'a [MeasurementSpecificationType]>, pub opaque_data_format: Option<&'a [OpaqueDataFormatType]>, @@ -571,6 +624,7 @@ pub struct AlgorithmPriorityTable<'a> { pub key_schedule: Option<&'a [KeyScheduleType]>, } +#[derive(Default)] pub struct LocalDeviceAlgorithms<'a> { pub device_algorithms: DeviceAlgorithms, pub algorithm_priority_table: AlgorithmPriorityTable<'a>, diff --git a/src/protocol/capabilities.rs b/src/protocol/capabilities.rs index 9f1b3cf..1401ccd 100644 --- a/src/protocol/capabilities.rs +++ b/src/protocol/capabilities.rs @@ -1,4 +1,16 @@ -// Licensed under the Apache-2.0 license +// Copyright 2025 +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. use bitfield::bitfield; use zerocopy::{FromBytes, Immutable, IntoBytes}; @@ -50,6 +62,12 @@ pub struct DeviceCapabilities { // Only used for >= SPDM 1.2 pub data_transfer_size: u32, pub max_spdm_msg_size: u32, + + /// Only valid for >= SPDM 1.3 + /// The Responder shall include the Supported Algorithms Block in its CAPABILITIES + /// response if it supports this extended capability. If the Requester does not + /// support the Large SPDM message transfer mechanism ( CHUNK_CAP=0 ), this bit shall be 0. + pub include_supported_algorithms: bool, } bitfield! { diff --git a/src/protocol/certs.rs b/src/protocol/certs.rs index d1b920b..41982a3 100644 --- a/src/protocol/certs.rs +++ b/src/protocol/certs.rs @@ -1,22 +1,80 @@ -// Licensed under the Apache-2.0 license -use crate::protocol::SHA384_HASH_SIZE; +// Copyright 2025 +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +use crate::protocol::{SpdmVersion, SHA384_HASH_SIZE}; use bitfield::bitfield; -use zerocopy::{FromBytes, Immutable, IntoBytes}; +use zerocopy::{little_endian, FromBytes, Immutable, IntoBytes, KnownLayout}; pub(crate) const SPDM_MAX_CERT_CHAIN_PORTION_LEN: u16 = 512; pub(crate) const SPDM_CERT_CHAIN_METADATA_LEN: u16 = size_of::() as u16 + SHA384_HASH_SIZE as u16; -#[derive(IntoBytes, FromBytes, Immutable, Debug, Default)] +#[derive(IntoBytes, FromBytes, Immutable, KnownLayout, Debug, Default)] #[repr(C, packed)] -pub(crate) struct SpdmCertChainHeader { - pub length: u16, - pub reserved: u16, +pub struct SpdmCertChainHeader { + /// Length of the CertChain, including this header and the following root hash + /// + /// # Versions + /// ## <= v1.2 + /// Little endian u16 followed by reserved u16. + /// (this is compatible witht the current >= v1.3 layout) + /// ## v1.3 and later + /// Little endian u32. + length: little_endian::U32, +} +impl SpdmCertChainHeader { + /// Get the length of the certificate chain + /// + /// This includes the length header and root hash. + pub fn get_length(&self) -> u32 { + self.length.get() + } + /// Checks the version for compatibility and assigns the provided length + /// + /// # Versions + /// ## <= v1.2 + /// Little endian u16 followed by reserved u16. + /// (this is compatible witht the current >= v1.3 layout) + /// ## v1.3 and later + /// Little endian u32. + pub fn set_length(&mut self, length: u32, version: SpdmVersion) -> Result<(), ()> { + if length > u16::MAX as u32 && version < SpdmVersion::V13 { + return Err(()); + } + self.length.set(length); + Ok(()) + } + /// Creates a new certificate chain header with checked version compatibility + /// + /// # Versions + /// ## <= v1.2 + /// Little endian u16 followed by reserved u16. + /// (this is compatible witht the current >= v1.3 layout) + /// ## v1.3 and later + /// Little endian u32. + pub fn new(length: u32, version: SpdmVersion) -> Result { + if length > u16::MAX as u32 && version < SpdmVersion::V13 { + return Err(()); + } + Ok(Self { + length: little_endian::U32::new(length), + }) + } } // SPDM CertificateInfo fields bitfield! { -#[derive(FromBytes, IntoBytes, Immutable, Default)] +#[derive(FromBytes, IntoBytes, Immutable, Default, Clone, Copy)] #[repr(C, packed)] pub struct CertificateInfo(u8); impl Debug; @@ -25,9 +83,43 @@ pub cert_model, set_cert_model: 0,2; reserved, _: 3,7; } +/// CertModel field used in Certificate Info bitfields in DIGESTS and CERTIFICATE responses +#[derive(Debug, Clone, Copy)] +#[non_exhaustive] +pub(crate) enum CertModel { + /// Indicates either that the certificate slot does not contain any certificates + /// or that the corresponding `MULTI_KEY_CONN_REQ` or `MULTI_KEY_CONN_RSP` is false. + None = 0, + /// Certificate slot uses the `DeviceCert` model. + DeviceCert = 1, + /// Certificate slot uses the `AliasCert` model. + AliasCert = 2, + /// Certificate slot uses the `GenericCert` model. + GenericCert = 3, + // TODO: Shoud we include a Reserved(u8) + // to propagate the error handling further up? +} + +#[derive(Debug)] +pub(crate) struct InvalidCertModelError; + +impl TryFrom for CertModel { + type Error = InvalidCertModelError; + + fn try_from(value: u8) -> Result { + match value { + 0 => Ok(CertModel::None), + 1 => Ok(CertModel::DeviceCert), + 2 => Ok(CertModel::AliasCert), + 3 => Ok(CertModel::GenericCert), + _ => Err(InvalidCertModelError), + } + } +} + // SPDM KeyUsageMask fields bitfield! { -#[derive(FromBytes, IntoBytes, Immutable, Default)] +#[derive(FromBytes, IntoBytes, Immutable, Default, Clone, Copy)] #[repr(C)] pub struct KeyUsageMask(u16); impl Debug; diff --git a/src/protocol/common.rs b/src/protocol/common.rs index 17b5236..83d3af8 100644 --- a/src/protocol/common.rs +++ b/src/protocol/common.rs @@ -1,4 +1,16 @@ -// Licensed under the Apache-2.0 license +// Copyright 2025 +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. use crate::codec::CommonCodec; use crate::error::{SpdmError, SpdmResult}; @@ -6,7 +18,7 @@ use crate::protocol::{version::SpdmVersion, REQUESTER_CONTEXT_LEN, SPDM_CONTEXT_ use zerocopy::{FromBytes, Immutable, IntoBytes}; #[derive(Debug, Clone, Copy, PartialEq)] -pub(crate) enum ReqRespCode { +pub enum ReqRespCode { GetVersion = 0x84, Version = 0x04, GetCapabilities = 0xE1, @@ -75,9 +87,17 @@ impl ReqRespCode { Ok(context) } + + pub(crate) fn is_response(&self) -> bool { + *self as u8 <= 0x7F + } + + pub(crate) fn is_request(&self) -> bool { + !self.is_response() + } } -#[derive(FromBytes, IntoBytes, Immutable)] +#[derive(FromBytes, IntoBytes, Immutable, Clone)] #[repr(C)] pub(crate) struct SpdmMsgHdr { version: u8, @@ -97,7 +117,8 @@ impl SpdmMsgHdr { } pub(crate) fn req_resp_code(&self) -> SpdmResult { - self.req_resp_code.try_into() + // self.req_resp_code.try_into() + ReqRespCode::try_from(self.req_resp_code) } } diff --git a/src/protocol/mod.rs b/src/protocol/mod.rs index 41502e4..59cae87 100644 --- a/src/protocol/mod.rs +++ b/src/protocol/mod.rs @@ -1,4 +1,16 @@ -// Licensed under the Apache-2.0 license +// Copyright 2025 +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. pub mod algorithms; pub mod capabilities; @@ -13,3 +25,5 @@ pub use certs::*; pub(crate) use common::*; pub(crate) use signature::*; pub use version::*; + +pub use common::ReqRespCode; diff --git a/src/protocol/signature.rs b/src/protocol/signature.rs index f59e999..71afa24 100644 --- a/src/protocol/signature.rs +++ b/src/protocol/signature.rs @@ -1,7 +1,19 @@ -// Licensed under the Apache-2.0 license +// Copyright 2025 +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. -use crate::protocol::*; use crate::platform::hash::{SpdmHash, SpdmHashError}; +use crate::protocol::*; pub const NONCE_LEN: usize = 32; @@ -21,9 +33,17 @@ pub enum SignCtxError { Platform(SpdmHashError), } +#[derive(Debug, PartialEq)] +pub enum SignatureError { + InvalidSignature, +} + pub type SignatureCtxResult = Result; -pub(crate) fn create_responder_signing_context( +pub type SignatureResult = Result; + +/// Creates the `combined_spdm_prefix` +pub fn create_responder_signing_context( spdm_version: SpdmVersion, opcode: ReqRespCode, ) -> SignatureCtxResult<[u8; SPDM_SIGNING_CONTEXT_LEN]> { diff --git a/src/protocol/version.rs b/src/protocol/version.rs index b4bf552..a12aa3f 100644 --- a/src/protocol/version.rs +++ b/src/protocol/version.rs @@ -1,4 +1,16 @@ -// Licensed under the Apache-2.0 license +// Copyright 2025 +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. use crate::error::{SpdmError, SpdmResult}; diff --git a/src/state.rs b/src/state.rs index 85e8dba..26e59d0 100644 --- a/src/state.rs +++ b/src/state.rs @@ -1,21 +1,38 @@ -// Licensed under the Apache-2.0 license - -use crate::protocol::{DeviceAlgorithms, DeviceCapabilities, SpdmVersion}; - -pub(crate) struct State { +// Copyright 2025 +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use crate::{ + cert_store::PeerCertStore, + protocol::{DeviceAlgorithms, DeviceCapabilities, SpdmVersion}, +}; + +pub(crate) struct State<'a> { pub(crate) connection_info: ConnectionInfo, + pub(crate) peer_cert_store: Option<&'a mut dyn PeerCertStore>, } -impl Default for State { +impl<'a> Default for State<'a> { fn default() -> Self { - Self::new() + Self::new(None) } } -impl State { - pub fn new() -> Self { +impl<'a> State<'a> { + pub fn new(peer_cert_store: Option<&'a mut dyn PeerCertStore>) -> Self { Self { connection_info: ConnectionInfo::default(), + peer_cert_store, } } @@ -24,11 +41,11 @@ impl State { } } -pub(crate) struct ConnectionInfo { +pub struct ConnectionInfo { version_number: SpdmVersion, state: ConnectionState, - peer_capabilities: DeviceCapabilities, peer_algorithms: DeviceAlgorithms, + peer_capabilities: DeviceCapabilities, multi_key_conn_rsp: bool, } @@ -49,7 +66,7 @@ impl ConnectionInfo { self.version_number } - pub fn set_version_number(&mut self, version_number: SpdmVersion) { + pub(crate) fn set_version_number(&mut self, version_number: SpdmVersion) { self.version_number = version_number; } @@ -57,20 +74,19 @@ impl ConnectionInfo { self.state } - pub fn set_state(&mut self, state: ConnectionState) { + pub(crate) fn set_state(&mut self, state: ConnectionState) { self.state = state; } - pub fn set_peer_capabilities(&mut self, peer_capabilities: DeviceCapabilities) { + pub(crate) fn set_peer_capabilities(&mut self, peer_capabilities: DeviceCapabilities) { self.peer_capabilities = peer_capabilities; } - #[allow(dead_code)] pub fn peer_capabilities(&self) -> DeviceCapabilities { self.peer_capabilities } - pub fn set_peer_algorithms(&mut self, peer_algorithms: DeviceAlgorithms) { + pub(crate) fn set_peer_algorithms(&mut self, peer_algorithms: DeviceAlgorithms) { self.peer_algorithms = peer_algorithms; } @@ -79,7 +95,7 @@ impl ConnectionInfo { } #[allow(dead_code)] - pub fn set_multi_key_conn_rsp(&mut self, multi_key_conn_rsp: bool) { + pub(crate) fn set_multi_key_conn_rsp(&mut self, multi_key_conn_rsp: bool) { self.multi_key_conn_rsp = multi_key_conn_rsp; } @@ -102,6 +118,15 @@ pub enum ConnectionState { AfterCapabilities, AlgorithmsNegotiated, AfterDigest, + /// Cert chain retrieval in process + DuringCertificate(GetCertificateState), AfterCertificate, Authenticated, } + +#[derive(Debug, Clone, Copy, PartialEq, PartialOrd, Default)] +pub struct GetCertificateState { + pub current_slot_id: u8, + pub offset: u16, + pub remainder_length: Option, +} diff --git a/src/test.rs b/src/test.rs new file mode 100644 index 0000000..4b6c8bc --- /dev/null +++ b/src/test.rs @@ -0,0 +1,248 @@ +// Copyright 2025 +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use crate::{ + cert_store::{CertStoreResult, SpdmCertStore}, + context::SpdmContext, + platform::{ + evidence::{SpdmEvidence, SpdmEvidenceResult}, + hash::SpdmHash, + rng::SpdmRng, + transport::SpdmTransport, + }, + protocol::{ + AsymAlgo, CertificateInfo, DeviceCapabilities, KeyUsageMask, LocalDeviceAlgorithms, + SpdmVersion, + }, +}; + +pub struct MockResources { + transport: MockTransport, + hasher: StdHash, + m1: StdHash, + l1: StdHash, + rng: StdRng, + cert_store: MockCertStore, + evidence: MockEvidence, +} +impl MockResources { + pub fn new() -> MockResources { + MockResources { + transport: MockTransport, + hasher: StdHash, + m1: StdHash, + l1: StdHash, + rng: StdRng, + cert_store: MockCertStore, + evidence: MockEvidence, + } + } +} + +pub fn create_context<'a>( + stack: &'a mut MockResources, + versions: &'a [SpdmVersion], + algorithms: LocalDeviceAlgorithms<'a>, +) -> SpdmContext<'a> { + SpdmContext::new( + versions, + &mut stack.transport, + DeviceCapabilities::default(), + algorithms, + &mut stack.cert_store, + None, + &mut stack.hasher, + &mut stack.m1, + &mut stack.l1, + &mut stack.rng, + &stack.evidence, + ) + .unwrap() +} + +/// Create a array with all available versions +pub fn versions_default() -> [SpdmVersion; 4] { + [ + SpdmVersion::V10, + SpdmVersion::V11, + SpdmVersion::V12, + SpdmVersion::V13, + ] +} + +struct StdHash; + +impl SpdmHash for StdHash { + fn hash( + &mut self, + _hash_algo: crate::platform::hash::SpdmHashAlgoType, + _data: &[u8], + _hash: &mut [u8], + ) -> crate::platform::hash::SpdmHashResult<()> { + todo!() + } + + fn init( + &mut self, + _hash_algo: crate::platform::hash::SpdmHashAlgoType, + _data: Option<&[u8]>, + ) -> crate::platform::hash::SpdmHashResult<()> { + todo!() + } + + fn update(&mut self, _data: &[u8]) -> crate::platform::hash::SpdmHashResult<()> { + todo!() + } + + fn finalize(&mut self, _hash: &mut [u8]) -> crate::platform::hash::SpdmHashResult<()> { + todo!() + } + + fn reset(&mut self) { + todo!() + } + + fn algo(&self) -> crate::platform::hash::SpdmHashAlgoType { + todo!() + } +} + +struct StdRng; + +impl SpdmRng for StdRng { + fn get_random_bytes(&mut self, _buf: &mut [u8]) -> crate::platform::rng::SpdmRngResult<()> { + todo!() + } + + fn generate_random_number( + &mut self, + _random_number: &mut [u8], + ) -> crate::platform::rng::SpdmRngResult<()> { + todo!() + } +} + +struct MockTransport; +impl SpdmTransport for MockTransport { + fn send_request<'a>( + &mut self, + _dest_eid: u8, + _req: &mut crate::codec::MessageBuf<'a>, + ) -> crate::platform::transport::TransportResult<()> { + todo!() + } + + fn receive_response<'a>( + &mut self, + _rsp: &mut crate::codec::MessageBuf<'a>, + ) -> crate::platform::transport::TransportResult<()> { + todo!() + } + + fn receive_request<'a>( + &mut self, + _req: &mut crate::codec::MessageBuf<'a>, + ) -> crate::platform::transport::TransportResult<()> { + todo!() + } + + fn send_response<'a>( + &mut self, + _resp: &mut crate::codec::MessageBuf<'a>, + ) -> crate::platform::transport::TransportResult<()> { + todo!() + } + + fn max_message_size(&self) -> crate::platform::transport::TransportResult { + todo!() + } + + fn header_size(&self) -> usize { + 0 + } + + fn init_sequence(&mut self) -> crate::platform::transport::TransportResult<()> { + todo!() + } +} + +struct MockCertStore; +impl SpdmCertStore for MockCertStore { + fn slot_count(&self) -> u8 { + 1 + } + fn is_provisioned(&self, slot_id: u8) -> bool { + slot_id == 0 + } + fn cert_chain_len(&mut self, _asym: AsymAlgo, _slot_id: u8) -> CertStoreResult { + Ok(128) + } + fn get_cert_chain( + &mut self, + _slot_id: u8, + _asym: AsymAlgo, + _offset: usize, + out: &mut [u8], + ) -> CertStoreResult { + let fill = out.len().min(16); + for b in &mut out[..fill] { + *b = 0x55; + } + Ok(fill) + } + fn root_cert_hash( + &mut self, + _slot_id: u8, + _asym: AsymAlgo, + out: &mut [u8; crate::protocol::SHA384_HASH_SIZE], + ) -> CertStoreResult<()> { + for b in out.iter_mut() { + *b = 0x11; + } + Ok(()) + } + fn sign_hash<'a>( + &self, + _slot_id: u8, + _hash: &'a [u8; crate::protocol::SHA384_HASH_SIZE], + sig: &'a mut [u8; crate::protocol::ECC_P384_SIGNATURE_SIZE], + ) -> CertStoreResult<()> { + for b in sig.iter_mut() { + *b = 0x22; + } + Ok(()) + } + fn key_pair_id(&self, _slot_id: u8) -> Option { + Some(0) + } + fn cert_info(&self, _slot_id: u8) -> Option { + None + } + fn key_usage_mask(&self, _slot_id: u8) -> Option { + None + } +} + +struct MockEvidence; +impl SpdmEvidence for MockEvidence { + fn pcr_quote(&self, buffer: &mut [u8], _with_pqc_sig: bool) -> SpdmEvidenceResult { + let data = b"EVID"; + let len = data.len().min(buffer.len()); + buffer[..len].copy_from_slice(&data[..len]); + Ok(len) + } + fn pcr_quote_size(&self, _with_pqc_sig: bool) -> SpdmEvidenceResult { + Ok(4) + } +} diff --git a/src/transcript.rs b/src/transcript.rs index 33acce1..77b7909 100644 --- a/src/transcript.rs +++ b/src/transcript.rs @@ -1,7 +1,19 @@ -// Licensed under the Apache-2.0 license +// Copyright 2025 +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. -use crate::protocol::{SpdmVersion, SHA384_HASH_SIZE}; use crate::platform::hash::{SpdmHash, SpdmHashError}; +use crate::protocol::{SpdmVersion, SHA384_HASH_SIZE}; #[derive(Debug, PartialEq)] pub enum TranscriptError { @@ -47,9 +59,33 @@ impl VcaBuffer { } } +#[derive(Debug)] pub enum TranscriptContext { + /// # VCA + /// VCA: Version, Capabilities, and Algorithms transcript context, containing + /// messages related to version negotiation, capabilities exchange, and algorithm negotiation. + /// VCA = Concatenate (GET_VERSION, VERSION, GET_CAPABILITIES, CAPABILITIES, NEGOTIATE_ALGORITHMS, ALGORITHMS) + /// + /// VCA is the same as message A in the M1 transcript. Vca, + + /// # M1 + /// M1=Concatenate(A, B, C) + /// where + /// - A: VCA = Concatenate (GET_VERSION, VERSION, GET_CAPABILITIES, CAPABILITIES, + /// NEGOTIATE_ALGORITHMS, ALGORITHMS) + /// - B: Certificate Exchange = Concatenate (GET_DIGESTS, DIGESTS, GET_CERTIFICATE + /// CERTIFICATE) + /// - C = Concatenate (CHALLENGE, CHALLENGE_AUTH excluding signature) M1, + + /// # L1 + /// SPDM Version 1.2 and later: L1 = Concatenate(A, M) where + /// - A: VCA = Concatenate (GET_VERSION, VERSION, GET_CAPABILITIES, NEGOTIATE_ALGORITHMS, ALGORITHMS) + /// - M: Concatenate (GET_MEASUREMENTS, MEASUREMENTS without signature) + /// + /// SPDM Version 1.0 and 1.1: L1 = Concatenate(M) where + /// - M: Concatenate (GET_MEASUREMENTS, MEASUREMENTS without signature) L1, } @@ -82,7 +118,7 @@ impl<'a> TranscriptManager<'a> { hash_ctx_m1: m1, m1_ctx_ready: false, hash_ctx_l1: l1, - l1_ctx_ready: false + l1_ctx_ready: false, } } @@ -114,8 +150,14 @@ impl<'a> TranscriptManager<'a> { pub fn reset_context(&mut self, context: TranscriptContext) { match context { TranscriptContext::Vca => self.vca_buf.reset(), - TranscriptContext::M1 => { self.hash_ctx_m1.reset(); self.m1_ctx_ready = false; }, - TranscriptContext::L1 => { self.hash_ctx_l1.reset(); self.l1_ctx_ready = false; }, + TranscriptContext::M1 => { + self.hash_ctx_m1.reset(); + self.m1_ctx_ready = false; + } + TranscriptContext::L1 => { + self.hash_ctx_l1.reset(); + self.l1_ctx_ready = false; + } } } @@ -127,11 +169,7 @@ impl<'a> TranscriptManager<'a> { /// /// # Returns /// * `TranscriptResult<()>` - Result indicating success or failure. - pub fn append( - &mut self, - context: TranscriptContext, - data: &[u8], - ) -> TranscriptResult<()> { + pub fn append(&mut self, context: TranscriptContext, data: &[u8]) -> TranscriptResult<()> { match context { TranscriptContext::Vca => self.vca_buf.append(data), TranscriptContext::M1 => self.append_m1(data), @@ -158,11 +196,19 @@ impl<'a> TranscriptManager<'a> { TranscriptContext::L1 => &mut self.hash_ctx_l1, }; - hash_ctx.finalize(hash).map_err(|e| TranscriptError::Hash(e))?; + hash_ctx + .finalize(hash) + .map_err(|e| TranscriptError::Hash(e))?; match context { - TranscriptContext::M1 => {self.hash_ctx_m1.reset(); self.m1_ctx_ready = false; }, - TranscriptContext::L1 => {self.hash_ctx_l1.reset(); self.l1_ctx_ready = false; }, + TranscriptContext::M1 => { + self.hash_ctx_m1.reset(); + self.m1_ctx_ready = false; + } + TranscriptContext::L1 => { + self.hash_ctx_l1.reset(); + self.l1_ctx_ready = false; + } _ => {} } @@ -171,11 +217,17 @@ impl<'a> TranscriptManager<'a> { fn append_m1(&mut self, data: &[u8]) -> TranscriptResult<()> { if self.m1_ctx_ready { - self.hash_ctx_m1.update(data).map_err(|e| TranscriptError::Hash(e)) + self.hash_ctx_m1 + .update(data) + .map_err(|e| TranscriptError::Hash(e)) } else { let vca_data = self.vca_buf.data(); - self.hash_ctx_m1.init(self.hash_ctx_m1.algo(), Some(vca_data)).map_err(|e| TranscriptError::Hash(e))?; - self.hash_ctx_m1.update(data).map_err(|e| TranscriptError::Hash(e))?; + self.hash_ctx_m1 + .init(self.hash_ctx_m1.algo(), Some(vca_data)) + .map_err(|e| TranscriptError::Hash(e))?; + self.hash_ctx_m1 + .update(data) + .map_err(|e| TranscriptError::Hash(e))?; self.m1_ctx_ready = true; Ok(()) } @@ -183,15 +235,21 @@ impl<'a> TranscriptManager<'a> { fn append_l1(&mut self, spdm_version: SpdmVersion, data: &[u8]) -> TranscriptResult<()> { if self.l1_ctx_ready { - self.hash_ctx_l1.update(data).map_err(|e| TranscriptError::Hash(e)) + self.hash_ctx_l1 + .update(data) + .map_err(|e| TranscriptError::Hash(e)) } else { let vca_data = if spdm_version >= SpdmVersion::V12 { Some(self.vca_buf.data()) } else { None }; - self.hash_ctx_l1.init(self.hash_ctx_l1.algo(), vca_data).map_err(|e| TranscriptError::Hash(e))?; - self.hash_ctx_l1.update(data).map_err(|e| TranscriptError::Hash(e))?; + self.hash_ctx_l1 + .init(self.hash_ctx_l1.algo(), vca_data) + .map_err(|e| TranscriptError::Hash(e))?; + self.hash_ctx_l1 + .update(data) + .map_err(|e| TranscriptError::Hash(e))?; self.l1_ctx_ready = true; Ok(()) diff --git a/src/transport.rs b/src/transport.rs index 0b733fd..faadc96 100644 --- a/src/transport.rs +++ b/src/transport.rs @@ -1,4 +1,16 @@ -// Licensed under the Apache-2.0 license +// Copyright 2025 +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. use crate::codec::MessageBuf; use crate::codec::{Codec, CodecError, CommonCodec, DataKind}; diff --git a/tests/spdm_validator_host.rs b/tests/spdm_validator_host.rs index 588eb4c..fc2034e 100644 --- a/tests/spdm_validator_host.rs +++ b/tests/spdm_validator_host.rs @@ -1,69 +1,178 @@ +// Copyright 2025 +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + //! Basic instantiation test for SpdmContext (host validator) //! Run with: cargo test --features std -- --nocapture -use spdm_lib::protocol::version::SpdmVersion; -use spdm_lib::protocol::algorithms::{DeviceAlgorithms, LocalDeviceAlgorithms, AlgorithmPriorityTable, MeasurementSpecification, OtherParamSupport, MeasurementHashAlgo, BaseAsymAlgo, BaseHashAlgo, MelSpecification, DheNamedGroup, AeadCipherSuite, ReqBaseAsymAlg, KeySchedule, AsymAlgo}; -use spdm_lib::protocol::DeviceCapabilities; +use spdm_lib::cert_store::{CertStoreResult, SpdmCertStore}; use spdm_lib::codec::MessageBuf; -use spdm_lib::error::SpdmResult; use spdm_lib::context::SpdmContext; -use spdm_lib::platform::transport::{SpdmTransport, TransportResult}; +use spdm_lib::error::SpdmResult; +use spdm_lib::platform::evidence::{SpdmEvidence, SpdmEvidenceResult}; use spdm_lib::platform::hash::{SpdmHash, SpdmHashAlgoType, SpdmHashResult}; use spdm_lib::platform::rng::{SpdmRng, SpdmRngResult}; -use spdm_lib::platform::evidence::{SpdmEvidence, SpdmEvidenceResult}; -use spdm_lib::cert_store::{SpdmCertStore, CertStoreResult}; +use spdm_lib::platform::transport::{SpdmTransport, TransportResult}; +use spdm_lib::protocol::algorithms::{ + AeadCipherSuite, AlgorithmPriorityTable, AsymAlgo, BaseAsymAlgo, BaseHashAlgo, + DeviceAlgorithms, DheNamedGroup, KeySchedule, LocalDeviceAlgorithms, MeasurementHashAlgo, + MeasurementSpecification, MelSpecification, OtherParamSupport, ReqBaseAsymAlg, +}; use spdm_lib::protocol::algorithms::{ECC_P384_SIGNATURE_SIZE, SHA384_HASH_SIZE}; +use spdm_lib::protocol::version::SpdmVersion; +use spdm_lib::protocol::DeviceCapabilities; struct MockTransport; impl SpdmTransport for MockTransport { - fn send_request<'a>(&mut self, _dest: u8, _req: &mut MessageBuf<'a>) -> TransportResult<()> { Ok(()) } - fn receive_response<'a>(&mut self, _rsp: &mut MessageBuf<'a>) -> TransportResult<()> { Ok(()) } - fn receive_request<'a>(&mut self, _req: &mut MessageBuf<'a>) -> TransportResult<()> { Ok(()) } - fn send_response<'a>(&mut self, _resp: &mut MessageBuf<'a>) -> TransportResult<()> { Ok(()) } - fn max_message_size(&self) -> TransportResult { Ok(1024) } - fn header_size(&self) -> usize { 0 } + fn send_request<'a>(&mut self, _dest: u8, _req: &mut MessageBuf<'a>) -> TransportResult<()> { + Ok(()) + } + fn receive_response<'a>(&mut self, _rsp: &mut MessageBuf<'a>) -> TransportResult<()> { + Ok(()) + } + fn receive_request<'a>(&mut self, _req: &mut MessageBuf<'a>) -> TransportResult<()> { + Ok(()) + } + fn send_response<'a>(&mut self, _resp: &mut MessageBuf<'a>) -> TransportResult<()> { + Ok(()) + } + fn max_message_size(&self) -> TransportResult { + Ok(1024) + } + fn header_size(&self) -> usize { + 0 + } + + fn init_sequence(&mut self) -> TransportResult<()> { + todo!() + } } struct MockHash { algo: SpdmHashAlgoType, } -impl MockHash { fn new() -> Self { Self { algo: SpdmHashAlgoType::SHA384 } } } +impl MockHash { + fn new() -> Self { + Self { + algo: SpdmHashAlgoType::SHA384, + } + } +} impl SpdmHash for MockHash { fn hash(&mut self, _algo: SpdmHashAlgoType, data: &[u8], out: &mut [u8]) -> SpdmHashResult<()> { let len = out.len().min(data.len()); out[..len].copy_from_slice(&data[..len]); Ok(()) } - fn init(&mut self, _algo: SpdmHashAlgoType, _data: Option<&[u8]>) -> SpdmHashResult<()> { Ok(()) } - fn update(&mut self, _data: &[u8]) -> SpdmHashResult<()> { Ok(()) } - fn finalize(&mut self, out: &mut [u8]) -> SpdmHashResult<()> { for b in out.iter_mut() { *b = 0xAA; } Ok(()) } + fn init(&mut self, _algo: SpdmHashAlgoType, _data: Option<&[u8]>) -> SpdmHashResult<()> { + Ok(()) + } + fn update(&mut self, _data: &[u8]) -> SpdmHashResult<()> { + Ok(()) + } + fn finalize(&mut self, out: &mut [u8]) -> SpdmHashResult<()> { + for b in out.iter_mut() { + *b = 0xAA; + } + Ok(()) + } fn reset(&mut self) {} - fn algo(&self) -> SpdmHashAlgoType { self.algo } + fn algo(&self) -> SpdmHashAlgoType { + self.algo + } } struct MockRng; impl SpdmRng for MockRng { - fn get_random_bytes(&mut self, buf: &mut [u8]) -> SpdmRngResult<()> { for (i,b) in buf.iter_mut().enumerate() { *b = i as u8; } Ok(()) } - fn generate_random_number(&mut self, buf: &mut [u8]) -> SpdmRngResult<()> { self.get_random_bytes(buf) } + fn get_random_bytes(&mut self, buf: &mut [u8]) -> SpdmRngResult<()> { + for (i, b) in buf.iter_mut().enumerate() { + *b = i as u8; + } + Ok(()) + } + fn generate_random_number(&mut self, buf: &mut [u8]) -> SpdmRngResult<()> { + self.get_random_bytes(buf) + } } struct MockEvidence; impl SpdmEvidence for MockEvidence { - fn pcr_quote(&self, buffer: &mut [u8], _with_pqc_sig: bool) -> SpdmEvidenceResult { let data = b"EVID"; let len = data.len().min(buffer.len()); buffer[..len].copy_from_slice(&data[..len]); Ok(len) } - fn pcr_quote_size(&self, _with_pqc_sig: bool) -> SpdmEvidenceResult { Ok(4) } + fn pcr_quote(&self, buffer: &mut [u8], _with_pqc_sig: bool) -> SpdmEvidenceResult { + let data = b"EVID"; + let len = data.len().min(buffer.len()); + buffer[..len].copy_from_slice(&data[..len]); + Ok(len) + } + fn pcr_quote_size(&self, _with_pqc_sig: bool) -> SpdmEvidenceResult { + Ok(4) + } } struct MockCertStore; impl SpdmCertStore for MockCertStore { - fn slot_count(&self) -> u8 { 1 } - fn is_provisioned(&self, slot_id: u8) -> bool { slot_id == 0 } - fn cert_chain_len(&mut self, _asym: AsymAlgo, _slot_id: u8) -> CertStoreResult { Ok(128) } - fn get_cert_chain<'a>(&mut self, _slot_id: u8, _asym: AsymAlgo, _offset: usize, out: &'a mut [u8]) -> CertStoreResult { let fill = out.len().min(16); for b in &mut out[..fill] { *b = 0x55; } Ok(fill) } - fn root_cert_hash<'a>(&mut self, _slot_id: u8, _asym: AsymAlgo, out: &'a mut [u8; SHA384_HASH_SIZE]) -> CertStoreResult<()> { for b in out.iter_mut() { *b = 0x11; } Ok(()) } - fn sign_hash<'a>(&self, _slot_id: u8, _hash: &'a [u8; SHA384_HASH_SIZE], sig: &'a mut [u8; ECC_P384_SIGNATURE_SIZE]) -> CertStoreResult<()> { for b in sig.iter_mut() { *b = 0x22; } Ok(()) } - fn key_pair_id(&self, _slot_id: u8) -> Option { Some(0) } - fn cert_info(&self, _slot_id: u8) -> Option { None } - fn key_usage_mask(&self, _slot_id: u8) -> Option { None } + fn slot_count(&self) -> u8 { + 1 + } + fn is_provisioned(&self, slot_id: u8) -> bool { + slot_id == 0 + } + fn cert_chain_len(&mut self, _asym: AsymAlgo, _slot_id: u8) -> CertStoreResult { + Ok(128) + } + fn get_cert_chain<'a>( + &mut self, + _slot_id: u8, + _asym: AsymAlgo, + _offset: usize, + out: &'a mut [u8], + ) -> CertStoreResult { + let fill = out.len().min(16); + for b in &mut out[..fill] { + *b = 0x55; + } + Ok(fill) + } + fn root_cert_hash<'a>( + &mut self, + _slot_id: u8, + _asym: AsymAlgo, + out: &'a mut [u8; SHA384_HASH_SIZE], + ) -> CertStoreResult<()> { + for b in out.iter_mut() { + *b = 0x11; + } + Ok(()) + } + fn sign_hash<'a>( + &self, + _slot_id: u8, + _hash: &'a [u8; SHA384_HASH_SIZE], + sig: &'a mut [u8; ECC_P384_SIGNATURE_SIZE], + ) -> CertStoreResult<()> { + for b in sig.iter_mut() { + *b = 0x22; + } + Ok(()) + } + fn key_pair_id(&self, _slot_id: u8) -> Option { + Some(0) + } + fn cert_info(&self, _slot_id: u8) -> Option { + None + } + fn key_usage_mask(&self, _slot_id: u8) -> Option { + None + } } #[test] @@ -72,12 +181,42 @@ fn spdm_validator_host() -> SpdmResult<()> { let versions = [SpdmVersion::V10]; // Capabilities (minimal plausible set) - let dev_caps = DeviceCapabilities { ct_exponent: 0, flags: spdm_lib::protocol::capabilities::CapabilityFlags::default(), data_transfer_size: 1024, max_spdm_msg_size: 2048 }; + let dev_caps = DeviceCapabilities { + ct_exponent: 0, + flags: spdm_lib::protocol::capabilities::CapabilityFlags::default(), + data_transfer_size: 1024, + max_spdm_msg_size: 2048, + include_supported_algorithms: false, + }; // Algorithms (provide trivial single-selection values) - let device_algo = DeviceAlgorithms { measurement_spec: MeasurementSpecification::default(), other_param_support: OtherParamSupport::default(), measurement_hash_algo: MeasurementHashAlgo::default(), base_asym_algo: BaseAsymAlgo::default(), base_hash_algo: BaseHashAlgo::default(), mel_specification: MelSpecification::default(), dhe_group: DheNamedGroup::default(), aead_cipher_suite: AeadCipherSuite::default(), req_base_asym_algo: ReqBaseAsymAlg::default(), key_schedule: KeySchedule::default() }; - let priority_table = AlgorithmPriorityTable { measurement_specification: None, opaque_data_format: None, base_asym_algo: None, base_hash_algo: None, mel_specification: None, dhe_group: None, aead_cipher_suite: None, req_base_asym_algo: None, key_schedule: None }; - let local_algos = LocalDeviceAlgorithms { device_algorithms: device_algo, algorithm_priority_table: priority_table }; + let device_algo = DeviceAlgorithms { + measurement_spec: MeasurementSpecification::default(), + other_param_support: OtherParamSupport::default(), + measurement_hash_algo: MeasurementHashAlgo::default(), + base_asym_algo: BaseAsymAlgo::default(), + base_hash_algo: BaseHashAlgo::default(), + mel_specification: MelSpecification::default(), + dhe_group: DheNamedGroup::default(), + aead_cipher_suite: AeadCipherSuite::default(), + req_base_asym_algo: ReqBaseAsymAlg::default(), + key_schedule: KeySchedule::default(), + }; + let priority_table = AlgorithmPriorityTable { + measurement_specification: None, + opaque_data_format: None, + base_asym_algo: None, + base_hash_algo: None, + mel_specification: None, + dhe_group: None, + aead_cipher_suite: None, + req_base_asym_algo: None, + key_schedule: None, + }; + let local_algos = LocalDeviceAlgorithms { + device_algorithms: device_algo, + algorithm_priority_table: priority_table, + }; let mut transport = MockTransport; let mut hash_main = MockHash::new(); @@ -93,6 +232,7 @@ fn spdm_validator_host() -> SpdmResult<()> { dev_caps, local_algos, &mut certs, + None, &mut hash_main, &mut hash_m1, &mut hash_l1,