From 1f202a084d5720265423f852a2afe978e2339528 Mon Sep 17 00:00:00 2001 From: Kyeongseob Sim Date: Mon, 14 Sep 2026 22:51:26 -0400 Subject: [PATCH 1/2] crypto: Migrate hash functionality from baselibs Add SHA-256, SHA-384, and SHA-512 support across the public API, daemon, OpenSSL, and PKCS#11 providers. Preserve empty-input, streaming, reset, digest-size, and actionable error behavior. Complete explicit and preferred provider selection, enforce stable wire values and request shapes, and keep provider validation failures retryable. Add known-answer, binary-input, provider, IPC/SHM, and integration coverage. Document the QM boundary and the Baselibs consumer migration mapping. Refs: #125 --- .gitattributes | 1 + README.md | 130 ++++ docs/features/crypto/architecture/index.rst | 170 ++-- examples/README.md | 4 + examples/hashing_example.cpp | 52 +- .../docs/architecture/api_description.rst | 4 +- .../docs/architecture/design_decisions.rst | 2 +- .../architecture/dynamic_architecture.rst | 6 +- score/crypto/docs/architecture/index.rst | 15 +- score/crypto/docs/architecture/interfaces.rst | 5 +- .../architecture/provider_architecture.rst | 123 +++ .../crypto/docs/requirements/requirements.rst | 134 +++- .../docs/safety_analysis/aou_requirements.rst | 106 ++- score/crypto/src/api/BUILD | 16 +- score/crypto/src/api/common/types.hpp | 45 +- .../src/api/config/base_context_config.hpp | 2 +- .../src/api/config/hash_context_config.hpp | 5 +- .../src/api/contexts/i_hash_context.hpp | 8 +- .../api/contexts/src/hash_context_impl.cpp | 42 +- .../api/contexts/src/hash_context_impl.hpp | 2 +- .../data_plane/src/shm_memory_allocator.cpp | 7 +- .../src/api/src/crypto_context_impl.cpp | 80 +- .../src/api/src/crypto_context_impl_test.cpp | 91 +++ .../src/api/src/provider_type_converter.hpp | 8 +- .../src/daemon/common/algorithm_info.hpp | 58 +- .../daemon/control_plane/control_protocol.h | 18 + score/crypto/src/daemon/mediator/BUILD | 21 +- .../daemon/mediator/mediator_operations.hpp | 15 +- .../src/daemon/mediator/src/mediator_impl.cpp | 336 ++++++-- .../mediator/src/mediator_impl_test.cpp | 252 ++++++ .../operations/hash_handler_operations.hpp | 8 +- .../provider/handler/src/handler_utils.cpp | 32 +- .../provider/handler/src/handler_utils.hpp | 28 +- score/crypto/src/daemon/provider/pkcs11/BUILD | 4 + .../pkcs11/detail/pkcs11_algorithm_info.hpp | 40 +- .../factory/pkcs11_handler_factory.cpp | 44 +- .../operations/hash/pkcs11_hash_executor.cpp | 172 +++-- .../operations/hash/pkcs11_hash_executor.hpp | 28 +- .../operations/hash/pkcs11_hash_handler.cpp | 88 ++- .../operations/hash/pkcs11_hash_handler.hpp | 4 +- .../daemon/provider/pkcs11/pkcs11_module.hpp | 23 +- .../provider/pkcs11/pkcs11_provider.hpp | 19 +- .../provider/pkcs11/src/pkcs11_module.cpp | 20 +- .../provider/pkcs11/src/pkcs11_provider.cpp | 34 +- .../provider/score_provider/openssl/BUILD | 1 + .../openssl/detail/openssl_algorithm_info.hpp | 43 +- .../operations/hash/openssl_hash_handler.cpp | 149 +--- .../operations/hash/openssl_hash_handler.hpp | 22 +- .../operations/hash/score_hash_handler.hpp | 14 +- .../operations/hash/src/hash_executor.cpp | 102 ++- .../hash/src/score_hash_handler.cpp | 21 +- .../daemon/provider/src/provider_manager.cpp | 10 + .../daemon/provider/tests/provider_test/BUILD | 3 +- .../provider_test/test_pkcs11_provider.cpp | 727 ++++++++++++++++-- .../tests/provider_test/test_provider.cpp | 641 ++++++++++----- .../grpc_control_plane/test_control_plane.cpp | 18 + .../integration_tests/score_api_hash_test.cpp | 320 ++++++-- score/tests/test_vectors/hash/input_abc.bin | 1 + score/tests/test_vectors/hash/input_empty.bin | 0 score/tests/test_vectors/hash/reference.md | 17 +- score/tests/test_vectors/hash/sha256_abc.bin | Bin 0 -> 32 bytes .../tests/test_vectors/hash/sha256_empty.bin | 1 + score/tests/test_vectors/hash/sha384_abc.bin | Bin 0 -> 48 bytes .../hash/sha384_complete_data.bin | 1 + .../tests/test_vectors/hash/sha384_empty.bin | 1 + score/tests/test_vectors/hash/sha512_abc.bin | 2 + .../hash/sha512_complete_data.bin | 1 + .../tests/test_vectors/hash/sha512_empty.bin | 1 + .../test_vectors/hash/sha512_hello_world.bin | Bin 0 -> 64 bytes 69 files changed, 3359 insertions(+), 1039 deletions(-) create mode 100644 score/crypto/src/api/src/crypto_context_impl_test.cpp create mode 100644 score/crypto/src/daemon/mediator/src/mediator_impl_test.cpp create mode 100644 score/tests/test_vectors/hash/input_abc.bin create mode 100644 score/tests/test_vectors/hash/input_empty.bin create mode 100644 score/tests/test_vectors/hash/sha256_abc.bin create mode 100644 score/tests/test_vectors/hash/sha256_empty.bin create mode 100644 score/tests/test_vectors/hash/sha384_abc.bin create mode 100644 score/tests/test_vectors/hash/sha384_complete_data.bin create mode 100644 score/tests/test_vectors/hash/sha384_empty.bin create mode 100644 score/tests/test_vectors/hash/sha512_abc.bin create mode 100644 score/tests/test_vectors/hash/sha512_complete_data.bin create mode 100644 score/tests/test_vectors/hash/sha512_empty.bin create mode 100644 score/tests/test_vectors/hash/sha512_hello_world.bin diff --git a/.gitattributes b/.gitattributes index 8d935b038..78144a154 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,3 +1,4 @@ score/iav_primula/src/*.rs text eol=lf score/iav_primula/tests/*.rs text eol=lf score/iav_primula/tests/BUILD text eol=lf +score/tests/test_vectors/hash/*.bin binary diff --git a/README.md b/README.md index 6948b644a..e1ab1277b 100644 --- a/README.md +++ b/README.md @@ -183,6 +183,136 @@ When used with macros like `dash_license_checker`, it allows dynamic selection o ## DevContainer Setup +The supported development environment is defined by +`.devcontainer/devcontainer.json` and `.devcontainer/Dockerfile`. It currently +uses `ghcr.io/eclipse-score/devcontainer:v1.11.0` and enables Docker-in-Docker +for integration tests. + +### Prerequisites + +- Docker Desktop or a compatible Docker Engine +- Visual Studio Code with the **Dev Containers** extension +- A local clone of this repository + +The `code` and `devcontainer` terminal commands are optional and are not +installed automatically with the macOS applications. + +Create the host paths that are mounted by the devcontainer before opening it: + +```bash +touch ~/.netrc +mkdir -p ~/.cache/bazel ~/.qnx/license +``` + +The QNX license directory may remain empty when only Linux targets are built. +The `.netrc` file may also remain empty when no authenticated dependency source +is required, but it must exist because the devcontainer mounts it as a file. + +### Open the official environment + +On macOS, open the repository without requiring the optional `code` command: + +```bash +open -a "Visual Studio Code" . +``` + +Alternatively, open Visual Studio Code from Applications and select +**File → Open Folder**. To enable `code .` later, open the Command Palette and +run **Shell Command: Install 'code' command in PATH**. + +1. Open the repository root in Visual Studio Code using either method above. +2. Run **Dev Containers: Reopen in Container** from the Command Palette. +3. Wait for the image build and the `onCreateCommand` setup to finish. +4. Open a new VS Code terminal. The terminal is now running inside the official + development container. + +Confirm the expected tools are available: + +```bash +bazel --version +clang-format --version +docker version +uname -m +``` + +The separate Dev Container CLI is only needed for terminal-based startup. Since +Node.js and npm are already available, it can be installed and verified with: + +```bash +npm install -g @devcontainers/cli +rehash +devcontainer --version +``` + +After the optional CLI is installed, the same environment can be started with: + +```bash +devcontainer up --workspace-folder . +devcontainer exec --workspace-folder . bash +``` + +The PR Linux workflow runs on x86_64. On Apple Silicon, the multi-architecture +image starts as `aarch64`; the crypto targets and focused tests below run in +that environment, but the repository-wide `//score/...` set currently reaches +an upstream `score_logging` Rust bridge that has no native aarch64 layout +configuration. Run the full CI-equivalent suite in an x86_64 Linux devcontainer +or CI runner. With the Dev Container CLI, an x86_64 container can be requested +from Apple Silicon as follows when Docker emulation is enabled: + +```bash +DOCKER_DEFAULT_PLATFORM=linux/amd64 devcontainer up --workspace-folder . +devcontainer exec --workspace-folder . bash +``` + +### Build and test inside the devcontainer + +Run all commands below from the devcontainer terminal: + +```bash +# Build the crypto component (also usable in the native Apple Silicon container). +bazel build //score/crypto/... + +# Build every Linux target on the x86_64 CI architecture. +bazel build //score/... + +# Run the provider-level hash tests used by the Baselibs hash migration. +bazel test \ + //score/crypto/src/daemon/provider/tests/provider_test:test_provider \ + //score/crypto/src/daemon/provider/tests/provider_test:test_pkcs11_provider \ + --test_output=errors + +# The Docker-based integration test needs this image in the devcontainer's +# Docker daemon. Pull it once after creating or rebuilding the devcontainer. +docker pull ubuntu:24.04 +bazel test //score/tests/integration_tests:integration_test \ + --test_output=all \ + --cache_test_results=no + +# Run the complete PR test scope on the x86_64 CI architecture. +bazel test //score/... --test_output=errors + +# Build the project documentation. +bazel run //:docs +``` + +`bazel run //examples:hashing_example` is a client-only example. It expects a +configured crypto daemon already listening on +`unix:///tmp/crypto_daemon.sock`; use the Docker integration target above for a +self-contained daemon-and-client execution. + +### Formatting and repository checks + +The repository's pre-commit configuration runs Bazel metadata checks, +`clang-format`, `clang-tidy`, and the Eclipse copyright checker: + +```bash +pre-commit run --all-files +``` + +The current upstream workflow temporarily skips pre-commit in the common PR +job because the repository-wide clang-tidy baseline is not yet clean. Changed +C++ files must still follow the checked-in `.clang-format` configuration. + ### Known Issue: Pre-commit Hook Not Running **Problem:** The pre-commit hook does not run when using `git commit` inside the DevContainer. diff --git a/docs/features/crypto/architecture/index.rst b/docs/features/crypto/architecture/index.rst index 66cf88c86..7fb121d37 100644 --- a/docs/features/crypto/architecture/index.rst +++ b/docs/features/crypto/architecture/index.rst @@ -12,7 +12,7 @@ # SPDX-License-Identifier: Apache-2.0 # ******************************************************************************* -.. _feature_architecture_example: +.. _crypto_feature_architecture: Feature Architecture ==================== @@ -21,92 +21,152 @@ Feature Architecture :id: doc__crypto_feat_architecture :version: 1 :status: draft - :safety: ASIL_B + :safety: QM :security: YES :realizes: wp__feature_arch - :tags: template + +.. feat:: Security & Crypto + :id: feat__security_crypto + :version: 1 + :security: YES + :safety: QM + :status: valid Overview -------- - -Description ------------ +The Security & Crypto feature provides applications with a provider-independent +way to execute cryptographic operations without coupling application code to a +specific software library, PKCS#11 token, HSM, or TEE. The current implementation +uses a client-daemon split: applications use the C++ API, while a dedicated daemon +selects providers, owns operation contexts, and executes cryptographic jobs. - +The feature is security-relevant but currently classified as QM. In particular, +the complete client, IPC/shared-memory, daemon, and provider path is not presented +as an ISO 26262-qualified replacement for safety-certified Baselibs hashing. - +Description +----------- - +The feature is decomposed into the following responsibilities: + +* The public API creates a crypto stack, logical crypto contexts, and typed + operation contexts such as hash and MAC contexts. +* The control plane serializes lifecycle and operation requests and transports + them to the daemon. +* The data plane selects in-band, pooled shared-memory, or registered bulk + shared-memory transport based on the supplied buffers. +* The daemon validates requests, maintains context state, selects a provider, + and dispatches the requested job. +* Provider adapters translate the common operation contract to a concrete + backend. The implemented provider families include OpenSSL and PKCS#11. +* Key-management services resolve key slots and provider-owned key objects + without exposing provider-specific handles through the application API. + +Important design decisions are: + +* Provider-independent algorithm identifiers are part of the API and wire + contract. Provider-native identifiers are resolved only inside the daemon. +* Operation contexts are explicit resources with a daemon-managed lifecycle. +* Large buffers use validated shared-memory references to avoid unnecessary + copies while keeping ownership with the caller. +* Provider selection is performed during context creation; an operation does + not silently change providers after the context has been created. +* Streaming jobs use an explicit state machine so invalid ordering is rejected + before a provider call is made. + +The design is constrained by provider capability differences, PKCS#11 token and +session limits, process-boundary failure modes, and the lifetime of caller-owned +buffers. Applications must handle unavailable providers and unsupported +algorithms explicitly. Algorithms with variable output or provider-specific +parameters require an API-level contract before they can be exposed portably. Requirements ------------ -The requirements for the feature architecture are defined in the `requirements` section of the feature documentation in the project repository. +Component requirements and assumptions of use are maintained with the Crypto +component documentation. Feature-level ownership and cross-repository consumer +migration for the Baselibs hash transition remain tracked by +`inc_security_crypto issue #125 `_. +The consumer migration and removal of Baselibs algorithms are deliberately not +claimed by this repository's component requirements. Rationale Behind Architecture Decomposition ******************************************* -Mandatory: A motivation for the decomposition - -.. note:: Common decisions across features / cross cutting concepts is at the high level. +The process boundary isolates applications from provider initialization, +credentials, sessions, and provider-specific failure handling. Separating the +control and data planes permits small requests to remain simple while large data +can use shared memory. A common handler contract lets OpenSSL serve development +and software deployments while PKCS#11 connects the same API to hardware-backed +implementations. Keeping key and operation resources in the daemon also reduces +the amount of provider-specific state exposed to clients. Static Architecture ------------------- - - -.. note:: - The Architecture can be split into multiple files, it is an high level architecture design - which can be shown without actual c++/rust interfaces and data types - and there will be link to internal architecture till code to get actual api descriptions. - -.. code-block:: rst - - .. feat_arc_sta:: Feature Static View - :id: feat_arc_sta__feature_name__static_view - :security: YES - :safety: ASIL_B - :status: invalid - :fulfils: feat_req__feature_name__some_title - :includes: logic_arc_int__feature_name__interface_name1 - :belongs_to: feat__feature_name - - .. needarch:: - :scale: 50 - :align: center - - {{ draw_feature(need(), needs) }} +The principal static dependencies are: + +.. code-block:: text + + Application + | + v + Crypto C++ API -- Control plane client -- IPC -- Crypto daemon + | | + +-- Buffer/SHM data plane -------------+ + | + v + Provider manager + / \ + v v + OpenSSL provider PKCS#11 provider + | + v + Token / HSM / TEE + +Detailed component, interface, data-plane, provider, and key-management views +are available in the ``score/crypto/docs/architecture`` documentation. Dynamic Architecture -------------------- - - -.. code-block:: rst - - .. feat_arc_dyn:: Dynamic View - :id: feat_arc_dyn__feature_name__dynamic_view - :security: YES - :safety: ASIL_B - :status: invalid - :fulfils: feat_req__feature_name__some_title - :belongs_to: feat__feature_name - - Put here a sequence diagram +A typical operation follows this sequence: + +#. The application creates a stack and connects to the configured daemon + endpoint. +#. It creates a crypto context and requests a typed operation context with an + algorithm and optional provider selection. +#. The daemon resolves and validates the provider, creates a handler, and + returns an opaque context identifier. +#. Each operation request carries control metadata and either in-band data or + validated shared-memory references. +#. The handler validates the operation state and dispatches to the selected + provider. +#. The daemon returns a status and output length; output bytes are written to + the caller-owned buffer. +#. Reset returns a reusable operation context to its idle state, while context + destruction releases daemon and provider resources. + +For hashing, valid flows are ``SingleShot`` or ``Init`` followed by zero or more +``Update`` calls and ``Finalize``. A retryable validation error such as an +undersized final output buffer does not consume the active stream. Logical Interfaces ------------------ -The logical interfaces of the feature are defined in the `logical interfaces` section of the feature documentation in the project repository. - -See `SCORE Features `_ for more information. +The public logical interface is the provider-independent Crypto C++ API. The +client-daemon protocol and provider handler interfaces are internal logical +interfaces and are versioned with the component implementation. Provider-native +APIs, including OpenSSL EVP and PKCS#11 Cryptoki, terminate at their respective +daemon adapters and are not exposed to applications. Used Components --------------- -The components used by the feature are defined in the `components` section of the module documentation. - -See :ref:`component_template` for an example component. +The feature is currently realized by the ``Crypto`` component +(``comp__crypto``). External consumers and the eventual Baselibs cleanup are +separate repository changes and are outside this component's implementation +boundary. diff --git a/examples/README.md b/examples/README.md index 9eb554b2e..8d013cc4a 100644 --- a/examples/README.md +++ b/examples/README.md @@ -16,3 +16,7 @@ The example source files are only kept for reference. For an actual executable or sample application refer to the tests/integration_tests folder. + +The hashing example uses the canonical, case-sensitive API identifier `SHA256`. +The corresponding standard algorithm name is SHA-256. The other migration-target +identifiers are `SHA384` and `SHA512`; hyphenated API aliases are not currently supported. diff --git a/examples/hashing_example.cpp b/examples/hashing_example.cpp index 8bd1c6939..be822d1d8 100644 --- a/examples/hashing_example.cpp +++ b/examples/hashing_example.cpp @@ -51,7 +51,7 @@ int main() { // 1. Create the crypto stack and connect to the daemon CryptoStackConfig stack_config; - stack_config.SetConnectionEndpoint("unix:///var/run/crypto-daemon.sock"); + stack_config.SetConnectionEndpoint("unix:///tmp/crypto_daemon.sock"); auto stack_result = CreateCryptoStack(stack_config); if (!stack_result.has_value()) @@ -72,7 +72,7 @@ int main() // 3. Configure and create a SHA-256 hash context HashContextConfig hash_config; - hash_config.SetAlgorithm("SHA-256"); + hash_config.SetAlgorithm("SHA256"); auto hash_result = ctx->CreateHashContext(hash_config); if (!hash_result.has_value()) @@ -96,8 +96,12 @@ int main() return 1; } - hash->Update({reinterpret_cast(chunk1), std::strlen(chunk1)}); - hash->Update({reinterpret_cast(chunk2), std::strlen(chunk2)}); + if (!hash->Update({reinterpret_cast(chunk1), std::strlen(chunk1)}).has_value() || + !hash->Update({reinterpret_cast(chunk2), std::strlen(chunk2)}).has_value()) + { + std::cout << "Error: Update failed" << std::endl; + return 1; + } auto finalize_result = hash->Finalize({digest.data(), digest.size()}); if (!finalize_result.has_value()) @@ -128,6 +132,11 @@ int main() { std::cout << "OK: Streaming and single-shot digests match" << std::endl; } + else + { + std::cout << "Error: Streaming and single-shot digests differ" << std::endl; + return 1; + } // 7. Context reuse via Reset() // Reset() returns the context to its post-construction state — the key @@ -146,8 +155,12 @@ int main() const char* second_msg = "Context reuse is efficient!"; std::array digest3{}; - hash->Init(); - hash->Update({reinterpret_cast(second_msg), std::strlen(second_msg)}); + if (!hash->Init().has_value() || + !hash->Update({reinterpret_cast(second_msg), std::strlen(second_msg)}).has_value()) + { + std::cout << "Error: Reused context initialization or update failed" << std::endl; + return 1; + } auto finalize3 = hash->Finalize({digest3.data(), digest3.size()}); if (!finalize3.has_value()) { @@ -157,19 +170,32 @@ int main() PrintHex("Reused-ctx SHA-256", digest3.data(), finalize3.value()); // Reset() also works mid-stream to abort and restart - hash->Init(); - hash->Update({reinterpret_cast(chunk1), std::strlen(chunk1)}); - hash->Reset(); // discard partial work - - hash->Init(); - hash->Update({reinterpret_cast(second_msg), std::strlen(second_msg)}); + if (!hash->Init().has_value() || + !hash->Update({reinterpret_cast(chunk1), std::strlen(chunk1)}).has_value() || + !hash->Reset().has_value() || // discard partial work + !hash->Init().has_value() || + !hash->Update({reinterpret_cast(second_msg), std::strlen(second_msg)}).has_value()) + { + std::cout << "Error: Mid-stream reset sequence failed" << std::endl; + return 1; + } std::array digest4{}; - hash->Finalize({digest4.data(), digest4.size()}); + auto finalize4 = hash->Finalize({digest4.data(), digest4.size()}); + if (!finalize4.has_value()) + { + std::cout << "Error: Finalize after mid-stream Reset failed" << std::endl; + return 1; + } if (digest3 == digest4) { std::cout << "OK: Reset mid-stream + re-hash matches" << std::endl; } + else + { + std::cout << "Error: Reset mid-stream + re-hash differs" << std::endl; + return 1; + } // 8. Query digest size auto digest_size = hash->GetDigestSize(); diff --git a/score/crypto/docs/architecture/api_description.rst b/score/crypto/docs/architecture/api_description.rst index c5e3b4b4a..3d8044e56 100644 --- a/score/crypto/docs/architecture/api_description.rst +++ b/score/crypto/docs/architecture/api_description.rst @@ -43,7 +43,7 @@ The API uses a two-phase resource identification model: ResourceType type; // kProvider, kKeySlot, kCertSlot, kVerificationTrustStore, // kKey, kCertificate, kCrl, kSecureObject, kDataObject ResourcePersistence persistence; // kPersistent or kEphemeral - uint16_t primary_provider; // owning device/provider index (0 = unbound) + uint16_t primary_provider; // owning device/provider index (UINT16_MAX = unbound) }; The struct is fully numeric, cheap to copy and hash, and includes @@ -608,7 +608,7 @@ the WCET bound. // Per-context override: 200 ms for hash, disabled for key gen HashContextConfig hash_cfg; - hash_cfg.SetAlgorithm("SHA-256") + hash_cfg.SetAlgorithm("SHA256") .SetOperationTimeout(std::chrono::milliseconds{200}); KeyManagementContextConfig keygen_cfg; diff --git a/score/crypto/docs/architecture/design_decisions.rst b/score/crypto/docs/architecture/design_decisions.rst index 35361d7ed..6c5c4bf3c 100644 --- a/score/crypto/docs/architecture/design_decisions.rst +++ b/score/crypto/docs/architecture/design_decisions.rst @@ -391,7 +391,7 @@ Context ------- Algorithm identifiers must accommodate current algorithms (e.g., ``"AES-256-GCM"``, -``"SHA-256"``, ``"SLH-DSA-SHA2-128s"``), future PQC schemes, and provider-specific +``"SHA256"``, ``"SLH-DSA-SHA2-128s"``), future PQC schemes, and provider-specific extensions — an open set that cannot be enumerated at compile time. Decision diff --git a/score/crypto/docs/architecture/dynamic_architecture.rst b/score/crypto/docs/architecture/dynamic_architecture.rst index 0b693bda3..215b054b1 100644 --- a/score/crypto/docs/architecture/dynamic_architecture.rst +++ b/score/crypto/docs/architecture/dynamic_architecture.rst @@ -138,7 +138,7 @@ Hashing Example .. code-block:: cpp HashContextConfig hash_config; - hash_config.SetAlgorithm("SHA-256"); + hash_config.SetAlgorithm("SHA256"); auto hash = ctx->CreateHashContext(hash_config).value(); // Streaming @@ -157,7 +157,7 @@ Context Reuse via Reset() // Create the context once — expensive (factory + IPC) HashContextConfig hash_config; - hash_config.SetAlgorithm("SHA-256"); + hash_config.SetAlgorithm("SHA256"); auto hash = ctx->CreateHashContext(hash_config).value(); // First message @@ -282,7 +282,7 @@ Bounding all IPC calls with a per-call deadline for safety analysis: // 2. Per-context override: tighter 200 ms deadline for hashing HashContextConfig hash_cfg; - hash_cfg.SetAlgorithm("SHA-256") + hash_cfg.SetAlgorithm("SHA256") .SetOperationTimeout(std::chrono::milliseconds{200}); auto hash = ctx->CreateHashContext(hash_cfg).value(); diff --git a/score/crypto/docs/architecture/index.rst b/score/crypto/docs/architecture/index.rst index 6b4097534..27e5290e7 100644 --- a/score/crypto/docs/architecture/index.rst +++ b/score/crypto/docs/architecture/index.rst @@ -90,14 +90,13 @@ Static Architecture The components are designed to cover the expectations from the feature architecture (i.e. if already exists a definition it should be taken over and enriched). -.. code-block:: rst - - .. comp:: Crypto - :id: comp__crypto - :security: YES - :safety: QM - :status: invalid - :implements: +.. comp:: Crypto + :id: comp__crypto + :version: 1 + :security: YES + :safety: QM + :status: valid + :belongs_to: feat__security_crypto .. image:: component_overview.png :align: center diff --git a/score/crypto/docs/architecture/interfaces.rst b/score/crypto/docs/architecture/interfaces.rst index d8e5a395c..2c84e3697 100644 --- a/score/crypto/docs/architecture/interfaces.rst +++ b/score/crypto/docs/architecture/interfaces.rst @@ -350,7 +350,10 @@ The public API surface is organized into the following interface groups: exposes ``Init()``, ``Update()``, ``Reset()``, and ``Finalize()`` from the base classes via ``using``-declarations plus ``SingleShot()`` and ``GetDigestSize()``. ``GetOutputSize()`` is intentionally not - exposed — use ``GetDigestSize()`` instead. + exposed — use ``GetDigestSize()`` instead. New integrations use the + case-sensitive identifiers ``SHA256``, ``SHA384``, and ``SHA512``; + provider and token mechanism availability is checked at context creation + or operation execution. .. real_arc_int:: IMacContext :id: real_arc_int__crypto__i_mac_context diff --git a/score/crypto/docs/architecture/provider_architecture.rst b/score/crypto/docs/architecture/provider_architecture.rst index 72d5eb806..93d5bcbdc 100644 --- a/score/crypto/docs/architecture/provider_architecture.rst +++ b/score/crypto/docs/architecture/provider_architecture.rst @@ -66,6 +66,129 @@ integer constants (``HASH_INIT``, ``HASH_UPDATE``, ``HASH_FINALIZE``, Both provider families include these headers directly — the constants are not specific to any algorithm family or provider. +Hash Algorithm Contract +~~~~~~~~~~~~~~~~~~~~~~~ + +Hash algorithm identifiers are case-sensitive wire-level values. The standard +algorithm name and the identifier passed to ``HashContextConfig`` are distinct: + +.. list-table:: Hash algorithms prepared for Baselibs migration + :header-rows: 1 + + * - Standard name + - Canonical API identifier + - Digest size + - OpenSSL mapping + - PKCS#11 mapping + * - SHA-256 + - ``SHA256`` + - 32 bytes + - ``EVP_sha256`` + - ``CKM_SHA256`` + * - SHA-384 + - ``SHA384`` + - 48 bytes + - ``EVP_sha384`` + - ``CKM_SHA384`` + * - SHA-512 + - ``SHA512`` + - 64 bytes + - ``EVP_sha512`` + - ``CKM_SHA512`` + +OpenSSL support is determined from the provider's EVP mapping. PKCS#11 support +requires both a daemon mapping and a token that implements the corresponding +mechanism. A configured hardware provider therefore may reject an algorithm +that the software provider supports; the daemon must return an explicit error +rather than silently select another algorithm or digest size. + +For PKCS#11, ``Pkcs11HandlerFactory`` resolves the canonical identifier to a +``CK_MECHANISM_TYPE`` and calls ``C_GetMechanismInfo`` for the provider's +selected slot before acquiring a session or constructing the hash handler. A +mechanism is accepted for hashing only when its returned flags include +``CKF_DIGEST``; merely being listed by the token is not sufficient. +``CKR_MECHANISM_INVALID`` is reported to the client as +``CryptoErrorCode::kUnsupportedAlgorithm``; other PKCS#11 query failures are +translated through the daemon error mapping. + +Both streaming and single-shot operations accept empty input. A streaming +caller may invoke ``Init()`` followed directly by ``Finalize()`` without an +intermediate ``Update()``; the result is the standard digest of the empty byte +sequence. + +SHA-224, SHA-1, and MD5 remain available for compatibility with existing +callers but are not approved targets for new Baselibs migrations. SHA-3, SHAKE, +CRC32, and CRC32 AUTOSAR are outside the current hash provider contract. + +Baselibs Migration Boundary +~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The algorithm mapping for consumers moving from ``score/hash`` is: + +.. list-table:: Baselibs to Crypto hash mapping + :header-rows: 1 + + * - Baselibs value + - Crypto identifier + - Crypto usage + * - ``HashAlgorithm::kSha256`` + - ``SHA256`` + - ``IHashContext`` streaming or single-shot + * - ``HashAlgorithm::kSha384`` + - ``SHA384`` + - ``IHashContext`` streaming or single-shot + * - ``HashAlgorithm::kSha512`` + - ``SHA512`` + - ``IHashContext`` streaming or single-shot + +The Crypto API returns raw digest bytes into a caller-owned buffer. Baselibs +``Hash``, ``TypedHash``, hexadecimal conversion, and ``std::istream`` helpers are +not reproduced here. A migrating consumer reads stream chunks and calls +``Update()`` itself, and owns any representation adapter it still requires. + +.. list-table:: Baselibs API migration guide + :header-rows: 1 + + * - Baselibs API + - Crypto API + - Migration note + * - ``HashCalculatorFactory::CreateHashCalculator(algorithm)`` + - ``ICryptoContext::CreateHashContext(config)`` + - Put the canonical string identifier in ``HashContextConfig`` and handle + context-creation errors. + * - ``IHashCalculator::Update(data)`` + - ``IHashContext::Init()`` followed by ``Update(data)`` + - ``Init`` is explicit and ``Update`` may be called zero or more times. + * - ``IHashCalculator::Finalize()`` + - ``IHashContext::Finalize(output)`` + - Allocate caller-owned output using ``GetDigestSize`` and consume only + the returned byte count. + * - ``IHashCalculatorFactory::CalculateHash(algorithm, data)`` + - ``IHashContext::SingleShot(input, output)`` + - Context creation is separate from execution, which permits reuse. + * - ``CalculateHash(algorithm, std::istream&)`` and + ``UpdateFromStream(std::istream&)`` + - Caller-managed read loop plus ``Update`` + - Stream ownership, chunk sizing, read errors, and maximum-read behavior + remain the consumer's responsibility. + * - ``Hash`` / ``TypedHash`` result objects + - Raw digest in caller-owned ``span`` + - Preserve any algorithm tag, fixed-size wrapper, equality policy, or + serialization in a consumer-side adapter. + * - Baselibs hexadecimal helpers + - Consumer-owned encoding adapter + - Do not hex-encode bytes before passing them to the hash operation. + +An undersized ``Finalize`` output is retryable: the caller may supply a larger +buffer and call ``Finalize`` again without replaying the input. Other provider or +transport errors must be handled according to their returned error code; callers +must not assume that every failure preserves a streaming operation. + +This mapping does not include CRC variants or the Baselibs native safety SHA-256 +implementation. The client, IPC/shared-memory transport, daemon, and provider +path described here is QM functionality and is not an ISO 26262-qualified +replacement unless the complete deployed path is independently qualified. + Provider Configuration ~~~~~~~~~~~~~~~~~~~~~~ diff --git a/score/crypto/docs/requirements/requirements.rst b/score/crypto/docs/requirements/requirements.rst index 692ff67b3..9c1613fee 100644 --- a/score/crypto/docs/requirements/requirements.rst +++ b/score/crypto/docs/requirements/requirements.rst @@ -13,7 +13,7 @@ # ******************************************************************************* Crypto Requirements -#################### +################### .. document:: Crypto Requirements :id: doc__crypto_requirements @@ -23,68 +23,118 @@ Crypto Requirements :security: YES :realizes: wp__requirements_comp[version==1] +Hash Functionality +================== +The requirements in this section specify the current cryptographic hash API. +They describe QM functionality and do not constitute an ISO 26262 qualification +claim for the complete client, IPC, daemon, and provider execution path. +Feature-level requirements and their ``derived_from`` links remain owned by the +S-CORE feature repository and will be linked when the cross-repository migration +tracked by issue #125 is integrated. These component requirements cover only +the implementation in ``inc_security_crypto``. - -=================================================================== +.. comp_req:: Provide migration-target hash algorithms + :id: comp_req__crypto__hash_migration_algorithms + :version: 1 + :reqtype: Functional + :security: YES + :safety: QM + :status: valid + :satisfied_by: comp__crypto -Functional Requirements ------------------------ + The Crypto component shall provide SHA-256, SHA-384, and SHA-512 hash + operations through the canonical algorithm identifiers ``SHA256``, + ``SHA384``, and ``SHA512`` respectively. -.. code-block:: +.. comp_req:: Provide streaming hash operation + :id: comp_req__crypto__hash_streaming + :version: 1 + :reqtype: Functional + :security: YES + :safety: QM + :status: valid + :satisfied_by: comp__crypto - .. comp_req:: Some Title - :id: comp_req__crypto__func_req_example - :reqtype: Process - :security: YES - :safety: ASIL_B - :derived_from: - :status: valid - :satisfied_by: comp__crypto + The Crypto component shall support incremental hashing through the ordered + ``Init()``, zero or more ``Update()``, and ``Finalize()`` operations. - The Component shall do xyz to another component to bring it to this condition at this time +.. comp_req:: Provide single-shot hash operation + :id: comp_req__crypto__hash_single_shot + :version: 1 + :reqtype: Functional + :security: YES + :safety: QM + :status: valid + :satisfied_by: comp__crypto - Note: (optional, not to be verified) + The Crypto component shall support hashing an input buffer through + ``SingleShot()`` and shall produce the same digest as the corresponding + streaming operation. +.. comp_req:: Report hash digest size + :id: comp_req__crypto__hash_digest_size + :version: 1 + :reqtype: Interface + :security: YES + :safety: QM + :status: valid + :satisfied_by: comp__crypto -Assumption of Use Requirements ------------------------------- + The Crypto component shall report digest sizes of 32, 48, and 64 bytes for + ``SHA256``, ``SHA384``, and ``SHA512`` respectively. -.. aou_req:: Crypto AoU Requirement Example - :id: aou_req__crypto_aou__next_title +.. comp_req:: Reject undersized hash output buffers + :id: comp_req__crypto__hash_output_buffer :version: 1 - :reqtype: Process + :reqtype: Interface :security: YES - :safety: ASIL_B + :safety: QM :status: valid + :satisfied_by: comp__crypto - The Component User shall do xyz to use the component safely/securely - -Environmental Requirements --------------------------- + The Crypto component shall reject a hash operation when the caller-provided + output buffer is smaller than the digest size of the configured algorithm. + For a streaming operation, this validation error shall not consume the + active digest state, allowing ``Finalize()`` to be retried with a sufficient + output buffer. -.. aou_req:: Crypto Environmental Requirement Example - :id: aou_req__crypto__crypto_env_req_ex +.. comp_req:: Reject unsupported hash algorithms + :id: comp_req__crypto__hash_unsupported_algorithm :version: 1 - :reqtype: Process + :reqtype: Functional :security: YES - :safety: ASIL_B - :status: invalid - :tags: environment + :safety: QM + :status: valid + :satisfied_by: comp__crypto - The Component shall only be used in a xyz environment to ensure its proper functioning. + The Crypto component shall report an unsupported-algorithm error when a hash + algorithm cannot be resolved by the selected provider and shall not substitute + a digest size or another algorithm. -Hints ------ +.. comp_req:: Maintain provider-equivalent hash results + :id: comp_req__crypto__hash_provider_parity + :version: 1 + :reqtype: Functional + :security: YES + :safety: QM + :status: valid + :satisfied_by: comp__crypto + + For a migration-target algorithm supported by both providers, the OpenSSL and + PKCS#11 providers shall produce identical digests for identical input bytes. -.. attention:: - The above directives must be updated according to your feature requirements. +.. comp_req:: Reset reusable hash contexts + :id: comp_req__crypto__hash_context_reset + :version: 1 + :reqtype: Functional + :security: YES + :safety: QM + :status: valid + :satisfied_by: comp__crypto - - Replace the example content by the real content for your first requirement (according to :need:`gd_guidl__req_engineering`) - - Set ``safety`` and ``security`` to the right value (ASIL B/QM; YES/NO) - - Set ``reqtype`` with a link to the right value () - - Add other needed requirements for your feature - - Set ``status`` to ``valid`` and start the review/merge process + The Crypto component shall allow an initialized or completed hash context to + be reset to the idle state without changing its algorithm or provider binding. .. needextend:: "c.this_doc()" :+tags: crypto diff --git a/score/crypto/docs/safety_analysis/aou_requirements.rst b/score/crypto/docs/safety_analysis/aou_requirements.rst index e9992de27..d82cbfb35 100644 --- a/score/crypto/docs/safety_analysis/aou_requirements.rst +++ b/score/crypto/docs/safety_analysis/aou_requirements.rst @@ -23,30 +23,96 @@ AoU Component Requirements :security: YES :realizes: wp__requirements_comp_aou +Hash Assumptions of Use +----------------------- -This page contains Assumption of Use requirement snippets that belong to the -template repository. +.. aou_req:: Use canonical hash identifiers + :id: aou_req__crypto__canonical_hash_identifiers + :version: 1 + :reqtype: Process + :security: YES + :safety: QM + :status: valid + + The component user shall select SHA-256, SHA-384, and SHA-512 with the + canonical identifiers ``SHA256``, ``SHA384``, and ``SHA512``. Hyphenated + aliases such as ``SHA-256`` are not part of the current API contract. + +.. aou_req:: Allocate hash output from the reported digest size + :id: aou_req__crypto__hash_output_buffer + :version: 1 + :reqtype: Process + :security: YES + :safety: QM + :status: valid -Component AoU -------------- + The component user shall provide a writable output buffer whose size is at + least the value returned by ``IHashContext::GetDigestSize()`` and shall use + the returned byte count when consuming the digest. A returned digest size of + zero indicates that the daemon query failed and shall be treated as an + operation failure rather than as a valid output size. -.. code-block:: rst +.. aou_req:: Verify provider and mechanism availability + :id: aou_req__crypto__hash_provider_availability + :version: 1 + :reqtype: Process + :security: YES + :safety: QM + :status: valid + + The integrator shall ensure that the selected provider is configured and + available. For a PKCS#11 provider, the selected token shall advertise the + required SHA mechanism. Applications shall handle an unsupported-algorithm or + provider-unavailable result without assuming a silent provider fallback. + +.. aou_req:: Keep caller-owned buffers valid for each hash call + :id: aou_req__crypto__hash_buffer_lifetime + :version: 1 + :reqtype: Process + :security: YES + :safety: QM + :status: valid - .. aou_req:: Next Title - :id: aou_req__mod_temp_crypto__next_title - :reqtype: Process - :security: YES - :safety: ASIL_B - :status: invalid + The component user shall keep input and output buffers valid and unmodified + for the duration of the corresponding synchronous ``Update()``, + ``Finalize()``, or ``SingleShot()`` call. - The Component User shall do xyz to use the component safely/securely +.. aou_req:: Handle daemon and operation failures + :id: aou_req__crypto__hash_failure_handling + :version: 1 + :reqtype: Process + :security: YES + :safety: QM + :status: valid + + The component user shall check every returned ``Result`` and define an + application-level reaction for daemon unavailability, operation timeout, + provider failure, invalid stream state, and insufficient output buffer. + +.. aou_req:: Do not claim safety qualification for the current hash path + :id: aou_req__crypto__hash_not_safety_qualified + :version: 1 + :reqtype: Process + :security: YES + :safety: ASIL_B + :status: valid + + The integrator shall not use the current client, IPC and shared-memory, + daemon, and OpenSSL or PKCS#11 hash path as an ISO 26262-qualified safety + mechanism unless the complete deployed path has been independently qualified + and the resulting safety case explicitly permits that use. + +.. aou_req:: Restrict new migrations to approved hash algorithms + :id: aou_req__crypto__hash_legacy_algorithms + :version: 1 + :reqtype: Process + :security: YES + :safety: QM + :status: valid - .. aou_req:: Another Title - :id: aou_req__mod_temp_crypto__another - :reqtype: Process - :security: YES - :safety: ASIL_B - :status: invalid - :tags: environment + New Baselibs migrations shall use ``SHA256``, ``SHA384``, or ``SHA512``. + Existing SHA-224, SHA-1, and MD5 provider support is retained only for + compatibility and shall not be interpreted as a recommendation for new use. - The Component shall only be used in a xyz environment to ensure its proper functioning. +.. needextend:: "c.this_doc()" + :+tags: crypto diff --git a/score/crypto/src/api/BUILD b/score/crypto/src/api/BUILD index 5cf44811d..d382e977e 100644 --- a/score/crypto/src/api/BUILD +++ b/score/crypto/src/api/BUILD @@ -11,7 +11,7 @@ # SPDX-License-Identifier: Apache-2.0 # ******************************************************************************* -load("@rules_cc//cc:defs.bzl", "cc_library") +load("@rules_cc//cc:defs.bzl", "cc_library", "cc_test") cc_library( name = "operations", @@ -27,6 +27,20 @@ cc_library( ], ) +cc_test( + name = "crypto_context_impl_unit_test", + srcs = ["src/crypto_context_impl_test.cpp"], + deps = [ + ":crypto_stack", + "//score/crypto/src/api/common:crypto_common", + "//score/crypto/src/api/config:context_configs", + "//score/crypto/src/api/contexts:context_bases", + "//score/crypto/src/api/control_plane", + "//score/crypto/src/daemon/mediator:mediator_operations", + "@googletest//:gtest_main", + ], +) + cc_library( name = "crypto_stack", srcs = [ diff --git a/score/crypto/src/api/common/types.hpp b/score/crypto/src/api/common/types.hpp index ebeb4918c..b8552e036 100644 --- a/score/crypto/src/api/common/types.hpp +++ b/score/crypto/src/api/common/types.hpp @@ -20,6 +20,7 @@ #include #include #include +#include #include namespace score @@ -44,30 +45,36 @@ using ResourceId = FixedCapacityString<64>; /// at the daemon level without modifying the client library — any algorithm name /// up to 64 characters is accepted at runtime. /// -/// Examples: "AES-256-CBC", "SHA-256", "ECDSA-P256", "ML-KEM-768", "ML-DSA-65", +/// Examples: "AES-256-CBC", "SHA256", "ECDSA-P256", "ML-KEM-768", "ML-DSA-65", /// "SLH-DSA-SHA2-128s", "XMSS-SHA2_10_256" /// /// Implicit conversion to std::string_view enables zero-copy interop. /// Explicit conversion to std::string available for IPC serialization. using AlgorithmId = FixedCapacityString<64>; +/// @brief Sentinel used when a resource is not bound to a crypto provider. +/// +/// Provider IDs are assigned from zero, so zero is a valid provider. The +/// maximum uint16_t value is reserved and must never be assigned by the daemon. +inline constexpr uint16_t kUnboundProviderId = std::numeric_limits::max(); + /// @brief Type of crypto resource managed by the daemon. /// /// kKey and kCertificate identify live objects (key material, parsed certs); /// kKeySlot and kCertSlot identify only persistent storage locations. enum class ResourceType : uint8_t { - kProvider, ///< Crypto provider / device - kKeySlot, ///< Persistent key storage slot - kCertSlot, ///< Persistent certificate storage slot - kVerificationTrustStore, ///< Named group of trusted CA certificates used for certificate chain - ///< verification. - kKey, ///< Key material (generated / loaded / derived / imported) - kCertificate, ///< Parsed or stored certificate object - kCrl, ///< Certificate Revocation List — shares the same numeric id - ///< as the issuer certificate resource (differentiated by type field) - kSecureObject, ///< Secure storage entry - kDataObject ///< Generic data blob + kProvider = 0U, ///< Crypto provider / device + kKeySlot = 1U, ///< Persistent key storage slot + kCertSlot = 2U, ///< Persistent certificate storage slot + kVerificationTrustStore = 3U, ///< Named group of trusted CA certificates used for certificate chain + ///< verification. + kKey = 4U, ///< Key material (generated / loaded / derived / imported) + kCertificate = 5U, ///< Parsed or stored certificate object + kCrl = 6U, ///< Certificate Revocation List — shares the same numeric id + ///< as the issuer certificate resource (differentiated by type field) + kSecureObject = 7U, ///< Secure storage entry + kDataObject = 8U ///< Generic data blob }; /// @brief Persistence classification of a crypto resource. @@ -89,10 +96,10 @@ struct CryptoResourceId uint64_t id{0U}; ///< Daemon-assigned, unique per session ResourceType type{ResourceType::kKeySlot}; ///< Resource classification ResourcePersistence persistence{ResourcePersistence::kEphemeral}; ///< Lifetime - uint16_t primary_provider{0U}; ///< Daemon-assigned numeric provider index. + uint16_t primary_provider{kUnboundProviderId}; ///< Daemon-assigned numeric provider index. ///< Embeds device binding: identifies which ///< provider/device owns this resource. - ///< 0 = unbound (e.g., trust anchors). + ///< kUnboundProviderId when not provider-bound. constexpr bool operator==(const CryptoResourceId& other) const noexcept { @@ -109,11 +116,11 @@ struct CryptoResourceId /// @brief Preference for selecting a crypto provider when not explicitly specified. enum class ProviderType : uint8_t { - kDefault, ///< Daemon selects the most appropriate provider - kHardware, ///< Require a hardware provider (HSM/TEE) - kSoftware, ///< Require a software provider (OpenSSL/wolfSSL) - kHardwarePreferred, ///< Prefer hardware, fall back to software - kSoftwarePreferred ///< Prefer software, fall back to hardware + kDefault = 0U, ///< Daemon selects the most appropriate provider + kHardware = 1U, ///< Require a hardware provider (HSM/TEE) + kSoftware = 2U, ///< Require a software provider (OpenSSL/wolfSSL) + kHardwarePreferred = 3U, ///< Prefer hardware, fall back to software + kSoftwarePreferred = 4U ///< Prefer software, fall back to hardware }; /// @brief Certificate and key data encoding format. diff --git a/score/crypto/src/api/config/base_context_config.hpp b/score/crypto/src/api/config/base_context_config.hpp index efe3cd18f..d84ecf5ee 100644 --- a/score/crypto/src/api/config/base_context_config.hpp +++ b/score/crypto/src/api/config/base_context_config.hpp @@ -36,7 +36,7 @@ namespace crypto /// of the key's primary. struct BaseContextConfig { - /// @brief Algorithm identifier (e.g., "AES-256-CBC", "SHA-384", "ML-DSA-65"). + /// @brief Algorithm identifier (e.g., "AES-256-CBC", "SHA384", "ML-DSA-65"). AlgorithmId algorithm{}; /// @brief Optional resolved provider handle. When set, overrides diff --git a/score/crypto/src/api/config/hash_context_config.hpp b/score/crypto/src/api/config/hash_context_config.hpp index 5402636c0..ffdd7670f 100644 --- a/score/crypto/src/api/config/hash_context_config.hpp +++ b/score/crypto/src/api/config/hash_context_config.hpp @@ -24,13 +24,14 @@ namespace crypto /// @brief Configuration for hash context creation. /// -/// Requires only an algorithm (e.g., "SHA-256", "SHA-384", "SHA3-256", "SHAKE-256"). +/// Requires only an algorithm. The canonical identifiers supported for new +/// integrations are "SHA256", "SHA384", and "SHA512" (without hyphens). /// No key slot is needed for hash operations. /// /// @par Example /// @code /// HashContextConfig config; -/// config.SetAlgorithm("SHA-256"); +/// config.SetAlgorithm("SHA256"); /// auto ctx = crypto_context->CreateHashContext(config); /// @endcode struct HashContextConfig : public BaseContextConfig diff --git a/score/crypto/src/api/contexts/i_hash_context.hpp b/score/crypto/src/api/contexts/i_hash_context.hpp index ff4e736c3..8f55585fc 100644 --- a/score/crypto/src/api/contexts/i_hash_context.hpp +++ b/score/crypto/src/api/contexts/i_hash_context.hpp @@ -34,8 +34,11 @@ namespace crypto /// Init() is exposed with the base default (iv = std::nullopt); /// hash implementations reject a non-null IV. /// -/// Compatible with classical algorithms (SHA-256, SHA-3) and PQC hash-based -/// schemes (e.g., XMSS/LMS hash functions, SHAKE for ML-DSA). +/// SHA-256, SHA-384, and SHA-512 are selected with the canonical identifiers +/// "SHA256", "SHA384", and "SHA512". Availability also depends on the selected +/// daemon provider and, for PKCS#11, the token mechanism set. +/// Empty input is valid: Init() followed directly by Finalize() returns the +/// digest of the empty byte sequence, as does SingleShot() with an empty span. class IHashContext : public IStreamingOutputContext { public: @@ -63,6 +66,7 @@ class IHashContext : public IStreamingOutputContext score::cpp::span output) = 0; /// @brief Returns the digest size in bytes for the configured algorithm. + /// @return Digest size, or zero when the daemon query or response validation fails. /// @note For variable-output hash functions (e.g., SHAKE), returns the /// default output length configured at context creation. virtual std::size_t GetDigestSize() const noexcept = 0; diff --git a/score/crypto/src/api/contexts/src/hash_context_impl.cpp b/score/crypto/src/api/contexts/src/hash_context_impl.cpp index 479716c77..eb1aaeb0e 100644 --- a/score/crypto/src/api/contexts/src/hash_context_impl.cpp +++ b/score/crypto/src/api/contexts/src/hash_context_impl.cpp @@ -45,6 +45,17 @@ namespace proto = ::score::crypto::daemon::control_plane::protocol; namespace actors = ::score::crypto::daemon::common::actors; namespace hash_ops = ::score::crypto::daemon::provider::handler::hash_handler_operations; +namespace +{ + +CryptoErrorCode GetResponseError(const proto::ControlResponseValidator& validator, + const CryptoErrorCode fallback) noexcept +{ + return validator.getErrorCode().value_or(fallback); +} + +} // namespace + HashContextImpl::HashContextImpl(std::shared_ptr connection, uint64_t context_id, AlgorithmId algorithm, @@ -151,14 +162,9 @@ score::Result HashContextImpl::Init(std::optional{score::unexpect, - MakeError(CryptoErrorCode::kOperationFailed, validator.getError())}; + return score::Result{ + score::unexpect, + MakeError(GetResponseError(validator, CryptoErrorCode::kOperationFailed), validator.getError())}; } return std::monostate{}; @@ -195,8 +201,9 @@ score::Result HashContextImpl::Update(score::cpp::span{score::unexpect, - MakeError(CryptoErrorCode::kOperationFailed, validator.getError())}; + return score::Result{ + score::unexpect, + MakeError(GetResponseError(validator, CryptoErrorCode::kOperationFailed), validator.getError())}; } return std::monostate{}; @@ -233,8 +240,9 @@ score::Result HashContextImpl::Finalize(score::cpp::span o if (!validator.isValid()) { - return score::Result{score::unexpect, - MakeError(CryptoErrorCode::kOperationFailed, validator.getError())}; + return score::Result{ + score::unexpect, + MakeError(GetResponseError(validator, CryptoErrorCode::kOperationFailed), validator.getError())}; } return m_transcoder->ExtractOutputBuffer(tspan, validator); @@ -280,8 +288,9 @@ score::Result HashContextImpl::SingleShot(score::cpp::span{score::unexpect, - MakeError(CryptoErrorCode::kOperationFailed, validator.getError())}; + return score::Result{ + score::unexpect, + MakeError(GetResponseError(validator, CryptoErrorCode::kOperationFailed), validator.getError())}; } return m_transcoder->ExtractOutputBuffer(output_tspan, validator); @@ -308,8 +317,9 @@ score::Result HashContextImpl::Reset() if (!validator.isValid()) { - return score::Result{score::unexpect, - MakeError(CryptoErrorCode::kOperationFailed, validator.getError())}; + return score::Result{ + score::unexpect, + MakeError(GetResponseError(validator, CryptoErrorCode::kOperationFailed), validator.getError())}; } return std::monostate{}; diff --git a/score/crypto/src/api/contexts/src/hash_context_impl.hpp b/score/crypto/src/api/contexts/src/hash_context_impl.hpp index 3c117b959..76e455c74 100644 --- a/score/crypto/src/api/contexts/src/hash_context_impl.hpp +++ b/score/crypto/src/api/contexts/src/hash_context_impl.hpp @@ -42,7 +42,7 @@ class HashContextImpl final : public IHashContext /// @brief Constructs a hash context bound to an existing daemon-side context. /// @param connection Shared connection for IPC communication (contains DataNodeId) /// @param context_id Daemon-assigned context identifier (from CTX_CREATE response) - /// @param algorithm Algorithm name (e.g., "SHA-256") for digest size queries + /// @param algorithm Canonical algorithm identifier (e.g., "SHA256") for digest size queries /// @param transcoder Stack-shared buffer-routing abstraction (pool/bulk/in-band). /// Shared with all other contexts in the same CryptoStack. /// When non-null, handles transparent copying via pool SHM. diff --git a/score/crypto/src/api/data_plane/src/shm_memory_allocator.cpp b/score/crypto/src/api/data_plane/src/shm_memory_allocator.cpp index 823da1892..9ff5b97b6 100644 --- a/score/crypto/src/api/data_plane/src/shm_memory_allocator.cpp +++ b/score/crypto/src/api/data_plane/src/shm_memory_allocator.cpp @@ -84,10 +84,9 @@ score::Result ShmMemoryAllocator::Allocate(std::size_t s score::Result ShmMemoryAllocator::Allocate(std::size_t size, const CryptoResourceId& provider) { - const std::optional provider_id = - (provider.primary_provider != med_ops::SHM_WIRE_PROVIDER_ID_UNBOUND) - ? std::optional{provider.primary_provider} - : std::nullopt; + const std::optional provider_id = (provider.primary_provider != kUnboundProviderId) + ? std::optional{provider.primary_provider} + : std::nullopt; auto result = AllocateInternal(size, std::nullopt, provider_id); if (!result.has_value()) { diff --git a/score/crypto/src/api/src/crypto_context_impl.cpp b/score/crypto/src/api/src/crypto_context_impl.cpp index 1afc8e301..97e5ac048 100644 --- a/score/crypto/src/api/src/crypto_context_impl.cpp +++ b/score/crypto/src/api/src/crypto_context_impl.cpp @@ -64,8 +64,16 @@ score::Result> CryptoContextImpl::CreateHashContex { namespace proto = ::score::crypto::daemon::control_plane::protocol; + if (config.provider.has_value() && (config.provider->type != ResourceType::kProvider)) + { + return score::Result>{ + score::unexpect, + MakeError(CryptoErrorCode::kInvalidResourceType, "Hash context provider must have type kProvider")}; + } + // Send CTX_CREATE to the daemon to create a server-side hash context. - // The daemon will validate the algorithm and return the context_id and digest_size. + // The daemon validates the algorithm and returns the context_id. Digest + // size is queried through the created hash context. auto request_builder = proto::ControlRequestBuilder() .forDataNodeId(m_connection->GetConnectionNodeId()) .operation(score::crypto::daemon::mediator::operations::CreateContext()) @@ -82,6 +90,18 @@ score::Result> CryptoContextImpl::CreateHashContex request_builder = request_builder.with_no_param(); } + // Hash has no key binding or operation mode. Keep the generic CTX_CREATE + // layout stable and append the optional explicit provider at param[5]. + request_builder = request_builder.with_no_param().with_no_param(); + if (config.provider.has_value()) + { + request_builder = request_builder.with_in_val_uint16(config.provider->primary_provider); + } + else + { + request_builder = request_builder.with_no_param(); + } + auto control_req_result = request_builder.build(); if (!control_req_result.has_value()) { @@ -98,8 +118,10 @@ score::Result> CryptoContextImpl::CreateHashContex if (!validator.isValid()) { - return score::Result>{ - score::unexpect, MakeError(CryptoErrorCode::kContextCreationFailed, "CTX_CREATE daemon response invalid")}; + const auto error_code = + validator.getErrorCode().value_or(score::crypto::CryptoErrorCode::kContextCreationFailed); + return score::Result>{score::unexpect, + MakeError(error_code, validator.getError())}; } auto ctx_id_result = validator.getParameterAt(0, 0); @@ -144,8 +166,8 @@ score::Result CryptoContextImpl::ResolveResource(const Resourc if (!validator.isValid()) { - return score::Result{ - score::unexpect, MakeError(CryptoErrorCode::kInternalError, "RESOURCE_RESOLVE daemon response invalid")}; + const auto error_code = validator.getErrorCode().value_or(score::crypto::CryptoErrorCode::kInternalError); + return score::Result{score::unexpect, MakeError(error_code, validator.getError())}; } auto id_result = validator.getParameterAt(0, 0); @@ -197,6 +219,13 @@ score::Result> CryptoContextImpl::CreateMacContext( { namespace proto = ::score::crypto::daemon::control_plane::protocol; + if (config.provider.has_value() && (config.provider->type != ResourceType::kProvider)) + { + return score::Result>{ + score::unexpect, + MakeError(CryptoErrorCode::kInvalidResourceType, "MAC context provider must have type kProvider")}; + } + if (config.key.id == 0) { return score::Result>{ @@ -233,6 +262,15 @@ score::Result> CryptoContextImpl::CreateMacContext( // Serialize operation_mode (param[4]) so the daemon can route to C_Sign* or C_Verify*. request_builder = request_builder.with_in_val_uint8(static_cast(config.operation_mode)); + if (config.provider.has_value()) + { + request_builder = request_builder.with_in_val_uint16(config.provider->primary_provider); + } + else + { + request_builder = request_builder.with_no_param(); + } + auto control_req_result = request_builder.build(); if (!control_req_result.has_value()) { @@ -250,9 +288,9 @@ score::Result> CryptoContextImpl::CreateMacContext( if (!validator.isValid()) { - return score::Result>{ - score::unexpect, - MakeError(CryptoErrorCode::kContextCreationFailed, "CTX_CREATE MAC daemon response invalid")}; + const auto error_code = validator.getErrorCode().value_or(CryptoErrorCode::kContextCreationFailed); + return score::Result>{score::unexpect, + MakeError(error_code, validator.getError())}; } auto ctx_id_result = validator.getParameterAt(0, 0); @@ -274,6 +312,14 @@ score::Result> CryptoContextImpl::CreateK { namespace proto = ::score::crypto::daemon::control_plane::protocol; + if (config.provider.has_value() && (config.provider->type != ResourceType::kProvider)) + { + return score::Result>{ + score::unexpect, + MakeError(CryptoErrorCode::kInvalidResourceType, + "Key management context provider must have type kProvider")}; + } + // Send CTX_CREATE to the daemon to create a server-side key management context. auto request_builder = proto::ControlRequestBuilder() .forDataNodeId(m_connection->GetConnectionNodeId()) @@ -291,6 +337,18 @@ score::Result> CryptoContextImpl::CreateK request_builder = request_builder.with_no_param(); } + // Key-management context creation has neither a bound key nor an operation + // mode. Preserve their slots before appending the explicit provider. + request_builder = request_builder.with_no_param().with_no_param(); + if (config.provider.has_value()) + { + request_builder = request_builder.with_in_val_uint16(config.provider->primary_provider); + } + else + { + request_builder = request_builder.with_no_param(); + } + auto control_req_result = request_builder.build(); if (!control_req_result.has_value()) { @@ -306,9 +364,9 @@ score::Result> CryptoContextImpl::CreateK if (!validator.isValid()) { - return score::Result>{ - score::unexpect, - MakeError(CryptoErrorCode::kContextCreationFailed, "CTX_CREATE KEY_MGMT daemon response invalid")}; + const auto error_code = validator.getErrorCode().value_or(CryptoErrorCode::kContextCreationFailed); + return score::Result>{score::unexpect, + MakeError(error_code, validator.getError())}; } auto ctx_id_result = validator.getParameterAt(0, 0); diff --git a/score/crypto/src/api/src/crypto_context_impl_test.cpp b/score/crypto/src/api/src/crypto_context_impl_test.cpp new file mode 100644 index 000000000..b7437a6bd --- /dev/null +++ b/score/crypto/src/api/src/crypto_context_impl_test.cpp @@ -0,0 +1,91 @@ +/******************************************************************************** + * Copyright (c) 2026 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ + +#include "score/crypto/src/api/src/crypto_context_impl.hpp" + +#include "score/crypto/src/api/common/error_domain.hpp" +#include "score/crypto/src/api/common/types.hpp" +#include "score/crypto/src/api/config/key_management_context_config.hpp" +#include "score/crypto/src/api/config/mac_context_config.hpp" +#include "score/crypto/src/api/contexts/i_key_management_context.hpp" +#include "score/crypto/src/api/contexts/i_mac_context.hpp" +#include "score/crypto/src/api/control_plane/i_connection.hpp" +#include "score/crypto/src/daemon/control_plane/control_protocol.h" +#include "score/crypto/src/daemon/mediator/mediator_operations.hpp" + +#include + +#include +#include + +namespace score::crypto +{ +namespace +{ + +namespace protocol = daemon::control_plane::protocol; + +class ErrorConnection final : public api::control_plane::IConnection +{ + public: + explicit ErrorConnection(const CryptoErrorCode error_code) : m_error_code{error_code} {} + + Expected SendRequest( + const protocol::ControlRequest& /*request*/) override + { + protocol::ControlResponse response{}; + response.operation.operations.push_back(protocol::SingleOperationResponse{ + daemon::mediator::operations::CreateContext(), static_cast(m_error_code), {}}); + return response; + } + + protocol::DataNodeId GetConnectionNodeId() const override + { + return 1U; + } + + private: + CryptoErrorCode m_error_code; +}; + +TEST(CryptoContextImplTest, PreservesMacContextCreationErrorFromDaemon) +{ + auto connection = std::make_shared(CryptoErrorCode::kUnsupportedAlgorithm); + CryptoContextImpl context{connection, nullptr}; + + CryptoResourceId key{}; + key.id = 7U; + key.type = ResourceType::kKey; + key.primary_provider = 0U; + + MacContextConfig config; + config.SetAlgorithm("HMAC-SHA256").SetKey(key); + const auto result = context.CreateMacContext(config); + + ASSERT_FALSE(result.has_value()); + EXPECT_EQ(*result.error(), static_cast(CryptoErrorCode::kUnsupportedAlgorithm)); +} + +TEST(CryptoContextImplTest, PreservesKeyManagementContextCreationErrorFromDaemon) +{ + auto connection = std::make_shared(CryptoErrorCode::kProviderNotAvailable); + CryptoContextImpl context{connection, nullptr}; + + const auto result = context.CreateKeyManagementContext(KeyManagementContextConfig{}); + + ASSERT_FALSE(result.has_value()); + EXPECT_EQ(*result.error(), static_cast(CryptoErrorCode::kProviderNotAvailable)); +} + +} // namespace +} // namespace score::crypto diff --git a/score/crypto/src/api/src/provider_type_converter.hpp b/score/crypto/src/api/src/provider_type_converter.hpp index 5f6372d76..c6c9e08f7 100644 --- a/score/crypto/src/api/src/provider_type_converter.hpp +++ b/score/crypto/src/api/src/provider_type_converter.hpp @@ -30,7 +30,7 @@ namespace ProviderTypeConverter /// @brief Encode a client-side ProviderType preference as its IPC wire value. /// /// The wire protocol carries the raw uint8_t representation of the ProviderType -/// enumerator. The daemon decodes this with its own FromWireProviderType() +/// enumerator. The daemon decodes this with DecodeProviderTypePreference() /// function and maps it to its internal CryptoProviderType classification. /// /// This function must NOT depend on daemon-internal headers — the client library @@ -47,6 +47,12 @@ inline constexpr std::uint8_t ToWireValue(ProviderType api_type) noexcept return static_cast(api_type); } +static_assert(ToWireValue(ProviderType::kDefault) == 0U, "ProviderType wire value changed"); +static_assert(ToWireValue(ProviderType::kHardware) == 1U, "ProviderType wire value changed"); +static_assert(ToWireValue(ProviderType::kSoftware) == 2U, "ProviderType wire value changed"); +static_assert(ToWireValue(ProviderType::kHardwarePreferred) == 3U, "ProviderType wire value changed"); +static_assert(ToWireValue(ProviderType::kSoftwarePreferred) == 4U, "ProviderType wire value changed"); + } // namespace ProviderTypeConverter } // namespace crypto diff --git a/score/crypto/src/daemon/common/algorithm_info.hpp b/score/crypto/src/daemon/common/algorithm_info.hpp index 1c8c62bcf..4c3508ac8 100644 --- a/score/crypto/src/daemon/common/algorithm_info.hpp +++ b/score/crypto/src/daemon/common/algorithm_info.hpp @@ -25,35 +25,71 @@ namespace score::crypto::daemon::common // Hash algorithm properties (provider-independent) // --------------------------------------------------------------------------- +enum class HashAlgorithmStatus +{ + kRecommended, + kLegacy, +}; + +enum class HashAlgorithm +{ + kSha256, + kSha384, + kSha512, + kSha224, + kSha1, + kMd5, +}; + struct HashAlgorithmInfo { + HashAlgorithm algorithm; std::string_view name; - std::size_t digest_size; ///< Output size in bytes + std::size_t digest_size; ///< Output size in bytes + HashAlgorithmStatus status; ///< Recommendation for new integrations }; inline constexpr HashAlgorithmInfo kHashAlgorithms[] = { - {"SHA256", 32U}, - {"SHA384", 48U}, - {"SHA512", 64U}, - {"SHA224", 28U}, - {"SHA1", 20U}, - {"MD5", 16U}, + {HashAlgorithm::kSha256, "SHA256", 32U, HashAlgorithmStatus::kRecommended}, + {HashAlgorithm::kSha384, "SHA384", 48U, HashAlgorithmStatus::kRecommended}, + {HashAlgorithm::kSha512, "SHA512", 64U, HashAlgorithmStatus::kRecommended}, + {HashAlgorithm::kSha224, "SHA224", 28U, HashAlgorithmStatus::kLegacy}, + {HashAlgorithm::kSha1, "SHA1", 20U, HashAlgorithmStatus::kLegacy}, + {HashAlgorithm::kMd5, "MD5", 16U, HashAlgorithmStatus::kLegacy}, }; -/// @brief Look up digest size by algorithm name. -/// @return digest size in bytes, or std::nullopt if unknown. -[[nodiscard]] inline constexpr std::optional LookupDigestSize(std::string_view algorithm) noexcept +/// @brief Look up provider-independent hash algorithm metadata. +/// @return algorithm metadata, or std::nullopt if unknown. +[[nodiscard]] inline constexpr std::optional LookupHashAlgorithmInfo( + std::string_view algorithm) noexcept { for (const auto& entry : kHashAlgorithms) { if (entry.name == algorithm) { - return entry.digest_size; + return entry; } } return std::nullopt; } +/// @brief Look up digest size by algorithm name. +/// @return digest size in bytes, or std::nullopt if unknown. +[[nodiscard]] inline constexpr std::optional LookupDigestSize(std::string_view algorithm) noexcept +{ + const auto info = LookupHashAlgorithmInfo(algorithm); + return info.has_value() ? std::optional{info->digest_size} : std::nullopt; +} + +/// @brief Return whether an algorithm is recommended for new integrations. +/// +/// Legacy algorithms remain available for compatibility. +[[nodiscard]] inline constexpr bool IsRecommendedHashAlgorithm(std::string_view algorithm) noexcept +{ + const auto info = LookupHashAlgorithmInfo(algorithm); + return info.has_value() && (info->status == HashAlgorithmStatus::kRecommended); +} + // --------------------------------------------------------------------------- // MAC algorithm properties (provider-independent) // --------------------------------------------------------------------------- diff --git a/score/crypto/src/daemon/control_plane/control_protocol.h b/score/crypto/src/daemon/control_plane/control_protocol.h index 7821c06c1..faf427013 100644 --- a/score/crypto/src/daemon/control_plane/control_protocol.h +++ b/score/crypto/src/daemon/control_plane/control_protocol.h @@ -20,6 +20,7 @@ #include #include +#include #include #include #include @@ -384,6 +385,12 @@ class OperationResponseBuilder return return_error(score::crypto::daemon::common::ToCryptoErrorCode(error)); }; + /// @brief Preserve an API-domain error returned by a provider factory. + OperationResponseBuilder& return_error(const score::result::Error& error) + { + return return_error(static_cast(*error)); + }; + OperationResponseBuilder& return_value_bool(bool val) { if (validateOperationExists()) @@ -588,6 +595,7 @@ class ControlResponseValidator auto errorCode = static_cast(op.result); m_isValid = false; + m_errorCode = errorCode; m_errorMsg = "Operation at index " + std::to_string(m_currentOpIndex) + " failed with error code " + std::string(score::crypto::kCryptoErrorDomain.MessageFor( static_cast(errorCode))); @@ -677,12 +685,22 @@ class ControlResponseValidator return m_errorMsg; } + /// @brief Return the daemon-provided operation error when validation failed at expectSuccess(). + /// + /// Structural validation failures and transport failures do not have an + /// operation-level CryptoErrorCode and therefore return std::nullopt. + [[nodiscard]] std::optional getErrorCode() const noexcept + { + return m_errorCode; + } + private: std::reference_wrapper m_response; size_t m_currentOpIndex; bool m_isValid = true; std::string m_errorMsg; bool m_logErrors = false; + std::optional m_errorCode; void logError() { diff --git a/score/crypto/src/daemon/mediator/BUILD b/score/crypto/src/daemon/mediator/BUILD index 6c5734ad2..7abab569c 100644 --- a/score/crypto/src/daemon/mediator/BUILD +++ b/score/crypto/src/daemon/mediator/BUILD @@ -11,7 +11,7 @@ # SPDX-License-Identifier: Apache-2.0 # ******************************************************************************* -load("@rules_cc//cc:defs.bzl", "cc_library") +load("@rules_cc//cc:defs.bzl", "cc_library", "cc_test") cc_library( name = "mediator_operations", @@ -22,6 +22,25 @@ cc_library( deps = ["//score/crypto/src/daemon/common"], ) +cc_test( + name = "mediator_unit_test", + srcs = ["src/mediator_impl_test.cpp"], + deps = [ + ":mediator", + ":mediator_operations", + "//score/crypto/src/api/common:crypto_common", + "//score/crypto/src/daemon/common", + "//score/crypto/src/daemon/config", + "//score/crypto/src/daemon/control_plane:request_handler_hdr", + "//score/crypto/src/daemon/data_manager", + "//score/crypto/src/daemon/provider:provider_headers", + "//score/crypto/src/daemon/provider:provider_manager", + "//score/crypto/src/daemon/provider/handler:crypto_handler_factory_headers", + "//score/crypto/src/daemon/provider/handler:handler_headers", + "@googletest//:gtest_main", + ], +) + cc_library( name = "mediator", srcs = [ diff --git a/score/crypto/src/daemon/mediator/mediator_operations.hpp b/score/crypto/src/daemon/mediator/mediator_operations.hpp index abd1cac30..28cb01766 100644 --- a/score/crypto/src/daemon/mediator/mediator_operations.hpp +++ b/score/crypto/src/daemon/mediator/mediator_operations.hpp @@ -40,7 +40,11 @@ inline constexpr std::uint64_t SHM_WIRE_TRANSPORT_TYPE_ABSENT = 0xFFU; inline constexpr std::uint64_t SHM_WIRE_PROVIDER_TYPE_ABSENT = 0xFFU; -inline constexpr std::uint64_t SHM_WIRE_PROVIDER_ID_UNBOUND = 0U; +/// @brief Wire sentinel for a resource without a provider binding. +/// +/// ProviderManager reserves kInvalidProviderId, so this value cannot collide +/// with a real provider (including provider zero). +inline constexpr std::uint64_t SHM_WIRE_PROVIDER_ID_UNBOUND = static_cast(common::kInvalidProviderId); using OperationAction = common::OperationAction; @@ -54,11 +58,20 @@ using OperationAction = common::OperationAction; // param[1]: string — algorithm name (e.g. "SHA256", "SHA512") // param[2]: optional uint8 — provider type preference (defaults to DEFAULT) // param[3]: optional uint64_t — node_id of key resource (CryptoResourceId.id) +// param[4]: optional uint8 — operation mode (used by MAC/signature contexts) +// param[5]: optional uint16_t — explicit numeric provider ID; overrides param[2] // Response: status_code (SUCCESS/error) // uint64_t — daemon-assigned context_id (DataNodeId) // Effect: Creates cryptographic context, initializes handler with specified algorithm inline constexpr OperationAction CTX_CREATE = 1; +inline constexpr std::size_t CTX_PARAM_HANDLER_TYPE = 0U; +inline constexpr std::size_t CTX_PARAM_ALGORITHM = 1U; +inline constexpr std::size_t CTX_PARAM_PROVIDER_TYPE = 2U; +inline constexpr std::size_t CTX_PARAM_KEY_NODE_ID = 3U; +inline constexpr std::size_t CTX_PARAM_OPERATION_MODE = 4U; +inline constexpr std::size_t CTX_PARAM_EXPLICIT_PROVIDER_ID = 5U; + // CTX_CLOSE // Request: data_node_id = context_id (the context to close), // no operation parameters diff --git a/score/crypto/src/daemon/mediator/src/mediator_impl.cpp b/score/crypto/src/daemon/mediator/src/mediator_impl.cpp index b4cd5d8ab..6d2fe18c9 100644 --- a/score/crypto/src/daemon/mediator/src/mediator_impl.cpp +++ b/score/crypto/src/daemon/mediator/src/mediator_impl.cpp @@ -15,13 +15,16 @@ #include #include +#include #include +#include #include #include #include #include #include "score/crypto/src/api/common/error_domain.hpp" +#include "score/crypto/src/api/common/types.hpp" #include "score/crypto/src/daemon/common/actors.hpp" #include "score/crypto/src/daemon/common/operation_names.hpp" #include "score/crypto/src/daemon/common/types.hpp" @@ -48,39 +51,77 @@ using ControlResponse = control_plane::ControlResponse; namespace score::crypto::daemon::mediator { -/// @brief Decode a ProviderType wire value (from the IPC protocol) into the -/// daemon-internal CryptoProviderType capability classification. -/// -/// The wire encoding is the uint8_t value of the client-side mw::crypto::ProviderType -/// enumerator (0=kDefault, 1=kHardware, 2=kSoftware, 3=kHardwarePreferred, 4=kSoftwarePreferred). -/// kHardwarePreferred / kSoftwarePreferred are resolved to their primary type; the -/// daemon's ProviderManager::GetProvider() handles fallback to SOFTWARE/HARDWARE if -/// the preferred type is not registered. -static common::CryptoProviderType FromWireProviderType(std::uint8_t wire_value) noexcept +struct ProviderTypePreference +{ + common::CryptoProviderType primary{common::CryptoProviderType::DEFAULT}; + std::optional fallback{}; +}; + +/// @brief Decode the stable public ProviderType wire values while retaining +/// preferred-provider fallback semantics. +static std::optional DecodeProviderTypePreference(const std::uint8_t wire_value) noexcept { - // Wire values match mw::crypto::ProviderType enumerator positions: - // 0=kDefault, 1=kHardware, 2=kSoftware, 3=kHardwarePreferred, 4=kSoftwarePreferred switch (wire_value) { + case 0: + return ProviderTypePreference{common::CryptoProviderType::DEFAULT, std::nullopt}; case 1: - return common::CryptoProviderType::HARDWARE; + return ProviderTypePreference{common::CryptoProviderType::HARDWARE, std::nullopt}; case 2: - return common::CryptoProviderType::SOFTWARE; + return ProviderTypePreference{common::CryptoProviderType::SOFTWARE, std::nullopt}; case 3: - return common::CryptoProviderType::HARDWARE; + return ProviderTypePreference{common::CryptoProviderType::HARDWARE, common::CryptoProviderType::SOFTWARE}; case 4: - return common::CryptoProviderType::SOFTWARE; + return ProviderTypePreference{common::CryptoProviderType::SOFTWARE, common::CryptoProviderType::HARDWARE}; default: - return common::CryptoProviderType::DEFAULT; + return std::nullopt; } } -MediatorImpl::MediatorImpl(MediatorDependencies deps) : IMediator(std::move(deps)) +static bool IsContextParameterPresent(const control_plane::SingleOperationRequest& operation, + const std::size_t parameter_index) noexcept { - if (m_km_service) + return (operation.parameters.size() > parameter_index) && + !std::holds_alternative(operation.parameters[parameter_index]); +} + +/// @brief Validate the handler-specific portion of the generic CTX_CREATE layout. +/// +/// Provider implementations receive the raw parameter vector, but the mediator +/// owns the stable wire schema and rejects malformed requests before selecting a +/// provider or creating a data node. +static bool IsContextCreationSchemaValid(const control_plane::SingleOperationRequest& operation, + const std::string_view context_type) noexcept +{ + const bool has_key = IsContextParameterPresent(operation, operations::CTX_PARAM_KEY_NODE_ID); + const bool has_operation_mode = IsContextParameterPresent(operation, operations::CTX_PARAM_OPERATION_MODE); + + if ((context_type == "HASH") || (context_type == "KEY_MANAGEMENT")) + { + return !has_key && !has_operation_mode; + } + + if (context_type == "MAC") { - RegisterResourceResolvers(); + if (!has_key || !has_operation_mode) + { + return false; + } + + const auto key_node = operation.getParameter(operations::CTX_PARAM_KEY_NODE_ID); + const auto operation_mode = operation.getParameter(operations::CTX_PARAM_OPERATION_MODE); + return key_node.has_value() && (key_node.value() != 0U) && operation_mode.has_value() && + (operation_mode.value() <= static_cast(score::crypto::OperationMode::kVerify)); } + + // Unknown context types are rejected by provider factories. Their parameter + // schemas remain unrestricted here so future context types stay extensible. + return true; +} + +MediatorImpl::MediatorImpl(MediatorDependencies deps) : IMediator(std::move(deps)) +{ + RegisterResourceResolvers(); } control_plane::ControlResponse MediatorImpl::processRequest(control_plane::ControlRequest& request) @@ -252,98 +293,172 @@ bool MediatorImpl::HandleContextCreationOperation(const score::crypto::daemon::c const control_plane::SingleOperationRequest& operation, control_plane::protocol::OperationResponseBuilder& responseBuilder) { - if (operation.parameters.size() < 2) + if ((operation.parameters.size() < 2U) || + (operation.parameters.size() > (operations::CTX_PARAM_EXPLICIT_PROVIDER_ID + 1U))) { - score::mw::log::LogError() << "[SCORE_API_MED] ERROR - Not enough parameters for request"; - responseBuilder.operation(operation.operationId).return_error(score::crypto::CryptoErrorCode::kInternalError); + score::mw::log::LogError() << "[SCORE_API_MED] ERROR - Invalid CTX_CREATE parameter count"; + responseBuilder.operation(operation.operationId).return_error(score::crypto::CryptoErrorCode::kInvalidArgument); return false; } - auto context_type_res = operation.getParameter(0); + auto context_type_res = operation.getParameter(operations::CTX_PARAM_HANDLER_TYPE); if (!context_type_res.has_value()) { score::mw::log::LogError() << "[SCORE_API_MED] ERROR - Wrong parameter type for context_type"; - responseBuilder.operation(operation.operationId).return_error(score::crypto::CryptoErrorCode::kInternalError); + responseBuilder.operation(operation.operationId).return_error(score::crypto::CryptoErrorCode::kInvalidArgument); return false; } auto context_type = context_type_res.value(); - auto algorithm_res = operation.getParameter(1); + auto algorithm_res = operation.getParameter(operations::CTX_PARAM_ALGORITHM); if (!algorithm_res.has_value()) { score::mw::log::LogError() << "[SCORE_API_MED] ERROR - Wrong parameter type for algorithm"; - responseBuilder.operation(operation.operationId).return_error(score::crypto::CryptoErrorCode::kInternalError); + responseBuilder.operation(operation.operationId).return_error(score::crypto::CryptoErrorCode::kInvalidArgument); return false; } auto algorithm = algorithm_res.value(); - // Read optional provider type parameter (param[2]) - // Default to DEFAULT provider type if not specified or invalid - common::CryptoProviderType requested_provider_type = common::CryptoProviderType::DEFAULT; - if (operation.parameters.size() >= 3) + if (!IsContextCreationSchemaValid(operation, context_type)) + { + score::mw::log::LogError() << "[SCORE_API_MED] ERROR - Invalid CTX_CREATE schema for context type: " + << context_type; + responseBuilder.operation(operation.operationId).return_error(score::crypto::CryptoErrorCode::kInvalidArgument); + return false; + } + + ProviderTypePreference provider_preference{}; + if (operation.parameters.size() > operations::CTX_PARAM_PROVIDER_TYPE && + !std::holds_alternative(operation.parameters[operations::CTX_PARAM_PROVIDER_TYPE])) { - auto provider_type_res = operation.getParameter(2); - if (provider_type_res.has_value()) + const auto provider_type_res = operation.getParameter(operations::CTX_PARAM_PROVIDER_TYPE); + if (!provider_type_res.has_value()) { - requested_provider_type = FromWireProviderType(provider_type_res.value()); + score::mw::log::LogError() << "[SCORE_API_MED] ERROR - Wrong parameter type for provider preference"; + responseBuilder.operation(operation.operationId) + .return_error(score::crypto::CryptoErrorCode::kInvalidArgument); + return false; } + + const auto decoded_preference = DecodeProviderTypePreference(provider_type_res.value()); + if (!decoded_preference.has_value()) + { + score::mw::log::LogError() << "[SCORE_API_MED] ERROR - Unknown provider preference value: " + << static_cast(provider_type_res.value()); + responseBuilder.operation(operation.operationId) + .return_error(score::crypto::CryptoErrorCode::kInvalidArgument); + return false; + } + provider_preference = decoded_preference.value(); } // Read optional key_node_id parameter (param[3]) — for binding a key at context creation std::uint64_t key_node_id{0U}; bool has_key_binding = false; - if (operation.parameters.size() >= 4) + if (operation.parameters.size() > operations::CTX_PARAM_KEY_NODE_ID && + !std::holds_alternative(operation.parameters[operations::CTX_PARAM_KEY_NODE_ID])) { - auto key_node_res = operation.getParameter(3); - if (key_node_res.has_value()) + const auto key_node_res = operation.getParameter(operations::CTX_PARAM_KEY_NODE_ID); + if (!key_node_res.has_value()) { - key_node_id = key_node_res.value(); - has_key_binding = true; + score::mw::log::LogError() << "[SCORE_API_MED] ERROR - Wrong parameter type for key resource"; + responseBuilder.operation(operation.operationId) + .return_error(score::crypto::CryptoErrorCode::kInvalidArgument); + return false; } + key_node_id = key_node_res.value(); + has_key_binding = true; } - // --- Resolve target provider (considers key/slot affinity when available) --- - std::shared_ptr provider; - if (m_km_service && has_key_binding) + std::optional explicit_provider_id{}; + if (operation.parameters.size() > operations::CTX_PARAM_EXPLICIT_PROVIDER_ID && + !std::holds_alternative(operation.parameters[operations::CTX_PARAM_EXPLICIT_PROVIDER_ID])) { - auto resolved_id_res = m_km_service->ResolveTargetProvider( - request.client_id, requested_provider_type, std::optional{key_node_id}); - if (!resolved_id_res.has_value()) + const auto provider_id_res = operation.getParameter(operations::CTX_PARAM_EXPLICIT_PROVIDER_ID); + if (!provider_id_res.has_value()) { - score::mw::log::LogError() << "[SCORE_API_MED] ERROR - Provider resolution failed for keyed context" - << " (key_node_id=" << key_node_id << ")"; + score::mw::log::LogError() << "[SCORE_API_MED] ERROR - Wrong parameter type for explicit provider"; responseBuilder.operation(operation.operationId) .return_error(score::crypto::CryptoErrorCode::kInvalidArgument); return false; } - provider = m_provider_manager->GetProvider(resolved_id_res.value()); + explicit_provider_id = provider_id_res.value(); + } + + const auto resolve_provider_by_type = [&](const common::CryptoProviderType provider_type) { + if (m_km_service && has_key_binding) + { + const auto resolved_id_res = m_km_service->ResolveTargetProvider( + request.client_id, provider_type, std::optional{key_node_id}); + return resolved_id_res.has_value() ? m_provider_manager->GetProvider(resolved_id_res.value()) + : std::shared_ptr{}; + } + return m_provider_manager->GetProvider(provider_type); + }; + + // --- Resolve target provider. Explicit selection takes precedence, followed + // by key affinity and finally provider-type preference. --- + std::shared_ptr provider; + bool used_fallback = false; + if (explicit_provider_id.has_value()) + { + provider = m_provider_manager->GetProvider(explicit_provider_id.value()); } else { - provider = m_provider_manager->GetProvider(requested_provider_type); + provider = resolve_provider_by_type(provider_preference.primary); + if (!provider && provider_preference.fallback.has_value()) + { + provider = resolve_provider_by_type(provider_preference.fallback.value()); + used_fallback = static_cast(provider); + } } if (!provider) { - score::mw::log::LogError() << "[SCORE_API_MED] ERROR - No providers available for type: " - << static_cast(requested_provider_type); - responseBuilder.operation(operation.operationId).return_error(score::crypto::CryptoErrorCode::kInternalError); + score::mw::log::LogError() << "[SCORE_API_MED] ERROR - No providers available for requested preference"; + responseBuilder.operation(operation.operationId) + .return_error(score::crypto::CryptoErrorCode::kProviderNotAvailable); return false; } - auto crypto_ops = provider->GetCryptoHandlerFactory(); - if (crypto_ops == nullptr) + const auto create_handler = + [&](const std::shared_ptr& candidate) -> score::Result { + const auto crypto_ops = candidate->GetCryptoHandlerFactory(); + if (!crypto_ops) + { + return score::Result{ + score::unexpect, + MakeError(score::crypto::CryptoErrorCode::kUnsupportedOperation, + "Crypto operations not available for provider")}; + } + return crypto_ops->CreateHandler(std::string(context_type), std::string(algorithm)); + }; + + auto create_result = create_handler(provider); + const auto may_fallback_after_creation_error = [&create_result]() { + if (create_result.has_value()) + { + return false; + } + const auto error_code = *create_result.error(); + return (error_code == static_cast(CryptoErrorCode::kUnsupportedOperation)) || + (error_code == static_cast(CryptoErrorCode::kUnsupportedAlgorithm)); + }; + if (may_fallback_after_creation_error() && !explicit_provider_id.has_value() && !used_fallback && + provider_preference.fallback.has_value()) { - score::mw::log::LogError() << "[SCORE_API_MED] ERROR - Crypto operations not available"; - responseBuilder.operation(operation.operationId) - .return_error(score::crypto::CryptoErrorCode::kUnsupportedOperation); - return false; + const auto fallback_provider = resolve_provider_by_type(provider_preference.fallback.value()); + if (fallback_provider && (fallback_provider->GetProviderId() != provider->GetProviderId())) + { + provider = fallback_provider; + create_result = create_handler(fallback_provider); + used_fallback = create_result.has_value(); + } } - - auto create_result = crypto_ops->CreateHandler(std::string(context_type), std::string(algorithm)); if (!create_result.has_value()) { score::mw::log::LogError() << "[SCORE_API_MED] ERROR - Handler or algorithm not supported:" << context_type << "/" << algorithm; - responseBuilder.operation(operation.operationId).return_error(score::crypto::CryptoErrorCode::kInternalError); + responseBuilder.operation(operation.operationId).return_error(create_result.error()); return false; } @@ -409,12 +524,23 @@ bool MediatorImpl::HandleContextCreationOperation(const score::crypto::daemon::c score::mw::log::LogError() << "[SCORE_API_MED] ERROR - Handler initialization failed for context with error: " << static_cast(init_result.error()); m_data_manager->deleteNode(client_id, context_node_id); - responseBuilder.operation(operation.operationId).return_error(score::crypto::CryptoErrorCode::kInternalError); + responseBuilder.operation(operation.operationId).return_error(init_result.error()); return false; } - const std::string_view provider_selection = - has_key_binding ? " (key-affinity resolved)" : " (type-based selection)"; + std::string_view provider_selection{" (type-based selection)"}; + if (explicit_provider_id.has_value()) + { + provider_selection = " (explicit selection)"; + } + else if (used_fallback) + { + provider_selection = " (preferred-provider fallback)"; + } + else if (has_key_binding) + { + provider_selection = " (key-affinity resolved)"; + } score::mw::log::LogVerbose() << "[SCORE_API_MED] CTX_CREATE [" << context_type << "/" << algorithm << "] selected provider: name='" << provider->GetProviderName() << "' id=" << provider->GetProviderId() << provider_selection @@ -526,6 +652,15 @@ bool MediatorImpl::HandleShmCreateObject(const control_plane::ControlRequest& re const auto type_hint = operation.getParameter(1); const auto id_hint = operation.getParameter(2); + if (id_hint.has_value() && + (id_hint.value() > static_cast(std::numeric_limits::max()))) + { + score::mw::log::LogError() << "[SCORE_API_MED] [SHM_SETUP_FAILED] Provider ID hint is out of range"; + responseBuilder.operation(operation.operationId) + .return_error(score::crypto::CryptoErrorCode::kInvalidArgument); + return false; + } + std::shared_ptr provider{}; if (id_hint.has_value() && id_hint.value() != operations::SHM_WIRE_PROVIDER_ID_UNBOUND) { @@ -533,8 +668,28 @@ bool MediatorImpl::HandleShmCreateObject(const control_plane::ControlRequest& re } else if (type_hint.has_value() && type_hint.value() != operations::SHM_WIRE_PROVIDER_TYPE_ABSENT) { - provider = - m_provider_manager->GetProvider(FromWireProviderType(static_cast(type_hint.value()))); + if (type_hint.value() > std::numeric_limits::max()) + { + score::mw::log::LogError() << "[SCORE_API_MED] [SHM_SETUP_FAILED] Invalid provider type hint"; + responseBuilder.operation(operation.operationId) + .return_error(score::crypto::CryptoErrorCode::kInvalidArgument); + return false; + } + + const auto preference = DecodeProviderTypePreference(static_cast(type_hint.value())); + if (!preference.has_value()) + { + score::mw::log::LogError() << "[SCORE_API_MED] [SHM_SETUP_FAILED] Unknown provider type hint"; + responseBuilder.operation(operation.operationId) + .return_error(score::crypto::CryptoErrorCode::kInvalidArgument); + return false; + } + + provider = m_provider_manager->GetProvider(preference->primary); + if (!provider && preference->fallback.has_value()) + { + provider = m_provider_manager->GetProvider(preference->fallback.value()); + } } else { @@ -649,20 +804,46 @@ void MediatorImpl::RegisterResourceResolvers() return false; } - // TODO: How to get the primary provider ? - // For now just return 0 + // The slot API does not yet expose its owning provider. Return the + // reserved unbound sentinel rather than provider zero, which is valid. responseBuilder.operation(op_id) .return_value_uint64(static_cast(node_id_result.value())) .return_value_uint8(static_cast(RT::kKeySlot)) .return_value_bool(true) // KeySlots are always persistent - .return_value_uint16(0) + .return_value_uint16(common::kInvalidProviderId) .return_success(); return true; }; - // Additional resource types (kProvider, kCertSlot, kTrustAnchor, …) are - // registered here as those subsystems are implemented. Each entry is - // self-contained; no existing resolvers are modified. + // --- kProvider ---------------------------------------------------------- + // Provider resources are process-wide and do not require a DataNode. The + // numeric provider ID is returned both as the opaque resource ID and as + // primary_provider so BaseContextConfig::SetProvider() can route CTX_CREATE. + m_resource_resolvers[static_cast(RT::kProvider)] = + [this](uint64_t /*client_id*/, + uint64_t /*session_id*/, + const std::string& resource_name, + const common::OperationIdentifier& op_id, + control_plane::protocol::OperationResponseBuilder& responseBuilder) -> bool { + const auto provider = m_provider_manager->GetProvider(resource_name); + if (!provider) + { + responseBuilder.operation(op_id).return_error(score::crypto::CryptoErrorCode::kProviderNotAvailable); + return false; + } + + const auto provider_id = provider->GetProviderId(); + responseBuilder.operation(op_id) + .return_value_uint64(static_cast(provider_id)) + .return_value_uint8(static_cast(RT::kProvider)) + .return_value_bool(true) + .return_value_uint16(provider_id) + .return_success(); + return true; + }; + + // Additional resource types (kCertSlot, kTrustAnchor, …) are registered + // here as those subsystems are implemented. } bool MediatorImpl::HandleResourceResolutionOperation(uint64_t client_id, @@ -670,7 +851,7 @@ bool MediatorImpl::HandleResourceResolutionOperation(uint64_t client_id, const control_plane::SingleOperationRequest& operation, control_plane::protocol::OperationResponseBuilder& responseBuilder) { - if (operation.parameters.empty()) + if (operation.parameters.empty() || (operation.parameters.size() > 2U)) { responseBuilder.operation(operation.operationId).return_error(score::crypto::CryptoErrorCode::kInvalidArgument); return false; @@ -685,15 +866,20 @@ bool MediatorImpl::HandleResourceResolutionOperation(uint64_t client_id, } const std::string resource_name{*name_param}; - // param[1]: ResourceType cast to uint64. Defaults to kKeySlot for + // param[1]: ResourceType encoded as uint8. Defaults to kKeySlot for // backward compatibility when the client omits the type parameter. auto resource_type = score::crypto::ResourceType::kKeySlot; if (operation.parameters.size() > 1U) { - if (const auto* type_param = std::get_if(&operation.parameters[1])) + const auto type_result = operation.getParameter(1U); + if (!type_result.has_value() || + (type_result.value() > static_cast(score::crypto::ResourceType::kDataObject))) { - resource_type = static_cast(static_cast(*type_param)); + responseBuilder.operation(operation.operationId) + .return_error(score::crypto::CryptoErrorCode::kInvalidArgument); + return false; } + resource_type = static_cast(type_result.value()); } const auto key = static_cast(resource_type); diff --git a/score/crypto/src/daemon/mediator/src/mediator_impl_test.cpp b/score/crypto/src/daemon/mediator/src/mediator_impl_test.cpp new file mode 100644 index 000000000..a98849d2e --- /dev/null +++ b/score/crypto/src/daemon/mediator/src/mediator_impl_test.cpp @@ -0,0 +1,252 @@ +/******************************************************************************** + * Copyright (c) 2026 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ + +#include "score/crypto/src/daemon/mediator/src/mediator_impl.hpp" + +#include "score/crypto/src/api/common/error_domain.hpp" +#include "score/crypto/src/api/common/types.hpp" +#include "score/crypto/src/daemon/common/types.hpp" +#include "score/crypto/src/daemon/config/inc/config.hpp" +#include "score/crypto/src/daemon/control_plane/control_protocol.h" +#include "score/crypto/src/daemon/data_manager/data_manager.hpp" +#include "score/crypto/src/daemon/data_manager/data_node.hpp" +#include "score/crypto/src/daemon/mediator/mediator_operations.hpp" +#include "score/crypto/src/daemon/provider/handler/i_crypto_handler_factory.hpp" +#include "score/crypto/src/daemon/provider/handler/i_handler.hpp" +#include "score/crypto/src/daemon/provider/i_provider.hpp" +#include "score/crypto/src/daemon/provider/provider_manager.hpp" + +#include + +#include +#include +#include +#include +#include + +namespace score::crypto::daemon::mediator +{ +namespace +{ + +namespace protocol = control_plane::protocol; + +class StubHandler final : public provider::handler::Handler +{ + public: + Expected InitializeContext( + const provider::handler::InitializationParams& init_params) override + { + provider_id = init_params.provider_id; + return std::monostate{}; + } + + Expected Execute( + const common::OperationIdentifier& /*operation_id*/, + common::RequestParameters& /*request*/) override + { + return common::ResponseParameters{}; + } + + Expected Reset() override + { + return std::monostate{}; + } + + common::ProviderId provider_id{common::kInvalidProviderId}; +}; + +class StubHandlerFactory final : public provider::handler::ICryptoHandlerFactory +{ + public: + explicit StubHandlerFactory(const bool supports_algorithm) : m_supports_algorithm{supports_algorithm} {} + + score::Result CreateHandler(const common::HandlerId& handler_id, + const common::AlgorithmId& algorithm) override + { + ++calls; + if (!m_supports_algorithm || (handler_id != "HASH") || (algorithm != "SHA256")) + { + return score::Result{ + score::unexpect, + MakeError(CryptoErrorCode::kUnsupportedAlgorithm, "Algorithm intentionally unsupported by stub")}; + } + handler = std::make_shared(); + return std::static_pointer_cast(handler); + } + + std::size_t calls{0U}; + std::shared_ptr handler{}; + + private: + bool m_supports_algorithm; +}; + +class StubProvider final : public provider::IProvider +{ + public: + explicit StubProvider(std::shared_ptr factory) : m_factory{std::move(factory)} {} + + bool Initialize(const provider::ProviderInitContext& ctx) override + { + m_id = ctx.numeric_id; + m_name = ctx.name; + m_initialized = true; + return true; + } + + void Shutdown() override + { + m_initialized = false; + } + + [[nodiscard]] bool IsInitialized() const override + { + return m_initialized; + } + + common::ProviderId GetProviderId() const override + { + return m_id; + } + + const common::ProviderName& GetProviderName() const override + { + return m_name; + } + + std::shared_ptr GetCryptoHandlerFactory() override + { + return m_factory; + } + + private: + common::ProviderId m_id{common::kInvalidProviderId}; + common::ProviderName m_name{}; + bool m_initialized{false}; + std::shared_ptr m_factory; +}; + +control_plane::ControlResponse ProcessContextCreation(common::RequestParameters parameters) +{ + MediatorImpl mediator{MediatorDependencies{nullptr, nullptr, nullptr, nullptr}}; + control_plane::ControlRequest request{}; + request.request_id = 1U; + request.client_id = 1U; + request.data_node_id = 1U; + request.operation.operations.push_back( + control_plane::SingleOperationRequest{operations::CreateContext(), std::move(parameters)}); + return mediator.processRequest(request); +} + +void ExpectInvalidContextCreation(common::RequestParameters parameters) +{ + const auto response = ProcessContextCreation(std::move(parameters)); + ASSERT_EQ(response.operation.operations.size(), 1U); + EXPECT_EQ(response.operation.operations.front().result, + static_cast(CryptoErrorCode::kInvalidArgument)); +} + +TEST(MediatorContextSchemaTest, RejectsKeyAndOperationModeForHash) +{ + ExpectInvalidContextCreation( + {std::string_view{"HASH"}, std::string_view{"SHA256"}, common::NoParam{}, std::uint64_t{7U}, std::uint8_t{0U}}); +} + +TEST(MediatorContextSchemaTest, RejectsKeyAndOperationModeForKeyManagement) +{ + ExpectInvalidContextCreation({std::string_view{"KEY_MANAGEMENT"}, + std::string_view{""}, + common::NoParam{}, + std::uint64_t{7U}, + std::uint8_t{0U}}); +} + +TEST(MediatorContextSchemaTest, RejectsMacWithoutKey) +{ + ExpectInvalidContextCreation({std::string_view{"MAC"}, + std::string_view{"HMAC-SHA256"}, + common::NoParam{}, + common::NoParam{}, + std::uint8_t{0U}}); +} + +TEST(MediatorContextSchemaTest, RejectsMacWithoutOperationMode) +{ + ExpectInvalidContextCreation( + {std::string_view{"MAC"}, std::string_view{"HMAC-SHA256"}, common::NoParam{}, std::uint64_t{7U}}); +} + +TEST(MediatorContextSchemaTest, RejectsMacWithUnknownOperationMode) +{ + ExpectInvalidContextCreation({std::string_view{"MAC"}, + std::string_view{"HMAC-SHA256"}, + common::NoParam{}, + std::uint64_t{7U}, + std::uint8_t{2U}}); +} + +TEST(MediatorProviderSelectionTest, FallsBackWhenPreferredProviderRejectsAlgorithm) +{ + config::Config config; + auto provider_manager = std::make_shared(config.GetProviderInitConfig()); + auto hardware_factory = std::make_shared(false); + auto software_factory = std::make_shared(true); + + ASSERT_TRUE(provider_manager->RegisterProvider( + "HW_STUB", std::make_shared(hardware_factory), common::CryptoProviderType::HARDWARE)); + ASSERT_TRUE(provider_manager->RegisterProvider( + "SW_STUB", std::make_shared(software_factory), common::CryptoProviderType::SOFTWARE)); + ASSERT_TRUE(provider_manager->Initialize()); + + auto data_manager = std::make_shared(); + constexpr data_manager::ClientId kClientId = 42U; + const auto parent_result = data_manager->addNode(kClientId, std::make_shared(false)); + ASSERT_TRUE(parent_result.has_value()); + + MediatorImpl mediator{MediatorDependencies{data_manager, provider_manager, nullptr, nullptr}}; + const auto operation = protocol::OperationRequestBuilder() + .operation(operations::CreateContext()) + .with_in_string("HASH") + .with_in_string("SHA256") + .with_in_val_uint8(static_cast(ProviderType::kHardwarePreferred)) + .with_no_param() + .with_no_param() + .with_no_param() + .build(); + ASSERT_TRUE(operation.has_value()); + + control_plane::ControlRequest request{}; + request.request_id = 1U; + request.client_id = kClientId; + request.data_node_id = parent_result.value(); + request.operation = operation.value(); + + const auto response = mediator.processRequest(request); + ASSERT_EQ(response.operation.operations.size(), 1U); + EXPECT_EQ(response.operation.operations.front().result, protocol::OPERATION_RESULT_SUCCESS); + EXPECT_EQ(hardware_factory->calls, 1U); + EXPECT_EQ(software_factory->calls, 1U); + ASSERT_NE(software_factory->handler, nullptr); + EXPECT_EQ(software_factory->handler->provider_id, 1U); +} + +TEST(MediatorProviderIdentityTest, UnboundSentinelDoesNotCollideWithFirstProvider) +{ + EXPECT_EQ(operations::SHM_WIRE_PROVIDER_ID_UNBOUND, static_cast(common::kInvalidProviderId)); + EXPECT_NE(operations::SHM_WIRE_PROVIDER_ID_UNBOUND, 0U); + EXPECT_EQ(CryptoResourceId{}.primary_provider, kUnboundProviderId); +} + +} // namespace +} // namespace score::crypto::daemon::mediator diff --git a/score/crypto/src/daemon/provider/handler/operations/hash_handler_operations.hpp b/score/crypto/src/daemon/provider/handler/operations/hash_handler_operations.hpp index 9ea9ff364..f9091007f 100644 --- a/score/crypto/src/daemon/provider/handler/operations/hash_handler_operations.hpp +++ b/score/crypto/src/daemon/provider/handler/operations/hash_handler_operations.hpp @@ -41,7 +41,7 @@ using OperationAction = common::OperationAction; // HASH_INIT // Request: data_node_id = context_id, -// param[0]: optional DataBuffer — initial data to hash or IV +// no operation parameters // Response: status_code (SUCCESS/error) // no output parameters // Effect: Calls InitHash(), initializes hash stream context, transitions state IDLE → INITIALIZED @@ -57,8 +57,7 @@ inline constexpr OperationAction HASH_UPDATE = 2; // HASH_FINALIZE // Request: data_node_id = context_id, -// param[0]: optional DataBuffer — output buffer for hash digest (modified in-place) -// param[1]: optional DataBuffer — final data chunk to include +// param[0]: DataBuffer — output buffer for hash digest (modified in-place) // Response: status_code (SUCCESS/error) // param[0]: uint64_t — digest length in bytes (hash bytes written to request param[0]) // Effect: Calls FinalizeHash(), computes final hash into param[0] buffer, clears stream context, transitions state → @@ -68,8 +67,7 @@ inline constexpr OperationAction HASH_FINALIZE = 3; // HASH_SS (Single-Shot Hash) // Request: data_node_id = context_id, // param[0]: DataBuffer — data to hash -// param[1]: optional DataBuffer — output buffer for hash digest (modified in-place) -// param[2]: optional DataBuffer — initialization vector (unused for hash) +// param[1]: DataBuffer — output buffer for hash digest (modified in-place) // Response: status_code (SUCCESS/error) // param[0]: uint64_t — digest length in bytes (hash bytes written to request param[1]) // Effect: Calls SingleShotHash(), requires IDLE state, performs init+update+finalize in one call diff --git a/score/crypto/src/daemon/provider/handler/src/handler_utils.cpp b/score/crypto/src/daemon/provider/handler/src/handler_utils.cpp index e3e742b3c..9cfc3781a 100644 --- a/score/crypto/src/daemon/provider/handler/src/handler_utils.cpp +++ b/score/crypto/src/daemon/provider/handler/src/handler_utils.cpp @@ -27,6 +27,21 @@ namespace handler namespace handler_utils { +Expected ValidateParameterCount( + const common::RequestParameters& parameters, + const std::size_t expected_count) noexcept +{ + if (parameters.size() < expected_count) + { + return make_unexpected(score::crypto::daemon::common::DaemonErrorCode::kInsufficientParameters); + } + if (parameters.size() > expected_count) + { + return make_unexpected(score::crypto::daemon::common::DaemonErrorCode::kInvalidArgument); + } + return std::monostate{}; +} + Expected ExtractOutputBufferData(common::RequestParameter& userData, uint8_t*& buffer, size_t& size) noexcept { @@ -53,6 +68,16 @@ ExtractOutputBufferData(common::RequestParameter& userData, uint8_t*& buffer, si Expected ValidateStreamOperationSequence( common::StreamOperationState currentState, StreamOperation streamOperation) noexcept +{ + return ValidateStreamOperationSequence( + currentState, streamOperation, false, score::crypto::daemon::common::DaemonErrorCode::kInvalidStreamOperation); +} + +Expected ValidateStreamOperationSequence( + common::StreamOperationState currentState, + StreamOperation streamOperation, + const bool allow_finalize_without_update, + const score::crypto::daemon::common::DaemonErrorCode invalid_sequence_error) noexcept { switch (streamOperation) { @@ -66,12 +91,13 @@ Expected. + * @param allow_empty Whether a zero-length span is accepted. A null data + * pointer is valid only for an accepted zero-length span. * @return The extracted span on success, or an error code otherwise. * * @retval score::cpp::span Span successfully extracted. * @retval score::crypto::daemon::common::DaemonErrorCode::kInvalidDataType Parameter is not the requested span type. - * @retval score::crypto::daemon::common::DaemonErrorCode::kInsufficientBufferSize Null data or zero size. + * @retval score::crypto::daemon::common::DaemonErrorCode::kInsufficientBufferSize Invalid null data or a disallowed + * zero size. */ template [[nodiscard]] Expected::SpanType, ::score::crypto::daemon::common::DaemonErrorCode> -CheckAndGetSpan(typename detail::SpanTraits::ParamType param) noexcept +CheckAndGetSpan(typename detail::SpanTraits::ParamType param, const bool allow_empty = false) noexcept { using SpanType = typename detail::SpanTraits::SpanType; auto* span = std::get_if(¶m); @@ -78,13 +81,24 @@ CheckAndGetSpan(typename detail::SpanTraits::ParamType param) noexcept { return make_unexpected(::score::crypto::daemon::common::DaemonErrorCode::kInvalidDataType); } - if (span->data() == nullptr || span->size() == 0U) + if ((span->data() == nullptr) && (span->size() != 0U)) + { + return make_unexpected(::score::crypto::daemon::common::DaemonErrorCode::kInsufficientBufferSize); + } + if (!allow_empty && (span->size() == 0U)) { return make_unexpected(::score::crypto::daemon::common::DaemonErrorCode::kInsufficientBufferSize); } return *span; } +/// @brief Require an exact operation-parameter count at the daemon boundary. +/// @return kInsufficientParameters when parameters are missing and +/// kInvalidArgument when unexpected trailing parameters are present. +[[nodiscard]] Expected ValidateParameterCount( + const common::RequestParameters& parameters, + std::size_t expected_count) noexcept; + /** * @brief Streaming operation kind used to drive the stream state machine. * @@ -111,6 +125,9 @@ enum class StreamOperation : std::uint8_t * * @param currentState The current operation state (IDLE, STREAM_INITIALIZED, or STREAM_ACTIVE) * @param streamOperation The streaming operation being requested + * @param allow_finalize_without_update Whether STREAM_INITIALIZED may transition + * directly to IDLE on Finalize (required for hashing empty input). + * @param invalid_sequence_error Error returned for Update/Finalize from an invalid state. * @return Expected containing the next StreamOperationState on success, or DaemonErrorCode on failure * * @retval StreamOperationState Transition valid; value is the resulting state @@ -118,6 +135,11 @@ enum class StreamOperation : std::uint8_t */ [[nodiscard]] Expected ValidateStreamOperationSequence(common::StreamOperationState currentState, StreamOperation streamOperation) noexcept; +[[nodiscard]] Expected +ValidateStreamOperationSequence(common::StreamOperationState currentState, + StreamOperation streamOperation, + bool allow_finalize_without_update, + ::score::crypto::daemon::common::DaemonErrorCode invalid_sequence_error) noexcept; } // namespace handler_utils diff --git a/score/crypto/src/daemon/provider/pkcs11/BUILD b/score/crypto/src/daemon/provider/pkcs11/BUILD index 3ef2248e2..f13304515 100644 --- a/score/crypto/src/daemon/provider/pkcs11/BUILD +++ b/score/crypto/src/daemon/provider/pkcs11/BUILD @@ -43,6 +43,10 @@ cc_library( visibility = [ "//:__subpackages__", ], + deps = [ + "//score/crypto/src/daemon/common", + "//score/crypto/src/daemon/common:algorithm_info", + ], ) cc_library( diff --git a/score/crypto/src/daemon/provider/pkcs11/detail/pkcs11_algorithm_info.hpp b/score/crypto/src/daemon/provider/pkcs11/detail/pkcs11_algorithm_info.hpp index a5f420ee4..ca7b55e5d 100644 --- a/score/crypto/src/daemon/provider/pkcs11/detail/pkcs11_algorithm_info.hpp +++ b/score/crypto/src/daemon/provider/pkcs11/detail/pkcs11_algorithm_info.hpp @@ -14,6 +14,7 @@ #ifndef SCORE_CRYPTO_SRC_DAEMON_PROVIDER_PKCS11_DETAIL_PKCS11_ALGORITHM_INFO_HPP #define SCORE_CRYPTO_SRC_DAEMON_PROVIDER_PKCS11_DETAIL_PKCS11_ALGORITHM_INFO_HPP +#include "score/crypto/src/daemon/common/algorithm_info.hpp" #include "score/crypto/src/daemon/common/types.hpp" #include @@ -80,30 +81,29 @@ inline constexpr Pkcs11AlgoEntry kAlgoEntries[] = { // Hash algorithm → CK_MECHANISM_TYPE // --------------------------------------------------------------------------- -struct Pkcs11HashMechanism -{ - std::string_view name; - CK_MECHANISM_TYPE mechanism; -}; - -inline constexpr Pkcs11HashMechanism kHashMechanisms[] = { - {"SHA256", CKM_SHA256}, - {"SHA384", CKM_SHA384}, - {"SHA512", CKM_SHA512}, - {"SHA224", CKM_SHA224}, - {"SHA1", CKM_SHA_1}, - {"MD5", CKM_MD5}, -}; - /// @brief Look up the PKCS#11 mechanism type for a hash algorithm. [[nodiscard]] inline CK_MECHANISM_TYPE LookupHashMechanism(std::string_view algorithm) noexcept { - for (const auto& entry : kHashMechanisms) + const auto info = common::LookupHashAlgorithmInfo(algorithm); + if (!info.has_value()) { - if (entry.name == algorithm) - { - return entry.mechanism; - } + return CK_UNAVAILABLE_INFORMATION; + } + + switch (info->algorithm) + { + case common::HashAlgorithm::kSha256: + return CKM_SHA256; + case common::HashAlgorithm::kSha384: + return CKM_SHA384; + case common::HashAlgorithm::kSha512: + return CKM_SHA512; + case common::HashAlgorithm::kSha224: + return CKM_SHA224; + case common::HashAlgorithm::kSha1: + return CKM_SHA_1; + case common::HashAlgorithm::kMd5: + return CKM_MD5; } return CK_UNAVAILABLE_INFORMATION; } diff --git a/score/crypto/src/daemon/provider/pkcs11/operations/factory/pkcs11_handler_factory.cpp b/score/crypto/src/daemon/provider/pkcs11/operations/factory/pkcs11_handler_factory.cpp index 66730919d..8b12c7aae 100644 --- a/score/crypto/src/daemon/provider/pkcs11/operations/factory/pkcs11_handler_factory.cpp +++ b/score/crypto/src/daemon/provider/pkcs11/operations/factory/pkcs11_handler_factory.cpp @@ -21,6 +21,7 @@ #include "score/crypto/src/common/types.hpp" #include "score/crypto/src/daemon/common/daemon_error.hpp" #include "score/crypto/src/daemon/provider/executors/key_mgmt_executor.hpp" +#include "score/crypto/src/daemon/provider/pkcs11/detail/pkcs11_algorithm_info.hpp" #include "score/crypto/src/daemon/provider/pkcs11/operations/hash/pkcs11_hash_executor.hpp" #include "score/crypto/src/daemon/provider/pkcs11/operations/hash/pkcs11_hash_handler.hpp" #include "score/crypto/src/daemon/provider/pkcs11/operations/key_management/pkcs11_key_management_handler.hpp" @@ -56,7 +57,7 @@ score::Result Pkcs11HandlerFactory::CreateHandler(const } const score::result::Error error( - static_cast(score::crypto::daemon::common::DaemonErrorCode::kUnsupportedOperation), + static_cast(score::crypto::CryptoErrorCode::kUnsupportedOperation), score::crypto::kCryptoErrorDomain, "Handler not supported by PKCS#11 provider: " + handlerId); return score::Result(score::unexpect, error); @@ -66,10 +67,29 @@ score::Result Pkcs11HandlerFactory::CreateHashHandler(co { if (!Pkcs11HashHandler::IsAlgorithmSupported(algorithm)) { - const score::result::Error error(static_cast( - score::crypto::daemon::common::DaemonErrorCode::kUnsupportedAlgorithm), + const score::result::Error error( + static_cast(score::crypto::CryptoErrorCode::kUnsupportedAlgorithm), + score::crypto::kCryptoErrorDomain, + "Algorithm not supported for PKCS#11 hash handler: " + algorithm); + return score::Result(score::unexpect, error); + } + + const auto mechanism = detail::LookupHashMechanism(std::string_view{algorithm.data(), algorithm.size()}); + const auto mechanism_support = m_provider.SupportsMechanism(mechanism, CKF_DIGEST); + if (!mechanism_support.has_value()) + { + const auto error_code = score::crypto::daemon::common::ToCryptoErrorCode(mechanism_support.error()); + const score::result::Error error(static_cast(error_code), score::crypto::kCryptoErrorDomain, - "Algorithm not supported for PKCS#11 hash handler: " + algorithm); + "Failed to query PKCS#11 hash mechanism: " + algorithm); + return score::Result(score::unexpect, error); + } + if (!mechanism_support.value()) + { + const score::result::Error error( + static_cast(score::crypto::CryptoErrorCode::kUnsupportedAlgorithm), + score::crypto::kCryptoErrorDomain, + "Selected PKCS#11 token does not support hash mechanism: " + algorithm); return score::Result(score::unexpect, error); } @@ -78,7 +98,8 @@ score::Result Pkcs11HandlerFactory::CreateHashHandler(co Pkcs11SessionGuard guard(m_provider, Pkcs11HashHandler::kRequirements); if (!guard) { - const score::result::Error error(static_cast(guard.error()), + const auto error_code = score::crypto::daemon::common::ToCryptoErrorCode(guard.error()); + const score::result::Error error(static_cast(error_code), score::crypto::kCryptoErrorDomain, "PKCS#11: failed to acquire session for handler"); return score::Result(score::unexpect, error); @@ -94,17 +115,18 @@ score::Result Pkcs11HandlerFactory::CreateMacHandler(con { if (!Pkcs11MacHandler::IsAlgorithmSupported(algorithm)) { - const score::result::Error error(static_cast( - score::crypto::daemon::common::DaemonErrorCode::kUnsupportedAlgorithm), - score::crypto::kCryptoErrorDomain, - "Algorithm not supported for PKCS#11 MAC handler: " + algorithm); + const score::result::Error error( + static_cast(score::crypto::CryptoErrorCode::kUnsupportedAlgorithm), + score::crypto::kCryptoErrorDomain, + "Algorithm not supported for PKCS#11 MAC handler: " + algorithm); return score::Result(score::unexpect, error); } Pkcs11SessionGuard guard(m_provider, Pkcs11MacHandler::kRequirements); if (!guard) { - const score::result::Error error(static_cast(guard.error()), + const auto error_code = score::crypto::daemon::common::ToCryptoErrorCode(guard.error()); + const score::result::Error error(static_cast(error_code), score::crypto::kCryptoErrorDomain, "PKCS#11: failed to acquire session for MAC handler"); return score::Result(score::unexpect, error); @@ -123,7 +145,7 @@ score::Result Pkcs11HandlerFactory::CreateKeyManagementH if (!km_service) { const score::result::Error error( - static_cast(score::crypto::daemon::common::DaemonErrorCode::kInvalidArgument), + static_cast(score::crypto::CryptoErrorCode::kInvalidArgument), score::crypto::kCryptoErrorDomain, "PKCS#11 key management handler requires KeyManagementService"); return score::Result(score::unexpect, error); diff --git a/score/crypto/src/daemon/provider/pkcs11/operations/hash/pkcs11_hash_executor.cpp b/score/crypto/src/daemon/provider/pkcs11/operations/hash/pkcs11_hash_executor.cpp index 8222eb9c8..3b53728a9 100644 --- a/score/crypto/src/daemon/provider/pkcs11/operations/hash/pkcs11_hash_executor.cpp +++ b/score/crypto/src/daemon/provider/pkcs11/operations/hash/pkcs11_hash_executor.cpp @@ -27,19 +27,25 @@ using common::ResponseParameters; using common::StreamOperationState; using score::crypto::daemon::common::DaemonErrorCode; using ::score::crypto::daemon::provider::handler::handler_utils::CheckAndGetSpan; +using ::score::crypto::daemon::provider::handler::handler_utils::ValidateParameterCount; -Pkcs11HashExecutor::Pkcs11HashExecutor(const Pkcs11Module& module) noexcept - : m_module{module}, - m_functionList{module.GetFunctionList()}, - m_supportsMessageDigest{module.GetCapabilities().supportsMessageDigest} +namespace { + +[[nodiscard]] constexpr bool IsRequestValidationError(const DaemonErrorCode error) noexcept +{ + return (error == DaemonErrorCode::kInsufficientParameters) || (error == DaemonErrorCode::kInvalidArgument) || + (error == DaemonErrorCode::kInvalidDataType) || (error == DaemonErrorCode::kInsufficientBufferSize); } -bool Pkcs11HashExecutor::SupportsMessageDigest() const noexcept +} // namespace + +Pkcs11HashExecutor::Pkcs11HashExecutor(const Pkcs11Module& module) noexcept : m_functionList{module.GetFunctionList()} { - return m_supportsMessageDigest; } +Pkcs11HashExecutor::Pkcs11HashExecutor(CK_FUNCTION_LIST& function_list) noexcept : m_functionList{&function_list} {} + // static Expected Pkcs11HashExecutor::ValidateStreamTransition( const common::OperationAction action, @@ -47,24 +53,26 @@ Expected Pkcs11H StreamOperationState& nextState) noexcept { namespace ops = handler::hash_handler_operations; - handler::handler_utils::StreamOperation op{}; + handler::handler_utils::StreamOperation streamOperation{}; if (action == ops::HASH_INIT) { - op = handler::handler_utils::StreamOperation::kInit; + streamOperation = handler::handler_utils::StreamOperation::kInit; } else if (action == ops::HASH_UPDATE) { - op = handler::handler_utils::StreamOperation::kUpdate; + streamOperation = handler::handler_utils::StreamOperation::kUpdate; } else if (action == ops::HASH_FINALIZE) { - op = handler::handler_utils::StreamOperation::kFinalize; + streamOperation = handler::handler_utils::StreamOperation::kFinalize; } else { - return make_unexpected(score::crypto::daemon::common::DaemonErrorCode::kInvalidOperation); + return make_unexpected(DaemonErrorCode::kInvalidOperation); } - const auto result = handler::handler_utils::ValidateStreamOperationSequence(currentState, op); + + const auto result = handler::handler_utils::ValidateStreamOperationSequence( + currentState, streamOperation, true, DaemonErrorCode::kStreamNotInitialized); if (!result.has_value()) { return make_unexpected(result.error()); @@ -81,6 +89,7 @@ Expected Pkc StreamOperationState& nextState) noexcept { namespace ops = handler::hash_handler_operations; + nextState = currentState; // --- Single-shot: no stream state transition needed --- if (operationAction == ops::HASH_SS) @@ -96,9 +105,18 @@ Expected Pkc // Reset operation: HASH_RESET if (operationAction == ops::HASH_RESET) { - // TODO: Is this correct for the reset? - Abort(ctx.session); - // Reset(); + const auto countResult = ValidateParameterCount(request, 0U); + if (!countResult.has_value()) + { + return make_unexpected(countResult.error()); + } + const auto abortResult = Abort(ctx.session); + if (!abortResult.has_value()) + { + nextState = currentState; + return make_unexpected(abortResult.error()); + } + nextState = StreamOperationState::IDLE; return {}; } @@ -110,6 +128,15 @@ Expected Pkc return make_unexpected(score::crypto::daemon::common::DaemonErrorCode::kUnsupportedOperation); } + if (operationAction == ops::HASH_INIT) + { + const auto countResult = ValidateParameterCount(request, 0U); + if (!countResult.has_value()) + { + return make_unexpected(countResult.error()); + } + } + // --- Streaming operations: validate state transition --- const auto sequenceResult = ValidateStreamTransition(operationAction, currentState, nextState); if (!sequenceResult.has_value()) @@ -117,13 +144,39 @@ Expected Pkc return make_unexpected(sequenceResult.error()); } + // PKCS#11 does not permit C_DigestInit while another digest operation is + // active. Implement the public Init()-restarts-stream contract explicitly. + const bool restartingStream = (operationAction == ops::HASH_INIT) && (currentState != StreamOperationState::IDLE); + if (restartingStream) + { + const auto abortResult = Abort(ctx.session); + if (!abortResult.has_value()) + { + nextState = currentState; + return make_unexpected(abortResult.error()); + } + } + // --- Dispatch to PKCS#11 call --- if (operationAction == ops::HASH_FINALIZE) { auto result = ExecuteDigestFinal(ctx.session, request); if (!result.has_value()) { - nextState = currentState; + // Caller-side validation failures and an undersized output buffer do not + // consume the token operation, so the caller may correct the request and retry. + const auto error = result.error(); + if (IsRequestValidationError(error)) + { + nextState = currentState; + } + else + { + // PKCS#11 does not guarantee that a digest operation remains active + // after other errors. Normalize the token state before allowing reuse. + const auto abortResult = Abort(ctx.session); + nextState = abortResult.has_value() ? StreamOperationState::IDLE : currentState; + } } return result; } @@ -139,7 +192,23 @@ Expected Pkc // Revert state on failure (caller should not advance) if (!result.has_value()) { - nextState = currentState; + const auto error = result.error(); + if ((operationAction == ops::HASH_UPDATE) && !IsRequestValidationError(error)) + { + const auto abortResult = Abort(ctx.session); + nextState = abortResult.has_value() ? StreamOperationState::IDLE : currentState; + } + else if (restartingStream) + { + // The previous stream was successfully aborted, but the new + // C_DigestInit failed. The session is therefore idle. + nextState = StreamOperationState::IDLE; + } + else + { + nextState = currentState; + } + return make_unexpected(result.error()); } return {}; @@ -165,20 +234,24 @@ Expected Pkcs11H const CK_SESSION_HANDLE session, RequestParameters& request) noexcept { - if (request.empty()) + const auto countResult = ValidateParameterCount(request, 1U); + if (!countResult.has_value()) { - return make_unexpected(score::crypto::daemon::common::DaemonErrorCode::kInsufficientParameters); + return make_unexpected(countResult.error()); } - const auto inputSpan = CheckAndGetSpan(request[0]); + const auto inputSpan = CheckAndGetSpan(request[0], true); if (!inputSpan.has_value()) { return make_unexpected(inputSpan.error()); } + CK_BYTE emptyInput{0U}; // MISRA C++:2023 Rule 8.2.3 deviation — PKCS#11 C API (C_DigestUpdate) requires non-const pPart. // NOLINTNEXTLINE(cppcoreguidelines-pro-type-const-cast) - auto* data = const_cast(static_cast(inputSpan.value().data())); + auto* data = inputSpan.value().empty() + ? &emptyInput + : const_cast(static_cast(inputSpan.value().data())); const CK_RV rv = m_functionList->C_DigestUpdate(session, data, static_cast(inputSpan.value().size())); if (rv != CKR_OK) { @@ -193,9 +266,10 @@ Expected Pkc { // For PKCS11, the output buffer comes from the handler's internal buffer // passed via parameters - if (request.empty()) + const auto countResult = ValidateParameterCount(request, 1U); + if (!countResult.has_value()) { - return make_unexpected(score::crypto::daemon::common::DaemonErrorCode::kInsufficientParameters); + return make_unexpected(countResult.error()); } const auto outputSpan = CheckAndGetSpan(request[0]); @@ -204,9 +278,6 @@ Expected Pkc return make_unexpected(outputSpan.error()); } - // TODO: The OpenSSL HashHandler as well as the general HashHandler operation does support an additional - // final data chunk. Not sure if this shall / can be supported for PKCS#11 - auto digestLen = static_cast(outputSpan.value().size()); const CK_RV rv = m_functionList->C_DigestFinal(session, outputSpan.value().data(), &digestLen); if (rv != CKR_OK) @@ -224,13 +295,14 @@ Pkcs11HashExecutor::ExecuteDigestSingleShot(const CK_SESSION_HANDLE session, CK_MECHANISM& mechanism, RequestParameters& request) noexcept { - if (request.size() < 2U) + const auto countResult = ValidateParameterCount(request, 2U); + if (!countResult.has_value()) { - return make_unexpected(score::crypto::daemon::common::DaemonErrorCode::kInsufficientParameters); + return make_unexpected(countResult.error()); } // Extract input buffer - const auto inputSpan = CheckAndGetSpan(request[0]); + const auto inputSpan = CheckAndGetSpan(request[0], true); if (!inputSpan.has_value()) { return make_unexpected(inputSpan.error()); @@ -243,18 +315,15 @@ Pkcs11HashExecutor::ExecuteDigestSingleShot(const CK_SESSION_HANDLE session, return make_unexpected(outputSpan.error()); } - // For PKCS#11 v2.40: C_DigestInit + C_Digest (two-step single-shot) - // For PKCS#11 v3.0+: C_MessageDigestInit + C_MessageDigest could be used - // when m_supportsMessageDigest is true, avoiding the active-operation - // slot on the session. Currently not dispatched because SoftHSM is v2.40. - // When a v3.0 token is available, add: - // if (m_supportsMessageDigest) { return ExecuteMessageDigest(session, mechanism, config); } - // Defensively abort any leftover operation to ensure session is clean // (in case a previous operation on this session was not properly finalised). // Dispatch through the function list — not via direct C-linkage — so that // the call correctly targets the library that owns this session. - Abort(session); + const auto initialCleanupResult = Abort(session); + if (!initialCleanupResult.has_value()) + { + return make_unexpected(initialCleanupResult.error()); + } const CK_RV initRv = m_functionList->C_DigestInit(session, &mechanism); if (initRv != CKR_OK) @@ -263,13 +332,24 @@ Pkcs11HashExecutor::ExecuteDigestSingleShot(const CK_SESSION_HANDLE session, } auto digestLen = static_cast(outputSpan.value().size()); + CK_BYTE emptyInput{0U}; // MISRA C++:2023 Rule 8.2.3 deviation — PKCS#11 C API (C_Digest) requires non-const pData. // NOLINTNEXTLINE(cppcoreguidelines-pro-type-const-cast) - auto* inputData = const_cast(static_cast(inputSpan.value().data())); + auto* inputData = inputSpan.value().empty() + ? &emptyInput + : const_cast(static_cast(inputSpan.value().data())); const CK_RV digestRv = m_functionList->C_Digest( session, inputData, static_cast(inputSpan.value().size()), outputSpan.value().data(), &digestLen); if (digestRv != CKR_OK) { + // C_Digest may leave the operation active after a retryable error such + // as CKR_BUFFER_TOO_SMALL. Single-shot is externally stateless, so + // always restore the session to an idle state before returning. + const auto cleanupResult = Abort(session); + if (!cleanupResult.has_value()) + { + return make_unexpected(cleanupResult.error()); + } return make_unexpected(Pkcs11Module::MapErrorReturn(digestRv)); } @@ -278,17 +358,23 @@ Pkcs11HashExecutor::ExecuteDigestSingleShot(const CK_SESSION_HANDLE session, return response; } -void Pkcs11HashExecutor::Abort(const CK_SESSION_HANDLE session) noexcept +Expected Pkcs11HashExecutor::Abort( + const CK_SESSION_HANDLE session) noexcept { // Call C_DigestFinal with a dummy buffer to abort any active digest operation - // and return the session to IDLE state. Errors are intentionally ignored: - // if no operation is active, C_DigestFinal returns CKR_OPERATION_NOT_INITIALIZED - // which is harmless here. + // and return the session to IDLE state. If no operation is active, + // C_DigestFinal returns CKR_OPERATION_NOT_INITIALIZED, which is also success + // from the cleanup caller's perspective. // Dispatch through the stored function list — never through a direct C-linkage // symbol — so that the call correctly targets the library that owns this session. std::uint8_t dummyBuf[64U]{0U}; // NOLINT(cppcoreguidelines-pro-bounds-array-init) CK_ULONG dummyLen = sizeof(dummyBuf); - static_cast(m_functionList->C_DigestFinal(session, dummyBuf, &dummyLen)); + const CK_RV rv = m_functionList->C_DigestFinal(session, dummyBuf, &dummyLen); + if ((rv == CKR_OK) || (rv == CKR_OPERATION_NOT_INITIALIZED)) + { + return std::monostate{}; + } + return make_unexpected(Pkcs11Module::MapErrorReturn(rv)); } } // namespace score::crypto::daemon::provider::pkcs11 diff --git a/score/crypto/src/daemon/provider/pkcs11/operations/hash/pkcs11_hash_executor.hpp b/score/crypto/src/daemon/provider/pkcs11/operations/hash/pkcs11_hash_executor.hpp index 65a93b3e2..205e05741 100644 --- a/score/crypto/src/daemon/provider/pkcs11/operations/hash/pkcs11_hash_executor.hpp +++ b/score/crypto/src/daemon/provider/pkcs11/operations/hash/pkcs11_hash_executor.hpp @@ -26,25 +26,26 @@ namespace score::crypto::daemon::provider::pkcs11 { class Pkcs11Module; -struct Pkcs11Capabilities; /// @brief Executor (visitor) that translates generic operation IDs to PKCS#11 C_Digest* calls. /// /// Owns no session state — receives the session handle and mechanism from the handler. /// Reuses handler_utils::ValidateStreamOperationSequence for stream state management. -/// -/// Version-aware dispatch: -/// - v2.40 (SoftHSM): uses C_DigestInit + C_Digest for single-shot -/// - v3.0+: uses C_MessageDigestInit + C_MessageDigest (when supportsMessageDigest is true) -/// This avoids occupying the session's active-operation slot for single-shot digests. +/// Single-shot hashing uses the PKCS#11 v2.40-compatible C_DigestInit + C_Digest sequence. class Pkcs11HashExecutor final { public: /// @brief Construct executor with reference to the PKCS#11 module. /// @param module Non-owning reference to the initialised Pkcs11Module. - /// Capabilities are cached from the module at construction time. explicit Pkcs11HashExecutor(const Pkcs11Module& module) noexcept; + /// @brief Construct executor from an injected PKCS#11 dispatch table. + /// @param function_list Non-owning reference to a function list that outlives this executor. + /// + /// This overload provides a deterministic seam for testing provider error handling without + /// requiring a physical token or modifying a process-global PKCS#11 function list. + explicit Pkcs11HashExecutor(CK_FUNCTION_LIST& function_list) noexcept; + ~Pkcs11HashExecutor() = default; Pkcs11HashExecutor(const Pkcs11HashExecutor&) = delete; @@ -72,11 +73,10 @@ class Pkcs11HashExecutor final /// All dispatch goes through the function list cached at construction — not /// through direct C-linkage symbols — so this correctly targets the library /// that owns the session. - /// @note Safe to call when the session has no active operation (error is ignored). - void Abort(CK_SESSION_HANDLE session) noexcept; - - /// @brief Query whether the underlying token supports v3.0 message-based digest. - [[nodiscard]] bool SupportsMessageDigest() const noexcept; + /// @return Success when cleanup completed or no digest operation was active; otherwise + /// the mapped provider error. + [[nodiscard]] Expected Abort( + CK_SESSION_HANDLE session) noexcept; private: /// @brief Validate a streaming operation action against the current state and compute the next @@ -105,9 +105,7 @@ class Pkcs11HashExecutor final CK_MECHANISM& mechanism, common::RequestParameters& request) noexcept; - const Pkcs11Module& m_module; - CK_FUNCTION_LIST* m_functionList; ///< cached at construction from m_module.GetFunctionList() - bool m_supportsMessageDigest; ///< cached from Pkcs11Capabilities at construction + CK_FUNCTION_LIST* m_functionList; ///< Non-owning PKCS#11 dispatch table. }; } // namespace score::crypto::daemon::provider::pkcs11 diff --git a/score/crypto/src/daemon/provider/pkcs11/operations/hash/pkcs11_hash_handler.cpp b/score/crypto/src/daemon/provider/pkcs11/operations/hash/pkcs11_hash_handler.cpp index ccc2933a6..bf5ad88a2 100644 --- a/score/crypto/src/daemon/provider/pkcs11/operations/hash/pkcs11_hash_handler.cpp +++ b/score/crypto/src/daemon/provider/pkcs11/operations/hash/pkcs11_hash_handler.cpp @@ -31,9 +31,6 @@ using common::ResponseParameters; using common::StreamOperationState; using score::crypto::daemon::common::DaemonErrorCode; -// --- Supported algorithms (same set as OpenSSL HashHandler) --- -static constexpr const char* kSupportedAlgorithms[] = {"SHA256", "SHA384", "SHA512", "SHA224", "SHA1", "MD5"}; - // --- Algorithm → CKM_* mapping --- CK_MECHANISM_TYPE Pkcs11HashHandler::MapAlgorithm(const std::string_view algorithm) noexcept @@ -41,11 +38,11 @@ CK_MECHANISM_TYPE Pkcs11HashHandler::MapAlgorithm(const std::string_view algorit return detail::LookupHashMechanism(algorithm); } -std::uint64_t Pkcs11HashHandler::GetDigestSize() const noexcept +std::optional Pkcs11HashHandler::GetDigestSize() const noexcept { - return static_cast( - score::crypto::daemon::common::LookupDigestSize(std::string_view{m_algorithm.data(), m_algorithm.size()}) - .value_or(64U)); // safe default (largest supported) + const auto size = + score::crypto::daemon::common::LookupDigestSize(std::string_view{m_algorithm.data(), m_algorithm.size()}); + return size.has_value() ? std::optional{static_cast(size.value())} : std::nullopt; } // --- Construction / destruction --- @@ -64,22 +61,32 @@ Pkcs11HashHandler::Pkcs11HashHandler(std::unique_ptr executo m_ctx.mechanism.mechanism = MapAlgorithm(m_algorithm); m_ctx.mechanism.pParameter = nullptr; m_ctx.mechanism.ulParameterLen = 0U; - m_ctx.digest_size = static_cast(GetDigestSize()); + m_ctx.digest_size = static_cast(GetDigestSize().value_or(0U)); } Pkcs11HashHandler::~Pkcs11HashHandler() { + if (m_ctx.session == CK_INVALID_HANDLE) + { + return; + } + // Abort any active PKCS#11 operation before returning the session to the pool. // This ensures the session is in IDLE state when released for reuse by the next handler. // If streaming was not completed (e.g. early destruction), C_DigestFinal is called // with a dummy buffer to cleanly abort the operation state. - m_executor->Abort(m_ctx.session); + const auto cleanupResult = m_executor->Abort(m_ctx.session); // Return the dedicated session to the provider pool. // Guard against nullptr provider (e.g. unit tests that mock without a provider). - if ((m_provider != nullptr) && (m_ctx.session != CK_INVALID_HANDLE)) + if (m_provider != nullptr) { - m_provider->ReleaseSession(m_ctx.session, kRequirements); + // A failed abort leaves the token operation state unspecified. Never + // return such a session to a soft-cleanup pool; closing it guarantees + // that the next handler receives a fresh PKCS#11 session. + const auto disposition = + cleanupResult.has_value() ? Pkcs11SessionDisposition::kReusable : Pkcs11SessionDisposition::kDiscard; + m_provider->ReleaseSession(m_ctx.session, kRequirements, disposition); m_ctx.session = CK_INVALID_HANDLE; } } @@ -88,14 +95,9 @@ Pkcs11HashHandler::~Pkcs11HashHandler() bool Pkcs11HashHandler::IsAlgorithmSupported(const common::AlgorithmId& algorithm) noexcept { - for (const char* supported : kSupportedAlgorithms) - { - if (algorithm == supported) - { - return true; - } - } - return false; + const std::string_view algorithmView{algorithm.data(), algorithm.size()}; + return score::crypto::daemon::common::LookupDigestSize(algorithmView).has_value() && + (MapAlgorithm(algorithmView) != CK_UNAVAILABLE_INFORMATION); } // --- Handler interface: InitializeContext --- @@ -104,24 +106,17 @@ Expected Pkcs11H const handler::InitializationParams& /*init_params*/) { // Validate algorithm (m_algorithm is set at construction). - bool found{false}; - for (const char* supported : kSupportedAlgorithms) - { - if (m_algorithm == supported) - { - found = true; - break; - } - } - if (!found) + const auto digestSize = GetDigestSize(); + const auto mechanism = MapAlgorithm(m_algorithm); + if (!digestSize.has_value() || (mechanism == CK_UNAVAILABLE_INFORMATION)) { return make_unexpected(score::crypto::daemon::common::DaemonErrorCode::kUnsupportedAlgorithm); } - m_ctx.mechanism.mechanism = MapAlgorithm(m_algorithm); + m_ctx.mechanism.mechanism = mechanism; m_ctx.mechanism.pParameter = nullptr; m_ctx.mechanism.ulParameterLen = 0U; - m_ctx.digest_size = static_cast(GetDigestSize()); + m_ctx.digest_size = static_cast(digestSize.value()); m_state = StreamOperationState::IDLE; return std::monostate{}; @@ -144,30 +139,32 @@ Expected Pkc // Handle GET_DIGEST_SIZE locally — no PKCS#11 call needed. if (operationId.operationAction == ops::HASH_GET_DIGEST_SIZE) { + if (!request.empty()) + { + return make_unexpected(score::crypto::daemon::common::DaemonErrorCode::kInvalidArgument); + } + const auto digestSize = GetDigestSize(); + if (!digestSize.has_value()) + { + return make_unexpected(score::crypto::daemon::common::DaemonErrorCode::kUnsupportedAlgorithm); + } ResponseParameters response; - response.push_back(GetDigestSize()); + response.push_back(digestSize.value()); return response; } - if (operationId.operationAction == ops::HASH_SS && request.size() < 2U) - { - return ::score::crypto::make_unexpected(::score::crypto::daemon::common::DaemonErrorCode::kInvalidArgument); - } - if (operationId.operationAction == ops::HASH_FINALIZE && request.empty()) - { - return ::score::crypto::make_unexpected(::score::crypto::daemon::common::DaemonErrorCode::kInvalidArgument); - } - StreamOperationState nextState{m_state}; const auto response = m_executor->Execute(m_ctx, operationId.operationAction, request, m_state, nextState); + // The executor may complete cleanup after a provider failure. Apply the + // resulting state even when the requested operation itself failed. + m_state = nextState; + if (!response.has_value()) { return response; } - m_state = nextState; - return response.value(); } @@ -177,9 +174,10 @@ Expected Pkcs11H { // Delegate the abort to the executor so that all PKCS#11 dispatch is // centralised there and goes through the function list, not direct C-linkage. - if (m_state != StreamOperationState::IDLE) + const auto abortResult = m_executor->Abort(m_ctx.session); + if (!abortResult.has_value()) { - m_executor->Abort(m_ctx.session); + return make_unexpected(abortResult.error()); } m_state = StreamOperationState::IDLE; diff --git a/score/crypto/src/daemon/provider/pkcs11/operations/hash/pkcs11_hash_handler.hpp b/score/crypto/src/daemon/provider/pkcs11/operations/hash/pkcs11_hash_handler.hpp index 96f58dcf7..64824ff2e 100644 --- a/score/crypto/src/daemon/provider/pkcs11/operations/hash/pkcs11_hash_handler.hpp +++ b/score/crypto/src/daemon/provider/pkcs11/operations/hash/pkcs11_hash_handler.hpp @@ -25,6 +25,7 @@ #include #include +#include #include #include @@ -81,7 +82,6 @@ class Pkcs11HashHandler final : public handler::Handler [[nodiscard]] Expected Reset() override; - /// @brief Returns a static handler configuration for PKCS#11 hash. /// @brief Check if the given algorithm is supported by this handler. [[nodiscard]] static bool IsAlgorithmSupported(const common::AlgorithmId& algorithm) noexcept; @@ -90,7 +90,7 @@ class Pkcs11HashHandler final : public handler::Handler [[nodiscard]] static CK_MECHANISM_TYPE MapAlgorithm(std::string_view algorithm) noexcept; /// @brief Return the digest output size for the current algorithm. - [[nodiscard]] std::uint64_t GetDigestSize() const noexcept; + [[nodiscard]] std::optional GetDigestSize() const noexcept; std::unique_ptr m_executor; Pkcs11HashExecutionContext m_ctx; ///< stable per-context parameters for executor diff --git a/score/crypto/src/daemon/provider/pkcs11/pkcs11_module.hpp b/score/crypto/src/daemon/provider/pkcs11/pkcs11_module.hpp index 9dd72ca79..1cbf2cab6 100644 --- a/score/crypto/src/daemon/provider/pkcs11/pkcs11_module.hpp +++ b/score/crypto/src/daemon/provider/pkcs11/pkcs11_module.hpp @@ -30,14 +30,6 @@ namespace score::crypto::daemon::provider::pkcs11 { -/// @brief Capability flags queried once at module initialisation. -struct Pkcs11Capabilities -{ - std::uint8_t versionMajor{0U}; - std::uint8_t versionMinor{0U}; - bool supportsMessageDigest{false}; ///< true if PKCS#11 v3.0+ C_MessageDigest* is available -}; - /// @brief Session access type. /// /// ReadOnly: sufficient for digest, verify, sign, encrypt/decrypt, MAC, AEAD (reads keys, never creates). @@ -156,6 +148,17 @@ enum class Pkcs11SessionCleanupStrategy : std::uint8_t kHardCleanup = 1U }; +/// @brief Whether a released PKCS#11 session may be returned to the pool. +/// +/// A handler selects kDiscard when operation cleanup fails and the session's +/// internal state can no longer be trusted. This overrides the configured +/// soft-cleanup policy for that one session. +enum class Pkcs11SessionDisposition : std::uint8_t +{ + kReusable = 0U, + kDiscard = 1U +}; + /// @brief Sentinel value for Pkcs11ProviderConfig::slotId indicating that the slot /// should be auto-discovered by matching tokenLabel (and optionally tokenModel) /// via C_GetSlotList + C_GetTokenInfo at Initialize() time. @@ -304,9 +307,6 @@ class Pkcs11Module final /// @brief Returns the function list pointer. Only valid after successful Init(). [[nodiscard]] CK_FUNCTION_LIST* GetFunctionList() const noexcept; - /// @brief Returns capability flags queried at init time. - [[nodiscard]] const Pkcs11Capabilities& GetCapabilities() const noexcept; - /// @brief Returns true if Init() has completed successfully. /// Use this to avoid calling Init() on a shared module that was already initialised. [[nodiscard]] bool IsInitialized() const noexcept; @@ -317,7 +317,6 @@ class Pkcs11Module final private: CK_FUNCTION_LIST* m_functionList; ModuleGuard m_moduleGuard; - Pkcs11Capabilities m_capabilities; }; } // namespace score::crypto::daemon::provider::pkcs11 diff --git a/score/crypto/src/daemon/provider/pkcs11/pkcs11_provider.hpp b/score/crypto/src/daemon/provider/pkcs11/pkcs11_provider.hpp index 500b7265d..cc1aef442 100644 --- a/score/crypto/src/daemon/provider/pkcs11/pkcs11_provider.hpp +++ b/score/crypto/src/daemon/provider/pkcs11/pkcs11_provider.hpp @@ -131,10 +131,12 @@ class Pkcs11Provider final : public IProvider, public std::enable_shared_from_th /// @brief Return a session from a handler being destroyed. /// - /// 1. Mark session slot idle in the appropriate pool. + /// 1. Mark the session slot idle, or close it when it must be discarded. /// 2. If usedAuth==User -> decrement active-user-handler count. /// 3. If count reaches zero -> C_Logout (token reverts to Public). - void ReleaseSession(CK_SESSION_HANDLE session, const Pkcs11HandlerRequirements& usedRequirements) noexcept; + void ReleaseSession(CK_SESSION_HANDLE session, + const Pkcs11HandlerRequirements& usedRequirements, + Pkcs11SessionDisposition disposition = Pkcs11SessionDisposition::kReusable) noexcept; /// @brief Validate that a session handle is still usable. /// @@ -143,6 +145,19 @@ class Pkcs11Provider final : public IProvider, public std::enable_shared_from_th /// @return true if the session is valid and open, false otherwise. [[nodiscard]] bool ValidateSession(CK_SESSION_HANDLE session) const noexcept; + /// @brief Query whether the configured token exposes a PKCS#11 mechanism + /// with all required capabilities. + /// + /// This is used by handler factories before allocating a context so an + /// algorithm listed by the software implementation is not advertised when + /// the selected token does not actually implement its mechanism for the + /// requested operation. + /// @param mechanism Mechanism to query. + /// @param requiredFlags PKCS#11 CKF_* capability flags that must all be set. + [[nodiscard]] Expected SupportsMechanism( + CK_MECHANISM_TYPE mechanism, + CK_FLAGS requiredFlags) const noexcept; + private: /// @brief A pooled session entry. struct PooledSession diff --git a/score/crypto/src/daemon/provider/pkcs11/src/pkcs11_module.cpp b/score/crypto/src/daemon/provider/pkcs11/src/pkcs11_module.cpp index 66a6f7562..14dabe8f4 100644 --- a/score/crypto/src/daemon/provider/pkcs11/src/pkcs11_module.cpp +++ b/score/crypto/src/daemon/provider/pkcs11/src/pkcs11_module.cpp @@ -268,7 +268,7 @@ void SessionGuard::Close() noexcept // Pkcs11Module // ============================================================================ -Pkcs11Module::Pkcs11Module() noexcept : m_functionList{nullptr}, m_moduleGuard{}, m_capabilities{} {} +Pkcs11Module::Pkcs11Module() noexcept : m_functionList{nullptr}, m_moduleGuard{} {} Expected Pkcs11Module::Init( CK_C_INITIALIZE_ARGS* initArgs) noexcept @@ -292,19 +292,6 @@ Expected Pkcs11M return initResult; } - // Query library info for version and capabilities - CK_INFO info{}; - const CK_RV infoRv = m_functionList->C_GetInfo(&info); - if (infoRv == CKR_OK) - { - m_capabilities.versionMajor = info.cryptokiVersion.major; - m_capabilities.versionMinor = info.cryptokiVersion.minor; - - // PKCS#11 v3.0+ supports C_MessageDigest* APIs - constexpr std::uint8_t kPkcs11V3Major{3U}; - m_capabilities.supportsMessageDigest = (m_capabilities.versionMajor >= kPkcs11V3Major); - } - return std::monostate{}; } @@ -318,11 +305,6 @@ bool Pkcs11Module::IsInitialized() const noexcept return m_functionList != nullptr; } -const Pkcs11Capabilities& Pkcs11Module::GetCapabilities() const noexcept -{ - return m_capabilities; -} - score::crypto::daemon::common::DaemonErrorCode Pkcs11Module::MapErrorReturn(const CK_RV rv) noexcept { switch (rv) diff --git a/score/crypto/src/daemon/provider/pkcs11/src/pkcs11_provider.cpp b/score/crypto/src/daemon/provider/pkcs11/src/pkcs11_provider.cpp index 4d56eb28e..5d11143d3 100644 --- a/score/crypto/src/daemon/provider/pkcs11/src/pkcs11_provider.cpp +++ b/score/crypto/src/daemon/provider/pkcs11/src/pkcs11_provider.cpp @@ -316,7 +316,8 @@ Expected Pkcs } void Pkcs11Provider::ReleaseSession(const CK_SESSION_HANDLE session, - const Pkcs11HandlerRequirements& usedRequirements) noexcept + const Pkcs11HandlerRequirements& usedRequirements, + const Pkcs11SessionDisposition disposition) noexcept { std::lock_guard lock(m_poolMutex); auto& pool = (usedRequirements.sessionType == Pkcs11SessionType::ReadWrite) ? m_rwPool : m_roPool; @@ -340,7 +341,8 @@ void Pkcs11Provider::ReleaseSession(const CK_SESSION_HANDLE session, if (it != pool.end()) { - if (m_config.cleanupStrategy == Pkcs11SessionCleanupStrategy::kHardCleanup) + if ((m_config.cleanupStrategy == Pkcs11SessionCleanupStrategy::kHardCleanup) || + (disposition == Pkcs11SessionDisposition::kDiscard)) { // Erase the entry: unique_ptr destructor calls Close(), // which calls C_CloseSession. No closed-but-idle slots are left in @@ -374,6 +376,34 @@ bool Pkcs11Provider::ValidateSession(const CK_SESSION_HANDLE session) const noex return (rv == CKR_OK); } +Expected Pkcs11Provider::SupportsMechanism( + const CK_MECHANISM_TYPE mechanism, + const CK_FLAGS requiredFlags) const noexcept +{ + if (!m_initialized || (m_module == nullptr) || (m_config.slotId == kSlotIdAutoDetect)) + { + return make_unexpected(score::crypto::daemon::common::DaemonErrorCode::kUninitializedStack); + } + + CK_FUNCTION_LIST* const fns = m_module->GetFunctionList(); + if ((fns == nullptr) || (fns->C_GetMechanismInfo == nullptr)) + { + return make_unexpected(score::crypto::daemon::common::DaemonErrorCode::kUnsupportedOperation); + } + + CK_MECHANISM_INFO mechanism_info{}; + const CK_RV rv = fns->C_GetMechanismInfo(m_config.slotId, mechanism, &mechanism_info); + if (rv == CKR_OK) + { + return (mechanism_info.flags & requiredFlags) == requiredFlags; + } + if (rv == CKR_MECHANISM_INVALID) + { + return false; + } + return make_unexpected(Pkcs11Module::MapErrorReturn(rv)); +} + // ============================================================================ // IProvider -- other interface methods // ============================================================================ diff --git a/score/crypto/src/daemon/provider/score_provider/openssl/BUILD b/score/crypto/src/daemon/provider/score_provider/openssl/BUILD index 1ba4c2fd0..cbba80df5 100644 --- a/score/crypto/src/daemon/provider/score_provider/openssl/BUILD +++ b/score/crypto/src/daemon/provider/score_provider/openssl/BUILD @@ -40,6 +40,7 @@ cc_library( "//conditions:default": ["@platforms//:incompatible"], }), visibility = ["//:__subpackages__"], + deps = ["//score/crypto/src/daemon/common:algorithm_info"], ) cc_library( diff --git a/score/crypto/src/daemon/provider/score_provider/openssl/detail/openssl_algorithm_info.hpp b/score/crypto/src/daemon/provider/score_provider/openssl/detail/openssl_algorithm_info.hpp index 398904b67..f669e4415 100644 --- a/score/crypto/src/daemon/provider/score_provider/openssl/detail/openssl_algorithm_info.hpp +++ b/score/crypto/src/daemon/provider/score_provider/openssl/detail/openssl_algorithm_info.hpp @@ -14,6 +14,8 @@ #ifndef SCORE_CRYPTO_SRC_DAEMON_PROVIDER_SCORE_PROVIDER_OPENSSL_DETAIL_OPENSSL_ALGORITHM_INFO_HPP #define SCORE_CRYPTO_SRC_DAEMON_PROVIDER_SCORE_PROVIDER_OPENSSL_DETAIL_OPENSSL_ALGORITHM_INFO_HPP +#include "score/crypto/src/daemon/common/algorithm_info.hpp" + #include #include @@ -22,33 +24,30 @@ namespace score::crypto::daemon::provider::openssl::detail { -/// @brief Algorithm → OpenSSL EVP_MD mapping entry. -struct OpensslDigestInfo -{ - std::string_view name; - const EVP_MD* (*evp_md_fn)(); ///< Function pointer returning the EVP_MD (avoids static init order) -}; - -/// @brief Static table of supported hash algorithms and their OpenSSL EVP_MD providers. -inline const OpensslDigestInfo kDigestAlgorithms[] = { - {"SHA256", EVP_sha256}, - {"SHA384", EVP_sha384}, - {"SHA512", EVP_sha512}, - {"SHA224", EVP_sha224}, - {"SHA1", EVP_sha1}, - {"MD5", EVP_md5}, -}; - /// @brief Look up the EVP_MD for a hash algorithm name. /// @return EVP_MD pointer, or nullptr if the algorithm is not supported. [[nodiscard]] inline const EVP_MD* LookupHashEVPMD(std::string_view algorithm) noexcept { - for (const auto& entry : kDigestAlgorithms) + const auto info = common::LookupHashAlgorithmInfo(algorithm); + if (!info.has_value()) { - if (entry.name == algorithm) - { - return entry.evp_md_fn(); - } + return nullptr; + } + + switch (info->algorithm) + { + case common::HashAlgorithm::kSha256: + return EVP_sha256(); + case common::HashAlgorithm::kSha384: + return EVP_sha384(); + case common::HashAlgorithm::kSha512: + return EVP_sha512(); + case common::HashAlgorithm::kSha224: + return EVP_sha224(); + case common::HashAlgorithm::kSha1: + return EVP_sha1(); + case common::HashAlgorithm::kMd5: + return EVP_md5(); } return nullptr; } diff --git a/score/crypto/src/daemon/provider/score_provider/openssl/operations/hash/openssl_hash_handler.cpp b/score/crypto/src/daemon/provider/score_provider/openssl/operations/hash/openssl_hash_handler.cpp index f5508d360..a6647a870 100644 --- a/score/crypto/src/daemon/provider/score_provider/openssl/operations/hash/openssl_hash_handler.cpp +++ b/score/crypto/src/daemon/provider/score_provider/openssl/operations/hash/openssl_hash_handler.cpp @@ -17,50 +17,32 @@ #include "score/crypto/src/common/types.hpp" #include "score/crypto/src/daemon/common/daemon_error.hpp" #include "score/crypto/src/daemon/common/types.hpp" -#include "score/crypto/src/daemon/provider/handler/src/handler_utils.hpp" #include "score/crypto/src/daemon/provider/score_provider/openssl/detail/openssl_algorithm_info.hpp" #include "score/crypto/src/daemon/provider/score_provider/openssl/operations/hash/openssl_hash_handler.hpp" #include "score/mw/log/logging.h" -#include #include -#include #include -#include -#include -#include -#include namespace score::crypto::daemon::provider::score_provider::openssl::handler { // Using declarations for convenience using common::DaemonErrorCode; -using common::RequestParameters; using common::ResponseParameters; using common::StreamOperationState; -using ::score::crypto::daemon::provider::handler::handler_utils::CheckAndGetSpan; - -// Static array initialization -static constexpr const char* SUPPORTED_ALGORITHMS[] = {"SHA256", "SHA384", "SHA512", "SHA224", "SHA1", "MD5"}; bool OpenSslHashHandler::IsAlgorithmSupported(const common::AlgorithmId& algorithm) noexcept { - for (const char* supported : SUPPORTED_ALGORITHMS) - { - if (algorithm == supported) - { - return true; - } - } - return false; + return ::score::crypto::daemon::provider::openssl::detail::LookupHashEVPMD(algorithm) != nullptr; } OpenSslHashHandler::OpenSslHashHandler( std::unique_ptr<::score::crypto::daemon::provider::score_provider::operations::hash::HashExecutor> executor, - common::AlgorithmId algorithm) - : ScoreHashHandler(std::move(executor), algorithm), mCurrentStreamContext(nullptr) + common::AlgorithmId algorithm, + const DigestUpdateFunction digestUpdate) + : ScoreHashHandler(std::move(executor), algorithm), mCurrentStreamContext(nullptr), mDigestUpdate(digestUpdate) { // Operation support is defined by the executor } @@ -72,14 +54,8 @@ OpenSslHashHandler::~OpenSslHashHandler() Expected OpenSslHashHandler::ValidateAlgorithm(const std::string& algorithm) const { - for (const char* supported : SUPPORTED_ALGORITHMS) - { - if (algorithm == supported) - { - return std::monostate{}; - } - } - return make_unexpected(DaemonErrorCode::kUnsupportedAlgorithm); + return GetEVPMD(algorithm) != nullptr ? Expected{std::monostate{}} + : make_unexpected(DaemonErrorCode::kUnsupportedAlgorithm); } Expected OpenSslHashHandler::InitializeContext( @@ -120,13 +96,9 @@ Expected OpenSslHashHandler::Reset() return {}; } -Expected OpenSslHashHandler::InitHash( - const std::optional initialDataOrIV) +Expected OpenSslHashHandler::InitHash() { - std::ostringstream tid; - tid << std::this_thread::get_id(); - score::mw::log::LogDebug() << "DEBUG: InitHash called with algorithm:" << m_algorithm << ", thread ID:" << tid.str() - << ", this:" << reinterpret_cast(this); + score::mw::log::LogDebug() << "[OPENSSL_HASH] Initializing stream for algorithm:" << m_algorithm; const EVP_MD* md = GetEVPMD(m_algorithm); if (md == nullptr) { @@ -146,28 +118,16 @@ Expected OpenSslHashHandler::InitHash( // Reset the context (OpenSSL-specific) if (EVP_DigestInit_ex(mCurrentStreamContext, md, nullptr) != 1) { + CleanupStreamContext(); + m_state = StreamOperationState::IDLE; return make_unexpected(DaemonErrorCode::kAlgorithmInitializationFailed); } - // If initial data is provided, process it (OpenSSL-specific) - if (initialDataOrIV.has_value()) - { - const auto inputSpan = CheckAndGetSpan(initialDataOrIV.value()); - if (!inputSpan.has_value()) - { - return make_unexpected(inputSpan.error()); - } - - if (EVP_DigestUpdate(mCurrentStreamContext, inputSpan.value().data(), inputSpan.value().size()) != 1) - { - return make_unexpected(DaemonErrorCode::kAlgorithmExecutionFailed); - } - } - return std::monostate{}; } -Expected OpenSslHashHandler::UpdateHash(const common::RequestParameter& dataToHash) +Expected OpenSslHashHandler::UpdateHash( + const score::cpp::span dataToHash) { // Validate stream context exists (OpenSSL-specific) if (mCurrentStreamContext == nullptr) @@ -175,14 +135,12 @@ Expected OpenSslHashHandler::UpdateHash(const c return make_unexpected(DaemonErrorCode::kStreamNotInitialized); } - const auto inputSpan = CheckAndGetSpan(dataToHash); - if (!inputSpan.has_value()) - { - return make_unexpected(inputSpan.error()); - } - - if (EVP_DigestUpdate(mCurrentStreamContext, inputSpan.value().data(), inputSpan.value().size()) != 1) + const std::uint8_t emptyInput{0U}; + const auto* inputData = dataToHash.empty() ? &emptyInput : dataToHash.data(); + if (mDigestUpdate(mCurrentStreamContext, inputData, dataToHash.size()) != 1) { + CleanupStreamContext(); + m_state = StreamOperationState::IDLE; return make_unexpected(DaemonErrorCode::kAlgorithmExecutionFailed); } @@ -190,56 +148,35 @@ Expected OpenSslHashHandler::UpdateHash(const c } Expected OpenSslHashHandler::FinalizeHash( - common::RequestParameter hashOutput, - const std::optional finalDataToHash) + const score::cpp::span hashOutput) { if (mCurrentStreamContext == nullptr) { return make_unexpected(DaemonErrorCode::kStreamNotInitialized); } - // Process final data if provided (OpenSSL-specific) - if (finalDataToHash.has_value()) - { - const auto inputSpan = CheckAndGetSpan(finalDataToHash.value()); - if (!inputSpan.has_value()) - { - CleanupStreamContext(); - return make_unexpected(inputSpan.error()); - } - - if (EVP_DigestUpdate(mCurrentStreamContext, inputSpan.value().data(), inputSpan.value().size()) != 1) - { - CleanupStreamContext(); - return make_unexpected(DaemonErrorCode::kAlgorithmExecutionFailed); - } - } - // Get the hash size (OpenSSL-specific) - unsigned int digestSize = EVP_MD_CTX_size(mCurrentStreamContext); - if (digestSize == 0) + const int digestSizeResult = EVP_MD_CTX_size(mCurrentStreamContext); + if (digestSizeResult <= 0) { CleanupStreamContext(); + m_state = StreamOperationState::IDLE; return make_unexpected(DaemonErrorCode::kAlgorithmExecutionFailed); } + const auto digestSize = static_cast(digestSizeResult); - // Extract and validate the caller-provided SHM output buffer - const auto outputSpan = CheckAndGetSpan(hashOutput); - if (!outputSpan.has_value()) + // On an undersized output buffer the digest operation remains active so + // the caller can retry Finalize() with a corrected buffer. + if (hashOutput.size() < digestSize) { - CleanupStreamContext(); - return make_unexpected(outputSpan.error()); - } - if (outputSpan.value().size() < digestSize) - { - CleanupStreamContext(); return make_unexpected(DaemonErrorCode::kInsufficientBufferSize); } unsigned int digestLen = 0; - if (EVP_DigestFinal_ex(mCurrentStreamContext, outputSpan.value().data(), &digestLen) != 1) + if (EVP_DigestFinal_ex(mCurrentStreamContext, hashOutput.data(), &digestLen) != 1) { CleanupStreamContext(); + m_state = StreamOperationState::IDLE; return make_unexpected(DaemonErrorCode::kAlgorithmExecutionFailed); } @@ -252,12 +189,9 @@ Expected OpenSslHashHandler::Finali } Expected OpenSslHashHandler::SingleShotHash( - const common::RequestParameter& dataToHash, - common::RequestParameter outputHash, - std::optional initializationVector) + const score::cpp::span dataToHash, + const score::cpp::span outputHash) { - (void)initializationVector; - if (m_algorithm.empty()) { return make_unexpected(DaemonErrorCode::kInsufficientParameters); @@ -275,32 +209,23 @@ Expected OpenSslHashHandler::Single return make_unexpected(DaemonErrorCode::kUnsupportedAlgorithm); } - unsigned int digestSize = EVP_MD_size(md); - - // Extract input data - const auto inputSpan = CheckAndGetSpan(dataToHash); - if (!inputSpan.has_value()) + const int digestSizeResult = EVP_MD_size(md); + if (digestSizeResult <= 0) { - return make_unexpected(inputSpan.error()); - } - - // Extract and validate the caller-provided SHM output buffer - const auto outputSpan = CheckAndGetSpan(outputHash); - if (!outputSpan.has_value()) - { - return make_unexpected(outputSpan.error()); + return make_unexpected(DaemonErrorCode::kAlgorithmExecutionFailed); } + const auto digestSize = static_cast(digestSizeResult); - if (outputSpan.value().size() < digestSize) + if (outputHash.size() < digestSize) { return make_unexpected(DaemonErrorCode::kInsufficientBufferSize); } // Single OpenSSL call: handles context creation, init, update, final, and cleanup internally unsigned int digestLen = 0; - if (EVP_Digest( - inputSpan.value().data(), inputSpan.value().size(), outputSpan.value().data(), &digestLen, md, nullptr) != - 1) + const std::uint8_t emptyInput{0U}; + const auto* inputData = dataToHash.empty() ? &emptyInput : dataToHash.data(); + if (EVP_Digest(inputData, dataToHash.size(), outputHash.data(), &digestLen, md, nullptr) != 1) { return make_unexpected(DaemonErrorCode::kAlgorithmExecutionFailed); } diff --git a/score/crypto/src/daemon/provider/score_provider/openssl/operations/hash/openssl_hash_handler.hpp b/score/crypto/src/daemon/provider/score_provider/openssl/operations/hash/openssl_hash_handler.hpp index 085708c4f..2aa7e2cd9 100644 --- a/score/crypto/src/daemon/provider/score_provider/openssl/operations/hash/openssl_hash_handler.hpp +++ b/score/crypto/src/daemon/provider/score_provider/openssl/operations/hash/openssl_hash_handler.hpp @@ -20,8 +20,9 @@ #include "score/crypto/src/daemon/provider/score_provider/operations/hash/hash_executor.hpp" #include "score/crypto/src/daemon/provider/score_provider/operations/hash/score_hash_handler.hpp" #include + +#include #include -#include #include namespace score::crypto::daemon::provider::score_provider::openssl::handler @@ -32,10 +33,12 @@ class OpenSslHashHandler final { public: using Sptr = std::shared_ptr; + using DigestUpdateFunction = int (*)(EVP_MD_CTX*, const void*, std::size_t); explicit OpenSslHashHandler( std::unique_ptr<::score::crypto::daemon::provider::score_provider::operations::hash::HashExecutor> executor, - common::AlgorithmId algorithm); + common::AlgorithmId algorithm, + DigestUpdateFunction digestUpdate = &EVP_DigestUpdate); ~OpenSslHashHandler() override; // Handler interface overrides (OpenSSL-specific initialization and cleanup) @@ -44,16 +47,14 @@ class OpenSslHashHandler final Expected Reset() override; // ScoreHashHandler typed method overrides (OpenSSL crypto implementation) - Expected InitHash( - const std::optional initialDataOrIV) override; - Expected UpdateHash(const common::RequestParameter& dataToHash) override; + Expected InitHash() override; + Expected UpdateHash( + score::cpp::span dataToHash) override; Expected FinalizeHash( - common::RequestParameter hashOutput, - const std::optional finalDataToHash) override; + score::cpp::span hashOutput) override; Expected SingleShotHash( - const common::RequestParameter& dataToHash, - common::RequestParameter outputHash, - std::optional initializationVector) override; + score::cpp::span dataToHash, + score::cpp::span outputHash) override; /// @brief Check if the given algorithm is supported by this handler. [[nodiscard]] static bool IsAlgorithmSupported(const common::AlgorithmId& algorithm) noexcept; @@ -61,6 +62,7 @@ class OpenSslHashHandler final private: // OpenSSL-specific stream context management EVP_MD_CTX* mCurrentStreamContext; + DigestUpdateFunction mDigestUpdate; // Helper methods (OpenSSL provider-specific) const EVP_MD* GetEVPMD(const std::string& algorithm) const; diff --git a/score/crypto/src/daemon/provider/score_provider/operations/hash/score_hash_handler.hpp b/score/crypto/src/daemon/provider/score_provider/operations/hash/score_hash_handler.hpp index ff196fed0..d74776ad5 100644 --- a/score/crypto/src/daemon/provider/score_provider/operations/hash/score_hash_handler.hpp +++ b/score/crypto/src/daemon/provider/score_provider/operations/hash/score_hash_handler.hpp @@ -22,7 +22,6 @@ #include #include -#include #include namespace score::crypto::daemon::provider::score_provider::operations::hash @@ -93,23 +92,20 @@ class ScoreHashHandler : public handler::Handler // ----------------------------------------------------------------------- /// @brief Initialize a hash operation on an existing context. - [[nodiscard]] virtual Expected InitHash( - const std::optional initialDataOrIV); + [[nodiscard]] virtual Expected InitHash(); /// @brief Add data to the active hash stream. [[nodiscard]] virtual Expected UpdateHash( - const common::RequestParameter& dataToHash); + score::cpp::span dataToHash); /// @brief Finalize the hash and produce the digest. [[nodiscard]] virtual Expected FinalizeHash( - common::RequestParameter hashOutput, - const std::optional finalDataToHash); + score::cpp::span hashOutput); /// @brief Perform single-shot hash without streaming. [[nodiscard]] virtual Expected SingleShotHash( - const common::RequestParameter& dataToHash, - common::RequestParameter outputHash, - std::optional iv); + score::cpp::span dataToHash, + score::cpp::span outputHash); /// @brief Get the digest size for the current algorithm. [[nodiscard]] virtual Expected GetDigestSize() const; diff --git a/score/crypto/src/daemon/provider/score_provider/operations/hash/src/hash_executor.cpp b/score/crypto/src/daemon/provider/score_provider/operations/hash/src/hash_executor.cpp index 1ea30e71d..25d4bf4c4 100644 --- a/score/crypto/src/daemon/provider/score_provider/operations/hash/src/hash_executor.cpp +++ b/score/crypto/src/daemon/provider/score_provider/operations/hash/src/hash_executor.cpp @@ -24,6 +24,8 @@ using common::DaemonErrorCode; using common::RequestParameters; using common::ResponseParameters; using common::StreamOperationState; +using ::score::crypto::daemon::provider::handler::handler_utils::CheckAndGetSpan; +using ::score::crypto::daemon::provider::handler::handler_utils::ValidateParameterCount; Expected HashExecutor::Execute(ScoreHashHandler& handler, const common::OperationIdentifier& operationId, @@ -100,101 +102,93 @@ Expected HashExecutor::Execute(ScoreHashHan Expected HashExecutor::ExecuteInit(ScoreHashHandler& handler, RequestParameters& request) { - std::optional> initialDataOrIV; - if (!request.empty()) + const auto countResult = ValidateParameterCount(request, 0U); + if (!countResult.has_value()) { - if (auto* buf = std::get_if>(&request[0])) - { - initialDataOrIV.emplace(*buf); - } + return make_unexpected(countResult.error()); } - return handler.InitHash(initialDataOrIV); + return handler.InitHash(); } Expected HashExecutor::ExecuteUpdate(ScoreHashHandler& handler, RequestParameters& request) { - if (request.empty()) + const auto countResult = ValidateParameterCount(request, 1U); + if (!countResult.has_value()) { - return make_unexpected(DaemonErrorCode::kInsufficientParameters); + return make_unexpected(countResult.error()); } - auto* buf = std::get_if>(&request[0]); - if (buf == nullptr) + const auto inputSpan = CheckAndGetSpan(request[0], true); + if (!inputSpan.has_value()) { - return make_unexpected(DaemonErrorCode::kInvalidDataType); + return make_unexpected(inputSpan.error()); } - return handler.UpdateHash(*buf); + return handler.UpdateHash(inputSpan.value()); } Expected HashExecutor::ExecuteFinalize(ScoreHashHandler& handler, RequestParameters& request) { - if (request.empty()) + const auto countResult = ValidateParameterCount(request, 1U); + if (!countResult.has_value()) { - return make_unexpected(DaemonErrorCode::kInsufficientParameters); + return make_unexpected(countResult.error()); } - auto* outputBuf = std::get_if>(&request[0]); - if (outputBuf == nullptr || outputBuf->data() == nullptr || outputBuf->size() == 0) + const auto outputSpan = CheckAndGetSpan(request[0]); + if (!outputSpan.has_value()) { - return make_unexpected(DaemonErrorCode::kInsufficientBufferSize); + return make_unexpected(outputSpan.error()); } - std::optional> finalData; - if (request.size() > 1) - { - if (auto* buf = std::get_if>(&request[1])) - { - finalData.emplace(*buf); - } - } - - return handler.FinalizeHash(*outputBuf, finalData); + return handler.FinalizeHash(outputSpan.value()); } Expected HashExecutor::ExecuteSingleShot(ScoreHashHandler& handler, RequestParameters& request) { - if (request.size() < 2U) + const auto countResult = ValidateParameterCount(request, 2U); + if (!countResult.has_value()) { - return make_unexpected(DaemonErrorCode::kInsufficientParameters); + return make_unexpected(countResult.error()); } - auto* data = std::get_if>(&request[0]); - if (data == nullptr) + const auto inputSpan = CheckAndGetSpan(request[0], true); + if (!inputSpan.has_value()) { - return make_unexpected(DaemonErrorCode::kInvalidDataType); + return make_unexpected(inputSpan.error()); } - auto* outputBuf = std::get_if>(&request[1]); - if (outputBuf == nullptr || outputBuf->data() == nullptr || outputBuf->size() == 0) + const auto outputSpan = CheckAndGetSpan(request[1]); + if (!outputSpan.has_value()) { - return make_unexpected(DaemonErrorCode::kInsufficientBufferSize); + return make_unexpected(outputSpan.error()); } - std::optional> iv; - if (request.size() > 2) - { - if (auto* buf = std::get_if>(&request[2])) - { - iv.emplace(*buf); - } - } - - return handler.SingleShotHash(*data, *outputBuf, iv); + return handler.SingleShotHash(inputSpan.value(), outputSpan.value()); } Expected HashExecutor::ExecuteReset(ScoreHashHandler& handler, - RequestParameters& /*request*/) + RequestParameters& request) { + const auto countResult = ValidateParameterCount(request, 0U); + if (!countResult.has_value()) + { + return make_unexpected(countResult.error()); + } return handler.Reset(); } Expected HashExecutor::GetDigestSize(const ScoreHashHandler& handler, - RequestParameters& /*request*/) + RequestParameters& request) { + const auto countResult = ValidateParameterCount(request, 0U); + if (!countResult.has_value()) + { + return make_unexpected(countResult.error()); + } return handler.GetDigestSize(); } @@ -204,24 +198,26 @@ Expected HashExecutor::ValidateStreamTransition const StreamOperationState currentState, StreamOperationState& nextState) { - handler::handler_utils::StreamOperation op{}; + handler::handler_utils::StreamOperation streamOperation{}; if (action == handler::hash_handler_operations::HASH_INIT) { - op = handler::handler_utils::StreamOperation::kInit; + streamOperation = handler::handler_utils::StreamOperation::kInit; } else if (action == handler::hash_handler_operations::HASH_UPDATE) { - op = handler::handler_utils::StreamOperation::kUpdate; + streamOperation = handler::handler_utils::StreamOperation::kUpdate; } else if (action == handler::hash_handler_operations::HASH_FINALIZE) { - op = handler::handler_utils::StreamOperation::kFinalize; + streamOperation = handler::handler_utils::StreamOperation::kFinalize; } else { return make_unexpected(DaemonErrorCode::kInvalidOperation); } - const auto result = handler::handler_utils::ValidateStreamOperationSequence(currentState, op); + + const auto result = handler::handler_utils::ValidateStreamOperationSequence( + currentState, streamOperation, true, DaemonErrorCode::kStreamNotInitialized); if (!result.has_value()) { return make_unexpected(result.error()); diff --git a/score/crypto/src/daemon/provider/score_provider/operations/hash/src/score_hash_handler.cpp b/score/crypto/src/daemon/provider/score_provider/operations/hash/src/score_hash_handler.cpp index 06db8f18b..93ffd9b93 100644 --- a/score/crypto/src/daemon/provider/score_provider/operations/hash/src/score_hash_handler.cpp +++ b/score/crypto/src/daemon/provider/score_provider/operations/hash/src/score_hash_handler.cpp @@ -49,28 +49,26 @@ Expected ScoreHashHandler::Reset() // Default typed operations — return unsupported unless overridden // --------------------------------------------------------------------------- -Expected ScoreHashHandler::InitHash( - const std::optional /*initialDataOrIV*/) +Expected ScoreHashHandler::InitHash() { return make_unexpected(DaemonErrorCode::kUnsupportedOperation); } -Expected ScoreHashHandler::UpdateHash(const common::RequestParameter& /*dataToHash*/) +Expected ScoreHashHandler::UpdateHash( + const score::cpp::span /*dataToHash*/) { return make_unexpected(DaemonErrorCode::kUnsupportedOperation); } Expected ScoreHashHandler::FinalizeHash( - common::RequestParameter /*hashOutput*/, - const std::optional /*finalDataToHash*/) + const score::cpp::span /*hashOutput*/) { return make_unexpected(DaemonErrorCode::kUnsupportedOperation); } Expected ScoreHashHandler::SingleShotHash( - const common::RequestParameter& /*dataToHash*/, - common::RequestParameter /*outputHash*/, - std::optional /*iv*/) + const score::cpp::span /*dataToHash*/, + const score::cpp::span /*outputHash*/) { return make_unexpected(DaemonErrorCode::kUnsupportedOperation); } @@ -78,8 +76,13 @@ Expected ScoreHashHandler::SingleShotHash( Expected ScoreHashHandler::GetDigestSize() const { const auto size = common::LookupDigestSize(std::string_view{m_algorithm.data(), m_algorithm.size()}); + if (!size.has_value()) + { + return make_unexpected(DaemonErrorCode::kUnsupportedAlgorithm); + } + ResponseParameters response; - response.push_back(size.value_or(64U)); + response.push_back(static_cast(size.value())); return response; } diff --git a/score/crypto/src/daemon/provider/src/provider_manager.cpp b/score/crypto/src/daemon/provider/src/provider_manager.cpp index 78f3eb632..6bf29bf17 100644 --- a/score/crypto/src/daemon/provider/src/provider_manager.cpp +++ b/score/crypto/src/daemon/provider/src/provider_manager.cpp @@ -161,6 +161,16 @@ bool ProviderManager::RegisterProvider(const common::ProviderName& providerName, return false; } + // The maximum ProviderId is reserved as the wire-level "unbound" value. + // Reject registration before the vector index could collide with it or + // overflow during the narrowing conversion below. + if (m_provider_by_id.size() >= static_cast(common::kInvalidProviderId)) + { + score::mw::log::LogError() << "[ProviderManager] Provider ID space exhausted; cannot register: " + << providerName; + return false; + } + // Assign numeric ID: next index in m_provider_by_id common::ProviderId numeric_id = static_cast(m_provider_by_id.size()); diff --git a/score/crypto/src/daemon/provider/tests/provider_test/BUILD b/score/crypto/src/daemon/provider/tests/provider_test/BUILD index dd88554cd..ea94bb889 100644 --- a/score/crypto/src/daemon/provider/tests/provider_test/BUILD +++ b/score/crypto/src/daemon/provider/tests/provider_test/BUILD @@ -23,11 +23,12 @@ cc_test( }), deps = [ "//score/crypto/src/daemon/common", - "//score/crypto/src/daemon/data_manager", + "//score/crypto/src/daemon/common:algorithm_info", "//score/crypto/src/daemon/provider/handler:crypto_handler_factory_headers", "//score/crypto/src/daemon/provider/handler:hash_handler_operations", "//score/crypto/src/daemon/provider/score_provider/openssl:provider_openssl_library", "//score/tests/utility", + "//third_party/openssl:openssl_shared", "@googletest//:gtest", ], ) diff --git a/score/crypto/src/daemon/provider/tests/provider_test/test_pkcs11_provider.cpp b/score/crypto/src/daemon/provider/tests/provider_test/test_pkcs11_provider.cpp index cc07d8eed..d3b144db5 100644 --- a/score/crypto/src/daemon/provider/tests/provider_test/test_pkcs11_provider.cpp +++ b/score/crypto/src/daemon/provider/tests/provider_test/test_pkcs11_provider.cpp @@ -13,9 +13,12 @@ #include +#include +#include #include #include #include +#include #include #include #include @@ -24,6 +27,8 @@ #include "score/crypto/src/daemon/common/types.hpp" #include "score/crypto/src/daemon/provider/handler/operations/hash_handler_operations.hpp" #include "score/crypto/src/daemon/provider/i_provider.hpp" +#include "score/crypto/src/daemon/provider/pkcs11/operations/hash/pkcs11_hash_executor.hpp" +#include "score/crypto/src/daemon/provider/pkcs11/operations/hash/pkcs11_hash_handler.hpp" #include "score/crypto/src/daemon/provider/pkcs11/pkcs11_module.hpp" #include "score/crypto/src/daemon/provider/pkcs11/pkcs11_provider.hpp" #include "score/tests/utility/test_utility.hpp" @@ -58,6 +63,10 @@ std::vector ExtractDigest(const common::ResponseParameters& respon if (const auto* size_ptr = std::get_if(¶m)) { const auto size = static_cast(*size_ptr); + if (size > outputBuffer.size()) + { + return {}; + } return {outputBuffer.begin(), outputBuffer.begin() + size}; } // Old protocol: full data in response (backward compatibility) @@ -73,6 +82,374 @@ std::vector ExtractDigest(const common::ResponseParameters& respon return {}; } +struct HashAlgorithmTestData +{ + const char* algorithm; + std::size_t digest_size; + const char* hello_digest_path; + const char* complete_digest_path; + const char* empty_digest_path; + const char* abc_digest_path; +}; + +struct HashVector +{ + const char* input_path; + const char* digest_path; +}; + +struct DigestFinalStubState +{ + std::vector results{}; + std::size_t call_count{0U}; +}; + +struct DigestUpdateStubState +{ + CK_RV result{CKR_OK}; + std::size_t call_count{0U}; +}; + +struct DigestStubState +{ + CK_RV init_result{CKR_OK}; + CK_RV digest_result{CKR_OK}; + std::size_t init_call_count{0U}; + std::size_t digest_call_count{0U}; +}; + +DigestFinalStubState& GetDigestFinalStubState() +{ + static DigestFinalStubState state{}; + return state; +} + +DigestUpdateStubState& GetDigestUpdateStubState() +{ + static DigestUpdateStubState state{}; + return state; +} + +DigestStubState& GetDigestStubState() +{ + static DigestStubState state{}; + return state; +} + +void ConfigureDigestFinalStub(std::initializer_list results) +{ + auto& state = GetDigestFinalStubState(); + state.results.assign(results); + state.call_count = 0U; +} + +void ConfigureDigestUpdateStub(const CK_RV result) +{ + auto& state = GetDigestUpdateStubState(); + state.result = result; + state.call_count = 0U; +} + +void ConfigureDigestStub(const CK_RV initResult, const CK_RV digestResult = CKR_OK) +{ + auto& state = GetDigestStubState(); + state.init_result = initResult; + state.digest_result = digestResult; + state.init_call_count = 0U; + state.digest_call_count = 0U; +} + +CK_DEFINE_FUNCTION(CK_RV, DigestFinalStub) +(CK_SESSION_HANDLE /*session*/, CK_BYTE_PTR /*digest*/, CK_ULONG_PTR /*digest_length*/) +{ + auto& state = GetDigestFinalStubState(); + if (state.call_count >= state.results.size()) + { + return CKR_GENERAL_ERROR; + } + return state.results[state.call_count++]; +} + +CK_DEFINE_FUNCTION(CK_RV, DigestUpdateStub) +(CK_SESSION_HANDLE /*session*/, CK_BYTE_PTR /*data*/, CK_ULONG /*data_length*/) +{ + auto& state = GetDigestUpdateStubState(); + ++state.call_count; + return state.result; +} + +CK_DEFINE_FUNCTION(CK_RV, DigestInitStub) +(CK_SESSION_HANDLE /*session*/, CK_MECHANISM_PTR /*mechanism*/) +{ + auto& state = GetDigestStubState(); + ++state.init_call_count; + return state.init_result; +} + +CK_DEFINE_FUNCTION(CK_RV, DigestStub) +(CK_SESSION_HANDLE /*session*/, + CK_BYTE_PTR /*data*/, + CK_ULONG /*data_length*/, + CK_BYTE_PTR /*digest*/, + CK_ULONG_PTR /*digest_length*/) +{ + auto& state = GetDigestStubState(); + ++state.digest_call_count; + return state.digest_result; +} + +pkcs11::Pkcs11HashExecutor MakeStubbedHashExecutor(CK_FUNCTION_LIST& functionList) +{ + functionList.C_DigestInit = &DigestInitStub; + functionList.C_Digest = &DigestStub; + functionList.C_DigestFinal = &DigestFinalStub; + functionList.C_DigestUpdate = &DigestUpdateStub; + return pkcs11::Pkcs11HashExecutor{functionList}; +} + +pkcs11::Pkcs11HashExecutionContext MakeHashExecutionContext() +{ + pkcs11::Pkcs11HashExecutionContext context{}; + context.session = 1U; + context.mechanism.mechanism = CKM_SHA256; + context.digest_size = 32U; + return context; +} + +TEST(Pkcs11HashExecutorErrorTest, AbortNormalizesNoActiveOperationAndPropagatesProviderFailure) +{ + CK_FUNCTION_LIST functionList{}; + auto executor = MakeStubbedHashExecutor(functionList); + + ConfigureDigestFinalStub({CKR_OPERATION_NOT_INITIALIZED}); + EXPECT_TRUE(executor.Abort(1U).has_value()); + + ConfigureDigestFinalStub({CKR_DEVICE_ERROR}); + const auto failedAbort = executor.Abort(1U); + ASSERT_FALSE(failedAbort.has_value()); + EXPECT_EQ(failedAbort.error(), common::DaemonErrorCode::kAlgorithmExecutionFailed); +} + +TEST(Pkcs11HashExecutorErrorTest, InitializationFailureKeepsStreamIdle) +{ + CK_FUNCTION_LIST functionList{}; + auto executor = MakeStubbedHashExecutor(functionList); + auto context = MakeHashExecutionContext(); + common::RequestParameters request{}; + auto nextState = common::StreamOperationState::IDLE; + + ConfigureDigestStub(CKR_DEVICE_ERROR); + const auto result = + executor.Execute(context, ops::HASH_INIT, request, common::StreamOperationState::IDLE, nextState); + + ASSERT_FALSE(result.has_value()); + EXPECT_EQ(result.error(), common::DaemonErrorCode::kAlgorithmExecutionFailed); + EXPECT_EQ(nextState, common::StreamOperationState::IDLE); + EXPECT_EQ(GetDigestStubState().init_call_count, 1U); +} + +TEST(Pkcs11HashExecutorErrorTest, ReinitializationAbortsThePreviousStream) +{ + CK_FUNCTION_LIST functionList{}; + auto executor = MakeStubbedHashExecutor(functionList); + auto context = MakeHashExecutionContext(); + common::RequestParameters request{}; + auto nextState = common::StreamOperationState::IDLE; + + ConfigureDigestStub(CKR_OK); + ConfigureDigestFinalStub({CKR_OK}); + const auto restarted = + executor.Execute(context, ops::HASH_INIT, request, common::StreamOperationState::STREAM_ACTIVE, nextState); + + EXPECT_TRUE(restarted.has_value()); + EXPECT_EQ(nextState, common::StreamOperationState::STREAM_INITIALIZED); + EXPECT_EQ(GetDigestFinalStubState().call_count, 1U); + EXPECT_EQ(GetDigestStubState().init_call_count, 1U); + + ConfigureDigestStub(CKR_OK); + ConfigureDigestFinalStub({CKR_DEVICE_ERROR}); + const auto cleanupFailure = + executor.Execute(context, ops::HASH_INIT, request, common::StreamOperationState::STREAM_ACTIVE, nextState); + + ASSERT_FALSE(cleanupFailure.has_value()); + EXPECT_EQ(cleanupFailure.error(), common::DaemonErrorCode::kAlgorithmExecutionFailed); + EXPECT_EQ(nextState, common::StreamOperationState::STREAM_ACTIVE); + EXPECT_EQ(GetDigestStubState().init_call_count, 0U); +} + +TEST(Pkcs11HashExecutorErrorTest, SingleShotFailureRestoresIdleTokenState) +{ + CK_FUNCTION_LIST functionList{}; + auto executor = MakeStubbedHashExecutor(functionList); + auto context = MakeHashExecutionContext(); + const std::array input{0x42U}; + std::array output{}; + common::RequestParameters request{ + score::cpp::span{input.data(), input.size()}, + score::cpp::span{output.data(), output.size()}, + }; + auto nextState = common::StreamOperationState::IDLE; + + ConfigureDigestStub(CKR_OK, CKR_BUFFER_TOO_SMALL); + ConfigureDigestFinalStub({CKR_OPERATION_NOT_INITIALIZED, CKR_OK}); + const auto recoveredFailure = + executor.Execute(context, ops::HASH_SS, request, common::StreamOperationState::IDLE, nextState); + + ASSERT_FALSE(recoveredFailure.has_value()); + EXPECT_EQ(recoveredFailure.error(), common::DaemonErrorCode::kInsufficientBufferSize); + EXPECT_EQ(nextState, common::StreamOperationState::IDLE); + EXPECT_EQ(GetDigestStubState().init_call_count, 1U); + EXPECT_EQ(GetDigestStubState().digest_call_count, 1U); + EXPECT_EQ(GetDigestFinalStubState().call_count, 2U); + + ConfigureDigestStub(CKR_OK, CKR_BUFFER_TOO_SMALL); + ConfigureDigestFinalStub({CKR_OPERATION_NOT_INITIALIZED, CKR_DEVICE_ERROR}); + const auto cleanupFailure = + executor.Execute(context, ops::HASH_SS, request, common::StreamOperationState::IDLE, nextState); + + ASSERT_FALSE(cleanupFailure.has_value()); + EXPECT_EQ(cleanupFailure.error(), common::DaemonErrorCode::kAlgorithmExecutionFailed); + EXPECT_EQ(nextState, common::StreamOperationState::IDLE); + EXPECT_EQ(GetDigestFinalStubState().call_count, 2U); +} + +TEST(Pkcs11HashExecutorErrorTest, ResetOnlyTransitionsToIdleAfterSuccessfulCleanup) +{ + CK_FUNCTION_LIST functionList{}; + auto executor = MakeStubbedHashExecutor(functionList); + auto context = MakeHashExecutionContext(); + common::RequestParameters request{}; + auto nextState = common::StreamOperationState::STREAM_ACTIVE; + + ConfigureDigestFinalStub({CKR_DEVICE_ERROR}); + const auto failedReset = + executor.Execute(context, ops::HASH_RESET, request, common::StreamOperationState::STREAM_ACTIVE, nextState); + ASSERT_FALSE(failedReset.has_value()); + EXPECT_EQ(failedReset.error(), common::DaemonErrorCode::kAlgorithmExecutionFailed); + EXPECT_EQ(nextState, common::StreamOperationState::STREAM_ACTIVE); + + ConfigureDigestFinalStub({CKR_OK}); + const auto successfulReset = + executor.Execute(context, ops::HASH_RESET, request, common::StreamOperationState::STREAM_ACTIVE, nextState); + EXPECT_TRUE(successfulReset.has_value()); + EXPECT_EQ(nextState, common::StreamOperationState::IDLE); +} + +TEST(Pkcs11HashExecutorErrorTest, FinalizePreservesOnlyRetryableOperationState) +{ + CK_FUNCTION_LIST functionList{}; + auto executor = MakeStubbedHashExecutor(functionList); + auto context = MakeHashExecutionContext(); + std::vector output(32U, 0U); + common::RequestParameters request{score::cpp::span{output.data(), output.size()}}; + auto nextState = common::StreamOperationState::IDLE; + + ConfigureDigestFinalStub({CKR_BUFFER_TOO_SMALL}); + const auto retryableFailure = + executor.Execute(context, ops::HASH_FINALIZE, request, common::StreamOperationState::STREAM_ACTIVE, nextState); + ASSERT_FALSE(retryableFailure.has_value()); + EXPECT_EQ(retryableFailure.error(), common::DaemonErrorCode::kInsufficientBufferSize); + EXPECT_EQ(nextState, common::StreamOperationState::STREAM_ACTIVE); + EXPECT_EQ(GetDigestFinalStubState().call_count, 1U); + + ConfigureDigestFinalStub({CKR_DEVICE_ERROR, CKR_OK}); + const auto recoveredFailure = + executor.Execute(context, ops::HASH_FINALIZE, request, common::StreamOperationState::STREAM_ACTIVE, nextState); + ASSERT_FALSE(recoveredFailure.has_value()); + EXPECT_EQ(recoveredFailure.error(), common::DaemonErrorCode::kAlgorithmExecutionFailed); + EXPECT_EQ(nextState, common::StreamOperationState::IDLE); + EXPECT_EQ(GetDigestFinalStubState().call_count, 2U); + + ConfigureDigestFinalStub({CKR_DEVICE_ERROR, CKR_DEVICE_ERROR}); + const auto unrecoveredFailure = + executor.Execute(context, ops::HASH_FINALIZE, request, common::StreamOperationState::STREAM_ACTIVE, nextState); + ASSERT_FALSE(unrecoveredFailure.has_value()); + EXPECT_EQ(unrecoveredFailure.error(), common::DaemonErrorCode::kAlgorithmExecutionFailed); + EXPECT_EQ(nextState, common::StreamOperationState::STREAM_ACTIVE); + EXPECT_EQ(GetDigestFinalStubState().call_count, 2U); +} + +TEST(Pkcs11HashExecutorErrorTest, UpdatePreservesValidationFailuresAndCleansUpProviderFailures) +{ + CK_FUNCTION_LIST functionList{}; + auto executor = MakeStubbedHashExecutor(functionList); + auto context = MakeHashExecutionContext(); + auto nextState = common::StreamOperationState::IDLE; + + ConfigureDigestUpdateStub(CKR_OK); + ConfigureDigestFinalStub({CKR_OK}); + common::RequestParameters invalidRequest{std::uint64_t{1U}}; + const auto validationFailure = executor.Execute( + context, ops::HASH_UPDATE, invalidRequest, common::StreamOperationState::STREAM_ACTIVE, nextState); + ASSERT_FALSE(validationFailure.has_value()); + EXPECT_EQ(validationFailure.error(), common::DaemonErrorCode::kInvalidDataType); + EXPECT_EQ(nextState, common::StreamOperationState::STREAM_ACTIVE); + EXPECT_EQ(GetDigestUpdateStubState().call_count, 0U); + EXPECT_EQ(GetDigestFinalStubState().call_count, 0U); + + ConfigureDigestUpdateStub(CKR_DEVICE_ERROR); + ConfigureDigestFinalStub({CKR_OK}); + const std::vector input{0x01U}; + common::RequestParameters validRequest{score::cpp::span{input.data(), input.size()}}; + const auto providerFailure = executor.Execute( + context, ops::HASH_UPDATE, validRequest, common::StreamOperationState::STREAM_ACTIVE, nextState); + ASSERT_FALSE(providerFailure.has_value()); + EXPECT_EQ(providerFailure.error(), common::DaemonErrorCode::kAlgorithmExecutionFailed); + EXPECT_EQ(nextState, common::StreamOperationState::IDLE); + EXPECT_EQ(GetDigestUpdateStubState().call_count, 1U); + EXPECT_EQ(GetDigestFinalStubState().call_count, 1U); +} + +TEST(Pkcs11HashExecutorErrorTest, RejectsUnexpectedHashParametersBeforeCallingToken) +{ + CK_FUNCTION_LIST functionList{}; + auto executor = MakeStubbedHashExecutor(functionList); + auto context = MakeHashExecutionContext(); + auto nextState = common::StreamOperationState::IDLE; + + ConfigureDigestStub(CKR_OK); + common::RequestParameters invalidInit{std::uint64_t{1U}}; + const auto initResult = + executor.Execute(context, ops::HASH_INIT, invalidInit, common::StreamOperationState::IDLE, nextState); + ASSERT_FALSE(initResult.has_value()); + EXPECT_EQ(initResult.error(), common::DaemonErrorCode::kInvalidArgument); + EXPECT_EQ(GetDigestStubState().init_call_count, 0U); + + const std::array input{0x42U}; + std::array output{}; + common::RequestParameters invalidSingleShot{ + score::cpp::span{input.data(), input.size()}, + score::cpp::span{output.data(), output.size()}, + std::uint64_t{1U}, + }; + const auto singleShotResult = + executor.Execute(context, ops::HASH_SS, invalidSingleShot, common::StreamOperationState::IDLE, nextState); + ASSERT_FALSE(singleShotResult.has_value()); + EXPECT_EQ(singleShotResult.error(), common::DaemonErrorCode::kInvalidArgument); + EXPECT_EQ(GetDigestStubState().init_call_count, 0U); + + common::RequestParameters invalidReset{std::uint64_t{1U}}; + ConfigureDigestFinalStub({CKR_OK}); + const auto resetResult = executor.Execute( + context, ops::HASH_RESET, invalidReset, common::StreamOperationState::STREAM_ACTIVE, nextState); + ASSERT_FALSE(resetResult.has_value()); + EXPECT_EQ(resetResult.error(), common::DaemonErrorCode::kInvalidArgument); + EXPECT_EQ(nextState, common::StreamOperationState::STREAM_ACTIVE); + EXPECT_EQ(GetDigestFinalStubState().call_count, 0U); + + common::RequestParameters invalidFinalize{ + score::cpp::span{output.data(), output.size()}, + std::uint64_t{1U}, + }; + ConfigureDigestFinalStub({CKR_OK}); + const auto finalizeResult = executor.Execute( + context, ops::HASH_FINALIZE, invalidFinalize, common::StreamOperationState::STREAM_ACTIVE, nextState); + ASSERT_FALSE(finalizeResult.has_value()); + EXPECT_EQ(finalizeResult.error(), common::DaemonErrorCode::kInvalidArgument); + EXPECT_EQ(nextState, common::StreamOperationState::STREAM_ACTIVE); + EXPECT_EQ(GetDigestFinalStubState().call_count, 0U); +} + /// @brief Test fixture that initialises a SoftHSM token before each test. /// /// The fixture mirrors the token-setup pattern from the existing SoftHSM block @@ -163,7 +540,7 @@ class Pkcs11ProviderHashTest : public ::testing::Test ASSERT_EQ(rv, CKR_OK); rv = fl->C_CloseSession(tmpSession); ASSERT_EQ(rv, CKR_OK); -#endif // USE_RUST_PKCS11 +#endif // USE_RUST_PKCS11 // NOTE: Do NOT call C_Finalize here — the provider manages module lifecycle. // The provider's Pkcs11Module will finalize when it's destroyed. @@ -179,9 +556,7 @@ class Pkcs11ProviderHashTest : public ::testing::Test // SoftHSM may report very small limits; we ensure at least 32 sessions available. cfg.maxRoSessionsOverride = 32U; cfg.maxRwSessionsOverride = 16U; - // Use hard cleanup strategy to ensure complete session reset between handlers. - // This addresses SoftHSM's soft cleanup edge cases with concurrent operations. - cfg.cleanupStrategy = pkcs11::Pkcs11SessionCleanupStrategy::kHardCleanup; + cfg.cleanupStrategy = CleanupStrategy(); // sessionType removed: session type is now per-handler via kRequirements provider_ = std::make_shared(std::move(cfg)); @@ -189,6 +564,13 @@ class Pkcs11ProviderHashTest : public ::testing::Test ASSERT_TRUE(provider_->Initialize(ctx)); } + [[nodiscard]] virtual pkcs11::Pkcs11SessionCleanupStrategy CleanupStrategy() const noexcept + { + // Hard cleanup isolates the functional hash tests from token-specific + // soft-cleanup behavior. Dedicated tests below exercise soft cleanup. + return pkcs11::Pkcs11SessionCleanupStrategy::kHardCleanup; + } + void TearDown() override { if (provider_ != nullptr) @@ -202,106 +584,259 @@ class Pkcs11ProviderHashTest : public ::testing::Test std::shared_ptr provider_; }; -// --------------------------------------------------------------------------- -// Single-shot hash tests -// --------------------------------------------------------------------------- - -TEST_F(Pkcs11ProviderHashTest, SHA256SingleShotHash) +class Pkcs11ProviderSoftCleanupHashTest : public Pkcs11ProviderHashTest { - auto cryptoOps = provider_->GetCryptoHandlerFactory(); - ASSERT_NE(cryptoOps, nullptr); - - // Create handler. - auto handlerResult = cryptoOps->CreateHandler("HASH", "SHA256"); - ASSERT_TRUE(handlerResult.has_value()); - auto handler = handlerResult.value(); - ASSERT_NE(handler, nullptr); + protected: + [[nodiscard]] pkcs11::Pkcs11SessionCleanupStrategy CleanupStrategy() const noexcept override + { + return pkcs11::Pkcs11SessionCleanupStrategy::kSoftCleanup; + } +}; - // Initialise. - auto initCtxResult = handler->InitializeContext(handler::InitializationParams{}); - ASSERT_TRUE(initCtxResult.has_value()) << "InitializeContext failed"; +TEST_F(Pkcs11ProviderSoftCleanupHashTest, DiscardsSessionWhenHandlerCleanupFails) +{ + const auto sessionResult = provider_->AcquireSession(pkcs11::Pkcs11HashHandler::kRequirements); + ASSERT_TRUE(sessionResult.has_value()); + const CK_SESSION_HANDLE session = sessionResult.value(); - // Prepare input from test vector file. - auto inputBuffer = tests::utility::read_bin("score/tests/test_vectors/hash/input_hello_world.bin"); - ASSERT_FALSE(inputBuffer.empty()); + CK_FUNCTION_LIST functionList{}; + functionList.C_DigestFinal = &DigestFinalStub; + ConfigureDigestFinalStub({CKR_DEVICE_ERROR}); + { + auto executor = std::make_unique(functionList); + pkcs11::Pkcs11HashHandler hashHandler{std::move(executor), session, "SHA256", provider_.get()}; + } - // Prepare output (SHA-256 → 32 bytes). - constexpr std::size_t kSha256DigestLen{32U}; - std::vector outputBuffer(kSha256DigestLen); + EXPECT_FALSE(provider_->ValidateSession(session)); - // Execute single-shot. - common::RequestParameters request{}; - request.push_back(score::cpp::span{inputBuffer.data(), inputBuffer.size()}); - request.push_back(score::cpp::span{outputBuffer.data(), outputBuffer.size()}); + const auto replacement = provider_->AcquireSession(pkcs11::Pkcs11HashHandler::kRequirements); + ASSERT_TRUE(replacement.has_value()); + EXPECT_TRUE(provider_->ValidateSession(replacement.value())); + provider_->ReleaseSession(replacement.value(), pkcs11::Pkcs11HashHandler::kRequirements); +} - auto executeResult = handler->Execute(MakeHashOp(ops::HASH_SS), request); - ASSERT_TRUE(executeResult.has_value()) << "Execute failed"; +TEST_F(Pkcs11ProviderSoftCleanupHashTest, ReusesSessionWhenHandlerCleanupSucceeds) +{ + const auto sessionResult = provider_->AcquireSession(pkcs11::Pkcs11HashHandler::kRequirements); + ASSERT_TRUE(sessionResult.has_value()); + const CK_SESSION_HANDLE session = sessionResult.value(); - // Extract digest from response parameters. - const auto digest = ExtractDigest(executeResult.value(), outputBuffer); + CK_FUNCTION_LIST functionList{}; + functionList.C_DigestFinal = &DigestFinalStub; + ConfigureDigestFinalStub({CKR_OPERATION_NOT_INITIALIZED}); + { + auto executor = std::make_unique(functionList); + pkcs11::Pkcs11HashHandler hashHandler{std::move(executor), session, "SHA256", provider_.get()}; + } - // Verify against reference test vector. - const auto expectedHash = tests::utility::read_bin("score/tests/test_vectors/hash/sha256_hello_world.bin"); - ASSERT_EQ(expectedHash.size(), kSha256DigestLen); - EXPECT_EQ(digest, expectedHash) << "Hash output does not match expected SHA-256 digest"; + EXPECT_TRUE(provider_->ValidateSession(session)); + const auto reused = provider_->AcquireSession(pkcs11::Pkcs11HashHandler::kRequirements); + ASSERT_TRUE(reused.has_value()); + EXPECT_EQ(reused.value(), session); + provider_->ReleaseSession(reused.value(), pkcs11::Pkcs11HashHandler::kRequirements); } // --------------------------------------------------------------------------- -// Streaming hash tests +// SHA-256/384/512 migration coverage // --------------------------------------------------------------------------- -TEST_F(Pkcs11ProviderHashTest, SHA256StreamingHash) +class Pkcs11ProviderHashAlgorithmTest : public Pkcs11ProviderHashTest, + public ::testing::WithParamInterface { +}; + +TEST_P(Pkcs11ProviderHashAlgorithmTest, SupportsSingleShotStreamingResetAndDigestSize) +{ + const auto& testData = GetParam(); auto cryptoOps = provider_->GetCryptoHandlerFactory(); ASSERT_NE(cryptoOps, nullptr); - auto handlerResult = cryptoOps->CreateHandler("HASH", "SHA256"); - ASSERT_TRUE(handlerResult.has_value()); - auto handler = handlerResult.value(); - ASSERT_NE(handler, nullptr); - - auto initCtxResult = handler->InitializeContext(handler::InitializationParams{}); - ASSERT_TRUE(initCtxResult.has_value()) << "InitializeContext failed"; + auto handlerResult = cryptoOps->CreateHandler("HASH", testData.algorithm); + ASSERT_TRUE(handlerResult.has_value()) << "Failed to create HASH/" << testData.algorithm; + auto hashHandler = handlerResult.value(); + ASSERT_NE(hashHandler, nullptr); + ASSERT_TRUE(hashHandler->InitializeContext(handler::InitializationParams{}).has_value()); - // HASH_INIT - common::RequestParameters initOp{}; - auto initResult = handler->Execute(MakeHashOp(ops::HASH_INIT), initOp); - ASSERT_TRUE(initResult.has_value()) << "HASH_INIT failed"; + const std::vector vectors{ + {"score/tests/test_vectors/hash/input_hello_world.bin", testData.hello_digest_path}, + {"score/tests/test_vectors/hash/input_complete_data.bin", testData.complete_digest_path}, + {"score/tests/test_vectors/hash/input_empty.bin", testData.empty_digest_path}, + {"score/tests/test_vectors/hash/input_abc.bin", testData.abc_digest_path}, + }; - // HASH_UPDATE — chunk 1: "Hello, " - const std::string chunk1Str = "Hello, "; - std::vector chunk1Buf(chunk1Str.begin(), chunk1Str.end()); + for (const auto& vector : vectors) + { + SCOPED_TRACE(std::string{testData.algorithm} + " / " + vector.input_path); + const auto input = tests::utility::read_bin(vector.input_path); + const auto expectedDigest = tests::utility::read_bin(vector.digest_path); + ASSERT_EQ(expectedDigest.size(), testData.digest_size); + + std::vector output(testData.digest_size, 0U); + common::RequestParameters request{ + score::cpp::span{input.data(), input.size()}, + score::cpp::span{output.data(), output.size()}, + }; + + const auto result = hashHandler->Execute(MakeHashOp(ops::HASH_SS), request); + ASSERT_TRUE(result.has_value()) << "Single-shot hash failed"; + EXPECT_EQ(ExtractDigest(result.value(), output), expectedDigest); + } - common::RequestParameters updateOp1{}; - updateOp1.push_back(score::cpp::span{chunk1Buf.data(), chunk1Buf.size()}); - auto update1Result = handler->Execute(MakeHashOp(ops::HASH_UPDATE), updateOp1); - ASSERT_TRUE(update1Result.has_value()) << "HASH_UPDATE chunk1 failed"; + common::RequestParameters digestSizeRequest{}; + const auto digestSizeResult = hashHandler->Execute(MakeHashOp(ops::HASH_GET_DIGEST_SIZE), digestSizeRequest); + ASSERT_TRUE(digestSizeResult.has_value()); + ASSERT_EQ(digestSizeResult.value().size(), 1U); + const auto* digestSize = std::get_if(&digestSizeResult.value().front()); + ASSERT_NE(digestSize, nullptr); + EXPECT_EQ(*digestSize, testData.digest_size); + + common::RequestParameters initRequest{}; + const auto empty = tests::utility::read_bin("score/tests/test_vectors/hash/input_empty.bin"); + const auto expectedEmpty = tests::utility::read_bin(testData.empty_digest_path); + ASSERT_TRUE(hashHandler->Execute(MakeHashOp(ops::HASH_INIT), initRequest).has_value()); + common::RequestParameters emptyUpdate{ + score::cpp::span{empty.data(), empty.size()}, + }; + ASSERT_TRUE(hashHandler->Execute(MakeHashOp(ops::HASH_UPDATE), emptyUpdate).has_value()); + std::vector emptyStreamingOutput(testData.digest_size, 0U); + common::RequestParameters emptyFinalize{ + score::cpp::span{emptyStreamingOutput.data(), emptyStreamingOutput.size()}, + }; + const auto emptyStreamingResult = hashHandler->Execute(MakeHashOp(ops::HASH_FINALIZE), emptyFinalize); + ASSERT_TRUE(emptyStreamingResult.has_value()); + EXPECT_EQ(ExtractDigest(emptyStreamingResult.value(), emptyStreamingOutput), expectedEmpty); + + ASSERT_TRUE(hashHandler->Execute(MakeHashOp(ops::HASH_INIT), initRequest).has_value()); + std::vector zeroUpdateOutput(testData.digest_size, 0U); + common::RequestParameters zeroUpdateFinalize{ + score::cpp::span{zeroUpdateOutput.data(), zeroUpdateOutput.size()}, + }; + const auto zeroUpdateResult = hashHandler->Execute(MakeHashOp(ops::HASH_FINALIZE), zeroUpdateFinalize); + ASSERT_TRUE(zeroUpdateResult.has_value()); + EXPECT_EQ(ExtractDigest(zeroUpdateResult.value(), zeroUpdateOutput), expectedEmpty) + << "Init followed directly by Finalize must hash empty input"; + + const auto hello = tests::utility::read_bin("score/tests/test_vectors/hash/input_hello_world.bin"); + const auto expectedHello = tests::utility::read_bin(testData.hello_digest_path); + ASSERT_FALSE(hello.empty()); + ASSERT_EQ(expectedHello.size(), testData.digest_size); + + ASSERT_TRUE(hashHandler->Execute(MakeHashOp(ops::HASH_INIT), initRequest).has_value()); + + const auto split = static_cast(hello.size() / 2U); + const std::vector firstChunk{hello.begin(), hello.begin() + split}; + const std::vector secondChunk{hello.begin() + split, hello.end()}; + common::RequestParameters firstUpdate{ + score::cpp::span{firstChunk.data(), firstChunk.size()}, + }; + common::RequestParameters secondUpdate{ + score::cpp::span{secondChunk.data(), secondChunk.size()}, + }; + ASSERT_TRUE(hashHandler->Execute(MakeHashOp(ops::HASH_UPDATE), firstUpdate).has_value()); + ASSERT_TRUE(hashHandler->Execute(MakeHashOp(ops::HASH_UPDATE), secondUpdate).has_value()); + + std::vector streamingOutput(testData.digest_size, 0U); + common::RequestParameters finalizeRequest{ + score::cpp::span{streamingOutput.data(), streamingOutput.size()}, + }; + const auto streamingResult = hashHandler->Execute(MakeHashOp(ops::HASH_FINALIZE), finalizeRequest); + ASSERT_TRUE(streamingResult.has_value()); + EXPECT_EQ(ExtractDigest(streamingResult.value(), streamingOutput), expectedHello); + + ASSERT_TRUE(hashHandler->Execute(MakeHashOp(ops::HASH_INIT), initRequest).has_value()); + ASSERT_TRUE(hashHandler->Execute(MakeHashOp(ops::HASH_UPDATE), firstUpdate).has_value()); + common::RequestParameters resetRequest{}; + ASSERT_TRUE(hashHandler->Execute(MakeHashOp(ops::HASH_RESET), resetRequest).has_value()); + + const auto complete = tests::utility::read_bin("score/tests/test_vectors/hash/input_complete_data.bin"); + const auto expectedComplete = tests::utility::read_bin(testData.complete_digest_path); + ASSERT_FALSE(complete.empty()); + ASSERT_TRUE(hashHandler->Execute(MakeHashOp(ops::HASH_INIT), initRequest).has_value()); + common::RequestParameters completeUpdate{ + score::cpp::span{complete.data(), complete.size()}, + }; + ASSERT_TRUE(hashHandler->Execute(MakeHashOp(ops::HASH_UPDATE), completeUpdate).has_value()); + std::vector resetOutput(testData.digest_size, 0U); + common::RequestParameters resetFinalize{ + score::cpp::span{resetOutput.data(), resetOutput.size()}, + }; + const auto resetResult = hashHandler->Execute(MakeHashOp(ops::HASH_FINALIZE), resetFinalize); + ASSERT_TRUE(resetResult.has_value()); + EXPECT_EQ(ExtractDigest(resetResult.value(), resetOutput), expectedComplete); + + std::vector undersizedOutput(testData.digest_size - 1U, 0U); + common::RequestParameters undersizedRequest{ + score::cpp::span{hello.data(), hello.size()}, + score::cpp::span{undersizedOutput.data(), undersizedOutput.size()}, + }; + const auto undersizedSingleShot = hashHandler->Execute(MakeHashOp(ops::HASH_SS), undersizedRequest); + ASSERT_FALSE(undersizedSingleShot.has_value()); + EXPECT_EQ(undersizedSingleShot.error(), common::DaemonErrorCode::kInsufficientBufferSize); + + // PKCS#11 keeps a digest operation active after CKR_BUFFER_TOO_SMALL. + // Verify the handler preserves that retry contract and its stream state. + ASSERT_TRUE(hashHandler->Execute(MakeHashOp(ops::HASH_INIT), initRequest).has_value()); + common::RequestParameters retryUpdate{ + score::cpp::span{hello.data(), hello.size()}, + }; + ASSERT_TRUE(hashHandler->Execute(MakeHashOp(ops::HASH_UPDATE), retryUpdate).has_value()); + common::RequestParameters undersizedFinalize{ + score::cpp::span{undersizedOutput.data(), undersizedOutput.size()}, + }; + const auto failedFinalize = hashHandler->Execute(MakeHashOp(ops::HASH_FINALIZE), undersizedFinalize); + ASSERT_FALSE(failedFinalize.has_value()); + EXPECT_EQ(failedFinalize.error(), common::DaemonErrorCode::kInsufficientBufferSize); + + std::vector retryOutput(testData.digest_size, 0U); + common::RequestParameters retryFinalize{ + score::cpp::span{retryOutput.data(), retryOutput.size()}, + }; + const auto retryResult = hashHandler->Execute(MakeHashOp(ops::HASH_FINALIZE), retryFinalize); + ASSERT_TRUE(retryResult.has_value()) << "Finalize retry failed after an undersized output buffer"; + EXPECT_EQ(ExtractDigest(retryResult.value(), retryOutput), expectedHello); +} - // HASH_UPDATE — chunk 2: "World!" - const std::string chunk2Str = "World!"; - std::vector chunk2Buf(chunk2Str.begin(), chunk2Str.end()); +TEST_F(Pkcs11ProviderHashTest, RejectsUnsupportedAlgorithm) +{ + auto cryptoOps = provider_->GetCryptoHandlerFactory(); + ASSERT_NE(cryptoOps, nullptr); + const auto result = cryptoOps->CreateHandler("HASH", "UNSUPPORTED_ALGORITHM"); + ASSERT_FALSE(result.has_value()); + EXPECT_EQ(*result.error(), + static_cast(score::crypto::CryptoErrorCode::kUnsupportedAlgorithm)); +} - common::RequestParameters updateOp2{}; - updateOp2.push_back(score::cpp::span{chunk2Buf.data(), chunk2Buf.size()}); - auto update2Result = handler->Execute(MakeHashOp(ops::HASH_UPDATE), updateOp2); - ASSERT_TRUE(update2Result.has_value()) << "HASH_UPDATE chunk2 failed"; +TEST_F(Pkcs11ProviderHashTest, QueriesSelectedTokenMechanisms) +{ + const auto sha256 = provider_->SupportsMechanism(CKM_SHA256, CKF_DIGEST); + ASSERT_TRUE(sha256.has_value()); + EXPECT_TRUE(sha256.value()); + + const auto sha256Encryption = provider_->SupportsMechanism(CKM_SHA256, CKF_ENCRYPT); + ASSERT_TRUE(sha256Encryption.has_value()); + EXPECT_FALSE(sha256Encryption.value()) << "SHA-256 must not be accepted for an unsupported operation flag"; + + constexpr CK_MECHANISM_TYPE kUnknownVendorMechanism{CKM_VENDOR_DEFINED | 0x0053434FUL}; + const auto unknown = provider_->SupportsMechanism(kUnknownVendorMechanism, CKF_DIGEST); + ASSERT_TRUE(unknown.has_value()); + EXPECT_FALSE(unknown.value()); +} - // HASH_FINALIZE - constexpr std::size_t kSha256DigestLen{32U}; - std::vector outputBuffer(kSha256DigestLen); +TEST_F(Pkcs11ProviderHashTest, ReportsExecutorUpdateFailure) +{ + auto cryptoOps = provider_->GetCryptoHandlerFactory(); + ASSERT_NE(cryptoOps, nullptr); - common::RequestParameters finishOp{}; - finishOp.push_back(score::cpp::span{outputBuffer.data(), outputBuffer.size()}); - auto finishResult = handler->Execute(MakeHashOp(ops::HASH_FINALIZE), finishOp); - ASSERT_TRUE(finishResult.has_value()) << "HASH_FINALIZE failed"; + auto handlerResult = cryptoOps->CreateHandler("HASH", "SHA256"); + ASSERT_TRUE(handlerResult.has_value()); + auto hashHandler = handlerResult.value(); + ASSERT_TRUE(hashHandler->InitializeContext(handler::InitializationParams{}).has_value()); - // Extract digest from response. - const auto digest = ExtractDigest(finishResult.value(), outputBuffer); + common::RequestParameters initRequest{}; + ASSERT_TRUE(hashHandler->Execute(MakeHashOp(ops::HASH_INIT), initRequest).has_value()); - // Verify same digest as single-shot using reference test vector. - const auto expectedHash = tests::utility::read_bin("score/tests/test_vectors/hash/sha256_hello_world.bin"); - ASSERT_EQ(expectedHash.size(), kSha256DigestLen); - EXPECT_EQ(digest, expectedHash) << "Streaming hash does not match expected SHA-256 digest"; + common::RequestParameters invalidUpdate{std::uint64_t{1U}}; + EXPECT_FALSE(hashHandler->Execute(MakeHashOp(ops::HASH_UPDATE), invalidUpdate).has_value()); } // --------------------------------------------------------------------------- @@ -329,7 +864,8 @@ TEST_F(Pkcs11ProviderHashTest, StreamStateViolation) updateOp.push_back(score::cpp::span{dataBuf.data(), dataBuf.size()}); auto updateResult = handler->Execute(MakeHashOp(ops::HASH_UPDATE), updateOp); - EXPECT_FALSE(updateResult.has_value()) << "HASH_UPDATE without HASH_INIT should fail"; + ASSERT_FALSE(updateResult.has_value()) << "HASH_UPDATE without HASH_INIT should fail"; + EXPECT_EQ(updateResult.error(), common::DaemonErrorCode::kStreamNotInitialized); // HASH_FINALIZE without HASH_INIT should also fail. std::vector outBuf(32U); @@ -338,7 +874,13 @@ TEST_F(Pkcs11ProviderHashTest, StreamStateViolation) finishOp.push_back(score::cpp::span{outBuf.data(), outBuf.size()}); auto finishResult = handler->Execute(MakeHashOp(ops::HASH_FINALIZE), finishOp); - EXPECT_FALSE(finishResult.has_value()) << "HASH_FINALIZE without active stream should fail"; + ASSERT_FALSE(finishResult.has_value()) << "HASH_FINALIZE without active stream should fail"; + EXPECT_EQ(finishResult.error(), common::DaemonErrorCode::kStreamNotInitialized); + + common::RequestParameters initOp{}; + ASSERT_TRUE(handler->Execute(MakeHashOp(ops::HASH_INIT), initOp).has_value()); + EXPECT_TRUE(handler->Execute(MakeHashOp(ops::HASH_INIT), initOp).has_value()) + << "Init on an active stream must restart it"; } // --------------------------------------------------------------------------- @@ -435,6 +977,31 @@ TEST_F(Pkcs11ProviderHashTest, TrueConcurrentStreamingOnSeparateSessions) handlerB.reset(); } +INSTANTIATE_TEST_SUITE_P( + BaselibsMigrationAlgorithms, + Pkcs11ProviderHashAlgorithmTest, + ::testing::Values(HashAlgorithmTestData{"SHA256", + 32U, + "score/tests/test_vectors/hash/sha256_hello_world.bin", + "score/tests/test_vectors/hash/sha256_complete_data.bin", + "score/tests/test_vectors/hash/sha256_empty.bin", + "score/tests/test_vectors/hash/sha256_abc.bin"}, + HashAlgorithmTestData{"SHA384", + 48U, + "score/tests/test_vectors/hash/sha384_hello_world.bin", + "score/tests/test_vectors/hash/sha384_complete_data.bin", + "score/tests/test_vectors/hash/sha384_empty.bin", + "score/tests/test_vectors/hash/sha384_abc.bin"}, + HashAlgorithmTestData{"SHA512", + 64U, + "score/tests/test_vectors/hash/sha512_hello_world.bin", + "score/tests/test_vectors/hash/sha512_complete_data.bin", + "score/tests/test_vectors/hash/sha512_empty.bin", + "score/tests/test_vectors/hash/sha512_abc.bin"}), + [](const ::testing::TestParamInfo& info) { + return info.param.algorithm; + }); + } // namespace int main(int argc, char** argv) diff --git a/score/crypto/src/daemon/provider/tests/provider_test/test_provider.cpp b/score/crypto/src/daemon/provider/tests/provider_test/test_provider.cpp index a6650adc3..a2826b478 100644 --- a/score/crypto/src/daemon/provider/tests/provider_test/test_provider.cpp +++ b/score/crypto/src/daemon/provider/tests/provider_test/test_provider.cpp @@ -12,279 +12,506 @@ ********************************************************************************/ #include + #include -#include +#include +#include #include #include +#include #include #include "score/crypto/src/daemon/common/actors.hpp" +#include "score/crypto/src/daemon/common/algorithm_info.hpp" #include "score/crypto/src/daemon/common/types.hpp" -#include "score/crypto/src/daemon/data_manager/data_manager.hpp" #include "score/crypto/src/daemon/provider/handler/operations/hash_handler_operations.hpp" #include "score/crypto/src/daemon/provider/i_provider.hpp" +#include "score/crypto/src/daemon/provider/score_provider/openssl/operations/hash/openssl_hash_handler.hpp" #include "score/crypto/src/daemon/provider/score_provider/openssl/provider_openssl.hpp" -#include "score/crypto/src/daemon/provider/score_provider/operations/hash/score_hash_handler.hpp" #include "score/tests/utility/test_utility.hpp" namespace common = score::crypto::daemon::common; namespace provider = score::crypto::daemon::provider; namespace handler = score::crypto::daemon::provider::handler; -namespace dm = score::crypto::daemon::data_manager; +namespace ops = score::crypto::daemon::provider::handler::hash_handler_operations; namespace { -/** - * @brief Test fixture for Provider and Hash Operation tests - */ -class ProviderHashTest : public ::testing::Test +struct HashAlgorithmTestData { - protected: - static void SetUpTestSuite() + const char* algorithm; + std::size_t digest_size; + const char* hello_digest_path; + const char* complete_digest_path; + const char* empty_digest_path; + const char* abc_digest_path; +}; + +struct HashVector +{ + const char* input_path; + const char* digest_path; +}; + +common::OperationIdentifier MakeHashOp(const common::OperationAction action) +{ + common::OperationIdentifier operation{}; + operation.operationActor = common::actors::OP_ACTOR_HASH_HANDLER; + operation.operationAction = action; + return operation; +} + +int FailingDigestUpdate(EVP_MD_CTX* /*context*/, const void* /*data*/, const std::size_t /*size*/) +{ + return 0; +} + +class OpenSslProviderEnvironment final : public ::testing::Environment +{ + public: + void SetUp() override { - // Create and initialize the OpenSSL provider provider_ = std::make_shared(); - ASSERT_TRUE(provider_ != nullptr); - provider::ProviderInitContext ctx{0, "OPENSSL"}; // ID 0, name "OPENSSL" - ASSERT_TRUE(provider_->Initialize(ctx)); + ASSERT_NE(provider_, nullptr); + provider::ProviderInitContext context{0, "OPENSSL"}; + ASSERT_TRUE(provider_->Initialize(context)); } - static void TearDownTestSuite() + void TearDown() override { - if (provider_) + if (provider_ != nullptr) { provider_->Shutdown(); provider_.reset(); } } + static const std::shared_ptr& GetProvider() + { + return provider_; + } + + private: static std::shared_ptr provider_; }; -// Static member initialization -std::shared_ptr ProviderHashTest::provider_; +std::shared_ptr OpenSslProviderEnvironment::provider_; -/** - * @brief Test: Verify provider can be created and initialized - */ -TEST_F(ProviderHashTest, ProviderInitialization) +class ProviderHashTest : public ::testing::Test { - ASSERT_TRUE(provider_ != nullptr); - EXPECT_EQ(provider_->GetProviderId(), 0); // OPENSSL provider ID -} + protected: + void SetUp() override + { + provider_ = OpenSslProviderEnvironment::GetProvider(); + ASSERT_NE(provider_, nullptr); + } -/** - * @brief Test: Verify crypto operations can be obtained for SCORE mediator - */ -TEST_F(ProviderHashTest, GetCryptoHandlerFactoryForSCOREMediator) -{ - auto crypto_ops = provider_->GetCryptoHandlerFactory(); - ASSERT_TRUE(crypto_ops != nullptr); -} + std::shared_ptr provider_; +}; -/** - * @brief Test: Verify hash handler can be created - */ -TEST_F(ProviderHashTest, CreateHashHandler) +class ProviderHashAlgorithmTest : public ProviderHashTest, public ::testing::WithParamInterface { - auto crypto_ops = provider_->GetCryptoHandlerFactory(); - ASSERT_TRUE(crypto_ops != nullptr); +}; - auto handler_result = crypto_ops->CreateHandler("HASH", "SHA256"); - ASSERT_TRUE(handler_result.has_value()) << "Failed to create handler for HASH/SHA256"; - EXPECT_NE(handler_result.value(), nullptr); +TEST_F(ProviderHashTest, ProviderInitialization) +{ + ASSERT_NE(provider_, nullptr); + EXPECT_EQ(provider_->GetProviderId(), 0); + EXPECT_NE(provider_->GetCryptoHandlerFactory(), nullptr); } -/** - * @brief Test: Perform a single-shot SHA256 hash operation - * - * This test demonstrates the complete flow: - * 1. Get crypto operations from provider via SCORE mediator - * 2. Create a hash handler for SHA256 - * 3. Initialize the handler - * 4. Execute a single-shot hash operation - * 5. Verify the result - */ -TEST_F(ProviderHashTest, PerformSHA256SingleShotHashOperation) +TEST_F(ProviderHashTest, MigrationTargetMetadata) { - // Step 1: Get crypto handler factory from the provider - auto crypto_ops = provider_->GetCryptoHandlerFactory(); - ASSERT_TRUE(crypto_ops != nullptr); - - // Step 2: Create the handler - auto handler_result = crypto_ops->CreateHandler("HASH", "SHA256"); - ASSERT_TRUE(handler_result.has_value()) << "Failed to create handler for HASH/SHA256"; - auto handler = handler_result.value(); - ASSERT_TRUE(handler != nullptr); - - // Step 3: Initialize handler with algorithm configuration - // Stream context will be created on-demand when START operation is called - auto init_result = handler->InitializeContext(handler::InitializationParams{}); - ASSERT_TRUE(init_result.has_value()); - EXPECT_TRUE(init_result.has_value()); - - // Step 4: Prepare test data - auto input_buffer = tests::utility::read_bin("score/tests/test_vectors/hash/input_hello_world.bin"); - ASSERT_FALSE(input_buffer.empty()); - - // Create input parameter as span - score::cpp::span inputBuf{input_buffer.data(), input_buffer.size()}; - - // Create output buffer for hash result (SHA256 produces 32 bytes) - constexpr std::size_t kSha256DigestLen{32U}; - std::vector output_buffer(kSha256DigestLen); - score::cpp::span outputBuf{output_buffer.data(), output_buffer.size()}; - - // Step 4: Create operation request for single-shot hash - common::RequestParameters op_request; - op_request = {inputBuf, outputBuf}; - - // Step 4: Execute hash operation - common::OperationIdentifier op; - op.operationActor = common::actors::OP_ACTOR_HASH_HANDLER; - op.operationAction = provider::handler::hash_handler_operations::HASH_SS; - auto execute_result = handler->Execute(op, op_request); - ASSERT_TRUE(execute_result.has_value()) << "Execute failed"; - - // Step 5: Verify output - // SHA256("Hello, World!") loaded from test vector file - const auto expected_hash = tests::utility::read_bin("score/tests/test_vectors/hash/sha256_hello_world.bin"); - ASSERT_EQ(expected_hash.size(), kSha256DigestLen); - ASSERT_EQ(output_buffer.size(), expected_hash.size()); - EXPECT_EQ(output_buffer, expected_hash) << "Hash output does not match expected SHA256 hash"; + EXPECT_TRUE(common::IsRecommendedHashAlgorithm("SHA256")); + EXPECT_TRUE(common::IsRecommendedHashAlgorithm("SHA384")); + EXPECT_TRUE(common::IsRecommendedHashAlgorithm("SHA512")); + + EXPECT_FALSE(common::IsRecommendedHashAlgorithm("SHA224")); + EXPECT_FALSE(common::IsRecommendedHashAlgorithm("SHA1")); + EXPECT_FALSE(common::IsRecommendedHashAlgorithm("MD5")); + EXPECT_FALSE(common::IsRecommendedHashAlgorithm("UNSUPPORTED_ALGORITHM")); + EXPECT_FALSE(common::LookupDigestSize("UNSUPPORTED_ALGORITHM").has_value()); } -/** - * @brief Test: Perform streaming SHA256 hash operation - * - * This test demonstrates streaming hash: - * 1. Initialize hash stream - * 2. Update with data in chunks - * 3. Finalize and get the hash - */ -TEST_F(ProviderHashTest, PerformSHA256StreamingHashOperation) +TEST_P(ProviderHashAlgorithmTest, SupportsSingleShotStreamingResetAndDigestSize) { - // Get crypto handler factory - auto crypto_ops = provider_->GetCryptoHandlerFactory(); - ASSERT_TRUE(crypto_ops != nullptr); - - // Create the handler - auto handler_result = crypto_ops->CreateHandler("HASH", "SHA256"); - ASSERT_TRUE(handler_result.has_value()) << "Failed to create handler for HASH/SHA256"; - auto handler = handler_result.value(); - ASSERT_TRUE(handler != nullptr); - - // Initialize handler with algorithm selection - // Stream context will be created automatically when HASH_INIT operation is - // executed - auto init_result = handler->InitializeContext(handler::InitializationParams{}); - ASSERT_TRUE(init_result.has_value()); - EXPECT_TRUE(init_result.has_value()); - - // Execute HASH_INIT operation to initialize streaming context - // HASH_INIT operation automatically creates the EVP context and transitions - // state - common::OperationIdentifier opId; - opId.operationActor = common::actors::OP_ACTOR_HASH_HANDLER; - opId.operationAction = provider::handler::hash_handler_operations::HASH_INIT; - - common::RequestParameters init_op; - auto init_stream_result = handler->Execute(opId, init_op); - ASSERT_TRUE(init_stream_result.has_value()) << "HASH_INIT execute failed"; - - // Update with first chunk - const std::string chunk1 = "Hello, "; - std::vector chunk1_buffer(chunk1.begin(), chunk1.end()); - score::cpp::span chunk1Buf{chunk1_buffer.data(), chunk1_buffer.size()}; - - // Update operation - common::RequestParameters update_op1; - update_op1 = {chunk1Buf}; - - opId.operationActor = common::actors::OP_ACTOR_HASH_HANDLER; - opId.operationAction = provider::handler::hash_handler_operations::HASH_UPDATE; - auto update_result1 = handler->Execute(opId, update_op1); - ASSERT_TRUE(update_result1.has_value()) << "HASH_UPDATE chunk1 failed"; - - // Update with second chunk - const std::string chunk2 = "World!"; - std::vector chunk2_buffer(chunk2.begin(), chunk2.end()); - score::cpp::span chunk2Buf{chunk2_buffer.data(), chunk2_buffer.size()}; - - common::RequestParameters update_op2; - update_op2 = {chunk2Buf}; - - opId.operationActor = common::actors::OP_ACTOR_HASH_HANDLER; - opId.operationAction = provider::handler::hash_handler_operations::HASH_UPDATE; - auto update_result2 = handler->Execute(opId, update_op2); - ASSERT_TRUE(update_result2.has_value()) << "HASH_UPDATE chunk2 failed"; - - // Finalize hash - constexpr std::size_t kSha256DigestLen{32U}; - std::vector output_buffer(kSha256DigestLen); - score::cpp::span outputBuf{output_buffer.data(), output_buffer.size()}; - - common::RequestParameters finalize_op; - finalize_op = {outputBuf}; - - opId.operationActor = common::actors::OP_ACTOR_HASH_HANDLER; - opId.operationAction = provider::handler::hash_handler_operations::HASH_FINALIZE; - auto finalize_result = handler->Execute(opId, finalize_op); - ASSERT_TRUE(finalize_result.has_value()) << "HASH_FINALIZE failed"; - - // Verify output is consistent with single-shot - const auto expected_hash = tests::utility::read_bin("score/tests/test_vectors/hash/sha256_hello_world.bin"); - ASSERT_EQ(expected_hash.size(), kSha256DigestLen); - ASSERT_EQ(output_buffer.size(), expected_hash.size()); - EXPECT_EQ(output_buffer, expected_hash) << "Streaming hash output does not match expected SHA256 hash"; + const auto& testData = GetParam(); + auto factory = provider_->GetCryptoHandlerFactory(); + ASSERT_NE(factory, nullptr); + + auto handlerResult = factory->CreateHandler("HASH", testData.algorithm); + ASSERT_TRUE(handlerResult.has_value()) << "Failed to create HASH/" << testData.algorithm; + auto hashHandler = handlerResult.value(); + ASSERT_NE(hashHandler, nullptr); + ASSERT_TRUE(hashHandler->InitializeContext(handler::InitializationParams{}).has_value()); + + const std::vector vectors{ + {"score/tests/test_vectors/hash/input_hello_world.bin", testData.hello_digest_path}, + {"score/tests/test_vectors/hash/input_complete_data.bin", testData.complete_digest_path}, + {"score/tests/test_vectors/hash/input_empty.bin", testData.empty_digest_path}, + {"score/tests/test_vectors/hash/input_abc.bin", testData.abc_digest_path}, + }; + + for (const auto& vector : vectors) + { + SCOPED_TRACE(std::string{testData.algorithm} + " / " + vector.input_path); + const auto input = tests::utility::read_bin(vector.input_path); + const auto expectedDigest = tests::utility::read_bin(vector.digest_path); + ASSERT_EQ(expectedDigest.size(), testData.digest_size); + + std::vector output(testData.digest_size, 0U); + common::RequestParameters request{ + score::cpp::span{input.data(), input.size()}, + score::cpp::span{output.data(), output.size()}, + }; + + const auto result = hashHandler->Execute(MakeHashOp(ops::HASH_SS), request); + ASSERT_TRUE(result.has_value()) << "Single-shot hash failed"; + ASSERT_EQ(result.value().size(), 1U); + const auto* bytesWritten = std::get_if(&result.value().front()); + ASSERT_NE(bytesWritten, nullptr); + EXPECT_EQ(*bytesWritten, testData.digest_size); + EXPECT_EQ(output, expectedDigest); + } + + common::RequestParameters digestSizeRequest{}; + const auto digestSizeResult = hashHandler->Execute(MakeHashOp(ops::HASH_GET_DIGEST_SIZE), digestSizeRequest); + ASSERT_TRUE(digestSizeResult.has_value()); + ASSERT_EQ(digestSizeResult.value().size(), 1U); + const auto* digestSize = std::get_if(&digestSizeResult.value().front()); + ASSERT_NE(digestSize, nullptr); + EXPECT_EQ(*digestSize, testData.digest_size); + + common::RequestParameters initRequest{}; + const auto empty = tests::utility::read_bin("score/tests/test_vectors/hash/input_empty.bin"); + const auto expectedEmpty = tests::utility::read_bin(testData.empty_digest_path); + ASSERT_TRUE(hashHandler->Execute(MakeHashOp(ops::HASH_INIT), initRequest).has_value()); + common::RequestParameters emptyUpdate{ + score::cpp::span{empty.data(), empty.size()}, + }; + ASSERT_TRUE(hashHandler->Execute(MakeHashOp(ops::HASH_UPDATE), emptyUpdate).has_value()); + std::vector emptyStreamingOutput(testData.digest_size, 0U); + common::RequestParameters emptyFinalize{ + score::cpp::span{emptyStreamingOutput.data(), emptyStreamingOutput.size()}, + }; + ASSERT_TRUE(hashHandler->Execute(MakeHashOp(ops::HASH_FINALIZE), emptyFinalize).has_value()); + EXPECT_EQ(emptyStreamingOutput, expectedEmpty); + + ASSERT_TRUE(hashHandler->Execute(MakeHashOp(ops::HASH_INIT), initRequest).has_value()); + std::vector zeroUpdateOutput(testData.digest_size, 0U); + common::RequestParameters zeroUpdateFinalize{ + score::cpp::span{zeroUpdateOutput.data(), zeroUpdateOutput.size()}, + }; + ASSERT_TRUE(hashHandler->Execute(MakeHashOp(ops::HASH_FINALIZE), zeroUpdateFinalize).has_value()); + EXPECT_EQ(zeroUpdateOutput, expectedEmpty) << "Init followed directly by Finalize must hash empty input"; + + const auto hello = tests::utility::read_bin("score/tests/test_vectors/hash/input_hello_world.bin"); + const auto expectedHello = tests::utility::read_bin(testData.hello_digest_path); + ASSERT_FALSE(hello.empty()); + ASSERT_EQ(expectedHello.size(), testData.digest_size); + + ASSERT_TRUE(hashHandler->Execute(MakeHashOp(ops::HASH_INIT), initRequest).has_value()); + + const auto split = static_cast(hello.size() / 2U); + const std::vector firstChunk{hello.begin(), hello.begin() + split}; + const std::vector secondChunk{hello.begin() + split, hello.end()}; + common::RequestParameters firstUpdate{ + score::cpp::span{firstChunk.data(), firstChunk.size()}, + }; + common::RequestParameters secondUpdate{ + score::cpp::span{secondChunk.data(), secondChunk.size()}, + }; + ASSERT_TRUE(hashHandler->Execute(MakeHashOp(ops::HASH_UPDATE), firstUpdate).has_value()); + ASSERT_TRUE(hashHandler->Execute(MakeHashOp(ops::HASH_UPDATE), secondUpdate).has_value()); + + std::vector streamingOutput(testData.digest_size, 0U); + common::RequestParameters finalizeRequest{ + score::cpp::span{streamingOutput.data(), streamingOutput.size()}, + }; + ASSERT_TRUE(hashHandler->Execute(MakeHashOp(ops::HASH_FINALIZE), finalizeRequest).has_value()); + EXPECT_EQ(streamingOutput, expectedHello); + + ASSERT_TRUE(hashHandler->Execute(MakeHashOp(ops::HASH_INIT), initRequest).has_value()); + ASSERT_TRUE(hashHandler->Execute(MakeHashOp(ops::HASH_UPDATE), firstUpdate).has_value()); + common::RequestParameters resetRequest{}; + ASSERT_TRUE(hashHandler->Execute(MakeHashOp(ops::HASH_RESET), resetRequest).has_value()); + + const auto complete = tests::utility::read_bin("score/tests/test_vectors/hash/input_complete_data.bin"); + const auto expectedComplete = tests::utility::read_bin(testData.complete_digest_path); + ASSERT_FALSE(complete.empty()); + ASSERT_TRUE(hashHandler->Execute(MakeHashOp(ops::HASH_INIT), initRequest).has_value()); + common::RequestParameters completeUpdate{ + score::cpp::span{complete.data(), complete.size()}, + }; + ASSERT_TRUE(hashHandler->Execute(MakeHashOp(ops::HASH_UPDATE), completeUpdate).has_value()); + std::vector resetOutput(testData.digest_size, 0U); + common::RequestParameters resetFinalize{ + score::cpp::span{resetOutput.data(), resetOutput.size()}, + }; + ASSERT_TRUE(hashHandler->Execute(MakeHashOp(ops::HASH_FINALIZE), resetFinalize).has_value()); + EXPECT_EQ(resetOutput, expectedComplete); + + std::vector undersizedOutput(testData.digest_size - 1U, 0U); + common::RequestParameters undersizedRequest{ + score::cpp::span{hello.data(), hello.size()}, + score::cpp::span{undersizedOutput.data(), undersizedOutput.size()}, + }; + const auto undersizedSingleShot = hashHandler->Execute(MakeHashOp(ops::HASH_SS), undersizedRequest); + ASSERT_FALSE(undersizedSingleShot.has_value()); + EXPECT_EQ(undersizedSingleShot.error(), common::DaemonErrorCode::kInsufficientBufferSize); + + // An undersized streaming output is a retryable caller error. Finalize + // must not discard the active digest operation. + ASSERT_TRUE(hashHandler->Execute(MakeHashOp(ops::HASH_INIT), initRequest).has_value()); + common::RequestParameters retryUpdate{ + score::cpp::span{hello.data(), hello.size()}, + }; + ASSERT_TRUE(hashHandler->Execute(MakeHashOp(ops::HASH_UPDATE), retryUpdate).has_value()); + common::RequestParameters undersizedFinalize{ + score::cpp::span{undersizedOutput.data(), undersizedOutput.size()}, + }; + const auto failedFinalize = hashHandler->Execute(MakeHashOp(ops::HASH_FINALIZE), undersizedFinalize); + ASSERT_FALSE(failedFinalize.has_value()); + EXPECT_EQ(failedFinalize.error(), common::DaemonErrorCode::kInsufficientBufferSize); + + std::vector retryOutput(testData.digest_size, 0U); + common::RequestParameters retryFinalize{ + score::cpp::span{retryOutput.data(), retryOutput.size()}, + }; + const auto retryResult = hashHandler->Execute(MakeHashOp(ops::HASH_FINALIZE), retryFinalize); + ASSERT_TRUE(retryResult.has_value()) << "Finalize retry failed after an undersized output buffer"; + EXPECT_EQ(retryOutput, expectedHello); } -/** - * @brief Test: Verify handler supports multiple hash algorithms - */ -TEST_F(ProviderHashTest, VerifySupportedHashAlgorithms) +TEST_F(ProviderHashTest, RetainsLegacyAlgorithmsForCompatibility) { - auto crypto_ops = provider_->GetCryptoHandlerFactory(); - ASSERT_TRUE(crypto_ops != nullptr); + auto factory = provider_->GetCryptoHandlerFactory(); + ASSERT_NE(factory, nullptr); - // Expected supported algorithms - std::vector expected_algorithms = {"SHA256", "SHA384", "SHA512", "SHA224", "SHA1", "MD5"}; - for (const auto& algo : expected_algorithms) + for (const char* algorithm : {"SHA224", "SHA1", "MD5"}) { - auto handler_result = crypto_ops->CreateHandler("HASH", algo); - EXPECT_TRUE(handler_result.has_value()) << "Algorithm " << algo << " not supported for HASH handler"; + EXPECT_TRUE(factory->CreateHandler("HASH", algorithm).has_value()) + << "Legacy algorithm unexpectedly removed: " << algorithm; } } -/** - * @brief Test: Verify CreateHandler returns error for unsupported handler - */ -TEST_F(ProviderHashTest, CreateHandlerUnsupportedHandler) +TEST_F(ProviderHashTest, RejectsUnsupportedHandlerAndAlgorithm) { - auto crypto_ops = provider_->GetCryptoHandlerFactory(); - ASSERT_TRUE(crypto_ops != nullptr); + auto factory = provider_->GetCryptoHandlerFactory(); + ASSERT_NE(factory, nullptr); - auto handler_result = crypto_ops->CreateHandler("UNSUPPORTED_HANDLER", "SHA256"); - EXPECT_FALSE(handler_result.has_value()) << "Should return error for unsupported handler"; + EXPECT_FALSE(factory->CreateHandler("UNSUPPORTED_HANDLER", "SHA256").has_value()); + EXPECT_FALSE(factory->CreateHandler("HASH", "UNSUPPORTED_ALGORITHM").has_value()); } -/** - * @brief Test: Verify CreateHandler returns error for unsupported algorithm - */ -TEST_F(ProviderHashTest, CreateHandlerUnsupportedAlgorithm) +TEST_F(ProviderHashTest, ReportsHashStreamStateViolations) { - auto crypto_ops = provider_->GetCryptoHandlerFactory(); - ASSERT_TRUE(crypto_ops != nullptr); + auto factory = provider_->GetCryptoHandlerFactory(); + ASSERT_NE(factory, nullptr); + auto handlerResult = factory->CreateHandler("HASH", "SHA256"); + ASSERT_TRUE(handlerResult.has_value()); + auto hashHandler = handlerResult.value(); + ASSERT_TRUE(hashHandler->InitializeContext(handler::InitializationParams{}).has_value()); + + const std::array input{0x42U}; + common::RequestParameters updateRequest{ + score::cpp::span{input.data(), input.size()}, + }; + const auto updateBeforeInit = hashHandler->Execute(MakeHashOp(ops::HASH_UPDATE), updateRequest); + ASSERT_FALSE(updateBeforeInit.has_value()); + EXPECT_EQ(updateBeforeInit.error(), common::DaemonErrorCode::kStreamNotInitialized); + + std::array output{}; + common::RequestParameters finalizeRequest{ + score::cpp::span{output.data(), output.size()}, + }; + const auto finalizeBeforeInit = hashHandler->Execute(MakeHashOp(ops::HASH_FINALIZE), finalizeRequest); + ASSERT_FALSE(finalizeBeforeInit.has_value()); + EXPECT_EQ(finalizeBeforeInit.error(), common::DaemonErrorCode::kStreamNotInitialized); + + common::RequestParameters initRequest{}; + ASSERT_TRUE(hashHandler->Execute(MakeHashOp(ops::HASH_INIT), initRequest).has_value()); + EXPECT_TRUE(hashHandler->Execute(MakeHashOp(ops::HASH_INIT), initRequest).has_value()) + << "Init on an active stream must restart it"; +} - auto handler_result = crypto_ops->CreateHandler("HASH", "UNSUPPORTED_ALGO"); - EXPECT_FALSE(handler_result.has_value()) << "Should return error for unsupported algorithm"; +TEST_F(ProviderHashTest, RejectsMalformedHashRequestShapesWithoutConsumingStream) +{ + auto factory = provider_->GetCryptoHandlerFactory(); + ASSERT_NE(factory, nullptr); + auto handlerResult = factory->CreateHandler("HASH", "SHA256"); + ASSERT_TRUE(handlerResult.has_value()); + auto hashHandler = handlerResult.value(); + ASSERT_TRUE(hashHandler->InitializeContext(handler::InitializationParams{}).has_value()); + + const std::array input{0x42U}; + std::array output{}; + + common::RequestParameters invalidInit{std::uint64_t{1U}}; + const auto invalidInitResult = hashHandler->Execute(MakeHashOp(ops::HASH_INIT), invalidInit); + ASSERT_FALSE(invalidInitResult.has_value()); + EXPECT_EQ(invalidInitResult.error(), common::DaemonErrorCode::kInvalidArgument); + + common::RequestParameters invalidSingleShot{ + score::cpp::span{input.data(), input.size()}, + score::cpp::span{output.data(), output.size()}, + std::uint64_t{1U}, + }; + const auto invalidSingleShotResult = hashHandler->Execute(MakeHashOp(ops::HASH_SS), invalidSingleShot); + ASSERT_FALSE(invalidSingleShotResult.has_value()); + EXPECT_EQ(invalidSingleShotResult.error(), common::DaemonErrorCode::kInvalidArgument); + + common::RequestParameters invalidDigestSize{std::uint64_t{1U}}; + const auto invalidDigestSizeResult = hashHandler->Execute(MakeHashOp(ops::HASH_GET_DIGEST_SIZE), invalidDigestSize); + ASSERT_FALSE(invalidDigestSizeResult.has_value()); + EXPECT_EQ(invalidDigestSizeResult.error(), common::DaemonErrorCode::kInvalidArgument); + + common::RequestParameters initRequest{}; + ASSERT_TRUE(hashHandler->Execute(MakeHashOp(ops::HASH_INIT), initRequest).has_value()); + + common::RequestParameters invalidUpdate{ + score::cpp::span{input.data(), input.size()}, + std::uint64_t{1U}, + }; + const auto invalidUpdateResult = hashHandler->Execute(MakeHashOp(ops::HASH_UPDATE), invalidUpdate); + ASSERT_FALSE(invalidUpdateResult.has_value()); + EXPECT_EQ(invalidUpdateResult.error(), common::DaemonErrorCode::kInvalidArgument); + + common::RequestParameters updateRequest{ + score::cpp::span{input.data(), input.size()}, + }; + ASSERT_TRUE(hashHandler->Execute(MakeHashOp(ops::HASH_UPDATE), updateRequest).has_value()); + + common::RequestParameters invalidReset{std::uint64_t{1U}}; + const auto invalidResetResult = hashHandler->Execute(MakeHashOp(ops::HASH_RESET), invalidReset); + ASSERT_FALSE(invalidResetResult.has_value()); + EXPECT_EQ(invalidResetResult.error(), common::DaemonErrorCode::kInvalidArgument); + + common::RequestParameters invalidFinalize{ + score::cpp::span{output.data(), output.size()}, + std::uint64_t{1U}, + }; + const auto invalidFinalizeResult = hashHandler->Execute(MakeHashOp(ops::HASH_FINALIZE), invalidFinalize); + ASSERT_FALSE(invalidFinalizeResult.has_value()); + EXPECT_EQ(invalidFinalizeResult.error(), common::DaemonErrorCode::kInvalidArgument); + + common::RequestParameters finalizeRequest{ + score::cpp::span{output.data(), output.size()}, + }; + EXPECT_TRUE(hashHandler->Execute(MakeHashOp(ops::HASH_FINALIZE), finalizeRequest).has_value()); } +TEST_F(ProviderHashTest, SupportsIndependentInterleavedHashContexts) +{ + auto factory = provider_->GetCryptoHandlerFactory(); + ASSERT_NE(factory, nullptr); + + auto sha256Result = factory->CreateHandler("HASH", "SHA256"); + auto sha384Result = factory->CreateHandler("HASH", "SHA384"); + ASSERT_TRUE(sha256Result.has_value()); + ASSERT_TRUE(sha384Result.has_value()); + auto sha256 = sha256Result.value(); + auto sha384 = sha384Result.value(); + ASSERT_TRUE(sha256->InitializeContext(handler::InitializationParams{}).has_value()); + ASSERT_TRUE(sha384->InitializeContext(handler::InitializationParams{}).has_value()); + + common::RequestParameters initRequest{}; + ASSERT_TRUE(sha256->Execute(MakeHashOp(ops::HASH_INIT), initRequest).has_value()); + ASSERT_TRUE(sha384->Execute(MakeHashOp(ops::HASH_INIT), initRequest).has_value()); + + const auto input = tests::utility::read_bin("score/tests/test_vectors/hash/input_hello_world.bin"); + ASSERT_FALSE(input.empty()); + const auto split = static_cast(input.size() / 2U); + const std::vector firstChunk{input.begin(), input.begin() + split}; + const std::vector secondChunk{input.begin() + split, input.end()}; + common::RequestParameters firstUpdate{ + score::cpp::span{firstChunk.data(), firstChunk.size()}, + }; + common::RequestParameters secondUpdate{ + score::cpp::span{secondChunk.data(), secondChunk.size()}, + }; + + ASSERT_TRUE(sha256->Execute(MakeHashOp(ops::HASH_UPDATE), firstUpdate).has_value()); + ASSERT_TRUE(sha384->Execute(MakeHashOp(ops::HASH_UPDATE), firstUpdate).has_value()); + ASSERT_TRUE(sha384->Execute(MakeHashOp(ops::HASH_UPDATE), secondUpdate).has_value()); + ASSERT_TRUE(sha256->Execute(MakeHashOp(ops::HASH_UPDATE), secondUpdate).has_value()); + + std::vector sha256Output(32U, 0U); + std::vector sha384Output(48U, 0U); + common::RequestParameters sha256Finalize{ + score::cpp::span{sha256Output.data(), sha256Output.size()}, + }; + common::RequestParameters sha384Finalize{ + score::cpp::span{sha384Output.data(), sha384Output.size()}, + }; + ASSERT_TRUE(sha384->Execute(MakeHashOp(ops::HASH_FINALIZE), sha384Finalize).has_value()); + ASSERT_TRUE(sha256->Execute(MakeHashOp(ops::HASH_FINALIZE), sha256Finalize).has_value()); + + EXPECT_EQ(sha256Output, tests::utility::read_bin("score/tests/test_vectors/hash/sha256_hello_world.bin")); + EXPECT_EQ(sha384Output, tests::utility::read_bin("score/tests/test_vectors/hash/sha384_hello_world.bin")); +} + +TEST(OpenSslHashHandlerErrorTest, DigestUpdateFailureAbortsStreamAndAllowsReuse) +{ + using HashExecutor = provider::score_provider::operations::hash::HashExecutor; + using OpenSslHashHandler = provider::score_provider::openssl::handler::OpenSslHashHandler; + + OpenSslHashHandler hashHandler{std::make_unique(), "SHA256", &FailingDigestUpdate}; + ASSERT_TRUE(hashHandler.InitializeContext(handler::InitializationParams{}).has_value()); + + const std::array input{0x42U}; + common::RequestParameters initRequest{}; + ASSERT_TRUE(hashHandler.Execute(MakeHashOp(ops::HASH_INIT), initRequest).has_value()); + + common::RequestParameters updateRequest{ + score::cpp::span{input.data(), input.size()}, + }; + const auto failedUpdate = hashHandler.Execute(MakeHashOp(ops::HASH_UPDATE), updateRequest); + ASSERT_FALSE(failedUpdate.has_value()); + EXPECT_EQ(failedUpdate.error(), common::DaemonErrorCode::kAlgorithmExecutionFailed); + EXPECT_EQ(hashHandler.GetOperationState(), common::StreamOperationState::IDLE); + + ASSERT_TRUE(hashHandler.Execute(MakeHashOp(ops::HASH_INIT), initRequest).has_value()); + std::array output{}; + common::RequestParameters finalizeRequest{ + score::cpp::span{output.data(), output.size()}, + }; + ASSERT_TRUE(hashHandler.Execute(MakeHashOp(ops::HASH_FINALIZE), finalizeRequest).has_value()); + const std::vector actualDigest{output.begin(), output.end()}; + EXPECT_EQ(actualDigest, tests::utility::read_bin("score/tests/test_vectors/hash/sha256_empty.bin")); +} + +INSTANTIATE_TEST_SUITE_P( + BaselibsMigrationAlgorithms, + ProviderHashAlgorithmTest, + ::testing::Values(HashAlgorithmTestData{"SHA256", + 32U, + "score/tests/test_vectors/hash/sha256_hello_world.bin", + "score/tests/test_vectors/hash/sha256_complete_data.bin", + "score/tests/test_vectors/hash/sha256_empty.bin", + "score/tests/test_vectors/hash/sha256_abc.bin"}, + HashAlgorithmTestData{"SHA384", + 48U, + "score/tests/test_vectors/hash/sha384_hello_world.bin", + "score/tests/test_vectors/hash/sha384_complete_data.bin", + "score/tests/test_vectors/hash/sha384_empty.bin", + "score/tests/test_vectors/hash/sha384_abc.bin"}, + HashAlgorithmTestData{"SHA512", + 64U, + "score/tests/test_vectors/hash/sha512_hello_world.bin", + "score/tests/test_vectors/hash/sha512_complete_data.bin", + "score/tests/test_vectors/hash/sha512_empty.bin", + "score/tests/test_vectors/hash/sha512_abc.bin"}), + [](const ::testing::TestParamInfo& info) { + return info.param.algorithm; + }); + } // namespace int main(int argc, char** argv) { ::testing::InitGoogleTest(&argc, argv); + ::testing::AddGlobalTestEnvironment(new OpenSslProviderEnvironment{}); return RUN_ALL_TESTS(); } diff --git a/score/crypto/tests/grpc_control_plane/test_control_plane.cpp b/score/crypto/tests/grpc_control_plane/test_control_plane.cpp index 1e6aba5ce..79c5f2ede 100644 --- a/score/crypto/tests/grpc_control_plane/test_control_plane.cpp +++ b/score/crypto/tests/grpc_control_plane/test_control_plane.cpp @@ -184,6 +184,24 @@ class ControlPlaneTest : public ::testing::Test std::thread _server_thread; }; +TEST(ControlProtocolTest, PreservesApiErrorCodeInOperationResponse) +{ + namespace protocol = score::crypto::daemon::control_plane::protocol; + const protocol::OperationIdentifier operation_id{score::crypto::test::dummyActorA, + score::crypto::test::dummyActionA}; + const auto api_error = score::crypto::MakeError(score::crypto::CryptoErrorCode::kUnsupportedAlgorithm); + + const auto response = protocol::OperationResponseBuilder().operation(operation_id).return_error(api_error).build(); + ASSERT_TRUE(response.has_value()); + + protocol::ControlResponseValidator validator(response.value()); + validator.expectOperation(operation_id).expectSuccess(); + + EXPECT_FALSE(validator.isValid()); + ASSERT_TRUE(validator.getErrorCode().has_value()); + EXPECT_EQ(validator.getErrorCode().value(), score::crypto::CryptoErrorCode::kUnsupportedAlgorithm); +} + TEST_F(ControlPlaneTest, Connection_SendRequest) { auto endpoint = "unix://" + _socket_path; diff --git a/score/tests/integration_tests/score_api_hash_test.cpp b/score/tests/integration_tests/score_api_hash_test.cpp index c7d2b873f..921483aa3 100644 --- a/score/tests/integration_tests/score_api_hash_test.cpp +++ b/score/tests/integration_tests/score_api_hash_test.cpp @@ -11,7 +11,7 @@ * SPDX-License-Identifier: Apache-2.0 ********************************************************************************/ -/// @file hashing_example.cpp +/// @file score_api_hash_test.cpp /// @brief Demonstrates SHA-256 hashing using the score::crypto API. /// /// Shows both streaming (Init → Update* → Finalize) and single-shot modes. @@ -25,6 +25,7 @@ #include +#include #include #include #include @@ -53,14 +54,15 @@ constexpr std::size_t kInBandThreshold = 32U; // Parameterized Test Data struct HashTestData { - std::string_view test_case_name; + std::string test_case_name; std::optional provider_type; - std::string_view algorithm; + std::string algorithm; size_t expected_out_data_size; - std::string_view in_data_relative_path; - std::string_view expected_out_data_relative_path; - std::string_view in_data_alternative_relative_path; - std::string_view expected_out_data_alternative_relative_path; + std::string in_data_relative_path; + std::string expected_out_data_relative_path; + std::string in_data_alternative_relative_path; + std::string expected_out_data_alternative_relative_path; + std::string expected_empty_data_relative_path; }; class ParameterizedHashTest : public ::testing::TestWithParam @@ -135,15 +137,15 @@ TEST_P(ParameterizedHashTest, HashingTest) auto init_result = hash->Init(); ASSERT_TRUE(init_result.has_value()) << "Init failed"; - hash->Update({chunk1_buffer.data(), chunk1_buffer.size()}); - hash->Update({chunk2_buffer.data(), chunk2_buffer.size()}); + ASSERT_TRUE(hash->Update({chunk1_buffer.data(), chunk1_buffer.size()}).has_value()); + ASSERT_TRUE(hash->Update({chunk2_buffer.data(), chunk2_buffer.size()}).has_value()); auto finalize_result = hash->Finalize({digest.data(), digest.size()}); ASSERT_TRUE(finalize_result.has_value()) << "Finalize failed"; print_hex("Streaming", digest, finalize_result.value()); ASSERT_EQ(digest.size(), expected_hash.size()); - EXPECT_EQ(digest, expected_hash) << "Hash output does not match expected SHA256 hash"; + EXPECT_EQ(digest, expected_hash) << "Streaming hash output does not match the expected digest"; // 5. Single-shot hash (equivalent to Init + Update + Finalize) std::vector digest2(expected_out_data_size, 0); @@ -153,8 +155,9 @@ TEST_P(ParameterizedHashTest, HashingTest) ASSERT_TRUE(single_result.has_value()) << "SingleShot failed"; print_hex("SingleShot", digest2, single_result.value()); - ASSERT_EQ(digest.size(), expected_hash.size()); - EXPECT_EQ(digest, expected_hash) << "Hash output does not match expected SHA256 hash"; + ASSERT_EQ(digest2.size(), expected_hash.size()); + EXPECT_EQ(digest2, expected_hash) << "Single-shot hash output does not match the expected digest"; + EXPECT_EQ(digest2, digest) << "Streaming and single-shot hash outputs differ"; // 6. Context reuse via Reset() // Reset() returns the context to its post-construction state — the key @@ -175,7 +178,7 @@ TEST_P(ParameterizedHashTest, HashingTest) print_hex("Reused-ctx", digest3, finalize3.value()); ASSERT_EQ(digest3.size(), expected_hash_alternative.size()); - EXPECT_EQ(digest3, expected_hash_alternative) << "Hash output does not match expected SHA256 hash"; + EXPECT_EQ(digest3, expected_hash_alternative) << "Reused context produced an unexpected digest"; // Reset() also works mid-stream to abort and restart ASSERT_TRUE(hash->Init()); @@ -190,10 +193,205 @@ TEST_P(ParameterizedHashTest, HashingTest) ASSERT_EQ(digest4.size(), expected_hash_alternative.size()); ASSERT_EQ(expected_hash_alternative.size(), expected_out_data_size); + EXPECT_EQ(digest4, expected_hash_alternative) << "Mid-stream reset produced an unexpected digest"; // 7. Query digest size auto digest_size = hash->GetDigestSize(); EXPECT_EQ(digest_size, expected_out_data_size) << "Unexpected Digest size of: " << digest_size; + + // 8. Reject caller-owned output buffers smaller than the algorithm digest. + std::vector undersized_digest(expected_out_data_size - 1U, 0U); + const auto undersized_single_shot = hash->SingleShot(input_buffer, undersized_digest); + ASSERT_FALSE(undersized_single_shot.has_value()); + EXPECT_EQ(*undersized_single_shot.error(), + static_cast(CryptoErrorCode::kInsufficientBufferSize)); + + // An undersized Finalize() is retryable without reinitializing or replaying + // the input. This must behave identically for OpenSSL and PKCS#11. + ASSERT_TRUE(hash->Init().has_value()); + ASSERT_TRUE(hash->Update(input_buffer).has_value()); + const auto undersized_finalize = hash->Finalize(undersized_digest); + ASSERT_FALSE(undersized_finalize.has_value()); + EXPECT_EQ(*undersized_finalize.error(), + static_cast(CryptoErrorCode::kInsufficientBufferSize)); + + std::vector retry_digest(expected_out_data_size, 0U); + const auto retry_finalize = hash->Finalize(retry_digest); + ASSERT_TRUE(retry_finalize.has_value()) << "Finalize retry failed after an undersized output buffer"; + EXPECT_EQ(retry_digest, expected_hash); + + // 9. Empty input is valid in both single-shot and zero-update streaming modes. + const auto empty_input = tests::utility::read_bin(GetTestVectorPath("/hash/input_empty.bin")); + ASSERT_TRUE(empty_input.empty()); + const auto expected_empty_hash = + tests::utility::read_bin(GetTestVectorPath(test_data.expected_empty_data_relative_path)); + ASSERT_EQ(expected_empty_hash.size(), expected_out_data_size); + + ASSERT_TRUE(hash->Reset().has_value()); + std::vector empty_single_shot_digest(expected_out_data_size, 0U); + const auto empty_single_shot = hash->SingleShot(empty_input, empty_single_shot_digest); + ASSERT_TRUE(empty_single_shot.has_value()) << "Empty SingleShot failed"; + EXPECT_EQ(empty_single_shot_digest, expected_empty_hash); + + ASSERT_TRUE(hash->Init().has_value()); + std::vector zero_update_digest(expected_out_data_size, 0U); + const auto zero_update_finalize = hash->Finalize(zero_update_digest); + ASSERT_TRUE(zero_update_finalize.has_value()) << "Init followed directly by Finalize failed"; + EXPECT_EQ(zero_update_digest, expected_empty_hash); +} + +TEST_F(HashExampleTest, PreservesArbitraryBinaryInputAcrossTheClientDaemonBoundary) +{ + CryptoStackConfig stack_config; + stack_config.SetConnectionEndpoint(kControlSocketEndpoint); + + auto stack_result = CreateCryptoStack(stack_config); + ASSERT_TRUE(stack_result.has_value()) << "Failed to create crypto stack"; + auto& stack = stack_result.value(); + + auto ctx_result = stack->CreateCryptoContext(); + ASSERT_TRUE(ctx_result.has_value()) << "Failed to create crypto context"; + auto& ctx = ctx_result.value(); + + HashContextConfig hash_config; + hash_config.SetAlgorithm("SHA256"); + auto hash_result = ctx->CreateHashContext(hash_config); + ASSERT_TRUE(hash_result.has_value()) << "Failed to create SHA-256 context"; + auto& hash = hash_result.value(); + + const std::array input{0x00U, 0x01U, 0x7fU, 0x80U, 0xffU, 0x00U, 0x42U}; + const std::array expected{ + 0x81U, 0x84U, 0x56U, 0x98U, 0x34U, 0xaeU, 0x09U, 0xc8U, 0x6fU, 0x52U, 0xf8U, 0x46U, 0x62U, 0xbeU, 0x23U, 0x13U, + 0x97U, 0x18U, 0xc4U, 0xacU, 0x9bU, 0x48U, 0x99U, 0x54U, 0xe4U, 0xacU, 0x6fU, 0x7eU, 0x18U, 0x37U, 0x82U, 0xfaU, + }; + std::array output{}; + + const auto result = hash->SingleShot(input, output); + ASSERT_TRUE(result.has_value()) << "Binary SingleShot failed"; + EXPECT_EQ(result.value(), output.size()); + EXPECT_EQ(output, expected); +} + +TEST_F(HashExampleTest, ReportsUnsupportedHashAlgorithmAtContextCreation) +{ + CryptoStackConfig stack_config; + stack_config.SetConnectionEndpoint(kControlSocketEndpoint); + + auto stack_result = CreateCryptoStack(stack_config); + ASSERT_TRUE(stack_result.has_value()); + auto ctx_result = stack_result.value()->CreateCryptoContext(); + ASSERT_TRUE(ctx_result.has_value()); + + HashContextConfig hash_config; + hash_config.SetAlgorithm("UNSUPPORTED_ALGORITHM"); + const auto hash_result = ctx_result.value()->CreateHashContext(hash_config); + ASSERT_FALSE(hash_result.has_value()); + EXPECT_EQ(*hash_result.error(), static_cast(CryptoErrorCode::kUnsupportedAlgorithm)); +} + +TEST_F(HashExampleTest, RejectsNonProviderResourceForExplicitSelection) +{ + CryptoStackConfig stack_config; + stack_config.SetConnectionEndpoint(kControlSocketEndpoint); + + auto stack_result = CreateCryptoStack(stack_config); + ASSERT_TRUE(stack_result.has_value()); + auto ctx_result = stack_result.value()->CreateCryptoContext(); + ASSERT_TRUE(ctx_result.has_value()); + + CryptoResourceId key_resource{}; + key_resource.id = 1U; + key_resource.type = ResourceType::kKey; + key_resource.primary_provider = 0U; + + HashContextConfig hash_config; + hash_config.SetAlgorithm("SHA256").SetProvider(key_resource); + const auto hash_result = ctx_result.value()->CreateHashContext(hash_config); + ASSERT_FALSE(hash_result.has_value()); + EXPECT_EQ(*hash_result.error(), static_cast(CryptoErrorCode::kInvalidResourceType)); +} + +TEST_F(HashExampleTest, ResolvesAndUsesExplicitProvider) +{ + CryptoStackConfig stack_config; + stack_config.SetConnectionEndpoint(kControlSocketEndpoint); + + auto stack_result = CreateCryptoStack(stack_config); + ASSERT_TRUE(stack_result.has_value()); + auto ctx_result = stack_result.value()->CreateCryptoContext(); + ASSERT_TRUE(ctx_result.has_value()); + + const auto provider_result = ctx_result.value()->ResolveResource("OPENSSL", ResourceType::kProvider); + ASSERT_TRUE(provider_result.has_value()); + EXPECT_EQ(provider_result.value().type, ResourceType::kProvider); + + HashContextConfig hash_config; + hash_config.SetAlgorithm("SHA256").SetProvider(provider_result.value()); + const auto hash_result = ctx_result.value()->CreateHashContext(hash_config); + EXPECT_TRUE(hash_result.has_value()); +} + +TEST_F(HashExampleTest, ReportsMissingExplicitProvider) +{ + CryptoStackConfig stack_config; + stack_config.SetConnectionEndpoint(kControlSocketEndpoint); + + auto stack_result = CreateCryptoStack(stack_config); + ASSERT_TRUE(stack_result.has_value()); + auto ctx_result = stack_result.value()->CreateCryptoContext(); + ASSERT_TRUE(ctx_result.has_value()); + + const auto provider_result = + ctx_result.value()->ResolveResource("PROVIDER_THAT_DOES_NOT_EXIST", ResourceType::kProvider); + ASSERT_FALSE(provider_result.has_value()); + EXPECT_EQ(*provider_result.error(), static_cast(CryptoErrorCode::kProviderNotAvailable)); +} + +TEST_F(HashExampleTest, ReportsStreamStateErrorsAndRecoversWithReset) +{ + CryptoStackConfig stack_config; + stack_config.SetConnectionEndpoint(kControlSocketEndpoint); + + auto stack_result = CreateCryptoStack(stack_config); + ASSERT_TRUE(stack_result.has_value()); + auto ctx_result = stack_result.value()->CreateCryptoContext(); + ASSERT_TRUE(ctx_result.has_value()); + + HashContextConfig hash_config; + hash_config.SetAlgorithm("SHA256"); + auto hash_result = ctx_result.value()->CreateHashContext(hash_config); + ASSERT_TRUE(hash_result.has_value()); + auto& hash = hash_result.value(); + + const std::array input{0x42U}; + std::array output{}; + + const auto update_before_init = hash->Update(input); + ASSERT_FALSE(update_before_init.has_value()); + EXPECT_EQ(*update_before_init.error(), + static_cast(CryptoErrorCode::kStreamNotInitialized)); + + const auto finalize_before_init = hash->Finalize(output); + ASSERT_FALSE(finalize_before_init.has_value()); + EXPECT_EQ(*finalize_before_init.error(), + static_cast(CryptoErrorCode::kStreamNotInitialized)); + + ASSERT_TRUE(hash->Init().has_value()); + ASSERT_TRUE(hash->Update(input).has_value()); + EXPECT_TRUE(hash->Init().has_value()) << "Init on an active stream must discard the previous input"; + + const auto single_shot_while_active = hash->SingleShot(input, output); + ASSERT_FALSE(single_shot_while_active.has_value()); + EXPECT_EQ(*single_shot_while_active.error(), + static_cast(CryptoErrorCode::kInvalidOperation)); + + const auto restarted_finalize = hash->Finalize(output); + ASSERT_TRUE(restarted_finalize.has_value()); + const std::vector restarted_digest{output.begin(), output.end()}; + EXPECT_EQ(restarted_digest, tests::utility::read_bin(GetTestVectorPath("/hash/sha256_empty.bin"))); + + EXPECT_TRUE(hash->SingleShot(input, output).has_value()); + EXPECT_TRUE(hash->Reset().has_value()); } /// @brief Demonstrates three SHM transport routing paths using SHA-256 (in-band) @@ -325,60 +523,60 @@ TEST_F(HashExampleTest, MemoryAllocationStrategyComparison) EXPECT_EQ(digest_pool, digest_bulk) << "Pool and bulk SHA-512 must match for identical input"; } -constexpr std::string_view kAlgSha256 = "SHA256"; -constexpr std::size_t kSha256DigestSize = 32; - -constexpr std::string_view kInDataRelativePath = "/hash/input_hello_world.bin"; -constexpr std::string_view kSha256OutDataRelativePath = "/hash/sha256_hello_world.bin"; -constexpr std::string_view kInDataAlternativeRelativePath = "/hash/input_complete_data.bin"; -constexpr std::string_view kSha256OutDataAlternativeRelativePath = "/hash/sha256_complete_data.bin"; - -// TODO: Daemon expects SHA256 here we planned to use SHA-256 -// Either we find the common standard or allow variations, which we would need to re-map +HashTestData MakeHashTestData(const std::string& algorithm, + const std::string& file_prefix, + const std::size_t digest_size, + const std::optional provider_type, + const std::string& provider_name) +{ + const std::string vector_root = "/hash/"; + return HashTestData{algorithm + "_" + provider_name, + provider_type, + algorithm, + digest_size, + vector_root + "input_hello_world.bin", + vector_root + file_prefix + "_hello_world.bin", + vector_root + "input_complete_data.bin", + vector_root + file_prefix + "_complete_data.bin", + vector_root + file_prefix + "_empty.bin"}; +} -INSTANTIATE_TEST_SUITE_P(SelectionOfProviderType, - ParameterizedHashTest, - ::testing::Values(HashTestData{"SHA256_NoProviderSelection", - std::nullopt, - kAlgSha256, - kSha256DigestSize, - kInDataRelativePath, - kSha256OutDataRelativePath, - kInDataAlternativeRelativePath, - kSha256OutDataAlternativeRelativePath}, - HashTestData{"SHA256_DefaultProviderType", - ProviderType::kDefault, - kAlgSha256, - kSha256DigestSize, - kInDataRelativePath, - kSha256OutDataRelativePath, - kInDataAlternativeRelativePath, - kSha256OutDataAlternativeRelativePath} +std::vector MakeHashTestDataSet() +{ + std::vector test_data; + const auto add_algorithm = + [&test_data](const std::string& algorithm, const std::string& file_prefix, const std::size_t digest_size) { + test_data.emplace_back( + MakeHashTestData(algorithm, file_prefix, digest_size, std::nullopt, "NoProviderSelection")); + test_data.emplace_back( + MakeHashTestData(algorithm, file_prefix, digest_size, ProviderType::kDefault, "DefaultProviderType")); + test_data.emplace_back(MakeHashTestData( + algorithm, file_prefix, digest_size, ProviderType::kHardwarePreferred, "HardwarePreferred")); + test_data.emplace_back(MakeHashTestData( + algorithm, file_prefix, digest_size, ProviderType::kSoftwarePreferred, "SoftwarePreferred")); #ifdef SCORE_CRYPTO_SOFTWARE_BACKEND_ENABLED - , - HashTestData{"SHA256_SoftwareProvider", - ProviderType::kSoftware, - kAlgSha256, - kSha256DigestSize, - kInDataRelativePath, - kSha256OutDataRelativePath, - kInDataAlternativeRelativePath, - kSha256OutDataAlternativeRelativePath} + test_data.emplace_back( + MakeHashTestData(algorithm, file_prefix, digest_size, ProviderType::kSoftware, "SoftwareProvider")); #endif #ifdef SCORE_CRYPTO_HARDWARE_BACKEND_ENABLED - , - HashTestData{"SHA256_HardwareProvider", - ProviderType::kHardware, - kAlgSha256, - kSha256DigestSize, - kInDataRelativePath, - kSha256OutDataRelativePath, - kInDataAlternativeRelativePath, - kSha256OutDataAlternativeRelativePath} + test_data.emplace_back( + MakeHashTestData(algorithm, file_prefix, digest_size, ProviderType::kHardware, "HardwareProvider")); #endif - ), + }; + + add_algorithm("SHA256", "sha256", 32U); + add_algorithm("SHA384", "sha384", 48U); + add_algorithm("SHA512", "sha512", 64U); + return test_data; +} + +const std::vector kHashTestData = MakeHashTestDataSet(); + +INSTANTIATE_TEST_SUITE_P(SelectionOfProviderType, + ParameterizedHashTest, + ::testing::ValuesIn(kHashTestData), [](const testing::TestParamInfo& info) { - return std::string{info.param.test_case_name}; + return info.param.test_case_name; }); } // namespace diff --git a/score/tests/test_vectors/hash/input_abc.bin b/score/tests/test_vectors/hash/input_abc.bin new file mode 100644 index 000000000..f2ba8f84a --- /dev/null +++ b/score/tests/test_vectors/hash/input_abc.bin @@ -0,0 +1 @@ +abc \ No newline at end of file diff --git a/score/tests/test_vectors/hash/input_empty.bin b/score/tests/test_vectors/hash/input_empty.bin new file mode 100644 index 000000000..e69de29bb diff --git a/score/tests/test_vectors/hash/reference.md b/score/tests/test_vectors/hash/reference.md index 670b75570..556075661 100644 --- a/score/tests/test_vectors/hash/reference.md +++ b/score/tests/test_vectors/hash/reference.md @@ -17,6 +17,8 @@ | File | Contents | Bytes | |------|----------|-------| +| `input_empty.bin` | Empty byte sequence | 0 | +| `input_abc.bin` | Raw ASCII `abc` (no null terminator or newline) | 3 | | `input_hello_world.bin` | Raw ASCII `Hello, World!` (no null terminator) | 13 | | `input_complete_data.bin` | Raw ASCII `complete_data` (no null terminator) | 13 | @@ -24,12 +26,23 @@ | File | Algorithm | Input | Expected Hex | |------|-----------|-------|-------------| +| `sha256_empty.bin` | SHA-256 | `input_empty.bin` | `e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855` | +| `sha256_abc.bin` | SHA-256 | `input_abc.bin` | `ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad` | | `sha256_hello_world.bin` | SHA-256 | `input_hello_world.bin` | `dffd6021bb2bd5b0af676290809ec3a53191dd81c7f70a4b28688a362182986f` | -| `sha384_hello_world.bin` | SHA-384 | `input_hello_world.bin` | `5485cc9b3365b4305dfb4e8337e0a598a574f8242bf17289e0dd6c20a3cd44a089de16ab4ab308f63e44b1170eb5f515` | | `sha256_complete_data.bin` | SHA-256 | `input_complete_data.bin` | `5757e96ca92c35872e34c6d38e457e78a39bd78ee2d96d3050aee2a54fc2a6cf` | +| `sha384_empty.bin` | SHA-384 | `input_empty.bin` | `38b060a751ac96384cd9327eb1b1e36a21fdb71114be07434c0cc7bf63f6e1da274edebfe76f65fbd51ad2f14898b95b` | +| `sha384_abc.bin` | SHA-384 | `input_abc.bin` | `cb00753f45a35e8bb5a03d699ac65007272c32ab0eded1631a8b605a43ff5bed8086072ba1e7cc2358baeca134c825a7` | +| `sha384_hello_world.bin` | SHA-384 | `input_hello_world.bin` | `5485cc9b3365b4305dfb4e8337e0a598a574f8242bf17289e0dd6c20a3cd44a089de16ab4ab308f63e44b1170eb5f515` | +| `sha384_complete_data.bin` | SHA-384 | `input_complete_data.bin` | `04efb41b79a3675f843df1e1b52e2845745129d0286abf10021d298c50fc7c3bc1e1225a8f7fa0a0477956fd3efed06e` | +| `sha512_empty.bin` | SHA-512 | `input_empty.bin` | `cf83e1357eefb8bdf1542850d66d8007d620e4050b5715dc83f4a921d36ce9ce47d0d13c5d85f2b0ff8318d2877eec2f63b931bd47417a81a538327af927da3e` | +| `sha512_abc.bin` | SHA-512 | `input_abc.bin` | `ddaf35a193617abacc417349ae20413112e6fa4e89a97ea20a9eeee64b55d39a2192992a274fc1a836ba3c23a3feebbd454d4423643ce80e2a9ac94fa54ca49f` | +| `sha512_hello_world.bin` | SHA-512 | `input_hello_world.bin` | `374d794a95cdcfd8b35993185fef9ba368f160d8daf432d08ba9f1ed1e5abe6cc69291e0fa2fe0006a52570ef18c19def4e617c33ce52ef0a6e5fbe318cb0387` | +| `sha512_complete_data.bin` | SHA-512 | `input_complete_data.bin` | `be92cc1d8780572b654339f1fb133eb9383d1c924c3751f0f745361318ed41a6387df1674d0c7213af592308b714197327c01cca21301f6185d287190f888ffc` | ## Conventions - Binary files contain raw digest bytes with **no** null terminator, consistent with the existing ECB-AES128 block-cipher test vector convention. -- Digests were generated with Python `hashlib` and verified against OpenSSL CLI. +- Digests were generated as binary files with `openssl dgst -sha{256,384,512} -binary`. +- The hexadecimal values were independently verified with `shasum -a 256`, + `shasum -a 384`, and `shasum -a 512`. diff --git a/score/tests/test_vectors/hash/sha256_abc.bin b/score/tests/test_vectors/hash/sha256_abc.bin new file mode 100644 index 0000000000000000000000000000000000000000..5b258ccd0ff4e3429065777b4e0b86868a2a5281 GIT binary patch literal 32 ocmdm0A-2Dt@%$@CM~8c{>y(r?FefgaCSEmXi@^WHPYj}K0rTAslK=n! literal 0 HcmV?d00001 diff --git a/score/tests/test_vectors/hash/sha256_empty.bin b/score/tests/test_vectors/hash/sha256_empty.bin new file mode 100644 index 000000000..4811487ee --- /dev/null +++ b/score/tests/test_vectors/hash/sha256_empty.bin @@ -0,0 +1 @@ +Bșo$'AdLxRU \ No newline at end of file diff --git a/score/tests/test_vectors/hash/sha384_abc.bin b/score/tests/test_vectors/hash/sha384_abc.bin new file mode 100644 index 0000000000000000000000000000000000000000..5a78468cb81f0e45a809da836fcc73a638466719 GIT binary patch literal 48 zcmV-00MGx+0Chh_qh5=(pgn1t#!v?*EHbMO-qB+ki(pzq|6A>Vh6gL5=gcEmy6mAe G$R(!+TNY9P literal 0 HcmV?d00001 diff --git a/score/tests/test_vectors/hash/sha384_complete_data.bin b/score/tests/test_vectors/hash/sha384_complete_data.bin new file mode 100644 index 000000000..19b410840 --- /dev/null +++ b/score/tests/test_vectors/hash/sha384_complete_data.bin @@ -0,0 +1 @@ +yg_=.(EtQ)(j)P|;"ZGyV>n \ No newline at end of file diff --git a/score/tests/test_vectors/hash/sha384_empty.bin b/score/tests/test_vectors/hash/sha384_empty.bin new file mode 100644 index 000000000..5c805201a --- /dev/null +++ b/score/tests/test_vectors/hash/sha384_empty.bin @@ -0,0 +1 @@ +8`Q8L2~j!CL ǿc'N޿oeH[ \ No newline at end of file diff --git a/score/tests/test_vectors/hash/sha512_abc.bin b/score/tests/test_vectors/hash/sha512_abc.bin new file mode 100644 index 000000000..cf0b8c908 --- /dev/null +++ b/score/tests/test_vectors/hash/sha512_abc.bin @@ -0,0 +1,2 @@ +ݯ5azAsI A1N~ +KUӚ!*'O6<#EMD#d<*OL \ No newline at end of file diff --git a/score/tests/test_vectors/hash/sha512_complete_data.bin b/score/tests/test_vectors/hash/sha512_complete_data.bin new file mode 100644 index 000000000..f988e5926 --- /dev/null +++ b/score/tests/test_vectors/hash/sha512_complete_data.bin @@ -0,0 +1 @@ +W+eC9>8=L7QE6A8}gM rY#s'!0a҇ \ No newline at end of file diff --git a/score/tests/test_vectors/hash/sha512_empty.bin b/score/tests/test_vectors/hash/sha512_empty.bin new file mode 100644 index 000000000..be664a711 --- /dev/null +++ b/score/tests/test_vectors/hash/sha512_empty.bin @@ -0,0 +1 @@ +σ5~︽T(Pm  W܃!lG<]҇~/c1GAz82z'> \ No newline at end of file diff --git a/score/tests/test_vectors/hash/sha512_hello_world.bin b/score/tests/test_vectors/hash/sha512_hello_world.bin new file mode 100644 index 0000000000000000000000000000000000000000..3b72cf57d4654c6b4218ccacb70c029ce2c80b24 GIT binary patch literal 64 zcmV-G0Kfk?O?gU{&Cl4gS(6xF@0+7&@nG26^fJ(ksqyU|TE1+?l9Ay0FW>-bQdbV~ Wj2Yha<`=^} Date: Tue, 15 Sep 2026 18:34:19 -0400 Subject: [PATCH 2/2] fix: pkcs11 provider openssl test error --- .../integration_tests/score_api_hash_test.cpp | 29 ++++++++++++++----- 1 file changed, 22 insertions(+), 7 deletions(-) diff --git a/score/tests/integration_tests/score_api_hash_test.cpp b/score/tests/integration_tests/score_api_hash_test.cpp index 921483aa3..96cfa0340 100644 --- a/score/tests/integration_tests/score_api_hash_test.cpp +++ b/score/tests/integration_tests/score_api_hash_test.cpp @@ -321,14 +321,29 @@ TEST_F(HashExampleTest, ResolvesAndUsesExplicitProvider) auto ctx_result = stack_result.value()->CreateCryptoContext(); ASSERT_TRUE(ctx_result.has_value()); - const auto provider_result = ctx_result.value()->ResolveResource("OPENSSL", ResourceType::kProvider); - ASSERT_TRUE(provider_result.has_value()); - EXPECT_EQ(provider_result.value().type, ResourceType::kProvider); + std::vector provider_names; +#ifdef SCORE_CRYPTO_SOFTWARE_BACKEND_ENABLED + provider_names.emplace_back("OPENSSL"); +#endif +#ifdef SCORE_CRYPTO_HARDWARE_BACKEND_ENABLED + provider_names.emplace_back("PKCS11_ENGINE"); +#endif + ASSERT_FALSE(provider_names.empty()); - HashContextConfig hash_config; - hash_config.SetAlgorithm("SHA256").SetProvider(provider_result.value()); - const auto hash_result = ctx_result.value()->CreateHashContext(hash_config); - EXPECT_TRUE(hash_result.has_value()); + for (const auto provider_name : provider_names) + { + SCOPED_TRACE(std::string{"Explicit provider: "} + std::string{provider_name}); + + const auto provider_result = + ctx_result.value()->ResolveResource(ResourceId{provider_name}, ResourceType::kProvider); + ASSERT_TRUE(provider_result.has_value()); + EXPECT_EQ(provider_result.value().type, ResourceType::kProvider); + + HashContextConfig hash_config; + hash_config.SetAlgorithm("SHA256").SetProvider(provider_result.value()); + const auto hash_result = ctx_result.value()->CreateHashContext(hash_config); + EXPECT_TRUE(hash_result.has_value()); + } } TEST_F(HashExampleTest, ReportsMissingExplicitProvider)