From 2b9fa33fe21b3e490e88bc2ffccbf2bb262b5b10 Mon Sep 17 00:00:00 2001 From: Athul Mallappallil Date: Thu, 23 Jul 2026 13:04:44 +0200 Subject: [PATCH 01/37] Make shared deployment loader and writer --- .../slot/deployment => common/storage}/BUILD | 16 +-- .../common/storage/deployment_descriptor.cpp | 19 +++ .../common/storage/deployment_descriptor.hpp | 74 +++++++++++ .../storage}/deployment_path_utils.hpp | 20 +-- .../common/storage/i_deployment_loader.hpp | 42 ++++++ .../common/storage/i_deployment_writer.hpp | 41 ++++++ .../storage/kv/kv_deployment_loader.cpp | 84 ++++++++++++ .../storage/kv/kv_deployment_loader.hpp | 54 ++++++++ .../storage}/kv/kv_deployment_writer.cpp | 34 +++-- .../storage/kv/kv_deployment_writer.hpp | 41 ++++++ score/crypto/src/daemon/key_management/BUILD | 4 +- .../slot/deployment/i_deployment_loader.hpp | 54 -------- .../slot/deployment/i_deployment_writer.hpp | 55 -------- .../deployment/kv/kv_deployment_loader.cpp | 120 ------------------ .../deployment/kv/kv_deployment_loader.hpp | 55 -------- .../deployment/kv/kv_deployment_writer.hpp | 45 ------- .../key_management/slot/deployment_loader.cpp | 17 ++- .../key_management/slot/deployment_writer.cpp | 11 +- 18 files changed, 406 insertions(+), 380 deletions(-) rename score/crypto/src/daemon/{key_management/slot/deployment => common/storage}/BUILD (68%) create mode 100644 score/crypto/src/daemon/common/storage/deployment_descriptor.cpp create mode 100644 score/crypto/src/daemon/common/storage/deployment_descriptor.hpp rename score/crypto/src/daemon/{key_management/slot/deployment => common/storage}/deployment_path_utils.hpp (59%) create mode 100644 score/crypto/src/daemon/common/storage/i_deployment_loader.hpp create mode 100644 score/crypto/src/daemon/common/storage/i_deployment_writer.hpp create mode 100644 score/crypto/src/daemon/common/storage/kv/kv_deployment_loader.cpp create mode 100644 score/crypto/src/daemon/common/storage/kv/kv_deployment_loader.hpp rename score/crypto/src/daemon/{key_management/slot/deployment => common/storage}/kv/kv_deployment_writer.cpp (62%) create mode 100644 score/crypto/src/daemon/common/storage/kv/kv_deployment_writer.hpp delete mode 100644 score/crypto/src/daemon/key_management/slot/deployment/i_deployment_loader.hpp delete mode 100644 score/crypto/src/daemon/key_management/slot/deployment/i_deployment_writer.hpp delete mode 100644 score/crypto/src/daemon/key_management/slot/deployment/kv/kv_deployment_loader.cpp delete mode 100644 score/crypto/src/daemon/key_management/slot/deployment/kv/kv_deployment_loader.hpp delete mode 100644 score/crypto/src/daemon/key_management/slot/deployment/kv/kv_deployment_writer.hpp diff --git a/score/crypto/src/daemon/key_management/slot/deployment/BUILD b/score/crypto/src/daemon/common/storage/BUILD similarity index 68% rename from score/crypto/src/daemon/key_management/slot/deployment/BUILD rename to score/crypto/src/daemon/common/storage/BUILD index 4039b8e1a..a0f30c38e 100644 --- a/score/crypto/src/daemon/key_management/slot/deployment/BUILD +++ b/score/crypto/src/daemon/common/storage/BUILD @@ -13,12 +13,13 @@ load("@rules_cc//cc:cc_library.bzl", "cc_library") -# Header-only: interfaces + shared path validation utility. -# No format-specific deps — adding a new format (json, flatbuffer, ...) only -# requires adding a new cc_library target in this file; nothing else changes. cc_library( name = "deployment_iface", + srcs = [ + "deployment_descriptor.cpp", + ], hdrs = [ + "deployment_descriptor.hpp", "deployment_path_utils.hpp", "i_deployment_loader.hpp", "i_deployment_writer.hpp", @@ -27,13 +28,9 @@ cc_library( deps = [ "//score/crypto/src/common:common_types", "//score/crypto/src/daemon/common", - "//score/crypto/src/daemon/key_management:key_management_headers", ], ) -# KV format implementation. Stdlib-only — no extra library deps required. -# To add a new format, create an analogous target here (e.g. json_deployment) -# and add it as a dep to //score/crypto/src/daemon/key_management:key_management. cc_library( name = "kv_deployment", srcs = [ @@ -45,5 +42,8 @@ cc_library( "kv/kv_deployment_writer.hpp", ], visibility = ["//:__subpackages__"], - deps = [":deployment_iface"], + deps = [ + ":deployment_iface", + "@score_baselibs//score/mw/log", + ], ) diff --git a/score/crypto/src/daemon/common/storage/deployment_descriptor.cpp b/score/crypto/src/daemon/common/storage/deployment_descriptor.cpp new file mode 100644 index 000000000..1c2598718 --- /dev/null +++ b/score/crypto/src/daemon/common/storage/deployment_descriptor.cpp @@ -0,0 +1,19 @@ +/******************************************************************************** + * 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/common/storage/deployment_descriptor.hpp" + +namespace score::crypto::daemon::common::storage +{ +const std::string DeploymentDescriptor::kEmptyString{}; +} // namespace score::crypto::daemon::common::storage diff --git a/score/crypto/src/daemon/common/storage/deployment_descriptor.hpp b/score/crypto/src/daemon/common/storage/deployment_descriptor.hpp new file mode 100644 index 000000000..4edcb1e4d --- /dev/null +++ b/score/crypto/src/daemon/common/storage/deployment_descriptor.hpp @@ -0,0 +1,74 @@ +/******************************************************************************** + * 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 + ********************************************************************************/ + +#ifndef SCORE_CRYPTO_SRC_DAEMON_COMMON_STORAGE_DEPLOYMENT_DESCRIPTOR_HPP +#define SCORE_CRYPTO_SRC_DAEMON_COMMON_STORAGE_DEPLOYMENT_DESCRIPTOR_HPP + +#include +#include + +namespace score::crypto::daemon::common::storage +{ + +/// @brief Generic section-based key-value deployment descriptor. +/// +/// Represents the parsed content of a deployment descriptor file as a nested map: +/// section name -> { key -> value } +/// +/// This is the generic in-memory representation. Specific components +/// (cert_management, future components) interpret section names and keys +/// according to their own schema. +struct DeploymentDescriptor +{ + /// @brief Section name -> (key -> value) map. + std::unordered_map> sections; + + /// @brief Get a value from a section, returning default_val if absent. + [[nodiscard]] const std::string& Get(const std::string& section, + const std::string& key, + const std::string& default_val = kEmptyString) const noexcept + { + const auto sit = sections.find(section); + if (sit == sections.end()) + { + return default_val; + } + const auto kit = sit->second.find(key); + return (kit != sit->second.end()) ? kit->second : default_val; + } + + /// @brief True if the named section is present (even if empty). + [[nodiscard]] bool HasSection(const std::string& section) const noexcept + { + return sections.count(section) > 0U; + } + + /// @brief Set a key in a section (creates section if absent). + void Set(const std::string& section, const std::string& key, const std::string& value) + { + sections[section][key] = value; + } + + /// @brief Remove the named section entirely. + void RemoveSection(const std::string& section) + { + sections.erase(section); + } + + private: + static const std::string kEmptyString; +}; + +} // namespace score::crypto::daemon::common::storage + +#endif // SCORE_CRYPTO_SRC_DAEMON_COMMON_STORAGE_DEPLOYMENT_DESCRIPTOR_HPP diff --git a/score/crypto/src/daemon/key_management/slot/deployment/deployment_path_utils.hpp b/score/crypto/src/daemon/common/storage/deployment_path_utils.hpp similarity index 59% rename from score/crypto/src/daemon/key_management/slot/deployment/deployment_path_utils.hpp rename to score/crypto/src/daemon/common/storage/deployment_path_utils.hpp index 5f7af12b6..2ece49a06 100644 --- a/score/crypto/src/daemon/key_management/slot/deployment/deployment_path_utils.hpp +++ b/score/crypto/src/daemon/common/storage/deployment_path_utils.hpp @@ -11,42 +11,34 @@ * SPDX-License-Identifier: Apache-2.0 ********************************************************************************/ -#ifndef SCORE_CRYPTO_SRC_DAEMON_KEY_MANAGEMENT_SLOT_DEPLOYMENT_DEPLOYMENT_PATH_UTILS_HPP -#define SCORE_CRYPTO_SRC_DAEMON_KEY_MANAGEMENT_SLOT_DEPLOYMENT_DEPLOYMENT_PATH_UTILS_HPP +#ifndef SCORE_CRYPTO_SRC_DAEMON_COMMON_STORAGE_DEPLOYMENT_PATH_UTILS_HPP +#define SCORE_CRYPTO_SRC_DAEMON_COMMON_STORAGE_DEPLOYMENT_PATH_UTILS_HPP #include -namespace score::crypto::daemon::key_management +namespace score::crypto::daemon::common::storage { /// @brief Returns true if the path is absolute and contains no ".." path traversal components. -/// -/// Used as a single shared security pre-check by DeploymentLoaderFactory and -/// DeploymentWriterFactory before dispatching to a format-specific implementation. -/// Centralises path validation so it is not duplicated across every format class. [[nodiscard]] inline bool IsDeploymentPathSafe(const std::string& path) noexcept { if (path.empty()) { return false; } - - // Require absolute path (Unix: starts with '/'). const bool is_absolute = (path[0] == '/') || (path.size() >= 3U && path[1] == ':'); if (!is_absolute) { return false; } - - // Reject path traversal: ".." as a standalone component. if (path.find("..") != std::string::npos) { return false; } - return true; } -} // namespace score::crypto::daemon::key_management +} // namespace score::crypto::daemon::common::storage + -#endif // SCORE_CRYPTO_SRC_DAEMON_KEY_MANAGEMENT_SLOT_DEPLOYMENT_DEPLOYMENT_PATH_UTILS_HPP +#endif // SCORE_CRYPTO_SRC_DAEMON_COMMON_STORAGE_DEPLOYMENT_PATH_UTILS_HPP diff --git a/score/crypto/src/daemon/common/storage/i_deployment_loader.hpp b/score/crypto/src/daemon/common/storage/i_deployment_loader.hpp new file mode 100644 index 000000000..917d67dfa --- /dev/null +++ b/score/crypto/src/daemon/common/storage/i_deployment_loader.hpp @@ -0,0 +1,42 @@ +/******************************************************************************** + * 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 + ********************************************************************************/ + +#ifndef SCORE_CRYPTO_SRC_DAEMON_COMMON_STORAGE_I_DEPLOYMENT_LOADER_HPP +#define SCORE_CRYPTO_SRC_DAEMON_COMMON_STORAGE_I_DEPLOYMENT_LOADER_HPP + +#include "score/crypto/src/common/types.hpp" +#include "score/crypto/src/daemon/common/daemon_error.hpp" +#include "score/crypto/src/daemon/common/storage/deployment_descriptor.hpp" + +#include + +namespace score::crypto::daemon::common::storage +{ + +/// @brief Interface for format-specific deployment descriptor loaders. +/// +/// Each concrete implementation handles exactly one serialization format. +/// Path safety pre-checks are performed by the factory before calling Load(). +class IDeploymentLoader +{ + public: + virtual ~IDeploymentLoader() = default; + + /// @brief Load a DeploymentDescriptor from the given (pre-validated) path. + [[nodiscard]] virtual score::crypto::Expected + Load(const std::string& path) = 0; +}; + +} // namespace score::crypto::daemon::common::storage + +#endif // SCORE_CRYPTO_SRC_DAEMON_COMMON_STORAGE_I_DEPLOYMENT_LOADER_HPP diff --git a/score/crypto/src/daemon/common/storage/i_deployment_writer.hpp b/score/crypto/src/daemon/common/storage/i_deployment_writer.hpp new file mode 100644 index 000000000..01cc65a3e --- /dev/null +++ b/score/crypto/src/daemon/common/storage/i_deployment_writer.hpp @@ -0,0 +1,41 @@ +/******************************************************************************** + * 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 + ********************************************************************************/ + +#ifndef SCORE_CRYPTO_SRC_DAEMON_COMMON_STORAGE_I_DEPLOYMENT_WRITER_HPP +#define SCORE_CRYPTO_SRC_DAEMON_COMMON_STORAGE_I_DEPLOYMENT_WRITER_HPP + +#include "score/crypto/src/common/types.hpp" +#include "score/crypto/src/daemon/common/daemon_error.hpp" +#include "score/crypto/src/daemon/common/storage/deployment_descriptor.hpp" + +#include +#include + +namespace score::crypto::daemon::common::storage +{ + +/// @brief Interface for format-specific deployment descriptor writers. +class IDeploymentWriter +{ + public: + virtual ~IDeploymentWriter() = default; + + /// @brief Write a DeploymentDescriptor to the given (pre-validated) path. + [[nodiscard]] virtual score::crypto::Expected Write( + const std::string& path, + const DeploymentDescriptor& descriptor) = 0; +}; + +} // namespace score::crypto::daemon::common::storage + +#endif // SCORE_CRYPTO_SRC_DAEMON_COMMON_STORAGE_I_DEPLOYMENT_WRITER_HPP diff --git a/score/crypto/src/daemon/common/storage/kv/kv_deployment_loader.cpp b/score/crypto/src/daemon/common/storage/kv/kv_deployment_loader.cpp new file mode 100644 index 000000000..eda2e4204 --- /dev/null +++ b/score/crypto/src/daemon/common/storage/kv/kv_deployment_loader.cpp @@ -0,0 +1,84 @@ +/******************************************************************************** + * 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/common/storage/kv/kv_deployment_loader.hpp" + +#include "score/mw/log/logging.h" + +#include +#include +#include + +namespace score::crypto::daemon::common::storage +{ + +namespace +{ + +[[nodiscard]] std::string Trim(std::string_view sv) noexcept +{ + const auto start = sv.find_first_not_of(" \t\r\n"); + if (start == std::string_view::npos) + { + return {}; + } + const auto end = sv.find_last_not_of(" \t\r\n"); + return std::string{sv.substr(start, end - start + 1U)}; +} + +} // namespace + +score::crypto::Expected KvDeploymentLoader::Load( + const std::string& path) +{ + std::ifstream file(path); + if (!file.is_open()) + { + score::mw::log::LogError() << kLogPrefix << "Cannot open: " << path; + return score::crypto::make_unexpected(score::crypto::daemon::common::DaemonErrorCode::kInternalError); + } + + DeploymentDescriptor descriptor; + std::string current_section; + std::string line; + + while (std::getline(file, line)) + { + const std::string trimmed = Trim(line); + if (trimmed.empty() || trimmed[0] == '#') + { + continue; + } + if (trimmed.front() == '[' && trimmed.back() == ']') + { + current_section = Trim(trimmed.substr(1U, trimmed.size() - 2U)); + continue; + } + if (current_section.empty()) + { + continue; + } + const auto eq_pos = trimmed.find('='); + if (eq_pos == std::string::npos) + { + continue; + } + const std::string key = Trim(trimmed.substr(0U, eq_pos)); + const std::string value = Trim(trimmed.substr(eq_pos + 1U)); + descriptor.sections[current_section][key] = value; + } + + return descriptor; +} + +} // namespace score::crypto::daemon::common::storage diff --git a/score/crypto/src/daemon/common/storage/kv/kv_deployment_loader.hpp b/score/crypto/src/daemon/common/storage/kv/kv_deployment_loader.hpp new file mode 100644 index 000000000..7ecdbe64d --- /dev/null +++ b/score/crypto/src/daemon/common/storage/kv/kv_deployment_loader.hpp @@ -0,0 +1,54 @@ +/******************************************************************************** + * 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 + ********************************************************************************/ + +#ifndef SCORE_CRYPTO_SRC_DAEMON_COMMON_STORAGE_KV_KV_DEPLOYMENT_LOADER_HPP +#define SCORE_CRYPTO_SRC_DAEMON_COMMON_STORAGE_KV_KV_DEPLOYMENT_LOADER_HPP + +#include "score/crypto/src/daemon/common/storage/i_deployment_loader.hpp" + +#include + +namespace score::crypto::daemon::common::storage +{ + +/// @brief Loads a DeploymentDescriptor from a key=value text file. +/// +/// File format: +/// @code +/// # comment +/// [section_name] +/// key = value +/// another_key = another_value +/// +/// [another_section] +/// foo = bar +/// @endcode +/// +/// - Lines starting with '#' are comments (ignored). +/// - Blank lines are ignored. +/// - Section headers switch the active section. +/// - Lines without '=' are silently skipped. +/// - Keys and values are whitespace-trimmed. +class KvDeploymentLoader final : public IDeploymentLoader +{ + public: + [[nodiscard]] score::crypto::Expected Load( + const std::string& path) override; + + private: + static constexpr std::string_view kLogPrefix = "[KV_DEPLOYMENT_LOADER_COMMON] "; +}; + +} // namespace score::crypto::daemon::common::storage + +#endif // SCORE_CRYPTO_SRC_DAEMON_COMMON_STORAGE_KV_KV_DEPLOYMENT_LOADER_HPP diff --git a/score/crypto/src/daemon/key_management/slot/deployment/kv/kv_deployment_writer.cpp b/score/crypto/src/daemon/common/storage/kv/kv_deployment_writer.cpp similarity index 62% rename from score/crypto/src/daemon/key_management/slot/deployment/kv/kv_deployment_writer.cpp rename to score/crypto/src/daemon/common/storage/kv/kv_deployment_writer.cpp index dd4f6a0bb..72d67a59f 100644 --- a/score/crypto/src/daemon/key_management/slot/deployment/kv/kv_deployment_writer.cpp +++ b/score/crypto/src/daemon/common/storage/kv/kv_deployment_writer.cpp @@ -11,46 +11,42 @@ * SPDX-License-Identifier: Apache-2.0 ********************************************************************************/ -#include "score/crypto/src/daemon/key_management/slot/deployment/kv/kv_deployment_writer.hpp" +#include "score/crypto/src/daemon/common/storage/kv/kv_deployment_writer.hpp" #include "score/mw/log/logging.h" -#include -#include +#include -namespace score::crypto::daemon::key_management +namespace score::crypto::daemon::common::storage { score::crypto::Expected KvDeploymentWriter::Write( const std::string& path, - const SlotDeploymentInfo& info) + const DeploymentDescriptor& descriptor) { std::ofstream file(path, std::ios::trunc); if (!file.is_open()) { - score::mw::log::LogError() << kLogPrefix << "Cannot open deployment descriptor for writing:" << path; - return score::crypto::make_unexpected(score::crypto::daemon::common::DaemonErrorCode::kInvalidArgument); - } - - file << "[metadata]\n"; - for (const auto& [key, value] : info.metadata) - { - file << key << '=' << value << '\n'; + score::mw::log::LogError() << kLogPrefix << "Cannot open for writing: " << path; + return score::crypto::make_unexpected(score::crypto::daemon::common::DaemonErrorCode::kInternalError); } - file << "\n[key]\n"; - for (const auto& [key, value] : info.key_properties) + for (const auto& [section, entries] : descriptor.sections) { - file << key << '=' << value << '\n'; + file << '[' << section << ']' << '\n'; + for (const auto& [key, value] : entries) + { + file << key << " = " << value << '\n'; + } + file << '\n'; } if (!file.good()) { - score::mw::log::LogError() << kLogPrefix << "Write error for deployment descriptor:" << path; + score::mw::log::LogError() << kLogPrefix << "Write failed for: " << path; return score::crypto::make_unexpected(score::crypto::daemon::common::DaemonErrorCode::kInternalError); } - return std::monostate{}; } -} // namespace score::crypto::daemon::key_management +} // namespace score::crypto::daemon::common::storage diff --git a/score/crypto/src/daemon/common/storage/kv/kv_deployment_writer.hpp b/score/crypto/src/daemon/common/storage/kv/kv_deployment_writer.hpp new file mode 100644 index 000000000..2ccb65420 --- /dev/null +++ b/score/crypto/src/daemon/common/storage/kv/kv_deployment_writer.hpp @@ -0,0 +1,41 @@ +/******************************************************************************** + * 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 + ********************************************************************************/ + +#ifndef SCORE_CRYPTO_SRC_DAEMON_COMMON_STORAGE_KV_KV_DEPLOYMENT_WRITER_HPP +#define SCORE_CRYPTO_SRC_DAEMON_COMMON_STORAGE_KV_KV_DEPLOYMENT_WRITER_HPP + +#include "score/crypto/src/daemon/common/storage/i_deployment_writer.hpp" + +#include + +namespace score::crypto::daemon::common::storage +{ + +/// @brief Writes a DeploymentDescriptor to a key=value text file. +/// +/// Produces the same section/key=value format that KvDeploymentLoader can read back. +/// Existing file content is replaced (opened with std::ios::trunc). +class KvDeploymentWriter final : public IDeploymentWriter +{ + public: + [[nodiscard]] score::crypto::Expected Write( + const std::string& path, + const DeploymentDescriptor& descriptor) override; + + private: + static constexpr std::string_view kLogPrefix = "[KV_DEPLOYMENT_WRITER_COMMON] "; +}; + +} // namespace score::crypto::daemon::common::storage + +#endif // SCORE_CRYPTO_SRC_DAEMON_COMMON_STORAGE_KV_KV_DEPLOYMENT_WRITER_HPP diff --git a/score/crypto/src/daemon/key_management/BUILD b/score/crypto/src/daemon/key_management/BUILD index 09d5232e0..07dc910e1 100644 --- a/score/crypto/src/daemon/key_management/BUILD +++ b/score/crypto/src/daemon/key_management/BUILD @@ -108,11 +108,11 @@ cc_library( visibility = ["//:__subpackages__"], deps = [ ":key_management_headers", - "//score/crypto/src/api/common:crypto_common", "//score/crypto/src/daemon/common", + "//score/crypto/src/daemon/common/storage:kv_deployment", "//score/crypto/src/daemon/data_manager", - "//score/crypto/src/daemon/key_management/slot/deployment:kv_deployment", "//score/crypto/src/daemon/provider:provider_headers", "//score/crypto/src/daemon/provider:provider_manager", + "//score/crypto/src/api/common:crypto_common", ], ) diff --git a/score/crypto/src/daemon/key_management/slot/deployment/i_deployment_loader.hpp b/score/crypto/src/daemon/key_management/slot/deployment/i_deployment_loader.hpp deleted file mode 100644 index 0febd0acc..000000000 --- a/score/crypto/src/daemon/key_management/slot/deployment/i_deployment_loader.hpp +++ /dev/null @@ -1,54 +0,0 @@ -/******************************************************************************** - * 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 - ********************************************************************************/ - -#ifndef SCORE_CRYPTO_SRC_DAEMON_KEY_MANAGEMENT_SLOT_DEPLOYMENT_I_DEPLOYMENT_LOADER_HPP -#define SCORE_CRYPTO_SRC_DAEMON_KEY_MANAGEMENT_SLOT_DEPLOYMENT_I_DEPLOYMENT_LOADER_HPP - -#include "score/crypto/src/common/types.hpp" -#include "score/crypto/src/daemon/common/daemon_error.hpp" -#include "score/crypto/src/daemon/key_management/interfaces/key_slot_config.hpp" - -#include - -namespace score::crypto::daemon::key_management -{ - -/// @brief Interface for format-specific deployment descriptor loaders. -/// -/// Each concrete implementation handles exactly one serialization format -/// (kv, json, flatbuffer, custom, ...). The path safety pre-check is -/// performed by DeploymentLoaderFactory *before* calling Load(), so -/// implementations can assume a valid, absolute, traversal-free path. -/// -/// ### Adding a new format -/// 1. Implement this interface in a new class (e.g., `JsonDeploymentLoader`). -/// 2. Place the class under `slot/deployment//`. -/// 3. Register it in `DeploymentLoaderFactory::Create()`. -/// No other files need to change. -class IDeploymentLoader -{ - public: - virtual ~IDeploymentLoader() = default; - - /// @brief Load a SlotDeploymentInfo from the given (pre-validated) path. - /// - /// @param path Absolute path to the deployment descriptor file. The caller - /// (factory) has already verified it is safe. - /// @return Parsed SlotDeploymentInfo on success, or DaemonErrorCode on failure. - [[nodiscard]] virtual score::crypto::Expected - Load(const std::string& path) = 0; -}; - -} // namespace score::crypto::daemon::key_management - -#endif // SCORE_CRYPTO_SRC_DAEMON_KEY_MANAGEMENT_SLOT_DEPLOYMENT_I_DEPLOYMENT_LOADER_HPP diff --git a/score/crypto/src/daemon/key_management/slot/deployment/i_deployment_writer.hpp b/score/crypto/src/daemon/key_management/slot/deployment/i_deployment_writer.hpp deleted file mode 100644 index 02aa2978d..000000000 --- a/score/crypto/src/daemon/key_management/slot/deployment/i_deployment_writer.hpp +++ /dev/null @@ -1,55 +0,0 @@ -/******************************************************************************** - * 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 - ********************************************************************************/ - -#ifndef SCORE_CRYPTO_SRC_DAEMON_KEY_MANAGEMENT_SLOT_DEPLOYMENT_I_DEPLOYMENT_WRITER_HPP -#define SCORE_CRYPTO_SRC_DAEMON_KEY_MANAGEMENT_SLOT_DEPLOYMENT_I_DEPLOYMENT_WRITER_HPP - -#include "score/crypto/src/common/types.hpp" -#include "score/crypto/src/daemon/common/daemon_error.hpp" -#include "score/crypto/src/daemon/key_management/interfaces/key_slot_config.hpp" - -#include -#include - -namespace score::crypto::daemon::key_management -{ - -/// @brief Interface for format-specific deployment descriptor writers. -/// -/// Mirrors IDeploymentLoader: one concrete class per serialization format. -/// The path safety pre-check is performed by DeploymentWriterFactory *before* -/// calling Write(), so implementations can assume a valid path. -/// -/// ### Adding a new format -/// 1. Implement this interface in a new class (e.g., `JsonDeploymentWriter`). -/// 2. Place the class under `slot/deployment//`. -/// 3. Register it in `DeploymentWriterFactory::Create()`. -class IDeploymentWriter -{ - public: - virtual ~IDeploymentWriter() = default; - - /// @brief Write a SlotDeploymentInfo to the given (pre-validated) path. - /// - /// @param path Absolute path to the deployment descriptor file. The caller - /// (factory) has already verified it is safe. - /// @param info The deployment info to persist. - /// @return std::monostate on success, or DaemonErrorCode on failure. - [[nodiscard]] virtual score::crypto::Expected Write( - const std::string& path, - const SlotDeploymentInfo& info) = 0; -}; - -} // namespace score::crypto::daemon::key_management - -#endif // SCORE_CRYPTO_SRC_DAEMON_KEY_MANAGEMENT_SLOT_DEPLOYMENT_I_DEPLOYMENT_WRITER_HPP diff --git a/score/crypto/src/daemon/key_management/slot/deployment/kv/kv_deployment_loader.cpp b/score/crypto/src/daemon/key_management/slot/deployment/kv/kv_deployment_loader.cpp deleted file mode 100644 index 2e7665608..000000000 --- a/score/crypto/src/daemon/key_management/slot/deployment/kv/kv_deployment_loader.cpp +++ /dev/null @@ -1,120 +0,0 @@ -/******************************************************************************** - * 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/key_management/slot/deployment/kv/kv_deployment_loader.hpp" - -#include "score/mw/log/logging.h" -#include -#include -#include - -#include - -namespace score::crypto::daemon::key_management -{ - -score::crypto::Expected KvDeploymentLoader::Load( - const std::string& path) -{ - std::ifstream file(path); - if (!file.is_open()) - { - score::mw::log::LogError() << kLogPrefix << "Cannot open deployment descriptor:" << path; - return score::crypto::make_unexpected(score::crypto::daemon::common::DaemonErrorCode::kInvalidArgument); - } - - SlotDeploymentInfo info{}; - - enum class Section : uint8_t - { - kMetadata, - kKey - }; - auto current_section = Section::kMetadata; - - std::string line; - while (std::getline(file, line)) - { - // Trim leading/trailing whitespace. - std::size_t start = line.find_first_not_of(" \t\r\n"); - if (start == std::string::npos) - { - continue; // blank line - } - std::size_t end = line.find_last_not_of(" \t\r\n"); - line = line.substr(start, end - start + 1U); - - // Skip comments. - if (line[0] == '#') - { - continue; - } - - // Section headers. - if (line == "[metadata]") - { - current_section = Section::kMetadata; - continue; - } - if (line == "[key]") - { - current_section = Section::kKey; - continue; - } - - // Parse key=value pairs. - const auto eq_pos = line.find('='); - if (eq_pos == std::string::npos) - { - continue; // malformed line — skip - } - - std::string key = line.substr(0U, eq_pos); - std::string value = line.substr(eq_pos + 1U); - - // Trim key and value. - auto trim = [](std::string& s) { - std::size_t s_start = s.find_first_not_of(" \t"); - std::size_t s_end = s.find_last_not_of(" \t"); - if (s_start == std::string::npos) - { - s.clear(); - } - else - { - s = s.substr(s_start, s_end - s_start + 1U); - } - }; - trim(key); - trim(value); - - if (key.empty()) - { - continue; - } - - switch (current_section) - { - case Section::kMetadata: - info.metadata[std::move(key)] = std::move(value); - break; - case Section::kKey: - info.key_properties[std::move(key)] = std::move(value); - break; - } - } - - return info; -} - -} // namespace score::crypto::daemon::key_management diff --git a/score/crypto/src/daemon/key_management/slot/deployment/kv/kv_deployment_loader.hpp b/score/crypto/src/daemon/key_management/slot/deployment/kv/kv_deployment_loader.hpp deleted file mode 100644 index e9973725c..000000000 --- a/score/crypto/src/daemon/key_management/slot/deployment/kv/kv_deployment_loader.hpp +++ /dev/null @@ -1,55 +0,0 @@ -/******************************************************************************** - * 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 - ********************************************************************************/ - -#ifndef SCORE_CRYPTO_SRC_DAEMON_KEY_MANAGEMENT_SLOT_DEPLOYMENT_KV_KV_DEPLOYMENT_LOADER_HPP -#define SCORE_CRYPTO_SRC_DAEMON_KEY_MANAGEMENT_SLOT_DEPLOYMENT_KV_KV_DEPLOYMENT_LOADER_HPP - -#include "score/crypto/src/daemon/key_management/slot/deployment/i_deployment_loader.hpp" - -#include - -namespace score::crypto::daemon::key_management -{ - -/// @brief Loads a SlotDeploymentInfo from a key=value text file. -/// -/// File format: -/// @code -/// # comment -/// [metadata] -/// availability = active -/// provisioned_at = 2025-11-03T08:42:00Z -/// -/// [key] -/// key_path = /etc/crypto/keys/hmac.bin -/// key_format = raw -/// @endcode -/// -/// - Lines starting with `#` are comments and are ignored. -/// - Blank lines are ignored. -/// - Section headers `[metadata]` and `[key]` switch the active map. -/// - Lines without a `=` separator are silently skipped. -/// - Keys and values are whitespace-trimmed. -class KvDeploymentLoader : public IDeploymentLoader -{ - public: - [[nodiscard]] score::crypto::Expected Load( - const std::string& path) override; - - private: - static constexpr std::string_view kLogPrefix = "[KV_DEPLOYMENT_LOADER] "; -}; - -} // namespace score::crypto::daemon::key_management - -#endif // SCORE_CRYPTO_SRC_DAEMON_KEY_MANAGEMENT_SLOT_DEPLOYMENT_KV_KV_DEPLOYMENT_LOADER_HPP diff --git a/score/crypto/src/daemon/key_management/slot/deployment/kv/kv_deployment_writer.hpp b/score/crypto/src/daemon/key_management/slot/deployment/kv/kv_deployment_writer.hpp deleted file mode 100644 index 1f62ef027..000000000 --- a/score/crypto/src/daemon/key_management/slot/deployment/kv/kv_deployment_writer.hpp +++ /dev/null @@ -1,45 +0,0 @@ -/******************************************************************************** - * 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 - ********************************************************************************/ - -#ifndef SCORE_CRYPTO_SRC_DAEMON_KEY_MANAGEMENT_SLOT_DEPLOYMENT_KV_KV_DEPLOYMENT_WRITER_HPP -#define SCORE_CRYPTO_SRC_DAEMON_KEY_MANAGEMENT_SLOT_DEPLOYMENT_KV_KV_DEPLOYMENT_WRITER_HPP - -#include "score/crypto/src/daemon/key_management/slot/deployment/i_deployment_writer.hpp" - -#include - -namespace score::crypto::daemon::key_management -{ - -/// @brief Writes a SlotDeploymentInfo to a key=value text file. -/// -/// Produces the same `[metadata]` / `[key]` section format that -/// KvDeploymentLoader can read back. Existing file content is replaced -/// (opened with `std::ios::trunc`). -/// -/// @note Writes are not currently atomic. A future extension may implement -/// write-then-rename to protect against partial writes on crash. -class KvDeploymentWriter : public IDeploymentWriter -{ - public: - [[nodiscard]] score::crypto::Expected Write( - const std::string& path, - const SlotDeploymentInfo& info) override; - - private: - static constexpr std::string_view kLogPrefix = "[KV_DEPLOYMENT_WRITER] "; -}; - -} // namespace score::crypto::daemon::key_management - -#endif // SCORE_CRYPTO_SRC_DAEMON_KEY_MANAGEMENT_SLOT_DEPLOYMENT_KV_KV_DEPLOYMENT_WRITER_HPP diff --git a/score/crypto/src/daemon/key_management/slot/deployment_loader.cpp b/score/crypto/src/daemon/key_management/slot/deployment_loader.cpp index e6d5691fe..cbd555cd3 100644 --- a/score/crypto/src/daemon/key_management/slot/deployment_loader.cpp +++ b/score/crypto/src/daemon/key_management/slot/deployment_loader.cpp @@ -13,8 +13,8 @@ #include "score/crypto/src/daemon/key_management/slot/deployment_loader.hpp" -#include "score/crypto/src/daemon/key_management/slot/deployment/deployment_path_utils.hpp" -#include "score/crypto/src/daemon/key_management/slot/deployment/kv/kv_deployment_loader.hpp" +#include "score/crypto/src/daemon/common/storage/deployment_path_utils.hpp" +#include "score/crypto/src/daemon/common/storage/kv/kv_deployment_loader.hpp" #include "score/mw/log/logging.h" @@ -27,7 +27,7 @@ score::crypto::Expectedsections["metadata"]; + info.key_properties = descriptor->sections["key"]; + return info; } // To add a new format: include its header above and add a branch here. // Example: if (format == "json") { return JsonDeploymentLoader{}.Load(path); } diff --git a/score/crypto/src/daemon/key_management/slot/deployment_writer.cpp b/score/crypto/src/daemon/key_management/slot/deployment_writer.cpp index 4bd3d1dc9..bc1d75917 100644 --- a/score/crypto/src/daemon/key_management/slot/deployment_writer.cpp +++ b/score/crypto/src/daemon/key_management/slot/deployment_writer.cpp @@ -13,8 +13,8 @@ #include "score/crypto/src/daemon/key_management/slot/deployment_writer.hpp" -#include "score/crypto/src/daemon/key_management/slot/deployment/deployment_path_utils.hpp" -#include "score/crypto/src/daemon/key_management/slot/deployment/kv/kv_deployment_writer.hpp" +#include "score/crypto/src/daemon/common/storage/deployment_path_utils.hpp" +#include "score/crypto/src/daemon/common/storage/kv/kv_deployment_writer.hpp" #include "score/mw/log/logging.h" @@ -26,7 +26,7 @@ namespace score::crypto::daemon::key_management score::crypto::Expected DeploymentWriter::Write(const std::string& path, const std::string& format, const SlotDeploymentInfo& info) { - if (!IsDeploymentPathSafe(path)) + if (!score::crypto::daemon::common::storage::IsDeploymentPathSafe(path)) { score::mw::log::LogError() << LOG_PREFIX << "Unsafe deployment path rejected:" << path; return score::crypto::make_unexpected(score::crypto::daemon::common::DaemonErrorCode::kInvalidArgument); @@ -34,7 +34,10 @@ DeploymentWriter::Write(const std::string& path, const std::string& format, cons if (format == "kv") { - return KvDeploymentWriter{}.Write(path, info); + score::crypto::daemon::common::storage::DeploymentDescriptor descriptor{}; + descriptor.sections["metadata"] = info.metadata; + descriptor.sections["key"] = info.key_properties; + return score::crypto::daemon::common::storage::KvDeploymentWriter{}.Write(path, descriptor); } // To add a new format: include its header above and add a branch here. // Example: if (format == "json") { return JsonDeploymentWriter{}.Write(path, info); } From a2cc0270ce28a61d7523e3a6d41f312857d89acc Mon Sep 17 00:00:00 2001 From: Athul Mallappallil Date: Thu, 30 Jul 2026 21:58:50 +0200 Subject: [PATCH 02/37] API update for cert mgmnt and ops --- score/crypto/src/api/certificate/BUILD | 29 +++++ .../{future => }/certificate/cert_types.hpp | 15 ++- .../certificate/i_ocsp_request_export.hpp | 6 +- score/crypto/src/api/common/types.hpp | 21 +++- score/crypto/src/api/config/BUILD | 14 +++ .../config/certificate_context_config.hpp | 6 +- ...ertificate_verification_context_config.hpp | 6 +- score/crypto/src/api/contexts/BUILD | 19 +++ .../i_certificate_management_context.hpp | 111 ++++++++++-------- .../i_certificate_verification_context.hpp | 108 +++++++++++------ score/crypto/src/api/future/certificate/BUILD | 5 +- score/crypto/src/api/future/config/BUILD | 5 +- score/crypto/src/api/future/contexts/BUILD | 10 +- score/crypto/src/api/future/objects/BUILD | 5 +- score/crypto/src/api/objects/BUILD | 16 +++ .../objects/i_cert_slot_object.hpp | 6 +- .../objects/i_certificate_object.hpp | 6 +- 17 files changed, 272 insertions(+), 116 deletions(-) create mode 100644 score/crypto/src/api/certificate/BUILD rename score/crypto/src/api/{future => }/certificate/cert_types.hpp (78%) rename score/crypto/src/api/{future => }/certificate/i_ocsp_request_export.hpp (89%) rename score/crypto/src/api/{future => }/config/certificate_context_config.hpp (89%) rename score/crypto/src/api/{future => }/config/certificate_verification_context_config.hpp (91%) rename score/crypto/src/api/{future => }/contexts/i_certificate_management_context.hpp (77%) rename score/crypto/src/api/{future => }/contexts/i_certificate_verification_context.hpp (53%) rename score/crypto/src/api/{future => }/objects/i_cert_slot_object.hpp (88%) rename score/crypto/src/api/{future => }/objects/i_certificate_object.hpp (95%) diff --git a/score/crypto/src/api/certificate/BUILD b/score/crypto/src/api/certificate/BUILD new file mode 100644 index 000000000..a0e9c52e4 --- /dev/null +++ b/score/crypto/src/api/certificate/BUILD @@ -0,0 +1,29 @@ +# ******************************************************************************* +# 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 +# ******************************************************************************* + +load("@rules_cc//cc:defs.bzl", "cc_library") + +cc_library( + name = "cert_types", + hdrs = [ + "cert_types.hpp", + "i_ocsp_request_export.hpp", + ], + includes = ["."], + visibility = ["//visibility:public"], + deps = [ + "//score/crypto/src/api/common:crypto_common", + "@score_baselibs//score/language/futurecpp", + "@score_baselibs//score/result", + ], +) diff --git a/score/crypto/src/api/future/certificate/cert_types.hpp b/score/crypto/src/api/certificate/cert_types.hpp similarity index 78% rename from score/crypto/src/api/future/certificate/cert_types.hpp rename to score/crypto/src/api/certificate/cert_types.hpp index 9d14deb7b..cd2f697be 100644 --- a/score/crypto/src/api/future/certificate/cert_types.hpp +++ b/score/crypto/src/api/certificate/cert_types.hpp @@ -11,8 +11,8 @@ * SPDX-License-Identifier: Apache-2.0 ********************************************************************************/ -#ifndef SCORE_CRYPTO_SRC_API_FUTURE_CERTIFICATE_CERT_TYPES_HPP -#define SCORE_CRYPTO_SRC_API_FUTURE_CERTIFICATE_CERT_TYPES_HPP +#ifndef SCORE_CRYPTO_SRC_API_CERTIFICATE_CERT_TYPES_HPP +#define SCORE_CRYPTO_SRC_API_CERTIFICATE_CERT_TYPES_HPP #include "score/crypto/src/api/common/types.hpp" @@ -39,6 +39,15 @@ enum class CertVerifyResult : uint8_t kUnknownError ///< Unspecified verification failure }; +/// @brief Controls where certificate-chain verification may terminate. +enum class ChainTerminationPolicy : uint8_t +{ + /// Require a complete path ending at a self-signed trust-store root. + kRootRequired, + /// Permit termination at the first certificate present in the trust store. + kTrustStoreTerminated +}; + /// @brief Status of an OCSP response. enum class OcspStatus : uint8_t { @@ -52,4 +61,4 @@ enum class OcspStatus : uint8_t } // namespace score -#endif // SCORE_CRYPTO_SRC_API_FUTURE_CERTIFICATE_CERT_TYPES_HPP +#endif // SCORE_CRYPTO_SRC_API_CERTIFICATE_CERT_TYPES_HPP diff --git a/score/crypto/src/api/future/certificate/i_ocsp_request_export.hpp b/score/crypto/src/api/certificate/i_ocsp_request_export.hpp similarity index 89% rename from score/crypto/src/api/future/certificate/i_ocsp_request_export.hpp rename to score/crypto/src/api/certificate/i_ocsp_request_export.hpp index 0627e7df2..93690bc23 100644 --- a/score/crypto/src/api/future/certificate/i_ocsp_request_export.hpp +++ b/score/crypto/src/api/certificate/i_ocsp_request_export.hpp @@ -11,8 +11,8 @@ * SPDX-License-Identifier: Apache-2.0 ********************************************************************************/ -#ifndef SCORE_CRYPTO_SRC_API_FUTURE_CERTIFICATE_I_OCSP_REQUEST_EXPORT_HPP -#define SCORE_CRYPTO_SRC_API_FUTURE_CERTIFICATE_I_OCSP_REQUEST_EXPORT_HPP +#ifndef SCORE_CRYPTO_SRC_API_CERTIFICATE_I_OCSP_REQUEST_EXPORT_HPP +#define SCORE_CRYPTO_SRC_API_CERTIFICATE_I_OCSP_REQUEST_EXPORT_HPP #include "score/result/result.h" #include "score/span.hpp" @@ -61,4 +61,4 @@ class IOcspRequestExport } // namespace score -#endif // SCORE_CRYPTO_SRC_API_FUTURE_CERTIFICATE_I_OCSP_REQUEST_EXPORT_HPP +#endif // SCORE_CRYPTO_SRC_API_CERTIFICATE_I_OCSP_REQUEST_EXPORT_HPP diff --git a/score/crypto/src/api/common/types.hpp b/score/crypto/src/api/common/types.hpp index ebeb4918c..31f75584f 100644 --- a/score/crypto/src/api/common/types.hpp +++ b/score/crypto/src/api/common/types.hpp @@ -133,6 +133,18 @@ enum class KeySlotState : uint8_t kLocked ///< Slot is in use and cannot be modified }; +/// @brief State of a certificate slot. +/// +/// Certificate slots contain certificate/CRL storage and are not bound to a +/// certificate-operation provider. Provider selection is made by the +/// operation/context that parses or verifies the material. +enum class CertificateSlotState : uint8_t +{ + kEmpty, ///< Slot contains no certificate + kOccupied, ///< Slot contains a certificate + kLocked ///< Slot is in use and cannot be modified +}; + /// @brief Validity status of a certificate. enum class CertificateStatus : uint8_t { @@ -284,14 +296,15 @@ inline constexpr bool HasPermission(KeyOperationPermission granted, KeyOperation return (g & r) == r; } -/// @brief Information about a certificate slot and its contents. +/// @brief Lightweight information about certificate-slot storage. /// /// Returned by ICertificateManagementContext::GetCertificateSlotInfo(). +/// Certificate-specific details such as subject, issuer, algorithm, and +/// validity are obtained by loading/parsing the certificate. struct CertificateSlotInfo { - bool occupied{false}; ///< Whether the slot contains a certificate - AlgorithmId algorithm{}; ///< Public key algorithm of the stored certificate (empty if unoccupied) - uint16_t primary_provider{0U}; ///< Provider/device that owns this slot + CertificateSlotState state{CertificateSlotState::kEmpty}; + bool has_crl{false}; ///< Whether a CRL is currently associated with the slot }; /// @brief Information about a key slot and its contents. diff --git a/score/crypto/src/api/config/BUILD b/score/crypto/src/api/config/BUILD index 5ff3b764a..1dd33a64e 100644 --- a/score/crypto/src/api/config/BUILD +++ b/score/crypto/src/api/config/BUILD @@ -29,3 +29,17 @@ cc_library( "//score/crypto/src/api/common:crypto_common", ], ) + +cc_library( + name = "cert_context_configs", + hdrs = [ + "certificate_context_config.hpp", + "certificate_verification_context_config.hpp", + ], + includes = ["."], + visibility = ["//visibility:public"], + deps = [ + ":context_configs", + "//score/crypto/src/api/common:crypto_common", + ], +) diff --git a/score/crypto/src/api/future/config/certificate_context_config.hpp b/score/crypto/src/api/config/certificate_context_config.hpp similarity index 89% rename from score/crypto/src/api/future/config/certificate_context_config.hpp rename to score/crypto/src/api/config/certificate_context_config.hpp index e47ea0183..8808c2def 100644 --- a/score/crypto/src/api/future/config/certificate_context_config.hpp +++ b/score/crypto/src/api/config/certificate_context_config.hpp @@ -11,8 +11,8 @@ * SPDX-License-Identifier: Apache-2.0 ********************************************************************************/ -#ifndef SCORE_CRYPTO_SRC_API_FUTURE_CONFIG_CERTIFICATE_CONTEXT_CONFIG_HPP -#define SCORE_CRYPTO_SRC_API_FUTURE_CONFIG_CERTIFICATE_CONTEXT_CONFIG_HPP +#ifndef SCORE_CRYPTO_SRC_API_CONFIG_CERTIFICATE_CONTEXT_CONFIG_HPP +#define SCORE_CRYPTO_SRC_API_CONFIG_CERTIFICATE_CONTEXT_CONFIG_HPP #include "score/crypto/src/api/config/base_context_config.hpp" @@ -67,4 +67,4 @@ struct CertificateContextConfig : public BaseContextConfig } // namespace score -#endif // SCORE_CRYPTO_SRC_API_FUTURE_CONFIG_CERTIFICATE_CONTEXT_CONFIG_HPP +#endif // SCORE_CRYPTO_SRC_API_CONFIG_CERTIFICATE_CONTEXT_CONFIG_HPP diff --git a/score/crypto/src/api/future/config/certificate_verification_context_config.hpp b/score/crypto/src/api/config/certificate_verification_context_config.hpp similarity index 91% rename from score/crypto/src/api/future/config/certificate_verification_context_config.hpp rename to score/crypto/src/api/config/certificate_verification_context_config.hpp index fb87cbb10..838fd0c75 100644 --- a/score/crypto/src/api/future/config/certificate_verification_context_config.hpp +++ b/score/crypto/src/api/config/certificate_verification_context_config.hpp @@ -11,8 +11,8 @@ * SPDX-License-Identifier: Apache-2.0 ********************************************************************************/ -#ifndef SCORE_CRYPTO_SRC_API_FUTURE_CONFIG_CERTIFICATE_VERIFICATION_CONTEXT_CONFIG_HPP -#define SCORE_CRYPTO_SRC_API_FUTURE_CONFIG_CERTIFICATE_VERIFICATION_CONTEXT_CONFIG_HPP +#ifndef SCORE_CRYPTO_SRC_API_CONFIG_CERTIFICATE_VERIFICATION_CONTEXT_CONFIG_HPP +#define SCORE_CRYPTO_SRC_API_CONFIG_CERTIFICATE_VERIFICATION_CONTEXT_CONFIG_HPP #include "score/crypto/src/api/common/types.hpp" #include "score/crypto/src/api/config/base_context_config.hpp" @@ -86,4 +86,4 @@ struct CertificateVerificationContextConfig : public BaseContextConfig } // namespace score -#endif // SCORE_CRYPTO_SRC_API_FUTURE_CONFIG_CERTIFICATE_VERIFICATION_CONTEXT_CONFIG_HPP +#endif // SCORE_CRYPTO_SRC_API_CONFIG_CERTIFICATE_VERIFICATION_CONTEXT_CONFIG_HPP diff --git a/score/crypto/src/api/contexts/BUILD b/score/crypto/src/api/contexts/BUILD index 6f905b7b3..06375baa8 100644 --- a/score/crypto/src/api/contexts/BUILD +++ b/score/crypto/src/api/contexts/BUILD @@ -48,6 +48,25 @@ cc_library( ], ) +cc_library( + name = "cert_contexts", + hdrs = [ + "i_certificate_management_context.hpp", + "i_certificate_verification_context.hpp", + ], + includes = ["."], + visibility = ["//visibility:public"], + deps = [ + ":context_bases", + "//score/crypto/src/api/certificate:cert_types", + "//score/crypto/src/api/common:crypto_common", + "//score/crypto/src/api/config:cert_context_configs", + "//score/crypto/src/api/objects:cert_objects", + "@score_baselibs//score/language/futurecpp", + "@score_baselibs//score/result", + ], +) + cc_library( name = "crypto_contexts_impl", srcs = [ diff --git a/score/crypto/src/api/future/contexts/i_certificate_management_context.hpp b/score/crypto/src/api/contexts/i_certificate_management_context.hpp similarity index 77% rename from score/crypto/src/api/future/contexts/i_certificate_management_context.hpp rename to score/crypto/src/api/contexts/i_certificate_management_context.hpp index 4f8747a8f..48491e34f 100644 --- a/score/crypto/src/api/future/contexts/i_certificate_management_context.hpp +++ b/score/crypto/src/api/contexts/i_certificate_management_context.hpp @@ -11,14 +11,14 @@ * SPDX-License-Identifier: Apache-2.0 ********************************************************************************/ -#ifndef SCORE_CRYPTO_SRC_API_FUTURE_CONTEXTS_I_CERTIFICATE_MANAGEMENT_CONTEXT_HPP -#define SCORE_CRYPTO_SRC_API_FUTURE_CONTEXTS_I_CERTIFICATE_MANAGEMENT_CONTEXT_HPP +#ifndef SCORE_CRYPTO_SRC_API_CONTEXTS_I_CERTIFICATE_MANAGEMENT_CONTEXT_HPP +#define SCORE_CRYPTO_SRC_API_CONTEXTS_I_CERTIFICATE_MANAGEMENT_CONTEXT_HPP #include "score/crypto/src/api/certificate/i_ocsp_request_export.hpp" #include "score/crypto/src/api/common/crypto_resource_guard.hpp" #include "score/crypto/src/api/common/types.hpp" #include "score/crypto/src/api/contexts/i_context.hpp" -#include "score/crypto/src/api/future/objects/i_certificate_object.hpp" +#include "score/crypto/src/api/objects/i_certificate_object.hpp" #include "score/result/result.h" #include "score/span.hpp" @@ -44,8 +44,8 @@ namespace crypto /// ICertificateObject goes out of scope). /// - **Export / convert** using a two-call pattern: query the required buffer /// size first, then fill the caller-supplied span. -/// - **Slot management**, **CRL management**, and **trust store management** -/// are all co-located here. +/// - **Slot management** and **trust store management** are co-located here. +/// - CRL, OCSP, and CSR operations are defined in future/ and not yet active. /// /// **ParseCertificate lifecycle**: /// @code @@ -170,46 +170,10 @@ class ICertificateManagementContext : public IContext /// @brief Queries the occupancy and metadata of a certificate slot. /// @param slot Handle to the slot (type = kCertSlot) - /// @return CertificateSlotInfo with occupancy, algorithm, and provider binding + /// @return CertificateSlotInfo with certificate-slot state and CRL presence virtual score::Result GetCertificateSlotInfo(const CryptoResourceId& slot) = 0; - // ---- CRL management ---- - - /// @brief Imports a Certificate Revocation List and associates it with its issuer. - /// - /// The returned CryptoResourceId uses ResourceType::kCrl and carries the - /// same numeric id as the resolved issuer certificate — callers can look up - /// the applicable CRL for any issuer by re-resolving with ResourceType::kCrl. - /// - /// @param crl_data Encoded CRL data - /// @param format Encoding format of the CRL - /// @param issuer_cert Handle to the issuer certificate - /// (type = kCertificate or kCertSlot). The daemon validates that the - /// CRL issuer DN matches and that the CRL signature is correct. - /// @param persist When true, the CRL survives the guard's destructor - /// (kPersistent — daemon retains the CRL when the client releases its - /// reference). When false, the CRL is deleted when the guard is destroyed. - /// @return Guard wrapping a handle with type = kCrl. Pass guard.GetId() to - /// SetCrl() or re-resolve via ResourceType::kCrl on the issuer id. - virtual score::Result ImportCrl(score::cpp::span crl_data, - FormatType format, - const CryptoResourceId& issuer_cert, - bool persist) = 0; - - /// @brief Removes a specific CRL from the store. - /// @param crl Handle to the CRL to delete (type = kCrl) - /// @return std::monostate on success, error if the CRL is not found - virtual score::Result DeleteCrl(const CryptoResourceId& crl) = 0; - - /// @brief Bulk-deletes expired CRLs from the store. - /// @return Number of CRLs deleted - virtual score::Result DeleteExpiredCrls() = 0; - - /// @brief Bulk-deletes expired certificates from persistent slots. - /// @return Number of certificates deleted - virtual score::Result DeleteExpiredCertificates() = 0; - - // ---- Key extraction and OCSP ---- + // ---- Key extraction ---- /// @brief Extracts the public key from a certificate as an ephemeral key resource. /// @@ -223,17 +187,64 @@ class ICertificateManagementContext : public IContext virtual score::Result> LoadCertificatePublicKey( const CryptoResourceId& cert) = 0; - /// @brief Constructs an OCSP request for a certificate's revocation status. + /// @brief Bulk-deletes expired certificates from persistent slots. + /// @return Number of certificates deleted + virtual score::Result DeleteExpiredCertificates() = 0; + + // ---- CRL management (not yet active — IPC implementation pending) ---- +#if 0 + /// @brief Imports a Certificate Revocation List and associates it with its issuer certificate. /// - /// The returned object exposes the DER-encoded OCSP request and the responder URL. - /// Send the request to the URL via HTTP POST, then feed the response to - /// ICertificateVerificationContext::SetOcspResponse(). + /// The CRL lifecycle follows the lifecycle of @p issuer_cert: + /// - kCertSlot + persist=true: CRL written to the slot's [crl] section; write access required. + /// - kCertSlot or kCertificate + persist=false (default): session-scoped; no write access needed. + /// + /// @param crl_data Encoded CRL data + /// @param format Encoding format of the CRL + /// @param issuer_cert Handle to the issuer certificate (type = kCertSlot or kCertificate) + /// @param persist When true, store permanently to the issuer slot (kCertSlot only). + /// @return std::monostate on success, error if validation fails or access is denied + virtual score::Result ImportCrl(score::cpp::span crl_data, + FormatType format, + const CryptoResourceId& issuer_cert, + bool persist = false) = 0; + + /// @brief Removes the CRL stored in a certificate slot. + /// @param cert_slot Handle to the slot whose CRL should be removed (type = kCertSlot) + /// @return std::monostate on success, error if no CRL is present or access is denied + virtual score::Result DeleteCrl(const CryptoResourceId& cert_slot) = 0; + + /// @brief Bulk-deletes expired CRLs across all certificate slots. + /// @return Number of CRLs deleted + virtual score::Result DeleteExpiredCrls() = 0; +#endif // CRL management + + // ---- OCSP (not yet active — IPC implementation pending) ---- +#if 0 + /// @brief Constructs an OCSP request for a certificate's revocation status. /// /// @param cert Handle to the certificate to check (type = kCertificate or kCertSlot) - /// @param issuer_cert Handle to the issuer certificate (required for OCSP request construction) - /// @return Export object providing the encoded request and responder URL + /// @param issuer_cert Handle to the issuer certificate + /// @return Export object providing the DER-encoded request and responder URL virtual score::Result GetOcspRequestData(const CryptoResourceId& cert, const CryptoResourceId& issuer_cert) = 0; +#endif // OCSP + + // ---- Trust-store membership management ---- + + /// @brief Adds a certificate to a persistent trust store. + /// + /// The certificate is assigned to a trust-store-managed slot. This is a + /// write operation and requires trust-store write access. + virtual score::Result AddCertificateToTrustStore(const CryptoResourceId& trust_store, + const CryptoResourceId& cert) = 0; + + /// @brief Removes a certificate from a persistent trust store by SHA-256 fingerprint. + /// + /// This changes trust-store deployment state and requires write access. + virtual score::Result RemoveCertificateFromTrustStore( + const CryptoResourceId& trust_store, + score::cpp::span certificate_fingerprint) = 0; protected: ICertificateManagementContext() = default; @@ -243,4 +254,4 @@ class ICertificateManagementContext : public IContext } // namespace score -#endif // SCORE_CRYPTO_SRC_API_FUTURE_CONTEXTS_I_CERTIFICATE_MANAGEMENT_CONTEXT_HPP +#endif // SCORE_CRYPTO_SRC_API_CONTEXTS_I_CERTIFICATE_MANAGEMENT_CONTEXT_HPP diff --git a/score/crypto/src/api/future/contexts/i_certificate_verification_context.hpp b/score/crypto/src/api/contexts/i_certificate_verification_context.hpp similarity index 53% rename from score/crypto/src/api/future/contexts/i_certificate_verification_context.hpp rename to score/crypto/src/api/contexts/i_certificate_verification_context.hpp index 6dea3ea90..f8059f9a6 100644 --- a/score/crypto/src/api/future/contexts/i_certificate_verification_context.hpp +++ b/score/crypto/src/api/contexts/i_certificate_verification_context.hpp @@ -11,8 +11,8 @@ * SPDX-License-Identifier: Apache-2.0 ********************************************************************************/ -#ifndef SCORE_CRYPTO_SRC_API_FUTURE_CONTEXTS_I_CERTIFICATE_VERIFICATION_CONTEXT_HPP -#define SCORE_CRYPTO_SRC_API_FUTURE_CONTEXTS_I_CERTIFICATE_VERIFICATION_CONTEXT_HPP +#ifndef SCORE_CRYPTO_SRC_API_CONTEXTS_I_CERTIFICATE_VERIFICATION_CONTEXT_HPP +#define SCORE_CRYPTO_SRC_API_CONTEXTS_I_CERTIFICATE_VERIFICATION_CONTEXT_HPP #include "score/crypto/src/api/certificate/cert_types.hpp" #include "score/crypto/src/api/common/types.hpp" @@ -41,19 +41,18 @@ namespace crypto /// auto ctx = crypto_context->CreateCertificateVerificationContext(config).value(); /// ctx->SetCertificate(leaf_cert); /// ctx->SetVerificationTrustStore(system_trust_store); -/// ctx->SetRevocationCheckPolicy(RevocationCheckPolicy::kOcspWithCrlFallback); -/// ctx->SetOcspResponse(ocsp_response_data); +/// ctx->SetRevocationCheckPolicy(RevocationCheckPolicy::kCrlOnly); /// auto result = ctx->Verify(); /// @endcode /// -/// @par Example — chain verification with additional trust anchors +/// @par Example — chain verification with additional untrusted certificates /// @code /// // ext_ca is a kCertificate from ParseCertificate() — not persisted. /// std::array extra = {ext_ca->GetId()}; /// auto ctx = crypto_context->CreateCertificateVerificationContext(config).value(); /// ctx->SetCertificateChain(chain); /// ctx->SetVerificationTrustStore(system_trust_store); -/// ctx->SetAdditionalTrustAnchors(extra); // local to this context, no system store change +/// ctx->SetAdditionalCertificates(extra); // untrusted chain-building inputs /// auto result = ctx->Verify(); /// @endcode class ICertificateVerificationContext : public IContext @@ -93,36 +92,60 @@ class ICertificateVerificationContext : public IContext /// @return std::monostate on success, error if handle is invalid virtual score::Result SetVerificationTrustStore(const CryptoResourceId& trust_store) = 0; - /// @brief Supplies additional trust anchors for this verification, beyond the system trust store. + /// @brief Sets the standalone trusted certificates for this verification context. /// - /// Use this to trust certificates not provisioned in any system trust store — - /// e.g. an external CA received at runtime, a rotation candidate, or a test root. - /// Does not modify the system trust store; these roots are local to this context instance. + /// This mode is mutually exclusive with SetVerificationTrustStore(). Each + /// call replaces the previously configured set, so callers that discover + /// anchors incrementally must collect them before calling this method. /// - /// Accepts both `kCertificate` (parsed, not persisted) and `kCertSlot` handles. - /// The daemon validates each certificate immediately: - /// - Expired certificates cause the whole call to fail. - /// - Certificates already present in the configured system trust store are - /// accepted silently (idempotent union — no duplicate anchors). + /// @param certs Span of certificate handles to treat as trust anchors + /// (type = kCertificate or kCertSlot) + /// @return std::monostate on success, error if any handle is invalid + virtual score::Result SetTrustedCertificates( + score::cpp::span certs) = 0; + + /// @brief Selects the chain termination rule for trust-store verification. /// - /// Replaces any previously set additional anchors on this context (set semantics). + /// The default is ChainTerminationPolicy::kRootRequired. This setting has + /// no effect in standalone trusted-certificate mode. + virtual score::Result SetChainTerminationPolicy(ChainTerminationPolicy policy) = 0; + + /// @brief Supplies additional untrusted certificates for chain building. /// - /// @param anchors Span of certificate handles (type = kCertificate or kCertSlot) - /// @return std::monostate on success, error if any handle is invalid or any certificate has expired - virtual score::Result SetAdditionalTrustAnchors( - score::cpp::span anchors) = 0; - - /// @brief Provides an OCSP response for revocation checking. - /// @param response_data DER-encoded OCSP response - /// @return std::monostate on success, error on parse failure - /// @note The context internally validates the OCSP responder's certificate - /// chain against the configured verification trust store. - virtual score::Result SetOcspResponse(score::cpp::span response_data) = 0; - - /// @brief References an imported CRL for revocation checking. - /// @param crl Handle to a previously imported CRL - /// @return std::monostate on success, error if handle is invalid - virtual score::Result SetCrl(const CryptoResourceId& crl) = 0; + /// Use this for intermediate certificates that are not provisioned in the + /// system trust store, such as an intermediate received with a peer chain. + /// These certificates are local to this context and do not establish trust. + /// + /// Accepts `kCertificate` and `kCertSlot` handles. Certificates already + /// present in the trust store are deduplicated by fingerprint. The daemon + /// uses these objects only as untrusted chain-building inputs; trust is + /// established exclusively by the configured trust store or standalone + /// trusted certificates. + /// + /// Replaces any previously set additional certificates on this context. + /// + /// @param certificates Span of untrusted certificate handles + /// (type = kCertificate or kCertSlot) + /// @return std::monostate on success, or an error if a handle is invalid + virtual score::Result SetAdditionalCertificates( + score::cpp::span certificates) = 0; + + // ---- OCSP (not yet active — IPC implementation pending) ---- +#if 0 + /// @brief Provides one or more OCSP responses for revocation checking. + /// + /// Each entry is a DER-encoded OCSP response. Supplying multiple responses + /// covers chains where both the leaf and one or more intermediates have + /// stapled OCSP responses (e.g. TLS 1.3 certificate_status records). + /// The daemon matches each response to the appropriate certificate in the + /// chain by the certID field embedded in the response; order does not matter. + /// Replaces any previously set responses on this context. + /// + /// @param responses Span of DER-encoded OCSP response byte spans + /// @return std::monostate on success, error if any response fails to parse + virtual score::Result SetOcspResponses( + score::cpp::span> responses) = 0; +#endif // OCSP /// @brief Overrides the verification time. /// @param epoch_seconds Verification time as seconds since Unix epoch @@ -144,6 +167,25 @@ class ICertificateVerificationContext : public IContext /// @note At minimum, a certificate (or chain) and trust anchor must be set. virtual score::Result Verify() = 0; + /// @brief Returns the number of certificates in the verified chain. + /// + /// Valid only after a successful Verify() call. The length is stable between + /// this call and GetVerifiedChain() provided no intervening Verify() is made. + /// + /// @return Number of entries in the chain (leaf to terminating anchor inclusive), + /// or an error if Verify() has not yet succeeded. + virtual score::Result GetVerifiedChainLength() const = 0; + + /// @brief Fills caller-provided buffer with verified chain certificate IDs. + /// + /// Certificates are ordered leaf-first, terminating anchor last. + /// The caller must size @p out to at least GetVerifiedChainLength() entries. + /// + /// @param out Caller-allocated span of CryptoResourceId to fill + /// @return Number of entries written, or an error if @p out is too small + /// or Verify() has not yet succeeded. + virtual score::Result GetVerifiedChain(score::cpp::span out) const = 0; + protected: ICertificateVerificationContext() = default; }; @@ -152,4 +194,4 @@ class ICertificateVerificationContext : public IContext } // namespace score -#endif // SCORE_CRYPTO_SRC_API_FUTURE_CONTEXTS_I_CERTIFICATE_VERIFICATION_CONTEXT_HPP +#endif // SCORE_CRYPTO_SRC_API_CONTEXTS_I_CERTIFICATE_VERIFICATION_CONTEXT_HPP diff --git a/score/crypto/src/api/future/certificate/BUILD b/score/crypto/src/api/future/certificate/BUILD index 877c2b40e..46c8ddd5e 100644 --- a/score/crypto/src/api/future/certificate/BUILD +++ b/score/crypto/src/api/future/certificate/BUILD @@ -17,12 +17,13 @@ load("@rules_cc//cc:defs.bzl", "cc_library") +# cert_types.hpp and i_ocsp_request_export.hpp moved to +# //score/crypto/src/api/certificate:cert_types (active). + # cc_library( # name = "crypto_certificate", # hdrs = [ -# "cert_types.hpp", # "i_csr_export.hpp", -# "i_ocsp_request_export.hpp", # ], # deps = [ # "//score/crypto/src/api/common:crypto_common", diff --git a/score/crypto/src/api/future/config/BUILD b/score/crypto/src/api/future/config/BUILD index bf631ee2c..0697cc05b 100644 --- a/score/crypto/src/api/future/config/BUILD +++ b/score/crypto/src/api/future/config/BUILD @@ -18,12 +18,13 @@ load("@rules_cc//cc:defs.bzl", "cc_library") +# certificate_context_config.hpp and certificate_verification_context_config.hpp +# moved to //score/crypto/src/api/config:cert_context_configs (active). + # cc_library( # name = "future_context_configs", # hdrs = [ # "aead_context_config.hpp", -# "certificate_context_config.hpp", -# "certificate_verification_context_config.hpp", # "cipher_context_config.hpp", # "csr_generation_context_config.hpp", # "random_context_config.hpp", diff --git a/score/crypto/src/api/future/contexts/BUILD b/score/crypto/src/api/future/contexts/BUILD index b425f9caf..b84dfa1f3 100644 --- a/score/crypto/src/api/future/contexts/BUILD +++ b/score/crypto/src/api/future/contexts/BUILD @@ -62,18 +62,18 @@ load("@rules_cc//cc:defs.bzl", "cc_library") # ], # ) -# -- Certificate management + verification + CSR -- +# i_certificate_management_context.hpp and i_certificate_verification_context.hpp +# moved to //score/crypto/src/api/contexts:cert_contexts (active). + +# -- CSR -- # cc_library( -# name = "certificate_contexts", +# name = "csr_generation_context", # hdrs = [ -# "i_certificate_management_context.hpp", -# "i_certificate_verification_context.hpp", # "i_csr_generation_context.hpp", # ], # deps = [ # "//score/crypto/src/api/contexts:context_bases", # "//score/crypto/src/api/common:crypto_common", -# "//score/crypto/src/api/future/certificate:crypto_certificate", # "@score_baselibs//score/result", # ], # ) diff --git a/score/crypto/src/api/future/objects/BUILD b/score/crypto/src/api/future/objects/BUILD index 2b0c6ec0f..6a6bf45e7 100644 --- a/score/crypto/src/api/future/objects/BUILD +++ b/score/crypto/src/api/future/objects/BUILD @@ -17,11 +17,12 @@ load("@rules_cc//cc:defs.bzl", "cc_library") +# i_certificate_object.hpp and i_cert_slot_object.hpp moved to +# //score/crypto/src/api/objects:cert_objects (active). + # cc_library( # name = "future_crypto_objects", # hdrs = [ -# "i_cert_slot_object.hpp", -# "i_certificate_object.hpp", # "i_data_object.hpp", # "i_provider_object.hpp", # "i_secure_object.hpp", diff --git a/score/crypto/src/api/objects/BUILD b/score/crypto/src/api/objects/BUILD index 0b45142c5..1702080ff 100644 --- a/score/crypto/src/api/objects/BUILD +++ b/score/crypto/src/api/objects/BUILD @@ -31,3 +31,19 @@ cc_library( "@score_baselibs//score/result", ], ) + +cc_library( + name = "cert_objects", + hdrs = [ + "i_cert_slot_object.hpp", + "i_certificate_object.hpp", + ], + includes = ["."], + visibility = ["//visibility:public"], + deps = [ + ":crypto_objects", + "//score/crypto/src/api/common:crypto_common", + "@score_baselibs//score/language/futurecpp", + "@score_baselibs//score/result", + ], +) diff --git a/score/crypto/src/api/future/objects/i_cert_slot_object.hpp b/score/crypto/src/api/objects/i_cert_slot_object.hpp similarity index 88% rename from score/crypto/src/api/future/objects/i_cert_slot_object.hpp rename to score/crypto/src/api/objects/i_cert_slot_object.hpp index bd778556f..d6444b5d5 100644 --- a/score/crypto/src/api/future/objects/i_cert_slot_object.hpp +++ b/score/crypto/src/api/objects/i_cert_slot_object.hpp @@ -11,8 +11,8 @@ * SPDX-License-Identifier: Apache-2.0 ********************************************************************************/ -#ifndef SCORE_CRYPTO_SRC_API_FUTURE_OBJECTS_I_CERT_SLOT_OBJECT_HPP -#define SCORE_CRYPTO_SRC_API_FUTURE_OBJECTS_I_CERT_SLOT_OBJECT_HPP +#ifndef SCORE_CRYPTO_SRC_API_OBJECTS_I_CERT_SLOT_OBJECT_HPP +#define SCORE_CRYPTO_SRC_API_OBJECTS_I_CERT_SLOT_OBJECT_HPP #include "score/crypto/src/api/objects/i_crypto_object.hpp" @@ -51,4 +51,4 @@ class ICertSlotObject : public ICryptoObject } // namespace score -#endif // SCORE_CRYPTO_SRC_API_FUTURE_OBJECTS_I_CERT_SLOT_OBJECT_HPP +#endif // SCORE_CRYPTO_SRC_API_OBJECTS_I_CERT_SLOT_OBJECT_HPP diff --git a/score/crypto/src/api/future/objects/i_certificate_object.hpp b/score/crypto/src/api/objects/i_certificate_object.hpp similarity index 95% rename from score/crypto/src/api/future/objects/i_certificate_object.hpp rename to score/crypto/src/api/objects/i_certificate_object.hpp index e3ed4992d..d09a62224 100644 --- a/score/crypto/src/api/future/objects/i_certificate_object.hpp +++ b/score/crypto/src/api/objects/i_certificate_object.hpp @@ -11,8 +11,8 @@ * SPDX-License-Identifier: Apache-2.0 ********************************************************************************/ -#ifndef SCORE_CRYPTO_SRC_API_FUTURE_OBJECTS_I_CERTIFICATE_OBJECT_HPP -#define SCORE_CRYPTO_SRC_API_FUTURE_OBJECTS_I_CERTIFICATE_OBJECT_HPP +#ifndef SCORE_CRYPTO_SRC_API_OBJECTS_I_CERTIFICATE_OBJECT_HPP +#define SCORE_CRYPTO_SRC_API_OBJECTS_I_CERTIFICATE_OBJECT_HPP #include "score/crypto/src/api/objects/i_crypto_object.hpp" #include "score/result/result.h" @@ -105,4 +105,4 @@ class ICertificateObject : public ICryptoObject } // namespace score -#endif // SCORE_CRYPTO_SRC_API_FUTURE_OBJECTS_I_CERTIFICATE_OBJECT_HPP +#endif // SCORE_CRYPTO_SRC_API_OBJECTS_I_CERTIFICATE_OBJECT_HPP From f6830b6950e2ea183ea9366996dd2a299b8a4f87 Mon Sep 17 00:00:00 2001 From: Athul Mallappallil Date: Thu, 30 Jul 2026 22:00:40 +0200 Subject: [PATCH 03/37] Utility fns to Enc/Dec Str to Hex val --- score/crypto/src/daemon/common/BUILD | 1 + score/crypto/src/daemon/common/hex.hpp | 64 ++++++++++++++++++++++++++ 2 files changed, 65 insertions(+) create mode 100644 score/crypto/src/daemon/common/hex.hpp diff --git a/score/crypto/src/daemon/common/BUILD b/score/crypto/src/daemon/common/BUILD index 1cfebd98d..03b571a32 100644 --- a/score/crypto/src/daemon/common/BUILD +++ b/score/crypto/src/daemon/common/BUILD @@ -18,6 +18,7 @@ cc_library( hdrs = [ "actors.hpp", "daemon_error.hpp", + "hex.hpp", "secure_memory.hpp", "types.hpp", ], diff --git a/score/crypto/src/daemon/common/hex.hpp b/score/crypto/src/daemon/common/hex.hpp new file mode 100644 index 000000000..18cc79872 --- /dev/null +++ b/score/crypto/src/daemon/common/hex.hpp @@ -0,0 +1,64 @@ +/******************************************************************************** + * 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 + ********************************************************************************/ + +#ifndef SCORE_CRYPTO_SRC_DAEMON_COMMON_HEX_HPP +#define SCORE_CRYPTO_SRC_DAEMON_COMMON_HEX_HPP + +#include "score/crypto/common/types.hpp" + +#include +#include +#include +#include +#include + +namespace score::crypto::daemon::common +{ + +[[nodiscard]] inline std::string EncodeHex(score::crypto::span bytes) +{ + static constexpr char kHex[] = "0123456789abcdef"; + std::string result; + result.reserve(bytes.size() * 2U); + for (const auto byte : bytes) + { + result.push_back(kHex[(byte >> 4U) & 0x0FU]); + result.push_back(kHex[byte & 0x0FU]); + } + return result; +} + +[[nodiscard]] inline std::optional> DecodeHex(const std::string& value) +{ + if ((value.size() % 2U) != 0U) + return std::nullopt; + + const auto nibble = [](char c) -> int { + c = static_cast(std::tolower(static_cast(c))); + return c >= '0' && c <= '9' ? c - '0' : (c >= 'a' && c <= 'f' ? c - 'a' + 10 : -1); + }; + std::vector result(value.size() / 2U); + for (std::size_t i = 0U; i < result.size(); ++i) + { + const int high = nibble(value[2U * i]); + const int low = nibble(value[2U * i + 1U]); + if (high < 0 || low < 0) + return std::nullopt; + result[i] = static_cast((high << 4) | low); + } + return result; +} + +} // namespace score::crypto::daemon::common + +#endif // SCORE_CRYPTO_SRC_DAEMON_COMMON_HEX_HPP From 71e76a312df2f207c2ed2bc257565eaa55e69477 Mon Sep 17 00:00:00 2001 From: Athul Mallappallil Date: Fri, 7 Aug 2026 20:52:18 +0000 Subject: [PATCH 04/37] Pvdr mgr fix and capability selection --- score/crypto/src/daemon/common/types.hpp | 44 ++++++ .../crypto/src/daemon/provider/i_provider.hpp | 33 +++++ .../src/daemon/provider/provider_manager.hpp | 25 ++++ .../daemon/provider/src/provider_manager.cpp | 91 ++++++++++-- .../provider/src/provider_manager_test.cpp | 135 +++++++++++++++++- 5 files changed, 317 insertions(+), 11 deletions(-) diff --git a/score/crypto/src/daemon/common/types.hpp b/score/crypto/src/daemon/common/types.hpp index 9f3bdfdf0..55b112cb2 100644 --- a/score/crypto/src/daemon/common/types.hpp +++ b/score/crypto/src/daemon/common/types.hpp @@ -180,6 +180,50 @@ inline CryptoProviderType CryptoProviderTypeFromString(const std::string& typeSt } } +/** + * @brief Functional capabilities a crypto provider can offer. + * + * Capabilities are orthogonal to CryptoProviderType (HARDWARE/SOFTWARE/...): a + * single provider may offer several at once. ProviderManager uses them to select + * a default provider for a specific functional area (e.g. certificate + * operations) instead of relying on the hardware/software category alone — a + * DEFAULT provider is not guaranteed to support every functional area. + * + * Values are a bitmask so a provider's full capability set fits in one field. + */ +enum class ProviderCapability : std::uint8_t +{ + kNone = 0x00U, ///< No functional capability advertised + kCrypto = 0x01U, ///< Symmetric cipher / hash / MAC handlers (GetCryptoHandlerFactory) + kKeyManagement = 0x02U, ///< Key generation / storage (GetKeyFactory / GetKeySlotHandler) + kCertManagement = 0x04U, ///< Certificate parse / verify / CSR (GetCertFactory) +}; + +/// @brief Bitwise OR for combining provider capabilities. +inline constexpr ProviderCapability operator|(ProviderCapability lhs, ProviderCapability rhs) noexcept +{ + return static_cast(static_cast(lhs) | static_cast(rhs)); +} + +/// @brief Bitwise AND for testing provider capabilities. +inline constexpr ProviderCapability operator&(ProviderCapability lhs, ProviderCapability rhs) noexcept +{ + return static_cast(static_cast(lhs) & static_cast(rhs)); +} + +/// @brief Bitwise OR-assign for accumulating provider capabilities. +inline constexpr ProviderCapability& operator|=(ProviderCapability& lhs, ProviderCapability rhs) noexcept +{ + lhs = lhs | rhs; + return lhs; +} + +/// @brief True when @p caps includes every capability bit in @p required. +inline constexpr bool HasCapability(ProviderCapability caps, ProviderCapability required) noexcept +{ + return (caps & required) == required; +} + } // namespace score::crypto::daemon::common /** diff --git a/score/crypto/src/daemon/provider/i_provider.hpp b/score/crypto/src/daemon/provider/i_provider.hpp index b7dc53b57..bbe001ed8 100644 --- a/score/crypto/src/daemon/provider/i_provider.hpp +++ b/score/crypto/src/daemon/provider/i_provider.hpp @@ -147,6 +147,39 @@ class IProvider { return nullptr; } + + // ----------------------------------------------------------------------- + // Capability advertisement + // ----------------------------------------------------------------------- + + /// @brief Report the functional capabilities this provider offers. + /// + /// The default derives the capability set directly from the capability + /// accessors above — the single source of truth — so a provider that + /// returns a non-null factory automatically advertises the matching + /// capability. Providers whose capability cannot be inferred from an + /// accessor (e.g. a backend that offers certificate operations only through + /// an extension) override this to declare the correct set. + /// + /// Used by ProviderManager::GetProviderForCapability() to pick a default + /// provider for a functional area rather than by hardware/software category. + [[nodiscard]] virtual common::ProviderCapability GetProviderCapabilities() + { + common::ProviderCapability caps = common::ProviderCapability::kNone; + if (GetCryptoHandlerFactory() != nullptr) + { + caps |= common::ProviderCapability::kCrypto; + } + if (GetKeyFactory() != nullptr) + { + caps |= common::ProviderCapability::kKeyManagement; + } + if (GetCertFactory() != nullptr) + { + caps |= common::ProviderCapability::kCertManagement; + } + return caps; + } }; } // namespace score::crypto::daemon::provider diff --git a/score/crypto/src/daemon/provider/provider_manager.hpp b/score/crypto/src/daemon/provider/provider_manager.hpp index 38fead7c8..eb0ef1193 100644 --- a/score/crypto/src/daemon/provider/provider_manager.hpp +++ b/score/crypto/src/daemon/provider/provider_manager.hpp @@ -160,6 +160,31 @@ class ProviderManager */ std::shared_ptr GetProvider(common::CryptoProviderType cryptoType) const; + /** + * @brief Select the preferred initialized provider that offers a capability. + * + * Filters registered providers to those that are initialized and advertise + * @p capability (via IProvider::GetProviderCapabilities()), then chooses + * among them by @p preferenceOrder over CryptoProviderType. Falls back to + * the lowest-id capable provider when none match the preference. + * + * This is the capability-aware counterpart to GetProvider(CryptoProviderType): + * callers that need a functional area (e.g. certificate operations) but do + * not care which specific provider serves it use this instead of the + * category-only default, so a DEFAULT provider that lacks the capability is + * never returned. Most certificate callers rely on this default resolution; + * clients that must pin a specific provider resolve it by name/ID instead. + * + * @param capability The functional capability the provider must offer. + * @param preferenceOrder Category priority among capable providers. Defaults + * to HARDWARE → SOFTWARE; certificate callers pass SOFTWARE first. + * @return The selected provider, or nullptr if none offers the capability. + */ + [[nodiscard]] std::shared_ptr GetProviderForCapability( + common::ProviderCapability capability, + const std::vector& preferenceOrder = {common::CryptoProviderType::HARDWARE, + common::CryptoProviderType::SOFTWARE}) const; + /** * @brief Set a provider as default for a specific crypto provider type * diff --git a/score/crypto/src/daemon/provider/src/provider_manager.cpp b/score/crypto/src/daemon/provider/src/provider_manager.cpp index 78f3eb632..b2f080f0b 100644 --- a/score/crypto/src/daemon/provider/src/provider_manager.cpp +++ b/score/crypto/src/daemon/provider/src/provider_manager.cpp @@ -80,30 +80,65 @@ void ProviderManager::RecordFactoryResult(const std::string& factoryName, Provid common::ProviderName ProviderManager::ResolveDefaultProviderName( const std::vector& preferenceOrder) { - std::unordered_map byType; - common::ProviderName anyName; - for (const auto& [name, entry] : m_providers) + // Prefer providers that initialized successfully so the default maps to a + // usable provider whenever one exists — InitializeAll() has already run by + // the time Initialize() calls this. Registered-but-failed providers are kept + // as a fallback only: if none initialized yet (e.g. all temporarily + // unavailable at startup), the default still resolves to a registered name + // so it can be retried on a later lookup via EnsureProviderInitialized(). + std::unordered_map initializedByType; + std::unordered_map registeredByType; + common::ProviderName anyInitializedName; + common::ProviderName anyRegisteredName; + + // Iterate in numeric-id (registration) order for deterministic selection. + for (common::ProviderId id = 0; id < m_provider_by_id.size(); ++id) { + const auto& entry = m_providers.at(m_name_by_id[id]); if (!entry.instance) { continue; } - byType.emplace(entry.cryptoType, name); - if (anyName.empty()) + registeredByType.emplace(entry.cryptoType, entry.name); + if (anyRegisteredName.empty()) { - anyName = name; + anyRegisteredName = entry.name; + } + if (entry.instance->IsInitialized()) + { + initializedByType.emplace(entry.cryptoType, entry.name); + if (anyInitializedName.empty()) + { + anyInitializedName = entry.name; + } } } + // First choice: preferred type among successfully-initialized providers. for (const auto& preferred : preferenceOrder) { - auto it = byType.find(preferred); - if (it != byType.end()) + const auto it = initializedByType.find(preferred); + if (it != initializedByType.end()) { return it->second; } } - return anyName; + if (!anyInitializedName.empty()) + { + return anyInitializedName; + } + + // Fallback: nothing initialized yet — resolve to a registered provider so a + // temporarily-unavailable provider can still be retried on later lookups. + for (const auto& preferred : preferenceOrder) + { + const auto it = registeredByType.find(preferred); + if (it != registeredByType.end()) + { + return it->second; + } + } + return anyRegisteredName; } bool ProviderManager::BuildTypeMappings( @@ -235,6 +270,44 @@ std::shared_ptr ProviderManager::GetProvider(common::CryptoProviderTy return provider_entry.instance; } +std::shared_ptr ProviderManager::GetProviderForCapability( + common::ProviderCapability capability, + const std::vector& preferenceOrder) const +{ + std::unordered_map> byType; + std::shared_ptr firstCapable; + + // Iterate in numeric-id order so the fallback selection is deterministic. + for (common::ProviderId id = 0; id < m_provider_by_id.size(); ++id) + { + auto& entry = const_cast(m_providers.at(m_name_by_id[id])); + if (!EnsureProviderInitialized(entry)) + { + continue; + } + if (!common::HasCapability(entry.instance->GetProviderCapabilities(), capability)) + { + continue; + } + // First capable provider of each category wins; earlier ids take priority. + byType.emplace(entry.cryptoType, entry.instance); + if (!firstCapable) + { + firstCapable = entry.instance; + } + } + + for (const auto& preferred : preferenceOrder) + { + const auto it = byType.find(preferred); + if (it != byType.end()) + { + return it->second; + } + } + return firstCapable; +} + bool ProviderManager::SetDefaultProviderForType(common::CryptoProviderType cryptoType, common::ProviderId providerId) { // Verify the provider exists by numeric ID and is initialized diff --git a/score/crypto/src/daemon/provider/src/provider_manager_test.cpp b/score/crypto/src/daemon/provider/src/provider_manager_test.cpp index 7cb656dd6..c3daef732 100644 --- a/score/crypto/src/daemon/provider/src/provider_manager_test.cpp +++ b/score/crypto/src/daemon/provider/src/provider_manager_test.cpp @@ -31,8 +31,11 @@ namespace class ConfigurableStubProvider final : public provider::IProvider { public: - ConfigurableStubProvider(const std::string& name, common::ProviderId id, bool fail_init) - : m_name{name}, m_id{id}, m_fail_init{fail_init} + ConfigurableStubProvider(const std::string& name, + common::ProviderId id, + bool fail_init, + common::ProviderCapability capabilities = common::ProviderCapability::kNone) + : m_name{name}, m_id{id}, m_fail_init{fail_init}, m_capabilities{capabilities} { } @@ -65,10 +68,16 @@ class ConfigurableStubProvider final : public provider::IProvider return m_name; } + common::ProviderCapability GetProviderCapabilities() override + { + return m_capabilities; + } + private: std::string m_name; common::ProviderId m_id; bool m_fail_init; + common::ProviderCapability m_capabilities; bool m_initialized{false}; }; } // namespace @@ -186,3 +195,125 @@ TEST(ProviderManagerInitStateTest, FailedProviderRemainsRegisteredButUnavailable EXPECT_TRUE(mgr.GetProviderType("OK_PROVIDER").has_value()); EXPECT_TRUE(mgr.GetProviderType("FAIL_PROVIDER").has_value()); } + +// =========================================================================== +// GetProviderForCapability +// =========================================================================== + +namespace +{ +// SW provider offers crypto + cert; HW provider offers crypto + key management. +provider::ProviderManager::Sptr MakeCapabilityManager() +{ + score::crypto::daemon::config::Config config; + auto mgr = std::make_shared(config.GetProviderInitConfig()); + + mgr->RegisterProvider( + "SW_PROVIDER", + std::make_shared( + "SW_PROVIDER", 0, false, + common::ProviderCapability::kCrypto | common::ProviderCapability::kCertManagement), + common::CryptoProviderType::SOFTWARE); + + mgr->RegisterProvider( + "HW_PROVIDER", + std::make_shared( + "HW_PROVIDER", 1, false, + common::ProviderCapability::kCrypto | common::ProviderCapability::kKeyManagement), + common::CryptoProviderType::HARDWARE); + + mgr->Initialize(); + return mgr; +} +} // namespace + +TEST(ProviderManagerCapabilityTest, ReturnsOnlyCapableProvider) +{ + auto mgr = MakeCapabilityManager(); + // Only the SW provider advertises certificate capability. + auto cert_prov = mgr->GetProviderForCapability(common::ProviderCapability::kCertManagement); + ASSERT_NE(cert_prov, nullptr); + EXPECT_EQ(cert_prov->GetProviderName(), "SW_PROVIDER"); +} + +TEST(ProviderManagerCapabilityTest, PreferenceOrderPicksAmongCapableProviders) +{ + auto mgr = MakeCapabilityManager(); + // Both providers offer crypto; preference decides which is returned. + auto sw_first = mgr->GetProviderForCapability( + common::ProviderCapability::kCrypto, + {common::CryptoProviderType::SOFTWARE, common::CryptoProviderType::HARDWARE}); + ASSERT_NE(sw_first, nullptr); + EXPECT_EQ(sw_first->GetProviderName(), "SW_PROVIDER"); + + auto hw_first = mgr->GetProviderForCapability( + common::ProviderCapability::kCrypto, + {common::CryptoProviderType::HARDWARE, common::CryptoProviderType::SOFTWARE}); + ASSERT_NE(hw_first, nullptr); + EXPECT_EQ(hw_first->GetProviderName(), "HW_PROVIDER"); +} + +TEST(ProviderManagerCapabilityTest, ReturnsNullWhenNoProviderOffersCapability) +{ + score::crypto::daemon::config::Config config; + provider::ProviderManager mgr(config.GetProviderInitConfig()); + mgr.RegisterProvider("SW_PROVIDER", + std::make_shared( + "SW_PROVIDER", 0, false, common::ProviderCapability::kCrypto), + common::CryptoProviderType::SOFTWARE); + mgr.Initialize(); + + EXPECT_EQ(mgr.GetProviderForCapability(common::ProviderCapability::kCertManagement), nullptr); +} + +TEST(ProviderManagerCapabilityTest, FallsBackToCapableProviderOutsidePreference) +{ + auto mgr = MakeCapabilityManager(); + // Key management is only on HW; a SOFTWARE-only preference still finds it + // via the lowest-id capable fallback rather than returning nullptr. + auto key_prov = + mgr->GetProviderForCapability(common::ProviderCapability::kKeyManagement, + {common::CryptoProviderType::SOFTWARE}); + ASSERT_NE(key_prov, nullptr); + EXPECT_EQ(key_prov->GetProviderName(), "HW_PROVIDER"); +} + +TEST(ProviderManagerCapabilityTest, UninitializedProviderIsNotSelected) +{ + score::crypto::daemon::config::Config config; + provider::ProviderManager mgr(config.GetProviderInitConfig()); + // Capable on paper, but initialization fails so it must be skipped. + mgr.RegisterProvider("FAIL_PROVIDER", + std::make_shared( + "FAIL_PROVIDER", 0, true, common::ProviderCapability::kCertManagement), + common::CryptoProviderType::SOFTWARE); + mgr.Initialize(); + + EXPECT_EQ(mgr.GetProviderForCapability(common::ProviderCapability::kCertManagement), nullptr); +} + +// =========================================================================== +// Default provider resolution prefers successfully-initialized providers +// =========================================================================== + +TEST(ProviderManagerDefaultResolutionTest, DefaultSkipsFailedPreferredProvider) +{ + score::crypto::daemon::config::Config config; + provider::ProviderManager mgr(config.GetProviderInitConfig()); + + // Preferred category (HARDWARE) is registered but fails to initialize; + // the SOFTWARE provider initializes successfully. + mgr.RegisterProvider("HW_PROVIDER", + std::make_shared("HW_PROVIDER", 0, /*fail_init=*/true), + common::CryptoProviderType::HARDWARE); + mgr.RegisterProvider("SW_PROVIDER", + std::make_shared("SW_PROVIDER", 1, /*fail_init=*/false), + common::CryptoProviderType::SOFTWARE); + mgr.Initialize(); + + // DEFAULT must resolve to the initialized SOFTWARE provider rather than the + // failed HARDWARE provider that the preference order would otherwise pick. + auto def = mgr.GetProvider(common::CryptoProviderType::DEFAULT); + ASSERT_NE(def, nullptr); + EXPECT_EQ(def->GetProviderName(), "SW_PROVIDER"); +} From 43d19ca5c908abef3cb11943ba823364ff8222e3 Mon Sep 17 00:00:00 2001 From: Athul Mallappallil Date: Tue, 11 Aug 2026 12:52:13 +0000 Subject: [PATCH 05/37] Correction in ResourceType --- score/crypto/docs/architecture/api_description.rst | 4 ++-- score/crypto/docs/architecture/design_decisions.rst | 2 +- score/crypto/docs/architecture/dynamic_architecture.rst | 4 ++-- score/crypto/src/api/common/types.hpp | 8 ++++---- .../api/contexts/i_certificate_verification_context.hpp | 4 ++-- 5 files changed, 11 insertions(+), 11 deletions(-) diff --git a/score/crypto/docs/architecture/api_description.rst b/score/crypto/docs/architecture/api_description.rst index c5e3b4b4a..da2de27a5 100644 --- a/score/crypto/docs/architecture/api_description.rst +++ b/score/crypto/docs/architecture/api_description.rst @@ -40,8 +40,8 @@ The API uses a two-phase resource identification model: struct CryptoResourceId { uint64_t id; // daemon-assigned, unique per session - ResourceType type; // kProvider, kKeySlot, kCertSlot, kVerificationTrustStore, - // kKey, kCertificate, kCrl, kSecureObject, kDataObject + ResourceType type; // kProvider, kKeySlot, kCertSlot, kCertificateTrustStore, + // kKey, kCertificate, kSecureObject, kDataObject ResourcePersistence persistence; // kPersistent or kEphemeral uint16_t primary_provider; // owning device/provider index (0 = unbound) }; diff --git a/score/crypto/docs/architecture/design_decisions.rst b/score/crypto/docs/architecture/design_decisions.rst index 35361d7ed..432b05f9f 100644 --- a/score/crypto/docs/architecture/design_decisions.rst +++ b/score/crypto/docs/architecture/design_decisions.rst @@ -336,7 +336,7 @@ Scope ^^^^^ Applies to ``kPersistent`` resources only: ``kKeySlot``, ``kCertificate``, -``kCertSlot``, ``kVerificationTrustStore``. Ephemeral (``kKey``) IDs remain session-scoped +``kCertSlot``, ``kCertificateTrustStore``. Ephemeral (``kKey``) IDs remain session-scoped (valid only within the ``IKeyManagementContext`` session that produced them). IPC Schema diff --git a/score/crypto/docs/architecture/dynamic_architecture.rst b/score/crypto/docs/architecture/dynamic_architecture.rst index 0b693bda3..a68be6a1d 100644 --- a/score/crypto/docs/architecture/dynamic_architecture.rst +++ b/score/crypto/docs/architecture/dynamic_architecture.rst @@ -19,7 +19,7 @@ API Dynamic Architecture .. code-block:: rst - + .. comp_arc_dyn:: Dynamic View :id: comp_arc_dyn__crypto__dynamic_view :security: YES @@ -208,7 +208,7 @@ Certificate Verification // Resolve certificate and verification trust store auto cert = ctx->ResolveResource("DeviceCert", ResourceType::kCertSlot).value(); - auto anchor = ctx->ResolveResource("RootCA", ResourceType::kVerificationTrustStore).value(); + auto anchor = ctx->ResolveResource("RootCA", ResourceType::kCertificateTrustStore).value(); // Verify using builder-style context CertificateVerificationContextConfig verify_cfg; diff --git a/score/crypto/src/api/common/types.hpp b/score/crypto/src/api/common/types.hpp index 31f75584f..4e8df63fc 100644 --- a/score/crypto/src/api/common/types.hpp +++ b/score/crypto/src/api/common/types.hpp @@ -60,12 +60,12 @@ 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 + kCertificateTrustStore, ///< 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) + kCertificate, ///< Parsed or stored certificate object. + ///< CRLs are not a resource type: they are co-located with the + ///< issuer's certificate slot and never independently resolvable. kSecureObject, ///< Secure storage entry kDataObject ///< Generic data blob }; diff --git a/score/crypto/src/api/contexts/i_certificate_verification_context.hpp b/score/crypto/src/api/contexts/i_certificate_verification_context.hpp index f8059f9a6..108e0838f 100644 --- a/score/crypto/src/api/contexts/i_certificate_verification_context.hpp +++ b/score/crypto/src/api/contexts/i_certificate_verification_context.hpp @@ -84,11 +84,11 @@ class ICertificateVerificationContext : public IContext /// @brief Sets the system trust store to use for certificate chain verification. /// /// The trust store is a manifest-configured named group of persistent certificate - /// slots. Resolve it by name with ResourceType::kVerificationTrustStore. + /// slots. Resolve it by name with ResourceType::kCertificateTrustStore. /// Empty slots in the store are silently skipped at verification time. /// /// @param trust_store Handle to the verification trust store - /// (type = kVerificationTrustStore) + /// (type = kCertificateTrustStore) /// @return std::monostate on success, error if handle is invalid virtual score::Result SetVerificationTrustStore(const CryptoResourceId& trust_store) = 0; From 735893ac220d9e555d1c19639aba790a8451dab7 Mon Sep 17 00:00:00 2001 From: Athul Mallappallil Date: Tue, 11 Aug 2026 13:39:43 +0000 Subject: [PATCH 06/37] Capability selection based on ctx id --- score/crypto/src/daemon/common/hex.hpp | 2 +- .../src/daemon/mediator/src/mediator_impl.cpp | 26 ++++++++++++++- .../crypto/src/daemon/provider/i_provider.hpp | 27 ++++------------ .../provider/pkcs11/pkcs11_provider.hpp | 7 ++++ .../src/daemon/provider/provider_manager.hpp | 32 ++++++++++++------- .../openssl/provider_openssl.cpp | 6 ++++ .../openssl/provider_openssl.hpp | 3 ++ .../daemon/provider/src/provider_manager.cpp | 16 ++++++++++ 8 files changed, 85 insertions(+), 34 deletions(-) diff --git a/score/crypto/src/daemon/common/hex.hpp b/score/crypto/src/daemon/common/hex.hpp index 18cc79872..c97f32eb1 100644 --- a/score/crypto/src/daemon/common/hex.hpp +++ b/score/crypto/src/daemon/common/hex.hpp @@ -14,7 +14,7 @@ #ifndef SCORE_CRYPTO_SRC_DAEMON_COMMON_HEX_HPP #define SCORE_CRYPTO_SRC_DAEMON_COMMON_HEX_HPP -#include "score/crypto/common/types.hpp" +#include "score/crypto/src/common/types.hpp" #include #include diff --git a/score/crypto/src/daemon/mediator/src/mediator_impl.cpp b/score/crypto/src/daemon/mediator/src/mediator_impl.cpp index b4cd5d8ab..589373d2f 100644 --- a/score/crypto/src/daemon/mediator/src/mediator_impl.cpp +++ b/score/crypto/src/daemon/mediator/src/mediator_impl.cpp @@ -18,6 +18,7 @@ #include #include #include +#include #include #include @@ -301,6 +302,22 @@ bool MediatorImpl::HandleContextCreationOperation(const score::crypto::daemon::c } } + // Scoped context types encode the capability as a prefix ("CERT:VERIFICATION", "KEY:MANAGEMENT"). + // Unscoped types ("HASH", "MAC") fall back to type-based provider selection. + // To add a new scope: add one entry to kScopeCapability. + static const std::unordered_map kScopeCapability{ + {"CERT", common::ProviderCapability::kCertManagement}, + {"KEY", common::ProviderCapability::kKeyManagement}, + }; + common::ProviderCapability required_capability = common::ProviderCapability::kNone; + const auto colon_pos = context_type.find(':'); + if (colon_pos != std::string_view::npos) + { + const auto it = kScopeCapability.find(context_type.substr(0, colon_pos)); + if (it != kScopeCapability.end()) + required_capability = it->second; + } + // --- Resolve target provider (considers key/slot affinity when available) --- std::shared_ptr provider; if (m_km_service && has_key_binding) @@ -317,6 +334,11 @@ bool MediatorImpl::HandleContextCreationOperation(const score::crypto::daemon::c } provider = m_provider_manager->GetProvider(resolved_id_res.value()); } + else if (required_capability != common::ProviderCapability::kNone + && requested_provider_type == common::CryptoProviderType::DEFAULT) + { + provider = m_provider_manager->GetProviderForCapability(required_capability); + } else { provider = m_provider_manager->GetProvider(requested_provider_type); @@ -414,7 +436,9 @@ bool MediatorImpl::HandleContextCreationOperation(const score::crypto::daemon::c } const std::string_view provider_selection = - has_key_binding ? " (key-affinity resolved)" : " (type-based selection)"; + has_key_binding ? " (key-affinity resolved)" + : required_capability != common::ProviderCapability::kNone ? " (capability-based selection)" + : " (type-based selection)"; score::mw::log::LogVerbose() << "[SCORE_API_MED] CTX_CREATE [" << context_type << "/" << algorithm << "] selected provider: name='" << provider->GetProviderName() << "' id=" << provider->GetProviderId() << provider_selection diff --git a/score/crypto/src/daemon/provider/i_provider.hpp b/score/crypto/src/daemon/provider/i_provider.hpp index bbe001ed8..c418ec559 100644 --- a/score/crypto/src/daemon/provider/i_provider.hpp +++ b/score/crypto/src/daemon/provider/i_provider.hpp @@ -154,31 +154,18 @@ class IProvider /// @brief Report the functional capabilities this provider offers. /// - /// The default derives the capability set directly from the capability - /// accessors above — the single source of truth — so a provider that - /// returns a non-null factory automatically advertises the matching - /// capability. Providers whose capability cannot be inferred from an - /// accessor (e.g. a backend that offers certificate operations only through - /// an extension) override this to declare the correct set. + /// Concrete providers MUST override this and return their full capability set + /// explicitly. The default returns kNone so an unoverridden provider is never + /// selected by GetProviderForCapability() — a silent miss is always preferable + /// to a mis-routed operation. Derivation from accessor returns is intentionally + /// avoided: as capability entry points evolve, heuristic derivation becomes + /// incorrect. /// /// Used by ProviderManager::GetProviderForCapability() to pick a default /// provider for a functional area rather than by hardware/software category. [[nodiscard]] virtual common::ProviderCapability GetProviderCapabilities() { - common::ProviderCapability caps = common::ProviderCapability::kNone; - if (GetCryptoHandlerFactory() != nullptr) - { - caps |= common::ProviderCapability::kCrypto; - } - if (GetKeyFactory() != nullptr) - { - caps |= common::ProviderCapability::kKeyManagement; - } - if (GetCertFactory() != nullptr) - { - caps |= common::ProviderCapability::kCertManagement; - } - return caps; + return common::ProviderCapability::kNone; } }; diff --git a/score/crypto/src/daemon/provider/pkcs11/pkcs11_provider.hpp b/score/crypto/src/daemon/provider/pkcs11/pkcs11_provider.hpp index 500b7265d..d94a41055 100644 --- a/score/crypto/src/daemon/provider/pkcs11/pkcs11_provider.hpp +++ b/score/crypto/src/daemon/provider/pkcs11/pkcs11_provider.hpp @@ -86,6 +86,13 @@ class Pkcs11Provider final : public IProvider, public std::enable_shared_from_th [[nodiscard]] common::ProviderId GetProviderId() const override; [[nodiscard]] const common::ProviderName& GetProviderName() const override; + // --- Capability advertisement --- + + [[nodiscard]] common::ProviderCapability GetProviderCapabilities() override + { + return common::ProviderCapability::kCrypto | common::ProviderCapability::kKeyManagement; + } + // --- Crypto capability --- [[nodiscard]] std::shared_ptr GetCryptoHandlerFactory() override; diff --git a/score/crypto/src/daemon/provider/provider_manager.hpp b/score/crypto/src/daemon/provider/provider_manager.hpp index eb0ef1193..fbcf975c2 100644 --- a/score/crypto/src/daemon/provider/provider_manager.hpp +++ b/score/crypto/src/daemon/provider/provider_manager.hpp @@ -165,25 +165,33 @@ class ProviderManager * * Filters registered providers to those that are initialized and advertise * @p capability (via IProvider::GetProviderCapabilities()), then chooses - * among them by @p preferenceOrder over CryptoProviderType. Falls back to - * the lowest-id capable provider when none match the preference. + * among them using the per-capability default preference order defined in + * ProviderManager. Falls back to the lowest-id capable provider when no + * preferred-category provider is found. * - * This is the capability-aware counterpart to GetProvider(CryptoProviderType): - * callers that need a functional area (e.g. certificate operations) but do - * not care which specific provider serves it use this instead of the - * category-only default, so a DEFAULT provider that lacks the capability is - * never returned. Most certificate callers rely on this default resolution; - * clients that must pin a specific provider resolve it by name/ID instead. + * Per-capability defaults: kCertManagement → SOFTWARE first (parsing/verification + * are inherently software); kKeyManagement and kCrypto → HARDWARE first. + * + * @param capability The functional capability the provider must offer. + * @return The selected provider, or nullptr if none offers the capability. + */ + [[nodiscard]] std::shared_ptr GetProviderForCapability( + common::ProviderCapability capability) const; + + /** + * @brief Select the preferred initialized provider with an explicit category order. + * + * Same as GetProviderForCapability(capability) but lets the caller override + * the preference order. Use this overload only when testing ordering behavior + * or when a specific call site needs a non-default preference. * * @param capability The functional capability the provider must offer. - * @param preferenceOrder Category priority among capable providers. Defaults - * to HARDWARE → SOFTWARE; certificate callers pass SOFTWARE first. + * @param preferenceOrder Category priority among capable providers. * @return The selected provider, or nullptr if none offers the capability. */ [[nodiscard]] std::shared_ptr GetProviderForCapability( common::ProviderCapability capability, - const std::vector& preferenceOrder = {common::CryptoProviderType::HARDWARE, - common::CryptoProviderType::SOFTWARE}) const; + const std::vector& preferenceOrder) const; /** * @brief Set a provider as default for a specific crypto provider type diff --git a/score/crypto/src/daemon/provider/score_provider/openssl/provider_openssl.cpp b/score/crypto/src/daemon/provider/score_provider/openssl/provider_openssl.cpp index f9285ea49..411a80357 100644 --- a/score/crypto/src/daemon/provider/score_provider/openssl/provider_openssl.cpp +++ b/score/crypto/src/daemon/provider/score_provider/openssl/provider_openssl.cpp @@ -68,6 +68,12 @@ std::shared_ptr<::score::crypto::daemon::provider::handler::ICryptoHandlerFactor return std::make_shared(m_factory, GetKeySlotHandler({}), m_keyManagementService); } +common::ProviderCapability OpenSSL::GetProviderCapabilities() +{ + return common::ProviderCapability::kCrypto | common::ProviderCapability::kKeyManagement | + common::ProviderCapability::kCertManagement; +} + std::shared_ptr OpenSSL::GetKeyFactory() { return m_factory; diff --git a/score/crypto/src/daemon/provider/score_provider/openssl/provider_openssl.hpp b/score/crypto/src/daemon/provider/score_provider/openssl/provider_openssl.hpp index 07d82a1d8..9858edbe3 100644 --- a/score/crypto/src/daemon/provider/score_provider/openssl/provider_openssl.hpp +++ b/score/crypto/src/daemon/provider/score_provider/openssl/provider_openssl.hpp @@ -41,6 +41,9 @@ class OpenSSL final : public ::score::crypto::daemon::provider::score_provider:: // --- IProvider lifecycle (OpenSSL-specific) --- void Shutdown() override; + // --- Capability advertisement --- + [[nodiscard]] common::ProviderCapability GetProviderCapabilities() override; + // --- Key management capability --- std::shared_ptr GetKeyFactory() override; std::shared_ptr GetKeySlotHandler( diff --git a/score/crypto/src/daemon/provider/src/provider_manager.cpp b/score/crypto/src/daemon/provider/src/provider_manager.cpp index b2f080f0b..a9771e491 100644 --- a/score/crypto/src/daemon/provider/src/provider_manager.cpp +++ b/score/crypto/src/daemon/provider/src/provider_manager.cpp @@ -270,6 +270,22 @@ std::shared_ptr ProviderManager::GetProvider(common::CryptoProviderTy return provider_entry.instance; } +std::shared_ptr ProviderManager::GetProviderForCapability( + common::ProviderCapability capability) const +{ + using Cap = common::ProviderCapability; + using Cat = common::CryptoProviderType; + // Per-capability defaults: cert operations are software-first; everything else hardware-first. + static const std::unordered_map> kDefaultPref{ + {Cap::kCertManagement, {Cat::SOFTWARE, Cat::HARDWARE}}, + {Cap::kKeyManagement, {Cat::HARDWARE, Cat::SOFTWARE}}, + {Cap::kCrypto, {Cat::HARDWARE, Cat::SOFTWARE}}, + }; + static const std::vector kFallback{Cat::HARDWARE, Cat::SOFTWARE}; + const auto it = kDefaultPref.find(capability); + return GetProviderForCapability(capability, it != kDefaultPref.end() ? it->second : kFallback); +} + std::shared_ptr ProviderManager::GetProviderForCapability( common::ProviderCapability capability, const std::vector& preferenceOrder) const From ec04b5e1391a8fe2a40e3c66ad836ecac73bd75d Mon Sep 17 00:00:00 2001 From: Athul Mallappallil Date: Tue, 11 Aug 2026 20:42:41 +0000 Subject: [PATCH 07/37] Common file io with atomic write --- score/crypto/src/daemon/common/storage/BUILD | 12 ++++ .../src/daemon/common/storage/file_io.cpp | 60 +++++++++++++++++++ .../src/daemon/common/storage/file_io.hpp | 44 ++++++++++++++ .../storage/kv/kv_deployment_writer.cpp | 28 ++++----- .../storage/kv/kv_deployment_writer.hpp | 5 +- 5 files changed, 128 insertions(+), 21 deletions(-) create mode 100644 score/crypto/src/daemon/common/storage/file_io.cpp create mode 100644 score/crypto/src/daemon/common/storage/file_io.hpp diff --git a/score/crypto/src/daemon/common/storage/BUILD b/score/crypto/src/daemon/common/storage/BUILD index a0f30c38e..a37a0059a 100644 --- a/score/crypto/src/daemon/common/storage/BUILD +++ b/score/crypto/src/daemon/common/storage/BUILD @@ -31,6 +31,17 @@ cc_library( ], ) +cc_library( + name = "file_io", + srcs = ["file_io.cpp"], + hdrs = ["file_io.hpp"], + visibility = ["//:__subpackages__"], + deps = [ + "//score/crypto/src/common:common_types", + "//score/crypto/src/daemon/common", + ], +) + cc_library( name = "kv_deployment", srcs = [ @@ -44,6 +55,7 @@ cc_library( visibility = ["//:__subpackages__"], deps = [ ":deployment_iface", + ":file_io", "@score_baselibs//score/mw/log", ], ) diff --git a/score/crypto/src/daemon/common/storage/file_io.cpp b/score/crypto/src/daemon/common/storage/file_io.cpp new file mode 100644 index 000000000..a2ed540da --- /dev/null +++ b/score/crypto/src/daemon/common/storage/file_io.cpp @@ -0,0 +1,60 @@ +/******************************************************************************** + * Copyright (c) 2026 Contributors to the Eclipse Foundation + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ +#include "score/crypto/src/daemon/common/storage/file_io.hpp" + +#include +#include + +namespace score::crypto::daemon::common::storage +{ + +score::crypto::Expected, DaemonErrorCode> +ReadFile(const std::string& path, std::size_t max_size) +{ + std::ifstream input(path, std::ios::binary); + if (!input) + return score::crypto::make_unexpected(DaemonErrorCode::kResourceNotAllocated); + input.seekg(0, std::ios::end); + const auto size = input.tellg(); + if (size <= 0 || static_cast(size) > max_size) + return score::crypto::make_unexpected(DaemonErrorCode::kInvalidArgument); + input.seekg(0, std::ios::beg); + std::vector data(static_cast(size)); + input.read(reinterpret_cast(data.data()), static_cast(data.size())); + if (!input) + return score::crypto::make_unexpected(DaemonErrorCode::kInternalError); + return data; +} + +score::crypto::Expected +WriteFile(const std::string& path, score::crypto::span data) +{ + if (path.empty() || data.empty()) + return score::crypto::make_unexpected(DaemonErrorCode::kInvalidArgument); + std::error_code ec; + std::filesystem::create_directories(std::filesystem::path(path).parent_path(), ec); + if (ec) + return score::crypto::make_unexpected(DaemonErrorCode::kInternalError); + const std::string temporary = path + ".tmp"; + { + std::ofstream output(temporary, std::ios::binary | std::ios::trunc); + if (!output) + return score::crypto::make_unexpected(DaemonErrorCode::kPersistFailed); + output.write(reinterpret_cast(data.data()), + static_cast(data.size())); + output.flush(); + if (!output) + return score::crypto::make_unexpected(DaemonErrorCode::kPersistFailed); + } + std::filesystem::rename(temporary, path, ec); + if (ec) + { + std::filesystem::remove(temporary, ec); + return score::crypto::make_unexpected(DaemonErrorCode::kPersistFailed); + } + return std::monostate{}; +} + +} // namespace score::crypto::daemon::common::storage diff --git a/score/crypto/src/daemon/common/storage/file_io.hpp b/score/crypto/src/daemon/common/storage/file_io.hpp new file mode 100644 index 000000000..185bde989 --- /dev/null +++ b/score/crypto/src/daemon/common/storage/file_io.hpp @@ -0,0 +1,44 @@ +/******************************************************************************** + * Copyright (c) 2026 Contributors to the Eclipse Foundation + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ +#ifndef SCORE_CRYPTO_SRC_DAEMON_COMMON_STORAGE_FILE_IO_HPP +#define SCORE_CRYPTO_SRC_DAEMON_COMMON_STORAGE_FILE_IO_HPP + +#include "score/crypto/src/common/types.hpp" +#include "score/crypto/src/daemon/common/daemon_error.hpp" + +#include +#include +#include +#include + +namespace score::crypto::daemon::common::storage +{ + +/// Read the entire binary contents of @p path into a byte vector. +/// +/// @param path Absolute or relative path to the file. +/// @param max_size Maximum accepted file size in bytes. Returns kInvalidArgument +/// if the file is empty or exceeds this limit. +/// @return Byte vector on success; kResourceNotAllocated if the file cannot be +/// opened, kInvalidArgument if size is out of range, kInternalError on +/// a partial read. +[[nodiscard]] score::crypto::Expected, DaemonErrorCode> +ReadFile(const std::string& path, std::size_t max_size); + +/// Write @p data to @p path atomically via a temporary file and rename. +/// +/// Creates parent directories if they do not exist. Writes to a sibling +/// .tmp first, then renames atomically. The temporary file is removed +/// on rename failure. +/// +/// @return std::monostate on success; kInvalidArgument if path or data is +/// empty, kInternalError if the directory cannot be created, +/// kPersistFailed on write or rename failure. +[[nodiscard]] score::crypto::Expected +WriteFile(const std::string& path, score::crypto::span data); + +} // namespace score::crypto::daemon::common::storage + +#endif // SCORE_CRYPTO_SRC_DAEMON_COMMON_STORAGE_FILE_IO_HPP diff --git a/score/crypto/src/daemon/common/storage/kv/kv_deployment_writer.cpp b/score/crypto/src/daemon/common/storage/kv/kv_deployment_writer.cpp index 72d67a59f..8d5270476 100644 --- a/score/crypto/src/daemon/common/storage/kv/kv_deployment_writer.cpp +++ b/score/crypto/src/daemon/common/storage/kv/kv_deployment_writer.cpp @@ -13,9 +13,9 @@ #include "score/crypto/src/daemon/common/storage/kv/kv_deployment_writer.hpp" -#include "score/mw/log/logging.h" +#include "score/crypto/src/daemon/common/storage/file_io.hpp" -#include +#include namespace score::crypto::daemon::common::storage { @@ -24,29 +24,21 @@ score::crypto::Expected{ + reinterpret_cast(content.data()), content.size()}; + return WriteFile(path, bytes); } } // namespace score::crypto::daemon::common::storage diff --git a/score/crypto/src/daemon/common/storage/kv/kv_deployment_writer.hpp b/score/crypto/src/daemon/common/storage/kv/kv_deployment_writer.hpp index 2ccb65420..086e3680d 100644 --- a/score/crypto/src/daemon/common/storage/kv/kv_deployment_writer.hpp +++ b/score/crypto/src/daemon/common/storage/kv/kv_deployment_writer.hpp @@ -24,7 +24,8 @@ namespace score::crypto::daemon::common::storage /// @brief Writes a DeploymentDescriptor to a key=value text file. /// /// Produces the same section/key=value format that KvDeploymentLoader can read back. -/// Existing file content is replaced (opened with std::ios::trunc). +/// Writes atomically via a temporary sibling file and rename — the existing file +/// is preserved until the new content is fully flushed. class KvDeploymentWriter final : public IDeploymentWriter { public: @@ -32,8 +33,6 @@ class KvDeploymentWriter final : public IDeploymentWriter const std::string& path, const DeploymentDescriptor& descriptor) override; - private: - static constexpr std::string_view kLogPrefix = "[KV_DEPLOYMENT_WRITER_COMMON] "; }; } // namespace score::crypto::daemon::common::storage From 752cea0f15688054798d362dfff941c4e8ec8116 Mon Sep 17 00:00:00 2001 From: Athul Mallappallil Date: Tue, 11 Aug 2026 20:59:40 +0000 Subject: [PATCH 08/37] Formatting fix --- score/crypto/src/api/common/types.hpp | 24 +++++++------- .../i_certificate_management_context.hpp | 6 ++-- .../i_certificate_verification_context.hpp | 5 ++- .../common/storage/deployment_path_utils.hpp | 1 - .../src/daemon/common/storage/file_io.cpp | 23 ++++++++++---- .../src/daemon/common/storage/file_io.hpp | 21 ++++++++++--- .../storage/kv/kv_deployment_writer.cpp | 4 +-- .../storage/kv/kv_deployment_writer.hpp | 1 - score/crypto/src/daemon/key_management/BUILD | 2 +- .../src/daemon/mediator/src/mediator_impl.cpp | 14 ++++----- .../src/daemon/provider/provider_manager.hpp | 3 +- .../daemon/provider/src/provider_manager.cpp | 7 ++--- .../provider/src/provider_manager_test.cpp | 31 +++++++++---------- 13 files changed, 79 insertions(+), 63 deletions(-) diff --git a/score/crypto/src/api/common/types.hpp b/score/crypto/src/api/common/types.hpp index 4e8df63fc..6e4f14ec4 100644 --- a/score/crypto/src/api/common/types.hpp +++ b/score/crypto/src/api/common/types.hpp @@ -57,17 +57,17 @@ using AlgorithmId = FixedCapacityString<64>; /// 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 - kCertificateTrustStore, ///< Named group of trusted CA certificates used for certificate chain - ///< verification. - kKey, ///< Key material (generated / loaded / derived / imported) - kCertificate, ///< Parsed or stored certificate object. - ///< CRLs are not a resource type: they are co-located with the - ///< issuer's certificate slot and never independently resolvable. - kSecureObject, ///< Secure storage entry - kDataObject ///< Generic data blob + kProvider, ///< Crypto provider / device + kKeySlot, ///< Persistent key storage slot + kCertSlot, ///< Persistent certificate storage slot + kCertificateTrustStore, ///< Named group of trusted CA certificates used for certificate chain + ///< verification. + kKey, ///< Key material (generated / loaded / derived / imported) + kCertificate, ///< Parsed or stored certificate object. + ///< CRLs are not a resource type: they are co-located with the + ///< issuer's certificate slot and never independently resolvable. + kSecureObject, ///< Secure storage entry + kDataObject ///< Generic data blob }; /// @brief Persistence classification of a crypto resource. @@ -304,7 +304,7 @@ inline constexpr bool HasPermission(KeyOperationPermission granted, KeyOperation struct CertificateSlotInfo { CertificateSlotState state{CertificateSlotState::kEmpty}; - bool has_crl{false}; ///< Whether a CRL is currently associated with the slot + bool has_crl{false}; ///< Whether a CRL is currently associated with the slot }; /// @brief Information about a key slot and its contents. diff --git a/score/crypto/src/api/contexts/i_certificate_management_context.hpp b/score/crypto/src/api/contexts/i_certificate_management_context.hpp index 48491e34f..7fd6a97b3 100644 --- a/score/crypto/src/api/contexts/i_certificate_management_context.hpp +++ b/score/crypto/src/api/contexts/i_certificate_management_context.hpp @@ -237,14 +237,14 @@ class ICertificateManagementContext : public IContext /// The certificate is assigned to a trust-store-managed slot. This is a /// write operation and requires trust-store write access. virtual score::Result AddCertificateToTrustStore(const CryptoResourceId& trust_store, - const CryptoResourceId& cert) = 0; + const CryptoResourceId& cert) = 0; /// @brief Removes a certificate from a persistent trust store by SHA-256 fingerprint. /// /// This changes trust-store deployment state and requires write access. virtual score::Result RemoveCertificateFromTrustStore( - const CryptoResourceId& trust_store, - score::cpp::span certificate_fingerprint) = 0; + const CryptoResourceId& trust_store, + score::cpp::span certificate_fingerprint) = 0; protected: ICertificateManagementContext() = default; diff --git a/score/crypto/src/api/contexts/i_certificate_verification_context.hpp b/score/crypto/src/api/contexts/i_certificate_verification_context.hpp index 108e0838f..8f0834efe 100644 --- a/score/crypto/src/api/contexts/i_certificate_verification_context.hpp +++ b/score/crypto/src/api/contexts/i_certificate_verification_context.hpp @@ -101,8 +101,7 @@ class ICertificateVerificationContext : public IContext /// @param certs Span of certificate handles to treat as trust anchors /// (type = kCertificate or kCertSlot) /// @return std::monostate on success, error if any handle is invalid - virtual score::Result SetTrustedCertificates( - score::cpp::span certs) = 0; + virtual score::Result SetTrustedCertificates(score::cpp::span certs) = 0; /// @brief Selects the chain termination rule for trust-store verification. /// @@ -128,7 +127,7 @@ class ICertificateVerificationContext : public IContext /// (type = kCertificate or kCertSlot) /// @return std::monostate on success, or an error if a handle is invalid virtual score::Result SetAdditionalCertificates( - score::cpp::span certificates) = 0; + score::cpp::span certificates) = 0; // ---- OCSP (not yet active — IPC implementation pending) ---- #if 0 diff --git a/score/crypto/src/daemon/common/storage/deployment_path_utils.hpp b/score/crypto/src/daemon/common/storage/deployment_path_utils.hpp index 2ece49a06..be3fe0e55 100644 --- a/score/crypto/src/daemon/common/storage/deployment_path_utils.hpp +++ b/score/crypto/src/daemon/common/storage/deployment_path_utils.hpp @@ -40,5 +40,4 @@ namespace score::crypto::daemon::common::storage } // namespace score::crypto::daemon::common::storage - #endif // SCORE_CRYPTO_SRC_DAEMON_COMMON_STORAGE_DEPLOYMENT_PATH_UTILS_HPP diff --git a/score/crypto/src/daemon/common/storage/file_io.cpp b/score/crypto/src/daemon/common/storage/file_io.cpp index a2ed540da..1a51d8b85 100644 --- a/score/crypto/src/daemon/common/storage/file_io.cpp +++ b/score/crypto/src/daemon/common/storage/file_io.cpp @@ -1,3 +1,15 @@ +/******************************************************************************** + * 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 + ********************************************************************************/ /******************************************************************************** * Copyright (c) 2026 Contributors to the Eclipse Foundation * SPDX-License-Identifier: Apache-2.0 @@ -10,8 +22,8 @@ namespace score::crypto::daemon::common::storage { -score::crypto::Expected, DaemonErrorCode> -ReadFile(const std::string& path, std::size_t max_size) +score::crypto::Expected, DaemonErrorCode> ReadFile(const std::string& path, + std::size_t max_size) { std::ifstream input(path, std::ios::binary); if (!input) @@ -28,8 +40,8 @@ ReadFile(const std::string& path, std::size_t max_size) return data; } -score::crypto::Expected -WriteFile(const std::string& path, score::crypto::span data) +score::crypto::Expected WriteFile(const std::string& path, + score::crypto::span data) { if (path.empty() || data.empty()) return score::crypto::make_unexpected(DaemonErrorCode::kInvalidArgument); @@ -42,8 +54,7 @@ WriteFile(const std::string& path, score::crypto::span data) std::ofstream output(temporary, std::ios::binary | std::ios::trunc); if (!output) return score::crypto::make_unexpected(DaemonErrorCode::kPersistFailed); - output.write(reinterpret_cast(data.data()), - static_cast(data.size())); + output.write(reinterpret_cast(data.data()), static_cast(data.size())); output.flush(); if (!output) return score::crypto::make_unexpected(DaemonErrorCode::kPersistFailed); diff --git a/score/crypto/src/daemon/common/storage/file_io.hpp b/score/crypto/src/daemon/common/storage/file_io.hpp index 185bde989..13e1c03f8 100644 --- a/score/crypto/src/daemon/common/storage/file_io.hpp +++ b/score/crypto/src/daemon/common/storage/file_io.hpp @@ -1,3 +1,15 @@ +/******************************************************************************** + * 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 + ********************************************************************************/ /******************************************************************************** * Copyright (c) 2026 Contributors to the Eclipse Foundation * SPDX-License-Identifier: Apache-2.0 @@ -24,8 +36,8 @@ namespace score::crypto::daemon::common::storage /// @return Byte vector on success; kResourceNotAllocated if the file cannot be /// opened, kInvalidArgument if size is out of range, kInternalError on /// a partial read. -[[nodiscard]] score::crypto::Expected, DaemonErrorCode> -ReadFile(const std::string& path, std::size_t max_size); +[[nodiscard]] score::crypto::Expected, DaemonErrorCode> ReadFile(const std::string& path, + std::size_t max_size); /// Write @p data to @p path atomically via a temporary file and rename. /// @@ -36,8 +48,9 @@ ReadFile(const std::string& path, std::size_t max_size); /// @return std::monostate on success; kInvalidArgument if path or data is /// empty, kInternalError if the directory cannot be created, /// kPersistFailed on write or rename failure. -[[nodiscard]] score::crypto::Expected -WriteFile(const std::string& path, score::crypto::span data); +[[nodiscard]] score::crypto::Expected WriteFile( + const std::string& path, + score::crypto::span data); } // namespace score::crypto::daemon::common::storage diff --git a/score/crypto/src/daemon/common/storage/kv/kv_deployment_writer.cpp b/score/crypto/src/daemon/common/storage/kv/kv_deployment_writer.cpp index 8d5270476..38517a959 100644 --- a/score/crypto/src/daemon/common/storage/kv/kv_deployment_writer.cpp +++ b/score/crypto/src/daemon/common/storage/kv/kv_deployment_writer.cpp @@ -36,8 +36,8 @@ score::crypto::Expected{ - reinterpret_cast(content.data()), content.size()}; + const auto bytes = + score::crypto::span{reinterpret_cast(content.data()), content.size()}; return WriteFile(path, bytes); } diff --git a/score/crypto/src/daemon/common/storage/kv/kv_deployment_writer.hpp b/score/crypto/src/daemon/common/storage/kv/kv_deployment_writer.hpp index 086e3680d..153bfa863 100644 --- a/score/crypto/src/daemon/common/storage/kv/kv_deployment_writer.hpp +++ b/score/crypto/src/daemon/common/storage/kv/kv_deployment_writer.hpp @@ -32,7 +32,6 @@ class KvDeploymentWriter final : public IDeploymentWriter [[nodiscard]] score::crypto::Expected Write( const std::string& path, const DeploymentDescriptor& descriptor) override; - }; } // namespace score::crypto::daemon::common::storage diff --git a/score/crypto/src/daemon/key_management/BUILD b/score/crypto/src/daemon/key_management/BUILD index 07dc910e1..1088c0130 100644 --- a/score/crypto/src/daemon/key_management/BUILD +++ b/score/crypto/src/daemon/key_management/BUILD @@ -108,11 +108,11 @@ cc_library( visibility = ["//:__subpackages__"], deps = [ ":key_management_headers", + "//score/crypto/src/api/common:crypto_common", "//score/crypto/src/daemon/common", "//score/crypto/src/daemon/common/storage:kv_deployment", "//score/crypto/src/daemon/data_manager", "//score/crypto/src/daemon/provider:provider_headers", "//score/crypto/src/daemon/provider:provider_manager", - "//score/crypto/src/api/common:crypto_common", ], ) diff --git a/score/crypto/src/daemon/mediator/src/mediator_impl.cpp b/score/crypto/src/daemon/mediator/src/mediator_impl.cpp index 589373d2f..77c7b93ce 100644 --- a/score/crypto/src/daemon/mediator/src/mediator_impl.cpp +++ b/score/crypto/src/daemon/mediator/src/mediator_impl.cpp @@ -307,7 +307,7 @@ bool MediatorImpl::HandleContextCreationOperation(const score::crypto::daemon::c // To add a new scope: add one entry to kScopeCapability. static const std::unordered_map kScopeCapability{ {"CERT", common::ProviderCapability::kCertManagement}, - {"KEY", common::ProviderCapability::kKeyManagement}, + {"KEY", common::ProviderCapability::kKeyManagement}, }; common::ProviderCapability required_capability = common::ProviderCapability::kNone; const auto colon_pos = context_type.find(':'); @@ -334,8 +334,8 @@ bool MediatorImpl::HandleContextCreationOperation(const score::crypto::daemon::c } provider = m_provider_manager->GetProvider(resolved_id_res.value()); } - else if (required_capability != common::ProviderCapability::kNone - && requested_provider_type == common::CryptoProviderType::DEFAULT) + else if (required_capability != common::ProviderCapability::kNone && + requested_provider_type == common::CryptoProviderType::DEFAULT) { provider = m_provider_manager->GetProviderForCapability(required_capability); } @@ -435,10 +435,10 @@ bool MediatorImpl::HandleContextCreationOperation(const score::crypto::daemon::c return false; } - const std::string_view provider_selection = - has_key_binding ? " (key-affinity resolved)" - : required_capability != common::ProviderCapability::kNone ? " (capability-based selection)" - : " (type-based selection)"; + const std::string_view provider_selection = has_key_binding ? " (key-affinity resolved)" + : required_capability != common::ProviderCapability::kNone + ? " (capability-based selection)" + : " (type-based selection)"; score::mw::log::LogVerbose() << "[SCORE_API_MED] CTX_CREATE [" << context_type << "/" << algorithm << "] selected provider: name='" << provider->GetProviderName() << "' id=" << provider->GetProviderId() << provider_selection diff --git a/score/crypto/src/daemon/provider/provider_manager.hpp b/score/crypto/src/daemon/provider/provider_manager.hpp index fbcf975c2..01a596c4b 100644 --- a/score/crypto/src/daemon/provider/provider_manager.hpp +++ b/score/crypto/src/daemon/provider/provider_manager.hpp @@ -175,8 +175,7 @@ class ProviderManager * @param capability The functional capability the provider must offer. * @return The selected provider, or nullptr if none offers the capability. */ - [[nodiscard]] std::shared_ptr GetProviderForCapability( - common::ProviderCapability capability) const; + [[nodiscard]] std::shared_ptr GetProviderForCapability(common::ProviderCapability capability) const; /** * @brief Select the preferred initialized provider with an explicit category order. diff --git a/score/crypto/src/daemon/provider/src/provider_manager.cpp b/score/crypto/src/daemon/provider/src/provider_manager.cpp index a9771e491..bb50a9d38 100644 --- a/score/crypto/src/daemon/provider/src/provider_manager.cpp +++ b/score/crypto/src/daemon/provider/src/provider_manager.cpp @@ -270,16 +270,15 @@ std::shared_ptr ProviderManager::GetProvider(common::CryptoProviderTy return provider_entry.instance; } -std::shared_ptr ProviderManager::GetProviderForCapability( - common::ProviderCapability capability) const +std::shared_ptr ProviderManager::GetProviderForCapability(common::ProviderCapability capability) const { using Cap = common::ProviderCapability; using Cat = common::CryptoProviderType; // Per-capability defaults: cert operations are software-first; everything else hardware-first. static const std::unordered_map> kDefaultPref{ {Cap::kCertManagement, {Cat::SOFTWARE, Cat::HARDWARE}}, - {Cap::kKeyManagement, {Cat::HARDWARE, Cat::SOFTWARE}}, - {Cap::kCrypto, {Cat::HARDWARE, Cat::SOFTWARE}}, + {Cap::kKeyManagement, {Cat::HARDWARE, Cat::SOFTWARE}}, + {Cap::kCrypto, {Cat::HARDWARE, Cat::SOFTWARE}}, }; static const std::vector kFallback{Cat::HARDWARE, Cat::SOFTWARE}; const auto it = kDefaultPref.find(capability); diff --git a/score/crypto/src/daemon/provider/src/provider_manager_test.cpp b/score/crypto/src/daemon/provider/src/provider_manager_test.cpp index c3daef732..e1b53cf3c 100644 --- a/score/crypto/src/daemon/provider/src/provider_manager_test.cpp +++ b/score/crypto/src/daemon/provider/src/provider_manager_test.cpp @@ -211,15 +211,13 @@ provider::ProviderManager::Sptr MakeCapabilityManager() mgr->RegisterProvider( "SW_PROVIDER", std::make_shared( - "SW_PROVIDER", 0, false, - common::ProviderCapability::kCrypto | common::ProviderCapability::kCertManagement), + "SW_PROVIDER", 0, false, common::ProviderCapability::kCrypto | common::ProviderCapability::kCertManagement), common::CryptoProviderType::SOFTWARE); mgr->RegisterProvider( "HW_PROVIDER", std::make_shared( - "HW_PROVIDER", 1, false, - common::ProviderCapability::kCrypto | common::ProviderCapability::kKeyManagement), + "HW_PROVIDER", 1, false, common::ProviderCapability::kCrypto | common::ProviderCapability::kKeyManagement), common::CryptoProviderType::HARDWARE); mgr->Initialize(); @@ -240,15 +238,15 @@ TEST(ProviderManagerCapabilityTest, PreferenceOrderPicksAmongCapableProviders) { auto mgr = MakeCapabilityManager(); // Both providers offer crypto; preference decides which is returned. - auto sw_first = mgr->GetProviderForCapability( - common::ProviderCapability::kCrypto, - {common::CryptoProviderType::SOFTWARE, common::CryptoProviderType::HARDWARE}); + auto sw_first = + mgr->GetProviderForCapability(common::ProviderCapability::kCrypto, + {common::CryptoProviderType::SOFTWARE, common::CryptoProviderType::HARDWARE}); ASSERT_NE(sw_first, nullptr); EXPECT_EQ(sw_first->GetProviderName(), "SW_PROVIDER"); - auto hw_first = mgr->GetProviderForCapability( - common::ProviderCapability::kCrypto, - {common::CryptoProviderType::HARDWARE, common::CryptoProviderType::SOFTWARE}); + auto hw_first = + mgr->GetProviderForCapability(common::ProviderCapability::kCrypto, + {common::CryptoProviderType::HARDWARE, common::CryptoProviderType::SOFTWARE}); ASSERT_NE(hw_first, nullptr); EXPECT_EQ(hw_first->GetProviderName(), "HW_PROVIDER"); } @@ -257,10 +255,10 @@ TEST(ProviderManagerCapabilityTest, ReturnsNullWhenNoProviderOffersCapability) { score::crypto::daemon::config::Config config; provider::ProviderManager mgr(config.GetProviderInitConfig()); - mgr.RegisterProvider("SW_PROVIDER", - std::make_shared( - "SW_PROVIDER", 0, false, common::ProviderCapability::kCrypto), - common::CryptoProviderType::SOFTWARE); + mgr.RegisterProvider( + "SW_PROVIDER", + std::make_shared("SW_PROVIDER", 0, false, common::ProviderCapability::kCrypto), + common::CryptoProviderType::SOFTWARE); mgr.Initialize(); EXPECT_EQ(mgr.GetProviderForCapability(common::ProviderCapability::kCertManagement), nullptr); @@ -271,9 +269,8 @@ TEST(ProviderManagerCapabilityTest, FallsBackToCapableProviderOutsidePreference) auto mgr = MakeCapabilityManager(); // Key management is only on HW; a SOFTWARE-only preference still finds it // via the lowest-id capable fallback rather than returning nullptr. - auto key_prov = - mgr->GetProviderForCapability(common::ProviderCapability::kKeyManagement, - {common::CryptoProviderType::SOFTWARE}); + auto key_prov = mgr->GetProviderForCapability(common::ProviderCapability::kKeyManagement, + {common::CryptoProviderType::SOFTWARE}); ASSERT_NE(key_prov, nullptr); EXPECT_EQ(key_prov->GetProviderName(), "HW_PROVIDER"); } From 656a245bb898a11dc4dbe88c1714d26ff563d2af Mon Sep 17 00:00:00 2001 From: Athul Mallappallil Date: Wed, 12 Aug 2026 14:34:31 +0000 Subject: [PATCH 09/37] Added two more file io fn --- .../src/daemon/common/storage/file_io.cpp | 17 ++++ .../src/daemon/common/storage/file_io.hpp | 14 ++++ .../slot/file_backed_slot_handler.cpp | 79 +++++-------------- .../slot/file_backed_slot_handler.hpp | 9 +-- 4 files changed, 51 insertions(+), 68 deletions(-) diff --git a/score/crypto/src/daemon/common/storage/file_io.cpp b/score/crypto/src/daemon/common/storage/file_io.cpp index 1a51d8b85..e54f7f79f 100644 --- a/score/crypto/src/daemon/common/storage/file_io.cpp +++ b/score/crypto/src/daemon/common/storage/file_io.cpp @@ -68,4 +68,21 @@ score::crypto::Expected WriteFile(const std::st return std::monostate{}; } +bool FileExists(const std::string& path) +{ + std::error_code ec; + return std::filesystem::is_regular_file(path, ec); +} + +score::crypto::Expected RemoveFile(const std::string& path) +{ + if (path.empty()) + return score::crypto::make_unexpected(DaemonErrorCode::kInvalidArgument); + std::error_code ec; + std::filesystem::remove(path, ec); + if (ec) + return score::crypto::make_unexpected(DaemonErrorCode::kPersistFailed); + return std::monostate{}; +} + } // namespace score::crypto::daemon::common::storage diff --git a/score/crypto/src/daemon/common/storage/file_io.hpp b/score/crypto/src/daemon/common/storage/file_io.hpp index 13e1c03f8..6d4483402 100644 --- a/score/crypto/src/daemon/common/storage/file_io.hpp +++ b/score/crypto/src/daemon/common/storage/file_io.hpp @@ -52,6 +52,20 @@ namespace score::crypto::daemon::common::storage const std::string& path, score::crypto::span data); +/// Return true if @p path refers to an existing regular file. +/// +/// Returns false for directories, symlinks to non-existent targets, and any +/// other non-regular-file entries. Errors resolve to false. +[[nodiscard]] bool FileExists(const std::string& path); + +/// Remove the file at @p path. +/// +/// Idempotent: returns success if the file does not exist. +/// +/// @return std::monostate on success or if the file was already absent; +/// kInvalidArgument if path is empty, kPersistFailed on a filesystem error. +[[nodiscard]] score::crypto::Expected RemoveFile(const std::string& path); + } // namespace score::crypto::daemon::common::storage #endif // SCORE_CRYPTO_SRC_DAEMON_COMMON_STORAGE_FILE_IO_HPP diff --git a/score/crypto/src/daemon/key_management/slot/file_backed_slot_handler.cpp b/score/crypto/src/daemon/key_management/slot/file_backed_slot_handler.cpp index 53e27f44c..555ecca22 100644 --- a/score/crypto/src/daemon/key_management/slot/file_backed_slot_handler.cpp +++ b/score/crypto/src/daemon/key_management/slot/file_backed_slot_handler.cpp @@ -14,43 +14,42 @@ #include "score/crypto/src/daemon/key_management/slot/file_backed_slot_handler.hpp" #include "score/crypto/src/daemon/common/secure_memory.hpp" +#include "score/crypto/src/daemon/common/storage/file_io.hpp" #include "score/crypto/src/daemon/key_management/detail/slot_info_builder.hpp" #include "score/crypto/src/daemon/key_management/interfaces/key_management_operations.hpp" #include "score/crypto/src/daemon/key_management/interfaces/key_slot_config.hpp" #include "score/crypto/src/daemon/key_management/slot/deployment_loader.hpp" #include -#include -#include -#include #include namespace score::crypto::daemon::key_management { +namespace +{ +namespace file_io = common::storage; +using Error = common::DaemonErrorCode; +} // namespace FileBackedSlotHandler::FileBackedSlotHandler(IKeyFactory::Sptr factory) : m_factory{std::move(factory)} {} -score::crypto::Expected -FileBackedSlotHandler::LoadKey(const KeySlotConfig& slot) +score::crypto::Expected FileBackedSlotHandler::LoadKey(const KeySlotConfig& slot) { - // Load deployment info to get the key file path. auto deploy_result = DeploymentLoader::Load(slot.deployment_path, slot.deployment_format); if (!deploy_result.has_value()) - { return score::crypto::make_unexpected(deploy_result.error()); - } const auto& deploy_info = deploy_result.value(); const auto path_it = deploy_info.key_properties.find(std::string{deployment_keys::kKeyPath}); - if ((path_it == deploy_info.key_properties.end()) || path_it->second.empty()) - { - return score::crypto::make_unexpected(score::crypto::daemon::common::DaemonErrorCode::kInvalidArgument); - } + if (path_it == deploy_info.key_properties.end() || path_it->second.empty()) + return score::crypto::make_unexpected(Error::kInvalidArgument); - auto read_result = ReadKeyFile(path_it->second); + auto read_result = file_io::ReadFile(path_it->second, kMaxKeyFileSize); if (!read_result.has_value()) { - return score::crypto::make_unexpected(read_result.error()); + const auto file_io_err = read_result.error(); + return score::crypto::make_unexpected(file_io_err == Error::kResourceNotAllocated ? Error::kKeySlotEmpty + : file_io_err); } auto& buffer = read_result.value(); @@ -62,72 +61,32 @@ FileBackedSlotHandler::LoadKey(const KeySlotConfig& slot) req.permissions = slot.allowed_operations; auto import_result = m_factory->ImportKey(req); - - // Securely zeroize regardless of import outcome. common::SecureZeroizeAndClear(buffer); - return import_result; } -score::crypto::Expected -FileBackedSlotHandler::GetSlotState(const KeySlotConfig& slot) +score::crypto::Expected FileBackedSlotHandler::GetSlotState( + const KeySlotConfig& slot) { auto deploy_result = DeploymentLoader::Load(slot.deployment_path, slot.deployment_format); if (!deploy_result.has_value()) - { return score::crypto::KeySlotState::kEmpty; - } const auto& deploy_info = deploy_result.value(); const auto path_it = deploy_info.key_properties.find(std::string{deployment_keys::kKeyPath}); - if ((path_it == deploy_info.key_properties.end()) || path_it->second.empty()) - { + if (path_it == deploy_info.key_properties.end() || path_it->second.empty()) return score::crypto::KeySlotState::kEmpty; - } - std::ifstream file(path_it->second, std::ios::binary); - if (file.good()) - { - return score::crypto::KeySlotState::kOccupied; - } - - return score::crypto::KeySlotState::kEmpty; + return file_io::FileExists(path_it->second) ? score::crypto::KeySlotState::kOccupied + : score::crypto::KeySlotState::kEmpty; } -score::crypto::Expected -FileBackedSlotHandler::GetSlotInfo(const KeySlotConfig& slot) +score::crypto::Expected FileBackedSlotHandler::GetSlotInfo(const KeySlotConfig& slot) { auto state_result = GetSlotState(slot); if (!state_result.has_value()) - { return score::crypto::make_unexpected(state_result.error()); - } return detail::BuildKeySlotInfo(slot, state_result.value()); } -score::crypto::Expected, score::crypto::daemon::common::DaemonErrorCode> -FileBackedSlotHandler::ReadKeyFile(const std::string& file_path) const -{ - std::ifstream file(file_path, std::ios::binary); - if (!file.is_open()) - { - return score::crypto::make_unexpected(score::crypto::daemon::common::DaemonErrorCode::kKeySlotEmpty); - } - - std::vector buffer((std::istreambuf_iterator(file)), std::istreambuf_iterator()); - - if (buffer.empty()) - { - return score::crypto::make_unexpected(score::crypto::daemon::common::DaemonErrorCode::kInvalidArgument); - } - - if (buffer.size() > kMaxKeyFileSize) - { - common::SecureZeroizeAndClear(buffer); - return score::crypto::make_unexpected(score::crypto::daemon::common::DaemonErrorCode::kInvalidArgument); - } - - return buffer; -} - } // namespace score::crypto::daemon::key_management diff --git a/score/crypto/src/daemon/key_management/slot/file_backed_slot_handler.hpp b/score/crypto/src/daemon/key_management/slot/file_backed_slot_handler.hpp index b6cfad295..d6f410ab5 100644 --- a/score/crypto/src/daemon/key_management/slot/file_backed_slot_handler.hpp +++ b/score/crypto/src/daemon/key_management/slot/file_backed_slot_handler.hpp @@ -19,8 +19,6 @@ #include #include -#include -#include namespace score::crypto::daemon::key_management { @@ -70,15 +68,10 @@ class FileBackedSlotHandler final : public IKeySlotHandler GetSlotInfo(const KeySlotConfig& slot) override; private: - [[nodiscard]] score::crypto::Expected, score::crypto::daemon::common::DaemonErrorCode> - ReadKeyFile(const std::string& file_path) const; - IKeyFactory::Sptr m_factory; - /// Maximum key file size to guard against reading unreasonably large files. + /// Maximum accepted key file size (8 KiB). static constexpr std::size_t kMaxKeyFileSize = 8U * 1024U; - - static constexpr std::string_view LOG_PREFIX = "[FILE_BACKED_SLOT_HANDLER]"; }; } // namespace score::crypto::daemon::key_management From 7a8904e1476566290d45bee182aa138c1f5f97f3 Mon Sep 17 00:00:00 2001 From: Athul Mallappallil Date: Wed, 12 Aug 2026 21:46:18 +0000 Subject: [PATCH 10/37] Use baselibs for filesystem IO --- score/crypto/src/daemon/common/storage/BUILD | 1 + .../src/daemon/common/storage/file_io.cpp | 80 +++++++++++-------- .../data_plane/src/base_shm_factory.cpp | 3 +- .../key_management/openssl_key_factory.cpp | 2 +- 4 files changed, 48 insertions(+), 38 deletions(-) diff --git a/score/crypto/src/daemon/common/storage/BUILD b/score/crypto/src/daemon/common/storage/BUILD index a37a0059a..6eecf280d 100644 --- a/score/crypto/src/daemon/common/storage/BUILD +++ b/score/crypto/src/daemon/common/storage/BUILD @@ -39,6 +39,7 @@ cc_library( deps = [ "//score/crypto/src/common:common_types", "//score/crypto/src/daemon/common", + "@score_baselibs//score/filesystem", ], ) diff --git a/score/crypto/src/daemon/common/storage/file_io.cpp b/score/crypto/src/daemon/common/storage/file_io.cpp index e54f7f79f..a08f789c6 100644 --- a/score/crypto/src/daemon/common/storage/file_io.cpp +++ b/score/crypto/src/daemon/common/storage/file_io.cpp @@ -10,14 +10,12 @@ * * SPDX-License-Identifier: Apache-2.0 ********************************************************************************/ -/******************************************************************************** - * Copyright (c) 2026 Contributors to the Eclipse Foundation - * SPDX-License-Identifier: Apache-2.0 - ********************************************************************************/ #include "score/crypto/src/daemon/common/storage/file_io.hpp" -#include -#include +#include "score/filesystem/error.h" +#include "score/filesystem/filestream/file_factory.h" +#include "score/filesystem/filestream/file_stream.h" +#include "score/filesystem/filesystem.h" namespace score::crypto::daemon::common::storage { @@ -25,17 +23,20 @@ namespace score::crypto::daemon::common::storage score::crypto::Expected, DaemonErrorCode> ReadFile(const std::string& path, std::size_t max_size) { - std::ifstream input(path, std::ios::binary); - if (!input) + score::filesystem::FileFactory factory{}; + auto open_result = factory.Open(score::filesystem::Path{path}, std::ios::binary | std::ios::in); + if (!open_result.has_value()) return score::crypto::make_unexpected(DaemonErrorCode::kResourceNotAllocated); - input.seekg(0, std::ios::end); - const auto size = input.tellg(); + + auto& stream = *open_result.value(); + stream.seekg(0, std::ios::end); + const auto size = stream.tellg(); if (size <= 0 || static_cast(size) > max_size) return score::crypto::make_unexpected(DaemonErrorCode::kInvalidArgument); - input.seekg(0, std::ios::beg); + stream.seekg(0, std::ios::beg); std::vector data(static_cast(size)); - input.read(reinterpret_cast(data.data()), static_cast(data.size())); - if (!input) + stream.read(reinterpret_cast(data.data()), static_cast(data.size())); + if (!stream) return score::crypto::make_unexpected(DaemonErrorCode::kInternalError); return data; } @@ -45,42 +46,51 @@ score::crypto::Expected WriteFile(const std::st { if (path.empty() || data.empty()) return score::crypto::make_unexpected(DaemonErrorCode::kInvalidArgument); - std::error_code ec; - std::filesystem::create_directories(std::filesystem::path(path).parent_path(), ec); - if (ec) - return score::crypto::make_unexpected(DaemonErrorCode::kInternalError); - const std::string temporary = path + ".tmp"; + + const score::filesystem::Path target_path{path}; + score::filesystem::StandardFilesystem fs{}; + const auto parent = target_path.ParentPath(); + if (!parent.Empty()) { - std::ofstream output(temporary, std::ios::binary | std::ios::trunc); - if (!output) - return score::crypto::make_unexpected(DaemonErrorCode::kPersistFailed); - output.write(reinterpret_cast(data.data()), static_cast(data.size())); - output.flush(); - if (!output) - return score::crypto::make_unexpected(DaemonErrorCode::kPersistFailed); + if (!fs.CreateDirectories(parent).has_value()) + return score::crypto::make_unexpected(DaemonErrorCode::kInternalError); } - std::filesystem::rename(temporary, path, ec); - if (ec) - { - std::filesystem::remove(temporary, ec); + + score::filesystem::FileFactory factory{}; + auto stream_result = factory.AtomicUpdate(target_path, std::ios::binary | std::ios::trunc); + if (!stream_result.has_value()) + return score::crypto::make_unexpected(DaemonErrorCode::kPersistFailed); + + auto& stream = *stream_result.value(); + stream.write(reinterpret_cast(data.data()), static_cast(data.size())); + stream.flush(); + if (!stream) + // Stream is in a bad state; Close() (called by the destructor) will detect this + // and clean up the temp file rather than renaming it. + return score::crypto::make_unexpected(DaemonErrorCode::kPersistFailed); + + // Close() triggers the atomic rename — check it explicitly to confirm success. + // The destructor calls Close() again on the way out; a second call on an already-closed + // stream returns an error that the destructor ignores, which is safe. + if (!stream_result.value()->Close().has_value()) return score::crypto::make_unexpected(DaemonErrorCode::kPersistFailed); - } return std::monostate{}; } bool FileExists(const std::string& path) { - std::error_code ec; - return std::filesystem::is_regular_file(path, ec); + score::filesystem::StandardFilesystem fs{}; + const auto result = fs.IsRegularFile(score::filesystem::Path{path}); + return result.has_value() && result.value(); } score::crypto::Expected RemoveFile(const std::string& path) { if (path.empty()) return score::crypto::make_unexpected(DaemonErrorCode::kInvalidArgument); - std::error_code ec; - std::filesystem::remove(path, ec); - if (ec) + score::filesystem::StandardFilesystem fs{}; + const auto result = fs.Remove(score::filesystem::Path{path}); + if (!result.has_value() && result.error() != score::filesystem::ErrorCode::kFileOrDirectoryDoesNotExist) return score::crypto::make_unexpected(DaemonErrorCode::kPersistFailed); return std::monostate{}; } diff --git a/score/crypto/src/daemon/data_plane/src/base_shm_factory.cpp b/score/crypto/src/daemon/data_plane/src/base_shm_factory.cpp index 5dffad5f9..6b874497f 100644 --- a/score/crypto/src/daemon/data_plane/src/base_shm_factory.cpp +++ b/score/crypto/src/daemon/data_plane/src/base_shm_factory.cpp @@ -43,8 +43,7 @@ Expected BaseShmFactory::Create(std::strin perm_map[score::os::Acl::Permission::kRead] = {uid}; perm_map[score::os::Acl::Permission::kWrite] = {uid}; - auto handle = ShmFactory::Create( - std::string(name), [](const auto&) noexcept {}, size, perm_map); + auto handle = ShmFactory::Create(std::string(name), [](const auto&) noexcept {}, size, perm_map); if (!handle) { return make_unexpected(common::DaemonErrorCode::kAllocationFailed); diff --git a/score/crypto/src/daemon/provider/score_provider/openssl/key_management/openssl_key_factory.cpp b/score/crypto/src/daemon/provider/score_provider/openssl/key_management/openssl_key_factory.cpp index dd3b1bb7c..9f0ed4e46 100644 --- a/score/crypto/src/daemon/provider/score_provider/openssl/key_management/openssl_key_factory.cpp +++ b/score/crypto/src/daemon/provider/score_provider/openssl/key_management/openssl_key_factory.cpp @@ -26,7 +26,7 @@ namespace score::crypto::daemon::provider::openssl { -OpenSslKeyFactory::OpenSslKeyFactory(common::ProviderId provider_id) : m_provider_id(provider_id){}; +OpenSslKeyFactory::OpenSslKeyFactory(common::ProviderId provider_id) : m_provider_id(provider_id) {}; ::score::crypto::Expected OpenSslKeyFactory::GenerateKey(const key_management::KeyGenerationRequest& request) From 6dca195a5e610f33640590c6b02cc419e1e62fa7 Mon Sep 17 00:00:00 2001 From: Athul Mallappallil Date: Wed, 12 Aug 2026 22:19:47 +0000 Subject: [PATCH 11/37] Minor doc update --- .../key_management_class_diagram.puml | 4 +-- .../architecture/key_management_details.rst | 27 ++++++++++--------- 2 files changed, 16 insertions(+), 15 deletions(-) diff --git a/score/crypto/docs/architecture/key_management_class_diagram.puml b/score/crypto/docs/architecture/key_management_class_diagram.puml index 2d340d86f..2a26895ee 100644 --- a/score/crypto/docs/architecture/key_management_class_diagram.puml +++ b/score/crypto/docs/architecture/key_management_class_diagram.puml @@ -184,7 +184,7 @@ package "Slot Management" <> #F0F8FF { } ' ==================================================================== -' SLOT DEPLOYMENT (slot/deployment/) +' SLOT DEPLOYMENT (slot/ façades → daemon/common/storage/ implementations) ' ==================================================================== package "Slot Deployment" <> #E8F0FE { @@ -230,7 +230,7 @@ package "Slot Deployment" <> #E8F0FE { class KvDeploymentWriter { + Write(path : string, info : SlotDeploymentInfo) : Expected .. writes [metadata] then [key] sections .. - .. opens with ios::trunc .. + .. delegates to file_io::WriteFile (atomic temp→rename) .. } } diff --git a/score/crypto/docs/architecture/key_management_details.rst b/score/crypto/docs/architecture/key_management_details.rst index 510ba4f5d..d3b55c445 100644 --- a/score/crypto/docs/architecture/key_management_details.rst +++ b/score/crypto/docs/architecture/key_management_details.rst @@ -619,22 +619,23 @@ implements ``IDeploymentLoader`` / ``IDeploymentWriter``: .. code-block:: text - slot/ - deployment_loader.hpp/.cpp ← façade (public API unchanged for all callers) + key_management/slot/ + deployment_loader.hpp/.cpp ← façade (delegates to common/storage/ impls) deployment_writer.hpp/.cpp ← façade - deployment/ - deployment_path_utils.hpp ← IsDeploymentPathSafe() — shared guard - i_deployment_loader.hpp ← pure-virtual interface - i_deployment_writer.hpp ← pure-virtual interface - kv/ - kv_deployment_loader.hpp/.cpp ← current implementation - kv_deployment_writer.hpp/.cpp - json/ ← reserved (add JsonDeploymentLoader when needed) - flatbuffer/ ← reserved + + daemon/common/storage/ ← shared by key_management and cert_management + deployment_path_utils.hpp ← IsDeploymentPathSafe() — shared guard + i_deployment_loader.hpp ← pure-virtual interface + i_deployment_writer.hpp ← pure-virtual interface + kv/ + kv_deployment_loader.hpp/.cpp ← current implementation + kv_deployment_writer.hpp/.cpp ← writes atomically via file_io::WriteFile + json/ ← reserved (add JsonDeploymentLoader when needed) + flatbuffer/ ← reserved To add a new format: implement ``IDeploymentLoader`` / ``IDeploymentWriter`` under -``slot/deployment//``, then add one ``if``-branch in each façade ``.cpp`` -and one dep in ``slot/deployment/BUILD``. No other files change. +``daemon/common/storage//``, then add one ``if``-branch in each façade ``.cpp`` +and one dep in ``daemon/common/storage/BUILD``. No other files change. **Key=value format (``"kv"``) — file layout** From 67989fa6bfcb2abcff3a35a31b7e5e0b698ef3a2 Mon Sep 17 00:00:00 2001 From: Athul Mallappallil Date: Thu, 13 Aug 2026 20:39:57 +0000 Subject: [PATCH 12/37] Make dependancy explicit in keym --- score/crypto/src/daemon/key_management/BUILD | 1 + 1 file changed, 1 insertion(+) diff --git a/score/crypto/src/daemon/key_management/BUILD b/score/crypto/src/daemon/key_management/BUILD index 1088c0130..11978775b 100644 --- a/score/crypto/src/daemon/key_management/BUILD +++ b/score/crypto/src/daemon/key_management/BUILD @@ -110,6 +110,7 @@ cc_library( ":key_management_headers", "//score/crypto/src/api/common:crypto_common", "//score/crypto/src/daemon/common", + "//score/crypto/src/daemon/common/storage:file_io", "//score/crypto/src/daemon/common/storage:kv_deployment", "//score/crypto/src/daemon/data_manager", "//score/crypto/src/daemon/provider:provider_headers", From 3763f70c1a2f37ca07b1f52ba8fba8ae85a3156d Mon Sep 17 00:00:00 2001 From: Athul Mallappallil Date: Fri, 14 Aug 2026 09:55:45 +0000 Subject: [PATCH 13/37] Minor code doc fix --- .../architecture/key_management_class_diagram.puml | 3 ++- score/crypto/src/api/common/types.hpp | 2 -- .../contexts/i_certificate_management_context.hpp | 6 ------ score/crypto/src/api/future/certificate/BUILD | 2 -- score/crypto/src/api/future/config/BUILD | 3 --- score/crypto/src/api/future/contexts/BUILD | 3 --- score/crypto/src/api/future/objects/BUILD | 2 -- .../common/storage/kv/kv_deployment_loader.cpp | 14 +++++++++----- 8 files changed, 11 insertions(+), 24 deletions(-) diff --git a/score/crypto/docs/architecture/key_management_class_diagram.puml b/score/crypto/docs/architecture/key_management_class_diagram.puml index 2a26895ee..c0f4f1c2f 100644 --- a/score/crypto/docs/architecture/key_management_class_diagram.puml +++ b/score/crypto/docs/architecture/key_management_class_diagram.puml @@ -225,12 +225,13 @@ package "Slot Deployment" <> #E8F0FE { + Load(path : string) : Expected .. parses [metadata] / [key] key=value sections .. .. blank lines and # comments ignored .. + .. delegates to file_io::ReadFile (score::filesystem) .. } class KvDeploymentWriter { + Write(path : string, info : SlotDeploymentInfo) : Expected .. writes [metadata] then [key] sections .. - .. delegates to file_io::WriteFile (atomic temp→rename) .. + .. delegates to file_io::WriteFile (atomic write via score::filesystem) .. } } diff --git a/score/crypto/src/api/common/types.hpp b/score/crypto/src/api/common/types.hpp index 6e4f14ec4..dcd4e149a 100644 --- a/score/crypto/src/api/common/types.hpp +++ b/score/crypto/src/api/common/types.hpp @@ -64,8 +64,6 @@ enum class ResourceType : uint8_t ///< verification. kKey, ///< Key material (generated / loaded / derived / imported) kCertificate, ///< Parsed or stored certificate object. - ///< CRLs are not a resource type: they are co-located with the - ///< issuer's certificate slot and never independently resolvable. kSecureObject, ///< Secure storage entry kDataObject ///< Generic data blob }; diff --git a/score/crypto/src/api/contexts/i_certificate_management_context.hpp b/score/crypto/src/api/contexts/i_certificate_management_context.hpp index 7fd6a97b3..6a275f454 100644 --- a/score/crypto/src/api/contexts/i_certificate_management_context.hpp +++ b/score/crypto/src/api/contexts/i_certificate_management_context.hpp @@ -36,7 +36,6 @@ namespace crypto /// @brief Interface for certificate lifecycle management operations. /// -/// Mirrors the structure of IKeyManagementContext for certificates: /// - **Parse** raw bytes into a daemon-backed ICertificateObject with an /// ephemeral resource ID. /// - **SaveCertificate** copies an ephemeral certificate to a persistent slot @@ -45,7 +44,6 @@ namespace crypto /// - **Export / convert** using a two-call pattern: query the required buffer /// size first, then fill the caller-supplied span. /// - **Slot management** and **trust store management** are co-located here. -/// - CRL, OCSP, and CSR operations are defined in future/ and not yet active. /// /// **ParseCertificate lifecycle**: /// @code @@ -213,10 +211,6 @@ class ICertificateManagementContext : public IContext /// @param cert_slot Handle to the slot whose CRL should be removed (type = kCertSlot) /// @return std::monostate on success, error if no CRL is present or access is denied virtual score::Result DeleteCrl(const CryptoResourceId& cert_slot) = 0; - - /// @brief Bulk-deletes expired CRLs across all certificate slots. - /// @return Number of CRLs deleted - virtual score::Result DeleteExpiredCrls() = 0; #endif // CRL management // ---- OCSP (not yet active — IPC implementation pending) ---- diff --git a/score/crypto/src/api/future/certificate/BUILD b/score/crypto/src/api/future/certificate/BUILD index 46c8ddd5e..2cf8d569d 100644 --- a/score/crypto/src/api/future/certificate/BUILD +++ b/score/crypto/src/api/future/certificate/BUILD @@ -17,8 +17,6 @@ load("@rules_cc//cc:defs.bzl", "cc_library") -# cert_types.hpp and i_ocsp_request_export.hpp moved to -# //score/crypto/src/api/certificate:cert_types (active). # cc_library( # name = "crypto_certificate", diff --git a/score/crypto/src/api/future/config/BUILD b/score/crypto/src/api/future/config/BUILD index 0697cc05b..d9b0d65c9 100644 --- a/score/crypto/src/api/future/config/BUILD +++ b/score/crypto/src/api/future/config/BUILD @@ -18,9 +18,6 @@ load("@rules_cc//cc:defs.bzl", "cc_library") -# certificate_context_config.hpp and certificate_verification_context_config.hpp -# moved to //score/crypto/src/api/config:cert_context_configs (active). - # cc_library( # name = "future_context_configs", # hdrs = [ diff --git a/score/crypto/src/api/future/contexts/BUILD b/score/crypto/src/api/future/contexts/BUILD index b84dfa1f3..226c95608 100644 --- a/score/crypto/src/api/future/contexts/BUILD +++ b/score/crypto/src/api/future/contexts/BUILD @@ -62,9 +62,6 @@ load("@rules_cc//cc:defs.bzl", "cc_library") # ], # ) -# i_certificate_management_context.hpp and i_certificate_verification_context.hpp -# moved to //score/crypto/src/api/contexts:cert_contexts (active). - # -- CSR -- # cc_library( # name = "csr_generation_context", diff --git a/score/crypto/src/api/future/objects/BUILD b/score/crypto/src/api/future/objects/BUILD index 6a6bf45e7..b31ce8318 100644 --- a/score/crypto/src/api/future/objects/BUILD +++ b/score/crypto/src/api/future/objects/BUILD @@ -17,8 +17,6 @@ load("@rules_cc//cc:defs.bzl", "cc_library") -# i_certificate_object.hpp and i_cert_slot_object.hpp moved to -# //score/crypto/src/api/objects:cert_objects (active). # cc_library( # name = "future_crypto_objects", diff --git a/score/crypto/src/daemon/common/storage/kv/kv_deployment_loader.cpp b/score/crypto/src/daemon/common/storage/kv/kv_deployment_loader.cpp index eda2e4204..34ec737cc 100644 --- a/score/crypto/src/daemon/common/storage/kv/kv_deployment_loader.cpp +++ b/score/crypto/src/daemon/common/storage/kv/kv_deployment_loader.cpp @@ -13,9 +13,9 @@ #include "score/crypto/src/daemon/common/storage/kv/kv_deployment_loader.hpp" +#include "score/crypto/src/daemon/common/storage/file_io.hpp" #include "score/mw/log/logging.h" -#include #include #include @@ -41,18 +41,22 @@ namespace score::crypto::Expected KvDeploymentLoader::Load( const std::string& path) { - std::ifstream file(path); - if (!file.is_open()) + constexpr std::size_t kMaxDescriptorSize = 64U * 1024U; + auto read_result = ReadFile(path, kMaxDescriptorSize); + if (!read_result.has_value()) { score::mw::log::LogError() << kLogPrefix << "Cannot open: " << path; - return score::crypto::make_unexpected(score::crypto::daemon::common::DaemonErrorCode::kInternalError); + return score::crypto::make_unexpected(read_result.error()); } + const auto& bytes = read_result.value(); + std::istringstream stream{std::string{reinterpret_cast(bytes.data()), bytes.size()}}; + DeploymentDescriptor descriptor; std::string current_section; std::string line; - while (std::getline(file, line)) + while (std::getline(stream, line)) { const std::string trimmed = Trim(line); if (trimmed.empty() || trimmed[0] == '#') From 5cc9b12139ec9ca0747c1f1c447b62d204b247db Mon Sep 17 00:00:00 2001 From: Athul Mallappallil Date: Tue, 18 Aug 2026 22:12:22 +0000 Subject: [PATCH 14/37] Cert APIs update and config entry rename --- .../i_certificate_management_context.hpp | 21 +++++++++++++++++-- .../i_certificate_verification_context.hpp | 4 ++-- score/crypto/src/daemon/common/types.hpp | 4 +++- score/crypto/src/daemon/config/inc/config.hpp | 16 +++++++------- .../slot/config_driven_slot_catalog.cpp | 2 +- 5 files changed, 33 insertions(+), 14 deletions(-) diff --git a/score/crypto/src/api/contexts/i_certificate_management_context.hpp b/score/crypto/src/api/contexts/i_certificate_management_context.hpp index 6a275f454..220234ba6 100644 --- a/score/crypto/src/api/contexts/i_certificate_management_context.hpp +++ b/score/crypto/src/api/contexts/i_certificate_management_context.hpp @@ -233,12 +233,29 @@ class ICertificateManagementContext : public IContext virtual score::Result AddCertificateToTrustStore(const CryptoResourceId& trust_store, const CryptoResourceId& cert) = 0; + /// @brief Removes a certificate from a persistent trust store by cert handle. + /// + /// The certificate must be loaded in the daemon (ephemeral or slot-loaded). + /// The daemon resolves the SHA-256 fingerprint internally from the handle. + /// + /// Normal path: caller has the cert already parsed or loaded from a slot. + /// + /// @param trust_store Handle to the trust store (type = kCertificateTrustStore) + /// @param cert Handle to the certificate to remove (type = kCertificate or kCertSlot) + virtual score::Result RemoveCertificateFromTrustStore(const CryptoResourceId& trust_store, + const CryptoResourceId& cert) = 0; + /// @brief Removes a certificate from a persistent trust store by SHA-256 fingerprint. /// - /// This changes trust-store deployment state and requires write access. + /// The certificate does not need to be loaded in the daemon. Use this when + /// the fingerprint is known from an external source (security bulletin, policy + /// document, trust-store listing) without the cert bytes being available. + /// + /// @param trust_store Handle to the trust store (type = kCertificateTrustStore) + /// @param sha256_fingerprint 32-byte SHA-256 fingerprint of the certificate to remove virtual score::Result RemoveCertificateFromTrustStore( const CryptoResourceId& trust_store, - score::cpp::span certificate_fingerprint) = 0; + score::cpp::span sha256_fingerprint) = 0; protected: ICertificateManagementContext() = default; diff --git a/score/crypto/src/api/contexts/i_certificate_verification_context.hpp b/score/crypto/src/api/contexts/i_certificate_verification_context.hpp index 8f0834efe..db6c7b106 100644 --- a/score/crypto/src/api/contexts/i_certificate_verification_context.hpp +++ b/score/crypto/src/api/contexts/i_certificate_verification_context.hpp @@ -72,13 +72,13 @@ class ICertificateVerificationContext : public IContext /// @brief Sets the leaf certificate to verify. /// @param cert Handle to the certificate to verify /// @return std::monostate on success, error if cert handle is invalid - /// @note Mutually exclusive with SetCertificateChain(). + /// @note Replaces any previously set certificate or chain on this context. virtual score::Result SetCertificate(const CryptoResourceId& cert) = 0; /// @brief Sets a certificate chain to verify (leaf first). /// @param chain Ordered chain of certificate handles (leaf first, root last) /// @return std::monostate on success, error if any handle is invalid - /// @note Mutually exclusive with SetCertificate(). + /// @note Replaces any previously set certificate or chain on this context. virtual score::Result SetCertificateChain(score::cpp::span chain) = 0; /// @brief Sets the system trust store to use for certificate chain verification. diff --git a/score/crypto/src/daemon/common/types.hpp b/score/crypto/src/daemon/common/types.hpp index 55b112cb2..77f435e92 100644 --- a/score/crypto/src/daemon/common/types.hpp +++ b/score/crypto/src/daemon/common/types.hpp @@ -196,7 +196,9 @@ enum class ProviderCapability : std::uint8_t kNone = 0x00U, ///< No functional capability advertised kCrypto = 0x01U, ///< Symmetric cipher / hash / MAC handlers (GetCryptoHandlerFactory) kKeyManagement = 0x02U, ///< Key generation / storage (GetKeyFactory / GetKeySlotHandler) - kCertManagement = 0x04U, ///< Certificate parse / verify / CSR (GetCertFactory) + kCertManagement = 0x04U, ///< Certificate parsing / verification (GetCertParser). + /// Does NOT imply cert-slot storage — slot handlers are + /// selected by name (slot.storage_backend), not by this bit. }; /// @brief Bitwise OR for combining provider capabilities. diff --git a/score/crypto/src/daemon/config/inc/config.hpp b/score/crypto/src/daemon/config/inc/config.hpp index cec1841d4..dd081dc29 100644 --- a/score/crypto/src/daemon/config/inc/config.hpp +++ b/score/crypto/src/daemon/config/inc/config.hpp @@ -272,11 +272,11 @@ class KeyConfig /// /// The daemon's SlotRegistry stores these mappings and resolves them /// transparently during ResolveResource IPC calls. - struct AppResourceEntry + struct AppKeySlotEntry { uint32_t uid; ///< UID of the application that owns this mapping std::string app_resource_id; ///< Application-local resource name - std::string slot_name; ///< Actual slot name registered in the daemon registry + std::string slot_name; ///< Actual key slot name registered in the daemon registry }; KeyConfig() = default; @@ -294,15 +294,15 @@ class KeyConfig } /// @brief Add an application resource mapping entry (called by parser). - void AddAppResourceEntry(AppResourceEntry entry) + void AddAppKeySlotEntry(AppKeySlotEntry entry) { - m_app_resource_entries.push_back(std::move(entry)); + m_app_key_slot_entries.push_back(std::move(entry)); } - /// @brief Get all per-application resource ID mappings. - const std::vector& GetAppResourceEntries() const + /// @brief Get all per-application key slot resource ID mappings. + const std::vector& GetAppKeySlotEntries() const { - return m_app_resource_entries; + return m_app_key_slot_entries; } /// @brief Path to the JSON key slot manifest file (optional). @@ -321,7 +321,7 @@ class KeyConfig private: std::vector m_slot_entries; - std::vector m_app_resource_entries; + std::vector m_app_key_slot_entries; std::string m_manifest_path; }; diff --git a/score/crypto/src/daemon/key_management/slot/config_driven_slot_catalog.cpp b/score/crypto/src/daemon/key_management/slot/config_driven_slot_catalog.cpp index 12037bb92..65f146549 100644 --- a/score/crypto/src/daemon/key_management/slot/config_driven_slot_catalog.cpp +++ b/score/crypto/src/daemon/key_management/slot/config_driven_slot_catalog.cpp @@ -123,7 +123,7 @@ void ConfigDrivenSlotCatalog::Load(SlotRegistry& registry) score::mw::log::LogDebug() << LOG_PREFIX << "Loaded" << entries.size() << " slot(s) from configuration."; // Register per-application resource ID mappings. - for (const auto& mapping : m_key_config.GetAppResourceEntries()) + for (const auto& mapping : m_key_config.GetAppKeySlotEntries()) { registry.RegisterAppResource(mapping.uid, mapping.app_resource_id, mapping.slot_name); } From af7c509fec7cd6a3c25633ddae72a959a68846e0 Mon Sep 17 00:00:00 2001 From: Athul Mallappallil Date: Wed, 19 Aug 2026 07:49:17 +0000 Subject: [PATCH 15/37] Fix name inconsistancy in config --- .../src/daemon/config/crypto_config.fbs | 10 +++--- .../config/src/flatbuffer_config_parser.cpp | 32 +++++++++---------- .../config/src/flatbuffer_config_parser.hpp | 8 ++--- .../config/key_management_test_config.json | 2 +- .../config/integration_test_config.json | 2 +- 5 files changed, 27 insertions(+), 27 deletions(-) diff --git a/score/crypto/src/daemon/config/crypto_config.fbs b/score/crypto/src/daemon/config/crypto_config.fbs index a5f09561a..ed720cddd 100644 --- a/score/crypto/src/daemon/config/crypto_config.fbs +++ b/score/crypto/src/daemon/config/crypto_config.fbs @@ -39,10 +39,10 @@ table KeySlotEntry { deployment_format: string (required); } -/// Per-application resource ID to slot name mapping +/// Per-application resource ID to key slot name mapping /// Applications reference keys by portable application-local names /// (e.g., "signing_key") -table AppResourceEntry { +table AppKeySlotEntry { /// UID of the application that owns this mapping uid: uint32; /// Application-local resource name @@ -51,12 +51,12 @@ table AppResourceEntry { slot_name: string (required); } -/// Key slot config contains all slot and app resource definitions +/// Key slot config contains all slot and app key slot definitions table KeySlotConfig { /// All key slot definitions slot_entries: [KeySlotEntry] (required); - /// All per-app resource mappings - app_resource_entries: [AppResourceEntry] (required); + /// All per-app key slot mappings + app_key_slot_entries: [AppKeySlotEntry] (required); } /// Top-level configuration root_type diff --git a/score/crypto/src/daemon/config/src/flatbuffer_config_parser.cpp b/score/crypto/src/daemon/config/src/flatbuffer_config_parser.cpp index 0f41d670c..6f3f24918 100644 --- a/score/crypto/src/daemon/config/src/flatbuffer_config_parser.cpp +++ b/score/crypto/src/daemon/config/src/flatbuffer_config_parser.cpp @@ -72,8 +72,8 @@ Expected FlatBufferConfigParser::ParseK return make_unexpected(slot_entries_result.error()); } - // Parse app resource entries - auto app_resource_result = ParseAppResourceEntries(key_slot_config, out_config); + // Parse app key slot entries + auto app_resource_result = ParseAppKeySlotEntries(key_slot_config, out_config); if (!app_resource_result.has_value()) { return make_unexpected(app_resource_result.error()); @@ -81,7 +81,7 @@ Expected FlatBufferConfigParser::ParseK score::mw::log::LogDebug() << LOG_PREFIX << "Successfully parsed configuration. Loaded " << out_config.GetSlotEntries().size() << " slot(s) and " - << out_config.GetAppResourceEntries().size() << " app resource mapping(s)."; + << out_config.GetAppKeySlotEntries().size() << " app key slot mapping(s)."; return std::monostate{}; } @@ -220,43 +220,43 @@ Expected FlatBufferConfigParser::ParseF return ParseFromBuffer(buffer.data(), buffer.size(), out_config); } -Expected FlatBufferConfigParser::ParseAppResourceEntries( +Expected FlatBufferConfigParser::ParseAppKeySlotEntries( const KeySlotConfig* key_slot_config, KeyConfig& out_config) { - const auto* app_resource_entries = key_slot_config->app_resource_entries(); - if (!app_resource_entries) + const auto* app_key_slot_entries = key_slot_config->app_key_slot_entries(); + if (!app_key_slot_entries) { - return std::monostate{}; // No app resource entries is not an error + return std::monostate{}; // No app key slot entries is not an error } - for (const auto* entry : *app_resource_entries) + for (const auto* entry : *app_key_slot_entries) { if (!entry) { - score::mw::log::LogError() << LOG_PREFIX << "Null app resource entry encountered - invalid configuration"; + score::mw::log::LogError() << LOG_PREFIX << "Null app key slot entry encountered - invalid configuration"; return make_unexpected(common::DaemonErrorCode::kInternalError); } - KeyConfig::AppResourceEntry resource_entry; - resource_entry.uid = entry->uid(); + KeyConfig::AppKeySlotEntry key_slot_entry; + key_slot_entry.uid = entry->uid(); // All fields below are required per schema - add defensive checks if (!entry->app_resource_id()) { - score::mw::log::LogError() << LOG_PREFIX << "App resource entry missing required field 'app_resource_id'"; + score::mw::log::LogError() << LOG_PREFIX << "App key slot entry missing required field 'app_resource_id'"; return make_unexpected(common::DaemonErrorCode::kInternalError); } - resource_entry.app_resource_id = entry->app_resource_id()->str(); + key_slot_entry.app_resource_id = entry->app_resource_id()->str(); if (!entry->slot_name()) { - score::mw::log::LogError() << LOG_PREFIX << "App resource entry missing required field 'slot_name'"; + score::mw::log::LogError() << LOG_PREFIX << "App key slot entry missing required field 'slot_name'"; return make_unexpected(common::DaemonErrorCode::kInternalError); } - resource_entry.slot_name = entry->slot_name()->str(); + key_slot_entry.slot_name = entry->slot_name()->str(); - out_config.AddAppResourceEntry(std::move(resource_entry)); + out_config.AddAppKeySlotEntry(std::move(key_slot_entry)); } return std::monostate{}; diff --git a/score/crypto/src/daemon/config/src/flatbuffer_config_parser.hpp b/score/crypto/src/daemon/config/src/flatbuffer_config_parser.hpp index 3766d7f4b..4efc26a1d 100644 --- a/score/crypto/src/daemon/config/src/flatbuffer_config_parser.hpp +++ b/score/crypto/src/daemon/config/src/flatbuffer_config_parser.hpp @@ -125,16 +125,16 @@ class FlatBufferConfigParser const keyslot::KeySlotConfig* key_slot_config, KeyConfig& out_config); - /// @brief Parse app resource entries from key slot configuration. + /// @brief Parse app key slot entries from key slot configuration. /// - /// Iterates through app resource entries in the KeySlotConfig and populates - /// KeyConfig with parsed AppResourceEntry objects. + /// Iterates through app key slot entries in the KeySlotConfig and populates + /// KeyConfig with parsed AppKeySlotEntry objects. /// /// @param key_slot_config Pointer to the KeySlotConfig FlatBuffers object /// @param out_config Output KeyConfig object to populate /// @return Success (monostate) on successful parsing, DaemonErrorCode on failure /// @retval kInternalError Invalid entry structure or missing required fields - static Expected ParseAppResourceEntries( + static Expected ParseAppKeySlotEntries( const keyslot::KeySlotConfig* key_slot_config, KeyConfig& out_config); }; diff --git a/score/crypto/tests/key_management/config/key_management_test_config.json b/score/crypto/tests/key_management/config/key_management_test_config.json index 6af995447..fae797185 100644 --- a/score/crypto/tests/key_management/config/key_management_test_config.json +++ b/score/crypto/tests/key_management/config/key_management_test_config.json @@ -33,7 +33,7 @@ "deployment_format": "kv" } ], - "app_resource_entries": [ + "app_key_slot_entries": [ { "uid": 0, "app_resource_id": "test/hmac", diff --git a/score/tests/test_vectors/config/integration_test_config.json b/score/tests/test_vectors/config/integration_test_config.json index 54d039393..01fe8f8df 100644 --- a/score/tests/test_vectors/config/integration_test_config.json +++ b/score/tests/test_vectors/config/integration_test_config.json @@ -53,7 +53,7 @@ "deployment_format" : "kv" } ], - "app_resource_entries": [ + "app_key_slot_entries": [ { "uid": 0, "app_resource_id": "test/hmac", From 7e8aefae85fe79a7322309b08f18c68af467332a Mon Sep 17 00:00:00 2001 From: Athul Mallappallil Date: Tue, 25 Aug 2026 10:21:16 +0000 Subject: [PATCH 16/37] Minor fix on the file write --- score/crypto/src/daemon/common/storage/file_io.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/score/crypto/src/daemon/common/storage/file_io.cpp b/score/crypto/src/daemon/common/storage/file_io.cpp index a08f789c6..bd542842f 100644 --- a/score/crypto/src/daemon/common/storage/file_io.cpp +++ b/score/crypto/src/daemon/common/storage/file_io.cpp @@ -57,7 +57,7 @@ score::crypto::Expected WriteFile(const std::st } score::filesystem::FileFactory factory{}; - auto stream_result = factory.AtomicUpdate(target_path, std::ios::binary | std::ios::trunc); + auto stream_result = factory.AtomicUpdate(target_path, std::ios::out | std::ios::binary | std::ios::trunc); if (!stream_result.has_value()) return score::crypto::make_unexpected(DaemonErrorCode::kPersistFailed); From 4371adf8305e6b0c79a3e11f87e2b38b6f5f2cc7 Mon Sep 17 00:00:00 2001 From: Athul Mallappallil Date: Fri, 28 Aug 2026 10:15:42 +0000 Subject: [PATCH 17/37] API update for cert management --- .../i_certificate_management_context.hpp | 112 +++++++++--- score/crypto/src/api/objects/BUILD | 1 + .../src/api/objects/i_certificate_object.hpp | 14 +- .../src/api/objects/i_trust_store_object.hpp | 165 ++++++++++++++++++ 4 files changed, 263 insertions(+), 29 deletions(-) create mode 100644 score/crypto/src/api/objects/i_trust_store_object.hpp diff --git a/score/crypto/src/api/contexts/i_certificate_management_context.hpp b/score/crypto/src/api/contexts/i_certificate_management_context.hpp index 220234ba6..71e075dc1 100644 --- a/score/crypto/src/api/contexts/i_certificate_management_context.hpp +++ b/score/crypto/src/api/contexts/i_certificate_management_context.hpp @@ -14,7 +14,6 @@ #ifndef SCORE_CRYPTO_SRC_API_CONTEXTS_I_CERTIFICATE_MANAGEMENT_CONTEXT_HPP #define SCORE_CRYPTO_SRC_API_CONTEXTS_I_CERTIFICATE_MANAGEMENT_CONTEXT_HPP -#include "score/crypto/src/api/certificate/i_ocsp_request_export.hpp" #include "score/crypto/src/api/common/crypto_resource_guard.hpp" #include "score/crypto/src/api/common/types.hpp" #include "score/crypto/src/api/contexts/i_context.hpp" @@ -26,6 +25,7 @@ #include #include #include +#include #include namespace score @@ -101,8 +101,7 @@ class ICertificateManagementContext : public IContext /// /// Typical usage: parse → inspect fields → save to slot. /// - /// @param cert CryptoResourceId of the certificate to save (type = kCertificate). - /// Pass the result of ICertificateObject::GetId() directly. + /// @param cert CryptoResourceId of the certificate to save (type = kCertificate or kCertSlot) /// @param target_slot Handle to the target slot (type = kCertSlot) /// @return std::monostate on success, error if slot is occupied or access is denied virtual score::Result SaveCertificate(const CryptoResourceId& cert, @@ -189,18 +188,39 @@ class ICertificateManagementContext : public IContext /// @return Number of certificates deleted virtual score::Result DeleteExpiredCertificates() = 0; - // ---- CRL management (not yet active — IPC implementation pending) ---- -#if 0 + // ---- Persistence — with optional CRL propagation ---- + + /// @brief Copies an ephemeral certificate to a persistent slot, optionally propagating its CRL. + /// + /// When @p with_crl is true the daemon propagates the CRL already held for @p cert: + /// - If a session-scoped CRL was previously associated via ImportCrl() (persist=false), + /// that CRL is used (works for both kCertificate and kCertSlot sources). + /// - Otherwise the CRL is read from @p cert's slot's persistent [crl] section + /// (only applicable when cert is a kCertSlot source). + /// + /// No CRL re-validation occurs — the daemon reuses the CRL it already accepted. + /// + /// @param cert CryptoResourceId of the certificate to save (type = kCertificate or kCertSlot) + /// @param target_slot Handle to the target slot (type = kCertSlot) + /// @param with_crl When true, propagate the associated CRL to the destination slot + virtual score::Result SaveCertificate(const CryptoResourceId& cert, + const CryptoResourceId& target_slot, + bool with_crl) = 0; + + // ---- CRL management ---- + /// @brief Imports a Certificate Revocation List and associates it with its issuer certificate. /// /// The CRL lifecycle follows the lifecycle of @p issuer_cert: /// - kCertSlot + persist=true: CRL written to the slot's [crl] section; write access required. - /// - kCertSlot or kCertificate + persist=false (default): session-scoped; no write access needed. + /// - kCertSlot or kCertificate + persist=false: session-scoped in-memory; no write access needed. + /// Session CRL is consumed by SaveCertificate(with_crl=true) and + /// AddCertificateToTrustStore(with_crl=true) without re-passing raw bytes. /// - /// @param crl_data Encoded CRL data - /// @param format Encoding format of the CRL + /// @param crl_data Encoded CRL data + /// @param format Encoding format of the CRL /// @param issuer_cert Handle to the issuer certificate (type = kCertSlot or kCertificate) - /// @param persist When true, store permanently to the issuer slot (kCertSlot only). + /// @param persist When true, store permanently to the issuer slot (kCertSlot only) /// @return std::monostate on success, error if validation fails or access is denied virtual score::Result ImportCrl(score::cpp::span crl_data, FormatType format, @@ -211,35 +231,37 @@ class ICertificateManagementContext : public IContext /// @param cert_slot Handle to the slot whose CRL should be removed (type = kCertSlot) /// @return std::monostate on success, error if no CRL is present or access is denied virtual score::Result DeleteCrl(const CryptoResourceId& cert_slot) = 0; -#endif // CRL management - // ---- OCSP (not yet active — IPC implementation pending) ---- -#if 0 - /// @brief Constructs an OCSP request for a certificate's revocation status. - /// - /// @param cert Handle to the certificate to check (type = kCertificate or kCertSlot) - /// @param issuer_cert Handle to the issuer certificate - /// @return Export object providing the DER-encoded request and responder URL - virtual score::Result GetOcspRequestData(const CryptoResourceId& cert, - const CryptoResourceId& issuer_cert) = 0; -#endif // OCSP + // ---- OCSP (reserved for future support) ---- + // /// @brief Constructs an OCSP request for a certificate's revocation status. + // /// + // /// @param cert Handle to the certificate to check (type = kCertificate or kCertSlot) + // /// @param issuer_cert Handle to the issuer certificate + // /// @return Export object providing the DER-encoded request and responder URL + // virtual score::Result GetOcspRequestData( + // const CryptoResourceId& cert, + // const CryptoResourceId& issuer_cert) = 0; // ---- Trust-store membership management ---- /// @brief Adds a certificate to a persistent trust store. /// - /// The certificate is assigned to a trust-store-managed slot. This is a - /// write operation and requires trust-store write access. + /// The certificate is assigned to a trust-store-managed exclusive slot. This is a + /// write operation and requires trust-store write access. Idempotent: if the cert + /// is already a member of any type, returns success without allocating a new slot. + /// + /// @param trust_store Handle to the trust store (type = kCertificateTrustStore) + /// @param cert Handle to the certificate to add (type = kCertificate or kCertSlot) + /// @param with_crl When true, propagate the associated CRL to the exclusive slot virtual score::Result AddCertificateToTrustStore(const CryptoResourceId& trust_store, - const CryptoResourceId& cert) = 0; + const CryptoResourceId& cert, + bool with_crl = false) = 0; /// @brief Removes a certificate from a persistent trust store by cert handle. /// /// The certificate must be loaded in the daemon (ephemeral or slot-loaded). /// The daemon resolves the SHA-256 fingerprint internally from the handle. /// - /// Normal path: caller has the cert already parsed or loaded from a slot. - /// /// @param trust_store Handle to the trust store (type = kCertificateTrustStore) /// @param cert Handle to the certificate to remove (type = kCertificate or kCertSlot) virtual score::Result RemoveCertificateFromTrustStore(const CryptoResourceId& trust_store, @@ -248,8 +270,7 @@ class ICertificateManagementContext : public IContext /// @brief Removes a certificate from a persistent trust store by SHA-256 fingerprint. /// /// The certificate does not need to be loaded in the daemon. Use this when - /// the fingerprint is known from an external source (security bulletin, policy - /// document, trust-store listing) without the cert bytes being available. + /// the fingerprint is known from an external source without the cert bytes being available. /// /// @param trust_store Handle to the trust store (type = kCertificateTrustStore) /// @param sha256_fingerprint 32-byte SHA-256 fingerprint of the certificate to remove @@ -257,6 +278,43 @@ class ICertificateManagementContext : public IContext const CryptoResourceId& trust_store, score::cpp::span sha256_fingerprint) = 0; + /// @brief Enables a previously disabled trust store member identified by its slot resource. + /// + /// Use the slot_id from ITrustStoreObject::MemberInfo to obtain the slot handle. + /// + /// @param trust_store Handle to the trust store (type = kCertificateTrustStore) + /// @param slot Handle to the member slot (type = kCertSlot) + virtual score::Result EnableTrustStoreMember(const CryptoResourceId& trust_store, + const CryptoResourceId& slot) = 0; + + /// @brief Disables a trust store member identified by its slot resource. + /// + /// A disabled member is excluded from anchor resolution; it remains in the store + /// and can be re-enabled. Use RemoveCertificateFromTrustStore to permanently remove. + /// + /// Use the slot_id from ITrustStoreObject::MemberInfo to obtain the slot handle. + /// + /// @param trust_store Handle to the trust store (type = kCertificateTrustStore) + /// @param slot Handle to the member slot (type = kCertSlot) + virtual score::Result DisableTrustStoreMember(const CryptoResourceId& trust_store, + const CryptoResourceId& slot) = 0; + + /// @brief Imports a CRL for a trust store exclusive member identified by its slot resource. + /// + /// Only kExclusiveMutable trust store slots are writable through this path. + /// For shared-static or conditional-external members, use ImportCrl directly on the slot. + /// + /// Use the slot_id from ITrustStoreObject::MemberInfo to obtain the slot handle. + /// + /// @param trust_store Handle to the trust store (type = kCertificateTrustStore) + /// @param slot Handle to the exclusive member slot (type = kCertSlot) + /// @param crl_data Encoded CRL bytes + /// @param format Encoding format of the CRL + virtual score::Result ImportCrlForTrustStoreMember(const CryptoResourceId& trust_store, + const CryptoResourceId& slot, + score::cpp::span crl_data, + FormatType format) = 0; + protected: ICertificateManagementContext() = default; }; diff --git a/score/crypto/src/api/objects/BUILD b/score/crypto/src/api/objects/BUILD index 1702080ff..ed7a69117 100644 --- a/score/crypto/src/api/objects/BUILD +++ b/score/crypto/src/api/objects/BUILD @@ -37,6 +37,7 @@ cc_library( hdrs = [ "i_cert_slot_object.hpp", "i_certificate_object.hpp", + "i_trust_store_object.hpp", ], includes = ["."], visibility = ["//visibility:public"], diff --git a/score/crypto/src/api/objects/i_certificate_object.hpp b/score/crypto/src/api/objects/i_certificate_object.hpp index d09a62224..fbea93ecf 100644 --- a/score/crypto/src/api/objects/i_certificate_object.hpp +++ b/score/crypto/src/api/objects/i_certificate_object.hpp @@ -14,14 +14,16 @@ #ifndef SCORE_CRYPTO_SRC_API_OBJECTS_I_CERTIFICATE_OBJECT_HPP #define SCORE_CRYPTO_SRC_API_OBJECTS_I_CERTIFICATE_OBJECT_HPP +#include "score/crypto/src/api/common/types.hpp" #include "score/crypto/src/api/objects/i_crypto_object.hpp" #include "score/result/result.h" #include "score/span.hpp" +#include #include #include #include -#include +#include namespace score { @@ -51,6 +53,8 @@ namespace crypto class ICertificateObject : public ICryptoObject { public: + static constexpr std::size_t kSha256FingerprintSize = 32U; + using Uptr = std::unique_ptr; ~ICertificateObject() override = default; @@ -76,9 +80,15 @@ class ICertificateObject : public ICryptoObject /// @return Algorithm string (e.g., "RSA-2048", "ECDSA-P256", "ML-DSA-65") virtual AlgorithmId GetPublicKeyAlgorithm() const noexcept = 0; - /// @brief Returns the certificate serial number as a hex-encoded string. + /// @brief Returns the certificate serial number as an uppercase hex string (e.g., "01ABCDEF"). virtual std::string GetSerialNumber() const = 0; + /// @brief Returns the SHA-256 fingerprint of the certificate as a 32-byte array. + /// + /// The fingerprint is the SHA-256 digest of the DER-encoded certificate. Together + /// with the issuer DN and serial number it uniquely identifies a certificate. + virtual std::array GetFingerprint() const noexcept = 0; + /// @brief Returns the byte size of the DER-encoded SubjectPublicKeyInfo. /// /// Call this before ExportPublicKey() to determine the required buffer size. diff --git a/score/crypto/src/api/objects/i_trust_store_object.hpp b/score/crypto/src/api/objects/i_trust_store_object.hpp new file mode 100644 index 000000000..3fb2b1c1a --- /dev/null +++ b/score/crypto/src/api/objects/i_trust_store_object.hpp @@ -0,0 +1,165 @@ +/******************************************************************************** + * 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 + ********************************************************************************/ + +#ifndef SCORE_CRYPTO_SRC_API_OBJECTS_I_TRUST_STORE_OBJECT_HPP +#define SCORE_CRYPTO_SRC_API_OBJECTS_I_TRUST_STORE_OBJECT_HPP + +#include "score/crypto/src/api/common/types.hpp" +#include "score/crypto/src/api/objects/i_crypto_object.hpp" +#include "score/span.hpp" + +#include +#include +#include +#include +#include +#include + +namespace score +{ + +namespace crypto +{ + +/// @brief Read-only typed view of a named trust store. +/// +/// Provides a point-in-time snapshot of trust store membership: which certificates +/// are present, their membership kind, and their enabled/disabled state. +/// +/// Mutations (add, remove, enable, disable, import CRL) are performed via +/// ICertificateManagementContext — not through this object. +/// +/// Obtained via ICryptoContext::GetTrustStoreObject(). +class ITrustStoreObject : public ICryptoObject +{ + public: + static constexpr std::size_t kSha256FingerprintSize = 32U; + + using Uptr = std::unique_ptr; + + /// @brief Membership kind of a trust store anchor. + enum class MemberKind : uint8_t + { + kSharedStatic = 0U, ///< Externally managed slot; read-only from trust store perspective. + kExclusiveMutable = 1U, ///< Trust-store-owned exclusive slot; mutable via management context. + kConditionalExternal = 2U ///< External slot; disabled after unexpected content change. + }; + + /// @brief Snapshot of a single trust store member. + /// + /// Both slot_id and sha256_fingerprint are provided so callers can: + /// - Use slot_id directly for enable/disable/importCrl operations without a round-trip. + /// - Use sha256_fingerprint for quick identity matching against externally known fingerprints + /// (e.g., from a security bulletin) without loading the full certificate. + /// - Use subject + issuer + serial_number for human-readable identification and logging. + struct MemberInfo + { + CryptoResourceId slot_id{}; ///< kCertSlot resource — use for management ops. + std::array + sha256_fingerprint{}; ///< SHA-256 fingerprint of the member certificate. + std::string subject; ///< RFC 4514 Subject DN (e.g., "CN=Root CA,O=ACME,C=DE"). + std::string issuer; ///< RFC 4514 Issuer DN. + std::string serial_number; ///< Uppercase hex serial (e.g., "01ABCDEF"). + MemberKind kind{MemberKind::kSharedStatic}; ///< Membership type. + bool is_enabled{true}; ///< Whether anchor is active for chain building. + }; + + ~ITrustStoreObject() override = default; + + ITrustStoreObject(const ITrustStoreObject&) = delete; + ITrustStoreObject& operator=(const ITrustStoreObject&) = delete; + ITrustStoreObject(ITrustStoreObject&&) = default; + ITrustStoreObject& operator=(ITrustStoreObject&&) = default; + + /// @brief Returns the point-in-time snapshot of all occupied trust store members. + /// + /// Each entry corresponds to a certificate slot that currently holds a certificate. + /// Empty slots (not yet populated) are omitted. + virtual const std::vector& GetMembers() const noexcept = 0; + + // ---- Non-virtual convenience accessors (operate on GetMembers() locally) ---- + + /// @brief Find the member entry for a given slot resource ID. + /// + /// Matches by id and type only — primary_provider is not compared because + /// MemberInfo slot_id has primary_provider=0 as a placeholder. + /// @returns Pointer to the matching MemberInfo, or nullptr if not a member. + [[nodiscard]] const MemberInfo* FindMember(const CryptoResourceId& slot) const noexcept + { + for (const auto& member : GetMembers()) + { + if (member.slot_id.id == slot.id && member.slot_id.type == slot.type) + { + return &member; + } + } + return nullptr; + } + + /// @brief Find the member with the given SHA-256 fingerprint. + /// + /// @param fingerprint 32-byte fingerprint span. Returns nullptr if its size is not 32. + /// @returns Pointer to the matching MemberInfo, or nullptr if not found. + [[nodiscard]] const MemberInfo* FindMemberByFingerprint(score::cpp::span fingerprint) const noexcept + { + if (fingerprint.size() != kSha256FingerprintSize) + { + return nullptr; + } + for (const auto& member : GetMembers()) + { + if (std::equal(fingerprint.begin(), fingerprint.end(), member.sha256_fingerprint.begin())) + { + return &member; + } + } + return nullptr; + } + + /// @brief Returns the slot IDs of all enabled trust store members. + [[nodiscard]] std::vector GetEnabledMemberSlotIds() const + { + std::vector result; + for (const auto& member : GetMembers()) + { + if (member.is_enabled) + { + result.push_back(member.slot_id); + } + } + return result; + } + + /// @brief Returns the slot IDs of all disabled trust store members. + [[nodiscard]] std::vector GetDisabledMemberSlotIds() const + { + std::vector result; + for (const auto& member : GetMembers()) + { + if (!member.is_enabled) + { + result.push_back(member.slot_id); + } + } + return result; + } + + protected: + ITrustStoreObject() = default; +}; + +} // namespace crypto + +} // namespace score + +#endif // SCORE_CRYPTO_SRC_API_OBJECTS_I_TRUST_STORE_OBJECT_HPP From f493baa2b2fc26f5b96572a9eedae890f4e48927 Mon Sep 17 00:00:00 2001 From: Athul Mallappallil Date: Wed, 9 Sep 2026 12:42:39 +0000 Subject: [PATCH 18/37] Fix coverage workflow --- tools/coverage/BUILD | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/tools/coverage/BUILD b/tools/coverage/BUILD index a5ae2a252..e2689d1b3 100644 --- a/tools/coverage/BUILD +++ b/tools/coverage/BUILD @@ -26,6 +26,10 @@ score_coverage_scope( visibility = ["//visibility:private"], deps = [ "//score/crypto/src/api:crypto_stack", + "//score/crypto/src/api/certificate:cert_types", + "//score/crypto/src/api/config:cert_context_configs", + "//score/crypto/src/api/contexts:cert_contexts", + "//score/crypto/src/api/objects:cert_objects", "//score/crypto/src/api/future/common:future_common", "//score/crypto/src/backend:active_pkcs11_backend", "//score/crypto/src/backend:pkcs11_backend_defines", From d48ac0d80c9da16a7a32ed3e665cb34fb03ec30e Mon Sep 17 00:00:00 2001 From: Athul Mallappallil Date: Fri, 18 Sep 2026 16:54:48 +0200 Subject: [PATCH 19/37] Refactor types to common, cert and keys --- score/crypto/src/api/BUILD | 2 + score/crypto/src/api/certificate/BUILD | 4 +- .../crypto/src/api/certificate/cert_types.hpp | 64 --- score/crypto/src/api/common/BUILD | 10 +- .../src/api/common/crypto_resource_guard.hpp | 2 +- .../src/api/common/i_memory_allocator.hpp | 2 +- .../src/api/common/src/i_release_callback.hpp | 2 +- score/crypto/src/api/common/types.hpp | 453 ------------------ score/crypto/src/api/config/BUILD | 2 + .../src/api/config/base_context_config.hpp | 2 +- .../api/config/certificate_context_config.hpp | 6 - ...ertificate_verification_context_config.hpp | 9 +- .../src/api/config/key_operation_params.hpp | 4 +- .../src/api/config/permission_builder.hpp | 2 +- score/crypto/src/api/contexts/BUILD | 7 +- .../i_certificate_management_context.hpp | 3 +- .../i_certificate_verification_context.hpp | 4 +- .../api/contexts/i_key_management_context.hpp | 3 +- .../api/contexts/src/hash_context_impl.cpp | 2 +- .../api/contexts/src/hash_context_impl.hpp | 2 +- .../src/key_management_context_impl.cpp | 3 +- .../src/key_management_context_impl.hpp | 3 +- .../src/api/contexts/src/mac_context_impl.cpp | 2 +- .../src/api/contexts/src/mac_context_impl.hpp | 2 +- score/crypto/src/api/data_plane/BUILD | 1 + .../api/future/config/aead_context_config.hpp | 2 +- .../future/config/cipher_context_config.hpp | 2 +- .../contexts/i_csr_generation_context.hpp | 2 +- score/crypto/src/api/i_crypto_context.hpp | 2 +- .../src/api/objects/i_certificate_object.hpp | 3 +- .../src/api/objects/i_crypto_object.hpp | 2 +- score/crypto/src/api/objects/i_key_object.hpp | 1 + .../src/api/objects/i_key_slot_object.hpp | 1 + .../src/api/objects/i_trust_store_object.hpp | 2 +- .../src/api/src/crypto_context_impl.cpp | 5 +- .../src/api/src/crypto_context_impl.hpp | 2 +- .../src/api/src/provider_type_converter.hpp | 2 +- score/crypto/src/api/types/BUILD | 46 ++ score/crypto/src/api/types/certificate.hpp | 112 +++++ score/crypto/src/api/types/common.hpp | 163 +++++++ score/crypto/src/api/types/key.hpp | 102 ++++ score/crypto/src/backend/BUILD | 2 +- score/crypto/src/daemon/common/BUILD | 1 + score/crypto/src/daemon/config/inc/config.hpp | 10 +- score/crypto/src/daemon/data_plane/BUILD | 1 + score/crypto/src/daemon/key_management/BUILD | 3 + .../daemon/key_management/core/key_entry.hpp | 2 +- .../detail/slot_info_builder.hpp | 3 +- .../interfaces/i_key_factory.hpp | 2 +- .../interfaces/i_key_slot_handler.hpp | 3 +- .../interfaces/key_slot_config.hpp | 3 +- .../key_management/interfaces/key_types.hpp | 3 +- .../nodes/key_slot_data_node.hpp | 3 +- .../slot/access_policy_enforcer.hpp | 3 +- .../slot/file_backed_slot_handler.cpp | 12 +- .../key_management/slot/slot_registry.hpp | 2 +- .../crypto/src/daemon/provider/handler/BUILD | 1 + .../provider/handler/handler_init_params.hpp | 2 +- .../key_management/pkcs11_key_store.cpp | 2 +- .../key_management/pkcs11_key_store.hpp | 2 +- .../operations/mac/pkcs11_mac_context.hpp | 2 +- .../operations/mac/pkcs11_mac_executor.cpp | 2 +- .../operations/mac/pkcs11_mac_executor.hpp | 2 +- .../operations/mac/pkcs11_mac_handler.cpp | 2 +- score/crypto/tests/key_management/BUILD | 1 + .../test_access_policy_enforcer.cpp | 3 +- .../test_key_config_manager.cpp | 3 +- .../test_key_management_context.cpp | 3 +- .../test_openssl_key_handler.cpp | 3 +- .../integration_tests/score_api_mac_test.cpp | 2 +- score/tests/integration_tests/score_demo.cpp | 2 +- tools/coverage/BUILD | 2 +- 72 files changed, 539 insertions(+), 593 deletions(-) delete mode 100644 score/crypto/src/api/certificate/cert_types.hpp delete mode 100644 score/crypto/src/api/common/types.hpp create mode 100644 score/crypto/src/api/types/BUILD create mode 100644 score/crypto/src/api/types/certificate.hpp create mode 100644 score/crypto/src/api/types/common.hpp create mode 100644 score/crypto/src/api/types/key.hpp diff --git a/score/crypto/src/api/BUILD b/score/crypto/src/api/BUILD index 5cf44811d..4d8e1ec9c 100644 --- a/score/crypto/src/api/BUILD +++ b/score/crypto/src/api/BUILD @@ -47,6 +47,8 @@ cc_library( deps = [ "//score/crypto/src/api:operations", "//score/crypto/src/api/common:crypto_common", + "//score/crypto/src/api/types:types", + "//score/crypto/src/api/config:cert_context_configs", "//score/crypto/src/api/config:context_configs", "//score/crypto/src/api/contexts:context_bases", "//score/crypto/src/api/contexts:crypto_contexts", diff --git a/score/crypto/src/api/certificate/BUILD b/score/crypto/src/api/certificate/BUILD index a0e9c52e4..c3047b02d 100644 --- a/score/crypto/src/api/certificate/BUILD +++ b/score/crypto/src/api/certificate/BUILD @@ -14,15 +14,13 @@ load("@rules_cc//cc:defs.bzl", "cc_library") cc_library( - name = "cert_types", + name = "ocsp_export", hdrs = [ - "cert_types.hpp", "i_ocsp_request_export.hpp", ], includes = ["."], visibility = ["//visibility:public"], deps = [ - "//score/crypto/src/api/common:crypto_common", "@score_baselibs//score/language/futurecpp", "@score_baselibs//score/result", ], diff --git a/score/crypto/src/api/certificate/cert_types.hpp b/score/crypto/src/api/certificate/cert_types.hpp deleted file mode 100644 index cd2f697be..000000000 --- a/score/crypto/src/api/certificate/cert_types.hpp +++ /dev/null @@ -1,64 +0,0 @@ -/******************************************************************************** - * 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 - ********************************************************************************/ - -#ifndef SCORE_CRYPTO_SRC_API_CERTIFICATE_CERT_TYPES_HPP -#define SCORE_CRYPTO_SRC_API_CERTIFICATE_CERT_TYPES_HPP - -#include "score/crypto/src/api/common/types.hpp" - -#include - -namespace score -{ - -namespace crypto -{ - -/// @brief Result of a certificate verification operation. -enum class CertVerifyResult : uint8_t -{ - kValid, ///< Certificate is valid and trusted - kExpired, ///< Certificate has expired - kNotYetValid, ///< Certificate is not yet valid (future notBefore) - kRevoked, ///< Certificate has been revoked - kNoRootFound, ///< Root CA is not in the trust store - kChainIncomplete, ///< Intermediate certificate missing - kSignatureInvalid, ///< Signature verification failed - kInvalidPurpose, ///< Key usage or extended key usage mismatch - kUnknownAlgorithm, ///< Unsupported or unknown algorithm in certificate - kUnknownError ///< Unspecified verification failure -}; - -/// @brief Controls where certificate-chain verification may terminate. -enum class ChainTerminationPolicy : uint8_t -{ - /// Require a complete path ending at a self-signed trust-store root. - kRootRequired, - /// Permit termination at the first certificate present in the trust store. - kTrustStoreTerminated -}; - -/// @brief Status of an OCSP response. -enum class OcspStatus : uint8_t -{ - kGood, ///< Certificate is not revoked - kRevoked, ///< Certificate has been revoked - kUnknown, ///< Responder does not know the certificate - kError ///< OCSP response could not be parsed or verified -}; - -} // namespace crypto - -} // namespace score - -#endif // SCORE_CRYPTO_SRC_API_CERTIFICATE_CERT_TYPES_HPP diff --git a/score/crypto/src/api/common/BUILD b/score/crypto/src/api/common/BUILD index 77b6993a4..63e81b5e8 100644 --- a/score/crypto/src/api/common/BUILD +++ b/score/crypto/src/api/common/BUILD @@ -13,6 +13,13 @@ load("@rules_cc//cc:defs.bzl", "cc_library") +cc_library( + name = "crypto_common_base", + hdrs = ["fixed_capacity_string.hpp"], + includes = ["."], + visibility = ["//visibility:public"], +) + cc_library( name = "crypto_common", srcs = [ @@ -27,11 +34,12 @@ cc_library( "fixed_capacity_string.hpp", "i_memory.hpp", "i_memory_allocator.hpp", - "types.hpp", ], includes = ["."], visibility = ["//visibility:public"], deps = [ + ":crypto_common_base", + "//score/crypto/src/api/types:common_types", "@score_baselibs//score/language/futurecpp", "@score_baselibs//score/result", ], diff --git a/score/crypto/src/api/common/crypto_resource_guard.hpp b/score/crypto/src/api/common/crypto_resource_guard.hpp index b1a69bfb1..831df0ec9 100644 --- a/score/crypto/src/api/common/crypto_resource_guard.hpp +++ b/score/crypto/src/api/common/crypto_resource_guard.hpp @@ -14,7 +14,7 @@ #ifndef SCORE_CRYPTO_SRC_API_COMMON_CRYPTO_RESOURCE_GUARD_HPP #define SCORE_CRYPTO_SRC_API_COMMON_CRYPTO_RESOURCE_GUARD_HPP -#include "score/crypto/src/api/common/types.hpp" +#include "score/crypto/src/api/types/common.hpp" #include "score/result/result.h" #include diff --git a/score/crypto/src/api/common/i_memory_allocator.hpp b/score/crypto/src/api/common/i_memory_allocator.hpp index 3ceaac56f..941bea398 100644 --- a/score/crypto/src/api/common/i_memory_allocator.hpp +++ b/score/crypto/src/api/common/i_memory_allocator.hpp @@ -15,7 +15,7 @@ #define SCORE_CRYPTO_SRC_API_COMMON_I_MEMORY_ALLOCATOR_HPP #include "score/crypto/src/api/common/i_memory.hpp" -#include "score/crypto/src/api/common/types.hpp" +#include "score/crypto/src/api/types/common.hpp" #include "score/result/result.h" #include diff --git a/score/crypto/src/api/common/src/i_release_callback.hpp b/score/crypto/src/api/common/src/i_release_callback.hpp index abdb6aae7..d76356741 100644 --- a/score/crypto/src/api/common/src/i_release_callback.hpp +++ b/score/crypto/src/api/common/src/i_release_callback.hpp @@ -14,7 +14,7 @@ #ifndef SCORE_CRYPTO_SRC_API_COMMON_SRC_I_RELEASE_CALLBACK_HPP #define SCORE_CRYPTO_SRC_API_COMMON_SRC_I_RELEASE_CALLBACK_HPP -#include "score/crypto/src/api/common/types.hpp" +#include "score/crypto/src/api/types/common.hpp" #include "score/result/result.h" #include diff --git a/score/crypto/src/api/common/types.hpp b/score/crypto/src/api/common/types.hpp deleted file mode 100644 index dcd4e149a..000000000 --- a/score/crypto/src/api/common/types.hpp +++ /dev/null @@ -1,453 +0,0 @@ -/******************************************************************************** - * 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 - ********************************************************************************/ - -#ifndef SCORE_CRYPTO_SRC_API_COMMON_TYPES_HPP -#define SCORE_CRYPTO_SRC_API_COMMON_TYPES_HPP - -#include "score/crypto/src/api/common/error_domain.hpp" -#include "score/crypto/src/api/common/fixed_capacity_string.hpp" - -#include -#include -#include -#include - -namespace score -{ - -namespace crypto -{ - -/// @brief Application-defined name for config files (e.g., "KeySlot_42"). -/// Only used at resolution time via ICryptoContext::ResolveResource(). -/// -/// Uses FixedCapacityString<64> — stack-allocated, no heap allocation. -/// Resource names are deployment-time configuration values (bounded, immutable). -/// All observed resource name strings are well under 64 characters. -using ResourceId = FixedCapacityString<64>; - -/// @brief String-based algorithm identifier for extensibility (including PQC). -/// -/// Uses FixedCapacityString<64> — a stack-allocated, non-heap-allocating string -/// that satisfies automotive safety requirements (deterministic memory, no heap -/// fragmentation) while preserving extensibility. New algorithms can be added -/// 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", -/// "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 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 - kCertificateTrustStore, ///< Named group of trusted CA certificates used for certificate chain - ///< verification. - kKey, ///< Key material (generated / loaded / derived / imported) - kCertificate, ///< Parsed or stored certificate object. - kSecureObject, ///< Secure storage entry - kDataObject ///< Generic data blob -}; - -/// @brief Persistence classification of a crypto resource. -enum class ResourcePersistence : uint8_t -{ - kPersistent, ///< Survives context/stack destruction, stored in provider - kEphemeral ///< Auto-cleaned on context/stack destruction -}; - -/// @brief The sole runtime handle for all resolved crypto resources. -/// -/// Applications resolve string-based ResourceId to CryptoResourceId once via -/// ICryptoContext::ResolveResource(), then use this compact struct for all -/// subsequent operations. No string fields — all comparisons are numeric/enum. -/// -/// @note Struct is ~16 bytes with padding, fully numeric, cheap to copy and hash. -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. - ///< Embeds device binding: identifies which - ///< provider/device owns this resource. - ///< 0 = unbound (e.g., trust anchors). - - constexpr bool operator==(const CryptoResourceId& other) const noexcept - { - return (id == other.id) && (type == other.type) && (persistence == other.persistence) && - (primary_provider == other.primary_provider); - } - - constexpr bool operator!=(const CryptoResourceId& other) const noexcept - { - return !(*this == other); - } -}; - -/// @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 -}; - -/// @brief Certificate and key data encoding format. -enum class FormatType : uint8_t -{ - kDer, ///< DER (binary) encoding - kPem ///< PEM (base64-armored) encoding -}; - -/// @brief State of a key slot. -/// -/// Renamed from KeySlotStatus to disambiguate from CertificateStatus. -enum class KeySlotState : uint8_t -{ - kEmpty, ///< Slot contains no key material - kOccupied, ///< Slot contains a key - kLocked ///< Slot is in use and cannot be modified -}; - -/// @brief State of a certificate slot. -/// -/// Certificate slots contain certificate/CRL storage and are not bound to a -/// certificate-operation provider. Provider selection is made by the -/// operation/context that parses or verifies the material. -enum class CertificateSlotState : uint8_t -{ - kEmpty, ///< Slot contains no certificate - kOccupied, ///< Slot contains a certificate - kLocked ///< Slot is in use and cannot be modified -}; - -/// @brief Validity status of a certificate. -enum class CertificateStatus : uint8_t -{ - kValid, ///< Certificate is valid - kRevoked, ///< Certificate has been revoked - kExpired, ///< Certificate has expired - kUnknown ///< Status cannot be determined -}; - -/// @brief Direction for symmetric cipher and AEAD operations. -enum class CipherDirection : uint8_t -{ - kEncrypt, ///< Encryption / sealing direction - kDecrypt ///< Decryption / opening direction -}; - -/// @brief Intended usage mode for MAC and signature contexts. -/// -/// Specifies whether the context will be used to generate (sign) or verify. -/// Used for: -/// - Key permission enforcement (kSign for generation, kVerify for verification). -/// - Provider-specific API selection (e.g. PKCS#11 C_SignInit vs C_VerifyInit). -/// -/// @note CipherDirection covers encrypt/decrypt for symmetric ciphers. -/// OperationMode covers MAC generation/verification and future signature contexts. -enum class OperationMode : uint8_t -{ - kGenerate, ///< MAC generation / signature creation - kVerify ///< MAC verification / signature verification -}; - -/// @brief Type of shared memory to allocate for the data plane. -enum class MemoryType : uint8_t -{ - kDefault, ///< Daemon-managed shared memory, suitable for most providers. - ///< The daemon may copy data into provider-compatible memory internally. - kProviderCompatible ///< Memory directly usable by a specific provider (e.g., DMA-capable - ///< for HW/TEE), enabling true zero-copy from application through - ///< daemon to the crypto device. -}; - -/// @brief Controls the revocation checking strategy in ICertificateVerificationContext. -enum class RevocationCheckPolicy : uint8_t -{ - kNone, ///< No revocation checking - kCrlOnly, ///< Check revocation using CRL only - kOcspOnly, ///< Check revocation using OCSP only - kOcspWithCrlFallback ///< Prefer OCSP, fall back to CRL if OCSP is unavailable -}; - -/// @brief Bitmask defining which cryptographic operations a key is permitted to perform. -/// -/// Key operation permissions enforce the principle of least privilege: a key -/// configured for signing cannot be misused for encryption, and vice versa. -/// Permissions are assigned when a key slot is provisioned (daemon-side -/// configuration) and optionally constrained further at key generation time. -/// -/// The permission model uses a capability-centric bitmask grouped by -/// operation category: -/// - **Data protection** (bits 0–3): encrypt, decrypt, wrap, unwrap -/// - **Authentication** (bits 4–7): sign, verify, mac, agree -/// - **Key lifecycle** (bits 8–10): derive, export, import -/// -/// Composite presets are provided for common deployment patterns. -/// Use bitwise OR to combine individual permissions. -/// -/// @note Permission enforcement is performed by the daemon at context -/// creation time. If a key's permissions do not include the operation -/// requested by the context, the daemon returns -/// CryptoErrorCode::kKeyOperationNotPermitted. -enum class KeyOperationPermission : uint32_t -{ - kNone = 0x0000U, ///< No operations permitted (storage-only key) - - // ---- Data protection (bits 0–3) ---- - kEncrypt = 0x0001U, ///< Symmetric/asymmetric encryption - kDecrypt = 0x0002U, ///< Symmetric/asymmetric decryption - kWrap = 0x0004U, ///< Key wrapping (encrypting another key) - kUnwrap = 0x0008U, ///< Key unwrapping (decrypting a wrapped key) - - // ---- Authentication (bits 4–7) ---- - kSign = 0x0010U, ///< Digital signature generation - kVerify = 0x0020U, ///< Digital signature verification - kMac = 0x0040U, ///< MAC generation and verification - kAgree = 0x0080U, ///< Key agreement (ECDH, ML-KEM decapsulation) - - // ---- Key lifecycle (bits 8–10) ---- - kDerive = 0x0100U, ///< Key derivation (as source key) - kExport = 0x0200U, ///< Export raw key material (for exportable keys) - kImport = 0x0400U, ///< Slot accepts imported key material - - // ---- Composite presets (common deployment patterns) ---- - - /// Data protection: encrypt + decrypt + wrap + unwrap - kDataProtection = 0x000FU, - /// Authentication: sign + verify + mac + agree - kAuthentication = 0x00F0U, - /// Full lifecycle: derive + export + import - kFullLifecycle = 0x0700U, - /// All operations permitted (no restrictions) - kAll = 0x07FFU, -}; - -/// @brief Bitwise OR for combining key operation permissions. -inline constexpr KeyOperationPermission operator|(KeyOperationPermission lhs, KeyOperationPermission rhs) noexcept -{ - return static_cast(static_cast(lhs) | static_cast(rhs)); -} - -/// @brief Bitwise AND for testing key operation permissions. -inline constexpr KeyOperationPermission operator&(KeyOperationPermission lhs, KeyOperationPermission rhs) noexcept -{ - return static_cast(static_cast(lhs) & static_cast(rhs)); -} - -/// @brief Bitwise NOT for inverting key operation permissions. -/// @note Result is masked to valid permission bits (0–10) to prevent undefined states. -inline constexpr KeyOperationPermission operator~(KeyOperationPermission perm) noexcept -{ - constexpr uint32_t kValidBitsMask = 0x07FFU; // Bits 0–10 only - return static_cast((~static_cast(perm)) & kValidBitsMask); -} - -/// @brief Bitwise OR-assign for accumulating key operation permissions. -inline constexpr KeyOperationPermission& operator|=(KeyOperationPermission& lhs, KeyOperationPermission rhs) noexcept -{ - lhs = lhs | rhs; - return lhs; -} - -/// @brief Bitwise AND-assign for masking key operation permissions. -inline constexpr KeyOperationPermission& operator&=(KeyOperationPermission& lhs, KeyOperationPermission rhs) noexcept -{ - lhs = lhs & rhs; - return lhs; -} - -/// @brief Tests whether a permission set includes a specific required permission. -/// @param granted The permission set to test (e.g., from KeySlotInfo) -/// @param required The permission(s) being checked -/// @return true if all bits in required are set in granted -inline constexpr bool HasPermission(KeyOperationPermission granted, KeyOperationPermission required) noexcept -{ - // Only consider defined permission bits (bits 0..10). Mask both operands - // to avoid granting permissions due to out-of-range/invalid bits. - constexpr uint32_t kValidBitsMask = 0x07FFU; // bits 0..10 - const uint32_t g = static_cast(granted) & kValidBitsMask; - const uint32_t r = static_cast(required) & kValidBitsMask; - return (g & r) == r; -} - -/// @brief Lightweight information about certificate-slot storage. -/// -/// Returned by ICertificateManagementContext::GetCertificateSlotInfo(). -/// Certificate-specific details such as subject, issuer, algorithm, and -/// validity are obtained by loading/parsing the certificate. -struct CertificateSlotInfo -{ - CertificateSlotState state{CertificateSlotState::kEmpty}; - bool has_crl{false}; ///< Whether a CRL is currently associated with the slot -}; - -/// @brief Information about a key slot and its contents. -/// -/// Returned by IKeyManagementContext::GetKeySlotInfo(). Exposes device binding, -/// cross-provider compatibility, and permitted operations so applications can -/// make informed decisions. -struct KeySlotInfo -{ - KeySlotState state{KeySlotState::kEmpty}; ///< State of the key slot - AlgorithmId algorithm{}; ///< Algorithm of the stored key (empty if slot is empty) - uint16_t primary_provider{0U}; ///< Provider/device that owns this slot - - /// @brief Secondary providers that can also use keys in this slot. - /// - /// Fixed-capacity array (max 8 providers). Use compatible_provider_count - /// to determine how many entries are valid. - static constexpr std::size_t kMaxCompatibleProviders = 8U; - std::array compatible_providers{}; - std::size_t compatible_provider_count{0U}; - - /// @brief Operations this key slot permits. - /// - /// Defaults to kAll for backward compatibility. When provisioned with - /// restricted permissions, the daemon enforces them at context creation - /// time — creating an encrypt context with a sign-only key returns - /// CryptoErrorCode::kKeyOperationNotPermitted. - KeyOperationPermission permitted_operations{KeyOperationPermission::kAll}; -}; - -/// @brief Human-readable provider metadata. -/// -/// Returned by ICryptoContext::GetProviderInfo(). Maps daemon-assigned numeric -/// provider IDs to descriptive information. -struct ProviderInfo -{ - uint16_t id{0U}; ///< Daemon-assigned provider index - ProviderType type{ProviderType::kDefault}; ///< Provider classification - FixedCapacityString<32> name{}; ///< Human-readable provider name (e.g., "OpenSSL", "SoftHSM") -}; - -/// @brief Cross-provider compatibility information for a resource. -/// -/// Returned by ICryptoContext::QueryProviderCompatibility(). Secondary providers -/// are those that can also use this resource (e.g., a SW-exported key re-importable -/// into another SW provider). Not embedded in CryptoResourceId because the -/// secondary list is variable-length and mutable daemon-side state. -struct ProviderCompatibilityInfo -{ - CryptoResourceId resource{}; ///< The queried resource - uint16_t primary_provider{0U}; ///< Owning provider - - /// @brief Providers that can also use this resource. - /// - /// Fixed-capacity array (max 8 providers). Use secondary_provider_count - /// to determine how many entries are valid. - static constexpr std::size_t kMaxSecondaryProviders = 8U; - std::array secondary_providers{}; - std::size_t secondary_provider_count{0U}; -}; - -/// @brief Capabilities of a specific algorithm as reported by the daemon. -/// -/// Returned by ICryptoContext::QueryCapabilities(). Includes PQC algorithms -/// (e.g., "ML-KEM-768", "ML-DSA-65") when supported by configured providers. -struct AlgorithmCapabilities -{ - AlgorithmId id{}; ///< Algorithm identifier - bool supported{false}; ///< Whether any configured provider supports this algorithm - - /// @brief Supported modes/variants (e.g., "CBC", "GCM", "CTR"). - /// - /// Fixed-capacity array (max 16 modes). Use mode_count to determine - /// how many entries are valid. - static constexpr std::size_t kMaxModes = 16U; - std::array, kMaxModes> modes{}; - std::size_t mode_count{0U}; -}; - -/// @brief Aggregate view of all providers and supported algorithms. -/// -/// Returned by the parameterless ICryptoContext::QueryCapabilities() overload. -/// Provides a complete snapshot of the system's crypto capabilities. -struct SystemCapabilities -{ - /// @brief All configured providers. - /// - /// Fixed-capacity array (max 16 providers). Use provider_count to determine - /// how many entries are valid. - static constexpr std::size_t kMaxProviders = 16U; - std::array providers{}; - std::size_t provider_count{0U}; - - /// @brief All supported algorithms. - /// - /// Fixed-capacity array (max 64 algorithms). Use algorithm_count to determine - /// how many entries are valid. - static constexpr std::size_t kMaxAlgorithms = 64U; - std::array algorithms{}; - std::size_t algorithm_count{0U}; -}; - -/// @brief Single key-value entry for algorithm- or operation-scoped extended parameters. -struct ExtendedParameterEntry -{ - FixedCapacityString<32> key{}; ///< Parameter name (middleware-defined, not provider-defined) - FixedCapacityString<64> value{}; ///< Parameter value -}; - -/// @brief Fixed-capacity key-value map for algorithm- or operation-scoped extended parameters. -/// -/// Provides a forward-compatible extension point in context configs for parameters -/// that are not yet modeled as typed fields (e.g., PQC parameter sets, key-derivation -/// context strings). Keys and their semantics are defined by the **middleware -/// specification** — never by the underlying crypto provider or hardware back-end. -/// -/// Provider-specific tuning (HSM slot indices, PIN policies, vendor flags, etc.) -/// belongs exclusively in the daemon's static configuration and must NOT appear -/// here. Application code using this struct must remain portable across all -/// compliant provider implementations. -/// -/// Max 16 entries. -struct ExtendedParameters -{ - static constexpr std::size_t kMaxEntries = 16U; - std::array entries{}; - std::size_t entry_count{0U}; -}; - -} // namespace crypto - -} // namespace score - -/// @brief std::hash specialization for CryptoResourceId, enabling use in unordered containers. -template <> -struct std::hash -{ - std::size_t operator()(const score::crypto::CryptoResourceId& rid) const noexcept - { - std::size_t h = std::hash{}(rid.id); - h ^= std::hash{}(static_cast(rid.type)) << 1U; - h ^= std::hash{}(static_cast(rid.persistence)) << 2U; - h ^= std::hash{}(rid.primary_provider) << 3U; - return h; - } -}; - -#endif // SCORE_CRYPTO_SRC_API_COMMON_TYPES_HPP diff --git a/score/crypto/src/api/config/BUILD b/score/crypto/src/api/config/BUILD index 1dd33a64e..4b5288625 100644 --- a/score/crypto/src/api/config/BUILD +++ b/score/crypto/src/api/config/BUILD @@ -27,6 +27,7 @@ cc_library( visibility = ["//visibility:public"], deps = [ "//score/crypto/src/api/common:crypto_common", + "//score/crypto/src/api/types:types", ], ) @@ -41,5 +42,6 @@ cc_library( deps = [ ":context_configs", "//score/crypto/src/api/common:crypto_common", + "//score/crypto/src/api/types:types", ], ) diff --git a/score/crypto/src/api/config/base_context_config.hpp b/score/crypto/src/api/config/base_context_config.hpp index efe3cd18f..3ec03ed4e 100644 --- a/score/crypto/src/api/config/base_context_config.hpp +++ b/score/crypto/src/api/config/base_context_config.hpp @@ -14,7 +14,7 @@ #ifndef SCORE_CRYPTO_SRC_API_CONFIG_BASE_CONTEXT_CONFIG_HPP #define SCORE_CRYPTO_SRC_API_CONFIG_BASE_CONTEXT_CONFIG_HPP -#include "score/crypto/src/api/common/types.hpp" +#include "score/crypto/src/api/types/common.hpp" #include #include diff --git a/score/crypto/src/api/config/certificate_context_config.hpp b/score/crypto/src/api/config/certificate_context_config.hpp index 8808c2def..a0b94b39c 100644 --- a/score/crypto/src/api/config/certificate_context_config.hpp +++ b/score/crypto/src/api/config/certificate_context_config.hpp @@ -38,12 +38,6 @@ struct CertificateContextConfig : public BaseContextConfig { // -- Fluent builder -- - CertificateContextConfig& SetAlgorithm(const AlgorithmId& alg) noexcept - { - BaseContextConfig::SetAlgorithm(alg); - return *this; - } - CertificateContextConfig& SetProvider(const CryptoResourceId& prov) noexcept { BaseContextConfig::SetProvider(prov); diff --git a/score/crypto/src/api/config/certificate_verification_context_config.hpp b/score/crypto/src/api/config/certificate_verification_context_config.hpp index 838fd0c75..0eac7dd5f 100644 --- a/score/crypto/src/api/config/certificate_verification_context_config.hpp +++ b/score/crypto/src/api/config/certificate_verification_context_config.hpp @@ -14,8 +14,9 @@ #ifndef SCORE_CRYPTO_SRC_API_CONFIG_CERTIFICATE_VERIFICATION_CONTEXT_CONFIG_HPP #define SCORE_CRYPTO_SRC_API_CONFIG_CERTIFICATE_VERIFICATION_CONTEXT_CONFIG_HPP -#include "score/crypto/src/api/common/types.hpp" #include "score/crypto/src/api/config/base_context_config.hpp" +#include "score/crypto/src/api/types/certificate.hpp" +#include "score/crypto/src/api/types/common.hpp" #include @@ -51,12 +52,6 @@ struct CertificateVerificationContextConfig : public BaseContextConfig // -- Fluent builder -- - CertificateVerificationContextConfig& SetAlgorithm(const AlgorithmId& alg) noexcept - { - BaseContextConfig::SetAlgorithm(alg); - return *this; - } - CertificateVerificationContextConfig& SetProvider(const CryptoResourceId& prov) noexcept { BaseContextConfig::SetProvider(prov); diff --git a/score/crypto/src/api/config/key_operation_params.hpp b/score/crypto/src/api/config/key_operation_params.hpp index 6e56182ac..94c50c0d8 100644 --- a/score/crypto/src/api/config/key_operation_params.hpp +++ b/score/crypto/src/api/config/key_operation_params.hpp @@ -14,7 +14,9 @@ #ifndef SCORE_CRYPTO_SRC_API_CONFIG_KEY_OPERATION_PARAMS_HPP #define SCORE_CRYPTO_SRC_API_CONFIG_KEY_OPERATION_PARAMS_HPP -#include "score/crypto/src/api/common/types.hpp" +#include "score/crypto/src/api/common/error_domain.hpp" +#include "score/crypto/src/api/types/common.hpp" +#include "score/crypto/src/api/types/key.hpp" #include "score/span.hpp" #include diff --git a/score/crypto/src/api/config/permission_builder.hpp b/score/crypto/src/api/config/permission_builder.hpp index 9b3272d8b..12def9a5d 100644 --- a/score/crypto/src/api/config/permission_builder.hpp +++ b/score/crypto/src/api/config/permission_builder.hpp @@ -14,7 +14,7 @@ #ifndef SCORE_CRYPTO_SRC_API_CONFIG_PERMISSION_BUILDER_HPP #define SCORE_CRYPTO_SRC_API_CONFIG_PERMISSION_BUILDER_HPP -#include "score/crypto/src/api/common/types.hpp" +#include "score/crypto/src/api/types/key.hpp" namespace score { diff --git a/score/crypto/src/api/contexts/BUILD b/score/crypto/src/api/contexts/BUILD index 06375baa8..bb3fe3f19 100644 --- a/score/crypto/src/api/contexts/BUILD +++ b/score/crypto/src/api/contexts/BUILD @@ -24,6 +24,7 @@ cc_library( visibility = ["//visibility:public"], deps = [ "//score/crypto/src/api/common:crypto_common", + "//score/crypto/src/api/types:types", "@score_baselibs//score/language/futurecpp", "@score_baselibs//score/result", ], @@ -41,6 +42,7 @@ cc_library( deps = [ ":context_bases", "//score/crypto/src/api/common:crypto_common", + "//score/crypto/src/api/types:types", "//score/crypto/src/api/config:context_configs", "//score/crypto/src/api/objects:crypto_objects", "@score_baselibs//score/language/futurecpp", @@ -58,8 +60,10 @@ cc_library( visibility = ["//visibility:public"], deps = [ ":context_bases", - "//score/crypto/src/api/certificate:cert_types", + "//score/crypto/src/api/types:certificate_types", + "//score/crypto/src/api/certificate:ocsp_export", "//score/crypto/src/api/common:crypto_common", + "//score/crypto/src/api/types:types", "//score/crypto/src/api/config:cert_context_configs", "//score/crypto/src/api/objects:cert_objects", "@score_baselibs//score/language/futurecpp", @@ -86,6 +90,7 @@ cc_library( "//score/crypto/src/api:operations", "//score/crypto/src/api/common:crypto_common", "//score/crypto/src/api/common:release_callback_iface", + "//score/crypto/src/api/types:types", "//score/crypto/src/api/control_plane", "//score/crypto/src/api/data_plane:data_plane_client", ], diff --git a/score/crypto/src/api/contexts/i_certificate_management_context.hpp b/score/crypto/src/api/contexts/i_certificate_management_context.hpp index 71e075dc1..f4c4882f9 100644 --- a/score/crypto/src/api/contexts/i_certificate_management_context.hpp +++ b/score/crypto/src/api/contexts/i_certificate_management_context.hpp @@ -15,9 +15,10 @@ #define SCORE_CRYPTO_SRC_API_CONTEXTS_I_CERTIFICATE_MANAGEMENT_CONTEXT_HPP #include "score/crypto/src/api/common/crypto_resource_guard.hpp" -#include "score/crypto/src/api/common/types.hpp" #include "score/crypto/src/api/contexts/i_context.hpp" #include "score/crypto/src/api/objects/i_certificate_object.hpp" +#include "score/crypto/src/api/types/certificate.hpp" +#include "score/crypto/src/api/types/common.hpp" #include "score/result/result.h" #include "score/span.hpp" diff --git a/score/crypto/src/api/contexts/i_certificate_verification_context.hpp b/score/crypto/src/api/contexts/i_certificate_verification_context.hpp index db6c7b106..c7e69f785 100644 --- a/score/crypto/src/api/contexts/i_certificate_verification_context.hpp +++ b/score/crypto/src/api/contexts/i_certificate_verification_context.hpp @@ -14,9 +14,9 @@ #ifndef SCORE_CRYPTO_SRC_API_CONTEXTS_I_CERTIFICATE_VERIFICATION_CONTEXT_HPP #define SCORE_CRYPTO_SRC_API_CONTEXTS_I_CERTIFICATE_VERIFICATION_CONTEXT_HPP -#include "score/crypto/src/api/certificate/cert_types.hpp" -#include "score/crypto/src/api/common/types.hpp" #include "score/crypto/src/api/contexts/i_context.hpp" +#include "score/crypto/src/api/types/certificate.hpp" +#include "score/crypto/src/api/types/common.hpp" #include "score/result/result.h" #include "score/span.hpp" diff --git a/score/crypto/src/api/contexts/i_key_management_context.hpp b/score/crypto/src/api/contexts/i_key_management_context.hpp index 4592cfa7e..c618cbf4c 100644 --- a/score/crypto/src/api/contexts/i_key_management_context.hpp +++ b/score/crypto/src/api/contexts/i_key_management_context.hpp @@ -15,9 +15,10 @@ #define SCORE_CRYPTO_SRC_API_CONTEXTS_I_KEY_MANAGEMENT_CONTEXT_HPP #include "score/crypto/src/api/common/crypto_resource_guard.hpp" -#include "score/crypto/src/api/common/types.hpp" #include "score/crypto/src/api/config/key_operation_params.hpp" #include "score/crypto/src/api/contexts/i_context.hpp" +#include "score/crypto/src/api/types/common.hpp" +#include "score/crypto/src/api/types/key.hpp" #include "score/result/result.h" #include "score/span.hpp" 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..34044bea6 100644 --- a/score/crypto/src/api/contexts/src/hash_context_impl.cpp +++ b/score/crypto/src/api/contexts/src/hash_context_impl.cpp @@ -14,7 +14,7 @@ #include "score/crypto/src/api/contexts/src/hash_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/types/common.hpp" #include "score/crypto/src/api/control_plane/i_connection.hpp" #include "score/crypto/src/daemon/common/actors.hpp" 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..615990d57 100644 --- a/score/crypto/src/api/contexts/src/hash_context_impl.hpp +++ b/score/crypto/src/api/contexts/src/hash_context_impl.hpp @@ -16,8 +16,8 @@ #include "score/crypto/src/api/contexts/i_hash_context.hpp" -#include "score/crypto/src/api/common/types.hpp" #include "score/crypto/src/api/data_plane/i_buffer_transcoder.hpp" +#include "score/crypto/src/api/types/common.hpp" #include "score/crypto/src/api/control_plane/i_connection.hpp" diff --git a/score/crypto/src/api/contexts/src/key_management_context_impl.cpp b/score/crypto/src/api/contexts/src/key_management_context_impl.cpp index bd4e2d5e3..1502062bc 100644 --- a/score/crypto/src/api/contexts/src/key_management_context_impl.cpp +++ b/score/crypto/src/api/contexts/src/key_management_context_impl.cpp @@ -17,8 +17,9 @@ #include "score/crypto/src/api/common/error_domain.hpp" #include "score/crypto/src/api/common/src/crypto_resource_guard_factory.hpp" #include "score/crypto/src/api/common/src/i_release_callback.hpp" -#include "score/crypto/src/api/common/types.hpp" #include "score/crypto/src/api/config/key_operation_params.hpp" +#include "score/crypto/src/api/types/common.hpp" +#include "score/crypto/src/api/types/key.hpp" #include "score/crypto/src/api/control_plane/i_connection.hpp" #include "score/crypto/src/daemon/common/actors.hpp" diff --git a/score/crypto/src/api/contexts/src/key_management_context_impl.hpp b/score/crypto/src/api/contexts/src/key_management_context_impl.hpp index 27fba5478..4a033ccf3 100644 --- a/score/crypto/src/api/contexts/src/key_management_context_impl.hpp +++ b/score/crypto/src/api/contexts/src/key_management_context_impl.hpp @@ -16,9 +16,10 @@ #include "score/crypto/src/api/common/crypto_resource_guard.hpp" #include "score/crypto/src/api/common/src/i_release_callback.hpp" -#include "score/crypto/src/api/common/types.hpp" #include "score/crypto/src/api/config/key_operation_params.hpp" #include "score/crypto/src/api/contexts/i_key_management_context.hpp" +#include "score/crypto/src/api/types/common.hpp" +#include "score/crypto/src/api/types/key.hpp" #include "score/crypto/src/api/control_plane/i_connection.hpp" #include "score/crypto/src/daemon/control_plane/control_protocol.h" diff --git a/score/crypto/src/api/contexts/src/mac_context_impl.cpp b/score/crypto/src/api/contexts/src/mac_context_impl.cpp index f0fd654fc..fe675617f 100644 --- a/score/crypto/src/api/contexts/src/mac_context_impl.cpp +++ b/score/crypto/src/api/contexts/src/mac_context_impl.cpp @@ -14,7 +14,7 @@ #include "score/crypto/src/api/contexts/src/mac_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/types/common.hpp" #include "score/crypto/src/api/control_plane/i_connection.hpp" #include "score/crypto/src/daemon/common/actors.hpp" diff --git a/score/crypto/src/api/contexts/src/mac_context_impl.hpp b/score/crypto/src/api/contexts/src/mac_context_impl.hpp index 10748adef..2f503c669 100644 --- a/score/crypto/src/api/contexts/src/mac_context_impl.hpp +++ b/score/crypto/src/api/contexts/src/mac_context_impl.hpp @@ -16,8 +16,8 @@ #include "score/crypto/src/api/contexts/i_mac_context.hpp" -#include "score/crypto/src/api/common/types.hpp" #include "score/crypto/src/api/data_plane/i_buffer_transcoder.hpp" +#include "score/crypto/src/api/types/common.hpp" #include "score/crypto/src/api/control_plane/i_connection.hpp" diff --git a/score/crypto/src/api/data_plane/BUILD b/score/crypto/src/api/data_plane/BUILD index d75ce015a..e53c4f5d2 100644 --- a/score/crypto/src/api/data_plane/BUILD +++ b/score/crypto/src/api/data_plane/BUILD @@ -39,6 +39,7 @@ cc_library( visibility = ["//visibility:public"], deps = [ "//score/crypto/src/api/common:crypto_common", + "//score/crypto/src/api/types:types", "//score/crypto/src/api/control_plane", "//score/crypto/src/common:common_types", "//score/crypto/src/daemon/common", diff --git a/score/crypto/src/api/future/config/aead_context_config.hpp b/score/crypto/src/api/future/config/aead_context_config.hpp index b69b9320a..e545f5f48 100644 --- a/score/crypto/src/api/future/config/aead_context_config.hpp +++ b/score/crypto/src/api/future/config/aead_context_config.hpp @@ -14,8 +14,8 @@ #ifndef SCORE_CRYPTO_SRC_API_FUTURE_CONFIG_AEAD_CONTEXT_CONFIG_HPP #define SCORE_CRYPTO_SRC_API_FUTURE_CONFIG_AEAD_CONTEXT_CONFIG_HPP -#include "score/crypto/src/api/common/types.hpp" #include "score/crypto/src/api/config/base_context_config.hpp" +#include "score/crypto/src/api/types/common.hpp" namespace score { diff --git a/score/crypto/src/api/future/config/cipher_context_config.hpp b/score/crypto/src/api/future/config/cipher_context_config.hpp index cb99e2a5b..59c2d853d 100644 --- a/score/crypto/src/api/future/config/cipher_context_config.hpp +++ b/score/crypto/src/api/future/config/cipher_context_config.hpp @@ -15,8 +15,8 @@ #define SCORE_CRYPTO_SRC_API_FUTURE_CONFIG_CIPHER_CONTEXT_CONFIG_HPP #include "score/crypto/src/api/common/crypto_resource_guard.hpp" -#include "score/crypto/src/api/common/types.hpp" #include "score/crypto/src/api/config/base_context_config.hpp" +#include "score/crypto/src/api/types/common.hpp" namespace score { diff --git a/score/crypto/src/api/future/contexts/i_csr_generation_context.hpp b/score/crypto/src/api/future/contexts/i_csr_generation_context.hpp index 2333cf320..75ac8e5aa 100644 --- a/score/crypto/src/api/future/contexts/i_csr_generation_context.hpp +++ b/score/crypto/src/api/future/contexts/i_csr_generation_context.hpp @@ -15,8 +15,8 @@ #define SCORE_CRYPTO_SRC_API_FUTURE_CONTEXTS_I_CSR_GENERATION_CONTEXT_HPP #include "score/crypto/src/api/certificate/i_csr_export.hpp" -#include "score/crypto/src/api/common/types.hpp" #include "score/crypto/src/api/contexts/i_context.hpp" +#include "score/crypto/src/api/types/common.hpp" #include "score/result/result.h" #include diff --git a/score/crypto/src/api/i_crypto_context.hpp b/score/crypto/src/api/i_crypto_context.hpp index 2e614f724..1ff2b6f62 100644 --- a/score/crypto/src/api/i_crypto_context.hpp +++ b/score/crypto/src/api/i_crypto_context.hpp @@ -14,7 +14,7 @@ #ifndef SCORE_CRYPTO_SRC_API_I_CRYPTO_CONTEXT_HPP #define SCORE_CRYPTO_SRC_API_I_CRYPTO_CONTEXT_HPP -#include "score/crypto/src/api/common/types.hpp" +#include "score/crypto/src/api/types/common.hpp" #include "score/result/result.h" #include diff --git a/score/crypto/src/api/objects/i_certificate_object.hpp b/score/crypto/src/api/objects/i_certificate_object.hpp index fbea93ecf..eefdb2ca5 100644 --- a/score/crypto/src/api/objects/i_certificate_object.hpp +++ b/score/crypto/src/api/objects/i_certificate_object.hpp @@ -14,8 +14,9 @@ #ifndef SCORE_CRYPTO_SRC_API_OBJECTS_I_CERTIFICATE_OBJECT_HPP #define SCORE_CRYPTO_SRC_API_OBJECTS_I_CERTIFICATE_OBJECT_HPP -#include "score/crypto/src/api/common/types.hpp" #include "score/crypto/src/api/objects/i_crypto_object.hpp" +#include "score/crypto/src/api/types/certificate.hpp" +#include "score/crypto/src/api/types/common.hpp" #include "score/result/result.h" #include "score/span.hpp" diff --git a/score/crypto/src/api/objects/i_crypto_object.hpp b/score/crypto/src/api/objects/i_crypto_object.hpp index 383e98ec9..764d65e03 100644 --- a/score/crypto/src/api/objects/i_crypto_object.hpp +++ b/score/crypto/src/api/objects/i_crypto_object.hpp @@ -14,7 +14,7 @@ #ifndef SCORE_CRYPTO_SRC_API_OBJECTS_I_CRYPTO_OBJECT_HPP #define SCORE_CRYPTO_SRC_API_OBJECTS_I_CRYPTO_OBJECT_HPP -#include "score/crypto/src/api/common/types.hpp" +#include "score/crypto/src/api/types/common.hpp" #include diff --git a/score/crypto/src/api/objects/i_key_object.hpp b/score/crypto/src/api/objects/i_key_object.hpp index 7f8700d12..6bea23ae3 100644 --- a/score/crypto/src/api/objects/i_key_object.hpp +++ b/score/crypto/src/api/objects/i_key_object.hpp @@ -15,6 +15,7 @@ #define SCORE_CRYPTO_SRC_API_OBJECTS_I_KEY_OBJECT_HPP #include "score/crypto/src/api/objects/i_crypto_object.hpp" +#include "score/crypto/src/api/types/key.hpp" #include #include diff --git a/score/crypto/src/api/objects/i_key_slot_object.hpp b/score/crypto/src/api/objects/i_key_slot_object.hpp index a001ac0d4..06af2dd02 100644 --- a/score/crypto/src/api/objects/i_key_slot_object.hpp +++ b/score/crypto/src/api/objects/i_key_slot_object.hpp @@ -15,6 +15,7 @@ #define SCORE_CRYPTO_SRC_API_OBJECTS_I_KEY_SLOT_OBJECT_HPP #include "score/crypto/src/api/objects/i_crypto_object.hpp" +#include "score/crypto/src/api/types/key.hpp" #include #include diff --git a/score/crypto/src/api/objects/i_trust_store_object.hpp b/score/crypto/src/api/objects/i_trust_store_object.hpp index 3fb2b1c1a..0c6477f22 100644 --- a/score/crypto/src/api/objects/i_trust_store_object.hpp +++ b/score/crypto/src/api/objects/i_trust_store_object.hpp @@ -14,8 +14,8 @@ #ifndef SCORE_CRYPTO_SRC_API_OBJECTS_I_TRUST_STORE_OBJECT_HPP #define SCORE_CRYPTO_SRC_API_OBJECTS_I_TRUST_STORE_OBJECT_HPP -#include "score/crypto/src/api/common/types.hpp" #include "score/crypto/src/api/objects/i_crypto_object.hpp" +#include "score/crypto/src/api/types/common.hpp" #include "score/span.hpp" #include diff --git a/score/crypto/src/api/src/crypto_context_impl.cpp b/score/crypto/src/api/src/crypto_context_impl.cpp index 1afc8e301..6673db782 100644 --- a/score/crypto/src/api/src/crypto_context_impl.cpp +++ b/score/crypto/src/api/src/crypto_context_impl.cpp @@ -14,7 +14,6 @@ #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/hash_context_config.hpp" #include "score/crypto/src/api/config/key_management_context_config.hpp" #include "score/crypto/src/api/config/mac_context_config.hpp" @@ -22,6 +21,10 @@ #include "score/crypto/src/api/contexts/src/key_management_context_impl.hpp" #include "score/crypto/src/api/contexts/src/mac_context_impl.hpp" #include "score/crypto/src/api/src/provider_type_converter.hpp" +#include "score/crypto/src/api/types/certificate.hpp" +#include "score/crypto/src/api/types/common.hpp" +#include "score/crypto/src/daemon/common/actors.hpp" +#include "score/crypto/src/daemon/common/types.hpp" #include "score/crypto/src/daemon/control_plane/control_protocol.h" #include "score/crypto/src/api/control_plane/i_connection.hpp" diff --git a/score/crypto/src/api/src/crypto_context_impl.hpp b/score/crypto/src/api/src/crypto_context_impl.hpp index ae84ec686..d85e63e76 100644 --- a/score/crypto/src/api/src/crypto_context_impl.hpp +++ b/score/crypto/src/api/src/crypto_context_impl.hpp @@ -14,9 +14,9 @@ #ifndef SCORE_CRYPTO_SRC_API_SRC_CRYPTO_CONTEXT_IMPL_HPP #define SCORE_CRYPTO_SRC_API_SRC_CRYPTO_CONTEXT_IMPL_HPP -#include "score/crypto/src/api/common/types.hpp" #include "score/crypto/src/api/data_plane/i_buffer_transcoder.hpp" #include "score/crypto/src/api/i_crypto_context.hpp" +#include "score/crypto/src/api/types/common.hpp" #include "score/crypto/src/api/control_plane/i_connection.hpp" #include "score/crypto/src/daemon/control_plane/control_protocol.h" diff --git a/score/crypto/src/api/src/provider_type_converter.hpp b/score/crypto/src/api/src/provider_type_converter.hpp index 5f6372d76..7b7758d53 100644 --- a/score/crypto/src/api/src/provider_type_converter.hpp +++ b/score/crypto/src/api/src/provider_type_converter.hpp @@ -14,7 +14,7 @@ #ifndef SCORE_CRYPTO_SRC_API_SRC_PROVIDER_TYPE_CONVERTER_HPP #define SCORE_CRYPTO_SRC_API_SRC_PROVIDER_TYPE_CONVERTER_HPP -#include "score/crypto/src/api/common/types.hpp" +#include "score/crypto/src/api/types/common.hpp" #include diff --git a/score/crypto/src/api/types/BUILD b/score/crypto/src/api/types/BUILD new file mode 100644 index 000000000..d2a329b27 --- /dev/null +++ b/score/crypto/src/api/types/BUILD @@ -0,0 +1,46 @@ +# ******************************************************************************* +# 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 +# ******************************************************************************* + +load("@rules_cc//cc:defs.bzl", "cc_library") + +cc_library( + name = "common_types", + hdrs = ["common.hpp"], + includes = ["."], + visibility = ["//visibility:public"], + deps = ["//score/crypto/src/api/common:crypto_common_base"], +) + +cc_library( + name = "certificate_types", + hdrs = ["certificate.hpp"], + includes = ["."], + visibility = ["//visibility:public"], + deps = [":common_types"], +) + +cc_library( + name = "key_types", + hdrs = ["key.hpp"], + includes = ["."], + visibility = ["//visibility:public"], + deps = [":common_types"], +) + +cc_library( + name = "types", + hdrs = ["common.hpp", "certificate.hpp", "key.hpp"], + includes = ["."], + visibility = ["//visibility:public"], + deps = [":certificate_types", ":key_types"], +) diff --git a/score/crypto/src/api/types/certificate.hpp b/score/crypto/src/api/types/certificate.hpp new file mode 100644 index 000000000..519b83526 --- /dev/null +++ b/score/crypto/src/api/types/certificate.hpp @@ -0,0 +1,112 @@ +/******************************************************************************** + * 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 + ********************************************************************************/ + +#ifndef SCORE_CRYPTO_SRC_API_TYPES_CERTIFICATE_HPP +#define SCORE_CRYPTO_SRC_API_TYPES_CERTIFICATE_HPP + +#include "score/crypto/src/api/types/common.hpp" + +#include +#include +#include + +namespace score::crypto +{ + +enum class CertificateSlotState : uint8_t +{ + kEmpty, + kOccupied, + kLocked +}; + +enum class CertificateStatus : uint8_t +{ + kValid, + kRevoked, + kExpired, + kUnknown +}; + +enum class CertVerifyResult : uint8_t +{ + kValid, + kExpired, + kNotYetValid, + kRevoked, + kNoRootFound, + kChainIncomplete, + kSignatureInvalid, + kInvalidPurpose, + kUnknownAlgorithm, + kUnknownError +}; + +enum class ChainTerminationPolicy : uint8_t +{ + kRootRequired, + kTrustStoreTerminated +}; + +enum class OcspStatus : uint8_t +{ + kGood, + kRevoked, + kUnknown, + kError +}; + +enum class RevocationCheckPolicy : uint8_t +{ + kNone, + kCrlOnly, + kOcspOnly, + kOcspWithCrlFallback +}; + +enum class VerificationEvidenceMode : uint8_t +{ + kNone, + kChain, + kChainAndCrl +}; + +struct CrlMetadata +{ + std::array fingerprint{}; + std::array issuer_fingerprint{}; + int64_t this_update{0}; + int64_t next_update{0}; + uint64_t crl_number{0U}; +}; + +struct CrlMetadataWireLayout final +{ + static constexpr std::size_t kFingerprintSize = 32U; + static constexpr std::size_t kCrlFingerprintOffset = 0U; + static constexpr std::size_t kIssuerFingerprintOffset = kCrlFingerprintOffset + kFingerprintSize; + static constexpr std::size_t kThisUpdateOffset = kIssuerFingerprintOffset + kFingerprintSize; + static constexpr std::size_t kNextUpdateOffset = kThisUpdateOffset + sizeof(std::int64_t); + static constexpr std::size_t kCrlNumberOffset = kNextUpdateOffset + sizeof(std::int64_t); + static constexpr std::size_t kEntrySize = kCrlNumberOffset + sizeof(std::uint64_t); +}; + +struct CertificateSlotInfo +{ + CertificateSlotState state{CertificateSlotState::kEmpty}; + bool has_crl{false}; +}; + +} // namespace score::crypto + +#endif // SCORE_CRYPTO_SRC_API_TYPES_CERTIFICATE_HPP diff --git a/score/crypto/src/api/types/common.hpp b/score/crypto/src/api/types/common.hpp new file mode 100644 index 000000000..bd532dfef --- /dev/null +++ b/score/crypto/src/api/types/common.hpp @@ -0,0 +1,163 @@ +/******************************************************************************** + * 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 + ********************************************************************************/ + +#ifndef SCORE_CRYPTO_SRC_API_TYPES_COMMON_HPP +#define SCORE_CRYPTO_SRC_API_TYPES_COMMON_HPP + +#include "score/crypto/src/api/common/fixed_capacity_string.hpp" + +#include +#include +#include +#include + +namespace score::crypto +{ + +using ResourceId = FixedCapacityString<64>; +using AlgorithmId = FixedCapacityString<64>; + +enum class ResourceType : uint8_t +{ + kProvider, + kKeySlot, + kCertSlot, + kCertificateTrustStore, + kKey, + kCertificate, + kSecureObject, + kDataObject +}; + +enum class ResourcePersistence : uint8_t +{ + kPersistent, + kEphemeral +}; + +struct CryptoResourceId +{ + uint64_t id{0U}; + ResourceType type{ResourceType::kKeySlot}; + ResourcePersistence persistence{ResourcePersistence::kEphemeral}; + uint16_t primary_provider{0U}; + + constexpr bool operator==(const CryptoResourceId& other) const noexcept + { + return (id == other.id) && (type == other.type) && (persistence == other.persistence) && + (primary_provider == other.primary_provider); + } + + constexpr bool operator!=(const CryptoResourceId& other) const noexcept + { + return !(*this == other); + } +}; + +enum class ProviderType : uint8_t +{ + kDefault, + kHardware, + kSoftware, + kHardwarePreferred, + kSoftwarePreferred +}; + +enum class FormatType : uint8_t +{ + kDer, + kPem +}; + +enum class CipherDirection : uint8_t +{ + kEncrypt, + kDecrypt +}; + +enum class OperationMode : uint8_t +{ + kGenerate, + kVerify +}; + +enum class MemoryType : uint8_t +{ + kDefault, + kProviderCompatible +}; + +struct ProviderInfo +{ + uint16_t id{0U}; + ProviderType type{ProviderType::kDefault}; + FixedCapacityString<32> name{}; +}; + +struct ProviderCompatibilityInfo +{ + CryptoResourceId resource{}; + uint16_t primary_provider{0U}; + static constexpr std::size_t kMaxSecondaryProviders = 8U; + std::array secondary_providers{}; + std::size_t secondary_provider_count{0U}; +}; + +struct AlgorithmCapabilities +{ + AlgorithmId id{}; + bool supported{false}; + static constexpr std::size_t kMaxModes = 16U; + std::array, kMaxModes> modes{}; + std::size_t mode_count{0U}; +}; + +struct SystemCapabilities +{ + static constexpr std::size_t kMaxProviders = 16U; + std::array providers{}; + std::size_t provider_count{0U}; + static constexpr std::size_t kMaxAlgorithms = 64U; + std::array algorithms{}; + std::size_t algorithm_count{0U}; +}; + +struct ExtendedParameterEntry +{ + FixedCapacityString<32> key{}; + FixedCapacityString<64> value{}; +}; + +struct ExtendedParameters +{ + static constexpr std::size_t kMaxEntries = 16U; + std::array entries{}; + std::size_t entry_count{0U}; +}; + +} // namespace score::crypto + +template <> +struct std::hash +{ + std::size_t operator()(const score::crypto::CryptoResourceId& rid) const noexcept + { + std::size_t h = std::hash{}(rid.id); + h ^= std::hash{}(static_cast(rid.type)) << 1U; + h ^= std::hash{}(static_cast(rid.persistence)) << 2U; + h ^= std::hash{}(rid.primary_provider) << 3U; + return h; + } +}; + +#endif // SCORE_CRYPTO_SRC_API_TYPES_COMMON_HPP diff --git a/score/crypto/src/api/types/key.hpp b/score/crypto/src/api/types/key.hpp new file mode 100644 index 000000000..6e3392675 --- /dev/null +++ b/score/crypto/src/api/types/key.hpp @@ -0,0 +1,102 @@ +/******************************************************************************** + * 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 + ********************************************************************************/ + +#ifndef SCORE_CRYPTO_SRC_API_TYPES_KEY_HPP +#define SCORE_CRYPTO_SRC_API_TYPES_KEY_HPP + +#include "score/crypto/src/api/types/common.hpp" + +#include +#include +#include + +namespace score::crypto +{ + +enum class KeySlotState : uint8_t +{ + kEmpty, + kOccupied, + kLocked +}; + +enum class KeyOperationPermission : uint32_t +{ + kNone = 0x0000U, + kEncrypt = 0x0001U, + kDecrypt = 0x0002U, + kWrap = 0x0004U, + kUnwrap = 0x0008U, + kSign = 0x0010U, + kVerify = 0x0020U, + kMac = 0x0040U, + kAgree = 0x0080U, + kDerive = 0x0100U, + kExport = 0x0200U, + kImport = 0x0400U, + kDataProtection = 0x000FU, + kAuthentication = 0x00F0U, + kFullLifecycle = 0x0700U, + kAll = 0x07FFU, +}; + +inline constexpr KeyOperationPermission operator|(KeyOperationPermission lhs, KeyOperationPermission rhs) noexcept +{ + return static_cast(static_cast(lhs) | static_cast(rhs)); +} + +inline constexpr KeyOperationPermission operator&(KeyOperationPermission lhs, KeyOperationPermission rhs) noexcept +{ + return static_cast(static_cast(lhs) & static_cast(rhs)); +} + +inline constexpr KeyOperationPermission operator~(KeyOperationPermission perm) noexcept +{ + constexpr uint32_t kValidBitsMask = 0x07FFU; + return static_cast((~static_cast(perm)) & kValidBitsMask); +} + +inline constexpr KeyOperationPermission& operator|=(KeyOperationPermission& lhs, KeyOperationPermission rhs) noexcept +{ + lhs = lhs | rhs; + return lhs; +} + +inline constexpr KeyOperationPermission& operator&=(KeyOperationPermission& lhs, KeyOperationPermission rhs) noexcept +{ + lhs = lhs & rhs; + return lhs; +} + +inline constexpr bool HasPermission(KeyOperationPermission granted, KeyOperationPermission required) noexcept +{ + constexpr uint32_t kValidBitsMask = 0x07FFU; + const uint32_t g = static_cast(granted) & kValidBitsMask; + const uint32_t r = static_cast(required) & kValidBitsMask; + return (g & r) == r; +} + +struct KeySlotInfo +{ + KeySlotState state{KeySlotState::kEmpty}; + AlgorithmId algorithm{}; + uint16_t primary_provider{0U}; + static constexpr std::size_t kMaxCompatibleProviders = 8U; + std::array compatible_providers{}; + std::size_t compatible_provider_count{0U}; + KeyOperationPermission permitted_operations{KeyOperationPermission::kAll}; +}; + +} // namespace score::crypto + +#endif // SCORE_CRYPTO_SRC_API_TYPES_KEY_HPP diff --git a/score/crypto/src/backend/BUILD b/score/crypto/src/backend/BUILD index 756d6b254..140a87457 100644 --- a/score/crypto/src/backend/BUILD +++ b/score/crypto/src/backend/BUILD @@ -77,7 +77,7 @@ config_setting( ":score_crypto_score_backend_enabled": "True", ":score_crypto_score_openssl_enabled": "True", }, - visibility = ["//:__subpackages__"], + visibility = ["//visibility:public"], ) # ============================================================================ diff --git a/score/crypto/src/daemon/common/BUILD b/score/crypto/src/daemon/common/BUILD index 03b571a32..b5888cf68 100644 --- a/score/crypto/src/daemon/common/BUILD +++ b/score/crypto/src/daemon/common/BUILD @@ -25,6 +25,7 @@ cc_library( visibility = ["//:__subpackages__"], deps = [ "//score/crypto/src/api/common:crypto_common", + "//score/crypto/src/api/types:types", "@score_baselibs//score/result", ], ) diff --git a/score/crypto/src/daemon/config/inc/config.hpp b/score/crypto/src/daemon/config/inc/config.hpp index dd081dc29..07373d506 100644 --- a/score/crypto/src/daemon/config/inc/config.hpp +++ b/score/crypto/src/daemon/config/inc/config.hpp @@ -222,15 +222,15 @@ class GeneralConfig /** * @brief Key management configuration section * - * Stores key slot definitions parsed from the daemon's configuration source - * (JSON manifest or flatbuffer). Each entry describes a persistent key slot: + * Stores key slot definitions parsed from the daemon's binary FlatBuffers + * configuration source. Each entry describes a persistent key slot: * its human-readable name, algorithm, owning provider, access policy, and * a deployment path that points to the external deployment descriptor. * * At daemon startup, a ConfigDrivenSlotCatalog reads these entries and calls * SlotRegistry::RegisterSlot() for each one. * - * Example JSON slot entry: + * Example slot entry: * @code * { * "slot_name": "vehicle/hmac-256", @@ -305,7 +305,7 @@ class KeyConfig return m_app_key_slot_entries; } - /// @brief Path to the JSON key slot manifest file (optional). + /// @brief Path to an optional key slot manifest used by catalog tooling. /// /// If non-empty, ConfigDrivenSlotCatalog reads this file during Load(). /// If empty, only the entries added via AddSlotEntry() are used. @@ -395,7 +395,7 @@ class Config bool ParseCommandLine(int argc, char** argv); /** - * @brief Parse configuration (like flatbuffer) + * @brief Parse the binary FlatBuffers configuration * @param none * @return true if parsing succeeded, false on error */ diff --git a/score/crypto/src/daemon/data_plane/BUILD b/score/crypto/src/daemon/data_plane/BUILD index 30a9edbdd..7f7188896 100644 --- a/score/crypto/src/daemon/data_plane/BUILD +++ b/score/crypto/src/daemon/data_plane/BUILD @@ -19,6 +19,7 @@ cc_library( visibility = ["//:__subpackages__"], deps = [ "//score/crypto/src/api/common:crypto_common", + "//score/crypto/src/api/types:types", "//score/crypto/src/common:common_types", "//score/crypto/src/daemon/common", "//score/crypto/src/daemon/data_manager", diff --git a/score/crypto/src/daemon/key_management/BUILD b/score/crypto/src/daemon/key_management/BUILD index 11978775b..2f79db00c 100644 --- a/score/crypto/src/daemon/key_management/BUILD +++ b/score/crypto/src/daemon/key_management/BUILD @@ -33,6 +33,7 @@ cc_library( visibility = ["//:__subpackages__"], deps = [ "//score/crypto/src/api/common:crypto_common", + "//score/crypto/src/api/types:types", "//score/crypto/src/api/config:context_configs", "//score/crypto/src/common:common_types", "//score/crypto/src/daemon/common", @@ -75,6 +76,7 @@ cc_library( deps = [ ":key_handler_iface", "//score/crypto/src/api/common:crypto_common", + "//score/crypto/src/api/types:types", "//score/crypto/src/api/config:context_configs", "//score/crypto/src/daemon/common", "//score/crypto/src/daemon/control_plane:request_handler_hdr", @@ -109,6 +111,7 @@ cc_library( deps = [ ":key_management_headers", "//score/crypto/src/api/common:crypto_common", + "//score/crypto/src/api/types:types", "//score/crypto/src/daemon/common", "//score/crypto/src/daemon/common/storage:file_io", "//score/crypto/src/daemon/common/storage:kv_deployment", diff --git a/score/crypto/src/daemon/key_management/core/key_entry.hpp b/score/crypto/src/daemon/key_management/core/key_entry.hpp index e0f56a5f2..4f9e701da 100644 --- a/score/crypto/src/daemon/key_management/core/key_entry.hpp +++ b/score/crypto/src/daemon/key_management/core/key_entry.hpp @@ -14,7 +14,7 @@ #ifndef SCORE_CRYPTO_SRC_DAEMON_KEY_MANAGEMENT_CORE_KEY_ENTRY_HPP #define SCORE_CRYPTO_SRC_DAEMON_KEY_MANAGEMENT_CORE_KEY_ENTRY_HPP -#include "score/crypto/src/api/common/types.hpp" +#include "score/crypto/src/api/types/common.hpp" #include "score/crypto/src/daemon/data_manager/data_node.hpp" #include "score/crypto/src/daemon/key_management/interfaces/i_key_handler.hpp" #include "score/crypto/src/daemon/key_management/slot/slot_registry.hpp" diff --git a/score/crypto/src/daemon/key_management/detail/slot_info_builder.hpp b/score/crypto/src/daemon/key_management/detail/slot_info_builder.hpp index 202645dfc..a6ef94466 100644 --- a/score/crypto/src/daemon/key_management/detail/slot_info_builder.hpp +++ b/score/crypto/src/daemon/key_management/detail/slot_info_builder.hpp @@ -14,7 +14,8 @@ #ifndef SCORE_CRYPTO_SRC_DAEMON_KEY_MANAGEMENT_DETAIL_SLOT_INFO_BUILDER_HPP #define SCORE_CRYPTO_SRC_DAEMON_KEY_MANAGEMENT_DETAIL_SLOT_INFO_BUILDER_HPP -#include "score/crypto/src/api/common/types.hpp" +#include "score/crypto/src/api/types/common.hpp" +#include "score/crypto/src/api/types/key.hpp" #include "score/crypto/src/daemon/key_management/interfaces/key_slot_config.hpp" namespace score::crypto::daemon::key_management::detail diff --git a/score/crypto/src/daemon/key_management/interfaces/i_key_factory.hpp b/score/crypto/src/daemon/key_management/interfaces/i_key_factory.hpp index 3b3142b9a..791e0a5a3 100644 --- a/score/crypto/src/daemon/key_management/interfaces/i_key_factory.hpp +++ b/score/crypto/src/daemon/key_management/interfaces/i_key_factory.hpp @@ -14,7 +14,7 @@ #ifndef SCORE_CRYPTO_SRC_DAEMON_KEY_MANAGEMENT_INTERFACES_I_KEY_FACTORY_HPP #define SCORE_CRYPTO_SRC_DAEMON_KEY_MANAGEMENT_INTERFACES_I_KEY_FACTORY_HPP -#include "score/crypto/src/api/common/types.hpp" +#include "score/crypto/src/api/types/common.hpp" #include "score/crypto/src/common/types.hpp" #include "score/crypto/src/daemon/common/daemon_error.hpp" #include "score/crypto/src/daemon/key_management/interfaces/i_key_handler.hpp" diff --git a/score/crypto/src/daemon/key_management/interfaces/i_key_slot_handler.hpp b/score/crypto/src/daemon/key_management/interfaces/i_key_slot_handler.hpp index f97c8a770..d15b2d9c8 100644 --- a/score/crypto/src/daemon/key_management/interfaces/i_key_slot_handler.hpp +++ b/score/crypto/src/daemon/key_management/interfaces/i_key_slot_handler.hpp @@ -14,7 +14,8 @@ #ifndef SCORE_CRYPTO_SRC_DAEMON_KEY_MANAGEMENT_INTERFACES_I_KEY_SLOT_HANDLER_HPP #define SCORE_CRYPTO_SRC_DAEMON_KEY_MANAGEMENT_INTERFACES_I_KEY_SLOT_HANDLER_HPP -#include "score/crypto/src/api/common/types.hpp" +#include "score/crypto/src/api/types/common.hpp" +#include "score/crypto/src/api/types/key.hpp" #include "score/crypto/src/common/types.hpp" #include "score/crypto/src/daemon/common/daemon_error.hpp" #include "score/crypto/src/daemon/key_management/interfaces/i_key_handler.hpp" diff --git a/score/crypto/src/daemon/key_management/interfaces/key_slot_config.hpp b/score/crypto/src/daemon/key_management/interfaces/key_slot_config.hpp index a8d8395d4..c935eb892 100644 --- a/score/crypto/src/daemon/key_management/interfaces/key_slot_config.hpp +++ b/score/crypto/src/daemon/key_management/interfaces/key_slot_config.hpp @@ -14,7 +14,8 @@ #ifndef SCORE_CRYPTO_SRC_DAEMON_KEY_MANAGEMENT_INTERFACES_KEY_SLOT_CONFIG_HPP #define SCORE_CRYPTO_SRC_DAEMON_KEY_MANAGEMENT_INTERFACES_KEY_SLOT_CONFIG_HPP -#include "score/crypto/src/api/common/types.hpp" +#include "score/crypto/src/api/types/common.hpp" +#include "score/crypto/src/api/types/key.hpp" #include "score/crypto/src/daemon/common/types.hpp" #include diff --git a/score/crypto/src/daemon/key_management/interfaces/key_types.hpp b/score/crypto/src/daemon/key_management/interfaces/key_types.hpp index 5c4b9a345..5373ee69c 100644 --- a/score/crypto/src/daemon/key_management/interfaces/key_types.hpp +++ b/score/crypto/src/daemon/key_management/interfaces/key_types.hpp @@ -14,8 +14,9 @@ #ifndef SCORE_CRYPTO_SRC_DAEMON_KEY_MANAGEMENT_INTERFACES_KEY_TYPES_HPP #define SCORE_CRYPTO_SRC_DAEMON_KEY_MANAGEMENT_INTERFACES_KEY_TYPES_HPP -#include "score/crypto/src/api/common/types.hpp" #include "score/crypto/src/api/config/key_operation_params.hpp" +#include "score/crypto/src/api/types/common.hpp" +#include "score/crypto/src/api/types/key.hpp" #include "score/crypto/src/daemon/common/types.hpp" #include diff --git a/score/crypto/src/daemon/key_management/nodes/key_slot_data_node.hpp b/score/crypto/src/daemon/key_management/nodes/key_slot_data_node.hpp index 4d546734f..1df88ec7b 100644 --- a/score/crypto/src/daemon/key_management/nodes/key_slot_data_node.hpp +++ b/score/crypto/src/daemon/key_management/nodes/key_slot_data_node.hpp @@ -14,7 +14,8 @@ #ifndef SCORE_CRYPTO_SRC_DAEMON_KEY_MANAGEMENT_NODES_KEY_SLOT_DATA_NODE_HPP #define SCORE_CRYPTO_SRC_DAEMON_KEY_MANAGEMENT_NODES_KEY_SLOT_DATA_NODE_HPP -#include "score/crypto/src/api/common/types.hpp" +#include "score/crypto/src/api/types/common.hpp" +#include "score/crypto/src/api/types/key.hpp" #include "score/crypto/src/daemon/data_manager/data_node.hpp" #include "score/crypto/src/daemon/key_management/slot/slot_registry.hpp" diff --git a/score/crypto/src/daemon/key_management/slot/access_policy_enforcer.hpp b/score/crypto/src/daemon/key_management/slot/access_policy_enforcer.hpp index c92ddcc90..405efcadc 100644 --- a/score/crypto/src/daemon/key_management/slot/access_policy_enforcer.hpp +++ b/score/crypto/src/daemon/key_management/slot/access_policy_enforcer.hpp @@ -14,7 +14,8 @@ #ifndef SCORE_CRYPTO_SRC_DAEMON_KEY_MANAGEMENT_SLOT_ACCESS_POLICY_ENFORCER_HPP #define SCORE_CRYPTO_SRC_DAEMON_KEY_MANAGEMENT_SLOT_ACCESS_POLICY_ENFORCER_HPP -#include "score/crypto/src/api/common/types.hpp" +#include "score/crypto/src/api/types/common.hpp" +#include "score/crypto/src/api/types/key.hpp" #include "score/crypto/src/daemon/common/daemon_error.hpp" #include "score/crypto/src/daemon/common/types.hpp" #include "score/crypto/src/daemon/data_manager/data_node.hpp" diff --git a/score/crypto/src/daemon/key_management/slot/file_backed_slot_handler.cpp b/score/crypto/src/daemon/key_management/slot/file_backed_slot_handler.cpp index 555ecca22..635a2ed97 100644 --- a/score/crypto/src/daemon/key_management/slot/file_backed_slot_handler.cpp +++ b/score/crypto/src/daemon/key_management/slot/file_backed_slot_handler.cpp @@ -70,15 +70,21 @@ score::crypto::Expected FileBackedSlotHandle { auto deploy_result = DeploymentLoader::Load(slot.deployment_path, slot.deployment_format); if (!deploy_result.has_value()) - return score::crypto::KeySlotState::kEmpty; + { + if (deploy_result.error() == Error::kResourceNotAllocated) + return score::crypto::KeySlotState::kEmpty; + return score::crypto::make_unexpected(deploy_result.error()); + } const auto& deploy_info = deploy_result.value(); const auto path_it = deploy_info.key_properties.find(std::string{deployment_keys::kKeyPath}); if (path_it == deploy_info.key_properties.end() || path_it->second.empty()) return score::crypto::KeySlotState::kEmpty; - return file_io::FileExists(path_it->second) ? score::crypto::KeySlotState::kOccupied - : score::crypto::KeySlotState::kEmpty; + const auto exists = file_io::FileExists(path_it->second); + if (!exists) + return score::crypto::make_unexpected(exists.error()); + return exists.value() ? score::crypto::KeySlotState::kOccupied : score::crypto::KeySlotState::kEmpty; } score::crypto::Expected FileBackedSlotHandler::GetSlotInfo(const KeySlotConfig& slot) diff --git a/score/crypto/src/daemon/key_management/slot/slot_registry.hpp b/score/crypto/src/daemon/key_management/slot/slot_registry.hpp index 0e4e98a35..7eacc0e39 100644 --- a/score/crypto/src/daemon/key_management/slot/slot_registry.hpp +++ b/score/crypto/src/daemon/key_management/slot/slot_registry.hpp @@ -14,7 +14,7 @@ #ifndef SCORE_CRYPTO_SRC_DAEMON_KEY_MANAGEMENT_SLOT_SLOT_REGISTRY_HPP #define SCORE_CRYPTO_SRC_DAEMON_KEY_MANAGEMENT_SLOT_SLOT_REGISTRY_HPP -#include "score/crypto/src/api/common/types.hpp" +#include "score/crypto/src/api/types/common.hpp" #include "score/crypto/src/daemon/common/daemon_error.hpp" #include "score/crypto/src/daemon/data_manager/data_node.hpp" #include "score/crypto/src/daemon/key_management/interfaces/key_slot_config.hpp" diff --git a/score/crypto/src/daemon/provider/handler/BUILD b/score/crypto/src/daemon/provider/handler/BUILD index 040b0fdb1..2b4e2af02 100644 --- a/score/crypto/src/daemon/provider/handler/BUILD +++ b/score/crypto/src/daemon/provider/handler/BUILD @@ -22,6 +22,7 @@ cc_library( visibility = ["//:__subpackages__"], deps = [ "//score/crypto/src/api/common:crypto_common", + "//score/crypto/src/api/types:types", "//score/crypto/src/common:common_types", "//score/crypto/src/daemon/common", "//score/crypto/src/daemon/key_management:key_handler_iface", diff --git a/score/crypto/src/daemon/provider/handler/handler_init_params.hpp b/score/crypto/src/daemon/provider/handler/handler_init_params.hpp index 0dba756ce..cc2d4d046 100644 --- a/score/crypto/src/daemon/provider/handler/handler_init_params.hpp +++ b/score/crypto/src/daemon/provider/handler/handler_init_params.hpp @@ -14,7 +14,7 @@ #ifndef SCORE_CRYPTO_SRC_DAEMON_PROVIDER_HANDLER_HANDLER_INIT_PARAMS_HPP #define SCORE_CRYPTO_SRC_DAEMON_PROVIDER_HANDLER_HANDLER_INIT_PARAMS_HPP -#include "score/crypto/src/api/common/types.hpp" +#include "score/crypto/src/api/types/common.hpp" #include "score/crypto/src/daemon/common/types.hpp" #include "score/crypto/src/daemon/key_management/interfaces/i_key_handler.hpp" #include diff --git a/score/crypto/src/daemon/provider/pkcs11/key_management/pkcs11_key_store.cpp b/score/crypto/src/daemon/provider/pkcs11/key_management/pkcs11_key_store.cpp index 01bfab2af..04d685745 100644 --- a/score/crypto/src/daemon/provider/pkcs11/key_management/pkcs11_key_store.cpp +++ b/score/crypto/src/daemon/provider/pkcs11/key_management/pkcs11_key_store.cpp @@ -13,7 +13,7 @@ #include "score/crypto/src/daemon/provider/pkcs11/key_management/pkcs11_key_store.hpp" -#include "score/crypto/src/api/common/types.hpp" +#include "score/crypto/src/api/types/common.hpp" #include "score/crypto/src/common/types.hpp" #include "score/crypto/src/daemon/common/daemon_error.hpp" #include "score/crypto/src/daemon/common/types.hpp" diff --git a/score/crypto/src/daemon/provider/pkcs11/key_management/pkcs11_key_store.hpp b/score/crypto/src/daemon/provider/pkcs11/key_management/pkcs11_key_store.hpp index 56d9dad9a..373476d4f 100644 --- a/score/crypto/src/daemon/provider/pkcs11/key_management/pkcs11_key_store.hpp +++ b/score/crypto/src/daemon/provider/pkcs11/key_management/pkcs11_key_store.hpp @@ -14,7 +14,7 @@ #ifndef SCORE_CRYPTO_SRC_DAEMON_PROVIDER_PKCS11_KEY_MANAGEMENT_PKCS11_KEY_STORE_HPP #define SCORE_CRYPTO_SRC_DAEMON_PROVIDER_PKCS11_KEY_MANAGEMENT_PKCS11_KEY_STORE_HPP -#include "score/crypto/src/api/common/types.hpp" +#include "score/crypto/src/api/types/common.hpp" #include "score/crypto/src/daemon/common/daemon_error.hpp" #include "score/crypto/src/daemon/key_management/interfaces/key_types.hpp" #include "score/crypto/src/daemon/provider/pkcs11/key_management/resolved_key.hpp" diff --git a/score/crypto/src/daemon/provider/pkcs11/operations/mac/pkcs11_mac_context.hpp b/score/crypto/src/daemon/provider/pkcs11/operations/mac/pkcs11_mac_context.hpp index 964a592b5..dc54aa12d 100644 --- a/score/crypto/src/daemon/provider/pkcs11/operations/mac/pkcs11_mac_context.hpp +++ b/score/crypto/src/daemon/provider/pkcs11/operations/mac/pkcs11_mac_context.hpp @@ -14,7 +14,7 @@ #ifndef SCORE_CRYPTO_SRC_DAEMON_PROVIDER_PKCS11_OPERATIONS_MAC_PKCS11_MAC_CONTEXT_HPP #define SCORE_CRYPTO_SRC_DAEMON_PROVIDER_PKCS11_OPERATIONS_MAC_PKCS11_MAC_CONTEXT_HPP -#include "score/crypto/src/api/common/types.hpp" // OperationMode +#include "score/crypto/src/api/types/common.hpp" // OperationMode #include diff --git a/score/crypto/src/daemon/provider/pkcs11/operations/mac/pkcs11_mac_executor.cpp b/score/crypto/src/daemon/provider/pkcs11/operations/mac/pkcs11_mac_executor.cpp index f988509bb..2ccf0cdb2 100644 --- a/score/crypto/src/daemon/provider/pkcs11/operations/mac/pkcs11_mac_executor.cpp +++ b/score/crypto/src/daemon/provider/pkcs11/operations/mac/pkcs11_mac_executor.cpp @@ -12,7 +12,7 @@ ********************************************************************************/ #include "score/crypto/src/daemon/provider/pkcs11/operations/mac/pkcs11_mac_executor.hpp" -#include "score/crypto/src/api/common/types.hpp" +#include "score/crypto/src/api/types/common.hpp" #include "score/crypto/src/daemon/provider/handler/operations/mac_handler_operations.hpp" #include "score/crypto/src/daemon/provider/handler/src/handler_utils.hpp" #include "score/crypto/src/daemon/provider/pkcs11/pkcs11_module.hpp" diff --git a/score/crypto/src/daemon/provider/pkcs11/operations/mac/pkcs11_mac_executor.hpp b/score/crypto/src/daemon/provider/pkcs11/operations/mac/pkcs11_mac_executor.hpp index 1881a2041..171cfb289 100644 --- a/score/crypto/src/daemon/provider/pkcs11/operations/mac/pkcs11_mac_executor.hpp +++ b/score/crypto/src/daemon/provider/pkcs11/operations/mac/pkcs11_mac_executor.hpp @@ -14,7 +14,7 @@ #ifndef SCORE_CRYPTO_SRC_DAEMON_PROVIDER_PKCS11_OPERATIONS_MAC_PKCS11_MAC_EXECUTOR_HPP #define SCORE_CRYPTO_SRC_DAEMON_PROVIDER_PKCS11_OPERATIONS_MAC_PKCS11_MAC_EXECUTOR_HPP -#include "score/crypto/src/api/common/types.hpp" // OperationMode +#include "score/crypto/src/api/types/common.hpp" // OperationMode #include "score/crypto/src/common/types.hpp" #include "score/crypto/src/daemon/common/daemon_error.hpp" #include "score/crypto/src/daemon/common/types.hpp" diff --git a/score/crypto/src/daemon/provider/pkcs11/operations/mac/pkcs11_mac_handler.cpp b/score/crypto/src/daemon/provider/pkcs11/operations/mac/pkcs11_mac_handler.cpp index 09b7a0cd3..250c9b24f 100644 --- a/score/crypto/src/daemon/provider/pkcs11/operations/mac/pkcs11_mac_handler.cpp +++ b/score/crypto/src/daemon/provider/pkcs11/operations/mac/pkcs11_mac_handler.cpp @@ -12,7 +12,7 @@ ********************************************************************************/ #include "score/crypto/src/daemon/provider/pkcs11/operations/mac/pkcs11_mac_handler.hpp" -#include "score/crypto/src/api/common/types.hpp" +#include "score/crypto/src/api/types/common.hpp" #include "score/crypto/src/common/types.hpp" #include "score/crypto/src/daemon/common/algorithm_info.hpp" #include "score/crypto/src/daemon/common/daemon_error.hpp" diff --git a/score/crypto/tests/key_management/BUILD b/score/crypto/tests/key_management/BUILD index a4ffdaa9d..bacdeeb2c 100644 --- a/score/crypto/tests/key_management/BUILD +++ b/score/crypto/tests/key_management/BUILD @@ -64,6 +64,7 @@ cc_test( }), deps = [ "//score/crypto/src/api/common:crypto_common", + "//score/crypto/src/api/types:types", "//score/crypto/src/daemon/common", "//score/crypto/src/daemon/config", "//score/crypto/src/daemon/data_manager", diff --git a/score/crypto/tests/key_management/test_access_policy_enforcer.cpp b/score/crypto/tests/key_management/test_access_policy_enforcer.cpp index 9a056d638..dcf551d39 100644 --- a/score/crypto/tests/key_management/test_access_policy_enforcer.cpp +++ b/score/crypto/tests/key_management/test_access_policy_enforcer.cpp @@ -11,7 +11,8 @@ * SPDX-License-Identifier: Apache-2.0 ********************************************************************************/ -#include "score/crypto/src/api/common/types.hpp" +#include "score/crypto/src/api/types/common.hpp" +#include "score/crypto/src/api/types/key.hpp" #include "score/crypto/src/daemon/key_management/interfaces/key_slot_config.hpp" #include "score/crypto/src/daemon/key_management/slot/access_policy_enforcer.hpp" diff --git a/score/crypto/tests/key_management/test_key_config_manager.cpp b/score/crypto/tests/key_management/test_key_config_manager.cpp index 2dec739ba..6f8adb72b 100644 --- a/score/crypto/tests/key_management/test_key_config_manager.cpp +++ b/score/crypto/tests/key_management/test_key_config_manager.cpp @@ -11,7 +11,8 @@ * SPDX-License-Identifier: Apache-2.0 ********************************************************************************/ -#include "score/crypto/src/api/common/types.hpp" +#include "score/crypto/src/api/types/common.hpp" +#include "score/crypto/src/api/types/key.hpp" #include "score/crypto/src/daemon/config/src/flatbuffer_config_parser.hpp" #include "score/crypto/src/daemon/key_management/interfaces/key_slot_config.hpp" #include "score/crypto/src/daemon/key_management/slot/config_driven_slot_catalog.hpp" diff --git a/score/crypto/tests/key_management/test_key_management_context.cpp b/score/crypto/tests/key_management/test_key_management_context.cpp index dcbbf55fc..e6c5f615a 100644 --- a/score/crypto/tests/key_management/test_key_management_context.cpp +++ b/score/crypto/tests/key_management/test_key_management_context.cpp @@ -20,7 +20,8 @@ /// handler dispatch flow without mocks. #include "score/crypto/src/api/common/error_domain.hpp" -#include "score/crypto/src/api/common/types.hpp" +#include "score/crypto/src/api/types/common.hpp" +#include "score/crypto/src/api/types/key.hpp" #include "score/crypto/src/daemon/common/actors.hpp" #include "score/crypto/src/daemon/config/src/flatbuffer_config_parser.hpp" #include "score/crypto/src/daemon/data_manager/data_manager.hpp" diff --git a/score/crypto/tests/key_management/test_openssl_key_handler.cpp b/score/crypto/tests/key_management/test_openssl_key_handler.cpp index 0b278fc9e..98ecc8a08 100644 --- a/score/crypto/tests/key_management/test_openssl_key_handler.cpp +++ b/score/crypto/tests/key_management/test_openssl_key_handler.cpp @@ -11,7 +11,8 @@ * SPDX-License-Identifier: Apache-2.0 ********************************************************************************/ -#include "score/crypto/src/api/common/types.hpp" +#include "score/crypto/src/api/types/common.hpp" +#include "score/crypto/src/api/types/key.hpp" #include "score/crypto/src/daemon/key_management/interfaces/i_key_factory.hpp" #include "score/crypto/src/daemon/provider/score_provider/openssl/key_management/openssl_key_factory.hpp" #include "score/crypto/src/daemon/provider/score_provider/openssl/key_management/openssl_key_handler.hpp" diff --git a/score/tests/integration_tests/score_api_mac_test.cpp b/score/tests/integration_tests/score_api_mac_test.cpp index 87dc5ee62..31b71bcd0 100644 --- a/score/tests/integration_tests/score_api_mac_test.cpp +++ b/score/tests/integration_tests/score_api_mac_test.cpp @@ -22,7 +22,6 @@ /// - Context reuse via Reset() /// - Automatic key release via CryptoResourceGuard RAII -#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/key_operation_params.hpp" #include "score/crypto/src/api/config/mac_context_config.hpp" @@ -31,6 +30,7 @@ #include "score/crypto/src/api/crypto_stack_factory.hpp" #include "score/crypto/src/api/i_crypto_context.hpp" #include "score/crypto/src/api/i_crypto_stack.hpp" +#include "score/crypto/src/api/types/common.hpp" #include "score/tests/utility/test_utility.hpp" #include diff --git a/score/tests/integration_tests/score_demo.cpp b/score/tests/integration_tests/score_demo.cpp index 017a43816..f8489c07c 100644 --- a/score/tests/integration_tests/score_demo.cpp +++ b/score/tests/integration_tests/score_demo.cpp @@ -25,12 +25,12 @@ /// 4. RAII pattern ensures automatic key zeroization on cleanup #include "score/crypto/src/api/common/error_domain.hpp" -#include "score/crypto/src/api/common/types.hpp" #include "score/crypto/src/api/config/mac_context_config.hpp" #include "score/crypto/src/api/contexts/i_mac_context.hpp" #include "score/crypto/src/api/crypto_stack_factory.hpp" #include "score/crypto/src/api/i_crypto_context.hpp" #include "score/crypto/src/api/i_crypto_stack.hpp" +#include "score/crypto/src/api/types/common.hpp" #include "score/result/result.h" #include "score/tests/utility/test_utility.hpp" diff --git a/tools/coverage/BUILD b/tools/coverage/BUILD index e2689d1b3..fbecbeb89 100644 --- a/tools/coverage/BUILD +++ b/tools/coverage/BUILD @@ -26,7 +26,7 @@ score_coverage_scope( visibility = ["//visibility:private"], deps = [ "//score/crypto/src/api:crypto_stack", - "//score/crypto/src/api/certificate:cert_types", + "//score/crypto/src/api/types:types", "//score/crypto/src/api/config:cert_context_configs", "//score/crypto/src/api/contexts:cert_contexts", "//score/crypto/src/api/objects:cert_objects", From b81ec7d85db2f2e2a12d49699a5eab5ae06fc369 Mon Sep 17 00:00:00 2001 From: Athul Mallappallil Date: Fri, 18 Sep 2026 16:57:13 +0200 Subject: [PATCH 20/37] Separate trust store mgmt ctx --- score/crypto/src/api/config/BUILD | 1 + .../trust_store_management_context_config.hpp | 55 ++++++ score/crypto/src/api/contexts/BUILD | 1 + .../i_certificate_management_context.hpp | 73 -------- .../i_trust_store_management_context.hpp | 171 ++++++++++++++++++ .../src/api/objects/i_trust_store_object.hpp | 2 +- 6 files changed, 229 insertions(+), 74 deletions(-) create mode 100644 score/crypto/src/api/config/trust_store_management_context_config.hpp create mode 100644 score/crypto/src/api/contexts/i_trust_store_management_context.hpp diff --git a/score/crypto/src/api/config/BUILD b/score/crypto/src/api/config/BUILD index 4b5288625..6dbe92095 100644 --- a/score/crypto/src/api/config/BUILD +++ b/score/crypto/src/api/config/BUILD @@ -36,6 +36,7 @@ cc_library( hdrs = [ "certificate_context_config.hpp", "certificate_verification_context_config.hpp", + "trust_store_management_context_config.hpp", ], includes = ["."], visibility = ["//visibility:public"], diff --git a/score/crypto/src/api/config/trust_store_management_context_config.hpp b/score/crypto/src/api/config/trust_store_management_context_config.hpp new file mode 100644 index 000000000..c82367ae6 --- /dev/null +++ b/score/crypto/src/api/config/trust_store_management_context_config.hpp @@ -0,0 +1,55 @@ +/******************************************************************************** + * 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 + ********************************************************************************/ + +#ifndef SCORE_CRYPTO_SRC_API_CONFIG_TRUST_STORE_MANAGEMENT_CONTEXT_CONFIG_HPP +#define SCORE_CRYPTO_SRC_API_CONFIG_TRUST_STORE_MANAGEMENT_CONTEXT_CONFIG_HPP + +#include "score/crypto/src/api/config/base_context_config.hpp" + +namespace score +{ + +namespace crypto +{ + +/// @brief Configuration for trust-store management context creation. +/// +/// Provider selection is optional. Trust-store management uses the +/// certificate-management provider capability and the CERT:TRUST_STORE +/// context type. +struct TrustStoreManagementContextConfig : public BaseContextConfig +{ + TrustStoreManagementContextConfig& SetProvider(const CryptoResourceId& prov) noexcept + { + BaseContextConfig::SetProvider(prov); + return *this; + } + + TrustStoreManagementContextConfig& SetProviderType(ProviderType type) noexcept + { + BaseContextConfig::SetProviderType(type); + return *this; + } + + TrustStoreManagementContextConfig& SetExtendedParameter(const std::string& key, const std::string& value) + { + BaseContextConfig::SetExtendedParameter(key, value); + return *this; + } +}; + +} // namespace crypto + +} // namespace score + +#endif // SCORE_CRYPTO_SRC_API_CONFIG_TRUST_STORE_MANAGEMENT_CONTEXT_CONFIG_HPP diff --git a/score/crypto/src/api/contexts/BUILD b/score/crypto/src/api/contexts/BUILD index bb3fe3f19..8b4b6af7c 100644 --- a/score/crypto/src/api/contexts/BUILD +++ b/score/crypto/src/api/contexts/BUILD @@ -55,6 +55,7 @@ cc_library( hdrs = [ "i_certificate_management_context.hpp", "i_certificate_verification_context.hpp", + "i_trust_store_management_context.hpp", ], includes = ["."], visibility = ["//visibility:public"], diff --git a/score/crypto/src/api/contexts/i_certificate_management_context.hpp b/score/crypto/src/api/contexts/i_certificate_management_context.hpp index f4c4882f9..7efabc6f3 100644 --- a/score/crypto/src/api/contexts/i_certificate_management_context.hpp +++ b/score/crypto/src/api/contexts/i_certificate_management_context.hpp @@ -243,79 +243,6 @@ class ICertificateManagementContext : public IContext // const CryptoResourceId& cert, // const CryptoResourceId& issuer_cert) = 0; - // ---- Trust-store membership management ---- - - /// @brief Adds a certificate to a persistent trust store. - /// - /// The certificate is assigned to a trust-store-managed exclusive slot. This is a - /// write operation and requires trust-store write access. Idempotent: if the cert - /// is already a member of any type, returns success without allocating a new slot. - /// - /// @param trust_store Handle to the trust store (type = kCertificateTrustStore) - /// @param cert Handle to the certificate to add (type = kCertificate or kCertSlot) - /// @param with_crl When true, propagate the associated CRL to the exclusive slot - virtual score::Result AddCertificateToTrustStore(const CryptoResourceId& trust_store, - const CryptoResourceId& cert, - bool with_crl = false) = 0; - - /// @brief Removes a certificate from a persistent trust store by cert handle. - /// - /// The certificate must be loaded in the daemon (ephemeral or slot-loaded). - /// The daemon resolves the SHA-256 fingerprint internally from the handle. - /// - /// @param trust_store Handle to the trust store (type = kCertificateTrustStore) - /// @param cert Handle to the certificate to remove (type = kCertificate or kCertSlot) - virtual score::Result RemoveCertificateFromTrustStore(const CryptoResourceId& trust_store, - const CryptoResourceId& cert) = 0; - - /// @brief Removes a certificate from a persistent trust store by SHA-256 fingerprint. - /// - /// The certificate does not need to be loaded in the daemon. Use this when - /// the fingerprint is known from an external source without the cert bytes being available. - /// - /// @param trust_store Handle to the trust store (type = kCertificateTrustStore) - /// @param sha256_fingerprint 32-byte SHA-256 fingerprint of the certificate to remove - virtual score::Result RemoveCertificateFromTrustStore( - const CryptoResourceId& trust_store, - score::cpp::span sha256_fingerprint) = 0; - - /// @brief Enables a previously disabled trust store member identified by its slot resource. - /// - /// Use the slot_id from ITrustStoreObject::MemberInfo to obtain the slot handle. - /// - /// @param trust_store Handle to the trust store (type = kCertificateTrustStore) - /// @param slot Handle to the member slot (type = kCertSlot) - virtual score::Result EnableTrustStoreMember(const CryptoResourceId& trust_store, - const CryptoResourceId& slot) = 0; - - /// @brief Disables a trust store member identified by its slot resource. - /// - /// A disabled member is excluded from anchor resolution; it remains in the store - /// and can be re-enabled. Use RemoveCertificateFromTrustStore to permanently remove. - /// - /// Use the slot_id from ITrustStoreObject::MemberInfo to obtain the slot handle. - /// - /// @param trust_store Handle to the trust store (type = kCertificateTrustStore) - /// @param slot Handle to the member slot (type = kCertSlot) - virtual score::Result DisableTrustStoreMember(const CryptoResourceId& trust_store, - const CryptoResourceId& slot) = 0; - - /// @brief Imports a CRL for a trust store exclusive member identified by its slot resource. - /// - /// Only kExclusiveMutable trust store slots are writable through this path. - /// For shared-static or conditional-external members, use ImportCrl directly on the slot. - /// - /// Use the slot_id from ITrustStoreObject::MemberInfo to obtain the slot handle. - /// - /// @param trust_store Handle to the trust store (type = kCertificateTrustStore) - /// @param slot Handle to the exclusive member slot (type = kCertSlot) - /// @param crl_data Encoded CRL bytes - /// @param format Encoding format of the CRL - virtual score::Result ImportCrlForTrustStoreMember(const CryptoResourceId& trust_store, - const CryptoResourceId& slot, - score::cpp::span crl_data, - FormatType format) = 0; - protected: ICertificateManagementContext() = default; }; diff --git a/score/crypto/src/api/contexts/i_trust_store_management_context.hpp b/score/crypto/src/api/contexts/i_trust_store_management_context.hpp new file mode 100644 index 000000000..9ec3cbeb6 --- /dev/null +++ b/score/crypto/src/api/contexts/i_trust_store_management_context.hpp @@ -0,0 +1,171 @@ +/******************************************************************************** + * 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 + ********************************************************************************/ + +#ifndef SCORE_CRYPTO_SRC_API_CONTEXTS_I_TRUST_STORE_MANAGEMENT_CONTEXT_HPP +#define SCORE_CRYPTO_SRC_API_CONTEXTS_I_TRUST_STORE_MANAGEMENT_CONTEXT_HPP + +#include "score/crypto/src/api/contexts/i_context.hpp" +#include "score/crypto/src/api/types/certificate.hpp" +#include "score/crypto/src/api/types/common.hpp" +#include "score/result/result.h" +#include "score/span.hpp" + +#include +#include +#include + +namespace score +{ + +namespace crypto +{ + +/// @brief Interface for trust-store membership curation. +/// +/// A trust store remains a collection of certificate slots owned by +/// `ICertificateManagementContext`; this context owns only the membership +/// and enablement policy on top of it — a distinct client-facing capability +/// under the same certificate-management provider scope (`CERT:TRUST_STORE`). +/// +/// Certificate lifecycle operations (parse, save, export, slot CRL import) +/// remain on `ICertificateManagementContext`. Read-only trust-store +/// inspection remains on `ITrustStoreObject`, obtained via +/// `ICryptoContext::GetTrustStoreObject()`. +class ITrustStoreManagementContext : public IContext +{ + public: + using Uptr = std::unique_ptr; + + ~ITrustStoreManagementContext() override = default; + + ITrustStoreManagementContext(const ITrustStoreManagementContext&) = delete; + ITrustStoreManagementContext& operator=(const ITrustStoreManagementContext&) = delete; + ITrustStoreManagementContext(ITrustStoreManagementContext&&) = default; + ITrustStoreManagementContext& operator=(ITrustStoreManagementContext&&) = default; + + /// @brief Adds a certificate to a persistent trust store. + /// + /// The certificate is assigned to a trust-store-managed exclusive slot. This is a + /// write operation and requires trust-store write access. Idempotent: if the cert + /// is already a member of any type, returns success without allocating a new slot. + /// + /// @param trust_store Handle to the trust store (type = kCertificateTrustStore) + /// @param cert Handle to the certificate to add (type = kCertificate or kCertSlot) + /// @return std::monostate on success, error if the trust store cannot be updated + virtual score::Result AddCertificateToTrustStore(const CryptoResourceId& trust_store, + const CryptoResourceId& cert) = 0; + + /// @brief Adds a certificate and propagates its associated CRL. + /// @param trust_store Handle to the trust store (type = kCertificateTrustStore) + /// @param cert Handle to the certificate to add (type = kCertificate or kCertSlot) + /// The CRL is resolved daemon-side from the certificate resource's + /// session-scoped CRL association (see + /// `ICertificateManagementContext::ImportCrl`) or, failing that, + /// from the source slot's persistent CRL — no raw bytes are passed here. + virtual score::Result AddCertificateToTrustStoreWithCrl(const CryptoResourceId& trust_store, + const CryptoResourceId& cert) = 0; + + /// @brief Removes a certificate from a persistent trust store by cert handle. + /// + /// The certificate must be loaded in the daemon (ephemeral or slot-loaded). + /// The daemon resolves the SHA-256 fingerprint internally from the handle. + /// + /// @param trust_store Handle to the trust store (type = kCertificateTrustStore) + /// @param cert Handle to the certificate to remove (type = kCertificate or kCertSlot) + virtual score::Result RemoveCertificateFromTrustStore(const CryptoResourceId& trust_store, + const CryptoResourceId& cert) = 0; + + /// @brief Removes a certificate from a persistent trust store by SHA-256 fingerprint. + /// + /// The certificate does not need to be loaded in the daemon. Use this when + /// the fingerprint is known from an external source without the cert bytes being available. + /// + /// @param trust_store Handle to the trust store (type = kCertificateTrustStore) + /// @param sha256_fingerprint 32-byte SHA-256 fingerprint of the certificate to remove + virtual score::Result RemoveCertificateFromTrustStore( + const CryptoResourceId& trust_store, + score::cpp::span sha256_fingerprint) = 0; + + /// @brief Enables a disabled trust store member identified by its slot resource. + /// + /// Use the slot_id from ITrustStoreObject::MemberInfo to obtain the slot handle. + /// + /// @param trust_store Handle to the trust store (type = kCertificateTrustStore) + /// @param slot Handle to the member slot (type = kCertSlot) + virtual score::Result EnableTrustStoreMember(const CryptoResourceId& trust_store, + const CryptoResourceId& slot) = 0; + + /// @brief Disables a trust store member identified by its slot resource. + /// + /// A disabled member is excluded from anchor resolution; it remains in the store + /// and can be re-enabled. Use RemoveCertificateFromTrustStore to permanently remove. + /// + /// Use the slot_id from ITrustStoreObject::MemberInfo to obtain the slot handle. + /// + /// @param trust_store Handle to the trust store (type = kCertificateTrustStore) + /// @param slot Handle to the member slot (type = kCertSlot) + virtual score::Result DisableTrustStoreMember(const CryptoResourceId& trust_store, + const CryptoResourceId& slot) = 0; + + /// @brief Acknowledges an unexpected content change on a conditional-external member. + /// + /// A kConditionalExternal member is automatically disabled when its slot content + /// changes without acknowledgement (see ITrustStoreObject::MemberInfo state). This + /// re-baselines the accepted fingerprint to the slot's current content and + /// re-enables the member. Not equivalent to EnableTrustStoreMember: enabling alone + /// does not update the accepted fingerprint, so the member would be disabled again + /// on the next anchor reload if the content is still unacknowledged. + /// + /// Use the slot_id from ITrustStoreObject::MemberInfo to obtain the slot handle. + /// + /// @param trust_store Handle to the trust store (type = kCertificateTrustStore) + /// @param slot Handle to the conditional-external member slot (type = kCertSlot) + virtual score::Result AcknowledgeTrustStoreMemberUpdate(const CryptoResourceId& trust_store, + const CryptoResourceId& slot) = 0; + + /// @brief Imports a CRL for a trust store exclusive member identified by its slot resource. + /// + /// Only kExclusiveMutable trust store slots are writable through this path. + /// For shared-static or conditional-external members, use + /// `ICertificateManagementContext::ImportCrlToSlot` directly on the slot. + /// + /// Use the slot_id from ITrustStoreObject::MemberInfo to obtain the slot handle. + /// + /// @param trust_store Handle to the trust store (type = kCertificateTrustStore) + /// @param slot Handle to the exclusive member slot (type = kCertSlot) + /// @param crl_data Encoded CRL bytes + /// @param format Encoding format of the CRL + virtual score::Result ImportCrlForTrustStoreMember(const CryptoResourceId& trust_store, + const CryptoResourceId& slot, + score::cpp::span crl_data, + FormatType format) = 0; + + /// @brief Deletes the CRL from a trust-store-owned exclusive member. + /// + /// Only kExclusiveMutable members are writable through this path. The + /// trust store's write permission is required. + /// + /// @param trust_store Handle to the trust store (type = kCertificateTrustStore) + /// @param slot Handle to the exclusive member slot (type = kCertSlot) + virtual score::Result DeleteCrlForTrustStoreMember(const CryptoResourceId& trust_store, + const CryptoResourceId& slot) = 0; + + protected: + ITrustStoreManagementContext() = default; +}; + +} // namespace crypto + +} // namespace score + +#endif // SCORE_CRYPTO_SRC_API_CONTEXTS_I_TRUST_STORE_MANAGEMENT_CONTEXT_HPP diff --git a/score/crypto/src/api/objects/i_trust_store_object.hpp b/score/crypto/src/api/objects/i_trust_store_object.hpp index 0c6477f22..9d5757af2 100644 --- a/score/crypto/src/api/objects/i_trust_store_object.hpp +++ b/score/crypto/src/api/objects/i_trust_store_object.hpp @@ -37,7 +37,7 @@ namespace crypto /// are present, their membership kind, and their enabled/disabled state. /// /// Mutations (add, remove, enable, disable, import CRL) are performed via -/// ICertificateManagementContext — not through this object. +/// ITrustStoreManagementContext — not through this object. /// /// Obtained via ICryptoContext::GetTrustStoreObject(). class ITrustStoreObject : public ICryptoObject From d3cf839c019b92b03d2f3412ecc835a5ff6ce164 Mon Sep 17 00:00:00 2001 From: Athul Mallappallil Date: Fri, 18 Sep 2026 16:59:19 +0200 Subject: [PATCH 21/37] Storage refactor --- score/crypto/src/daemon/common/storage/BUILD | 27 +++++ .../common/storage/deployment_descriptor.hpp | 25 +++- .../src/daemon/common/storage/file_io.cpp | 35 +++++- .../src/daemon/common/storage/file_io.hpp | 20 ++-- .../storage/kv/kv_deployment_loader.cpp | 10 +- .../storage/kv/kv_deployment_loader.hpp | 3 + .../tests/test_deployment_descriptor.cpp | 57 +++++++++ .../common/storage/tests/test_file_io.cpp | 81 +++++++++++++ .../tests/test_kv_deployment_loader.cpp | 113 ++++++++++++++++++ 9 files changed, 349 insertions(+), 22 deletions(-) create mode 100644 score/crypto/src/daemon/common/storage/tests/test_deployment_descriptor.cpp create mode 100644 score/crypto/src/daemon/common/storage/tests/test_file_io.cpp create mode 100644 score/crypto/src/daemon/common/storage/tests/test_kv_deployment_loader.cpp diff --git a/score/crypto/src/daemon/common/storage/BUILD b/score/crypto/src/daemon/common/storage/BUILD index 6eecf280d..a9c43b26d 100644 --- a/score/crypto/src/daemon/common/storage/BUILD +++ b/score/crypto/src/daemon/common/storage/BUILD @@ -60,3 +60,30 @@ cc_library( "@score_baselibs//score/mw/log", ], ) + +cc_test( + name = "test_deployment_descriptor", + srcs = ["tests/test_deployment_descriptor.cpp"], + deps = [ + ":deployment_iface", + "@googletest//:gtest_main", + ], +) + +cc_test( + name = "test_file_io", + srcs = ["tests/test_file_io.cpp"], + deps = [ + ":file_io", + "@googletest//:gtest_main", + ], +) + +cc_test( + name = "test_kv_deployment_loader", + srcs = ["tests/test_kv_deployment_loader.cpp"], + deps = [ + ":kv_deployment", + "@googletest//:gtest_main", + ], +) diff --git a/score/crypto/src/daemon/common/storage/deployment_descriptor.hpp b/score/crypto/src/daemon/common/storage/deployment_descriptor.hpp index 4edcb1e4d..2c8812367 100644 --- a/score/crypto/src/daemon/common/storage/deployment_descriptor.hpp +++ b/score/crypto/src/daemon/common/storage/deployment_descriptor.hpp @@ -15,6 +15,7 @@ #define SCORE_CRYPTO_SRC_DAEMON_COMMON_STORAGE_DEPLOYMENT_DESCRIPTOR_HPP #include +#include #include namespace score::crypto::daemon::common::storage @@ -33,18 +34,30 @@ struct DeploymentDescriptor /// @brief Section name -> (key -> value) map. std::unordered_map> sections; - /// @brief Get a value from a section, returning default_val if absent. - [[nodiscard]] const std::string& Get(const std::string& section, - const std::string& key, - const std::string& default_val = kEmptyString) const noexcept + /// @brief Get a value from a section, returning an empty string if absent. + [[nodiscard]] const std::string& Get(const std::string& section, const std::string& key) const noexcept { const auto sit = sections.find(section); if (sit == sections.end()) { - return default_val; + return kEmptyString; } const auto kit = sit->second.find(key); - return (kit != sit->second.end()) ? kit->second : default_val; + return (kit != sit->second.end()) ? kit->second : kEmptyString; + } + + /// @brief Get a value from a section, returning an owning default if absent. + [[nodiscard]] std::string Get(const std::string& section, + const std::string& key, + std::string_view default_val) const + { + const auto sit = sections.find(section); + if (sit == sections.end()) + { + return std::string{default_val}; + } + const auto kit = sit->second.find(key); + return (kit != sit->second.end()) ? kit->second : std::string{default_val}; } /// @brief True if the named section is present (even if empty). diff --git a/score/crypto/src/daemon/common/storage/file_io.cpp b/score/crypto/src/daemon/common/storage/file_io.cpp index bd542842f..5db4c4548 100644 --- a/score/crypto/src/daemon/common/storage/file_io.cpp +++ b/score/crypto/src/daemon/common/storage/file_io.cpp @@ -23,10 +23,21 @@ namespace score::crypto::daemon::common::storage score::crypto::Expected, DaemonErrorCode> ReadFile(const std::string& path, std::size_t max_size) { + const auto exists = FileExists(path); + if (!exists) + return score::crypto::make_unexpected(exists.error()); + if (!*exists) + return score::crypto::make_unexpected(DaemonErrorCode::kResourceNotAllocated); + score::filesystem::FileFactory factory{}; auto open_result = factory.Open(score::filesystem::Path{path}, std::ios::binary | std::ios::in); if (!open_result.has_value()) - return score::crypto::make_unexpected(DaemonErrorCode::kResourceNotAllocated); + { + return score::crypto::make_unexpected(open_result.error() == + score::filesystem::ErrorCode::kFileOrDirectoryDoesNotExist + ? DaemonErrorCode::kResourceNotAllocated + : DaemonErrorCode::kInternalError); + } auto& stream = *open_result.value(); stream.seekg(0, std::ios::end); @@ -77,11 +88,20 @@ score::crypto::Expected WriteFile(const std::st return std::monostate{}; } -bool FileExists(const std::string& path) +score::crypto::Expected FileExists(const std::string& path) { + if (path.empty()) + return score::crypto::make_unexpected(DaemonErrorCode::kInvalidArgument); + score::filesystem::StandardFilesystem fs{}; const auto result = fs.IsRegularFile(score::filesystem::Path{path}); - return result.has_value() && result.value(); + if (!result.has_value()) + { + if (result.error() == score::filesystem::ErrorCode::kFileOrDirectoryDoesNotExist) + return false; + return score::crypto::make_unexpected(DaemonErrorCode::kInternalError); + } + return result.value(); } score::crypto::Expected RemoveFile(const std::string& path) @@ -90,8 +110,15 @@ score::crypto::Expected RemoveFile(const std::s return score::crypto::make_unexpected(DaemonErrorCode::kInvalidArgument); score::filesystem::StandardFilesystem fs{}; const auto result = fs.Remove(score::filesystem::Path{path}); - if (!result.has_value() && result.error() != score::filesystem::ErrorCode::kFileOrDirectoryDoesNotExist) + if (!result.has_value()) + { + const auto status = fs.Status(score::filesystem::Path{path}); + if (status.has_value() && status->Type() == score::filesystem::FileType::kNotFound) + return std::monostate{}; + if (result.error() == score::filesystem::ErrorCode::kFileOrDirectoryDoesNotExist) + return std::monostate{}; return score::crypto::make_unexpected(DaemonErrorCode::kPersistFailed); + } return std::monostate{}; } diff --git a/score/crypto/src/daemon/common/storage/file_io.hpp b/score/crypto/src/daemon/common/storage/file_io.hpp index 6d4483402..844e4db71 100644 --- a/score/crypto/src/daemon/common/storage/file_io.hpp +++ b/score/crypto/src/daemon/common/storage/file_io.hpp @@ -10,10 +10,7 @@ * * SPDX-License-Identifier: Apache-2.0 ********************************************************************************/ -/******************************************************************************** - * Copyright (c) 2026 Contributors to the Eclipse Foundation - * SPDX-License-Identifier: Apache-2.0 - ********************************************************************************/ + #ifndef SCORE_CRYPTO_SRC_DAEMON_COMMON_STORAGE_FILE_IO_HPP #define SCORE_CRYPTO_SRC_DAEMON_COMMON_STORAGE_FILE_IO_HPP @@ -33,9 +30,9 @@ namespace score::crypto::daemon::common::storage /// @param path Absolute or relative path to the file. /// @param max_size Maximum accepted file size in bytes. Returns kInvalidArgument /// if the file is empty or exceeds this limit. -/// @return Byte vector on success; kResourceNotAllocated if the file cannot be -/// opened, kInvalidArgument if size is out of range, kInternalError on -/// a partial read. +/// @return Byte vector on success; kResourceNotAllocated if the file is absent, +/// kInvalidArgument if size is out of range, kInternalError if the file +/// cannot be opened for another reason or on a partial read. [[nodiscard]] score::crypto::Expected, DaemonErrorCode> ReadFile(const std::string& path, std::size_t max_size); @@ -52,11 +49,12 @@ namespace score::crypto::daemon::common::storage const std::string& path, score::crypto::span data); -/// Return true if @p path refers to an existing regular file. +/// Check whether @p path refers to an existing regular file. /// -/// Returns false for directories, symlinks to non-existent targets, and any -/// other non-regular-file entries. Errors resolve to false. -[[nodiscard]] bool FileExists(const std::string& path); +/// Returns false for missing paths, directories, symlinks to non-existent +/// targets, and other non-regular-file entries. Filesystem errors are returned +/// as failures instead of being reported as absence. +[[nodiscard]] score::crypto::Expected FileExists(const std::string& path); /// Remove the file at @p path. /// diff --git a/score/crypto/src/daemon/common/storage/kv/kv_deployment_loader.cpp b/score/crypto/src/daemon/common/storage/kv/kv_deployment_loader.cpp index 34ec737cc..0524bfe36 100644 --- a/score/crypto/src/daemon/common/storage/kv/kv_deployment_loader.cpp +++ b/score/crypto/src/daemon/common/storage/kv/kv_deployment_loader.cpp @@ -79,7 +79,15 @@ score::crypto::Expected + +#include + +namespace score::crypto::daemon::common::storage +{ +namespace +{ + +TEST(DeploymentDescriptorTest, MissingKeyWithoutDefaultReturnsStableEmptyString) +{ + DeploymentDescriptor descriptor; + + const auto& value = descriptor.Get("certificate", "cert_path"); + + EXPECT_TRUE(value.empty()); + EXPECT_EQ(&value, &descriptor.Get("certificate", "cert_path")); +} + +TEST(DeploymentDescriptorTest, ExplicitDefaultReturnsOwningValue) +{ + DeploymentDescriptor descriptor; + + const auto& value = descriptor.Get("certificate", "cert_path", "/default/path"); + + EXPECT_EQ(value, "/default/path"); + static_assert(std::is_same_v); +} + +TEST(DeploymentDescriptorTest, ExistingValueWithoutDefaultReturnsDescriptorValue) +{ + DeploymentDescriptor descriptor; + descriptor.Set("certificate", "cert_path", "/certificate/path"); + + const auto& value = descriptor.Get("certificate", "cert_path"); + + EXPECT_EQ(value, "/certificate/path"); + static_assert(std::is_same_v); +} + +} // namespace +} // namespace score::crypto::daemon::common::storage diff --git a/score/crypto/src/daemon/common/storage/tests/test_file_io.cpp b/score/crypto/src/daemon/common/storage/tests/test_file_io.cpp new file mode 100644 index 000000000..f91e1f987 --- /dev/null +++ b/score/crypto/src/daemon/common/storage/tests/test_file_io.cpp @@ -0,0 +1,81 @@ +/******************************************************************************** + * 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/common/storage/file_io.hpp" + +#include + +#include +#include +#include +#include + +namespace score::crypto::daemon::common::storage +{ +namespace +{ + +std::filesystem::path TestPath(const std::string& suffix) +{ + return std::filesystem::temp_directory_path() / + ("score_crypto_file_io_" + std::to_string(std::chrono::steady_clock::now().time_since_epoch().count()) + + "_" + suffix); +} + +TEST(FileIoTest, MissingFileIsReportedAsAbsent) +{ + const auto path = TestPath("missing"); + + const auto result = FileExists(path.string()); + + ASSERT_TRUE(result.has_value()); + EXPECT_FALSE(result.value()); +} + +TEST(FileIoTest, RegularFileIsReportedAsPresent) +{ + const auto path = TestPath("present"); + { + std::ofstream output{path}; + ASSERT_TRUE(output.is_open()); + output << "data"; + } + + const auto result = FileExists(path.string()); + + ASSERT_TRUE(result.has_value()); + EXPECT_TRUE(result.value()); + std::filesystem::remove(path); +} + +TEST(FileIoTest, MissingFileReadRetainsEmptyResourceMeaning) +{ + const auto path = TestPath("missing_read"); + + const auto result = ReadFile(path.string(), 1024U); + + ASSERT_FALSE(result.has_value()); + EXPECT_EQ(result.error(), DaemonErrorCode::kResourceNotAllocated); +} + +TEST(FileIoTest, RemovingMissingFileIsIdempotent) +{ + const auto path = TestPath("missing_remove"); + + const auto result = RemoveFile(path.string()); + + EXPECT_TRUE(result.has_value()); +} + +} // namespace +} // namespace score::crypto::daemon::common::storage diff --git a/score/crypto/src/daemon/common/storage/tests/test_kv_deployment_loader.cpp b/score/crypto/src/daemon/common/storage/tests/test_kv_deployment_loader.cpp new file mode 100644 index 000000000..029681b22 --- /dev/null +++ b/score/crypto/src/daemon/common/storage/tests/test_kv_deployment_loader.cpp @@ -0,0 +1,113 @@ +/******************************************************************************** + * 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/common/storage/kv/kv_deployment_loader.hpp" + +#include + +#include +#include +#include +#include + +namespace score::crypto::daemon::common::storage +{ +namespace +{ + +std::filesystem::path TestPath(const std::string& suffix) +{ + return std::filesystem::temp_directory_path() / + ("score_crypto_kv_deployment_loader_" + + std::to_string(std::chrono::steady_clock::now().time_since_epoch().count()) + "_" + suffix); +} + +std::string WriteDescriptor(const std::string& suffix, const std::string& content) +{ + const auto path = TestPath(suffix); + std::ofstream output{path}; + output << content; + output.close(); + return path.string(); +} + +TEST(KvDeploymentLoaderTest, ValidDescriptorLoadsSuccessfully) +{ + const auto path = WriteDescriptor("valid", + "[certificate]\n" + "cert_path = /tmp/cert.pem\n" + "cert_format = pem\n" + "\n" + "[crl]\n" + "crl_path = /tmp/cert.crl\n"); + + KvDeploymentLoader loader; + const auto result = loader.Load(path); + + ASSERT_TRUE(result.has_value()); + EXPECT_EQ(result->Get("certificate", "cert_path"), "/tmp/cert.pem"); + EXPECT_EQ(result->Get("certificate", "cert_format"), "pem"); + EXPECT_EQ(result->Get("crl", "crl_path"), "/tmp/cert.crl"); + std::filesystem::remove(path); +} + +TEST(KvDeploymentLoaderTest, DuplicateKeyInSameSectionIsRejected) +{ + const auto path = WriteDescriptor("duplicate", + "[certificate]\n" + "cert_path = /tmp/first.pem\n" + "cert_path = /tmp/second.pem\n"); + + KvDeploymentLoader loader; + const auto result = loader.Load(path); + + ASSERT_FALSE(result.has_value()); + EXPECT_EQ(result.error(), DaemonErrorCode::kInvalidArgument); + std::filesystem::remove(path); +} + +TEST(KvDeploymentLoaderTest, DuplicateKeyWithSurroundingWhitespaceIsRejected) +{ + const auto path = WriteDescriptor("duplicate_whitespace", + "[certificate]\n" + " cert_path = /tmp/first.pem\n" + "cert_path= /tmp/second.pem\n"); + + KvDeploymentLoader loader; + const auto result = loader.Load(path); + + ASSERT_FALSE(result.has_value()); + EXPECT_EQ(result.error(), DaemonErrorCode::kInvalidArgument); + std::filesystem::remove(path); +} + +TEST(KvDeploymentLoaderTest, SameKeyInDifferentSectionsIsAllowed) +{ + const auto path = WriteDescriptor("same_key_different_sections", + "[certificate]\n" + "path = /tmp/cert.pem\n" + "\n" + "[crl]\n" + "path = /tmp/cert.crl\n"); + + KvDeploymentLoader loader; + const auto result = loader.Load(path); + + ASSERT_TRUE(result.has_value()); + EXPECT_EQ(result->Get("certificate", "path"), "/tmp/cert.pem"); + EXPECT_EQ(result->Get("crl", "path"), "/tmp/cert.crl"); + std::filesystem::remove(path); +} + +} // namespace +} // namespace score::crypto::daemon::common::storage From 7acea47cd61ed0bf89afa98ca20daabd37a37ba8 Mon Sep 17 00:00:00 2001 From: Athul Mallappallil Date: Fri, 18 Sep 2026 17:00:03 +0200 Subject: [PATCH 22/37] Mediator provider capability fix --- .../src/daemon/mediator/src/mediator_impl.cpp | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/score/crypto/src/daemon/mediator/src/mediator_impl.cpp b/score/crypto/src/daemon/mediator/src/mediator_impl.cpp index 77c7b93ce..e69d96f5b 100644 --- a/score/crypto/src/daemon/mediator/src/mediator_impl.cpp +++ b/score/crypto/src/daemon/mediator/src/mediator_impl.cpp @@ -351,6 +351,16 @@ bool MediatorImpl::HandleContextCreationOperation(const score::crypto::daemon::c return false; } + if (required_capability != common::ProviderCapability::kNone && + !common::HasCapability(provider->GetProviderCapabilities(), required_capability)) + { + score::mw::log::LogError() + << "[SCORE_API_MED] ERROR - Selected provider lacks required capability for context: " << context_type; + responseBuilder.operation(operation.operationId) + .return_error(score::crypto::CryptoErrorCode::kUnsupportedOperation); + return false; + } + auto crypto_ops = provider->GetCryptoHandlerFactory(); if (crypto_ops == nullptr) { @@ -365,7 +375,8 @@ bool MediatorImpl::HandleContextCreationOperation(const score::crypto::daemon::c { 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(score::crypto::CryptoErrorCode::kUnsupportedOperation); return false; } From b734df0f9335b6657213ec465bcd262a500e26a4 Mon Sep 17 00:00:00 2001 From: Athul Mallappallil Date: Fri, 18 Sep 2026 17:01:04 +0200 Subject: [PATCH 23/37] Crl inspect and more verify evidence --- .../i_certificate_management_context.hpp | 97 +++++++++---------- .../i_certificate_verification_context.hpp | 70 +++++++------ score/crypto/src/api/objects/BUILD | 2 + .../src/api/objects/i_cert_slot_object.hpp | 3 + .../src/api/objects/i_certificate_object.hpp | 21 ++-- 5 files changed, 105 insertions(+), 88 deletions(-) diff --git a/score/crypto/src/api/contexts/i_certificate_management_context.hpp b/score/crypto/src/api/contexts/i_certificate_management_context.hpp index 7efabc6f3..2521e7b3f 100644 --- a/score/crypto/src/api/contexts/i_certificate_management_context.hpp +++ b/score/crypto/src/api/contexts/i_certificate_management_context.hpp @@ -37,23 +37,21 @@ namespace crypto /// @brief Interface for certificate lifecycle management operations. /// -/// - **Parse** raw bytes into a daemon-backed ICertificateObject with an -/// ephemeral resource ID. -/// - **SaveCertificate** copies an ephemeral certificate to a persistent slot -/// (copy semantics — the ephemeral cert remains valid until the -/// ICertificateObject goes out of scope). +/// - **Parse** raw bytes into a guarded ephemeral certificate resource. +/// - **SaveCertificate** copies an ephemeral certificate to a persistent slot. /// - **Export / convert** using a two-call pattern: query the required buffer /// size first, then fill the caller-supplied span. -/// - **Slot management** and **trust store management** are co-located here. +/// - **Certificate-slot and slot-CRL lifecycle** including loading, saving, +/// clearing, and persistent CRL import. /// /// **ParseCertificate lifecycle**: /// @code /// auto cert = cert_mgmt->ParseCertificate(der_bytes, FormatType::kDer).value(); -/// // cert->GetId() is valid — daemon assigned an ephemeral kCertificate ID. -/// // Inspect before committing: -/// if (cert->GetNotAfter() < current_time) { return Error; } -/// cert_mgmt->SaveCertificate(cert->GetId(), target_slot).value(); -/// // cert goes out of scope → destructor → daemon releases ephemeral copy. +/// auto view = crypto_context->GetCertificateObject(cert).value(); +/// // Inspect the certificate while cert owns the daemon resource. +/// if (view->GetNotAfter() < current_time) { return Error; } +/// cert_mgmt->SaveCertificate(cert, target_slot).value(); +/// // cert goes out of scope → guard releases the ephemeral certificate. /// @endcode class ICertificateManagementContext : public IContext { @@ -73,32 +71,29 @@ class ICertificateManagementContext : public IContext /// /// Sends the raw bytes to the daemon, which validates the certificate /// structure and assigns an ephemeral kCertificate resource ID. - /// The returned ICertificateObject::GetId() is immediately valid. + /// The returned guard owns the ephemeral kCertificate resource. /// /// @param cert_data DER or PEM encoded certificate bytes /// @param format Encoding format of the input data - /// @return ICertificateObject with a daemon-assigned ephemeral ID. - /// Destroying the object releases the ephemeral ID. - virtual score::Result ParseCertificate(score::cpp::span cert_data, - FormatType format) = 0; + /// @return CryptoResourceGuard owning the daemon-assigned ephemeral resource. + virtual score::Result ParseCertificate(score::cpp::span cert_data, + FormatType format) = 0; /// @brief Parses multiple certificates from a PEM bundle or DER chain. /// /// @param cert_data PEM bundle or concatenated certificate data /// @param format Encoding format of the data - /// @return Ordered vector of ICertificateObject (first = leaf/first in bundle). - /// Each object has a daemon-assigned ephemeral ID. - virtual score::Result> ParseCertificates( - score::cpp::span cert_data, - FormatType format) = 0; + /// @return Ordered vector of guards (first = leaf/first in bundle). + /// Each guard owns one daemon-assigned ephemeral resource. + virtual score::Result> ParseCertificates(score::cpp::span cert_data, + FormatType format) = 0; // ---- Persistence ---- /// @brief Copies an ephemeral certificate to a persistent certificate slot. /// - /// Copy semantics: the ephemeral certificate (and its ICertificateObject) - /// remains valid after this call. The object releases the ephemeral copy - /// independently when it is destroyed. + /// Copy semantics: the source guard continues to own the ephemeral + /// certificate after this call. /// /// Typical usage: parse → inspect fields → save to slot. /// @@ -166,10 +161,9 @@ class ICertificateManagementContext : public IContext /// @return std::monostate on success, error if the slot is not found or access is denied virtual score::Result ClearCertificate(const CryptoResourceId& slot) = 0; - /// @brief Queries the occupancy and metadata of a certificate slot. - /// @param slot Handle to the slot (type = kCertSlot) - /// @return CertificateSlotInfo with certificate-slot state and CRL presence - virtual score::Result GetCertificateSlotInfo(const CryptoResourceId& slot) = 0; + /// @brief Loads a certificate from a persistent slot into an ephemeral resource. + /// The returned guard may be reused with multiple certificate contexts. + virtual score::Result LoadCertificate(const CryptoResourceId& slot) = 0; // ---- Key extraction ---- @@ -185,17 +179,13 @@ class ICertificateManagementContext : public IContext virtual score::Result> LoadCertificatePublicKey( const CryptoResourceId& cert) = 0; - /// @brief Bulk-deletes expired certificates from persistent slots. - /// @return Number of certificates deleted - virtual score::Result DeleteExpiredCertificates() = 0; + // ---- Persistence with CRL propagation ---- - // ---- Persistence — with optional CRL propagation ---- - - /// @brief Copies an ephemeral certificate to a persistent slot, optionally propagating its CRL. + /// @brief Copies a certificate to a persistent slot and propagates its CRL. /// - /// When @p with_crl is true the daemon propagates the CRL already held for @p cert: - /// - If a session-scoped CRL was previously associated via ImportCrl() (persist=false), - /// that CRL is used (works for both kCertificate and kCertSlot sources). + /// The daemon propagates the CRL already held for @p cert: + /// - If a session-scoped CRL is associated via ImportCrl() with a + /// kCertificate source, that CRL is used. /// - Otherwise the CRL is read from @p cert's slot's persistent [crl] section /// (only applicable when cert is a kCertSlot source). /// @@ -203,30 +193,35 @@ class ICertificateManagementContext : public IContext /// /// @param cert CryptoResourceId of the certificate to save (type = kCertificate or kCertSlot) /// @param target_slot Handle to the target slot (type = kCertSlot) - /// @param with_crl When true, propagate the associated CRL to the destination slot - virtual score::Result SaveCertificate(const CryptoResourceId& cert, - const CryptoResourceId& target_slot, - bool with_crl) = 0; + virtual score::Result SaveCertificateWithCrl(const CryptoResourceId& cert, + const CryptoResourceId& target_slot) = 0; // ---- CRL management ---- - /// @brief Imports a Certificate Revocation List and associates it with its issuer certificate. + /// @brief Imports a session-scoped CRL for a `kCertificate` resource. /// - /// The CRL lifecycle follows the lifecycle of @p issuer_cert: - /// - kCertSlot + persist=true: CRL written to the slot's [crl] section; write access required. - /// - kCertSlot or kCertificate + persist=false: session-scoped in-memory; no write access needed. - /// Session CRL is consumed by SaveCertificate(with_crl=true) and - /// AddCertificateToTrustStore(with_crl=true) without re-passing raw bytes. + /// The association follows the lifetime of @p issuer_cert. No slot write + /// access is required. The session CRL is consumed by + /// SaveCertificateWithCrl and AddCertificateToTrustStoreWithCrl + /// without re-passing raw bytes. /// /// @param crl_data Encoded CRL data /// @param format Encoding format of the CRL - /// @param issuer_cert Handle to the issuer certificate (type = kCertSlot or kCertificate) - /// @param persist When true, store permanently to the issuer slot (kCertSlot only) + /// @param issuer_cert Handle to a `kCertificate` resource. Obtain one by + /// parsing a certificate or loading it from a slot. /// @return std::monostate on success, error if validation fails or access is denied virtual score::Result ImportCrl(score::cpp::span crl_data, FormatType format, - const CryptoResourceId& issuer_cert, - bool persist = false) = 0; + const CryptoResourceId& issuer_cert) = 0; + + /// @brief Imports a CRL to a persistent certificate slot. + /// @param crl_data Encoded CRL data + /// @param format Encoding format of the CRL + /// @param cert_slot Handle to the certificate slot (type = kCertSlot) + /// @return std::monostate on success, error if validation fails or access is denied + virtual score::Result ImportCrlToSlot(score::cpp::span crl_data, + FormatType format, + const CryptoResourceId& cert_slot) = 0; /// @brief Removes the CRL stored in a certificate slot. /// @param cert_slot Handle to the slot whose CRL should be removed (type = kCertSlot) diff --git a/score/crypto/src/api/contexts/i_certificate_verification_context.hpp b/score/crypto/src/api/contexts/i_certificate_verification_context.hpp index c7e69f785..51d91dd89 100644 --- a/score/crypto/src/api/contexts/i_certificate_verification_context.hpp +++ b/score/crypto/src/api/contexts/i_certificate_verification_context.hpp @@ -48,7 +48,7 @@ namespace crypto /// @par Example — chain verification with additional untrusted certificates /// @code /// // ext_ca is a kCertificate from ParseCertificate() — not persisted. -/// std::array extra = {ext_ca->GetId()}; +/// std::array extra = {ext_ca.Id()}; /// auto ctx = crypto_context->CreateCertificateVerificationContext(config).value(); /// ctx->SetCertificateChain(chain); /// ctx->SetVerificationTrustStore(system_trust_store); @@ -72,13 +72,13 @@ class ICertificateVerificationContext : public IContext /// @brief Sets the leaf certificate to verify. /// @param cert Handle to the certificate to verify /// @return std::monostate on success, error if cert handle is invalid - /// @note Replaces any previously set certificate or chain on this context. + /// @note Replaces the certificate or chain configured on this context. virtual score::Result SetCertificate(const CryptoResourceId& cert) = 0; /// @brief Sets a certificate chain to verify (leaf first). /// @param chain Ordered chain of certificate handles (leaf first, root last) /// @return std::monostate on success, error if any handle is invalid - /// @note Replaces any previously set certificate or chain on this context. + /// @note Replaces the certificate or chain configured on this context. virtual score::Result SetCertificateChain(score::cpp::span chain) = 0; /// @brief Sets the system trust store to use for certificate chain verification. @@ -92,21 +92,23 @@ class ICertificateVerificationContext : public IContext /// @return std::monostate on success, error if handle is invalid virtual score::Result SetVerificationTrustStore(const CryptoResourceId& trust_store) = 0; - /// @brief Sets the standalone trusted certificates for this verification context. + /// @brief Sets explicit trusted certificates for this verification context. /// - /// This mode is mutually exclusive with SetVerificationTrustStore(). Each - /// call replaces the previously configured set, so callers that discover - /// anchors incrementally must collect them before calling this method. + /// When a verification trust store is configured, these certificates are + /// added to the trust-store anchors. Without a trust store, they form the + /// complete standalone trust-anchor set. Each call replaces only the + /// explicitly configured certificates. /// /// @param certs Span of certificate handles to treat as trust anchors /// (type = kCertificate or kCertSlot) /// @return std::monostate on success, error if any handle is invalid virtual score::Result SetTrustedCertificates(score::cpp::span certs) = 0; - /// @brief Selects the chain termination rule for trust-store verification. + /// @brief Selects the chain termination rule for configured-anchor verification. /// - /// The default is ChainTerminationPolicy::kRootRequired. This setting has - /// no effect in standalone trusted-certificate mode. + /// The default is ChainTerminationPolicy::kRootRequired. With + /// kTrustStoreTerminated, verification stops at the first certificate in + /// the effective trust-anchor set, including explicit trusted certificates. virtual score::Result SetChainTerminationPolicy(ChainTerminationPolicy policy) = 0; /// @brief Supplies additional untrusted certificates for chain building. @@ -121,7 +123,7 @@ class ICertificateVerificationContext : public IContext /// established exclusively by the configured trust store or standalone /// trusted certificates. /// - /// Replaces any previously set additional certificates on this context. + /// Replaces the additional certificates configured on this context. /// /// @param certificates Span of untrusted certificate handles /// (type = kCertificate or kCertSlot) @@ -129,7 +131,7 @@ class ICertificateVerificationContext : public IContext virtual score::Result SetAdditionalCertificates( score::cpp::span certificates) = 0; - // ---- OCSP (not yet active — IPC implementation pending) ---- + // ---- OCSP ---- #if 0 /// @brief Provides one or more OCSP responses for revocation checking. /// @@ -138,7 +140,7 @@ class ICertificateVerificationContext : public IContext /// stapled OCSP responses (e.g. TLS 1.3 certificate_status records). /// The daemon matches each response to the appropriate certificate in the /// chain by the certID field embedded in the response; order does not matter. - /// Replaces any previously set responses on this context. + /// Replaces the OCSP responses configured on this context. /// /// @param responses Span of DER-encoded OCSP response byte spans /// @return std::monostate on success, error if any response fails to parse @@ -159,6 +161,10 @@ class ICertificateVerificationContext : public IContext /// @note Overrides the default policy set in the config. virtual score::Result SetRevocationCheckPolicy(RevocationCheckPolicy policy) = 0; + /// @brief Selects which evidence is retained after verification. + /// @note The default is kNone. Configure before Verify(). + virtual score::Result SetEvidenceMode(VerificationEvidenceMode mode) = 0; + // ---- Execution ---- /// @brief Executes the configured certificate verification. @@ -167,23 +173,31 @@ class ICertificateVerificationContext : public IContext virtual score::Result Verify() = 0; /// @brief Returns the number of certificates in the verified chain. - /// - /// Valid only after a successful Verify() call. The length is stable between - /// this call and GetVerifiedChain() provided no intervening Verify() is made. - /// - /// @return Number of entries in the chain (leaf to terminating anchor inclusive), - /// or an error if Verify() has not yet succeeded. - virtual score::Result GetVerifiedChainLength() const = 0; + /// @return Number of certificates, or an error before Verify(). + virtual score::Result GetVerifiedChainCertificateCount() const = 0; - /// @brief Fills caller-provided buffer with verified chain certificate IDs. + /// @brief Returns the encoded size of the verified chain. /// - /// Certificates are ordered leaf-first, terminating anchor last. - /// The caller must size @p out to at least GetVerifiedChainLength() entries. - /// - /// @param out Caller-allocated span of CryptoResourceId to fill - /// @return Number of entries written, or an error if @p out is too small - /// or Verify() has not yet succeeded. - virtual score::Result GetVerifiedChain(score::cpp::span out) const = 0; + /// Certificates are ordered leaf-first. PEM output is a concatenated PEM + /// chain; DER output is concatenated DER certificates in the same order. + virtual score::Result GetVerifiedChainExportSize(FormatType format) const = 0; + + /// @brief Exports the verified chain in leaf-first order. + virtual score::Result ExportVerifiedChain(FormatType format, score::cpp::span out) const = 0; + + /// @brief Returns the encoded size of one certificate in the verified chain. + virtual score::Result GetVerifiedCertificateExportSize(std::size_t index, FormatType format) const = 0; + + /// @brief Exports one certificate from the verified chain. + virtual score::Result ExportVerifiedCertificate(std::size_t index, + FormatType format, + score::cpp::span out) const = 0; + + /// @brief Returns the number of retained CRL evidence entries. + virtual score::Result GetSelectedCrlMetadataCount() const = 0; + + /// @brief Fills caller-provided storage with selected CRL metadata. + virtual score::Result GetSelectedCrlMetadata(score::cpp::span out) const = 0; protected: ICertificateVerificationContext() = default; diff --git a/score/crypto/src/api/objects/BUILD b/score/crypto/src/api/objects/BUILD index ed7a69117..929d9898b 100644 --- a/score/crypto/src/api/objects/BUILD +++ b/score/crypto/src/api/objects/BUILD @@ -27,6 +27,7 @@ cc_library( visibility = ["//visibility:public"], deps = [ "//score/crypto/src/api/common:crypto_common", + "//score/crypto/src/api/types:types", "@score_baselibs//score/language/futurecpp", "@score_baselibs//score/result", ], @@ -44,6 +45,7 @@ cc_library( deps = [ ":crypto_objects", "//score/crypto/src/api/common:crypto_common", + "//score/crypto/src/api/types:types", "@score_baselibs//score/language/futurecpp", "@score_baselibs//score/result", ], diff --git a/score/crypto/src/api/objects/i_cert_slot_object.hpp b/score/crypto/src/api/objects/i_cert_slot_object.hpp index d6444b5d5..24aedd0da 100644 --- a/score/crypto/src/api/objects/i_cert_slot_object.hpp +++ b/score/crypto/src/api/objects/i_cert_slot_object.hpp @@ -43,6 +43,9 @@ class ICertSlotObject : public ICryptoObject /// @brief Whether the certificate slot currently holds a certificate. virtual bool IsOccupied() const noexcept = 0; + /// @brief Whether the slot currently stores a persistent CRL. + virtual bool HasCrl() const noexcept = 0; + protected: ICertSlotObject() = default; }; diff --git a/score/crypto/src/api/objects/i_certificate_object.hpp b/score/crypto/src/api/objects/i_certificate_object.hpp index eefdb2ca5..772d6c6b8 100644 --- a/score/crypto/src/api/objects/i_certificate_object.hpp +++ b/score/crypto/src/api/objects/i_certificate_object.hpp @@ -34,19 +34,16 @@ namespace crypto /// @brief Typed view of a certificate resource. /// -/// The single certificate abstraction used for both ephemeral (parsed from bytes) -/// and persistent (loaded from a slot) certificates. All instances are +/// The single certificate view abstraction used for both ephemeral (parsed from +/// bytes) and persistent (loaded from a slot) certificates. All instances are /// daemon-backed and carry a valid `GetId()` from the moment they are obtained. /// -/// **Lifecycle**: destroying this object releases the daemon-side resource. -/// For ephemeral certificates created by ParseCertificate(), the daemon frees -/// the resource when the last ICertificateObject referring to it is destroyed. -/// For persistent certificates loaded from a slot, the slot and its content -/// are unaffected — only the in-memory view object is released. +/// **Lifecycle**: this object is non-owning. A parsed certificate's +/// CryptoResourceGuard owns the daemon-side resource and must outlive this view. +/// For persistent certificates, the slot owns the stored content. /// /// **Persistence**: use ICertificateManagementContext::SaveCertificate() to -/// copy an ephemeral certificate to a persistent slot. The ephemeral copy -/// remains valid and is released independently when this object is destroyed. +/// copy an ephemeral certificate to a persistent slot. /// /// Provides field access, serial number, public key metadata, and public key /// export. Certificates with PQC keys (ML-DSA, SLH-DSA, XMSS, LMS) may @@ -90,6 +87,12 @@ class ICertificateObject : public ICryptoObject /// with the issuer DN and serial number it uniquely identifies a certificate. virtual std::array GetFingerprint() const noexcept = 0; + /// @brief Returns metadata for the CRL associated with this certificate. + /// + /// The result is empty when no session-scoped or persistent CRL is + /// associated with the certificate. + virtual std::optional GetCrlMetadata() const noexcept = 0; + /// @brief Returns the byte size of the DER-encoded SubjectPublicKeyInfo. /// /// Call this before ExportPublicKey() to determine the required buffer size. From a16d97da0efd70ce36e0aca468b00c2a4b9d7cfb Mon Sep 17 00:00:00 2001 From: Athul Mallappallil Date: Fri, 18 Sep 2026 17:01:33 +0200 Subject: [PATCH 24/37] Doc update --- .../api_certificate_contexts.puml | 56 +++++++++++++++---- .../docs/architecture/api_description.rst | 19 ++++++- .../docs/architecture/api_typed_objects.puml | 2 + .../architecture/configuration_objects.puml | 1 - score/crypto/docs/architecture/interfaces.rst | 19 ++++++- 5 files changed, 79 insertions(+), 18 deletions(-) diff --git a/score/crypto/docs/architecture/api_certificate_contexts.puml b/score/crypto/docs/architecture/api_certificate_contexts.puml index ebb37ff0b..4b8e9109a 100644 --- a/score/crypto/docs/architecture/api_certificate_contexts.puml +++ b/score/crypto/docs/architecture/api_certificate_contexts.puml @@ -30,10 +30,11 @@ package "Context Base" { package "Certificate Management" { interface ICertificateManagementContext { __ Parsing __ - + ParseCertificate(cert_data, format) : Result - + ParseCertificates(cert_data, format) : Result> + + ParseCertificate(cert_data, format) : Result + + ParseCertificates(cert_data, format) : Result> __ Persistence (copy semantics) __ + SaveCertificate(id, slot) : Result + + SaveCertificateWithCrl(id, slot) : Result __ Export __ + GetCertificateExportSize(cert) : Result + ExportCertificate(cert, format, output) : Result @@ -41,21 +42,40 @@ package "Certificate Management" { + ConvertCertificateFormat(input, in_fmt, out_fmt, output) : Result __ Slot Management __ + ClearCertificate(slot) : Result - + GetCertificateSlotInfo(slot) : Result + + LoadCertificate(slot) : Result __ CRL Management __ - + ImportCrl(crl_data, format, issuer_cert, persist) : Result - + DeleteCrl(crl) : Result - + DeleteExpiredCrls() : Result - + DeleteExpiredCertificates() : Result + + ImportCrl(crl_data, format, certificate) : Result + + ImportCrlToSlot(crl_data, format, cert_slot) : Result + + DeleteCrl(cert_slot) : Result __ Key Extraction __ + LoadCertificatePublicKey(cert) : Result> - __ OCSP __ - + GetOcspRequestData(cert, issuer_cert) : Result + __ OCSP (reserved) __ + + GetOcspRequestData(...) : deferred } ICertificateManagementContext --|> IContext : inherits } +' ============================================================ +' Trust-Store Management +' ============================================================ +package "Trust-Store Management" { + interface ITrustStoreManagementContext { + __ Membership __ + + AddCertificateToTrustStoreWithCrl(trust_store, cert) : Result + + RemoveCertificateFromTrustStore(trust_store, cert) : Result + + RemoveCertificateFromTrustStore(trust_store, fingerprint) : Result + + EnableTrustStoreMember(trust_store, slot) : Result + + DisableTrustStoreMember(trust_store, slot) : Result + + AcknowledgeTrustStoreMemberUpdate(trust_store, slot) : Result + __ Member CRL __ + + ImportCrlForTrustStoreMember(trust_store, slot, crl_data, format) : Result + + DeleteCrlForTrustStoreMember(trust_store, slot) : Result + } + + ITrustStoreManagementContext --|> IContext : inherits +} + ' ============================================================ ' Certificate Verification ' ============================================================ @@ -65,13 +85,19 @@ package "Certificate Verification" { + SetCertificate(cert) : Result + SetCertificateChain(chain) : Result + SetVerificationTrustStore(trust_store) : Result - + SetAdditionalTrustAnchors(anchors : span) : Result - + SetOcspResponse(response_data) : Result - + SetCrl(crl) : Result + + SetAdditionalCertificates(certificates : span) : Result + + SetOcspResponses(responses : span) : deferred + SetVerificationTime(epoch_seconds) : Result + SetRevocationCheckPolicy(policy) : Result + + SetEvidenceMode(mode) : Result __ Execution __ + Verify() : Result + + GetVerifiedChainExportSize(format) : Result + + ExportVerifiedChain(format, output) : Result + + GetVerifiedCertificateExportSize(index, format) : Result + + ExportVerifiedCertificate(index, format, output) : Result + + GetSelectedCrlMetadataCount() : Result + + GetSelectedCrlMetadata(out : span) : Result } ICertificateVerificationContext --|> IContext : inherits @@ -111,6 +137,11 @@ package "Configuration" { + SetProviderType(type) : CertificateContextConfig& } + class TrustStoreManagementContextConfig <> { + + SetProvider(prov) : TrustStoreManagementContextConfig& + + SetProviderType(type) : TrustStoreManagementContextConfig& + } + class CertificateVerificationContextConfig <> { + revocation_policy : optional --- @@ -134,6 +165,7 @@ package "Configuration" { ' Relationships ' ============================================================ CertificateContextConfig ..> ICertificateManagementContext : configures +TrustStoreManagementContextConfig ..> ITrustStoreManagementContext : configures CertificateVerificationContextConfig ..> ICertificateVerificationContext : configures CsrGenerationContextConfig ..> ICsrGenerationContext : configures diff --git a/score/crypto/docs/architecture/api_description.rst b/score/crypto/docs/architecture/api_description.rst index da2de27a5..6aff07582 100644 --- a/score/crypto/docs/architecture/api_description.rst +++ b/score/crypto/docs/architecture/api_description.rst @@ -226,6 +226,7 @@ promoting DRY code reuse: ├── IRandomContext (Generate, Seed) ├── IKeyManagementContext (key lifecycle operations) ├── ICertificateManagementContext (certificate lifecycle operations) + ├── ITrustStoreManagementContext (trust-store membership curation) ├── ICertificateVerificationContext (builder-style chain verification) └── ICsrGenerationContext (builder-style CSR generation) @@ -458,10 +459,13 @@ Certificate Lifecycle - **Parsing**: ``ParseCertificate()`` returns an ``ICertificateObject::Uptr`` with field accessors (subject, issuer, serial, validity dates, algorithm). The object is backed by a daemon-assigned ephemeral ``CryptoResourceId``. -- **Persistence**: ``SaveCertificate(id, slot)`` promotes a parsed certificate to a slot +- **Persistence**: ``SaveCertificate(id, slot)`` promotes a parsed certificate to a slot; + ``SaveCertificateWithCrl(id, slot)`` also propagates the associated CRL (copy semantics — the parsed object remains valid after the call) - **Export**: ``GetCertificateExportSize()`` + ``ExportCertificate()`` two-call pattern -- **CRL**: ``ImportCrl()``, ``DeleteCrl()``, ``DeleteExpiredCrls()`` for offline revocation +- **CRL**: ``ImportCrl()`` for session-scoped CRLs on loaded certificates, + ``ImportCrlToSlot()`` and ``DeleteCrl()`` for persistent slot-scoped + revocation, with CRL metadata available for inspection - **Key extraction**: ``LoadCertificatePublicKey()`` extracts the public key as a ``CryptoResourceGuard`` wrapping an ephemeral ``CryptoResourceId`` with ``type == kKey``, following the same guard model as key-producing methods. @@ -469,6 +473,17 @@ Certificate Lifecycle - **OCSP**: ``GetOcspRequestData()`` generates a request; the response is consumed via ``ICertificateVerificationContext::SetOcspResponse()`` +**ITrustStoreManagementContext** handles trust-store membership curation: + +- adding and removing certificate members +- enabling, disabling, and acknowledging member updates +- importing CRLs for trust-store-owned exclusive members +- deleting CRLs from trust-store-owned exclusive members + +It uses ``TrustStoreManagementContextConfig`` and the certificate-management +provider capability, but routes to the distinct ``CERT:TRUST_STORE`` daemon +context. ``ITrustStoreObject`` remains a read-only snapshot interface. + **ICertificateVerificationContext** provides builder-style chain verification: - ``SetCertificate()``, ``SetCertificateChain()``, ``SetVerificationTrustStore()``, ``SetAdditionalTrustAnchors()`` diff --git a/score/crypto/docs/architecture/api_typed_objects.puml b/score/crypto/docs/architecture/api_typed_objects.puml index fc261072c..5cd6a8d3d 100644 --- a/score/crypto/docs/architecture/api_typed_objects.puml +++ b/score/crypto/docs/architecture/api_typed_objects.puml @@ -70,6 +70,7 @@ package "Slot Objects" { interface ICertSlotObject { + IsOccupied() : bool + + HasCrl() : bool } IKeySlotObject --|> ICryptoObject : inherits @@ -86,6 +87,7 @@ package "Certificate Objects" { + GetNotBefore() : int64_t + GetNotAfter() : int64_t + GetPublicKeyAlgorithm() : AlgorithmId + + GetCrlMetadata() : optional } ICertificateObject --|> ICryptoObject : inherits diff --git a/score/crypto/docs/architecture/configuration_objects.puml b/score/crypto/docs/architecture/configuration_objects.puml index 35e2c60e8..b19e22bcf 100644 --- a/score/crypto/docs/architecture/configuration_objects.puml +++ b/score/crypto/docs/architecture/configuration_objects.puml @@ -106,7 +106,6 @@ package "Configuration" { } class CertificateContextConfig <> { - + SetAlgorithm(alg) : CertificateContextConfig& + SetProvider(prov) : CertificateContextConfig& + SetProviderType(type) : CertificateContextConfig& } diff --git a/score/crypto/docs/architecture/interfaces.rst b/score/crypto/docs/architecture/interfaces.rst index d8e5a395c..81ade8acc 100644 --- a/score/crypto/docs/architecture/interfaces.rst +++ b/score/crypto/docs/architecture/interfaces.rst @@ -192,9 +192,8 @@ The public API surface is organized into the following interface groups: (copy semantics — object remains valid after persist), export (``GetCertificateExportSize`` + ``ExportCertificate``), format conversion (``GetConvertedCertificateSize`` + ``ConvertCertificateFormat``), - ``ClearCertificate``, ``GetCertificateSlotInfo``, CRL management - (``ImportCrl``, ``DeleteCrl``, ``DeleteExpiredCrls``, - ``DeleteExpiredCertificates``), + ``ClearCertificate``, CRL management + (``ImportCrl``, ``DeleteCrl``, and CRL metadata inspection), public key extraction (``LoadCertificatePublicKey`` — returns a ``CryptoResourceGuard`` wrapping an ephemeral ``kKey`` resource, following the same guard model as key-producing methods), and OCSP @@ -212,6 +211,20 @@ The public API surface is organized into the following interface groups: certificate, chain, verification trust store, and revocation check policy via fluent setters, then executes verification with ``Verify()``. +.. real_arc_int:: ITrustStoreManagementContext + :id: real_arc_int__crypto__i_trust_store_mgmt_ctx + :version: 1 + :security: YES + :safety: QM + :status: invalid + :language: cpp + + Trust-store membership management. Adds and removes certificate + members, enables or disables members, acknowledges + conditional-member updates, and imports CRLs for exclusive members + managed by the trust store. Read-only inspection is provided + through ``ITrustStoreObject``. + .. real_arc_int:: ICsrGenerationContext :id: real_arc_int__crypto__i_csr_gen_context :version: 1 From 67d87b8cf7a27c5e43b5851a4b3ef2a234e00621 Mon Sep 17 00:00:00 2001 From: Athul Mallappallil Date: Fri, 18 Sep 2026 22:52:07 +0200 Subject: [PATCH 25/37] Trust store object refactor --- .../i_trust_store_management_context.hpp | 10 +- .../src/api/objects/i_certificate_object.hpp | 2 - .../src/api/objects/i_trust_store_object.hpp | 91 ++----------------- .../src/api/src/crypto_context_impl.cpp | 2 +- score/crypto/src/api/types/certificate.hpp | 38 +++++++- 5 files changed, 48 insertions(+), 95 deletions(-) diff --git a/score/crypto/src/api/contexts/i_trust_store_management_context.hpp b/score/crypto/src/api/contexts/i_trust_store_management_context.hpp index 9ec3cbeb6..d9905d8d1 100644 --- a/score/crypto/src/api/contexts/i_trust_store_management_context.hpp +++ b/score/crypto/src/api/contexts/i_trust_store_management_context.hpp @@ -98,7 +98,7 @@ class ITrustStoreManagementContext : public IContext /// @brief Enables a disabled trust store member identified by its slot resource. /// - /// Use the slot_id from ITrustStoreObject::MemberInfo to obtain the slot handle. + /// Use the slot_id from MemberInfo to obtain the slot handle. /// /// @param trust_store Handle to the trust store (type = kCertificateTrustStore) /// @param slot Handle to the member slot (type = kCertSlot) @@ -110,7 +110,7 @@ class ITrustStoreManagementContext : public IContext /// A disabled member is excluded from anchor resolution; it remains in the store /// and can be re-enabled. Use RemoveCertificateFromTrustStore to permanently remove. /// - /// Use the slot_id from ITrustStoreObject::MemberInfo to obtain the slot handle. + /// Use the slot_id from MemberInfo to obtain the slot handle. /// /// @param trust_store Handle to the trust store (type = kCertificateTrustStore) /// @param slot Handle to the member slot (type = kCertSlot) @@ -120,13 +120,13 @@ class ITrustStoreManagementContext : public IContext /// @brief Acknowledges an unexpected content change on a conditional-external member. /// /// A kConditionalExternal member is automatically disabled when its slot content - /// changes without acknowledgement (see ITrustStoreObject::MemberInfo state). This + /// changes without acknowledgement (see MemberInfo state). This /// re-baselines the accepted fingerprint to the slot's current content and /// re-enables the member. Not equivalent to EnableTrustStoreMember: enabling alone /// does not update the accepted fingerprint, so the member would be disabled again /// on the next anchor reload if the content is still unacknowledged. /// - /// Use the slot_id from ITrustStoreObject::MemberInfo to obtain the slot handle. + /// Use the slot_id from MemberInfo to obtain the slot handle. /// /// @param trust_store Handle to the trust store (type = kCertificateTrustStore) /// @param slot Handle to the conditional-external member slot (type = kCertSlot) @@ -139,7 +139,7 @@ class ITrustStoreManagementContext : public IContext /// For shared-static or conditional-external members, use /// `ICertificateManagementContext::ImportCrlToSlot` directly on the slot. /// - /// Use the slot_id from ITrustStoreObject::MemberInfo to obtain the slot handle. + /// Use the slot_id from MemberInfo to obtain the slot handle. /// /// @param trust_store Handle to the trust store (type = kCertificateTrustStore) /// @param slot Handle to the exclusive member slot (type = kCertSlot) diff --git a/score/crypto/src/api/objects/i_certificate_object.hpp b/score/crypto/src/api/objects/i_certificate_object.hpp index 772d6c6b8..4cf72d4ae 100644 --- a/score/crypto/src/api/objects/i_certificate_object.hpp +++ b/score/crypto/src/api/objects/i_certificate_object.hpp @@ -51,8 +51,6 @@ namespace crypto class ICertificateObject : public ICryptoObject { public: - static constexpr std::size_t kSha256FingerprintSize = 32U; - using Uptr = std::unique_ptr; ~ICertificateObject() override = default; diff --git a/score/crypto/src/api/objects/i_trust_store_object.hpp b/score/crypto/src/api/objects/i_trust_store_object.hpp index 9d5757af2..6c49e2ffa 100644 --- a/score/crypto/src/api/objects/i_trust_store_object.hpp +++ b/score/crypto/src/api/objects/i_trust_store_object.hpp @@ -15,14 +15,11 @@ #define SCORE_CRYPTO_SRC_API_OBJECTS_I_TRUST_STORE_OBJECT_HPP #include "score/crypto/src/api/objects/i_crypto_object.hpp" -#include "score/crypto/src/api/types/common.hpp" +#include "score/crypto/src/api/types/certificate.hpp" #include "score/span.hpp" -#include -#include #include #include -#include #include namespace score @@ -43,37 +40,8 @@ namespace crypto class ITrustStoreObject : public ICryptoObject { public: - static constexpr std::size_t kSha256FingerprintSize = 32U; - using Uptr = std::unique_ptr; - /// @brief Membership kind of a trust store anchor. - enum class MemberKind : uint8_t - { - kSharedStatic = 0U, ///< Externally managed slot; read-only from trust store perspective. - kExclusiveMutable = 1U, ///< Trust-store-owned exclusive slot; mutable via management context. - kConditionalExternal = 2U ///< External slot; disabled after unexpected content change. - }; - - /// @brief Snapshot of a single trust store member. - /// - /// Both slot_id and sha256_fingerprint are provided so callers can: - /// - Use slot_id directly for enable/disable/importCrl operations without a round-trip. - /// - Use sha256_fingerprint for quick identity matching against externally known fingerprints - /// (e.g., from a security bulletin) without loading the full certificate. - /// - Use subject + issuer + serial_number for human-readable identification and logging. - struct MemberInfo - { - CryptoResourceId slot_id{}; ///< kCertSlot resource — use for management ops. - std::array - sha256_fingerprint{}; ///< SHA-256 fingerprint of the member certificate. - std::string subject; ///< RFC 4514 Subject DN (e.g., "CN=Root CA,O=ACME,C=DE"). - std::string issuer; ///< RFC 4514 Issuer DN. - std::string serial_number; ///< Uppercase hex serial (e.g., "01ABCDEF"). - MemberKind kind{MemberKind::kSharedStatic}; ///< Membership type. - bool is_enabled{true}; ///< Whether anchor is active for chain building. - }; - ~ITrustStoreObject() override = default; ITrustStoreObject(const ITrustStoreObject&) = delete; @@ -87,72 +55,27 @@ class ITrustStoreObject : public ICryptoObject /// Empty slots (not yet populated) are omitted. virtual const std::vector& GetMembers() const noexcept = 0; - // ---- Non-virtual convenience accessors (operate on GetMembers() locally) ---- + // ---- Convenience accessors (defined in terms of GetMembers()) ---- /// @brief Find the member entry for a given slot resource ID. /// /// Matches by id and type only — primary_provider is not compared because /// MemberInfo slot_id has primary_provider=0 as a placeholder. /// @returns Pointer to the matching MemberInfo, or nullptr if not a member. - [[nodiscard]] const MemberInfo* FindMember(const CryptoResourceId& slot) const noexcept - { - for (const auto& member : GetMembers()) - { - if (member.slot_id.id == slot.id && member.slot_id.type == slot.type) - { - return &member; - } - } - return nullptr; - } + [[nodiscard]] virtual const MemberInfo* FindMember(const CryptoResourceId& slot) const noexcept = 0; /// @brief Find the member with the given SHA-256 fingerprint. /// /// @param fingerprint 32-byte fingerprint span. Returns nullptr if its size is not 32. /// @returns Pointer to the matching MemberInfo, or nullptr if not found. - [[nodiscard]] const MemberInfo* FindMemberByFingerprint(score::cpp::span fingerprint) const noexcept - { - if (fingerprint.size() != kSha256FingerprintSize) - { - return nullptr; - } - for (const auto& member : GetMembers()) - { - if (std::equal(fingerprint.begin(), fingerprint.end(), member.sha256_fingerprint.begin())) - { - return &member; - } - } - return nullptr; - } + [[nodiscard]] virtual const MemberInfo* FindMemberByFingerprint( + score::cpp::span fingerprint) const noexcept = 0; /// @brief Returns the slot IDs of all enabled trust store members. - [[nodiscard]] std::vector GetEnabledMemberSlotIds() const - { - std::vector result; - for (const auto& member : GetMembers()) - { - if (member.is_enabled) - { - result.push_back(member.slot_id); - } - } - return result; - } + [[nodiscard]] virtual std::vector GetEnabledMemberSlotIds() const = 0; /// @brief Returns the slot IDs of all disabled trust store members. - [[nodiscard]] std::vector GetDisabledMemberSlotIds() const - { - std::vector result; - for (const auto& member : GetMembers()) - { - if (!member.is_enabled) - { - result.push_back(member.slot_id); - } - } - return result; - } + [[nodiscard]] virtual std::vector GetDisabledMemberSlotIds() const = 0; protected: ITrustStoreObject() = default; diff --git a/score/crypto/src/api/src/crypto_context_impl.cpp b/score/crypto/src/api/src/crypto_context_impl.cpp index 6673db782..14f5aa67b 100644 --- a/score/crypto/src/api/src/crypto_context_impl.cpp +++ b/score/crypto/src/api/src/crypto_context_impl.cpp @@ -131,7 +131,7 @@ score::Result CryptoContextImpl::ResolveResource(const Resourc .forDataNodeId(m_connection->GetConnectionNodeId()) .operation(score::crypto::daemon::mediator::operations::ResolveResource()) .with_in_string(resource_id) - .with_in_val_uint8(static_cast(type)) + .with_in_val_uint64(static_cast(type)) .build(); if (!control_req_result.has_value()) diff --git a/score/crypto/src/api/types/certificate.hpp b/score/crypto/src/api/types/certificate.hpp index 519b83526..566624b63 100644 --- a/score/crypto/src/api/types/certificate.hpp +++ b/score/crypto/src/api/types/certificate.hpp @@ -19,10 +19,15 @@ #include #include #include +#include namespace score::crypto { +/// Byte length of a SHA-256 digest, shared by every certificate/CRL/trust-store +/// fingerprint field in this domain. +inline constexpr std::size_t kSha256FingerprintSize = 32U; + enum class CertificateSlotState : uint8_t { kEmpty, @@ -83,8 +88,8 @@ enum class VerificationEvidenceMode : uint8_t struct CrlMetadata { - std::array fingerprint{}; - std::array issuer_fingerprint{}; + std::array fingerprint{}; + std::array issuer_fingerprint{}; int64_t this_update{0}; int64_t next_update{0}; uint64_t crl_number{0U}; @@ -92,7 +97,7 @@ struct CrlMetadata struct CrlMetadataWireLayout final { - static constexpr std::size_t kFingerprintSize = 32U; + static constexpr std::size_t kFingerprintSize = kSha256FingerprintSize; static constexpr std::size_t kCrlFingerprintOffset = 0U; static constexpr std::size_t kIssuerFingerprintOffset = kCrlFingerprintOffset + kFingerprintSize; static constexpr std::size_t kThisUpdateOffset = kIssuerFingerprintOffset + kFingerprintSize; @@ -107,6 +112,33 @@ struct CertificateSlotInfo bool has_crl{false}; }; +/// Membership kind of a trust store anchor. +enum class MemberKind : uint8_t +{ + kSharedStatic = 0U, ///< Externally managed slot; read-only from trust store perspective. + kExclusiveMutable = 1U, ///< Trust-store-owned exclusive slot; mutable via management context. + kConditionalExternal = 2U ///< External slot; disabled after unexpected content change. +}; + +/// Snapshot of a single trust store member. +/// +/// Both slot_id and sha256_fingerprint are provided so callers can: +/// - Use slot_id directly for enable/disable/importCrl operations without a round-trip. +/// - Use sha256_fingerprint for quick identity matching against externally known fingerprints +/// (e.g., from a security bulletin) without loading the full certificate. +/// - Use subject + issuer + serial_number for human-readable identification and logging. +struct MemberInfo +{ + CryptoResourceId slot_id{}; ///< kCertSlot resource — use for management ops. + std::array + sha256_fingerprint{}; ///< SHA-256 fingerprint of the member certificate. + std::string subject; ///< RFC 4514 Subject DN (e.g., "CN=Root CA,O=ACME,C=DE"). + std::string issuer; ///< RFC 4514 Issuer DN. + std::string serial_number; ///< Uppercase hex serial (e.g., "01ABCDEF"). + MemberKind kind{MemberKind::kSharedStatic}; ///< Membership type. + bool is_enabled{true}; ///< Whether anchor is active for chain building. +}; + } // namespace score::crypto #endif // SCORE_CRYPTO_SRC_API_TYPES_CERTIFICATE_HPP From 233fef3d4de370aca8a11f51be960aa7483f3485 Mon Sep 17 00:00:00 2001 From: Athul Mallappallil Date: Tue, 25 Aug 2026 10:18:05 +0000 Subject: [PATCH 26/37] Cert management core and test --- score/crypto/src/daemon/cert_management/BUILD | 76 +++ .../cert_management_module.cpp | 82 +++ .../cert_management_module.hpp | 51 ++ .../cert_management/core/cert_entry.hpp | 107 ++++ .../core/cert_management_service.cpp | 237 +++++++ .../core/cert_management_service.hpp | 129 ++++ .../cert_management/core/cert_registry.cpp | 161 +++++ .../cert_management/core/cert_registry.hpp | 102 +++ .../detail/slot_info_builder.hpp | 35 ++ .../interfaces/access_policy.hpp | 42 ++ .../interfaces/cert_object.hpp | 149 +++++ .../interfaces/cert_slot_config.hpp | 123 ++++ .../cert_management/interfaces/cert_types.hpp | 234 +++++++ .../interfaces/i_cert_slot_catalog.hpp | 55 ++ .../interfaces/i_cert_slot_handler.cpp | 58 ++ .../interfaces/i_cert_slot_handler.hpp | 188 ++++++ .../interfaces/i_trust_store_handler.hpp | 132 ++++ .../interfaces/trust_store_config.hpp | 79 +++ .../cert_management/nodes/cert_data_node.hpp | 95 +++ .../nodes/cert_slot_data_node.hpp | 71 +++ .../nodes/trust_store_data_node.hpp | 66 ++ .../policy/access_policy_enforcer.cpp | 73 +++ .../policy/access_policy_enforcer.hpp | 55 ++ .../slot/config_driven_slot_catalog.cpp | 61 ++ .../slot/config_driven_slot_catalog.hpp | 46 ++ .../cert_management/slot/crl_handler.cpp | 135 ++++ .../cert_management/slot/crl_handler.hpp | 84 +++ .../slot/deployment_loader.cpp | 43 ++ .../slot/deployment_loader.hpp | 52 ++ .../slot/deployment_writer.cpp | 44 ++ .../slot/deployment_writer.hpp | 51 ++ .../slot/file_backed_slot_handler.cpp | 171 +++++ .../slot/file_backed_slot_handler.hpp | 64 ++ .../cert_management/slot/slot_registry.cpp | 148 +++++ .../cert_management/slot/slot_registry.hpp | 137 ++++ .../src/daemon/cert_management/tests/BUILD | 107 ++++ .../core/test_access_policy_enforcer.cpp | 69 +++ .../tests/core/test_cert_registry.cpp | 108 ++++ ...est_certificate_management_integration.cpp | 155 +++++ .../service/test_cert_management_service.cpp | 344 +++++++++++ .../slot/test_file_backed_slot_handler.cpp | 217 +++++++ .../truststore/test_trust_store_manager.cpp | 534 ++++++++++++++++ .../config_driven_trust_store_catalog.cpp | 62 ++ .../config_driven_trust_store_catalog.hpp | 54 ++ .../truststore/trust_store_handler.cpp | 101 +++ .../truststore/trust_store_handler.hpp | 67 ++ .../truststore/trust_store_manager.cpp | 584 ++++++++++++++++++ .../truststore/trust_store_manager.hpp | 264 ++++++++ .../crypto/src/daemon/common/daemon_error.hpp | 3 + score/crypto/src/daemon/config/inc/config.hpp | 147 ++++- .../src/daemon/data_manager/data_node.hpp | 3 + .../src/daemon/provider/cert_management/BUILD | 44 ++ .../cert_management_operations.hpp | 57 ++ .../provider/cert_management/cert_types.hpp | 98 +++ .../cert_management/i_cert_parser.hpp | 63 ++ .../crypto/src/daemon/provider/i_provider.hpp | 44 ++ .../provider/score_provider/openssl/BUILD | 28 + .../cert_management/openssl_cert_parser.cpp | 236 +++++++ .../cert_management/openssl_cert_parser.hpp | 60 ++ .../openssl/provider_openssl.cpp | 17 +- .../openssl/provider_openssl.hpp | 6 + .../provider/tests/cert_management/BUILD | 24 + .../test_openssl_cert_parser.cpp | 71 +++ score/tests/test_vectors/certificate/BUILD | 18 + .../tests/test_vectors/certificate/README.md | 23 + .../test_vectors/certificate/certificate.pem | 19 + .../certificate/certificate_manifest.json | 34 + .../certificate/certificate_slot.kv | 3 + .../certificate/certificate_updated.pem | 19 + .../certificate/certificate_updated_slot.kv | 3 + .../certificate/generate_certificates.py | 159 +++++ .../certificate/private/.gitignore | 2 + .../test_vectors/certificate/trust_store.kv | 1 + 73 files changed, 7280 insertions(+), 4 deletions(-) create mode 100644 score/crypto/src/daemon/cert_management/BUILD create mode 100644 score/crypto/src/daemon/cert_management/cert_management_module.cpp create mode 100644 score/crypto/src/daemon/cert_management/cert_management_module.hpp create mode 100644 score/crypto/src/daemon/cert_management/core/cert_entry.hpp create mode 100644 score/crypto/src/daemon/cert_management/core/cert_management_service.cpp create mode 100644 score/crypto/src/daemon/cert_management/core/cert_management_service.hpp create mode 100644 score/crypto/src/daemon/cert_management/core/cert_registry.cpp create mode 100644 score/crypto/src/daemon/cert_management/core/cert_registry.hpp create mode 100644 score/crypto/src/daemon/cert_management/detail/slot_info_builder.hpp create mode 100644 score/crypto/src/daemon/cert_management/interfaces/access_policy.hpp create mode 100644 score/crypto/src/daemon/cert_management/interfaces/cert_object.hpp create mode 100644 score/crypto/src/daemon/cert_management/interfaces/cert_slot_config.hpp create mode 100644 score/crypto/src/daemon/cert_management/interfaces/cert_types.hpp create mode 100644 score/crypto/src/daemon/cert_management/interfaces/i_cert_slot_catalog.hpp create mode 100644 score/crypto/src/daemon/cert_management/interfaces/i_cert_slot_handler.cpp create mode 100644 score/crypto/src/daemon/cert_management/interfaces/i_cert_slot_handler.hpp create mode 100644 score/crypto/src/daemon/cert_management/interfaces/i_trust_store_handler.hpp create mode 100644 score/crypto/src/daemon/cert_management/interfaces/trust_store_config.hpp create mode 100644 score/crypto/src/daemon/cert_management/nodes/cert_data_node.hpp create mode 100644 score/crypto/src/daemon/cert_management/nodes/cert_slot_data_node.hpp create mode 100644 score/crypto/src/daemon/cert_management/nodes/trust_store_data_node.hpp create mode 100644 score/crypto/src/daemon/cert_management/policy/access_policy_enforcer.cpp create mode 100644 score/crypto/src/daemon/cert_management/policy/access_policy_enforcer.hpp create mode 100644 score/crypto/src/daemon/cert_management/slot/config_driven_slot_catalog.cpp create mode 100644 score/crypto/src/daemon/cert_management/slot/config_driven_slot_catalog.hpp create mode 100644 score/crypto/src/daemon/cert_management/slot/crl_handler.cpp create mode 100644 score/crypto/src/daemon/cert_management/slot/crl_handler.hpp create mode 100644 score/crypto/src/daemon/cert_management/slot/deployment_loader.cpp create mode 100644 score/crypto/src/daemon/cert_management/slot/deployment_loader.hpp create mode 100644 score/crypto/src/daemon/cert_management/slot/deployment_writer.cpp create mode 100644 score/crypto/src/daemon/cert_management/slot/deployment_writer.hpp create mode 100644 score/crypto/src/daemon/cert_management/slot/file_backed_slot_handler.cpp create mode 100644 score/crypto/src/daemon/cert_management/slot/file_backed_slot_handler.hpp create mode 100644 score/crypto/src/daemon/cert_management/slot/slot_registry.cpp create mode 100644 score/crypto/src/daemon/cert_management/slot/slot_registry.hpp create mode 100644 score/crypto/src/daemon/cert_management/tests/BUILD create mode 100644 score/crypto/src/daemon/cert_management/tests/core/test_access_policy_enforcer.cpp create mode 100644 score/crypto/src/daemon/cert_management/tests/core/test_cert_registry.cpp create mode 100644 score/crypto/src/daemon/cert_management/tests/integration/test_certificate_management_integration.cpp create mode 100644 score/crypto/src/daemon/cert_management/tests/service/test_cert_management_service.cpp create mode 100644 score/crypto/src/daemon/cert_management/tests/slot/test_file_backed_slot_handler.cpp create mode 100644 score/crypto/src/daemon/cert_management/tests/truststore/test_trust_store_manager.cpp create mode 100644 score/crypto/src/daemon/cert_management/truststore/config_driven_trust_store_catalog.cpp create mode 100644 score/crypto/src/daemon/cert_management/truststore/config_driven_trust_store_catalog.hpp create mode 100644 score/crypto/src/daemon/cert_management/truststore/trust_store_handler.cpp create mode 100644 score/crypto/src/daemon/cert_management/truststore/trust_store_handler.hpp create mode 100644 score/crypto/src/daemon/cert_management/truststore/trust_store_manager.cpp create mode 100644 score/crypto/src/daemon/cert_management/truststore/trust_store_manager.hpp create mode 100644 score/crypto/src/daemon/provider/cert_management/BUILD create mode 100644 score/crypto/src/daemon/provider/cert_management/cert_management_operations.hpp create mode 100644 score/crypto/src/daemon/provider/cert_management/cert_types.hpp create mode 100644 score/crypto/src/daemon/provider/cert_management/i_cert_parser.hpp create mode 100644 score/crypto/src/daemon/provider/score_provider/openssl/cert_management/openssl_cert_parser.cpp create mode 100644 score/crypto/src/daemon/provider/score_provider/openssl/cert_management/openssl_cert_parser.hpp create mode 100644 score/crypto/src/daemon/provider/tests/cert_management/BUILD create mode 100644 score/crypto/src/daemon/provider/tests/cert_management/test_openssl_cert_parser.cpp create mode 100644 score/tests/test_vectors/certificate/BUILD create mode 100644 score/tests/test_vectors/certificate/README.md create mode 100644 score/tests/test_vectors/certificate/certificate.pem create mode 100644 score/tests/test_vectors/certificate/certificate_manifest.json create mode 100644 score/tests/test_vectors/certificate/certificate_slot.kv create mode 100644 score/tests/test_vectors/certificate/certificate_updated.pem create mode 100644 score/tests/test_vectors/certificate/certificate_updated_slot.kv create mode 100644 score/tests/test_vectors/certificate/generate_certificates.py create mode 100644 score/tests/test_vectors/certificate/private/.gitignore create mode 100644 score/tests/test_vectors/certificate/trust_store.kv diff --git a/score/crypto/src/daemon/cert_management/BUILD b/score/crypto/src/daemon/cert_management/BUILD new file mode 100644 index 000000000..f6e6540ae --- /dev/null +++ b/score/crypto/src/daemon/cert_management/BUILD @@ -0,0 +1,76 @@ +# ******************************************************************************* +# 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 +# ******************************************************************************* +# ******************************************************************************* +# Copyright (c) 2026 Contributors to the Eclipse Foundation +# SPDX-License-Identifier: Apache-2.0 +# ******************************************************************************* +load("@rules_cc//cc:cc_library.bzl", "cc_library") + +cc_library( + name = "cert_management_headers", + hdrs = glob([ + "interfaces/*.hpp", + "core/*.hpp", + "nodes/*.hpp", + "policy/*.hpp", + "slot/*.hpp", + "truststore/*.hpp", + "detail/*.hpp", + "cert_management_module.hpp", + ]), + includes = ["."], + visibility = ["//:__subpackages__"], + deps = [ + "//score/crypto/src/api/common:crypto_common", + "//score/crypto/src/api/config:context_configs", + "//score/crypto/src/common:common_types", + "//score/crypto/src/daemon/common", + "//score/crypto/src/daemon/common/storage:deployment_iface", + "//score/crypto/src/daemon/data_manager", + "//score/crypto/src/daemon/key_management:key_handler_iface", + "//score/crypto/src/daemon/provider:provider_headers", + "//score/crypto/src/daemon/provider/cert_management:cert_parser_headers", + ], +) + +cc_library( + name = "cert_management", + srcs = [ + "cert_management_module.cpp", + "core/cert_management_service.cpp", + "core/cert_registry.cpp", + "interfaces/i_cert_slot_handler.cpp", + "policy/access_policy_enforcer.cpp", + "slot/config_driven_slot_catalog.cpp", + "truststore/config_driven_trust_store_catalog.cpp", + "slot/crl_handler.cpp", + "slot/deployment_loader.cpp", + "slot/deployment_writer.cpp", + "slot/file_backed_slot_handler.cpp", + "slot/slot_registry.cpp", + "truststore/trust_store_handler.cpp", + "truststore/trust_store_manager.cpp", + ], + includes = ["."], + linkstatic = True, + visibility = ["//:__subpackages__"], + deps = [ + ":cert_management_headers", + "//score/crypto/src/daemon/common/storage:file_io", + "//score/crypto/src/daemon/common/storage:kv_deployment", + "//score/crypto/src/daemon/config", + "//score/crypto/src/daemon/provider:provider_manager", + "//score/crypto/src/daemon/provider/cert_management:cert_management_provider_headers", + "@score_baselibs//score/mw/log", + ], +) diff --git a/score/crypto/src/daemon/cert_management/cert_management_module.cpp b/score/crypto/src/daemon/cert_management/cert_management_module.cpp new file mode 100644 index 000000000..2c2f39c5b --- /dev/null +++ b/score/crypto/src/daemon/cert_management/cert_management_module.cpp @@ -0,0 +1,82 @@ +/******************************************************************************** + * 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/cert_management/cert_management_module.hpp" + +#include "score/crypto/src/daemon/cert_management/slot/config_driven_slot_catalog.hpp" +#include "score/crypto/src/daemon/cert_management/slot/file_backed_slot_handler.hpp" +#include "score/crypto/src/daemon/cert_management/truststore/config_driven_trust_store_catalog.hpp" +#include "score/mw/log/logging.h" + +namespace score::crypto::daemon::cert_management +{ +CertManagementModule::Sptr CertManagementModule::Create(data_manager::IDataManager::Sptr data_manager, + provider::ProviderManager::Sptr provider_manager, + const config::CertificateConfig& config) +{ + auto module = Sptr(new CertManagementModule()); + module->m_provider_manager = std::move(provider_manager); + module->m_slots = std::make_shared(); + ConfigDrivenSlotCatalog catalog{config}; + catalog.Load(*module->m_slots); + module->m_trust_stores = std::make_shared(); + ConfigDrivenTrustStoreCatalog trust_catalog{config}; + // Resolve the cert parser once at startup; injected into every FileBackedSlotHandler. + // Contract: a provider that advertises kCertManagement must implement GetCertParser(). + // Failure to do so is a misconfiguration — log it loudly so it is visible at startup + // rather than silently postponed until the first slot load attempt. + provider::cert_management::ICertParser::Sptr cert_parser; + if (module->m_provider_manager) + { + auto cert_prov = + module->m_provider_manager->GetProviderForCapability(common::ProviderCapability::kCertManagement); + if (cert_prov) + { + cert_parser = cert_prov->GetCertParser(); + if (!cert_parser) + score::mw::log::LogError() << "[CertMgmt] Provider '" << cert_prov->GetProviderName() + << "' advertises kCertManagement but GetCertParser() returned null." + << " Providers claiming kCertManagement must implement GetCertParser()." + << " File-backed slot loads will fail until this is resolved."; + } + else + { + score::mw::log::LogWarn() << "[CertMgmt] No provider with kCertManagement capability registered." + << " File-backed certificate slot loads will fail."; + } + } + + CertSlotHandlerFactory slot_handler_factory = [provider_manager = module->m_provider_manager, + cert_parser](const CertSlotConfig& slot) -> ICertSlotHandler::Sptr { + if (slot.storage_backend == "DEFAULT") + return std::make_shared(cert_parser); + + if (!provider_manager) + return nullptr; + + auto provider = provider_manager->GetProvider(slot.storage_backend); + if (!provider) + return nullptr; + return provider->GetCertSlotHandler(slot, cert_parser); + }; + trust_catalog.Load(*module->m_trust_stores, module->m_slots, slot_handler_factory); + module->m_service = std::make_shared( + std::move(data_manager), module->m_slots, module->m_trust_stores, std::move(slot_handler_factory)); + if (module->m_provider_manager) + { + module->m_provider_manager->ForEachProvider([&](const auto& /*id*/, const auto& provider) { + provider->SetCertManagementService(module->m_service); + }); + } + return module; +} +} // namespace score::crypto::daemon::cert_management diff --git a/score/crypto/src/daemon/cert_management/cert_management_module.hpp b/score/crypto/src/daemon/cert_management/cert_management_module.hpp new file mode 100644 index 000000000..f461a4df6 --- /dev/null +++ b/score/crypto/src/daemon/cert_management/cert_management_module.hpp @@ -0,0 +1,51 @@ +/******************************************************************************** + * 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 + ********************************************************************************/ +#ifndef SCORE_CRYPTO_SRC_DAEMON_CERT_MANAGEMENT_CERT_MANAGEMENT_MODULE_HPP +#define SCORE_CRYPTO_SRC_DAEMON_CERT_MANAGEMENT_CERT_MANAGEMENT_MODULE_HPP + +#include "score/crypto/src/daemon/cert_management/core/cert_management_service.hpp" +#include "score/crypto/src/daemon/cert_management/slot/slot_registry.hpp" +#include "score/crypto/src/daemon/config/inc/config.hpp" +#include "score/crypto/src/daemon/provider/provider_manager.hpp" + +namespace score::crypto::daemon::cert_management +{ +class CertManagementModule final +{ + public: + using Sptr = std::shared_ptr; + static Sptr Create(data_manager::IDataManager::Sptr, + provider::ProviderManager::Sptr, + const config::CertificateConfig&); + CertSlotRegistry::Sptr GetSlotRegistry() const + { + return m_slots; + } + CertManagementService::Sptr GetService() const + { + return m_service; + } + provider::ProviderManager::Sptr GetProviderManager() const + { + return m_provider_manager; + } + + private: + CertManagementModule() = default; + CertSlotRegistry::Sptr m_slots; + TrustStoreManager::Sptr m_trust_stores; + CertManagementService::Sptr m_service; + provider::ProviderManager::Sptr m_provider_manager; +}; +} // namespace score::crypto::daemon::cert_management +#endif diff --git a/score/crypto/src/daemon/cert_management/core/cert_entry.hpp b/score/crypto/src/daemon/cert_management/core/cert_entry.hpp new file mode 100644 index 000000000..a9f032913 --- /dev/null +++ b/score/crypto/src/daemon/cert_management/core/cert_entry.hpp @@ -0,0 +1,107 @@ +/******************************************************************************** + * 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 + ********************************************************************************/ + +#ifndef SCORE_CRYPTO_SRC_DAEMON_CERT_MANAGEMENT_CORE_CERT_ENTRY_HPP +#define SCORE_CRYPTO_SRC_DAEMON_CERT_MANAGEMENT_CORE_CERT_ENTRY_HPP + +#include "score/crypto/src/daemon/cert_management/interfaces/cert_object.hpp" +#include "score/crypto/src/daemon/cert_management/interfaces/cert_types.hpp" +#include "score/crypto/src/daemon/cert_management/slot/slot_registry.hpp" +#include "score/crypto/src/daemon/data_manager/data_node.hpp" + +#include +#include +#include +#include +#include +#include + +namespace score::crypto::daemon::cert_management +{ + +/// Registry entry for a single live certificate (ephemeral or slot-loaded). +/// +/// Holds shared ownership of a provider-neutral CertObject. The bytes and +/// metadata are freed automatically when the last shared_ptr is destroyed. +/// +/// Client-visible references are CertDataNode instances in the DataManager +/// client tree. +class CertEntry final : public std::enable_shared_from_this +{ + public: + /// @param object Owning cert object (must not be nullptr). + /// @param slot_handle Non-default only when cert was loaded from a slot. + CertEntry(CertObject::Sptr object, CertSlotHandle slot_handle = CertSlotHandle{}) + : m_object{std::move(object)}, m_slot_handle{slot_handle} + { + } + + ~CertEntry() = default; + + CertEntry(const CertEntry&) = delete; + CertEntry& operator=(const CertEntry&) = delete; + CertEntry(CertEntry&&) = delete; + CertEntry& operator=(CertEntry&&) = delete; + + [[nodiscard]] CertObject::Sptr GetCertObject() const noexcept + { + return m_object; + } + + [[nodiscard]] CertSlotHandle GetSlotHandle() const noexcept + { + return m_slot_handle; + } + + // ----------------------------------------------------------------------- + // Reference counting + // ----------------------------------------------------------------------- + + void AddRef(data_manager::ClientId client_id) + { + const std::lock_guard lock(m_ref_mutex); + m_ref_count.fetch_add(1U, std::memory_order_relaxed); + m_referencing_clients.push_back(client_id); + } + + /// @return true when the reference count has reached zero. + bool Release(data_manager::ClientId client_id) + { + const std::lock_guard lock(m_ref_mutex); + auto it = std::find(m_referencing_clients.begin(), m_referencing_clients.end(), client_id); + if (it == m_referencing_clients.end()) + { + return false; + } + m_referencing_clients.erase(it); + const std::uint32_t prev = m_ref_count.fetch_sub(1U, std::memory_order_acq_rel); + return prev == 1U; + } + + [[nodiscard]] std::uint32_t GetRefCount() const noexcept + { + return m_ref_count.load(std::memory_order_acquire); + } + + private: + CertObject::Sptr m_object; + CertSlotHandle m_slot_handle; + + mutable std::mutex m_ref_mutex; + std::atomic m_ref_count{0U}; + std::vector m_referencing_clients; +}; + +} // namespace score::crypto::daemon::cert_management + +#endif // SCORE_CRYPTO_SRC_DAEMON_CERT_MANAGEMENT_CORE_CERT_ENTRY_HPP diff --git a/score/crypto/src/daemon/cert_management/core/cert_management_service.cpp b/score/crypto/src/daemon/cert_management/core/cert_management_service.cpp new file mode 100644 index 000000000..c233b9b63 --- /dev/null +++ b/score/crypto/src/daemon/cert_management/core/cert_management_service.cpp @@ -0,0 +1,237 @@ +/******************************************************************************** + * 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/cert_management/core/cert_management_service.hpp" +#include "score/crypto/src/daemon/cert_management/nodes/cert_slot_data_node.hpp" +#include "score/crypto/src/daemon/cert_management/nodes/trust_store_data_node.hpp" +#include "score/crypto/src/daemon/data_manager/data_node_accessor.hpp" + +namespace score::crypto::daemon::cert_management +{ +using Error = common::DaemonErrorCode; + +CertManagementService::CertManagementService(data_manager::IDataManager::Sptr data_manager, + CertSlotRegistry::Sptr slot_registry, + TrustStoreManager::Sptr trust_store_manager, + CertSlotHandlerFactory slot_handler_factory) + : m_data_manager{std::move(data_manager)}, + m_slot_registry{std::move(slot_registry)}, + m_trust_stores{std::move(trust_store_manager)}, + m_slot_handler_factory{std::move(slot_handler_factory)} +{ +} + +score::crypto::Expected CertManagementService::RegisterCertMaterial( + const CertRegistrationParams& params, + CertObject::Sptr object) +{ + if (!object || !m_data_manager) + return score::crypto::make_unexpected(Error::kInvalidArgument); + auto entry = std::make_shared(std::move(object), params.slot_handle); + auto id = params.slot_handle.IsValid() ? m_cert_registry.RegisterSlotCert(params.slot_handle, entry) + : m_cert_registry.RegisterEphemeralCert(entry); + if (id == 0U) + { + id = m_cert_registry.FindSlotRegistryId(params.slot_handle); + entry = m_cert_registry.FindById(id); + if (!entry) + return score::crypto::make_unexpected(Error::kInternalError); + } + auto captured = entry; + auto node = std::make_shared(entry, id, params.client_id, [this](CertRegistryId rid) { + m_cert_registry.Unregister(rid); + }); + auto node_id = m_data_manager->addChildNode(params.client_id, params.parent_id, node); + if (!node_id) + return score::crypto::make_unexpected(Error::kInternalError); + return CertDataNodeResult{*node_id, std::move(captured)}; +} + +score::crypto::Expected CertManagementService::LoadOrShare( + const CertRegistrationParams& params, + ICertSlotHandler& handler, + const CertSlotConfig& config) +{ + auto existing = m_cert_registry.FindBySlot(params.slot_handle); + if (existing) + { + const auto id = m_cert_registry.FindSlotRegistryId(params.slot_handle); + auto node = std::make_shared(existing, id, params.client_id, [this](CertRegistryId rid) { + m_cert_registry.Unregister(rid); + }); + auto node_id = m_data_manager->addChildNode(params.client_id, params.parent_id, node); + if (!node_id) + return score::crypto::make_unexpected(Error::kInternalError); + return CertDataNodeResult{*node_id, std::move(existing)}; + } + auto loaded = handler.LoadCertificate(config); + if (!loaded) + return score::crypto::make_unexpected(loaded.error()); + return RegisterCertMaterial(params, std::move(*loaded)); +} + +score::crypto::Expected CertManagementService::ReleaseCert(data_manager::ClientId client_id, + data_manager::DataNodeId node_id) +{ + if (!m_data_manager->deleteNode(client_id, node_id)) + return score::crypto::make_unexpected(Error::kInvalidResourceId); + return std::monostate{}; +} + +void CertManagementService::CleanupClient(data_manager::ClientId client_id) +{ + m_cert_registry.CleanupClient(client_id); + m_cert_slot_node_cache.erase(client_id); + m_trust_store_node_cache.erase(client_id); + if (m_trust_stores) + m_trust_stores->CleanupClient(client_id); +} + +// --------------------------------------------------------------------------- +// Resource resolution — mediator resource resolvers +// --------------------------------------------------------------------------- + +score::crypto::Expected CertManagementService::ResolveCertSlot( + const std::string& resource_name, + data_manager::ClientId client_id) +{ + if (!m_data_manager || !m_slot_registry) + return score::crypto::make_unexpected(Error::kInternalError); + + // Dedup: same client resolving the same slot twice returns the cached node id. + auto& client_cache = m_cert_slot_node_cache[client_id]; + const auto it = client_cache.find(resource_name); + if (it != client_cache.end()) + return it->second; + + auto handle_res = m_slot_registry->ResolveAppResource(resource_name, client_id); + if (!handle_res.has_value()) + return score::crypto::make_unexpected(handle_res.error()); + + auto node = std::make_shared(handle_res.value(), m_slot_registry); + auto node_id = m_data_manager->addNode(client_id, std::move(node)); + if (!node_id.has_value()) + return score::crypto::make_unexpected(Error::kInternalError); + + client_cache[resource_name] = node_id.value(); + return node_id.value(); +} + +score::crypto::Expected CertManagementService::ResolveTrustStore( + const std::string& resource_name, + data_manager::ClientId client_id) +{ + if (!m_data_manager || !m_trust_stores) + return score::crypto::make_unexpected(Error::kInternalError); + + // Dedup: same client resolving the same trust store twice returns the cached node id. + auto& client_cache = m_trust_store_node_cache[client_id]; + const auto it = client_cache.find(resource_name); + if (it != client_cache.end()) + return it->second; + + auto handle_res = m_trust_stores->ResolveAppResource(resource_name, client_id); + if (!handle_res.has_value()) + return score::crypto::make_unexpected(handle_res.error()); + + auto node = std::make_shared(handle_res.value(), m_trust_stores); + auto node_id = m_data_manager->addNode(client_id, std::move(node)); + if (!node_id.has_value()) + return score::crypto::make_unexpected(Error::kInternalError); + + client_cache[resource_name] = node_id.value(); + return node_id.value(); +} + +// --------------------------------------------------------------------------- +// Operation helpers — executor call sites +// --------------------------------------------------------------------------- + +score::crypto::Expected CertManagementService::ResolveSlotForOperation( + data_manager::ClientId client_id, + data_manager::DataNodeId slot_node_id) +{ + if (!m_data_manager) + return score::crypto::make_unexpected(Error::kInternalError); + + auto acc_res = m_data_manager->getNodeAccessor(client_id, slot_node_id); + if (!acc_res.has_value()) + return score::crypto::make_unexpected(Error::kInvalidArgument); + + auto typed_res = std::move(acc_res).value().downCast(); + if (!typed_res.has_value()) + return score::crypto::make_unexpected(Error::kInvalidArgument); + + auto& slot_node = *typed_res.value(); + const auto handle = slot_node.GetSlotHandle(); + auto config_res = m_slot_registry->GetConfig(handle); + if (!config_res.has_value()) + return score::crypto::make_unexpected(config_res.error()); + + if (!m_slot_handler_factory) + return score::crypto::make_unexpected(Error::kInternalError); + auto handler = m_slot_handler_factory(*config_res.value()); + if (!handler) + return score::crypto::make_unexpected(Error::kInternalError); + + return ResolvedCertSlot{handle, config_res.value(), std::move(handler)}; +} + +score::crypto::Expected CertManagementService::ResolveCertForOperation( + data_manager::ClientId client_id, + data_manager::DataNodeId cert_node_id) +{ + if (!m_data_manager) + return score::crypto::make_unexpected(Error::kInternalError); + + auto acc_res = m_data_manager->getNodeAccessor(client_id, cert_node_id); + if (!acc_res.has_value()) + return score::crypto::make_unexpected(Error::kInvalidArgument); + + auto typed_res = std::move(acc_res).value().downCast(); + if (!typed_res.has_value()) + return score::crypto::make_unexpected(Error::kInvalidArgument); + + auto entry = typed_res.value()->GetCertEntry(); + if (!entry) + return score::crypto::make_unexpected(Error::kInternalError); + return entry->GetCertObject(); +} + +score::crypto::Expected CertManagementService::ResolveTrustStoreForOperation( + data_manager::ClientId client_id, + data_manager::DataNodeId ts_node_id) +{ + if (!m_data_manager) + return score::crypto::make_unexpected(Error::kInternalError); + + auto acc_res = m_data_manager->getNodeAccessor(client_id, ts_node_id); + if (!acc_res.has_value()) + return score::crypto::make_unexpected(Error::kInvalidArgument); + + auto typed_res = std::move(acc_res).value().downCast(); + if (!typed_res.has_value()) + return score::crypto::make_unexpected(Error::kInvalidArgument); + + return typed_res.value()->GetTrustStoreHandle(); +} + +void CertManagementService::NotifySlotCertChanged(CertSlotHandle slot_handle) +{ + if (!m_trust_stores) + return; + const auto memberships = m_trust_stores->GetMembershipsForSlot(slot_handle); + for (const auto ts_id : memberships) + m_trust_stores->NotifySlotChanged(ts_id, slot_handle); +} + +} // namespace score::crypto::daemon::cert_management diff --git a/score/crypto/src/daemon/cert_management/core/cert_management_service.hpp b/score/crypto/src/daemon/cert_management/core/cert_management_service.hpp new file mode 100644 index 000000000..ea471ab39 --- /dev/null +++ b/score/crypto/src/daemon/cert_management/core/cert_management_service.hpp @@ -0,0 +1,129 @@ +/******************************************************************************** + * 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 + ********************************************************************************/ +#ifndef SCORE_CRYPTO_SRC_DAEMON_CERT_MANAGEMENT_CORE_CERT_MANAGEMENT_SERVICE_HPP +#define SCORE_CRYPTO_SRC_DAEMON_CERT_MANAGEMENT_CORE_CERT_MANAGEMENT_SERVICE_HPP + +#include "score/crypto/src/daemon/cert_management/core/cert_registry.hpp" +#include "score/crypto/src/daemon/cert_management/interfaces/i_cert_slot_handler.hpp" +#include "score/crypto/src/daemon/cert_management/nodes/cert_data_node.hpp" +#include "score/crypto/src/daemon/cert_management/truststore/trust_store_manager.hpp" +#include "score/crypto/src/daemon/data_manager/i_data_manager.hpp" + +#include +#include + +namespace score::crypto::daemon::cert_management +{ +struct CertDataNodeResult +{ + data_manager::DataNodeId node_id{}; + std::shared_ptr entry; +}; + +/// Resolved certificate slot — handler + config + handle returned by ResolveSlotForOperation. +struct ResolvedCertSlot +{ + CertSlotHandle handle; + const CertSlotConfig* config{nullptr}; + ICertSlotHandler::Sptr handler; +}; + +class CertManagementService final +{ + public: + using Sptr = std::shared_ptr; + + CertManagementService(data_manager::IDataManager::Sptr data_manager, + CertSlotRegistry::Sptr slot_registry, + TrustStoreManager::Sptr trust_store_manager, + CertSlotHandlerFactory slot_handler_factory = {}); + + // ----------------------------------------------------------------------- + // Existing cert lifecycle methods + // ----------------------------------------------------------------------- + + score::crypto::Expected RegisterCertMaterial( + const CertRegistrationParams&, + CertObject::Sptr); + score::crypto::Expected LoadOrShare(const CertRegistrationParams&, + ICertSlotHandler&, + const CertSlotConfig&); + score::crypto::Expected ReleaseCert(data_manager::ClientId, + data_manager::DataNodeId); + void CleanupClient(data_manager::ClientId); + + TrustStoreManager::Sptr GetTrustStoreManager() const + { + return m_trust_stores; + } + + // ----------------------------------------------------------------------- + // Resource resolution — called by mediator resource resolvers + // ----------------------------------------------------------------------- + + /// Resolve an application cert slot resource name to a client-scoped DataNodeId. + /// + /// Creates a CertSlotDataNode under the client root in the data manager. + /// The node survives across context opens, matching key slot lifecycle. + score::crypto::Expected ResolveCertSlot( + const std::string& resource_name, + data_manager::ClientId client_id); + + /// Resolve an application trust store resource name to a client-scoped DataNodeId. + /// + /// Creates a TrustStoreDataNode under the client root in the data manager. + score::crypto::Expected ResolveTrustStore( + const std::string& resource_name, + data_manager::ClientId client_id); + + // ----------------------------------------------------------------------- + // Operation helpers — called by the cert management executor + // ----------------------------------------------------------------------- + + /// Look up a CertSlotDataNode by node_id and return its slot config + handler. + score::crypto::Expected ResolveSlotForOperation( + data_manager::ClientId client_id, + data_manager::DataNodeId slot_node_id); + + /// Look up a CertDataNode by node_id and return the underlying CertObject. + score::crypto::Expected ResolveCertForOperation( + data_manager::ClientId client_id, + data_manager::DataNodeId cert_node_id); + + /// Look up a TrustStoreDataNode by node_id and return the trust store handle. + score::crypto::Expected ResolveTrustStoreForOperation( + data_manager::ClientId client_id, + data_manager::DataNodeId ts_node_id); + + /// Fan out NotifyUpdate() to all trust stores referencing the given cert slot. + /// + /// Called after a successful StoreCertificate to keep trust store anchors current. + void NotifySlotCertChanged(CertSlotHandle slot_handle); + + private: + data_manager::IDataManager::Sptr m_data_manager; + CertSlotRegistry::Sptr m_slot_registry; + TrustStoreManager::Sptr m_trust_stores; + CertRegistry m_cert_registry; + CertSlotHandlerFactory m_slot_handler_factory; + + // Node-id dedup caches — keyed by (client_id, resource_name). + // Mirrors the key-slot dedup pattern so that resolving the same resource + // twice from the same client reuses the existing DataNodeId. + std::unordered_map> + m_cert_slot_node_cache; + std::unordered_map> + m_trust_store_node_cache; +}; +} // namespace score::crypto::daemon::cert_management +#endif diff --git a/score/crypto/src/daemon/cert_management/core/cert_registry.cpp b/score/crypto/src/daemon/cert_management/core/cert_registry.cpp new file mode 100644 index 000000000..bf184d89c --- /dev/null +++ b/score/crypto/src/daemon/cert_management/core/cert_registry.cpp @@ -0,0 +1,161 @@ +/******************************************************************************** + * 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/cert_management/core/cert_registry.hpp" +#include "score/crypto/src/daemon/cert_management/core/cert_entry.hpp" + +#include "score/mw/log/logging.h" + +#include + +namespace score::crypto::daemon::cert_management +{ +namespace +{ +constexpr std::string_view kLogPrefix = "[CERT_REGISTRY] "; +} // namespace + +CertRegistryId CertRegistry::RegisterSlotCert(CertSlotHandle slot_handle, std::shared_ptr cert_entry) +{ + const std::lock_guard lock(m_mutex); + + if (m_slot_to_id.count(slot_handle.index) != 0U) + { + score::mw::log::LogError() << kLogPrefix << "RegisterSlotCert: slot " << slot_handle.index + << " already registered"; + return 0U; + } + + const CertRegistryId id = m_next_id++; + m_certs.emplace(id, std::move(cert_entry)); + m_slot_to_id.emplace(slot_handle.index, id); + + return id; +} + +CertRegistryId CertRegistry::RegisterEphemeralCert(std::shared_ptr cert_entry) +{ + const std::lock_guard lock(m_mutex); + + const CertRegistryId id = m_next_id++; + m_certs.emplace(id, std::move(cert_entry)); + + return id; +} + +std::shared_ptr CertRegistry::FindBySlot(CertSlotHandle slot_handle) const +{ + const std::lock_guard lock(m_mutex); + + const auto slot_it = m_slot_to_id.find(slot_handle.index); + if (slot_it == m_slot_to_id.end()) + { + return nullptr; + } + + const auto cert_it = m_certs.find(slot_it->second); + if (cert_it == m_certs.end()) + { + return nullptr; + } + + return cert_it->second; +} + +CertRegistryId CertRegistry::FindSlotRegistryId(CertSlotHandle slot_handle) const +{ + const std::lock_guard lock(m_mutex); + + const auto slot_it = m_slot_to_id.find(slot_handle.index); + if (slot_it == m_slot_to_id.end()) + { + return 0U; + } + + return slot_it->second; +} + +std::shared_ptr CertRegistry::FindById(CertRegistryId id) const +{ + const std::lock_guard lock(m_mutex); + + const auto it = m_certs.find(id); + if (it == m_certs.end()) + { + return nullptr; + } + + return it->second; +} + +bool CertRegistry::Unregister(CertRegistryId id) +{ + const std::lock_guard lock(m_mutex); + + const auto it = m_certs.find(id); + if (it == m_certs.end()) + { + return false; + } + + for (auto slot_it = m_slot_to_id.begin(); slot_it != m_slot_to_id.end(); ++slot_it) + { + if (slot_it->second == id) + { + m_slot_to_id.erase(slot_it); + break; + } + } + + m_certs.erase(it); + return true; +} + +void CertRegistry::CleanupClient(data_manager::ClientId client_id) +{ + const std::lock_guard lock(m_mutex); + + std::vector to_remove; + + for (auto& [id, cert_entry] : m_certs) + { + if (cert_entry->Release(client_id)) + { + to_remove.push_back(id); + } + } + + for (const auto id : to_remove) + { + for (auto slot_it = m_slot_to_id.begin(); slot_it != m_slot_to_id.end(); ++slot_it) + { + if (slot_it->second == id) + { + m_slot_to_id.erase(slot_it); + break; + } + } + + score::mw::log::LogDebug() << kLogPrefix << "CleanupClient: removing cert " << id + << " (ref_count reached 0 after client " << client_id << " cleanup)"; + m_certs.erase(id); + } +} + +std::size_t CertRegistry::Size() const +{ + const std::lock_guard lock(m_mutex); + return m_certs.size(); +} + +} // namespace score::crypto::daemon::cert_management diff --git a/score/crypto/src/daemon/cert_management/core/cert_registry.hpp b/score/crypto/src/daemon/cert_management/core/cert_registry.hpp new file mode 100644 index 000000000..a42037e55 --- /dev/null +++ b/score/crypto/src/daemon/cert_management/core/cert_registry.hpp @@ -0,0 +1,102 @@ +/******************************************************************************** + * 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 + ********************************************************************************/ + +#ifndef SCORE_CRYPTO_SRC_DAEMON_CERT_MANAGEMENT_CORE_CERT_REGISTRY_HPP +#define SCORE_CRYPTO_SRC_DAEMON_CERT_MANAGEMENT_CORE_CERT_REGISTRY_HPP + +#include "score/crypto/src/daemon/cert_management/interfaces/cert_types.hpp" +#include "score/crypto/src/daemon/cert_management/slot/slot_registry.hpp" +#include "score/crypto/src/daemon/data_manager/data_node.hpp" + +#include +#include +#include +#include +#include + +namespace score::crypto::daemon::cert_management +{ + +class CertEntry; + +/// Per-provider registry of live certificates. +/// +/// Holds shared ownership of every CertEntry produced by a single provider +/// (both slot-loaded and ephemeral). CertDataNode instances in the client +/// tree hold an additional shared_ptr to the same CertEntry, keeping it alive +/// as long as any client references it. +/// +/// Thread safety: all public methods serialise on an internal mutex. +class CertRegistry final +{ + public: + using Sptr = std::shared_ptr; + + CertRegistry() = default; + ~CertRegistry() = default; + + CertRegistry(const CertRegistry&) = delete; + CertRegistry& operator=(const CertRegistry&) = delete; + CertRegistry(CertRegistry&&) = delete; + CertRegistry& operator=(CertRegistry&&) = delete; + + // ------------------------------------------------------------------ + // Registration + // ------------------------------------------------------------------ + + /// Register a certificate that was loaded from a persistent slot. + /// + /// Returns 0 if the slot is already registered (caller should call + /// FindBySlot() first to check). + [[nodiscard]] CertRegistryId RegisterSlotCert(CertSlotHandle slot_handle, std::shared_ptr cert_entry); + + /// Register an ephemeral (non-slot) certificate. + [[nodiscard]] CertRegistryId RegisterEphemeralCert(std::shared_ptr cert_entry); + + // ------------------------------------------------------------------ + // Lookup + // ------------------------------------------------------------------ + + [[nodiscard]] std::shared_ptr FindBySlot(CertSlotHandle slot_handle) const; + [[nodiscard]] CertRegistryId FindSlotRegistryId(CertSlotHandle slot_handle) const; + [[nodiscard]] std::shared_ptr FindById(CertRegistryId id) const; + + // ------------------------------------------------------------------ + // Removal + // ------------------------------------------------------------------ + + /// @return true if the cert was found and removed. + bool Unregister(CertRegistryId id); + + // ------------------------------------------------------------------ + // Crash cleanup + // ------------------------------------------------------------------ + + void CleanupClient(data_manager::ClientId client_id); + + // ------------------------------------------------------------------ + // Query + // ------------------------------------------------------------------ + + [[nodiscard]] std::size_t Size() const; + + private: + mutable std::mutex m_mutex; + std::unordered_map> m_certs; + std::unordered_map m_slot_to_id; + CertRegistryId m_next_id{1U}; +}; + +} // namespace score::crypto::daemon::cert_management + +#endif // SCORE_CRYPTO_SRC_DAEMON_CERT_MANAGEMENT_CORE_CERT_REGISTRY_HPP diff --git a/score/crypto/src/daemon/cert_management/detail/slot_info_builder.hpp b/score/crypto/src/daemon/cert_management/detail/slot_info_builder.hpp new file mode 100644 index 000000000..1935b3e04 --- /dev/null +++ b/score/crypto/src/daemon/cert_management/detail/slot_info_builder.hpp @@ -0,0 +1,35 @@ +/******************************************************************************** + * 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 + ********************************************************************************/ + +#ifndef SCORE_CRYPTO_SRC_DAEMON_CERT_MANAGEMENT_DETAIL_SLOT_INFO_BUILDER_HPP +#define SCORE_CRYPTO_SRC_DAEMON_CERT_MANAGEMENT_DETAIL_SLOT_INFO_BUILDER_HPP + +#include "score/crypto/src/api/common/types.hpp" +#include "score/crypto/src/daemon/cert_management/interfaces/cert_slot_config.hpp" + +namespace score::crypto::daemon::cert_management::detail +{ + +inline score::crypto::CertificateSlotInfo BuildCertSlotInfo(const CertSlotConfig& /*slot*/, + score::crypto::CertificateSlotState state, + bool has_crl) noexcept +{ + score::crypto::CertificateSlotInfo info{}; + info.state = state; + info.has_crl = has_crl; + return info; +} + +} // namespace score::crypto::daemon::cert_management::detail + +#endif // SCORE_CRYPTO_SRC_DAEMON_CERT_MANAGEMENT_DETAIL_SLOT_INFO_BUILDER_HPP diff --git a/score/crypto/src/daemon/cert_management/interfaces/access_policy.hpp b/score/crypto/src/daemon/cert_management/interfaces/access_policy.hpp new file mode 100644 index 000000000..87b9137a3 --- /dev/null +++ b/score/crypto/src/daemon/cert_management/interfaces/access_policy.hpp @@ -0,0 +1,42 @@ +/******************************************************************************** + * 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 + ********************************************************************************/ + +#ifndef SCORE_CRYPTO_SRC_DAEMON_CERT_MANAGEMENT_INTERFACES_ACCESS_POLICY_HPP +#define SCORE_CRYPTO_SRC_DAEMON_CERT_MANAGEMENT_INTERFACES_ACCESS_POLICY_HPP + +#include +#include + +namespace score::crypto::daemon::cert_management +{ + +/// UID-based read/write access control policy for a certificate slot or trust store. +struct AccessPolicy +{ + /// UIDs permitted to read from this slot (LoadCertificate, GetSlotInfo, + /// CERT_EXPORT, CERT_PUBLIC_KEY, CERT_VERIFY). + /// + /// An empty list means no UID-based restriction (all clients may read). + std::vector allowed_uids; + + /// UIDs permitted to write to this slot (SaveCertificate, CRL_IMPORT, + /// CERT_CLEAR, TRUST_STORE_ADD_CERT, TRUST_STORE_REMOVE_CERT). + /// + /// A UID in allowed_uids but not in allowed_write_uids may read but not + /// write. An empty list means no UID-based write restriction. + std::vector allowed_write_uids; +}; + +} // namespace score::crypto::daemon::cert_management + +#endif // SCORE_CRYPTO_SRC_DAEMON_CERT_MANAGEMENT_INTERFACES_ACCESS_POLICY_HPP diff --git a/score/crypto/src/daemon/cert_management/interfaces/cert_object.hpp b/score/crypto/src/daemon/cert_management/interfaces/cert_object.hpp new file mode 100644 index 000000000..a1859cb80 --- /dev/null +++ b/score/crypto/src/daemon/cert_management/interfaces/cert_object.hpp @@ -0,0 +1,149 @@ +/******************************************************************************** + * 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 + ********************************************************************************/ + +#ifndef SCORE_CRYPTO_SRC_DAEMON_CERT_MANAGEMENT_INTERFACES_CERT_OBJECT_HPP +#define SCORE_CRYPTO_SRC_DAEMON_CERT_MANAGEMENT_INTERFACES_CERT_OBJECT_HPP + +#include "score/crypto/src/api/common/types.hpp" +#include "score/crypto/src/common/types.hpp" +#include "score/crypto/src/daemon/cert_management/interfaces/cert_types.hpp" + +#include +#include +#include +#include +#include + +namespace score::crypto::daemon::cert_management +{ + +/// Provider-neutral value type for a single parsed certificate. +/// +/// A certificate is public, fully serializable data — unlike a private key, it +/// has no opaque provider-internal secret that must stay bound to the provider +/// that created it. `CertObject` therefore replaces the previous polymorphic +/// `ICertHandler`: it is a plain, immutable value holding the raw DER/PEM bytes, +/// their format, and the metadata extracted at parse time (`CertChainMetadata`). +/// +/// Parsing remains a provider capability (`ICertParser`), but +/// its product is neutral: any provider — or none — can consume a CertObject, +/// which is what lets a certificate stored via one backend (e.g. PKCS#11) be +/// parsed/verified by the software provider. Operations that transform bytes +/// (format conversion, public-key extraction) live on the provider, not here. +/// +/// Shared via CertObject::Sptr because the same certificate is commonly +/// referenced by the CertRegistry and one or more trust stores at once. +/// +/// Thread safety: instances are immutable after construction and safe to read +/// concurrently through a shared_ptr. +class CertObject final +{ + public: + using Sptr = std::shared_ptr; + + CertObject(CertChainMetadata metadata, std::vector bytes, score::crypto::FormatType format) + : m_metadata{std::move(metadata)}, m_bytes{std::move(bytes)}, m_format{format} + { + } + + ~CertObject() = default; + + CertObject(const CertObject&) = delete; + CertObject& operator=(const CertObject&) = delete; + CertObject(CertObject&&) = delete; + CertObject& operator=(CertObject&&) = delete; + + // ----------------------------------------------------------------------- + // Metadata accessors (read from the cached CertChainMetadata) + // ----------------------------------------------------------------------- + + /// RFC 4514 canonical string representation of the Subject Distinguished Name. + [[nodiscard]] std::string_view GetSubject() const noexcept + { + return m_metadata.subject_canonical; + } + + /// RFC 4514 canonical string representation of the Issuer Distinguished Name. + [[nodiscard]] std::string_view GetIssuer() const noexcept + { + return m_metadata.issuer_canonical; + } + + /// Unix epoch seconds of the certificate's notBefore validity field. + [[nodiscard]] int64_t GetNotBefore() const noexcept + { + return m_metadata.not_before_epoch_s; + } + + /// Unix epoch seconds of the certificate's notAfter validity field. + [[nodiscard]] int64_t GetNotAfter() const noexcept + { + return m_metadata.not_after_epoch_s; + } + + /// Raw bytes of the Subject Key Identifier extension value (empty if absent). + [[nodiscard]] score::crypto::span GetSkid() const noexcept + { + return {m_metadata.skid.data(), m_metadata.skid.size()}; + } + + /// Raw bytes of the Authority Key Identifier extension value (empty if absent). + [[nodiscard]] score::crypto::span GetAkid() const noexcept + { + return {m_metadata.akid.data(), m_metadata.akid.size()}; + } + + /// True when the BasicConstraints extension marks this as a CA certificate. + [[nodiscard]] bool IsCA() const noexcept + { + return m_metadata.is_ca; + } + + /// SHA-256 fingerprint of the certificate's DER encoding (32 bytes). + [[nodiscard]] score::crypto::span GetFingerprint() const noexcept + { + return {m_metadata.fingerprint.data(), m_metadata.fingerprint.size()}; + } + + /// The full precomputed chain metadata struct. + [[nodiscard]] const CertChainMetadata& GetChainMetadata() const noexcept + { + return m_metadata; + } + + // ----------------------------------------------------------------------- + // Raw bytes access — the neutral payload for re-parse / verification + // ----------------------------------------------------------------------- + + /// Raw certificate bytes as parsed. Pass to any ICertParser to re-parse, + /// or to a cert context handler for verification — no per-provider binding required. + [[nodiscard]] score::crypto::span GetRawBytes() const noexcept + { + return {m_bytes.data(), m_bytes.size()}; + } + + /// Format of the bytes returned by GetRawBytes() (kPem or kDer). + [[nodiscard]] score::crypto::FormatType GetFormat() const noexcept + { + return m_format; + } + + private: + CertChainMetadata m_metadata; + std::vector m_bytes; + score::crypto::FormatType m_format; +}; + +} // namespace score::crypto::daemon::cert_management + +#endif // SCORE_CRYPTO_SRC_DAEMON_CERT_MANAGEMENT_INTERFACES_CERT_OBJECT_HPP diff --git a/score/crypto/src/daemon/cert_management/interfaces/cert_slot_config.hpp b/score/crypto/src/daemon/cert_management/interfaces/cert_slot_config.hpp new file mode 100644 index 000000000..5ceb14b62 --- /dev/null +++ b/score/crypto/src/daemon/cert_management/interfaces/cert_slot_config.hpp @@ -0,0 +1,123 @@ +/******************************************************************************** + * 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 + ********************************************************************************/ + +#ifndef SCORE_CRYPTO_SRC_DAEMON_CERT_MANAGEMENT_INTERFACES_CERT_SLOT_CONFIG_HPP +#define SCORE_CRYPTO_SRC_DAEMON_CERT_MANAGEMENT_INTERFACES_CERT_SLOT_CONFIG_HPP + +#include "score/crypto/src/daemon/cert_management/interfaces/access_policy.hpp" +#include "score/crypto/src/daemon/cert_management/interfaces/cert_types.hpp" + +#include +#include + +namespace score::crypto::daemon::cert_management +{ + +// --------------------------------------------------------------------------- +// IntegrityPolicy +// --------------------------------------------------------------------------- + +/// Integrity enforcement policy for a certificate slot. +/// +/// kDisabled: no check is performed; descriptor hash fields are ignored even if present. +/// kRequired: LoadCertificate fails if the KV descriptor [certificate] section has no +/// cert_hash entry, or if the stored hash does not match the file content. +/// This prevents a compromised descriptor from bypassing the check. +/// +/// The policy is a static slot property set at startup by the integrator. The actual +/// hash value and algorithm live in the KV descriptor [certificate] section and are +/// updated atomically by the writer whenever a certificate is stored. +enum class IntegrityPolicy : uint8_t +{ + kDisabled = 0, + kRequired = 1, +}; + +// --------------------------------------------------------------------------- +// CertSlotConfig +// --------------------------------------------------------------------------- + +/// Immutable configuration for a certificate slot. +/// +/// Owned centrally by the CertSlotRegistry. CertSlotDataNodes hold only a +/// CertSlotHandle referencing back to the central registry — they do NOT +/// copy this struct. +/// +/// All fields are immutable after registration. Dynamic certificate and CRL +/// content is stored in a deployment descriptor at deployment_path, using +/// the KV sections [metadata], [certificate], [certificate_metadata], [crl]. +/// +/// ### Storage backend +/// +/// `storage_backend` is a scalar string that identifies which ICertSlotHandler +/// subclass manages the physical storage for this slot. It is immutable after +/// startup — a slot is structurally bound to one backend type. Changing the +/// backend requires reconfiguration and re-provisioning. +/// +/// Built-in value: "DEFAULT" → FileBackedSlotHandler. +/// Any other value is treated as a provider name; the named provider must implement +/// ICertSlotHandler (e.g. Pkcs11CertSlotHandler for "pkcs11"). Backend-specific +/// parameters (file path, PKCS#11 token label, object handle) live in the KV +/// descriptor, not here. +/// +/// Cert operations (parse, chain verify, CSR) are provider-agnostic and route to +/// the global software provider by default — they do not depend on this field. +/// +/// ### Certificate format +/// +/// The on-disk certificate format (PEM or DER) is NOT stored here; it is recorded +/// as cert_format in the [certificate] KV descriptor section and updated atomically +/// by the writer on each StoreCertificate call. +struct CertSlotConfig +{ + /// Human-readable resource ID for this slot (e.g., "device/tls-cert"). + /// + /// Must be unique within the CertSlotRegistry. Used as the stable + /// identifier in trust-store membership entries and API resource paths. + std::string slot_name; + + /// Storage backend identifier. "DEFAULT" selects FileBackedSlotHandler; + /// any other value is resolved as a provider name by CertManagementModule. + /// The value is matched exactly (case-sensitive). + std::string storage_backend{"DEFAULT"}; + + /// UID-based access control for this slot. + AccessPolicy access_policy; + + /// Path to the deployment descriptor (file or folder) for this cert slot. + /// + /// The deployment descriptor uses the KV format with sections defined in + /// cert_section_names: [metadata], [certificate], [certificate_metadata], [crl]. + /// Must be an absolute path with no ".." traversal components. + std::string deployment_path; + + /// Format of the deployment descriptor: "kv" (default), "json", "bin". + std::string deployment_format{"kv"}; + + /// Integrity enforcement policy. See IntegrityPolicy enum doc. + IntegrityPolicy integrity_policy{IntegrityPolicy::kDisabled}; + + // ----------------------------------------------------------------------- + // Convenience accessor + // ----------------------------------------------------------------------- + + /// True when the CertSlotConfig is structurally valid (non-empty name and backend). + [[nodiscard]] bool IsValid() const noexcept + { + return !slot_name.empty() && !storage_backend.empty(); + } +}; + +} // namespace score::crypto::daemon::cert_management + +#endif // SCORE_CRYPTO_SRC_DAEMON_CERT_MANAGEMENT_INTERFACES_CERT_SLOT_CONFIG_HPP diff --git a/score/crypto/src/daemon/cert_management/interfaces/cert_types.hpp b/score/crypto/src/daemon/cert_management/interfaces/cert_types.hpp new file mode 100644 index 000000000..e679eee37 --- /dev/null +++ b/score/crypto/src/daemon/cert_management/interfaces/cert_types.hpp @@ -0,0 +1,234 @@ +/******************************************************************************** + * 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 + ********************************************************************************/ + +#ifndef SCORE_CRYPTO_SRC_DAEMON_CERT_MANAGEMENT_INTERFACES_CERT_TYPES_HPP +#define SCORE_CRYPTO_SRC_DAEMON_CERT_MANAGEMENT_INTERFACES_CERT_TYPES_HPP + +#include "score/crypto/src/api/common/types.hpp" +#include "score/crypto/src/common/types.hpp" +#include "score/crypto/src/daemon/common/types.hpp" + +#include +#include +#include +#include +#include +#include + +namespace score::crypto::daemon::cert_management +{ + +// --------------------------------------------------------------------------- +// Opaque registry and trust-store identifiers +// --------------------------------------------------------------------------- + +/// Monotonically increasing identifier for a certificate entry in the CertRegistry. +using CertRegistryId = uint64_t; + +/// Compact identifier for a named trust store. +using TrustStoreId = uint32_t; + +// --------------------------------------------------------------------------- +// Slot and store handles — thin wrappers around uint32_t indices +// --------------------------------------------------------------------------- + +/// Runtime reference to a certificate slot held by the SlotRegistry. +/// +/// The index maps to an entry in the SlotRegistry internal table. +/// Callers must not interpret or persist the raw index value across daemon +/// restarts; always resolve through CertManagementService. +struct CertSlotHandle +{ + uint32_t index{UINT32_MAX}; + + [[nodiscard]] bool IsValid() const noexcept + { + return index != UINT32_MAX; + } + + bool operator==(const CertSlotHandle& o) const noexcept + { + return index == o.index; + } + + bool operator!=(const CertSlotHandle& o) const noexcept + { + return !(*this == o); + } +}; + +/// Runtime reference to a named trust store held by TrustStoreManager. +struct TrustStoreHandle +{ + uint32_t index{UINT32_MAX}; + + [[nodiscard]] bool IsValid() const noexcept + { + return index != UINT32_MAX; + } + + bool operator==(const TrustStoreHandle& o) const noexcept + { + return index == o.index; + } + + bool operator!=(const TrustStoreHandle& o) const noexcept + { + return !(*this == o); + } +}; + +// --------------------------------------------------------------------------- +// Certificate chain metadata (precomputed at parse time) +// --------------------------------------------------------------------------- + +/// Extracted X.509 fields used for chain building and trust store indexing. +/// +/// Populated by a provider parser and cached in the CertObject. +/// All binary fields (skid, akid, fingerprint) are stored as raw bytes. +/// String fields use the RFC 4514 canonical representation. +struct CertChainMetadata +{ + /// RFC 4514 string representation of the Subject DN (e.g., "CN=...,O=...,C=..."). + std::string subject_canonical; + + /// RFC 4514 string representation of the Issuer DN. + std::string issuer_canonical; + + /// DER-encoded Subject Key Identifier extension value; empty if the extension + /// is absent from the certificate. + std::vector skid; + + /// DER-encoded Authority Key Identifier extension value; empty if absent. + std::vector akid; + + /// SHA-256 digest of the certificate's DER encoding (32 bytes). + std::vector fingerprint; + + /// Unix epoch seconds of the notBefore validity field. + int64_t not_before_epoch_s{0}; + + /// Unix epoch seconds of the notAfter validity field. + int64_t not_after_epoch_s{0}; + + /// True when the BasicConstraints extension marks this as a CA certificate. + bool is_ca{false}; +}; + +// --------------------------------------------------------------------------- +// Deployment descriptor section and key name constants +// --------------------------------------------------------------------------- + +/// KV descriptor section names used for certificate and CRL storage. +/// +/// The deployment descriptor maps section -> (key -> value) and follows the +/// same INI-style KV format used by key_management. These constants are the +/// canonical section names shared between FileBackedSlotHandler, the +/// deployment loader, and the DataNode serialization layer. +namespace cert_section_names +{ + +/// Slot-level lifecycle metadata (availability, provisioned_at, update_counter). +inline constexpr std::string_view kMetadata = "metadata"; + +/// Certificate file location and format for a cert slot. +inline constexpr std::string_view kCertificate = "certificate"; + +/// Cached parsed certificate metadata (subject, issuer, validity, extensions). +/// Written by the daemon on certificate save; read by catalogs at reload. +inline constexpr std::string_view kCertificateMetadata = "certificate_metadata"; + +/// CRL file location, format, and nextUpdate for the co-located CRL. +/// Section is absent when no CRL has been stored for the slot. +inline constexpr std::string_view kCrl = "crl"; + +/// Runtime enable/accept state for trust store members. +/// Written by TrustStoreManager on every mutation; absent until the first mutation. +inline constexpr std::string_view kTrustStoreState = "trust_store_state"; + +} // namespace cert_section_names + +/// KV descriptor key names within the cert_section_names sections. +namespace cert_deployment_keys +{ + +// ---- [certificate] section ----------------------------------------------- + +/// PKCS#11 token object label used by a token-backed certificate slot. +inline constexpr std::string_view kPkcs11Label = "pkcs11.label"; + +/// Hex-encoded PKCS#11 CKA_ID used by a token-backed certificate slot. +inline constexpr std::string_view kPkcs11ObjectId = "pkcs11.object_id"; + +/// Absolute path to the PEM or DER certificate file. +inline constexpr std::string_view kCertPath = "cert_path"; + +/// Encoding of the certificate file: "pem" or "der". +inline constexpr std::string_view kCertFormat = "cert_format"; + +// ---- [crl] section ------------------------------------------------------- + +/// Absolute path to the PEM or DER CRL file co-located with the cert slot. +inline constexpr std::string_view kCrlPath = "crl_path"; + +/// Encoding of the CRL file: "pem" or "der". +inline constexpr std::string_view kCrlFormat = "crl_format"; + +/// ISO-8601 UTC timestamp of the CRL's nextUpdate field. +/// Written when the CRL is imported; read by the daemon to schedule refresh. +inline constexpr std::string_view kCrlNextUpdate = "crl_next_update"; + +// ---- [certificate_metadata] section -------------------------------------- + +/// RFC 4514 canonical Subject DN string. +inline constexpr std::string_view kSubject = "subject"; + +/// RFC 4514 canonical Issuer DN string. +inline constexpr std::string_view kIssuer = "issuer"; + +/// ISO-8601 UTC timestamp corresponding to the notBefore validity field. +inline constexpr std::string_view kNotBefore = "not_before"; + +/// ISO-8601 UTC timestamp corresponding to the notAfter validity field. +inline constexpr std::string_view kNotAfter = "not_after"; + +/// Hex-encoded Subject Key Identifier extension value; empty string if absent. +inline constexpr std::string_view kSkid = "skid"; + +/// Hex-encoded Authority Key Identifier extension value; empty string if absent. +inline constexpr std::string_view kAkid = "akid"; + +/// "true" when the certificate is a CA (BasicConstraints cA=TRUE); "false" otherwise. +inline constexpr std::string_view kIsCA = "is_ca"; + +/// Hex-encoded SHA-256 fingerprint of the certificate DER encoding. +inline constexpr std::string_view kFingerprint = "fingerprint"; + +// ---- [metadata] section -------------------------------------------------- + +/// Slot availability override: "active" | "disabled" | "unavailable". +/// When absent, the slot is assumed active. +inline constexpr std::string_view kAvailability = "availability"; + +/// ISO-8601 UTC timestamp of the last successful certificate provisioning. +inline constexpr std::string_view kProvisionedAt = "provisioned_at"; + +/// Monotonically increasing update counter (decimal string). +/// Incremented on every certificate replacement. +inline constexpr std::string_view kUpdateCounter = "update_counter"; + +} // namespace cert_deployment_keys + +} // namespace score::crypto::daemon::cert_management + +#endif // SCORE_CRYPTO_SRC_DAEMON_CERT_MANAGEMENT_INTERFACES_CERT_TYPES_HPP diff --git a/score/crypto/src/daemon/cert_management/interfaces/i_cert_slot_catalog.hpp b/score/crypto/src/daemon/cert_management/interfaces/i_cert_slot_catalog.hpp new file mode 100644 index 000000000..100e29004 --- /dev/null +++ b/score/crypto/src/daemon/cert_management/interfaces/i_cert_slot_catalog.hpp @@ -0,0 +1,55 @@ +/******************************************************************************** + * 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 + ********************************************************************************/ + +#ifndef SCORE_CRYPTO_SRC_DAEMON_CERT_MANAGEMENT_INTERFACES_I_CERT_SLOT_CATALOG_HPP +#define SCORE_CRYPTO_SRC_DAEMON_CERT_MANAGEMENT_INTERFACES_I_CERT_SLOT_CATALOG_HPP + +namespace score::crypto::daemon::cert_management +{ + +class CertSlotRegistry; + +/// Abstract source of certificate slot definitions. +/// +/// A catalog is a **one-shot loader**: it is instantiated, Load() is called +/// exactly once to populate the registry, and the catalog object may then be +/// discarded. This keeps CertSlotRegistry a pure registry with no knowledge +/// of where slot definitions come from. +/// +/// Current implementations: +/// - ConfigDrivenSlotCatalog — reads slot definitions from parsed CertificateConfig +/// +/// Future implementations: +/// - SecureStoreCatalog — reads provisioned slot metadata from a TEE-backed store +/// - Pkcs11CertSlotCatalog — enumerates PKCS#11 token certificate objects +/// +/// @note Catalog implementations MUST be idempotent: calling Load() on an +/// already-populated registry is safe. Duplicate slot names are rejected +/// by the registry boundary rather than overwriting existing slots. +class ICertSlotCatalog +{ + public: + virtual ~ICertSlotCatalog() = default; + + /// Register all certificate slots from this catalog into the given registry. + /// + /// Each slot is registered via CertSlotRegistry::RegisterSlot(CertSlotConfig). + /// The catalog does NOT retain a reference to the registry after this call. + /// + /// @param registry The central CertSlotRegistry to populate. + virtual void Load(CertSlotRegistry& registry) = 0; +}; + +} // namespace score::crypto::daemon::cert_management + +#endif // SCORE_CRYPTO_SRC_DAEMON_CERT_MANAGEMENT_INTERFACES_I_CERT_SLOT_CATALOG_HPP diff --git a/score/crypto/src/daemon/cert_management/interfaces/i_cert_slot_handler.cpp b/score/crypto/src/daemon/cert_management/interfaces/i_cert_slot_handler.cpp new file mode 100644 index 000000000..7bc2a4156 --- /dev/null +++ b/score/crypto/src/daemon/cert_management/interfaces/i_cert_slot_handler.cpp @@ -0,0 +1,58 @@ +/******************************************************************************** + * 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/cert_management/interfaces/i_cert_slot_handler.hpp" +#include "score/crypto/src/daemon/common/daemon_error.hpp" + +namespace score::crypto::daemon::cert_management +{ + +score::crypto::Expected +ICertSlotHandler::StoreCertificate(const CertSlotConfig& /*slot*/, const CertObject& /*cert*/) +{ + return score::crypto::make_unexpected(score::crypto::daemon::common::DaemonErrorCode::kUnsupportedOperation); +} + +score::crypto::Expected ICertSlotHandler::ClearSlot( + const CertSlotConfig& /*slot*/) +{ + return score::crypto::make_unexpected(score::crypto::daemon::common::DaemonErrorCode::kUnsupportedOperation); +} + +score::crypto::Expected, score::crypto::daemon::common::DaemonErrorCode> ICertSlotHandler::LoadCrl( + const CertSlotConfig& /*slot*/) +{ + return score::crypto::make_unexpected(score::crypto::daemon::common::DaemonErrorCode::kUnsupportedOperation); +} + +score::crypto::Expected ICertSlotHandler::StoreCrl( + const CertSlotConfig& /*slot*/, + score::crypto::span /*crl_data*/, + score::crypto::FormatType /*format*/) +{ + return score::crypto::make_unexpected(score::crypto::daemon::common::DaemonErrorCode::kUnsupportedOperation); +} + +score::crypto::Expected ICertSlotHandler::ClearCrl( + const CertSlotConfig& /*slot*/) +{ + return score::crypto::make_unexpected(score::crypto::daemon::common::DaemonErrorCode::kUnsupportedOperation); +} + +score::crypto::Expected ICertSlotHandler::GetCrlNextUpdate( + const CertSlotConfig& /*slot*/) +{ + return score::crypto::make_unexpected(score::crypto::daemon::common::DaemonErrorCode::kUnsupportedOperation); +} + +} // namespace score::crypto::daemon::cert_management diff --git a/score/crypto/src/daemon/cert_management/interfaces/i_cert_slot_handler.hpp b/score/crypto/src/daemon/cert_management/interfaces/i_cert_slot_handler.hpp new file mode 100644 index 000000000..296a79e08 --- /dev/null +++ b/score/crypto/src/daemon/cert_management/interfaces/i_cert_slot_handler.hpp @@ -0,0 +1,188 @@ +/******************************************************************************** + * 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 + ********************************************************************************/ + +#ifndef SCORE_CRYPTO_SRC_DAEMON_CERT_MANAGEMENT_INTERFACES_I_CERT_SLOT_HANDLER_HPP +#define SCORE_CRYPTO_SRC_DAEMON_CERT_MANAGEMENT_INTERFACES_I_CERT_SLOT_HANDLER_HPP + +#include "score/crypto/src/api/common/types.hpp" +#include "score/crypto/src/common/types.hpp" +#include "score/crypto/src/daemon/cert_management/interfaces/cert_object.hpp" +#include "score/crypto/src/daemon/cert_management/interfaces/cert_slot_config.hpp" +#include "score/crypto/src/daemon/cert_management/interfaces/cert_types.hpp" +#include "score/crypto/src/daemon/common/daemon_error.hpp" + +#include +#include +#include +#include +#include +#include +#include + +namespace score::crypto::daemon::cert_management +{ + +/// Interface for provider-specific certificate slot operations. +/// +/// A slot handler manages the storage backend for one deployment category: +/// - FileBackedSlotHandler : file system (OpenSSL / software provider) +/// - (future) hardware providers implement their own ICertSlotHandler subclass +/// +/// LoadCertificate is the primary operation: it retrieves certificate material +/// from the slot and returns a CertObject that owns the parsed form. +/// +/// CRL operations are co-located with the certificate slot: +/// the [crl] section of the slot's KV deployment descriptor holds the CRL. +/// ICertSlotHandler is the single management surface for both the certificate +/// and its associated CRL (if any). +/// +/// Default implementations of optional methods return kUnsupportedOperation. +/// Implementations that support only read-only certificate material (e.g., a +/// static-provisioning slot) override only LoadCertificate, GetSlotState, and +/// GetSlotInfo. +/// +/// Thread safety: individual slot handler instances are not thread-safe. +/// The CertManagementService serializes concurrent access via DataNode locks. +class ICertSlotHandler +{ + public: + using Sptr = std::shared_ptr; + + ICertSlotHandler() = default; + + virtual ~ICertSlotHandler() = default; + + ICertSlotHandler(const ICertSlotHandler&) = delete; + ICertSlotHandler& operator=(const ICertSlotHandler&) = delete; + ICertSlotHandler(ICertSlotHandler&&) = delete; + ICertSlotHandler& operator=(ICertSlotHandler&&) = delete; + + // ----------------------------------------------------------------------- + // Pure virtual — must be implemented by every slot handler + // ----------------------------------------------------------------------- + + /// Load certificate material from the slot and return a handler that owns it. + /// + /// For file-backed slots: reads the file referenced in the [certificate] + /// section of the deployment descriptor, delegates to the injected ICertParser. + /// For hardware slots: retrieves the certificate object from the secure element. + /// + /// The returned CertObject must be transferred to a CertDataNode + /// (via CertManagementService::RegisterCertMaterial()) immediately; the + /// caller must not hold a bare reference across yield points. + /// + /// Returns kKeySlotEmpty when no certificate has been stored in this slot. + [[nodiscard]] virtual score::crypto::Expected + LoadCertificate(const CertSlotConfig& slot) = 0; + + /// Query certificate-slot state (kEmpty or kOccupied). + /// + /// Must not load the certificate — for file-backed slots this is a + /// lightweight existence check (stat) against the deployment descriptor. + [[nodiscard]] virtual score::crypto::Expected + GetSlotState(const CertSlotConfig& slot) = 0; + + /// Return slot metadata (state, subject, issuer, validity, provider). + /// + /// May read cached metadata from the [certificate_metadata] section of the + /// deployment descriptor without fully parsing the certificate file. + [[nodiscard]] virtual score::crypto::Expected + GetSlotInfo(const CertSlotConfig& slot) = 0; + + /// True when the slot has an associated CRL stored in its [crl] section. + /// + /// For file-backed slots: checks existence of the crl_path key in the + /// deployment descriptor and verifies the file is present. + [[nodiscard]] virtual bool HasCrl(const CertSlotConfig& slot) = 0; + + // ----------------------------------------------------------------------- + // Optional — defaulted to kUnsupportedOperation + // ----------------------------------------------------------------------- + + /// Persist a CertObject's certificate material into the slot. + /// + /// For file-backed slots: serializes to the format specified in the + /// deployment descriptor and writes to the path in the [certificate] + /// section. Updates the [certificate_metadata] section. + /// For hardware slots: stores the certificate object in the secure element. + /// + /// Default: returns kUnsupportedOperation. + [[nodiscard]] virtual score::crypto::Expected + StoreCertificate(const CertSlotConfig& slot, const CertObject& cert); + + /// Erase certificate material (and CRL if present) from the slot. + /// + /// After this call, GetSlotState() must return kEmpty and HasCrl() false. + /// + /// Default: returns kUnsupportedOperation. + [[nodiscard]] virtual score::crypto::Expected + ClearSlot(const CertSlotConfig& slot); + + // ---- CRL management -------------------------------------------------- + + /// Load the raw CRL bytes from the slot's [crl] section. + /// + /// Returns the DER or PEM bytes as stored; callers inspect the format via + /// the cert_deployment_keys::kCrlFormat key in the deployment descriptor. + /// + /// Default: returns kUnsupportedOperation. + [[nodiscard]] virtual score::crypto::Expected, score::crypto::daemon::common::DaemonErrorCode> + LoadCrl(const CertSlotConfig& slot); + + /// Store raw CRL bytes into the slot's [crl] section. + /// + /// crl_data points to caller-owned memory valid for the duration of this call. + /// The implementation must copy the bytes and write them to the configured path. + /// Updates cert_deployment_keys::kCrlNextUpdate in the deployment descriptor. + /// + /// Consistency model: the operation is two steps — write CRL file, then write + /// descriptor. Each step is individually atomic (temp-file + rename). If the + /// descriptor write fails after the file write succeeds, the CRL file is an + /// orphan that HasCrl() will not surface (it cross-validates file existence + /// against the descriptor path). The orphan is silently overwritten on the + /// next StoreCrl call. No explicit rollback is required. + /// + /// Default: returns kUnsupportedOperation. + [[nodiscard]] virtual score::crypto::Expected + StoreCrl(const CertSlotConfig& slot, score::crypto::span crl_data, score::crypto::FormatType format); + + /// Remove the CRL from the slot's [crl] section. + /// + /// After this call, HasCrl() must return false. + /// + /// Default: returns kUnsupportedOperation. + [[nodiscard]] virtual score::crypto::Expected + ClearCrl(const CertSlotConfig& slot); + + /// Return the nextUpdate time of the stored CRL as Unix epoch seconds. + /// + /// Used by the daemon's CRL refresh scheduler. Returns kUnsupportedOperation + /// when no CRL is stored (HasCrl() == false). + /// + /// Default: returns kUnsupportedOperation. + [[nodiscard]] virtual score::crypto::Expected + GetCrlNextUpdate(const CertSlotConfig& slot); +}; + +/// Factory function type for creating a slot handler from a slot configuration. +/// +/// Defined here so that all components (CertManagementService, TrustStoreManager, +/// ConfigDrivenTrustStoreCatalog) share one canonical type rather than each +/// declaring an identical nested alias. +using CertSlotHandlerFactory = std::function; + +} // namespace score::crypto::daemon::cert_management + +#endif // SCORE_CRYPTO_SRC_DAEMON_CERT_MANAGEMENT_INTERFACES_I_CERT_SLOT_HANDLER_HPP diff --git a/score/crypto/src/daemon/cert_management/interfaces/i_trust_store_handler.hpp b/score/crypto/src/daemon/cert_management/interfaces/i_trust_store_handler.hpp new file mode 100644 index 000000000..2e5b29af1 --- /dev/null +++ b/score/crypto/src/daemon/cert_management/interfaces/i_trust_store_handler.hpp @@ -0,0 +1,132 @@ +/******************************************************************************** + * 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 + ********************************************************************************/ + +#ifndef SCORE_CRYPTO_SRC_DAEMON_CERT_MANAGEMENT_INTERFACES_I_TRUST_STORE_HANDLER_HPP +#define SCORE_CRYPTO_SRC_DAEMON_CERT_MANAGEMENT_INTERFACES_I_TRUST_STORE_HANDLER_HPP + +#include "score/crypto/src/common/types.hpp" +#include "score/crypto/src/daemon/cert_management/interfaces/cert_object.hpp" +#include "score/crypto/src/daemon/cert_management/interfaces/cert_types.hpp" +#include "score/crypto/src/daemon/common/daemon_error.hpp" + +#include +#include +#include +#include +#include +#include +#include + +namespace score::crypto::daemon::cert_management +{ + +/// Per-trust-store object managing a named collection of trust anchors. +/// +/// A trust store is a logical set of CA certificates used as verification +/// anchors by the cert verification context handler. +/// +/// All anchors are loaded from named certificate slots via the +/// CertSlotRegistry. Their content is daemon-mediated: a SaveCertificate to +/// any member slot triggers NotifySlotUpdate() on every referencing store. +/// +/// Membership is represented by typed certificate-slot references. Runtime +/// membership and acceptance mutations are coordinated by TrustStoreManager +/// and persisted in the trust-store deployment descriptor. +/// +/// Chain-building support: +/// FindBySubject and FindBySkid return nullable CertObject::Sptr for +/// efficient O(1) / O(log n) anchor lookup during chain building. +/// +/// Thread safety: ITrustStoreHandler instances are not individually thread-safe. +/// TrustStoreManager coordinates concurrent access. +class ITrustStoreHandler +{ + public: + using Sptr = std::shared_ptr; + + ITrustStoreHandler() = default; + + virtual ~ITrustStoreHandler() = default; + + ITrustStoreHandler(const ITrustStoreHandler&) = delete; + ITrustStoreHandler& operator=(const ITrustStoreHandler&) = delete; + ITrustStoreHandler(ITrustStoreHandler&&) = delete; + ITrustStoreHandler& operator=(ITrustStoreHandler&&) = delete; + + // ----------------------------------------------------------------------- + // Identity + // ----------------------------------------------------------------------- + + /// Return the runtime handle for this trust store. + /// + /// The handle encodes the index in TrustStoreManager's internal table. + /// Valid for the lifetime of the daemon; do not persist across restarts. + [[nodiscard]] virtual TrustStoreHandle GetHandle() const noexcept = 0; + + // ----------------------------------------------------------------------- + // Anchor enumeration + // ----------------------------------------------------------------------- + + /// Return all current enabled slot-backed trust anchors. + /// + /// Slot-backed anchors may be null if the underlying cert slot is empty. + /// Callers should filter out null entries before passing to a verifier. + /// + /// Returns kInternalError when the anchor set cannot be assembled + /// (e.g., static file I/O failure during lazy reload). + [[nodiscard]] virtual score::crypto::Expected, + score::crypto::daemon::common::DaemonErrorCode> + GetAnchors() = 0; + + // ----------------------------------------------------------------------- + // Slot update notification (called by TrustStoreManager) + // ----------------------------------------------------------------------- + + /// Update the in-memory anchor for the given cert slot. + /// + /// Called by TrustStoreManager::NotifyUpdate() when SaveCertificate writes + /// to a member slot. If cert is nullptr, the slot's anchor entry is + /// cleared (models an empty slot). + /// + /// Implementations must update the internal slot->anchor mapping and + /// invalidate / refresh the chain-building indices (FindBySubject, FindBySkid). + virtual void NotifySlotUpdate(CertSlotHandle slot, CertObject::Sptr cert) = 0; + + // ----------------------------------------------------------------------- + // Chain-building lookup indices + // ----------------------------------------------------------------------- + + /// Find a trust anchor by its RFC 4514 canonical Subject DN. + /// + /// Used during chain building to match an intermediate certificate's + /// issuer field against the anchors in this trust store. + /// + /// Returns a non-null CertObject::Sptr when a matching anchor is found, + /// nullptr otherwise (callers must check before dereferencing). + [[nodiscard]] virtual CertObject::Sptr FindBySubject(const std::string& canonical_subject) const = 0; + + /// Find a trust anchor by its Subject Key Identifier (SKID) extension value. + /// + /// Used during chain building to match an intermediate certificate's + /// Authority Key Identifier (AKID) against the anchors in this trust store. + /// + /// skid contains the raw SKID extension bytes (variable length; typically 20). + /// + /// Returns a non-null CertObject::Sptr when a matching anchor is found, + /// nullptr otherwise. + [[nodiscard]] virtual CertObject::Sptr FindBySkid(score::crypto::span skid) const = 0; +}; + +} // namespace score::crypto::daemon::cert_management + +#endif // SCORE_CRYPTO_SRC_DAEMON_CERT_MANAGEMENT_INTERFACES_I_TRUST_STORE_HANDLER_HPP diff --git a/score/crypto/src/daemon/cert_management/interfaces/trust_store_config.hpp b/score/crypto/src/daemon/cert_management/interfaces/trust_store_config.hpp new file mode 100644 index 000000000..6cf850ec4 --- /dev/null +++ b/score/crypto/src/daemon/cert_management/interfaces/trust_store_config.hpp @@ -0,0 +1,79 @@ +/******************************************************************************** + * 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 + ********************************************************************************/ + +#ifndef SCORE_CRYPTO_SRC_DAEMON_CERT_MANAGEMENT_INTERFACES_TRUST_STORE_CONFIG_HPP +#define SCORE_CRYPTO_SRC_DAEMON_CERT_MANAGEMENT_INTERFACES_TRUST_STORE_CONFIG_HPP + +#include "score/crypto/src/daemon/cert_management/interfaces/access_policy.hpp" + +#include +#include +#include + +namespace score::crypto::daemon::cert_management +{ + +enum class TrustStoreMemberKind : uint8_t +{ + kSharedStatic = 0U, + kExclusiveMutable = 1U, + kConditionalExternal = 2U, +}; + +enum class ConditionalSlotInitialization : uint8_t +{ + kEnableAndAcceptCurrent = 0U, + kDisableUntilAccepted = 1U, +}; + +struct TrustStoreMemberConfig +{ + std::string slot_name; + TrustStoreMemberKind kind{TrustStoreMemberKind::kSharedStatic}; +}; + +/// Configuration for a named trust store entry. +/// +/// A trust store is a logical collection of trust anchors (CA certificates) +/// used for certificate chain verification. Member slots are certificate slots +/// already defined in the cert slot catalog; the trust store references them +/// by name. The same cert slot can back multiple trust stores. +/// +/// Membership policy is fixed at startup. Runtime enablement, conditional +/// acceptance, and mutable membership state are deployment state; member-slot +/// certificate content is daemon-mediated through the slot handler. +struct TrustStoreConfig +{ + /// Human-readable name for this trust store (e.g., "tls-server-auth", "code-signing"). + std::string store_name; + + /// Certificate slots serving as trust anchors, with explicit membership policy. + std::vector members; + + /// Default used only for conditional members without persisted state. + ConditionalSlotInitialization conditional_slot_initialization{ConditionalSlotInitialization::kDisableUntilAccepted}; + + /// UID-based access control for this trust store. + AccessPolicy access_policy; + + /// Path to the deployment descriptor for this trust store entry. + /// + /// Used by the deployment layer to persist mutable trust-store state, + /// including exclusive membership and conditional acceptance state. + std::string deployment_path; + std::string deployment_format{"kv"}; +}; + +} // namespace score::crypto::daemon::cert_management + +#endif // SCORE_CRYPTO_SRC_DAEMON_CERT_MANAGEMENT_INTERFACES_TRUST_STORE_CONFIG_HPP diff --git a/score/crypto/src/daemon/cert_management/nodes/cert_data_node.hpp b/score/crypto/src/daemon/cert_management/nodes/cert_data_node.hpp new file mode 100644 index 000000000..f5106fb7c --- /dev/null +++ b/score/crypto/src/daemon/cert_management/nodes/cert_data_node.hpp @@ -0,0 +1,95 @@ +/******************************************************************************** + * 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 + ********************************************************************************/ + +#ifndef SCORE_CRYPTO_SRC_DAEMON_CERT_MANAGEMENT_NODES_CERT_DATA_NODE_HPP +#define SCORE_CRYPTO_SRC_DAEMON_CERT_MANAGEMENT_NODES_CERT_DATA_NODE_HPP + +#include "score/crypto/src/daemon/cert_management/core/cert_entry.hpp" +#include "score/crypto/src/daemon/cert_management/core/cert_registry.hpp" +#include "score/crypto/src/daemon/data_manager/data_node.hpp" + +#include +#include +#include + +namespace score::crypto::daemon::cert_management +{ + +/// DataNode placed in the client's tree that references a shared CertEntry +/// living in the per-provider CertRegistry. +/// +/// On construction, increments the CertEntry's reference count. +/// On destruction, decrements it. When the count reaches zero, the provided +/// cleanup callback unregisters the cert from the registry. +/// +/// exclusiveAccess = false: multiple threads may read concurrently. +class CertDataNode final : public data_manager::DataNode +{ + public: + using UnregisterCallback = std::function; + + [[nodiscard]] data_manager::DataNodeType GetNodeType() const noexcept override + { + return data_manager::DataNodeType::kCertData; + } + + CertDataNode(std::shared_ptr cert_entry, + CertRegistryId registry_id, + data_manager::ClientId client_id, + UnregisterCallback on_last_release) + : DataNode(false), + m_cert_entry{std::move(cert_entry)}, + m_registry_id{registry_id}, + m_client_id{client_id}, + m_on_last_release{std::move(on_last_release)} + { + m_cert_entry->AddRef(m_client_id); + } + + ~CertDataNode() override + { + if (m_cert_entry != nullptr) + { + const bool last = m_cert_entry->Release(m_client_id); + if (last && m_on_last_release) + { + m_on_last_release(m_registry_id); + } + } + } + + CertDataNode(const CertDataNode&) = delete; + CertDataNode& operator=(const CertDataNode&) = delete; + CertDataNode(CertDataNode&&) = delete; + CertDataNode& operator=(CertDataNode&&) = delete; + + [[nodiscard]] std::shared_ptr GetCertEntry() const noexcept + { + return m_cert_entry; + } + + [[nodiscard]] CertRegistryId GetRegistryId() const noexcept + { + return m_registry_id; + } + + private: + std::shared_ptr m_cert_entry; + CertRegistryId m_registry_id; + data_manager::ClientId m_client_id; + UnregisterCallback m_on_last_release; +}; + +} // namespace score::crypto::daemon::cert_management + +#endif // SCORE_CRYPTO_SRC_DAEMON_CERT_MANAGEMENT_NODES_CERT_DATA_NODE_HPP diff --git a/score/crypto/src/daemon/cert_management/nodes/cert_slot_data_node.hpp b/score/crypto/src/daemon/cert_management/nodes/cert_slot_data_node.hpp new file mode 100644 index 000000000..07fe7de8e --- /dev/null +++ b/score/crypto/src/daemon/cert_management/nodes/cert_slot_data_node.hpp @@ -0,0 +1,71 @@ +/******************************************************************************** + * 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 + ********************************************************************************/ + +#ifndef SCORE_CRYPTO_SRC_DAEMON_CERT_MANAGEMENT_NODES_CERT_SLOT_DATA_NODE_HPP +#define SCORE_CRYPTO_SRC_DAEMON_CERT_MANAGEMENT_NODES_CERT_SLOT_DATA_NODE_HPP + +#include "score/crypto/src/daemon/cert_management/interfaces/cert_slot_config.hpp" +#include "score/crypto/src/daemon/cert_management/interfaces/cert_types.hpp" +#include "score/crypto/src/daemon/cert_management/slot/slot_registry.hpp" +#include "score/crypto/src/daemon/data_manager/data_node.hpp" + +#include + +namespace score::crypto::daemon::cert_management +{ + +/// DataNode for a resolved persistent certificate slot. +/// +/// Created on ResolveResource(), lives for the connection lifetime. +/// Holds only a lightweight CertSlotHandle and a reference to the CertSlotRegistry. +/// exclusiveAccess = false (concurrent reads OK). +class CertSlotDataNode : public data_manager::DataNode +{ + public: + CertSlotDataNode(CertSlotHandle slot_handle, CertSlotRegistry::Sptr slot_registry) + : DataNode(false), m_slot_handle{slot_handle}, m_slot_registry{std::move(slot_registry)} + { + } + + ~CertSlotDataNode() override = default; + + [[nodiscard]] data_manager::DataNodeType GetNodeType() const noexcept override + { + return data_manager::DataNodeType::kCertSlot; + } + + [[nodiscard]] CertSlotHandle GetSlotHandle() const noexcept + { + return m_slot_handle; + } + + /// @brief Access config from central registry. + [[nodiscard]] score::crypto::Expected + GetConfig() const + { + return m_slot_registry->GetConfig(m_slot_handle); + } + + [[nodiscard]] CertSlotRegistry::Sptr GetSlotRegistry() const + { + return m_slot_registry; + } + + private: + CertSlotHandle m_slot_handle; + CertSlotRegistry::Sptr m_slot_registry; +}; + +} // namespace score::crypto::daemon::cert_management + +#endif // SCORE_CRYPTO_SRC_DAEMON_CERT_MANAGEMENT_NODES_CERT_SLOT_DATA_NODE_HPP diff --git a/score/crypto/src/daemon/cert_management/nodes/trust_store_data_node.hpp b/score/crypto/src/daemon/cert_management/nodes/trust_store_data_node.hpp new file mode 100644 index 000000000..bc50a81ff --- /dev/null +++ b/score/crypto/src/daemon/cert_management/nodes/trust_store_data_node.hpp @@ -0,0 +1,66 @@ +/******************************************************************************** + * 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 + ********************************************************************************/ + +#ifndef SCORE_CRYPTO_SRC_DAEMON_CERT_MANAGEMENT_NODES_TRUST_STORE_DATA_NODE_HPP +#define SCORE_CRYPTO_SRC_DAEMON_CERT_MANAGEMENT_NODES_TRUST_STORE_DATA_NODE_HPP + +#include "score/crypto/src/daemon/cert_management/interfaces/cert_types.hpp" +#include "score/crypto/src/daemon/cert_management/truststore/trust_store_manager.hpp" +#include "score/crypto/src/daemon/data_manager/data_node.hpp" + +namespace score::crypto::daemon::cert_management +{ + +/// Application-scoped reference to a manager-owned trust store. +/// +/// Lightweight handle node — holds only a TrustStoreHandle, analogous to +/// CertSlotDataNode for cert slots. Cert content is NOT loaded or cached here. +/// +/// Lifetime: parented under the root connection node; survives for the full +/// application connection, not just for a single verification operation. +class TrustStoreDataNode final : public data_manager::DataNode +{ + public: + TrustStoreDataNode(TrustStoreHandle handle, TrustStoreManager::Sptr manager) + : DataNode(false), m_handle{handle}, m_manager{std::move(manager)} + { + } + + data_manager::DataNodeType GetNodeType() const noexcept override + { + return data_manager::DataNodeType::kTrustStore; + } + + TrustStoreHandle GetTrustStoreHandle() const noexcept + { + return m_handle; + } + + TrustStoreManager::Sptr GetTrustStoreManager() const + { + return m_manager; + } + + ITrustStoreHandler::Sptr GetStore() const + { + return m_manager ? m_manager->GetStore(m_handle.index) : nullptr; + } + + private: + TrustStoreHandle m_handle; + TrustStoreManager::Sptr m_manager; +}; + +} // namespace score::crypto::daemon::cert_management + +#endif // SCORE_CRYPTO_SRC_DAEMON_CERT_MANAGEMENT_NODES_TRUST_STORE_DATA_NODE_HPP diff --git a/score/crypto/src/daemon/cert_management/policy/access_policy_enforcer.cpp b/score/crypto/src/daemon/cert_management/policy/access_policy_enforcer.cpp new file mode 100644 index 000000000..096e4e244 --- /dev/null +++ b/score/crypto/src/daemon/cert_management/policy/access_policy_enforcer.cpp @@ -0,0 +1,73 @@ +/******************************************************************************** + * 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/cert_management/policy/access_policy_enforcer.hpp" +#include "score/crypto/src/daemon/control_plane/control_protocol.h" + +#include + +namespace score::crypto::daemon::cert_management +{ + +score::crypto::Expected +AccessPolicyEnforcer::CheckSlotAccess(const CertSlotConfig& slot, data_manager::ClientId client_id) +{ + // Certificate material is public information. Resource resolution still + // uses the caller UID, but reading a resolved certificate is unrestricted. + static_cast(slot); + static_cast(client_id); + return std::monostate{}; +} + +score::crypto::Expected +AccessPolicyEnforcer::CheckWritePermission(const CertSlotConfig& slot, data_manager::ClientId client_id) +{ + const uint32_t uid = control_plane::protocol::GetUidFromClientId(client_id); + + const auto& allowed = slot.access_policy.allowed_write_uids; + // An omitted write allowlist must never grant mutation access. + if (!allowed.empty() && std::find(allowed.begin(), allowed.end(), uid) != allowed.end()) + { + return std::monostate{}; + } + + return score::crypto::make_unexpected(score::crypto::daemon::common::DaemonErrorCode::kAccessDenied); +} + +score::crypto::Expected +AccessPolicyEnforcer::CheckTrustStoreAccess(const TrustStoreConfig& store, data_manager::ClientId client_id) +{ + // Trust-store reads expose certificates, not private key material. The + // application-resource mapping has already selected the store for UID. + static_cast(store); + static_cast(client_id); + return std::monostate{}; +} + +score::crypto::Expected +AccessPolicyEnforcer::CheckTrustStoreWritePermission(const TrustStoreConfig& store, data_manager::ClientId client_id) +{ + const uint32_t uid = control_plane::protocol::GetUidFromClientId(client_id); + + const auto& allowed = store.access_policy.allowed_write_uids; + // Trust-store mutations are default-deny: an explicit writer UID is + // required even when read access is unrestricted. + if (!allowed.empty() && std::find(allowed.begin(), allowed.end(), uid) != allowed.end()) + { + return std::monostate{}; + } + + return score::crypto::make_unexpected(score::crypto::daemon::common::DaemonErrorCode::kAccessDenied); +} + +} // namespace score::crypto::daemon::cert_management diff --git a/score/crypto/src/daemon/cert_management/policy/access_policy_enforcer.hpp b/score/crypto/src/daemon/cert_management/policy/access_policy_enforcer.hpp new file mode 100644 index 000000000..cc454a220 --- /dev/null +++ b/score/crypto/src/daemon/cert_management/policy/access_policy_enforcer.hpp @@ -0,0 +1,55 @@ +/******************************************************************************** + * 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 + ********************************************************************************/ + +#ifndef SCORE_CRYPTO_SRC_DAEMON_CERT_MANAGEMENT_POLICY_ACCESS_POLICY_ENFORCER_HPP +#define SCORE_CRYPTO_SRC_DAEMON_CERT_MANAGEMENT_POLICY_ACCESS_POLICY_ENFORCER_HPP + +#include "score/crypto/src/daemon/cert_management/interfaces/cert_slot_config.hpp" +#include "score/crypto/src/daemon/cert_management/interfaces/trust_store_config.hpp" +#include "score/crypto/src/daemon/common/daemon_error.hpp" +#include "score/crypto/src/daemon/data_manager/data_node.hpp" + +#include + +namespace score::crypto::daemon::cert_management +{ + +/// @brief UID-based access control for certificate slots and trust stores. +/// +/// All access decisions are made here — providers never implement access control. +/// Kept local to cert_management (mirrors key_management::AccessPolicyEnforcer). +class AccessPolicyEnforcer +{ + public: + /// @brief Certificate reads are unrestricted after resource resolution. + static score::crypto::Expected CheckSlotAccess( + const CertSlotConfig& slot, + data_manager::ClientId client_id); + + /// @brief Check if client UID is in the slot's allowed_write_uids list. + static score::crypto::Expected CheckWritePermission( + const CertSlotConfig& slot, + data_manager::ClientId client_id); + + /// @brief Trust-store reads are unrestricted after resource resolution. + static score::crypto::Expected + CheckTrustStoreAccess(const TrustStoreConfig& store, data_manager::ClientId client_id); + + /// @brief Check if client UID is in the trust store's allowed_write_uids list. + static score::crypto::Expected + CheckTrustStoreWritePermission(const TrustStoreConfig& store, data_manager::ClientId client_id); +}; + +} // namespace score::crypto::daemon::cert_management + +#endif // SCORE_CRYPTO_SRC_DAEMON_CERT_MANAGEMENT_POLICY_ACCESS_POLICY_ENFORCER_HPP diff --git a/score/crypto/src/daemon/cert_management/slot/config_driven_slot_catalog.cpp b/score/crypto/src/daemon/cert_management/slot/config_driven_slot_catalog.cpp new file mode 100644 index 000000000..1e4edb61b --- /dev/null +++ b/score/crypto/src/daemon/cert_management/slot/config_driven_slot_catalog.cpp @@ -0,0 +1,61 @@ +/******************************************************************************** + * 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/cert_management/slot/config_driven_slot_catalog.hpp" + +#include "score/crypto/src/daemon/cert_management/interfaces/cert_slot_config.hpp" +#include "score/crypto/src/daemon/cert_management/slot/slot_registry.hpp" + +#include "score/mw/log/logging.h" + +namespace score::crypto::daemon::cert_management +{ + +ConfigDrivenSlotCatalog::ConfigDrivenSlotCatalog(const config::CertificateConfig& cert_config) + : m_cert_config{cert_config} +{ +} + +void ConfigDrivenSlotCatalog::Load(CertSlotRegistry& registry) +{ + const auto& entries = m_cert_config.GetSlotEntries(); + + for (const auto& entry : entries) + { + CertSlotConfig config{}; + config.slot_name = entry.slot_name; + config.storage_backend = entry.storage_backend; + config.access_policy.allowed_uids = entry.allowed_uids; + config.access_policy.allowed_write_uids = entry.allowed_write_uids; + config.deployment_path = entry.deployment_path; + config.deployment_format = entry.deployment_format; + config.integrity_policy = + (entry.integrity_policy == "required") ? IntegrityPolicy::kRequired : IntegrityPolicy::kDisabled; + + registry.RegisterSlot(std::move(config)); + + score::mw::log::LogDebug() << kLogPrefix << "Registered cert slot '" << entry.slot_name + << "' (storage_backend=" << entry.storage_backend + << ", integrity_policy=" << entry.integrity_policy << ")"; + } + + score::mw::log::LogDebug() << kLogPrefix << "Loaded " << entries.size() << " cert slot(s) from configuration."; + + // Register per-application resource ID mappings. + for (const auto& mapping : m_cert_config.GetAppCertSlotEntries()) + { + registry.RegisterAppResource(mapping.uid, mapping.app_resource_id, mapping.slot_name); + } +} + +} // namespace score::crypto::daemon::cert_management diff --git a/score/crypto/src/daemon/cert_management/slot/config_driven_slot_catalog.hpp b/score/crypto/src/daemon/cert_management/slot/config_driven_slot_catalog.hpp new file mode 100644 index 000000000..afdf317f1 --- /dev/null +++ b/score/crypto/src/daemon/cert_management/slot/config_driven_slot_catalog.hpp @@ -0,0 +1,46 @@ +/******************************************************************************** + * 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 + ********************************************************************************/ + +#ifndef SCORE_CRYPTO_SRC_DAEMON_CERT_MANAGEMENT_SLOT_CONFIG_DRIVEN_SLOT_CATALOG_HPP +#define SCORE_CRYPTO_SRC_DAEMON_CERT_MANAGEMENT_SLOT_CONFIG_DRIVEN_SLOT_CATALOG_HPP + +#include "score/crypto/src/daemon/cert_management/interfaces/i_cert_slot_catalog.hpp" +#include "score/crypto/src/daemon/config/inc/config.hpp" + +#include + +namespace score::crypto::daemon::cert_management +{ + +/// @brief ICertSlotCatalog that reads certificate slot definitions from CertificateConfig. +/// +/// Production catalog: converts each CertificateConfig::CertSlotEntry into a +/// CertSlotConfig and registers it with the CertSlotRegistry. +class ConfigDrivenSlotCatalog final : public ICertSlotCatalog +{ + public: + explicit ConfigDrivenSlotCatalog(const config::CertificateConfig& cert_config); + ~ConfigDrivenSlotCatalog() override = default; + + /// @copydoc ICertSlotCatalog::Load + void Load(CertSlotRegistry& registry) override; + + private: + const config::CertificateConfig& m_cert_config; + + static constexpr std::string_view kLogPrefix = "[CERT_CONFIG_DRIVEN_CATALOG] "; +}; + +} // namespace score::crypto::daemon::cert_management + +#endif // SCORE_CRYPTO_SRC_DAEMON_CERT_MANAGEMENT_SLOT_CONFIG_DRIVEN_SLOT_CATALOG_HPP diff --git a/score/crypto/src/daemon/cert_management/slot/crl_handler.cpp b/score/crypto/src/daemon/cert_management/slot/crl_handler.cpp new file mode 100644 index 000000000..7c85eafef --- /dev/null +++ b/score/crypto/src/daemon/cert_management/slot/crl_handler.cpp @@ -0,0 +1,135 @@ +/******************************************************************************** + * 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 + ********************************************************************************/ +/******************************************************************************** + * Copyright (c) 2026 Contributors to the Eclipse Foundation + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ +#include "score/crypto/src/daemon/cert_management/slot/crl_handler.hpp" + +#include "score/crypto/src/daemon/cert_management/slot/deployment_loader.hpp" +#include "score/crypto/src/daemon/cert_management/slot/deployment_writer.hpp" +#include "score/crypto/src/daemon/common/storage/file_io.hpp" + +namespace score::crypto::daemon::cert_management +{ +namespace +{ +using Error = common::DaemonErrorCode; +namespace file_io = common::storage; +using Descriptor = common::storage::DeploymentDescriptor; + +score::crypto::Expected LoadDescriptor(const CertSlotConfig& slot) +{ + return DeploymentLoader::Load(slot.deployment_path, slot.deployment_format); +} + +score::crypto::Expected SaveDescriptor(const CertSlotConfig& slot, const Descriptor& d) +{ + return DeploymentWriter::Write(slot.deployment_path, slot.deployment_format, d); +} +} // namespace + +std::string CrlHandler::FormatName(score::crypto::FormatType format) +{ + return format == score::crypto::FormatType::kDer ? "der" : "pem"; +} + +bool CrlHandler::HasCrl(const CertSlotConfig& slot) const +{ + auto descriptor = LoadDescriptor(slot); + if (!descriptor) + return false; + const auto path = descriptor->Get("crl", "crl_path"); + return !path.empty() && file_io::FileExists(path); +} + +score::crypto::Expected, Error> CrlHandler::LoadCrl(const CertSlotConfig& slot) const +{ + auto descriptor = LoadDescriptor(slot); + if (!descriptor) + return score::crypto::make_unexpected(descriptor.error()); + const auto path = descriptor->Get("crl", "crl_path"); + if (path.empty()) + return score::crypto::make_unexpected(Error::kResourceNotAllocated); + return file_io::ReadFile(path, kMaxCrlSize); +} + +score::crypto::Expected CrlHandler::StoreCrl(const CertSlotConfig& slot, + score::crypto::span data, + score::crypto::FormatType format) +{ + if (data.empty()) + return score::crypto::make_unexpected(Error::kInvalidArgument); + auto descriptor = LoadDescriptor(slot); + if (!descriptor) + return score::crypto::make_unexpected(descriptor.error()); + auto path = descriptor->Get("crl", "crl_path"); + if (path.empty()) + { + // Prefer a sibling of the cert file; fall back to alongside the descriptor. + const auto cert_path = descriptor->Get("certificate", "cert_path"); + if (!cert_path.empty()) + { + const auto dot = cert_path.rfind('.'); + path = (dot != std::string::npos ? cert_path.substr(0U, dot) : cert_path) + ".crl"; + } + else + { + path = slot.deployment_path + ".crl"; + } + } + auto result = file_io::WriteFile(path, data); + if (!result) + return result; + descriptor->Set("crl", "crl_path", path); + descriptor->Set("crl", "crl_format", FormatName(format)); + return SaveDescriptor(slot, *descriptor); +} + +score::crypto::Expected CrlHandler::ClearCrl(const CertSlotConfig& slot) +{ + auto descriptor = LoadDescriptor(slot); + if (!descriptor) + return score::crypto::make_unexpected(descriptor.error()); + const auto path = descriptor->Get("crl", "crl_path"); + if (!path.empty()) + { + auto remove_result = file_io::RemoveFile(path); + if (!remove_result) + return score::crypto::make_unexpected(remove_result.error()); + } + descriptor->RemoveSection("crl"); + if (!path.empty()) + descriptor->Set("crl", "crl_path", path); + return SaveDescriptor(slot, *descriptor); +} + +score::crypto::Expected CrlHandler::GetCrlNextUpdate(const CertSlotConfig& slot) const +{ + auto descriptor = LoadDescriptor(slot); + if (!descriptor) + return score::crypto::make_unexpected(descriptor.error()); + const auto value = descriptor->Get("crl", "crl_next_update"); + if (value.empty()) + return score::crypto::make_unexpected(Error::kResourceNotAllocated); + try + { + return std::stoll(value); + } + catch (...) + { + return score::crypto::make_unexpected(Error::kInvalidArgument); + } +} + +} // namespace score::crypto::daemon::cert_management diff --git a/score/crypto/src/daemon/cert_management/slot/crl_handler.hpp b/score/crypto/src/daemon/cert_management/slot/crl_handler.hpp new file mode 100644 index 000000000..7bcbb9515 --- /dev/null +++ b/score/crypto/src/daemon/cert_management/slot/crl_handler.hpp @@ -0,0 +1,84 @@ +/******************************************************************************** + * 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 + ********************************************************************************/ +/******************************************************************************** + * Copyright (c) 2026 Contributors to the Eclipse Foundation + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ +#ifndef SCORE_CRYPTO_SRC_DAEMON_CERT_MANAGEMENT_SLOT_CRL_HANDLER_HPP +#define SCORE_CRYPTO_SRC_DAEMON_CERT_MANAGEMENT_SLOT_CRL_HANDLER_HPP + +#include "score/crypto/src/api/common/types.hpp" +#include "score/crypto/src/common/types.hpp" +#include "score/crypto/src/daemon/cert_management/interfaces/cert_slot_config.hpp" +#include "score/crypto/src/daemon/cert_management/interfaces/cert_types.hpp" +#include "score/crypto/src/daemon/common/daemon_error.hpp" + +#include +#include +#include + +namespace score::crypto::daemon::cert_management +{ + +/// Manages CRL persistence for a certificate slot via the slot's deployment +/// descriptor [crl] section. CRL data is always stored on the filesystem — +/// PKCS#11 tokens do not have a native CRL object type, and CRLs are public +/// data that requires no hardware protection. +/// +/// Used by composition in FileBackedSlotHandler and Pkcs11CertSlotHandler to +/// provide a uniform file-backed CRL implementation regardless of where the +/// certificate itself is stored (filesystem or token). +class CrlHandler final +{ + public: + /// Maximum accepted CRL file size (16 MiB). + static constexpr std::size_t kMaxCrlSize = 16U * 1024U * 1024U; + + CrlHandler() = default; + ~CrlHandler() = default; + + CrlHandler(const CrlHandler&) = delete; + CrlHandler& operator=(const CrlHandler&) = delete; + CrlHandler(CrlHandler&&) = delete; + CrlHandler& operator=(CrlHandler&&) = delete; + + /// Return true if the slot's deployment descriptor references an existing CRL file. + [[nodiscard]] bool HasCrl(const CertSlotConfig& slot) const; + + /// Load raw CRL bytes from the file referenced by the descriptor's [crl] section. + /// Returns kResourceNotAllocated if no CRL is configured. + [[nodiscard]] score::crypto::Expected, common::DaemonErrorCode> LoadCrl( + const CertSlotConfig& slot) const; + + /// Write @p data to the CRL file and update the descriptor's [crl] section. + /// Derives the CRL path from the deployment path if not already set. + [[nodiscard]] score::crypto::Expected StoreCrl( + const CertSlotConfig& slot, + score::crypto::span data, + score::crypto::FormatType format); + + /// Remove the CRL file and erase the [crl] section from the descriptor. + [[nodiscard]] score::crypto::Expected ClearCrl(const CertSlotConfig& slot); + + /// Return the cached crl_next_update epoch from the descriptor's [crl] section. + /// Returns kResourceNotAllocated if absent. + [[nodiscard]] score::crypto::Expected GetCrlNextUpdate( + const CertSlotConfig& slot) const; + + private: + static std::string FormatName(score::crypto::FormatType format); +}; + +} // namespace score::crypto::daemon::cert_management + +#endif // SCORE_CRYPTO_SRC_DAEMON_CERT_MANAGEMENT_SLOT_CRL_HANDLER_HPP diff --git a/score/crypto/src/daemon/cert_management/slot/deployment_loader.cpp b/score/crypto/src/daemon/cert_management/slot/deployment_loader.cpp new file mode 100644 index 000000000..78ed0038d --- /dev/null +++ b/score/crypto/src/daemon/cert_management/slot/deployment_loader.cpp @@ -0,0 +1,43 @@ +/******************************************************************************** + * 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/cert_management/slot/deployment_loader.hpp" + +#include "score/crypto/src/daemon/common/storage/deployment_path_utils.hpp" +#include "score/crypto/src/daemon/common/storage/kv/kv_deployment_loader.hpp" + +#include "score/mw/log/logging.h" + +namespace score::crypto::daemon::cert_management +{ + +score::crypto::Expected +DeploymentLoader::Load(const std::string& path, const std::string& format) +{ + if (!score::crypto::daemon::common::storage::IsDeploymentPathSafe(path)) + { + score::mw::log::LogError() << kLogPrefix << "Unsafe deployment path rejected: " << path; + return score::crypto::make_unexpected(score::crypto::daemon::common::DaemonErrorCode::kInvalidArgument); + } + + if (format == "kv") + { + return score::crypto::daemon::common::storage::KvDeploymentLoader{}.Load(path); + } + + score::mw::log::LogError() << kLogPrefix << "Unsupported deployment format: " << format; + return score::crypto::make_unexpected(score::crypto::daemon::common::DaemonErrorCode::kUnsupportedOperation); +} + +} // namespace score::crypto::daemon::cert_management diff --git a/score/crypto/src/daemon/cert_management/slot/deployment_loader.hpp b/score/crypto/src/daemon/cert_management/slot/deployment_loader.hpp new file mode 100644 index 000000000..966e906f0 --- /dev/null +++ b/score/crypto/src/daemon/cert_management/slot/deployment_loader.hpp @@ -0,0 +1,52 @@ +/******************************************************************************** + * 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 + ********************************************************************************/ + +#ifndef SCORE_CRYPTO_SRC_DAEMON_CERT_MANAGEMENT_SLOT_DEPLOYMENT_LOADER_HPP +#define SCORE_CRYPTO_SRC_DAEMON_CERT_MANAGEMENT_SLOT_DEPLOYMENT_LOADER_HPP + +#include "score/crypto/src/common/types.hpp" +#include "score/crypto/src/daemon/common/daemon_error.hpp" +#include "score/crypto/src/daemon/common/storage/deployment_descriptor.hpp" + +#include +#include + +namespace score::crypto::daemon::cert_management +{ + +/// @brief Loads a certificate slot or trust store deployment descriptor. +/// +/// The descriptor uses the section-based KV format from daemon/common/storage/. +/// Certificate and CRL sections are defined by cert_section_names / +/// cert_deployment_keys in cert_types.hpp. +/// +/// Thread safety: Load() is stateless and may be called concurrently. +class DeploymentLoader +{ + public: + /// @brief Load a deployment descriptor from the given path and format. + /// + /// @param path Absolute path to the descriptor file. + /// @param format Format hint: "kv" (default); future: "json", "bin". + /// @return Parsed DeploymentDescriptor on success, or DaemonErrorCode on failure. + [[nodiscard]] static score::crypto::Expected + Load(const std::string& path, const std::string& format); + + private: + static constexpr std::string_view kLogPrefix = "[CERT_DEPLOYMENT_LOADER] "; +}; + +} // namespace score::crypto::daemon::cert_management + +#endif // SCORE_CRYPTO_SRC_DAEMON_CERT_MANAGEMENT_SLOT_DEPLOYMENT_LOADER_HPP diff --git a/score/crypto/src/daemon/cert_management/slot/deployment_writer.cpp b/score/crypto/src/daemon/cert_management/slot/deployment_writer.cpp new file mode 100644 index 000000000..7f18ad1a5 --- /dev/null +++ b/score/crypto/src/daemon/cert_management/slot/deployment_writer.cpp @@ -0,0 +1,44 @@ +/******************************************************************************** + * 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/cert_management/slot/deployment_writer.hpp" + +#include "score/crypto/src/daemon/common/storage/deployment_path_utils.hpp" +#include "score/crypto/src/daemon/common/storage/kv/kv_deployment_writer.hpp" + +#include "score/mw/log/logging.h" + +namespace score::crypto::daemon::cert_management +{ + +score::crypto::Expected DeploymentWriter::Write( + const std::string& path, + const std::string& format, + const score::crypto::daemon::common::storage::DeploymentDescriptor& descriptor) +{ + if (!score::crypto::daemon::common::storage::IsDeploymentPathSafe(path)) + { + score::mw::log::LogError() << kLogPrefix << "Unsafe deployment path rejected: " << path; + return score::crypto::make_unexpected(score::crypto::daemon::common::DaemonErrorCode::kInvalidArgument); + } + + if (format == "kv") + { + return score::crypto::daemon::common::storage::KvDeploymentWriter{}.Write(path, descriptor); + } + + score::mw::log::LogError() << kLogPrefix << "Unsupported deployment format: " << format; + return score::crypto::make_unexpected(score::crypto::daemon::common::DaemonErrorCode::kUnsupportedOperation); +} + +} // namespace score::crypto::daemon::cert_management diff --git a/score/crypto/src/daemon/cert_management/slot/deployment_writer.hpp b/score/crypto/src/daemon/cert_management/slot/deployment_writer.hpp new file mode 100644 index 000000000..03e362383 --- /dev/null +++ b/score/crypto/src/daemon/cert_management/slot/deployment_writer.hpp @@ -0,0 +1,51 @@ +/******************************************************************************** + * 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 + ********************************************************************************/ + +#ifndef SCORE_CRYPTO_SRC_DAEMON_CERT_MANAGEMENT_SLOT_DEPLOYMENT_WRITER_HPP +#define SCORE_CRYPTO_SRC_DAEMON_CERT_MANAGEMENT_SLOT_DEPLOYMENT_WRITER_HPP + +#include "score/crypto/src/common/types.hpp" +#include "score/crypto/src/daemon/common/daemon_error.hpp" +#include "score/crypto/src/daemon/common/storage/deployment_descriptor.hpp" + +#include +#include +#include + +namespace score::crypto::daemon::cert_management +{ + +/// @brief Writes a certificate slot or trust store deployment descriptor. +/// +/// Concurrent writes to the same deployment path require external synchronisation. +class DeploymentWriter +{ + public: + /// @brief Write a DeploymentDescriptor to the given path in the specified format. + /// + /// @param path Absolute path to the descriptor file. + /// @param format Format hint: "kv"; future: "json", "bin". + /// @param descriptor The descriptor to write. + /// @return std::monostate on success, or DaemonErrorCode on failure. + [[nodiscard]] static score::crypto::Expected Write( + const std::string& path, + const std::string& format, + const score::crypto::daemon::common::storage::DeploymentDescriptor& descriptor); + + private: + static constexpr std::string_view kLogPrefix = "[CERT_DEPLOYMENT_WRITER] "; +}; + +} // namespace score::crypto::daemon::cert_management + +#endif // SCORE_CRYPTO_SRC_DAEMON_CERT_MANAGEMENT_SLOT_DEPLOYMENT_WRITER_HPP diff --git a/score/crypto/src/daemon/cert_management/slot/file_backed_slot_handler.cpp b/score/crypto/src/daemon/cert_management/slot/file_backed_slot_handler.cpp new file mode 100644 index 000000000..8619d7996 --- /dev/null +++ b/score/crypto/src/daemon/cert_management/slot/file_backed_slot_handler.cpp @@ -0,0 +1,171 @@ +/******************************************************************************** + * 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/cert_management/slot/file_backed_slot_handler.hpp" + +#include "score/crypto/src/daemon/common/storage/deployment_descriptor.hpp" +#include "score/crypto/src/daemon/common/storage/file_io.hpp" + +namespace score::crypto::daemon::cert_management +{ +namespace +{ +using Error = common::DaemonErrorCode; +using Descriptor = common::storage::DeploymentDescriptor; +namespace file_io = common::storage; + +score::crypto::Expected LoadDescriptor(const CertSlotConfig& slot) +{ + return DeploymentLoader::Load(slot.deployment_path, slot.deployment_format); +} + +score::crypto::Expected SaveDescriptor(const CertSlotConfig& slot, const Descriptor& d) +{ + return DeploymentWriter::Write(slot.deployment_path, slot.deployment_format, d); +} + +std::string ResolvePath(const Descriptor& d, const std::string& section, const std::string& key) +{ + return d.Get(section, key); +} + +score::crypto::FormatType ParseFormat(const std::string& value, score::crypto::FormatType fallback) +{ + if (value == "der") + return score::crypto::FormatType::kDer; + if (value == "pem") + return score::crypto::FormatType::kPem; + return fallback; +} +} // namespace + +score::crypto::Expected FileBackedSlotHandler::LoadCertificate(const CertSlotConfig& slot) +{ + auto descriptor = LoadDescriptor(slot); + if (!descriptor) + return score::crypto::make_unexpected(descriptor.error()); + const auto path = ResolvePath(*descriptor, "certificate", "cert_path"); + if (path.empty()) + return score::crypto::make_unexpected(Error::kKeySlotEmpty); + auto bytes = file_io::ReadFile(path, kMaxCertSize); + if (!bytes) + return score::crypto::make_unexpected(bytes.error()); + + if (slot.integrity_policy == IntegrityPolicy::kRequired) + { + const auto hash = descriptor->Get("certificate", "cert_hash"); + if (hash.empty()) + return score::crypto::make_unexpected(Error::kInvalidArgument); + // The descriptor hash is validated by the certificate verification layer. + } + + const auto format = ParseFormat(descriptor->Get("certificate", "cert_format"), score::crypto::FormatType::kPem); + if (!m_parser) + return score::crypto::make_unexpected(Error::kUnsupportedOperation); + return m_parser->ParseCertificate(bytes->data(), bytes->size(), format); +} + +score::crypto::Expected FileBackedSlotHandler::GetSlotState( + const CertSlotConfig& slot) +{ + auto descriptor = LoadDescriptor(slot); + if (!descriptor) + return score::crypto::make_unexpected(descriptor.error()); + const auto path = ResolvePath(*descriptor, "certificate", "cert_path"); + return (!path.empty() && file_io::FileExists(path)) ? score::crypto::CertificateSlotState::kOccupied + : score::crypto::CertificateSlotState::kEmpty; +} + +score::crypto::Expected FileBackedSlotHandler::GetSlotInfo( + const CertSlotConfig& slot) +{ + auto state = GetSlotState(slot); + if (!state) + return score::crypto::make_unexpected(state.error()); + score::crypto::CertificateSlotInfo info{}; + info.state = *state; + info.has_crl = HasCrl(slot); + return info; +} + +bool FileBackedSlotHandler::HasCrl(const CertSlotConfig& slot) +{ + return m_crl.HasCrl(slot); +} + +score::crypto::Expected FileBackedSlotHandler::StoreCertificate(const CertSlotConfig& slot, + const CertObject& cert) +{ + auto descriptor = LoadDescriptor(slot); + if (!descriptor) + return score::crypto::make_unexpected(descriptor.error()); + auto path = descriptor->Get("certificate", "cert_path"); + if (path.empty()) + path = slot.deployment_path + ".pem"; + // The CertObject already holds serialized bytes; store them as-is and record + // the object's format. Callers needing a specific on-disk encoding convert + // via the provider's ConvertFormat before storing. + const auto bytes = cert.GetRawBytes(); + auto result = file_io::WriteFile(path, bytes); + if (!result) + return result; + descriptor->Set("certificate", "cert_format", cert.GetFormat() == score::crypto::FormatType::kDer ? "der" : "pem"); + descriptor->Set("certificate_metadata", "subject", std::string(cert.GetSubject())); + descriptor->Set("certificate_metadata", "issuer", std::string(cert.GetIssuer())); + descriptor->Set("certificate_metadata", "not_before", std::to_string(cert.GetNotBefore())); + descriptor->Set("certificate_metadata", "not_after", std::to_string(cert.GetNotAfter())); + descriptor->Set("certificate_metadata", "is_ca", cert.IsCA() ? "true" : "false"); + return SaveDescriptor(slot, *descriptor); +} + +score::crypto::Expected FileBackedSlotHandler::ClearSlot(const CertSlotConfig& slot) +{ + auto descriptor = LoadDescriptor(slot); + if (!descriptor) + return score::crypto::make_unexpected(descriptor.error()); + const auto cert = descriptor->Get("certificate", "cert_path"); + if (!cert.empty()) + file_io::RemoveFile(cert); + // Preserve crl_path so a future StoreCrl re-uses the same location. + // Only the file and volatile metadata (format, next_update) are discarded. + const auto crl_path = descriptor->Get("crl", "crl_path"); + if (!crl_path.empty()) + file_io::RemoveFile(crl_path); + descriptor->RemoveSection("crl"); + if (!crl_path.empty()) + descriptor->Set("crl", "crl_path", crl_path); + descriptor->RemoveSection("certificate_metadata"); + return SaveDescriptor(slot, *descriptor); +} + +score::crypto::Expected, Error> FileBackedSlotHandler::LoadCrl(const CertSlotConfig& slot) +{ + return m_crl.LoadCrl(slot); +} + +score::crypto::Expected FileBackedSlotHandler::StoreCrl(const CertSlotConfig& slot, + score::crypto::span data, + score::crypto::FormatType format) +{ + return m_crl.StoreCrl(slot, data, format); +} + +score::crypto::Expected FileBackedSlotHandler::ClearCrl(const CertSlotConfig& slot) +{ + return m_crl.ClearCrl(slot); +} + +score::crypto::Expected FileBackedSlotHandler::GetCrlNextUpdate(const CertSlotConfig& slot) +{ + return m_crl.GetCrlNextUpdate(slot); +} +} // namespace score::crypto::daemon::cert_management diff --git a/score/crypto/src/daemon/cert_management/slot/file_backed_slot_handler.hpp b/score/crypto/src/daemon/cert_management/slot/file_backed_slot_handler.hpp new file mode 100644 index 000000000..86e5d7154 --- /dev/null +++ b/score/crypto/src/daemon/cert_management/slot/file_backed_slot_handler.hpp @@ -0,0 +1,64 @@ +/******************************************************************************** + * 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 + ********************************************************************************/ +/******************************************************************************** + * Copyright (c) 2026 Contributors to the Eclipse Foundation + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ +#ifndef SCORE_CRYPTO_SRC_DAEMON_CERT_MANAGEMENT_SLOT_FILE_BACKED_SLOT_HANDLER_HPP +#define SCORE_CRYPTO_SRC_DAEMON_CERT_MANAGEMENT_SLOT_FILE_BACKED_SLOT_HANDLER_HPP + +#include "score/crypto/src/daemon/cert_management/interfaces/i_cert_slot_handler.hpp" +#include "score/crypto/src/daemon/cert_management/slot/crl_handler.hpp" +#include "score/crypto/src/daemon/cert_management/slot/deployment_loader.hpp" +#include "score/crypto/src/daemon/cert_management/slot/deployment_writer.hpp" +#include "score/crypto/src/daemon/provider/cert_management/i_cert_parser.hpp" + +#include +#include + +namespace score::crypto::daemon::cert_management +{ +class FileBackedSlotHandler final : public ICertSlotHandler +{ + public: + static constexpr std::size_t kMaxCertSize = 64U * 1024U; // 64 KiB — ample for any X.509 cert or chain + + explicit FileBackedSlotHandler(provider::cert_management::ICertParser::Sptr parser) : m_parser{std::move(parser)} {} + ~FileBackedSlotHandler() override = default; + + FileBackedSlotHandler(const FileBackedSlotHandler&) = delete; + FileBackedSlotHandler& operator=(const FileBackedSlotHandler&) = delete; + + score::crypto::Expected LoadCertificate(const CertSlotConfig&) override; + score::crypto::Expected GetSlotState( + const CertSlotConfig&) override; + score::crypto::Expected GetSlotInfo( + const CertSlotConfig&) override; + bool HasCrl(const CertSlotConfig&) override; + score::crypto::Expected StoreCertificate(const CertSlotConfig&, + const CertObject&) override; + score::crypto::Expected ClearSlot(const CertSlotConfig&) override; + score::crypto::Expected, common::DaemonErrorCode> LoadCrl(const CertSlotConfig&) override; + score::crypto::Expected StoreCrl(const CertSlotConfig&, + score::crypto::span, + score::crypto::FormatType) override; + score::crypto::Expected ClearCrl(const CertSlotConfig&) override; + score::crypto::Expected GetCrlNextUpdate(const CertSlotConfig&) override; + + private: + using Handler = CrlHandler; + provider::cert_management::ICertParser::Sptr m_parser; + Handler m_crl; +}; +} // namespace score::crypto::daemon::cert_management +#endif diff --git a/score/crypto/src/daemon/cert_management/slot/slot_registry.cpp b/score/crypto/src/daemon/cert_management/slot/slot_registry.cpp new file mode 100644 index 000000000..9ce3dba32 --- /dev/null +++ b/score/crypto/src/daemon/cert_management/slot/slot_registry.cpp @@ -0,0 +1,148 @@ +/******************************************************************************** + * 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/cert_management/slot/slot_registry.hpp" +#include "score/crypto/src/daemon/cert_management/policy/access_policy_enforcer.hpp" +#include "score/crypto/src/daemon/control_plane/control_protocol.h" + +#include "score/mw/log/logging.h" + +namespace score::crypto::daemon::cert_management +{ + +CertSlotHandle CertSlotRegistry::RegisterSlot(CertSlotConfig config) +{ + std::lock_guard lock(m_mutex); + const std::string name = config.slot_name; + + if (m_name_index.find(name) != m_name_index.end()) + { + score::mw::log::LogError() << kLogPrefix << "Duplicate slot name ignored: '" << name << "'"; + return CertSlotHandle{}; + } + + const auto index = static_cast(m_registry.size()); + + CertSlotRegistryEntry entry{}; + entry.config = std::move(config); + + m_name_index[name] = index; + m_registry.push_back(std::move(entry)); + return CertSlotHandle{index}; +} + +score::crypto::Expected CertSlotRegistry::ResolveSlot( + const std::string& slot_name, + data_manager::ClientId client_id) const +{ + auto it = m_name_index.find(slot_name); + if (it == m_name_index.end()) + { + return score::crypto::make_unexpected(score::crypto::daemon::common::DaemonErrorCode::kInvalidResourceId); + } + + const auto index = it->second; + const auto& entry = m_registry[index]; + + auto access_result = AccessPolicyEnforcer::CheckSlotAccess(entry.config, client_id); + if (!access_result.has_value()) + { + return score::crypto::make_unexpected(access_result.error()); + } + + return CertSlotHandle{index}; +} + +score::crypto::Expected +CertSlotRegistry::ResolveSlotInternal(const std::string& slot_name) const +{ + auto it = m_name_index.find(slot_name); + if (it == m_name_index.end()) + { + return score::crypto::make_unexpected(score::crypto::daemon::common::DaemonErrorCode::kInvalidResourceId); + } + return CertSlotHandle{it->second}; +} + +score::crypto::Expected +CertSlotRegistry::GetConfig(CertSlotHandle handle) const +{ + if (!IsValidHandle(handle)) + { + return score::crypto::make_unexpected(score::crypto::daemon::common::DaemonErrorCode::kInvalidResourceId); + } + return &m_registry[handle.index].config; +} + +std::size_t CertSlotRegistry::GetSlotCount() const noexcept +{ + return m_registry.size(); +} + +std::vector CertSlotRegistry::GetAllHandles() const +{ + std::lock_guard lock(m_mutex); + std::vector handles; + handles.reserve(m_registry.size()); + for (uint32_t i = 0U; i < static_cast(m_registry.size()); ++i) + { + handles.push_back(CertSlotHandle{i}); + } + return handles; +} + +bool CertSlotRegistry::IsValidHandle(CertSlotHandle handle) const noexcept +{ + return handle.IsValid() && handle.index < static_cast(m_registry.size()); +} + +void CertSlotRegistry::RegisterAppResource(uint32_t uid, + const std::string& app_resource_id, + const std::string& slot_name) +{ + std::lock_guard lock(m_mutex); + auto& uid_map = m_app_resource_map[uid]; + if (uid_map.find(app_resource_id) != uid_map.end()) + { + score::mw::log::LogError() << kLogPrefix << "Duplicate app resource mapping ignored: uid=" << uid + << " resource='" << app_resource_id << "'"; + return; + } + uid_map[app_resource_id] = slot_name; +} + +score::crypto::Expected +CertSlotRegistry::ResolveAppResource(const std::string& app_resource_id, data_manager::ClientId client_id) const +{ + const uint32_t uid = control_plane::protocol::GetUidFromClientId(client_id); + + std::string slot_name; + { + std::lock_guard lock(m_mutex); + auto uid_it = m_app_resource_map.find(uid); + if (uid_it == m_app_resource_map.end()) + { + return score::crypto::make_unexpected(score::crypto::daemon::common::DaemonErrorCode::kInvalidResourceId); + } + auto res_it = uid_it->second.find(app_resource_id); + if (res_it == uid_it->second.end()) + { + return score::crypto::make_unexpected(score::crypto::daemon::common::DaemonErrorCode::kInvalidResourceId); + } + slot_name = res_it->second; + } + + return ResolveSlot(slot_name, client_id); +} + +} // namespace score::crypto::daemon::cert_management diff --git a/score/crypto/src/daemon/cert_management/slot/slot_registry.hpp b/score/crypto/src/daemon/cert_management/slot/slot_registry.hpp new file mode 100644 index 000000000..b91886033 --- /dev/null +++ b/score/crypto/src/daemon/cert_management/slot/slot_registry.hpp @@ -0,0 +1,137 @@ +/******************************************************************************** + * 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 + ********************************************************************************/ + +#ifndef SCORE_CRYPTO_SRC_DAEMON_CERT_MANAGEMENT_SLOT_SLOT_REGISTRY_HPP +#define SCORE_CRYPTO_SRC_DAEMON_CERT_MANAGEMENT_SLOT_SLOT_REGISTRY_HPP + +#include "score/crypto/src/daemon/cert_management/interfaces/cert_slot_config.hpp" +#include "score/crypto/src/daemon/cert_management/interfaces/cert_types.hpp" +#include "score/crypto/src/daemon/common/daemon_error.hpp" +#include "score/crypto/src/daemon/data_manager/data_node.hpp" + +#include +#include +#include +#include +#include +#include +#include + +namespace score::crypto::daemon::cert_management +{ + +/// @brief Internal registry entry for a certificate slot. Owned by CertSlotRegistry. +struct CertSlotRegistryEntry +{ + CertSlotConfig config; +}; + +/// @brief Groups the stable identity/location parameters for cert registration operations. +struct CertRegistrationParams +{ + data_manager::ClientId client_id{0U}; + data_manager::DataNodeId parent_id{0U}; + CertSlotHandle slot_handle{}; ///< Non-default only when loading from a slot. +}; + +/// @brief Central registry for all certificate slot configurations. +/// +/// Single source of truth for slot configs and per-slot state. +/// Per-connection CertSlotDataNodes hold only a lightweight CertSlotHandle +/// referencing entries in this registry. +/// +/// Thread safety: all public methods serialise on an internal mutex. +class CertSlotRegistry : public std::enable_shared_from_this +{ + public: + using Sptr = std::shared_ptr; + + CertSlotRegistry() = default; + ~CertSlotRegistry() = default; + + CertSlotRegistry(const CertSlotRegistry&) = delete; + CertSlotRegistry& operator=(const CertSlotRegistry&) = delete; + CertSlotRegistry(CertSlotRegistry&&) = delete; + CertSlotRegistry& operator=(CertSlotRegistry&&) = delete; + + /// @brief Register a certificate slot with the registry. + /// + /// @param config Fully-populated slot configuration. + /// @return A lightweight handle to the registered slot. + CertSlotHandle RegisterSlot(CertSlotConfig config); + + /// @brief Resolve slot name + client_id → CertSlotHandle. + /// + /// Checks access policy before returning the handle. + score::crypto::Expected ResolveSlot( + const std::string& slot_name, + data_manager::ClientId client_id) const; + + /// @brief Resolve slot name → CertSlotHandle without access-policy checks. + /// + /// For daemon-internal callers (e.g. TrustStoreManager) that operate on + /// integrator-configured slot names and are not subject to client access + /// control. Must not be called from client-request code paths. + score::crypto::Expected ResolveSlotInternal( + const std::string& slot_name) const; + + /// @brief Read-only access to config via handle. + score::crypto::Expected GetConfig( + CertSlotHandle handle) const; + + /// @brief Get the total number of registered slots. + std::size_t GetSlotCount() const noexcept; + + /// @brief Return a snapshot of all registered slot configs. + /// + /// Used by services that need to iterate all slots (e.g., DeleteExpiredCrls). + std::vector GetAllHandles() const; + + /// @brief Register an application-local resource ID mapping. + /// + /// Called at startup by ConfigDrivenSlotCatalog for each AppCertSlotEntry. + /// Maps (uid, app_resource_id) → actual cert slot name in this registry. + /// + /// @param uid UID of the application. + /// @param app_resource_id Application-local name (e.g., "vehicle_tls_cert"). + /// @param slot_name Actual slot name registered in this registry. + void RegisterAppResource(uint32_t uid, const std::string& app_resource_id, const std::string& slot_name); + + /// @brief Resolve an application resource ID to a CertSlotHandle. + /// + /// Looks up (uid, app_resource_id) in the per-UID resource map, then + /// resolves the resulting slot name with access-policy checks. + /// + /// @param app_resource_id Application-local resource name. + /// @param client_id Composite PID|UID of the requesting connection. + /// @return CertSlotHandle on success, or kInvalidResourceId if no mapping exists. + score::crypto::Expected ResolveAppResource( + const std::string& app_resource_id, + data_manager::ClientId client_id) const; + + private: + bool IsValidHandle(CertSlotHandle handle) const noexcept; + + std::vector m_registry; + std::unordered_map m_name_index; + /// Per-UID app resource map: uid → { app_resource_id → slot_name }. + /// Populated at startup by RegisterAppResource(); read-only after that. + std::unordered_map> m_app_resource_map; + mutable std::mutex m_mutex; + + static constexpr std::string_view kLogPrefix = "[CERT_SLOT_REGISTRY] "; +}; + +} // namespace score::crypto::daemon::cert_management + +#endif // SCORE_CRYPTO_SRC_DAEMON_CERT_MANAGEMENT_SLOT_SLOT_REGISTRY_HPP diff --git a/score/crypto/src/daemon/cert_management/tests/BUILD b/score/crypto/src/daemon/cert_management/tests/BUILD new file mode 100644 index 000000000..3d460d9c2 --- /dev/null +++ b/score/crypto/src/daemon/cert_management/tests/BUILD @@ -0,0 +1,107 @@ +# ******************************************************************************* +# Copyright (c) 2026 Contributors to the Eclipse Foundation +# SPDX-License-Identifier: Apache-2.0 +# ******************************************************************************* + +load("@rules_cc//cc:cc_test.bzl", "cc_test") + +cc_test( + name = "test_cert_registry", + srcs = ["core/test_cert_registry.cpp"], + deps = [ + "//score/crypto/src/daemon/cert_management:cert_management", + "//score/crypto/src/daemon/cert_management:cert_management_headers", + "@googletest//:gtest_main", + ], +) + +cc_test( + name = "test_access_policy_enforcer", + srcs = ["core/test_access_policy_enforcer.cpp"], + deps = [ + "//score/crypto/src/daemon/cert_management:cert_management", + "//score/crypto/src/daemon/cert_management:cert_management_headers", + "@googletest//:gtest_main", + ], +) + +cc_test( + name = "test_file_backed_slot_handler", + srcs = ["slot/test_file_backed_slot_handler.cpp"], + data = [ + "//score/tests/test_vectors/certificate:certificate_test_vectors", + ], + target_compatible_with = select({ + "//score/crypto/src/backend:openssl_backend_active": [], + "//conditions:default": ["@platforms//:incompatible"], + }), + deps = [ + "//score/crypto/src/daemon/cert_management:cert_management", + "//score/crypto/src/daemon/cert_management:cert_management_headers", + "//score/crypto/src/daemon/common/storage:kv_deployment", + "//score/crypto/src/daemon/provider/score_provider/openssl:openssl_cert_management_headers", + "//score/crypto/src/daemon/provider/score_provider/openssl:openssl_cert_parser_library", + "@googletest//:gtest_main", + ], +) + +cc_test( + name = "test_trust_store_manager", + srcs = ["truststore/test_trust_store_manager.cpp"], + data = [ + "//score/tests/test_vectors/certificate:certificate_test_vectors", + ], + target_compatible_with = select({ + "//score/crypto/src/backend:openssl_backend_active": [], + "//conditions:default": ["@platforms//:incompatible"], + }), + deps = [ + "//score/crypto/src/daemon/cert_management:cert_management", + "//score/crypto/src/daemon/cert_management:cert_management_headers", + "//score/crypto/src/daemon/common/storage:kv_deployment", + "//score/crypto/src/daemon/provider/score_provider/openssl:openssl_cert_management_headers", + "//score/crypto/src/daemon/provider/score_provider/openssl:openssl_cert_parser_library", + "@googletest//:gtest_main", + ], +) + +cc_test( + name = "test_cert_management_service", + srcs = ["service/test_cert_management_service.cpp"], + data = [ + "//score/tests/test_vectors/certificate:certificate_test_vectors", + ], + target_compatible_with = select({ + "//score/crypto/src/backend:openssl_backend_active": [], + "//conditions:default": ["@platforms//:incompatible"], + }), + deps = [ + "//score/crypto/src/daemon/cert_management:cert_management", + "//score/crypto/src/daemon/cert_management:cert_management_headers", + "//score/crypto/src/daemon/common/storage:kv_deployment", + "//score/crypto/src/daemon/data_manager:data_manager", + "//score/crypto/src/daemon/provider/score_provider/openssl:openssl_cert_management_headers", + "//score/crypto/src/daemon/provider/score_provider/openssl:openssl_cert_parser_library", + "@googletest//:gtest_main", + ], +) + +cc_test( + name = "test_certificate_management_integration", + srcs = ["integration/test_certificate_management_integration.cpp"], + data = [ + "//score/tests/test_vectors/certificate:certificate_test_vectors", + ], + target_compatible_with = select({ + "//score/crypto/src/backend:openssl_backend_active": [], + "//conditions:default": ["@platforms//:incompatible"], + }), + deps = [ + "//score/crypto/src/daemon/cert_management:cert_management", + "//score/crypto/src/daemon/cert_management:cert_management_headers", + "//score/crypto/src/daemon/provider/score_provider/openssl:openssl_cert_management_headers", + "//score/crypto/src/daemon/provider/score_provider/openssl:openssl_cert_parser_library", + "//score/crypto/src/daemon/common/storage:kv_deployment", + "@googletest//:gtest_main", + ], +) diff --git a/score/crypto/src/daemon/cert_management/tests/core/test_access_policy_enforcer.cpp b/score/crypto/src/daemon/cert_management/tests/core/test_access_policy_enforcer.cpp new file mode 100644 index 000000000..ada79313d --- /dev/null +++ b/score/crypto/src/daemon/cert_management/tests/core/test_access_policy_enforcer.cpp @@ -0,0 +1,69 @@ +#include "score/crypto/src/daemon/cert_management/interfaces/cert_slot_config.hpp" +#include "score/crypto/src/daemon/cert_management/interfaces/trust_store_config.hpp" +#include "score/crypto/src/daemon/cert_management/policy/access_policy_enforcer.hpp" +#include + +#include + +namespace +{ +using namespace score::crypto::daemon::cert_management; + +score::crypto::daemon::data_manager::ClientId ClientId(std::uint32_t pid, std::uint32_t uid) +{ + union + { + score::crypto::daemon::data_manager::ClientId id; + struct + { + std::uint32_t process_id; + std::uint32_t user_id; + } parts; + } value{0U}; + value.parts.process_id = pid; + value.parts.user_id = uid; + return value.id; +} + +TEST(AccessPolicyEnforcerTest, AllowsUnrestrictedSlotRead) +{ + CertSlotConfig slot; + EXPECT_TRUE(AccessPolicyEnforcer::CheckSlotAccess(slot, ClientId(100U, 200U)).has_value()); +} + +TEST(AccessPolicyEnforcerTest, DeniesSlotWriteWhenUidIsNotAllowed) +{ + CertSlotConfig slot; + slot.access_policy.allowed_write_uids = {100U}; + + EXPECT_FALSE(AccessPolicyEnforcer::CheckWritePermission(slot, ClientId(1U, 200U)).has_value()); +} + +TEST(AccessPolicyEnforcerTest, AllowsSlotWriteForConfiguredUid) +{ + CertSlotConfig slot; + slot.access_policy.allowed_write_uids = {200U}; + + EXPECT_TRUE(AccessPolicyEnforcer::CheckWritePermission(slot, ClientId(1U, 200U)).has_value()); +} + +TEST(AccessPolicyEnforcerTest, AllowsUnrestrictedTrustStoreRead) +{ + TrustStoreConfig store; + EXPECT_TRUE(AccessPolicyEnforcer::CheckTrustStoreAccess(store, ClientId(1U, 200U)).has_value()); +} + +TEST(AccessPolicyEnforcerTest, DeniesTrustStoreWriteWhenAllowlistIsEmpty) +{ + TrustStoreConfig store; + EXPECT_FALSE(AccessPolicyEnforcer::CheckTrustStoreWritePermission(store, ClientId(1U, 200U)).has_value()); +} + +TEST(AccessPolicyEnforcerTest, AllowsTrustStoreWriteForConfiguredUid) +{ + TrustStoreConfig store; + store.access_policy.allowed_write_uids = {200U}; + + EXPECT_TRUE(AccessPolicyEnforcer::CheckTrustStoreWritePermission(store, ClientId(1U, 200U)).has_value()); +} +} // namespace diff --git a/score/crypto/src/daemon/cert_management/tests/core/test_cert_registry.cpp b/score/crypto/src/daemon/cert_management/tests/core/test_cert_registry.cpp new file mode 100644 index 000000000..619eec3c9 --- /dev/null +++ b/score/crypto/src/daemon/cert_management/tests/core/test_cert_registry.cpp @@ -0,0 +1,108 @@ +#include "score/crypto/src/daemon/cert_management/core/cert_entry.hpp" +#include "score/crypto/src/daemon/cert_management/core/cert_registry.hpp" + +#include + +#include +#include +#include + +namespace +{ +using namespace score::crypto::daemon::cert_management; + +CertObject::Sptr MakeCertificate() +{ + CertChainMetadata metadata; + metadata.subject_canonical = "CN=registry-test"; + metadata.issuer_canonical = "CN=registry-test"; + metadata.fingerprint = std::vector(32U, 0x42U); + return std::make_shared( + std::move(metadata), std::vector{0x01U, 0x02U}, score::crypto::FormatType::kDer); +} + +TEST(CertRegistryTest, RegistersFindsAndUnregistersEphemeralCertificate) +{ + CertRegistry registry; + auto entry = std::make_shared(MakeCertificate()); + + const auto id = registry.RegisterEphemeralCert(entry); + + ASSERT_NE(id, 0U); + EXPECT_EQ(registry.Size(), 1U); + EXPECT_EQ(registry.FindById(id), entry); + EXPECT_TRUE(registry.Unregister(id)); + EXPECT_EQ(registry.Size(), 0U); + EXPECT_FALSE(registry.FindById(id)); +} + +TEST(CertRegistryTest, RejectsDuplicateCertificateSlotRegistration) +{ + CertRegistry registry; + const CertSlotHandle slot{7U}; + auto first = std::make_shared(MakeCertificate(), slot); + auto second = std::make_shared(MakeCertificate(), slot); + + ASSERT_NE(registry.RegisterSlotCert(slot, first), 0U); + EXPECT_EQ(registry.RegisterSlotCert(slot, second), 0U); + EXPECT_EQ(registry.FindBySlot(slot), first); + EXPECT_EQ(registry.Size(), 1U); +} + +// Two ephemeral certs registered under two different client IDs. +// CleanupClient for one must remove exactly that client's entries; the other +// client's entry must remain intact. +TEST(CertRegistryTest, CleanupClient_RemovesOnlyThatClientsEntries) +{ + using ClientId = score::crypto::daemon::data_manager::ClientId; + static constexpr ClientId kClientA = 1U; + static constexpr ClientId kClientB = 2U; + + CertRegistry registry; + + auto entry_a = std::make_shared(MakeCertificate()); + auto entry_b = std::make_shared(MakeCertificate()); + entry_a->AddRef(kClientA); + entry_b->AddRef(kClientB); + + const auto id_a = registry.RegisterEphemeralCert(entry_a); + const auto id_b = registry.RegisterEphemeralCert(entry_b); + ASSERT_NE(id_a, 0U); + ASSERT_NE(id_b, 0U); + EXPECT_EQ(registry.Size(), 2U); + + registry.CleanupClient(kClientA); + + // Client A's entry is gone. + EXPECT_EQ(registry.FindById(id_a), nullptr); + // Client B's entry survives. + EXPECT_EQ(registry.FindById(id_b), entry_b); + EXPECT_EQ(registry.Size(), 1U); +} + +// A slot cert registered for one handle remains findable via both ID and slot +// after a CleanupClient for an unrelated client. +TEST(CertRegistryTest, FindBySlot_SlotCertPersistsAfterUnrelatedCleanup) +{ + using ClientId = score::crypto::daemon::data_manager::ClientId; + static constexpr ClientId kClientA = 10U; + static constexpr ClientId kClientB = 20U; + + CertRegistry registry; + const CertSlotHandle slot{3U}; + + auto slot_entry = std::make_shared(MakeCertificate(), slot); + auto ephemeral_entry = std::make_shared(MakeCertificate()); + slot_entry->AddRef(kClientA); + ephemeral_entry->AddRef(kClientB); + + const auto slot_id = registry.RegisterSlotCert(slot, slot_entry); + ASSERT_NE(slot_id, 0U); + ASSERT_NE(registry.RegisterEphemeralCert(ephemeral_entry), 0U); + + registry.CleanupClient(kClientB); + + EXPECT_EQ(registry.FindBySlot(slot), slot_entry); + EXPECT_EQ(registry.FindById(slot_id), slot_entry); +} +} // namespace diff --git a/score/crypto/src/daemon/cert_management/tests/integration/test_certificate_management_integration.cpp b/score/crypto/src/daemon/cert_management/tests/integration/test_certificate_management_integration.cpp new file mode 100644 index 000000000..4fa835ed5 --- /dev/null +++ b/score/crypto/src/daemon/cert_management/tests/integration/test_certificate_management_integration.cpp @@ -0,0 +1,155 @@ +#include "score/crypto/src/daemon/cert_management/slot/config_driven_slot_catalog.hpp" +#include "score/crypto/src/daemon/cert_management/slot/deployment_loader.hpp" +#include "score/crypto/src/daemon/cert_management/slot/file_backed_slot_handler.hpp" +#include "score/crypto/src/daemon/cert_management/truststore/config_driven_trust_store_catalog.hpp" +#include "score/crypto/src/daemon/common/storage/kv/kv_deployment_writer.hpp" +#include "score/crypto/src/daemon/provider/score_provider/openssl/cert_management/openssl_cert_parser.hpp" + +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +namespace +{ +namespace cert = score::crypto::daemon::cert_management; +namespace config = score::crypto::daemon::config; +namespace storage = score::crypto::daemon::common::storage; +namespace openssl = score::crypto::daemon::provider::score_provider::openssl; + +class CertificateManagementIntegrationTest : public ::testing::Test +{ + protected: + void SetUp() override + { + m_directory = std::filesystem::temp_directory_path() / "score_cert_management_integration"; + std::filesystem::remove_all(m_directory); + std::filesystem::create_directories(m_directory); + m_descriptor_path = m_directory / "device_root.kv"; + m_certificate_path = m_directory / "device_root.pem"; + m_trust_store_path = m_directory / "tls_roots.kv"; + + ASSERT_TRUE(std::filesystem::copy_file("score/tests/test_vectors/certificate/certificate.pem", + m_certificate_path, + std::filesystem::copy_options::overwrite_existing)); + ASSERT_TRUE(std::filesystem::copy_file("score/tests/test_vectors/certificate/certificate_updated.pem", + m_directory / "certificate_updated.pem", + std::filesystem::copy_options::overwrite_existing)); + + storage::DeploymentDescriptor slot_desc; + slot_desc.Set("certificate", "cert_path", m_certificate_path.string()); + slot_desc.Set("certificate", "cert_format", "pem"); + ASSERT_TRUE(storage::KvDeploymentWriter{}.Write(m_descriptor_path.string(), slot_desc).has_value()); + + ASSERT_TRUE(std::filesystem::copy_file("score/tests/test_vectors/certificate/trust_store.kv", + m_trust_store_path, + std::filesystem::copy_options::overwrite_existing)); + + config::CertificateConfig::CertSlotEntry slot; + slot.slot_name = "device/root-ca"; + slot.storage_backend = "DEFAULT"; + slot.deployment_path = m_descriptor_path.string(); + slot.deployment_format = "kv"; + slot.allowed_uids = {0U}; + slot.allowed_write_uids = {0U}; + m_config.AddSlotEntry(std::move(slot)); + + config::CertificateConfig::TrustStoreEntry trust_store; + trust_store.store_name = "tls-roots"; + trust_store.members.push_back( + {"device/root-ca", config::CertificateConfig::TrustStoreMemberKind::kSharedStatic}); + trust_store.deployment_path = m_trust_store_path.string(); + trust_store.deployment_format = "kv"; + trust_store.allowed_uids = {0U}; + trust_store.allowed_write_uids = {0U}; + m_config.AddTrustStoreEntry(std::move(trust_store)); + m_config.AddAppCertSlotEntry({0U, "device_certificate", "device/root-ca"}); + m_config.AddAppTrustStoreEntry({0U, "tls_roots", "tls-roots"}); + + m_parser = std::make_shared(1U); + m_slot_registry = std::make_shared(); + cert::ConfigDrivenSlotCatalog slot_catalog{m_config}; + slot_catalog.Load(*m_slot_registry); + + m_trust_store_manager = std::make_shared(); + cert::ConfigDrivenTrustStoreCatalog trust_store_catalog{m_config}; + trust_store_catalog.Load(*m_trust_store_manager, m_slot_registry, [this](const cert::CertSlotConfig&) { + return std::make_shared(m_parser); + }); + } + + void TearDown() override + { + std::error_code error; + std::filesystem::remove_all(m_directory, error); + } + + std::string ReadFile(const std::filesystem::path& path) const + { + std::ifstream input{path}; + return {std::istreambuf_iterator{input}, std::istreambuf_iterator{}}; + } + + std::filesystem::path m_directory; + std::filesystem::path m_descriptor_path; + std::filesystem::path m_certificate_path; + std::filesystem::path m_trust_store_path; + config::CertificateConfig m_config; + std::shared_ptr m_parser; + cert::CertSlotRegistry::Sptr m_slot_registry; + cert::TrustStoreManager::Sptr m_trust_store_manager; +}; + +TEST_F(CertificateManagementIntegrationTest, LoadsPersistsUpdatesAndInvalidatesTrustStoreAnchor) +{ + const auto slot = m_slot_registry->ResolveAppResource("device_certificate", 0U); + ASSERT_TRUE(slot.has_value()); + const auto slot_config = m_slot_registry->GetConfig(*slot); + ASSERT_TRUE(slot_config.has_value()); + + auto slot_handler = std::make_shared(m_parser); + const auto initial = slot_handler->LoadCertificate(**slot_config); + ASSERT_TRUE(initial.has_value()); + EXPECT_EQ((*initial)->GetSubject(), "CN=cert-management-test,O=Eclipse"); + EXPECT_TRUE((*initial)->IsCA()); + + auto trust_store_handle = m_trust_store_manager->ResolveAppResource("tls_roots", 0U); + ASSERT_TRUE(trust_store_handle.has_value()); + auto trust_store = m_trust_store_manager->GetStore(trust_store_handle->index); + ASSERT_NE(trust_store, nullptr); + + const auto initial_anchors = trust_store->GetAnchors(); + ASSERT_TRUE(initial_anchors.has_value()); + ASSERT_EQ(initial_anchors->size(), 1U); + EXPECT_EQ((*initial_anchors)[0]->GetSubject(), "CN=cert-management-test,O=Eclipse"); + + const auto updated_pem = ReadFile(m_directory / "certificate_updated.pem"); + ASSERT_FALSE(updated_pem.empty()); + const auto updated = m_parser->ParseCertificate( + reinterpret_cast(updated_pem.data()), updated_pem.size(), score::crypto::FormatType::kPem); + ASSERT_TRUE(updated.has_value()); + ASSERT_TRUE(slot_handler->StoreCertificate(**slot_config, **updated).has_value()); + + const auto descriptor = cert::DeploymentLoader::Load(m_descriptor_path.string(), "kv"); + ASSERT_TRUE(descriptor.has_value()); + EXPECT_EQ(descriptor->Get("certificate_metadata", "subject"), "CN=cert-management-updated,O=Eclipse"); + EXPECT_EQ(descriptor->Get("certificate_metadata", "issuer"), "CN=cert-management-updated,O=Eclipse"); + EXPECT_EQ(descriptor->Get("certificate_metadata", "is_ca"), "true"); + + m_trust_store_manager->NotifySlotChanged(trust_store_handle->index, *slot); + const auto updated_anchors = trust_store->GetAnchors(); + ASSERT_TRUE(updated_anchors.has_value()); + ASSERT_EQ(updated_anchors->size(), 1U); + EXPECT_EQ((*updated_anchors)[0]->GetSubject(), "CN=cert-management-updated,O=Eclipse"); + EXPECT_FALSE(std::equal((*updated_anchors)[0]->GetFingerprint().begin(), + (*updated_anchors)[0]->GetFingerprint().end(), + (*initial_anchors)[0]->GetFingerprint().begin(), + (*initial_anchors)[0]->GetFingerprint().end())); +} +} // namespace diff --git a/score/crypto/src/daemon/cert_management/tests/service/test_cert_management_service.cpp b/score/crypto/src/daemon/cert_management/tests/service/test_cert_management_service.cpp new file mode 100644 index 000000000..297c50e7d --- /dev/null +++ b/score/crypto/src/daemon/cert_management/tests/service/test_cert_management_service.cpp @@ -0,0 +1,344 @@ +// ******************************************************************************* +// Copyright (c) 2026 Contributors to the Eclipse Foundation +// SPDX-License-Identifier: Apache-2.0 +// ******************************************************************************* +// +// Component-level tests for CertManagementService. +// +// Uses a real DataManager, real CertSlotRegistry, real TrustStoreManager, and +// the real OpenSslCertParser + FileBackedSlotHandler backed by the central test +// vector at score/tests/test_vectors/certificate/certificate.pem. +// +// All cert bytes come from the central test-vector directory so that these +// tests share the same known-answer inputs as the integration and provider +// parser tests. +// +// Test subjects: +// - Slot resolution and cert load via the executor call sequence +// (ResolveCertSlot → ResolveSlotForOperation → LoadOrShare) +// - ResolveCertForOperation round-trip +// - Cert node release and subsequent unresolvability +// - LoadOrShare deduplication — second call for the same slot shares the +// existing CertEntry without a second handler LoadCertificate call +// - Mediator-style client cleanup isolates clients — purging one leaves the other intact +// - NotifySlotCertChanged propagates through TrustStoreManager anchor cache + +#include "score/crypto/src/daemon/cert_management/core/cert_management_service.hpp" +#include "score/crypto/src/daemon/cert_management/slot/file_backed_slot_handler.hpp" +#include "score/crypto/src/daemon/cert_management/truststore/trust_store_manager.hpp" +#include "score/crypto/src/daemon/common/storage/kv/kv_deployment_writer.hpp" +#include "score/crypto/src/daemon/data_manager/data_manager.hpp" +#include "score/crypto/src/daemon/provider/score_provider/openssl/cert_management/openssl_cert_parser.hpp" + +#include + +#include +#include +#include +#include +#include +#include + +namespace +{ +namespace cert = score::crypto::daemon::cert_management; +namespace dm = score::crypto::daemon::data_manager; +namespace openssl_ns = score::crypto::daemon::provider::score_provider::openssl; +namespace storage = score::crypto::daemon::common::storage; + +// client_id layout: upper 32 bits = UID, lower 32 bits = PID. +// Using UID=0 to match RegisterAppResource(uid=0, ...) calls below. +static constexpr dm::ClientId kClientA = 1ULL; // pid=1, uid=0 +static constexpr dm::ClientId kClientB = 2ULL; // pid=2, uid=0 + +static constexpr std::string_view kSlotName = "test/root-ca"; +static constexpr std::string_view kAppResource = "root_ca"; +static constexpr std::string_view kSubjectInitial = "CN=cert-management-test,O=Eclipse"; +static constexpr std::string_view kSubjectUpdated = "CN=cert-management-updated,O=Eclipse"; + +// Common fixture: wires up a single file-backed cert slot and the service. +class CertManagementServiceTest : public ::testing::Test +{ + protected: + void SetUp() override + { + m_dir = std::filesystem::temp_directory_path() / "score_cert_mgmt_service"; + std::filesystem::remove_all(m_dir); + std::filesystem::create_directories(m_dir); + m_cert_path = m_dir / "root_ca.pem"; + m_cert_updated_path = m_dir / "root_ca_updated.pem"; + m_descriptor_path = m_dir / "root_ca.kv"; + + ASSERT_TRUE(std::filesystem::copy_file("score/tests/test_vectors/certificate/certificate.pem", + m_cert_path, + std::filesystem::copy_options::overwrite_existing)); + ASSERT_TRUE(std::filesystem::copy_file("score/tests/test_vectors/certificate/certificate_updated.pem", + m_cert_updated_path, + std::filesystem::copy_options::overwrite_existing)); + + // Write the KV descriptor pointing at the initial cert. + storage::DeploymentDescriptor desc; + desc.Set("certificate", "cert_path", m_cert_path.string()); + desc.Set("certificate", "cert_format", "pem"); + ASSERT_TRUE(storage::KvDeploymentWriter{}.Write(m_descriptor_path.string(), desc).has_value()); + + m_parser = std::make_shared(score::crypto::daemon::common::ProviderId{1U}); + + m_registry = std::make_shared(); + cert::CertSlotConfig slot_cfg; + slot_cfg.slot_name = std::string{kSlotName}; + slot_cfg.storage_backend = "DEFAULT"; + slot_cfg.deployment_path = m_descriptor_path.string(); + slot_cfg.deployment_format = "kv"; + m_slot_handle = m_registry->RegisterSlot(slot_cfg); + // Map uid=0 app resource "root_ca" → slot "test/root-ca" + m_registry->RegisterAppResource(0U, std::string{kAppResource}, std::string{kSlotName}); + + m_trust_store_manager = std::make_shared(); + m_data_manager = std::make_shared(); + + m_service = std::make_shared( + m_data_manager, m_registry, m_trust_store_manager, [this](const cert::CertSlotConfig&) { + return std::make_shared(m_parser); + }); + } + + void TearDown() override + { + std::error_code ec; + std::filesystem::remove_all(m_dir, ec); + } + + // Mirror the executor call sequence: resolve the slot node, then load the cert. + // Combines ResolveCertSlot → ResolveSlotForOperation → LoadOrShare. + cert::CertDataNodeResult LoadCert(dm::ClientId client_id) + { + auto slot_res = m_service->ResolveCertSlot(std::string{kAppResource}, client_id); + if (!slot_res.has_value()) + return {}; + const auto slot_node_id = slot_res.value(); + + auto resolved = m_service->ResolveSlotForOperation(client_id, slot_node_id); + if (!resolved.has_value()) + return {}; + + cert::CertRegistrationParams params; + params.client_id = client_id; + params.parent_id = slot_node_id; + params.slot_handle = resolved->handle; + + auto result = m_service->LoadOrShare(params, *resolved->handler, *resolved->config); + if (!result.has_value()) + return {}; + return result.value(); + } + + std::filesystem::path m_dir; + std::filesystem::path m_cert_path; + std::filesystem::path m_cert_updated_path; + std::filesystem::path m_descriptor_path; + cert::CertSlotHandle m_slot_handle; + + std::shared_ptr m_parser; + cert::CertSlotRegistry::Sptr m_registry; + cert::TrustStoreManager::Sptr m_trust_store_manager; + std::shared_ptr m_data_manager; + cert::CertManagementService::Sptr m_service; +}; + +// --------------------------------------------------------------------------- +// Basic load and metadata +// --------------------------------------------------------------------------- + +TEST_F(CertManagementServiceTest, LoadCertFromSlot_ReturnsObjectWithExpectedSubject) +{ + const auto result = LoadCert(kClientA); + + ASSERT_NE(result.node_id, 0U); + ASSERT_NE(result.entry, nullptr); + EXPECT_EQ(result.entry->GetCertObject()->GetSubject(), kSubjectInitial); + EXPECT_TRUE(result.entry->GetCertObject()->IsCA()); + EXPECT_EQ(result.entry->GetCertObject()->GetFingerprint().size(), 32U); + EXPECT_EQ(result.entry->GetCertObject()->GetSkid().size(), 20U); +} + +TEST_F(CertManagementServiceTest, ResolveCertForOperation_ReturnsSameObjectAsLoaded) +{ + const auto result = LoadCert(kClientA); + ASSERT_NE(result.node_id, 0U); + + const auto resolved = m_service->ResolveCertForOperation(kClientA, result.node_id); + ASSERT_TRUE(resolved.has_value()); + EXPECT_EQ((*resolved)->GetSubject(), kSubjectInitial); +} + +// --------------------------------------------------------------------------- +// Node release +// --------------------------------------------------------------------------- + +TEST_F(CertManagementServiceTest, ReleaseCert_NodeBecomesUnresolvable) +{ + const auto result = LoadCert(kClientA); + ASSERT_NE(result.node_id, 0U); + + ASSERT_TRUE(m_service->ReleaseCert(kClientA, result.node_id).has_value()); + + EXPECT_FALSE(m_service->ResolveCertForOperation(kClientA, result.node_id).has_value()); +} + +TEST_F(CertManagementServiceTest, ReleaseCert_UnknownNodeId_ReturnsError) +{ + EXPECT_FALSE(m_service->ReleaseCert(kClientA, 999U).has_value()); +} + +// --------------------------------------------------------------------------- +// Deduplication — second call for the same slot must share the CertEntry +// --------------------------------------------------------------------------- + +TEST_F(CertManagementServiceTest, LoadOrShare_SecondCallSharesRegistryEntry) +{ + const auto result1 = LoadCert(kClientA); + ASSERT_NE(result1.node_id, 0U); + ASSERT_NE(result1.entry, nullptr); + + const auto result2 = LoadCert(kClientA); + ASSERT_NE(result2.node_id, 0U); + ASSERT_NE(result2.entry, nullptr); + + // Separate data-manager nodes but the same live CertEntry. + EXPECT_NE(result1.node_id, result2.node_id); + EXPECT_EQ(result1.entry.get(), result2.entry.get()); +} + +// --------------------------------------------------------------------------- +// Slot-node deduplication +// --------------------------------------------------------------------------- + +TEST_F(CertManagementServiceTest, ResolveCertSlot_SameClientAndResource_ReturnsSameNodeId) +{ + const auto id1 = m_service->ResolveCertSlot(std::string{kAppResource}, kClientA); + const auto id2 = m_service->ResolveCertSlot(std::string{kAppResource}, kClientA); + + ASSERT_TRUE(id1.has_value()); + ASSERT_TRUE(id2.has_value()); + EXPECT_EQ(id1.value(), id2.value()); +} + +TEST_F(CertManagementServiceTest, ResolveCertSlot_UnknownResource_ReturnsError) +{ + EXPECT_FALSE(m_service->ResolveCertSlot("no_such_resource", kClientA).has_value()); +} + +// --------------------------------------------------------------------------- +// Client isolation +// --------------------------------------------------------------------------- + +TEST_F(CertManagementServiceTest, MediatorCleanupThenServiceCleanup_IsolatesClients) +{ + const auto result_a = LoadCert(kClientA); + const auto result_b = LoadCert(kClientB); + ASSERT_NE(result_a.node_id, 0U); + ASSERT_NE(result_b.node_id, 0U); + + ASSERT_TRUE(m_data_manager->deleteClientNodes(kClientA).has_value()); + m_service->CleanupClient(kClientA); + + // Client A's node must be gone. + EXPECT_FALSE(m_service->ResolveCertForOperation(kClientA, result_a.node_id).has_value()); + // Client B must be unaffected. + EXPECT_TRUE(m_service->ResolveCertForOperation(kClientB, result_b.node_id).has_value()); +} + +// --------------------------------------------------------------------------- +// Trust-store propagation +// --------------------------------------------------------------------------- + +TEST_F(CertManagementServiceTest, NotifySlotCertChanged_RefreshesAnchorSubjectInTrustStore) +{ + // Wire up a trust store backed by the same slot. + cert::TrustStoreConfig ts_cfg; + ts_cfg.store_name = "test-roots"; + ts_cfg.members.push_back( + cert::TrustStoreMemberConfig{std::string{kSlotName}, cert::TrustStoreMemberKind::kSharedStatic}); + + m_trust_store_manager->Load({ts_cfg}, m_registry, [this](const cert::CertSlotConfig&) { + return std::make_shared(m_parser); + }); + + const auto ts_id = m_trust_store_manager->ResolveByName("test-roots"); + auto store = m_trust_store_manager->GetStore(ts_id); + ASSERT_NE(store, nullptr); + + // Verify the initial anchor subject. + auto initial = store->GetAnchors(); + ASSERT_TRUE(initial.has_value()); + ASSERT_EQ(initial->size(), 1U); + EXPECT_EQ((*initial)[0]->GetSubject(), kSubjectInitial); + + // Replace the cert on disk — point the descriptor at the updated cert. + storage::DeploymentDescriptor updated_desc; + updated_desc.Set("certificate", "cert_path", m_cert_updated_path.string()); + updated_desc.Set("certificate", "cert_format", "pem"); + ASSERT_TRUE(storage::KvDeploymentWriter{}.Write(m_descriptor_path.string(), updated_desc).has_value()); + + // Fan-out the change notification through the service. + m_service->NotifySlotCertChanged(m_slot_handle); + + // The anchor cache must reflect the new cert. + auto updated = store->GetAnchors(); + ASSERT_TRUE(updated.has_value()); + ASSERT_EQ(updated->size(), 1U); + EXPECT_EQ((*updated)[0]->GetSubject(), kSubjectUpdated); + // Fingerprints must differ — different certs. + EXPECT_FALSE(std::equal((*initial)[0]->GetFingerprint().begin(), + (*initial)[0]->GetFingerprint().end(), + (*updated)[0]->GetFingerprint().begin(), + (*updated)[0]->GetFingerprint().end())); +} + +// --------------------------------------------------------------------------- +// Ephemeral cert registration — RegisterCertMaterial with an invalid slot +// handle (no persistent backing). The caller supplies a pre-parsed CertObject; +// the service creates a CertDataNode under the given parent and registers the +// entry in the ephemeral (non-slot-keyed) section of the cert registry. +// --------------------------------------------------------------------------- + +TEST_F(CertManagementServiceTest, RegisterCertMaterial_EphemeralPath_CreatesResolvableNode) +{ + // Read cert bytes from the test-vector file (already copied to m_cert_path in SetUp). + std::ifstream file(m_cert_path, std::ios::binary); + ASSERT_TRUE(file.is_open()); + const std::vector bytes{std::istreambuf_iterator(file), {}}; + ASSERT_FALSE(bytes.empty()); + + auto parsed = m_parser->ParseCertificate(bytes.data(), bytes.size(), score::crypto::FormatType::kPem); + ASSERT_TRUE(parsed.has_value()); + EXPECT_EQ((*parsed)->GetSubject(), kSubjectInitial); + + // Use the slot node as the parent (mirrors the executor call sequence). + const auto slot_node_id = m_service->ResolveCertSlot(std::string{kAppResource}, kClientA); + ASSERT_TRUE(slot_node_id.has_value()); + + // Pass an invalid CertSlotHandle to register as ephemeral (no slot backing). + cert::CertRegistrationParams params; + params.client_id = kClientA; + params.parent_id = slot_node_id.value(); + params.slot_handle = cert::CertSlotHandle{}; + + const auto result = m_service->RegisterCertMaterial(params, *parsed); + ASSERT_TRUE(result.has_value()); + EXPECT_NE(result->node_id, 0U); + ASSERT_NE(result->entry, nullptr); + EXPECT_EQ(result->entry->GetCertObject()->GetSubject(), kSubjectInitial); + + // The node must be resolvable immediately after registration. + const auto resolved = m_service->ResolveCertForOperation(kClientA, result->node_id); + ASSERT_TRUE(resolved.has_value()); + EXPECT_EQ((*resolved)->GetSubject(), kSubjectInitial); + + // Releasing the ephemeral node must make it unresolvable. + ASSERT_TRUE(m_service->ReleaseCert(kClientA, result->node_id).has_value()); + EXPECT_FALSE(m_service->ResolveCertForOperation(kClientA, result->node_id).has_value()); +} + +} // namespace diff --git a/score/crypto/src/daemon/cert_management/tests/slot/test_file_backed_slot_handler.cpp b/score/crypto/src/daemon/cert_management/tests/slot/test_file_backed_slot_handler.cpp new file mode 100644 index 000000000..06fed3b9c --- /dev/null +++ b/score/crypto/src/daemon/cert_management/tests/slot/test_file_backed_slot_handler.cpp @@ -0,0 +1,217 @@ +#include "score/crypto/src/daemon/cert_management/slot/file_backed_slot_handler.hpp" +#include "score/crypto/src/daemon/common/storage/kv/kv_deployment_writer.hpp" +#include "score/crypto/src/daemon/provider/score_provider/openssl/cert_management/openssl_cert_parser.hpp" + +#include + +#include +#include +#include +#include +#include +#include + +namespace +{ +namespace cert = score::crypto::daemon::cert_management; +namespace provider = score::crypto::daemon::provider::cert_management; +namespace storage = score::crypto::daemon::common::storage; +using Error = score::crypto::daemon::common::DaemonErrorCode; + +class FakeParser final : public provider::ICertParser +{ + public: + score::crypto::Expected ParseCertificate(const std::uint8_t* bytes, + std::size_t size, + score::crypto::FormatType format) override + { + if (bytes == nullptr || size == 0U) + return score::crypto::make_unexpected(Error::kCertificateParsingFailed); + cert::CertChainMetadata metadata; + metadata.subject_canonical = "CN=file-test"; + metadata.issuer_canonical = "CN=file-test"; + metadata.fingerprint = std::vector(32U, 0x11U); + return std::make_shared( + std::move(metadata), std::vector{bytes, bytes + size}, format); + } + + score::crypto::Expected, Error> + ParseCertificates(const std::uint8_t* bytes, std::size_t size, score::crypto::FormatType format) override + { + auto parsed = ParseCertificate(bytes, size, format); + if (!parsed) + return score::crypto::make_unexpected(parsed.error()); + return std::vector{*parsed}; + } +}; + +class FileBackedSlotHandlerTest : public ::testing::Test +{ + protected: + void SetUp() override + { + m_directory = std::filesystem::temp_directory_path() / "score_cert_management_test"; + std::filesystem::create_directories(m_directory); + m_descriptor = m_directory / "slot.kv"; + m_certificate = m_directory / "certificate.pem"; + m_crl = m_directory / "certificate.crl"; + + storage::DeploymentDescriptor descriptor; + descriptor.Set("certificate", "cert_path", m_certificate.string()); + descriptor.Set("certificate", "cert_format", "pem"); + descriptor.Set("crl", "crl_path", m_crl.string()); + ASSERT_TRUE(storage::KvDeploymentWriter{}.Write(m_descriptor.string(), descriptor).has_value()); + + m_slot.deployment_path = m_descriptor.string(); + m_slot.deployment_format = "kv"; + m_handler = std::make_unique(std::make_shared()); + } + + void TearDown() override + { + std::error_code error; + std::filesystem::remove_all(m_directory, error); + } + + std::filesystem::path m_directory; + std::filesystem::path m_descriptor; + std::filesystem::path m_certificate; + std::filesystem::path m_crl; + cert::CertSlotConfig m_slot; + std::unique_ptr m_handler; +}; + +TEST_F(FileBackedSlotHandlerTest, StoresLoadsAndClearsCertificate) +{ + cert::CertChainMetadata metadata; + metadata.subject_canonical = "CN=file-test"; + metadata.issuer_canonical = "CN=file-test"; + auto certificate = std::make_shared( + std::move(metadata), std::vector{1U, 2U, 3U}, score::crypto::FormatType::kDer); + + ASSERT_TRUE(m_handler->StoreCertificate(m_slot, *certificate).has_value()); + ASSERT_TRUE(m_handler->LoadCertificate(m_slot).has_value()); + EXPECT_EQ(m_handler->GetSlotState(m_slot).value(), score::crypto::CertificateSlotState::kOccupied); + EXPECT_EQ(m_handler->LoadCertificate(m_slot).value()->GetRawBytes().size(), 3U); + + ASSERT_TRUE(m_handler->ClearSlot(m_slot).has_value()); + EXPECT_EQ(m_handler->GetSlotState(m_slot).value(), score::crypto::CertificateSlotState::kEmpty); +} + +TEST_F(FileBackedSlotHandlerTest, StoresLoadsAndClearsCrl) +{ + const std::vector crl{4U, 5U, 6U}; + const auto crl_span = score::crypto::span{crl.data(), crl.size()}; + ASSERT_TRUE(m_handler->StoreCrl(m_slot, crl_span, score::crypto::FormatType::kDer).has_value()); + EXPECT_TRUE(m_handler->HasCrl(m_slot)); + ASSERT_TRUE(m_handler->LoadCrl(m_slot).has_value()); + EXPECT_EQ(*m_handler->LoadCrl(m_slot), crl); + + ASSERT_TRUE(m_handler->ClearCrl(m_slot).has_value()); + EXPECT_FALSE(m_handler->HasCrl(m_slot)); +} + +// A freshly-configured slot with no stored CRL must report HasCrl() == false +// without any prior store call. This baseline is separate from the +// StoresLoadsAndClearsCrl flow so that a regression in the empty-state +// detection doesn't go unnoticed. +TEST_F(FileBackedSlotHandlerTest, HasCrl_FalseForFreshSlot) +{ + EXPECT_FALSE(m_handler->HasCrl(m_slot)); +} + +// GetSlotState on a fresh slot must report kEmpty without a prior store. +TEST_F(FileBackedSlotHandlerTest, GetSlotState_EmptyForFreshSlot) +{ + EXPECT_EQ(m_handler->GetSlotState(m_slot).value(), score::crypto::CertificateSlotState::kEmpty); +} + +// --------------------------------------------------------------------------- +// OpenSSL-backed tests — verify real metadata extraction from test vectors. +// These tests require the openssl_backend_active build constraint. +// --------------------------------------------------------------------------- + +namespace openssl_ns = score::crypto::daemon::provider::score_provider::openssl; + +// Fixture that uses the real OpenSslCertParser and the central test vector. +// This verifies that FileBackedSlotHandler feeds bytes correctly to the parser +// and that the resulting CertObject carries the expected metadata. +class FileBackedSlotHandlerRealParserTest : public ::testing::Test +{ + protected: + void SetUp() override + { + m_dir = std::filesystem::temp_directory_path() / "score_cert_slot_real"; + m_descriptor = m_dir / "slot.kv"; + m_cert_file = m_dir / "cert.pem"; + std::filesystem::remove_all(m_dir); + std::filesystem::create_directories(m_dir); + + ASSERT_TRUE(std::filesystem::copy_file("score/tests/test_vectors/certificate/certificate.pem", + m_cert_file, + std::filesystem::copy_options::overwrite_existing)); + + storage::DeploymentDescriptor descriptor; + descriptor.Set("certificate", "cert_path", m_cert_file.string()); + descriptor.Set("certificate", "cert_format", "pem"); + ASSERT_TRUE(storage::KvDeploymentWriter{}.Write(m_descriptor.string(), descriptor).has_value()); + + m_slot.deployment_path = m_descriptor.string(); + m_slot.deployment_format = "kv"; + + m_parser = std::make_shared(score::crypto::daemon::common::ProviderId{1U}); + m_handler = std::make_unique(m_parser); + } + + void TearDown() override + { + std::error_code ec; + std::filesystem::remove_all(m_dir, ec); + } + + std::filesystem::path m_dir; + std::filesystem::path m_descriptor; + std::filesystem::path m_cert_file; + cert::CertSlotConfig m_slot; + std::shared_ptr m_parser; + std::unique_ptr m_handler; +}; + +// LoadCertificate must return a CertObject whose metadata matches the +// known-answer values recorded in certificate_manifest.json. +TEST_F(FileBackedSlotHandlerRealParserTest, LoadCertificate_MetadataMatchesTestVector) +{ + const auto result = m_handler->LoadCertificate(m_slot); + ASSERT_TRUE(result.has_value()); + ASSERT_NE(*result, nullptr); + + const auto& cert = **result; + EXPECT_EQ(cert.GetSubject(), "CN=cert-management-test,O=Eclipse"); + EXPECT_EQ(cert.GetIssuer(), "CN=cert-management-test,O=Eclipse"); + EXPECT_TRUE(cert.IsCA()); + // SKID extension is present in the test vector (20 bytes for a SHA-1 key ID). + EXPECT_EQ(cert.GetSkid().size(), 20U); + // SHA-256 fingerprint is always 32 bytes. + EXPECT_EQ(cert.GetFingerprint().size(), 32U); +} + +// Storing a cert then loading it back must round-trip subject and CA flag +// using a fresh handler instance to exclude any in-memory cache effects. +TEST_F(FileBackedSlotHandlerRealParserTest, StoreThenLoad_SubjectAndIsCAMatch) +{ + // Load the initial cert to get a CertObject to store. + const auto initial = m_handler->LoadCertificate(m_slot); + ASSERT_TRUE(initial.has_value()); + + // Store it back (overwrites the same path, which is fine for this test). + ASSERT_TRUE(m_handler->StoreCertificate(m_slot, **initial).has_value()); + + // Load with a fresh handler to avoid any in-memory state. + cert::FileBackedSlotHandler fresh_handler{m_parser}; + const auto reloaded = fresh_handler.LoadCertificate(m_slot); + ASSERT_TRUE(reloaded.has_value()); + + EXPECT_EQ((*reloaded)->GetSubject(), (*initial)->GetSubject()); + EXPECT_EQ((*reloaded)->IsCA(), (*initial)->IsCA()); +} +} // namespace diff --git a/score/crypto/src/daemon/cert_management/tests/truststore/test_trust_store_manager.cpp b/score/crypto/src/daemon/cert_management/tests/truststore/test_trust_store_manager.cpp new file mode 100644 index 000000000..177ccf5c6 --- /dev/null +++ b/score/crypto/src/daemon/cert_management/tests/truststore/test_trust_store_manager.cpp @@ -0,0 +1,534 @@ +// ******************************************************************************* +// Copyright (c) 2026 Contributors to the Eclipse Foundation +// SPDX-License-Identifier: Apache-2.0 +// ******************************************************************************* +// +// Component-level tests for TrustStoreManager. +// +// Uses the real FileBackedSlotHandler, real OpenSslCertParser, and the central +// test vectors so that trust-store anchor loading, eviction, and mutation +// operations are exercised against real certificate material — not stubs. +// +// All cert bytes come from score/tests/test_vectors/certificate/ and all slot +// state is backed by KV descriptor files in a per-test temp directory. +// +// Test subjects: +// - Lazy anchor loading and slot membership index +// - Per-client AddRef/ReleaseRef — cache is evicted only when ALL clients release +// - CleanupClient — drops all refs for a crashed client and evicts the cache +// - DisableMember / EnableMember — toggle anchor visibility synchronously +// - Two stores sharing one slot — same CertObject instance, single disk read +// - AddMember — stores a cert into an empty exclusive slot, persists to disk +// - RemoveMember — removes anchor by real SHA-256 fingerprint +// - AcknowledgeMemberUpdate — re-enables a disabled slot with fresh load + +#include "score/crypto/src/daemon/cert_management/slot/file_backed_slot_handler.hpp" +#include "score/crypto/src/daemon/cert_management/truststore/trust_store_manager.hpp" +#include "score/crypto/src/daemon/common/storage/kv/kv_deployment_writer.hpp" +#include "score/crypto/src/daemon/provider/score_provider/openssl/cert_management/openssl_cert_parser.hpp" + +#include + +#include +#include +#include +#include +#include +#include +#include + +namespace +{ +namespace cert = score::crypto::daemon::cert_management; +namespace dm = score::crypto::daemon::data_manager; +namespace openssl_ns = score::crypto::daemon::provider::score_provider::openssl; +namespace storage = score::crypto::daemon::common::storage; +using Error = score::crypto::daemon::common::DaemonErrorCode; + +// Test-vector subjects — match certificate_manifest.json +static constexpr std::string_view kSubjectInitial = "CN=cert-management-test,O=Eclipse"; +static constexpr std::string_view kSubjectUpdated = "CN=cert-management-updated,O=Eclipse"; + +// client_id layout: upper 32 bits = UID, lower 32 bits = PID. +// UID=0 matches allowed_write_uids = {0U} in trust-store access policies. +static constexpr dm::ClientId kClientA = 1U; // uid=0, pid=1 +static constexpr dm::ClientId kClientB = 2U; // uid=0, pid=2 + +// --------------------------------------------------------------------------- +// Common fixture — one temp directory per test, two cert slots pre-configured. +// +// "root-anchor" : KV descriptor + certificate.pem (kSubjectInitial) +// "empty-anchor" : KV descriptor with cert_path set, but file absent +// +// Tests that need a second cert ("certificate_updated.pem") copy it over +// "root-anchor"'s cert file after capturing the initial anchor, simulating an +// on-disk rotation observed after cache eviction. +// --------------------------------------------------------------------------- + +class TrustStoreManagerTest : public ::testing::Test +{ + protected: + void SetUp() override + { + m_dir = std::filesystem::temp_directory_path() / "score_ts_mgr_test"; + std::filesystem::remove_all(m_dir); + std::filesystem::create_directories(m_dir); + + m_parser = std::make_shared(score::crypto::daemon::common::ProviderId{1U}); + + // root-anchor: slot backed by the initial test vector cert. + m_root_cert = m_dir / "root_ca.pem"; + m_root_slot_kv = m_dir / "root_ca.kv"; + ASSERT_TRUE(std::filesystem::copy_file("score/tests/test_vectors/certificate/certificate.pem", + m_root_cert, + std::filesystem::copy_options::overwrite_existing)); + WriteSlotDescriptor(m_root_slot_kv, m_root_cert); + + // empty-anchor: descriptor ready, but cert file not created yet. + m_empty_cert = m_dir / "empty_ca.pem"; + m_empty_slot_kv = m_dir / "empty_ca.kv"; + WriteSlotDescriptor(m_empty_slot_kv, m_empty_cert); + // m_empty_cert is intentionally NOT created here. + } + + void TearDown() override + { + std::error_code ec; + std::filesystem::remove_all(m_dir, ec); + } + + void WriteSlotDescriptor(const std::filesystem::path& kv, const std::filesystem::path& cert_file) + { + storage::DeploymentDescriptor desc; + desc.Set("certificate", "cert_path", cert_file.string()); + desc.Set("certificate", "cert_format", "pem"); + ASSERT_TRUE(storage::KvDeploymentWriter{}.Write(kv.string(), desc).has_value()); + } + + cert::CertSlotHandlerFactory MakeHandlerFactory() + { + auto parser = m_parser; + return [parser](const cert::CertSlotConfig&) { + return std::make_shared(parser); + }; + } + + // MakeSlotConfig — for tests that mutate state (overwrite cert file, AddMember, etc.) + // Points to a per-test temp directory copy so the committed test vectors stay clean. + cert::CertSlotConfig MakeSlotConfig(const std::string& name, const std::filesystem::path& kv) const + { + cert::CertSlotConfig cfg; + cfg.slot_name = name; + cfg.storage_backend = "DEFAULT"; + cfg.deployment_path = kv.string(); + cfg.deployment_format = "kv"; + return cfg; + } + + // StaticSlotConfig — for read-only tests that never overwrite cert files. + // Uses the per-test descriptor and certificate created in SetUp(). + cert::CertSlotConfig StaticSlotConfig(const std::string& name) const + { + cert::CertSlotConfig cfg; + cfg.slot_name = name; + cfg.storage_backend = "DEFAULT"; + cfg.deployment_path = m_root_slot_kv.string(); + cfg.deployment_format = "kv"; + return cfg; + } + + // Parse the cert at the given path and return the first CertObject. + cert::CertObject::Sptr ParseCert(const std::filesystem::path& path) const + { + std::ifstream file(path, std::ios::binary); + std::vector bytes{std::istreambuf_iterator(file), {}}; + auto result = m_parser->ParseCertificate(bytes.data(), bytes.size(), score::crypto::FormatType::kPem); + return result.has_value() ? *result : nullptr; + } + + std::filesystem::path m_dir; + std::filesystem::path m_root_cert; + std::filesystem::path m_root_slot_kv; + std::filesystem::path m_empty_cert; + std::filesystem::path m_empty_slot_kv; + std::shared_ptr m_parser; +}; + +// --------------------------------------------------------------------------- +// Basic: lazy anchor loading and slot membership index +// --------------------------------------------------------------------------- + +TEST_F(TrustStoreManagerTest, LoadsAnchorsLazilyAndBuildsSlotMembershipIndex) +{ + auto registry = std::make_shared(); + const auto slot = registry->RegisterSlot(StaticSlotConfig("root-anchor")); + + cert::TrustStoreConfig ts_cfg; + ts_cfg.store_name = "tls-roots"; + ts_cfg.members.push_back(cert::TrustStoreMemberConfig{"root-anchor", cert::TrustStoreMemberKind::kSharedStatic}); + + cert::TrustStoreManager manager; + manager.Load({ts_cfg}, registry, MakeHandlerFactory()); + + // Membership index is built at Load() time without touching cert bytes. + ASSERT_EQ(manager.GetMembershipsForSlot(slot).size(), 1U); + EXPECT_EQ(manager.GetMembershipsForSlot(slot)[0], 0U); + + // Anchors are loaded lazily on the first GetAnchors() call. + auto store = manager.GetStore(0U); + ASSERT_NE(store, nullptr); + auto anchors = store->GetAnchors(); + ASSERT_TRUE(anchors.has_value()); + ASSERT_EQ(anchors->size(), 1U); + EXPECT_EQ((*anchors)[0]->GetSubject(), kSubjectInitial); + EXPECT_EQ((*anchors)[0]->GetFingerprint().size(), 32U); +} + +// --------------------------------------------------------------------------- +// Rejection of unknown handles — no I/O needed. +// --------------------------------------------------------------------------- + +TEST(TrustStoreManagerStandaloneTest, RejectsUnknownStoreAndSlotHandles) +{ + cert::TrustStoreManager manager; + EXPECT_EQ(manager.GetStore(0U), nullptr); + EXPECT_EQ(manager.GetMembershipsForSlot(cert::CertSlotHandle{4U}).size(), 0U); +} + +// --------------------------------------------------------------------------- +// Per-client AddRef/ReleaseRef — anchor cache is evicted only when ALL clients +// release. After eviction, the next GetAnchors() reloads from disk. +// +// To make eviction observable without stub counters, the cert file is replaced +// on disk after the initial load. Releasing one client does not evict (the +// other still holds a ref), so GetAnchors() returns the old subject. Only +// after both clients release does the cache evict; the next GetAnchors() reads +// the new cert from disk. +// --------------------------------------------------------------------------- + +TEST_F(TrustStoreManagerTest, PerClientAddRef_DoesNotEvictCacheWhileOtherClientHoldsRef) +{ + auto registry = std::make_shared(); + static_cast(registry->RegisterSlot(MakeSlotConfig("root-anchor", m_root_slot_kv))); + + cert::TrustStoreConfig ts_cfg; + ts_cfg.store_name = "tls-roots"; + ts_cfg.members.push_back(cert::TrustStoreMemberConfig{"root-anchor", cert::TrustStoreMemberKind::kSharedStatic}); + + cert::TrustStoreManager manager; + manager.Load({ts_cfg}, registry, MakeHandlerFactory()); + + const auto ts_id = manager.ResolveByName("tls-roots"); + auto store = manager.GetStore(ts_id); + ASSERT_NE(store, nullptr); + + // Trigger initial load; drop the result so the anchor's strong ref lives + // only inside TrustStoreHandler::m_slots (not in the test frame). + { + auto anchors = store->GetAnchors(); + ASSERT_TRUE(anchors.has_value()); + EXPECT_EQ((*anchors)[0]->GetSubject(), kSubjectInitial); + } + + const cert::TrustStoreHandle ts_handle{ts_id}; + manager.AddRef(ts_handle, kClientA); + manager.AddRef(ts_handle, kClientB); + + // Rotate cert on disk — subsequent reloads will see kSubjectUpdated. + ASSERT_TRUE(std::filesystem::copy_file("score/tests/test_vectors/certificate/certificate_updated.pem", + m_root_cert, + std::filesystem::copy_options::overwrite_existing)); + + // Release kClientA — kClientB still holds a ref; no eviction. + manager.ReleaseRef(ts_handle, kClientA); + { + auto anchors = store->GetAnchors(); + ASSERT_TRUE(anchors.has_value()); + // Cache is intact (not evicted) — stale cert A still returned. + EXPECT_EQ((*anchors)[0]->GetSubject(), kSubjectInitial); + } + + // Release kClientB — last ref gone; cache is evicted (m_slots cleared). + manager.ReleaseRef(ts_handle, kClientB); + + // Next GetAnchors() reloads from disk — must now see the rotated cert. + auto reloaded = store->GetAnchors(); + ASSERT_TRUE(reloaded.has_value()); + ASSERT_EQ(reloaded->size(), 1U); + EXPECT_EQ((*reloaded)[0]->GetSubject(), kSubjectUpdated); +} + +// --------------------------------------------------------------------------- +// CleanupClient — drops all refs for a disconnected/crashed client and evicts +// the anchor cache when no other client holds refs. +// --------------------------------------------------------------------------- + +TEST_F(TrustStoreManagerTest, CleanupClient_ReleasesAllRefsAndEvictsCache) +{ + auto registry = std::make_shared(); + static_cast(registry->RegisterSlot(MakeSlotConfig("root-anchor", m_root_slot_kv))); + + cert::TrustStoreConfig ts_cfg; + ts_cfg.store_name = "tls-roots"; + ts_cfg.members.push_back(cert::TrustStoreMemberConfig{"root-anchor", cert::TrustStoreMemberKind::kSharedStatic}); + + cert::TrustStoreManager manager; + manager.Load({ts_cfg}, registry, MakeHandlerFactory()); + + const auto ts_id = manager.ResolveByName("tls-roots"); + auto store = manager.GetStore(ts_id); + ASSERT_NE(store, nullptr); + + // Simulate two open verification contexts for kClientA. + const cert::TrustStoreHandle ts_handle{ts_id}; + manager.AddRef(ts_handle, kClientA); + manager.AddRef(ts_handle, kClientA); + + // Populate anchor cache and drop the caller's strong ref. + { + auto anchors = store->GetAnchors(); + ASSERT_TRUE(anchors.has_value()); + EXPECT_EQ((*anchors)[0]->GetSubject(), kSubjectInitial); + } + + // Rotate cert on disk. + ASSERT_TRUE(std::filesystem::copy_file("score/tests/test_vectors/certificate/certificate_updated.pem", + m_root_cert, + std::filesystem::copy_options::overwrite_existing)); + + // Crash cleanup drops all kClientA refs — no other client holds refs. + manager.CleanupClient(kClientA); + + // Next GetAnchors() must reload from disk and return the rotated cert. + auto reloaded = store->GetAnchors(); + ASSERT_TRUE(reloaded.has_value()); + ASSERT_EQ(reloaded->size(), 1U); + EXPECT_EQ((*reloaded)[0]->GetSubject(), kSubjectUpdated); +} + +// --------------------------------------------------------------------------- +// DisableMember / EnableMember — anchor visibility toggles synchronously. +// Default-deny write policy: allowed_write_uids must contain caller UID. +// kClientA = 1U → GetUidFromClientId = upper 32 bits = 0. +// --------------------------------------------------------------------------- + +TEST_F(TrustStoreManagerTest, DisableAndReEnable_MemberTogglesAnchorVisibility) +{ + auto registry = std::make_shared(); + const auto slot = registry->RegisterSlot(StaticSlotConfig("root-anchor")); + + cert::TrustStoreConfig ts_cfg; + ts_cfg.store_name = "tls-roots"; + ts_cfg.access_policy.allowed_write_uids = {0U}; + ts_cfg.members.push_back(cert::TrustStoreMemberConfig{"root-anchor", cert::TrustStoreMemberKind::kSharedStatic}); + + cert::TrustStoreManager manager; + manager.Load({ts_cfg}, registry, MakeHandlerFactory()); + + const auto ts_id = manager.ResolveByName("tls-roots"); + auto store = manager.GetStore(ts_id); + ASSERT_NE(store, nullptr); + + // Initial load: one anchor present. + auto initial = store->GetAnchors(); + ASSERT_TRUE(initial.has_value()); + ASSERT_EQ(initial->size(), 1U); + EXPECT_EQ((*initial)[0]->GetSubject(), kSubjectInitial); + + // Disable: anchor must disappear from the store immediately. + ASSERT_TRUE(manager.DisableMember(ts_id, slot, kClientA).has_value()); + auto after_disable = store->GetAnchors(); + ASSERT_TRUE(after_disable.has_value()); + EXPECT_EQ(after_disable->size(), 0U); + + // Re-enable: anchor must reappear with the same subject. + ASSERT_TRUE(manager.EnableMember(ts_id, slot, kClientA).has_value()); + auto after_enable = store->GetAnchors(); + ASSERT_TRUE(after_enable.has_value()); + ASSERT_EQ(after_enable->size(), 1U); + EXPECT_EQ((*after_enable)[0]->GetSubject(), kSubjectInitial); +} + +// --------------------------------------------------------------------------- +// Two trust stores sharing one cert slot — both get the same CertObject +// instance from the shared weak-ptr cache, and the slot is loaded from disk +// exactly once. +// --------------------------------------------------------------------------- + +TEST_F(TrustStoreManagerTest, TwoStores_OneSlot_BothGetMemberships) +{ + auto registry = std::make_shared(); + const auto slot = registry->RegisterSlot(StaticSlotConfig("shared-anchor")); + + cert::TrustStoreConfig store_a; + store_a.store_name = "store-a"; + store_a.members.push_back(cert::TrustStoreMemberConfig{"shared-anchor", cert::TrustStoreMemberKind::kSharedStatic}); + + cert::TrustStoreConfig store_b; + store_b.store_name = "store-b"; + store_b.members.push_back(cert::TrustStoreMemberConfig{"shared-anchor", cert::TrustStoreMemberKind::kSharedStatic}); + + cert::TrustStoreManager manager; + manager.Load({store_a, store_b}, registry, MakeHandlerFactory()); + + // Both stores report membership for the shared slot. + EXPECT_EQ(manager.GetMembershipsForSlot(slot).size(), 2U); + + const auto ts_id_a = manager.ResolveByName("store-a"); + const auto ts_id_b = manager.ResolveByName("store-b"); + auto anchors_a = manager.GetStore(ts_id_a)->GetAnchors(); + auto anchors_b = manager.GetStore(ts_id_b)->GetAnchors(); + ASSERT_TRUE(anchors_a.has_value()); + ASSERT_TRUE(anchors_b.has_value()); + ASSERT_EQ(anchors_a->size(), 1U); + ASSERT_EQ(anchors_b->size(), 1U); + + // Shared weak-ptr cache: both stores return the same CertObject pointer. + EXPECT_EQ((*anchors_a)[0].get(), (*anchors_b)[0].get()); + EXPECT_EQ((*anchors_a)[0]->GetSubject(), kSubjectInitial); +} + +// --------------------------------------------------------------------------- +// AddMember — stores a parsed cert into an empty exclusive slot and immediately +// makes it visible as a trust anchor. Persistence is verified by loading with +// a fresh FileBackedSlotHandler after AddMember completes. +// --------------------------------------------------------------------------- + +TEST_F(TrustStoreManagerTest, AddMember_ToExclusiveSlot_AddsAnchorAndPersistsToDisk) +{ + auto registry = std::make_shared(); + static_cast(registry->RegisterSlot(MakeSlotConfig("empty-anchor", m_empty_slot_kv))); + + cert::TrustStoreConfig ts_cfg; + ts_cfg.store_name = "mutable-store"; + ts_cfg.access_policy.allowed_write_uids = {0U}; + ts_cfg.members.push_back( + cert::TrustStoreMemberConfig{"empty-anchor", cert::TrustStoreMemberKind::kExclusiveMutable}); + + cert::TrustStoreManager manager; + manager.Load({ts_cfg}, registry, MakeHandlerFactory()); + + const auto ts_id = manager.ResolveByName("mutable-store"); + auto store = manager.GetStore(ts_id); + ASSERT_NE(store, nullptr); + + // Slot is empty — no anchors before AddMember. + auto before = store->GetAnchors(); + ASSERT_TRUE(before.has_value()); + EXPECT_EQ(before->size(), 0U); + + // Parse the test vector cert to get a real CertObject to add. + auto cert = ParseCert(m_root_cert); + ASSERT_NE(cert, nullptr); + EXPECT_EQ(cert->GetSubject(), kSubjectInitial); + + ASSERT_TRUE(manager.AddMember(ts_id, cert, kClientA).has_value()); + + // Anchor must be visible immediately after AddMember. + auto after = store->GetAnchors(); + ASSERT_TRUE(after.has_value()); + ASSERT_EQ(after->size(), 1U); + EXPECT_EQ((*after)[0]->GetSubject(), kSubjectInitial); + + // Verify persistence: a fresh handler must read the cert from disk. + cert::FileBackedSlotHandler fresh_handler{m_parser}; + const auto empty_cfg = MakeSlotConfig("empty-anchor", m_empty_slot_kv); + auto persisted = fresh_handler.LoadCertificate(empty_cfg); + ASSERT_TRUE(persisted.has_value()); + EXPECT_EQ((*persisted)->GetSubject(), kSubjectInitial); +} + +// --------------------------------------------------------------------------- +// RemoveMember — finds the anchor by its real SHA-256 fingerprint, clears the +// slot, and removes it from the in-memory anchor set. +// --------------------------------------------------------------------------- + +TEST_F(TrustStoreManagerTest, RemoveMember_ByFingerprint_RemovesAnchor) +{ + auto registry = std::make_shared(); + static_cast(registry->RegisterSlot(MakeSlotConfig("root-anchor", m_root_slot_kv))); + + cert::TrustStoreConfig ts_cfg; + ts_cfg.store_name = "mutable-store"; + ts_cfg.access_policy.allowed_write_uids = {0U}; + ts_cfg.members.push_back( + cert::TrustStoreMemberConfig{"root-anchor", cert::TrustStoreMemberKind::kExclusiveMutable}); + + cert::TrustStoreManager manager; + manager.Load({ts_cfg}, registry, MakeHandlerFactory()); + + const auto ts_id = manager.ResolveByName("mutable-store"); + auto store = manager.GetStore(ts_id); + ASSERT_NE(store, nullptr); + + // Load the anchors to obtain the real SHA-256 fingerprint. + auto before = store->GetAnchors(); + ASSERT_TRUE(before.has_value()); + ASSERT_EQ(before->size(), 1U); + EXPECT_EQ((*before)[0]->GetSubject(), kSubjectInitial); + + const auto fp_span = (*before)[0]->GetFingerprint(); + ASSERT_EQ(fp_span.size(), 32U); + const std::vector fingerprint{fp_span.begin(), fp_span.end()}; + + ASSERT_TRUE(manager.RemoveMember(ts_id, fingerprint, kClientA).has_value()); + + // Anchor must be gone after removal. + auto after = store->GetAnchors(); + ASSERT_TRUE(after.has_value()); + EXPECT_EQ(after->size(), 0U); +} + +// --------------------------------------------------------------------------- +// AcknowledgeMemberUpdate — re-enables a disabled member slot, triggers a +// fresh LoadCertificate from disk, and records the accepted fingerprint. +// --------------------------------------------------------------------------- + +TEST_F(TrustStoreManagerTest, AcknowledgeMemberUpdate_TransitionsDisabledMemberToEnabled) +{ + auto registry = std::make_shared(); + const auto slot = registry->RegisterSlot(MakeSlotConfig("root-anchor", m_root_slot_kv)); + + cert::TrustStoreConfig ts_cfg; + ts_cfg.store_name = "cond-store"; + ts_cfg.access_policy.allowed_write_uids = {0U}; + ts_cfg.members.push_back(cert::TrustStoreMemberConfig{"root-anchor", cert::TrustStoreMemberKind::kSharedStatic}); + + cert::TrustStoreManager manager; + manager.Load({ts_cfg}, registry, MakeHandlerFactory()); + + const auto ts_id = manager.ResolveByName("cond-store"); + auto store = manager.GetStore(ts_id); + ASSERT_NE(store, nullptr); + + // Initial load — anchor present. + auto initial = store->GetAnchors(); + ASSERT_TRUE(initial.has_value()); + ASSERT_EQ(initial->size(), 1U); + EXPECT_EQ((*initial)[0]->GetSubject(), kSubjectInitial); + + // Rotate cert on disk so that AcknowledgeMemberUpdate reloads a different cert. + ASSERT_TRUE(std::filesystem::copy_file("score/tests/test_vectors/certificate/certificate_updated.pem", + m_root_cert, + std::filesystem::copy_options::overwrite_existing)); + + // Disable — simulates detection of an unexpected content change. + ASSERT_TRUE(manager.DisableMember(ts_id, slot, kClientA).has_value()); + { + auto after_disable = store->GetAnchors(); + ASSERT_TRUE(after_disable.has_value()); + EXPECT_EQ(after_disable->size(), 0U); + } + + // Acknowledge — fresh load from disk, records accepted_fingerprint, re-enables. + ASSERT_TRUE(manager.AcknowledgeMemberUpdate(ts_id, slot, kClientA).has_value()); + + auto after_ack = store->GetAnchors(); + ASSERT_TRUE(after_ack.has_value()); + ASSERT_EQ(after_ack->size(), 1U); + // AcknowledgeMemberUpdate reloaded from disk — must now show the rotated cert. + EXPECT_EQ((*after_ack)[0]->GetSubject(), kSubjectUpdated); + EXPECT_EQ((*after_ack)[0]->GetFingerprint().size(), 32U); +} + +} // namespace diff --git a/score/crypto/src/daemon/cert_management/truststore/config_driven_trust_store_catalog.cpp b/score/crypto/src/daemon/cert_management/truststore/config_driven_trust_store_catalog.cpp new file mode 100644 index 000000000..a830ae107 --- /dev/null +++ b/score/crypto/src/daemon/cert_management/truststore/config_driven_trust_store_catalog.cpp @@ -0,0 +1,62 @@ +/******************************************************************************** + * 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/cert_management/truststore/config_driven_trust_store_catalog.hpp" + +#include "score/mw/log/logging.h" + +namespace score::crypto::daemon::cert_management +{ + +ConfigDrivenTrustStoreCatalog::ConfigDrivenTrustStoreCatalog(const config::CertificateConfig& config) : m_config{config} +{ +} + +void ConfigDrivenTrustStoreCatalog::Load(TrustStoreManager& manager, + CertSlotRegistry::Sptr registry, + CertSlotHandlerFactory slot_handler_factory) +{ + const auto& entries = m_config.GetTrustStoreEntries(); + + std::vector configs; + configs.reserve(entries.size()); + + for (const auto& src : entries) + { + TrustStoreConfig cfg; + cfg.store_name = src.store_name; + cfg.conditional_slot_initialization = + static_cast(src.conditional_slot_initialization); + for (const auto& src_member : src.members) + { + TrustStoreMemberConfig member; + member.slot_name = src_member.slot_name; + member.kind = static_cast(src_member.kind); + cfg.members.push_back(std::move(member)); + } + cfg.access_policy = {src.allowed_uids, src.allowed_write_uids}; + cfg.deployment_path = src.deployment_path; + cfg.deployment_format = src.deployment_format; + configs.push_back(std::move(cfg)); + + score::mw::log::LogDebug() << kLogPrefix << "Registered trust store '" << src.store_name << "' (" + << src.members.size() << " member slot(s))"; + } + + score::mw::log::LogDebug() << kLogPrefix << "Loading " << configs.size() << " trust store(s) from configuration."; + manager.Load(std::move(configs), std::move(registry), std::move(slot_handler_factory)); + for (const auto& mapping : m_config.GetAppTrustStoreEntries()) + manager.RegisterAppResource(mapping.uid, mapping.app_resource_id, mapping.trust_store_name); +} + +} // namespace score::crypto::daemon::cert_management diff --git a/score/crypto/src/daemon/cert_management/truststore/config_driven_trust_store_catalog.hpp b/score/crypto/src/daemon/cert_management/truststore/config_driven_trust_store_catalog.hpp new file mode 100644 index 000000000..aaad970bc --- /dev/null +++ b/score/crypto/src/daemon/cert_management/truststore/config_driven_trust_store_catalog.hpp @@ -0,0 +1,54 @@ +/******************************************************************************** + * 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 + ********************************************************************************/ + +#ifndef SCORE_CRYPTO_SRC_DAEMON_CERT_MANAGEMENT_TRUSTSTORE_CONFIG_DRIVEN_TRUST_STORE_CATALOG_HPP +#define SCORE_CRYPTO_SRC_DAEMON_CERT_MANAGEMENT_TRUSTSTORE_CONFIG_DRIVEN_TRUST_STORE_CATALOG_HPP + +#include "score/crypto/src/daemon/cert_management/slot/slot_registry.hpp" +#include "score/crypto/src/daemon/cert_management/truststore/trust_store_manager.hpp" +#include "score/crypto/src/daemon/config/inc/config.hpp" + +#include + +namespace score::crypto::daemon::cert_management +{ + +/// @brief One-shot loader that populates a TrustStoreManager from CertificateConfig. +/// +/// Mirrors the role of ConfigDrivenSlotCatalog for cert slots: converts each +/// CertificateConfig::TrustStoreEntry into a TrustStoreConfig and delegates to +/// TrustStoreManager::Load(). Must be called after CertSlotRegistry is fully +/// populated (slot name resolution happens inside TrustStoreManager::Load()). +class ConfigDrivenTrustStoreCatalog final +{ + public: + explicit ConfigDrivenTrustStoreCatalog(const config::CertificateConfig& config); + + /// @brief Adapt config entries and populate the trust store manager. + /// + /// @param manager The TrustStoreManager to populate. + /// @param registry Fully-populated CertSlotRegistry for member slot resolution. + /// @param factory Certificate factory for loading static anchor files (may be null). + void Load(TrustStoreManager& manager, + CertSlotRegistry::Sptr registry, + CertSlotHandlerFactory slot_handler_factory = {}); + + private: + const config::CertificateConfig& m_config; + + static constexpr std::string_view kLogPrefix = "[CERT_TRUST_STORE_CATALOG] "; +}; + +} // namespace score::crypto::daemon::cert_management + +#endif // SCORE_CRYPTO_SRC_DAEMON_CERT_MANAGEMENT_TRUSTSTORE_CONFIG_DRIVEN_TRUST_STORE_CATALOG_HPP diff --git a/score/crypto/src/daemon/cert_management/truststore/trust_store_handler.cpp b/score/crypto/src/daemon/cert_management/truststore/trust_store_handler.cpp new file mode 100644 index 000000000..cad3b36c7 --- /dev/null +++ b/score/crypto/src/daemon/cert_management/truststore/trust_store_handler.cpp @@ -0,0 +1,101 @@ +/******************************************************************************** + * 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/cert_management/truststore/trust_store_handler.hpp" + +#include + +namespace score::crypto::daemon::cert_management +{ +namespace +{ +using Error = common::DaemonErrorCode; +} + +TrustStoreHandler::TrustStoreHandler(TrustStoreHandle handle, AnchorLoader loader) + : m_handle{handle}, m_loader{std::move(loader)} +{ +} + +TrustStoreHandle TrustStoreHandler::GetHandle() const noexcept +{ + return m_handle; +} + +score::crypto::Expected, Error> TrustStoreHandler::GetAnchors() +{ + EnsureLoaded(); + return All(); +} + +void TrustStoreHandler::EnsureLoaded() +{ + if (!m_loaded) + { + if (m_loader) + m_loader(*this); + m_loaded = true; + } +} + +void TrustStoreHandler::NotifySlotUpdate(CertSlotHandle slot, CertObject::Sptr cert) +{ + // In-place update of one slot. Does not touch m_loaded — the caller + // (TrustStoreManager) is responsible for deciding whether a reload is needed. + m_slots[slot.index] = std::move(cert); +} + +void TrustStoreHandler::InvalidateSlot(CertSlotHandle slot) +{ + m_slots.erase(slot.index); + m_loaded = false; +} + +void TrustStoreHandler::ClearAnchorCache() +{ + m_slots.clear(); + m_loaded = false; +} + +CertObject::Sptr TrustStoreHandler::FindBySubject(const std::string& subject) const +{ + for (const auto& cert : All()) + { + if (cert && cert->GetSubject() == subject) + return cert; + } + return nullptr; +} + +CertObject::Sptr TrustStoreHandler::FindBySkid(score::crypto::span skid) const +{ + for (const auto& cert : All()) + { + if (cert && cert->GetSkid().size() == skid.size() && + std::equal(skid.begin(), skid.end(), cert->GetSkid().begin())) + return cert; + } + return nullptr; +} + +std::vector TrustStoreHandler::All() const +{ + std::vector all; + for (const auto& [slot, cert] : m_slots) + { + if (cert) + all.push_back(cert); + } + return all; +} + +} // namespace score::crypto::daemon::cert_management diff --git a/score/crypto/src/daemon/cert_management/truststore/trust_store_handler.hpp b/score/crypto/src/daemon/cert_management/truststore/trust_store_handler.hpp new file mode 100644 index 000000000..44aae5e96 --- /dev/null +++ b/score/crypto/src/daemon/cert_management/truststore/trust_store_handler.hpp @@ -0,0 +1,67 @@ +/******************************************************************************** + * 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 + ********************************************************************************/ + +#ifndef SCORE_CRYPTO_SRC_DAEMON_CERT_MANAGEMENT_TRUSTSTORE_TRUST_STORE_HANDLER_HPP +#define SCORE_CRYPTO_SRC_DAEMON_CERT_MANAGEMENT_TRUSTSTORE_TRUST_STORE_HANDLER_HPP + +#include "score/crypto/src/daemon/cert_management/interfaces/i_trust_store_handler.hpp" + +#include +#include +#include +#include +#include + +namespace score::crypto::daemon::cert_management +{ + +class TrustStoreHandler final : public ITrustStoreHandler +{ + public: + /// Called by TrustStoreManager to populate this handler's anchor cache on demand. + /// The callee receives a reference to the handler and must call NotifySlotUpdate() + /// for every member slot. + using AnchorLoader = std::function; + + TrustStoreHandler(TrustStoreHandle handle, AnchorLoader loader); + + TrustStoreHandle GetHandle() const noexcept override; + + score::crypto::Expected, common::DaemonErrorCode> GetAnchors() override; + void NotifySlotUpdate(CertSlotHandle slot, CertObject::Sptr cert) override; + + CertObject::Sptr FindBySubject(const std::string& subject) const override; + CertObject::Sptr FindBySkid(score::crypto::span skid) const override; + + /// Remove one slot's cached cert and force a full reload on the next GetAnchors() call. + /// Called by TrustStoreManager when a member slot's content changes externally. + void InvalidateSlot(CertSlotHandle slot); + + /// Drop all cached anchor strong-refs and reset the loaded flag. + /// Called by TrustStoreManager when the last DataNode for this store is released, + /// so that the CertObject memory is freed when no other store holds it. + void ClearAnchorCache(); + + private: + void EnsureLoaded(); + std::vector All() const; + + TrustStoreHandle m_handle{}; + std::unordered_map m_slots; + AnchorLoader m_loader; + bool m_loaded{false}; +}; + +} // namespace score::crypto::daemon::cert_management + +#endif // SCORE_CRYPTO_SRC_DAEMON_CERT_MANAGEMENT_TRUSTSTORE_TRUST_STORE_HANDLER_HPP diff --git a/score/crypto/src/daemon/cert_management/truststore/trust_store_manager.cpp b/score/crypto/src/daemon/cert_management/truststore/trust_store_manager.cpp new file mode 100644 index 000000000..cbbca50a0 --- /dev/null +++ b/score/crypto/src/daemon/cert_management/truststore/trust_store_manager.cpp @@ -0,0 +1,584 @@ +/******************************************************************************** + * 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/cert_management/truststore/trust_store_manager.hpp" +#include "score/crypto/src/daemon/cert_management/slot/deployment_loader.hpp" +#include "score/crypto/src/daemon/cert_management/slot/deployment_writer.hpp" +#include "score/crypto/src/daemon/common/hex.hpp" +#include "score/crypto/src/daemon/control_plane/control_protocol.h" + +#include "score/mw/log/logging.h" + +#include + +namespace score::crypto::daemon::cert_management +{ +namespace +{ +using Error = common::DaemonErrorCode; + +std::optional> CopyFingerprint(score::crypto::span fingerprint) +{ + if (fingerprint.size() != 32U) + return std::nullopt; + + std::array copy{}; + std::copy(fingerprint.begin(), fingerprint.end(), copy.begin()); + return copy; +} + +} // namespace + +// --------------------------------------------------------------------------- +// Startup +// --------------------------------------------------------------------------- + +void TrustStoreManager::Load(const std::vector& configs, + CertSlotRegistry::Sptr registry, + CertSlotHandlerFactory slot_handler_factory) +{ + std::lock_guard lock(m_mutex); + m_stores.clear(); + m_name_index.clear(); + m_app_resource_map.clear(); + m_slot_memberships.clear(); + m_member_states.clear(); + m_slot_cert_cache.clear(); + m_client_ref_counts.clear(); + m_slot_registry = std::move(registry); + m_slot_handler_factory = std::move(slot_handler_factory); + m_slot_handlers.clear(); // lazy cache — populated by GetOrCreateHandler() on first access + for (const auto& config : configs) + { + const TrustStoreId id = static_cast(m_stores.size()); + // Loader lambda is called by TrustStoreHandler::EnsureLoaded() on the first + // GetAnchors() — never at startup. 'this' is safe: the handler is owned by + // TrustStoreManager and never outlives it. + auto handler = std::make_shared(TrustStoreHandle{id}, [this, id](TrustStoreHandler& h) { + this->LoadAnchorsIntoHandler(id, h); + }); + TrustStoreEntry entry{config, handler, id}; + m_stores.push_back(std::move(entry)); + m_name_index[config.store_name] = id; + LoadState(id); // reads m_member_states from descriptor — no cert I/O + if (!m_slot_registry) + continue; + // Build reverse membership index; no cert bytes loaded here. + for (const auto& member : config.members) + { + const auto slot = m_slot_registry->ResolveSlotInternal(member.slot_name); + if (slot) + m_slot_memberships[slot->index].push_back(id); + } + } +} + +// --------------------------------------------------------------------------- +// State helpers (unchanged logic, no cert loading) +// --------------------------------------------------------------------------- + +void TrustStoreManager::LoadState(TrustStoreId id) +{ + if (!m_slot_registry) + return; + if (id >= m_stores.size() || m_stores[id].config.deployment_path.empty()) + return; + const auto descriptor = + DeploymentLoader::Load(m_stores[id].config.deployment_path, m_stores[id].config.deployment_format); + if (!descriptor) + return; + const auto section = descriptor->sections.find(std::string{cert_section_names::kTrustStoreState}); + if (section == descriptor->sections.end()) + return; + for (const auto& member : m_stores[id].config.members) + { + const auto slot = m_slot_registry->ResolveSlotInternal(member.slot_name); + if (!slot) + continue; + MemberState state; + const auto prefix = std::string{"slot."} + member.slot_name + "."; + const auto enabled = section->second.find(prefix + "enabled"); + if (enabled != section->second.end()) + state.enabled = enabled->second == "true"; + const auto fingerprint = section->second.find(prefix + "accepted_fingerprint"); + if (fingerprint != section->second.end()) + { + const auto decoded = common::DecodeHex(fingerprint->second); + if (decoded && decoded->size() == 32U) + { + std::array accepted{}; + std::copy(decoded->begin(), decoded->end(), accepted.begin()); + state.accepted_fingerprint = accepted; + } + } + m_member_states[id][slot->index] = state; + } +} + +score::crypto::Expected TrustStoreManager::PersistState(TrustStoreId id) const +{ + if (!m_slot_registry) + return std::monostate{}; + if (id >= m_stores.size() || m_stores[id].config.deployment_path.empty()) + return std::monostate{}; + common::storage::DeploymentDescriptor descriptor; + const auto existing = + DeploymentLoader::Load(m_stores[id].config.deployment_path, m_stores[id].config.deployment_format); + if (existing) + descriptor = *existing; + const auto states = m_member_states.find(id); + if (states != m_member_states.end()) + { + for (const auto& member : m_stores[id].config.members) + { + const auto slot = m_slot_registry->ResolveSlotInternal(member.slot_name); + if (!slot) + continue; + const auto state_it = states->second.find(slot->index); + if (state_it == states->second.end()) + continue; + const auto& state = state_it->second; + const auto prefix = std::string{"slot."} + member.slot_name + "."; + descriptor.Set(std::string{cert_section_names::kTrustStoreState}, + prefix + "enabled", + state.enabled ? "true" : "false"); + if (state.accepted_fingerprint.has_value()) + descriptor.Set(std::string{cert_section_names::kTrustStoreState}, + prefix + "accepted_fingerprint", + common::EncodeHex(score::crypto::span{ + state.accepted_fingerprint->data(), state.accepted_fingerprint->size()})); + } + } + return DeploymentWriter::Write( + m_stores[id].config.deployment_path, m_stores[id].config.deployment_format, descriptor); +} + +// --------------------------------------------------------------------------- +// Lazy anchor loading — called by TrustStoreHandler::EnsureLoaded() +// --------------------------------------------------------------------------- + +ICertSlotHandler* TrustStoreManager::GetOrCreateHandler(CertSlotHandle slot) +{ + // Must be called with m_mutex held. + auto it = m_slot_handlers.find(slot.index); + if (it != m_slot_handlers.end()) + return it->second.get(); + + if (!m_slot_handler_factory || !m_slot_registry) + return nullptr; + const auto cfg = m_slot_registry->GetConfig(slot); + if (!cfg) + return nullptr; + auto handler = m_slot_handler_factory(**cfg); + if (!handler) + return nullptr; + return m_slot_handlers.emplace(slot.index, std::move(handler)).first->second.get(); +} + +CertObject::Sptr TrustStoreManager::LoadOrGetCached(CertSlotHandle slot) +{ + // Must be called with m_mutex held. + auto& weak = m_slot_cert_cache[slot.index]; + if (auto cert = weak.lock()) + return cert; // another active trust store handler already holds it + + auto* handler = GetOrCreateHandler(slot); + const auto cfg = m_slot_registry ? m_slot_registry->GetConfig(slot) : nullptr; + if (!handler || !cfg) + return nullptr; + + auto loaded = handler->LoadCertificate(**cfg); + if (!loaded) + return nullptr; + + weak = *loaded; // cache as weak_ptr; expires when all handler caches drop their strong ref + return std::move(*loaded); +} + +void TrustStoreManager::LoadAnchorsIntoHandler(TrustStoreId id, TrustStoreHandler& handler) +{ + std::lock_guard lock(m_mutex); + if (id >= m_stores.size() || !m_slot_registry) + return; + + const auto& store = m_stores[id]; + for (const auto& member : store.config.members) + { + const auto slot = m_slot_registry->ResolveSlotInternal(member.slot_name); + if (!slot) + continue; + + auto& state = m_member_states[id][slot->index]; + if (!state.enabled) + { + handler.NotifySlotUpdate(*slot, nullptr); + continue; + } + + auto cert = LoadOrGetCached(*slot); + if (!cert) + { + handler.NotifySlotUpdate(*slot, nullptr); + continue; + } + + bool usable = true; + if (member.kind == TrustStoreMemberKind::kConditionalExternal) + { + const auto fingerprint = cert->GetFingerprint(); + if (state.accepted_fingerprint.has_value()) + { + const bool unchanged = + fingerprint.size() == state.accepted_fingerprint->size() && + std::equal(fingerprint.begin(), fingerprint.end(), state.accepted_fingerprint->begin()); + if (!unchanged) + { + // Content changed without acknowledgement — disable for this store only. + state.enabled = false; + usable = false; + score::mw::log::LogWarn() + << kLogPrefix << "Conditional slot '" << member.slot_name << "' fingerprint mismatch in store '" + << store.config.store_name << "' — disabled until acknowledged"; + } + } + else if (store.config.conditional_slot_initialization == + ConditionalSlotInitialization::kEnableAndAcceptCurrent) + { + state.accepted_fingerprint = CopyFingerprint(fingerprint); + } + else + { + usable = false; + } + } + + handler.NotifySlotUpdate(*slot, usable ? cert : nullptr); + } +} + +// --------------------------------------------------------------------------- +// Store access +// --------------------------------------------------------------------------- + +ITrustStoreHandler::Sptr TrustStoreManager::GetStore(TrustStoreId id) const +{ + std::lock_guard lock(m_mutex); + return id < m_stores.size() ? m_stores[id].handler : nullptr; +} + +TrustStoreId TrustStoreManager::ResolveByName(const std::string& name) const +{ + std::lock_guard lock(m_mutex); + const auto it = m_name_index.find(name); + return it == m_name_index.end() ? UINT32_MAX : it->second; +} + +void TrustStoreManager::RegisterAppResource(uint32_t uid, + const std::string& app_resource_id, + const std::string& store_name) +{ + std::lock_guard lock(m_mutex); + m_app_resource_map[uid][app_resource_id] = store_name; +} + +score::crypto::Expected TrustStoreManager::ResolveAppResource( + const std::string& app_resource_id, + data_manager::ClientId client_id) const +{ + std::lock_guard lock(m_mutex); + const uint32_t uid = control_plane::protocol::GetUidFromClientId(client_id); + const auto resources = m_app_resource_map.find(uid); + if (resources == m_app_resource_map.end()) + return score::crypto::make_unexpected(Error::kInvalidResourceId); + const auto resource = resources->second.find(app_resource_id); + if (resource == resources->second.end()) + return score::crypto::make_unexpected(Error::kInvalidResourceId); + const auto store = m_name_index.find(resource->second); + if (store == m_name_index.end()) + return score::crypto::make_unexpected(Error::kInvalidResourceId); + return TrustStoreHandle{store->second}; +} + +// --------------------------------------------------------------------------- +// Slot membership query +// --------------------------------------------------------------------------- + +std::vector TrustStoreManager::GetMembershipsForSlot(CertSlotHandle slot) const +{ + std::lock_guard lock(m_mutex); + const auto it = m_slot_memberships.find(slot.index); + return it == m_slot_memberships.end() ? std::vector{} : it->second; +} + +// --------------------------------------------------------------------------- +// Ref counting — verification context lifecycle (per-client scoped) +// --------------------------------------------------------------------------- + +void TrustStoreManager::MaybeEvictAnchorCache(TrustStoreHandle handle) +{ + // Must be called with m_mutex held. + for (const auto& [cid, stores] : m_client_ref_counts) + { + if (stores.count(handle.index)) + return; // at least one client still active for this store + } + if (handle.index < m_stores.size()) + { + auto* h = static_cast(m_stores[handle.index].handler.get()); + if (h) + h->ClearAnchorCache(); + } + score::mw::log::LogDebug() << kLogPrefix << "No active clients for store index " << handle.index + << " — anchor cache cleared"; +} + +void TrustStoreManager::AddRef(TrustStoreHandle handle, data_manager::ClientId client_id) +{ + std::lock_guard lock(m_mutex); + ++m_client_ref_counts[client_id][handle.index]; +} + +void TrustStoreManager::ReleaseRef(TrustStoreHandle handle, data_manager::ClientId client_id) +{ + std::lock_guard lock(m_mutex); + const auto client_it = m_client_ref_counts.find(client_id); + if (client_it == m_client_ref_counts.end()) + return; + auto& per_store = client_it->second; + const auto store_it = per_store.find(handle.index); + if (store_it == per_store.end() || store_it->second == 0U) + return; + if (--store_it->second == 0U) + { + per_store.erase(store_it); + if (per_store.empty()) + m_client_ref_counts.erase(client_it); + MaybeEvictAnchorCache(handle); + } +} + +void TrustStoreManager::CleanupClient(data_manager::ClientId client_id) +{ + std::lock_guard lock(m_mutex); + const auto client_it = m_client_ref_counts.find(client_id); + if (client_it == m_client_ref_counts.end()) + return; + // Collect affected stores before erasing the client entry. + std::vector affected; + affected.reserve(client_it->second.size()); + for (const auto& [store_id, count] : client_it->second) + affected.push_back(TrustStoreHandle{store_id}); + m_client_ref_counts.erase(client_it); + // Evict caches for stores that now have no remaining active clients. + for (const auto& handle : affected) + MaybeEvictAnchorCache(handle); +} + +// --------------------------------------------------------------------------- +// Slot change notification +// --------------------------------------------------------------------------- + +void TrustStoreManager::NotifySlotChanged(TrustStoreId id, CertSlotHandle changed_slot) +{ + std::lock_guard lock(m_mutex); + if (id >= m_stores.size()) + return; + // Evict the changed slot from the shared cache so LoadOrGetCached does fresh I/O. + m_slot_cert_cache.erase(changed_slot.index); + // Tell the handler to drop just this slot and mark itself for reload. + auto* h = static_cast(m_stores[id].handler.get()); + if (h) + h->InvalidateSlot(changed_slot); +} + +// --------------------------------------------------------------------------- +// Mutation operations +// --------------------------------------------------------------------------- + +std::optional TrustStoreManager::ResolveMember(const TrustStoreMemberConfig& member) +{ + if (!m_slot_registry) + return std::nullopt; + const auto slot = m_slot_registry->ResolveSlotInternal(member.slot_name); + if (!slot) + return std::nullopt; + const auto cfg = m_slot_registry->GetConfig(*slot); + if (!cfg) + return std::nullopt; + auto* handler = GetOrCreateHandler(*slot); + if (!handler) + return std::nullopt; + return ResolvedMember{*slot, handler, *cfg}; +} + +score::crypto::Expected TrustStoreManager::ResolveSlotBackend( + CertSlotHandle slot) +{ + if (!m_slot_registry) + return score::crypto::make_unexpected(Error::kInvalidResourceId); + const auto cfg = m_slot_registry->GetConfig(slot); + if (!cfg) + return score::crypto::make_unexpected(Error::kInvalidResourceId); + auto* handler = GetOrCreateHandler(slot); + if (!handler) + return score::crypto::make_unexpected(Error::kInvalidResourceId); + return ResolvedBackend{*cfg, handler}; +} + +score::crypto::Expected TrustStoreManager::AddMember(TrustStoreId id, + CertObject::Sptr cert, + data_manager::ClientId client_id) +{ + std::lock_guard lock(m_mutex); + if (id >= m_stores.size() || !cert) + return score::crypto::make_unexpected(Error::kInvalidArgument); + if (!AccessPolicyEnforcer::CheckTrustStoreWritePermission(m_stores[id].config, client_id).has_value()) + return score::crypto::make_unexpected(Error::kAccessDenied); + for (const auto& member : m_stores[id].config.members) + { + if (member.kind != TrustStoreMemberKind::kExclusiveMutable) + continue; + const auto resolved = ResolveMember(member); + if (!resolved) + continue; + const auto current = resolved->handler->LoadCertificate(*resolved->cfg); + if (current) + { + const auto& current_fp = (*current)->GetFingerprint(); + const auto& cert_fp = cert->GetFingerprint(); + if (current_fp.size() != cert_fp.size() || + !std::equal(current_fp.begin(), current_fp.end(), cert_fp.begin())) + continue; // Slot occupied by a different cert — try the next exclusive slot. + // Idempotent: cert already stored — re-enable and refresh cache. + m_member_states[id][resolved->slot.index].enabled = true; + m_slot_cert_cache[resolved->slot.index] = cert; + static_cast(m_stores[id].handler.get())->NotifySlotUpdate(resolved->slot, cert); + if (const auto persisted = PersistState(id); !persisted) + return score::crypto::make_unexpected(persisted.error()); + return std::monostate{}; + } + // Empty slot — store cert here. + const auto stored = resolved->handler->StoreCertificate(*resolved->cfg, *cert); + if (!stored) + return score::crypto::make_unexpected(stored.error()); + m_member_states[id][resolved->slot.index].enabled = true; + m_slot_cert_cache[resolved->slot.index] = cert; + static_cast(m_stores[id].handler.get())->NotifySlotUpdate(resolved->slot, cert); + if (const auto persisted = PersistState(id); !persisted) + return score::crypto::make_unexpected(persisted.error()); + return std::monostate{}; + } + return score::crypto::make_unexpected(Error::kTrustStoreCapacityExceeded); +} + +score::crypto::Expected TrustStoreManager::RemoveMember(TrustStoreId id, + const std::vector& fingerprint, + data_manager::ClientId client_id) +{ + std::lock_guard lock(m_mutex); + if (id >= m_stores.size()) + return score::crypto::make_unexpected(Error::kInvalidResourceId); + if (!AccessPolicyEnforcer::CheckTrustStoreWritePermission(m_stores[id].config, client_id).has_value()) + return score::crypto::make_unexpected(Error::kAccessDenied); + for (const auto& member : m_stores[id].config.members) + { + if (member.kind != TrustStoreMemberKind::kExclusiveMutable) + continue; + const auto resolved = ResolveMember(member); + if (!resolved) + continue; + const auto current = resolved->handler->LoadCertificate(*resolved->cfg); + if (!current) + continue; + const auto& current_fp = (*current)->GetFingerprint(); + if (current_fp.size() != fingerprint.size() || + !std::equal(current_fp.begin(), current_fp.end(), fingerprint.begin())) + continue; + static_cast(resolved->handler->ClearSlot(*resolved->cfg)); + // Cert bytes gone — evict from shared cache and clear from handler. + m_slot_cert_cache.erase(resolved->slot.index); + m_member_states[id][resolved->slot.index].enabled = false; + static_cast(m_stores[id].handler.get())->NotifySlotUpdate(resolved->slot, nullptr); + if (const auto persisted = PersistState(id); !persisted) + return score::crypto::make_unexpected(persisted.error()); + return std::monostate{}; + } + return score::crypto::make_unexpected(Error::kInvalidArgument); +} + +score::crypto::Expected TrustStoreManager::EnableMember(TrustStoreId id, + CertSlotHandle slot, + data_manager::ClientId client_id) +{ + std::lock_guard lock(m_mutex); + if (id >= m_stores.size() || !slot.IsValid()) + return score::crypto::make_unexpected(Error::kInvalidResourceId); + if (!AccessPolicyEnforcer::CheckTrustStoreWritePermission(m_stores[id].config, client_id).has_value()) + return score::crypto::make_unexpected(Error::kAccessDenied); + // Load via shared cache so other stores benefit from the strong ref. + auto cert = LoadOrGetCached(slot); + m_member_states[id][slot.index].enabled = true; + static_cast(m_stores[id].handler.get())->NotifySlotUpdate(slot, std::move(cert)); + if (const auto persisted = PersistState(id); !persisted) + return score::crypto::make_unexpected(persisted.error()); + return std::monostate{}; +} + +score::crypto::Expected TrustStoreManager::DisableMember(TrustStoreId id, + CertSlotHandle slot, + data_manager::ClientId client_id) +{ + std::lock_guard lock(m_mutex); + if (id >= m_stores.size() || !slot.IsValid()) + return score::crypto::make_unexpected(Error::kInvalidResourceId); + if (!AccessPolicyEnforcer::CheckTrustStoreWritePermission(m_stores[id].config, client_id).has_value()) + return score::crypto::make_unexpected(Error::kAccessDenied); + m_member_states[id][slot.index].enabled = false; + static_cast(m_stores[id].handler.get())->NotifySlotUpdate(slot, nullptr); + if (const auto persisted = PersistState(id); !persisted) + return score::crypto::make_unexpected(persisted.error()); + return std::monostate{}; +} + +score::crypto::Expected +TrustStoreManager::AcknowledgeMemberUpdate(TrustStoreId id, CertSlotHandle slot, data_manager::ClientId client_id) +{ + std::lock_guard lock(m_mutex); + if (id >= m_stores.size() || !slot.IsValid()) + return score::crypto::make_unexpected(Error::kInvalidResourceId); + if (!AccessPolicyEnforcer::CheckTrustStoreWritePermission(m_stores[id].config, client_id).has_value()) + return score::crypto::make_unexpected(Error::kAccessDenied); + // Fresh load to capture the new cert and update the shared cache. + const auto resolved = ResolveSlotBackend(slot); + if (!resolved) + return score::crypto::make_unexpected(resolved.error()); + auto loaded = resolved->handler->LoadCertificate(*resolved->cfg); + if (!loaded) + return score::crypto::make_unexpected(Error::kInvalidArgument); + auto cert = std::move(*loaded); + m_slot_cert_cache[slot.index] = cert; + auto& state = m_member_states[id][slot.index]; + state.accepted_fingerprint = CopyFingerprint(cert->GetFingerprint()); + state.enabled = true; + static_cast(m_stores[id].handler.get())->NotifySlotUpdate(slot, cert); + if (const auto persisted = PersistState(id); !persisted) + return score::crypto::make_unexpected(persisted.error()); + return std::monostate{}; +} + +const TrustStoreConfig* TrustStoreManager::GetStoreConfig(TrustStoreId id) const +{ + std::lock_guard lock(m_mutex); + return id < m_stores.size() ? &m_stores[id].config : nullptr; +} + +} // namespace score::crypto::daemon::cert_management diff --git a/score/crypto/src/daemon/cert_management/truststore/trust_store_manager.hpp b/score/crypto/src/daemon/cert_management/truststore/trust_store_manager.hpp new file mode 100644 index 000000000..e2a29f86c --- /dev/null +++ b/score/crypto/src/daemon/cert_management/truststore/trust_store_manager.hpp @@ -0,0 +1,264 @@ +/******************************************************************************** + * 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 + ********************************************************************************/ + +#ifndef SCORE_CRYPTO_SRC_DAEMON_CERT_MANAGEMENT_TRUSTSTORE_TRUST_STORE_MANAGER_HPP +#define SCORE_CRYPTO_SRC_DAEMON_CERT_MANAGEMENT_TRUSTSTORE_TRUST_STORE_MANAGER_HPP + +#include "score/crypto/src/daemon/cert_management/interfaces/cert_object.hpp" +#include "score/crypto/src/daemon/cert_management/interfaces/cert_slot_config.hpp" +#include "score/crypto/src/daemon/cert_management/interfaces/cert_types.hpp" +#include "score/crypto/src/daemon/cert_management/interfaces/i_cert_slot_handler.hpp" +#include "score/crypto/src/daemon/cert_management/interfaces/i_trust_store_handler.hpp" +#include "score/crypto/src/daemon/cert_management/interfaces/trust_store_config.hpp" +#include "score/crypto/src/daemon/cert_management/policy/access_policy_enforcer.hpp" +#include "score/crypto/src/daemon/cert_management/slot/slot_registry.hpp" +#include "score/crypto/src/daemon/cert_management/truststore/trust_store_handler.hpp" +#include "score/crypto/src/daemon/common/daemon_error.hpp" +#include "score/crypto/src/daemon/data_manager/data_node.hpp" + +#include +#include +#include +#include +#include +#include +#include + +namespace score::crypto::daemon::cert_management +{ + +/// @brief Manages named trust stores — collections of certificate trust anchors. +/// +/// A trust store is a named, many-to-many view over typed certificate slots. A slot may back +/// multiple trust stores simultaneously; a single SaveCertificate to the slot triggers +/// NotifySlotChanged() for every referencing store. +/// +/// Startup: +/// Load() populates the store list, slot-membership reverse index, and deployment-backed +/// member state. Cert content is NOT loaded at startup — it is loaded lazily on the first +/// GetAnchors() call to the handler (demand-paged, demand-freed). +/// +/// Lazy loading and shared cert cache: +/// m_slot_cert_cache holds a weak_ptr per slot index. A TrustStoreHandler +/// holds strong refs in its internal map for the duration of active use. When the last +/// DataNode for a trust store is released (ReleaseRef drops to zero), the handler's +/// anchor cache is cleared, strong refs drop, and the weak_ptrs expire unless another +/// active store holds the same slot's cert. +/// +/// Runtime: +/// AddRef()/ReleaseRef() track active DataNode count per store. +/// NotifySlotChanged(id, slot) invalidates one slot and forces reload on next GetAnchors(). +/// AddMember() / RemoveMember() mutate membership and persist to the descriptor. +/// +/// Thread safety: internal mutex guards all public methods. +class TrustStoreManager +{ + public: + using Sptr = std::shared_ptr; + + TrustStoreManager() = default; + ~TrustStoreManager() = default; + + TrustStoreManager(const TrustStoreManager&) = delete; + TrustStoreManager& operator=(const TrustStoreManager&) = delete; + TrustStoreManager(TrustStoreManager&&) = delete; + TrustStoreManager& operator=(TrustStoreManager&&) = delete; + + // ----------------------------------------------------------------------- + // Startup loading + // ----------------------------------------------------------------------- + + /// @brief Populate the trust store list from configuration. + /// + /// Called once at daemon startup, after CertSlotRegistry is populated. + /// For each TrustStoreConfig: + /// - Resolves typed members (slot_name → CertSlotHandle via registry) + /// - Populates m_slot_memberships reverse index and m_member_states from descriptor + /// - Does NOT load cert content — certs are loaded lazily on first GetAnchors() + void Load(const std::vector& store_configs, + CertSlotRegistry::Sptr slot_registry, + CertSlotHandlerFactory slot_handler_factory = {}); + + // ----------------------------------------------------------------------- + // Store access + // ----------------------------------------------------------------------- + + [[nodiscard]] ITrustStoreHandler::Sptr GetStore(TrustStoreId id) const; + [[nodiscard]] TrustStoreId ResolveByName(const std::string& name) const; + + void RegisterAppResource(uint32_t uid, const std::string& app_resource_id, const std::string& store_name); + [[nodiscard]] score::crypto::Expected + ResolveAppResource(const std::string& app_resource_id, data_manager::ClientId client_id) const; + + // ----------------------------------------------------------------------- + // Slot membership query + // ----------------------------------------------------------------------- + + /// @brief Return the set of trust store IDs that contain a given cert slot. + /// + /// Used by CertManagementService::SaveCertificate to fan out NotifyUpdate(). + [[nodiscard]] std::vector GetMembershipsForSlot(CertSlotHandle slot_handle) const; + + // ----------------------------------------------------------------------- + // Runtime notifications and mutations + // ----------------------------------------------------------------------- + + /// @brief Increment the active-context count for a trust store on behalf of @p client_id. + /// + /// Called by ScoreCertVerificationHandler when SetVerificationTrustStore() binds a + /// store to a verification context. Certs are loaded lazily on first GetAnchors(). + /// Refs are per-client so that releasing one application's contexts does not affect + /// another application's active references to the same shared trust store. + void AddRef(TrustStoreHandle handle, data_manager::ClientId client_id); + + /// @brief Decrement the active-context count for @p client_id on @p handle. + /// + /// When a client's count for the store reaches zero and no other client holds refs, + /// the anchor cache is cleared. CertObject strong-refs drop; memory is freed unless + /// another active trust store shares the same slot's cert via the weak_ptr cache. + void ReleaseRef(TrustStoreHandle handle, data_manager::ClientId client_id); + + /// @brief Release all refs held by @p client_id across all trust stores. + /// + /// Called by CertManagementService::CleanupClient() on client crash or disconnect. + /// Unconditionally removes all per-client counts for the dead client, then evicts + /// anchor caches for any trust store that has no remaining active clients. + void CleanupClient(data_manager::ClientId client_id); + + /// @brief Invalidate one member slot in the given trust store. + /// + /// Evicts the slot from the shared cert cache and marks the handler for reload. + /// Called by CertManagementService::NotifySlotCertChanged() after StoreCertificate. + /// Unchanged member slots retain their cached strong-refs; only the changed slot + /// pays a reload cost on the next GetAnchors() call. + void NotifySlotChanged(TrustStoreId id, CertSlotHandle changed_slot); + + /// @brief Add a certificate to a trust store's runtime anchor set. + /// + /// Persists the new member to the trust store descriptor. + /// Write access to the trust store must be checked by CertManagementService + /// before calling this method. + [[nodiscard]] score::crypto::Expected + AddMember(TrustStoreId id, CertObject::Sptr cert, data_manager::ClientId client_id); + + /// @brief Remove a certificate from a trust store by fingerprint. + /// + /// Persists the change to the trust store descriptor and calls NotifyUpdate(). + /// Write access must be checked before calling. + [[nodiscard]] score::crypto::Expected + RemoveMember(TrustStoreId id, const std::vector& fingerprint, data_manager::ClientId client_id); + + [[nodiscard]] score::crypto::Expected + EnableMember(TrustStoreId id, CertSlotHandle slot, data_manager::ClientId client_id); + [[nodiscard]] score::crypto::Expected + DisableMember(TrustStoreId id, CertSlotHandle slot, data_manager::ClientId client_id); + [[nodiscard]] score::crypto::Expected + AcknowledgeMemberUpdate(TrustStoreId id, CertSlotHandle slot, data_manager::ClientId client_id); + + // ----------------------------------------------------------------------- + // Configuration access + // ----------------------------------------------------------------------- + + /// @brief Return the TrustStoreConfig for a given store ID. + [[nodiscard]] const TrustStoreConfig* GetStoreConfig(TrustStoreId id) const; + + private: + struct MemberState + { + bool enabled{true}; + std::optional> accepted_fingerprint; + }; + + /// Result of resolving a TrustStoreMemberConfig to its live slot, handler, and config. + /// All pointers are non-null. Valid only while m_mutex is held. + struct ResolvedMember + { + CertSlotHandle slot; + ICertSlotHandler* handler; + const CertSlotConfig* cfg; + }; + /// Returns nullopt if the member's slot name cannot be resolved or has no registered handler. + [[nodiscard]] std::optional ResolveMember(const TrustStoreMemberConfig& member); + + /// Result of looking up a CertSlotHandle's config and handler. + /// All pointers are non-null. Valid only while m_mutex is held. + struct ResolvedBackend + { + const CertSlotConfig* cfg; + ICertSlotHandler* handler; + }; + /// Returns kInvalidResourceId if the slot is not registered or has no handler. + [[nodiscard]] score::crypto::Expected ResolveSlotBackend( + CertSlotHandle slot); + + /// Returns the handler for @p slot, creating it via m_slot_handler_factory on first access. + /// Returns nullptr if the factory is absent, the slot has no config, or factory returns nullptr. + /// Must be called with m_mutex held. + ICertSlotHandler* GetOrCreateHandler(CertSlotHandle slot); + + void LoadState(TrustStoreId id); + score::crypto::Expected PersistState(TrustStoreId id) const; + + /// Populate handler's anchor cache for one trust store. Called by the AnchorLoader + /// lambda captured inside each TrustStoreHandler. Acquires m_mutex. + void LoadAnchorsIntoHandler(TrustStoreId id, TrustStoreHandler& handler); + + /// Evict the anchor cache for @p handle if no client currently holds a ref to it. + /// Must be called with m_mutex held. + void MaybeEvictAnchorCache(TrustStoreHandle handle); + + /// Promote the weak_ptr for slot from m_slot_cert_cache, or load fresh and cache it. + /// Returns nullptr if the slot is empty or the backend returns an error. + /// Must be called with m_mutex held. + CertObject::Sptr LoadOrGetCached(CertSlotHandle slot); + + struct TrustStoreEntry + { + TrustStoreConfig config; + ITrustStoreHandler::Sptr handler; + TrustStoreId id{0U}; + }; + + mutable std::mutex m_mutex; + + std::vector m_stores; + std::unordered_map m_name_index; + std::unordered_map> m_app_resource_map; + + /// Reverse index: slot handle index → list of trust store IDs that reference it. + std::unordered_map> m_slot_memberships; + /// Runtime state loaded from/persisted to each trust-store deployment descriptor. + std::unordered_map> m_member_states; + + /// Cross-trust-store cert cache. Holds a weak_ptr so the cert is freed automatically + /// when no TrustStoreHandler (or other holder) keeps a strong ref alive. + std::unordered_map> m_slot_cert_cache; + + /// Per-client, per-store active context reference counts. + /// Outer key: ClientId (one entry per connected application). + /// Inner key: TrustStoreId index. Value: count of verification contexts currently + /// bound to that store for that client. + /// An entry is removed when the count drops to zero. + /// The anchor cache for a store is evicted only when ALL clients' counts drop to zero. + std::unordered_map> m_client_ref_counts; + + CertSlotRegistry::Sptr m_slot_registry; + std::unordered_map m_slot_handlers; + CertSlotHandlerFactory m_slot_handler_factory; + + static constexpr std::string_view kLogPrefix = "[TRUST_STORE_MANAGER] "; +}; + +} // namespace score::crypto::daemon::cert_management + +#endif // SCORE_CRYPTO_SRC_DAEMON_CERT_MANAGEMENT_TRUSTSTORE_TRUST_STORE_MANAGER_HPP diff --git a/score/crypto/src/daemon/common/daemon_error.hpp b/score/crypto/src/daemon/common/daemon_error.hpp index 7ff0b8a1c..034729ecd 100644 --- a/score/crypto/src/daemon/common/daemon_error.hpp +++ b/score/crypto/src/daemon/common/daemon_error.hpp @@ -99,6 +99,7 @@ enum class DaemonErrorCode : std::uint32_t kCsrGenerationFailed = 0x0907, kOcspError = 0x0908, kTrustAnchorNotFound = 0x0909, + kTrustStoreCapacityExceeded = 0x090A, ///< All exclusive-mutable slots in the trust store are occupied // ---- Provider ---- kProviderNotAvailable = 0x0A01, @@ -239,6 +240,8 @@ inline score::crypto::CryptoErrorCode ToCryptoErrorCode(DaemonErrorCode code) no return C::kOcspError; case DaemonErrorCode::kTrustAnchorNotFound: return C::kTrustAnchorNotFound; + case DaemonErrorCode::kTrustStoreCapacityExceeded: + return C::kQuotaExceeded; // ---- Provider ---- case DaemonErrorCode::kProviderNotAvailable: return C::kProviderNotAvailable; diff --git a/score/crypto/src/daemon/config/inc/config.hpp b/score/crypto/src/daemon/config/inc/config.hpp index 07373d506..c896c268d 100644 --- a/score/crypto/src/daemon/config/inc/config.hpp +++ b/score/crypto/src/daemon/config/inc/config.hpp @@ -342,13 +342,156 @@ using ScoreProviderConfig = ::score::crypto::daemon::provider::score_provider::S #endif /** - * @brief Certificate management configuration section (placeholder for future) + * @brief Certificate management configuration section + * + * Stores certificate slot and trust store definitions parsed from the daemon's + * configuration source. + * + * At daemon startup, a ConfigDrivenSlotCatalog reads CertSlotEntry items and + * registers each slot with the CertSlotRegistry. TrustStoreEntry items are + * consumed by TrustStoreManager during initialisation. */ class CertificateConfig { public: + enum class TrustStoreMemberKind : uint8_t + { + kSharedStatic = 0U, + kExclusiveMutable = 1U, + kConditionalExternal = 2U, + }; + + enum class ConditionalSlotInitialization : uint8_t + { + kEnableAndAcceptCurrent = 0U, + kDisableUntilAccepted = 1U, + }; + + struct TrustStoreMemberEntry + { + std::string slot_name; + TrustStoreMemberKind kind{TrustStoreMemberKind::kSharedStatic}; + }; + + /// @brief Per-application resource ID to certificate slot name mapping. + /// + /// Applications reference cert slots by portable local names rather than + /// daemon slot names. + struct AppCertSlotEntry + { + uint32_t uid; ///< UID of the application that owns this mapping + std::string app_resource_id; ///< Application-local resource name + std::string slot_name; ///< Actual cert slot name registered in the CertSlotRegistry + }; + + /// @brief Per-application resource ID to trust store name mapping. + /// + /// Applications reference trust stores by portable local names rather than + /// daemon trust store names. + struct AppTrustStoreEntry + { + uint32_t uid; ///< UID of the application that owns this mapping + std::string app_resource_id; ///< Application-local resource name + std::string trust_store_name; ///< Actual trust store name registered in the daemon + }; + + /// @brief A single certificate slot definition from the configuration source. + struct CertSlotEntry + { + std::string slot_name; + /// @brief Storage backend for this slot. Immutable after startup. + /// + /// Identifies which ICertSlotHandler implementation manages physical storage. + /// "DEFAULT" → FileBackedSlotHandler (legacy "file" is accepted). + /// Any other value is a provider name; that provider supplies the + /// ICertSlotHandler implementation. + /// Backend-specific locators (file path, token label) live in the KV descriptor. + std::string storage_backend{"DEFAULT"}; + std::vector allowed_uids; ///< UIDs permitted to read/load from the slot + std::vector allowed_write_uids; ///< UIDs permitted to write into the slot + /// @brief Absolute path to the KV deployment descriptor file. + std::string deployment_path; + /// @brief Format of the deployment descriptor (default "kv"). + std::string deployment_format{"kv"}; + /// @brief Integrity enforcement policy: "disabled" (default) or "required". + /// + /// "required": LoadCertificate fails if the [certificate] descriptor section + /// has no cert_hash entry or the stored hash does not match the file content. + /// The policy lives here (out-of-band from the descriptor) so a compromised + /// descriptor cannot bypass the check. + std::string integrity_policy{"disabled"}; + }; + + /// @brief A single trust store definition from the configuration source. + /// + /// Trust stores reference typed certificate-slot memberships; shared-static + /// slots can back multiple named trust stores. + struct TrustStoreEntry + { + std::string store_name; + /// @brief Certificate-slot memberships and their trust-store policy. + std::vector members; + /// @brief Default policy for conditional members without persisted state. + ConditionalSlotInitialization conditional_slot_initialization{ + ConditionalSlotInitialization::kDisableUntilAccepted}; + std::vector allowed_uids; ///< UIDs permitted to query this store + std::vector allowed_write_uids; ///< UIDs permitted to mutate this store + /// @brief Absolute path for persisting trust store state. + std::string deployment_path; + std::string deployment_format{"kv"}; + }; + CertificateConfig() = default; - // Add methods as needed + + /// @brief Add a certificate slot definition (called by parser). + void AddSlotEntry(CertSlotEntry entry) + { + m_slot_entries.push_back(std::move(entry)); + } + + /// @brief Get all parsed certificate slot definitions. + const std::vector& GetSlotEntries() const + { + return m_slot_entries; + } + + /// @brief Add a trust store definition (called by parser). + void AddTrustStoreEntry(TrustStoreEntry entry) + { + m_trust_store_entries.push_back(std::move(entry)); + } + + void AddAppCertSlotEntry(AppCertSlotEntry entry) + { + m_app_cert_slot_entries.push_back(std::move(entry)); + } + + const std::vector& GetAppCertSlotEntries() const + { + return m_app_cert_slot_entries; + } + + void AddAppTrustStoreEntry(AppTrustStoreEntry entry) + { + m_app_trust_store_entries.push_back(std::move(entry)); + } + + const std::vector& GetAppTrustStoreEntries() const + { + return m_app_trust_store_entries; + } + + /// @brief Get all parsed trust store definitions. + const std::vector& GetTrustStoreEntries() const + { + return m_trust_store_entries; + } + + private: + std::vector m_slot_entries; + std::vector m_trust_store_entries; + std::vector m_app_cert_slot_entries; + std::vector m_app_trust_store_entries; }; /** diff --git a/score/crypto/src/daemon/data_manager/data_node.hpp b/score/crypto/src/daemon/data_manager/data_node.hpp index 8a93a2cbf..9bb33b11a 100644 --- a/score/crypto/src/daemon/data_manager/data_node.hpp +++ b/score/crypto/src/daemon/data_manager/data_node.hpp @@ -47,6 +47,9 @@ enum class DataNodeType : std::uint8_t kKeySlot = 3U, ///< Key slot reference (KeySlotDataNode) kKeyData = 4U, ///< Loaded-key reference (KeyDataNode) kShm = 5U, ///< SHM region (ShmDataNode) + kCertSlot = 6U, ///< Certificate slot reference (CertSlotDataNode) + kCertData = 7U, ///< Loaded-certificate reference (CertDataNode) + kTrustStore = 8U, ///< Resolved trust-store reference (TrustStoreDataNode) }; /** diff --git a/score/crypto/src/daemon/provider/cert_management/BUILD b/score/crypto/src/daemon/provider/cert_management/BUILD new file mode 100644 index 000000000..2a9046948 --- /dev/null +++ b/score/crypto/src/daemon/provider/cert_management/BUILD @@ -0,0 +1,44 @@ +# ******************************************************************************* +# 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 +# ******************************************************************************* +# ******************************************************************************* +# Copyright (c) 2026 Contributors to the Eclipse Foundation +# SPDX-License-Identifier: Apache-2.0 +# ******************************************************************************* +load("@rules_cc//cc:cc_library.bzl", "cc_library") + +cc_library( + name = "cert_parser_headers", + hdrs = ["i_cert_parser.hpp"], + includes = ["."], + visibility = ["//:__subpackages__"], + deps = [ + "//score/crypto/src/api/common:crypto_common", + "//score/crypto/src/common:common_types", + "//score/crypto/src/daemon/common", + ], +) + +cc_library( + name = "cert_management_provider_headers", + hdrs = [ + "cert_management_operations.hpp", + "cert_types.hpp", + ], + includes = ["."], + visibility = ["//:__subpackages__"], + deps = [ + ":cert_parser_headers", + "//score/crypto/src/api/common:crypto_common", + "//score/crypto/src/daemon/cert_management:cert_management_headers", + ], +) diff --git a/score/crypto/src/daemon/provider/cert_management/cert_management_operations.hpp b/score/crypto/src/daemon/provider/cert_management/cert_management_operations.hpp new file mode 100644 index 000000000..1f93295a9 --- /dev/null +++ b/score/crypto/src/daemon/provider/cert_management/cert_management_operations.hpp @@ -0,0 +1,57 @@ +/******************************************************************************** + * 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 + ********************************************************************************/ +/******************************************************************************** + * Copyright (c) 2026 Contributors to the Eclipse Foundation + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ +#ifndef SCORE_CRYPTO_SRC_DAEMON_PROVIDER_CERT_MANAGEMENT_CERT_MANAGEMENT_OPERATIONS_HPP +#define SCORE_CRYPTO_SRC_DAEMON_PROVIDER_CERT_MANAGEMENT_CERT_MANAGEMENT_OPERATIONS_HPP + +#include "score/crypto/src/daemon/common/types.hpp" +#include + +namespace score::crypto::daemon::provider::cert_management +{ +using OperationAction = common::OperationAction; +inline constexpr OperationAction CERT_PARSE = 0x10U; +inline constexpr OperationAction CERT_PARSE_CHAIN = 0x11U; +inline constexpr OperationAction CERT_SAVE = 0x12U; +inline constexpr OperationAction CERT_GET_METADATA = 0x13U; +inline constexpr OperationAction CERT_LOAD = 0x20U; +inline constexpr OperationAction CERT_EXPORT = 0x30U; +inline constexpr OperationAction CERT_GET_EXPORT_SIZE = 0x31U; +inline constexpr OperationAction CERT_CONVERT = 0x32U; +inline constexpr OperationAction CERT_GET_CONVERT_SIZE = 0x33U; +inline constexpr OperationAction CERT_CLEAR = 0x40U; +inline constexpr OperationAction CERT_SLOT_INFO = 0x50U; +inline constexpr OperationAction CERT_PUBLIC_KEY = 0x60U; +inline constexpr OperationAction CERT_DELETE_EXPIRED = 0x70U; +inline constexpr OperationAction CRL_IMPORT = 0x80U; +inline constexpr OperationAction CRL_DELETE = 0x81U; +inline constexpr OperationAction CRL_DELETE_EXPIRED = 0x82U; +inline constexpr OperationAction OCSP_REQUEST = 0x90U; +inline constexpr OperationAction TRUST_STORE_ADD_CERT = 0xC0U; +inline constexpr OperationAction TRUST_STORE_REMOVE_CERT = 0xC1U; +inline constexpr OperationAction TRUST_STORE_ENABLE_CERT = 0xC2U; +inline constexpr OperationAction TRUST_STORE_DISABLE_CERT = 0xC3U; +inline constexpr OperationAction TRUST_STORE_ACK_UPDATE = 0xC4U; +inline constexpr OperationAction TRUST_STORE_REMOVE_CERT_BY_ID = 0xC5U; // remove by cert node_id (lib resolves fp) +inline constexpr OperationAction CERT_RELEASE = 0xF0U; + +// Provider-specific operation IDs must be >= CUSTOM_OP_START. +inline constexpr OperationAction CUSTOM_OP_START = + static_cast(1U << (std::numeric_limits::digits - 1)); + +} // namespace score::crypto::daemon::provider::cert_management + +#endif // SCORE_CRYPTO_SRC_DAEMON_PROVIDER_CERT_MANAGEMENT_CERT_MANAGEMENT_OPERATIONS_HPP diff --git a/score/crypto/src/daemon/provider/cert_management/cert_types.hpp b/score/crypto/src/daemon/provider/cert_management/cert_types.hpp new file mode 100644 index 000000000..1ad42db8c --- /dev/null +++ b/score/crypto/src/daemon/provider/cert_management/cert_types.hpp @@ -0,0 +1,98 @@ +/******************************************************************************** + * 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 + ********************************************************************************/ +/******************************************************************************** + * Copyright (c) 2026 Contributors to the Eclipse Foundation + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ +#ifndef SCORE_CRYPTO_SRC_DAEMON_PROVIDER_CERT_MANAGEMENT_CERT_TYPES_HPP +#define SCORE_CRYPTO_SRC_DAEMON_PROVIDER_CERT_MANAGEMENT_CERT_TYPES_HPP + +#include "score/crypto/src/api/common/types.hpp" +#include "score/crypto/src/daemon/cert_management/interfaces/cert_object.hpp" +#include "score/crypto/src/daemon/cert_management/interfaces/cert_types.hpp" +#include "score/crypto/src/daemon/key_management/interfaces/key_types.hpp" + +#include +#include +#include +#include +#include +#include + +namespace score::crypto::daemon::provider::cert_management +{ +using ::score::crypto::daemon::cert_management::CertObject; +using ::score::crypto::daemon::cert_management::CertSlotHandle; +using ::score::crypto::daemon::cert_management::TrustStoreHandle; + +enum class ChainTerminationPolicy : std::uint8_t +{ + kRootRequired = 0, + kTrustStoreTerminated = 1 +}; +enum class CertVerifyErrorCode : std::uint16_t +{ + kNone = 0, + kExpired, + kNotYetValid, + kRevoked, + kNoRootFound, + kChainIncomplete, + kSignatureInvalid, + kInvalidPurpose, + kUnknownAlgorithm, + kUnknownError +}; + +/// Verification result. The established chain is returned as neutral CertObjects +/// (leaf-first, terminating anchor last), never as provider-bound handles. +struct CertVerifyResult +{ + bool is_valid{false}; + CertVerifyErrorCode error_code{CertVerifyErrorCode::kNone}; + std::vector verified_chain; +}; + +struct CrlImportRequest +{ + const std::uint8_t* crl_data{nullptr}; + std::size_t crl_data_size{0}; + score::crypto::FormatType format{score::crypto::FormatType::kDer}; + CertSlotHandle cert_slot; +}; +struct CsrGenerationRequest +{ + score::crypto::daemon::key_management::ProviderKeyHandle subject_key; + std::string signature_algorithm; + std::string subject_dn; +}; + +/// Chain-verification inputs. All certificates are provider-neutral CertObjects; +/// the service resolves client CryptoResourceIds to CertObjects before calling +/// the provider, so the provider surface never sees provider-bound cert handles. +struct VerificationRequest +{ + std::vector chain; + std::optional trust_store; + std::vector standalone_trusted_certs; + ChainTerminationPolicy chain_termination{ChainTerminationPolicy::kRootRequired}; + std::vector additional_certificates; + std::optional> ephemeral_crl; + std::optional crl_slot; + std::optional> ocsp_response; + std::optional verification_time_epoch_s; + score::crypto::RevocationCheckPolicy revocation_policy{score::crypto::RevocationCheckPolicy::kNone}; +}; +} // namespace score::crypto::daemon::provider::cert_management + +#endif // SCORE_CRYPTO_SRC_DAEMON_PROVIDER_CERT_MANAGEMENT_CERT_TYPES_HPP diff --git a/score/crypto/src/daemon/provider/cert_management/i_cert_parser.hpp b/score/crypto/src/daemon/provider/cert_management/i_cert_parser.hpp new file mode 100644 index 000000000..d7322ec42 --- /dev/null +++ b/score/crypto/src/daemon/provider/cert_management/i_cert_parser.hpp @@ -0,0 +1,63 @@ +/******************************************************************************** + * 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 + ********************************************************************************/ +/******************************************************************************** + * Copyright (c) 2026 Contributors to the Eclipse Foundation + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ +#ifndef SCORE_CRYPTO_SRC_DAEMON_PROVIDER_CERT_MANAGEMENT_I_CERT_PARSER_HPP +#define SCORE_CRYPTO_SRC_DAEMON_PROVIDER_CERT_MANAGEMENT_I_CERT_PARSER_HPP + +#include "score/crypto/src/api/common/types.hpp" +#include "score/crypto/src/common/types.hpp" +#include "score/crypto/src/daemon/common/daemon_error.hpp" + +#include +#include +#include +#include + +namespace score::crypto::daemon::cert_management +{ +class CertObject; +} + +namespace score::crypto::daemon::provider::cert_management +{ + +/// Narrow provider capability: parse certificate bytes into a provider-neutral +/// CertObject. This is the only cross-boundary cert interface on IProvider — +/// used exclusively by FileBackedSlotHandler at startup to reconstruct a +/// CertObject from persisted bytes. All other cert operations (verification, +/// CSR generation, format conversion, public-key extraction) are performed +/// inside certificate context handlers created by ICryptoHandlerFactory. +class ICertParser +{ + public: + using Sptr = std::shared_ptr; + virtual ~ICertParser() = default; + + /// Parse a single DER/PEM certificate into a neutral CertObject. + [[nodiscard]] virtual score::crypto::Expected, + common::DaemonErrorCode> + ParseCertificate(const std::uint8_t* bytes, std::size_t size, score::crypto::FormatType format) = 0; + + /// Parse one or more concatenated certificates (e.g. a PEM bundle). + [[nodiscard]] virtual score::crypto::Expected< + std::vector>, + common::DaemonErrorCode> + ParseCertificates(const std::uint8_t* bytes, std::size_t size, score::crypto::FormatType format) = 0; +}; + +} // namespace score::crypto::daemon::provider::cert_management + +#endif // SCORE_CRYPTO_SRC_DAEMON_PROVIDER_CERT_MANAGEMENT_I_CERT_PARSER_HPP diff --git a/score/crypto/src/daemon/provider/i_provider.hpp b/score/crypto/src/daemon/provider/i_provider.hpp index c418ec559..13140a126 100644 --- a/score/crypto/src/daemon/provider/i_provider.hpp +++ b/score/crypto/src/daemon/provider/i_provider.hpp @@ -36,6 +36,17 @@ namespace score::crypto::daemon::data_plane { class IShmFactory; } // namespace score::crypto::daemon::data_plane +namespace score::crypto::daemon::provider::cert_management +{ +class ICertParser; +} // namespace score::crypto::daemon::provider::cert_management + +namespace score::crypto::daemon::cert_management +{ +class CertManagementService; +class ICertSlotHandler; +struct CertSlotConfig; +} // namespace score::crypto::daemon::cert_management namespace score::crypto::daemon::provider { @@ -121,6 +132,39 @@ class IProvider return nullptr; } + /// Return the provider's certificate parser for FileBackedSlotHandler injection at startup. + /// + /// Returns nullptr if the provider does not support certificate parsing. Only called + /// by CertManagementModule::Create() to obtain a parser for file-backed slots. + /// Cert context operations (verify, CSR, etc.) are handled by ICryptoHandlerFactory. + virtual std::shared_ptr GetCertParser() + { + return nullptr; + } + + /// @brief Inject the daemon-wide certificate management service. + /// + /// Called once at daemon startup before any cert handler is created. + /// Providers that do not support cert management may ignore this (default no-op). + virtual void SetCertManagementService( + std::shared_ptr<::score::crypto::daemon::cert_management::CertManagementService> /*service*/) + { + } + + /// Return a handler for a provider-owned certificate storage slot. + /// + /// Returns nullptr when this provider does not implement certificate-slot + /// storage. The caller selects the provider by name via slot.storage_backend + /// (name-based lookup), NOT via the kCertManagement capability bit. A provider + /// may implement this method without advertising kCertManagement — that bit + /// exclusively governs GetCertParser() selection. + virtual std::shared_ptr<::score::crypto::daemon::cert_management::ICertSlotHandler> GetCertSlotHandler( + const ::score::crypto::daemon::cert_management::CertSlotConfig& /*config*/, + std::shared_ptr /*parser*/) + { + return nullptr; + } + /// @brief Return a key slot handler for the given slot configuration. /// /// Returns nullptr if the provider does not support key slot management. diff --git a/score/crypto/src/daemon/provider/score_provider/openssl/BUILD b/score/crypto/src/daemon/provider/score_provider/openssl/BUILD index 1ba4c2fd0..689e00960 100644 --- a/score/crypto/src/daemon/provider/score_provider/openssl/BUILD +++ b/score/crypto/src/daemon/provider/score_provider/openssl/BUILD @@ -30,6 +30,34 @@ cc_library( ], ) +cc_library( + name = "openssl_cert_management_headers", + hdrs = ["cert_management/openssl_cert_parser.hpp"], + target_compatible_with = select({ + "//score/crypto/src/backend:openssl_backend_active": [], + "//conditions:default": ["@platforms//:incompatible"], + }), + visibility = ["//:__subpackages__"], + deps = [ + "//score/crypto/src/daemon/provider/cert_management:cert_parser_headers", + ], +) + +cc_library( + name = "openssl_cert_parser_library", + srcs = ["cert_management/openssl_cert_parser.cpp"], + implementation_deps = ["//third_party/openssl:openssl_shared"], + target_compatible_with = select({ + "//score/crypto/src/backend:openssl_backend_active": [], + "//conditions:default": ["@platforms//:incompatible"], + }), + visibility = ["//:__subpackages__"], + deps = [ + ":openssl_cert_management_headers", + "//score/crypto/src/daemon/cert_management:cert_management_headers", + ], +) + # Header-only library for OpenSSL algorithm detail headers. cc_library( name = "openssl_detail_headers", diff --git a/score/crypto/src/daemon/provider/score_provider/openssl/cert_management/openssl_cert_parser.cpp b/score/crypto/src/daemon/provider/score_provider/openssl/cert_management/openssl_cert_parser.cpp new file mode 100644 index 000000000..1471c9bda --- /dev/null +++ b/score/crypto/src/daemon/provider/score_provider/openssl/cert_management/openssl_cert_parser.cpp @@ -0,0 +1,236 @@ +/******************************************************************************** + * 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 + ********************************************************************************/ +/******************************************************************************** + * Copyright (c) 2026 Contributors to the Eclipse Foundation + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ +#include "score/crypto/src/daemon/provider/score_provider/openssl/cert_management/openssl_cert_parser.hpp" + +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +namespace score::crypto::daemon::provider::score_provider::openssl +{ +namespace +{ +using CertObject = ::score::crypto::daemon::cert_management::CertObject; +using Metadata = ::score::crypto::daemon::cert_management::CertChainMetadata; +using Error = common::DaemonErrorCode; + +struct X509Deleter +{ + void operator()(X509* value) const noexcept + { + X509_free(value); + } +}; +using X509Ptr = std::unique_ptr; + +std::string NameToString(X509_NAME* name) +{ + if (name == nullptr) + return {}; + BIO* raw_bio = BIO_new(BIO_s_mem()); + if (raw_bio == nullptr) + return {}; + std::unique_ptr bio{raw_bio, &BIO_free}; + if (X509_NAME_print_ex(bio.get(), name, 0, XN_FLAG_RFC2253 & ~XN_FLAG_DN_REV) < 0) + return {}; + char* data = nullptr; + const long size = BIO_get_mem_data(bio.get(), &data); + return size > 0 && data != nullptr ? std::string{data, static_cast(size)} : std::string{}; +} + +bool Asn1TimeToEpoch(const ASN1_TIME* value, int64_t& result) +{ + if (value == nullptr) + return false; + std::tm calendar{}; + if (ASN1_TIME_to_tm(value, &calendar) != 1) + return false; + const std::time_t epoch = timegm(&calendar); + if (epoch == static_cast(-1)) + return false; + result = static_cast(epoch); + return true; +} + +template +void CopyOpenSslBytes(const T* data, std::size_t size, std::vector& destination) +{ + if (data != nullptr && size != 0U) + { + const auto* first = reinterpret_cast(data); + destination.assign(first, first + size); + } +} + +score::crypto::Expected BuildObject(X509* certificate, + const std::uint8_t* bytes, + std::size_t size, + score::crypto::FormatType format) +{ + if (certificate == nullptr || bytes == nullptr || size == 0U) + return score::crypto::make_unexpected(Error::kCertificateParsingFailed); + + Metadata metadata; + metadata.subject_canonical = NameToString(X509_get_subject_name(certificate)); + metadata.issuer_canonical = NameToString(X509_get_issuer_name(certificate)); + if (metadata.subject_canonical.empty() || metadata.issuer_canonical.empty() || + !Asn1TimeToEpoch(X509_get0_notBefore(certificate), metadata.not_before_epoch_s) || + !Asn1TimeToEpoch(X509_get0_notAfter(certificate), metadata.not_after_epoch_s)) + { + return score::crypto::make_unexpected(Error::kCertificateParsingFailed); + } + + ASN1_OCTET_STRING* skid = + static_cast(X509_get_ext_d2i(certificate, NID_subject_key_identifier, nullptr, nullptr)); + if (skid != nullptr) + { + CopyOpenSslBytes( + ASN1_STRING_get0_data(skid), static_cast(ASN1_STRING_length(skid)), metadata.skid); + ASN1_OCTET_STRING_free(skid); + } + + auto* akid = + static_cast(X509_get_ext_d2i(certificate, NID_authority_key_identifier, nullptr, nullptr)); + if (akid != nullptr) + { + if (akid->keyid != nullptr) + CopyOpenSslBytes(ASN1_STRING_get0_data(akid->keyid), + static_cast(ASN1_STRING_length(akid->keyid)), + metadata.akid); + AUTHORITY_KEYID_free(akid); + } + + unsigned int digest_size = 0U; + std::array digest{}; + if (X509_digest(certificate, EVP_sha256(), digest.data(), &digest_size) != 1 || digest_size != SHA256_DIGEST_LENGTH) + { + return score::crypto::make_unexpected(Error::kCertificateParsingFailed); + } + metadata.fingerprint.assign(digest.begin(), digest.end()); + + BASIC_CONSTRAINTS* constraints = + static_cast(X509_get_ext_d2i(certificate, NID_basic_constraints, nullptr, nullptr)); + if (constraints != nullptr) + { + metadata.is_ca = constraints->ca != 0; + BASIC_CONSTRAINTS_free(constraints); + } + + return std::make_shared(std::move(metadata), std::vector{bytes, bytes + size}, format); +} + +X509Ptr ParseX509(const std::uint8_t* bytes, std::size_t size, score::crypto::FormatType format) +{ + if (bytes == nullptr || size == 0U) + return {nullptr}; + if (format == score::crypto::FormatType::kDer) + { + const unsigned char* cursor = bytes; + return X509Ptr{d2i_X509(nullptr, &cursor, static_cast(size))}; + } + BIO* raw_bio = BIO_new_mem_buf(bytes, static_cast(size)); + if (raw_bio == nullptr) + return {nullptr}; + std::unique_ptr bio{raw_bio, &BIO_free}; + return X509Ptr{PEM_read_bio_X509(bio.get(), nullptr, nullptr, nullptr)}; +} + +} // namespace + +score::crypto::Expected<::score::crypto::daemon::cert_management::CertObject::Sptr, common::DaemonErrorCode> +OpenSslCertParser::ParseCertificate(const std::uint8_t* bytes, std::size_t size, score::crypto::FormatType format) +{ + auto certificate = ParseX509(bytes, size, format); + if (!certificate) + return score::crypto::make_unexpected(common::DaemonErrorCode::kCertificateParsingFailed); + return BuildObject(certificate.get(), bytes, size, format); +} + +score::crypto::Expected, + common::DaemonErrorCode> +OpenSslCertParser::ParseCertificates(const std::uint8_t* bytes, std::size_t size, score::crypto::FormatType format) +{ + if (bytes == nullptr || size == 0U) + return score::crypto::make_unexpected(common::DaemonErrorCode::kCertificateParsingFailed); + + std::vector result; + + if (format == score::crypto::FormatType::kPem) + { + BIO* raw_bio = BIO_new_mem_buf(bytes, static_cast(size)); + if (raw_bio == nullptr) + return score::crypto::make_unexpected(common::DaemonErrorCode::kCertificateParsingFailed); + std::unique_ptr bio{raw_bio, &BIO_free}; + + while (true) + { + X509Ptr cert{PEM_read_bio_X509(bio.get(), nullptr, nullptr, nullptr)}; + if (!cert) + { + // PEM_R_NO_START_LINE signals no more PEM headers — clean EOF, not a parse error + const unsigned long err = ERR_peek_last_error(); + ERR_clear_error(); + if (ERR_GET_REASON(err) == PEM_R_NO_START_LINE) + break; + return score::crypto::make_unexpected(common::DaemonErrorCode::kCertificateParsingFailed); + } + // Re-serialize each cert to DER so BuildObject stores per-cert bytes, not the full bundle + unsigned char* der_buf = nullptr; + const int der_len = i2d_X509(cert.get(), &der_buf); + if (der_len <= 0 || der_buf == nullptr) + return score::crypto::make_unexpected(common::DaemonErrorCode::kCertificateParsingFailed); + auto obj = + BuildObject(cert.get(), der_buf, static_cast(der_len), score::crypto::FormatType::kDer); + OPENSSL_free(der_buf); + if (!obj) + return score::crypto::make_unexpected(obj.error()); + result.push_back(*obj); + } + } + else + { + // DER: loop advancing cursor per certificate until the buffer is consumed + const unsigned char* cursor = bytes; + const unsigned char* const end = bytes + size; + while (cursor < end) + { + const unsigned char* const start = cursor; + X509Ptr cert{d2i_X509(nullptr, &cursor, static_cast(end - cursor))}; + if (!cert) + return score::crypto::make_unexpected(common::DaemonErrorCode::kCertificateParsingFailed); + auto obj = BuildObject( + cert.get(), start, static_cast(cursor - start), score::crypto::FormatType::kDer); + if (!obj) + return score::crypto::make_unexpected(obj.error()); + result.push_back(*obj); + } + } + + if (result.empty()) + return score::crypto::make_unexpected(common::DaemonErrorCode::kCertificateParsingFailed); + return result; +} + +} // namespace score::crypto::daemon::provider::score_provider::openssl diff --git a/score/crypto/src/daemon/provider/score_provider/openssl/cert_management/openssl_cert_parser.hpp b/score/crypto/src/daemon/provider/score_provider/openssl/cert_management/openssl_cert_parser.hpp new file mode 100644 index 000000000..8957b06e1 --- /dev/null +++ b/score/crypto/src/daemon/provider/score_provider/openssl/cert_management/openssl_cert_parser.hpp @@ -0,0 +1,60 @@ +/******************************************************************************** + * 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 + ********************************************************************************/ +/******************************************************************************** + * Copyright (c) 2026 Contributors to the Eclipse Foundation + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ +#ifndef SCORE_CRYPTO_SRC_DAEMON_PROVIDER_SCORE_PROVIDER_OPENSSL_CERT_MANAGEMENT_OPENSSL_CERT_PARSER_HPP +#define SCORE_CRYPTO_SRC_DAEMON_PROVIDER_SCORE_PROVIDER_OPENSSL_CERT_MANAGEMENT_OPENSSL_CERT_PARSER_HPP + +#include "score/crypto/src/daemon/cert_management/interfaces/cert_object.hpp" +#include "score/crypto/src/daemon/common/types.hpp" +#include "score/crypto/src/daemon/provider/cert_management/i_cert_parser.hpp" + +namespace score::crypto::daemon::provider::score_provider::openssl +{ + +/// OpenSSL implementation of ICertParser. +/// +/// Parses DER/PEM certificate bytes into provider-neutral CertObjects using +/// OpenSSL's X509 stack. Stateless beyond the provider ID; created on demand +/// by OpenSSL::GetCertParser() for injection into FileBackedSlotHandler. +/// +/// Verification, CSR generation, public-key extraction, and format conversion +/// are certificate context-handler responsibilities, not parser concerns. +class OpenSslCertParser final : public cert_management::ICertParser +{ + public: + explicit OpenSslCertParser(common::ProviderId provider_id) noexcept : m_provider_id{provider_id} {} + ~OpenSslCertParser() override = default; + + OpenSslCertParser(const OpenSslCertParser&) = delete; + OpenSslCertParser& operator=(const OpenSslCertParser&) = delete; + OpenSslCertParser(OpenSslCertParser&&) = delete; + OpenSslCertParser& operator=(OpenSslCertParser&&) = delete; + + [[nodiscard]] score::crypto::Expected<::score::crypto::daemon::cert_management::CertObject::Sptr, + common::DaemonErrorCode> + ParseCertificate(const std::uint8_t* bytes, std::size_t size, score::crypto::FormatType format) override; + + [[nodiscard]] score::crypto::Expected, + common::DaemonErrorCode> + ParseCertificates(const std::uint8_t* bytes, std::size_t size, score::crypto::FormatType format) override; + + private: + common::ProviderId m_provider_id; +}; + +} // namespace score::crypto::daemon::provider::score_provider::openssl + +#endif // SCORE_CRYPTO_SRC_DAEMON_PROVIDER_SCORE_PROVIDER_OPENSSL_CERT_MANAGEMENT_OPENSSL_CERT_PARSER_HPP diff --git a/score/crypto/src/daemon/provider/score_provider/openssl/provider_openssl.cpp b/score/crypto/src/daemon/provider/score_provider/openssl/provider_openssl.cpp index 411a80357..e4ae6a06e 100644 --- a/score/crypto/src/daemon/provider/score_provider/openssl/provider_openssl.cpp +++ b/score/crypto/src/daemon/provider/score_provider/openssl/provider_openssl.cpp @@ -14,6 +14,7 @@ #include "score/crypto/src/daemon/provider/score_provider/openssl/provider_openssl.hpp" #include "score/crypto/src/daemon/data_plane/src/base_shm_factory.hpp" #include "score/crypto/src/daemon/key_management/slot/file_backed_slot_handler.hpp" +#include "score/crypto/src/daemon/provider/score_provider/openssl/cert_management/openssl_cert_parser.hpp" #include "score/crypto/src/daemon/provider/score_provider/openssl/key_management/openssl_key_factory.hpp" #include "score/crypto/src/daemon/provider/score_provider/openssl/operations/factory/openssl_handler_factory.hpp" #include "score/mw/log/logging.h" @@ -39,7 +40,6 @@ bool OpenSSL::InitialiseBackend(const ProviderInitContext& /*ctx*/) return false; } m_factory = std::make_shared<::score::crypto::daemon::provider::openssl::OpenSslKeyFactory>(GetProviderId()); - m_shm_factory = std::make_shared(); return true; @@ -55,6 +55,7 @@ void OpenSSL::Shutdown() m_factory.reset(); m_shm_factory.reset(); m_keyManagementService.reset(); + m_certManagementService.reset(); // Clean up OpenSSL resources OPENSSL_cleanup(); @@ -65,7 +66,14 @@ void OpenSSL::Shutdown() std::shared_ptr<::score::crypto::daemon::provider::handler::ICryptoHandlerFactory> OpenSSL::CreateHandlerFactory() { - return std::make_shared(m_factory, GetKeySlotHandler({}), m_keyManagementService); + return std::make_shared( + m_factory, GetKeySlotHandler({}), m_keyManagementService, GetCertParser(), m_certManagementService); +} + +void OpenSSL::SetCertManagementService( + std::shared_ptr<::score::crypto::daemon::cert_management::CertManagementService> service) +{ + m_certManagementService = std::move(service); } common::ProviderCapability OpenSSL::GetProviderCapabilities() @@ -79,6 +87,11 @@ std::shared_ptr OpenSSL::GetKeyFactory() return m_factory; } +std::shared_ptr OpenSSL::GetCertParser() +{ + return std::make_shared(GetProviderId()); +} + ::score::crypto::daemon::key_management::IKeySlotHandler::Sptr OpenSSL::GetKeySlotHandler( const ::score::crypto::daemon::key_management::KeySlotConfig& /*config*/) { diff --git a/score/crypto/src/daemon/provider/score_provider/openssl/provider_openssl.hpp b/score/crypto/src/daemon/provider/score_provider/openssl/provider_openssl.hpp index 9858edbe3..f849768f7 100644 --- a/score/crypto/src/daemon/provider/score_provider/openssl/provider_openssl.hpp +++ b/score/crypto/src/daemon/provider/score_provider/openssl/provider_openssl.hpp @@ -50,6 +50,11 @@ class OpenSSL final : public ::score::crypto::daemon::provider::score_provider:: const key_management::KeySlotConfig& config) override; void SetKeyManagementService(std::shared_ptr service) override; + // --- Certificate management capability --- + std::shared_ptr GetCertParser() override; + void SetCertManagementService( + std::shared_ptr<::score::crypto::daemon::cert_management::CertManagementService> service) override; + // --- SHM capability --- std::shared_ptr GetShmFactory() override; @@ -63,6 +68,7 @@ class OpenSSL final : public ::score::crypto::daemon::provider::score_provider:: std::shared_ptr m_factory; std::shared_ptr m_keyManagementService; + std::shared_ptr<::score::crypto::daemon::cert_management::CertManagementService> m_certManagementService; std::shared_ptr m_shm_factory; }; diff --git a/score/crypto/src/daemon/provider/tests/cert_management/BUILD b/score/crypto/src/daemon/provider/tests/cert_management/BUILD new file mode 100644 index 000000000..d03322485 --- /dev/null +++ b/score/crypto/src/daemon/provider/tests/cert_management/BUILD @@ -0,0 +1,24 @@ +# ******************************************************************************* +# Copyright (c) 2026 Contributors to the Eclipse Foundation +# SPDX-License-Identifier: Apache-2.0 +# ******************************************************************************* + +load("@rules_cc//cc:cc_test.bzl", "cc_test") + +cc_test( + name = "test_openssl_cert_parser", + srcs = ["test_openssl_cert_parser.cpp"], + data = [ + "//score/tests/test_vectors/certificate:certificate_test_vectors", + ], + target_compatible_with = select({ + "//score/crypto/src/backend:openssl_backend_active": [], + "//conditions:default": ["@platforms//:incompatible"], + }), + deps = [ + "//score/crypto/src/daemon/cert_management:cert_management_headers", + "//score/crypto/src/daemon/provider/score_provider/openssl:openssl_cert_management_headers", + "//score/crypto/src/daemon/provider/score_provider/openssl:openssl_cert_parser_library", + "@googletest//:gtest_main", + ], +) diff --git a/score/crypto/src/daemon/provider/tests/cert_management/test_openssl_cert_parser.cpp b/score/crypto/src/daemon/provider/tests/cert_management/test_openssl_cert_parser.cpp new file mode 100644 index 000000000..861bd7a6b --- /dev/null +++ b/score/crypto/src/daemon/provider/tests/cert_management/test_openssl_cert_parser.cpp @@ -0,0 +1,71 @@ +#include "score/crypto/src/daemon/provider/score_provider/openssl/cert_management/openssl_cert_parser.hpp" + +#include + +#include +#include +#include +#include +#include +#include + +namespace +{ +using score::crypto::FormatType; +using score::crypto::daemon::common::ProviderId; +using score::crypto::daemon::provider::score_provider::openssl::OpenSslCertParser; + +std::string ReadCertificateVector() +{ + std::ifstream input{"score/tests/test_vectors/certificate/certificate.pem"}; + return {std::istreambuf_iterator{input}, std::istreambuf_iterator{}}; +} + +TEST(OpenSslCertParserTest, ParsesPemCertificateAndExtractsMetadata) +{ + const auto pem = ReadCertificateVector(); + ASSERT_FALSE(pem.empty()); + OpenSslCertParser parser{ProviderId{1U}}; + const auto result = + parser.ParseCertificate(reinterpret_cast(pem.data()), pem.size(), FormatType::kPem); + + ASSERT_TRUE(result.has_value()); + ASSERT_NE(*result, nullptr); + EXPECT_EQ((*result)->GetFormat(), FormatType::kPem); + EXPECT_EQ((*result)->GetSubject(), "CN=cert-management-test,O=Eclipse"); + EXPECT_EQ((*result)->GetIssuer(), "CN=cert-management-test,O=Eclipse"); + EXPECT_TRUE((*result)->IsCA()); + EXPECT_EQ((*result)->GetSkid().size(), 20U); + EXPECT_EQ((*result)->GetFingerprint().size(), 32U); + EXPECT_EQ((*result)->GetRawBytes().size(), pem.size()); +} + +TEST(OpenSslCertParserTest, ParsesPemBundleIntoSeparateCertificateObjects) +{ + const auto pem = ReadCertificateVector(); + ASSERT_FALSE(pem.empty()); + const auto bundle = pem + pem; + OpenSslCertParser parser{ProviderId{1U}}; + const auto result = + parser.ParseCertificates(reinterpret_cast(bundle.data()), bundle.size(), FormatType::kPem); + + ASSERT_TRUE(result.has_value()); + ASSERT_EQ(result->size(), 2U); + EXPECT_EQ((*result)[0]->GetFormat(), FormatType::kDer); + EXPECT_EQ((*result)[1]->GetFormat(), FormatType::kDer); + EXPECT_TRUE(std::equal((*result)[0]->GetFingerprint().begin(), + (*result)[0]->GetFingerprint().end(), + (*result)[1]->GetFingerprint().begin(), + (*result)[1]->GetFingerprint().end())); +} + +TEST(OpenSslCertParserTest, RejectsMalformedCertificate) +{ + constexpr std::string malformed{"not a certificate"}; + OpenSslCertParser parser{ProviderId{1U}}; + const auto result = parser.ParseCertificate( + reinterpret_cast(malformed.data()), malformed.size(), FormatType::kPem); + + EXPECT_FALSE(result.has_value()); +} +} // namespace diff --git a/score/tests/test_vectors/certificate/BUILD b/score/tests/test_vectors/certificate/BUILD new file mode 100644 index 000000000..87ac6f2c5 --- /dev/null +++ b/score/tests/test_vectors/certificate/BUILD @@ -0,0 +1,18 @@ +# ******************************************************************************* +# 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 2.0 which is available at +# https://www.apache.org/licenses/LICENSE-2.0 +# +# SPDX-License-Identifier: Apache-2.0 +# ******************************************************************************* + +filegroup( + name = "certificate_test_vectors", + srcs = glob(["*.json", "*.kv", "*.pem"]), + visibility = ["//visibility:public"], +) diff --git a/score/tests/test_vectors/certificate/README.md b/score/tests/test_vectors/certificate/README.md new file mode 100644 index 000000000..8661a815d --- /dev/null +++ b/score/tests/test_vectors/certificate/README.md @@ -0,0 +1,23 @@ +# Certificate Test Vectors + +`certificate_manifest.json` is the inventory for certificate-management test certificates. It records each certificate's purpose, subject, private-key location, validity policy, and the metadata snapshot of the checked-in public certificate. + +Private keys are generated locally under `private/` and are ignored by Git. Never commit them. The checked-in certificates use a long validity period because their private keys are intentionally not stored in the public repository. + +From the repository root, generate a certificate and its local key with: + +```sh +python3 score/tests/test_vectors/certificate/generate_certificates.py generate certificate +``` + +When a public snapshot needs renewal, run `generate` again. This creates a new local private key and a new self-signed public certificate, then refreshes the manifest snapshot. Review and commit the public PEM and manifest together. + +Only use `update` when the corresponding local private key is available and preserving the existing key is intentional: + +```sh +python3 score/tests/test_vectors/certificate/generate_certificates.py update certificate +``` + +Use `certificate_updated` for the replacement snapshot used by the anchor-invalidation test. To add another certificate, add an entry to `certificate_manifest.json`, then run the `generate` command with its `name`. The script updates the certificate's `snapshot` from OpenSSL after generation. Do not make tests depend on a private key being present. + +The generated public certificate and manifest changes should be reviewed together. The KV descriptor templates in this directory describe the storage layouts used by the certificate-management integration tests. diff --git a/score/tests/test_vectors/certificate/certificate.pem b/score/tests/test_vectors/certificate/certificate.pem new file mode 100644 index 000000000..3626b3a0a --- /dev/null +++ b/score/tests/test_vectors/certificate/certificate.pem @@ -0,0 +1,19 @@ +-----BEGIN CERTIFICATE----- +MIIDIjCCAgqgAwIBAgIUU4j+3FNq76/IVhNmGXR+y2LmFD4wDQYJKoZIhvcNAQEL +BQAwMTEdMBsGA1UEAwwUY2VydC1tYW5hZ2VtZW50LXRlc3QxEDAOBgNVBAoMB0Vj +bGlwc2UwHhcNMjYwODI0MjIyMDE4WhcNMzYwODIxMjIyMDE4WjAxMR0wGwYDVQQD +DBRjZXJ0LW1hbmFnZW1lbnQtdGVzdDEQMA4GA1UECgwHRWNsaXBzZTCCASIwDQYJ +KoZIhvcNAQEBBQADggEPADCCAQoCggEBALyvZCutqNd91haJUfTXV1ZxLphNwHxI +ODkemUGvB+l11Tuu8UuvGFwXB99C39uKqcD7hvY/iBj4d1F/n7fACQOI+GobZ0uR +iL32bW5mQ7d9iAz06ujDJtNegaw5P9+nVVnh2LOQIIAAGImFFnqvF0/HEiOYM9Wp +kHb4WTJrU46IpCOSll2vaOjLpbwsnHTmJLByXkAJUqQE3PYoC1AoV+xVwjlYsTSF +Xu8C0Fiv5K8tFsY/zzFgzoHbkUoAzk6HCGIviy445aH3mbNk+27RXyUKxWpuoFUs +Kcnvh9H5dKjKmR8uqmyWQnBQFhU2hinujzefy3U3DdStspqzjVtJZt8CAwEAAaMy +MDAwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUkR2v/xGNZX8mjEvTlsaxGmOK +NXgwDQYJKoZIhvcNAQELBQADggEBAHfJ5i6F+6LhQzJl3j4EA/n0AaPz/QywqcwH +M/MdPqrY280CqGRdkmkPE/JabOEle6EkBnHhbTTdCPAhZ21NVC34jARUpg4qhQ2s +TrdVxw3cibLnlrDvOjM5v13D2+K8X4FWX/4TLN9Xc5+U9bVxOK9QYj5vcG8W0TCF +S2lYg9ye65glZplOXmEQrzFxngYt9iBMG23CLVW8+HuL6R8I9602adVxKZzKEXyh +kKD3aMTAtdCgcs5AXvks9yLKI5bk9sLrnaqkXtbW7UGy1JFk4C52bLns9qZs2VFf +dxdkRzMyp1xFv8MnPvNgjNGbkaX0ztHw9mp3tOCT3ot1Ki6kYgY= +-----END CERTIFICATE----- diff --git a/score/tests/test_vectors/certificate/certificate_manifest.json b/score/tests/test_vectors/certificate/certificate_manifest.json new file mode 100644 index 000000000..249afb964 --- /dev/null +++ b/score/tests/test_vectors/certificate/certificate_manifest.json @@ -0,0 +1,34 @@ +{ + "schema_version": 1, + "description": "Certificate-management test certificate inventory. Public certificates are checked in; private keys are generated locally and are not committed.", + "certificates": [ + { + "name": "certificate", + "certificate_path": "certificate.pem", + "private_key_path": "private/certificate.key.pem", + "purpose": "Initial CA certificate for file-backed slot and trust-store tests", + "subject": "/CN=cert-management-test/O=Eclipse", + "key_algorithm": "RSA-2048", + "validity_days": 3650, + "snapshot": { + "not_before": "2026-08-24T22:20:18Z", + "not_after": "2036-08-21T22:20:18Z", + "sha256_fingerprint": "3E7A581BCAAB80F73535B10968F8C3E68517194B6A404CE5F58AACABE0A62D0C" + } + }, + { + "name": "certificate_updated", + "certificate_path": "certificate_updated.pem", + "private_key_path": "private/certificate_updated.key.pem", + "purpose": "Replacement CA certificate used to test slot updates and trust-store anchor invalidation", + "subject": "/CN=cert-management-updated/O=Eclipse", + "key_algorithm": "RSA-2048", + "validity_days": 3650, + "snapshot": { + "not_before": "2026-08-24T22:20:18Z", + "not_after": "2036-08-21T22:20:18Z", + "sha256_fingerprint": "2324A755EB36B44DF2495504AF85A8ED3FE89A8328F01788905DB3A0E1EAED7E" + } + } + ] +} diff --git a/score/tests/test_vectors/certificate/certificate_slot.kv b/score/tests/test_vectors/certificate/certificate_slot.kv new file mode 100644 index 000000000..5c3a0d5e9 --- /dev/null +++ b/score/tests/test_vectors/certificate/certificate_slot.kv @@ -0,0 +1,3 @@ +[certificate] +cert_path = score/tests/test_vectors/certificate/certificate.pem +cert_format = pem diff --git a/score/tests/test_vectors/certificate/certificate_updated.pem b/score/tests/test_vectors/certificate/certificate_updated.pem new file mode 100644 index 000000000..8d6664e9f --- /dev/null +++ b/score/tests/test_vectors/certificate/certificate_updated.pem @@ -0,0 +1,19 @@ +-----BEGIN CERTIFICATE----- +MIIDKDCCAhCgAwIBAgIUE/0SRGfs5UWVCnkbS4XjR7XwLCwwDQYJKoZIhvcNAQEL +BQAwNDEgMB4GA1UEAwwXY2VydC1tYW5hZ2VtZW50LXVwZGF0ZWQxEDAOBgNVBAoM +B0VjbGlwc2UwHhcNMjYwODI0MjIyMDE4WhcNMzYwODIxMjIyMDE4WjA0MSAwHgYD +VQQDDBdjZXJ0LW1hbmFnZW1lbnQtdXBkYXRlZDEQMA4GA1UECgwHRWNsaXBzZTCC +ASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALz8Hx8tRcY7oGJTabS13gKQ +7G3eb71sZHBa2YD97+D9kA5ly7mjdI+dBO6F31J3AS8KhbfhHLU31INFYRnMwOeT +JU3MsIi+SGRsCIejVWk/tjtS+MCfz2/A2t9xGP530iLNGxuRBkbTnRtN04H2n0jT +Dv8P8Jliy3MYm7C5DvwrD13K0y91EVqO0O9C59LKfgKzOzxxWwwNdC4gC9PjjTsK +5Mvu6jbmO2Apu7GugZZM5sghYo3DTIguCT6/F2eKGlnBbygnJK8LvlACOyTM495A +/vJyBQIFEZWJAv5XaJ6BgckDX26KqMeO+ddhvns5r9sqx0krNdljc7cqpvXx8EEC +AwEAAaMyMDAwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUCoVu+hLgvod4P+tM +WOFpICMdoHswDQYJKoZIhvcNAQELBQADggEBADg9ob/4kcclUgPWBqmLvmjN4A3n +YuTNYxBdhEuv4NdXu77fKg1/yCr9zkvVtnvfBQe4vYTMXa6bJVoBT4WuevbD86wn +qiCLo6dV4yjtVhOHKdNioeWDUT+cgAc5pMZ3cE28VLUWzgFUdHN/Oy1KX5bkv6w5 +BtKTLNc20jBQgg0MO+dlZVFX+4/QAIhHsm0yN2Wj9S7IszKuY+DYsJ/BiBIiW0Zn +uvCOC/hNMDTODdyRIFze7aNhaa/nGaxGZkzRCUzimQuhhFEtBTEoGzmM0tWYzHAz +7kAK9lQeBFfr3uYgx9nZapuP4aBF5p5rhM8vGSiL/HEO4Zx+ZGeyYtBIpfQ= +-----END CERTIFICATE----- diff --git a/score/tests/test_vectors/certificate/certificate_updated_slot.kv b/score/tests/test_vectors/certificate/certificate_updated_slot.kv new file mode 100644 index 000000000..9366ea2dc --- /dev/null +++ b/score/tests/test_vectors/certificate/certificate_updated_slot.kv @@ -0,0 +1,3 @@ +[certificate] +cert_path = score/tests/test_vectors/certificate/certificate_updated.pem +cert_format = pem diff --git a/score/tests/test_vectors/certificate/generate_certificates.py b/score/tests/test_vectors/certificate/generate_certificates.py new file mode 100644 index 000000000..8d70d4834 --- /dev/null +++ b/score/tests/test_vectors/certificate/generate_certificates.py @@ -0,0 +1,159 @@ +#!/usr/bin/env python3 +"""Generate or refresh certificate-management test certificates from a manifest.""" + +import argparse +import datetime as datetime_module +import json +import pathlib +import subprocess + + +def run_openssl(openssl, arguments): + return subprocess.run([openssl, *arguments], check=True, capture_output=True, text=True) + + +def parse_snapshot(openssl, certificate_path): + result = run_openssl( + openssl, + [ + "x509", + "-in", + str(certificate_path), + "-noout", + "-subject", + "-startdate", + "-enddate", + "-fingerprint", + "-sha256", + ], + ) + values = {} + for line in result.stdout.splitlines(): + key, separator, value = line.partition("=") + if separator: + values[key] = value.strip() + + def parse_date(value): + parsed = datetime_module.datetime.strptime(value, "%b %d %H:%M:%S %Y GMT") + return parsed.replace(tzinfo=datetime_module.timezone.utc).isoformat().replace("+00:00", "Z") + + fingerprint = values["sha256 Fingerprint"].replace(":", "").upper() + return { + "not_before": parse_date(values["notBefore"]), + "not_after": parse_date(values["notAfter"]), + "sha256_fingerprint": fingerprint, + } + + +def load_manifest(path): + with path.open(encoding="utf-8") as manifest_file: + return json.load(manifest_file) + + +def save_manifest(path, manifest): + with path.open("w", encoding="utf-8") as manifest_file: + json.dump(manifest, manifest_file, indent=2) + manifest_file.write("\n") + + +def find_certificate(manifest, name): + for certificate in manifest["certificates"]: + if certificate["name"] == name: + return certificate + names = ", ".join(entry["name"] for entry in manifest["certificates"]) + raise SystemExit(f"Unknown certificate '{name}'. Available names: {names}") + + +def write_slot_kv(root, certificate): + """Write a KV slot descriptor alongside the generated certificate. + + The cert_path inside the descriptor uses a workspace-relative path so that + tests can reference the descriptor directly from the Bazel runfiles root + without copying or modifying it at runtime. + """ + cert_rel = pathlib.Path(certificate["certificate_path"]) + kv_name = cert_rel.stem + "_slot.kv" + kv_path = root / kv_name + + try: + workspace_cert = (root / cert_rel).relative_to(pathlib.Path.cwd()) + cert_path_str = workspace_cert.as_posix() + except ValueError: + # Fallback when the script is not invoked from the workspace root. + cert_path_str = str(root / cert_rel) + + kv_path.write_text( + f"[certificate]\ncert_path = {cert_path_str}\ncert_format = pem\n", + encoding="utf-8", + ) + return kv_path + + +def generate_certificate(openssl, root, certificate, update): + certificate_path = root / certificate["certificate_path"] + key_path = root / certificate["private_key_path"] + certificate_path.parent.mkdir(parents=True, exist_ok=True) + key_path.parent.mkdir(parents=True, exist_ok=True) + + if update and not key_path.exists(): + raise SystemExit(f"Cannot update '{certificate['name']}': private key is missing at {key_path}") + + subject = certificate["subject"] + arguments = [ + "req", + "-x509", + "-new", + "-sha256", + "-nodes", + "-days", + str(certificate["validity_days"]), + "-subj", + subject, + "-out", + str(certificate_path), + "-addext", + "basicConstraints=critical,CA:TRUE", + "-addext", + "subjectKeyIdentifier=hash", + "-addext", + "authorityKeyIdentifier=keyid", + ] + if update: + arguments.extend(["-key", str(key_path)]) + else: + arguments.extend(["-newkey", "rsa:2048", "-keyout", str(key_path)]) + run_openssl(openssl, arguments) + certificate["snapshot"] = parse_snapshot(openssl, certificate_path) + kv_path = write_slot_kv(root, certificate) + return certificate_path, kv_path + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("action", choices=["generate", "update"]) + parser.add_argument("name", help="Manifest certificate name") + parser.add_argument( + "--manifest", + type=pathlib.Path, + default=pathlib.Path(__file__).with_name("certificate_manifest.json"), + ) + parser.add_argument("--openssl", default="openssl", help="OpenSSL executable") + args = parser.parse_args() + + manifest_path = args.manifest.resolve() + manifest = load_manifest(manifest_path) + certificate = find_certificate(manifest, args.name) + certificate_path, kv_path = generate_certificate( + args.openssl, + manifest_path.parent, + certificate, + update=args.action == "update", + ) + save_manifest(manifest_path, manifest) + print(f"Wrote {certificate_path}") + print(f"Wrote {kv_path}") + print(json.dumps(certificate["snapshot"], indent=2)) + + +if __name__ == "__main__": + main() diff --git a/score/tests/test_vectors/certificate/private/.gitignore b/score/tests/test_vectors/certificate/private/.gitignore new file mode 100644 index 000000000..d6b7ef32c --- /dev/null +++ b/score/tests/test_vectors/certificate/private/.gitignore @@ -0,0 +1,2 @@ +* +!.gitignore diff --git a/score/tests/test_vectors/certificate/trust_store.kv b/score/tests/test_vectors/certificate/trust_store.kv new file mode 100644 index 000000000..4ad069426 --- /dev/null +++ b/score/tests/test_vectors/certificate/trust_store.kv @@ -0,0 +1 @@ +[trust_store_state] From 6d21704271af92ca6dc03037a99d339323faa10f Mon Sep 17 00:00:00 2001 From: Athul Mallappallil Date: Tue, 25 Aug 2026 16:54:56 +0200 Subject: [PATCH 27/37] Cert management documentation --- score/crypto/docs/index.rst | 1 + .../architecture/cert_management_dynamic.puml | 55 +++++++ .../architecture/cert_management_static.puml | 70 +++++++++ .../architecture/component_architecture.rst | 140 ++++++++++++++++++ .../docs/architecture/index.rst | 14 ++ .../docs/detailed_design/detailed_design.rst | 87 +++++++++++ .../docs/detailed_design/index.rst | 14 ++ .../src/daemon/cert_management/docs/index.rst | 129 ++++++++++++++++ .../docs/requirements/index.rst | 14 ++ .../docs/requirements/requirements.rst | 103 +++++++++++++ 10 files changed, 627 insertions(+) create mode 100644 score/crypto/src/daemon/cert_management/docs/architecture/cert_management_dynamic.puml create mode 100644 score/crypto/src/daemon/cert_management/docs/architecture/cert_management_static.puml create mode 100644 score/crypto/src/daemon/cert_management/docs/architecture/component_architecture.rst create mode 100644 score/crypto/src/daemon/cert_management/docs/architecture/index.rst create mode 100644 score/crypto/src/daemon/cert_management/docs/detailed_design/detailed_design.rst create mode 100644 score/crypto/src/daemon/cert_management/docs/detailed_design/index.rst create mode 100644 score/crypto/src/daemon/cert_management/docs/index.rst create mode 100644 score/crypto/src/daemon/cert_management/docs/requirements/index.rst create mode 100644 score/crypto/src/daemon/cert_management/docs/requirements/requirements.rst diff --git a/score/crypto/docs/index.rst b/score/crypto/docs/index.rst index b120b447c..e9e051c1c 100644 --- a/score/crypto/docs/index.rst +++ b/score/crypto/docs/index.rst @@ -165,6 +165,7 @@ Additional documentation for relevant Crypto subcomponents can be found here: :maxdepth: 1 ../src/daemon/data_manager/docs/index + ../src/daemon/cert_management/docs/index Component Detail Information ============================ diff --git a/score/crypto/src/daemon/cert_management/docs/architecture/cert_management_dynamic.puml b/score/crypto/src/daemon/cert_management/docs/architecture/cert_management_dynamic.puml new file mode 100644 index 000000000..5b7e15986 --- /dev/null +++ b/score/crypto/src/daemon/cert_management/docs/architecture/cert_management_dynamic.puml @@ -0,0 +1,55 @@ +@startuml +participant "Flat-config parser" as Config +participant "ConfigDrivenSlotCatalog" as SlotCatalog +participant "CertSlotRegistry" as Slots +participant "ConfigDrivenTrustStoreCatalog" as TrustCatalog +participant "TrustStoreManager" as Stores +participant "CertManagementService" as Service +participant "FileBackedSlotHandler" as Handler +participant "OpenSslCertParser" as Parser +participant "TrustStoreHandler" as StoreHandler +participant "DataManager" as Data + +Config -> SlotCatalog : CertificateConfig +SlotCatalog -> Slots : RegisterSlot(config) +Config -> TrustCatalog : CertificateConfig +TrustCatalog -> Stores : Load(store configs, registry, factory) +TrustCatalog -> Stores : RegisterAppResource() + +Service -> Slots : ResolveAppResource(resource, client) +Slots --> Service : CertSlotHandle +Service -> Data : addNode(CertSlotDataNode) +Data --> Service : slot_node_id + +Service -> Handler : LoadCertificate(slot config) +Handler -> Parser : ParseCertificate(bytes, format) +Parser --> Handler : CertObject +Handler --> Service : CertObject +Service -> Data : addChildNode(CertDataNode) + +Service -> Stores : ResolveAppResource(trust resource, client) +Stores --> Service : TrustStoreHandle +Service -> Stores : GetStore(handle) +Stores --> Service : TrustStoreHandler +Service -> StoreHandler : GetAnchors() +StoreHandler -> Stores : lazy AnchorLoader +Stores -> Handler : LoadCertificate(slot config) +Handler -> Parser : ParseCertificate(bytes, format) +Parser --> Handler : CertObject +Handler --> Stores : CertObject +Stores -> StoreHandler : NotifySlotUpdate(slot, cert) +StoreHandler --> Service : anchors + +Service -> Handler : StoreCertificate(updated cert) +Handler -> "Shared Deployment Storage" : atomic cert + metadata write +Service -> Stores : NotifySlotChanged(store, slot) +Stores -> StoreHandler : InvalidateSlot(slot) +Service -> StoreHandler : GetAnchors() +StoreHandler -> Stores : reload changed slot +Stores -> Handler : LoadCertificate() +Handler -> Parser : ParseCertificate() +Parser --> Handler : updated CertObject +Handler --> Stores : updated CertObject +Stores -> StoreHandler : NotifySlotUpdate() +StoreHandler --> Service : updated anchors +@enduml diff --git a/score/crypto/src/daemon/cert_management/docs/architecture/cert_management_static.puml b/score/crypto/src/daemon/cert_management/docs/architecture/cert_management_static.puml new file mode 100644 index 000000000..4ff005f18 --- /dev/null +++ b/score/crypto/src/daemon/cert_management/docs/architecture/cert_management_static.puml @@ -0,0 +1,70 @@ +@startuml +skinparam componentStyle rectangle +skinparam packageStyle rectangle + +package "cert_management" { + package "core" { + [CertManagementService] + [CertRegistry] + } + package "interfaces" { + interface ICertSlotHandler + interface ITrustStoreHandler + interface ICertParser + [CertObject] + } + package "nodes" { + [CertSlotDataNode] + [CertDataNode] + [TrustStoreDataNode] + } + package "slot" { + [CertSlotRegistry] + [ConfigDrivenSlotCatalog] + [FileBackedSlotHandler] + [CrlHandler] + } + package "truststore" { + [TrustStoreManager] + [TrustStoreHandler] + [ConfigDrivenTrustStoreCatalog] + } + package "policy" { + [AccessPolicyEnforcer] + } +} + +package "daemon/common" { + [DataManager] + [Shared Deployment Storage] +} + +package "provider" { + [OpenSslCertParser] + [Provider Context Handlers] +} + +[CertManagementService] --> [CertRegistry] +[CertManagementService] --> [CertSlotRegistry] +[CertManagementService] --> [TrustStoreManager] +[CertManagementService] --> [DataManager] +[CertManagementService] --> [AccessPolicyEnforcer] +[CertManagementService] ..> [CertSlotDataNode] : creates +[CertManagementService] ..> [CertDataNode] : creates +[CertManagementService] ..> [TrustStoreDataNode] : creates + +[ConfigDrivenSlotCatalog] --> [CertSlotRegistry] +[ConfigDrivenTrustStoreCatalog] --> [TrustStoreManager] +[TrustStoreManager] --> [CertSlotRegistry] +[TrustStoreManager] --> [TrustStoreHandler] +[TrustStoreManager] --> ICertSlotHandler +[TrustStoreHandler] ..|> ITrustStoreHandler +[FileBackedSlotHandler] ..|> ICertSlotHandler +[FileBackedSlotHandler] --> [CrlHandler] +[FileBackedSlotHandler] --> ICertParser +[FileBackedSlotHandler] --> [Shared Deployment Storage] +[OpenSslCertParser] ..|> ICertParser +[OpenSslCertParser] --> [CertObject] +[Provider Context Handlers] --> [CertObject] + +@enduml diff --git a/score/crypto/src/daemon/cert_management/docs/architecture/component_architecture.rst b/score/crypto/src/daemon/cert_management/docs/architecture/component_architecture.rst new file mode 100644 index 000000000..47719b71b --- /dev/null +++ b/score/crypto/src/daemon/cert_management/docs/architecture/component_architecture.rst @@ -0,0 +1,140 @@ +.. + # ******************************************************************************* + # Copyright (c) 2026 Contributors to the Eclipse Foundation + # SPDX-License-Identifier: Apache-2.0 + # ******************************************************************************* + +Certificate Management Component Architecture +============================================= + +.. document:: Certificate Management Component Architecture + :id: doc__crypto_cert_management_architecture + :version: 1 + :status: draft + :safety: QM + :security: YES + :realizes: wp__cmpt_request_dummy + :tags: cert_management, architecture + +.. comp:: Certificate Management Component + :id: comp__crypto_cert_management + :version: 1 + :security: YES + :safety: QM + :status: valid + :belongs_to: feat__mtef + +Purpose +------- + +``cert_management`` is a daemon subcomponent parallel to +``key_management``. It owns certificate and trust-store resource lifecycle; +it does not own private keys or provider-specific cryptographic objects. + +Static decomposition +-------------------- + +The implementation is divided by responsibility: + +* ``interfaces/`` contains provider-neutral certificate values, slot and trust + store contracts, configuration types, and public handles. +* ``core/`` contains ``CertManagementService``, ``CertRegistry``, and + ``CertEntry``. +* ``nodes/`` contains DataManager nodes for certificate slots, loaded + certificates, and trust stores. +* ``slot/`` contains slot registration, file-backed storage, deployment + dispatch, and co-located CRL storage. +* ``truststore/`` contains trust-store membership, anchor caching, persistence, + and per-client references. +* ``policy/`` contains the shared slot/trust-store access-policy checks. +* ``provider/`` supplies parsing and provider-specific context handlers. The + selected certificate-management provider must expose ``ICertParser``; + certificate management does not require a particular provider. + +.. uml:: cert_management_static.puml + +Runtime boundaries +------------------ + +Resource resolution is client-scoped through the Data Manager. A resolved +certificate slot or trust store is represented by a lightweight DataNode. A +certificate loaded from a slot becomes a ``CertDataNode`` backed by a shared +``CertEntry``. Trust-store anchor content is loaded lazily and cached by +``TrustStoreManager``. + +The current provider boundary is intentionally narrow: + +* ``ICertParser`` converts DER/PEM bytes into ``CertObject``. +* ``ICertSlotHandler`` loads and stores slot data. +* Provider context handlers perform verification, CSR generation, conversion, + and public-key operations. +* Cross-context services may provide signing and public-key operations for + non-exportable keys while preserving provider ownership of private keys. + +Architecture constraints +------------------------ + +* Certificate slots reference one storage backend selected at startup. +* Trust stores reference certificate slots, never raw certificate paths. +* CRLs are slot-scoped and are not independently resolved. +* All trust-store mutation paths require trust-store write authorization. +* Shared deployment writes are atomic; a partially written descriptor must not + replace the previous valid descriptor. + +Key interfaces +-------------- + +``ICertSlotHandler`` is implemented by certificate storage backends such as +``FileBackedSlotHandler`` and ``Pkcs11CertSlotHandler``. The handler factory is +injected into ``TrustStoreManager`` so the core component does not depend on a +concrete provider backend. + +``ITrustStoreHandler`` exposes anchor retrieval and chain-building lookups. +``TrustStoreHandler`` receives an anchor-loader callback from +``TrustStoreManager`` and loads its anchor content on demand. + +``ICertParser`` is the narrow provider boundary for converting DER or PEM +bytes into a provider-neutral ``CertObject``. Verification, CSR generation, +format conversion, and public-key extraction are provider context operations, +not responsibilities of the core storage component. + +Runtime flows +------------- + +During startup, the configuration adapter registers certificate slots and +trust stores. A client resolves an application resource into a DataManager +node. Loading a certificate resolves its slot handler, reads the deployment +descriptor and payload, parses the bytes, and registers a shared +``CertEntry``. Repeated loads of the same slot share the registry entry. + +Trust-store anchors are loaded lazily. ``TrustStoreManager`` resolves each +typed member slot and caches the resulting ``CertObject`` through a weak +reference. The trust-store handler holds strong references while active. + +After a certificate update, ``CertManagementService`` finds every trust store +that references the slot and calls ``NotifySlotChanged``. The affected cache +entry is invalidated, and the next anchor request reloads and reparses the +certificate. Per-client references prevent one client's cleanup from evicting +another client's active cache. + +Design decisions +---------------- + +* Trust stores reference certificate slots, not raw certificate paths. +* CRLs are stored in the certificate slot's optional ``[crl]`` section; there + is no independent CRL resource or registry. +* Access-policy checks are shared by slots and trust stores. Mutation is + default-deny and requires an explicit writer UID. +* File-backed certificate and descriptor writes use the shared atomic storage + utilities. +* Provider-specific objects and private keys never cross the certificate + management boundary. + +Current limitations +-------------------- + +The current implementation does not provide CRL validation during +verification, OCSP, or hardware-key CSR signing. Hardware CSR signing +requires a cross-context service using ``Sign`` and ``GetPublicKeyDer`` +without exporting private key material. Provider and daemon dispatch +integration is outside this component's storage and lifecycle boundary. diff --git a/score/crypto/src/daemon/cert_management/docs/architecture/index.rst b/score/crypto/src/daemon/cert_management/docs/architecture/index.rst new file mode 100644 index 000000000..58ff6e56d --- /dev/null +++ b/score/crypto/src/daemon/cert_management/docs/architecture/index.rst @@ -0,0 +1,14 @@ +.. + # ******************************************************************************* + # Copyright (c) 2026 Contributors to the Eclipse Foundation + # SPDX-License-Identifier: Apache-2.0 + # ******************************************************************************* + +.. _crypto_cert_management_architecture: + +Architecture +============ + +.. toctree:: + + component_architecture diff --git a/score/crypto/src/daemon/cert_management/docs/detailed_design/detailed_design.rst b/score/crypto/src/daemon/cert_management/docs/detailed_design/detailed_design.rst new file mode 100644 index 000000000..f47427b1f --- /dev/null +++ b/score/crypto/src/daemon/cert_management/docs/detailed_design/detailed_design.rst @@ -0,0 +1,87 @@ +.. + # ******************************************************************************* + # Copyright (c) 2026 Contributors to the Eclipse Foundation + # SPDX-License-Identifier: Apache-2.0 + # ******************************************************************************* + +Certificate Management Detailed Design +====================================== + +.. document:: Certificate Management Detailed Design + :id: doc__crypto_cert_management_detailed_design + :version: 1 + :status: draft + :safety: QM + :security: YES + :realizes: wp__cmpt_request_dummy + :tags: cert_management, detailed_design + +Implementation units +-------------------- + +``CertManagementService`` + Coordinates resource resolution, DataManager nodes, certificate registry + access, slot operations, and trust-store update notifications. + +``CertRegistry`` and ``CertEntry`` + Own live certificate entries and share immutable ``CertObject`` values + between clients and trust stores. + +``CertSlotRegistry`` + Stores immutable slot configuration and application resource mappings. + +``FileBackedSlotHandler`` and ``CrlHandler`` + Read and write certificate/CRL data using the deployment descriptor and + shared atomic file I/O. The slot handler delegates parsing to ``ICertParser``. + +``TrustStoreManager`` and ``TrustStoreHandler`` + Resolve typed slot memberships, maintain reverse indices, load anchors + lazily, persist mutable member state, and manage per-client references. + +``AccessPolicyEnforcer`` + Applies UID-based read/write policy. Mutation is default-deny when no writer + UID is explicitly configured. + +Data and lifetime model +----------------------- + +* ``CertSlotDataNode`` is a client-scoped reference to a configured slot. +* ``CertDataNode`` is a client-scoped reference to a registry-owned + ``CertEntry``. +* ``TrustStoreDataNode`` is a client-scoped reference to a manager-owned trust + store. +* ``CertObject`` is immutable and provider-neutral. +* Trust-store anchor contents are loaded on demand. A weak cache avoids + duplicate certificate objects across active stores. +* Per-client trust-store references prevent one client from evicting another + client's active anchor cache. + +Storage contract +---------------- + +A certificate slot uses a KV deployment descriptor with a ``[certificate]`` +section and optional ``[certificate_metadata]`` and ``[crl]`` sections. The +certificate and CRL payloads are stored in files referenced by the descriptor. +Descriptor and payload writes use the shared storage utilities; the previous +descriptor remains available until the replacement is complete. + +Trust-store update contract +--------------------------- + +After a successful slot certificate update, the service obtains the reverse +membership list and calls ``TrustStoreManager::NotifySlotChanged``. The manager +invalidates the affected ``TrustStoreHandler`` cache. The next ``GetAnchors`` +operation reloads the slot and reconstructs its ``CertObject`` through the +injected parser. + +Provider boundary and scope +---------------------------- + +The core component does not depend on OpenSSL or PKCS#11 concrete types. +OpenSSL currently supplies parsing and verification implementations; PKCS#11 +supplies a read-only certificate-slot backend. Hardware-key CSR generation, +CRL validation, OCSP, and mediator dispatch are outside this component's +storage and lifecycle scope. Hardware CSR signing must use a cross-context +service without exporting private key material. + +.. uml:: ../architecture/cert_management_dynamic.puml diff --git a/score/crypto/src/daemon/cert_management/docs/detailed_design/index.rst b/score/crypto/src/daemon/cert_management/docs/detailed_design/index.rst new file mode 100644 index 000000000..121c9f648 --- /dev/null +++ b/score/crypto/src/daemon/cert_management/docs/detailed_design/index.rst @@ -0,0 +1,14 @@ +.. + # ******************************************************************************* + # Copyright (c) 2026 Contributors to the Eclipse Foundation + # SPDX-License-Identifier: Apache-2.0 + # ******************************************************************************* + +.. _crypto_cert_management_detailed_design: + +Detailed Design +=============== + +.. toctree:: + + detailed_design diff --git a/score/crypto/src/daemon/cert_management/docs/index.rst b/score/crypto/src/daemon/cert_management/docs/index.rst new file mode 100644 index 000000000..b25d6a0f4 --- /dev/null +++ b/score/crypto/src/daemon/cert_management/docs/index.rst @@ -0,0 +1,129 @@ +Certificate Management Component +================================ + +.. document:: Certificate Management Component + :id: doc__crypto_cert_mgmt + :version: 1 + :status: draft + :safety: QM + :security: YES + :tags: cert_management, crypto_daemon, certificate, trust_store + :realizes: wp__cmpt_request_dummy + +.. toctree:: + :hidden: + + requirements/index + architecture/index + detailed_design/index + +Abstract +-------- + +The ``cert_management`` daemon subcomponent provides lifecycle management +for X.509 certificates, CRLs, and trust stores inside the crypto daemon. +It sits parallel to ``key_management`` and implements the IPC back-end +for the certificate-management, certificate-verification, and CSR-generation +API contexts. + +The component manages certificate slots (named, persistent locations for a +single certificate and its optional co-located CRL), trust stores (named +collections of trust anchors backed by typed slot references), and a +runtime certificate registry (live in-memory handles returned to clients). +Certificate bytes are treated as provider-neutral immutable values +(``CertObject``). All parsing, verification, CSR generation, and +hardware-bound operations are delegated to provider context handlers; +the core component owns only lifecycle, access control, and storage. + +The default storage backend is file-system based (``FileBackedSlotHandler``). +The provider selected for the certificate-management capability supplies the +parser used by that backend. Other providers may supply their own +``ICertSlotHandler`` implementation for provider-owned storage. + +Rationale +--------- + +Certificate material is public information — unlike private keys, raw +certificate bytes may be shared between clients and cached freely. +This drives several design decisions: + +* **Software-first provider default** — unlike ``key_management`` (which + prefers hardware), the cert management capability defaults to the + file-backed handler so that all certificate operations are accessible + without hardware. + +* **CertObject as a value type** — parsed certificates are immutable + ``shared_ptr``-managed values. The same ``CertObject`` instance is + shared across trust stores and client handles via a weak-ptr cache in + ``TrustStoreManager``, avoiding redundant parses and copies. + +* **Trust store membership by typed slot reference** — anchors are + certificate slots, not raw file paths. This allows the daemon to track + content changes (``NotifySlotCertChanged``) and invalidate cached + anchors atomically without polling. + +* **CRL co-located with the CA cert slot** — an optional ``[crl]`` + section in the slot's KV deployment descriptor holds the CRL path and + ``nextUpdate`` epoch. No separate CRL registry is needed. + +* **Per-client anchor reference counting** — trust store anchor caches + are reference counted per client, not globally. Releasing one + application's verification context cannot evict another application's + active anchor cache. + +Backwards Compatibility Impact +------------------------------- + +The component introduces no changes to existing certificate-management +callers. The certificate-management, certificate-verification, and +CSR-generation context interfaces are defined in ``score/crypto/src/api/``. + +Security Impact +--------------- + +* Certificate bytes are public; no secret material is handled by this + component. The deployment infrastructure (``daemon/common/storage/``) + shared with key_management uses atomic rename-based writes to avoid + partial updates. +* Write access to cert slots and trust stores is default-deny: + ``AccessPolicyEnforcer::CheckWritePermission`` requires an explicit + ``allowed_write_uids`` entry for mutation operations. +* Trust store mutations (``AddMember``, ``RemoveMember``, + ``DisableMember``, ``EnableMember``) carry a separate write permission + check on the trust store policy, independent of the member slot's + write policy. + +Safety Impact +------------- + +No runtime safety-relevant behaviour beyond the general daemon isolation +contract. Certificate validation results (from the provider verification +handler) are returned to callers without modification; the component does +not cache or interpret verification outcomes. + +Rejected Ideas +-------------- + +* **Separate ``HsmBackedSlotHandler`` stub** — rejected; hardware + providers implement ``ICertSlotHandler`` directly (mirrors key_management + pattern). A daemon-side stub would create an untested code path. +* **Static trust-store paths (``static_cert_paths``, ``static_cert_dir``)** — + rejected; shared baseline anchors are represented by shared-static + certificate slots, keeping the deployment model uniform across all + anchor types. +* **``ICertFactory`` / ``ICertHandler``** — rejected; cert context + operations (verify, CSR, convert, key-extract) live inside context + handlers created by ``ICryptoHandlerFactory``. A separate factory + interface would duplicate handler lifecycle management. +* **Separate ``CrlRegistry``** — rejected; CRL data is co-located with its + CA cert slot in the KV descriptor. A standalone registry would require + a separate resolution path and complicate the slot lifecycle. + +Current Limitations +------------------- + +* CSR generation with hardware-bound keys requires a cross-context signing + service. Private key material is not exported by certificate management. +* CRL storage is supported, while CRL validation during certificate + verification is outside the current component behavior. +* Certificate operations require provider and daemon dispatch integration. diff --git a/score/crypto/src/daemon/cert_management/docs/requirements/index.rst b/score/crypto/src/daemon/cert_management/docs/requirements/index.rst new file mode 100644 index 000000000..4b3e70a70 --- /dev/null +++ b/score/crypto/src/daemon/cert_management/docs/requirements/index.rst @@ -0,0 +1,14 @@ +.. + # ******************************************************************************* + # Copyright (c) 2026 Contributors to the Eclipse Foundation + # SPDX-License-Identifier: Apache-2.0 + # ******************************************************************************* + +.. _crypto_cert_management_requirements: + +Requirements +============ + +.. toctree:: + + requirements diff --git a/score/crypto/src/daemon/cert_management/docs/requirements/requirements.rst b/score/crypto/src/daemon/cert_management/docs/requirements/requirements.rst new file mode 100644 index 000000000..844ecce8b --- /dev/null +++ b/score/crypto/src/daemon/cert_management/docs/requirements/requirements.rst @@ -0,0 +1,103 @@ +.. + # ******************************************************************************* + # Copyright (c) 2026 Contributors to the Eclipse Foundation + # SPDX-License-Identifier: Apache-2.0 + # ******************************************************************************* + +Certificate Management Requirements +=================================== + +.. document:: Certificate Management Requirements + :id: doc__crypto_cert_management_requirements + :version: 1 + :status: draft + :safety: QM + :security: YES + :realizes: wp__cmpt_request_dummy + :tags: cert_management, crypto_daemon + +Scope +----- + +The requirements below describe the current daemon component boundary. API +context and mediator requirements are included only where they affect the +component contract. + +Functional requirements +----------------------- + +.. comp_req:: Manage certificate slots + :id: comp_req__crypto_cert_management__slots + :version: 1 + :reqtype: Functional + :security: YES + :safety: QM + :status: valid + :satisfied_by: comp__crypto_cert_management + + The component shall resolve configured certificate slots and provide + loading, storage, state, metadata, and release operations through a slot + handler. + +.. comp_req:: Parse provider-neutral certificates + :id: comp_req__crypto_cert_management__parse + :version: 1 + :reqtype: Functional + :security: YES + :safety: QM + :status: valid + :satisfied_by: comp__crypto_cert_management + + The component shall represent parsed certificate bytes as immutable + ``CertObject`` values containing raw bytes, format, and chain metadata. + Parsing shall be supplied through the narrow ``ICertParser`` interface. + +.. comp_req:: Manage trust-store membership + :id: comp_req__crypto_cert_mgmt__trust_stores + :version: 1 + :reqtype: Functional + :security: YES + :safety: QM + :status: valid + :satisfied_by: comp__crypto_cert_management + + The component shall manage named trust stores whose members are typed + certificate-slot references. Slot changes shall invalidate affected anchor + caches. + +.. comp_req:: Persist certificate and CRL state + :id: comp_req__crypto_cert_management__persistence + :version: 1 + :reqtype: Functional + :security: YES + :safety: QM + :status: valid + :satisfied_by: comp__crypto_cert_management + + The component shall persist certificate, CRL, trust-store state, and + metadata using the shared deployment storage. Certificate and CRL files + shall be written atomically. + +.. comp_req:: Enforce mutation authorization + :id: comp_req__crypto_cert_mgmt__access_control + :version: 1 + :reqtype: Functional + :security: YES + :safety: QM + :status: valid + :satisfied_by: comp__crypto_cert_management + + Certificate-slot and trust-store mutations shall require an explicitly + authorized client UID. Empty write allowlists shall not grant access. + + +Scope boundary +-------------- + +Hardware-key CSR signing, CRL validation, and mediator routing are outside the +certificate-management storage and lifecycle boundary described here. A +provider integration may supply those capabilities through the corresponding +provider and daemon services. + +.. needextend:: "c.this_doc()" + :+tags: cert_management From 76fcb7827582c7125b40b99b04ba5dffa9e01326 Mon Sep 17 00:00:00 2001 From: Athul Mallappallil Date: Tue, 25 Aug 2026 17:02:21 +0200 Subject: [PATCH 28/37] Copyright fix --- score/crypto/src/daemon/cert_management/BUILD | 5 +---- .../architecture/cert_management_dynamic.puml | 13 +++++++++++++ .../architecture/cert_management_static.puml | 13 +++++++++++++ .../docs/architecture/component_architecture.rst | 8 ++++++++ .../cert_management/docs/architecture/index.rst | 9 +++++++++ .../docs/detailed_design/detailed_design.rst | 8 ++++++++ .../docs/detailed_design/index.rst | 8 ++++++++ .../src/daemon/cert_management/docs/index.rst | 14 ++++++++++++++ .../cert_management/docs/requirements/index.rst | 8 ++++++++ .../docs/requirements/requirements.rst | 8 ++++++++ .../daemon/cert_management/slot/crl_handler.cpp | 5 +---- .../daemon/cert_management/slot/crl_handler.hpp | 5 +---- .../slot/file_backed_slot_handler.hpp | 5 +---- .../tests/core/test_access_policy_enforcer.cpp | 13 +++++++++++++ .../tests/core/test_cert_registry.cpp | 13 +++++++++++++ .../test_certificate_management_integration.cpp | 13 +++++++++++++ .../service/test_cert_management_service.cpp | 16 ++++++++++++---- .../tests/slot/test_file_backed_slot_handler.cpp | 13 +++++++++++++ .../src/daemon/provider/cert_management/BUILD | 5 +---- .../cert_management_operations.hpp | 5 +---- .../provider/cert_management/cert_types.hpp | 5 +---- .../provider/cert_management/i_cert_parser.hpp | 5 +---- .../cert_management/openssl_cert_parser.cpp | 5 +---- .../cert_management/openssl_cert_parser.hpp | 5 +---- .../daemon/provider/tests/cert_management/BUILD | 8 ++++++++ .../cert_management/test_openssl_cert_parser.cpp | 13 +++++++++++++ score/tests/test_vectors/certificate/BUILD | 2 +- score/tests/test_vectors/certificate/README.md | 13 +++++++++++++ .../certificate/generate_certificates.py | 13 +++++++++++++ 29 files changed, 211 insertions(+), 45 deletions(-) diff --git a/score/crypto/src/daemon/cert_management/BUILD b/score/crypto/src/daemon/cert_management/BUILD index f6e6540ae..37c06e6bb 100644 --- a/score/crypto/src/daemon/cert_management/BUILD +++ b/score/crypto/src/daemon/cert_management/BUILD @@ -10,10 +10,7 @@ # # SPDX-License-Identifier: Apache-2.0 # ******************************************************************************* -# ******************************************************************************* -# Copyright (c) 2026 Contributors to the Eclipse Foundation -# SPDX-License-Identifier: Apache-2.0 -# ******************************************************************************* + load("@rules_cc//cc:cc_library.bzl", "cc_library") cc_library( diff --git a/score/crypto/src/daemon/cert_management/docs/architecture/cert_management_dynamic.puml b/score/crypto/src/daemon/cert_management/docs/architecture/cert_management_dynamic.puml index 5b7e15986..2f4d4cc7c 100644 --- a/score/crypto/src/daemon/cert_management/docs/architecture/cert_management_dynamic.puml +++ b/score/crypto/src/daemon/cert_management/docs/architecture/cert_management_dynamic.puml @@ -1,3 +1,16 @@ +' ******************************************************************************* +' 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 +' ******************************************************************************* + @startuml participant "Flat-config parser" as Config participant "ConfigDrivenSlotCatalog" as SlotCatalog diff --git a/score/crypto/src/daemon/cert_management/docs/architecture/cert_management_static.puml b/score/crypto/src/daemon/cert_management/docs/architecture/cert_management_static.puml index 4ff005f18..a613f506b 100644 --- a/score/crypto/src/daemon/cert_management/docs/architecture/cert_management_static.puml +++ b/score/crypto/src/daemon/cert_management/docs/architecture/cert_management_static.puml @@ -1,3 +1,16 @@ +' ******************************************************************************* +' 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 +' ******************************************************************************* + @startuml skinparam componentStyle rectangle skinparam packageStyle rectangle diff --git a/score/crypto/src/daemon/cert_management/docs/architecture/component_architecture.rst b/score/crypto/src/daemon/cert_management/docs/architecture/component_architecture.rst index 47719b71b..d1b7f1667 100644 --- a/score/crypto/src/daemon/cert_management/docs/architecture/component_architecture.rst +++ b/score/crypto/src/daemon/cert_management/docs/architecture/component_architecture.rst @@ -1,6 +1,14 @@ .. # ******************************************************************************* # 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 # ******************************************************************************* diff --git a/score/crypto/src/daemon/cert_management/docs/architecture/index.rst b/score/crypto/src/daemon/cert_management/docs/architecture/index.rst index 58ff6e56d..9a41a9ebe 100644 --- a/score/crypto/src/daemon/cert_management/docs/architecture/index.rst +++ b/score/crypto/src/daemon/cert_management/docs/architecture/index.rst @@ -1,6 +1,15 @@ .. + # ******************************************************************************* # ******************************************************************************* # 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 # ******************************************************************************* diff --git a/score/crypto/src/daemon/cert_management/docs/detailed_design/detailed_design.rst b/score/crypto/src/daemon/cert_management/docs/detailed_design/detailed_design.rst index f47427b1f..ef6e24dd5 100644 --- a/score/crypto/src/daemon/cert_management/docs/detailed_design/detailed_design.rst +++ b/score/crypto/src/daemon/cert_management/docs/detailed_design/detailed_design.rst @@ -1,6 +1,14 @@ .. # ******************************************************************************* # 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 # ******************************************************************************* diff --git a/score/crypto/src/daemon/cert_management/docs/detailed_design/index.rst b/score/crypto/src/daemon/cert_management/docs/detailed_design/index.rst index 121c9f648..6381f9eb2 100644 --- a/score/crypto/src/daemon/cert_management/docs/detailed_design/index.rst +++ b/score/crypto/src/daemon/cert_management/docs/detailed_design/index.rst @@ -1,6 +1,14 @@ .. # ******************************************************************************* # 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 # ******************************************************************************* diff --git a/score/crypto/src/daemon/cert_management/docs/index.rst b/score/crypto/src/daemon/cert_management/docs/index.rst index b25d6a0f4..40aa8b3f0 100644 --- a/score/crypto/src/daemon/cert_management/docs/index.rst +++ b/score/crypto/src/daemon/cert_management/docs/index.rst @@ -1,3 +1,17 @@ +.. + # ******************************************************************************* + # 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 + # ******************************************************************************* + Certificate Management Component ================================ diff --git a/score/crypto/src/daemon/cert_management/docs/requirements/index.rst b/score/crypto/src/daemon/cert_management/docs/requirements/index.rst index 4b3e70a70..433992fd6 100644 --- a/score/crypto/src/daemon/cert_management/docs/requirements/index.rst +++ b/score/crypto/src/daemon/cert_management/docs/requirements/index.rst @@ -1,6 +1,14 @@ .. # ******************************************************************************* # 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 # ******************************************************************************* diff --git a/score/crypto/src/daemon/cert_management/docs/requirements/requirements.rst b/score/crypto/src/daemon/cert_management/docs/requirements/requirements.rst index 844ecce8b..1f4bb59ef 100644 --- a/score/crypto/src/daemon/cert_management/docs/requirements/requirements.rst +++ b/score/crypto/src/daemon/cert_management/docs/requirements/requirements.rst @@ -1,6 +1,14 @@ .. # ******************************************************************************* # 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 # ******************************************************************************* diff --git a/score/crypto/src/daemon/cert_management/slot/crl_handler.cpp b/score/crypto/src/daemon/cert_management/slot/crl_handler.cpp index 7c85eafef..3f3271192 100644 --- a/score/crypto/src/daemon/cert_management/slot/crl_handler.cpp +++ b/score/crypto/src/daemon/cert_management/slot/crl_handler.cpp @@ -10,10 +10,7 @@ * * SPDX-License-Identifier: Apache-2.0 ********************************************************************************/ -/******************************************************************************** - * Copyright (c) 2026 Contributors to the Eclipse Foundation - * SPDX-License-Identifier: Apache-2.0 - ********************************************************************************/ + #include "score/crypto/src/daemon/cert_management/slot/crl_handler.hpp" #include "score/crypto/src/daemon/cert_management/slot/deployment_loader.hpp" diff --git a/score/crypto/src/daemon/cert_management/slot/crl_handler.hpp b/score/crypto/src/daemon/cert_management/slot/crl_handler.hpp index 7bcbb9515..1fc4c9908 100644 --- a/score/crypto/src/daemon/cert_management/slot/crl_handler.hpp +++ b/score/crypto/src/daemon/cert_management/slot/crl_handler.hpp @@ -10,10 +10,7 @@ * * SPDX-License-Identifier: Apache-2.0 ********************************************************************************/ -/******************************************************************************** - * Copyright (c) 2026 Contributors to the Eclipse Foundation - * SPDX-License-Identifier: Apache-2.0 - ********************************************************************************/ + #ifndef SCORE_CRYPTO_SRC_DAEMON_CERT_MANAGEMENT_SLOT_CRL_HANDLER_HPP #define SCORE_CRYPTO_SRC_DAEMON_CERT_MANAGEMENT_SLOT_CRL_HANDLER_HPP diff --git a/score/crypto/src/daemon/cert_management/slot/file_backed_slot_handler.hpp b/score/crypto/src/daemon/cert_management/slot/file_backed_slot_handler.hpp index 86e5d7154..637e5bed9 100644 --- a/score/crypto/src/daemon/cert_management/slot/file_backed_slot_handler.hpp +++ b/score/crypto/src/daemon/cert_management/slot/file_backed_slot_handler.hpp @@ -10,10 +10,7 @@ * * SPDX-License-Identifier: Apache-2.0 ********************************************************************************/ -/******************************************************************************** - * Copyright (c) 2026 Contributors to the Eclipse Foundation - * SPDX-License-Identifier: Apache-2.0 - ********************************************************************************/ + #ifndef SCORE_CRYPTO_SRC_DAEMON_CERT_MANAGEMENT_SLOT_FILE_BACKED_SLOT_HANDLER_HPP #define SCORE_CRYPTO_SRC_DAEMON_CERT_MANAGEMENT_SLOT_FILE_BACKED_SLOT_HANDLER_HPP diff --git a/score/crypto/src/daemon/cert_management/tests/core/test_access_policy_enforcer.cpp b/score/crypto/src/daemon/cert_management/tests/core/test_access_policy_enforcer.cpp index ada79313d..05cca0344 100644 --- a/score/crypto/src/daemon/cert_management/tests/core/test_access_policy_enforcer.cpp +++ b/score/crypto/src/daemon/cert_management/tests/core/test_access_policy_enforcer.cpp @@ -1,3 +1,16 @@ +/******************************************************************************** + * 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/cert_management/interfaces/cert_slot_config.hpp" #include "score/crypto/src/daemon/cert_management/interfaces/trust_store_config.hpp" #include "score/crypto/src/daemon/cert_management/policy/access_policy_enforcer.hpp" diff --git a/score/crypto/src/daemon/cert_management/tests/core/test_cert_registry.cpp b/score/crypto/src/daemon/cert_management/tests/core/test_cert_registry.cpp index 619eec3c9..d0d8ac4a7 100644 --- a/score/crypto/src/daemon/cert_management/tests/core/test_cert_registry.cpp +++ b/score/crypto/src/daemon/cert_management/tests/core/test_cert_registry.cpp @@ -1,3 +1,16 @@ +/******************************************************************************** + * 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/cert_management/core/cert_entry.hpp" #include "score/crypto/src/daemon/cert_management/core/cert_registry.hpp" diff --git a/score/crypto/src/daemon/cert_management/tests/integration/test_certificate_management_integration.cpp b/score/crypto/src/daemon/cert_management/tests/integration/test_certificate_management_integration.cpp index 4fa835ed5..23e0639f9 100644 --- a/score/crypto/src/daemon/cert_management/tests/integration/test_certificate_management_integration.cpp +++ b/score/crypto/src/daemon/cert_management/tests/integration/test_certificate_management_integration.cpp @@ -1,3 +1,16 @@ +/******************************************************************************** + * 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/cert_management/slot/config_driven_slot_catalog.hpp" #include "score/crypto/src/daemon/cert_management/slot/deployment_loader.hpp" #include "score/crypto/src/daemon/cert_management/slot/file_backed_slot_handler.hpp" diff --git a/score/crypto/src/daemon/cert_management/tests/service/test_cert_management_service.cpp b/score/crypto/src/daemon/cert_management/tests/service/test_cert_management_service.cpp index 297c50e7d..6158a16fc 100644 --- a/score/crypto/src/daemon/cert_management/tests/service/test_cert_management_service.cpp +++ b/score/crypto/src/daemon/cert_management/tests/service/test_cert_management_service.cpp @@ -1,7 +1,15 @@ -// ******************************************************************************* -// Copyright (c) 2026 Contributors to the Eclipse Foundation -// SPDX-License-Identifier: Apache-2.0 -// ******************************************************************************* +/******************************************************************************* + * 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 + *******************************************************************************/ // // Component-level tests for CertManagementService. // diff --git a/score/crypto/src/daemon/cert_management/tests/slot/test_file_backed_slot_handler.cpp b/score/crypto/src/daemon/cert_management/tests/slot/test_file_backed_slot_handler.cpp index 06fed3b9c..300e7f927 100644 --- a/score/crypto/src/daemon/cert_management/tests/slot/test_file_backed_slot_handler.cpp +++ b/score/crypto/src/daemon/cert_management/tests/slot/test_file_backed_slot_handler.cpp @@ -1,3 +1,16 @@ +/******************************************************************************** + * 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/cert_management/slot/file_backed_slot_handler.hpp" #include "score/crypto/src/daemon/common/storage/kv/kv_deployment_writer.hpp" #include "score/crypto/src/daemon/provider/score_provider/openssl/cert_management/openssl_cert_parser.hpp" diff --git a/score/crypto/src/daemon/provider/cert_management/BUILD b/score/crypto/src/daemon/provider/cert_management/BUILD index 2a9046948..5daf42b41 100644 --- a/score/crypto/src/daemon/provider/cert_management/BUILD +++ b/score/crypto/src/daemon/provider/cert_management/BUILD @@ -10,10 +10,7 @@ # # SPDX-License-Identifier: Apache-2.0 # ******************************************************************************* -# ******************************************************************************* -# Copyright (c) 2026 Contributors to the Eclipse Foundation -# SPDX-License-Identifier: Apache-2.0 -# ******************************************************************************* + load("@rules_cc//cc:cc_library.bzl", "cc_library") cc_library( diff --git a/score/crypto/src/daemon/provider/cert_management/cert_management_operations.hpp b/score/crypto/src/daemon/provider/cert_management/cert_management_operations.hpp index 1f93295a9..b71abe73f 100644 --- a/score/crypto/src/daemon/provider/cert_management/cert_management_operations.hpp +++ b/score/crypto/src/daemon/provider/cert_management/cert_management_operations.hpp @@ -10,10 +10,7 @@ * * SPDX-License-Identifier: Apache-2.0 ********************************************************************************/ -/******************************************************************************** - * Copyright (c) 2026 Contributors to the Eclipse Foundation - * SPDX-License-Identifier: Apache-2.0 - ********************************************************************************/ + #ifndef SCORE_CRYPTO_SRC_DAEMON_PROVIDER_CERT_MANAGEMENT_CERT_MANAGEMENT_OPERATIONS_HPP #define SCORE_CRYPTO_SRC_DAEMON_PROVIDER_CERT_MANAGEMENT_CERT_MANAGEMENT_OPERATIONS_HPP diff --git a/score/crypto/src/daemon/provider/cert_management/cert_types.hpp b/score/crypto/src/daemon/provider/cert_management/cert_types.hpp index 1ad42db8c..83899038e 100644 --- a/score/crypto/src/daemon/provider/cert_management/cert_types.hpp +++ b/score/crypto/src/daemon/provider/cert_management/cert_types.hpp @@ -10,10 +10,7 @@ * * SPDX-License-Identifier: Apache-2.0 ********************************************************************************/ -/******************************************************************************** - * Copyright (c) 2026 Contributors to the Eclipse Foundation - * SPDX-License-Identifier: Apache-2.0 - ********************************************************************************/ + #ifndef SCORE_CRYPTO_SRC_DAEMON_PROVIDER_CERT_MANAGEMENT_CERT_TYPES_HPP #define SCORE_CRYPTO_SRC_DAEMON_PROVIDER_CERT_MANAGEMENT_CERT_TYPES_HPP diff --git a/score/crypto/src/daemon/provider/cert_management/i_cert_parser.hpp b/score/crypto/src/daemon/provider/cert_management/i_cert_parser.hpp index d7322ec42..ef33f23eb 100644 --- a/score/crypto/src/daemon/provider/cert_management/i_cert_parser.hpp +++ b/score/crypto/src/daemon/provider/cert_management/i_cert_parser.hpp @@ -10,10 +10,7 @@ * * SPDX-License-Identifier: Apache-2.0 ********************************************************************************/ -/******************************************************************************** - * Copyright (c) 2026 Contributors to the Eclipse Foundation - * SPDX-License-Identifier: Apache-2.0 - ********************************************************************************/ + #ifndef SCORE_CRYPTO_SRC_DAEMON_PROVIDER_CERT_MANAGEMENT_I_CERT_PARSER_HPP #define SCORE_CRYPTO_SRC_DAEMON_PROVIDER_CERT_MANAGEMENT_I_CERT_PARSER_HPP diff --git a/score/crypto/src/daemon/provider/score_provider/openssl/cert_management/openssl_cert_parser.cpp b/score/crypto/src/daemon/provider/score_provider/openssl/cert_management/openssl_cert_parser.cpp index 1471c9bda..c377411d6 100644 --- a/score/crypto/src/daemon/provider/score_provider/openssl/cert_management/openssl_cert_parser.cpp +++ b/score/crypto/src/daemon/provider/score_provider/openssl/cert_management/openssl_cert_parser.cpp @@ -10,10 +10,7 @@ * * SPDX-License-Identifier: Apache-2.0 ********************************************************************************/ -/******************************************************************************** - * Copyright (c) 2026 Contributors to the Eclipse Foundation - * SPDX-License-Identifier: Apache-2.0 - ********************************************************************************/ + #include "score/crypto/src/daemon/provider/score_provider/openssl/cert_management/openssl_cert_parser.hpp" #include diff --git a/score/crypto/src/daemon/provider/score_provider/openssl/cert_management/openssl_cert_parser.hpp b/score/crypto/src/daemon/provider/score_provider/openssl/cert_management/openssl_cert_parser.hpp index 8957b06e1..9cb8796f6 100644 --- a/score/crypto/src/daemon/provider/score_provider/openssl/cert_management/openssl_cert_parser.hpp +++ b/score/crypto/src/daemon/provider/score_provider/openssl/cert_management/openssl_cert_parser.hpp @@ -10,10 +10,7 @@ * * SPDX-License-Identifier: Apache-2.0 ********************************************************************************/ -/******************************************************************************** - * Copyright (c) 2026 Contributors to the Eclipse Foundation - * SPDX-License-Identifier: Apache-2.0 - ********************************************************************************/ + #ifndef SCORE_CRYPTO_SRC_DAEMON_PROVIDER_SCORE_PROVIDER_OPENSSL_CERT_MANAGEMENT_OPENSSL_CERT_PARSER_HPP #define SCORE_CRYPTO_SRC_DAEMON_PROVIDER_SCORE_PROVIDER_OPENSSL_CERT_MANAGEMENT_OPENSSL_CERT_PARSER_HPP diff --git a/score/crypto/src/daemon/provider/tests/cert_management/BUILD b/score/crypto/src/daemon/provider/tests/cert_management/BUILD index d03322485..51dccc98f 100644 --- a/score/crypto/src/daemon/provider/tests/cert_management/BUILD +++ b/score/crypto/src/daemon/provider/tests/cert_management/BUILD @@ -1,5 +1,13 @@ # ******************************************************************************* # 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 # ******************************************************************************* diff --git a/score/crypto/src/daemon/provider/tests/cert_management/test_openssl_cert_parser.cpp b/score/crypto/src/daemon/provider/tests/cert_management/test_openssl_cert_parser.cpp index 861bd7a6b..83d093805 100644 --- a/score/crypto/src/daemon/provider/tests/cert_management/test_openssl_cert_parser.cpp +++ b/score/crypto/src/daemon/provider/tests/cert_management/test_openssl_cert_parser.cpp @@ -1,3 +1,16 @@ +/******************************************************************************** + * 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/provider/score_provider/openssl/cert_management/openssl_cert_parser.hpp" #include diff --git a/score/tests/test_vectors/certificate/BUILD b/score/tests/test_vectors/certificate/BUILD index 87ac6f2c5..d389c9e59 100644 --- a/score/tests/test_vectors/certificate/BUILD +++ b/score/tests/test_vectors/certificate/BUILD @@ -5,7 +5,7 @@ # information regarding copyright ownership. # # This program and the accompanying materials are made available under the -# terms of the Apache License 2.0 which is available at +# 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 diff --git a/score/tests/test_vectors/certificate/README.md b/score/tests/test_vectors/certificate/README.md index 8661a815d..ff5f88f00 100644 --- a/score/tests/test_vectors/certificate/README.md +++ b/score/tests/test_vectors/certificate/README.md @@ -1,3 +1,16 @@ + + # Certificate Test Vectors `certificate_manifest.json` is the inventory for certificate-management test certificates. It records each certificate's purpose, subject, private-key location, validity policy, and the metadata snapshot of the checked-in public certificate. diff --git a/score/tests/test_vectors/certificate/generate_certificates.py b/score/tests/test_vectors/certificate/generate_certificates.py index 8d70d4834..d7b39cd42 100644 --- a/score/tests/test_vectors/certificate/generate_certificates.py +++ b/score/tests/test_vectors/certificate/generate_certificates.py @@ -1,4 +1,17 @@ #!/usr/bin/env python3 +# ******************************************************************************* +# 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 +# ******************************************************************************* + """Generate or refresh certificate-management test certificates from a manifest.""" import argparse From 73701506af01067e9d542e4367795434cd66b672 Mon Sep 17 00:00:00 2001 From: Athul Mallappallil Date: Tue, 25 Aug 2026 21:55:53 +0000 Subject: [PATCH 29/37] Handle remove file error --- .../cert_management/slot/crl_handler.cpp | 2 +- .../slot/file_backed_slot_handler.cpp | 26 ++++++++++-- .../slot/test_file_backed_slot_handler.cpp | 41 +++++++++++++++++++ .../truststore/test_trust_store_manager.cpp | 16 ++++++-- .../truststore/trust_store_manager.cpp | 3 +- 5 files changed, 78 insertions(+), 10 deletions(-) diff --git a/score/crypto/src/daemon/cert_management/slot/crl_handler.cpp b/score/crypto/src/daemon/cert_management/slot/crl_handler.cpp index 3f3271192..313c19caa 100644 --- a/score/crypto/src/daemon/cert_management/slot/crl_handler.cpp +++ b/score/crypto/src/daemon/cert_management/slot/crl_handler.cpp @@ -99,7 +99,7 @@ score::crypto::Expected CrlHandler::ClearCrl(const CertSl if (!descriptor) return score::crypto::make_unexpected(descriptor.error()); const auto path = descriptor->Get("crl", "crl_path"); - if (!path.empty()) + if (!path.empty() && file_io::FileExists(path)) { auto remove_result = file_io::RemoveFile(path); if (!remove_result) diff --git a/score/crypto/src/daemon/cert_management/slot/file_backed_slot_handler.cpp b/score/crypto/src/daemon/cert_management/slot/file_backed_slot_handler.cpp index 8619d7996..f824f6be4 100644 --- a/score/crypto/src/daemon/cert_management/slot/file_backed_slot_handler.cpp +++ b/score/crypto/src/daemon/cert_management/slot/file_backed_slot_handler.cpp @@ -118,6 +118,18 @@ score::crypto::Expected FileBackedSlotHandler::StoreCerti auto result = file_io::WriteFile(path, bytes); if (!result) return result; + // Clear any stale CRL: it was issued for the previous CA key and is no longer + // valid for the incoming certificate. Preserve crl_path so that a future + // StoreCrl call re-uses the same location — this is essential for backends + // (e.g. PKCS#11) where cert_path is absent and the CRL path cannot be + // re-derived from the certificate file. File removal is best-effort; a + // stale CRL file will be overwritten by the next StoreCrl. + const auto old_crl_path = descriptor->Get("crl", "crl_path"); + if (!old_crl_path.empty()) + static_cast(file_io::RemoveFile(old_crl_path)); + descriptor->RemoveSection("crl"); + if (!old_crl_path.empty()) + descriptor->Set("crl", "crl_path", old_crl_path); descriptor->Set("certificate", "cert_format", cert.GetFormat() == score::crypto::FormatType::kDer ? "der" : "pem"); descriptor->Set("certificate_metadata", "subject", std::string(cert.GetSubject())); descriptor->Set("certificate_metadata", "issuer", std::string(cert.GetIssuer())); @@ -133,13 +145,19 @@ score::crypto::Expected FileBackedSlotHandler::ClearSlot( if (!descriptor) return score::crypto::make_unexpected(descriptor.error()); const auto cert = descriptor->Get("certificate", "cert_path"); - if (!cert.empty()) - file_io::RemoveFile(cert); + if (!cert.empty() && file_io::FileExists(cert)) + { + if (const auto rm = file_io::RemoveFile(cert); !rm) + return rm; + } // Preserve crl_path so a future StoreCrl re-uses the same location. // Only the file and volatile metadata (format, next_update) are discarded. const auto crl_path = descriptor->Get("crl", "crl_path"); - if (!crl_path.empty()) - file_io::RemoveFile(crl_path); + if (!crl_path.empty() && file_io::FileExists(crl_path)) + { + if (const auto rm = file_io::RemoveFile(crl_path); !rm) + return rm; + } descriptor->RemoveSection("crl"); if (!crl_path.empty()) descriptor->Set("crl", "crl_path", crl_path); diff --git a/score/crypto/src/daemon/cert_management/tests/slot/test_file_backed_slot_handler.cpp b/score/crypto/src/daemon/cert_management/tests/slot/test_file_backed_slot_handler.cpp index 300e7f927..1b825e5b2 100644 --- a/score/crypto/src/daemon/cert_management/tests/slot/test_file_backed_slot_handler.cpp +++ b/score/crypto/src/daemon/cert_management/tests/slot/test_file_backed_slot_handler.cpp @@ -124,6 +124,47 @@ TEST_F(FileBackedSlotHandlerTest, StoresLoadsAndClearsCrl) EXPECT_FALSE(m_handler->HasCrl(m_slot)); } +// Storing a new certificate must invalidate any existing CRL: the CRL was +// issued for the previous CA key and is meaningless for the new cert. +// After StoreCertificate, HasCrl() must return false even though a CRL was +// stored before the update. +TEST_F(FileBackedSlotHandlerTest, StoreCertificate_ClearsExistingCrl) +{ + cert::CertChainMetadata metadata; + metadata.subject_canonical = "CN=file-test"; + metadata.issuer_canonical = "CN=file-test"; + auto certificate = std::make_shared( + std::move(metadata), std::vector{1U, 2U, 3U}, score::crypto::FormatType::kDer); + + // Store a CRL first so the slot has one. + const std::vector crl_bytes{7U, 8U, 9U}; + ASSERT_TRUE(m_handler + ->StoreCrl(m_slot, + score::crypto::span{crl_bytes.data(), crl_bytes.size()}, + score::crypto::FormatType::kDer) + .has_value()); + ASSERT_TRUE(m_handler->HasCrl(m_slot)); + + // Storing a new cert must invalidate the stale CRL. + // crl_path is preserved in the descriptor (for future StoreCrl re-use) + // but the CRL file is removed, so HasCrl() must return false. + ASSERT_TRUE(m_handler->StoreCertificate(m_slot, *certificate).has_value()); + + EXPECT_FALSE(m_handler->HasCrl(m_slot)); // file gone → FileExists false + EXPECT_FALSE(std::filesystem::exists(m_crl)); + + // crl_path is preserved in the descriptor so a subsequent StoreCrl re-uses + // the same on-disk location without having to recompute it. + const std::vector new_crl{0xAAU, 0xBBU}; + ASSERT_TRUE(m_handler + ->StoreCrl(m_slot, + score::crypto::span{new_crl.data(), new_crl.size()}, + score::crypto::FormatType::kDer) + .has_value()); + EXPECT_TRUE(m_handler->HasCrl(m_slot)); + EXPECT_TRUE(std::filesystem::exists(m_crl)); +} + // A freshly-configured slot with no stored CRL must report HasCrl() == false // without any prior store call. This baseline is separate from the // StoresLoadsAndClearsCrl flow so that a regression in the empty-state diff --git a/score/crypto/src/daemon/cert_management/tests/truststore/test_trust_store_manager.cpp b/score/crypto/src/daemon/cert_management/tests/truststore/test_trust_store_manager.cpp index 177ccf5c6..27a19f331 100644 --- a/score/crypto/src/daemon/cert_management/tests/truststore/test_trust_store_manager.cpp +++ b/score/crypto/src/daemon/cert_management/tests/truststore/test_trust_store_manager.cpp @@ -1,7 +1,15 @@ -// ******************************************************************************* -// Copyright (c) 2026 Contributors to the Eclipse Foundation -// SPDX-License-Identifier: Apache-2.0 -// ******************************************************************************* +/******************************************************************************* + * 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 + ******************************************************************************/ // // Component-level tests for TrustStoreManager. // diff --git a/score/crypto/src/daemon/cert_management/truststore/trust_store_manager.cpp b/score/crypto/src/daemon/cert_management/truststore/trust_store_manager.cpp index cbbca50a0..635cb1030 100644 --- a/score/crypto/src/daemon/cert_management/truststore/trust_store_manager.cpp +++ b/score/crypto/src/daemon/cert_management/truststore/trust_store_manager.cpp @@ -503,7 +503,8 @@ score::crypto::Expected TrustStoreManager::RemoveMember(T if (current_fp.size() != fingerprint.size() || !std::equal(current_fp.begin(), current_fp.end(), fingerprint.begin())) continue; - static_cast(resolved->handler->ClearSlot(*resolved->cfg)); + if (const auto cleared = resolved->handler->ClearSlot(*resolved->cfg); !cleared) + return score::crypto::make_unexpected(cleared.error()); // Cert bytes gone — evict from shared cache and clear from handler. m_slot_cert_cache.erase(resolved->slot.index); m_member_states[id][resolved->slot.index].enabled = false; From a02dd50670fdbabff8d2dd5dcca19f28bd373452 Mon Sep 17 00:00:00 2001 From: Athul Mallappallil Date: Wed, 26 Aug 2026 21:56:06 +0000 Subject: [PATCH 30/37] crl propogation during cert storage --- .../cert_management/core/cert_entry.hpp | 52 +++++ .../core/cert_management_service.cpp | 51 +++++ .../core/cert_management_service.hpp | 27 +++ .../interfaces/i_cert_slot_handler.cpp | 5 + .../interfaces/i_cert_slot_handler.hpp | 8 + .../cert_management/slot/crl_handler.cpp | 9 + .../cert_management/slot/crl_handler.hpp | 4 + .../slot/file_backed_slot_handler.cpp | 5 + .../slot/file_backed_slot_handler.hpp | 1 + .../service/test_cert_management_service.cpp | 58 ++++++ .../slot/test_file_backed_slot_handler.cpp | 59 ++++++ .../truststore/test_trust_store_manager.cpp | 197 ++++++++++++++++++ .../truststore/trust_store_manager.cpp | 96 +++++++-- .../truststore/trust_store_manager.hpp | 37 +++- .../cert_management_operations.hpp | 4 +- 15 files changed, 597 insertions(+), 16 deletions(-) diff --git a/score/crypto/src/daemon/cert_management/core/cert_entry.hpp b/score/crypto/src/daemon/cert_management/core/cert_entry.hpp index a9f032913..99b3491a6 100644 --- a/score/crypto/src/daemon/cert_management/core/cert_entry.hpp +++ b/score/crypto/src/daemon/cert_management/core/cert_entry.hpp @@ -14,6 +14,7 @@ #ifndef SCORE_CRYPTO_SRC_DAEMON_CERT_MANAGEMENT_CORE_CERT_ENTRY_HPP #define SCORE_CRYPTO_SRC_DAEMON_CERT_MANAGEMENT_CORE_CERT_ENTRY_HPP +#include "score/crypto/src/api/common/types.hpp" #include "score/crypto/src/daemon/cert_management/interfaces/cert_object.hpp" #include "score/crypto/src/daemon/cert_management/interfaces/cert_types.hpp" #include "score/crypto/src/daemon/cert_management/slot/slot_registry.hpp" @@ -24,6 +25,7 @@ #include #include #include +#include #include namespace score::crypto::daemon::cert_management @@ -63,6 +65,53 @@ class CertEntry final : public std::enable_shared_from_this return m_slot_handle; } + // ----------------------------------------------------------------------- + // Session-scoped CRL association + // + // A session CRL is validated externally (signature, issuer match, validity) + // before being attached. CertEntry stores the bytes verbatim; no re-validation + // occurs. The association is session-lifetime — it does not survive a daemon + // restart and is never written to disk. This allows ImportCrl(persist=false) + // to associate a CRL with either an ephemeral kCertificate or a persistent + // kCertSlot without requiring write access to the slot's deployment descriptor. + // ----------------------------------------------------------------------- + + /// Attach a previously-validated CRL to this entry for the current session. + void AttachSessionCrl(std::vector crl_bytes, score::crypto::FormatType format) + { + const std::lock_guard lock(m_ref_mutex); + m_session_crl = std::move(crl_bytes); + m_session_crl_format = format; + } + + /// True when a session-scoped CRL has been attached via AttachSessionCrl(). + [[nodiscard]] bool HasSessionCrl() const noexcept + { + const std::lock_guard lock(m_ref_mutex); + return m_session_crl.has_value(); + } + + /// Return a copy of the session CRL bytes, or an empty optional if none. + [[nodiscard]] std::optional> GetSessionCrl() const + { + const std::lock_guard lock(m_ref_mutex); + return m_session_crl; + } + + [[nodiscard]] score::crypto::FormatType GetSessionCrlFormat() const noexcept + { + const std::lock_guard lock(m_ref_mutex); + return m_session_crl_format; + } + + /// Remove the session CRL association (e.g. after persisting to a slot). + void ClearSessionCrl() + { + const std::lock_guard lock(m_ref_mutex); + m_session_crl.reset(); + m_session_crl_format = score::crypto::FormatType::kDer; + } + // ----------------------------------------------------------------------- // Reference counting // ----------------------------------------------------------------------- @@ -100,6 +149,9 @@ class CertEntry final : public std::enable_shared_from_this mutable std::mutex m_ref_mutex; std::atomic m_ref_count{0U}; std::vector m_referencing_clients; + + std::optional> m_session_crl; + score::crypto::FormatType m_session_crl_format{score::crypto::FormatType::kDer}; }; } // namespace score::crypto::daemon::cert_management diff --git a/score/crypto/src/daemon/cert_management/core/cert_management_service.cpp b/score/crypto/src/daemon/cert_management/core/cert_management_service.cpp index c233b9b63..64a3006f7 100644 --- a/score/crypto/src/daemon/cert_management/core/cert_management_service.cpp +++ b/score/crypto/src/daemon/cert_management/core/cert_management_service.cpp @@ -207,6 +207,27 @@ score::crypto::Expected CertManagementService::ResolveC return entry->GetCertObject(); } +score::crypto::Expected, Error> CertManagementService::ResolveCertEntryForOperation( + data_manager::ClientId client_id, + data_manager::DataNodeId cert_node_id) +{ + if (!m_data_manager) + return score::crypto::make_unexpected(Error::kInternalError); + + auto acc_res = m_data_manager->getNodeAccessor(client_id, cert_node_id); + if (!acc_res.has_value()) + return score::crypto::make_unexpected(Error::kInvalidArgument); + + auto typed_res = std::move(acc_res).value().downCast(); + if (!typed_res.has_value()) + return score::crypto::make_unexpected(Error::kInvalidArgument); + + auto entry = typed_res.value()->GetCertEntry(); + if (!entry) + return score::crypto::make_unexpected(Error::kInternalError); + return entry; +} + score::crypto::Expected CertManagementService::ResolveTrustStoreForOperation( data_manager::ClientId client_id, data_manager::DataNodeId ts_node_id) @@ -234,4 +255,34 @@ void CertManagementService::NotifySlotCertChanged(CertSlotHandle slot_handle) m_trust_stores->NotifySlotChanged(ts_id, slot_handle); } +// --------------------------------------------------------------------------- +// CRL helpers +// --------------------------------------------------------------------------- + +score::crypto::Expected CertManagementService::AttachSessionCrl( + data_manager::ClientId client_id, + data_manager::DataNodeId cert_node_id, + std::vector crl_bytes, + score::crypto::FormatType format) +{ + if (!m_data_manager) + return score::crypto::make_unexpected(Error::kInternalError); + if (crl_bytes.empty()) + return score::crypto::make_unexpected(Error::kInvalidArgument); + + auto acc_res = m_data_manager->getNodeAccessor(client_id, cert_node_id); + if (!acc_res.has_value()) + return score::crypto::make_unexpected(Error::kInvalidArgument); + + auto typed_res = std::move(acc_res).value().downCast(); + if (!typed_res.has_value()) + return score::crypto::make_unexpected(Error::kInvalidArgument); + + auto entry = typed_res.value()->GetCertEntry(); + if (!entry) + return score::crypto::make_unexpected(Error::kInternalError); + entry->AttachSessionCrl(std::move(crl_bytes), format); + return std::monostate{}; +} + } // namespace score::crypto::daemon::cert_management diff --git a/score/crypto/src/daemon/cert_management/core/cert_management_service.hpp b/score/crypto/src/daemon/cert_management/core/cert_management_service.hpp index ea471ab39..9b1136b3b 100644 --- a/score/crypto/src/daemon/cert_management/core/cert_management_service.hpp +++ b/score/crypto/src/daemon/cert_management/core/cert_management_service.hpp @@ -13,14 +13,17 @@ #ifndef SCORE_CRYPTO_SRC_DAEMON_CERT_MANAGEMENT_CORE_CERT_MANAGEMENT_SERVICE_HPP #define SCORE_CRYPTO_SRC_DAEMON_CERT_MANAGEMENT_CORE_CERT_MANAGEMENT_SERVICE_HPP +#include "score/crypto/src/api/common/types.hpp" #include "score/crypto/src/daemon/cert_management/core/cert_registry.hpp" #include "score/crypto/src/daemon/cert_management/interfaces/i_cert_slot_handler.hpp" #include "score/crypto/src/daemon/cert_management/nodes/cert_data_node.hpp" #include "score/crypto/src/daemon/cert_management/truststore/trust_store_manager.hpp" #include "score/crypto/src/daemon/data_manager/i_data_manager.hpp" +#include #include #include +#include namespace score::crypto::daemon::cert_management { @@ -100,6 +103,14 @@ class CertManagementService final data_manager::ClientId client_id, data_manager::DataNodeId cert_node_id); + /// Look up a CertDataNode by node_id and return the underlying CertEntry. + /// + /// Use instead of ResolveCertForOperation when the session CRL association + /// (CertEntry::GetSessionCrl) is also needed — e.g. in HandleTrustStoreAdd. + score::crypto::Expected, common::DaemonErrorCode> ResolveCertEntryForOperation( + data_manager::ClientId client_id, + data_manager::DataNodeId cert_node_id); + /// Look up a TrustStoreDataNode by node_id and return the trust store handle. score::crypto::Expected ResolveTrustStoreForOperation( data_manager::ClientId client_id, @@ -110,6 +121,22 @@ class CertManagementService final /// Called after a successful StoreCertificate to keep trust store anchors current. void NotifySlotCertChanged(CertSlotHandle slot_handle); + // ----------------------------------------------------------------------- + // CRL helpers — called by the cert management executor + // ----------------------------------------------------------------------- + + /// Attach a session-scoped CRL to an ephemeral or slot-loaded certificate. + /// + /// The CRL bytes must be pre-validated by the caller (signature, issuer match, + /// validity). This stores the bytes in-memory on the CertEntry; they are not + /// written to disk. Subsequent SaveCertificate / AddCertificateToTrustStore + /// with with_crl=true will propagate from this in-memory association. + score::crypto::Expected AttachSessionCrl( + data_manager::ClientId client_id, + data_manager::DataNodeId cert_node_id, + std::vector crl_bytes, + score::crypto::FormatType format); + private: data_manager::IDataManager::Sptr m_data_manager; CertSlotRegistry::Sptr m_slot_registry; diff --git a/score/crypto/src/daemon/cert_management/interfaces/i_cert_slot_handler.cpp b/score/crypto/src/daemon/cert_management/interfaces/i_cert_slot_handler.cpp index 7bc2a4156..92211ddb3 100644 --- a/score/crypto/src/daemon/cert_management/interfaces/i_cert_slot_handler.cpp +++ b/score/crypto/src/daemon/cert_management/interfaces/i_cert_slot_handler.cpp @@ -55,4 +55,9 @@ score::crypto::Expected return score::crypto::make_unexpected(score::crypto::daemon::common::DaemonErrorCode::kUnsupportedOperation); } +score::crypto::FormatType ICertSlotHandler::GetCrlFormat(const CertSlotConfig& /*slot*/) +{ + return score::crypto::FormatType::kDer; +} + } // namespace score::crypto::daemon::cert_management diff --git a/score/crypto/src/daemon/cert_management/interfaces/i_cert_slot_handler.hpp b/score/crypto/src/daemon/cert_management/interfaces/i_cert_slot_handler.hpp index 296a79e08..ef0d515d9 100644 --- a/score/crypto/src/daemon/cert_management/interfaces/i_cert_slot_handler.hpp +++ b/score/crypto/src/daemon/cert_management/interfaces/i_cert_slot_handler.hpp @@ -174,6 +174,14 @@ class ICertSlotHandler /// Default: returns kUnsupportedOperation. [[nodiscard]] virtual score::crypto::Expected GetCrlNextUpdate(const CertSlotConfig& slot); + + /// Return the format (DER or PEM) of the CRL stored in the slot's [crl] section. + /// + /// Reads the crl_format key from the deployment descriptor without loading + /// the CRL bytes. Returns kDer when no [crl] section or format key is present. + /// + /// Default: returns kDer. + [[nodiscard]] virtual score::crypto::FormatType GetCrlFormat(const CertSlotConfig& slot); }; /// Factory function type for creating a slot handler from a slot configuration. diff --git a/score/crypto/src/daemon/cert_management/slot/crl_handler.cpp b/score/crypto/src/daemon/cert_management/slot/crl_handler.cpp index 313c19caa..655022657 100644 --- a/score/crypto/src/daemon/cert_management/slot/crl_handler.cpp +++ b/score/crypto/src/daemon/cert_management/slot/crl_handler.cpp @@ -129,4 +129,13 @@ score::crypto::Expected CrlHandler::GetCrlNextUpdate(const } } +score::crypto::FormatType CrlHandler::GetCrlFormat(const CertSlotConfig& slot) const +{ + const auto descriptor = LoadDescriptor(slot); + if (!descriptor) + return score::crypto::FormatType::kDer; + const auto fmt = descriptor->Get("crl", "crl_format"); + return (fmt == "pem") ? score::crypto::FormatType::kPem : score::crypto::FormatType::kDer; +} + } // namespace score::crypto::daemon::cert_management diff --git a/score/crypto/src/daemon/cert_management/slot/crl_handler.hpp b/score/crypto/src/daemon/cert_management/slot/crl_handler.hpp index 1fc4c9908..35c6375cb 100644 --- a/score/crypto/src/daemon/cert_management/slot/crl_handler.hpp +++ b/score/crypto/src/daemon/cert_management/slot/crl_handler.hpp @@ -72,6 +72,10 @@ class CrlHandler final [[nodiscard]] score::crypto::Expected GetCrlNextUpdate( const CertSlotConfig& slot) const; + /// Return the format (DER or PEM) recorded in the descriptor's [crl] crl_format key. + /// Returns kDer when the key is absent or the descriptor cannot be loaded. + [[nodiscard]] score::crypto::FormatType GetCrlFormat(const CertSlotConfig& slot) const; + private: static std::string FormatName(score::crypto::FormatType format); }; diff --git a/score/crypto/src/daemon/cert_management/slot/file_backed_slot_handler.cpp b/score/crypto/src/daemon/cert_management/slot/file_backed_slot_handler.cpp index f824f6be4..7a8311374 100644 --- a/score/crypto/src/daemon/cert_management/slot/file_backed_slot_handler.cpp +++ b/score/crypto/src/daemon/cert_management/slot/file_backed_slot_handler.cpp @@ -186,4 +186,9 @@ score::crypto::Expected FileBackedSlotHandler::GetCrlNextUpdate( { return m_crl.GetCrlNextUpdate(slot); } + +score::crypto::FormatType FileBackedSlotHandler::GetCrlFormat(const CertSlotConfig& slot) +{ + return m_crl.GetCrlFormat(slot); +} } // namespace score::crypto::daemon::cert_management diff --git a/score/crypto/src/daemon/cert_management/slot/file_backed_slot_handler.hpp b/score/crypto/src/daemon/cert_management/slot/file_backed_slot_handler.hpp index 637e5bed9..f9fcc94fa 100644 --- a/score/crypto/src/daemon/cert_management/slot/file_backed_slot_handler.hpp +++ b/score/crypto/src/daemon/cert_management/slot/file_backed_slot_handler.hpp @@ -51,6 +51,7 @@ class FileBackedSlotHandler final : public ICertSlotHandler score::crypto::FormatType) override; score::crypto::Expected ClearCrl(const CertSlotConfig&) override; score::crypto::Expected GetCrlNextUpdate(const CertSlotConfig&) override; + score::crypto::FormatType GetCrlFormat(const CertSlotConfig&) override; private: using Handler = CrlHandler; diff --git a/score/crypto/src/daemon/cert_management/tests/service/test_cert_management_service.cpp b/score/crypto/src/daemon/cert_management/tests/service/test_cert_management_service.cpp index 6158a16fc..f308ed080 100644 --- a/score/crypto/src/daemon/cert_management/tests/service/test_cert_management_service.cpp +++ b/score/crypto/src/daemon/cert_management/tests/service/test_cert_management_service.cpp @@ -349,4 +349,62 @@ TEST_F(CertManagementServiceTest, RegisterCertMaterial_EphemeralPath_CreatesReso EXPECT_FALSE(m_service->ResolveCertForOperation(kClientA, result->node_id).has_value()); } +// --------------------------------------------------------------------------- +// ResolveCertEntryForOperation — returns the CertEntry behind a cert node. +// Enables callers to access session CRL state alongside the CertObject. +// --------------------------------------------------------------------------- + +TEST_F(CertManagementServiceTest, ResolveCertEntryForOperation_ReturnsSameEntryAsLoaded) +{ + const auto result = LoadCert(kClientA); + ASSERT_NE(result.node_id, 0U); + ASSERT_NE(result.entry, nullptr); + + const auto entry_res = m_service->ResolveCertEntryForOperation(kClientA, result.node_id); + ASSERT_TRUE(entry_res.has_value()); + // Must be the exact same CertEntry (shared ownership), not a copy. + EXPECT_EQ(entry_res->get(), result.entry.get()); + // The resolved entry must expose the same cert object. + EXPECT_EQ((*entry_res)->GetCertObject()->GetSubject(), kSubjectInitial); +} + +TEST_F(CertManagementServiceTest, ResolveCertEntryForOperation_UnknownNode_ReturnsError) +{ + EXPECT_FALSE(m_service->ResolveCertEntryForOperation(kClientA, 999U).has_value()); +} + +// --------------------------------------------------------------------------- +// AttachSessionCrl — stores CRL bytes in-memory on CertEntry; no disk write. +// Subsequent GetSessionCrl() on the entry returns the attached bytes. +// --------------------------------------------------------------------------- + +TEST_F(CertManagementServiceTest, AttachSessionCrl_StoresInMemoryNotOnDisk) +{ + const auto result = LoadCert(kClientA); + ASSERT_NE(result.node_id, 0U); + + // No CRL on disk before the call. + EXPECT_FALSE(result.entry->HasSessionCrl()); + + const std::vector crl_bytes{0xC0U, 0xC1U, 0xC2U}; + ASSERT_TRUE( + m_service->AttachSessionCrl(kClientA, result.node_id, crl_bytes, score::crypto::FormatType::kDer).has_value()); + + // CRL must be accessible in-memory through the entry. + EXPECT_TRUE(result.entry->HasSessionCrl()); + const auto session_crl = result.entry->GetSessionCrl(); + ASSERT_TRUE(session_crl.has_value()); + EXPECT_EQ(*session_crl, crl_bytes); + EXPECT_EQ(result.entry->GetSessionCrlFormat(), score::crypto::FormatType::kDer); + + // No CRL file must have been written to disk. + EXPECT_FALSE(std::filesystem::exists(m_dir / "root_ca.crl")); +} + +TEST_F(CertManagementServiceTest, AttachSessionCrl_UnknownNode_ReturnsError) +{ + const std::vector crl_bytes{0x01U}; + EXPECT_FALSE(m_service->AttachSessionCrl(kClientA, 999U, crl_bytes, score::crypto::FormatType::kDer).has_value()); +} + } // namespace diff --git a/score/crypto/src/daemon/cert_management/tests/slot/test_file_backed_slot_handler.cpp b/score/crypto/src/daemon/cert_management/tests/slot/test_file_backed_slot_handler.cpp index 1b825e5b2..f33d33106 100644 --- a/score/crypto/src/daemon/cert_management/tests/slot/test_file_backed_slot_handler.cpp +++ b/score/crypto/src/daemon/cert_management/tests/slot/test_file_backed_slot_handler.cpp @@ -180,6 +180,65 @@ TEST_F(FileBackedSlotHandlerTest, GetSlotState_EmptyForFreshSlot) EXPECT_EQ(m_handler->GetSlotState(m_slot).value(), score::crypto::CertificateSlotState::kEmpty); } +// GetCrlFormat must return kDer (default) when no crl_format key is present in +// the descriptor's [crl] section. The fixture's SetUp writes crl_path but not +// crl_format, so this covers the "key absent" path in CrlHandler::GetCrlFormat. +TEST_F(FileBackedSlotHandlerTest, GetCrlFormat_ReturnsDerWhenNoFormatKey) +{ + EXPECT_EQ(m_handler->GetCrlFormat(m_slot), score::crypto::FormatType::kDer); +} + +// After storing a CRL with kPem format, GetCrlFormat must return kPem. This +// verifies that StoreCrl writes the format to the descriptor and GetCrlFormat +// reads it back correctly. +TEST_F(FileBackedSlotHandlerTest, GetCrlFormat_ReadsFormatFromDescriptor) +{ + const std::vector crl{0x01U, 0x02U, 0x03U}; + ASSERT_TRUE(m_handler + ->StoreCrl(m_slot, + score::crypto::span{crl.data(), crl.size()}, + score::crypto::FormatType::kPem) + .has_value()); + EXPECT_EQ(m_handler->GetCrlFormat(m_slot), score::crypto::FormatType::kPem); +} + +// ClearSlot must be idempotent: calling it a second time on an already-cleared +// slot must return success rather than an error. The FileExists guard ensures +// RemoveFile is not called when the cert file is already absent. +TEST_F(FileBackedSlotHandlerTest, ClearSlot_IsIdempotent) +{ + cert::CertChainMetadata metadata; + metadata.subject_canonical = "CN=file-test"; + metadata.issuer_canonical = "CN=file-test"; + auto certificate = std::make_shared( + std::move(metadata), std::vector{1U, 2U, 3U}, score::crypto::FormatType::kDer); + + ASSERT_TRUE(m_handler->StoreCertificate(m_slot, *certificate).has_value()); + + ASSERT_TRUE(m_handler->ClearSlot(m_slot).has_value()); + // Second call must succeed — slot is already empty. + ASSERT_TRUE(m_handler->ClearSlot(m_slot).has_value()); + EXPECT_EQ(m_handler->GetSlotState(m_slot).value(), score::crypto::CertificateSlotState::kEmpty); +} + +// ClearCrl must be idempotent: calling it a second time when no CRL file exists +// must return success. The FileExists guard prevents a spurious error from +// RemoveFile on an absent file. +TEST_F(FileBackedSlotHandlerTest, ClearCrl_IsIdempotent) +{ + const std::vector crl{0x0AU, 0x0BU}; + ASSERT_TRUE(m_handler + ->StoreCrl(m_slot, + score::crypto::span{crl.data(), crl.size()}, + score::crypto::FormatType::kDer) + .has_value()); + + ASSERT_TRUE(m_handler->ClearCrl(m_slot).has_value()); + // Second call must succeed — CRL file is already gone. + ASSERT_TRUE(m_handler->ClearCrl(m_slot).has_value()); + EXPECT_FALSE(m_handler->HasCrl(m_slot)); +} + // --------------------------------------------------------------------------- // OpenSSL-backed tests — verify real metadata extraction from test vectors. // These tests require the openssl_backend_active build constraint. diff --git a/score/crypto/src/daemon/cert_management/tests/truststore/test_trust_store_manager.cpp b/score/crypto/src/daemon/cert_management/tests/truststore/test_trust_store_manager.cpp index 27a19f331..8425ba65f 100644 --- a/score/crypto/src/daemon/cert_management/tests/truststore/test_trust_store_manager.cpp +++ b/score/crypto/src/daemon/cert_management/tests/truststore/test_trust_store_manager.cpp @@ -539,4 +539,201 @@ TEST_F(TrustStoreManagerTest, AcknowledgeMemberUpdate_TransitionsDisabledMemberT EXPECT_EQ((*after_ack)[0]->GetFingerprint().size(), 32U); } +// --------------------------------------------------------------------------- +// AddMember — CRL propagation: non-empty crl_bytes are stored to the exclusive +// slot alongside the certificate. +// --------------------------------------------------------------------------- + +TEST_F(TrustStoreManagerTest, AddMember_WithCrlBytes_StoresAndPersistsCrl) +{ + auto registry = std::make_shared(); + static_cast(registry->RegisterSlot(MakeSlotConfig("empty-anchor", m_empty_slot_kv))); + + cert::TrustStoreConfig ts_cfg; + ts_cfg.store_name = "mutable-store"; + ts_cfg.access_policy.allowed_write_uids = {0U}; + ts_cfg.members.push_back( + cert::TrustStoreMemberConfig{"empty-anchor", cert::TrustStoreMemberKind::kExclusiveMutable}); + + cert::TrustStoreManager manager; + manager.Load({ts_cfg}, registry, MakeHandlerFactory()); + + const auto ts_id = manager.ResolveByName("mutable-store"); + + auto cert = ParseCert(m_root_cert); + ASSERT_NE(cert, nullptr); + + const std::vector crl{0xC0U, 0xC1U, 0xC2U}; + const auto crl_span = score::crypto::span{crl.data(), crl.size()}; + + ASSERT_TRUE(manager.AddMember(ts_id, cert, kClientA, crl_span, score::crypto::FormatType::kDer).has_value()); + + // Verify via a fresh handler: cert and CRL are both on disk. + cert::FileBackedSlotHandler fresh_handler{m_parser}; + const auto cfg = MakeSlotConfig("empty-anchor", m_empty_slot_kv); + ASSERT_TRUE(fresh_handler.LoadCertificate(cfg).has_value()); + ASSERT_TRUE(fresh_handler.HasCrl(cfg)); + const auto loaded_crl = fresh_handler.LoadCrl(cfg); + ASSERT_TRUE(loaded_crl.has_value()); + EXPECT_EQ(*loaded_crl, crl); + EXPECT_EQ(fresh_handler.GetCrlFormat(cfg), score::crypto::FormatType::kDer); +} + +// --------------------------------------------------------------------------- +// AddMember — upsert semantics: if the cert is already an exclusive member and +// crl_bytes is non-empty, the CRL is stored/updated without re-adding the cert. +// --------------------------------------------------------------------------- + +TEST_F(TrustStoreManagerTest, AddMember_ExistingExclusiveMember_UpsertsCrl) +{ + auto registry = std::make_shared(); + static_cast(registry->RegisterSlot(MakeSlotConfig("empty-anchor", m_empty_slot_kv))); + + cert::TrustStoreConfig ts_cfg; + ts_cfg.store_name = "mutable-store"; + ts_cfg.access_policy.allowed_write_uids = {0U}; + ts_cfg.members.push_back( + cert::TrustStoreMemberConfig{"empty-anchor", cert::TrustStoreMemberKind::kExclusiveMutable}); + + cert::TrustStoreManager manager; + manager.Load({ts_cfg}, registry, MakeHandlerFactory()); + + const auto ts_id = manager.ResolveByName("mutable-store"); + + auto cert = ParseCert(m_root_cert); + ASSERT_NE(cert, nullptr); + + // First add: cert only, no CRL. + ASSERT_TRUE(manager.AddMember(ts_id, cert, kClientA).has_value()); + { + cert::FileBackedSlotHandler fh{m_parser}; + const auto cfg = MakeSlotConfig("empty-anchor", m_empty_slot_kv); + EXPECT_FALSE(fh.HasCrl(cfg)); + } + + // Second add: same cert, with CRL — upsert path. + const std::vector crl{0xAAU, 0xBBU, 0xCCU}; + const auto crl_span = score::crypto::span{crl.data(), crl.size()}; + ASSERT_TRUE(manager.AddMember(ts_id, cert, kClientA, crl_span, score::crypto::FormatType::kDer).has_value()); + + cert::FileBackedSlotHandler fresh_handler{m_parser}; + const auto cfg = MakeSlotConfig("empty-anchor", m_empty_slot_kv); + ASSERT_TRUE(fresh_handler.HasCrl(cfg)); + const auto loaded_crl = fresh_handler.LoadCrl(cfg); + ASSERT_TRUE(loaded_crl.has_value()); + EXPECT_EQ(*loaded_crl, crl); +} + +// --------------------------------------------------------------------------- +// AddMember — dedup: cert already present as a shared-static member with CRL +// bytes supplied → kUnsupportedOperation (trust store does not own shared slots). +// --------------------------------------------------------------------------- + +TEST_F(TrustStoreManagerTest, AddMember_SharedStaticMember_WithCrl_ReturnsUnsupportedOperation) +{ + auto registry = std::make_shared(); + static_cast(registry->RegisterSlot(MakeSlotConfig("root-anchor", m_root_slot_kv))); + + cert::TrustStoreConfig ts_cfg; + ts_cfg.store_name = "mixed-store"; + ts_cfg.access_policy.allowed_write_uids = {0U}; + ts_cfg.members.push_back(cert::TrustStoreMemberConfig{"root-anchor", cert::TrustStoreMemberKind::kSharedStatic}); + + cert::TrustStoreManager manager; + manager.Load({ts_cfg}, registry, MakeHandlerFactory()); + + const auto ts_id = manager.ResolveByName("mixed-store"); + + // Parse the same cert that is already the shared-static member. + auto cert = ParseCert(m_root_cert); + ASSERT_NE(cert, nullptr); + + const std::vector crl{0x01U, 0x02U}; + const auto crl_span = score::crypto::span{crl.data(), crl.size()}; + + const auto result = manager.AddMember(ts_id, cert, kClientA, crl_span, score::crypto::FormatType::kDer); + EXPECT_FALSE(result.has_value()); + EXPECT_EQ(result.error(), score::crypto::daemon::common::DaemonErrorCode::kUnsupportedOperation); +} + +// --------------------------------------------------------------------------- +// AddMember — dedup covers ALL member types: cert already in shared-static slot +// is found before exclusive slots are searched; no exclusive slot is consumed. +// --------------------------------------------------------------------------- + +TEST_F(TrustStoreManagerTest, AddMember_DeduplicationChecksAllMemberTypes) +{ + auto registry = std::make_shared(); + static_cast(registry->RegisterSlot(MakeSlotConfig("root-anchor", m_root_slot_kv))); + static_cast(registry->RegisterSlot(MakeSlotConfig("empty-anchor", m_empty_slot_kv))); + + cert::TrustStoreConfig ts_cfg; + ts_cfg.store_name = "mixed-store"; + ts_cfg.access_policy.allowed_write_uids = {0U}; + ts_cfg.members.push_back(cert::TrustStoreMemberConfig{"root-anchor", cert::TrustStoreMemberKind::kSharedStatic}); + ts_cfg.members.push_back( + cert::TrustStoreMemberConfig{"empty-anchor", cert::TrustStoreMemberKind::kExclusiveMutable}); + + cert::TrustStoreManager manager; + manager.Load({ts_cfg}, registry, MakeHandlerFactory()); + + const auto ts_id = manager.ResolveByName("mixed-store"); + + // Parse the same cert that is already the shared-static member. + auto cert = ParseCert(m_root_cert); + ASSERT_NE(cert, nullptr); + + // AddMember without CRL: dedup on shared-static → idempotent success. + ASSERT_TRUE(manager.AddMember(ts_id, cert, kClientA).has_value()); + + // The exclusive slot must still be empty — not consumed by AddMember. + cert::FileBackedSlotHandler fresh_handler{m_parser}; + const auto exclusive_cfg = MakeSlotConfig("empty-anchor", m_empty_slot_kv); + const auto state = fresh_handler.GetSlotState(exclusive_cfg); + ASSERT_TRUE(state.has_value()); + EXPECT_EQ(*state, score::crypto::CertificateSlotState::kEmpty); +} + +// --------------------------------------------------------------------------- +// ImportCrlForMember — writes CRL to the exclusive slot holding the given cert. +// --------------------------------------------------------------------------- + +TEST_F(TrustStoreManagerTest, ImportCrlForMember_WritesToExclusiveSlot) +{ + auto registry = std::make_shared(); + static_cast(registry->RegisterSlot(MakeSlotConfig("empty-anchor", m_empty_slot_kv))); + + cert::TrustStoreConfig ts_cfg; + ts_cfg.store_name = "mutable-store"; + ts_cfg.access_policy.allowed_write_uids = {0U}; + ts_cfg.members.push_back( + cert::TrustStoreMemberConfig{"empty-anchor", cert::TrustStoreMemberKind::kExclusiveMutable}); + + cert::TrustStoreManager manager; + manager.Load({ts_cfg}, registry, MakeHandlerFactory()); + + const auto ts_id = manager.ResolveByName("mutable-store"); + + auto cert = ParseCert(m_root_cert); + ASSERT_NE(cert, nullptr); + + // First add the cert without CRL. + ASSERT_TRUE(manager.AddMember(ts_id, cert, kClientA).has_value()); + + // Now import a CRL for that member by fingerprint. + const auto fp_span = cert->GetFingerprint(); + const std::vector crl{0xD0U, 0xD1U, 0xD2U}; + const auto crl_span = score::crypto::span{crl.data(), crl.size()}; + + ASSERT_TRUE(manager.ImportCrlForMember(ts_id, fp_span, crl_span, score::crypto::FormatType::kDer).has_value()); + + // Verify persistence: fresh handler must read the CRL. + cert::FileBackedSlotHandler fresh_handler{m_parser}; + const auto cfg = MakeSlotConfig("empty-anchor", m_empty_slot_kv); + ASSERT_TRUE(fresh_handler.HasCrl(cfg)); + const auto loaded_crl = fresh_handler.LoadCrl(cfg); + ASSERT_TRUE(loaded_crl.has_value()); + EXPECT_EQ(*loaded_crl, crl); +} + } // namespace diff --git a/score/crypto/src/daemon/cert_management/truststore/trust_store_manager.cpp b/score/crypto/src/daemon/cert_management/truststore/trust_store_manager.cpp index 635cb1030..1c2d2684a 100644 --- a/score/crypto/src/daemon/cert_management/truststore/trust_store_manager.cpp +++ b/score/crypto/src/daemon/cert_management/truststore/trust_store_manager.cpp @@ -434,31 +434,42 @@ score::crypto::Expected TrustStoreMan return ResolvedBackend{*cfg, handler}; } -score::crypto::Expected TrustStoreManager::AddMember(TrustStoreId id, - CertObject::Sptr cert, - data_manager::ClientId client_id) +score::crypto::Expected TrustStoreManager::AddMember( + TrustStoreId id, + CertObject::Sptr cert, + data_manager::ClientId client_id, + score::crypto::span crl_bytes, + score::crypto::FormatType crl_format) { std::lock_guard lock(m_mutex); if (id >= m_stores.size() || !cert) return score::crypto::make_unexpected(Error::kInvalidArgument); if (!AccessPolicyEnforcer::CheckTrustStoreWritePermission(m_stores[id].config, client_id).has_value()) return score::crypto::make_unexpected(Error::kAccessDenied); + + const auto& cert_fp = cert->GetFingerprint(); + const bool has_crl = !crl_bytes.empty(); + + // Dedup: check all member types before consuming an exclusive slot. for (const auto& member : m_stores[id].config.members) { - if (member.kind != TrustStoreMemberKind::kExclusiveMutable) - continue; const auto resolved = ResolveMember(member); if (!resolved) continue; const auto current = resolved->handler->LoadCertificate(*resolved->cfg); - if (current) + if (!current) + continue; + const auto& current_fp = (*current)->GetFingerprint(); + if (current_fp.size() != cert_fp.size() || !std::equal(current_fp.begin(), current_fp.end(), cert_fp.begin())) + continue; + + // Fingerprint match — cert is already a member. + if (member.kind == TrustStoreMemberKind::kExclusiveMutable) { - const auto& current_fp = (*current)->GetFingerprint(); - const auto& cert_fp = cert->GetFingerprint(); - if (current_fp.size() != cert_fp.size() || - !std::equal(current_fp.begin(), current_fp.end(), cert_fp.begin())) - continue; // Slot occupied by a different cert — try the next exclusive slot. - // Idempotent: cert already stored — re-enable and refresh cache. + // Upsert CRL if provided — trust store owns this exclusive slot. + if (has_crl) + static_cast(resolved->handler->StoreCrl(*resolved->cfg, crl_bytes, crl_format)); + // Re-enable in case it was disabled and refresh the anchor cache. m_member_states[id][resolved->slot.index].enabled = true; m_slot_cert_cache[resolved->slot.index] = cert; static_cast(m_stores[id].handler.get())->NotifySlotUpdate(resolved->slot, cert); @@ -466,10 +477,39 @@ score::crypto::Expected TrustStoreManager::AddMember(Trus return score::crypto::make_unexpected(persisted.error()); return std::monostate{}; } - // Empty slot — store cert here. + + // Shared-static or conditional-external: trust store does not own this slot. + // CRL writes on externally-managed slots must go through ImportCrl directly. + if (has_crl) + { + score::mw::log::LogError() << kLogPrefix << "AddMember: cannot write CRL to non-exclusive member '" + << member.slot_name << "' — use ImportCrl on the slot resource directly"; + return score::crypto::make_unexpected(Error::kUnsupportedOperation); + } + return std::monostate{}; + } + + // No fingerprint match — find an empty exclusive slot and store the cert. + for (const auto& member : m_stores[id].config.members) + { + if (member.kind != TrustStoreMemberKind::kExclusiveMutable) + continue; + const auto resolved = ResolveMember(member); + if (!resolved) + continue; + const auto state = resolved->handler->GetSlotState(*resolved->cfg); + if (!state || *state != score::crypto::CertificateSlotState::kEmpty) + continue; // occupied by a different cert + const auto stored = resolved->handler->StoreCertificate(*resolved->cfg, *cert); if (!stored) return score::crypto::make_unexpected(stored.error()); + + // Best-effort CRL write — cert already persisted; CRL failure is non-fatal. + // Caller may retry via ImportCrlForMember. + if (has_crl) + static_cast(resolved->handler->StoreCrl(*resolved->cfg, crl_bytes, crl_format)); + m_member_states[id][resolved->slot.index].enabled = true; m_slot_cert_cache[resolved->slot.index] = cert; static_cast(m_stores[id].handler.get())->NotifySlotUpdate(resolved->slot, cert); @@ -480,6 +520,36 @@ score::crypto::Expected TrustStoreManager::AddMember(Trus return score::crypto::make_unexpected(Error::kTrustStoreCapacityExceeded); } +score::crypto::Expected TrustStoreManager::ImportCrlForMember( + TrustStoreId id, + score::crypto::span fingerprint, + score::crypto::span crl_data, + score::crypto::FormatType format) +{ + std::lock_guard lock(m_mutex); + if (id >= m_stores.size()) + return score::crypto::make_unexpected(Error::kInvalidResourceId); + if (fingerprint.size() != 32U || crl_data.empty()) + return score::crypto::make_unexpected(Error::kInvalidArgument); + + for (const auto& member : m_stores[id].config.members) + { + if (member.kind != TrustStoreMemberKind::kExclusiveMutable) + continue; + const auto resolved = ResolveMember(member); + if (!resolved) + continue; + const auto current = resolved->handler->LoadCertificate(*resolved->cfg); + if (!current) + continue; + const auto& fp = (*current)->GetFingerprint(); + if (fp.size() != fingerprint.size() || !std::equal(fp.begin(), fp.end(), fingerprint.begin())) + continue; + return resolved->handler->StoreCrl(*resolved->cfg, crl_data, format); + } + return score::crypto::make_unexpected(Error::kInvalidResourceId); +} + score::crypto::Expected TrustStoreManager::RemoveMember(TrustStoreId id, const std::vector& fingerprint, data_manager::ClientId client_id) diff --git a/score/crypto/src/daemon/cert_management/truststore/trust_store_manager.hpp b/score/crypto/src/daemon/cert_management/truststore/trust_store_manager.hpp index e2a29f86c..9282fc0c1 100644 --- a/score/crypto/src/daemon/cert_management/truststore/trust_store_manager.hpp +++ b/score/crypto/src/daemon/cert_management/truststore/trust_store_manager.hpp @@ -14,6 +14,8 @@ #ifndef SCORE_CRYPTO_SRC_DAEMON_CERT_MANAGEMENT_TRUSTSTORE_TRUST_STORE_MANAGER_HPP #define SCORE_CRYPTO_SRC_DAEMON_CERT_MANAGEMENT_TRUSTSTORE_TRUST_STORE_MANAGER_HPP +#include "score/crypto/src/api/common/types.hpp" +#include "score/crypto/src/common/types.hpp" #include "score/crypto/src/daemon/cert_management/interfaces/cert_object.hpp" #include "score/crypto/src/daemon/cert_management/interfaces/cert_slot_config.hpp" #include "score/crypto/src/daemon/cert_management/interfaces/cert_types.hpp" @@ -145,11 +147,42 @@ class TrustStoreManager /// @brief Add a certificate to a trust store's runtime anchor set. /// - /// Persists the new member to the trust store descriptor. + /// Performs a fingerprint dedup across ALL member types before searching for + /// an empty exclusive slot — if the cert is already a member (shared-static, + /// conditional-external, or exclusive), returns success without consuming a + /// new slot. + /// + /// When @p crl_bytes is non-empty the CRL is written to the exclusive slot + /// atomically with the cert (new add) or as an upsert (cert already present). + /// + /// Upsert semantics for existing members: + /// - kExclusiveMutable match: CRL is stored/updated; cert is re-enabled. + /// - kSharedStatic / kConditionalExternal match: trust store does not own + /// these slots; if crl_bytes is non-empty, kUnsupportedOperation is + /// returned. Callers should use ImportCrl directly on the slot resource. + /// /// Write access to the trust store must be checked by CertManagementService /// before calling this method. + [[nodiscard]] score::crypto::Expected AddMember( + TrustStoreId id, + CertObject::Sptr cert, + data_manager::ClientId client_id, + score::crypto::span crl_bytes = {}, + score::crypto::FormatType crl_format = score::crypto::FormatType::kDer); + + /// @brief Import a CRL to the exclusive trust store slot holding the cert + /// whose SHA-256 fingerprint matches @p fingerprint. + /// + /// Only operates on kExclusiveMutable members — the trust store owns these. + /// Shared-static and conditional-external slots are externally managed; + /// callers use CertManagementService::ImportCrlToSlot directly for those. + /// + /// Returns kInvalidResourceId if no exclusive member fingerprint matches. [[nodiscard]] score::crypto::Expected - AddMember(TrustStoreId id, CertObject::Sptr cert, data_manager::ClientId client_id); + ImportCrlForMember(TrustStoreId id, + score::crypto::span fingerprint, + score::crypto::span crl_data, + score::crypto::FormatType format); /// @brief Remove a certificate from a trust store by fingerprint. /// diff --git a/score/crypto/src/daemon/provider/cert_management/cert_management_operations.hpp b/score/crypto/src/daemon/provider/cert_management/cert_management_operations.hpp index b71abe73f..41d8aae88 100644 --- a/score/crypto/src/daemon/provider/cert_management/cert_management_operations.hpp +++ b/score/crypto/src/daemon/provider/cert_management/cert_management_operations.hpp @@ -42,7 +42,9 @@ inline constexpr OperationAction TRUST_STORE_REMOVE_CERT = 0xC1U; inline constexpr OperationAction TRUST_STORE_ENABLE_CERT = 0xC2U; inline constexpr OperationAction TRUST_STORE_DISABLE_CERT = 0xC3U; inline constexpr OperationAction TRUST_STORE_ACK_UPDATE = 0xC4U; -inline constexpr OperationAction TRUST_STORE_REMOVE_CERT_BY_ID = 0xC5U; // remove by cert node_id (lib resolves fp) +inline constexpr OperationAction TRUST_STORE_REMOVE_CERT_BY_ID = 0xC5U; // remove by cert node_id (lib resolves fp) +inline constexpr OperationAction TRUST_STORE_IMPORT_CRL_FOR_MEMBER = 0xC6U; // fingerprint + CRL bytes → exclusive slot + inline constexpr OperationAction CERT_RELEASE = 0xF0U; // Provider-specific operation IDs must be >= CUSTOM_OP_START. From 4140d2d9934e5e423e5db84e4848f15693d9a4a9 Mon Sep 17 00:00:00 2001 From: Athul Mallappallil Date: Fri, 28 Aug 2026 09:40:31 +0000 Subject: [PATCH 31/37] Enhanced trust store APIs --- score/crypto/src/daemon/cert_management/BUILD | 13 + .../architecture/cert_management_static.puml | 13 + .../architecture/component_architecture.rst | 9 +- .../docs/detailed_design/detailed_design.rst | 23 +- .../cert_management/interfaces/cert_types.hpp | 8 + .../query/cert_object_serializer.cpp | 96 +++ .../query/cert_object_serializer.hpp | 76 +++ .../src/daemon/cert_management/tests/BUILD | 23 +- ...est_certificate_management_integration.cpp | 3 +- .../query/test_cert_object_serializer.cpp | 389 +++++++++++ .../service/test_cert_management_service.cpp | 20 +- .../slot/test_file_backed_slot_handler.cpp | 131 +++- .../tests/test_environment.hpp | 50 ++ .../truststore/test_trust_store_manager.cpp | 10 +- .../truststore/trust_store_manager.cpp | 69 +- .../truststore/trust_store_manager.hpp | 34 +- .../cert_management_operations.hpp | 4 + .../cert_management/openssl_cert_parser.cpp | 20 + .../tests/test_vectors/certificate/README.md | 74 ++- .../test_vectors/certificate/certificate.pem | 56 ++ .../certificate/certificate_manifest.json | 127 +++- .../certificate/certificate_updated.pem | 56 ++ .../test_vectors/certificate/ec_p256.pem | 44 ++ .../test_vectors/certificate/ec_p256_slot.kv | 3 + .../test_vectors/certificate/ec_p384.pem | 49 ++ .../test_vectors/certificate/ec_p384_slot.kv | 3 + .../test_vectors/certificate/ec_p521.pem | 55 ++ .../test_vectors/certificate/ec_p521_slot.kv | 3 + .../test_vectors/certificate/ed25519.pem | 39 ++ .../test_vectors/certificate/ed25519_slot.kv | 3 + .../tests/test_vectors/certificate/ed448.pem | 44 ++ .../test_vectors/certificate/ed448_slot.kv | 3 + .../certificate/generate_certificates.py | 265 ++++++-- .../test_vectors/certificate/ml_dsa_44.pem | 331 ++++++++++ .../certificate/ml_dsa_44_slot.kv | 3 + .../test_vectors/certificate/ml_dsa_65.pem | 455 +++++++++++++ .../certificate/ml_dsa_65_slot.kv | 3 + .../test_vectors/certificate/ml_dsa_87.pem | 612 ++++++++++++++++++ .../certificate/ml_dsa_87_slot.kv | 3 + .../test_vectors/certificate/rsa_3072.pem | 96 +++ .../test_vectors/certificate/rsa_3072_slot.kv | 3 + .../test_vectors/certificate/rsa_4096.pem | 117 ++++ .../test_vectors/certificate/rsa_4096_slot.kv | 3 + 43 files changed, 3347 insertions(+), 94 deletions(-) create mode 100644 score/crypto/src/daemon/cert_management/query/cert_object_serializer.cpp create mode 100644 score/crypto/src/daemon/cert_management/query/cert_object_serializer.hpp create mode 100644 score/crypto/src/daemon/cert_management/tests/query/test_cert_object_serializer.cpp create mode 100644 score/crypto/src/daemon/cert_management/tests/test_environment.hpp create mode 100644 score/tests/test_vectors/certificate/ec_p256.pem create mode 100644 score/tests/test_vectors/certificate/ec_p256_slot.kv create mode 100644 score/tests/test_vectors/certificate/ec_p384.pem create mode 100644 score/tests/test_vectors/certificate/ec_p384_slot.kv create mode 100644 score/tests/test_vectors/certificate/ec_p521.pem create mode 100644 score/tests/test_vectors/certificate/ec_p521_slot.kv create mode 100644 score/tests/test_vectors/certificate/ed25519.pem create mode 100644 score/tests/test_vectors/certificate/ed25519_slot.kv create mode 100644 score/tests/test_vectors/certificate/ed448.pem create mode 100644 score/tests/test_vectors/certificate/ed448_slot.kv create mode 100644 score/tests/test_vectors/certificate/ml_dsa_44.pem create mode 100644 score/tests/test_vectors/certificate/ml_dsa_44_slot.kv create mode 100644 score/tests/test_vectors/certificate/ml_dsa_65.pem create mode 100644 score/tests/test_vectors/certificate/ml_dsa_65_slot.kv create mode 100644 score/tests/test_vectors/certificate/ml_dsa_87.pem create mode 100644 score/tests/test_vectors/certificate/ml_dsa_87_slot.kv create mode 100644 score/tests/test_vectors/certificate/rsa_3072.pem create mode 100644 score/tests/test_vectors/certificate/rsa_3072_slot.kv create mode 100644 score/tests/test_vectors/certificate/rsa_4096.pem create mode 100644 score/tests/test_vectors/certificate/rsa_4096_slot.kv diff --git a/score/crypto/src/daemon/cert_management/BUILD b/score/crypto/src/daemon/cert_management/BUILD index 37c06e6bb..084481585 100644 --- a/score/crypto/src/daemon/cert_management/BUILD +++ b/score/crypto/src/daemon/cert_management/BUILD @@ -13,6 +13,19 @@ load("@rules_cc//cc:cc_library.bzl", "cc_library") +cc_library( + name = "cert_object_serializer", + srcs = ["query/cert_object_serializer.cpp"], + hdrs = ["query/cert_object_serializer.hpp"], + includes = ["."], + visibility = ["//:__subpackages__"], + deps = [ + ":cert_management", + ":cert_management_headers", + "//score/crypto/src/daemon/common", + ], +) + cc_library( name = "cert_management_headers", hdrs = glob([ diff --git a/score/crypto/src/daemon/cert_management/docs/architecture/cert_management_static.puml b/score/crypto/src/daemon/cert_management/docs/architecture/cert_management_static.puml index a613f506b..74364702c 100644 --- a/score/crypto/src/daemon/cert_management/docs/architecture/cert_management_static.puml +++ b/score/crypto/src/daemon/cert_management/docs/architecture/cert_management_static.puml @@ -35,6 +35,7 @@ package "cert_management" { [CertSlotRegistry] [ConfigDrivenSlotCatalog] [FileBackedSlotHandler] + [Pkcs11CertSlotHandler] [CrlHandler] } package "truststore" { @@ -45,6 +46,9 @@ package "cert_management" { package "policy" { [AccessPolicyEnforcer] } + package "query" { + [CertObjectSerializer] + } } package "daemon/common" { @@ -57,6 +61,10 @@ package "provider" { [Provider Context Handlers] } +package "daemon/mediator" { + [Mediator] +} + [CertManagementService] --> [CertRegistry] [CertManagementService] --> [CertSlotRegistry] [CertManagementService] --> [TrustStoreManager] @@ -76,8 +84,13 @@ package "provider" { [FileBackedSlotHandler] --> [CrlHandler] [FileBackedSlotHandler] --> ICertParser [FileBackedSlotHandler] --> [Shared Deployment Storage] +[Pkcs11CertSlotHandler] ..|> ICertSlotHandler +[Pkcs11CertSlotHandler] --> [CrlHandler] [OpenSslCertParser] ..|> ICertParser [OpenSslCertParser] --> [CertObject] [Provider Context Handlers] --> [CertObject] +[CertObjectSerializer] --> [CertManagementService] +[Provider Context Handlers] --> [CertObjectSerializer] +[Mediator] --> [CertObjectSerializer] @enduml diff --git a/score/crypto/src/daemon/cert_management/docs/architecture/component_architecture.rst b/score/crypto/src/daemon/cert_management/docs/architecture/component_architecture.rst index d1b7f1667..d5953ddf9 100644 --- a/score/crypto/src/daemon/cert_management/docs/architecture/component_architecture.rst +++ b/score/crypto/src/daemon/cert_management/docs/architecture/component_architecture.rst @@ -50,11 +50,16 @@ The implementation is divided by responsibility: ``CertEntry``. * ``nodes/`` contains DataManager nodes for certificate slots, loaded certificates, and trust stores. -* ``slot/`` contains slot registration, file-backed storage, deployment - dispatch, and co-located CRL storage. +* ``slot/`` contains slot registration, file-backed storage + (``FileBackedSlotHandler``), PKCS#11 storage (``Pkcs11CertSlotHandler``), + deployment dispatch, and co-located CRL storage (``CrlHandler``). * ``truststore/`` contains trust-store membership, anchor caching, persistence, and per-client references. * ``policy/`` contains the shared slot/trust-store access-policy checks. +* ``query/`` contains ``CertObjectSerializer`` — the single source of IPC + wire-format encoding for certificate, slot, and trust-store objects. Both + the executor and the mediator's typed-object handlers depend on this module; + changing the wire layout requires one edit. * ``provider/`` supplies parsing and provider-specific context handlers. The selected certificate-management provider must expose ``ICertParser``; certificate management does not require a particular provider. diff --git a/score/crypto/src/daemon/cert_management/docs/detailed_design/detailed_design.rst b/score/crypto/src/daemon/cert_management/docs/detailed_design/detailed_design.rst index ef6e24dd5..f4e3042d8 100644 --- a/score/crypto/src/daemon/cert_management/docs/detailed_design/detailed_design.rst +++ b/score/crypto/src/daemon/cert_management/docs/detailed_design/detailed_design.rst @@ -33,7 +33,9 @@ Implementation units ``CertRegistry`` and ``CertEntry`` Own live certificate entries and share immutable ``CertObject`` values - between clients and trust stores. + between clients and trust stores. ``CertEntry`` optionally holds a + session-scoped CRL (from ``ImportCrl`` with ``persist=false``) that is + never written to disk. ``CertSlotRegistry`` Stores immutable slot configuration and application resource mappings. @@ -41,15 +43,34 @@ Implementation units ``FileBackedSlotHandler`` and ``CrlHandler`` Read and write certificate/CRL data using the deployment descriptor and shared atomic file I/O. The slot handler delegates parsing to ``ICertParser``. + ``CrlHandler`` is composed into both ``FileBackedSlotHandler`` and + ``Pkcs11CertSlotHandler`` and handles all ``[crl]`` section operations. + +``Pkcs11CertSlotHandler`` + Storage backend for PKCS#11 token certificate slots. Implements + ``ICertSlotHandler`` using ``C_FindObjects`` / ``CKA_VALUE`` for load and + ``C_CreateObject`` / ``C_DestroyObject`` for write and clear. CRL operations + delegate to a composed ``CrlHandler`` (identical to the file-backed handler + because PKCS#11 tokens have no native CRL object type). ``TrustStoreManager`` and ``TrustStoreHandler`` Resolve typed slot memberships, maintain reverse indices, load anchors lazily, persist mutable member state, and manage per-client references. + Slot handlers are created on demand (lazy cache in ``GetOrCreateHandler``). + Reference counting is per-client: one client releasing its verification + context cannot evict another client's active anchor cache. ``AccessPolicyEnforcer`` Applies UID-based read/write policy. Mutation is default-deny when no writer UID is explicitly configured. +``CertObjectSerializer`` (``query/``) + Free functions that encode ``CertObject``, ``ICertSlotHandler`` state, and + trust-store member snapshots into the ``common::ResponseParameters`` IPC wire + format. Both ``CertManagementExecutor`` (executor path) and the mediator's + typed-object handlers call the same functions, guaranteeing a + single wire-layout definition for certificate, slot, and trust-store objects. + Data and lifetime model ----------------------- diff --git a/score/crypto/src/daemon/cert_management/interfaces/cert_types.hpp b/score/crypto/src/daemon/cert_management/interfaces/cert_types.hpp index e679eee37..6424c6b9a 100644 --- a/score/crypto/src/daemon/cert_management/interfaces/cert_types.hpp +++ b/score/crypto/src/daemon/cert_management/interfaces/cert_types.hpp @@ -105,6 +105,11 @@ struct CertChainMetadata /// RFC 4514 string representation of the Issuer DN. std::string issuer_canonical; + /// Certificate serial number as an uppercase hex string (e.g., "01ABCDEF"). + /// Empty if not available. Together with issuer_canonical this is the RFC 5280 + /// canonical certificate identifier used in CRLs and OCSP. + std::string serial_number_hex; + /// DER-encoded Subject Key Identifier extension value; empty if the extension /// is absent from the certificate. std::vector skid; @@ -214,6 +219,9 @@ inline constexpr std::string_view kIsCA = "is_ca"; /// Hex-encoded SHA-256 fingerprint of the certificate DER encoding. inline constexpr std::string_view kFingerprint = "fingerprint"; +/// Certificate serial number as an uppercase hex string (e.g., "01ABCDEF"). +inline constexpr std::string_view kSerialNumber = "serial_number"; + // ---- [metadata] section -------------------------------------------------- /// Slot availability override: "active" | "disabled" | "unavailable". diff --git a/score/crypto/src/daemon/cert_management/query/cert_object_serializer.cpp b/score/crypto/src/daemon/cert_management/query/cert_object_serializer.cpp new file mode 100644 index 000000000..0fd543967 --- /dev/null +++ b/score/crypto/src/daemon/cert_management/query/cert_object_serializer.cpp @@ -0,0 +1,96 @@ +/******************************************************************************** + * 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/cert_management/query/cert_object_serializer.hpp" +#include "score/crypto/src/daemon/cert_management/core/cert_management_service.hpp" + +#include + +namespace score::crypto::daemon::cert_management::query +{ + +common::ResponseParameters SerializeCertObject(const CertObject& cert) +{ + const auto& meta = cert.GetChainMetadata(); + common::ResponseParameters out; + out.push_back(common::OwnedString{meta.subject_canonical}); + out.push_back(common::OwnedString{meta.issuer_canonical}); + out.push_back(static_cast(static_cast(meta.not_before_epoch_s))); + out.push_back(static_cast(static_cast(meta.not_after_epoch_s))); + out.push_back(static_cast(meta.is_ca ? 1U : 0U)); + out.push_back(common::OwnedBuffer{meta.skid.begin(), meta.skid.end()}); + out.push_back(common::OwnedBuffer{meta.akid.begin(), meta.akid.end()}); + out.push_back(common::OwnedString{meta.serial_number_hex}); + out.push_back(common::OwnedBuffer{meta.fingerprint.begin(), meta.fingerprint.end()}); + return out; +} + +score::crypto::Expected SerializeCertSlotInfo( + ICertSlotHandler& handler, + const CertSlotConfig& config) +{ + auto info_res = handler.GetSlotInfo(config); + if (!info_res.has_value()) + return score::crypto::make_unexpected(info_res.error()); + + const bool has_crl = handler.HasCrl(config); + int64_t crl_next = 0; + if (has_crl) + { + auto nu = handler.GetCrlNextUpdate(config); + if (nu.has_value()) + crl_next = nu.value(); + } + + common::ResponseParameters out; + out.push_back(static_cast(info_res.value().state)); + out.push_back(static_cast(has_crl ? 1U : 0U)); + out.push_back(static_cast(static_cast(crl_next))); + return out; +} + +common::ResponseParameters SerializeTrustStoreMembers(const std::vector& snapshot, + CertManagementService& service, + std::uint64_t client_id) +{ + struct ResolvedEntry + { + std::uint64_t slot_node_id; + const TrustStoreManager::MemberSnapshot* snap; + }; + std::vector entries; + entries.reserve(snapshot.size()); + for (const auto& member : snapshot) + { + auto nid_res = service.ResolveCertSlot(member.slot_name, client_id); + if (!nid_res.has_value()) + continue; // slot not resolvable — omit silently rather than failing + entries.push_back({static_cast(nid_res.value()), &member}); + } + + common::ResponseParameters out; + out.push_back(static_cast(entries.size())); + for (const auto& entry : entries) + { + out.push_back(entry.slot_node_id); + out.push_back(common::OwnedBuffer{entry.snap->fingerprint.begin(), entry.snap->fingerprint.end()}); + out.push_back(common::OwnedString{entry.snap->subject}); + out.push_back(common::OwnedString{entry.snap->issuer}); + out.push_back(common::OwnedString{entry.snap->serial_number}); + out.push_back(static_cast(static_cast(entry.snap->kind))); + out.push_back(static_cast(entry.snap->is_enabled ? 1U : 0U)); + } + return out; +} + +} // namespace score::crypto::daemon::cert_management::query diff --git a/score/crypto/src/daemon/cert_management/query/cert_object_serializer.hpp b/score/crypto/src/daemon/cert_management/query/cert_object_serializer.hpp new file mode 100644 index 000000000..85ff8fefd --- /dev/null +++ b/score/crypto/src/daemon/cert_management/query/cert_object_serializer.hpp @@ -0,0 +1,76 @@ +/******************************************************************************** + * 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 + ********************************************************************************/ + +#ifndef SCORE_CRYPTO_SRC_DAEMON_CERT_MANAGEMENT_QUERY_CERT_OBJECT_SERIALIZER_HPP +#define SCORE_CRYPTO_SRC_DAEMON_CERT_MANAGEMENT_QUERY_CERT_OBJECT_SERIALIZER_HPP + +#include "score/crypto/src/daemon/cert_management/interfaces/cert_object.hpp" +#include "score/crypto/src/daemon/cert_management/interfaces/i_cert_slot_handler.hpp" +#include "score/crypto/src/daemon/cert_management/truststore/trust_store_manager.hpp" +#include "score/crypto/src/daemon/common/daemon_error.hpp" +#include "score/crypto/src/daemon/common/types.hpp" + +#include +#include + +// Forward-declare to avoid pulling in cert_management_service.hpp from the header. +namespace score::crypto::daemon::cert_management +{ +class CertManagementService; +} + +namespace score::crypto::daemon::cert_management::query +{ + +/// @brief Serialize a CertObject's chain metadata into IPC response parameters. +/// +/// Wire layout (9 params, indices 0–8): +/// [0] subject (OwnedString), [1] issuer (OwnedString), +/// [2] not_before_epoch_s (uint64), [3] not_after_epoch_s (uint64), +/// [4] is_ca (uint8), [5] skid (OwnedBuffer), [6] akid (OwnedBuffer), +/// [7] serial_number_hex (OwnedString), [8] SHA-256 fingerprint (OwnedBuffer 32B) +/// +/// This is the canonical layout for GET_CERTIFICATE_OBJECT and CERT_GET_METADATA. +/// Both the mediator typed-object handler and the cert management executor use this +/// function so the format is defined exactly once. +common::ResponseParameters SerializeCertObject(const CertObject& cert); + +/// @brief Serialize certificate slot state + CRL metadata into IPC response parameters. +/// +/// Wire layout (3 params): +/// [0] slot state (uint8, CertificateSlotState), [1] has_crl (uint8), +/// [2] crl_next_update_epoch (uint64, 0 if no CRL) +/// +/// Returns an error if GetSlotInfo fails. Both the mediator typed-object handler +/// and the cert management executor use this function. +score::crypto::Expected SerializeCertSlotInfo( + ICertSlotHandler& handler, + const CertSlotConfig& config); + +/// @brief Serialize trust store member snapshot into IPC response parameters. +/// +/// Resolves per-client slot DataNodeIds via @p service; members whose slot cannot +/// be resolved are silently omitted rather than failing the whole response. +/// +/// Wire layout: [0] count N (uint64), then for each member i in [0, N) (7 params): +/// [1+i*7+0] slot_node_id (uint64), [1+i*7+1] fingerprint (OwnedBuffer 32B), +/// [1+i*7+2] subject (OwnedString), [1+i*7+3] issuer (OwnedString), +/// [1+i*7+4] serial_number (OwnedString), [1+i*7+5] kind (uint8), +/// [1+i*7+6] is_enabled (uint8) +common::ResponseParameters SerializeTrustStoreMembers(const std::vector& snapshot, + CertManagementService& service, + std::uint64_t client_id); + +} // namespace score::crypto::daemon::cert_management::query + +#endif // SCORE_CRYPTO_SRC_DAEMON_CERT_MANAGEMENT_QUERY_CERT_OBJECT_SERIALIZER_HPP diff --git a/score/crypto/src/daemon/cert_management/tests/BUILD b/score/crypto/src/daemon/cert_management/tests/BUILD index 3d460d9c2..a67663c4c 100644 --- a/score/crypto/src/daemon/cert_management/tests/BUILD +++ b/score/crypto/src/daemon/cert_management/tests/BUILD @@ -5,6 +5,20 @@ load("@rules_cc//cc:cc_test.bzl", "cc_test") +# Unit tests for the cert_object_serializer query module. +# No OpenSSL dependency — CertObject is constructed synthetically. +# SerializeTrustStoreMembers slot-resolution coverage is in test_cert_management_service. +cc_test( + name = "test_cert_object_serializer", + srcs = ["query/test_cert_object_serializer.cpp"], + deps = [ + "//score/crypto/src/daemon/cert_management:cert_management_headers", + "//score/crypto/src/daemon/cert_management:cert_object_serializer", + "//score/crypto/src/daemon/common", + "@googletest//:gtest_main", + ], +) + cc_test( name = "test_cert_registry", srcs = ["core/test_cert_registry.cpp"], @@ -27,7 +41,7 @@ cc_test( cc_test( name = "test_file_backed_slot_handler", - srcs = ["slot/test_file_backed_slot_handler.cpp"], + srcs = ["slot/test_file_backed_slot_handler.cpp", "test_environment.hpp"], data = [ "//score/tests/test_vectors/certificate:certificate_test_vectors", ], @@ -47,7 +61,7 @@ cc_test( cc_test( name = "test_trust_store_manager", - srcs = ["truststore/test_trust_store_manager.cpp"], + srcs = ["truststore/test_trust_store_manager.cpp", "test_environment.hpp"], data = [ "//score/tests/test_vectors/certificate:certificate_test_vectors", ], @@ -67,8 +81,9 @@ cc_test( cc_test( name = "test_cert_management_service", - srcs = ["service/test_cert_management_service.cpp"], + srcs = ["service/test_cert_management_service.cpp", "test_environment.hpp"], data = [ + "//score/tests/config:logging.json", "//score/tests/test_vectors/certificate:certificate_test_vectors", ], target_compatible_with = select({ @@ -88,7 +103,7 @@ cc_test( cc_test( name = "test_certificate_management_integration", - srcs = ["integration/test_certificate_management_integration.cpp"], + srcs = ["integration/test_certificate_management_integration.cpp", "test_environment.hpp"], data = [ "//score/tests/test_vectors/certificate:certificate_test_vectors", ], diff --git a/score/crypto/src/daemon/cert_management/tests/integration/test_certificate_management_integration.cpp b/score/crypto/src/daemon/cert_management/tests/integration/test_certificate_management_integration.cpp index 23e0639f9..d9b8638a8 100644 --- a/score/crypto/src/daemon/cert_management/tests/integration/test_certificate_management_integration.cpp +++ b/score/crypto/src/daemon/cert_management/tests/integration/test_certificate_management_integration.cpp @@ -14,6 +14,7 @@ #include "score/crypto/src/daemon/cert_management/slot/config_driven_slot_catalog.hpp" #include "score/crypto/src/daemon/cert_management/slot/deployment_loader.hpp" #include "score/crypto/src/daemon/cert_management/slot/file_backed_slot_handler.hpp" +#include "score/crypto/src/daemon/cert_management/tests/test_environment.hpp" #include "score/crypto/src/daemon/cert_management/truststore/config_driven_trust_store_catalog.hpp" #include "score/crypto/src/daemon/common/storage/kv/kv_deployment_writer.hpp" #include "score/crypto/src/daemon/provider/score_provider/openssl/cert_management/openssl_cert_parser.hpp" @@ -41,7 +42,7 @@ class CertificateManagementIntegrationTest : public ::testing::Test protected: void SetUp() override { - m_directory = std::filesystem::temp_directory_path() / "score_cert_management_integration"; + m_directory = cert::test::TempDirectory("score_cert_management_integration"); std::filesystem::remove_all(m_directory); std::filesystem::create_directories(m_directory); m_descriptor_path = m_directory / "device_root.kv"; diff --git a/score/crypto/src/daemon/cert_management/tests/query/test_cert_object_serializer.cpp b/score/crypto/src/daemon/cert_management/tests/query/test_cert_object_serializer.cpp new file mode 100644 index 000000000..edd2f1e3c --- /dev/null +++ b/score/crypto/src/daemon/cert_management/tests/query/test_cert_object_serializer.cpp @@ -0,0 +1,389 @@ +/******************************************************************************* + * 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 + *******************************************************************************/ +// +// Unit tests for cert_management/query/cert_object_serializer. +// +// Tests verify the IPC wire format produced by each serializer function: +// - Parameter count and order +// - Parameter variant type (OwnedString, OwnedBuffer, uint64, uint8) +// - Parameter values for known synthetic inputs +// +// These tests do NOT require OpenSSL. CertObject is constructed synthetically +// from a known CertChainMetadata. ICertSlotHandler is stubbed inline. +// +// SerializeTrustStoreMembers requires a live CertManagementService for slot +// resolution and is therefore covered by test_cert_management_service.cpp +// (service-level integration tests) rather than here. + +#include "score/crypto/src/daemon/cert_management/interfaces/cert_object.hpp" +#include "score/crypto/src/daemon/cert_management/interfaces/cert_slot_config.hpp" +#include "score/crypto/src/daemon/cert_management/interfaces/cert_types.hpp" +#include "score/crypto/src/daemon/cert_management/interfaces/i_cert_slot_handler.hpp" +#include "score/crypto/src/daemon/cert_management/query/cert_object_serializer.hpp" +#include "score/crypto/src/daemon/common/daemon_error.hpp" +#include "score/crypto/src/daemon/common/types.hpp" + +#include + +#include +#include +#include + +namespace +{ +namespace cert = score::crypto::daemon::cert_management; +namespace query = cert::query; +namespace common = score::crypto::daemon::common; + +// --------------------------------------------------------------------------- +// Helpers — extract typed value from ResponseParameter variant +// --------------------------------------------------------------------------- + +template +const T* GetParam(const score::crypto::daemon::common::ResponseParameters& params, std::size_t idx) +{ + if (idx >= params.size()) + return nullptr; + return std::get_if(¶ms[idx]); +} + +// --------------------------------------------------------------------------- +// Synthetic CertObject factory — no OpenSSL dependency +// --------------------------------------------------------------------------- + +cert::CertObject MakeSyntheticCert(bool is_ca = true) +{ + cert::CertChainMetadata meta; + meta.subject_canonical = "CN=Test CA,O=SCORE,C=DE"; + meta.issuer_canonical = "CN=Root CA,O=SCORE,C=DE"; + meta.serial_number_hex = "01ABCDEF"; + // Epoch values chosen to fit in int64 and uint64 without sign issues. + meta.not_before_epoch_s = 1700000000LL; + meta.not_after_epoch_s = 1730000000LL; + meta.is_ca = is_ca; + meta.skid = {0x11, 0x22, 0x33}; + meta.akid = {0xAA, 0xBB}; + meta.fingerprint.assign(32U, 0x5A); + + // Raw bytes not exercised by serializer — single placeholder byte is sufficient. + return cert::CertObject{std::move(meta), {0x30}, score::crypto::FormatType::kDer}; +} + +// --------------------------------------------------------------------------- +// ICertSlotHandler stub — configurable inline +// --------------------------------------------------------------------------- + +struct SlotHandlerStub : public cert::ICertSlotHandler +{ + // Mutable so the stub can be used from const methods. + mutable score::crypto::Expected + slot_info_result{score::crypto::CertificateSlotInfo{score::crypto::CertificateSlotState::kOccupied}}; + + bool has_crl_result{false}; + mutable std::optional crl_next_update_value{std::nullopt}; + + score::crypto::Expected LoadCertificate( + const cert::CertSlotConfig&) override + { + return score::crypto::make_unexpected(score::crypto::daemon::common::DaemonErrorCode::kUnsupportedOperation); + } + + score::crypto::Expected StoreCertificate( + const cert::CertSlotConfig&, + const cert::CertObject&) override + { + return score::crypto::make_unexpected(score::crypto::daemon::common::DaemonErrorCode::kUnsupportedOperation); + } + + score::crypto::Expected ClearSlot( + const cert::CertSlotConfig&) override + { + return score::crypto::make_unexpected(score::crypto::daemon::common::DaemonErrorCode::kUnsupportedOperation); + } + + score::crypto::Expected + GetSlotState(const cert::CertSlotConfig&) override + { + return score::crypto::CertificateSlotState::kEmpty; + } + + score::crypto::Expected + GetSlotInfo(const cert::CertSlotConfig&) override + { + return slot_info_result; + } + + bool HasCrl(const cert::CertSlotConfig&) override + { + return has_crl_result; + } + + score::crypto::Expected, score::crypto::daemon::common::DaemonErrorCode> LoadCrl( + const cert::CertSlotConfig&) override + { + return score::crypto::make_unexpected(score::crypto::daemon::common::DaemonErrorCode::kUnsupportedOperation); + } + + score::crypto::Expected + StoreCrl(const cert::CertSlotConfig&, score::crypto::span, score::crypto::FormatType) override + { + return score::crypto::make_unexpected(score::crypto::daemon::common::DaemonErrorCode::kUnsupportedOperation); + } + + score::crypto::Expected ClearCrl( + const cert::CertSlotConfig&) override + { + return score::crypto::make_unexpected(score::crypto::daemon::common::DaemonErrorCode::kUnsupportedOperation); + } + + score::crypto::Expected GetCrlNextUpdate( + const cert::CertSlotConfig&) override + { + if (!crl_next_update_value.has_value()) + return score::crypto::make_unexpected( + score::crypto::daemon::common::DaemonErrorCode::kUnsupportedOperation); + return *crl_next_update_value; + } + + score::crypto::FormatType GetCrlFormat(const cert::CertSlotConfig&) override + { + return score::crypto::FormatType::kDer; + } +}; + +// =========================================================================== +// SerializeCertObject +// =========================================================================== + +TEST(SerializeCertObject, ProducesNineParameters) +{ + const auto cert = MakeSyntheticCert(); + EXPECT_EQ(query::SerializeCertObject(cert).size(), 9U); +} + +TEST(SerializeCertObject, Param0_SubjectString) +{ + const auto cert = MakeSyntheticCert(); + const auto params = query::SerializeCertObject(cert); + const auto* val = GetParam(params, 0U); + ASSERT_NE(val, nullptr); + EXPECT_EQ(*val, "CN=Test CA,O=SCORE,C=DE"); +} + +TEST(SerializeCertObject, Param1_IssuerString) +{ + const auto cert = MakeSyntheticCert(); + const auto params = query::SerializeCertObject(cert); + const auto* val = GetParam(params, 1U); + ASSERT_NE(val, nullptr); + EXPECT_EQ(*val, "CN=Root CA,O=SCORE,C=DE"); +} + +TEST(SerializeCertObject, Param2_NotBeforeEpochUint64) +{ + const auto cert = MakeSyntheticCert(); + const auto params = query::SerializeCertObject(cert); + const auto* val = GetParam(params, 2U); + ASSERT_NE(val, nullptr); + EXPECT_EQ(*val, static_cast(1700000000ULL)); +} + +TEST(SerializeCertObject, Param3_NotAfterEpochUint64) +{ + const auto cert = MakeSyntheticCert(); + const auto params = query::SerializeCertObject(cert); + const auto* val = GetParam(params, 3U); + ASSERT_NE(val, nullptr); + EXPECT_EQ(*val, static_cast(1730000000ULL)); +} + +TEST(SerializeCertObject, Param4_IsCaTrue_EncodesAs1) +{ + const auto cert = MakeSyntheticCert(/*is_ca=*/true); + const auto params = query::SerializeCertObject(cert); + const auto* val = GetParam(params, 4U); + ASSERT_NE(val, nullptr); + EXPECT_EQ(*val, 1U); +} + +TEST(SerializeCertObject, Param4_IsCaFalse_EncodesAs0) +{ + const auto cert = MakeSyntheticCert(/*is_ca=*/false); + const auto params = query::SerializeCertObject(cert); + const auto* val = GetParam(params, 4U); + ASSERT_NE(val, nullptr); + EXPECT_EQ(*val, 0U); +} + +TEST(SerializeCertObject, Param5_SkidBuffer) +{ + const auto cert = MakeSyntheticCert(); + const auto params = query::SerializeCertObject(cert); + const auto* val = GetParam(params, 5U); + ASSERT_NE(val, nullptr); + ASSERT_EQ(val->size(), 3U); + EXPECT_EQ((*val)[0], 0x11U); + EXPECT_EQ((*val)[1], 0x22U); + EXPECT_EQ((*val)[2], 0x33U); +} + +TEST(SerializeCertObject, Param6_AkidBuffer) +{ + const auto cert = MakeSyntheticCert(); + const auto params = query::SerializeCertObject(cert); + const auto* val = GetParam(params, 6U); + ASSERT_NE(val, nullptr); + ASSERT_EQ(val->size(), 2U); + EXPECT_EQ((*val)[0], 0xAAU); + EXPECT_EQ((*val)[1], 0xBBU); +} + +TEST(SerializeCertObject, Param7_SerialNumberString) +{ + const auto cert = MakeSyntheticCert(); + const auto params = query::SerializeCertObject(cert); + const auto* val = GetParam(params, 7U); + ASSERT_NE(val, nullptr); + EXPECT_EQ(*val, "01ABCDEF"); +} + +TEST(SerializeCertObject, Param8_Fingerprint32ByteBuffer) +{ + const auto cert = MakeSyntheticCert(); + const auto params = query::SerializeCertObject(cert); + const auto* val = GetParam(params, 8U); + ASSERT_NE(val, nullptr); + ASSERT_EQ(val->size(), 32U); + for (const auto byte : *val) + EXPECT_EQ(byte, 0x5AU); +} + +TEST(SerializeCertObject, EmptySkidAndAkidEncodeAsEmptyBuffers) +{ + cert::CertChainMetadata meta; + meta.subject_canonical = "CN=Leaf"; + meta.issuer_canonical = "CN=CA"; + meta.fingerprint.assign(32U, 0x00); + // skid and akid left default (empty vectors) + + cert::CertObject leaf{std::move(meta), {0x30}, score::crypto::FormatType::kDer}; + const auto params = query::SerializeCertObject(leaf); + const auto* skid = GetParam(params, 5U); + const auto* akid = GetParam(params, 6U); + ASSERT_NE(skid, nullptr); + ASSERT_NE(akid, nullptr); + EXPECT_TRUE(skid->empty()); + EXPECT_TRUE(akid->empty()); +} + +// =========================================================================== +// SerializeCertSlotInfo +// =========================================================================== + +TEST(SerializeCertSlotInfo, ProducesThreeParameters) +{ + SlotHandlerStub stub; + cert::CertSlotConfig config{}; + const auto result = query::SerializeCertSlotInfo(stub, config); + ASSERT_TRUE(result.has_value()); + EXPECT_EQ(result.value().size(), 3U); +} + +TEST(SerializeCertSlotInfo, Param0_SlotStateUint8_Occupied) +{ + SlotHandlerStub stub; + stub.slot_info_result = score::crypto::CertificateSlotInfo{score::crypto::CertificateSlotState::kOccupied}; + cert::CertSlotConfig config{}; + const auto result = query::SerializeCertSlotInfo(stub, config); + ASSERT_TRUE(result.has_value()); + const auto* val = GetParam(result.value(), 0U); + ASSERT_NE(val, nullptr); + EXPECT_EQ(*val, static_cast(score::crypto::CertificateSlotState::kOccupied)); +} + +TEST(SerializeCertSlotInfo, Param0_SlotStateUint8_Empty) +{ + SlotHandlerStub stub; + stub.slot_info_result = score::crypto::CertificateSlotInfo{score::crypto::CertificateSlotState::kEmpty}; + cert::CertSlotConfig config{}; + const auto result = query::SerializeCertSlotInfo(stub, config); + ASSERT_TRUE(result.has_value()); + const auto* val = GetParam(result.value(), 0U); + ASSERT_NE(val, nullptr); + EXPECT_EQ(*val, static_cast(score::crypto::CertificateSlotState::kEmpty)); +} + +TEST(SerializeCertSlotInfo, NoCrl_Param1IsZero_Param2IsZero) +{ + SlotHandlerStub stub; + stub.has_crl_result = false; + cert::CertSlotConfig config{}; + const auto result = query::SerializeCertSlotInfo(stub, config); + ASSERT_TRUE(result.has_value()); + + const auto* has_crl = GetParam(result.value(), 1U); + const auto* crl_next = GetParam(result.value(), 2U); + ASSERT_NE(has_crl, nullptr); + ASSERT_NE(crl_next, nullptr); + EXPECT_EQ(*has_crl, 0U); + EXPECT_EQ(*crl_next, 0U); +} + +TEST(SerializeCertSlotInfo, CrlPresent_Param1IsOne_Param2IsEpoch) +{ + SlotHandlerStub stub; + stub.has_crl_result = true; + stub.crl_next_update_value = 1700000000LL; + cert::CertSlotConfig config{}; + const auto result = query::SerializeCertSlotInfo(stub, config); + ASSERT_TRUE(result.has_value()); + + const auto* has_crl = GetParam(result.value(), 1U); + const auto* crl_next = GetParam(result.value(), 2U); + ASSERT_NE(has_crl, nullptr); + ASSERT_NE(crl_next, nullptr); + EXPECT_EQ(*has_crl, 1U); + EXPECT_EQ(*crl_next, static_cast(1700000000ULL)); +} + +TEST(SerializeCertSlotInfo, CrlPresent_GetCrlNextUpdateFails_Param2IsZero) +{ + // HasCrl() returns true but GetCrlNextUpdate() is unavailable. + // The serializer should encode has_crl=1 and crl_next=0 (not an error). + SlotHandlerStub stub; + stub.has_crl_result = true; + stub.crl_next_update_value = std::nullopt; // causes GetCrlNextUpdate to return error + cert::CertSlotConfig config{}; + const auto result = query::SerializeCertSlotInfo(stub, config); + ASSERT_TRUE(result.has_value()); + + const auto* has_crl = GetParam(result.value(), 1U); + const auto* crl_next = GetParam(result.value(), 2U); + ASSERT_NE(has_crl, nullptr); + ASSERT_NE(crl_next, nullptr); + EXPECT_EQ(*has_crl, 1U); + EXPECT_EQ(*crl_next, 0U); +} + +TEST(SerializeCertSlotInfo, GetSlotInfoError_PropagatesError) +{ + SlotHandlerStub stub; + stub.slot_info_result = + score::crypto::make_unexpected(score::crypto::daemon::common::DaemonErrorCode::kInternalError); + cert::CertSlotConfig config{}; + const auto result = query::SerializeCertSlotInfo(stub, config); + ASSERT_FALSE(result.has_value()); + EXPECT_EQ(result.error(), score::crypto::daemon::common::DaemonErrorCode::kInternalError); +} + +} // namespace diff --git a/score/crypto/src/daemon/cert_management/tests/service/test_cert_management_service.cpp b/score/crypto/src/daemon/cert_management/tests/service/test_cert_management_service.cpp index f308ed080..bfd5dc4ca 100644 --- a/score/crypto/src/daemon/cert_management/tests/service/test_cert_management_service.cpp +++ b/score/crypto/src/daemon/cert_management/tests/service/test_cert_management_service.cpp @@ -33,6 +33,7 @@ #include "score/crypto/src/daemon/cert_management/core/cert_management_service.hpp" #include "score/crypto/src/daemon/cert_management/slot/file_backed_slot_handler.hpp" +#include "score/crypto/src/daemon/cert_management/tests/test_environment.hpp" #include "score/crypto/src/daemon/cert_management/truststore/trust_store_manager.hpp" #include "score/crypto/src/daemon/common/storage/kv/kv_deployment_writer.hpp" #include "score/crypto/src/daemon/data_manager/data_manager.hpp" @@ -70,19 +71,22 @@ class CertManagementServiceTest : public ::testing::Test protected: void SetUp() override { - m_dir = std::filesystem::temp_directory_path() / "score_cert_mgmt_service"; + cert::test::ConfigureTestLogging(); + m_dir = cert::test::TempDirectory("score_cert_mgmt_service"); std::filesystem::remove_all(m_dir); std::filesystem::create_directories(m_dir); m_cert_path = m_dir / "root_ca.pem"; m_cert_updated_path = m_dir / "root_ca_updated.pem"; m_descriptor_path = m_dir / "root_ca.kv"; - ASSERT_TRUE(std::filesystem::copy_file("score/tests/test_vectors/certificate/certificate.pem", - m_cert_path, - std::filesystem::copy_options::overwrite_existing)); - ASSERT_TRUE(std::filesystem::copy_file("score/tests/test_vectors/certificate/certificate_updated.pem", - m_cert_updated_path, - std::filesystem::copy_options::overwrite_existing)); + ASSERT_TRUE(std::filesystem::copy_file( + cert::test::TestVectorPath("score/tests/test_vectors/certificate/certificate.pem"), + m_cert_path, + std::filesystem::copy_options::overwrite_existing)); + ASSERT_TRUE(std::filesystem::copy_file( + cert::test::TestVectorPath("score/tests/test_vectors/certificate/certificate_updated.pem"), + m_cert_updated_path, + std::filesystem::copy_options::overwrite_existing)); // Write the KV descriptor pointing at the initial cert. storage::DeploymentDescriptor desc; @@ -113,6 +117,8 @@ class CertManagementServiceTest : public ::testing::Test void TearDown() override { + static_cast(m_data_manager->deleteClientNodes(kClientA)); + static_cast(m_data_manager->deleteClientNodes(kClientB)); std::error_code ec; std::filesystem::remove_all(m_dir, ec); } diff --git a/score/crypto/src/daemon/cert_management/tests/slot/test_file_backed_slot_handler.cpp b/score/crypto/src/daemon/cert_management/tests/slot/test_file_backed_slot_handler.cpp index f33d33106..2f4f13131 100644 --- a/score/crypto/src/daemon/cert_management/tests/slot/test_file_backed_slot_handler.cpp +++ b/score/crypto/src/daemon/cert_management/tests/slot/test_file_backed_slot_handler.cpp @@ -12,6 +12,7 @@ ********************************************************************************/ #include "score/crypto/src/daemon/cert_management/slot/file_backed_slot_handler.hpp" +#include "score/crypto/src/daemon/cert_management/tests/test_environment.hpp" #include "score/crypto/src/daemon/common/storage/kv/kv_deployment_writer.hpp" #include "score/crypto/src/daemon/provider/score_provider/openssl/cert_management/openssl_cert_parser.hpp" @@ -63,7 +64,7 @@ class FileBackedSlotHandlerTest : public ::testing::Test protected: void SetUp() override { - m_directory = std::filesystem::temp_directory_path() / "score_cert_management_test"; + m_directory = cert::test::TempDirectory("score_cert_management_test"); std::filesystem::create_directories(m_directory); m_descriptor = m_directory / "slot.kv"; m_certificate = m_directory / "certificate.pem"; @@ -254,7 +255,7 @@ class FileBackedSlotHandlerRealParserTest : public ::testing::Test protected: void SetUp() override { - m_dir = std::filesystem::temp_directory_path() / "score_cert_slot_real"; + m_dir = cert::test::TempDirectory("score_cert_slot_real"); m_descriptor = m_dir / "slot.kv"; m_cert_file = m_dir / "cert.pem"; std::filesystem::remove_all(m_dir); @@ -327,4 +328,130 @@ TEST_F(FileBackedSlotHandlerRealParserTest, StoreThenLoad_SubjectAndIsCAMatch) EXPECT_EQ((*reloaded)->GetSubject(), (*initial)->GetSubject()); EXPECT_EQ((*reloaded)->IsCA(), (*initial)->IsCA()); } + +// --------------------------------------------------------------------------- +// Algorithm-variety parameterized tests +// +// Verify that OpenSslCertParser correctly parses every certificate in the +// test-vector suite: RSA (2048/3072/4096), EC (P-256/P-384/P-521), +// EdDSA (Ed25519/Ed448), and PQC (ML-DSA-44/65/87). +// +// Each test copies the PEM from the test-vector directory into a temp +// directory, sets up a FileBackedSlotHandler with a real OpenSslCertParser, +// and verifies common metadata invariants that must hold for every cert: +// - Subject matches the manifest's expected value +// - CA flag is set (all test-vector certs are self-signed CAs) +// - SKID is present and 20 bytes (SHA-1 key ID, algorithm-independent) +// - SHA-256 fingerprint is 32 bytes (algorithm-independent) +// --------------------------------------------------------------------------- + +struct AlgorithmVarietyParam +{ + const char* pem_path; // workspace-relative path to the PEM test vector + const char* expected_subject; // RFC 4514 DN as returned by GetSubject() +}; + +class FileBackedSlotHandlerAlgorithmVarietyTest : public ::testing::TestWithParam +{ + protected: + void SetUp() override + { + m_dir = cert::test::TempDirectory("score_cert_alg_variety"); + std::filesystem::remove_all(m_dir); + std::filesystem::create_directories(m_dir); + + const auto& p = GetParam(); + m_cert_file = m_dir / "cert.pem"; + ASSERT_TRUE( + std::filesystem::copy_file(p.pem_path, m_cert_file, std::filesystem::copy_options::overwrite_existing)) + << "Failed to copy test vector: " << p.pem_path; + + const auto descriptor_path = m_dir / "slot.kv"; + storage::DeploymentDescriptor descriptor; + descriptor.Set("certificate", "cert_path", m_cert_file.string()); + descriptor.Set("certificate", "cert_format", "pem"); + ASSERT_TRUE(storage::KvDeploymentWriter{}.Write(descriptor_path.string(), descriptor).has_value()); + + m_slot.deployment_path = descriptor_path.string(); + m_slot.deployment_format = "kv"; + + m_parser = std::make_shared(score::crypto::daemon::common::ProviderId{1U}); + m_handler = std::make_unique(m_parser); + } + + void TearDown() override + { + std::error_code ec; + std::filesystem::remove_all(m_dir, ec); + } + + std::filesystem::path m_dir; + std::filesystem::path m_cert_file; + cert::CertSlotConfig m_slot; + std::shared_ptr m_parser; + std::unique_ptr m_handler; +}; + +TEST_P(FileBackedSlotHandlerAlgorithmVarietyTest, LoadCertificate_MetadataIsCorrect) +{ + const auto& p = GetParam(); + const auto result = m_handler->LoadCertificate(m_slot); + ASSERT_TRUE(result.has_value()) << "LoadCertificate failed for: " << p.pem_path; + ASSERT_NE(*result, nullptr); + + const auto& c = **result; + EXPECT_EQ(c.GetSubject(), p.expected_subject); + EXPECT_TRUE(c.IsCA()); + // SKID is SHA-1(public key) — 20 bytes regardless of signature algorithm. + EXPECT_EQ(c.GetSkid().size(), 20U) << "Unexpected SKID size for: " << p.pem_path; + // SHA-256 fingerprint is always 32 bytes — independent of cert algorithm. + EXPECT_EQ(c.GetFingerprint().size(), 32U) << "Unexpected fingerprint size for: " << p.pem_path; +} + +// clang-format off +INSTANTIATE_TEST_SUITE_P( + AlgorithmVariety, + FileBackedSlotHandlerAlgorithmVarietyTest, + ::testing::Values( + // RSA + AlgorithmVarietyParam{"score/tests/test_vectors/certificate/certificate.pem", + "CN=cert-management-test,O=Eclipse"}, + AlgorithmVarietyParam{"score/tests/test_vectors/certificate/rsa_3072.pem", + "CN=cert-mgmt-rsa-3072,O=Eclipse"}, + AlgorithmVarietyParam{"score/tests/test_vectors/certificate/rsa_4096.pem", + "CN=cert-mgmt-rsa-4096,O=Eclipse"}, + // EC + AlgorithmVarietyParam{"score/tests/test_vectors/certificate/ec_p256.pem", + "CN=cert-mgmt-ec-p256,O=Eclipse"}, + AlgorithmVarietyParam{"score/tests/test_vectors/certificate/ec_p384.pem", + "CN=cert-mgmt-ec-p384,O=Eclipse"}, + AlgorithmVarietyParam{"score/tests/test_vectors/certificate/ec_p521.pem", + "CN=cert-mgmt-ec-p521,O=Eclipse"}, + // EdDSA + AlgorithmVarietyParam{"score/tests/test_vectors/certificate/ed25519.pem", + "CN=cert-mgmt-ed25519,O=Eclipse"}, + AlgorithmVarietyParam{"score/tests/test_vectors/certificate/ed448.pem", + "CN=cert-mgmt-ed448,O=Eclipse"}, + // PQC — ML-DSA (NIST FIPS 204) + AlgorithmVarietyParam{"score/tests/test_vectors/certificate/ml_dsa_44.pem", + "CN=cert-mgmt-ml-dsa-44,O=Eclipse"}, + AlgorithmVarietyParam{"score/tests/test_vectors/certificate/ml_dsa_65.pem", + "CN=cert-mgmt-ml-dsa-65,O=Eclipse"}, + AlgorithmVarietyParam{"score/tests/test_vectors/certificate/ml_dsa_87.pem", + "CN=cert-mgmt-ml-dsa-87,O=Eclipse"} + ), + [](const ::testing::TestParamInfo& info) { + // Build a test name from the PEM filename stem (e.g. "rsa_3072"). + std::string name = info.param.pem_path; + const auto slash = name.rfind('/'); + if (slash != std::string::npos) + name = name.substr(slash + 1U); + const auto dot = name.rfind('.'); + if (dot != std::string::npos) + name = name.substr(0U, dot); + return name; + } +); +// clang-format on + } // namespace diff --git a/score/crypto/src/daemon/cert_management/tests/test_environment.hpp b/score/crypto/src/daemon/cert_management/tests/test_environment.hpp new file mode 100644 index 000000000..53332afce --- /dev/null +++ b/score/crypto/src/daemon/cert_management/tests/test_environment.hpp @@ -0,0 +1,50 @@ +/******************************************************************************** + * 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 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0. + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ + +#ifndef SCORE_CRYPTO_SRC_DAEMON_CERT_MANAGEMENT_TESTS_TEST_ENVIRONMENT_HPP +#define SCORE_CRYPTO_SRC_DAEMON_CERT_MANAGEMENT_TESTS_TEST_ENVIRONMENT_HPP + +#include +#include +#include + +namespace score::crypto::daemon::cert_management::test +{ +inline std::filesystem::path TempDirectory(std::string_view name) +{ + const char* test_tmpdir = std::getenv("TEST_TMPDIR"); + const auto base = (test_tmpdir != nullptr && *test_tmpdir != '\0') ? std::filesystem::path{test_tmpdir} + : std::filesystem::path{"/tmp"}; + return base / name; +} + +inline std::filesystem::path TestVectorPath(std::string_view relative_path) +{ + const char* test_srcdir = std::getenv("TEST_SRCDIR"); + if (test_srcdir != nullptr && *test_srcdir != '\0') + { + const char* test_workspace = std::getenv("TEST_WORKSPACE"); + const auto workspace = (test_workspace != nullptr && *test_workspace != '\0') ? test_workspace : "_main"; + return std::filesystem::path{test_srcdir} / workspace / relative_path; + } + return std::filesystem::path{relative_path}; +} + +inline void ConfigureTestLogging() +{ + const auto config = TestVectorPath("score/tests/config/logging.json"); + static_cast(setenv("MW_LOG_CONFIG_FILE", config.c_str(), 1)); +} +} // namespace score::crypto::daemon::cert_management::test + +#endif // SCORE_CRYPTO_SRC_DAEMON_CERT_MANAGEMENT_TESTS_TEST_ENVIRONMENT_HPP diff --git a/score/crypto/src/daemon/cert_management/tests/truststore/test_trust_store_manager.cpp b/score/crypto/src/daemon/cert_management/tests/truststore/test_trust_store_manager.cpp index 8425ba65f..762dea142 100644 --- a/score/crypto/src/daemon/cert_management/tests/truststore/test_trust_store_manager.cpp +++ b/score/crypto/src/daemon/cert_management/tests/truststore/test_trust_store_manager.cpp @@ -31,6 +31,7 @@ // - AcknowledgeMemberUpdate — re-enables a disabled slot with fresh load #include "score/crypto/src/daemon/cert_management/slot/file_backed_slot_handler.hpp" +#include "score/crypto/src/daemon/cert_management/tests/test_environment.hpp" #include "score/crypto/src/daemon/cert_management/truststore/trust_store_manager.hpp" #include "score/crypto/src/daemon/common/storage/kv/kv_deployment_writer.hpp" #include "score/crypto/src/daemon/provider/score_provider/openssl/cert_management/openssl_cert_parser.hpp" @@ -78,7 +79,7 @@ class TrustStoreManagerTest : public ::testing::Test protected: void SetUp() override { - m_dir = std::filesystem::temp_directory_path() / "score_ts_mgr_test"; + m_dir = cert::test::TempDirectory("score_ts_mgr_test"); std::filesystem::remove_all(m_dir); std::filesystem::create_directories(m_dir); @@ -701,7 +702,7 @@ TEST_F(TrustStoreManagerTest, AddMember_DeduplicationChecksAllMemberTypes) TEST_F(TrustStoreManagerTest, ImportCrlForMember_WritesToExclusiveSlot) { auto registry = std::make_shared(); - static_cast(registry->RegisterSlot(MakeSlotConfig("empty-anchor", m_empty_slot_kv))); + const auto slot_handle = registry->RegisterSlot(MakeSlotConfig("empty-anchor", m_empty_slot_kv)); cert::TrustStoreConfig ts_cfg; ts_cfg.store_name = "mutable-store"; @@ -720,12 +721,11 @@ TEST_F(TrustStoreManagerTest, ImportCrlForMember_WritesToExclusiveSlot) // First add the cert without CRL. ASSERT_TRUE(manager.AddMember(ts_id, cert, kClientA).has_value()); - // Now import a CRL for that member by fingerprint. - const auto fp_span = cert->GetFingerprint(); + // Now import a CRL for that member by slot handle. const std::vector crl{0xD0U, 0xD1U, 0xD2U}; const auto crl_span = score::crypto::span{crl.data(), crl.size()}; - ASSERT_TRUE(manager.ImportCrlForMember(ts_id, fp_span, crl_span, score::crypto::FormatType::kDer).has_value()); + ASSERT_TRUE(manager.ImportCrlForMember(ts_id, slot_handle, crl_span, score::crypto::FormatType::kDer).has_value()); // Verify persistence: fresh handler must read the CRL. cert::FileBackedSlotHandler fresh_handler{m_parser}; diff --git a/score/crypto/src/daemon/cert_management/truststore/trust_store_manager.cpp b/score/crypto/src/daemon/cert_management/truststore/trust_store_manager.cpp index 1c2d2684a..700df34b1 100644 --- a/score/crypto/src/daemon/cert_management/truststore/trust_store_manager.cpp +++ b/score/crypto/src/daemon/cert_management/truststore/trust_store_manager.cpp @@ -522,29 +522,23 @@ score::crypto::Expected TrustStoreManager::AddMember( score::crypto::Expected TrustStoreManager::ImportCrlForMember( TrustStoreId id, - score::crypto::span fingerprint, + CertSlotHandle slot, score::crypto::span crl_data, score::crypto::FormatType format) { std::lock_guard lock(m_mutex); - if (id >= m_stores.size()) + if (id >= m_stores.size() || !slot.IsValid()) return score::crypto::make_unexpected(Error::kInvalidResourceId); - if (fingerprint.size() != 32U || crl_data.empty()) + if (crl_data.empty()) return score::crypto::make_unexpected(Error::kInvalidArgument); for (const auto& member : m_stores[id].config.members) { - if (member.kind != TrustStoreMemberKind::kExclusiveMutable) - continue; const auto resolved = ResolveMember(member); - if (!resolved) - continue; - const auto current = resolved->handler->LoadCertificate(*resolved->cfg); - if (!current) - continue; - const auto& fp = (*current)->GetFingerprint(); - if (fp.size() != fingerprint.size() || !std::equal(fp.begin(), fp.end(), fingerprint.begin())) + if (!resolved || resolved->slot.index != slot.index) continue; + if (member.kind != TrustStoreMemberKind::kExclusiveMutable) + return score::crypto::make_unexpected(Error::kUnsupportedOperation); return resolved->handler->StoreCrl(*resolved->cfg, crl_data, format); } return score::crypto::make_unexpected(Error::kInvalidResourceId); @@ -652,4 +646,55 @@ const TrustStoreConfig* TrustStoreManager::GetStoreConfig(TrustStoreId id) const return id < m_stores.size() ? &m_stores[id].config : nullptr; } +// --------------------------------------------------------------------------- +// Member snapshot (for ITrustStoreObject read-only view) +// --------------------------------------------------------------------------- + +std::vector TrustStoreManager::GetMembersSnapshot(TrustStoreId id) +{ + std::lock_guard lock(m_mutex); + if (id >= m_stores.size() || !m_slot_registry) + return {}; + + std::vector result; + const auto& store = m_stores[id]; + + for (const auto& member : store.config.members) + { + const auto slot = m_slot_registry->ResolveSlotInternal(member.slot_name); + if (!slot) + continue; + + auto cert = LoadOrGetCached(*slot); + if (!cert) + continue; // slot is empty — omit from snapshot + + MemberSnapshot snap; + snap.slot_handle = *slot; + snap.slot_name = member.slot_name; + snap.kind = member.kind; + + const auto& meta = cert->GetChainMetadata(); + snap.subject = meta.subject_canonical; + snap.issuer = meta.issuer_canonical; + snap.serial_number = meta.serial_number_hex; + + const auto fp = cert->GetFingerprint(); + if (fp.size() == 32U) + std::copy(fp.begin(), fp.end(), snap.fingerprint.begin()); + + // Default enabled=true; override from persisted state if present. + const auto states_it = m_member_states.find(id); + if (states_it != m_member_states.end()) + { + const auto state_it = states_it->second.find(slot->index); + if (state_it != states_it->second.end()) + snap.is_enabled = state_it->second.enabled; + } + + result.push_back(std::move(snap)); + } + return result; +} + } // namespace score::crypto::daemon::cert_management diff --git a/score/crypto/src/daemon/cert_management/truststore/trust_store_manager.hpp b/score/crypto/src/daemon/cert_management/truststore/trust_store_manager.hpp index 9282fc0c1..401db26c3 100644 --- a/score/crypto/src/daemon/cert_management/truststore/trust_store_manager.hpp +++ b/score/crypto/src/daemon/cert_management/truststore/trust_store_manager.hpp @@ -170,17 +170,17 @@ class TrustStoreManager score::crypto::span crl_bytes = {}, score::crypto::FormatType crl_format = score::crypto::FormatType::kDer); - /// @brief Import a CRL to the exclusive trust store slot holding the cert - /// whose SHA-256 fingerprint matches @p fingerprint. + /// @brief Import a CRL to the exclusive trust store slot identified by @p slot. /// /// Only operates on kExclusiveMutable members — the trust store owns these. /// Shared-static and conditional-external slots are externally managed; /// callers use CertManagementService::ImportCrlToSlot directly for those. /// - /// Returns kInvalidResourceId if no exclusive member fingerprint matches. + /// Returns kInvalidResourceId if @p slot is not a member of the trust store. + /// Returns kUnsupportedOperation if the slot is not kExclusiveMutable. [[nodiscard]] score::crypto::Expected ImportCrlForMember(TrustStoreId id, - score::crypto::span fingerprint, + CertSlotHandle slot, score::crypto::span crl_data, score::crypto::FormatType format); @@ -198,6 +198,32 @@ class TrustStoreManager [[nodiscard]] score::crypto::Expected AcknowledgeMemberUpdate(TrustStoreId id, CertSlotHandle slot, data_manager::ClientId client_id); + // ----------------------------------------------------------------------- + // Snapshot for read-only typed object access (ITrustStoreObject) + // ----------------------------------------------------------------------- + + /// @brief Point-in-time snapshot of a trust store's member list. + /// + /// Used by the cert management executor to populate the TRUST_STORE_GET_INFO response + /// consumed by ITrustStoreObject on the lib side. Slots with no cert present are omitted. + struct MemberSnapshot + { + CertSlotHandle slot_handle{0U}; ///< Daemon-internal slot index (for enable/disable routing). + std::string slot_name; ///< Slot name (used by executor to call ResolveCertSlot). + std::array fingerprint{}; ///< SHA-256 fingerprint of the member certificate. + std::string subject; ///< RFC 4514 Subject DN. + std::string issuer; ///< RFC 4514 Issuer DN. + std::string serial_number; ///< Uppercase hex serial number. + TrustStoreMemberKind kind{TrustStoreMemberKind::kSharedStatic}; + bool is_enabled{true}; + }; + + /// @brief Load and return a snapshot of all occupied member slots for trust store @p id. + /// + /// Certs are loaded via the shared cache where possible; fresh loads are taken for + /// uncached slots. Non-const because it may populate handler and cert caches. + [[nodiscard]] std::vector GetMembersSnapshot(TrustStoreId id); + // ----------------------------------------------------------------------- // Configuration access // ----------------------------------------------------------------------- diff --git a/score/crypto/src/daemon/provider/cert_management/cert_management_operations.hpp b/score/crypto/src/daemon/provider/cert_management/cert_management_operations.hpp index 41d8aae88..94f47107a 100644 --- a/score/crypto/src/daemon/provider/cert_management/cert_management_operations.hpp +++ b/score/crypto/src/daemon/provider/cert_management/cert_management_operations.hpp @@ -44,6 +44,10 @@ inline constexpr OperationAction TRUST_STORE_DISABLE_CERT = 0xC3U; inline constexpr OperationAction TRUST_STORE_ACK_UPDATE = 0xC4U; inline constexpr OperationAction TRUST_STORE_REMOVE_CERT_BY_ID = 0xC5U; // remove by cert node_id (lib resolves fp) inline constexpr OperationAction TRUST_STORE_IMPORT_CRL_FOR_MEMBER = 0xC6U; // fingerprint + CRL bytes → exclusive slot +inline constexpr OperationAction TRUST_STORE_GET_INFO = 0xC7U; // read-only snapshot: member list + state +// Enable/disable now fingerprint-based at the IPC layer (no slot_node_id round-trip needed from lib side): +// TRUST_STORE_ENABLE_CERT (0xC2): [0]=ts_node_id, [1]=fingerprint_bytes +// TRUST_STORE_DISABLE_CERT (0xC3): [0]=ts_node_id, [1]=fingerprint_bytes inline constexpr OperationAction CERT_RELEASE = 0xF0U; diff --git a/score/crypto/src/daemon/provider/score_provider/openssl/cert_management/openssl_cert_parser.cpp b/score/crypto/src/daemon/provider/score_provider/openssl/cert_management/openssl_cert_parser.cpp index c377411d6..23e98190b 100644 --- a/score/crypto/src/daemon/provider/score_provider/openssl/cert_management/openssl_cert_parser.cpp +++ b/score/crypto/src/daemon/provider/score_provider/openssl/cert_management/openssl_cert_parser.cpp @@ -99,6 +99,26 @@ score::crypto::Expected BuildObject(X509* certificate, return score::crypto::make_unexpected(Error::kCertificateParsingFailed); } + // Extract serial number as uppercase hex string (e.g., "01ABCDEF"). + // (issuer, serial) is the RFC 5280 canonical certificate identifier. + { + const ASN1_INTEGER* serial = X509_get_serialNumber(certificate); + if (serial != nullptr) + { + BIGNUM* bn = ASN1_INTEGER_to_BN(serial, nullptr); + if (bn != nullptr) + { + char* hex = BN_bn2hex(bn); + if (hex != nullptr) + { + metadata.serial_number_hex = hex; + OPENSSL_free(hex); + } + BN_free(bn); + } + } + } + ASN1_OCTET_STRING* skid = static_cast(X509_get_ext_d2i(certificate, NID_subject_key_identifier, nullptr, nullptr)); if (skid != nullptr) diff --git a/score/tests/test_vectors/certificate/README.md b/score/tests/test_vectors/certificate/README.md index ff5f88f00..6d384f23e 100644 --- a/score/tests/test_vectors/certificate/README.md +++ b/score/tests/test_vectors/certificate/README.md @@ -13,24 +13,82 @@ # Certificate Test Vectors -`certificate_manifest.json` is the inventory for certificate-management test certificates. It records each certificate's purpose, subject, private-key location, validity policy, and the metadata snapshot of the checked-in public certificate. +`certificate_manifest.json` is the inventory for certificate-management test +certificates. It records each certificate's purpose, subject, key algorithm, +private-key location, validity policy, and the metadata snapshot of the +checked-in public certificate. -Private keys are generated locally under `private/` and are ignored by Git. Never commit them. The checked-in certificates use a long validity period because their private keys are intentionally not stored in the public repository. +Private keys are generated locally under `private/` and are ignored by Git. +Never commit them. The checked-in certificates use a long validity period +because their private keys are intentionally not stored in the public repository. + +## Certificate inventory + +| Name | Algorithm | Purpose | +|------|-----------|---------| +| `certificate` | RSA-2048 | Primary CA cert for file-backed slot and trust-store tests | +| `certificate_updated` | RSA-2048 | Replacement CA cert for slot-update and anchor-invalidation tests | +| `rsa_3072` | RSA-3072 | RSA key-size variety | +| `rsa_4096` | RSA-4096 | RSA key-size variety | +| `ec_p256` | EC P-256 | ECDSA elliptic-curve algorithm tests | +| `ec_p384` | EC P-384 | ECDSA elliptic-curve algorithm tests | +| `ec_p521` | EC P-521 | ECDSA elliptic-curve algorithm tests | +| `ed25519` | Ed25519 | Edwards-curve algorithm tests | +| `ed448` | Ed448 | Edwards-curve algorithm tests | +| `ml_dsa_44` | ML-DSA-44 | Post-quantum tests (NIST FIPS 204, security level 2) | +| `ml_dsa_65` | ML-DSA-65 | Post-quantum tests (NIST FIPS 204, security level 3) | +| `ml_dsa_87` | ML-DSA-87 | Post-quantum tests (NIST FIPS 204, security level 5) | + +All PEM files carry a human-readable header (produced by `openssl x509 -text`) +before the `-----BEGIN CERTIFICATE-----` block. This header is ignored by all +conforming PEM parsers and is present only for readability. + +## Supported `key_algorithm` values + +The `generate_certificates.py` script accepts the following values for the +`key_algorithm` field in the manifest: + +| Family | Values | +|--------|--------| +| RSA | `RSA-2048`, `RSA-3072`, `RSA-4096` (any `RSA-`) | +| EC | `EC-P256`, `EC-P384`, `EC-P521` | +| EdDSA | `Ed25519`, `Ed448` | +| PQC | `ML-DSA-44`, `ML-DSA-65`, `ML-DSA-87` | + +PQC support requires OpenSSL ≥ 3.5 (ML-DSA is in the OpenSSL default provider). + +## Generating and refreshing certificates From the repository root, generate a certificate and its local key with: ```sh -python3 score/tests/test_vectors/certificate/generate_certificates.py generate certificate +python3 score/tests/test_vectors/certificate/generate_certificates.py generate +``` + +For example: + +```sh +python3 score/tests/test_vectors/certificate/generate_certificates.py generate ec_p256 +python3 score/tests/test_vectors/certificate/generate_certificates.py generate ml_dsa_65 ``` -When a public snapshot needs renewal, run `generate` again. This creates a new local private key and a new self-signed public certificate, then refreshes the manifest snapshot. Review and commit the public PEM and manifest together. +Running `generate` creates a new local private key and a new self-signed public +certificate, then refreshes the manifest snapshot. Review and commit the public +PEM, the corresponding `_slot.kv` descriptor, and the manifest together. -Only use `update` when the corresponding local private key is available and preserving the existing key is intentional: +Only use `update` when the corresponding local private key is available and +preserving the existing key is intentional: ```sh -python3 score/tests/test_vectors/certificate/generate_certificates.py update certificate +python3 score/tests/test_vectors/certificate/generate_certificates.py update ``` -Use `certificate_updated` for the replacement snapshot used by the anchor-invalidation test. To add another certificate, add an entry to `certificate_manifest.json`, then run the `generate` command with its `name`. The script updates the certificate's `snapshot` from OpenSSL after generation. Do not make tests depend on a private key being present. +Do not make tests depend on a private key being present. + +## Adding a new certificate -The generated public certificate and manifest changes should be reviewed together. The KV descriptor templates in this directory describe the storage layouts used by the certificate-management integration tests. +1. Add an entry to `certificate_manifest.json` with `name`, `certificate_path`, + `private_key_path`, `purpose`, `subject`, `key_algorithm`, and `validity_days`. +2. Run `generate_certificates.py generate `. The script writes the PEM, + the `_slot.kv` KV descriptor, and updates the manifest snapshot. +3. Commit the PEM, the KV descriptor, and the updated manifest together. diff --git a/score/tests/test_vectors/certificate/certificate.pem b/score/tests/test_vectors/certificate/certificate.pem index 3626b3a0a..26aaf5cb3 100644 --- a/score/tests/test_vectors/certificate/certificate.pem +++ b/score/tests/test_vectors/certificate/certificate.pem @@ -1,3 +1,59 @@ +Certificate: + Data: + Version: 3 (0x2) + Serial Number: + 53:88:fe:dc:53:6a:ef:af:c8:56:13:66:19:74:7e:cb:62:e6:14:3e + Signature Algorithm: sha256WithRSAEncryption + Issuer: CN=cert-management-test, O=Eclipse + Validity + Not Before: Aug 24 22:20:18 2026 GMT + Not After : Aug 21 22:20:18 2036 GMT + Subject: CN=cert-management-test, O=Eclipse + Subject Public Key Info: + Public Key Algorithm: rsaEncryption + Public-Key: (2048 bit) + Modulus: + 00:bc:af:64:2b:ad:a8:d7:7d:d6:16:89:51:f4:d7: + 57:56:71:2e:98:4d:c0:7c:48:38:39:1e:99:41:af: + 07:e9:75:d5:3b:ae:f1:4b:af:18:5c:17:07:df:42: + df:db:8a:a9:c0:fb:86:f6:3f:88:18:f8:77:51:7f: + 9f:b7:c0:09:03:88:f8:6a:1b:67:4b:91:88:bd:f6: + 6d:6e:66:43:b7:7d:88:0c:f4:ea:e8:c3:26:d3:5e: + 81:ac:39:3f:df:a7:55:59:e1:d8:b3:90:20:80:00: + 18:89:85:16:7a:af:17:4f:c7:12:23:98:33:d5:a9: + 90:76:f8:59:32:6b:53:8e:88:a4:23:92:96:5d:af: + 68:e8:cb:a5:bc:2c:9c:74:e6:24:b0:72:5e:40:09: + 52:a4:04:dc:f6:28:0b:50:28:57:ec:55:c2:39:58: + b1:34:85:5e:ef:02:d0:58:af:e4:af:2d:16:c6:3f: + cf:31:60:ce:81:db:91:4a:00:ce:4e:87:08:62:2f: + 8b:2e:38:e5:a1:f7:99:b3:64:fb:6e:d1:5f:25:0a: + c5:6a:6e:a0:55:2c:29:c9:ef:87:d1:f9:74:a8:ca: + 99:1f:2e:aa:6c:96:42:70:50:16:15:36:86:29:ee: + 8f:37:9f:cb:75:37:0d:d4:ad:b2:9a:b3:8d:5b:49: + 66:df + Exponent: 65537 (0x10001) + X509v3 extensions: + X509v3 Basic Constraints: critical + CA:TRUE + X509v3 Subject Key Identifier: + 91:1D:AF:FF:11:8D:65:7F:26:8C:4B:D3:96:C6:B1:1A:63:8A:35:78 + Signature Algorithm: sha256WithRSAEncryption + Signature Value: + 77:c9:e6:2e:85:fb:a2:e1:43:32:65:de:3e:04:03:f9:f4:01: + a3:f3:fd:0c:b0:a9:cc:07:33:f3:1d:3e:aa:d8:db:cd:02:a8: + 64:5d:92:69:0f:13:f2:5a:6c:e1:25:7b:a1:24:06:71:e1:6d: + 34:dd:08:f0:21:67:6d:4d:54:2d:f8:8c:04:54:a6:0e:2a:85: + 0d:ac:4e:b7:55:c7:0d:dc:89:b2:e7:96:b0:ef:3a:33:39:bf: + 5d:c3:db:e2:bc:5f:81:56:5f:fe:13:2c:df:57:73:9f:94:f5: + b5:71:38:af:50:62:3e:6f:70:6f:16:d1:30:85:4b:69:58:83: + dc:9e:eb:98:25:66:99:4e:5e:61:10:af:31:71:9e:06:2d:f6: + 20:4c:1b:6d:c2:2d:55:bc:f8:7b:8b:e9:1f:08:f7:ad:36:69: + d5:71:29:9c:ca:11:7c:a1:90:a0:f7:68:c4:c0:b5:d0:a0:72: + ce:40:5e:f9:2c:f7:22:ca:23:96:e4:f6:c2:eb:9d:aa:a4:5e: + d6:d6:ed:41:b2:d4:91:64:e0:2e:76:6c:b9:ec:f6:a6:6c:d9: + 51:5f:77:17:64:47:33:32:a7:5c:45:bf:c3:27:3e:f3:60:8c: + d1:9b:91:a5:f4:ce:d1:f0:f6:6a:77:b4:e0:93:de:8b:75:2a: + 2e:a4:62:06 -----BEGIN CERTIFICATE----- MIIDIjCCAgqgAwIBAgIUU4j+3FNq76/IVhNmGXR+y2LmFD4wDQYJKoZIhvcNAQEL BQAwMTEdMBsGA1UEAwwUY2VydC1tYW5hZ2VtZW50LXRlc3QxEDAOBgNVBAoMB0Vj diff --git a/score/tests/test_vectors/certificate/certificate_manifest.json b/score/tests/test_vectors/certificate/certificate_manifest.json index 249afb964..15c594494 100644 --- a/score/tests/test_vectors/certificate/certificate_manifest.json +++ b/score/tests/test_vectors/certificate/certificate_manifest.json @@ -1,16 +1,18 @@ { - "schema_version": 1, + "schema_version": 2, "description": "Certificate-management test certificate inventory. Public certificates are checked in; private keys are generated locally and are not committed.", + "defaults": { + "cert_dir": ".", + "key_dir": "private", + "validity_days": 3650 + }, "certificates": [ { "name": "certificate", - "certificate_path": "certificate.pem", - "private_key_path": "private/certificate.key.pem", "purpose": "Initial CA certificate for file-backed slot and trust-store tests", "subject": "/CN=cert-management-test/O=Eclipse", "key_algorithm": "RSA-2048", - "validity_days": 3650, - "snapshot": { + "generated": { "not_before": "2026-08-24T22:20:18Z", "not_after": "2036-08-21T22:20:18Z", "sha256_fingerprint": "3E7A581BCAAB80F73535B10968F8C3E68517194B6A404CE5F58AACABE0A62D0C" @@ -18,17 +20,124 @@ }, { "name": "certificate_updated", - "certificate_path": "certificate_updated.pem", - "private_key_path": "private/certificate_updated.key.pem", "purpose": "Replacement CA certificate used to test slot updates and trust-store anchor invalidation", "subject": "/CN=cert-management-updated/O=Eclipse", "key_algorithm": "RSA-2048", - "validity_days": 3650, - "snapshot": { + "generated": { "not_before": "2026-08-24T22:20:18Z", "not_after": "2036-08-21T22:20:18Z", "sha256_fingerprint": "2324A755EB36B44DF2495504AF85A8ED3FE89A8328F01788905DB3A0E1EAED7E" } + }, + { + "name": "rsa_3072", + "purpose": "RSA-3072 CA certificate for key-size variety tests", + "subject": "/CN=cert-mgmt-rsa-3072/O=Eclipse", + "key_algorithm": "RSA-3072", + "generated": { + "not_before": "2026-08-28T07:38:42Z", + "not_after": "2036-08-25T07:38:42Z", + "sha256_fingerprint": "C7AD557FD1C810DE4AF562082B182BB0B6E3D31CC9C368D0309A083B1019E67A" + } + }, + { + "name": "rsa_4096", + "purpose": "RSA-4096 CA certificate for key-size variety tests", + "subject": "/CN=cert-mgmt-rsa-4096/O=Eclipse", + "key_algorithm": "RSA-4096", + "generated": { + "not_before": "2026-08-28T07:39:03Z", + "not_after": "2036-08-25T07:39:03Z", + "sha256_fingerprint": "FEF07C92839DA3601FB8675855027D114410FF25BF38725CFAAB4816E1D9961F" + } + }, + { + "name": "ec_p256", + "purpose": "ECDSA P-256 CA certificate for elliptic-curve algorithm tests", + "subject": "/CN=cert-mgmt-ec-p256/O=Eclipse", + "key_algorithm": "EC-P256", + "generated": { + "not_before": "2026-08-28T07:39:04Z", + "not_after": "2036-08-25T07:39:04Z", + "sha256_fingerprint": "518B26F75E5FF9F8AD885CD9009B32088A6568F9801A4573A08646D8CA4B9D7E" + } + }, + { + "name": "ec_p384", + "purpose": "ECDSA P-384 CA certificate for elliptic-curve algorithm tests", + "subject": "/CN=cert-mgmt-ec-p384/O=Eclipse", + "key_algorithm": "EC-P384", + "generated": { + "not_before": "2026-08-28T07:39:04Z", + "not_after": "2036-08-25T07:39:04Z", + "sha256_fingerprint": "A3DDF05A72139BC6C1651AD5FFEE9A6B9267171D29D9BFCE8BB40277147B97DF" + } + }, + { + "name": "ec_p521", + "purpose": "ECDSA P-521 CA certificate for elliptic-curve algorithm tests", + "subject": "/CN=cert-mgmt-ec-p521/O=Eclipse", + "key_algorithm": "EC-P521", + "generated": { + "not_before": "2026-08-28T07:39:04Z", + "not_after": "2036-08-25T07:39:04Z", + "sha256_fingerprint": "27A9B1D95C7105C8501BEAAA5AEF4DAC12E9D530EA5179483384A43862C9707F" + } + }, + { + "name": "ed25519", + "purpose": "Ed25519 CA certificate for Edwards-curve algorithm tests", + "subject": "/CN=cert-mgmt-ed25519/O=Eclipse", + "key_algorithm": "Ed25519", + "generated": { + "not_before": "2026-08-28T07:39:05Z", + "not_after": "2036-08-25T07:39:05Z", + "sha256_fingerprint": "A65D474FFEB3868BFB0D3930AB3B75120FD1BB37C2EB7775F0A76F437056F4A8" + } + }, + { + "name": "ed448", + "purpose": "Ed448 CA certificate for Edwards-curve algorithm tests", + "subject": "/CN=cert-mgmt-ed448/O=Eclipse", + "key_algorithm": "Ed448", + "generated": { + "not_before": "2026-08-28T07:39:05Z", + "not_after": "2036-08-25T07:39:05Z", + "sha256_fingerprint": "604EEBDC8B3ABE10D0D61C9D00BB4461AFA4B0B6C4D9F00934C36CA92F4CA2DF" + } + }, + { + "name": "ml_dsa_44", + "purpose": "ML-DSA-44 CA certificate for post-quantum algorithm tests (NIST FIPS 204 security level 2)", + "subject": "/CN=cert-mgmt-ml-dsa-44/O=Eclipse", + "key_algorithm": "ML-DSA-44", + "generated": { + "not_before": "2026-08-28T07:39:05Z", + "not_after": "2036-08-25T07:39:05Z", + "sha256_fingerprint": "3486F8E8FB941936B5C2E900D5FC24080026D1885AB18E944EDE404C1FDD1908" + } + }, + { + "name": "ml_dsa_65", + "purpose": "ML-DSA-65 CA certificate for post-quantum algorithm tests (NIST FIPS 204 security level 3)", + "subject": "/CN=cert-mgmt-ml-dsa-65/O=Eclipse", + "key_algorithm": "ML-DSA-65", + "generated": { + "not_before": "2026-08-28T07:39:21Z", + "not_after": "2036-08-25T07:39:21Z", + "sha256_fingerprint": "151B8ED50DCDEDF92D67A9ABAB39ABAD113A8DFB59C50E8AB8E415DD4E0C1E5E" + } + }, + { + "name": "ml_dsa_87", + "purpose": "ML-DSA-87 CA certificate for post-quantum algorithm tests (NIST FIPS 204 security level 5)", + "subject": "/CN=cert-mgmt-ml-dsa-87/O=Eclipse", + "key_algorithm": "ML-DSA-87", + "generated": { + "not_before": "2026-08-28T07:39:06Z", + "not_after": "2036-08-25T07:39:06Z", + "sha256_fingerprint": "B59C6AEECDE777AC81A8D32701A92DFF54EFDEDC8D8BC9DD8F5187D76269A2B6" + } } ] } diff --git a/score/tests/test_vectors/certificate/certificate_updated.pem b/score/tests/test_vectors/certificate/certificate_updated.pem index 8d6664e9f..721d8ab95 100644 --- a/score/tests/test_vectors/certificate/certificate_updated.pem +++ b/score/tests/test_vectors/certificate/certificate_updated.pem @@ -1,3 +1,59 @@ +Certificate: + Data: + Version: 3 (0x2) + Serial Number: + 13:fd:12:44:67:ec:e5:45:95:0a:79:1b:4b:85:e3:47:b5:f0:2c:2c + Signature Algorithm: sha256WithRSAEncryption + Issuer: CN=cert-management-updated, O=Eclipse + Validity + Not Before: Aug 24 22:20:18 2026 GMT + Not After : Aug 21 22:20:18 2036 GMT + Subject: CN=cert-management-updated, O=Eclipse + Subject Public Key Info: + Public Key Algorithm: rsaEncryption + Public-Key: (2048 bit) + Modulus: + 00:bc:fc:1f:1f:2d:45:c6:3b:a0:62:53:69:b4:b5: + de:02:90:ec:6d:de:6f:bd:6c:64:70:5a:d9:80:fd: + ef:e0:fd:90:0e:65:cb:b9:a3:74:8f:9d:04:ee:85: + df:52:77:01:2f:0a:85:b7:e1:1c:b5:37:d4:83:45: + 61:19:cc:c0:e7:93:25:4d:cc:b0:88:be:48:64:6c: + 08:87:a3:55:69:3f:b6:3b:52:f8:c0:9f:cf:6f:c0: + da:df:71:18:fe:77:d2:22:cd:1b:1b:91:06:46:d3: + 9d:1b:4d:d3:81:f6:9f:48:d3:0e:ff:0f:f0:99:62: + cb:73:18:9b:b0:b9:0e:fc:2b:0f:5d:ca:d3:2f:75: + 11:5a:8e:d0:ef:42:e7:d2:ca:7e:02:b3:3b:3c:71: + 5b:0c:0d:74:2e:20:0b:d3:e3:8d:3b:0a:e4:cb:ee: + ea:36:e6:3b:60:29:bb:b1:ae:81:96:4c:e6:c8:21: + 62:8d:c3:4c:88:2e:09:3e:bf:17:67:8a:1a:59:c1: + 6f:28:27:24:af:0b:be:50:02:3b:24:cc:e3:de:40: + fe:f2:72:05:02:05:11:95:89:02:fe:57:68:9e:81: + 81:c9:03:5f:6e:8a:a8:c7:8e:f9:d7:61:be:7b:39: + af:db:2a:c7:49:2b:35:d9:63:73:b7:2a:a6:f5:f1: + f0:41 + Exponent: 65537 (0x10001) + X509v3 extensions: + X509v3 Basic Constraints: critical + CA:TRUE + X509v3 Subject Key Identifier: + 0A:85:6E:FA:12:E0:BE:87:78:3F:EB:4C:58:E1:69:20:23:1D:A0:7B + Signature Algorithm: sha256WithRSAEncryption + Signature Value: + 38:3d:a1:bf:f8:91:c7:25:52:03:d6:06:a9:8b:be:68:cd:e0: + 0d:e7:62:e4:cd:63:10:5d:84:4b:af:e0:d7:57:bb:be:df:2a: + 0d:7f:c8:2a:fd:ce:4b:d5:b6:7b:df:05:07:b8:bd:84:cc:5d: + ae:9b:25:5a:01:4f:85:ae:7a:f6:c3:f3:ac:27:aa:20:8b:a3: + a7:55:e3:28:ed:56:13:87:29:d3:62:a1:e5:83:51:3f:9c:80: + 07:39:a4:c6:77:70:4d:bc:54:b5:16:ce:01:54:74:73:7f:3b: + 2d:4a:5f:96:e4:bf:ac:39:06:d2:93:2c:d7:36:d2:30:50:82: + 0d:0c:3b:e7:65:65:51:57:fb:8f:d0:00:88:47:b2:6d:32:37: + 65:a3:f5:2e:c8:b3:32:ae:63:e0:d8:b0:9f:c1:88:12:22:5b: + 46:67:ba:f0:8e:0b:f8:4d:30:34:ce:0d:dc:91:20:5c:de:ed: + a3:61:69:af:e7:19:ac:46:66:4c:d1:09:4c:e2:99:0b:a1:84: + 51:2d:05:31:28:1b:39:8c:d2:d5:98:cc:70:33:ee:40:0a:f6: + 54:1e:04:57:eb:de:e6:20:c7:d9:d9:6a:9b:8f:e1:a0:45:e6: + 9e:6b:84:cf:2f:19:28:8b:fc:71:0e:e1:9c:7e:64:67:b2:62: + d0:48:a5:f4 -----BEGIN CERTIFICATE----- MIIDKDCCAhCgAwIBAgIUE/0SRGfs5UWVCnkbS4XjR7XwLCwwDQYJKoZIhvcNAQEL BQAwNDEgMB4GA1UEAwwXY2VydC1tYW5hZ2VtZW50LXVwZGF0ZWQxEDAOBgNVBAoM diff --git a/score/tests/test_vectors/certificate/ec_p256.pem b/score/tests/test_vectors/certificate/ec_p256.pem new file mode 100644 index 000000000..a18702ca4 --- /dev/null +++ b/score/tests/test_vectors/certificate/ec_p256.pem @@ -0,0 +1,44 @@ +Certificate: + Data: + Version: 3 (0x2) + Serial Number: + 3b:88:e7:57:ad:19:37:d7:5c:47:21:d9:9a:39:8d:0b:d4:33:2c:14 + Signature Algorithm: ecdsa-with-SHA256 + Issuer: CN=cert-mgmt-ec-p256, O=Eclipse + Validity + Not Before: Aug 28 07:39:04 2026 GMT + Not After : Aug 25 07:39:04 2036 GMT + Subject: CN=cert-mgmt-ec-p256, O=Eclipse + Subject Public Key Info: + Public Key Algorithm: id-ecPublicKey + Public-Key: (256 bit) + pub: + 04:3d:94:5c:f7:90:f0:63:2e:33:c7:35:e9:e2:20: + 42:79:6e:56:b1:0f:9b:ad:95:d8:6d:86:fe:ff:c9: + 7f:f0:a2:78:85:6c:c2:61:9b:b6:32:d6:4b:d9:fa: + c8:6d:27:fb:04:f3:a1:18:94:83:0c:c9:1e:95:14: + 11:5e:96:49:f9 + ASN1 OID: prime256v1 + NIST CURVE: P-256 + X509v3 extensions: + X509v3 Basic Constraints: critical + CA:TRUE + X509v3 Subject Key Identifier: + D3:91:84:7C:96:E4:A3:97:EC:31:C2:56:38:3D:EA:1A:89:DC:B1:77 + Signature Algorithm: ecdsa-with-SHA256 + Signature Value: + 30:45:02:21:00:bd:59:c5:ac:ce:f2:c0:cc:a2:b0:31:b7:93: + 93:76:93:ec:8f:45:91:e5:69:a1:0d:58:db:40:b0:bf:ca:76: + 5f:02:20:79:79:0d:ab:88:83:a5:d9:71:e8:81:d9:f4:95:b5: + fd:52:70:22:ac:7a:c7:10:66:68:1c:58:76:ed:d8:dd:86 +-----BEGIN CERTIFICATE----- +MIIBkDCCATagAwIBAgIUO4jnV60ZN9dcRyHZmjmNC9QzLBQwCgYIKoZIzj0EAwIw +LjEaMBgGA1UEAwwRY2VydC1tZ210LWVjLXAyNTYxEDAOBgNVBAoMB0VjbGlwc2Uw +HhcNMjYwODI4MDczOTA0WhcNMzYwODI1MDczOTA0WjAuMRowGAYDVQQDDBFjZXJ0 +LW1nbXQtZWMtcDI1NjEQMA4GA1UECgwHRWNsaXBzZTBZMBMGByqGSM49AgEGCCqG +SM49AwEHA0IABD2UXPeQ8GMuM8c16eIgQnluVrEPm62V2G2G/v/Jf/CieIVswmGb +tjLWS9n6yG0n+wTzoRiUgwzJHpUUEV6WSfmjMjAwMA8GA1UdEwEB/wQFMAMBAf8w +HQYDVR0OBBYEFNORhHyW5KOX7DHCVjg96hqJ3LF3MAoGCCqGSM49BAMCA0gAMEUC +IQC9WcWszvLAzKKwMbeTk3aT7I9FkeVpoQ1Y20Cwv8p2XwIgeXkNq4iDpdlx6IHZ +9JW1/VJwIqx6xxBmaBxYdu3Y3YY= +-----END CERTIFICATE----- diff --git a/score/tests/test_vectors/certificate/ec_p256_slot.kv b/score/tests/test_vectors/certificate/ec_p256_slot.kv new file mode 100644 index 000000000..7d4d4b241 --- /dev/null +++ b/score/tests/test_vectors/certificate/ec_p256_slot.kv @@ -0,0 +1,3 @@ +[certificate] +cert_path = score/tests/test_vectors/certificate/ec_p256.pem +cert_format = pem diff --git a/score/tests/test_vectors/certificate/ec_p384.pem b/score/tests/test_vectors/certificate/ec_p384.pem new file mode 100644 index 000000000..d6ae27365 --- /dev/null +++ b/score/tests/test_vectors/certificate/ec_p384.pem @@ -0,0 +1,49 @@ +Certificate: + Data: + Version: 3 (0x2) + Serial Number: + 61:dd:68:28:fa:91:31:8c:53:d1:2f:ae:da:85:e5:d0:90:46:8c:de + Signature Algorithm: ecdsa-with-SHA256 + Issuer: CN=cert-mgmt-ec-p384, O=Eclipse + Validity + Not Before: Aug 28 07:39:04 2026 GMT + Not After : Aug 25 07:39:04 2036 GMT + Subject: CN=cert-mgmt-ec-p384, O=Eclipse + Subject Public Key Info: + Public Key Algorithm: id-ecPublicKey + Public-Key: (384 bit) + pub: + 04:d1:e5:35:15:7c:de:b1:3b:88:1e:66:a3:c3:48: + c9:28:f2:10:6e:18:0c:25:e8:37:91:01:27:c1:c8: + 22:43:17:b9:a2:c7:58:dc:c1:ef:9a:d3:5d:9a:a3: + d7:fd:03:53:c1:84:39:52:19:87:63:53:e0:b3:d1: + 2e:b9:21:98:ad:fe:a5:4d:03:af:3f:c2:66:e0:3f: + e5:f7:19:97:9b:57:f3:0d:37:0c:68:53:74:bc:77: + db:9f:f6:e6:78:b1:a1 + ASN1 OID: secp384r1 + NIST CURVE: P-384 + X509v3 extensions: + X509v3 Basic Constraints: critical + CA:TRUE + X509v3 Subject Key Identifier: + 14:85:E8:37:6B:86:11:78:8C:F0:A1:55:BE:14:DE:5C:D3:A9:E5:03 + Signature Algorithm: ecdsa-with-SHA256 + Signature Value: + 30:64:02:30:58:51:ae:8c:28:24:ed:13:ce:f7:09:93:fd:1d: + d2:16:67:b8:57:24:d5:f7:3a:9b:f4:d5:93:27:fe:50:67:61: + 01:54:7b:bd:0c:7f:c1:a2:a3:94:4a:f8:77:e0:7c:6f:02:30: + 15:ad:cf:37:49:ca:f2:c0:74:5e:ec:26:de:8b:70:63:c2:8a: + cd:00:cb:3d:04:56:0a:86:fa:8d:dc:58:2d:a4:96:ad:1a:c1: + 2e:1e:15:e4:26:f3:31:2c:e8:10:df:84 +-----BEGIN CERTIFICATE----- +MIIBzDCCAVOgAwIBAgIUYd1oKPqRMYxT0S+u2oXl0JBGjN4wCgYIKoZIzj0EAwIw +LjEaMBgGA1UEAwwRY2VydC1tZ210LWVjLXAzODQxEDAOBgNVBAoMB0VjbGlwc2Uw +HhcNMjYwODI4MDczOTA0WhcNMzYwODI1MDczOTA0WjAuMRowGAYDVQQDDBFjZXJ0 +LW1nbXQtZWMtcDM4NDEQMA4GA1UECgwHRWNsaXBzZTB2MBAGByqGSM49AgEGBSuB +BAAiA2IABNHlNRV83rE7iB5mo8NIySjyEG4YDCXoN5EBJ8HIIkMXuaLHWNzB75rT +XZqj1/0DU8GEOVIZh2NT4LPRLrkhmK3+pU0Drz/CZuA/5fcZl5tX8w03DGhTdLx3 +25/25nixoaMyMDAwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUFIXoN2uGEXiM +8KFVvhTeXNOp5QMwCgYIKoZIzj0EAwIDZwAwZAIwWFGujCgk7RPO9wmT/R3SFme4 +VyTV9zqb9NWTJ/5QZ2EBVHu9DH/BoqOUSvh34HxvAjAVrc83ScrywHRe7Cbei3Bj +worNAMs9BFYKhvqN3FgtpJatGsEuHhXkJvMxLOgQ34Q= +-----END CERTIFICATE----- diff --git a/score/tests/test_vectors/certificate/ec_p384_slot.kv b/score/tests/test_vectors/certificate/ec_p384_slot.kv new file mode 100644 index 000000000..3bc1c1776 --- /dev/null +++ b/score/tests/test_vectors/certificate/ec_p384_slot.kv @@ -0,0 +1,3 @@ +[certificate] +cert_path = score/tests/test_vectors/certificate/ec_p384.pem +cert_format = pem diff --git a/score/tests/test_vectors/certificate/ec_p521.pem b/score/tests/test_vectors/certificate/ec_p521.pem new file mode 100644 index 000000000..2ee427c1e --- /dev/null +++ b/score/tests/test_vectors/certificate/ec_p521.pem @@ -0,0 +1,55 @@ +Certificate: + Data: + Version: 3 (0x2) + Serial Number: + 18:33:13:9b:1e:11:83:40:6b:7a:3a:8e:f0:8b:da:a1:d7:6f:2b:c6 + Signature Algorithm: ecdsa-with-SHA256 + Issuer: CN=cert-mgmt-ec-p521, O=Eclipse + Validity + Not Before: Aug 28 07:39:04 2026 GMT + Not After : Aug 25 07:39:04 2036 GMT + Subject: CN=cert-mgmt-ec-p521, O=Eclipse + Subject Public Key Info: + Public Key Algorithm: id-ecPublicKey + Public-Key: (521 bit) + pub: + 04:00:bf:fd:46:a5:d7:6a:64:42:c1:b4:bd:fd:80: + 0c:8b:c2:4e:37:73:9e:d5:12:99:6d:1a:4c:7a:c1: + a1:18:f5:18:0b:c9:e1:0d:df:43:33:4e:61:cf:1d: + dc:f4:dd:ba:18:3c:ef:1f:43:39:41:7f:34:55:c0: + 4f:43:74:56:d0:b4:c9:01:42:ac:7e:63:ab:4b:f9: + 59:fb:d0:fa:95:5f:17:1f:58:6f:e1:66:f3:c0:cf: + 44:ce:0d:3b:31:62:1b:77:89:c9:83:ca:3b:5c:d7: + b4:ef:57:6f:77:15:8a:04:96:87:24:7b:d2:23:a6: + 00:76:4f:b7:64:07:43:d0:07:81:77:96:d5 + ASN1 OID: secp521r1 + NIST CURVE: P-521 + X509v3 extensions: + X509v3 Basic Constraints: critical + CA:TRUE + X509v3 Subject Key Identifier: + B3:ED:44:55:2A:72:B9:A5:2E:C2:96:4A:69:62:0F:45:8C:EA:83:F1 + Signature Algorithm: ecdsa-with-SHA256 + Signature Value: + 30:81:88:02:42:01:63:ec:26:7b:8e:99:ba:aa:23:7c:f9:d3: + 33:64:d1:d6:a2:76:36:73:f5:7c:f0:e5:78:2a:27:b4:68:b4: + 97:15:f8:52:94:03:3d:7b:41:aa:f8:19:c8:ac:8e:0e:8d:72: + 49:ec:be:1f:c1:1a:ef:8d:fb:a5:b9:0d:2a:51:79:6e:76:02: + 42:00:a7:5d:bc:81:5b:68:25:52:c3:30:8a:6c:c7:8d:d4:60: + 36:2e:59:15:f5:f4:86:c0:41:ae:fb:a3:69:34:fb:23:1f:f5: + 51:26:56:58:4a:5c:ad:33:7f:b8:9d:d0:d4:c6:2f:f8:47:06: + 24:30:56:73:d9:fd:b8:49:7c:6e:00:0f:aa +-----BEGIN CERTIFICATE----- +MIICGDCCAXmgAwIBAgIUGDMTmx4Rg0BrejqO8IvaoddvK8YwCgYIKoZIzj0EAwIw +LjEaMBgGA1UEAwwRY2VydC1tZ210LWVjLXA1MjExEDAOBgNVBAoMB0VjbGlwc2Uw +HhcNMjYwODI4MDczOTA0WhcNMzYwODI1MDczOTA0WjAuMRowGAYDVQQDDBFjZXJ0 +LW1nbXQtZWMtcDUyMTEQMA4GA1UECgwHRWNsaXBzZTCBmzAQBgcqhkjOPQIBBgUr +gQQAIwOBhgAEAL/9RqXXamRCwbS9/YAMi8JON3Oe1RKZbRpMesGhGPUYC8nhDd9D +M05hzx3c9N26GDzvH0M5QX80VcBPQ3RW0LTJAUKsfmOrS/lZ+9D6lV8XH1hv4Wbz +wM9Ezg07MWIbd4nJg8o7XNe071dvdxWKBJaHJHvSI6YAdk+3ZAdD0AeBd5bVozIw +MDAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBSz7URVKnK5pS7ClkppYg9FjOqD +8TAKBggqhkjOPQQDAgOBjAAwgYgCQgFj7CZ7jpm6qiN8+dMzZNHWonY2c/V88OV4 +Kie0aLSXFfhSlAM9e0Gq+BnIrI4OjXJJ7L4fwRrvjfuluQ0qUXludgJCAKddvIFb +aCVSwzCKbMeN1GA2LlkV9fSGwEGu+6NpNPsjH/VRJlZYSlytM3+4ndDUxi/4RwYk +MFZz2f24SXxuAA+q +-----END CERTIFICATE----- diff --git a/score/tests/test_vectors/certificate/ec_p521_slot.kv b/score/tests/test_vectors/certificate/ec_p521_slot.kv new file mode 100644 index 000000000..3b7bbd26d --- /dev/null +++ b/score/tests/test_vectors/certificate/ec_p521_slot.kv @@ -0,0 +1,3 @@ +[certificate] +cert_path = score/tests/test_vectors/certificate/ec_p521.pem +cert_format = pem diff --git a/score/tests/test_vectors/certificate/ed25519.pem b/score/tests/test_vectors/certificate/ed25519.pem new file mode 100644 index 000000000..18fe57803 --- /dev/null +++ b/score/tests/test_vectors/certificate/ed25519.pem @@ -0,0 +1,39 @@ +Certificate: + Data: + Version: 3 (0x2) + Serial Number: + 41:8b:99:85:b2:9d:53:e9:d8:f4:06:ab:b1:1b:a7:9b:c7:e8:d4:27 + Signature Algorithm: ED25519 + Issuer: CN=cert-mgmt-ed25519, O=Eclipse + Validity + Not Before: Aug 28 07:39:05 2026 GMT + Not After : Aug 25 07:39:05 2036 GMT + Subject: CN=cert-mgmt-ed25519, O=Eclipse + Subject Public Key Info: + Public Key Algorithm: ED25519 + ED25519 Public-Key: + pub: + e5:93:b5:b7:bb:9f:6a:76:2b:c4:24:02:2b:13:9f: + c3:aa:06:95:8d:4e:b7:2a:f4:d6:2b:2c:cc:d0:aa: + 62:6b + X509v3 extensions: + X509v3 Basic Constraints: critical + CA:TRUE + X509v3 Subject Key Identifier: + 92:00:95:74:5A:5D:2F:32:00:AB:6B:23:6B:AE:9C:84:C3:4A:A2:36 + Signature Algorithm: ED25519 + Signature Value: + 1a:bf:4d:52:9f:84:35:20:88:4e:c2:3a:fe:80:87:f0:fe:d0: + 7d:3c:94:69:4f:53:f9:35:e7:9d:1b:da:07:71:e2:a0:88:d4: + 7f:40:f8:88:73:bf:75:9e:0e:17:3b:64:d9:b7:7a:a1:93:5d: + fa:14:14:f5:bf:c7:e3:7a:4c:0b +-----BEGIN CERTIFICATE----- +MIIBUDCCAQKgAwIBAgIUQYuZhbKdU+nY9AarsRunm8fo1CcwBQYDK2VwMC4xGjAY +BgNVBAMMEWNlcnQtbWdtdC1lZDI1NTE5MRAwDgYDVQQKDAdFY2xpcHNlMB4XDTI2 +MDgyODA3MzkwNVoXDTM2MDgyNTA3MzkwNVowLjEaMBgGA1UEAwwRY2VydC1tZ210 +LWVkMjU1MTkxEDAOBgNVBAoMB0VjbGlwc2UwKjAFBgMrZXADIQDlk7W3u59qdivE +JAIrE5/DqgaVjU63KvTWKyzM0Kpia6MyMDAwDwYDVR0TAQH/BAUwAwEB/zAdBgNV +HQ4EFgQUkgCVdFpdLzIAq2sja66chMNKojYwBQYDK2VwA0EAGr9NUp+ENSCITsI6 +/oCH8P7QfTyUaU9T+TXnnRvaB3HioIjUf0D4iHO/dZ4OFztk2bd6oZNd+hQU9b/H +43pMCw== +-----END CERTIFICATE----- diff --git a/score/tests/test_vectors/certificate/ed25519_slot.kv b/score/tests/test_vectors/certificate/ed25519_slot.kv new file mode 100644 index 000000000..664ad36d2 --- /dev/null +++ b/score/tests/test_vectors/certificate/ed25519_slot.kv @@ -0,0 +1,3 @@ +[certificate] +cert_path = score/tests/test_vectors/certificate/ed25519.pem +cert_format = pem diff --git a/score/tests/test_vectors/certificate/ed448.pem b/score/tests/test_vectors/certificate/ed448.pem new file mode 100644 index 000000000..ba9fea6ae --- /dev/null +++ b/score/tests/test_vectors/certificate/ed448.pem @@ -0,0 +1,44 @@ +Certificate: + Data: + Version: 3 (0x2) + Serial Number: + 78:0e:6f:c3:ec:36:61:3e:71:b8:36:54:90:0a:26:b2:9b:17:f2:b3 + Signature Algorithm: ED448 + Issuer: CN=cert-mgmt-ed448, O=Eclipse + Validity + Not Before: Aug 28 07:39:05 2026 GMT + Not After : Aug 25 07:39:05 2036 GMT + Subject: CN=cert-mgmt-ed448, O=Eclipse + Subject Public Key Info: + Public Key Algorithm: ED448 + ED448 Public-Key: + pub: + cf:80:32:4c:a7:36:55:2e:8f:ed:80:66:52:89:d8: + 40:a4:3f:77:84:53:80:2b:25:92:03:ae:0f:53:f8: + 26:e7:0c:7b:7b:ec:07:9d:46:27:12:21:06:2c:ea: + f8:6b:8e:e7:e5:dd:eb:75:f4:7f:cc:80 + X509v3 extensions: + X509v3 Basic Constraints: critical + CA:TRUE + X509v3 Subject Key Identifier: + 7F:2C:1C:AA:DC:A3:F0:8D:B9:8B:BE:A5:74:47:30:20:C4:6B:AF:A0 + Signature Algorithm: ED448 + Signature Value: + 5b:32:3c:c4:69:22:f3:29:43:4a:61:93:dd:6c:c6:51:55:6e: + 87:15:9d:0c:67:92:51:d1:93:7d:af:b4:66:8a:38:be:6c:a9: + d9:8a:89:cf:ef:3c:58:be:e9:ad:de:90:90:67:7b:ee:31:d0: + b0:e3:80:b5:09:76:9f:d7:00:60:80:ac:b7:cf:e0:5d:d4:a4: + 22:a7:5a:1f:02:e2:98:e9:f4:89:40:f7:ab:25:c6:84:a4:af: + 85:40:ea:4e:7b:5c:55:17:97:82:52:b3:3e:55:10:6c:e3:b7: + a0:68:0c:82:05:00 +-----BEGIN CERTIFICATE----- +MIIBlzCCARegAwIBAgIUeA5vw+w2YT5xuDZUkAomspsX8rMwBQYDK2VxMCwxGDAW +BgNVBAMMD2NlcnQtbWdtdC1lZDQ0ODEQMA4GA1UECgwHRWNsaXBzZTAeFw0yNjA4 +MjgwNzM5MDVaFw0zNjA4MjUwNzM5MDVaMCwxGDAWBgNVBAMMD2NlcnQtbWdtdC1l +ZDQ0ODEQMA4GA1UECgwHRWNsaXBzZTBDMAUGAytlcQM6AM+AMkynNlUuj+2AZlKJ +2ECkP3eEU4ArJZIDrg9T+CbnDHt77AedRicSIQYs6vhrjufl3et19H/MgKMyMDAw +DwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUfywcqtyj8I25i76ldEcwIMRrr6Aw +BQYDK2VxA3MAWzI8xGki8ylDSmGT3WzGUVVuhxWdDGeSUdGTfa+0Zoo4vmyp2YqJ +z+88WL7prd6QkGd77jHQsOOAtQl2n9cAYICst8/gXdSkIqdaHwLimOn0iUD3qyXG +hKSvhUDqTntcVReXglKzPlUQbOO3oGgMggUA +-----END CERTIFICATE----- diff --git a/score/tests/test_vectors/certificate/ed448_slot.kv b/score/tests/test_vectors/certificate/ed448_slot.kv new file mode 100644 index 000000000..b23e952ef --- /dev/null +++ b/score/tests/test_vectors/certificate/ed448_slot.kv @@ -0,0 +1,3 @@ +[certificate] +cert_path = score/tests/test_vectors/certificate/ed448.pem +cert_format = pem diff --git a/score/tests/test_vectors/certificate/generate_certificates.py b/score/tests/test_vectors/certificate/generate_certificates.py index d7b39cd42..955668cc7 100644 --- a/score/tests/test_vectors/certificate/generate_certificates.py +++ b/score/tests/test_vectors/certificate/generate_certificates.py @@ -12,20 +12,145 @@ # SPDX-License-Identifier: Apache-2.0 # ******************************************************************************* -"""Generate or refresh certificate-management test certificates from a manifest.""" +"""Generate or refresh certificate-management test certificates from a manifest. + +Supported key_algorithm values in the manifest: + + RSA RSA-2048, RSA-3072, RSA-4096 + EC EC-P256, EC-P384, EC-P521 + EdDSA Ed25519, Ed448 + PQC ML-DSA-44, ML-DSA-65, ML-DSA-87 (NIST FIPS 204, OpenSSL >= 3.5) + +The key_algorithm field drives both the -newkey arguments and whether an +explicit message-digest flag (-sha256) is passed to openssl req. Ed25519, +Ed448, and ML-DSA variants carry their own inherent digest and must not +receive -sha256. + +Manifest schema (v2) +-------------------- +Paths are derived from the entry ``name`` field and the top-level ``defaults`` +block — they are not stored redundantly in each entry. Convention: + + certificate -> /.pem (default cert_dir = ".") + private key -> /.key.pem (default key_dir = "private") + slot KV -> /_slot.kv + +The ``generated`` block within each entry is written by this script after a +successful generation run and must not be edited by hand. +""" + +from __future__ import annotations import argparse import datetime as datetime_module import json import pathlib import subprocess +from typing import Any + +# --------------------------------------------------------------------------- +# Algorithm tables +# --------------------------------------------------------------------------- + +_EC_CURVES: dict[str, str] = { + "EC-P256": "P-256", + "EC-P384": "P-384", + "EC-P521": "P-521", +} +_EDDSA_ALGS: frozenset[str] = frozenset({"ED25519", "ED448"}) +_MLDSA_LEVELS: frozenset[str] = frozenset({"ML-DSA-44", "ML-DSA-65", "ML-DSA-87"}) + + +def newkey_args(key_algorithm: str) -> list[str]: + """Return the -newkey flag sequence for *openssl req -x509*. + + The returned list is ready to extend() into the openssl argument list + during key generation (not used for the ``update`` path which reuses an + existing key with -key). + """ + alg = key_algorithm.upper() + + if alg.startswith("RSA-"): + bits = alg[4:] + if not bits.isdigit(): + raise SystemExit(f"Invalid RSA key size in key_algorithm: {key_algorithm!r}") + return ["-newkey", f"rsa:{bits}"] + + if alg in _EC_CURVES: + curve = _EC_CURVES[alg] + return ["-newkey", "ec", "-pkeyopt", f"ec_paramgen_curve:{curve}"] + + if alg in _EDDSA_ALGS: + return ["-newkey", alg.lower()] # ed25519 / ed448 + + if alg in _MLDSA_LEVELS: + return ["-newkey", alg.lower()] # ml-dsa-44 / ml-dsa-65 / ml-dsa-87 + + raise SystemExit( + f"Unknown key_algorithm: {key_algorithm!r}. " + f"Supported: RSA-, EC-P256/P384/P521, Ed25519, Ed448, " + f"ML-DSA-44/65/87." + ) + + +def uses_separate_digest(key_algorithm: str) -> bool: + """True for RSA and EC where an explicit hash algorithm can be selected. + + Ed25519, Ed448, and ML-DSA carry their own inherent digest; passing + -sha256 to openssl req for these algorithms is incorrect and may produce + an error on strict OpenSSL builds. + """ + alg = key_algorithm.upper() + return alg.startswith("RSA-") or alg in _EC_CURVES + + +# --------------------------------------------------------------------------- +# Path derivation +# --------------------------------------------------------------------------- + + +def cert_path(root: pathlib.Path, defaults: dict[str, Any], entry: dict[str, Any]) -> pathlib.Path: + cert_dir = entry.get("cert_dir", defaults.get("cert_dir", ".")) + return root / cert_dir / f"{entry['name']}.pem" -def run_openssl(openssl, arguments): +def key_path(root: pathlib.Path, defaults: dict[str, Any], entry: dict[str, Any]) -> pathlib.Path: + key_dir = entry.get("key_dir", defaults.get("key_dir", "private")) + return root / key_dir / f"{entry['name']}.key.pem" + + +def slot_kv_path(root: pathlib.Path, defaults: dict[str, Any], entry: dict[str, Any]) -> pathlib.Path: + cert_dir = entry.get("cert_dir", defaults.get("cert_dir", ".")) + return root / cert_dir / f"{entry['name']}_slot.kv" + + +def entry_validity_days(defaults: dict[str, Any], entry: dict[str, Any]) -> int: + return int(entry.get("validity_days", defaults.get("validity_days", 3650))) + + +# --------------------------------------------------------------------------- +# OpenSSL helpers +# --------------------------------------------------------------------------- + + +def run_openssl(openssl: str, arguments: list[str]) -> subprocess.CompletedProcess[str]: return subprocess.run([openssl, *arguments], check=True, capture_output=True, text=True) -def parse_snapshot(openssl, certificate_path): +def annotate_pem(openssl: str, certificate_path: pathlib.Path) -> None: + """Prepend human-readable certificate info to a PEM file. + + Runs ``openssl x509 -text`` which outputs the decoded fields followed by + the PEM block. The result is written back to the same file so the cert + remains machine-parseable while also being human-readable at a glance. + Most PEM parsers (OpenSSL, Python cryptography, etc.) skip the text header + and process only the base64 armored block. + """ + result = run_openssl(openssl, ["x509", "-in", str(certificate_path), "-text"]) + certificate_path.write_text(result.stdout, encoding="utf-8") + + +def parse_snapshot(openssl: str, certificate_path: pathlib.Path) -> dict[str, str]: result = run_openssl( openssl, [ @@ -40,17 +165,17 @@ def parse_snapshot(openssl, certificate_path): "-sha256", ], ) - values = {} + values: dict[str, str] = {} for line in result.stdout.splitlines(): key, separator, value = line.partition("=") if separator: values[key] = value.strip() - def parse_date(value): + def parse_date(value: str) -> str: parsed = datetime_module.datetime.strptime(value, "%b %d %H:%M:%S %Y GMT") return parsed.replace(tzinfo=datetime_module.timezone.utc).isoformat().replace("+00:00", "Z") - fingerprint = values["sha256 Fingerprint"].replace(":", "").upper() + fingerprint: str = values["sha256 Fingerprint"].replace(":", "").upper() return { "not_before": parse_date(values["notBefore"]), "not_after": parse_date(values["notAfter"]), @@ -58,42 +183,78 @@ def parse_date(value): } -def load_manifest(path): +# --------------------------------------------------------------------------- +# Manifest I/O +# --------------------------------------------------------------------------- + + +def load_manifest(path: pathlib.Path) -> dict[str, Any]: with path.open(encoding="utf-8") as manifest_file: - return json.load(manifest_file) + return json.load(manifest_file) # type: ignore[no-any-return] -def save_manifest(path, manifest): +def save_manifest(path: pathlib.Path, manifest: dict[str, Any]) -> None: with path.open("w", encoding="utf-8") as manifest_file: json.dump(manifest, manifest_file, indent=2) manifest_file.write("\n") -def find_certificate(manifest, name): +def find_certificate(manifest: dict[str, Any], name: str) -> dict[str, Any]: for certificate in manifest["certificates"]: if certificate["name"] == name: - return certificate + return certificate # type: ignore[no-any-return] names = ", ".join(entry["name"] for entry in manifest["certificates"]) raise SystemExit(f"Unknown certificate '{name}'. Available names: {names}") -def write_slot_kv(root, certificate): +# --------------------------------------------------------------------------- +# Slot KV descriptor +# --------------------------------------------------------------------------- + + +def _find_workspace_root(start: pathlib.Path) -> pathlib.Path | None: + """Walk upward from *start* looking for a Bazel workspace marker file.""" + path = start.resolve() + for _ in range(12): + for marker in ("MODULE.bazel", "WORKSPACE.bazel", "WORKSPACE", ".git"): + if (path / marker).exists(): + return path + parent = path.parent + if parent == path: + break + path = parent + return None + + +def write_slot_kv( + root: pathlib.Path, + defaults: dict[str, Any], + entry: dict[str, Any], +) -> pathlib.Path: """Write a KV slot descriptor alongside the generated certificate. The cert_path inside the descriptor uses a workspace-relative path so that tests can reference the descriptor directly from the Bazel runfiles root without copying or modifying it at runtime. + + Workspace root discovery is done via marker-file search (MODULE.bazel, + WORKSPACE.bazel, WORKSPACE, .git) so the path is computed correctly + regardless of whether Python is invoked via a WSL executable, a Windows + native interpreter, or a Unix shell from any working directory. """ - cert_rel = pathlib.Path(certificate["certificate_path"]) - kv_name = cert_rel.stem + "_slot.kv" - kv_path = root / kv_name + kv_path = slot_kv_path(root, defaults, entry) + pem_path = cert_path(root, defaults, entry) - try: - workspace_cert = (root / cert_rel).relative_to(pathlib.Path.cwd()) - cert_path_str = workspace_cert.as_posix() - except ValueError: - # Fallback when the script is not invoked from the workspace root. - cert_path_str = str(root / cert_rel) + workspace_root = _find_workspace_root(root) + if workspace_root is not None: + try: + cert_path_str = pem_path.resolve().relative_to(workspace_root).as_posix() + except ValueError: + cert_path_str = pem_path.name + else: + # Last-resort fallback: use the filename only; the test must locate it + # relative to its own runfiles root. + cert_path_str = pem_path.name kv_path.write_text( f"[certificate]\ncert_path = {cert_path_str}\ncert_format = pem\n", @@ -102,24 +263,41 @@ def write_slot_kv(root, certificate): return kv_path -def generate_certificate(openssl, root, certificate, update): - certificate_path = root / certificate["certificate_path"] - key_path = root / certificate["private_key_path"] +# --------------------------------------------------------------------------- +# Certificate generation +# --------------------------------------------------------------------------- + + +def generate_certificate( + openssl: str, + root: pathlib.Path, + defaults: dict[str, Any], + entry: dict[str, Any], + update: bool, +) -> tuple[pathlib.Path, pathlib.Path]: + certificate_path = cert_path(root, defaults, entry) + private_key_path = key_path(root, defaults, entry) certificate_path.parent.mkdir(parents=True, exist_ok=True) - key_path.parent.mkdir(parents=True, exist_ok=True) + private_key_path.parent.mkdir(parents=True, exist_ok=True) + + if update and not private_key_path.exists(): + raise SystemExit( + f"Cannot update '{entry['name']}': private key is missing at {private_key_path}" + ) - if update and not key_path.exists(): - raise SystemExit(f"Cannot update '{certificate['name']}': private key is missing at {key_path}") + key_alg: str = entry["key_algorithm"] + subject: str = entry["subject"] + days: int = entry_validity_days(defaults, entry) - subject = certificate["subject"] + digest_flags = ["-sha256"] if uses_separate_digest(key_alg) else [] arguments = [ "req", "-x509", "-new", - "-sha256", + *digest_flags, "-nodes", "-days", - str(certificate["validity_days"]), + str(days), "-subj", subject, "-out", @@ -132,16 +310,23 @@ def generate_certificate(openssl, root, certificate, update): "authorityKeyIdentifier=keyid", ] if update: - arguments.extend(["-key", str(key_path)]) + arguments.extend(["-key", str(private_key_path)]) else: - arguments.extend(["-newkey", "rsa:2048", "-keyout", str(key_path)]) + arguments.extend([*newkey_args(key_alg), "-keyout", str(private_key_path)]) + run_openssl(openssl, arguments) - certificate["snapshot"] = parse_snapshot(openssl, certificate_path) - kv_path = write_slot_kv(root, certificate) - return certificate_path, kv_path + annotate_pem(openssl, certificate_path) + entry["generated"] = parse_snapshot(openssl, certificate_path) + kv = write_slot_kv(root, defaults, entry) + return certificate_path, kv + + +# --------------------------------------------------------------------------- +# Entry point +# --------------------------------------------------------------------------- -def main(): +def main() -> None: parser = argparse.ArgumentParser(description=__doc__) parser.add_argument("action", choices=["generate", "update"]) parser.add_argument("name", help="Manifest certificate name") @@ -155,17 +340,19 @@ def main(): manifest_path = args.manifest.resolve() manifest = load_manifest(manifest_path) - certificate = find_certificate(manifest, args.name) + defaults: dict[str, Any] = manifest.get("defaults", {}) + entry = find_certificate(manifest, args.name) certificate_path, kv_path = generate_certificate( args.openssl, manifest_path.parent, - certificate, + defaults, + entry, update=args.action == "update", ) save_manifest(manifest_path, manifest) print(f"Wrote {certificate_path}") print(f"Wrote {kv_path}") - print(json.dumps(certificate["snapshot"], indent=2)) + print(json.dumps(entry["generated"], indent=2)) if __name__ == "__main__": diff --git a/score/tests/test_vectors/certificate/ml_dsa_44.pem b/score/tests/test_vectors/certificate/ml_dsa_44.pem new file mode 100644 index 000000000..f941ea0cb --- /dev/null +++ b/score/tests/test_vectors/certificate/ml_dsa_44.pem @@ -0,0 +1,331 @@ +Certificate: + Data: + Version: 3 (0x2) + Serial Number: + 1c:60:24:ec:5d:60:0a:57:56:a5:08:85:25:11:a6:78:20:89:2e:70 + Signature Algorithm: ML-DSA-44 + Issuer: CN=cert-mgmt-ml-dsa-44, O=Eclipse + Validity + Not Before: Aug 28 07:39:05 2026 GMT + Not After : Aug 25 07:39:05 2036 GMT + Subject: CN=cert-mgmt-ml-dsa-44, O=Eclipse + Subject Public Key Info: + Public Key Algorithm: ML-DSA-44 + ML-DSA-44 Public-Key: + pub: + c3:9e:8d:61:8f:fa:88:4f:af:57:76:20:28:80:de: + 68:8c:fd:d0:b8:33:4d:7c:db:7b:ed:6d:b1:35:28: + e6:5a:f0:3f:61:58:36:75:83:b2:ca:0a:c2:b0:eb: + a3:92:94:49:94:3f:2f:a9:6d:f5:0c:d1:ab:85:49: + e3:3e:d9:18:44:7e:42:f2:61:e2:55:dc:01:27:ab: + 52:d0:e6:95:e2:58:be:a2:73:74:65:47:48:be:1a: + aa:03:e0:d6:91:10:c5:80:fc:62:18:c4:8b:94:da: + 15:c1:62:2d:33:b5:70:80:4e:f6:b0:e5:a6:04:a0: + 49:fb:80:a2:93:19:24:fe:95:e6:49:24:36:00:80: + a1:a7:2e:f2:51:52:2c:f7:1b:5d:5a:b0:45:65:1e: + 2b:af:4d:9d:2f:67:72:6c:e1:7f:bd:0e:a5:57:71: + b9:28:a8:66:aa:d2:4a:ee:43:05:34:47:e6:e5:a4: + 8e:fd:ac:76:20:7f:06:d5:9e:c5:50:fa:6d:a0:74: + 68:2f:1c:b7:c7:5e:50:c7:14:5b:4b:a2:80:c4:e4: + 90:76:eb:a5:b0:f5:2b:cd:8b:1d:59:b3:90:27:f1: + af:f0:34:ca:bb:53:96:eb:ef:43:3e:d6:39:5d:fd: + 05:9b:0a:c9:28:b0:d1:72:f2:bf:d2:87:b6:0c:06: + 7e:3b:f4:39:93:72:ec:a3:f7:98:90:fd:c2:1f:9b: + 6d:50:13:08:75:4a:24:29:b4:65:82:8d:82:6c:fb: + 2f:12:13:9a:5a:36:be:ab:10:b8:b8:62:5d:96:1a: + d5:9d:34:ee:aa:ca:03:54:9e:c7:e2:a6:3a:a9:bd: + 71:65:eb:d2:96:37:86:3e:e9:ec:3d:bb:28:0a:43: + 87:99:dd:a2:74:95:21:98:f4:26:97:55:1c:47:fa: + 68:ba:32:02:a3:f9:78:51:5d:cc:ce:81:4f:e8:27: + 86:6a:20:95:89:60:83:02:06:bc:4d:81:c4:b2:73: + e8:b2:db:4a:8f:ab:b8:78:a6:71:bf:24:dd:42:1e: + 8d:81:b5:e2:8c:86:c1:92:f3:03:24:7d:68:ac:85: + 6b:27:9f:0b:22:8a:5f:d4:bb:88:a7:1e:c0:4a:0d: + fa:a0:bc:ea:e3:7c:d0:e2:6e:48:b5:06:5f:eb:44: + d9:7c:28:17:e7:d1:0a:7a:12:18:c6:2f:59:c7:29: + 09:8a:57:a5:57:80:e4:a9:74:59:49:30:10:46:82: + bf:ed:cc:10:ac:bb:55:43:b5:50:88:34:31:97:ad: + 9e:52:2f:41:62:27:65:5f:46:3f:76:23:8a:5e:c9: + 49:96:c4:81:9a:9b:98:07:d5:70:b5:97:ba:62:c0: + 1b:d6:be:b7:c8:6b:e1:fe:0d:78:23:1e:75:e2:03: + 62:e9:3c:22:12:a5:19:a4:d8:11:f3:10:c7:e6:1a: + 34:c0:d4:c8:d5:28:d4:76:97:7f:1e:ad:b6:25:51: + 2e:f7:72:b6:d4:79:da:bf:a4:a9:a4:41:4b:ee:fe: + 07:f5:43:d7:0a:e1:f3:27:ca:91:9d:42:6b:d1:52: + 78:25:cf:0d:1f:e8:d1:2c:c2:5a:84:ad:9a:d9:97: + bc:fd:3f:3c:ff:9b:03:22:f2:06:e6:bd:e8:72:4c: + 38:fd:8d:65:df:42:96:ac:40:6f:a7:d3:50:3b:4f: + 02:48:8f:ab:db:fb:cd:2b:af:8d:b6:2a:c7:2c:a1: + b6:59:83:70:f9:bb:89:a9:a2:ca:4f:61:eb:ab:c7: + 3c:fa:92:a0:7c:b7:6f:c6:8a:86:ee:0b:fb:c0:1c: + 07:cd:91:1c:2b:3d:07:18:54:46:c0:e8:a9:11:2e: + 60:25:11:b5:f1:2d:37:5d:40:85:3c:18:2c:98:81: + 1b:c5:c4:65:9d:51:8e:7c:9e:3d:48:92:fe:87:c3: + ce:55:65:40:78:8a:4f:97:d7:ab:13:48:9c:bb:8a: + 88:e0:be:6e:73:41:89:de:14:db:56:37:4e:90:a5: + c0:a1:a6:f6:01:4a:4f:35:95:92:d1:66:16:6f:6f: + 53:3e:c7:3f:3d:db:8c:7e:62:82:d2:2a:52:4c:c0: + 29:54:4f:2e:15:f9:e7:b1:c6:10:28:f3:f0:c8:52: + 3e:11:b2:93:43:4f:6b:14:04:76:58:0b:59:54:56: + 15:01:c5:47:c1:63:7d:58:c6:ea:29:3d:22:0a:a7: + 98:9c:ec:a0:b5:06:53:7b:b0:89:c1:7c:cf:3b:69: + 9e:f8:19:15:8a:a9:5d:77:d2:6f:0c:7a:7c:35:12: + 5d:1d:e4:cc:23:df:a2:90:1a:de:b7:50:6c:26:36: + 2a:fd:e8:1c:40:5d:a3:5b:10:1b:60:85:50:31:5a: + 87:08:a0:71:58:8b:af:da:25:3f:ad:35:a8:39:76: + 86:74:a0:0f:cb:43:84:a9:4e:bd:68:da:69:fa:17: + 39:e2:48:c2:37:ee:8f:85:fb:70:8f:55:63:56:5f: + 9d:ee:17:3c:2b:b9:4d:6e:ff:84:0b:06:4e:bb:4a: + 9a:3f:64:40:74:38:b9:60:90:51:da:fc:5f:c7:d6: + d9:ce:3b:b7:87:24:0e:95:a1:7a:8c:b8:92:bc:06: + d1:28:95:dc:f8:77:fd:02:9e:9e:a6:a8:7c:f3:eb: + ea:c5:71:aa:0e:63:7e:de:88:d5:bb:55:46:74:93: + b5:1c:2d:6b:2a:e8:4b:08:cd:6b:24:35:d9:9c:90: + be:f4:ba:a1:e5:17:bc:6a:41:35:c0:59:e2:db:8a: + 6c:5f:af:89:65:b4:84:f8:bb:5e:3f:e0:16:0a:c1: + 0b:8c:c5:ce:5a:15:3d:85:6b:74:a0:57:ff:80:7d: + e0:5c:85:46:3c:be:49:75:1d:2d:90:bd:1b:9f:ff: + 4d:21:94:ce:69:65:bc:c5:e7:07:c3:e0:57:63:ba: + ad:a7:ba:27:6c:60:99:c0:5a:8c:7a:ea:65:b0:33: + cb:9a:b5:90:c1:d4:ae:de:70:2a:be:04:17:08:d6: + 80:3e:4c:51:76:4e:f5:59:f4:ee:76:7e:cd:f5:5f: + de:43:c3:77:f0:75:66:d4:4e:a6:b4:c6:2b:aa:52: + 0b:c1:08:95:cd:39:d8:ba:82:8a:47:8a:38:c8:4f: + a7:25:10:77:e8:a8:9c:13:be:f3:b2:89:1d:53:65: + 13:c2:06:f6:d5:44:d7:a4:a9:26:d5:2d:b1:35:5d: + 5b:a1:22:54:be:1e:d4:47:a1:28:c6:63:7a:39:4a: + 96:11:90:c4:b8:55:8a:8a:67:06:b2:28:47:fc:73: + 20:c2:74:e9:15:c9:47:02:b5:c6:6d:59:5e:af:0b: + bc:62:74:9e:32:8d:70:ff:14:29:03:d6:d8:38:67: + fb:d4:99:a9:1c:de:36:26:26:fe:46:85:89:19:c4: + 36:ea:72:35:63:04:e4:48:b5:a9:49:6e:a8:39:d3: + 47:7f:d8:a3:f2:e4:fe:f3:03:ee:3c:da:bc:32:0d: + 11:83:dd:5a:e6:5b:64 + X509v3 extensions: + X509v3 Basic Constraints: critical + CA:TRUE + X509v3 Subject Key Identifier: + C0:9C:DE:36:F9:5D:9C:B6:85:A3:7D:63:B4:39:00:D8:84:46:13:8F + Signature Algorithm: ML-DSA-44 + Signature Value: + 26:a0:1b:5c:73:a9:b3:ec:73:ee:e6:99:d0:6f:5a:6b:a7:3c: + 3b:b4:76:db:95:d2:73:f7:7b:05:e9:ad:70:a3:9d:a1:fe:7d: + c4:7b:bb:7f:89:cb:1c:d4:c1:ee:e1:3f:48:c5:de:42:76:75: + 9f:51:8c:be:53:34:e6:45:a7:de:d2:1e:ca:88:41:e5:cd:75: + 5f:14:df:f6:a9:2a:03:25:7a:02:29:3e:0f:4d:7c:e5:a3:14: + 0e:4b:4d:09:0d:ab:32:14:ae:2f:6d:98:74:47:34:bd:6d:7f: + cd:8f:ef:51:cb:9f:42:89:23:8d:7c:96:ed:8a:52:b6:94:9a: + a0:d2:16:ca:fc:21:fd:69:34:e5:1b:d9:19:ad:e1:ef:08:5e: + aa:aa:72:c0:e7:0c:72:c6:fc:a1:23:bc:f2:b0:56:df:61:19: + 7b:58:fa:ea:78:d0:a8:65:8e:7c:28:b5:5e:27:f4:92:98:d2: + ea:8c:99:52:99:6a:44:ac:1e:09:30:4a:4a:a3:89:ac:5e:de: + ba:f1:46:8f:f5:24:a7:91:23:17:01:50:9a:34:e9:46:a9:b0: + be:18:aa:a4:6b:df:ed:b0:b7:c6:8a:cf:4d:a2:33:e6:68:bc: + 8d:24:a8:77:46:75:d7:44:f1:d0:d6:4b:9c:2f:22:08:ce:6c: + ae:98:3d:31:33:5d:c5:f0:bd:bb:4f:eb:c3:ea:ae:8b:7a:f8: + 00:4d:c6:17:60:a2:e7:a3:99:f4:db:e7:cd:ab:8a:5e:72:98: + ad:eb:3a:44:bb:23:08:b5:01:53:9a:b3:65:fb:50:58:b2:0a: + 81:96:4a:7d:ce:76:8a:70:c8:0e:42:0d:50:b5:58:60:be:7b: + 74:4b:af:e6:7f:ff:38:de:3f:75:f0:22:63:df:81:3b:68:d5: + 9f:7e:8a:89:86:f4:7c:84:e0:5e:c2:ba:fc:3c:0d:2e:1c:fb: + 34:e0:d2:e2:d5:06:f9:70:b6:d6:0c:57:60:6c:02:3d:52:b4: + 4d:35:9c:7b:27:85:c7:ec:bf:e5:43:53:5b:fe:61:b7:a5:13: + d1:4f:77:9f:df:87:80:5c:fa:73:00:5e:33:01:e6:3c:a1:29: + 9f:94:ca:70:86:67:82:42:a0:bb:11:5e:bd:0e:37:8b:5d:5f: + ec:b6:25:f5:d4:b1:97:3b:10:78:8c:37:86:cf:4e:28:2c:65: + 86:0e:84:ab:a9:d9:45:ec:3a:1e:c4:13:2e:69:ed:24:24:c2: + 3f:a3:95:bc:fc:d9:c6:1a:a7:83:1b:b0:f9:0d:b8:df:cf:38: + 22:d6:50:de:78:50:e0:25:71:4b:0f:1a:49:69:fe:15:28:a1: + 83:a2:6f:0e:1d:b6:5c:35:f7:10:1f:5e:fb:0c:2a:7f:5c:71: + 25:c6:f0:c3:25:fc:be:93:2b:19:dd:6a:0d:15:f3:ab:5c:18: + 4a:59:1d:f8:80:46:da:a1:9b:9f:8c:58:6a:1c:5e:7a:f4:54: + eb:f9:f9:4b:df:78:f0:1f:4b:45:13:2c:5b:f4:93:c4:1e:9f: + 9f:7a:b2:47:02:a1:56:7a:2b:f6:1f:47:b7:2f:be:db:71:27: + d7:0e:9a:18:76:b1:13:a2:72:14:ce:3d:79:0e:bc:2e:3f:41: + 0c:b2:58:be:88:dd:c1:ab:02:c2:64:71:ca:42:47:ae:4f:4b: + 1e:80:04:bb:c1:1e:62:50:cb:8e:e8:8a:d0:ad:b2:0d:bc:4a: + e8:72:8d:c3:63:4c:26:53:f3:21:9c:d0:59:1b:e2:29:4c:1e: + 8a:81:db:76:ba:af:6b:54:22:fe:a2:49:1e:ba:8e:7f:da:2d: + b8:df:ef:06:03:fb:b7:ea:01:2c:ad:52:95:6d:10:4f:d4:69: + f6:d1:d6:62:07:84:83:63:1d:a3:fd:97:33:65:0d:2c:d4:b1: + a7:49:9b:16:5b:20:7c:46:73:69:10:1f:54:93:0c:ab:f9:7c: + 07:22:2b:f2:e6:67:7a:50:28:4c:4d:e0:99:da:a4:ce:1a:ad: + fc:8f:ac:24:51:6d:78:26:46:18:d0:35:4c:55:99:a1:a1:24: + b0:c4:a9:13:47:7a:d6:75:04:25:5e:2f:62:cb:4f:d1:76:44: + 58:31:30:34:aa:48:cf:09:db:90:4d:d0:92:ec:13:c2:a9:ae: + 51:01:a5:ed:5a:e2:03:35:f8:1e:1f:66:97:11:fa:27:1f:98: + b8:57:bf:fd:d3:83:7e:d7:ad:75:42:89:18:77:56:18:ee:e7: + 75:55:a8:06:da:8e:72:cc:c3:70:51:29:89:6d:75:37:4e:5e: + 55:54:cc:ab:fb:f6:18:d0:79:91:ea:6b:84:cf:a1:c6:6e:da: + f4:c2:ab:f8:ef:d0:d9:e8:f3:c6:94:ee:f8:bd:98:1d:37:cb: + 0e:82:87:ec:cd:d2:a8:ce:07:86:ce:f6:75:1d:66:45:ff:61: + cc:4f:b9:69:2b:3b:e5:80:88:ff:01:9f:87:a7:b7:cc:14:6c: + d3:c4:b5:13:b8:5e:50:6a:38:7c:f0:4c:95:00:a5:69:8b:ae: + b0:39:62:be:22:5c:87:3f:c3:83:06:60:fb:eb:99:76:a3:ad: + 76:ef:96:3d:c6:36:cd:3a:40:36:d7:41:67:96:4b:26:78:96: + 7b:4b:13:d1:f7:96:49:73:ee:97:e4:2a:ed:1d:3e:89:39:ee: + fd:1f:c7:ec:bd:07:69:75:af:8b:44:4d:dc:84:e6:19:14:68: + 9f:ed:58:08:dc:5e:70:4a:59:53:2d:a9:ae:8f:46:3f:b4:b9: + 9f:09:ef:06:c4:ca:4e:54:8a:e6:d7:63:32:d0:68:4d:41:74: + ce:bd:34:dd:cb:aa:2d:b9:ab:0b:b5:fe:34:c8:8a:c1:8a:82: + 10:6e:b3:b1:9f:8c:74:ea:b9:84:fe:f2:54:96:c1:28:a4:5e: + a2:14:b8:06:a9:88:89:f2:69:50:f7:f5:6d:03:82:60:47:88: + 3a:cf:e7:86:1d:2d:93:18:9d:f1:0f:a5:c3:ad:3d:f4:0c:4d: + ef:dd:9c:86:f8:39:02:70:8d:44:32:a1:0e:a1:f2:ad:dd:e2: + 78:58:28:3e:00:ce:02:3a:b7:1b:e1:4c:b6:f3:f3:a1:1c:de: + d1:23:e8:29:0e:16:f3:6c:83:12:7d:22:8c:01:a4:ad:83:cb: + c7:ae:5f:37:ab:f7:e4:f5:99:09:64:7d:02:ee:a3:e7:f3:03: + 86:d0:10:28:2f:2c:cd:c4:91:75:51:af:cc:31:c6:f3:6d:2f: + e4:1e:84:c0:1d:0f:a3:2c:c1:a2:e0:5c:5f:7b:f5:8b:d8:3e: + 11:ed:95:4a:19:ee:3a:cb:b1:b9:fc:a9:5f:a2:8c:21:ad:1d: + 05:de:51:c3:7f:d2:88:10:d9:1b:cf:47:d4:62:cb:7d:ce:20: + 58:79:27:ec:0d:9d:85:29:11:86:92:d7:e5:43:36:0f:55:76: + 7c:d4:40:cb:e5:2d:ba:d0:11:47:f8:1e:a5:54:bb:7d:0b:d9: + 6a:6d:e0:2e:55:1e:db:f9:e0:01:86:f1:3f:a0:d6:d5:5b:ac: + 6a:57:38:f7:f8:2e:2e:8c:45:78:bb:68:85:ab:cc:c6:ea:03: + f9:2c:7f:89:40:c8:66:08:db:fe:b0:9d:a9:2a:08:bf:c2:9d: + 84:a2:d6:17:e0:a9:aa:61:1b:1d:c3:26:10:f0:85:00:8b:e9: + 15:62:11:eb:d5:be:a4:f1:e5:6c:6c:f4:28:ef:6d:60:74:58: + cf:fb:18:bb:f1:e1:4a:b7:84:a6:48:c8:54:6b:ab:67:12:91: + c4:6b:1e:f1:ad:98:87:fb:49:5e:16:cc:18:57:5e:e3:65:4e: + a2:08:ad:9e:91:67:98:c6:29:dc:b0:2c:f3:59:e6:72:b4:05: + 52:9b:29:64:5f:27:8e:f5:a7:c6:1a:31:67:90:d1:9f:b8:81: + 6c:0e:1c:e2:77:71:93:72:10:5a:96:dd:69:6a:80:62:5f:93: + cf:a4:4e:e3:8f:88:20:e5:6c:82:f2:44:35:42:60:77:e0:7d: + 04:5c:44:2a:ac:1a:40:63:dc:63:de:9b:ee:17:76:9e:4b:cc: + 1b:59:30:5a:84:e7:3d:67:67:56:bb:cb:10:af:2b:f1:8a:3b: + 07:74:9f:fe:33:04:79:7e:4c:24:3e:71:e3:12:fd:9c:cb:19: + 61:76:10:ea:74:0e:da:dc:43:1a:b9:e5:df:77:50:2c:b8:26: + 39:a8:69:f8:a7:81:6d:04:8a:52:eb:f3:d1:71:e7:6e:11:2f: + fb:74:52:75:8f:d0:0a:c1:3c:2b:a7:e6:4d:25:a4:5d:ce:09: + 4c:b2:4b:9b:c4:0b:90:b6:3b:f5:18:c7:a1:b3:31:7e:a5:5f: + 42:c6:af:7f:5d:c4:ca:10:c4:1e:e6:16:9d:68:71:b8:74:82: + 12:6a:54:5b:3e:3b:de:0d:98:16:3e:3a:b9:bd:c5:b7:90:7f: + 3a:21:8f:59:09:22:f0:91:e6:f9:a6:0c:08:b2:7b:61:fd:12: + 5a:8c:99:d1:ef:a0:6a:72:a1:ec:c5:19:fc:0e:c6:fb:c3:e2: + 8e:f5:9c:d1:80:71:a7:65:d8:b0:ef:4c:dc:d4:47:dd:36:45: + bc:f6:3f:4a:20:2e:6f:22:35:50:1e:5d:e2:9d:56:0d:63:7b: + fc:b6:6c:2f:f3:1b:44:cc:bb:3f:8c:17:11:d9:6d:74:1d:71: + 31:1a:df:ff:0a:ca:87:fe:62:89:31:72:57:1c:eb:13:1a:bf: + e9:50:b4:1a:5d:3c:5e:f8:64:ec:df:e9:ae:6e:f4:19:93:12: + e9:01:e6:df:6c:33:aa:28:c2:b9:64:61:bc:c6:95:6f:c0:f3: + 6a:6a:83:1a:8c:46:2a:f5:2a:ab:a4:c7:c0:b6:d6:b2:a2:57: + ba:4b:41:f0:43:f3:ce:b3:57:5c:10:f3:64:c0:89:46:f3:a9: + bc:ff:dd:ca:74:ec:78:64:da:f9:e2:b9:80:53:4a:06:94:b9: + b8:aa:f2:24:ea:c8:05:a2:4d:8a:42:1b:a1:74:f8:91:0e:90: + 34:92:07:30:57:34:cb:39:9e:c6:f1:ef:6f:3d:75:1b:2c:cf: + c9:1b:ef:0b:67:ce:fa:69:b5:6f:1d:47:a3:28:2a:5b:c6:2a: + 02:b9:4d:6f:79:61:d2:20:b6:3a:8d:b2:c1:a0:14:b0:75:09: + 43:39:02:97:83:7f:21:a7:01:87:3a:e0:91:b4:ff:ed:ad:25: + 56:e1:be:bc:a7:ad:eb:37:8e:41:4f:5e:38:cb:e9:d9:7f:3d: + 69:67:35:53:1d:90:8d:a3:a6:d5:c4:3b:94:f9:71:57:f3:ec: + eb:35:74:bb:32:a5:f9:62:ec:cb:d6:76:80:6b:7d:e1:3d:62: + f6:a1:6c:3c:36:3f:38:c4:4c:b3:0a:3e:b5:0e:a6:4d:b8:b1: + d2:54:30:97:7a:14:80:c7:49:f7:7b:85:c5:82:0b:db:89:a5: + ae:9e:bd:01:6b:be:32:ec:1b:55:a6:ea:48:76:b9:5a:d9:41: + c4:eb:45:86:e4:ec:2c:86:97:9d:54:44:62:66:e8:22:c0:92: + c6:c2:fa:a0:9a:0a:92:ed:b3:bc:4c:88:32:5c:49:1a:48:04: + d4:fb:6a:6f:d1:2b:a0:75:02:ec:85:34:4c:92:fb:a6:a7:4f: + 34:49:a5:25:07:6c:07:48:82:dc:ae:69:b3:e5:53:48:65:cf: + 98:79:44:53:45:39:10:c4:56:8d:08:91:7d:fd:ee:9c:fa:61: + 51:53:8e:59:4e:89:69:01:b5:cc:7f:04:23:47:08:db:99:7d: + 15:2a:db:a9:75:93:3f:37:59:de:78:15:9d:d2:44:3c:5c:89: + e7:4f:fa:db:63:57:c3:f2:37:96:59:6c:90:0e:6c:a0:06:e1: + 4e:ce:02:43:fe:77:d4:21:f8:9d:6b:34:58:c8:01:83:e0:67: + 49:30:5d:5c:08:b3:c9:52:f5:ac:3f:e1:e7:b3:16:9f:0b:cb: + 23:23:72:e2:bd:2c:9c:9f:9f:99:3c:df:ae:ed:3c:29:38:e6: + d1:74:c0:4e:ec:a8:6d:eb:7a:f6:1d:11:87:df:be:3d:13:10: + fb:cb:b5:fe:03:3f:10:ee:90:6a:88:5f:14:2a:e9:0f:dc:f2: + a4:eb:e5:61:e0:dd:ea:a7:b2:8f:d3:59:f1:64:41:42:ad:87: + 65:62:85:2e:e4:34:40:20:f6:2c:70:29:9b:62:06:1a:27:41: + 43:4a:4c:65:79:82:8f:92:a9:b8:b9:c6:cd:e0:e1:ef:25:3b: + 45:a2:a8:aa:c7:d3:da:dc:f3:f6:10:16:19:21:51:53:56:5d: + 69:70:a7:ab:d1:d9:da:e6:06:26:49:50:60:89:8c:99:9b:a3: + af:c5:f2:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00: + 00:00:00:00:14:20:30:3d +-----BEGIN CERTIFICATE----- +MIIPoDCCBhagAwIBAgIUHGAk7F1gCldWpQiFJRGmeCCJLnAwCwYJYIZIAWUDBAMR +MDAxHDAaBgNVBAMME2NlcnQtbWdtdC1tbC1kc2EtNDQxEDAOBgNVBAoMB0VjbGlw +c2UwHhcNMjYwODI4MDczOTA1WhcNMzYwODI1MDczOTA1WjAwMRwwGgYDVQQDDBNj +ZXJ0LW1nbXQtbWwtZHNhLTQ0MRAwDgYDVQQKDAdFY2xpcHNlMIIFMjALBglghkgB +ZQMEAxEDggUhAMOejWGP+ohPr1d2ICiA3miM/dC4M01823vtbbE1KOZa8D9hWDZ1 +g7LKCsKw66OSlEmUPy+pbfUM0auFSeM+2RhEfkLyYeJV3AEnq1LQ5pXiWL6ic3Rl +R0i+GqoD4NaREMWA/GIYxIuU2hXBYi0ztXCATvaw5aYEoEn7gKKTGST+leZJJDYA +gKGnLvJRUiz3G11asEVlHiuvTZ0vZ3Js4X+9DqVXcbkoqGaq0kruQwU0R+blpI79 +rHYgfwbVnsVQ+m2gdGgvHLfHXlDHFFtLooDE5JB266Ww9SvNix1Zs5An8a/wNMq7 +U5br70M+1jld/QWbCskosNFy8r/Sh7YMBn479DmTcuyj95iQ/cIfm21QEwh1SiQp +tGWCjYJs+y8SE5paNr6rELi4Yl2WGtWdNO6qygNUnsfipjqpvXFl69KWN4Y+6ew9 +uygKQ4eZ3aJ0lSGY9CaXVRxH+mi6MgKj+XhRXczOgU/oJ4ZqIJWJYIMCBrxNgcSy +c+iy20qPq7h4pnG/JN1CHo2BteKMhsGS8wMkfWishWsnnwsiil/Uu4inHsBKDfqg +vOrjfNDibki1Bl/rRNl8KBfn0Qp6EhjGL1nHKQmKV6VXgOSpdFlJMBBGgr/tzBCs +u1VDtVCINDGXrZ5SL0FiJ2VfRj92I4peyUmWxIGam5gH1XC1l7piwBvWvrfIa+H+ +DXgjHnXiA2LpPCISpRmk2BHzEMfmGjTA1MjVKNR2l38erbYlUS73crbUedq/pKmk +QUvu/gf1Q9cK4fMnypGdQmvRUnglzw0f6NEswlqErZrZl7z9Pzz/mwMi8gbmvehy +TDj9jWXfQpasQG+n01A7TwJIj6vb+80rr422KscsobZZg3D5u4mpospPYeurxzz6 +kqB8t2/GiobuC/vAHAfNkRwrPQcYVEbA6KkRLmAlEbXxLTddQIU8GCyYgRvFxGWd +UY58nj1Ikv6Hw85VZUB4ik+X16sTSJy7iojgvm5zQYneFNtWN06QpcChpvYBSk81 +lZLRZhZvb1M+xz8924x+YoLSKlJMwClUTy4V+eexxhAo8/DIUj4RspNDT2sUBHZY +C1lUVhUBxUfBY31YxuopPSIKp5ic7KC1BlN7sInBfM87aZ74GRWKqV130m8Menw1 +El0d5Mwj36KQGt63UGwmNir96BxAXaNbEBtghVAxWocIoHFYi6/aJT+tNag5doZ0 +oA/LQ4SpTr1o2mn6FzniSMI37o+F+3CPVWNWX53uFzwruU1u/4QLBk67Spo/ZEB0 +OLlgkFHa/F/H1tnOO7eHJA6VoXqMuJK8BtEoldz4d/0Cnp6mqHzz6+rFcaoOY37e +iNW7VUZ0k7UcLWsq6EsIzWskNdmckL70uqHlF7xqQTXAWeLbimxfr4lltIT4u14/ +4BYKwQuMxc5aFT2Fa3SgV/+AfeBchUY8vkl1HS2QvRuf/00hlM5pZbzF5wfD4Fdj +uq2nuidsYJnAWox66mWwM8uatZDB1K7ecCq+BBcI1oA+TFF2TvVZ9O52fs31X95D +w3fwdWbUTqa0xiuqUgvBCJXNOdi6gopHijjIT6clEHfoqJwTvvOyiR1TZRPCBvbV +RNekqSbVLbE1XVuhIlS+HtRHoSjGY3o5SpYRkMS4VYqKZwayKEf8cyDCdOkVyUcC +tcZtWV6vC7xidJ4yjXD/FCkD1tg4Z/vUmakc3jYmJv5GhYkZxDbqcjVjBORItalJ +bqg500d/2KPy5P7zA+482rwyDRGD3VrmW2SjMjAwMA8GA1UdEwEB/wQFMAMBAf8w +HQYDVR0OBBYEFMCc3jb5XZy2haN9Y7Q5ANiERhOPMAsGCWCGSAFlAwQDEQOCCXUA +JqAbXHOps+xz7uaZ0G9aa6c8O7R225XSc/d7BemtcKOdof59xHu7f4nLHNTB7uE/ +SMXeQnZ1n1GMvlM05kWn3tIeyohB5c11XxTf9qkqAyV6Aik+D0185aMUDktNCQ2r +MhSuL22YdEc0vW1/zY/vUcufQokjjXyW7YpStpSaoNIWyvwh/Wk05RvZGa3h7whe +qqpywOcMcsb8oSO88rBW32EZe1j66njQqGWOfCi1Xif0kpjS6oyZUplqRKweCTBK +SqOJrF7euvFGj/Ukp5EjFwFQmjTpRqmwvhiqpGvf7bC3xorPTaIz5mi8jSSod0Z1 +10Tx0NZLnC8iCM5srpg9MTNdxfC9u0/rw+qui3r4AE3GF2Ci56OZ9NvnzauKXnKY +res6RLsjCLUBU5qzZftQWLIKgZZKfc52inDIDkINULVYYL57dEuv5n//ON4/dfAi +Y9+BO2jVn36KiYb0fITgXsK6/DwNLhz7NODS4tUG+XC21gxXYGwCPVK0TTWceyeF +x+y/5UNTW/5ht6UT0U93n9+HgFz6cwBeMwHmPKEpn5TKcIZngkKguxFevQ43i11f +7LYl9dSxlzsQeIw3hs9OKCxlhg6Eq6nZRew6HsQTLmntJCTCP6OVvPzZxhqngxuw ++Q243884ItZQ3nhQ4CVxSw8aSWn+FSihg6JvDh22XDX3EB9e+wwqf1xxJcbwwyX8 +vpMrGd1qDRXzq1wYSlkd+IBG2qGbn4xYahxeevRU6/n5S9948B9LRRMsW/STxB6f +n3qyRwKhVnor9h9Hty++23En1w6aGHaxE6JyFM49eQ68Lj9BDLJYvojdwasCwmRx +ykJHrk9LHoAEu8EeYlDLjuiK0K2yDbxK6HKNw2NMJlPzIZzQWRviKUweioHbdrqv +a1Qi/qJJHrqOf9otuN/vBgP7t+oBLK1SlW0QT9Rp9tHWYgeEg2Mdo/2XM2UNLNSx +p0mbFlsgfEZzaRAfVJMMq/l8ByIr8uZnelAoTE3gmdqkzhqt/I+sJFFteCZGGNA1 +TFWZoaEksMSpE0d61nUEJV4vYstP0XZEWDEwNKpIzwnbkE3QkuwTwqmuUQGl7Vri +AzX4Hh9mlxH6Jx+YuFe//dODftetdUKJGHdWGO7ndVWoBtqOcszDcFEpiW11N05e +VVTMq/v2GNB5keprhM+hxm7a9MKr+O/Q2ejzxpTu+L2YHTfLDoKH7M3SqM4Hhs72 +dR1mRf9hzE+5aSs75YCI/wGfh6e3zBRs08S1E7heUGo4fPBMlQClaYuusDliviJc +hz/DgwZg++uZdqOtdu+WPcY2zTpANtdBZ5ZLJniWe0sT0feWSXPul+Qq7R0+iTnu +/R/H7L0HaXWvi0RN3ITmGRRon+1YCNxecEpZUy2pro9GP7S5nwnvBsTKTlSK5tdj +MtBoTUF0zr003cuqLbmrC7X+NMiKwYqCEG6zsZ+MdOq5hP7yVJbBKKReohS4BqmI +ifJpUPf1bQOCYEeIOs/nhh0tkxid8Q+lw6099AxN792chvg5AnCNRDKhDqHyrd3i +eFgoPgDOAjq3G+FMtvPzoRze0SPoKQ4W82yDEn0ijAGkrYPLx65fN6v35PWZCWR9 +Au6j5/MDhtAQKC8szcSRdVGvzDHG820v5B6EwB0PoyzBouBcX3v1i9g+Ee2VShnu +OsuxufypX6KMIa0dBd5Rw3/SiBDZG89H1GLLfc4gWHkn7A2dhSkRhpLX5UM2D1V2 +fNRAy+UtutARR/gepVS7fQvZam3gLlUe2/ngAYbxP6DW1Vusalc49/guLoxFeLto +havMxuoD+Sx/iUDIZgjb/rCdqSoIv8KdhKLWF+CpqmEbHcMmEPCFAIvpFWIR69W+ +pPHlbGz0KO9tYHRYz/sYu/HhSreEpkjIVGurZxKRxGse8a2Yh/tJXhbMGFde42VO +ogitnpFnmMYp3LAs81nmcrQFUpspZF8njvWnxhoxZ5DRn7iBbA4c4ndxk3IQWpbd +aWqAYl+Tz6RO44+IIOVsgvJENUJgd+B9BFxEKqwaQGPcY96b7hd2nkvMG1kwWoTn +PWdnVrvLEK8r8Yo7B3Sf/jMEeX5MJD5x4xL9nMsZYXYQ6nQO2txDGrnl33dQLLgm +Oahp+KeBbQSKUuvz0XHnbhEv+3RSdY/QCsE8K6fmTSWkXc4JTLJLm8QLkLY79RjH +obMxfqVfQsavf13EyhDEHuYWnWhxuHSCEmpUWz473g2YFj46ub3Ft5B/OiGPWQki +8JHm+aYMCLJ7Yf0SWoyZ0e+ganKh7MUZ/A7G+8PijvWc0YBxp2XYsO9M3NRH3TZF +vPY/SiAubyI1UB5d4p1WDWN7/LZsL/MbRMy7P4wXEdltdB1xMRrf/wrKh/5iiTFy +VxzrExq/6VC0Gl08Xvhk7N/prm70GZMS6QHm32wzqijCuWRhvMaVb8DzamqDGoxG +KvUqq6THwLbWsqJXuktB8EPzzrNXXBDzZMCJRvOpvP/dynTseGTa+eK5gFNKBpS5 +uKryJOrIBaJNikIboXT4kQ6QNJIHMFc0yzmexvHvbz11GyzPyRvvC2fO+mm1bx1H +oygqW8YqArlNb3lh0iC2Oo2ywaAUsHUJQzkCl4N/IacBhzrgkbT/7a0lVuG+vKet +6zeOQU9eOMvp2X89aWc1Ux2QjaOm1cQ7lPlxV/Ps6zV0uzKl+WLsy9Z2gGt94T1i +9qFsPDY/OMRMswo+tQ6mTbix0lQwl3oUgMdJ93uFxYIL24mlrp69AWu+MuwbVabq +SHa5WtlBxOtFhuTsLIaXnVREYmboIsCSxsL6oJoKku2zvEyIMlxJGkgE1Ptqb9Er +oHUC7IU0TJL7pqdPNEmlJQdsB0iC3K5ps+VTSGXPmHlEU0U5EMRWjQiRff3unPph +UVOOWU6JaQG1zH8EI0cI25l9FSrbqXWTPzdZ3ngVndJEPFyJ50/622NXw/I3llls +kA5soAbhTs4CQ/531CH4nWs0WMgBg+BnSTBdXAizyVL1rD/h57MWnwvLIyNy4r0s +nJ+fmTzfru08KTjm0XTATuyobet69h0Rh9++PRMQ+8u1/gM/EO6QaohfFCrpD9zy +pOvlYeDd6qeyj9NZ8WRBQq2HZWKFLuQ0QCD2LHApm2IGGidBQ0pMZXmCj5KpuLnG +zeDh7yU7RaKoqsfT2tzz9hAWGSFRU1ZdaXCnq9HZ2uYGJklQYImMmZujr8XyAAAA +AAAAAAAAAAAAAAAAAAAAABQgMD0= +-----END CERTIFICATE----- diff --git a/score/tests/test_vectors/certificate/ml_dsa_44_slot.kv b/score/tests/test_vectors/certificate/ml_dsa_44_slot.kv new file mode 100644 index 000000000..5ce349503 --- /dev/null +++ b/score/tests/test_vectors/certificate/ml_dsa_44_slot.kv @@ -0,0 +1,3 @@ +[certificate] +cert_path = score/tests/test_vectors/certificate/ml_dsa_44.pem +cert_format = pem diff --git a/score/tests/test_vectors/certificate/ml_dsa_65.pem b/score/tests/test_vectors/certificate/ml_dsa_65.pem new file mode 100644 index 000000000..96f312966 --- /dev/null +++ b/score/tests/test_vectors/certificate/ml_dsa_65.pem @@ -0,0 +1,455 @@ +Certificate: + Data: + Version: 3 (0x2) + Serial Number: + 13:8e:40:0c:12:ff:09:8c:6c:56:31:e8:16:6b:77:41:cc:1f:76:04 + Signature Algorithm: ML-DSA-65 + Issuer: CN=cert-mgmt-ml-dsa-65, O=Eclipse + Validity + Not Before: Aug 28 07:39:21 2026 GMT + Not After : Aug 25 07:39:21 2036 GMT + Subject: CN=cert-mgmt-ml-dsa-65, O=Eclipse + Subject Public Key Info: + Public Key Algorithm: ML-DSA-65 + ML-DSA-65 Public-Key: + pub: + f0:d9:4a:e1:7b:fe:f0:a4:e1:18:91:ac:e4:cf:41: + bf:22:fa:49:10:c6:43:01:f4:fa:46:44:33:44:1f: + 52:9b:f8:45:fc:c1:3e:c3:c0:5c:e4:77:02:f0:c1: + 7b:2e:e4:1a:9f:9e:66:d6:1f:c5:af:5d:3c:c3:bc: + 9c:44:63:16:42:16:f1:58:1f:a4:46:2d:c8:eb:ef: + f1:1a:c2:b4:ef:3e:ad:2a:93:5e:bc:a2:de:6b:32: + 7c:a6:1a:b7:92:a5:42:44:dd:c5:9d:06:e4:a6:d4: + a4:30:a4:83:0e:30:8b:f8:e9:14:08:fa:bf:45:2b: + 36:bd:15:43:b7:69:c7:42:93:6a:f2:8b:9c:12:84: + fb:44:6a:e1:7d:35:20:b3:b1:07:f7:87:18:d0:26: + cc:05:1f:d2:aa:8a:c9:9c:6e:75:de:c9:5d:72:52: + b4:a7:59:d6:6c:7c:6a:d9:45:5e:78:90:b1:56:43: + d6:49:f2:c7:b8:01:78:37:29:97:05:9b:fb:fc:b1: + e3:d2:71:94:b1:12:15:62:e7:70:6f:2d:56:ad:4f: + 74:e4:cf:4c:26:db:73:11:41:be:0c:83:08:3e:09: + 63:3e:86:29:35:9b:ac:fe:12:1d:b7:c5:eb:d9:c0: + 5b:d9:a8:14:02:bf:4f:01:98:f4:56:c8:d1:1c:be: + 8e:ee:bd:d6:6f:be:7a:5a:e1:6b:53:cd:3f:a4:ba: + db:13:84:e7:59:ff:e1:ac:f1:92:8f:2d:02:c7:e5: + 70:3b:cb:19:40:f3:ae:67:82:39:2a:72:55:d3:ad: + b0:c6:f0:01:74:67:f1:6c:10:46:d9:b5:e6:df:a8: + a9:c3:33:51:af:81:c6:48:6b:5e:b6:16:40:a7:91: + 26:4d:c1:eb:e1:d1:f0:22:a6:d6:59:7c:aa:ce:3e: + c3:d9:9f:10:82:b5:21:1d:7b:7a:5c:d9:12:2e:94: + 3b:59:c9:a7:67:c9:aa:fc:ff:30:75:ca:65:9e:2c: + 22:d3:59:ad:fa:e8:df:05:67:69:ca:fd:f3:ce:26: + 4f:15:a1:a6:f2:ea:6d:37:03:b0:62:8f:d6:05:bc: + ff:27:e4:6e:16:07:ad:fb:03:df:43:b4:90:e0:8e: + d6:f5:9c:fa:52:c1:78:9a:0b:0e:6b:d2:e1:7b:1f: + 1a:16:92:24:e6:77:89:de:b6:b7:e0:c3:b8:55:ac: + 38:fd:b0:8b:95:32:96:ab:4f:58:c4:e2:d2:48:cc: + c4:44:c9:c8:e9:da:0e:57:f0:4f:10:91:02:bf:14: + 38:d9:bc:48:9c:03:80:83:2c:2e:f1:ce:50:46:ca: + 49:29:d3:7c:32:cf:ab:74:6d:77:2d:8a:ec:76:87: + ae:22:ec:b7:c7:ea:b1:d4:ae:94:01:08:56:89:da: + b6:0c:ea:57:23:56:94:6c:fa:12:9f:05:56:15:1b: + 24:67:6f:cf:c3:f5:da:98:d9:a0:f2:50:ef:d4:25: + 88:65:35:0f:37:66:dd:29:f6:e0:df:a7:a8:47:79: + 5c:b4:61:60:03:7d:73:c0:53:eb:f1:56:8c:9e:7b: + 8e:de:1b:42:cb:3b:c1:a8:88:b5:ed:14:39:96:04: + b9:a4:1a:44:5d:ec:3f:e0:5f:c7:c7:af:a8:39:26: + 58:8a:46:c9:4a:13:f9:61:1e:8a:96:37:b5:77:73: + a7:b3:88:39:60:3d:1d:4f:2e:bd:8a:da:53:64:38: + 3b:5f:61:24:88:80:1b:ea:37:34:84:28:99:33:a5: + 0e:43:41:92:db:a5:f4:89:ea:6e:49:5c:c3:a6:57: + 1d:8d:21:82:2a:42:5a:4c:0b:07:af:23:06:2d:31: + 5d:bb:eb:8a:c5:44:ea:93:e2:44:9b:20:5e:77:17: + ae:7b:5f:36:f6:4a:ad:bc:79:b5:fa:13:98:f7:ef: + 91:ce:a4:56:74:77:9c:42:fb:c9:a1:38:2e:84:89: + 37:b6:d9:c5:a7:04:1e:1f:81:3b:d7:6c:a7:3b:13: + 62:3d:29:36:a7:ff:e4:93:00:d7:c2:16:87:e7:48: + b5:f6:1f:6c:a9:d3:61:6f:18:e8:fa:e4:af:fd:86: + 2d:8a:97:d5:35:f1:46:46:d0:c9:8c:bb:fe:1e:76: + 8b:53:2d:ed:66:43:bf:18:23:a3:0c:60:6b:ff:19: + 15:c8:62:1d:ac:d7:12:b5:70:e9:65:10:3b:96:01: + 81:90:e2:81:3b:01:cf:9b:75:f9:22:db:18:79:ce: + 38:a7:f9:b8:42:bf:33:72:f5:3d:e9:7e:4a:2f:32: + a4:a7:7c:6d:65:45:47:c6:5c:65:0c:e3:e4:d3:49: + a2:13:80:f5:61:a6:65:5a:2c:73:5a:6f:d3:f6:61: + 07:cb:7f:81:9b:78:10:ca:89:c9:48:d1:34:88:2a: + 47:58:0c:9f:f3:98:66:c1:18:78:8e:5a:0b:8f:7a: + 96:74:cd:41:41:d1:e6:49:80:f9:7c:1d:fd:48:98: + 60:91:9a:25:12:2f:85:9d:8e:5a:96:08:40:7c:ee: + 00:36:54:60:bc:40:04:1c:8e:da:32:3b:78:44:0a: + f9:52:51:16:95:e8:12:e5:b4:03:f6:56:2d:ae:15: + 85:89:d0:e8:64:b2:45:68:df:97:21:ff:87:a8:85: + 56:89:80:a5:a9:be:fd:a3:c3:30:70:2c:bc:23:22: + 8a:c0:0f:b2:0a:1c:03:fa:4f:a2:cb:a5:62:c6:19: + bc:f3:01:d0:b3:1b:56:ea:6d:c3:68:cd:d8:8c:64: + 37:94:db:be:22:bf:78:6d:2c:45:82:dd:ef:26:b2: + f8:63:3c:05:18:47:f0:7e:bf:7a:cb:72:76:b2:94: + bb:1f:c1:b1:a2:f6:a4:f5:60:e3:9a:bb:b9:ef:a4: + f7:b7:7e:16:84:74:2b:9e:53:f8:33:c1:50:56:21: + 18:3a:82:d0:1b:8e:3f:88:4d:08:e3:f6:fb:4f:88: + c5:95:9f:3a:63:d8:37:6e:76:a5:e5:1b:0d:4d:a4: + ab:33:2f:91:0a:fd:b1:18:dc:4a:1f:99:ee:8f:c7: + cd:38:9e:85:1a:a1:bf:d0:1d:ad:f0:eb:7b:93:8c: + aa:a7:c1:0d:82:0f:9e:8d:45:07:04:35:f1:5a:81: + 92:cd:53:11:d3:b3:56:20:4d:5a:ca:88:8d:4c:12: + 7a:b6:cb:d1:22:7e:39:b8:f3:ed:59:0b:5d:53:f0: + 40:eb:34:d6:99:a0:7d:34:cf:89:0a:ac:3a:7a:0e: + 57:ae:d3:21:33:03:38:bf:93:e2:b7:18:09:b8:73: + f0:56:34:f2:f3:92:7c:7c:42:c4:80:b6:6b:7e:b0: + 20:70:b2:a3:25:21:f3:de:08:43:7e:bf:a6:50:65: + 4a:7a:1b:fa:3e:28:fa:c1:9e:db:be:ee:3b:46:64: + cf:07:97:50:27:a1:b6:ff:35:4e:d7:30:20:ea:77: + 81:90:2b:15:c0:45:c9:62:a4:4a:24:42:5b:a3:4c: + b6:9b:cd:52:c1:e2:52:be:57:c1:12:ec:b7:2f:e9: + b7:d7:8f:9b:97:76:41:fa:d6:d0:6f:e0:7c:77:12: + db:b6:be:59:19:e1:2a:d4:bd:ea:32:14:c6:ca:8d: + 08:ab:0b:d0:00:e3:9e:90:4d:83:da:2c:75:0f:37: + 68:09:43:a3:21:cc:8f:68:ae:c3:c0:f8:6b:62:14: + 27:76:65:60:d9:25:c5:9a:87:bd:c0:93:f7:fa:77: + 05:ce:8e:97:6d:47:ed:01:f3:84:50:81:56:50:d2: + af:8a:d4:d1:40:63:dc:c2:60:39:6e:50:27:83:85: + 1e:a8:0f:f4:ce:17:6b:8c:a1:a0:51:74:44:8f:b0: + 9e:ce:3b:a1:b4:71:75:d8:a1:b8:b3:35:e8:ba:62: + 83:2c:bf:d7:29:24:4f:e5:dd:f9:31:a5:e9:1b:70: + 52:fd:14:22:8c:4d:40:91:3d:13:7d:97:b2:9b:76: + d3:eb:c5:64:be:72:ab:e3:d6:79:2d:5e:bf:77:42: + c8:eb:50:81:3c:6c:92:44:82:57:51:af:e5:b9:89: + 65:09:ad:cd:af:8c:34:14:0f:5e:bb:9f:77:a4:c8: + d3:3c:ed:6e:62:62:f9:4d:5d:a8:ac:bd:ad:0b:d5: + 5d:6a:a4:42:9b:66:ad:c7:d8:d6:e1:e0:42:1b:35: + 3d:35:18:92:c0:f4:23:48:53:58:23:22:16:a0:68: + 63:12:ad:b9:5b:9d:b1:08:32:55:c2:26:a0:47:c9: + 2d:6d:cc:77:0b:48:2e:5f:f7:5e:84:c9:75:2a:55: + be:87:9a:bd:e6:08:46:1e:1b:90:8c:63:aa:7c:b7: + 43:59:02:ef:4d:aa:8d:79:f0:5a:aa:21:ee:d7:23: + f1:59:9e:35:53:3e:9d:c9:91:fb:54:f4:17:b6:a7: + a1:64:c0:ed:ef:83:ff:72:1a:f5:55:de:8b:d2:6d: + 8d:4c:c9:24:ad:d8:3e:17:71:13:04:f1:2c:d4:93: + 33:de:9c:30:c6:08:ee:5f:06:a6:80:da:b6:7a:76: + 83:be:ca:7b:1f:98:eb:82:f3:3d:9a:6a:24:c9:f5: + 8d:6d:a2:a9:e6:b9:28:e3:66:f9:d8:d2:66:ff:3b: + 37:7c:e0:f7:f3:af:05:71:69:db:19:eb:ce:8a:0b: + 74:38:2f:29:9b:50:6a:9c:fc:65:96:fb:f8:7f:00: + 8b:ad:59:9b:49:99:bb:a6:72:63:12:11:80:87:dd: + 7e:0e:10:97:a1:12:9a:da:18:23:b4:49:95:b4:7e: + 4a:96:13:54:64:23:c8:78:eb:e2:4d:33:11:62:7e: + ec:92:c1:76:25:e0:92:6b:e6:4f:1b:86:3a:80:2b: + 40:f4:73:08:59:5f:33:1c:70:78:c1:14:76:7c:e9: + 77:1e:9a:74:4b:a7:1a:2f:fe:36:ab:42:0c:e5:89: + 6d:3b:97:8c:dd:ca:f7:23:29:41:19:10:cf:a7:25: + a1:63:19:92:58:46:dd:a2:d7:09:3d:78:1f:23:2f: + 99:c9:51:81:aa:0b:48:5e:6c:93:af:98:09:28:d8: + 86:53:55:8b:8b:fa:46:68:46:a6:7d:3d:25:2d:ef: + 99:80:6d:b5:c5:9b:92:82:47:10:eb:7a:54:ff:db: + ad:f1:42:16:ee:7d:6b:42:3d:12:fe:a4:59:06:4b: + ad:b9:cf:de:db:7c:f5:ae:50:ed:d6:67:fe:ad:3c: + dd:ad + X509v3 extensions: + X509v3 Basic Constraints: critical + CA:TRUE + X509v3 Subject Key Identifier: + 39:41:1F:72:3F:0F:D6:F5:64:E2:2E:14:2B:6C:4A:EA:51:DD:23:F1 + Signature Algorithm: ML-DSA-65 + Signature Value: + c5:62:47:cb:ee:69:55:b5:d5:0a:f9:1e:09:9d:e0:35:f8:8c: + 31:52:08:24:54:d2:82:28:05:60:20:c2:f3:2a:ef:9e:59:39: + 3a:11:fa:6a:02:04:10:48:39:4c:c9:b8:83:aa:4f:a3:64:95: + 76:d0:b9:11:e8:b1:e4:14:4c:75:48:42:59:b9:71:14:89:ed: + 04:7c:ac:e1:07:30:6e:f9:dc:1d:c4:05:7a:77:26:d8:03:23: + 8c:b6:c3:24:5e:14:87:14:d3:6a:bb:4c:18:52:fa:83:96:0c: + 95:b1:7d:a7:6a:ea:dc:ca:ec:f2:a2:38:d7:f6:34:68:f1:08: + e2:81:32:33:b8:92:84:8c:b6:a8:dc:9d:16:c1:ae:0a:c4:6e: + 92:24:42:a8:20:7d:48:cb:9c:36:79:f3:cf:58:75:bb:a4:c4: + db:75:6b:b4:6f:9c:34:1f:69:62:54:c1:5d:1c:e5:48:1c:5e: + fb:64:b9:f2:4d:bd:a7:bf:68:84:37:e7:12:49:00:6b:20:14: + dc:ac:5b:88:be:73:4e:2f:9b:ad:64:fd:f0:dc:e0:0c:a5:4f: + 2c:5a:f3:e1:38:72:98:5b:82:c7:9c:f2:c9:73:68:6f:69:16: + a8:a0:98:dc:fc:8d:32:c4:51:9d:1b:5c:04:1e:19:08:4f:6c: + 29:a1:5c:bf:33:56:47:18:5d:0f:57:55:89:79:bc:b0:88:68: + c8:d2:53:fd:1f:c4:8a:fd:6b:e4:67:e8:ef:a5:5a:49:60:6e: + bd:52:1c:9c:8e:8c:ea:e8:ab:06:cf:c1:39:b6:b0:7f:00:b8: + e5:2e:ca:90:87:bc:fc:bf:52:e4:9f:10:01:5c:71:30:e4:ac: + ab:6c:15:e2:c3:60:dd:9e:51:06:8c:94:aa:ba:e4:0b:db:c3: + 8c:64:d5:cb:97:fa:ef:b5:20:4d:63:cd:0b:8c:7c:20:73:d3: + 20:9f:4c:fe:b5:6a:6b:66:15:0f:56:db:a6:f3:20:ce:11:5b: + 39:d8:16:b2:19:cd:36:fd:da:71:ee:49:60:44:e4:81:c2:df: + b5:53:42:03:c1:2a:ef:a8:de:6c:28:bb:8b:a3:83:12:bc:b4: + 0f:5e:75:21:0a:d0:07:73:04:23:e4:8a:cb:8e:d7:5f:7c:84: + 7e:30:6e:ac:6f:ac:1a:52:78:78:42:5f:66:82:9c:8d:0f:e9: + 02:5c:ff:57:d9:aa:a3:1a:16:1d:ef:a6:70:76:db:cb:d3:59: + 27:e3:83:07:59:53:5c:dc:64:bf:74:47:99:e2:ce:b4:78:6f: + 4c:f7:bb:c6:16:cf:12:9d:18:c5:0c:32:e3:42:ae:35:6a:80: + 0b:fa:3c:5c:59:2b:74:e8:be:2c:0d:12:a0:c5:1e:df:6c:1a: + 5f:5a:69:c0:e0:fd:b3:c0:42:7b:45:75:d8:54:bb:94:e1:55: + 17:3f:61:fa:15:74:11:f9:26:33:8a:34:f7:ca:9d:38:6d:ca: + b1:23:b9:ff:1b:3f:4c:54:51:dd:88:a3:8c:8b:b8:19:22:ea: + 75:22:95:7a:af:fb:6a:d8:73:b0:89:b1:da:fb:e7:c5:9f:ac: + 9c:fc:c1:11:81:ee:6a:aa:27:9b:01:1e:bb:18:60:15:88:6c: + 61:9a:c8:25:e7:da:fc:d6:d6:eb:18:e6:2d:71:51:c9:87:da: + 1e:57:fe:52:af:a6:08:eb:4b:9a:01:8d:83:11:aa:ac:8b:fe: + 81:38:fd:25:a9:69:5c:15:8c:84:4e:ed:99:48:77:1e:e9:5d: + 6e:54:b0:23:c9:13:58:31:03:20:c3:11:03:b2:d7:8d:44:83: + 06:b7:43:30:2d:02:b5:2b:3a:47:6f:d6:05:dd:e4:4c:3a:b7: + 62:eb:9a:e6:33:c0:67:24:a8:f0:a3:b8:91:e9:af:b0:05:87: + fd:84:13:50:27:c5:7a:d2:0b:3b:e2:7c:1a:9f:e8:45:8b:e0: + 93:84:dd:17:d6:e5:d6:e5:09:cd:59:c9:06:7d:8a:e3:3a:b8: + 8b:78:04:55:52:b8:d2:a9:28:79:6f:5f:0c:43:65:e6:e7:64: + d6:fb:5d:07:b7:1c:61:90:88:19:15:ff:43:ab:4c:15:bd:04: + 14:7d:86:47:bb:24:4a:8e:f4:48:f6:ba:25:f5:5c:86:07:7f: + 1c:d9:c7:0d:a4:b7:8d:73:55:f4:ac:6a:ae:d3:94:0c:1e:45: + c6:97:7c:77:c0:43:d9:d1:ee:64:4e:96:e0:ff:88:35:65:4b: + 83:e6:42:88:44:ea:01:65:06:f1:cf:42:59:dc:22:25:99:29: + 6d:ab:2e:4b:eb:25:c7:03:ce:ae:54:2e:6a:ed:64:04:55:11: + b9:57:fd:b9:d4:15:7f:f6:a8:ec:2a:61:16:49:58:c4:00:09: + 5d:9c:52:78:9a:2d:aa:05:99:d3:f2:6e:9e:79:25:e3:99:59: + dc:5f:12:29:7e:3e:7e:6e:0f:10:08:db:b3:18:bf:57:70:2a: + 38:68:cf:6b:88:dc:ac:e2:61:4c:c3:0b:fd:08:9b:74:ed:04: + 69:93:9f:98:a3:c5:39:05:5e:70:35:d0:3f:4f:fa:e6:89:13: + 9d:fa:0b:80:46:79:dd:6f:47:ec:d7:ec:3d:5a:a9:d6:42:bb: + 04:22:cc:b6:c2:fd:c6:7f:29:07:69:77:46:a8:2a:2f:43:d8: + a2:f2:7d:6a:42:48:27:6b:a5:fa:71:47:29:83:8b:93:a2:b1: + 7c:0a:06:28:0a:9e:51:26:c7:17:21:38:9b:ef:11:c5:5a:4b: + 7a:ab:dd:5b:1d:34:a1:30:33:cb:80:55:0a:50:38:ef:4c:b8: + e8:bb:e1:33:97:84:86:c9:f7:d9:b1:11:ce:33:0f:8b:7c:92: + ab:80:42:58:e7:15:50:5c:38:fe:9e:31:f2:8c:d8:b2:3a:1e: + 3a:44:ff:6f:7a:72:39:3b:1e:1d:a0:d6:b0:7a:ee:8d:d1:4e: + 40:dd:b5:47:e8:9a:24:22:92:d9:ff:56:5f:b2:f6:ca:fc:e2: + bf:82:a4:36:96:dc:09:0e:d9:03:53:93:2b:d2:85:a8:33:b5: + c3:88:68:fd:2b:cc:9c:8b:6a:b6:b4:a6:15:47:af:28:ef:f1: + 8e:36:ca:19:8b:20:7b:f4:60:b8:98:03:8f:00:07:0e:cb:09: + 40:5f:f7:f6:0b:cb:e4:02:de:2f:b1:3d:11:1b:a0:c3:cf:00: + 6e:1d:a2:95:d1:b0:ed:93:cc:b4:e5:83:ab:aa:91:27:0a:c9: + 5c:df:d2:d9:66:40:4b:fe:ca:0b:53:d2:cf:eb:46:0c:27:22: + 91:0f:c0:56:d0:e4:a8:c9:bd:56:3a:5f:5c:db:3d:8d:4b:18: + 30:bd:f8:bb:ca:bb:fb:6f:a0:5c:55:fd:14:66:7a:9a:c1:eb: + 6e:90:7e:5e:f6:22:97:44:ad:67:02:e0:53:cb:60:a8:ea:11: + 4d:13:73:d3:38:19:e7:aa:62:c0:a8:fe:f4:3a:b1:62:61:e6: + b2:c2:36:67:38:53:1d:9e:a6:64:83:81:ab:bb:4f:a2:82:9f: + aa:56:5a:b2:cd:03:d3:aa:5d:9e:d6:db:1c:79:77:2d:5a:29: + 9f:39:08:83:19:15:ec:81:fb:ac:14:78:ad:6a:79:95:e0:98: + 2c:7b:27:6a:de:0d:d1:4a:17:51:8d:98:47:71:26:4c:7f:5d: + 63:b6:fb:9d:ea:b1:ff:a9:43:c7:60:ea:f3:b3:de:5b:13:c5: + 32:c1:86:d2:96:96:6c:65:32:a9:40:bb:7a:5d:c3:64:30:be: + 56:e4:cf:a9:97:82:99:74:0b:15:69:2c:51:a4:bc:aa:b5:5b: + bc:70:a0:02:19:46:e8:1a:cf:1c:d1:2e:7e:f9:7c:47:fb:0e: + ac:29:de:8b:95:96:1c:36:23:90:03:9d:ad:61:ca:79:ad:0d: + a5:0c:f6:c6:48:2e:b5:d7:3e:a6:92:8a:8d:d8:74:82:b8:3a: + f8:58:62:6f:93:e5:ab:97:95:02:f7:d3:59:4b:23:da:d2:f5: + a4:b5:cc:51:0c:f0:a7:99:86:74:57:ac:90:4c:fa:15:1a:6c: + 90:a6:c5:9a:81:71:14:56:48:07:38:77:10:f7:63:7d:a3:8a: + 31:f2:87:d7:98:bb:99:e1:8f:64:78:43:bc:17:fb:7f:4e:64: + 1a:08:27:24:d9:62:95:b4:f6:1f:fb:8b:f2:04:11:8f:b0:f9: + 52:20:3b:15:ac:b5:bf:44:5b:0d:07:38:17:ad:4a:e7:45:4a: + 5d:f0:10:55:33:de:3d:99:f0:f6:bc:c0:70:da:fe:04:77:b4: + 4a:08:62:4a:e3:32:04:2a:7d:eb:b7:df:12:23:e2:f0:3f:fa: + b0:ad:b5:2d:34:53:23:a6:2b:e1:00:63:a8:fb:73:50:b6:7c: + de:17:80:94:45:eb:42:62:2d:84:c4:aa:95:32:1f:48:04:5a: + 59:72:12:f9:7e:42:9d:f7:ca:00:dc:9f:78:49:7e:92:e6:2b: + 7d:6a:06:9a:a3:a1:04:d0:b9:40:1c:df:a9:51:68:c8:89:76: + 40:bf:eb:7c:d8:7b:bc:7f:a8:90:02:6a:58:ac:8b:ee:73:42: + ac:c6:69:b4:5f:39:75:c9:f8:d9:59:4d:f3:06:e3:ac:6f:d7: + 77:24:6a:99:a4:46:01:cb:cc:1c:9a:fe:85:29:1f:34:5c:29: + 78:67:a8:71:53:9b:48:0b:72:06:11:cd:8b:77:e8:7e:87:42: + ee:8a:1f:24:ee:4c:09:e6:94:dd:d8:87:e8:f2:7a:69:0c:66: + c9:0b:d0:e7:43:6d:e7:f3:42:17:6d:4c:5a:5e:30:8c:bb:67: + 52:35:bf:af:36:a9:8f:fd:16:fc:1b:54:61:85:c6:ce:5f:fd: + 19:1a:2c:2a:d7:00:03:90:4d:5c:13:72:41:b5:6e:ce:5e:44: + 3e:6e:46:90:ac:4a:99:d6:1f:5c:71:0a:bb:b1:04:41:c8:f0: + e2:27:b5:68:83:13:f2:a8:c1:a3:75:3d:41:30:b8:67:ed:39: + 69:23:a0:b4:58:9f:bc:1e:06:c8:1d:03:52:4a:10:5d:83:5a: + 26:0b:49:7e:4b:ff:f3:cb:68:42:84:40:54:23:40:ce:4d:d3: + 87:22:b0:8f:8d:f9:8f:31:b5:14:f2:67:4e:fa:39:ae:00:b7: + cb:07:4f:b6:39:b0:b0:85:1e:bd:94:be:d6:8e:02:47:da:b9: + bd:25:1c:45:fe:30:15:00:65:a7:f5:ca:1b:54:77:d5:17:f2: + 37:25:19:e0:ce:e5:27:a1:01:f0:37:b5:ed:0e:7c:30:04:26: + 71:1c:04:4f:da:de:84:8b:e8:63:b2:1f:df:8b:79:54:96:28: + 89:35:b5:11:94:4a:1f:ef:5b:97:cb:bb:e3:b6:be:c5:56:29: + 5d:d1:e3:30:e1:68:82:48:cd:8b:d6:36:b6:a6:b5:47:2b:cd: + 9f:23:87:17:f7:25:75:2a:dc:db:75:36:a4:d6:10:1e:52:8f: + e1:c8:ef:6b:7c:f4:8d:2d:3f:66:6a:9e:d5:11:ac:bd:11:76: + 8c:bc:6e:c0:82:a8:09:e5:cf:bb:52:d4:41:90:b4:3c:fc:e6: + 30:e5:86:c4:da:ac:b4:4e:ca:fb:e0:0a:59:67:59:40:59:03: + a2:a8:1b:14:92:92:cf:78:38:37:56:53:ed:20:29:ba:4a:aa: + af:19:df:90:40:d5:23:65:d2:a0:65:e1:87:92:64:0e:3a:c1: + 96:73:bb:09:6c:36:d9:50:84:bf:36:76:ae:7b:42:0c:55:70: + 39:73:3f:31:eb:d6:af:77:d4:f5:ba:45:ff:6b:00:31:13:f6: + 09:c1:c9:92:68:29:b6:df:dd:ff:71:32:e8:0e:53:43:72:11: + 96:4a:27:94:95:f5:bc:42:31:0e:b7:5a:50:f9:c4:15:d8:d2: + b7:af:be:c2:78:d9:90:35:38:15:35:ae:9c:3f:dd:19:99:63: + 41:db:1e:e2:dd:16:36:85:36:09:96:bd:2f:5d:13:1b:69:13: + 61:91:28:1c:4e:fd:aa:92:ec:26:f6:08:48:92:29:6b:fa:44: + 48:08:11:1c:ae:ee:a8:32:cf:4e:da:9a:32:87:f6:8b:e1:0c: + d8:9b:30:5a:a8:d7:45:14:14:70:30:0c:9d:07:9d:47:64:5c: + a1:68:a0:95:bf:76:15:c7:ee:05:5d:69:cf:b5:e1:3b:6b:f2: + 14:ce:61:91:92:dd:57:ea:ea:79:cf:78:db:d7:8c:0a:dc:db: + 5f:77:db:0a:c1:b1:ad:2e:3e:8c:7e:c9:06:99:bd:88:cf:e5: + 6f:f9:48:1e:a2:a6:60:58:79:4b:71:35:1e:f9:12:0f:c8:91: + 7f:f1:67:1e:60:3c:15:0d:e6:ee:b6:87:2a:6e:f8:2e:12:86: + a8:b3:fa:3a:5b:6a:31:bd:fc:55:0b:02:f4:5b:85:35:16:0f: + 0a:8d:72:d5:15:da:cb:cb:ef:51:85:32:dd:05:d4:ae:47:e6: + d1:aa:39:cc:12:48:1c:51:df:90:a2:9c:08:12:b9:f8:d8:45: + 7e:4d:17:8b:e1:21:fc:bf:4b:4f:4e:3f:af:64:c3:75:14:e5: + a6:5d:39:18:f3:d6:39:5e:7d:37:6b:45:32:cd:de:c5:b8:10: + 12:75:b0:bd:07:21:cb:08:18:bb:a1:2e:ac:20:b0:fa:8f:60: + 8e:94:8f:2a:11:4a:41:b2:f3:4d:9e:00:f3:e3:ff:1c:f0:5a: + 17:7a:27:ce:36:ae:33:77:ff:79:4b:17:44:bd:d3:21:ed:23: + 22:b1:8a:5a:a6:cc:33:b6:a3:e6:a2:f0:22:bd:9f:3c:ed:6f: + 1f:ce:9c:03:dc:cd:2f:ca:5c:fc:8e:02:5f:23:3f:67:4c:78: + 60:48:47:2e:25:6d:9b:ac:d5:ea:2d:31:e5:28:f5:1f:a0:26: + f9:a6:f3:ee:53:52:8c:2b:4a:b9:d6:75:d5:cb:3c:24:2c:36: + cf:be:57:6d:ef:74:2f:04:f6:7e:84:fd:e9:ee:eb:7a:81:b3: + 64:36:2b:f7:98:a3:70:12:2f:1d:ec:b1:09:e4:5b:03:67:86: + 76:ca:53:c7:b9:a8:06:a4:b0:d4:d1:10:78:b1:73:a3:55:f1: + 34:32:77:e4:d5:e0:bd:9b:2a:f7:ea:ee:05:5d:8a:ea:55:f6: + 76:c8:f9:f8:1a:37:cb:db:64:76:f4:6a:98:17:c9:45:70:a0: + 2d:0e:a1:50:ac:cf:6d:48:ce:ee:eb:f6:f5:60:78:63:64:fa: + 30:a3:ec:5b:1d:ed:d7:47:ae:a5:cb:96:22:2b:62:80:78:92: + 8c:b2:24:7a:bd:4c:1f:80:d3:e1:aa:5a:1f:e7:e6:02:0f:79: + 91:ad:e3:03:12:ba:5e:25:bc:ca:59:17:ee:5a:0a:fd:75:95: + 16:8c:a4:33:51:93:07:f7:f2:93:f5:4c:4a:ef:8c:cc:41:fa: + 9c:20:8c:31:53:a0:1e:1d:e9:a0:43:2e:49:9f:97:70:54:82: + ff:ee:72:3c:c4:11:80:84:de:eb:97:82:cc:65:64:27:60:c9: + 74:fa:d7:43:c3:fa:1d:2b:84:24:97:a2:c8:5c:77:a3:85:ad: + a0:58:e8:1e:ee:ff:e1:40:5b:7d:dd:e1:e0:d0:d0:b9:44:aa: + ae:cd:3e:fa:9e:39:f1:28:75:42:c3:36:f5:de:83:b7:fb:84: + cf:e5:3c:a0:75:a7:9c:0e:29:b0:70:0f:cb:a3:41:95:d5:5a: + a4:c4:05:b4:ee:e5:12:66:66:96:93:40:8f:cd:ce:a5:a7:54: + 37:98:df:5f:10:3f:f8:ba:31:b4:e2:91:11:a3:14:86:38:e2: + 05:a4:f7:08:29:72:b0:9b:98:6f:de:73:56:c5:ab:0a:5f:4d: + 53:dc:00:8f:4e:44:a1:53:a4:6b:6e:bb:0c:4e:5c:12:15:7f: + 8e:75:b4:9f:a2:d4:d3:5c:08:57:dc:59:a8:5f:71:29:18:03: + b0:1b:cb:cd:37:d8:56:87:91:be:f2:85:78:dd:b9:77:b9:c9: + 16:89:7b:70:2a:b8:66:06:03:9e:73:ef:ac:7e:65:78:0e:21: + 1d:51:6a:6e:0f:bb:7a:e7:9d:8e:c0:03:9e:a3:d2:14:2d:66: + 50:d5:c9:73:5d:6b:0f:ca:75:bb:38:3a:93:65:b7:cb:b2:fb: + c9:a1:81:bd:ff:6e:41:5f:85:9a:ad:a1:cb:ea:62:e1:91:bb: + 1a:20:c8:c9:f3:d6:f2:c9:12:c0:cd:55:95:cc:a2:08:d6:9e: + 55:2d:71:e1:59:dd:80:08:4b:9d:e8:87:61:77:01:7b:bc:40: + 87:1c:e5:0e:b1:e9:39:f1:0c:8c:9e:1b:b0:38:d9:74:be:2c: + 35:c1:e7:14:3c:a5:bb:01:9e:69:56:92:ac:2d:5d:54:5c:d3: + 65:0b:9f:39:22:5b:c2:34:75:29:ff:af:7d:9d:a5:e7:b6:ee: + 44:67:a6:d0:28:f3:0d:ad:1f:14:be:65:c7:5d:6e:d1:b6:e3: + 38:67:08:57:9a:8c:85:c8:06:b9:84:64:82:1b:28:2d:fd:dc: + 32:b7:d5:b7:93:8c:fd:cc:41:4c:16:d4:5e:da:99:c8:eb:25: + 26:49:97:cc:14:0b:0c:43:0a:68:7a:96:e5:23:6c:c1:cf:44: + 6c:71:82:8a:c8:43:55:cc:36:49:58:89:ce:43:6c:e7:00:00: + 00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00: + 00:00:00:00:00:00:00:00:00:05:09:0f:12:17:1a +-----BEGIN CERTIFICATE----- +MIIVmTCCCJagAwIBAgIUE45ADBL/CYxsVjHoFmt3QcwfdgQwCwYJYIZIAWUDBAMS +MDAxHDAaBgNVBAMME2NlcnQtbWdtdC1tbC1kc2EtNjUxEDAOBgNVBAoMB0VjbGlw +c2UwHhcNMjYwODI4MDczOTIxWhcNMzYwODI1MDczOTIxWjAwMRwwGgYDVQQDDBNj +ZXJ0LW1nbXQtbWwtZHNhLTY1MRAwDgYDVQQKDAdFY2xpcHNlMIIHsjALBglghkgB +ZQMEAxIDggehAPDZSuF7/vCk4RiRrOTPQb8i+kkQxkMB9PpGRDNEH1Kb+EX8wT7D +wFzkdwLwwXsu5BqfnmbWH8WvXTzDvJxEYxZCFvFYH6RGLcjr7/EawrTvPq0qk168 +ot5rMnymGreSpUJE3cWdBuSm1KQwpIMOMIv46RQI+r9FKza9FUO3acdCk2ryi5wS +hPtEauF9NSCzsQf3hxjQJswFH9KqismcbnXeyV1yUrSnWdZsfGrZRV54kLFWQ9ZJ +8se4AXg3KZcFm/v8sePScZSxEhVi53BvLVatT3Tkz0wm23MRQb4Mgwg+CWM+hik1 +m6z+Eh23xevZwFvZqBQCv08BmPRWyNEcvo7uvdZvvnpa4WtTzT+kutsThOdZ/+Gs +8ZKPLQLH5XA7yxlA865ngjkqclXTrbDG8AF0Z/FsEEbZtebfqKnDM1GvgcZIa162 +FkCnkSZNwevh0fAiptZZfKrOPsPZnxCCtSEde3pc2RIulDtZyadnyar8/zB1ymWe +LCLTWa366N8FZ2nK/fPOJk8Voaby6m03A7Bij9YFvP8n5G4WB637A99DtJDgjtb1 +nPpSwXiaCw5r0uF7HxoWkiTmd4netrfgw7hVrDj9sIuVMparT1jE4tJIzMREycjp +2g5X8E8QkQK/FDjZvEicA4CDLC7xzlBGykkp03wyz6t0bXctiux2h64i7LfH6rHU +rpQBCFaJ2rYM6lcjVpRs+hKfBVYVGyRnb8/D9dqY2aDyUO/UJYhlNQ83Zt0p9uDf +p6hHeVy0YWADfXPAU+vxVoyee47eG0LLO8GoiLXtFDmWBLmkGkRd7D/gX8fHr6g5 +JliKRslKE/lhHoqWN7V3c6eziDlgPR1PLr2K2lNkODtfYSSIgBvqNzSEKJkzpQ5D +QZLbpfSJ6m5JXMOmVx2NIYIqQlpMCwevIwYtMV2764rFROqT4kSbIF53F657Xzb2 +Sq28ebX6E5j375HOpFZ0d5xC+8mhOC6EiTe22cWnBB4fgTvXbKc7E2I9KTan/+ST +ANfCFofnSLX2H2yp02FvGOj65K/9hi2Kl9U18UZG0MmMu/4edotTLe1mQ78YI6MM +YGv/GRXIYh2s1xK1cOllEDuWAYGQ4oE7Ac+bdfki2xh5zjin+bhCvzNy9T3pfkov +MqSnfG1lRUfGXGUM4+TTSaITgPVhpmVaLHNab9P2YQfLf4GbeBDKiclI0TSIKkdY +DJ/zmGbBGHiOWguPepZ0zUFB0eZJgPl8Hf1ImGCRmiUSL4WdjlqWCEB87gA2VGC8 +QAQcjtoyO3hECvlSURaV6BLltAP2Vi2uFYWJ0OhkskVo35ch/4eohVaJgKWpvv2j +wzBwLLwjIorAD7IKHAP6T6LLpWLGGbzzAdCzG1bqbcNozdiMZDeU274iv3htLEWC +3e8msvhjPAUYR/B+v3rLcnaylLsfwbGi9qT1YOOau7nvpPe3fhaEdCueU/gzwVBW +IRg6gtAbjj+ITQjj9vtPiMWVnzpj2DdudqXlGw1NpKszL5EK/bEY3Eofme6Px804 +noUaob/QHa3w63uTjKqnwQ2CD56NRQcENfFagZLNUxHTs1YgTVrKiI1MEnq2y9Ei +fjm48+1ZC11T8EDrNNaZoH00z4kKrDp6Dleu0yEzAzi/k+K3GAm4c/BWNPLzknx8 +QsSAtmt+sCBwsqMlIfPeCEN+v6ZQZUp6G/o+KPrBntu+7jtGZM8Hl1Anobb/NU7X +MCDqd4GQKxXARclipEokQlujTLabzVLB4lK+V8ES7Lcv6bfXj5uXdkH61tBv4Hx3 +Etu2vlkZ4SrUveoyFMbKjQirC9AA456QTYPaLHUPN2gJQ6MhzI9orsPA+GtiFCd2 +ZWDZJcWah73Ak/f6dwXOjpdtR+0B84RQgVZQ0q+K1NFAY9zCYDluUCeDhR6oD/TO +F2uMoaBRdESPsJ7OO6G0cXXYobizNei6YoMsv9cpJE/l3fkxpekbcFL9FCKMTUCR +PRN9l7KbdtPrxWS+cqvj1nktXr93QsjrUIE8bJJEgldRr+W5iWUJrc2vjDQUD167 +n3ekyNM87W5iYvlNXaisva0L1V1qpEKbZq3H2Nbh4EIbNT01GJLA9CNIU1gjIhag +aGMSrblbnbEIMlXCJqBHyS1tzHcLSC5f916EyXUqVb6Hmr3mCEYeG5CMY6p8t0NZ +Au9Nqo158FqqIe7XI/FZnjVTPp3JkftU9Be2p6FkwO3vg/9yGvVV3ovSbY1MySSt +2D4XcRME8SzUkzPenDDGCO5fBqaA2rZ6doO+ynsfmOuC8z2aaiTJ9Y1toqnmuSjj +ZvnY0mb/Ozd84PfzrwVxadsZ686KC3Q4LymbUGqc/GWW+/h/AIutWZtJmbumcmMS +EYCH3X4OEJehEpraGCO0SZW0fkqWE1RkI8h46+JNMxFifuySwXYl4JJr5k8bhjqA +K0D0cwhZXzMccHjBFHZ86XcemnRLpxov/jarQgzliW07l4zdyvcjKUEZEM+nJaFj +GZJYRt2i1wk9eB8jL5nJUYGqC0hebJOvmAko2IZTVYuL+kZoRqZ9PSUt75mAbbXF +m5KCRxDrelT/263xQhbufWtCPRL+pFkGS625z97bfPWuUO3WZ/6tPN2tozIwMDAP +BgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBQ5QR9yPw/W9WTiLhQrbErqUd0j8TAL +BglghkgBZQMEAxIDggzuAMViR8vuaVW11Qr5Hgmd4DX4jDFSCCRU0oIoBWAgwvMq +755ZOToR+moCBBBIOUzJuIOqT6NklXbQuRHoseQUTHVIQlm5cRSJ7QR8rOEHMG75 +3B3EBXp3JtgDI4y2wyReFIcU02q7TBhS+oOWDJWxfadq6tzK7PKiONf2NGjxCOKB +MjO4koSMtqjcnRbBrgrEbpIkQqggfUjLnDZ5889YdbukxNt1a7RvnDQfaWJUwV0c +5UgcXvtkufJNvae/aIQ35xJJAGsgFNysW4i+c04vm61k/fDc4AylTyxa8+E4cphb +gsec8slzaG9pFqigmNz8jTLEUZ0bXAQeGQhPbCmhXL8zVkcYXQ9XVYl5vLCIaMjS +U/0fxIr9a+Rn6O+lWklgbr1SHJyOjOroqwbPwTm2sH8AuOUuypCHvPy/UuSfEAFc +cTDkrKtsFeLDYN2eUQaMlKq65Avbw4xk1cuX+u+1IE1jzQuMfCBz0yCfTP61amtm +FQ9W26bzIM4RWznYFrIZzTb92nHuSWBE5IHC37VTQgPBKu+o3mwou4ujgxK8tA9e +dSEK0AdzBCPkisuO1198hH4wbqxvrBpSeHhCX2aCnI0P6QJc/1fZqqMaFh3vpnB2 +28vTWSfjgwdZU1zcZL90R5nizrR4b0z3u8YWzxKdGMUMMuNCrjVqgAv6PFxZK3To +viwNEqDFHt9sGl9aacDg/bPAQntFddhUu5ThVRc/YfoVdBH5JjOKNPfKnThtyrEj +uf8bP0xUUd2Io4yLuBki6nUilXqv+2rYc7CJsdr758WfrJz8wRGB7mqqJ5sBHrsY +YBWIbGGayCXn2vzW1usY5i1xUcmH2h5X/lKvpgjrS5oBjYMRqqyL/oE4/SWpaVwV +jIRO7ZlIdx7pXW5UsCPJE1gxAyDDEQOy141Egwa3QzAtArUrOkdv1gXd5Ew6t2Lr +muYzwGckqPCjuJHpr7AFh/2EE1AnxXrSCzvifBqf6EWL4JOE3RfW5dblCc1ZyQZ9 +iuM6uIt4BFVSuNKpKHlvXwxDZebnZNb7XQe3HGGQiBkV/0OrTBW9BBR9hke7JEqO +9Ej2uiX1XIYHfxzZxw2kt41zVfSsaq7TlAweRcaXfHfAQ9nR7mROluD/iDVlS4Pm +QohE6gFlBvHPQlncIiWZKW2rLkvrJccDzq5ULmrtZARVEblX/bnUFX/2qOwqYRZJ +WMQACV2cUniaLaoFmdPybp55JeOZWdxfEil+Pn5uDxAI27MYv1dwKjhoz2uI3Kzi +YUzDC/0Im3TtBGmTn5ijxTkFXnA10D9P+uaJE536C4BGed1vR+zX7D1aqdZCuwQi +zLbC/cZ/KQdpd0aoKi9D2KLyfWpCSCdrpfpxRymDi5OisXwKBigKnlEmxxchOJvv +EcVaS3qr3VsdNKEwM8uAVQpQOO9MuOi74TOXhIbJ99mxEc4zD4t8kquAQljnFVBc +OP6eMfKM2LI6HjpE/296cjk7Hh2g1rB67o3RTkDdtUfomiQiktn/Vl+y9sr84r+C +pDaW3AkO2QNTkyvShagztcOIaP0rzJyLara0phVHryjv8Y42yhmLIHv0YLiYA48A +Bw7LCUBf9/YLy+QC3i+xPREboMPPAG4dopXRsO2TzLTlg6uqkScKyVzf0tlmQEv+ +ygtT0s/rRgwnIpEPwFbQ5KjJvVY6X1zbPY1LGDC9+LvKu/tvoFxV/RRmeprB626Q +fl72IpdErWcC4FPLYKjqEU0Tc9M4GeeqYsCo/vQ6sWJh5rLCNmc4Ux2epmSDgau7 +T6KCn6pWWrLNA9OqXZ7W2xx5dy1aKZ85CIMZFeyB+6wUeK1qeZXgmCx7J2reDdFK +F1GNmEdxJkx/XWO2+53qsf+pQ8dg6vOz3lsTxTLBhtKWlmxlMqlAu3pdw2Qwvlbk +z6mXgpl0CxVpLFGkvKq1W7xwoAIZRugazxzRLn75fEf7Dqwp3ouVlhw2I5ADna1h +ynmtDaUM9sZILrXXPqaSio3YdIK4OvhYYm+T5auXlQL301lLI9rS9aS1zFEM8KeZ +hnRXrJBM+hUabJCmxZqBcRRWSAc4dxD3Y32jijHyh9eYu5nhj2R4Q7wX+39OZBoI +JyTZYpW09h/7i/IEEY+w+VIgOxWstb9EWw0HOBetSudFSl3wEFUz3j2Z8Pa8wHDa +/gR3tEoIYkrjMgQqfeu33xIj4vA/+rCttS00UyOmK+EAY6j7c1C2fN4XgJRF60Ji +LYTEqpUyH0gEWllyEvl+Qp33ygDcn3hJfpLmK31qBpqjoQTQuUAc36lRaMiJdkC/ +63zYe7x/qJACalisi+5zQqzGabRfOXXJ+NlZTfMG46xv13ckapmkRgHLzBya/oUp +HzRcKXhnqHFTm0gLcgYRzYt36H6HQu6KHyTuTAnmlN3Yh+jyemkMZskL0OdDbefz +QhdtTFpeMIy7Z1I1v682qY/9FvwbVGGFxs5f/RkaLCrXAAOQTVwTckG1bs5eRD5u +RpCsSpnWH1xxCruxBEHI8OIntWiDE/KowaN1PUEwuGftOWkjoLRYn7weBsgdA1JK +EF2DWiYLSX5L//PLaEKEQFQjQM5N04cisI+N+Y8xtRTyZ076Oa4At8sHT7Y5sLCF +Hr2UvtaOAkfaub0lHEX+MBUAZaf1yhtUd9UX8jclGeDO5SehAfA3te0OfDAEJnEc +BE/a3oSL6GOyH9+LeVSWKIk1tRGUSh/vW5fLu+O2vsVWKV3R4zDhaIJIzYvWNram +tUcrzZ8jhxf3JXUq3Nt1NqTWEB5Sj+HI72t89I0tP2ZqntURrL0Rdoy8bsCCqAnl +z7tS1EGQtDz85jDlhsTarLROyvvgCllnWUBZA6KoGxSSks94ODdWU+0gKbpKqq8Z +35BA1SNl0qBl4YeSZA46wZZzuwlsNtlQhL82dq57QgxVcDlzPzHr1q931PW6Rf9r +ADET9gnByZJoKbbf3f9xMugOU0NyEZZKJ5SV9bxCMQ63WlD5xBXY0revvsJ42ZA1 +OBU1rpw/3RmZY0HbHuLdFjaFNgmWvS9dExtpE2GRKBxO/aqS7Cb2CEiSKWv6REgI +ERyu7qgyz07amjKH9ovhDNibMFqo10UUFHAwDJ0HnUdkXKFooJW/dhXH7gVdac+1 +4Ttr8hTOYZGS3Vfq6nnPeNvXjArc21932wrBsa0uPox+yQaZvYjP5W/5SB6ipmBY +eUtxNR75Eg/IkX/xZx5gPBUN5u62hypu+C4Shqiz+jpbajG9/FULAvRbhTUWDwqN +ctUV2svL71GFMt0F1K5H5tGqOcwSSBxR35CinAgSufjYRX5NF4vhIfy/S09OP69k +w3UU5aZdORjz1jlefTdrRTLN3sW4EBJ1sL0HIcsIGLuhLqwgsPqPYI6UjyoRSkGy +802eAPPj/xzwWhd6J842rjN3/3lLF0S90yHtIyKxilqmzDO2o+ai8CK9nzztbx/O +nAPczS/KXPyOAl8jP2dMeGBIRy4lbZus1eotMeUo9R+gJvmm8+5TUowrSrnWddXL +PCQsNs++V23vdC8E9n6E/enu63qBs2Q2K/eYo3ASLx3ssQnkWwNnhnbKU8e5qAak +sNTREHixc6NV8TQyd+TV4L2bKvfq7gVdiupV9nbI+fgaN8vbZHb0apgXyUVwoC0O +oVCsz21Izu7r9vVgeGNk+jCj7Fsd7ddHrqXLliIrYoB4koyyJHq9TB+A0+GqWh/n +5gIPeZGt4wMSul4lvMpZF+5aCv11lRaMpDNRkwf38pP1TErvjMxB+pwgjDFToB4d +6aBDLkmfl3BUgv/ucjzEEYCE3uuXgsxlZCdgyXT610PD+h0rhCSXoshcd6OFraBY +6B7u/+FAW33d4eDQ0LlEqq7NPvqeOfEodULDNvXeg7f7hM/lPKB1p5wOKbBwD8uj +QZXVWqTEBbTu5RJmZpaTQI/NzqWnVDeY318QP/i6MbTikRGjFIY44gWk9wgpcrCb +mG/ec1bFqwpfTVPcAI9ORKFTpGtuuwxOXBIVf451tJ+i1NNcCFfcWahfcSkYA7Ab +y8032FaHkb7yhXjduXe5yRaJe3AquGYGA55z76x+ZXgOIR1Ram4Pu3rnnY7AA56j +0hQtZlDVyXNdaw/Kdbs4OpNlt8uy+8mhgb3/bkFfhZqtocvqYuGRuxogyMnz1vLJ +EsDNVZXMogjWnlUtceFZ3YAIS53oh2F3AXu8QIcc5Q6x6TnxDIyeG7A42XS+LDXB +5xQ8pbsBnmlWkqwtXVRc02ULnzkiW8I0dSn/r32dpee27kRnptAo8w2tHxS+Zcdd +btG24zhnCFeajIXIBrmEZIIbKC393DK31beTjP3MQUwW1F7amcjrJSZJl8wUCwxD +Cmh6luUjbMHPRGxxgorIQ1XMNklYic5DbOcAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAUJDxIXGg== +-----END CERTIFICATE----- diff --git a/score/tests/test_vectors/certificate/ml_dsa_65_slot.kv b/score/tests/test_vectors/certificate/ml_dsa_65_slot.kv new file mode 100644 index 000000000..dc6c6423a --- /dev/null +++ b/score/tests/test_vectors/certificate/ml_dsa_65_slot.kv @@ -0,0 +1,3 @@ +[certificate] +cert_path = score/tests/test_vectors/certificate/ml_dsa_65.pem +cert_format = pem diff --git a/score/tests/test_vectors/certificate/ml_dsa_87.pem b/score/tests/test_vectors/certificate/ml_dsa_87.pem new file mode 100644 index 000000000..8a6387323 --- /dev/null +++ b/score/tests/test_vectors/certificate/ml_dsa_87.pem @@ -0,0 +1,612 @@ +Certificate: + Data: + Version: 3 (0x2) + Serial Number: + 37:db:ea:5c:9e:c7:7d:f1:05:11:b0:2d:de:e1:fd:cd:71:90:92:98 + Signature Algorithm: ML-DSA-87 + Issuer: CN=cert-mgmt-ml-dsa-87, O=Eclipse + Validity + Not Before: Aug 28 07:39:06 2026 GMT + Not After : Aug 25 07:39:06 2036 GMT + Subject: CN=cert-mgmt-ml-dsa-87, O=Eclipse + Subject Public Key Info: + Public Key Algorithm: ML-DSA-87 + ML-DSA-87 Public-Key: + pub: + f5:0f:0d:ec:e5:8e:a9:fc:6a:a1:4c:22:05:55:27: + eb:d0:96:ce:41:b2:c9:52:73:46:6c:99:b9:42:35: + 42:51:d4:28:55:d6:4e:d2:1f:bd:5f:13:8a:8f:66: + 9e:69:99:e6:24:06:4f:5b:5c:58:d5:3d:56:5f:41: + 35:2d:eb:43:52:3b:91:7a:2f:fc:da:49:37:1b:75: + 1c:5c:0f:03:36:7f:18:d1:67:c0:0c:a4:79:f7:25: + 32:ea:e7:c1:de:04:2b:b7:5a:99:47:0a:43:8b:ee: + 6b:9b:84:ba:1f:09:19:60:30:19:b4:9a:f5:5d:1c: + fa:95:87:04:ac:dd:ee:f7:de:05:f6:4c:39:50:a3: + 5a:7e:fa:9f:0c:17:6f:c4:71:89:34:47:27:b6:a2: + 64:9a:d0:ee:a3:1f:15:f9:e9:ad:22:bb:eb:bc:08: + d1:e0:ac:b0:04:63:85:0b:b1:32:a0:26:70:e4:34: + 7c:6e:86:89:ad:04:5f:31:dc:01:26:c0:17:b0:17: + 74:cd:95:16:6c:ba:2c:db:6d:98:46:eb:bf:36:ab: + 85:d4:6b:67:c9:54:c2:76:2e:4f:71:2f:41:6f:a6: + ec:71:fe:1b:54:88:68:3d:8f:08:7f:bb:89:42:d6: + e2:93:ff:ee:23:74:f7:0e:9b:d4:ba:ee:18:2b:aa: + b6:25:fa:6a:7d:f6:c5:3f:c0:4b:c5:7f:2b:01:e2: + 85:3b:cb:1b:5a:74:bc:d4:37:e9:94:88:70:9e:ba: + 61:c2:18:21:0e:48:96:32:6e:0c:08:9f:c9:18:3f: + de:cc:33:b6:15:26:45:45:97:0c:36:ae:1e:99:0e: + 0b:15:9f:0b:5f:ba:d3:ad:69:67:fd:af:88:e1:4a: + 8a:40:7d:51:2f:d2:cb:70:cd:96:16:89:f3:e6:07: + e0:51:f1:c2:c6:f0:86:b5:b0:0b:c7:7d:f1:8d:b1: + 0c:50:2a:47:5c:e6:65:f7:7f:8c:d9:bb:92:67:0d: + ef:f4:cb:5f:2e:c3:35:79:bc:dc:58:de:b1:aa:e9: + 27:f3:e2:c9:dd:94:26:8f:f9:54:6e:a4:f5:77:f2: + c5:92:2c:54:3e:0c:9a:89:b8:ef:46:a2:32:ae:08: + 60:08:e6:56:2a:12:80:4b:a0:78:f1:38:84:86:a6: + 94:68:67:ab:a0:61:ed:40:dc:10:88:c0:ee:a5:d5: + f4:e7:35:96:68:d8:ba:35:32:89:be:d3:48:ee:32: + 95:cc:ad:b9:79:44:1b:c0:5e:f4:98:9c:3f:03:ed: + 1d:44:62:eb:11:2b:05:cf:40:a5:84:7f:4f:c0:fb: + e2:c5:d1:ee:83:13:10:88:4f:f2:09:62:d3:a2:a5: + 79:1e:92:ae:68:5e:12:b9:df:66:0e:cd:ec:7d:3a: + da:97:81:15:f5:f4:68:ed:18:54:6e:f8:74:b2:aa: + c5:c2:01:9d:cd:53:01:a5:a9:7d:61:46:ac:a8:77: + 91:ce:35:e7:a9:9f:44:1a:77:31:bb:cb:37:06:b2: + d9:33:23:d0:a3:45:7a:b0:5c:f9:26:55:4f:8c:e5: + 5c:fe:f5:b0:b4:c6:60:23:73:95:58:00:8c:36:90: + b6:55:b0:05:6f:cc:ca:cb:07:36:88:0e:6b:30:c3: + fb:1d:1e:51:dc:20:f3:7e:63:d8:ee:07:09:91:8c: + 79:08:a9:62:0f:41:67:db:cc:6d:e4:c6:f5:b6:d8: + 80:2b:f0:76:97:11:46:cc:b1:58:83:cd:b2:90:b2: + 40:0b:65:b2:16:a9:e0:f0:c7:95:19:41:15:05:9a: + 04:5d:c1:28:8d:d8:87:7d:b8:95:4f:7b:73:e3:55: + 79:20:25:6b:d1:33:e9:db:44:af:9a:a4:d9:8e:66: + e7:b2:e7:2b:ca:c5:54:30:16:b6:a7:12:26:b6:d9: + 76:ae:93:a7:2b:ec:f6:2a:f6:bc:82:2d:13:b7:53: + ee:bb:22:43:da:23:e6:2a:42:96:12:de:81:09:ab: + 04:79:ac:8c:53:02:ba:a0:4d:45:e8:74:5d:e4:2e: + 8f:e2:25:1e:20:aa:13:ff:2d:62:b3:75:f1:fd:ed: + 86:cc:05:bb:d3:b1:2c:65:81:68:48:a8:70:d3:cf: + 52:de:2a:75:da:f4:a9:44:e0:65:14:11:5f:5d:bb: + f8:61:a2:0e:7c:c1:0b:94:a2:05:a4:1a:e0:54:ec: + 9d:63:06:1a:ac:fe:f6:bf:4a:08:76:40:d5:d2:49: + dd:aa:d9:39:96:9e:ec:47:81:c3:56:07:c0:77:e1: + 57:78:87:7a:8d:8c:1e:64:da:f4:a8:dc:6a:05:a2: + c8:01:5a:19:95:2f:62:6b:94:df:39:c9:18:c8:55: + fa:f7:c3:d4:61:a3:94:94:45:a6:51:15:61:da:af: + df:c4:96:d7:d9:2f:c2:e3:ed:72:29:4b:1d:cd:8e: + 91:69:99:51:fd:6a:2f:47:48:8b:3a:44:24:c4:8c: + 83:80:f3:98:10:dc:09:d1:34:21:d1:cd:aa:33:09: + 2e:91:02:c5:0a:ba:72:08:d4:44:4c:25:f0:be:93: + 87:3b:8e:8c:1f:61:f7:2e:46:f5:24:f2:0f:06:32: + 2b:3f:50:ec:42:a2:90:fc:a5:9f:5e:89:d3:b0:d9: + dc:57:fa:80:dd:62:96:4b:31:fc:cd:19:ff:62:01: + 42:82:c0:57:33:a3:aa:9f:a5:61:6d:f1:ce:2d:94: + ff:b2:8a:26:b0:ff:4c:0f:76:35:fc:52:ef:54:77: + b6:59:1b:5f:4e:3f:17:91:b7:ef:5d:9f:0a:0b:35: + c5:86:fe:4b:b5:ef:07:44:00:fc:c7:f9:fb:37:63: + b4:96:13:53:ce:07:2c:47:52:bc:0e:5c:1c:26:3c: + ea:90:cb:f7:b6:38:34:71:c5:06:f6:44:5e:57:13: + 88:35:a0:87:e5:cd:f8:e0:d2:05:b3:65:ef:53:8d: + 89:03:eb:0b:07:9b:84:30:04:29:3a:54:41:47:3d: + ed:4d:f6:4c:de:37:8a:99:55:ae:e4:17:a1:8a:13: + d4:45:59:2b:1c:2c:8a:2f:a3:b9:af:94:cb:70:f5: + bb:61:ad:42:63:c7:1b:03:1d:3a:c0:46:48:3b:4f: + 8f:10:bb:d9:0b:f4:89:bd:e3:60:28:dc:22:f7:55: + 04:49:57:22:5e:42:be:fa:2a:05:0f:e6:05:d9:d8: + f0:6a:62:6a:3f:3c:e1:2a:65:20:60:44:53:af:ab: + 90:6c:85:49:58:67:68:09:bf:d8:42:f9:4c:5b:ee: + ff:3e:ee:7d:e9:50:24:a1:93:a6:04:c1:01:b9:e5: + 11:78:0a:e2:4b:55:ba:9f:d0:0a:1b:18:00:4e:cf: + 00:a3:73:06:78:a2:7d:ce:b5:27:e3:99:82:ca:c9: + 6f:32:80:0c:75:f7:3f:d6:1d:a2:29:5a:be:9e:1e: + b5:b3:a4:09:7b:6e:be:56:93:34:de:a2:1d:15:65: + 3a:47:b4:25:5b:4d:46:38:4e:f8:69:7e:e0:42:23: + 2d:91:a3:7e:49:b6:dd:39:df:34:e7:62:3a:d1:37: + 88:c9:93:00:59:ab:10:0e:42:55:04:c3:75:ee:6d: + d7:63:21:18:e7:c5:bf:b4:f8:c5:59:e9:ef:35:7c: + 46:9f:66:5a:78:12:d6:d7:37:cc:c1:4a:26:ae:1b: + 68:9a:ba:c9:d5:05:93:63:d0:6f:7d:2f:43:25:0b: + c5:92:bd:82:16:4e:d9:2b:62:cf:4f:4f:a1:15:c4: + 4c:04:f0:a7:c3:24:05:2b:a7:39:fa:2f:33:32:5b: + 8e:e5:a5:68:5f:13:22:3b:10:82:a5:4f:a9:28:15: + 2e:30:89:ee:4b:b8:22:33:3a:b7:8e:17:ed:ec:92: + cc:3c:01:30:a7:01:ab:76:2c:4a:ba:77:4f:c5:61: + 69:e6:36:bc:7c:9d:a8:5e:27:bc:97:26:8c:4b:c0: + 88:2a:e4:f5:bc:12:3c:4e:6d:2e:23:d4:e6:86:d9: + af:fc:19:0b:5e:a9:98:cf:c7:fa:99:cd:97:af:a6: + 20:5a:2a:c7:fa:db:dd:6b:c4:03:77:1d:47:15:44: + 05:27:ba:cc:ac:be:7c:61:43:89:4b:5f:b5:ac:64: + 9f:33:42:bc:11:eb:98:56:96:a0:14:1b:e3:2c:1f: + 8e:14:d3:f5:04:04:51:57:1b:26:49:59:cd:24:84: + df:23:63:ef:ea:36:32:c6:bc:53:91:42:32:1b:95: + e2:19:7b:83:ff:aa:32:83:d0:3d:b2:94:99:d3:f2: + b8:a8:38:54:0f:a6:d2:b0:e6:4d:14:68:9a:15:c9: + fc:7f:a9:d4:ee:10:ec:85:ec:07:b1:2d:0a:47:39: + 2c:81:a4:a5:80:74:7e:4c:19:e1:82:4c:81:1f:18: + 6c:1f:38:5f:c5:2b:28:89:7d:fe:6e:0a:e8:56:47: + 73:98:40:6b:1c:ab:0f:70:52:21:d8:d0:12:32:54: + d7:3c:13:88:58:08:af:69:d8:9a:44:82:7f:ea:de: + eb:e5:e6:3d:2c:bc:cd:85:62:92:45:59:22:3c:9d: + 01:80:db:40:ee:4e:c2:f9:cb:91:db:f4:ad:6d:64: + 49:0e:75:5e:ca:43:55:cb:4f:3a:0e:19:40:13:00: + c1:e4:c8:c9:f6:3e:60:fe:dc:7d:f0:13:3c:1e:81: + 24:77:99:65:82:dc:7d:b0:45:3f:e0:6b:67:25:c5: + d4:2f:70:90:03:b7:fd:e3:27:41:6c:b6:95:ea:39: + e2:6e:ad:1b:93:3d:8b:93:63:f0:cd:5f:09:09:03: + 88:5a:5f:f2:fd:92:ad:98:12:98:01:92:d0:75:b8: + e4:5d:54:16:eb:a3:8b:b0:07:f5:e2:1e:36:70:88: + 4d:80:6b:d7:f2:f7:bd:d4:4e:e9:5a:75:58:13:7e: + 01:26:92:52:77:7e:4e:5c:44:83:c7:92:6b:f8:45: + 55:4e:2b:11:ce:3f:b2:57:2a:bf:e9:6b:a8:1f:b3: + 97:8f:35:23:36:7f:db:da:d5:00:d7:4c:2a:ae:70: + 80:bf:0f:af:bc:03:4f:5d:32:a2:23:65:02:c5:2e: + 48:f6:15:74:e4:de:47:01:58:13:5a:9a:12:51:4b: + d3:1f:84:d9:04:66:1e:52:14:6b:11:16:ef:d2:17: + 00:59:c1:5b:b7:a5:90:cd:e3:5e:a4:50:56:62:51: + 11:4e:0e:53:e4:7b:47:7f:ec:fc:42:d3:4d:aa:1d: + c3:b6:81:b7:1b:e7:5d:e3:10:be:77:1c:83:95:fa: + 2e:56:ee:0d:4d:21:d4:0e:84:9b:c1:4a:36:05:7b: + de:9b:3b:1c:61:2c:cd:cd:2a:ba:ad:99:99:66:43: + 2d:29:4a:d9:c0:de:77:a4:45:09:ee:46:69:88:ec: + fb:2c:96:7e:64:fb:6b:bd:73:2f:a8:00:02:d5:c9: + 2a:06:0c:4b:07:c4:1f:d7:2a:62:03:a6:a7:48:5c: + 85:80:af:cc:d7:b2:ac:a1:70:ab:35:7a:f9:1e:7a: + 7d:9e:78:04:9c:9a:22:9a:0a:81:a7:74:7d:10:00: + 62:48:3a:d6:52:0a:e8:51:0f:a6:43:7d:10:6f:4a: + e7:7c:0c:db:da:13:d7:e9:9c:af:64:b2:5e:d0:8a: + d9:7d:fe:61:4f:14:11:22:e9:ee:01:07:33:06:4c: + 49:36:9d:79:fc:18:24:48:57:10:b0:b9:91:b5:55: + 2f:1b:ec:2e:fc:7c:b9:9d:08:14:a1:45:0c:1c:f9: + 17:3c:46:cc:68:79:fa:57:19:a7:fa:44:e9:39:c5: + 9c:7e:99:0c:c6:ff:c5:d3:b4:de:26:fb:77:88:5c: + 72:ca:df:ea:da:6c:f1:7a:fb:68:fd:1a:bc:39:10: + 39:19:18:4d:f5:a3:5b:2c:24:c5:03:5d:d0:91:1c: + 74:ac:c8:c3:60:a8:9b:c6:2d:cd:34:9c:eb:32:a2: + 7b:15:9a:8c:d3:8b:34:7c:a9:c7:98:ef:ca:57:a9: + bb:28:23:f4:7f:ab:b3:c2:41:ea:b7:a0:4b:ab:7b: + e5:49:07:8d:2f:97:32:3b:c6:fd:58:98:1d:cf:18: + d8:fe:a0:d1:cd:f6:25:db:eb:5e:64:e8:d3:31:45: + b8:c8:dd:3a:6c:d0:71:7e:ff:c6:f8:1b:c7:07:68: + 57:3b:a0:10:9f:b7:67:33:d4:f3:52:23:0d:4d:ff: + ff:40:21:a3:1b:18:d0:30:66:2d:b8:32:32:ce:28: + bc:b7:5a:61:ca:74:53:30:50:86:87:51:88:43:cb: + ba:33:73:25:d7:be:12:24:38:5d:1a:42:ab:34:fb: + a5:c4:08:09:bb:24:11:5f:ae:b6:de:13:95:2f:27: + 0b:fe:aa:b1:0f:50:ef:15:08:84:f8:fd:cb:d0:96: + ee:04:b2:66:62:e9:32:88:4b:88:06:10:80:99:86: + 5c:2d:7c:76:92:f4:1a:e2:80:b5:6f:61:22:90:5e: + bd:e4:10:bb:1f:40:59:7d:58:85:da:98:4f:56:63: + 70:ee:e1:42:a6:c2:db:15:f0:5e:7a:8a:11:bc:61: + 98:68:fa:ea:fd:24:2d:f7:e4:1b:18:c8:df:b2:9b: + 2f:5a:2d:88:90:10:d3:0c:a1:f9:ee:d0:2d:15:e4: + 80:ce:3e:1e:16:18:49:a9:10:d0:96:58:bb:2d:47: + e7:78:08:30:12:cc:f9:d2:16:3e:7d:39:a4:6b:04: + 57:3e:75:62:2c:7b:ce:80:5a:1e:22:7e:8a:65:04: + 9d:26:aa:c6:01:b6:8a:f9:06:3a:0a:25:13:b4:b3: + 15:d7:3f:38:86:4c:14:32:ba:c3:d9:3d:d1:33:eb: + d9:4e:19:16:59:f6:9f:03:bc:7d:e3:0a:16:c1:80: + 3b:66:72:f0:1f:11:3d:2f:8d:97:5f:f0 + X509v3 extensions: + X509v3 Basic Constraints: critical + CA:TRUE + X509v3 Subject Key Identifier: + 6B:D9:CA:34:AE:38:56:D9:EA:EA:AA:B0:41:35:EF:06:77:11:AD:4B + Signature Algorithm: ML-DSA-87 + Signature Value: + a8:4a:d6:cb:a5:0b:2f:ac:be:2f:ef:00:8b:fb:34:02:58:6d: + 4d:87:4e:6c:51:b3:0d:51:10:c4:fb:9b:2f:6f:c3:b1:f0:02: + 53:40:42:70:94:a8:9b:1a:98:2f:3d:76:68:6d:2d:0f:59:24: + 31:f8:1c:4c:ca:d0:0e:ba:9b:24:2f:e2:09:05:fa:0d:29:aa: + 9a:ae:36:10:ae:df:f6:91:84:9a:42:c3:9d:b2:a0:67:8c:70: + a0:1d:85:cf:a1:a2:22:c9:6d:3d:18:14:dc:d6:59:f7:4d:71: + f7:19:0a:61:46:12:dc:3d:23:b9:6d:35:48:fa:05:e0:40:a8: + 97:f1:18:24:d7:6a:02:f8:90:d2:ae:3e:ab:9d:f5:e9:f9:ed: + 69:a0:b2:dc:02:d6:d1:5b:11:80:78:d7:2d:f8:b6:0b:5a:3a: + a8:f7:c8:f7:c1:87:96:af:13:78:18:02:73:37:41:1e:a7:05: + 27:03:d0:4a:4d:7d:25:76:88:9f:78:90:4a:55:4d:74:4e:2c: + 16:cc:00:d5:ae:79:a9:23:1e:bb:2c:58:e7:b4:9d:d9:d4:11: + 33:d3:ab:62:87:59:19:df:e0:2e:29:0b:25:a5:08:05:7e:cd: + ef:fd:98:0a:b3:5f:f1:fa:0f:1a:96:14:e7:50:49:b3:4c:e6: + 7f:77:ea:d5:21:c3:89:5f:30:be:3a:08:48:b4:dc:48:99:94: + ad:47:fb:44:e4:19:59:b9:47:26:de:c7:f5:d4:ee:3a:67:6a: + a1:71:0f:b4:96:82:ac:cd:d8:c1:fd:76:a7:38:fb:03:72:89: + 3b:d1:16:c9:2b:3e:18:bf:2e:4e:c2:b0:ff:07:67:b3:ed:07: + 72:80:22:81:0c:cc:98:2c:74:a1:c0:c3:be:8f:ed:a6:30:10: + 8f:e5:54:12:87:82:fb:a2:ad:64:12:af:2b:93:17:db:01:27: + 6b:42:bf:8d:a4:f3:29:d4:03:2c:58:45:e4:b7:1c:25:b8:33: + d4:54:2b:75:93:64:fa:40:15:47:0b:58:2f:96:db:36:fd:75: + 25:56:da:2f:5c:28:88:0f:ef:d3:cf:16:95:5a:54:89:c6:31: + 07:ed:ba:ee:b4:c0:5f:e7:bf:e7:bd:f5:84:53:78:c4:40:11: + 04:6f:0a:5a:2b:7d:db:38:7e:74:71:d7:73:8c:88:71:a9:b5: + 97:c0:1a:0f:3a:fa:3c:d8:2d:19:f8:41:9a:79:67:8a:ef:f3: + d3:0a:d1:90:53:25:94:9f:43:dc:10:5b:28:9c:79:ee:92:60: + e0:79:c4:6d:08:5f:88:34:8b:5c:68:1c:b0:ce:82:bf:f5:76: + 2a:42:57:93:72:d4:17:35:55:b5:71:61:2c:5d:59:b9:eb:fe: + 58:ad:ea:de:23:f4:19:28:82:4b:fe:49:50:a8:ba:ba:33:4d: + 11:2f:aa:9d:41:81:3b:b3:7f:ff:c4:d3:a2:fa:92:85:cf:00: + bc:eb:19:6a:01:6c:3b:c3:9f:e2:c9:ae:0c:6e:b9:15:64:45: + dd:4d:50:7e:6f:ab:54:41:21:54:dd:57:3b:10:a2:97:89:02: + 00:07:05:33:bc:0f:62:5f:d6:8e:98:43:89:c1:e9:04:be:28: + ee:14:94:1e:95:59:6b:00:05:f6:ac:e2:48:18:36:0c:1c:0c: + 12:7d:02:fe:49:f5:56:da:d0:62:7b:a0:fe:83:fa:ca:49:b1: + 0f:dd:39:45:1d:49:c4:70:01:fe:e6:c3:a8:87:79:48:3b:99: + e1:0d:90:5c:4c:68:43:96:8c:83:b3:a5:a4:48:c9:df:cb:b7: + a8:80:49:0f:b2:04:9c:04:ed:f3:4e:4b:7b:9e:3d:95:6a:1c: + 45:05:ae:5d:61:e9:21:ec:0a:66:bd:65:2b:ff:fa:97:3e:7f: + e5:af:0a:35:42:f4:da:cd:9f:31:82:3c:c4:fa:8e:72:44:35: + ef:02:bd:a4:d5:2e:94:14:a2:ae:52:6c:c2:17:07:f3:f2:a9: + 96:12:b8:3d:07:5d:d7:32:ef:46:ef:4b:07:10:75:e7:e6:cb: + 83:74:de:0b:db:00:ab:c8:d6:7e:a9:e7:4b:83:84:f9:01:3a: + 13:2d:d1:6a:c9:df:e2:72:d9:4c:13:5e:15:7a:5e:e8:5a:90: + 5b:75:4b:f6:bc:f2:ed:f2:04:e1:2f:dc:2c:58:f8:df:6e:3c: + 69:1c:c3:6b:36:3a:57:86:e8:48:ff:7b:e1:fe:5f:59:7d:36: + 76:45:a3:17:a8:a5:f7:2b:ad:20:74:5f:cc:f5:49:e1:8e:17: + db:a5:42:e2:e0:8e:57:ca:76:3c:f2:c0:db:7f:ff:74:08:ea: + a1:71:93:f0:b3:40:8e:c5:d0:c2:8c:ac:b7:c0:68:68:5a:d7: + 2e:dd:3d:3d:d9:9b:f0:a7:49:42:c4:7e:1e:95:e2:ca:7f:ae: + 81:b6:49:25:04:a9:96:ed:51:8f:e8:f2:53:e5:0e:0f:6f:48: + 59:14:87:d7:9c:91:0d:05:06:ca:db:81:1e:f7:05:38:ad:14: + 25:8d:40:2c:94:f3:f9:42:1c:ce:b6:24:1d:13:6f:15:23:db: + f1:6d:72:53:32:19:e6:23:51:e3:eb:1a:ca:a0:f0:76:db:3f: + a4:e7:46:10:21:2f:33:a0:40:75:43:59:9c:42:3e:aa:6f:bb: + ba:2e:60:78:09:05:9d:21:34:53:59:ae:1e:2f:6e:76:11:bd: + d6:57:b9:34:f3:c4:be:9a:94:52:cb:36:02:47:6d:7c:98:c2: + 2d:bf:bd:11:8f:db:9d:4f:89:f5:57:b8:fd:51:99:29:7a:c4: + f0:bf:7d:7f:eb:4e:e8:19:66:62:19:08:56:25:16:0b:02:03: + 61:6e:db:88:09:06:f6:97:18:78:57:a5:f0:dd:b2:3c:9b:35: + a3:ed:06:d2:f7:1c:9c:68:50:99:32:31:3e:4d:72:72:16:c0: + 12:0e:95:2d:c0:cf:92:a3:64:82:f0:69:60:76:26:2d:f0:b1: + 54:7c:09:59:78:62:42:cf:2d:fd:62:dd:31:ac:1d:b7:7c:0f: + a0:a4:63:1e:90:24:b6:a1:2e:4f:bf:0c:bf:0d:74:42:23:de: + 3b:08:30:d1:f7:27:85:a6:cc:9e:f9:db:39:20:05:c0:7d:6b: + 4b:91:27:78:3c:17:89:50:00:ff:52:d8:ea:00:7c:b9:86:6d: + 61:00:4f:da:a9:93:5c:31:69:3e:2a:a8:8a:4f:31:f5:8c:55: + 5a:79:9e:ba:bd:f4:fb:08:eb:fb:e5:d4:98:ff:48:30:dd:61: + f0:8a:70:56:d6:b3:9c:56:de:6a:a5:51:13:3b:72:16:78:2b: + 8d:a7:34:89:af:a1:3e:a6:36:12:84:98:aa:2e:38:07:a6:93: + 52:22:51:c5:51:f1:a3:0e:f2:3c:c5:87:63:69:4a:e4:1b:8f: + 2a:4d:dd:77:09:85:26:13:1e:be:21:fd:c4:a7:02:bd:a9:e0: + 3d:39:50:e3:89:39:38:4c:f1:d9:2f:99:32:86:4c:f0:dc:51: + 2f:63:37:e5:4e:9b:da:03:eb:90:51:14:78:71:ee:f5:17:47: + 52:fe:16:b1:c4:ce:5d:3e:68:db:b4:a0:ac:15:e6:d2:32:51: + c5:9c:71:b1:f1:dd:75:ec:42:b7:4a:d3:24:5c:1b:d5:6e:d1: + b9:06:7d:33:94:d3:81:46:81:bb:b8:bb:84:66:4a:57:bd:26: + a6:02:5e:c1:4b:14:7e:fe:50:62:78:5c:50:37:05:ed:ef:24: + 0e:a0:2a:df:80:bf:e2:88:67:96:b6:5e:b7:d7:26:65:17:3e: + 5c:77:2f:5d:db:f9:94:b5:bb:46:17:39:76:20:c8:8c:2a:99: + c8:df:05:68:74:9c:e2:a9:a2:71:f7:b0:0f:16:76:50:c7:34: + 46:c0:c2:fe:1e:a5:fd:0d:dd:58:6d:de:4b:23:68:91:e6:92: + 75:9e:d8:74:12:ba:9b:fe:5b:16:71:d9:6a:dc:05:77:f5:45: + 7a:6f:de:99:93:ca:15:bd:c4:80:97:9f:e1:17:d3:3b:f1:d4: + f4:86:f0:46:10:60:ff:77:d7:6b:b2:c3:c6:b3:6f:b7:62:1d: + 09:5b:a0:a0:aa:0c:06:c7:84:c1:ee:6d:a6:a3:dd:ad:89:3c: + 94:51:76:dd:9d:27:92:c0:ec:31:9d:af:4c:83:7a:b1:3e:3d: + 69:79:90:82:7e:84:7e:29:6b:de:0a:eb:06:0b:3d:25:a5:54: + 4d:ce:d5:6e:7e:43:08:95:ba:cd:00:4e:95:9d:c3:82:a0:63: + 78:60:89:a5:1d:36:56:0f:0b:27:4b:46:02:bc:ec:89:32:4f: + 26:47:df:02:62:e7:57:ad:a6:5e:86:64:9f:1e:b6:bb:87:22: + d0:a8:55:e0:a0:a4:bf:d5:3a:e7:51:c2:dd:d4:c2:5a:0e:e8: + 40:91:44:55:78:13:0b:ab:7a:b2:ff:68:7e:06:0d:49:37:57: + 63:68:c0:4f:37:e8:82:82:42:b2:5f:99:6f:b8:7a:1c:c0:ce: + 77:2d:d4:cc:12:b9:d0:67:84:b7:6c:83:22:94:89:87:d7:cd: + 02:6f:46:82:46:73:23:5b:b1:f6:47:7f:a0:ad:53:00:75:da: + ca:92:97:cb:49:0b:ab:7a:e2:b6:2f:77:a8:2e:b0:4c:a2:6f: + 2e:38:4b:e2:13:56:cd:56:62:91:da:9e:e7:ff:bb:a3:5c:df: + a2:05:bf:ef:8a:d6:b9:e6:f1:63:21:d9:b5:1a:d3:a5:6b:fe: + 5f:ac:80:a0:4e:dc:5f:96:62:d4:3d:86:45:f1:c2:3f:e5:6a: + 29:75:4a:1e:50:77:ad:7a:49:74:85:ca:9d:d6:43:9f:63:0a: + dc:c5:6d:41:b3:8e:09:e7:7a:0c:c1:a1:48:a5:91:dd:bb:d1: + 17:80:d4:6a:59:08:99:98:27:3e:a6:e3:c6:8e:3b:b4:da:85: + 56:4f:89:36:38:65:a8:34:77:f9:3f:bb:d3:23:82:65:8d:54: + c2:95:b3:c6:8c:94:49:1a:ab:06:47:30:e5:0b:33:ff:15:3e: + ea:b4:d2:8c:8f:b8:97:89:aa:cb:44:75:5f:f2:07:11:de:d1: + 0c:64:2c:0b:c0:55:18:3f:7a:44:a2:c6:54:af:cc:db:48:1b: + 23:ad:02:11:56:16:b2:45:95:32:00:ba:df:14:1d:c3:1f:7e: + 8f:9a:28:13:22:c5:f1:a8:6d:55:ee:ef:a3:6f:01:b0:3f:1e: + e1:ee:55:c5:ca:be:1d:0e:b5:fd:2b:aa:91:9a:fd:1c:af:62: + 85:90:a5:79:44:46:30:17:58:86:a3:36:3e:a2:e7:3a:b6:5e: + a8:42:e1:f1:b4:73:6d:59:b5:22:03:06:3d:f7:31:e0:6b:97: + 20:e0:72:ab:0d:ed:f8:51:13:df:75:f0:ea:ef:6c:db:87:73: + ee:f8:76:c4:d0:45:64:f7:e0:6b:69:70:de:f1:f4:92:1b:67: + f7:8b:96:43:74:ce:63:cf:4b:d5:8f:67:6c:24:02:7c:6f:72: + da:72:29:74:25:38:ef:05:86:9c:93:43:fe:e4:88:0a:99:25: + 6d:9b:6e:a4:8f:f4:dd:a0:2f:49:ef:20:19:97:4e:b8:33:b7: + 19:b8:56:76:ec:55:b9:eb:41:bb:c3:d2:34:3e:c1:93:92:6e: + 5a:9e:55:a9:87:ec:38:e3:59:94:db:d1:8f:36:aa:16:8d:87: + d0:66:ad:60:d2:7c:8a:05:07:b9:f6:d5:86:28:45:23:d9:37: + ad:49:97:bb:aa:16:6f:dc:a0:95:10:dd:57:51:db:03:f8:01: + 7c:5b:04:15:fe:36:e1:1a:5b:e4:d5:01:3f:be:42:39:63:a0: + c4:4d:2a:d3:f1:dd:07:b9:6d:c4:ac:21:a1:e3:74:0b:e8:c1: + bc:e5:b5:54:5a:a5:3f:cc:f3:c4:8c:fa:36:3f:a9:2d:b4:de: + f2:5e:32:d9:75:b8:e9:cc:3b:b3:cb:cb:a3:2b:f1:c8:8c:df: + e4:ef:d9:b1:bb:21:78:b6:c5:67:0f:62:ca:b9:49:81:15:c3: + 97:a2:63:dc:4f:7a:fc:26:dc:7d:b3:77:b3:85:fa:fc:ac:a4: + 14:06:7e:dc:68:15:0d:6d:4d:92:74:91:76:ec:41:19:c7:64: + 57:4a:25:65:b1:8e:bd:26:7e:8f:09:a1:7a:bb:c0:00:12:7a: + e4:bb:0e:15:91:3d:be:1f:d3:c1:94:da:f3:80:cd:82:d9:e5: + 9c:77:46:f3:e0:17:a9:31:63:10:f7:66:24:26:d0:8e:82:ab: + e8:db:e2:d8:72:6f:e7:c4:d7:ba:3f:44:8c:a4:09:ac:d4:07: + cc:6f:e7:13:f4:29:a4:33:ca:b8:84:14:08:63:d9:6b:0c:ec: + 9b:ac:fc:c2:c5:59:2c:98:1f:f1:6a:63:35:b5:46:fc:77:a8: + 6e:32:55:d8:fe:dd:33:23:8d:e8:68:95:27:d9:67:e0:af:fd: + 3e:28:ed:a5:92:dd:f4:6a:3a:b0:7e:46:76:da:53:99:e4:c1: + a6:26:e1:b7:7d:f4:56:3c:46:13:b3:a1:a7:23:23:68:07:f3: + 2c:32:8e:bd:01:58:da:a5:0d:06:c1:f0:28:b7:58:bb:40:23: + ef:08:ef:f4:2f:17:27:e0:86:7d:ce:5f:74:a1:8c:78:94:91: + bb:73:87:63:b4:96:c8:ae:67:e8:6b:c4:0c:9f:4c:cc:b6:c2: + 05:00:bd:10:55:66:85:2e:58:78:e6:12:ea:8a:b9:1d:67:7c: + 25:ef:6f:d2:72:f2:5d:22:81:fc:dc:7e:28:3e:a2:0a:6f:b0: + da:28:2b:ad:6c:55:10:da:2d:85:c2:5d:f0:f1:6b:b1:3f:ea: + 74:d9:4f:b5:54:a0:29:d4:1e:30:a1:c6:80:b1:ad:74:1b:dd: + 38:37:b2:c7:ef:18:df:c7:a8:19:e0:1d:8f:3f:bb:43:c3:7b: + 8e:49:af:07:0c:41:3c:30:5f:57:0c:8f:4d:2a:eb:58:3e:47: + 27:af:6a:18:12:54:9b:28:10:57:c9:82:51:f9:d2:82:93:0d: + 1e:40:05:41:4b:93:7c:9c:ca:4a:91:82:ea:f4:2a:26:f8:e7: + c7:c7:e8:f4:7b:da:63:a8:87:f2:5a:65:54:32:fa:11:6e:97: + 81:65:0c:e9:d1:d7:10:7b:c9:9e:d1:27:14:3b:25:7c:a5:d7: + 62:a9:4b:5f:58:c0:51:d7:08:c0:c2:e0:84:de:a4:84:68:0c: + 0f:20:b2:6c:01:63:f0:f3:69:a1:a2:b9:a5:02:d1:d2:12:f4: + 8f:6b:10:7d:41:b8:ae:ac:85:21:16:e9:dc:ea:27:17:e9:ba: + 9d:d4:0d:b8:ea:18:50:d5:8a:be:51:7f:22:f8:1d:36:f0:f9: + 56:38:24:39:d5:f8:95:c2:21:34:da:2b:39:92:3e:0b:09:1a: + bb:d2:23:d6:37:e8:97:ae:db:a8:ed:8b:54:1a:16:9b:fb:a8: + dd:b8:bf:be:a0:c5:20:52:06:c0:22:ca:fe:17:e3:9d:50:0b: + 06:11:b7:c6:7f:58:e7:14:50:50:53:9d:51:cc:38:a9:b9:de: + c4:5b:a8:6f:93:88:a1:3e:ad:3e:fc:9e:f9:28:b5:79:06:f3: + 0a:05:c4:30:f5:fa:75:63:0e:7f:d2:f9:f1:b5:3b:8f:ce:56: + 60:28:c7:5e:d1:b3:93:33:2f:93:95:dc:d0:2e:80:6c:a7:78: + 34:99:69:3b:82:67:cd:19:84:83:1c:27:f6:62:fe:e5:bd:52: + 9b:40:27:28:81:ff:b6:88:35:ea:bc:7a:8f:bd:be:b0:75:7d: + 01:2a:31:6e:2d:b2:9f:c3:a5:4a:cd:db:02:f2:c2:20:9c:6b: + 7e:0b:66:c7:12:59:9e:2f:0d:42:f5:cd:f1:41:0c:2f:a6:56: + 47:c3:c4:0f:7b:92:4b:8f:d0:5f:0b:11:f4:da:7e:ab:f8:84: + a1:f9:e5:9b:3c:4f:56:a2:f2:15:ec:87:77:a0:cc:7b:44:a7: + 23:7e:d6:04:c7:c1:43:b0:e4:db:61:b6:4c:3b:fa:9d:25:09: + 4d:81:fb:f3:9f:e7:e7:08:18:85:f6:af:e7:b1:93:c7:c2:29: + aa:64:76:5a:da:c1:11:64:e1:12:e3:f5:82:07:17:d5:3b:60: + 5d:48:60:fb:ed:6f:b3:b7:17:de:0e:75:6f:64:2d:a1:94:4d: + 46:01:d6:6d:96:46:c9:90:82:61:c3:0a:2d:69:ff:49:e7:52: + a1:a1:40:1d:06:2f:5e:16:c7:23:d2:7b:88:b5:d9:71:fe:e7: + 7a:07:1c:d0:26:a4:cc:a7:6a:90:7d:ee:7b:04:8f:56:39:48: + 4f:aa:73:03:ad:d3:76:c2:ac:ca:3b:e2:d3:e7:83:4c:e6:4d: + d5:d6:d1:aa:54:35:dc:62:c7:c7:71:02:a3:bf:f9:62:79:66: + 39:97:5b:c4:4b:c1:c7:02:7b:33:4a:bd:ee:91:e1:bc:96:71: + 73:53:05:44:28:67:03:66:2d:26:c0:ac:fc:9d:95:30:ba:93: + c5:a2:43:5a:8c:6c:84:76:5d:04:42:dc:da:bb:69:14:90:3e: + 8a:5f:57:7a:84:81:51:b5:a6:7b:fd:de:35:08:43:77:22:e3: + 5c:f4:63:38:72:11:8a:45:e2:eb:5c:af:cc:eb:45:5a:2a:ac: + c8:32:bf:ac:20:81:5b:f0:e7:03:1c:f6:24:49:1b:0f:3d:ee: + 5a:76:1c:bb:b7:b7:f3:ea:6a:e3:17:29:d8:03:cd:40:b7:8a: + c6:0d:69:df:73:ef:84:e4:57:63:56:55:95:81:34:6d:b9:ca: + cf:60:60:b0:3f:80:1c:c3:72:ff:8a:17:bf:1c:64:a8:bd:3c: + 29:cb:d0:b7:9c:52:ca:fd:d9:91:55:13:f0:9c:d6:37:34:e3: + 5e:fa:cb:44:0f:3c:37:31:ed:9b:ee:8c:43:84:98:4c:c4:5b: + eb:7e:7b:ed:9e:f4:25:ba:9c:ae:76:35:41:86:94:9f:59:51: + 36:c8:7a:48:27:74:fd:c5:16:da:93:48:b6:9d:9b:d9:20:b1: + ee:54:84:5b:f6:36:4d:34:1c:1c:be:e0:84:2c:ba:94:fb:01: + 1b:7c:ab:57:f4:07:32:40:85:f7:ea:4a:be:50:c8:d1:d4:99: + e0:be:6c:e5:47:10:01:e9:9d:03:92:cb:31:a5:42:a2:1c:91: + 88:42:d1:18:76:06:41:3d:3a:56:f7:67:d0:a3:37:ef:77:b7: + 7b:d2:21:ac:9b:ef:84:e6:61:d8:78:26:17:6c:05:ed:2c:9e: + e0:c0:3a:e2:66:0e:4c:d6:ad:94:fb:97:d4:89:66:da:2b:40: + db:d2:14:ba:45:9e:72:b7:5e:68:89:67:6f:e7:30:be:ca:3a: + 91:45:3e:60:38:30:fd:90:8b:e7:a7:fa:25:35:54:c5:b3:cb: + 1d:ff:25:f5:04:74:ac:13:35:36:95:78:a4:a8:7f:2c:63:6d: + 19:eb:ac:2a:e4:da:f4:86:5f:dc:e4:8f:05:e8:a3:55:eb:8b: + 19:15:92:95:79:5a:28:a5:42:23:7d:d5:c5:e0:66:fc:4a:20: + 4f:4c:e9:33:1d:a3:f9:04:4e:b1:cd:47:3b:18:a9:05:9d:10: + 79:a5:8b:f0:7d:94:e3:23:eb:20:4e:81:64:b2:02:ac:fa:30: + d4:ed:4f:67:aa:59:85:5b:ab:9d:23:ef:a1:be:b3:da:9e:91: + d2:b9:65:29:ed:4d:d4:26:c2:d7:85:37:b3:75:f4:90:3a:cc: + 97:09:d3:6e:26:b8:00:1c:1e:cf:d1:33:45:10:71:c9:33:f2: + 4f:8d:43:b3:0a:c3:da:dd:73:4e:f3:03:f5:72:94:28:bd:34: + 74:26:f0:ec:95:80:d4:d2:1a:24:e1:e6:d0:4b:e6:a2:e2:f0: + 52:ca:a9:6c:05:02:9d:a5:de:97:2b:28:88:19:55:6a:85:67: + 2d:32:b0:e4:3c:35:3e:3e:8c:c5:24:7c:91:03:a3:b7:d0:8b: + 37:02:81:a5:bb:9e:85:38:a6:a0:59:5e:1a:5f:8e:ba:93:be: + af:b1:bd:96:9e:df:99:19:8d:e9:45:cf:ef:a0:f3:ad:9a:a7: + 1b:86:23:60:d9:f3:04:86:cf:a7:c2:f5:08:9c:72:da:7d:20: + e5:73:ec:f7:13:50:d4:b9:6f:71:4f:3a:68:c6:5a:46:c7:1f: + 56:3d:f2:dd:ce:2b:25:be:e8:33:23:43:a9:55:39:c9:b7:56: + f6:d6:14:38:39:5f:cb:e9:91:28:28:dc:bb:c4:8a:ab:76:be: + 70:58:b9:34:0e:f3:90:61:93:6f:60:44:29:44:00:0e:fa:bd: + a4:fb:8f:71:fb:aa:e6:38:d7:03:27:f5:e4:f6:48:dc:4d:2c: + e8:e5:1b:64:e9:54:1f:9d:3f:fb:b4:05:46:85:85:a4:de:ce: + f9:10:8f:c1:1c:27:cf:6a:3f:ee:53:13:24:59:43:74:4b:ce: + 19:a4:fe:ad:bb:dc:e7:f6:4d:8f:8b:fa:5d:e7:c5:17:e9:56: + 12:98:ba:87:fa:c7:64:a2:5d:0a:8f:94:0e:15:3f:aa:c6:99: + d0:c9:0c:4a:11:ed:f2:14:9e:b0:92:82:fc:e7:15:89:69:cf: + 0f:90:18:18:99:9c:1a:4b:48:26:fa:e8:89:fb:c2:d9:9d:d5: + c4:92:db:b7:88:bd:17:f6:82:e8:32:7f:4d:40:da:07:a4:5c: + 6d:09:fe:8a:a2:93:d4:1f:d7:ba:2a:80:b2:07:39:dc:76:7e: + b5:ae:d3:91:d0:5d:5d:7e:d2:71:b3:5f:ab:31:12:95:d7:b9: + 26:b7:a8:96:82:7e:15:34:f8:c4:3c:2e:68:a8:c8:19:94:57: + 05:07:80:63:54:87:8f:fd:58:ae:4a:4d:4c:54:fb:ad:74:17: + 70:40:85:df:c7:3b:94:64:54:cf:a5:41:52:c8:ad:a5:45:6b: + 20:bf:43:9a:74:b4:fa:e4:de:67:c3:e1:78:32:42:62:79:41: + e2:f8:92:96:09:43:a6:d5:d7:c4:fb:4e:8a:1d:e6:83:43:3f: + ec:33:f3:65:2b:fa:25:b6:5e:9c:73:0d:57:c4:b8:ef:0f:d2: + b0:01:25:c9:cb:6b:05:74:34:3e:50:f3:3d:7d:35:4c:a1:01: + 18:f7:0c:01:75:08:6a:f7:86:9a:a9:b3:d4:1a:53:8c:64:62: + a8:56:70:59:5f:c2:21:1b:13:24:64:13:d0:51:2f:9d:69:d0: + 5c:c1:d3:ac:30:14:20:68:1e:38:a4:d0:41:da:fc:1a:77:92: + 14:c1:95:9b:30:29:e4:e3:81:95:02:8d:3d:e4:1b:dd:49:d7: + 19:5f:0d:95:9f:90:69:7a:49:e6:f5:ba:b4:48:c3:91:80:43: + 5b:13:d6:ed:86:57:80:fe:61:8f:a0:a7:84:a2:4d:b2:cb:7c: + 3a:05:20:fb:ce:a0:50:dd:98:c5:7b:41:3e:00:93:d6:36:ae: + 41:4b:2f:b7:a5:3a:84:c7:86:56:c1:c7:0a:12:e9:dd:c5:c0: + 68:2d:dc:f4:f6:4b:19:67:0f:a4:c3:db:f8:d1:35:e5:0d:ee: + 17:5f:32:b0:05:c1:77:28:5c:34:cc:c6:33:e2:e0:67:a2:d5: + 85:78:69:d3:ef:f2:6e:53:c0:b4:f9:50:e2:5f:ed:b3:e7:41: + 1e:cd:d2:68:e3:56:01:35:c4:c8:73:a2:6c:d3:08:c7:9b:aa: + 14:22:66:be:98:57:84:2f:0b:bb:82:f0:01:56:bb:c4:3b:c0: + cb:28:be:d3:0b:1d:a5:e7:5e:ed:96:fc:4c:9b:8c:4f:8d:a1: + a4:01:de:8c:86:a9:5b:8d:9e:8b:26:c6:f1:91:de:e3:30:ab: + 8f:da:0c:28:3b:b3:1d:1a:63:f8:ec:3f:f1:8a:79:d2:3c:a8: + c9:50:c6:71:c4:44:b7:94:2b:68:9b:04:d7:9f:18:dc:75:b1: + 19:98:07:68:fa:8d:56:2d:69:1a:e2:6d:65:55:6c:f3:dc:a3: + 9d:24:69:76:9b:04:b8:7d:25:2a:30:52:74:9b:b6:de:f4:28: + 47:59:8f:be:c9:e9:ee:fb:2c:55:7f:9d:0d:8b:a8:d9:fc:2c: + 65:7f:ba:c0:d1:39:7e:92:ac:ed:38:4e:74:81:8d:92:b3:d2: + e4:fd:2b:51:73:a7:b7:bf:c5:ce:00:00:00:00:00:00:00:00: + 00:00:00:00:00:00:00:00:00:00:00:09:12:16:1b:21:26:30: + 38 +-----BEGIN CERTIFICATE----- +MIIdPzCCCxagAwIBAgIUN9vqXJ7HffEFEbAt3uH9zXGQkpgwCwYJYIZIAWUDBAMT +MDAxHDAaBgNVBAMME2NlcnQtbWdtdC1tbC1kc2EtODcxEDAOBgNVBAoMB0VjbGlw +c2UwHhcNMjYwODI4MDczOTA2WhcNMzYwODI1MDczOTA2WjAwMRwwGgYDVQQDDBNj +ZXJ0LW1nbXQtbWwtZHNhLTg3MRAwDgYDVQQKDAdFY2xpcHNlMIIKMjALBglghkgB +ZQMEAxMDggohAPUPDezljqn8aqFMIgVVJ+vQls5BsslSc0ZsmblCNUJR1ChV1k7S +H71fE4qPZp5pmeYkBk9bXFjVPVZfQTUt60NSO5F6L/zaSTcbdRxcDwM2fxjRZ8AM +pHn3JTLq58HeBCu3WplHCkOL7mubhLofCRlgMBm0mvVdHPqVhwSs3e733gX2TDlQ +o1p++p8MF2/EcYk0Rye2omSa0O6jHxX56a0iu+u8CNHgrLAEY4ULsTKgJnDkNHxu +homtBF8x3AEmwBewF3TNlRZsuizbbZhG6782q4XUa2fJVMJ2Lk9xL0Fvpuxx/htU +iGg9jwh/u4lC1uKT/+4jdPcOm9S67hgrqrYl+mp99sU/wEvFfysB4oU7yxtadLzU +N+mUiHCeumHCGCEOSJYybgwIn8kYP97MM7YVJkVFlww2rh6ZDgsVnwtfutOtaWf9 +r4jhSopAfVEv0stwzZYWifPmB+BR8cLG8Ia1sAvHffGNsQxQKkdc5mX3f4zZu5Jn +De/0y18uwzV5vNxY3rGq6Sfz4sndlCaP+VRupPV38sWSLFQ+DJqJuO9GojKuCGAI +5lYqEoBLoHjxOISGppRoZ6ugYe1A3BCIwO6l1fTnNZZo2Lo1Mom+00juMpXMrbl5 +RBvAXvSYnD8D7R1EYusRKwXPQKWEf0/A++LF0e6DExCIT/IJYtOipXkekq5oXhK5 +32YOzex9OtqXgRX19GjtGFRu+HSyqsXCAZ3NUwGlqX1hRqyod5HONeepn0QadzG7 +yzcGstkzI9CjRXqwXPkmVU+M5Vz+9bC0xmAjc5VYAIw2kLZVsAVvzMrLBzaIDmsw +w/sdHlHcIPN+Y9juBwmRjHkIqWIPQWfbzG3kxvW22IAr8HaXEUbMsViDzbKQskAL +ZbIWqeDwx5UZQRUFmgRdwSiN2Id9uJVPe3PjVXkgJWvRM+nbRK+apNmOZuey5yvK +xVQwFranEia22Xauk6cr7PYq9ryCLRO3U+67IkPaI+YqQpYS3oEJqwR5rIxTArqg +TUXodF3kLo/iJR4gqhP/LWKzdfH97YbMBbvTsSxlgWhIqHDTz1LeKnXa9KlE4GUU +EV9du/hhog58wQuUogWkGuBU7J1jBhqs/va/Sgh2QNXSSd2q2TmWnuxHgcNWB8B3 +4Vd4h3qNjB5k2vSo3GoFosgBWhmVL2JrlN85yRjIVfr3w9Rho5SURaZRFWHar9/E +ltfZL8Lj7XIpSx3NjpFpmVH9ai9HSIs6RCTEjIOA85gQ3AnRNCHRzaozCS6RAsUK +unII1ERMJfC+k4c7jowfYfcuRvUk8g8GMis/UOxCopD8pZ9eidOw2dxX+oDdYpZL +MfzNGf9iAUKCwFczo6qfpWFt8c4tlP+yiiaw/0wPdjX8Uu9Ud7ZZG19OPxeRt+9d +nwoLNcWG/ku17wdEAPzH+fs3Y7SWE1POByxHUrwOXBwmPOqQy/e2ODRxxQb2RF5X +E4g1oIflzfjg0gWzZe9TjYkD6wsHm4QwBCk6VEFHPe1N9kzeN4qZVa7kF6GKE9RF +WSscLIovo7mvlMtw9bthrUJjxxsDHTrARkg7T48Qu9kL9Im942Ao3CL3VQRJVyJe +Qr76KgUP5gXZ2PBqYmo/POEqZSBgRFOvq5BshUlYZ2gJv9hC+Uxb7v8+7n3pUCSh +k6YEwQG55RF4CuJLVbqf0AobGABOzwCjcwZ4on3OtSfjmYLKyW8ygAx19z/WHaIp +Wr6eHrWzpAl7br5WkzTeoh0VZTpHtCVbTUY4TvhpfuBCIy2Ro35Jtt053zTnYjrR +N4jJkwBZqxAOQlUEw3XubddjIRjnxb+0+MVZ6e81fEafZlp4EtbXN8zBSiauG2ia +usnVBZNj0G99L0MlC8WSvYIWTtkrYs9PT6EVxEwE8KfDJAUrpzn6LzMyW47lpWhf +EyI7EIKlT6koFS4wie5LuCIzOreOF+3sksw8ATCnAat2LEq6d0/FYWnmNrx8nahe +J7yXJoxLwIgq5PW8EjxObS4j1OaG2a/8GQteqZjPx/qZzZevpiBaKsf6291rxAN3 +HUcVRAUnusysvnxhQ4lLX7WsZJ8zQrwR65hWlqAUG+MsH44U0/UEBFFXGyZJWc0k +hN8jY+/qNjLGvFORQjIbleIZe4P/qjKD0D2ylJnT8rioOFQPptKw5k0UaJoVyfx/ +qdTuEOyF7AexLQpHOSyBpKWAdH5MGeGCTIEfGGwfOF/FKyiJff5uCuhWR3OYQGsc +qw9wUiHY0BIyVNc8E4hYCK9p2JpEgn/q3uvl5j0svM2FYpJFWSI8nQGA20DuTsL5 +y5Hb9K1tZEkOdV7KQ1XLTzoOGUATAMHkyMn2PmD+3H3wEzwegSR3mWWC3H2wRT/g +a2clxdQvcJADt/3jJ0FstpXqOeJurRuTPYuTY/DNXwkJA4haX/L9kq2YEpgBktB1 +uORdVBbro4uwB/XiHjZwiE2Aa9fy973UTuladVgTfgEmklJ3fk5cRIPHkmv4RVVO +KxHOP7JXKr/pa6gfs5ePNSM2f9va1QDXTCqucIC/D6+8A09dMqIjZQLFLkj2FXTk +3kcBWBNamhJRS9MfhNkEZh5SFGsRFu/SFwBZwVu3pZDN416kUFZiURFODlPke0d/ +7PxC002qHcO2gbcb513jEL53HIOV+i5W7g1NIdQOhJvBSjYFe96bOxxhLM3NKrqt +mZlmQy0pStnA3nekRQnuRmmI7Pssln5k+2u9cy+oAALVySoGDEsHxB/XKmIDpqdI +XIWAr8zXsqyhcKs1evkeen2eeAScmiKaCoGndH0QAGJIOtZSCuhRD6ZDfRBvSud8 +DNvaE9fpnK9ksl7Qitl9/mFPFBEi6e4BBzMGTEk2nXn8GCRIVxCwuZG1VS8b7C78 +fLmdCBShRQwc+Rc8RsxoefpXGaf6ROk5xZx+mQzG/8XTtN4m+3eIXHLK3+rabPF6 ++2j9Grw5EDkZGE31o1ssJMUDXdCRHHSsyMNgqJvGLc00nOsyonsVmozTizR8qceY +78pXqbsoI/R/q7PCQeq3oEure+VJB40vlzI7xv1YmB3PGNj+oNHN9iXb615k6NMx +RbjI3Tps0HF+/8b4G8cHaFc7oBCft2cz1PNSIw1N//9AIaMbGNAwZi24MjLOKLy3 +WmHKdFMwUIaHUYhDy7ozcyXXvhIkOF0aQqs0+6XECAm7JBFfrrbeE5UvJwv+qrEP +UO8VCIT4/cvQlu4EsmZi6TKIS4gGEICZhlwtfHaS9BrigLVvYSKQXr3kELsfQFl9 +WIXamE9WY3Du4UKmwtsV8F56ihG8YZho+ur9JC335BsYyN+ymy9aLYiQENMMofnu +0C0V5IDOPh4WGEmpENCWWLstR+d4CDASzPnSFj59OaRrBFc+dWIse86AWh4ifopl +BJ0mqsYBtor5BjoKJRO0sxXXPziGTBQyusPZPdEz69lOGRZZ9p8DvH3jChbBgDtm +cvAfET0vjZdf8KMyMDAwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUa9nKNK44 +Vtnq6qqwQTXvBncRrUswCwYJYIZIAWUDBAMTA4ISFACoStbLpQsvrL4v7wCL+zQC +WG1Nh05sUbMNURDE+5svb8Ox8AJTQEJwlKibGpgvPXZobS0PWSQx+BxMytAOupsk +L+IJBfoNKaqarjYQrt/2kYSaQsOdsqBnjHCgHYXPoaIiyW09GBTc1ln3TXH3GQph +RhLcPSO5bTVI+gXgQKiX8Rgk12oC+JDSrj6rnfXp+e1poLLcAtbRWxGAeNct+LYL +Wjqo98j3wYeWrxN4GAJzN0EepwUnA9BKTX0ldoifeJBKVU10TiwWzADVrnmpIx67 +LFjntJ3Z1BEz06tih1kZ3+AuKQslpQgFfs3v/ZgKs1/x+g8alhTnUEmzTOZ/d+rV +IcOJXzC+OghItNxImZStR/tE5BlZuUcm3sf11O46Z2qhcQ+0loKszdjB/XanOPsD +cok70RbJKz4Yvy5OwrD/B2ez7QdygCKBDMyYLHShwMO+j+2mMBCP5VQSh4L7oq1k +Eq8rkxfbASdrQr+NpPMp1AMsWEXktxwluDPUVCt1k2T6QBVHC1gvlts2/XUlVtov +XCiID+/TzxaVWlSJxjEH7brutMBf57/nvfWEU3jEQBEEbwpaK33bOH50cddzjIhx +qbWXwBoPOvo82C0Z+EGaeWeK7/PTCtGQUyWUn0PcEFsonHnukmDgecRtCF+INItc +aBywzoK/9XYqQleTctQXNVW1cWEsXVm56/5YrereI/QZKIJL/klQqLq6M00RL6qd +QYE7s3//xNOi+pKFzwC86xlqAWw7w5/iya4MbrkVZEXdTVB+b6tUQSFU3Vc7EKKX +iQIABwUzvA9iX9aOmEOJwekEvijuFJQelVlrAAX2rOJIGDYMHAwSfQL+SfVW2tBi +e6D+g/rKSbEP3TlFHUnEcAH+5sOoh3lIO5nhDZBcTGhDloyDs6WkSMnfy7eogEkP +sgScBO3zTkt7nj2VahxFBa5dYekh7ApmvWUr//qXPn/lrwo1QvTazZ8xgjzE+o5y +RDXvAr2k1S6UFKKuUmzCFwfz8qmWErg9B13XMu9G70sHEHXn5suDdN4L2wCryNZ+ +qedLg4T5AToTLdFqyd/ictlME14Vel7oWpBbdUv2vPLt8gThL9wsWPjfbjxpHMNr +NjpXhuhI/3vh/l9ZfTZ2RaMXqKX3K60gdF/M9UnhjhfbpULi4I5XynY88sDbf/90 +COqhcZPws0COxdDCjKy3wGhoWtcu3T092Zvwp0lCxH4eleLKf66BtkklBKmW7VGP +6PJT5Q4Pb0hZFIfXnJENBQbK24Ee9wU4rRQljUAslPP5QhzOtiQdE28VI9vxbXJT +MhnmI1Hj6xrKoPB22z+k50YQIS8zoEB1Q1mcQj6qb7u6LmB4CQWdITRTWa4eL252 +Eb3WV7k088S+mpRSyzYCR218mMItv70Rj9udT4n1V7j9UZkpesTwv31/607oGWZi +GQhWJRYLAgNhbtuICQb2lxh4V6Xw3bI8mzWj7QbS9xycaFCZMjE+TXJyFsASDpUt +wM+So2SC8GlgdiYt8LFUfAlZeGJCzy39Yt0xrB23fA+gpGMekCS2oS5Pvwy/DXRC +I947CDDR9yeFpsye+ds5IAXAfWtLkSd4PBeJUAD/UtjqAHy5hm1hAE/aqZNcMWk+ +KqiKTzH1jFVaeZ66vfT7COv75dSY/0gw3WHwinBW1rOcVt5qpVETO3IWeCuNpzSJ +r6E+pjYShJiqLjgHppNSIlHFUfGjDvI8xYdjaUrkG48qTd13CYUmEx6+If3EpwK9 +qeA9OVDjiTk4TPHZL5kyhkzw3FEvYzflTpvaA+uQURR4ce71F0dS/haxxM5dPmjb +tKCsFebSMlHFnHGx8d117EK3StMkXBvVbtG5Bn0zlNOBRoG7uLuEZkpXvSamAl7B +SxR+/lBieFxQNwXt7yQOoCrfgL/iiGeWtl631yZlFz5cdy9d2/mUtbtGFzl2IMiM +KpnI3wVodJziqaJx97APFnZQxzRGwML+HqX9Dd1Ybd5LI2iR5pJ1nth0Erqb/lsW +cdlq3AV39UV6b96Zk8oVvcSAl5/hF9M78dT0hvBGEGD/d9drssPGs2+3Yh0JW6Cg +qgwGx4TB7m2mo92tiTyUUXbdnSeSwOwxna9Mg3qxPj1peZCCfoR+KWveCusGCz0l +pVRNztVufkMIlbrNAE6VncOCoGN4YImlHTZWDwsnS0YCvOyJMk8mR98CYudXraZe +hmSfHra7hyLQqFXgoKS/1TrnUcLd1MJaDuhAkURVeBMLq3qy/2h+Bg1JN1djaMBP +N+iCgkKyX5lvuHocwM53LdTMErnQZ4S3bIMilImH180Cb0aCRnMjW7H2R3+grVMA +ddrKkpfLSQureuK2L3eoLrBMom8uOEviE1bNVmKR2p7n/7ujXN+iBb/vita55vFj +Idm1GtOla/5frICgTtxflmLUPYZF8cI/5WopdUoeUHetekl0hcqd1kOfYwrcxW1B +s44J53oMwaFIpZHdu9EXgNRqWQiZmCc+puPGjju02oVWT4k2OGWoNHf5P7vTI4Jl +jVTClbPGjJRJGqsGRzDlCzP/FT7qtNKMj7iXiarLRHVf8gcR3tEMZCwLwFUYP3pE +osZUr8zbSBsjrQIRVhayRZUyALrfFB3DH36PmigTIsXxqG1V7u+jbwGwPx7h7lXF +yr4dDrX9K6qRmv0cr2KFkKV5REYwF1iGozY+ouc6tl6oQuHxtHNtWbUiAwY99zHg +a5cg4HKrDe34URPfdfDq72zbh3Pu+HbE0EVk9+BraXDe8fSSG2f3i5ZDdM5jz0vV +j2dsJAJ8b3Lacil0JTjvBYack0P+5IgKmSVtm26kj/TdoC9J7yAZl064M7cZuFZ2 +7FW560G7w9I0PsGTkm5anlWph+w441mU29GPNqoWjYfQZq1g0nyKBQe59tWGKEUj +2TetSZe7qhZv3KCVEN1XUdsD+AF8WwQV/jbhGlvk1QE/vkI5Y6DETSrT8d0HuW3E +rCGh43QL6MG85bVUWqU/zPPEjPo2P6kttN7yXjLZdbjpzDuzy8ujK/HIjN/k79mx +uyF4tsVnD2LKuUmBFcOXomPcT3r8Jtx9s3ezhfr8rKQUBn7caBUNbU2SdJF27EEZ +x2RXSiVlsY69Jn6PCaF6u8AAEnrkuw4VkT2+H9PBlNrzgM2C2eWcd0bz4BepMWMQ +92YkJtCOgqvo2+LYcm/nxNe6P0SMpAms1AfMb+cT9CmkM8q4hBQIY9lrDOybrPzC +xVksmB/xamM1tUb8d6huMlXY/t0zI43oaJUn2Wfgr/0+KO2lkt30ajqwfkZ22lOZ +5MGmJuG3ffRWPEYTs6GnIyNoB/MsMo69AVjapQ0GwfAot1i7QCPvCO/0Lxcn4IZ9 +zl90oYx4lJG7c4djtJbIrmfoa8QMn0zMtsIFAL0QVWaFLlh45hLqirkdZ3wl72/S +cvJdIoH83H4oPqIKb7DaKCutbFUQ2i2Fwl3w8WuxP+p02U+1VKAp1B4wocaAsa10 +G904N7LH7xjfx6gZ4B2PP7tDw3uOSa8HDEE8MF9XDI9NKutYPkcnr2oYElSbKBBX +yYJR+dKCkw0eQAVBS5N8nMpKkYLq9Com+OfHx+j0e9pjqIfyWmVUMvoRbpeBZQzp +0dcQe8me0ScUOyV8pddiqUtfWMBR1wjAwuCE3qSEaAwPILJsAWPw82mhormlAtHS +EvSPaxB9QbiurIUhFunc6icX6bqd1A246hhQ1Yq+UX8i+B028PlWOCQ51fiVwiE0 +2is5kj4LCRq70iPWN+iXrtuo7YtUGhab+6jduL++oMUgUgbAIsr+F+OdUAsGEbfG +f1jnFFBQU51RzDipud7EW6hvk4ihPq0+/J75KLV5BvMKBcQw9fp1Yw5/0vnxtTuP +zlZgKMde0bOTMy+TldzQLoBsp3g0mWk7gmfNGYSDHCf2Yv7lvVKbQCcogf+2iDXq +vHqPvb6wdX0BKjFuLbKfw6VKzdsC8sIgnGt+C2bHElmeLw1C9c3xQQwvplZHw8QP +e5JLj9BfCxH02n6r+ISh+eWbPE9WovIV7Id3oMx7RKcjftYEx8FDsOTbYbZMO/qd +JQlNgfvzn+fnCBiF9q/nsZPHwimqZHZa2sERZOES4/WCBxfVO2BdSGD77W+ztxfe +DnVvZC2hlE1GAdZtlkbJkIJhwwotaf9J51KhoUAdBi9eFscj0nuItdlx/ud6BxzQ +JqTMp2qQfe57BI9WOUhPqnMDrdN2wqzKO+LT54NM5k3V1tGqVDXcYsfHcQKjv/li +eWY5l1vES8HHAnszSr3ukeG8lnFzUwVEKGcDZi0mwKz8nZUwupPFokNajGyEdl0E +Qtzau2kUkD6KX1d6hIFRtaZ7/d41CEN3IuNc9GM4chGKReLrXK/M60VaKqzIMr+s +IIFb8OcDHPYkSRsPPe5adhy7t7fz6mrjFynYA81At4rGDWnfc++E5FdjVlWVgTRt +ucrPYGCwP4Acw3L/ihe/HGSovTwpy9C3nFLK/dmRVRPwnNY3NONe+stEDzw3Me2b +7oxDhJhMxFvrfnvtnvQlupyudjVBhpSfWVE2yHpIJ3T9xRbak0i2nZvZILHuVIRb +9jZNNBwcvuCELLqU+wEbfKtX9AcyQIX36kq+UMjR1JngvmzlRxAB6Z0DkssxpUKi +HJGIQtEYdgZBPTpW92fQozfvd7d70iGsm++E5mHYeCYXbAXtLJ7gwDriZg5M1q2U ++5fUiWbaK0Db0hS6RZ5yt15oiWdv5zC+yjqRRT5gODD9kIvnp/olNVTFs8sd/yX1 +BHSsEzU2lXikqH8sY20Z66wq5Nr0hl/c5I8F6KNV64sZFZKVeVoopUIjfdXF4Gb8 +SiBPTOkzHaP5BE6xzUc7GKkFnRB5pYvwfZTjI+sgToFksgKs+jDU7U9nqlmFW6ud +I++hvrPanpHSuWUp7U3UJsLXhTezdfSQOsyXCdNuJrgAHB7P0TNFEHHJM/JPjUOz +CsPa3XNO8wP1cpQovTR0JvDslYDU0hok4ebQS+ai4vBSyqlsBQKdpd6XKyiIGVVq +hWctMrDkPDU+PozFJHyRA6O30Is3AoGlu56FOKagWV4aX466k76vsb2Wnt+ZGY3p +Rc/voPOtmqcbhiNg2fMEhs+nwvUInHLafSDlc+z3E1DUuW9xTzpoxlpGxx9WPfLd +zislvugzI0OpVTnJt1b21hQ4OV/L6ZEoKNy7xIqrdr5wWLk0DvOQYZNvYEQpRAAO ++r2k+49x+6rmONcDJ/Xk9kjcTSzo5Rtk6VQfnT/7tAVGhYWk3s75EI/BHCfPaj/u +UxMkWUN0S84ZpP6tu9zn9k2Pi/pd58UX6VYSmLqH+sdkol0Kj5QOFT+qxpnQyQxK +Ee3yFJ6wkoL85xWJac8PkBgYmZwaS0gm+uiJ+8LZndXEktu3iL0X9oLoMn9NQNoH +pFxtCf6KopPUH9e6KoCyBzncdn61rtOR0F1dftJxs1+rMRKV17kmt6iWgn4VNPjE +PC5oqMgZlFcFB4BjVIeP/ViuSk1MVPutdBdwQIXfxzuUZFTPpUFSyK2lRWsgv0Oa +dLT65N5nw+F4MkJieUHi+JKWCUOm1dfE+06KHeaDQz/sM/NlK/oltl6ccw1XxLjv +D9KwASXJy2sFdDQ+UPM9fTVMoQEY9wwBdQhq94aaqbPUGlOMZGKoVnBZX8IhGxMk +ZBPQUS+dadBcwdOsMBQgaB44pNBB2vwad5IUwZWbMCnk44GVAo095BvdSdcZXw2V +n5Bpeknm9bq0SMORgENbE9bthleA/mGPoKeEok2yy3w6BSD7zqBQ3ZjFe0E+AJPW +Nq5BSy+3pTqEx4ZWwccKEundxcBoLdz09ksZZw+kw9v40TXlDe4XXzKwBcF3KFw0 +zMYz4uBnotWFeGnT7/JuU8C0+VDiX+2z50EezdJo41YBNcTIc6Js0wjHm6oUIma+ +mFeELwu7gvABVrvEO8DLKL7TCx2l517tlvxMm4xPjaGkAd6MhqlbjZ6LJsbxkd7j +MKuP2gwoO7MdGmP47D/xinnSPKjJUMZxxES3lCtomwTXnxjcdbEZmAdo+o1WLWka +4m1lVWzz3KOdJGl2mwS4fSUqMFJ0m7be9ChHWY++yenu+yxVf50Ni6jZ/Cxlf7rA +0Tl+kqztOE50gY2Ss9Lk/StRc6e3v8XOAAAAAAAAAAAAAAAAAAAAAAAAAAkSFhsh +JjA4 +-----END CERTIFICATE----- diff --git a/score/tests/test_vectors/certificate/ml_dsa_87_slot.kv b/score/tests/test_vectors/certificate/ml_dsa_87_slot.kv new file mode 100644 index 000000000..d8df213b3 --- /dev/null +++ b/score/tests/test_vectors/certificate/ml_dsa_87_slot.kv @@ -0,0 +1,3 @@ +[certificate] +cert_path = score/tests/test_vectors/certificate/ml_dsa_87.pem +cert_format = pem diff --git a/score/tests/test_vectors/certificate/rsa_3072.pem b/score/tests/test_vectors/certificate/rsa_3072.pem new file mode 100644 index 000000000..e9ae2104e --- /dev/null +++ b/score/tests/test_vectors/certificate/rsa_3072.pem @@ -0,0 +1,96 @@ +Certificate: + Data: + Version: 3 (0x2) + Serial Number: + 5e:1c:94:35:40:8a:92:0f:0f:f6:7b:22:d8:97:ab:6e:59:46:16:ca + Signature Algorithm: sha256WithRSAEncryption + Issuer: CN=cert-mgmt-rsa-3072, O=Eclipse + Validity + Not Before: Aug 28 07:38:42 2026 GMT + Not After : Aug 25 07:38:42 2036 GMT + Subject: CN=cert-mgmt-rsa-3072, O=Eclipse + Subject Public Key Info: + Public Key Algorithm: rsaEncryption + Public-Key: (3072 bit) + Modulus: + 00:aa:aa:96:96:85:b7:62:d4:cd:5b:a1:ec:ed:cd: + 16:97:8a:80:e2:94:8f:db:b4:5e:13:8d:f5:4f:8e: + 91:60:39:32:c8:a6:c0:d8:0a:47:69:03:e1:dc:3a: + ac:16:e3:b5:4e:f0:da:54:f2:a6:e6:e1:f0:42:00: + 1e:66:6e:ec:7c:84:06:35:26:02:a6:0e:82:47:7c: + ba:31:86:d2:99:e9:d3:e6:4a:81:02:1e:6f:d5:7f: + 10:d4:76:3c:f1:47:85:76:37:d1:91:1b:66:7e:db: + 97:e2:de:7b:18:b8:cb:e4:0e:2a:08:ac:13:ba:b4: + e7:18:ca:69:dc:2d:b8:48:e4:01:07:4f:f4:c1:d8: + 75:a9:80:9f:b5:bf:cc:60:5e:3f:70:57:b3:a0:e5: + 79:fe:e0:74:f9:80:26:7c:f2:3f:15:84:b0:ea:da: + 8f:99:36:7c:36:ff:05:97:2e:20:6f:fd:6b:c5:4d: + d1:80:4a:c9:58:bc:42:7d:e4:a2:52:b1:db:46:f4: + 84:42:d8:18:52:78:1b:92:5a:a2:fb:4d:6c:b9:4f: + ae:35:07:94:54:95:23:e8:af:95:17:f0:58:e7:d1: + 98:69:7f:5c:55:ad:2f:23:a1:80:0b:ca:b0:10:54: + 3c:68:d9:cc:2f:4e:54:b9:3b:29:56:fd:03:b5:8f: + 81:d3:0d:e3:da:ca:2f:49:e9:9b:8e:91:9c:cc:a3: + 59:33:06:bd:60:c4:1b:35:9b:c4:12:bf:83:0b:7e: + ee:b8:58:ea:3f:11:a6:69:6c:6d:46:63:a6:1b:c6: + 38:b2:be:89:22:99:f6:7a:47:7b:69:23:85:80:c1: + 8f:00:6e:ca:53:52:b3:6d:ad:7d:e6:2c:25:c1:dc: + 6b:ad:c6:02:34:59:ff:33:56:a0:3e:f1:fc:cd:e1: + 37:b6:d0:27:11:0e:83:e3:07:43:17:b5:f8:c1:1d: + c9:07:5d:e4:27:01:9f:61:7f:7f:15:94:71:2c:39: + c8:a7:61:ca:c0:f1:5f:7e:10:f3 + Exponent: 65537 (0x10001) + X509v3 extensions: + X509v3 Basic Constraints: critical + CA:TRUE + X509v3 Subject Key Identifier: + E9:3B:CA:D7:E5:4A:4D:5E:88:72:83:79:71:93:35:B0:1D:AB:FA:B2 + Signature Algorithm: sha256WithRSAEncryption + Signature Value: + 33:e0:80:a0:c7:b3:69:91:3d:88:3c:f0:84:0a:47:88:c4:c0: + 75:60:5b:c2:12:ee:d9:58:1f:64:e3:a5:5d:37:71:05:c5:f1: + 03:e7:5a:6b:12:40:54:c1:ea:c9:db:86:c5:af:c5:57:65:50: + 4b:7a:a3:35:bc:e3:f0:3f:d8:c6:f2:2b:94:15:9b:14:d9:7f: + f6:17:7f:95:99:65:4a:11:2b:31:0b:14:c0:31:9b:19:d2:bf: + 15:07:3c:02:be:94:05:0a:48:c7:e9:2e:f1:96:a0:ba:8a:2b: + 6c:07:f4:b0:d0:e2:4c:9c:87:2a:c2:a7:fb:62:9f:1b:1f:cb: + c4:0f:82:e4:90:db:d7:5e:1a:af:6e:d0:f4:1b:e9:5e:a6:7c: + 21:12:02:94:24:91:46:6f:00:32:80:18:52:74:8a:72:f2:43: + c6:f7:f5:e0:55:f2:25:27:3f:25:89:77:b2:26:f2:bb:d4:fc: + fc:5a:c0:2e:56:af:8b:04:17:69:5c:92:3f:d6:91:e0:38:00: + 1c:0c:10:3d:a1:4d:90:47:12:e1:67:21:1a:22:73:39:d1:61: + bb:60:b8:c9:a0:0e:cc:80:2b:8f:ea:5f:70:98:78:54:01:da: + fe:0f:72:95:c4:7a:01:a0:a0:f7:64:89:a2:88:33:ca:4f:77: + 74:e7:ad:04:c8:0c:b2:5e:1a:d7:ca:66:20:77:ce:ce:76:ed: + 87:60:2d:ee:27:e2:a5:5a:62:10:76:34:9c:56:e7:df:7e:1b: + 05:2f:20:17:0c:95:a4:fe:de:59:c2:c2:fc:28:8b:83:e0:e4: + 3c:5f:6b:83:82:e6:1d:00:1c:6f:50:ec:d3:03:56:b0:20:4f: + 68:8c:4b:fa:95:67:f0:ba:51:85:45:d6:44:63:0b:ee:0e:81: + d6:17:11:0a:c1:ab:ca:7a:aa:ef:23:73:88:16:92:72:de:86: + 36:43:c3:e7:ab:2c:7f:ea:2a:96:c5:7f:e3:aa:71:a9:c0:32: + 6a:42:3f:cf:06:22 +-----BEGIN CERTIFICATE----- +MIIEHjCCAoagAwIBAgIUXhyUNUCKkg8P9nsi2JerbllGFsowDQYJKoZIhvcNAQEL +BQAwLzEbMBkGA1UEAwwSY2VydC1tZ210LXJzYS0zMDcyMRAwDgYDVQQKDAdFY2xp +cHNlMB4XDTI2MDgyODA3Mzg0MloXDTM2MDgyNTA3Mzg0MlowLzEbMBkGA1UEAwwS +Y2VydC1tZ210LXJzYS0zMDcyMRAwDgYDVQQKDAdFY2xpcHNlMIIBojANBgkqhkiG +9w0BAQEFAAOCAY8AMIIBigKCAYEAqqqWloW3YtTNW6Hs7c0Wl4qA4pSP27ReE431 +T46RYDkyyKbA2ApHaQPh3DqsFuO1TvDaVPKm5uHwQgAeZm7sfIQGNSYCpg6CR3y6 +MYbSmenT5kqBAh5v1X8Q1HY88UeFdjfRkRtmftuX4t57GLjL5A4qCKwTurTnGMpp +3C24SOQBB0/0wdh1qYCftb/MYF4/cFezoOV5/uB0+YAmfPI/FYSw6tqPmTZ8Nv8F +ly4gb/1rxU3RgErJWLxCfeSiUrHbRvSEQtgYUngbklqi+01suU+uNQeUVJUj6K+V +F/BY59GYaX9cVa0vI6GAC8qwEFQ8aNnML05UuTspVv0DtY+B0w3j2sovSembjpGc +zKNZMwa9YMQbNZvEEr+DC37uuFjqPxGmaWxtRmOmG8Y4sr6JIpn2ekd7aSOFgMGP +AG7KU1Kzba195iwlwdxrrcYCNFn/M1agPvH8zeE3ttAnEQ6D4wdDF7X4wR3JB13k +JwGfYX9/FZRxLDnIp2HKwPFffhDzAgMBAAGjMjAwMA8GA1UdEwEB/wQFMAMBAf8w +HQYDVR0OBBYEFOk7ytflSk1eiHKDeXGTNbAdq/qyMA0GCSqGSIb3DQEBCwUAA4IB +gQAz4ICgx7NpkT2IPPCECkeIxMB1YFvCEu7ZWB9k46VdN3EFxfED51prEkBUwerJ +24bFr8VXZVBLeqM1vOPwP9jG8iuUFZsU2X/2F3+VmWVKESsxCxTAMZsZ0r8VBzwC +vpQFCkjH6S7xlqC6iitsB/Sw0OJMnIcqwqf7Yp8bH8vED4LkkNvXXhqvbtD0G+le +pnwhEgKUJJFGbwAygBhSdIpy8kPG9/XgVfIlJz8liXeyJvK71Pz8WsAuVq+LBBdp +XJI/1pHgOAAcDBA9oU2QRxLhZyEaInM50WG7YLjJoA7MgCuP6l9wmHhUAdr+D3KV +xHoBoKD3ZImiiDPKT3d0560EyAyyXhrXymYgd87Odu2HYC3uJ+KlWmIQdjScVuff +fhsFLyAXDJWk/t5ZwsL8KIuD4OQ8X2uDguYdABxvUOzTA1awIE9ojEv6lWfwulGF +RdZEYwvuDoHWFxEKwavKeqrvI3OIFpJy3oY2Q8Pnqyx/6iqWxX/jqnGpwDJqQj/P +BiI= +-----END CERTIFICATE----- diff --git a/score/tests/test_vectors/certificate/rsa_3072_slot.kv b/score/tests/test_vectors/certificate/rsa_3072_slot.kv new file mode 100644 index 000000000..e73cf47f7 --- /dev/null +++ b/score/tests/test_vectors/certificate/rsa_3072_slot.kv @@ -0,0 +1,3 @@ +[certificate] +cert_path = score/tests/test_vectors/certificate/rsa_3072.pem +cert_format = pem diff --git a/score/tests/test_vectors/certificate/rsa_4096.pem b/score/tests/test_vectors/certificate/rsa_4096.pem new file mode 100644 index 000000000..4ff0b73c3 --- /dev/null +++ b/score/tests/test_vectors/certificate/rsa_4096.pem @@ -0,0 +1,117 @@ +Certificate: + Data: + Version: 3 (0x2) + Serial Number: + 2f:9c:67:d0:aa:6d:80:af:08:8a:c8:28:56:bd:95:20:50:6e:64:6f + Signature Algorithm: sha256WithRSAEncryption + Issuer: CN=cert-mgmt-rsa-4096, O=Eclipse + Validity + Not Before: Aug 28 07:39:03 2026 GMT + Not After : Aug 25 07:39:03 2036 GMT + Subject: CN=cert-mgmt-rsa-4096, O=Eclipse + Subject Public Key Info: + Public Key Algorithm: rsaEncryption + Public-Key: (4096 bit) + Modulus: + 00:b3:8c:ef:8c:71:4b:c5:5b:97:a2:6e:3a:20:9a: + f4:f3:f9:88:75:19:f8:3d:02:96:b9:36:db:6e:e9: + f7:c4:6f:71:03:c7:22:0b:21:49:8d:e0:6e:44:f5: + b8:6d:61:2f:44:86:38:c3:32:1e:b3:ca:76:3b:0d: + ca:c7:21:ab:0e:5d:2b:b8:55:3d:3e:42:0a:c5:9e: + 22:d0:61:d8:1c:72:45:6e:04:07:93:62:66:a2:42: + fc:db:3f:b9:a5:57:90:fd:63:21:54:a4:bd:90:27: + 97:9e:91:b0:0f:87:f7:1d:e8:34:43:ee:1a:be:89: + b4:81:21:c4:7d:e0:75:a5:6b:bc:eb:bc:3a:48:94: + d9:c7:f0:df:0f:30:45:42:c5:df:46:3a:e9:e9:31: + 7d:ec:1d:e9:66:a4:5c:bc:3a:13:af:2e:d8:75:db: + 07:7d:bc:0a:c7:95:a4:85:f1:19:5d:7b:60:e9:76: + 7c:ca:4b:a0:bf:7e:b1:0b:bd:d9:cc:a9:d9:27:ee: + cf:77:be:41:5f:5f:19:be:de:a5:2c:60:ac:6a:92: + 48:ef:55:72:f4:c8:f4:f9:ff:a3:39:ff:0e:f8:19: + da:44:39:71:38:8d:3a:af:c2:e4:0e:45:29:76:6f: + cd:71:39:df:55:ad:1c:b0:39:d6:f6:1d:f9:89:8b: + a5:dd:2e:9a:41:15:f4:1d:c6:ae:c3:a3:cf:6a:9d: + d7:15:fc:8f:49:0d:e9:c5:b6:05:d5:a3:12:cd:dd: + ac:8a:97:a0:0f:8b:17:1a:d0:34:1c:ec:36:6d:46: + 56:ef:3e:2b:cd:9a:57:a9:60:ac:ff:58:eb:bf:c3: + e1:ee:4d:bc:0e:d6:e3:c1:58:9c:69:ea:98:92:46: + 90:bb:6b:93:15:b5:2e:39:25:8a:80:0f:d3:2e:c6: + 82:2d:f4:cf:8a:64:12:40:4a:21:b2:d4:18:ff:df: + ae:37:e8:ad:f1:e2:89:0e:69:e5:9c:0e:a0:16:81: + ef:25:25:86:10:7a:e2:dc:23:ec:d1:87:ce:20:ce: + a6:f0:1e:2e:28:c6:6d:2a:14:5c:3e:05:54:2d:ff: + a9:9c:78:2f:5d:ac:3f:60:ac:0a:fe:26:5f:47:78: + 53:91:9d:70:f2:e2:e2:34:8f:4e:a8:3e:85:54:e8: + a6:c2:93:e4:79:44:a4:95:c5:98:96:e5:cc:71:42: + ea:d3:19:6f:d9:6a:55:89:0e:c2:2d:5d:5c:31:77: + b2:e9:58:34:9c:7d:24:bd:80:e8:9e:b5:c8:1e:7c: + 83:cb:45:62:fe:8c:04:8c:5c:fd:fa:49:3f:9f:09: + 9b:31:b0:a8:04:54:86:6d:13:cb:f8:5f:27:e0:e1: + 65:f6:53 + Exponent: 65537 (0x10001) + X509v3 extensions: + X509v3 Basic Constraints: critical + CA:TRUE + X509v3 Subject Key Identifier: + D9:72:AC:E1:C8:EA:83:A4:5D:9C:F0:33:6F:08:DD:A4:1C:AA:F5:3D + Signature Algorithm: sha256WithRSAEncryption + Signature Value: + 85:3a:90:0d:68:fc:ce:39:a5:94:eb:cb:c4:73:be:93:a6:f9: + 65:cf:8c:74:b1:46:f1:15:db:df:75:6a:fc:e6:54:0f:c7:8e: + 3e:2f:6d:9a:5f:cd:b1:67:ed:31:12:d4:2a:c1:bf:6b:d3:d1: + d5:eb:75:5d:ef:8a:af:c0:b2:7e:18:1d:4a:d2:b9:bd:69:c6: + 3e:ae:72:16:ff:9a:86:a1:51:f3:5e:71:0f:bb:e8:7f:f4:4b: + f5:59:29:f3:4c:aa:db:db:30:48:09:b4:70:57:3e:a1:1e:a6: + 52:0c:e3:48:27:d8:a1:8c:08:4f:0f:20:d6:c4:1e:4e:eb:16: + 0f:68:b4:04:59:ea:72:6f:bb:da:06:7b:7d:c4:d2:34:16:30: + 4a:ff:ad:9e:1e:c5:c3:ae:85:a5:34:4c:b2:99:27:b0:aa:79: + fb:fc:d5:ba:85:fa:1b:da:c2:f0:53:aa:93:a4:78:08:71:fa: + 99:d6:51:84:c4:b3:9e:5f:36:6f:ce:1c:c2:13:6e:dd:98:5b: + f7:75:6d:e7:76:cd:b3:71:8a:81:4c:0f:82:4e:a7:96:64:c3: + c9:6b:20:f6:67:53:b0:56:ae:65:51:5e:4d:46:fb:a1:7b:65: + 97:40:0c:86:13:e9:dc:fe:29:ac:68:df:33:d7:26:f2:9b:51: + 31:1d:9c:d8:8e:bd:fa:38:14:2a:4b:5b:6c:d3:b5:8e:b3:d8: + 91:b4:56:bf:eb:f6:ab:a8:73:c1:2f:bf:69:08:2f:ac:c6:1c: + ef:38:f6:5d:f7:4f:6a:8c:48:b0:a2:30:bc:39:8d:20:db:47: + e0:be:85:05:fc:19:6c:92:c1:6b:35:ca:22:d9:17:04:27:b1: + 97:be:cd:45:6f:2f:b4:4d:31:56:66:a3:22:6f:4b:19:ef:7f: + 64:09:0e:10:7a:78:fc:ac:a3:04:ae:86:0c:c0:37:e3:5d:d4: + ba:84:2e:72:b6:8a:fa:c3:8b:10:2c:7c:75:4c:47:e8:20:2d: + e7:3a:e6:30:2b:39:01:c3:9c:d1:88:fd:33:02:ea:53:6f:f5: + 50:d6:b4:10:24:c8:86:cd:d8:ca:2d:47:41:be:d5:4f:4d:65: + 59:2c:b2:3a:b9:48:b8:5c:f2:91:2a:75:11:0a:d5:62:cf:a6: + 30:e7:7e:1b:65:5b:a6:2b:10:c7:a8:2d:b9:1a:d2:93:f4:43: + d4:d9:57:12:60:74:95:96:5d:4b:76:d6:80:87:c3:b2:78:79: + e7:35:59:af:93:c4:9e:0c:6a:12:31:ae:f9:ff:15:62:32:f2: + bb:3d:3f:97:ea:20:cf:f5:0c:c7:fa:74:ed:30:fe:27:05:e5: + 5c:69:c4:2b:b5:f9:a3:3f +-----BEGIN CERTIFICATE----- +MIIFHjCCAwagAwIBAgIUL5xn0KptgK8IisgoVr2VIFBuZG8wDQYJKoZIhvcNAQEL +BQAwLzEbMBkGA1UEAwwSY2VydC1tZ210LXJzYS00MDk2MRAwDgYDVQQKDAdFY2xp +cHNlMB4XDTI2MDgyODA3MzkwM1oXDTM2MDgyNTA3MzkwM1owLzEbMBkGA1UEAwwS +Y2VydC1tZ210LXJzYS00MDk2MRAwDgYDVQQKDAdFY2xpcHNlMIICIjANBgkqhkiG +9w0BAQEFAAOCAg8AMIICCgKCAgEAs4zvjHFLxVuXom46IJr08/mIdRn4PQKWuTbb +bun3xG9xA8ciCyFJjeBuRPW4bWEvRIY4wzIes8p2Ow3KxyGrDl0ruFU9PkIKxZ4i +0GHYHHJFbgQHk2JmokL82z+5pVeQ/WMhVKS9kCeXnpGwD4f3Heg0Q+4avom0gSHE +feB1pWu867w6SJTZx/DfDzBFQsXfRjrp6TF97B3pZqRcvDoTry7YddsHfbwKx5Wk +hfEZXXtg6XZ8ykugv36xC73ZzKnZJ+7Pd75BX18Zvt6lLGCsapJI71Vy9Mj0+f+j +Of8O+BnaRDlxOI06r8LkDkUpdm/NcTnfVa0csDnW9h35iYul3S6aQRX0Hcauw6PP +ap3XFfyPSQ3pxbYF1aMSzd2sipegD4sXGtA0HOw2bUZW7z4rzZpXqWCs/1jrv8Ph +7k28DtbjwVicaeqYkkaQu2uTFbUuOSWKgA/TLsaCLfTPimQSQEohstQY/9+uN+it +8eKJDmnlnA6gFoHvJSWGEHri3CPs0YfOIM6m8B4uKMZtKhRcPgVULf+pnHgvXaw/ +YKwK/iZfR3hTkZ1w8uLiNI9OqD6FVOimwpPkeUSklcWYluXMcULq0xlv2WpViQ7C +LV1cMXey6Vg0nH0kvYDonrXIHnyDy0Vi/owEjFz9+kk/nwmbMbCoBFSGbRPL+F8n +4OFl9lMCAwEAAaMyMDAwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQU2XKs4cjq +g6RdnPAzbwjdpByq9T0wDQYJKoZIhvcNAQELBQADggIBAIU6kA1o/M45pZTry8Rz +vpOm+WXPjHSxRvEV2991avzmVA/Hjj4vbZpfzbFn7TES1CrBv2vT0dXrdV3viq/A +sn4YHUrSub1pxj6uchb/moahUfNecQ+76H/0S/VZKfNMqtvbMEgJtHBXPqEeplIM +40gn2KGMCE8PINbEHk7rFg9otARZ6nJvu9oGe33E0jQWMEr/rZ4excOuhaU0TLKZ +J7Cqefv81bqF+hvawvBTqpOkeAhx+pnWUYTEs55fNm/OHMITbt2YW/d1bed2zbNx +ioFMD4JOp5Zkw8lrIPZnU7BWrmVRXk1G+6F7ZZdADIYT6dz+Kaxo3zPXJvKbUTEd +nNiOvfo4FCpLW2zTtY6z2JG0Vr/r9quoc8Evv2kIL6zGHO849l33T2qMSLCiMLw5 +jSDbR+C+hQX8GWySwWs1yiLZFwQnsZe+zUVvL7RNMVZmoyJvSxnvf2QJDhB6ePys +owSuhgzAN+Nd1LqELnK2ivrDixAsfHVMR+ggLec65jArOQHDnNGI/TMC6lNv9VDW +tBAkyIbN2MotR0G+1U9NZVkssjq5SLhc8pEqdREK1WLPpjDnfhtlW6YrEMeoLbka +0pP0Q9TZVxJgdJWWXUt21oCHw7J4eec1Wa+TxJ4MahIxrvn/FWIy8rs9P5fqIM/1 +DMf6dO0w/icF5VxpxCu1+aM/ +-----END CERTIFICATE----- diff --git a/score/tests/test_vectors/certificate/rsa_4096_slot.kv b/score/tests/test_vectors/certificate/rsa_4096_slot.kv new file mode 100644 index 000000000..41bb6d61d --- /dev/null +++ b/score/tests/test_vectors/certificate/rsa_4096_slot.kv @@ -0,0 +1,3 @@ +[certificate] +cert_path = score/tests/test_vectors/certificate/rsa_4096.pem +cert_format = pem From d49f2a302092b1eaaaae95336537558e4ab9f2f2 Mon Sep 17 00:00:00 2001 From: Athul Mallappallil Date: Fri, 28 Aug 2026 13:10:32 +0000 Subject: [PATCH 32/37] Crl support and validation --- .../cert_management/interfaces/cert_types.hpp | 10 + .../interfaces/i_cert_slot_handler.cpp | 3 +- .../interfaces/i_cert_slot_handler.hpp | 5 +- .../interfaces/i_trust_store_handler.hpp | 19 + .../cert_management/slot/crl_handler.cpp | 5 +- .../cert_management/slot/crl_handler.hpp | 4 +- .../slot/file_backed_slot_handler.cpp | 5 +- .../slot/file_backed_slot_handler.hpp | 8 +- .../query/test_cert_object_serializer.cpp | 7 +- .../slot/test_file_backed_slot_handler.cpp | 10 + .../truststore/trust_store_handler.cpp | 20 + .../truststore/trust_store_handler.hpp | 10 +- .../truststore/trust_store_manager.cpp | 1448 +++++++++-------- .../truststore/trust_store_manager.hpp | 3 +- .../provider/cert_management/cert_types.hpp | 10 +- .../cert_management/i_cert_parser.hpp | 17 + .../provider/score_provider/openssl/BUILD | 3 + .../cert_management/openssl_cert_parser.cpp | 105 ++ .../cert_management/openssl_cert_parser.hpp | 8 + .../openssl/provider_openssl.cpp | 3 +- .../test_openssl_cert_parser.cpp | 3 +- 21 files changed, 984 insertions(+), 722 deletions(-) diff --git a/score/crypto/src/daemon/cert_management/interfaces/cert_types.hpp b/score/crypto/src/daemon/cert_management/interfaces/cert_types.hpp index 6424c6b9a..c9a7d9723 100644 --- a/score/crypto/src/daemon/cert_management/interfaces/cert_types.hpp +++ b/score/crypto/src/daemon/cert_management/interfaces/cert_types.hpp @@ -130,6 +130,16 @@ struct CertChainMetadata bool is_ca{false}; }; +/// Raw CRL bytes together with their encoding format. +/// +/// Used as the cache unit in TrustStoreHandler and as the return element of +/// ICertSlotHandler::LoadCrl paired with ICertSlotHandler::GetCrlFormat(). +struct CrlEntry +{ + std::vector bytes; + score::crypto::FormatType format{score::crypto::FormatType::kDer}; +}; + // --------------------------------------------------------------------------- // Deployment descriptor section and key name constants // --------------------------------------------------------------------------- diff --git a/score/crypto/src/daemon/cert_management/interfaces/i_cert_slot_handler.cpp b/score/crypto/src/daemon/cert_management/interfaces/i_cert_slot_handler.cpp index 92211ddb3..827bfde3d 100644 --- a/score/crypto/src/daemon/cert_management/interfaces/i_cert_slot_handler.cpp +++ b/score/crypto/src/daemon/cert_management/interfaces/i_cert_slot_handler.cpp @@ -38,7 +38,8 @@ score::crypto::Expected, score::crypto::daemon::common::Dae score::crypto::Expected ICertSlotHandler::StoreCrl( const CertSlotConfig& /*slot*/, score::crypto::span /*crl_data*/, - score::crypto::FormatType /*format*/) + score::crypto::FormatType /*format*/, + std::int64_t /*next_update_epoch_s*/) { return score::crypto::make_unexpected(score::crypto::daemon::common::DaemonErrorCode::kUnsupportedOperation); } diff --git a/score/crypto/src/daemon/cert_management/interfaces/i_cert_slot_handler.hpp b/score/crypto/src/daemon/cert_management/interfaces/i_cert_slot_handler.hpp index ef0d515d9..7693b570e 100644 --- a/score/crypto/src/daemon/cert_management/interfaces/i_cert_slot_handler.hpp +++ b/score/crypto/src/daemon/cert_management/interfaces/i_cert_slot_handler.hpp @@ -156,7 +156,10 @@ class ICertSlotHandler /// /// Default: returns kUnsupportedOperation. [[nodiscard]] virtual score::crypto::Expected - StoreCrl(const CertSlotConfig& slot, score::crypto::span crl_data, score::crypto::FormatType format); + StoreCrl(const CertSlotConfig& slot, + score::crypto::span crl_data, + score::crypto::FormatType format, + std::int64_t next_update_epoch_s = 0); /// Remove the CRL from the slot's [crl] section. /// diff --git a/score/crypto/src/daemon/cert_management/interfaces/i_trust_store_handler.hpp b/score/crypto/src/daemon/cert_management/interfaces/i_trust_store_handler.hpp index 2e5b29af1..f2269af67 100644 --- a/score/crypto/src/daemon/cert_management/interfaces/i_trust_store_handler.hpp +++ b/score/crypto/src/daemon/cert_management/interfaces/i_trust_store_handler.hpp @@ -23,6 +23,7 @@ #include #include #include +#include #include #include #include @@ -102,6 +103,24 @@ class ITrustStoreHandler /// invalidate / refresh the chain-building indices (FindBySubject, FindBySkid). virtual void NotifySlotUpdate(CertSlotHandle slot, CertObject::Sptr cert) = 0; + // ----------------------------------------------------------------------- + // CRL cache — co-located with the anchor cache + // ----------------------------------------------------------------------- + + /// Return all CRL entries currently cached for enabled member slots. + /// + /// Entries are loaded lazily alongside the anchor cache (EnsureLoaded). + /// Used by the OpenSSL verification handler when kCrlOnly revocation + /// checking is active — avoids re-reading CRL files on every Verify() call. + [[nodiscard]] virtual std::vector GetCrls() = 0; + + /// Update the in-memory CRL cache for a single slot. + /// + /// Called by TrustStoreManager after a successful StoreCrl (AddMember or + /// ImportCrlForMember) to keep the cache coherent without a full reload. + /// An empty @p entry evicts the slot's CRL from the cache. + virtual void NotifyCrlUpdate(CertSlotHandle slot, std::optional entry) = 0; + // ----------------------------------------------------------------------- // Chain-building lookup indices // ----------------------------------------------------------------------- diff --git a/score/crypto/src/daemon/cert_management/slot/crl_handler.cpp b/score/crypto/src/daemon/cert_management/slot/crl_handler.cpp index 655022657..9e55d3aa5 100644 --- a/score/crypto/src/daemon/cert_management/slot/crl_handler.cpp +++ b/score/crypto/src/daemon/cert_management/slot/crl_handler.cpp @@ -63,7 +63,8 @@ score::crypto::Expected, Error> CrlHandler::LoadCrl(co score::crypto::Expected CrlHandler::StoreCrl(const CertSlotConfig& slot, score::crypto::span data, - score::crypto::FormatType format) + score::crypto::FormatType format, + std::int64_t next_update_epoch_s) { if (data.empty()) return score::crypto::make_unexpected(Error::kInvalidArgument); @@ -90,6 +91,8 @@ score::crypto::Expected CrlHandler::StoreCrl(const CertSl return result; descriptor->Set("crl", "crl_path", path); descriptor->Set("crl", "crl_format", FormatName(format)); + if (next_update_epoch_s != 0) + descriptor->Set("crl", "crl_next_update", std::to_string(next_update_epoch_s)); return SaveDescriptor(slot, *descriptor); } diff --git a/score/crypto/src/daemon/cert_management/slot/crl_handler.hpp b/score/crypto/src/daemon/cert_management/slot/crl_handler.hpp index 35c6375cb..2a2b436db 100644 --- a/score/crypto/src/daemon/cert_management/slot/crl_handler.hpp +++ b/score/crypto/src/daemon/cert_management/slot/crl_handler.hpp @@ -59,10 +59,12 @@ class CrlHandler final /// Write @p data to the CRL file and update the descriptor's [crl] section. /// Derives the CRL path from the deployment path if not already set. + /// When @p next_update_epoch_s is non-zero, caches the value in the descriptor. [[nodiscard]] score::crypto::Expected StoreCrl( const CertSlotConfig& slot, score::crypto::span data, - score::crypto::FormatType format); + score::crypto::FormatType format, + std::int64_t next_update_epoch_s = 0); /// Remove the CRL file and erase the [crl] section from the descriptor. [[nodiscard]] score::crypto::Expected ClearCrl(const CertSlotConfig& slot); diff --git a/score/crypto/src/daemon/cert_management/slot/file_backed_slot_handler.cpp b/score/crypto/src/daemon/cert_management/slot/file_backed_slot_handler.cpp index 7a8311374..49ef2e7fd 100644 --- a/score/crypto/src/daemon/cert_management/slot/file_backed_slot_handler.cpp +++ b/score/crypto/src/daemon/cert_management/slot/file_backed_slot_handler.cpp @@ -172,9 +172,10 @@ score::crypto::Expected, Error> FileBackedSlotHandler::Load score::crypto::Expected FileBackedSlotHandler::StoreCrl(const CertSlotConfig& slot, score::crypto::span data, - score::crypto::FormatType format) + score::crypto::FormatType format, + std::int64_t next_update_epoch_s) { - return m_crl.StoreCrl(slot, data, format); + return m_crl.StoreCrl(slot, data, format, next_update_epoch_s); } score::crypto::Expected FileBackedSlotHandler::ClearCrl(const CertSlotConfig& slot) diff --git a/score/crypto/src/daemon/cert_management/slot/file_backed_slot_handler.hpp b/score/crypto/src/daemon/cert_management/slot/file_backed_slot_handler.hpp index f9fcc94fa..f972e62e9 100644 --- a/score/crypto/src/daemon/cert_management/slot/file_backed_slot_handler.hpp +++ b/score/crypto/src/daemon/cert_management/slot/file_backed_slot_handler.hpp @@ -46,9 +46,11 @@ class FileBackedSlotHandler final : public ICertSlotHandler const CertObject&) override; score::crypto::Expected ClearSlot(const CertSlotConfig&) override; score::crypto::Expected, common::DaemonErrorCode> LoadCrl(const CertSlotConfig&) override; - score::crypto::Expected StoreCrl(const CertSlotConfig&, - score::crypto::span, - score::crypto::FormatType) override; + score::crypto::Expected StoreCrl( + const CertSlotConfig&, + score::crypto::span, + score::crypto::FormatType, + std::int64_t next_update_epoch_s = 0) override; score::crypto::Expected ClearCrl(const CertSlotConfig&) override; score::crypto::Expected GetCrlNextUpdate(const CertSlotConfig&) override; score::crypto::FormatType GetCrlFormat(const CertSlotConfig&) override; diff --git a/score/crypto/src/daemon/cert_management/tests/query/test_cert_object_serializer.cpp b/score/crypto/src/daemon/cert_management/tests/query/test_cert_object_serializer.cpp index edd2f1e3c..d7c4285b1 100644 --- a/score/crypto/src/daemon/cert_management/tests/query/test_cert_object_serializer.cpp +++ b/score/crypto/src/daemon/cert_management/tests/query/test_cert_object_serializer.cpp @@ -134,8 +134,11 @@ struct SlotHandlerStub : public cert::ICertSlotHandler return score::crypto::make_unexpected(score::crypto::daemon::common::DaemonErrorCode::kUnsupportedOperation); } - score::crypto::Expected - StoreCrl(const cert::CertSlotConfig&, score::crypto::span, score::crypto::FormatType) override + score::crypto::Expected StoreCrl( + const cert::CertSlotConfig&, + score::crypto::span, + score::crypto::FormatType, + std::int64_t) override { return score::crypto::make_unexpected(score::crypto::daemon::common::DaemonErrorCode::kUnsupportedOperation); } diff --git a/score/crypto/src/daemon/cert_management/tests/slot/test_file_backed_slot_handler.cpp b/score/crypto/src/daemon/cert_management/tests/slot/test_file_backed_slot_handler.cpp index 2f4f13131..bf5d57ff8 100644 --- a/score/crypto/src/daemon/cert_management/tests/slot/test_file_backed_slot_handler.cpp +++ b/score/crypto/src/daemon/cert_management/tests/slot/test_file_backed_slot_handler.cpp @@ -57,6 +57,16 @@ class FakeParser final : public provider::ICertParser return score::crypto::make_unexpected(parsed.error()); return std::vector{*parsed}; } + + score::crypto::Expected ValidateCrl(const std::uint8_t*, + std::size_t, + score::crypto::FormatType, + const std::uint8_t*, + std::size_t, + score::crypto::FormatType) override + { + return 0; + } }; class FileBackedSlotHandlerTest : public ::testing::Test diff --git a/score/crypto/src/daemon/cert_management/truststore/trust_store_handler.cpp b/score/crypto/src/daemon/cert_management/truststore/trust_store_handler.cpp index cad3b36c7..e965a9b1b 100644 --- a/score/crypto/src/daemon/cert_management/truststore/trust_store_handler.cpp +++ b/score/crypto/src/daemon/cert_management/truststore/trust_store_handler.cpp @@ -54,15 +54,35 @@ void TrustStoreHandler::NotifySlotUpdate(CertSlotHandle slot, CertObject::Sptr c m_slots[slot.index] = std::move(cert); } +std::vector TrustStoreHandler::GetCrls() +{ + EnsureLoaded(); + std::vector result; + result.reserve(m_crls.size()); + for (const auto& [slot_index, entry] : m_crls) + result.push_back(entry); + return result; +} + +void TrustStoreHandler::NotifyCrlUpdate(CertSlotHandle slot, std::optional entry) +{ + if (entry.has_value()) + m_crls[slot.index] = std::move(*entry); + else + m_crls.erase(slot.index); +} + void TrustStoreHandler::InvalidateSlot(CertSlotHandle slot) { m_slots.erase(slot.index); + m_crls.erase(slot.index); m_loaded = false; } void TrustStoreHandler::ClearAnchorCache() { m_slots.clear(); + m_crls.clear(); m_loaded = false; } diff --git a/score/crypto/src/daemon/cert_management/truststore/trust_store_handler.hpp b/score/crypto/src/daemon/cert_management/truststore/trust_store_handler.hpp index 44aae5e96..2d5a0ab20 100644 --- a/score/crypto/src/daemon/cert_management/truststore/trust_store_handler.hpp +++ b/score/crypto/src/daemon/cert_management/truststore/trust_store_handler.hpp @@ -40,16 +40,13 @@ class TrustStoreHandler final : public ITrustStoreHandler score::crypto::Expected, common::DaemonErrorCode> GetAnchors() override; void NotifySlotUpdate(CertSlotHandle slot, CertObject::Sptr cert) override; + std::vector GetCrls() override; + void NotifyCrlUpdate(CertSlotHandle slot, std::optional entry) override; + CertObject::Sptr FindBySubject(const std::string& subject) const override; CertObject::Sptr FindBySkid(score::crypto::span skid) const override; - /// Remove one slot's cached cert and force a full reload on the next GetAnchors() call. - /// Called by TrustStoreManager when a member slot's content changes externally. void InvalidateSlot(CertSlotHandle slot); - - /// Drop all cached anchor strong-refs and reset the loaded flag. - /// Called by TrustStoreManager when the last DataNode for this store is released, - /// so that the CertObject memory is freed when no other store holds it. void ClearAnchorCache(); private: @@ -58,6 +55,7 @@ class TrustStoreHandler final : public ITrustStoreHandler TrustStoreHandle m_handle{}; std::unordered_map m_slots; + std::unordered_map m_crls; AnchorLoader m_loader; bool m_loaded{false}; }; diff --git a/score/crypto/src/daemon/cert_management/truststore/trust_store_manager.cpp b/score/crypto/src/daemon/cert_management/truststore/trust_store_manager.cpp index 700df34b1..1806cfbd4 100644 --- a/score/crypto/src/daemon/cert_management/truststore/trust_store_manager.cpp +++ b/score/crypto/src/daemon/cert_management/truststore/trust_store_manager.cpp @@ -1,700 +1,748 @@ -/******************************************************************************** - * 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/cert_management/truststore/trust_store_manager.hpp" -#include "score/crypto/src/daemon/cert_management/slot/deployment_loader.hpp" -#include "score/crypto/src/daemon/cert_management/slot/deployment_writer.hpp" -#include "score/crypto/src/daemon/common/hex.hpp" -#include "score/crypto/src/daemon/control_plane/control_protocol.h" - -#include "score/mw/log/logging.h" - -#include - -namespace score::crypto::daemon::cert_management -{ -namespace -{ -using Error = common::DaemonErrorCode; - -std::optional> CopyFingerprint(score::crypto::span fingerprint) -{ - if (fingerprint.size() != 32U) - return std::nullopt; - - std::array copy{}; - std::copy(fingerprint.begin(), fingerprint.end(), copy.begin()); - return copy; -} - -} // namespace - -// --------------------------------------------------------------------------- -// Startup -// --------------------------------------------------------------------------- - -void TrustStoreManager::Load(const std::vector& configs, - CertSlotRegistry::Sptr registry, - CertSlotHandlerFactory slot_handler_factory) -{ - std::lock_guard lock(m_mutex); - m_stores.clear(); - m_name_index.clear(); - m_app_resource_map.clear(); - m_slot_memberships.clear(); - m_member_states.clear(); - m_slot_cert_cache.clear(); - m_client_ref_counts.clear(); - m_slot_registry = std::move(registry); - m_slot_handler_factory = std::move(slot_handler_factory); - m_slot_handlers.clear(); // lazy cache — populated by GetOrCreateHandler() on first access - for (const auto& config : configs) - { - const TrustStoreId id = static_cast(m_stores.size()); - // Loader lambda is called by TrustStoreHandler::EnsureLoaded() on the first - // GetAnchors() — never at startup. 'this' is safe: the handler is owned by - // TrustStoreManager and never outlives it. - auto handler = std::make_shared(TrustStoreHandle{id}, [this, id](TrustStoreHandler& h) { - this->LoadAnchorsIntoHandler(id, h); - }); - TrustStoreEntry entry{config, handler, id}; - m_stores.push_back(std::move(entry)); - m_name_index[config.store_name] = id; - LoadState(id); // reads m_member_states from descriptor — no cert I/O - if (!m_slot_registry) - continue; - // Build reverse membership index; no cert bytes loaded here. - for (const auto& member : config.members) - { - const auto slot = m_slot_registry->ResolveSlotInternal(member.slot_name); - if (slot) - m_slot_memberships[slot->index].push_back(id); - } - } -} - -// --------------------------------------------------------------------------- -// State helpers (unchanged logic, no cert loading) -// --------------------------------------------------------------------------- - -void TrustStoreManager::LoadState(TrustStoreId id) -{ - if (!m_slot_registry) - return; - if (id >= m_stores.size() || m_stores[id].config.deployment_path.empty()) - return; - const auto descriptor = - DeploymentLoader::Load(m_stores[id].config.deployment_path, m_stores[id].config.deployment_format); - if (!descriptor) - return; - const auto section = descriptor->sections.find(std::string{cert_section_names::kTrustStoreState}); - if (section == descriptor->sections.end()) - return; - for (const auto& member : m_stores[id].config.members) - { - const auto slot = m_slot_registry->ResolveSlotInternal(member.slot_name); - if (!slot) - continue; - MemberState state; - const auto prefix = std::string{"slot."} + member.slot_name + "."; - const auto enabled = section->second.find(prefix + "enabled"); - if (enabled != section->second.end()) - state.enabled = enabled->second == "true"; - const auto fingerprint = section->second.find(prefix + "accepted_fingerprint"); - if (fingerprint != section->second.end()) - { - const auto decoded = common::DecodeHex(fingerprint->second); - if (decoded && decoded->size() == 32U) - { - std::array accepted{}; - std::copy(decoded->begin(), decoded->end(), accepted.begin()); - state.accepted_fingerprint = accepted; - } - } - m_member_states[id][slot->index] = state; - } -} - -score::crypto::Expected TrustStoreManager::PersistState(TrustStoreId id) const -{ - if (!m_slot_registry) - return std::monostate{}; - if (id >= m_stores.size() || m_stores[id].config.deployment_path.empty()) - return std::monostate{}; - common::storage::DeploymentDescriptor descriptor; - const auto existing = - DeploymentLoader::Load(m_stores[id].config.deployment_path, m_stores[id].config.deployment_format); - if (existing) - descriptor = *existing; - const auto states = m_member_states.find(id); - if (states != m_member_states.end()) - { - for (const auto& member : m_stores[id].config.members) - { - const auto slot = m_slot_registry->ResolveSlotInternal(member.slot_name); - if (!slot) - continue; - const auto state_it = states->second.find(slot->index); - if (state_it == states->second.end()) - continue; - const auto& state = state_it->second; - const auto prefix = std::string{"slot."} + member.slot_name + "."; - descriptor.Set(std::string{cert_section_names::kTrustStoreState}, - prefix + "enabled", - state.enabled ? "true" : "false"); - if (state.accepted_fingerprint.has_value()) - descriptor.Set(std::string{cert_section_names::kTrustStoreState}, - prefix + "accepted_fingerprint", - common::EncodeHex(score::crypto::span{ - state.accepted_fingerprint->data(), state.accepted_fingerprint->size()})); - } - } - return DeploymentWriter::Write( - m_stores[id].config.deployment_path, m_stores[id].config.deployment_format, descriptor); -} - -// --------------------------------------------------------------------------- -// Lazy anchor loading — called by TrustStoreHandler::EnsureLoaded() -// --------------------------------------------------------------------------- - -ICertSlotHandler* TrustStoreManager::GetOrCreateHandler(CertSlotHandle slot) -{ - // Must be called with m_mutex held. - auto it = m_slot_handlers.find(slot.index); - if (it != m_slot_handlers.end()) - return it->second.get(); - - if (!m_slot_handler_factory || !m_slot_registry) - return nullptr; - const auto cfg = m_slot_registry->GetConfig(slot); - if (!cfg) - return nullptr; - auto handler = m_slot_handler_factory(**cfg); - if (!handler) - return nullptr; - return m_slot_handlers.emplace(slot.index, std::move(handler)).first->second.get(); -} - -CertObject::Sptr TrustStoreManager::LoadOrGetCached(CertSlotHandle slot) -{ - // Must be called with m_mutex held. - auto& weak = m_slot_cert_cache[slot.index]; - if (auto cert = weak.lock()) - return cert; // another active trust store handler already holds it - - auto* handler = GetOrCreateHandler(slot); - const auto cfg = m_slot_registry ? m_slot_registry->GetConfig(slot) : nullptr; - if (!handler || !cfg) - return nullptr; - - auto loaded = handler->LoadCertificate(**cfg); - if (!loaded) - return nullptr; - - weak = *loaded; // cache as weak_ptr; expires when all handler caches drop their strong ref - return std::move(*loaded); -} - -void TrustStoreManager::LoadAnchorsIntoHandler(TrustStoreId id, TrustStoreHandler& handler) -{ - std::lock_guard lock(m_mutex); - if (id >= m_stores.size() || !m_slot_registry) - return; - - const auto& store = m_stores[id]; - for (const auto& member : store.config.members) - { - const auto slot = m_slot_registry->ResolveSlotInternal(member.slot_name); - if (!slot) - continue; - - auto& state = m_member_states[id][slot->index]; - if (!state.enabled) - { - handler.NotifySlotUpdate(*slot, nullptr); - continue; - } - - auto cert = LoadOrGetCached(*slot); - if (!cert) - { - handler.NotifySlotUpdate(*slot, nullptr); - continue; - } - - bool usable = true; - if (member.kind == TrustStoreMemberKind::kConditionalExternal) - { - const auto fingerprint = cert->GetFingerprint(); - if (state.accepted_fingerprint.has_value()) - { - const bool unchanged = - fingerprint.size() == state.accepted_fingerprint->size() && - std::equal(fingerprint.begin(), fingerprint.end(), state.accepted_fingerprint->begin()); - if (!unchanged) - { - // Content changed without acknowledgement — disable for this store only. - state.enabled = false; - usable = false; - score::mw::log::LogWarn() - << kLogPrefix << "Conditional slot '" << member.slot_name << "' fingerprint mismatch in store '" - << store.config.store_name << "' — disabled until acknowledged"; - } - } - else if (store.config.conditional_slot_initialization == - ConditionalSlotInitialization::kEnableAndAcceptCurrent) - { - state.accepted_fingerprint = CopyFingerprint(fingerprint); - } - else - { - usable = false; - } - } - - handler.NotifySlotUpdate(*slot, usable ? cert : nullptr); - } -} - -// --------------------------------------------------------------------------- -// Store access -// --------------------------------------------------------------------------- - -ITrustStoreHandler::Sptr TrustStoreManager::GetStore(TrustStoreId id) const -{ - std::lock_guard lock(m_mutex); - return id < m_stores.size() ? m_stores[id].handler : nullptr; -} - -TrustStoreId TrustStoreManager::ResolveByName(const std::string& name) const -{ - std::lock_guard lock(m_mutex); - const auto it = m_name_index.find(name); - return it == m_name_index.end() ? UINT32_MAX : it->second; -} - -void TrustStoreManager::RegisterAppResource(uint32_t uid, - const std::string& app_resource_id, - const std::string& store_name) -{ - std::lock_guard lock(m_mutex); - m_app_resource_map[uid][app_resource_id] = store_name; -} - -score::crypto::Expected TrustStoreManager::ResolveAppResource( - const std::string& app_resource_id, - data_manager::ClientId client_id) const -{ - std::lock_guard lock(m_mutex); - const uint32_t uid = control_plane::protocol::GetUidFromClientId(client_id); - const auto resources = m_app_resource_map.find(uid); - if (resources == m_app_resource_map.end()) - return score::crypto::make_unexpected(Error::kInvalidResourceId); - const auto resource = resources->second.find(app_resource_id); - if (resource == resources->second.end()) - return score::crypto::make_unexpected(Error::kInvalidResourceId); - const auto store = m_name_index.find(resource->second); - if (store == m_name_index.end()) - return score::crypto::make_unexpected(Error::kInvalidResourceId); - return TrustStoreHandle{store->second}; -} - -// --------------------------------------------------------------------------- -// Slot membership query -// --------------------------------------------------------------------------- - -std::vector TrustStoreManager::GetMembershipsForSlot(CertSlotHandle slot) const -{ - std::lock_guard lock(m_mutex); - const auto it = m_slot_memberships.find(slot.index); - return it == m_slot_memberships.end() ? std::vector{} : it->second; -} - -// --------------------------------------------------------------------------- -// Ref counting — verification context lifecycle (per-client scoped) -// --------------------------------------------------------------------------- - -void TrustStoreManager::MaybeEvictAnchorCache(TrustStoreHandle handle) -{ - // Must be called with m_mutex held. - for (const auto& [cid, stores] : m_client_ref_counts) - { - if (stores.count(handle.index)) - return; // at least one client still active for this store - } - if (handle.index < m_stores.size()) - { - auto* h = static_cast(m_stores[handle.index].handler.get()); - if (h) - h->ClearAnchorCache(); - } - score::mw::log::LogDebug() << kLogPrefix << "No active clients for store index " << handle.index - << " — anchor cache cleared"; -} - -void TrustStoreManager::AddRef(TrustStoreHandle handle, data_manager::ClientId client_id) -{ - std::lock_guard lock(m_mutex); - ++m_client_ref_counts[client_id][handle.index]; -} - -void TrustStoreManager::ReleaseRef(TrustStoreHandle handle, data_manager::ClientId client_id) -{ - std::lock_guard lock(m_mutex); - const auto client_it = m_client_ref_counts.find(client_id); - if (client_it == m_client_ref_counts.end()) - return; - auto& per_store = client_it->second; - const auto store_it = per_store.find(handle.index); - if (store_it == per_store.end() || store_it->second == 0U) - return; - if (--store_it->second == 0U) - { - per_store.erase(store_it); - if (per_store.empty()) - m_client_ref_counts.erase(client_it); - MaybeEvictAnchorCache(handle); - } -} - -void TrustStoreManager::CleanupClient(data_manager::ClientId client_id) -{ - std::lock_guard lock(m_mutex); - const auto client_it = m_client_ref_counts.find(client_id); - if (client_it == m_client_ref_counts.end()) - return; - // Collect affected stores before erasing the client entry. - std::vector affected; - affected.reserve(client_it->second.size()); - for (const auto& [store_id, count] : client_it->second) - affected.push_back(TrustStoreHandle{store_id}); - m_client_ref_counts.erase(client_it); - // Evict caches for stores that now have no remaining active clients. - for (const auto& handle : affected) - MaybeEvictAnchorCache(handle); -} - -// --------------------------------------------------------------------------- -// Slot change notification -// --------------------------------------------------------------------------- - -void TrustStoreManager::NotifySlotChanged(TrustStoreId id, CertSlotHandle changed_slot) -{ - std::lock_guard lock(m_mutex); - if (id >= m_stores.size()) - return; - // Evict the changed slot from the shared cache so LoadOrGetCached does fresh I/O. - m_slot_cert_cache.erase(changed_slot.index); - // Tell the handler to drop just this slot and mark itself for reload. - auto* h = static_cast(m_stores[id].handler.get()); - if (h) - h->InvalidateSlot(changed_slot); -} - -// --------------------------------------------------------------------------- -// Mutation operations -// --------------------------------------------------------------------------- - -std::optional TrustStoreManager::ResolveMember(const TrustStoreMemberConfig& member) -{ - if (!m_slot_registry) - return std::nullopt; - const auto slot = m_slot_registry->ResolveSlotInternal(member.slot_name); - if (!slot) - return std::nullopt; - const auto cfg = m_slot_registry->GetConfig(*slot); - if (!cfg) - return std::nullopt; - auto* handler = GetOrCreateHandler(*slot); - if (!handler) - return std::nullopt; - return ResolvedMember{*slot, handler, *cfg}; -} - -score::crypto::Expected TrustStoreManager::ResolveSlotBackend( - CertSlotHandle slot) -{ - if (!m_slot_registry) - return score::crypto::make_unexpected(Error::kInvalidResourceId); - const auto cfg = m_slot_registry->GetConfig(slot); - if (!cfg) - return score::crypto::make_unexpected(Error::kInvalidResourceId); - auto* handler = GetOrCreateHandler(slot); - if (!handler) - return score::crypto::make_unexpected(Error::kInvalidResourceId); - return ResolvedBackend{*cfg, handler}; -} - -score::crypto::Expected TrustStoreManager::AddMember( - TrustStoreId id, - CertObject::Sptr cert, - data_manager::ClientId client_id, - score::crypto::span crl_bytes, - score::crypto::FormatType crl_format) -{ - std::lock_guard lock(m_mutex); - if (id >= m_stores.size() || !cert) - return score::crypto::make_unexpected(Error::kInvalidArgument); - if (!AccessPolicyEnforcer::CheckTrustStoreWritePermission(m_stores[id].config, client_id).has_value()) - return score::crypto::make_unexpected(Error::kAccessDenied); - - const auto& cert_fp = cert->GetFingerprint(); - const bool has_crl = !crl_bytes.empty(); - - // Dedup: check all member types before consuming an exclusive slot. - for (const auto& member : m_stores[id].config.members) - { - const auto resolved = ResolveMember(member); - if (!resolved) - continue; - const auto current = resolved->handler->LoadCertificate(*resolved->cfg); - if (!current) - continue; - const auto& current_fp = (*current)->GetFingerprint(); - if (current_fp.size() != cert_fp.size() || !std::equal(current_fp.begin(), current_fp.end(), cert_fp.begin())) - continue; - - // Fingerprint match — cert is already a member. - if (member.kind == TrustStoreMemberKind::kExclusiveMutable) - { - // Upsert CRL if provided — trust store owns this exclusive slot. - if (has_crl) - static_cast(resolved->handler->StoreCrl(*resolved->cfg, crl_bytes, crl_format)); - // Re-enable in case it was disabled and refresh the anchor cache. - m_member_states[id][resolved->slot.index].enabled = true; - m_slot_cert_cache[resolved->slot.index] = cert; - static_cast(m_stores[id].handler.get())->NotifySlotUpdate(resolved->slot, cert); - if (const auto persisted = PersistState(id); !persisted) - return score::crypto::make_unexpected(persisted.error()); - return std::monostate{}; - } - - // Shared-static or conditional-external: trust store does not own this slot. - // CRL writes on externally-managed slots must go through ImportCrl directly. - if (has_crl) - { - score::mw::log::LogError() << kLogPrefix << "AddMember: cannot write CRL to non-exclusive member '" - << member.slot_name << "' — use ImportCrl on the slot resource directly"; - return score::crypto::make_unexpected(Error::kUnsupportedOperation); - } - return std::monostate{}; - } - - // No fingerprint match — find an empty exclusive slot and store the cert. - for (const auto& member : m_stores[id].config.members) - { - if (member.kind != TrustStoreMemberKind::kExclusiveMutable) - continue; - const auto resolved = ResolveMember(member); - if (!resolved) - continue; - const auto state = resolved->handler->GetSlotState(*resolved->cfg); - if (!state || *state != score::crypto::CertificateSlotState::kEmpty) - continue; // occupied by a different cert - - const auto stored = resolved->handler->StoreCertificate(*resolved->cfg, *cert); - if (!stored) - return score::crypto::make_unexpected(stored.error()); - - // Best-effort CRL write — cert already persisted; CRL failure is non-fatal. - // Caller may retry via ImportCrlForMember. - if (has_crl) - static_cast(resolved->handler->StoreCrl(*resolved->cfg, crl_bytes, crl_format)); - - m_member_states[id][resolved->slot.index].enabled = true; - m_slot_cert_cache[resolved->slot.index] = cert; - static_cast(m_stores[id].handler.get())->NotifySlotUpdate(resolved->slot, cert); - if (const auto persisted = PersistState(id); !persisted) - return score::crypto::make_unexpected(persisted.error()); - return std::monostate{}; - } - return score::crypto::make_unexpected(Error::kTrustStoreCapacityExceeded); -} - -score::crypto::Expected TrustStoreManager::ImportCrlForMember( - TrustStoreId id, - CertSlotHandle slot, - score::crypto::span crl_data, - score::crypto::FormatType format) -{ - std::lock_guard lock(m_mutex); - if (id >= m_stores.size() || !slot.IsValid()) - return score::crypto::make_unexpected(Error::kInvalidResourceId); - if (crl_data.empty()) - return score::crypto::make_unexpected(Error::kInvalidArgument); - - for (const auto& member : m_stores[id].config.members) - { - const auto resolved = ResolveMember(member); - if (!resolved || resolved->slot.index != slot.index) - continue; - if (member.kind != TrustStoreMemberKind::kExclusiveMutable) - return score::crypto::make_unexpected(Error::kUnsupportedOperation); - return resolved->handler->StoreCrl(*resolved->cfg, crl_data, format); - } - return score::crypto::make_unexpected(Error::kInvalidResourceId); -} - -score::crypto::Expected TrustStoreManager::RemoveMember(TrustStoreId id, - const std::vector& fingerprint, - data_manager::ClientId client_id) -{ - std::lock_guard lock(m_mutex); - if (id >= m_stores.size()) - return score::crypto::make_unexpected(Error::kInvalidResourceId); - if (!AccessPolicyEnforcer::CheckTrustStoreWritePermission(m_stores[id].config, client_id).has_value()) - return score::crypto::make_unexpected(Error::kAccessDenied); - for (const auto& member : m_stores[id].config.members) - { - if (member.kind != TrustStoreMemberKind::kExclusiveMutable) - continue; - const auto resolved = ResolveMember(member); - if (!resolved) - continue; - const auto current = resolved->handler->LoadCertificate(*resolved->cfg); - if (!current) - continue; - const auto& current_fp = (*current)->GetFingerprint(); - if (current_fp.size() != fingerprint.size() || - !std::equal(current_fp.begin(), current_fp.end(), fingerprint.begin())) - continue; - if (const auto cleared = resolved->handler->ClearSlot(*resolved->cfg); !cleared) - return score::crypto::make_unexpected(cleared.error()); - // Cert bytes gone — evict from shared cache and clear from handler. - m_slot_cert_cache.erase(resolved->slot.index); - m_member_states[id][resolved->slot.index].enabled = false; - static_cast(m_stores[id].handler.get())->NotifySlotUpdate(resolved->slot, nullptr); - if (const auto persisted = PersistState(id); !persisted) - return score::crypto::make_unexpected(persisted.error()); - return std::monostate{}; - } - return score::crypto::make_unexpected(Error::kInvalidArgument); -} - -score::crypto::Expected TrustStoreManager::EnableMember(TrustStoreId id, - CertSlotHandle slot, - data_manager::ClientId client_id) -{ - std::lock_guard lock(m_mutex); - if (id >= m_stores.size() || !slot.IsValid()) - return score::crypto::make_unexpected(Error::kInvalidResourceId); - if (!AccessPolicyEnforcer::CheckTrustStoreWritePermission(m_stores[id].config, client_id).has_value()) - return score::crypto::make_unexpected(Error::kAccessDenied); - // Load via shared cache so other stores benefit from the strong ref. - auto cert = LoadOrGetCached(slot); - m_member_states[id][slot.index].enabled = true; - static_cast(m_stores[id].handler.get())->NotifySlotUpdate(slot, std::move(cert)); - if (const auto persisted = PersistState(id); !persisted) - return score::crypto::make_unexpected(persisted.error()); - return std::monostate{}; -} - -score::crypto::Expected TrustStoreManager::DisableMember(TrustStoreId id, - CertSlotHandle slot, - data_manager::ClientId client_id) -{ - std::lock_guard lock(m_mutex); - if (id >= m_stores.size() || !slot.IsValid()) - return score::crypto::make_unexpected(Error::kInvalidResourceId); - if (!AccessPolicyEnforcer::CheckTrustStoreWritePermission(m_stores[id].config, client_id).has_value()) - return score::crypto::make_unexpected(Error::kAccessDenied); - m_member_states[id][slot.index].enabled = false; - static_cast(m_stores[id].handler.get())->NotifySlotUpdate(slot, nullptr); - if (const auto persisted = PersistState(id); !persisted) - return score::crypto::make_unexpected(persisted.error()); - return std::monostate{}; -} - -score::crypto::Expected -TrustStoreManager::AcknowledgeMemberUpdate(TrustStoreId id, CertSlotHandle slot, data_manager::ClientId client_id) -{ - std::lock_guard lock(m_mutex); - if (id >= m_stores.size() || !slot.IsValid()) - return score::crypto::make_unexpected(Error::kInvalidResourceId); - if (!AccessPolicyEnforcer::CheckTrustStoreWritePermission(m_stores[id].config, client_id).has_value()) - return score::crypto::make_unexpected(Error::kAccessDenied); - // Fresh load to capture the new cert and update the shared cache. - const auto resolved = ResolveSlotBackend(slot); - if (!resolved) - return score::crypto::make_unexpected(resolved.error()); - auto loaded = resolved->handler->LoadCertificate(*resolved->cfg); - if (!loaded) - return score::crypto::make_unexpected(Error::kInvalidArgument); - auto cert = std::move(*loaded); - m_slot_cert_cache[slot.index] = cert; - auto& state = m_member_states[id][slot.index]; - state.accepted_fingerprint = CopyFingerprint(cert->GetFingerprint()); - state.enabled = true; - static_cast(m_stores[id].handler.get())->NotifySlotUpdate(slot, cert); - if (const auto persisted = PersistState(id); !persisted) - return score::crypto::make_unexpected(persisted.error()); - return std::monostate{}; -} - -const TrustStoreConfig* TrustStoreManager::GetStoreConfig(TrustStoreId id) const -{ - std::lock_guard lock(m_mutex); - return id < m_stores.size() ? &m_stores[id].config : nullptr; -} - -// --------------------------------------------------------------------------- -// Member snapshot (for ITrustStoreObject read-only view) -// --------------------------------------------------------------------------- - -std::vector TrustStoreManager::GetMembersSnapshot(TrustStoreId id) -{ - std::lock_guard lock(m_mutex); - if (id >= m_stores.size() || !m_slot_registry) - return {}; - - std::vector result; - const auto& store = m_stores[id]; - - for (const auto& member : store.config.members) - { - const auto slot = m_slot_registry->ResolveSlotInternal(member.slot_name); - if (!slot) - continue; - - auto cert = LoadOrGetCached(*slot); - if (!cert) - continue; // slot is empty — omit from snapshot - - MemberSnapshot snap; - snap.slot_handle = *slot; - snap.slot_name = member.slot_name; - snap.kind = member.kind; - - const auto& meta = cert->GetChainMetadata(); - snap.subject = meta.subject_canonical; - snap.issuer = meta.issuer_canonical; - snap.serial_number = meta.serial_number_hex; - - const auto fp = cert->GetFingerprint(); - if (fp.size() == 32U) - std::copy(fp.begin(), fp.end(), snap.fingerprint.begin()); - - // Default enabled=true; override from persisted state if present. - const auto states_it = m_member_states.find(id); - if (states_it != m_member_states.end()) - { - const auto state_it = states_it->second.find(slot->index); - if (state_it != states_it->second.end()) - snap.is_enabled = state_it->second.enabled; - } - - result.push_back(std::move(snap)); - } - return result; -} - -} // namespace score::crypto::daemon::cert_management +/******************************************************************************** + * 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/cert_management/truststore/trust_store_manager.hpp" +#include "score/crypto/src/daemon/cert_management/slot/deployment_loader.hpp" +#include "score/crypto/src/daemon/cert_management/slot/deployment_writer.hpp" +#include "score/crypto/src/daemon/common/hex.hpp" +#include "score/crypto/src/daemon/control_plane/control_protocol.h" + +#include "score/mw/log/logging.h" + +#include + +namespace score::crypto::daemon::cert_management +{ +namespace +{ +using Error = common::DaemonErrorCode; + +std::optional> CopyFingerprint(score::crypto::span fingerprint) +{ + if (fingerprint.size() != 32U) + return std::nullopt; + + std::array copy{}; + std::copy(fingerprint.begin(), fingerprint.end(), copy.begin()); + return copy; +} + +} // namespace + +// --------------------------------------------------------------------------- +// Startup +// --------------------------------------------------------------------------- + +void TrustStoreManager::Load(const std::vector& configs, + CertSlotRegistry::Sptr registry, + CertSlotHandlerFactory slot_handler_factory) +{ + std::lock_guard lock(m_mutex); + m_stores.clear(); + m_name_index.clear(); + m_app_resource_map.clear(); + m_slot_memberships.clear(); + m_member_states.clear(); + m_slot_cert_cache.clear(); + m_client_ref_counts.clear(); + m_slot_registry = std::move(registry); + m_slot_handler_factory = std::move(slot_handler_factory); + m_slot_handlers.clear(); // lazy cache — populated by GetOrCreateHandler() on first access + for (const auto& config : configs) + { + const TrustStoreId id = static_cast(m_stores.size()); + // Loader lambda is called by TrustStoreHandler::EnsureLoaded() on the first + // GetAnchors() — never at startup. 'this' is safe: the handler is owned by + // TrustStoreManager and never outlives it. + auto handler = std::make_shared(TrustStoreHandle{id}, [this, id](TrustStoreHandler& h) { + this->LoadAnchorsIntoHandler(id, h); + }); + TrustStoreEntry entry{config, handler, id}; + m_stores.push_back(std::move(entry)); + m_name_index[config.store_name] = id; + LoadState(id); // reads m_member_states from descriptor — no cert I/O + if (!m_slot_registry) + continue; + // Build reverse membership index; no cert bytes loaded here. + for (const auto& member : config.members) + { + const auto slot = m_slot_registry->ResolveSlotInternal(member.slot_name); + if (slot) + m_slot_memberships[slot->index].push_back(id); + } + } +} + +// --------------------------------------------------------------------------- +// State helpers (unchanged logic, no cert loading) +// --------------------------------------------------------------------------- + +void TrustStoreManager::LoadState(TrustStoreId id) +{ + if (!m_slot_registry) + return; + if (id >= m_stores.size() || m_stores[id].config.deployment_path.empty()) + return; + const auto descriptor = + DeploymentLoader::Load(m_stores[id].config.deployment_path, m_stores[id].config.deployment_format); + if (!descriptor) + return; + const auto section = descriptor->sections.find(std::string{cert_section_names::kTrustStoreState}); + if (section == descriptor->sections.end()) + return; + for (const auto& member : m_stores[id].config.members) + { + const auto slot = m_slot_registry->ResolveSlotInternal(member.slot_name); + if (!slot) + continue; + MemberState state; + const auto prefix = std::string{"slot."} + member.slot_name + "."; + const auto enabled = section->second.find(prefix + "enabled"); + if (enabled != section->second.end()) + state.enabled = enabled->second == "true"; + const auto fingerprint = section->second.find(prefix + "accepted_fingerprint"); + if (fingerprint != section->second.end()) + { + const auto decoded = common::DecodeHex(fingerprint->second); + if (decoded && decoded->size() == 32U) + { + std::array accepted{}; + std::copy(decoded->begin(), decoded->end(), accepted.begin()); + state.accepted_fingerprint = accepted; + } + } + m_member_states[id][slot->index] = state; + } +} + +score::crypto::Expected TrustStoreManager::PersistState(TrustStoreId id) const +{ + if (!m_slot_registry) + return std::monostate{}; + if (id >= m_stores.size() || m_stores[id].config.deployment_path.empty()) + return std::monostate{}; + common::storage::DeploymentDescriptor descriptor; + const auto existing = + DeploymentLoader::Load(m_stores[id].config.deployment_path, m_stores[id].config.deployment_format); + if (existing) + descriptor = *existing; + const auto states = m_member_states.find(id); + if (states != m_member_states.end()) + { + for (const auto& member : m_stores[id].config.members) + { + const auto slot = m_slot_registry->ResolveSlotInternal(member.slot_name); + if (!slot) + continue; + const auto state_it = states->second.find(slot->index); + if (state_it == states->second.end()) + continue; + const auto& state = state_it->second; + const auto prefix = std::string{"slot."} + member.slot_name + "."; + descriptor.Set(std::string{cert_section_names::kTrustStoreState}, + prefix + "enabled", + state.enabled ? "true" : "false"); + if (state.accepted_fingerprint.has_value()) + descriptor.Set(std::string{cert_section_names::kTrustStoreState}, + prefix + "accepted_fingerprint", + common::EncodeHex(score::crypto::span{ + state.accepted_fingerprint->data(), state.accepted_fingerprint->size()})); + } + } + return DeploymentWriter::Write( + m_stores[id].config.deployment_path, m_stores[id].config.deployment_format, descriptor); +} + +// --------------------------------------------------------------------------- +// Lazy anchor loading — called by TrustStoreHandler::EnsureLoaded() +// --------------------------------------------------------------------------- + +ICertSlotHandler* TrustStoreManager::GetOrCreateHandler(CertSlotHandle slot) +{ + // Must be called with m_mutex held. + auto it = m_slot_handlers.find(slot.index); + if (it != m_slot_handlers.end()) + return it->second.get(); + + if (!m_slot_handler_factory || !m_slot_registry) + return nullptr; + const auto cfg = m_slot_registry->GetConfig(slot); + if (!cfg) + return nullptr; + auto handler = m_slot_handler_factory(**cfg); + if (!handler) + return nullptr; + return m_slot_handlers.emplace(slot.index, std::move(handler)).first->second.get(); +} + +CertObject::Sptr TrustStoreManager::LoadOrGetCached(CertSlotHandle slot) +{ + // Must be called with m_mutex held. + auto& weak = m_slot_cert_cache[slot.index]; + if (auto cert = weak.lock()) + return cert; // another active trust store handler already holds it + + auto* handler = GetOrCreateHandler(slot); + const auto cfg = m_slot_registry ? m_slot_registry->GetConfig(slot) : nullptr; + if (!handler || !cfg) + return nullptr; + + auto loaded = handler->LoadCertificate(**cfg); + if (!loaded) + return nullptr; + + weak = *loaded; // cache as weak_ptr; expires when all handler caches drop their strong ref + return std::move(*loaded); +} + +void TrustStoreManager::LoadAnchorsIntoHandler(TrustStoreId id, TrustStoreHandler& handler) +{ + std::lock_guard lock(m_mutex); + if (id >= m_stores.size() || !m_slot_registry) + return; + + const auto& store = m_stores[id]; + for (const auto& member : store.config.members) + { + const auto slot = m_slot_registry->ResolveSlotInternal(member.slot_name); + if (!slot) + continue; + + auto& state = m_member_states[id][slot->index]; + if (!state.enabled) + { + handler.NotifySlotUpdate(*slot, nullptr); + continue; + } + + auto cert = LoadOrGetCached(*slot); + if (!cert) + { + handler.NotifySlotUpdate(*slot, nullptr); + continue; + } + + bool usable = true; + if (member.kind == TrustStoreMemberKind::kConditionalExternal) + { + const auto fingerprint = cert->GetFingerprint(); + if (state.accepted_fingerprint.has_value()) + { + const bool unchanged = + fingerprint.size() == state.accepted_fingerprint->size() && + std::equal(fingerprint.begin(), fingerprint.end(), state.accepted_fingerprint->begin()); + if (!unchanged) + { + // Content changed without acknowledgement — disable for this store only. + state.enabled = false; + usable = false; + score::mw::log::LogWarn() + << kLogPrefix << "Conditional slot '" << member.slot_name << "' fingerprint mismatch in store '" + << store.config.store_name << "' — disabled until acknowledged"; + } + } + else if (store.config.conditional_slot_initialization == + ConditionalSlotInitialization::kEnableAndAcceptCurrent) + { + state.accepted_fingerprint = CopyFingerprint(fingerprint); + } + else + { + usable = false; + } + } + + handler.NotifySlotUpdate(*slot, usable ? cert : nullptr); + + // Co-load the CRL for this slot into the handler's CRL cache. + if (usable && cert) + { + auto* slot_handler = GetOrCreateHandler(*slot); + const auto cfg = m_slot_registry->GetConfig(*slot); + if (slot_handler && cfg && slot_handler->HasCrl(**cfg)) + { + auto crl_res = slot_handler->LoadCrl(**cfg); + if (crl_res) + { + CrlEntry entry{std::move(*crl_res), slot_handler->GetCrlFormat(**cfg)}; + handler.NotifyCrlUpdate(*slot, std::move(entry)); + } + } + } + } +} + +// --------------------------------------------------------------------------- +// Store access +// --------------------------------------------------------------------------- + +ITrustStoreHandler::Sptr TrustStoreManager::GetStore(TrustStoreId id) const +{ + std::lock_guard lock(m_mutex); + return id < m_stores.size() ? m_stores[id].handler : nullptr; +} + +TrustStoreId TrustStoreManager::ResolveByName(const std::string& name) const +{ + std::lock_guard lock(m_mutex); + const auto it = m_name_index.find(name); + return it == m_name_index.end() ? UINT32_MAX : it->second; +} + +void TrustStoreManager::RegisterAppResource(uint32_t uid, + const std::string& app_resource_id, + const std::string& store_name) +{ + std::lock_guard lock(m_mutex); + m_app_resource_map[uid][app_resource_id] = store_name; +} + +score::crypto::Expected TrustStoreManager::ResolveAppResource( + const std::string& app_resource_id, + data_manager::ClientId client_id) const +{ + std::lock_guard lock(m_mutex); + const uint32_t uid = control_plane::protocol::GetUidFromClientId(client_id); + const auto resources = m_app_resource_map.find(uid); + if (resources == m_app_resource_map.end()) + return score::crypto::make_unexpected(Error::kInvalidResourceId); + const auto resource = resources->second.find(app_resource_id); + if (resource == resources->second.end()) + return score::crypto::make_unexpected(Error::kInvalidResourceId); + const auto store = m_name_index.find(resource->second); + if (store == m_name_index.end()) + return score::crypto::make_unexpected(Error::kInvalidResourceId); + return TrustStoreHandle{store->second}; +} + +// --------------------------------------------------------------------------- +// Slot membership query +// --------------------------------------------------------------------------- + +std::vector TrustStoreManager::GetMembershipsForSlot(CertSlotHandle slot) const +{ + std::lock_guard lock(m_mutex); + const auto it = m_slot_memberships.find(slot.index); + return it == m_slot_memberships.end() ? std::vector{} : it->second; +} + +// --------------------------------------------------------------------------- +// Ref counting — verification context lifecycle (per-client scoped) +// --------------------------------------------------------------------------- + +void TrustStoreManager::MaybeEvictAnchorCache(TrustStoreHandle handle) +{ + // Must be called with m_mutex held. + for (const auto& [cid, stores] : m_client_ref_counts) + { + if (stores.count(handle.index)) + return; // at least one client still active for this store + } + if (handle.index < m_stores.size()) + { + auto* h = static_cast(m_stores[handle.index].handler.get()); + if (h) + h->ClearAnchorCache(); + } + score::mw::log::LogDebug() << kLogPrefix << "No active clients for store index " << handle.index + << " — anchor cache cleared"; +} + +void TrustStoreManager::AddRef(TrustStoreHandle handle, data_manager::ClientId client_id) +{ + std::lock_guard lock(m_mutex); + ++m_client_ref_counts[client_id][handle.index]; +} + +void TrustStoreManager::ReleaseRef(TrustStoreHandle handle, data_manager::ClientId client_id) +{ + std::lock_guard lock(m_mutex); + const auto client_it = m_client_ref_counts.find(client_id); + if (client_it == m_client_ref_counts.end()) + return; + auto& per_store = client_it->second; + const auto store_it = per_store.find(handle.index); + if (store_it == per_store.end() || store_it->second == 0U) + return; + if (--store_it->second == 0U) + { + per_store.erase(store_it); + if (per_store.empty()) + m_client_ref_counts.erase(client_it); + MaybeEvictAnchorCache(handle); + } +} + +void TrustStoreManager::CleanupClient(data_manager::ClientId client_id) +{ + std::lock_guard lock(m_mutex); + const auto client_it = m_client_ref_counts.find(client_id); + if (client_it == m_client_ref_counts.end()) + return; + // Collect affected stores before erasing the client entry. + std::vector affected; + affected.reserve(client_it->second.size()); + for (const auto& [store_id, count] : client_it->second) + affected.push_back(TrustStoreHandle{store_id}); + m_client_ref_counts.erase(client_it); + // Evict caches for stores that now have no remaining active clients. + for (const auto& handle : affected) + MaybeEvictAnchorCache(handle); +} + +// --------------------------------------------------------------------------- +// Slot change notification +// --------------------------------------------------------------------------- + +void TrustStoreManager::NotifySlotChanged(TrustStoreId id, CertSlotHandle changed_slot) +{ + std::lock_guard lock(m_mutex); + if (id >= m_stores.size()) + return; + // Evict the changed slot from the shared cache so LoadOrGetCached does fresh I/O. + m_slot_cert_cache.erase(changed_slot.index); + // Tell the handler to drop just this slot and mark itself for reload. + auto* h = static_cast(m_stores[id].handler.get()); + if (h) + h->InvalidateSlot(changed_slot); +} + +// --------------------------------------------------------------------------- +// Mutation operations +// --------------------------------------------------------------------------- + +std::optional TrustStoreManager::ResolveMember(const TrustStoreMemberConfig& member) +{ + if (!m_slot_registry) + return std::nullopt; + const auto slot = m_slot_registry->ResolveSlotInternal(member.slot_name); + if (!slot) + return std::nullopt; + const auto cfg = m_slot_registry->GetConfig(*slot); + if (!cfg) + return std::nullopt; + auto* handler = GetOrCreateHandler(*slot); + if (!handler) + return std::nullopt; + return ResolvedMember{*slot, handler, *cfg}; +} + +score::crypto::Expected TrustStoreManager::ResolveSlotBackend( + CertSlotHandle slot) +{ + if (!m_slot_registry) + return score::crypto::make_unexpected(Error::kInvalidResourceId); + const auto cfg = m_slot_registry->GetConfig(slot); + if (!cfg) + return score::crypto::make_unexpected(Error::kInvalidResourceId); + auto* handler = GetOrCreateHandler(slot); + if (!handler) + return score::crypto::make_unexpected(Error::kInvalidResourceId); + return ResolvedBackend{*cfg, handler}; +} + +score::crypto::Expected TrustStoreManager::AddMember( + TrustStoreId id, + CertObject::Sptr cert, + data_manager::ClientId client_id, + score::crypto::span crl_bytes, + score::crypto::FormatType crl_format) +{ + std::lock_guard lock(m_mutex); + if (id >= m_stores.size() || !cert) + return score::crypto::make_unexpected(Error::kInvalidArgument); + if (!AccessPolicyEnforcer::CheckTrustStoreWritePermission(m_stores[id].config, client_id).has_value()) + return score::crypto::make_unexpected(Error::kAccessDenied); + + const auto& cert_fp = cert->GetFingerprint(); + const bool has_crl = !crl_bytes.empty(); + + // Dedup: check all member types before consuming an exclusive slot. + for (const auto& member : m_stores[id].config.members) + { + const auto resolved = ResolveMember(member); + if (!resolved) + continue; + const auto current = resolved->handler->LoadCertificate(*resolved->cfg); + if (!current) + continue; + const auto& current_fp = (*current)->GetFingerprint(); + if (current_fp.size() != cert_fp.size() || !std::equal(current_fp.begin(), current_fp.end(), cert_fp.begin())) + continue; + + // Fingerprint match — cert is already a member. + if (member.kind == TrustStoreMemberKind::kExclusiveMutable) + { + // Upsert CRL if provided — trust store owns this exclusive slot. + if (has_crl) + { + const auto stored = resolved->handler->StoreCrl(*resolved->cfg, crl_bytes, crl_format); + if (stored) + { + CrlEntry entry{std::vector(crl_bytes.begin(), crl_bytes.end()), crl_format}; + static_cast(m_stores[id].handler.get()) + ->NotifyCrlUpdate(resolved->slot, std::move(entry)); + } + } + // Re-enable in case it was disabled and refresh the anchor cache. + m_member_states[id][resolved->slot.index].enabled = true; + m_slot_cert_cache[resolved->slot.index] = cert; + static_cast(m_stores[id].handler.get())->NotifySlotUpdate(resolved->slot, cert); + if (const auto persisted = PersistState(id); !persisted) + return score::crypto::make_unexpected(persisted.error()); + return std::monostate{}; + } + + // Shared-static or conditional-external: trust store does not own this slot. + // CRL writes on externally-managed slots must go through ImportCrl directly. + if (has_crl) + { + score::mw::log::LogError() << kLogPrefix << "AddMember: cannot write CRL to non-exclusive member '" + << member.slot_name << "' — use ImportCrl on the slot resource directly"; + return score::crypto::make_unexpected(Error::kUnsupportedOperation); + } + return std::monostate{}; + } + + // No fingerprint match — find an empty exclusive slot and store the cert. + for (const auto& member : m_stores[id].config.members) + { + if (member.kind != TrustStoreMemberKind::kExclusiveMutable) + continue; + const auto resolved = ResolveMember(member); + if (!resolved) + continue; + const auto state = resolved->handler->GetSlotState(*resolved->cfg); + if (!state || *state != score::crypto::CertificateSlotState::kEmpty) + continue; // occupied by a different cert + + const auto stored = resolved->handler->StoreCertificate(*resolved->cfg, *cert); + if (!stored) + return score::crypto::make_unexpected(stored.error()); + + // Best-effort CRL write — cert already persisted; CRL failure is non-fatal. + // Caller may retry via ImportCrlForMember. + if (has_crl) + { + const auto stored = resolved->handler->StoreCrl(*resolved->cfg, crl_bytes, crl_format); + if (stored) + { + CrlEntry entry{std::vector(crl_bytes.begin(), crl_bytes.end()), crl_format}; + static_cast(m_stores[id].handler.get()) + ->NotifyCrlUpdate(resolved->slot, std::move(entry)); + } + } + + m_member_states[id][resolved->slot.index].enabled = true; + m_slot_cert_cache[resolved->slot.index] = cert; + static_cast(m_stores[id].handler.get())->NotifySlotUpdate(resolved->slot, cert); + if (const auto persisted = PersistState(id); !persisted) + return score::crypto::make_unexpected(persisted.error()); + return std::monostate{}; + } + return score::crypto::make_unexpected(Error::kTrustStoreCapacityExceeded); +} + +score::crypto::Expected TrustStoreManager::ImportCrlForMember( + TrustStoreId id, + CertSlotHandle slot, + score::crypto::span crl_data, + score::crypto::FormatType format, + std::int64_t next_update_epoch_s) +{ + std::lock_guard lock(m_mutex); + if (id >= m_stores.size() || !slot.IsValid()) + return score::crypto::make_unexpected(Error::kInvalidResourceId); + if (crl_data.empty()) + return score::crypto::make_unexpected(Error::kInvalidArgument); + + for (const auto& member : m_stores[id].config.members) + { + const auto resolved = ResolveMember(member); + if (!resolved || resolved->slot.index != slot.index) + continue; + if (member.kind != TrustStoreMemberKind::kExclusiveMutable) + return score::crypto::make_unexpected(Error::kUnsupportedOperation); + const auto stored = resolved->handler->StoreCrl(*resolved->cfg, crl_data, format, next_update_epoch_s); + if (!stored) + return score::crypto::make_unexpected(stored.error()); + CrlEntry entry{std::vector(crl_data.begin(), crl_data.end()), format}; + static_cast(m_stores[id].handler.get())->NotifyCrlUpdate(resolved->slot, std::move(entry)); + return std::monostate{}; + } + return score::crypto::make_unexpected(Error::kInvalidResourceId); +} + +score::crypto::Expected TrustStoreManager::RemoveMember(TrustStoreId id, + const std::vector& fingerprint, + data_manager::ClientId client_id) +{ + std::lock_guard lock(m_mutex); + if (id >= m_stores.size()) + return score::crypto::make_unexpected(Error::kInvalidResourceId); + if (!AccessPolicyEnforcer::CheckTrustStoreWritePermission(m_stores[id].config, client_id).has_value()) + return score::crypto::make_unexpected(Error::kAccessDenied); + for (const auto& member : m_stores[id].config.members) + { + if (member.kind != TrustStoreMemberKind::kExclusiveMutable) + continue; + const auto resolved = ResolveMember(member); + if (!resolved) + continue; + const auto current = resolved->handler->LoadCertificate(*resolved->cfg); + if (!current) + continue; + const auto& current_fp = (*current)->GetFingerprint(); + if (current_fp.size() != fingerprint.size() || + !std::equal(current_fp.begin(), current_fp.end(), fingerprint.begin())) + continue; + if (const auto cleared = resolved->handler->ClearSlot(*resolved->cfg); !cleared) + return score::crypto::make_unexpected(cleared.error()); + // Cert bytes gone — evict from shared cache and clear from handler. + m_slot_cert_cache.erase(resolved->slot.index); + m_member_states[id][resolved->slot.index].enabled = false; + static_cast(m_stores[id].handler.get())->NotifySlotUpdate(resolved->slot, nullptr); + if (const auto persisted = PersistState(id); !persisted) + return score::crypto::make_unexpected(persisted.error()); + return std::monostate{}; + } + return score::crypto::make_unexpected(Error::kInvalidArgument); +} + +score::crypto::Expected TrustStoreManager::EnableMember(TrustStoreId id, + CertSlotHandle slot, + data_manager::ClientId client_id) +{ + std::lock_guard lock(m_mutex); + if (id >= m_stores.size() || !slot.IsValid()) + return score::crypto::make_unexpected(Error::kInvalidResourceId); + if (!AccessPolicyEnforcer::CheckTrustStoreWritePermission(m_stores[id].config, client_id).has_value()) + return score::crypto::make_unexpected(Error::kAccessDenied); + // Verify the slot is a registered member of this trust store. + const auto ms_it = m_slot_memberships.find(slot.index); + if (ms_it == m_slot_memberships.end() || + std::find(ms_it->second.begin(), ms_it->second.end(), id) == ms_it->second.end()) + return score::crypto::make_unexpected(Error::kInvalidArgument); + // Load via shared cache so other stores benefit from the strong ref. + auto cert = LoadOrGetCached(slot); + m_member_states[id][slot.index].enabled = true; + static_cast(m_stores[id].handler.get())->NotifySlotUpdate(slot, std::move(cert)); + if (const auto persisted = PersistState(id); !persisted) + return score::crypto::make_unexpected(persisted.error()); + return std::monostate{}; +} + +score::crypto::Expected TrustStoreManager::DisableMember(TrustStoreId id, + CertSlotHandle slot, + data_manager::ClientId client_id) +{ + std::lock_guard lock(m_mutex); + if (id >= m_stores.size() || !slot.IsValid()) + return score::crypto::make_unexpected(Error::kInvalidResourceId); + if (!AccessPolicyEnforcer::CheckTrustStoreWritePermission(m_stores[id].config, client_id).has_value()) + return score::crypto::make_unexpected(Error::kAccessDenied); + // Verify the slot is a registered member of this trust store. + const auto ms_it = m_slot_memberships.find(slot.index); + if (ms_it == m_slot_memberships.end() || + std::find(ms_it->second.begin(), ms_it->second.end(), id) == ms_it->second.end()) + return score::crypto::make_unexpected(Error::kInvalidArgument); + m_member_states[id][slot.index].enabled = false; + static_cast(m_stores[id].handler.get())->NotifySlotUpdate(slot, nullptr); + if (const auto persisted = PersistState(id); !persisted) + return score::crypto::make_unexpected(persisted.error()); + return std::monostate{}; +} + +score::crypto::Expected +TrustStoreManager::AcknowledgeMemberUpdate(TrustStoreId id, CertSlotHandle slot, data_manager::ClientId client_id) +{ + std::lock_guard lock(m_mutex); + if (id >= m_stores.size() || !slot.IsValid()) + return score::crypto::make_unexpected(Error::kInvalidResourceId); + if (!AccessPolicyEnforcer::CheckTrustStoreWritePermission(m_stores[id].config, client_id).has_value()) + return score::crypto::make_unexpected(Error::kAccessDenied); + // Fresh load to capture the new cert and update the shared cache. + const auto resolved = ResolveSlotBackend(slot); + if (!resolved) + return score::crypto::make_unexpected(resolved.error()); + auto loaded = resolved->handler->LoadCertificate(*resolved->cfg); + if (!loaded) + return score::crypto::make_unexpected(Error::kInvalidArgument); + auto cert = std::move(*loaded); + m_slot_cert_cache[slot.index] = cert; + auto& state = m_member_states[id][slot.index]; + state.accepted_fingerprint = CopyFingerprint(cert->GetFingerprint()); + state.enabled = true; + static_cast(m_stores[id].handler.get())->NotifySlotUpdate(slot, cert); + if (const auto persisted = PersistState(id); !persisted) + return score::crypto::make_unexpected(persisted.error()); + return std::monostate{}; +} + +const TrustStoreConfig* TrustStoreManager::GetStoreConfig(TrustStoreId id) const +{ + std::lock_guard lock(m_mutex); + return id < m_stores.size() ? &m_stores[id].config : nullptr; +} + +// --------------------------------------------------------------------------- +// Member snapshot (for ITrustStoreObject read-only view) +// --------------------------------------------------------------------------- + +std::vector TrustStoreManager::GetMembersSnapshot(TrustStoreId id) +{ + std::lock_guard lock(m_mutex); + if (id >= m_stores.size() || !m_slot_registry) + return {}; + + std::vector result; + const auto& store = m_stores[id]; + + for (const auto& member : store.config.members) + { + const auto slot = m_slot_registry->ResolveSlotInternal(member.slot_name); + if (!slot) + continue; + + auto cert = LoadOrGetCached(*slot); + if (!cert) + continue; // slot is empty — omit from snapshot + + MemberSnapshot snap; + snap.slot_handle = *slot; + snap.slot_name = member.slot_name; + snap.kind = member.kind; + + const auto& meta = cert->GetChainMetadata(); + snap.subject = meta.subject_canonical; + snap.issuer = meta.issuer_canonical; + snap.serial_number = meta.serial_number_hex; + + const auto fp = cert->GetFingerprint(); + if (fp.size() == 32U) + std::copy(fp.begin(), fp.end(), snap.fingerprint.begin()); + + // Default enabled=true; override from persisted state if present. + const auto states_it = m_member_states.find(id); + if (states_it != m_member_states.end()) + { + const auto state_it = states_it->second.find(slot->index); + if (state_it != states_it->second.end()) + snap.is_enabled = state_it->second.enabled; + } + + result.push_back(std::move(snap)); + } + return result; +} + +} // namespace score::crypto::daemon::cert_management diff --git a/score/crypto/src/daemon/cert_management/truststore/trust_store_manager.hpp b/score/crypto/src/daemon/cert_management/truststore/trust_store_manager.hpp index 401db26c3..bb0cf4210 100644 --- a/score/crypto/src/daemon/cert_management/truststore/trust_store_manager.hpp +++ b/score/crypto/src/daemon/cert_management/truststore/trust_store_manager.hpp @@ -182,7 +182,8 @@ class TrustStoreManager ImportCrlForMember(TrustStoreId id, CertSlotHandle slot, score::crypto::span crl_data, - score::crypto::FormatType format); + score::crypto::FormatType format, + std::int64_t next_update_epoch_s = 0); /// @brief Remove a certificate from a trust store by fingerprint. /// diff --git a/score/crypto/src/daemon/provider/cert_management/cert_types.hpp b/score/crypto/src/daemon/provider/cert_management/cert_types.hpp index 83899038e..879b305f5 100644 --- a/score/crypto/src/daemon/provider/cert_management/cert_types.hpp +++ b/score/crypto/src/daemon/provider/cert_management/cert_types.hpp @@ -51,6 +51,14 @@ enum class CertVerifyErrorCode : std::uint16_t kUnknownError }; +enum class RevocationCheckPolicy : std::uint8_t +{ + kNone = 0, + kCrlOnly, + kOcspOnly, + kOcspWithCrlFallback +}; + /// Verification result. The established chain is returned as neutral CertObjects /// (leaf-first, terminating anchor last), never as provider-bound handles. struct CertVerifyResult @@ -88,7 +96,7 @@ struct VerificationRequest std::optional crl_slot; std::optional> ocsp_response; std::optional verification_time_epoch_s; - score::crypto::RevocationCheckPolicy revocation_policy{score::crypto::RevocationCheckPolicy::kNone}; + RevocationCheckPolicy revocation_policy{RevocationCheckPolicy::kNone}; }; } // namespace score::crypto::daemon::provider::cert_management diff --git a/score/crypto/src/daemon/provider/cert_management/i_cert_parser.hpp b/score/crypto/src/daemon/provider/cert_management/i_cert_parser.hpp index ef33f23eb..02ac6f372 100644 --- a/score/crypto/src/daemon/provider/cert_management/i_cert_parser.hpp +++ b/score/crypto/src/daemon/provider/cert_management/i_cert_parser.hpp @@ -53,6 +53,23 @@ class ICertParser std::vector>, common::DaemonErrorCode> ParseCertificates(const std::uint8_t* bytes, std::size_t size, score::crypto::FormatType format) = 0; + + /// Validate raw CRL bytes against the CA certificate that should have issued it. + /// + /// Checks (in order): + /// 1. The bytes are a parseable X.509 CRL. + /// 2. The CRL issuer DN equals the issuer cert's subject DN. + /// 3. The CRL signature verifies against the issuer cert's public key. + /// + /// @return The CRL's nextUpdate field as Unix epoch seconds on success. + /// 0 is returned when the CRL omits the optional nextUpdate field. + [[nodiscard]] virtual score::crypto::Expected ValidateCrl( + const std::uint8_t* crl_data, + std::size_t crl_size, + score::crypto::FormatType crl_format, + const std::uint8_t* issuer_cert_data, + std::size_t issuer_cert_size, + score::crypto::FormatType issuer_cert_format) = 0; }; } // namespace score::crypto::daemon::provider::cert_management diff --git a/score/crypto/src/daemon/provider/score_provider/openssl/BUILD b/score/crypto/src/daemon/provider/score_provider/openssl/BUILD index 689e00960..45a768dfe 100644 --- a/score/crypto/src/daemon/provider/score_provider/openssl/BUILD +++ b/score/crypto/src/daemon/provider/score_provider/openssl/BUILD @@ -86,6 +86,7 @@ cc_library( }), visibility = ["//:__subpackages__"], deps = [ + ":openssl_cert_management_headers", ":openssl_key_management_headers", "//score/crypto/src/daemon/key_management:key_management_headers", "//score/crypto/src/daemon/provider/executors:key_mgmt_executor", @@ -120,9 +121,11 @@ cc_library( }), visibility = ["//:__subpackages__"], deps = [ + ":openssl_cert_parser_library", ":openssl_detail_headers", ":openssl_key_management_headers", ":provider_openssl_headers", + "//score/crypto/src/daemon/cert_management", "//score/crypto/src/daemon/common", "//score/crypto/src/daemon/common:algorithm_info", "//score/crypto/src/daemon/data_manager", diff --git a/score/crypto/src/daemon/provider/score_provider/openssl/cert_management/openssl_cert_parser.cpp b/score/crypto/src/daemon/provider/score_provider/openssl/cert_management/openssl_cert_parser.cpp index 23e98190b..e6b6a4661 100644 --- a/score/crypto/src/daemon/provider/score_provider/openssl/cert_management/openssl_cert_parser.cpp +++ b/score/crypto/src/daemon/provider/score_provider/openssl/cert_management/openssl_cert_parser.cpp @@ -250,4 +250,109 @@ OpenSslCertParser::ParseCertificates(const std::uint8_t* bytes, std::size_t size return result; } +// --------------------------------------------------------------------------- +// CRL validation +// --------------------------------------------------------------------------- + +namespace +{ +struct X509CrlDeleter +{ + void operator()(X509_CRL* p) const noexcept + { + X509_CRL_free(p); + } +}; +using X509CrlPtr = std::unique_ptr; + +// Parse raw CRL bytes (DER or PEM) into an OpenSSL CRL object. +X509CrlPtr ParseCrlBytes(const std::uint8_t* data, std::size_t size, score::crypto::FormatType format) +{ + if (format == score::crypto::FormatType::kDer) + { + const uint8_t* ptr = data; + return X509CrlPtr(d2i_X509_CRL(nullptr, &ptr, static_cast(size))); + } + auto* bio_raw = BIO_new_mem_buf(data, static_cast(size)); + if (!bio_raw) + return nullptr; + std::unique_ptr bio{bio_raw, &BIO_free}; + return X509CrlPtr(PEM_read_bio_X509_CRL(bio.get(), nullptr, nullptr, nullptr)); +} + +// Convert an ASN1_TIME to a Unix epoch (seconds). Returns 0 if unavailable. +std::int64_t Asn1TimeToEpoch(const ASN1_TIME* asn1) +{ + if (!asn1) + return 0; + struct tm t{}; + if (ASN1_TIME_to_tm(asn1, &t) != 1) + return 0; +#ifdef _WIN32 + return static_cast(_mkgmtime(&t)); +#else + return static_cast(timegm(&t)); +#endif +} +} // namespace + +score::crypto::Expected OpenSslCertParser::ValidateCrl( + const std::uint8_t* crl_data, + std::size_t crl_size, + score::crypto::FormatType crl_format, + const std::uint8_t* issuer_cert_data, + std::size_t issuer_cert_size, + score::crypto::FormatType issuer_cert_format) +{ + if (!crl_data || crl_size == 0U || !issuer_cert_data || issuer_cert_size == 0U) + return score::crypto::make_unexpected(Error::kInvalidArgument); + + // 1. Parse the CRL. + X509CrlPtr crl = ParseCrlBytes(crl_data, crl_size, crl_format); + if (!crl) + { + ERR_clear_error(); + return score::crypto::make_unexpected(Error::kCertificateParsingFailed); + } + + // 2. Parse the issuer certificate. + X509Ptr issuer_x509; + if (issuer_cert_format == score::crypto::FormatType::kDer) + { + const uint8_t* ptr = issuer_cert_data; + issuer_x509.reset(d2i_X509(nullptr, &ptr, static_cast(issuer_cert_size))); + } + else + { + auto* bio_raw = BIO_new_mem_buf(issuer_cert_data, static_cast(issuer_cert_size)); + if (bio_raw) + { + std::unique_ptr bio{bio_raw, &BIO_free}; + issuer_x509.reset(PEM_read_bio_X509(bio.get(), nullptr, nullptr, nullptr)); + } + } + if (!issuer_x509) + { + ERR_clear_error(); + return score::crypto::make_unexpected(Error::kInvalidArgument); + } + + // 3. Verify CRL issuer DN matches the certificate's subject DN. + if (X509_NAME_cmp(X509_CRL_get_issuer(crl.get()), X509_get_subject_name(issuer_x509.get())) != 0) + return score::crypto::make_unexpected(Error::kInvalidArgument); + + // 4. Verify the CRL signature using the issuer certificate's public key. + EVP_PKEY* pkey = X509_get0_pubkey(issuer_x509.get()); + if (!pkey) + return score::crypto::make_unexpected(Error::kInternalError); + if (X509_CRL_verify(crl.get(), pkey) != 1) + { + ERR_clear_error(); + return score::crypto::make_unexpected(Error::kOperationFailed); + } + + // 5. Extract nextUpdate (optional field — 0 when absent). + return Asn1TimeToEpoch(X509_CRL_get0_nextUpdate(crl.get())); +} + } // namespace score::crypto::daemon::provider::score_provider::openssl diff --git a/score/crypto/src/daemon/provider/score_provider/openssl/cert_management/openssl_cert_parser.hpp b/score/crypto/src/daemon/provider/score_provider/openssl/cert_management/openssl_cert_parser.hpp index 9cb8796f6..5bc6c6b7e 100644 --- a/score/crypto/src/daemon/provider/score_provider/openssl/cert_management/openssl_cert_parser.hpp +++ b/score/crypto/src/daemon/provider/score_provider/openssl/cert_management/openssl_cert_parser.hpp @@ -48,6 +48,14 @@ class OpenSslCertParser final : public cert_management::ICertParser common::DaemonErrorCode> ParseCertificates(const std::uint8_t* bytes, std::size_t size, score::crypto::FormatType format) override; + [[nodiscard]] score::crypto::Expected ValidateCrl( + const std::uint8_t* crl_data, + std::size_t crl_size, + score::crypto::FormatType crl_format, + const std::uint8_t* issuer_cert_data, + std::size_t issuer_cert_size, + score::crypto::FormatType issuer_cert_format) override; + private: common::ProviderId m_provider_id; }; diff --git a/score/crypto/src/daemon/provider/score_provider/openssl/provider_openssl.cpp b/score/crypto/src/daemon/provider/score_provider/openssl/provider_openssl.cpp index e4ae6a06e..03bbeddd3 100644 --- a/score/crypto/src/daemon/provider/score_provider/openssl/provider_openssl.cpp +++ b/score/crypto/src/daemon/provider/score_provider/openssl/provider_openssl.cpp @@ -66,8 +66,7 @@ void OpenSSL::Shutdown() std::shared_ptr<::score::crypto::daemon::provider::handler::ICryptoHandlerFactory> OpenSSL::CreateHandlerFactory() { - return std::make_shared( - m_factory, GetKeySlotHandler({}), m_keyManagementService, GetCertParser(), m_certManagementService); + return std::make_shared(m_factory, GetKeySlotHandler({}), m_keyManagementService); } void OpenSSL::SetCertManagementService( diff --git a/score/crypto/src/daemon/provider/tests/cert_management/test_openssl_cert_parser.cpp b/score/crypto/src/daemon/provider/tests/cert_management/test_openssl_cert_parser.cpp index 83d093805..cb1f2cc3d 100644 --- a/score/crypto/src/daemon/provider/tests/cert_management/test_openssl_cert_parser.cpp +++ b/score/crypto/src/daemon/provider/tests/cert_management/test_openssl_cert_parser.cpp @@ -20,6 +20,7 @@ #include #include #include +#include #include namespace @@ -74,7 +75,7 @@ TEST(OpenSslCertParserTest, ParsesPemBundleIntoSeparateCertificateObjects) TEST(OpenSslCertParserTest, RejectsMalformedCertificate) { - constexpr std::string malformed{"not a certificate"}; + constexpr std::string_view malformed{"not a certificate"}; OpenSslCertParser parser{ProviderId{1U}}; const auto result = parser.ParseCertificate( reinterpret_cast(malformed.data()), malformed.size(), FormatType::kPem); From c0f2ade0f418cd306944c2142fffa035d83afd76 Mon Sep 17 00:00:00 2001 From: Athul Mallappallil Date: Wed, 2 Sep 2026 22:50:10 +0000 Subject: [PATCH 33/37] Minor fixes and test adaptation --- score/crypto/src/daemon/cert_management/BUILD | 1 - .../cert_management/core/cert_entry.hpp | 4 +- .../core/cert_management_service.cpp | 4 +- .../detail/slot_info_builder.hpp | 35 - .../architecture/cert_management_static.puml | 21 +- .../architecture/component_architecture.rst | 24 +- .../docs/architecture/design_decisions.rst | 457 +++++++++++ .../docs/architecture/index.rst | 1 + .../cert_management_dynamic.puml | 0 .../docs/detailed_design/detailed_design.rst | 2 +- .../interfaces/i_trust_store_handler.hpp | 11 + .../nodes/trust_store_data_node.hpp | 2 +- .../query/cert_object_serializer.cpp | 10 +- .../cert_management/slot/crl_handler.cpp | 13 +- .../cert_management/slot/slot_registry.cpp | 23 +- ...est_certificate_management_integration.cpp | 10 +- .../query/test_cert_object_serializer.cpp | 9 +- .../service/test_cert_management_service.cpp | 10 +- .../slot/test_file_backed_slot_handler.cpp | 76 +- .../truststore/test_trust_store_manager.cpp | 344 +++++++- .../truststore/trust_store_manager.cpp | 78 +- .../truststore/trust_store_manager.hpp | 32 +- .../storage/kv/kv_deployment_writer.cpp | 4 +- .../cert_management/openssl_cert_parser.cpp | 5 +- .../provider/tests/cert_management/BUILD | 1 + .../test_openssl_cert_parser.cpp | 396 ++++++++- score/tests/test_vectors/certificate/BUILD | 63 +- .../tests/test_vectors/certificate/README.md | 240 +++++- .../{ => algorithm_variety}/ec_p256.pem | 0 .../algorithm_variety/ec_p256_slot.kv | 3 + .../{ => algorithm_variety}/ec_p384.pem | 0 .../algorithm_variety/ec_p384_slot.kv | 3 + .../{ => algorithm_variety}/ec_p521.pem | 0 .../algorithm_variety/ec_p521_slot.kv | 3 + .../{ => algorithm_variety}/ed25519.pem | 0 .../algorithm_variety/ed25519_slot.kv | 3 + .../{ => algorithm_variety}/ed448.pem | 0 .../algorithm_variety/ed448_slot.kv | 3 + .../manifest.json} | 44 +- .../{ => algorithm_variety}/ml_dsa_44.pem | 0 .../algorithm_variety/ml_dsa_44_slot.kv | 3 + .../{ => algorithm_variety}/ml_dsa_65.pem | 0 .../algorithm_variety/ml_dsa_65_slot.kv | 3 + .../{ => algorithm_variety}/ml_dsa_87.pem | 0 .../algorithm_variety/ml_dsa_87_slot.kv | 3 + .../private/.gitignore | 0 .../{ => algorithm_variety}/rsa_3072.pem | 0 .../algorithm_variety/rsa_3072_slot.kv | 3 + .../{ => algorithm_variety}/rsa_4096.pem | 0 .../algorithm_variety/rsa_4096_slot.kv | 3 + .../certificate/{ => basic}/certificate.pem | 0 .../certificate/basic/certificate_slot.kv | 3 + .../{ => basic}/certificate_updated.pem | 0 .../basic/certificate_updated_slot.kv | 3 + .../certificate/basic/manifest.json | 55 ++ .../certificate/basic/private/.gitignore | 2 + .../certificate/{ => basic}/trust_store.kv | 0 .../certificate/certificate_slot.kv | 3 - .../certificate/certificate_updated_slot.kv | 3 - .../test_vectors/certificate/ec_p256_slot.kv | 3 - .../test_vectors/certificate/ec_p384_slot.kv | 3 - .../test_vectors/certificate/ec_p521_slot.kv | 3 - .../test_vectors/certificate/ed25519_slot.kv | 3 - .../test_vectors/certificate/ed448_slot.kv | 3 - .../certificate/generate_certificates.py | 761 ++++++++++++++---- .../certificate/ml_dsa_44_slot.kv | 3 - .../certificate/ml_dsa_65_slot.kv | 3 - .../certificate/ml_dsa_87_slot.kv | 3 - .../certificate/pki_chain/manifest.json | 67 ++ .../pki_chain/ocsp_signer.ext.conf | 6 + .../test_vectors/certificate/rsa_3072_slot.kv | 3 - .../test_vectors/certificate/rsa_4096_slot.kv | 3 - 72 files changed, 2424 insertions(+), 459 deletions(-) delete mode 100644 score/crypto/src/daemon/cert_management/detail/slot_info_builder.hpp create mode 100644 score/crypto/src/daemon/cert_management/docs/architecture/design_decisions.rst rename score/crypto/src/daemon/cert_management/docs/{architecture => detailed_design}/cert_management_dynamic.puml (100%) rename score/tests/test_vectors/certificate/{ => algorithm_variety}/ec_p256.pem (100%) create mode 100644 score/tests/test_vectors/certificate/algorithm_variety/ec_p256_slot.kv rename score/tests/test_vectors/certificate/{ => algorithm_variety}/ec_p384.pem (100%) create mode 100644 score/tests/test_vectors/certificate/algorithm_variety/ec_p384_slot.kv rename score/tests/test_vectors/certificate/{ => algorithm_variety}/ec_p521.pem (100%) create mode 100644 score/tests/test_vectors/certificate/algorithm_variety/ec_p521_slot.kv rename score/tests/test_vectors/certificate/{ => algorithm_variety}/ed25519.pem (100%) create mode 100644 score/tests/test_vectors/certificate/algorithm_variety/ed25519_slot.kv rename score/tests/test_vectors/certificate/{ => algorithm_variety}/ed448.pem (100%) create mode 100644 score/tests/test_vectors/certificate/algorithm_variety/ed448_slot.kv rename score/tests/test_vectors/certificate/{certificate_manifest.json => algorithm_variety/manifest.json} (64%) rename score/tests/test_vectors/certificate/{ => algorithm_variety}/ml_dsa_44.pem (100%) create mode 100644 score/tests/test_vectors/certificate/algorithm_variety/ml_dsa_44_slot.kv rename score/tests/test_vectors/certificate/{ => algorithm_variety}/ml_dsa_65.pem (100%) create mode 100644 score/tests/test_vectors/certificate/algorithm_variety/ml_dsa_65_slot.kv rename score/tests/test_vectors/certificate/{ => algorithm_variety}/ml_dsa_87.pem (100%) create mode 100644 score/tests/test_vectors/certificate/algorithm_variety/ml_dsa_87_slot.kv rename score/tests/test_vectors/certificate/{ => algorithm_variety}/private/.gitignore (100%) rename score/tests/test_vectors/certificate/{ => algorithm_variety}/rsa_3072.pem (100%) create mode 100644 score/tests/test_vectors/certificate/algorithm_variety/rsa_3072_slot.kv rename score/tests/test_vectors/certificate/{ => algorithm_variety}/rsa_4096.pem (100%) create mode 100644 score/tests/test_vectors/certificate/algorithm_variety/rsa_4096_slot.kv rename score/tests/test_vectors/certificate/{ => basic}/certificate.pem (100%) create mode 100644 score/tests/test_vectors/certificate/basic/certificate_slot.kv rename score/tests/test_vectors/certificate/{ => basic}/certificate_updated.pem (100%) create mode 100644 score/tests/test_vectors/certificate/basic/certificate_updated_slot.kv create mode 100644 score/tests/test_vectors/certificate/basic/manifest.json create mode 100644 score/tests/test_vectors/certificate/basic/private/.gitignore rename score/tests/test_vectors/certificate/{ => basic}/trust_store.kv (100%) delete mode 100644 score/tests/test_vectors/certificate/certificate_slot.kv delete mode 100644 score/tests/test_vectors/certificate/certificate_updated_slot.kv delete mode 100644 score/tests/test_vectors/certificate/ec_p256_slot.kv delete mode 100644 score/tests/test_vectors/certificate/ec_p384_slot.kv delete mode 100644 score/tests/test_vectors/certificate/ec_p521_slot.kv delete mode 100644 score/tests/test_vectors/certificate/ed25519_slot.kv delete mode 100644 score/tests/test_vectors/certificate/ed448_slot.kv delete mode 100644 score/tests/test_vectors/certificate/ml_dsa_44_slot.kv delete mode 100644 score/tests/test_vectors/certificate/ml_dsa_65_slot.kv delete mode 100644 score/tests/test_vectors/certificate/ml_dsa_87_slot.kv create mode 100644 score/tests/test_vectors/certificate/pki_chain/manifest.json create mode 100644 score/tests/test_vectors/certificate/pki_chain/ocsp_signer.ext.conf delete mode 100644 score/tests/test_vectors/certificate/rsa_3072_slot.kv delete mode 100644 score/tests/test_vectors/certificate/rsa_4096_slot.kv diff --git a/score/crypto/src/daemon/cert_management/BUILD b/score/crypto/src/daemon/cert_management/BUILD index 084481585..95792fed6 100644 --- a/score/crypto/src/daemon/cert_management/BUILD +++ b/score/crypto/src/daemon/cert_management/BUILD @@ -35,7 +35,6 @@ cc_library( "policy/*.hpp", "slot/*.hpp", "truststore/*.hpp", - "detail/*.hpp", "cert_management_module.hpp", ]), includes = ["."], diff --git a/score/crypto/src/daemon/cert_management/core/cert_entry.hpp b/score/crypto/src/daemon/cert_management/core/cert_entry.hpp index 99b3491a6..99b926301 100644 --- a/score/crypto/src/daemon/cert_management/core/cert_entry.hpp +++ b/score/crypto/src/daemon/cert_management/core/cert_entry.hpp @@ -85,7 +85,7 @@ class CertEntry final : public std::enable_shared_from_this } /// True when a session-scoped CRL has been attached via AttachSessionCrl(). - [[nodiscard]] bool HasSessionCrl() const noexcept + [[nodiscard]] bool HasSessionCrl() const { const std::lock_guard lock(m_ref_mutex); return m_session_crl.has_value(); @@ -98,7 +98,7 @@ class CertEntry final : public std::enable_shared_from_this return m_session_crl; } - [[nodiscard]] score::crypto::FormatType GetSessionCrlFormat() const noexcept + [[nodiscard]] score::crypto::FormatType GetSessionCrlFormat() const { const std::lock_guard lock(m_ref_mutex); return m_session_crl_format; diff --git a/score/crypto/src/daemon/cert_management/core/cert_management_service.cpp b/score/crypto/src/daemon/cert_management/core/cert_management_service.cpp index 64a3006f7..376ddf823 100644 --- a/score/crypto/src/daemon/cert_management/core/cert_management_service.cpp +++ b/score/crypto/src/daemon/cert_management/core/cert_management_service.cpp @@ -251,8 +251,8 @@ void CertManagementService::NotifySlotCertChanged(CertSlotHandle slot_handle) if (!m_trust_stores) return; const auto memberships = m_trust_stores->GetMembershipsForSlot(slot_handle); - for (const auto ts_id : memberships) - m_trust_stores->NotifySlotChanged(ts_id, slot_handle); + for (const auto ts_handle : memberships) + m_trust_stores->NotifySlotChanged(ts_handle, slot_handle); } // --------------------------------------------------------------------------- diff --git a/score/crypto/src/daemon/cert_management/detail/slot_info_builder.hpp b/score/crypto/src/daemon/cert_management/detail/slot_info_builder.hpp deleted file mode 100644 index 1935b3e04..000000000 --- a/score/crypto/src/daemon/cert_management/detail/slot_info_builder.hpp +++ /dev/null @@ -1,35 +0,0 @@ -/******************************************************************************** - * 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 - ********************************************************************************/ - -#ifndef SCORE_CRYPTO_SRC_DAEMON_CERT_MANAGEMENT_DETAIL_SLOT_INFO_BUILDER_HPP -#define SCORE_CRYPTO_SRC_DAEMON_CERT_MANAGEMENT_DETAIL_SLOT_INFO_BUILDER_HPP - -#include "score/crypto/src/api/common/types.hpp" -#include "score/crypto/src/daemon/cert_management/interfaces/cert_slot_config.hpp" - -namespace score::crypto::daemon::cert_management::detail -{ - -inline score::crypto::CertificateSlotInfo BuildCertSlotInfo(const CertSlotConfig& /*slot*/, - score::crypto::CertificateSlotState state, - bool has_crl) noexcept -{ - score::crypto::CertificateSlotInfo info{}; - info.state = state; - info.has_crl = has_crl; - return info; -} - -} // namespace score::crypto::daemon::cert_management::detail - -#endif // SCORE_CRYPTO_SRC_DAEMON_CERT_MANAGEMENT_DETAIL_SLOT_INFO_BUILDER_HPP diff --git a/score/crypto/src/daemon/cert_management/docs/architecture/cert_management_static.puml b/score/crypto/src/daemon/cert_management/docs/architecture/cert_management_static.puml index 74364702c..0a73d7cfd 100644 --- a/score/crypto/src/daemon/cert_management/docs/architecture/cert_management_static.puml +++ b/score/crypto/src/daemon/cert_management/docs/architecture/cert_management_static.puml @@ -11,7 +11,8 @@ ' SPDX-License-Identifier: Apache-2.0 ' ******************************************************************************* -@startuml +@startuml cert_management_static +top to bottom direction skinparam componentStyle rectangle skinparam packageStyle rectangle @@ -65,25 +66,25 @@ package "daemon/mediator" { [Mediator] } -[CertManagementService] --> [CertRegistry] +[CertManagementService] -> [CertRegistry] [CertManagementService] --> [CertSlotRegistry] [CertManagementService] --> [TrustStoreManager] -[CertManagementService] --> [DataManager] +[CertManagementService] -->[DataManager] [CertManagementService] --> [AccessPolicyEnforcer] -[CertManagementService] ..> [CertSlotDataNode] : creates -[CertManagementService] ..> [CertDataNode] : creates -[CertManagementService] ..> [TrustStoreDataNode] : creates +[CertSlotDataNode] <.. [CertManagementService] : creates +[CertDataNode] <.. [CertManagementService] : creates +[TrustStoreDataNode] <.. [CertManagementService] : creates -[ConfigDrivenSlotCatalog] --> [CertSlotRegistry] -[ConfigDrivenTrustStoreCatalog] --> [TrustStoreManager] +[CertSlotRegistry] <-- [ConfigDrivenSlotCatalog] +[ConfigDrivenTrustStoreCatalog] ---> [TrustStoreManager] [TrustStoreManager] --> [CertSlotRegistry] [TrustStoreManager] --> [TrustStoreHandler] [TrustStoreManager] --> ICertSlotHandler [TrustStoreHandler] ..|> ITrustStoreHandler [FileBackedSlotHandler] ..|> ICertSlotHandler [FileBackedSlotHandler] --> [CrlHandler] -[FileBackedSlotHandler] --> ICertParser -[FileBackedSlotHandler] --> [Shared Deployment Storage] +[FileBackedSlotHandler] ---> ICertParser +[Shared Deployment Storage] <-- [FileBackedSlotHandler] [Pkcs11CertSlotHandler] ..|> ICertSlotHandler [Pkcs11CertSlotHandler] --> [CrlHandler] [OpenSslCertParser] ..|> ICertParser diff --git a/score/crypto/src/daemon/cert_management/docs/architecture/component_architecture.rst b/score/crypto/src/daemon/cert_management/docs/architecture/component_architecture.rst index d5953ddf9..969134cb5 100644 --- a/score/crypto/src/daemon/cert_management/docs/architecture/component_architecture.rst +++ b/score/crypto/src/daemon/cert_management/docs/architecture/component_architecture.rst @@ -133,14 +133,22 @@ another client's active cache. Design decisions ---------------- -* Trust stores reference certificate slots, not raw certificate paths. -* CRLs are stored in the certificate slot's optional ``[crl]`` section; there - is no independent CRL resource or registry. -* Access-policy checks are shared by slots and trust stores. Mutation is - default-deny and requires an explicit writer UID. -* File-backed certificate and descriptor writes use the shared atomic storage - utilities. -* Provider-specific objects and private keys never cross the certificate +The five structural decisions that shape the component's storage model are +documented with full context, alternatives considered, and consequences in +:ref:`crypto_cert_management_design_decisions`: + +* :need:`dec_rec__crypto_cert_mgmt__ts_ref_slots` — trust-store members are + named cert slot references, not raw paths. +* :need:`dec_rec__crypto_cert_mgmt__crl_co_location` — CRLs occupy an optional + ``[crl]`` section of the cert slot descriptor; no independent CRL registry. +* :need:`dec_rec__crypto_cert_mgmt__deny_mutation` — all mutations are + default-deny; write access requires an explicit UID in the resource access + policy. +* :need:`dec_rec__crypto_cert_mgmt__atomic_writes` — file-backed cert and + descriptor writes use the shared ``file_io::WriteFile`` temp-file + rename + primitive. +* :need:`dec_rec__crypto_cert_mgmt__provider_boundary` — provider interaction is + limited to ``ICertParser``; no private key or HSM handle crosses the cert management boundary. Current limitations diff --git a/score/crypto/src/daemon/cert_management/docs/architecture/design_decisions.rst b/score/crypto/src/daemon/cert_management/docs/architecture/design_decisions.rst new file mode 100644 index 000000000..54270005b --- /dev/null +++ b/score/crypto/src/daemon/cert_management/docs/architecture/design_decisions.rst @@ -0,0 +1,457 @@ +.. + # ******************************************************************************* + # 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 + # ******************************************************************************* + +.. _crypto_cert_management_design_decisions: + +Design Decisions +================ + +Trust Stores Reference Certificate Slots, Not Raw Paths +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. dec_rec:: Trust Stores Reference Certificate Slots, Not Raw Paths + :id: dec_rec__crypto_cert_mgmt__ts_ref_slots + :version: 1 + :status: accepted + :context: doc__crypto_cert_management_architecture + :decision: Trust-store membership is expressed as typed references to named certificate slots, never as raw filesystem paths or inline certificate bytes. The slot is the indirection point for loading, caching, and change notification. + + .. :affects: comp__crypto_cert_management + +Trust-store membership is expressed as typed references to named certificate +slots (``kSharedStatic``, ``kExclusiveMutable``, ``kConditionalExternal``). +Neither raw filesystem paths nor inline certificate bytes appear in a trust-store +configuration or in the in-memory member list. + +Context +------- + +A trust store must react correctly when the certificate it anchors changes. +The component also requires lazy loading (cert bytes are not read at startup), +shared cert content across stores (same physical cert referenced by multiple +stores), and cache invalidation that is scoped to the changed slot rather than +requiring a full store reload. + +Three representations were considered for trust-store membership: + +1. **Raw filesystem paths** — ``[trust_store]/member_paths = ["/certs/root.pem"]``. + Simple to configure, but paths must be validated at startup, change + notification requires watching the filesystem (inotify or polling), there is + no shared-ownership model for the cert content, and CRL co-location is lost. + +2. **Inline certificate bytes in the trust-store descriptor** — the trust-store + deployment file contains base64 PEM content. No slot indirection needed. + Removing, replacing, or CRL-associating an anchor requires re-writing the + descriptor with new content. There is no sharing across trust stores and no + single change-notification point. + +3. **Named certificate slot references** — the trust store lists slot names; the + ``CertSlotRegistry`` resolves each name to a ``CertSlotHandle`` at startup. + The ``TrustStoreManager`` maintains a reverse index (slot → stores) so that a + single ``NotifySlotChanged(slot)`` call after ``StoreCertificate`` invalidates + every affected trust store's anchor cache in O(1). + +Decision +-------- + +Named certificate slot references (option 3) were selected. The slot layer +already provides lazy loading, deployment-descriptor management, and atomic +writes. Trust stores piggy-back on this infrastructure without duplicating it. + +The ``TrustStoreHandler`` holds strong ``CertObject`` references during active +use; ``TrustStoreManager`` holds corresponding weak references in +``m_slot_cert_cache``. A cert shared by *N* trust stores allocates its bytes +exactly once. + +A typed membership kind (``TrustStoreMemberKind``) records the ownership +relationship: + +.. list-table:: + :header-rows: 1 + :widths: 30 70 + + * - Kind + - Ownership semantics + * - ``kSharedStatic`` + - Slot is externally managed; the trust store may not mutate it. + * - ``kExclusiveMutable`` + - Slot is trust-store-owned; ``AddCertificateToTrustStore`` may write to it. + * - ``kConditionalExternal`` + - Slot is externally managed; anchor is disabled if the cert fingerprint + changes unexpectedly until the application acknowledges the update. + +Consequences +------------ + +**Positive:** + +* ``NotifySlotChanged`` targets exactly one slot; unchanged anchors retain their + cached ``CertObject`` strong references. +* The same cert slot can anchor multiple trust stores simultaneously with zero + additional memory. +* CRL co-location (see ``dec_rec__crypto_cert_mgmt__crl_co_location``) is a + natural consequence of slot-scoped membership — the CRL is already at the slot. +* Startup loading is strictly lazy — no cert bytes are read during ``Load()``. + +**Negative:** + +* Every trust-store member must be a registered cert slot; there is no escape + hatch for one-off ephemeral certs. Ephemeral anchors are instead set with + ``SetTrustedCertificate`` on the verification context. +* Configuration must name slots explicitly; adding a new anchor requires both a + slot entry and a trust-store membership entry. + +--- + +CRL Co-located with the Certificate Slot +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. dec_rec:: CRL Co-located with the Certificate Slot (No Separate CRL Registry) + :id: dec_rec__crypto_cert_mgmt__crl_co_location + :version: 1 + :status: accepted + :context: doc__crypto_cert_management_architecture + :decision: A CRL is stored in an optional ``[crl]`` section of the certificate slot's KV deployment descriptor. There is no independent CRL registry, CRL resource type, or CRL-specific DataNode. The slot is the resolution unit for both the certificate and its associated CRL. + + .. :affects: comp__crypto_cert_management + +A CRL is stored in the optional ``[crl]`` section of the certificate slot's KV +deployment descriptor alongside the ``[certificate]`` section. CRL storage and +retrieval are encapsulated in ``CrlHandler``, which is composed into both +``FileBackedSlotHandler`` and ``Pkcs11CertSlotHandler``. + +Context +------- + +CRLs are revocation data for a specific issuing CA. Revocation status for a +given certificate chain therefore depends on the issuer's CRL, which is +naturally associated with the issuer's certificate slot. Three storage models +were evaluated: + +1. **Separate CRL registry** — a ``CrlRegistry`` analogous to ``CertRegistry``, + with its own DataNodes, resource IDs, and deployment descriptors. Gives CRLs + a first-class resource identity. Requires separate resolution paths, a new + ``kCrl`` resource type in the API, cross-resource foreign-key management + (cert slot → CRL resource), and orphan handling when a cert slot is cleared. + +2. **Global CRL file per trust store** — each trust store holds one CRL bundle + file. Simple but coarse: updating one issuer's CRL requires rewriting the + full bundle; per-issuer revocation is not possible; the slot-to-CRL mapping + is implicit. + +3. **CRL co-located with the certificate slot** — the ``[crl]`` section lives + in the same KV file as ``[certificate]``. Lifecycle is automatic: clearing + the slot clears the CRL; updating the certificate invalidates the CRL + (``StoreCertificate`` clears ``[crl]`` fields while preserving ``crl_path`` + so the next ``StoreCrl`` reuses the same location). No orphan CRL is possible + because no independent resource exists. + +Decision +-------- + +CRL co-location with the cert slot (option 3) was selected. ``ICertSlotHandler`` +is extended with ``HasCrl``, ``LoadCrl``, ``StoreCrl``, ``ClearCrl``, and +``GetCrlNextUpdate`` — implemented by the composed ``CrlHandler``. The +``crl_next_update`` epoch is written by ``StoreCrl`` and exposed through the +descriptor so the daemon can evaluate CRL freshness without loading the full DER. + +For PKCS#11 token slots (where no filesystem cert path exists), ``CrlHandler`` +stores CRL data in the deployment filesystem using the same ``crl_path`` +convention — the PKCS#11 token provides no native CRL object type. + +Consequences +------------ + +**Positive:** + +* No new resource type, DataNode subclass, or registry required. +* CRL lifecycle is entirely derived from slot lifecycle — no orphan CRLs. +* ``StoreCertificate`` stale-CRL invalidation is a single write path with no + cross-registry coordination. +* The verification handler walks trust-store member slots and calls ``HasCrl`` + per slot — no separate CRL lookup service. +* ``crl_next_update`` in the descriptor enables freshness checks without + deserialising the DER. + +**Negative:** + +* A CRL can only be associated with a slot that holds a matching certificate. + Detached CRLs (no corresponding cert slot) are not supported. +* CRL data is not independently addressable — no ``kCrl`` API resource type. + Applications use ``ImportCrl(cert_resource_id, ...)`` rather than + ``ImportCrl(crl_resource_id, ...)``. + +--- + +Mutation Default-Deny with Explicit Writer UID +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. dec_rec:: Mutation Default-Deny with Explicit Writer UID + :id: dec_rec__crypto_cert_mgmt__deny_mutation + :version: 1 + :status: accepted + :context: doc__crypto_cert_management_architecture + :decision: All certificate slot and trust-store mutations (StoreCertificate, ImportCrl, AddMember, RemoveMember, EnableMember, DisableMember) are default-deny. Write access is granted only to UIDs listed in the slot or trust-store access policy. Reads do not require explicit authorisation beyond resource resolution. + + .. :affects: comp__crypto_cert_management + +Certificate slot and trust-store mutations require explicit write authorisation. +The caller's UID must appear in the ``writers`` list of the ``AccessPolicy`` +attached to the resource. Read operations (``LoadCertificate``, ``GetSlotInfo``, +``GetAnchors``) are accessible to any UID that can resolve the resource. + +Context +------- + +Certificate slots may contain CA root anchors or intermediate CA certificates +whose integrity is a security prerequisite for the entire verification chain. +Allowing any connected application to overwrite an anchor would undermine all +chain verification guarantees. Three approaches were evaluated: + +1. **Per-call capability tokens** — the client presents a short-lived + capability signed by a policy authority. Flexible, but requires a separate + capability-issuing service, token validation logic, and replay protection. + Overhead is disproportionate to the requirement at the current stage. + +2. **Separate ACL service** — a dedicated access-control component that the + cert management core queries before each mutation. Cleanly separable, but + introduces an additional IPC hop and a new inter-component dependency for + every mutation operation. + +3. **UID-based access policy per resource** — each slot and trust-store + configuration declares a ``writers`` set of UIDs (and optionally ``readers`` + if further restriction is desired). ``AccessPolicyEnforcer`` checks + ``client_uid`` against the policy before the mutation is dispatched to the + handler. Follows the same pattern already established by ``key_management``. + +Decision +-------- + +UID-based per-resource access policy (option 3) was selected. +``AccessPolicyEnforcer`` is local to ``cert_management``; the long-term goal is +a shared access-control component across the daemon, but that refactoring is +deferred. The ``AccessPolicy`` type is defined in +``cert_management/interfaces/access_policy.hpp`` and is composed into both +``CertSlotConfig`` and ``TrustStoreConfig``. + +Enforcement points: + +.. list-table:: + :header-rows: 1 + :widths: 50 50 + + * - Operation + - Access required + * - ``StoreCertificate`` / ``ClearSlot`` + - Slot write + * - ``ImportCrl`` / ``DeleteCrl`` + - Slot write + * - ``AddCertificateToTrustStore`` / ``RemoveCertificateFromTrustStore`` + - Trust-store write + * - ``EnableTrustStoreMember`` / ``DisableTrustStoreMember`` + - Trust-store write + * - ``SaveCertificate`` (to a slot via mgmt context) + - Slot write + * - ``LoadCertificate`` / ``GetSlotInfo`` / ``GetAnchors`` + - None (resource resolution grants access) + +Consequences +------------ + +**Positive:** + +* CA anchor slots can be locked so that only the provisioning UID may write + them; application UIDs can only read. +* Write access to a trust store is independent of write access to its member + slots — a trust-store administrator can add/remove exclusive members without + holding write access to the shared-static backing slot. +* Consistent with the ``key_management`` pattern — same mental model for + operators configuring both subsystems. + +**Negative:** + +* UID-based access control is coarser than capability tokens; it cannot + express time-limited or operation-specific delegation. +* Misconfigured ``writers`` sets are caught at runtime, not at compile time. +* Long-term refactoring to a shared ACL component will require updating + enforcement sites in both ``key_management`` and ``cert_management``. + +--- + +Atomic File Writes via Temp-File and Rename +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. dec_rec:: Atomic File Writes via Temp-File and Rename + :id: dec_rec__crypto_cert_mgmt__atomic_writes + :version: 1 + :status: accepted + :context: doc__crypto_cert_management_architecture + :decision: All file-backed certificate and descriptor writes use the shared ``file_io::WriteFile`` primitive from ``daemon/common/storage/``, which writes to a temporary file and then renames it atomically over the target. A partially written descriptor or certificate payload can never replace the previous valid state. + + .. :affects: comp__crypto_cert_management + +File-backed certificate payloads and KV deployment descriptors are written via +``file_io::WriteFile`` (``daemon/common/storage/file_io.hpp``). That primitive +writes to a sibling temporary file, then issues an atomic ``rename`` to replace +the target. The same mechanism is used by ``KvDeploymentWriter`` for descriptor +updates. + +Context +------- + +The crypto daemon may be killed by SIGKILL (watchdog expiry, power loss) at any +point during a write. A partially written certificate or descriptor must not +leave the slot in a state where the daemon reads corrupt data on the next startup. +Three strategies were evaluated: + +1. **Direct writes** — open the target file and write in place. Simple, but a + crash mid-write leaves a truncated or partially overwritten file. The previous + valid content is lost with no recovery path. + +2. **Write-ahead log (WAL)** — record the intent before writing, then apply and + mark complete. Provides full crash recovery but requires a WAL reader at + startup, adds ~100–200 LOC of journal management, and is disproportionate + for individual file writes without transactional multi-file requirements. + +3. **Temp-file + ``rename``** — write the new content to a sibling temporary + file; call ``rename(tmp, target)`` which is atomic on POSIX filesystems. + The old content survives until ``rename`` succeeds; after ``rename``, the new + content is fully visible. No reader of the target file ever observes a partial + state. + +Decision +-------- + +Temp-file + ``rename`` (option 3) was selected and implemented in +``file_io::WriteFile``. The utility is shared between ``cert_management`` and +``key_management``; it is the only file-write primitive that components in +``daemon/common/storage/`` expose. Certificate payload bytes use +``WriteFile``; key material continues to use ``WriteKeyFile`` which adds +``SecureZeroizeAndClear`` semantics on the temporary. + +``KvDeploymentWriter`` applies the same pattern for descriptor files: the +descriptor is preserved on disk until the new content is fully written and +renamed in place. + +Consequences +------------ + +**Positive:** + +* A daemon crash at any point during a write leaves the slot in the previous + valid state — no manual recovery or fsck-style startup scan is needed. +* Readers (including a concurrently running verification context) never observe + a partial file — POSIX ``rename`` is atomic with respect to other + ``open``/``read`` calls on the target path. +* A single shared utility eliminates per-component write-safety logic. + +**Negative:** + +* Requires the temporary file and the target to be on the same filesystem + (``rename`` across mount points is not atomic). Deployment configuration must + not place ``tmp`` and target on different volumes — this is documented but not + enforced at runtime. +* Write amplification: every update writes a full new copy of the file rather + than patching in place. Acceptable for certificate and descriptor sizes (a few + kilobytes); not suitable for large append-only data. + +--- + +Provider Boundary: Parsing Only; No Private Key or HSM Handle Crossing +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. dec_rec:: Provider Boundary — Parsing Only; No Private Key or HSM Handle Crossing + :id: dec_rec__crypto_cert_mgmt__provider_boundary + :version: 1 + :status: accepted + :context: doc__crypto_cert_management_architecture + :decision: The certificate management component receives DER or PEM bytes from a provider via ``ICertParser`` and stores an immutable ``CertObject`` value. Provider-specific handles (PKCS#11 object handles, HSM references), raw private keys, and provider internal types do not cross the certificate management boundary in either direction. + + .. :affects: comp__crypto_cert_management + +The certificate management core interacts with providers through one narrow +interface: ``ICertParser::ParseCertificate`` converts raw bytes into an +immutable ``CertObject`` value. No provider handle, HSM reference, or raw +private key enters or leaves the core component. Provider context handlers +(verification, CSR generation, format conversion) are separate objects created +by the provider factory; they do not share types with the storage layer. + +Context +------- + +A cert management component that allows provider handles to cross its boundary +would couple core storage logic to specific HSM APIs. Two alternative designs +were evaluated: + +1. **Provider-owned certificate objects** — the provider allocates a + ``ProviderCertHandle`` (analogous to a PKCS#11 ``CK_OBJECT_HANDLE``) and + passes it through the component boundary. The core stores the handle. + Loading a cert from storage calls the provider to deserialise its own handle. + This design ties cert lifetime to provider availability and prevents sharing + cert content between providers or between the storage layer and the + verification handler. + +2. **Immutable value type (``CertObject``) with a narrow parsing interface** — + the provider converts bytes to a provider-neutral value once at load time. + The core stores and shares the value; the provider is not involved in + subsequent reads. Verification handlers receive ``CertObject`` values and + apply provider-specific OpenSSL or HSM operations internally. + +Decision +-------- + +Immutable ``CertObject`` value type with ``ICertParser`` as the sole +provider-crossing interface (option 2) was selected. ``CertObject`` contains: + +* Raw DER or PEM bytes (reproduced faithfully from what was stored). +* ``CertChainMetadata`` — SKID, AKID, SHA-256 fingerprint, ``is_ca`` flag, + subject, issuer, serial number — extracted at parse time and cached. + +``ICertFactory``, ``ICertHandler``, ``ICertLoader``, and ``ProviderCertHandle`` +were considered during early design and are explicitly **not present** in the +implementation. Cert context operations (verify, CSR, convert, key extract) +belong inside handlers created by ``ICryptoHandlerFactory::CreateHandler``; they +are not responsibilities of the storage component. + +For CSR generation, signing requires the private key to remain inside the +provider. The planned cross-context broker (``IKeyOperationEndpoint``) provides +a ``Sign(data, algorithm)`` call that the CSR handler invokes without the +private key material ever leaving the key management provider boundary. + +Consequences +------------ + +**Positive:** + +* ``CertObject`` can be shared across trust stores, verification contexts, and + the cert registry without provider involvement after the initial parse. +* Replacing the OpenSSL provider with a different implementation requires only a + new ``ICertParser`` and new context handlers — the storage and trust-store + layers are unaffected. +* Private keys cannot leak through the certificate management path; the boundary + is structurally enforced (``ICertParser`` receives only cert bytes and returns + only cert values). +* Verification handlers receive fully self-contained ``CertObject`` values and + operate without callback into the storage layer during chain building. + +**Negative:** + +* Parse-time extraction of ``CertChainMetadata`` means the full cert is parsed + even when only the fingerprint is needed. The overhead is proportional to + cert count at startup (lazy loading mitigates this — certs are not parsed + until a trust store is first accessed). +* Some provider-specific cert attributes (e.g., PKCS#11 token labels) are not + captured in ``CertObject``. Applications that need them must use + ``ICertSlotHandler``-specific query paths, which the current API does not + expose publicly. diff --git a/score/crypto/src/daemon/cert_management/docs/architecture/index.rst b/score/crypto/src/daemon/cert_management/docs/architecture/index.rst index 9a41a9ebe..42812e250 100644 --- a/score/crypto/src/daemon/cert_management/docs/architecture/index.rst +++ b/score/crypto/src/daemon/cert_management/docs/architecture/index.rst @@ -21,3 +21,4 @@ Architecture .. toctree:: component_architecture + design_decisions diff --git a/score/crypto/src/daemon/cert_management/docs/architecture/cert_management_dynamic.puml b/score/crypto/src/daemon/cert_management/docs/detailed_design/cert_management_dynamic.puml similarity index 100% rename from score/crypto/src/daemon/cert_management/docs/architecture/cert_management_dynamic.puml rename to score/crypto/src/daemon/cert_management/docs/detailed_design/cert_management_dynamic.puml diff --git a/score/crypto/src/daemon/cert_management/docs/detailed_design/detailed_design.rst b/score/crypto/src/daemon/cert_management/docs/detailed_design/detailed_design.rst index f4e3042d8..dfde34b18 100644 --- a/score/crypto/src/daemon/cert_management/docs/detailed_design/detailed_design.rst +++ b/score/crypto/src/daemon/cert_management/docs/detailed_design/detailed_design.rst @@ -113,4 +113,4 @@ CRL validation, OCSP, and mediator dispatch are outside this component's storage and lifecycle scope. Hardware CSR signing must use a cross-context service without exporting private key material. -.. uml:: ../architecture/cert_management_dynamic.puml +.. uml:: cert_management_dynamic.puml diff --git a/score/crypto/src/daemon/cert_management/interfaces/i_trust_store_handler.hpp b/score/crypto/src/daemon/cert_management/interfaces/i_trust_store_handler.hpp index f2269af67..d38b0dd28 100644 --- a/score/crypto/src/daemon/cert_management/interfaces/i_trust_store_handler.hpp +++ b/score/crypto/src/daemon/cert_management/interfaces/i_trust_store_handler.hpp @@ -85,6 +85,17 @@ class ITrustStoreHandler /// /// Returns kInternalError when the anchor set cannot be assembled /// (e.g., static file I/O failure during lazy reload). + /// + /// Lifetime contract: callers must bracket every usage of GetAnchors() with + /// TrustStoreManager::AddRef() before the first call and ReleaseRef() after + /// the last call. AddRef pins the anchor cache so it is not evicted while + /// certs are in use; ReleaseRef allows eviction when no client holds refs. + /// Calling GetAnchors() without a matching AddRef/ReleaseRef pair populates + /// the cache with no corresponding cleanup trigger — the loaded certs will + /// remain in memory until an unrelated ReleaseRef happens to evict them. + /// The sole production call path (ScoreCertVerificationHandler) already + /// satisfies this contract; direct calls are only safe in tests or + /// read-only tooling where memory lifetime is not a concern. [[nodiscard]] virtual score::crypto::Expected, score::crypto::daemon::common::DaemonErrorCode> GetAnchors() = 0; diff --git a/score/crypto/src/daemon/cert_management/nodes/trust_store_data_node.hpp b/score/crypto/src/daemon/cert_management/nodes/trust_store_data_node.hpp index bc50a81ff..efeebddd6 100644 --- a/score/crypto/src/daemon/cert_management/nodes/trust_store_data_node.hpp +++ b/score/crypto/src/daemon/cert_management/nodes/trust_store_data_node.hpp @@ -53,7 +53,7 @@ class TrustStoreDataNode final : public data_manager::DataNode ITrustStoreHandler::Sptr GetStore() const { - return m_manager ? m_manager->GetStore(m_handle.index) : nullptr; + return m_manager ? m_manager->GetStore(m_handle) : nullptr; } private: diff --git a/score/crypto/src/daemon/cert_management/query/cert_object_serializer.cpp b/score/crypto/src/daemon/cert_management/query/cert_object_serializer.cpp index 0fd543967..b21b5a566 100644 --- a/score/crypto/src/daemon/cert_management/query/cert_object_serializer.cpp +++ b/score/crypto/src/daemon/cert_management/query/cert_object_serializer.cpp @@ -25,8 +25,8 @@ common::ResponseParameters SerializeCertObject(const CertObject& cert) common::ResponseParameters out; out.push_back(common::OwnedString{meta.subject_canonical}); out.push_back(common::OwnedString{meta.issuer_canonical}); - out.push_back(static_cast(static_cast(meta.not_before_epoch_s))); - out.push_back(static_cast(static_cast(meta.not_after_epoch_s))); + out.push_back(static_cast(meta.not_before_epoch_s)); + out.push_back(static_cast(meta.not_after_epoch_s)); out.push_back(static_cast(meta.is_ca ? 1U : 0U)); out.push_back(common::OwnedBuffer{meta.skid.begin(), meta.skid.end()}); out.push_back(common::OwnedBuffer{meta.akid.begin(), meta.akid.end()}); @@ -43,7 +43,7 @@ score::crypto::Expected Ser if (!info_res.has_value()) return score::crypto::make_unexpected(info_res.error()); - const bool has_crl = handler.HasCrl(config); + const bool has_crl = info_res.value().has_crl; int64_t crl_next = 0; if (has_crl) { @@ -55,7 +55,7 @@ score::crypto::Expected Ser common::ResponseParameters out; out.push_back(static_cast(info_res.value().state)); out.push_back(static_cast(has_crl ? 1U : 0U)); - out.push_back(static_cast(static_cast(crl_next))); + out.push_back(static_cast(crl_next)); return out; } @@ -87,7 +87,7 @@ common::ResponseParameters SerializeTrustStoreMembers(const std::vectorsubject}); out.push_back(common::OwnedString{entry.snap->issuer}); out.push_back(common::OwnedString{entry.snap->serial_number}); - out.push_back(static_cast(static_cast(entry.snap->kind))); + out.push_back(static_cast(entry.snap->kind)); out.push_back(static_cast(entry.snap->is_enabled ? 1U : 0U)); } return out; diff --git a/score/crypto/src/daemon/cert_management/slot/crl_handler.cpp b/score/crypto/src/daemon/cert_management/slot/crl_handler.cpp index 9e55d3aa5..ca64105f1 100644 --- a/score/crypto/src/daemon/cert_management/slot/crl_handler.cpp +++ b/score/crypto/src/daemon/cert_management/slot/crl_handler.cpp @@ -17,6 +17,8 @@ #include "score/crypto/src/daemon/cert_management/slot/deployment_writer.hpp" #include "score/crypto/src/daemon/common/storage/file_io.hpp" +#include + namespace score::crypto::daemon::cert_management { namespace @@ -122,14 +124,11 @@ score::crypto::Expected CrlHandler::GetCrlNextUpdate(const const auto value = descriptor->Get("crl", "crl_next_update"); if (value.empty()) return score::crypto::make_unexpected(Error::kResourceNotAllocated); - try - { - return std::stoll(value); - } - catch (...) - { + std::int64_t result{}; + const auto [end, ec] = std::from_chars(value.data(), value.data() + value.size(), result); + if (ec != std::errc{} || end != value.data() + value.size()) return score::crypto::make_unexpected(Error::kInvalidArgument); - } + return result; } score::crypto::FormatType CrlHandler::GetCrlFormat(const CertSlotConfig& slot) const diff --git a/score/crypto/src/daemon/cert_management/slot/slot_registry.cpp b/score/crypto/src/daemon/cert_management/slot/slot_registry.cpp index 9ce3dba32..87796fa15 100644 --- a/score/crypto/src/daemon/cert_management/slot/slot_registry.cpp +++ b/score/crypto/src/daemon/cert_management/slot/slot_registry.cpp @@ -45,42 +45,35 @@ score::crypto::Expectedsecond; - const auto& entry = m_registry[index]; - - auto access_result = AccessPolicyEnforcer::CheckSlotAccess(entry.config, client_id); + auto access_result = AccessPolicyEnforcer::CheckSlotAccess(m_registry[it->second].config, client_id); if (!access_result.has_value()) - { return score::crypto::make_unexpected(access_result.error()); - } - - return CertSlotHandle{index}; + return CertSlotHandle{it->second}; } score::crypto::Expected CertSlotRegistry::ResolveSlotInternal(const std::string& slot_name) const { + // See ResolveSlot: m_name_index is read-only after startup. auto it = m_name_index.find(slot_name); if (it == m_name_index.end()) - { return score::crypto::make_unexpected(score::crypto::daemon::common::DaemonErrorCode::kInvalidResourceId); - } return CertSlotHandle{it->second}; } score::crypto::Expected CertSlotRegistry::GetConfig(CertSlotHandle handle) const { + // See ResolveSlot: m_registry is read-only after startup. IsValidHandle reads + // m_registry.size() without a lock — consistent with GetSlotCount() and IsValidHandle(). if (!IsValidHandle(handle)) - { return score::crypto::make_unexpected(score::crypto::daemon::common::DaemonErrorCode::kInvalidResourceId); - } return &m_registry[handle.index].config; } diff --git a/score/crypto/src/daemon/cert_management/tests/integration/test_certificate_management_integration.cpp b/score/crypto/src/daemon/cert_management/tests/integration/test_certificate_management_integration.cpp index d9b8638a8..05c2848a4 100644 --- a/score/crypto/src/daemon/cert_management/tests/integration/test_certificate_management_integration.cpp +++ b/score/crypto/src/daemon/cert_management/tests/integration/test_certificate_management_integration.cpp @@ -49,10 +49,10 @@ class CertificateManagementIntegrationTest : public ::testing::Test m_certificate_path = m_directory / "device_root.pem"; m_trust_store_path = m_directory / "tls_roots.kv"; - ASSERT_TRUE(std::filesystem::copy_file("score/tests/test_vectors/certificate/certificate.pem", + ASSERT_TRUE(std::filesystem::copy_file("score/tests/test_vectors/certificate/basic/certificate.pem", m_certificate_path, std::filesystem::copy_options::overwrite_existing)); - ASSERT_TRUE(std::filesystem::copy_file("score/tests/test_vectors/certificate/certificate_updated.pem", + ASSERT_TRUE(std::filesystem::copy_file("score/tests/test_vectors/certificate/basic/certificate_updated.pem", m_directory / "certificate_updated.pem", std::filesystem::copy_options::overwrite_existing)); @@ -61,7 +61,7 @@ class CertificateManagementIntegrationTest : public ::testing::Test slot_desc.Set("certificate", "cert_format", "pem"); ASSERT_TRUE(storage::KvDeploymentWriter{}.Write(m_descriptor_path.string(), slot_desc).has_value()); - ASSERT_TRUE(std::filesystem::copy_file("score/tests/test_vectors/certificate/trust_store.kv", + ASSERT_TRUE(std::filesystem::copy_file("score/tests/test_vectors/certificate/basic/trust_store.kv", m_trust_store_path, std::filesystem::copy_options::overwrite_existing)); @@ -135,7 +135,7 @@ TEST_F(CertificateManagementIntegrationTest, LoadsPersistsUpdatesAndInvalidatesT auto trust_store_handle = m_trust_store_manager->ResolveAppResource("tls_roots", 0U); ASSERT_TRUE(trust_store_handle.has_value()); - auto trust_store = m_trust_store_manager->GetStore(trust_store_handle->index); + auto trust_store = m_trust_store_manager->GetStore(*trust_store_handle); ASSERT_NE(trust_store, nullptr); const auto initial_anchors = trust_store->GetAnchors(); @@ -156,7 +156,7 @@ TEST_F(CertificateManagementIntegrationTest, LoadsPersistsUpdatesAndInvalidatesT EXPECT_EQ(descriptor->Get("certificate_metadata", "issuer"), "CN=cert-management-updated,O=Eclipse"); EXPECT_EQ(descriptor->Get("certificate_metadata", "is_ca"), "true"); - m_trust_store_manager->NotifySlotChanged(trust_store_handle->index, *slot); + m_trust_store_manager->NotifySlotChanged(*trust_store_handle, *slot); const auto updated_anchors = trust_store->GetAnchors(); ASSERT_TRUE(updated_anchors.has_value()); ASSERT_EQ(updated_anchors->size(), 1U); diff --git a/score/crypto/src/daemon/cert_management/tests/query/test_cert_object_serializer.cpp b/score/crypto/src/daemon/cert_management/tests/query/test_cert_object_serializer.cpp index d7c4285b1..c2b9dd4dd 100644 --- a/score/crypto/src/daemon/cert_management/tests/query/test_cert_object_serializer.cpp +++ b/score/crypto/src/daemon/cert_management/tests/query/test_cert_object_serializer.cpp @@ -89,7 +89,6 @@ struct SlotHandlerStub : public cert::ICertSlotHandler mutable score::crypto::Expected slot_info_result{score::crypto::CertificateSlotInfo{score::crypto::CertificateSlotState::kOccupied}}; - bool has_crl_result{false}; mutable std::optional crl_next_update_value{std::nullopt}; score::crypto::Expected LoadCertificate( @@ -125,7 +124,7 @@ struct SlotHandlerStub : public cert::ICertSlotHandler bool HasCrl(const cert::CertSlotConfig&) override { - return has_crl_result; + return slot_info_result->has_crl; } score::crypto::Expected, score::crypto::daemon::common::DaemonErrorCode> LoadCrl( @@ -329,7 +328,7 @@ TEST(SerializeCertSlotInfo, Param0_SlotStateUint8_Empty) TEST(SerializeCertSlotInfo, NoCrl_Param1IsZero_Param2IsZero) { SlotHandlerStub stub; - stub.has_crl_result = false; + stub.slot_info_result->has_crl = false; cert::CertSlotConfig config{}; const auto result = query::SerializeCertSlotInfo(stub, config); ASSERT_TRUE(result.has_value()); @@ -345,7 +344,7 @@ TEST(SerializeCertSlotInfo, NoCrl_Param1IsZero_Param2IsZero) TEST(SerializeCertSlotInfo, CrlPresent_Param1IsOne_Param2IsEpoch) { SlotHandlerStub stub; - stub.has_crl_result = true; + stub.slot_info_result->has_crl = true; stub.crl_next_update_value = 1700000000LL; cert::CertSlotConfig config{}; const auto result = query::SerializeCertSlotInfo(stub, config); @@ -364,7 +363,7 @@ TEST(SerializeCertSlotInfo, CrlPresent_GetCrlNextUpdateFails_Param2IsZero) // HasCrl() returns true but GetCrlNextUpdate() is unavailable. // The serializer should encode has_crl=1 and crl_next=0 (not an error). SlotHandlerStub stub; - stub.has_crl_result = true; + stub.slot_info_result->has_crl = true; stub.crl_next_update_value = std::nullopt; // causes GetCrlNextUpdate to return error cert::CertSlotConfig config{}; const auto result = query::SerializeCertSlotInfo(stub, config); diff --git a/score/crypto/src/daemon/cert_management/tests/service/test_cert_management_service.cpp b/score/crypto/src/daemon/cert_management/tests/service/test_cert_management_service.cpp index bfd5dc4ca..92398a396 100644 --- a/score/crypto/src/daemon/cert_management/tests/service/test_cert_management_service.cpp +++ b/score/crypto/src/daemon/cert_management/tests/service/test_cert_management_service.cpp @@ -15,7 +15,7 @@ // // Uses a real DataManager, real CertSlotRegistry, real TrustStoreManager, and // the real OpenSslCertParser + FileBackedSlotHandler backed by the central test -// vector at score/tests/test_vectors/certificate/certificate.pem. +// vector at score/tests/test_vectors/certificate/basic/certificate.pem. // // All cert bytes come from the central test-vector directory so that these // tests share the same known-answer inputs as the integration and provider @@ -80,11 +80,11 @@ class CertManagementServiceTest : public ::testing::Test m_descriptor_path = m_dir / "root_ca.kv"; ASSERT_TRUE(std::filesystem::copy_file( - cert::test::TestVectorPath("score/tests/test_vectors/certificate/certificate.pem"), + cert::test::TestVectorPath("score/tests/test_vectors/certificate/basic/certificate.pem"), m_cert_path, std::filesystem::copy_options::overwrite_existing)); ASSERT_TRUE(std::filesystem::copy_file( - cert::test::TestVectorPath("score/tests/test_vectors/certificate/certificate_updated.pem"), + cert::test::TestVectorPath("score/tests/test_vectors/certificate/basic/certificate_updated.pem"), m_cert_updated_path, std::filesystem::copy_options::overwrite_existing)); @@ -279,8 +279,8 @@ TEST_F(CertManagementServiceTest, NotifySlotCertChanged_RefreshesAnchorSubjectIn return std::make_shared(m_parser); }); - const auto ts_id = m_trust_store_manager->ResolveByName("test-roots"); - auto store = m_trust_store_manager->GetStore(ts_id); + const auto ts_handle = m_trust_store_manager->ResolveByName("test-roots"); + auto store = m_trust_store_manager->GetStore(ts_handle); ASSERT_NE(store, nullptr); // Verify the initial anchor subject. diff --git a/score/crypto/src/daemon/cert_management/tests/slot/test_file_backed_slot_handler.cpp b/score/crypto/src/daemon/cert_management/tests/slot/test_file_backed_slot_handler.cpp index bf5d57ff8..89b7162fa 100644 --- a/score/crypto/src/daemon/cert_management/tests/slot/test_file_backed_slot_handler.cpp +++ b/score/crypto/src/daemon/cert_management/tests/slot/test_file_backed_slot_handler.cpp @@ -250,6 +250,58 @@ TEST_F(FileBackedSlotHandlerTest, ClearCrl_IsIdempotent) EXPECT_FALSE(m_handler->HasCrl(m_slot)); } +// StoreCrl with next_update_epoch_s == 0 must NOT write a crl_next_update key +// to the descriptor. Subsequent GetCrlNextUpdate must return kResourceNotAllocated. +TEST_F(FileBackedSlotHandlerTest, StoreCrl_ZeroNextUpdate_DoesNotWriteNextUpdateKey) +{ + const std::vector crl{0x01U, 0x02U}; + ASSERT_TRUE(m_handler + ->StoreCrl(m_slot, + score::crypto::span{crl.data(), crl.size()}, + score::crypto::FormatType::kDer, + 0 /* next_update_epoch_s = 0 */) + .has_value()); + + const auto nu = m_handler->GetCrlNextUpdate(m_slot); + EXPECT_FALSE(nu.has_value()); + EXPECT_EQ(nu.error(), Error::kResourceNotAllocated); +} + +// StoreCrl with a non-zero next_update_epoch_s must persist the epoch and +// GetCrlNextUpdate must read it back. +TEST_F(FileBackedSlotHandlerTest, StoreCrl_NonZeroNextUpdate_RoundTrips) +{ + constexpr std::int64_t kEpoch = 1800000000LL; + const std::vector crl{0x03U, 0x04U}; + ASSERT_TRUE(m_handler + ->StoreCrl(m_slot, + score::crypto::span{crl.data(), crl.size()}, + score::crypto::FormatType::kDer, + kEpoch) + .has_value()); + + const auto nu = m_handler->GetCrlNextUpdate(m_slot); + ASSERT_TRUE(nu.has_value()); + EXPECT_EQ(*nu, kEpoch); +} + +// GetCrlNextUpdate with a malformed value in the descriptor must return +// kInvalidArgument and must not throw or call std::terminate. +TEST_F(FileBackedSlotHandlerTest, GetCrlNextUpdate_MalformedValue_ReturnsInvalidArgument) +{ + // Write an intentionally invalid epoch string directly to the descriptor. + storage::DeploymentDescriptor descriptor; + descriptor.Set("certificate", "cert_path", m_certificate.string()); + descriptor.Set("certificate", "cert_format", "pem"); + descriptor.Set("crl", "crl_path", m_crl.string()); + descriptor.Set("crl", "crl_next_update", "not_a_number"); + ASSERT_TRUE(storage::KvDeploymentWriter{}.Write(m_descriptor.string(), descriptor).has_value()); + + const auto nu = m_handler->GetCrlNextUpdate(m_slot); + EXPECT_FALSE(nu.has_value()); + EXPECT_EQ(nu.error(), Error::kInvalidArgument); +} + // --------------------------------------------------------------------------- // OpenSSL-backed tests — verify real metadata extraction from test vectors. // These tests require the openssl_backend_active build constraint. @@ -271,7 +323,7 @@ class FileBackedSlotHandlerRealParserTest : public ::testing::Test std::filesystem::remove_all(m_dir); std::filesystem::create_directories(m_dir); - ASSERT_TRUE(std::filesystem::copy_file("score/tests/test_vectors/certificate/certificate.pem", + ASSERT_TRUE(std::filesystem::copy_file("score/tests/test_vectors/certificate/basic/certificate.pem", m_cert_file, std::filesystem::copy_options::overwrite_existing)); @@ -424,30 +476,30 @@ INSTANTIATE_TEST_SUITE_P( FileBackedSlotHandlerAlgorithmVarietyTest, ::testing::Values( // RSA - AlgorithmVarietyParam{"score/tests/test_vectors/certificate/certificate.pem", + AlgorithmVarietyParam{"score/tests/test_vectors/certificate/basic/certificate.pem", "CN=cert-management-test,O=Eclipse"}, - AlgorithmVarietyParam{"score/tests/test_vectors/certificate/rsa_3072.pem", + AlgorithmVarietyParam{"score/tests/test_vectors/certificate/algorithm_variety/rsa_3072.pem", "CN=cert-mgmt-rsa-3072,O=Eclipse"}, - AlgorithmVarietyParam{"score/tests/test_vectors/certificate/rsa_4096.pem", + AlgorithmVarietyParam{"score/tests/test_vectors/certificate/algorithm_variety/rsa_4096.pem", "CN=cert-mgmt-rsa-4096,O=Eclipse"}, // EC - AlgorithmVarietyParam{"score/tests/test_vectors/certificate/ec_p256.pem", + AlgorithmVarietyParam{"score/tests/test_vectors/certificate/algorithm_variety/ec_p256.pem", "CN=cert-mgmt-ec-p256,O=Eclipse"}, - AlgorithmVarietyParam{"score/tests/test_vectors/certificate/ec_p384.pem", + AlgorithmVarietyParam{"score/tests/test_vectors/certificate/algorithm_variety/ec_p384.pem", "CN=cert-mgmt-ec-p384,O=Eclipse"}, - AlgorithmVarietyParam{"score/tests/test_vectors/certificate/ec_p521.pem", + AlgorithmVarietyParam{"score/tests/test_vectors/certificate/algorithm_variety/ec_p521.pem", "CN=cert-mgmt-ec-p521,O=Eclipse"}, // EdDSA - AlgorithmVarietyParam{"score/tests/test_vectors/certificate/ed25519.pem", + AlgorithmVarietyParam{"score/tests/test_vectors/certificate/algorithm_variety/ed25519.pem", "CN=cert-mgmt-ed25519,O=Eclipse"}, - AlgorithmVarietyParam{"score/tests/test_vectors/certificate/ed448.pem", + AlgorithmVarietyParam{"score/tests/test_vectors/certificate/algorithm_variety/ed448.pem", "CN=cert-mgmt-ed448,O=Eclipse"}, // PQC — ML-DSA (NIST FIPS 204) - AlgorithmVarietyParam{"score/tests/test_vectors/certificate/ml_dsa_44.pem", + AlgorithmVarietyParam{"score/tests/test_vectors/certificate/algorithm_variety/ml_dsa_44.pem", "CN=cert-mgmt-ml-dsa-44,O=Eclipse"}, - AlgorithmVarietyParam{"score/tests/test_vectors/certificate/ml_dsa_65.pem", + AlgorithmVarietyParam{"score/tests/test_vectors/certificate/algorithm_variety/ml_dsa_65.pem", "CN=cert-mgmt-ml-dsa-65,O=Eclipse"}, - AlgorithmVarietyParam{"score/tests/test_vectors/certificate/ml_dsa_87.pem", + AlgorithmVarietyParam{"score/tests/test_vectors/certificate/algorithm_variety/ml_dsa_87.pem", "CN=cert-mgmt-ml-dsa-87,O=Eclipse"} ), [](const ::testing::TestParamInfo& info) { diff --git a/score/crypto/src/daemon/cert_management/tests/truststore/test_trust_store_manager.cpp b/score/crypto/src/daemon/cert_management/tests/truststore/test_trust_store_manager.cpp index 762dea142..48e048654 100644 --- a/score/crypto/src/daemon/cert_management/tests/truststore/test_trust_store_manager.cpp +++ b/score/crypto/src/daemon/cert_management/tests/truststore/test_trust_store_manager.cpp @@ -17,7 +17,7 @@ // test vectors so that trust-store anchor loading, eviction, and mutation // operations are exercised against real certificate material — not stubs. // -// All cert bytes come from score/tests/test_vectors/certificate/ and all slot +// All cert bytes come from score/tests/test_vectors/certificate/basic/ and all slot // state is backed by KV descriptor files in a per-test temp directory. // // Test subjects: @@ -29,6 +29,7 @@ // - AddMember — stores a cert into an empty exclusive slot, persists to disk // - RemoveMember — removes anchor by real SHA-256 fingerprint // - AcknowledgeMemberUpdate — re-enables a disabled slot with fresh load +// - ConditionalExternal — fingerprint mismatch auto-disables on reload; AcknowledgeMemberUpdate re-enables #include "score/crypto/src/daemon/cert_management/slot/file_backed_slot_handler.hpp" #include "score/crypto/src/daemon/cert_management/tests/test_environment.hpp" @@ -88,7 +89,7 @@ class TrustStoreManagerTest : public ::testing::Test // root-anchor: slot backed by the initial test vector cert. m_root_cert = m_dir / "root_ca.pem"; m_root_slot_kv = m_dir / "root_ca.kv"; - ASSERT_TRUE(std::filesystem::copy_file("score/tests/test_vectors/certificate/certificate.pem", + ASSERT_TRUE(std::filesystem::copy_file("score/tests/test_vectors/certificate/basic/certificate.pem", m_root_cert, std::filesystem::copy_options::overwrite_existing)); WriteSlotDescriptor(m_root_slot_kv, m_root_cert); @@ -181,10 +182,10 @@ TEST_F(TrustStoreManagerTest, LoadsAnchorsLazilyAndBuildsSlotMembershipIndex) // Membership index is built at Load() time without touching cert bytes. ASSERT_EQ(manager.GetMembershipsForSlot(slot).size(), 1U); - EXPECT_EQ(manager.GetMembershipsForSlot(slot)[0], 0U); + EXPECT_EQ(manager.GetMembershipsForSlot(slot)[0], cert::TrustStoreHandle{0U}); // Anchors are loaded lazily on the first GetAnchors() call. - auto store = manager.GetStore(0U); + auto store = manager.GetStore(cert::TrustStoreHandle{0U}); ASSERT_NE(store, nullptr); auto anchors = store->GetAnchors(); ASSERT_TRUE(anchors.has_value()); @@ -200,7 +201,7 @@ TEST_F(TrustStoreManagerTest, LoadsAnchorsLazilyAndBuildsSlotMembershipIndex) TEST(TrustStoreManagerStandaloneTest, RejectsUnknownStoreAndSlotHandles) { cert::TrustStoreManager manager; - EXPECT_EQ(manager.GetStore(0U), nullptr); + EXPECT_EQ(manager.GetStore(cert::TrustStoreHandle{0U}), nullptr); EXPECT_EQ(manager.GetMembershipsForSlot(cert::CertSlotHandle{4U}).size(), 0U); } @@ -227,8 +228,8 @@ TEST_F(TrustStoreManagerTest, PerClientAddRef_DoesNotEvictCacheWhileOtherClientH cert::TrustStoreManager manager; manager.Load({ts_cfg}, registry, MakeHandlerFactory()); - const auto ts_id = manager.ResolveByName("tls-roots"); - auto store = manager.GetStore(ts_id); + const auto ts_handle = manager.ResolveByName("tls-roots"); + auto store = manager.GetStore(ts_handle); ASSERT_NE(store, nullptr); // Trigger initial load; drop the result so the anchor's strong ref lives @@ -239,12 +240,11 @@ TEST_F(TrustStoreManagerTest, PerClientAddRef_DoesNotEvictCacheWhileOtherClientH EXPECT_EQ((*anchors)[0]->GetSubject(), kSubjectInitial); } - const cert::TrustStoreHandle ts_handle{ts_id}; manager.AddRef(ts_handle, kClientA); manager.AddRef(ts_handle, kClientB); // Rotate cert on disk — subsequent reloads will see kSubjectUpdated. - ASSERT_TRUE(std::filesystem::copy_file("score/tests/test_vectors/certificate/certificate_updated.pem", + ASSERT_TRUE(std::filesystem::copy_file("score/tests/test_vectors/certificate/basic/certificate_updated.pem", m_root_cert, std::filesystem::copy_options::overwrite_existing)); @@ -284,12 +284,11 @@ TEST_F(TrustStoreManagerTest, CleanupClient_ReleasesAllRefsAndEvictsCache) cert::TrustStoreManager manager; manager.Load({ts_cfg}, registry, MakeHandlerFactory()); - const auto ts_id = manager.ResolveByName("tls-roots"); - auto store = manager.GetStore(ts_id); + const auto ts_handle = manager.ResolveByName("tls-roots"); + auto store = manager.GetStore(ts_handle); ASSERT_NE(store, nullptr); // Simulate two open verification contexts for kClientA. - const cert::TrustStoreHandle ts_handle{ts_id}; manager.AddRef(ts_handle, kClientA); manager.AddRef(ts_handle, kClientA); @@ -301,7 +300,7 @@ TEST_F(TrustStoreManagerTest, CleanupClient_ReleasesAllRefsAndEvictsCache) } // Rotate cert on disk. - ASSERT_TRUE(std::filesystem::copy_file("score/tests/test_vectors/certificate/certificate_updated.pem", + ASSERT_TRUE(std::filesystem::copy_file("score/tests/test_vectors/certificate/basic/certificate_updated.pem", m_root_cert, std::filesystem::copy_options::overwrite_existing)); @@ -334,8 +333,8 @@ TEST_F(TrustStoreManagerTest, DisableAndReEnable_MemberTogglesAnchorVisibility) cert::TrustStoreManager manager; manager.Load({ts_cfg}, registry, MakeHandlerFactory()); - const auto ts_id = manager.ResolveByName("tls-roots"); - auto store = manager.GetStore(ts_id); + const auto ts_handle = manager.ResolveByName("tls-roots"); + auto store = manager.GetStore(ts_handle); ASSERT_NE(store, nullptr); // Initial load: one anchor present. @@ -345,13 +344,13 @@ TEST_F(TrustStoreManagerTest, DisableAndReEnable_MemberTogglesAnchorVisibility) EXPECT_EQ((*initial)[0]->GetSubject(), kSubjectInitial); // Disable: anchor must disappear from the store immediately. - ASSERT_TRUE(manager.DisableMember(ts_id, slot, kClientA).has_value()); + ASSERT_TRUE(manager.DisableMember(ts_handle, slot, kClientA).has_value()); auto after_disable = store->GetAnchors(); ASSERT_TRUE(after_disable.has_value()); EXPECT_EQ(after_disable->size(), 0U); // Re-enable: anchor must reappear with the same subject. - ASSERT_TRUE(manager.EnableMember(ts_id, slot, kClientA).has_value()); + ASSERT_TRUE(manager.EnableMember(ts_handle, slot, kClientA).has_value()); auto after_enable = store->GetAnchors(); ASSERT_TRUE(after_enable.has_value()); ASSERT_EQ(after_enable->size(), 1U); @@ -383,10 +382,10 @@ TEST_F(TrustStoreManagerTest, TwoStores_OneSlot_BothGetMemberships) // Both stores report membership for the shared slot. EXPECT_EQ(manager.GetMembershipsForSlot(slot).size(), 2U); - const auto ts_id_a = manager.ResolveByName("store-a"); - const auto ts_id_b = manager.ResolveByName("store-b"); - auto anchors_a = manager.GetStore(ts_id_a)->GetAnchors(); - auto anchors_b = manager.GetStore(ts_id_b)->GetAnchors(); + const auto ts_handle_a = manager.ResolveByName("store-a"); + const auto ts_handle_b = manager.ResolveByName("store-b"); + auto anchors_a = manager.GetStore(ts_handle_a)->GetAnchors(); + auto anchors_b = manager.GetStore(ts_handle_b)->GetAnchors(); ASSERT_TRUE(anchors_a.has_value()); ASSERT_TRUE(anchors_b.has_value()); ASSERT_EQ(anchors_a->size(), 1U); @@ -417,8 +416,8 @@ TEST_F(TrustStoreManagerTest, AddMember_ToExclusiveSlot_AddsAnchorAndPersistsToD cert::TrustStoreManager manager; manager.Load({ts_cfg}, registry, MakeHandlerFactory()); - const auto ts_id = manager.ResolveByName("mutable-store"); - auto store = manager.GetStore(ts_id); + const auto ts_handle = manager.ResolveByName("mutable-store"); + auto store = manager.GetStore(ts_handle); ASSERT_NE(store, nullptr); // Slot is empty — no anchors before AddMember. @@ -431,7 +430,7 @@ TEST_F(TrustStoreManagerTest, AddMember_ToExclusiveSlot_AddsAnchorAndPersistsToD ASSERT_NE(cert, nullptr); EXPECT_EQ(cert->GetSubject(), kSubjectInitial); - ASSERT_TRUE(manager.AddMember(ts_id, cert, kClientA).has_value()); + ASSERT_TRUE(manager.AddMember(ts_handle, cert, kClientA).has_value()); // Anchor must be visible immediately after AddMember. auto after = store->GetAnchors(); @@ -466,8 +465,8 @@ TEST_F(TrustStoreManagerTest, RemoveMember_ByFingerprint_RemovesAnchor) cert::TrustStoreManager manager; manager.Load({ts_cfg}, registry, MakeHandlerFactory()); - const auto ts_id = manager.ResolveByName("mutable-store"); - auto store = manager.GetStore(ts_id); + const auto ts_handle = manager.ResolveByName("mutable-store"); + auto store = manager.GetStore(ts_handle); ASSERT_NE(store, nullptr); // Load the anchors to obtain the real SHA-256 fingerprint. @@ -480,7 +479,7 @@ TEST_F(TrustStoreManagerTest, RemoveMember_ByFingerprint_RemovesAnchor) ASSERT_EQ(fp_span.size(), 32U); const std::vector fingerprint{fp_span.begin(), fp_span.end()}; - ASSERT_TRUE(manager.RemoveMember(ts_id, fingerprint, kClientA).has_value()); + ASSERT_TRUE(manager.RemoveMember(ts_handle, fingerprint, kClientA).has_value()); // Anchor must be gone after removal. auto after = store->GetAnchors(); @@ -506,8 +505,8 @@ TEST_F(TrustStoreManagerTest, AcknowledgeMemberUpdate_TransitionsDisabledMemberT cert::TrustStoreManager manager; manager.Load({ts_cfg}, registry, MakeHandlerFactory()); - const auto ts_id = manager.ResolveByName("cond-store"); - auto store = manager.GetStore(ts_id); + const auto ts_handle = manager.ResolveByName("cond-store"); + auto store = manager.GetStore(ts_handle); ASSERT_NE(store, nullptr); // Initial load — anchor present. @@ -517,12 +516,12 @@ TEST_F(TrustStoreManagerTest, AcknowledgeMemberUpdate_TransitionsDisabledMemberT EXPECT_EQ((*initial)[0]->GetSubject(), kSubjectInitial); // Rotate cert on disk so that AcknowledgeMemberUpdate reloads a different cert. - ASSERT_TRUE(std::filesystem::copy_file("score/tests/test_vectors/certificate/certificate_updated.pem", + ASSERT_TRUE(std::filesystem::copy_file("score/tests/test_vectors/certificate/basic/certificate_updated.pem", m_root_cert, std::filesystem::copy_options::overwrite_existing)); // Disable — simulates detection of an unexpected content change. - ASSERT_TRUE(manager.DisableMember(ts_id, slot, kClientA).has_value()); + ASSERT_TRUE(manager.DisableMember(ts_handle, slot, kClientA).has_value()); { auto after_disable = store->GetAnchors(); ASSERT_TRUE(after_disable.has_value()); @@ -530,7 +529,7 @@ TEST_F(TrustStoreManagerTest, AcknowledgeMemberUpdate_TransitionsDisabledMemberT } // Acknowledge — fresh load from disk, records accepted_fingerprint, re-enables. - ASSERT_TRUE(manager.AcknowledgeMemberUpdate(ts_id, slot, kClientA).has_value()); + ASSERT_TRUE(manager.AcknowledgeMemberUpdate(ts_handle, slot, kClientA).has_value()); auto after_ack = store->GetAnchors(); ASSERT_TRUE(after_ack.has_value()); @@ -540,6 +539,84 @@ TEST_F(TrustStoreManagerTest, AcknowledgeMemberUpdate_TransitionsDisabledMemberT EXPECT_EQ((*after_ack)[0]->GetFingerprint().size(), 32U); } +// --------------------------------------------------------------------------- +// ConditionalExternal — fingerprint mismatch triggers auto-disable; the entire +// scenario from initial acceptance through rotation, mismatch detection, +// and re-acknowledgement is exercised here. +// +// Steps: +// 1. kConditionalExternal member with kEnableAndAcceptCurrent init policy: +// first GetAnchors() records accepted_fingerprint = fingerprint(cert A). +// 2. Cert rotated on disk (cert A → cert B) while a client ref is held. +// 3. ReleaseRef → cache evicted (m_loaded = false, m_slots cleared). +// 4. Next GetAnchors() → LoadAnchorsIntoHandler reloads cert B from disk, +// detects fingerprint(B) ≠ accepted_fingerprint(A) → auto-disables +// the member → returns empty anchors. +// 5. AcknowledgeMemberUpdate → fresh load of cert B, records new fingerprint, +// re-enables via NotifySlotUpdate. +// 6. GetAnchors() returns cert B without another reload cycle. +// --------------------------------------------------------------------------- + +TEST_F(TrustStoreManagerTest, ConditionalExternal_FingerprintMismatch_AutoDisablesUntilAcknowledged) +{ + auto registry = std::make_shared(); + const auto slot = registry->RegisterSlot(MakeSlotConfig("root-anchor", m_root_slot_kv)); + + cert::TrustStoreConfig ts_cfg; + ts_cfg.store_name = "cond-store"; + ts_cfg.access_policy.allowed_write_uids = {0U}; + ts_cfg.conditional_slot_initialization = cert::ConditionalSlotInitialization::kEnableAndAcceptCurrent; + ts_cfg.members.push_back( + cert::TrustStoreMemberConfig{"root-anchor", cert::TrustStoreMemberKind::kConditionalExternal}); + + cert::TrustStoreManager manager; + manager.Load({ts_cfg}, registry, MakeHandlerFactory()); + + const auto ts_handle = manager.ResolveByName("cond-store"); + auto store = manager.GetStore(ts_handle); + ASSERT_NE(store, nullptr); + + // Step 1: Initial load — kEnableAndAcceptCurrent records accepted_fingerprint. + // The ref is established before the load so that ReleaseRef later triggers eviction. + manager.AddRef(ts_handle, kClientA); + { + auto initial = store->GetAnchors(); + ASSERT_TRUE(initial.has_value()); + ASSERT_EQ(initial->size(), 1U); + EXPECT_EQ((*initial)[0]->GetSubject(), kSubjectInitial); + // 'initial' drops here; the only remaining strong ref is inside TrustStoreHandler::m_slots. + } + + // Step 2: Rotate cert on disk without acknowledgement. + ASSERT_TRUE(std::filesystem::copy_file("score/tests/test_vectors/certificate/basic/certificate_updated.pem", + m_root_cert, + std::filesystem::copy_options::overwrite_existing)); + + // Step 3: ReleaseRef — last client ref gone → MaybeEvictAnchorCache → ClearAnchorCache + // (m_loaded = false, m_slots cleared, weak_ptr in m_slot_cert_cache expires). + manager.ReleaseRef(ts_handle, kClientA); + + // Step 4: GetAnchors() triggers LoadAnchorsIntoHandler which loads cert B from disk, + // compares fingerprint(B) with the recorded fingerprint(A), detects mismatch, + // sets state.enabled = false, and calls NotifySlotUpdate with nullptr. + { + auto after_rotate = store->GetAnchors(); + ASSERT_TRUE(after_rotate.has_value()); + EXPECT_EQ(after_rotate->size(), 0U); + } + + // Step 5: Acknowledge — fresh load of cert B records new fingerprint and re-enables. + ASSERT_TRUE(manager.AcknowledgeMemberUpdate(ts_handle, slot, kClientA).has_value()); + + // Step 6: GetAnchors() returns cert B without another reload cycle (m_loaded is + // still true; AcknowledgeMemberUpdate updated m_slots directly via NotifySlotUpdate). + auto after_ack = store->GetAnchors(); + ASSERT_TRUE(after_ack.has_value()); + ASSERT_EQ(after_ack->size(), 1U); + EXPECT_EQ((*after_ack)[0]->GetSubject(), kSubjectUpdated); + EXPECT_EQ((*after_ack)[0]->GetFingerprint().size(), 32U); +} + // --------------------------------------------------------------------------- // AddMember — CRL propagation: non-empty crl_bytes are stored to the exclusive // slot alongside the certificate. @@ -559,7 +636,7 @@ TEST_F(TrustStoreManagerTest, AddMember_WithCrlBytes_StoresAndPersistsCrl) cert::TrustStoreManager manager; manager.Load({ts_cfg}, registry, MakeHandlerFactory()); - const auto ts_id = manager.ResolveByName("mutable-store"); + const auto ts_handle = manager.ResolveByName("mutable-store"); auto cert = ParseCert(m_root_cert); ASSERT_NE(cert, nullptr); @@ -567,7 +644,7 @@ TEST_F(TrustStoreManagerTest, AddMember_WithCrlBytes_StoresAndPersistsCrl) const std::vector crl{0xC0U, 0xC1U, 0xC2U}; const auto crl_span = score::crypto::span{crl.data(), crl.size()}; - ASSERT_TRUE(manager.AddMember(ts_id, cert, kClientA, crl_span, score::crypto::FormatType::kDer).has_value()); + ASSERT_TRUE(manager.AddMember(ts_handle, cert, kClientA, crl_span, score::crypto::FormatType::kDer).has_value()); // Verify via a fresh handler: cert and CRL are both on disk. cert::FileBackedSlotHandler fresh_handler{m_parser}; @@ -599,13 +676,13 @@ TEST_F(TrustStoreManagerTest, AddMember_ExistingExclusiveMember_UpsertsCrl) cert::TrustStoreManager manager; manager.Load({ts_cfg}, registry, MakeHandlerFactory()); - const auto ts_id = manager.ResolveByName("mutable-store"); + const auto ts_handle = manager.ResolveByName("mutable-store"); auto cert = ParseCert(m_root_cert); ASSERT_NE(cert, nullptr); // First add: cert only, no CRL. - ASSERT_TRUE(manager.AddMember(ts_id, cert, kClientA).has_value()); + ASSERT_TRUE(manager.AddMember(ts_handle, cert, kClientA).has_value()); { cert::FileBackedSlotHandler fh{m_parser}; const auto cfg = MakeSlotConfig("empty-anchor", m_empty_slot_kv); @@ -615,7 +692,7 @@ TEST_F(TrustStoreManagerTest, AddMember_ExistingExclusiveMember_UpsertsCrl) // Second add: same cert, with CRL — upsert path. const std::vector crl{0xAAU, 0xBBU, 0xCCU}; const auto crl_span = score::crypto::span{crl.data(), crl.size()}; - ASSERT_TRUE(manager.AddMember(ts_id, cert, kClientA, crl_span, score::crypto::FormatType::kDer).has_value()); + ASSERT_TRUE(manager.AddMember(ts_handle, cert, kClientA, crl_span, score::crypto::FormatType::kDer).has_value()); cert::FileBackedSlotHandler fresh_handler{m_parser}; const auto cfg = MakeSlotConfig("empty-anchor", m_empty_slot_kv); @@ -643,7 +720,7 @@ TEST_F(TrustStoreManagerTest, AddMember_SharedStaticMember_WithCrl_ReturnsUnsupp cert::TrustStoreManager manager; manager.Load({ts_cfg}, registry, MakeHandlerFactory()); - const auto ts_id = manager.ResolveByName("mixed-store"); + const auto ts_handle = manager.ResolveByName("mixed-store"); // Parse the same cert that is already the shared-static member. auto cert = ParseCert(m_root_cert); @@ -652,7 +729,7 @@ TEST_F(TrustStoreManagerTest, AddMember_SharedStaticMember_WithCrl_ReturnsUnsupp const std::vector crl{0x01U, 0x02U}; const auto crl_span = score::crypto::span{crl.data(), crl.size()}; - const auto result = manager.AddMember(ts_id, cert, kClientA, crl_span, score::crypto::FormatType::kDer); + const auto result = manager.AddMember(ts_handle, cert, kClientA, crl_span, score::crypto::FormatType::kDer); EXPECT_FALSE(result.has_value()); EXPECT_EQ(result.error(), score::crypto::daemon::common::DaemonErrorCode::kUnsupportedOperation); } @@ -678,14 +755,14 @@ TEST_F(TrustStoreManagerTest, AddMember_DeduplicationChecksAllMemberTypes) cert::TrustStoreManager manager; manager.Load({ts_cfg}, registry, MakeHandlerFactory()); - const auto ts_id = manager.ResolveByName("mixed-store"); + const auto ts_handle = manager.ResolveByName("mixed-store"); // Parse the same cert that is already the shared-static member. auto cert = ParseCert(m_root_cert); ASSERT_NE(cert, nullptr); // AddMember without CRL: dedup on shared-static → idempotent success. - ASSERT_TRUE(manager.AddMember(ts_id, cert, kClientA).has_value()); + ASSERT_TRUE(manager.AddMember(ts_handle, cert, kClientA).has_value()); // The exclusive slot must still be empty — not consumed by AddMember. cert::FileBackedSlotHandler fresh_handler{m_parser}; @@ -713,19 +790,20 @@ TEST_F(TrustStoreManagerTest, ImportCrlForMember_WritesToExclusiveSlot) cert::TrustStoreManager manager; manager.Load({ts_cfg}, registry, MakeHandlerFactory()); - const auto ts_id = manager.ResolveByName("mutable-store"); + const auto ts_handle = manager.ResolveByName("mutable-store"); auto cert = ParseCert(m_root_cert); ASSERT_NE(cert, nullptr); // First add the cert without CRL. - ASSERT_TRUE(manager.AddMember(ts_id, cert, kClientA).has_value()); + ASSERT_TRUE(manager.AddMember(ts_handle, cert, kClientA).has_value()); // Now import a CRL for that member by slot handle. const std::vector crl{0xD0U, 0xD1U, 0xD2U}; const auto crl_span = score::crypto::span{crl.data(), crl.size()}; - ASSERT_TRUE(manager.ImportCrlForMember(ts_id, slot_handle, crl_span, score::crypto::FormatType::kDer).has_value()); + ASSERT_TRUE( + manager.ImportCrlForMember(ts_handle, slot_handle, crl_span, score::crypto::FormatType::kDer).has_value()); // Verify persistence: fresh handler must read the CRL. cert::FileBackedSlotHandler fresh_handler{m_parser}; @@ -736,4 +814,182 @@ TEST_F(TrustStoreManagerTest, ImportCrlForMember_WritesToExclusiveSlot) EXPECT_EQ(*loaded_crl, crl); } +// --------------------------------------------------------------------------- +// AddMember — capacity exceeded: all exclusive slots are occupied by a +// different cert. The trust store has no shared-static member and no empty +// exclusive slot. +// --------------------------------------------------------------------------- + +TEST_F(TrustStoreManagerTest, AddMember_NoEmptyExclusiveSlot_ReturnsCapacityExceeded) +{ + auto registry = std::make_shared(); + static_cast(registry->RegisterSlot(MakeSlotConfig("root-anchor", m_root_slot_kv))); + + cert::TrustStoreConfig ts_cfg; + ts_cfg.store_name = "full-store"; + ts_cfg.access_policy.allowed_write_uids = {0U}; + ts_cfg.members.push_back( + cert::TrustStoreMemberConfig{"root-anchor", cert::TrustStoreMemberKind::kExclusiveMutable}); + + cert::TrustStoreManager manager; + manager.Load({ts_cfg}, registry, MakeHandlerFactory()); + + const auto ts_handle = manager.ResolveByName("full-store"); + + // root-anchor already holds a cert (kSubjectInitial). Add the same cert first to fill the slot. + auto cert_a = ParseCert(m_root_cert); + ASSERT_NE(cert_a, nullptr); + ASSERT_TRUE(manager.AddMember(ts_handle, cert_a, kClientA).has_value()); + + // Now try to add a *different* cert — slot is occupied, no empty exclusive slot. + // Use the updated cert (different fingerprint) to bypass dedup. + ASSERT_TRUE(std::filesystem::copy_file("score/tests/test_vectors/certificate/basic/certificate_updated.pem", + m_dir / "alt.pem", + std::filesystem::copy_options::overwrite_existing)); + auto cert_b = ParseCert(m_dir / "alt.pem"); + ASSERT_NE(cert_b, nullptr); + + const auto result = manager.AddMember(ts_handle, cert_b, kClientA); + EXPECT_FALSE(result.has_value()); + EXPECT_EQ(result.error(), score::crypto::daemon::common::DaemonErrorCode::kTrustStoreCapacityExceeded); +} + +// --------------------------------------------------------------------------- +// RemoveMember — wrong fingerprint: no exclusive member matches; returns +// kInvalidArgument rather than silently succeeding. +// --------------------------------------------------------------------------- + +TEST_F(TrustStoreManagerTest, RemoveMember_WrongFingerprint_ReturnsInvalidArgument) +{ + auto registry = std::make_shared(); + static_cast(registry->RegisterSlot(MakeSlotConfig("root-anchor", m_root_slot_kv))); + + cert::TrustStoreConfig ts_cfg; + ts_cfg.store_name = "mutable-store"; + ts_cfg.access_policy.allowed_write_uids = {0U}; + ts_cfg.members.push_back( + cert::TrustStoreMemberConfig{"root-anchor", cert::TrustStoreMemberKind::kExclusiveMutable}); + + cert::TrustStoreManager manager; + manager.Load({ts_cfg}, registry, MakeHandlerFactory()); + + const auto ts_handle = manager.ResolveByName("mutable-store"); + + const std::vector wrong_fp(32U, 0xFFU); + const auto result = manager.RemoveMember(ts_handle, wrong_fp, kClientA); + EXPECT_FALSE(result.has_value()); + EXPECT_EQ(result.error(), score::crypto::daemon::common::DaemonErrorCode::kInvalidArgument); +} + +// --------------------------------------------------------------------------- +// ImportCrlForMember — returns kUnsupportedOperation when the target slot is +// a shared-static member (the trust store does not own it). +// --------------------------------------------------------------------------- + +TEST_F(TrustStoreManagerTest, ImportCrlForMember_SharedStaticSlot_ReturnsUnsupportedOperation) +{ + auto registry = std::make_shared(); + const auto slot_handle = registry->RegisterSlot(MakeSlotConfig("root-anchor", m_root_slot_kv)); + + cert::TrustStoreConfig ts_cfg; + ts_cfg.store_name = "shared-store"; + ts_cfg.access_policy.allowed_write_uids = {0U}; + ts_cfg.members.push_back(cert::TrustStoreMemberConfig{"root-anchor", cert::TrustStoreMemberKind::kSharedStatic}); + + cert::TrustStoreManager manager; + manager.Load({ts_cfg}, registry, MakeHandlerFactory()); + + const auto ts_handle = manager.ResolveByName("shared-store"); + + const std::vector crl{0x01U, 0x02U, 0x03U}; + const auto crl_span = score::crypto::span{crl.data(), crl.size()}; + + const auto result = manager.ImportCrlForMember(ts_handle, slot_handle, crl_span, score::crypto::FormatType::kDer); + EXPECT_FALSE(result.has_value()); + EXPECT_EQ(result.error(), score::crypto::daemon::common::DaemonErrorCode::kUnsupportedOperation); +} + +// --------------------------------------------------------------------------- +// EnableMember / DisableMember — slot is not a registered member of the trust +// store; returns kInvalidArgument. +// --------------------------------------------------------------------------- + +TEST_F(TrustStoreManagerTest, EnableMember_UnregisteredSlot_ReturnsInvalidArgument) +{ + auto registry = std::make_shared(); + static_cast(registry->RegisterSlot(StaticSlotConfig("root-anchor"))); + // Register a second slot that is NOT a member of the trust store. + const auto unrelated_slot = registry->RegisterSlot(MakeSlotConfig("unrelated", m_empty_slot_kv)); + + cert::TrustStoreConfig ts_cfg; + ts_cfg.store_name = "tls-roots"; + ts_cfg.access_policy.allowed_write_uids = {0U}; + ts_cfg.members.push_back(cert::TrustStoreMemberConfig{"root-anchor", cert::TrustStoreMemberKind::kSharedStatic}); + + cert::TrustStoreManager manager; + manager.Load({ts_cfg}, registry, MakeHandlerFactory()); + + const auto ts_handle = manager.ResolveByName("tls-roots"); + + const auto result = manager.EnableMember(ts_handle, unrelated_slot, kClientA); + EXPECT_FALSE(result.has_value()); + EXPECT_EQ(result.error(), score::crypto::daemon::common::DaemonErrorCode::kInvalidArgument); + + const auto result2 = manager.DisableMember(ts_handle, unrelated_slot, kClientA); + EXPECT_FALSE(result2.has_value()); + EXPECT_EQ(result2.error(), score::crypto::daemon::common::DaemonErrorCode::kInvalidArgument); +} + +// --------------------------------------------------------------------------- +// Trust-store state persistence — after DisableMember, a second manager +// instance loading from the same descriptor must see the slot as disabled. +// --------------------------------------------------------------------------- + +TEST_F(TrustStoreManagerTest, PersistState_DisabledMemberSurvivesReload) +{ + // Need a deployment path for the trust store itself (stores enabled/disabled state). + const auto ts_kv = m_dir / "ts_state.kv"; + // Create an empty descriptor so DeploymentLoader finds the file on second load. + storage::DeploymentDescriptor empty_desc; + ASSERT_TRUE(storage::KvDeploymentWriter{}.Write(ts_kv.string(), empty_desc).has_value()); + + auto registry = std::make_shared(); + const auto slot = registry->RegisterSlot(StaticSlotConfig("root-anchor")); + + cert::TrustStoreConfig ts_cfg; + ts_cfg.store_name = "persist-store"; + ts_cfg.access_policy.allowed_write_uids = {0U}; + ts_cfg.deployment_path = ts_kv.string(); + ts_cfg.deployment_format = "kv"; + ts_cfg.members.push_back(cert::TrustStoreMemberConfig{"root-anchor", cert::TrustStoreMemberKind::kSharedStatic}); + + { + cert::TrustStoreManager manager; + manager.Load({ts_cfg}, registry, MakeHandlerFactory()); + + const auto ts_handle = manager.ResolveByName("persist-store"); + auto store = manager.GetStore(ts_handle); + // Confirm anchor is visible before disabling. + auto before = store->GetAnchors(); + ASSERT_TRUE(before.has_value()); + ASSERT_EQ(before->size(), 1U); + + // Disable and persist. + ASSERT_TRUE(manager.DisableMember(ts_handle, slot, kClientA).has_value()); + } + + // Second manager instance — simulates daemon restart. + cert::TrustStoreManager manager2; + manager2.Load({ts_cfg}, registry, MakeHandlerFactory()); + + const auto ts_handle2 = manager2.ResolveByName("persist-store"); + auto store2 = manager2.GetStore(ts_handle2); + ASSERT_NE(store2, nullptr); + + // The disabled state was persisted; GetAnchors must return empty. + auto after_reload = store2->GetAnchors(); + ASSERT_TRUE(after_reload.has_value()); + EXPECT_EQ(after_reload->size(), 0U); +} + } // namespace diff --git a/score/crypto/src/daemon/cert_management/truststore/trust_store_manager.cpp b/score/crypto/src/daemon/cert_management/truststore/trust_store_manager.cpp index 1806cfbd4..50f3b9d23 100644 --- a/score/crypto/src/daemon/cert_management/truststore/trust_store_manager.cpp +++ b/score/crypto/src/daemon/cert_management/truststore/trust_store_manager.cpp @@ -193,7 +193,7 @@ CertObject::Sptr TrustStoreManager::LoadOrGetCached(CertSlotHandle slot) auto* handler = GetOrCreateHandler(slot); const auto cfg = m_slot_registry ? m_slot_registry->GetConfig(slot) : nullptr; - if (!handler || !cfg) + if (handler == nullptr || !cfg) return nullptr; auto loaded = handler->LoadCertificate(**cfg); @@ -268,7 +268,7 @@ void TrustStoreManager::LoadAnchorsIntoHandler(TrustStoreId id, TrustStoreHandle { auto* slot_handler = GetOrCreateHandler(*slot); const auto cfg = m_slot_registry->GetConfig(*slot); - if (slot_handler && cfg && slot_handler->HasCrl(**cfg)) + if (slot_handler != nullptr && cfg && slot_handler->HasCrl(**cfg)) { auto crl_res = slot_handler->LoadCrl(**cfg); if (crl_res) @@ -285,17 +285,19 @@ void TrustStoreManager::LoadAnchorsIntoHandler(TrustStoreId id, TrustStoreHandle // Store access // --------------------------------------------------------------------------- -ITrustStoreHandler::Sptr TrustStoreManager::GetStore(TrustStoreId id) const +ITrustStoreHandler::Sptr TrustStoreManager::GetStore(TrustStoreHandle handle) const { std::lock_guard lock(m_mutex); - return id < m_stores.size() ? m_stores[id].handler : nullptr; + if (!handle.IsValid()) + return nullptr; + return handle.index < m_stores.size() ? m_stores[handle.index].handler : nullptr; } -TrustStoreId TrustStoreManager::ResolveByName(const std::string& name) const +TrustStoreHandle TrustStoreManager::ResolveByName(const std::string& name) const { std::lock_guard lock(m_mutex); const auto it = m_name_index.find(name); - return it == m_name_index.end() ? UINT32_MAX : it->second; + return it == m_name_index.end() ? TrustStoreHandle{} : TrustStoreHandle{it->second}; } void TrustStoreManager::RegisterAppResource(uint32_t uid, @@ -328,11 +330,17 @@ score::crypto::Expected TrustStoreManager::ResolveAppRe // Slot membership query // --------------------------------------------------------------------------- -std::vector TrustStoreManager::GetMembershipsForSlot(CertSlotHandle slot) const +std::vector TrustStoreManager::GetMembershipsForSlot(CertSlotHandle slot) const { std::lock_guard lock(m_mutex); const auto it = m_slot_memberships.find(slot.index); - return it == m_slot_memberships.end() ? std::vector{} : it->second; + if (it == m_slot_memberships.end()) + return {}; + std::vector result; + result.reserve(it->second.size()); + for (const auto id : it->second) + result.push_back(TrustStoreHandle{id}); + return result; } // --------------------------------------------------------------------------- @@ -344,13 +352,13 @@ void TrustStoreManager::MaybeEvictAnchorCache(TrustStoreHandle handle) // Must be called with m_mutex held. for (const auto& [cid, stores] : m_client_ref_counts) { - if (stores.count(handle.index)) + if (stores.count(handle.index) != 0U) return; // at least one client still active for this store } if (handle.index < m_stores.size()) { auto* h = static_cast(m_stores[handle.index].handler.get()); - if (h) + if (h != nullptr) h->ClearAnchorCache(); } score::mw::log::LogDebug() << kLogPrefix << "No active clients for store index " << handle.index @@ -403,16 +411,16 @@ void TrustStoreManager::CleanupClient(data_manager::ClientId client_id) // Slot change notification // --------------------------------------------------------------------------- -void TrustStoreManager::NotifySlotChanged(TrustStoreId id, CertSlotHandle changed_slot) +void TrustStoreManager::NotifySlotChanged(TrustStoreHandle handle, CertSlotHandle changed_slot) { std::lock_guard lock(m_mutex); - if (id >= m_stores.size()) + if (!handle.IsValid() || handle.index >= m_stores.size()) return; // Evict the changed slot from the shared cache so LoadOrGetCached does fresh I/O. m_slot_cert_cache.erase(changed_slot.index); // Tell the handler to drop just this slot and mark itself for reload. - auto* h = static_cast(m_stores[id].handler.get()); - if (h) + auto* h = static_cast(m_stores[handle.index].handler.get()); + if (h != nullptr) h->InvalidateSlot(changed_slot); } @@ -431,7 +439,7 @@ std::optional TrustStoreManager::ResolveMembe if (!cfg) return std::nullopt; auto* handler = GetOrCreateHandler(*slot); - if (!handler) + if (handler == nullptr) return std::nullopt; return ResolvedMember{*slot, handler, *cfg}; } @@ -445,19 +453,20 @@ score::crypto::Expected TrustStoreMan if (!cfg) return score::crypto::make_unexpected(Error::kInvalidResourceId); auto* handler = GetOrCreateHandler(slot); - if (!handler) + if (handler == nullptr) return score::crypto::make_unexpected(Error::kInvalidResourceId); return ResolvedBackend{*cfg, handler}; } score::crypto::Expected TrustStoreManager::AddMember( - TrustStoreId id, + TrustStoreHandle handle, CertObject::Sptr cert, data_manager::ClientId client_id, score::crypto::span crl_bytes, score::crypto::FormatType crl_format) { std::lock_guard lock(m_mutex); + const TrustStoreId id = handle.index; if (id >= m_stores.size() || !cert) return score::crypto::make_unexpected(Error::kInvalidArgument); if (!AccessPolicyEnforcer::CheckTrustStoreWritePermission(m_stores[id].config, client_id).has_value()) @@ -496,7 +505,8 @@ score::crypto::Expected TrustStoreManager::AddMember( // Re-enable in case it was disabled and refresh the anchor cache. m_member_states[id][resolved->slot.index].enabled = true; m_slot_cert_cache[resolved->slot.index] = cert; - static_cast(m_stores[id].handler.get())->NotifySlotUpdate(resolved->slot, cert); + static_cast(m_stores[id].handler.get()) + ->NotifySlotUpdate(resolved->slot, std::move(cert)); if (const auto persisted = PersistState(id); !persisted) return score::crypto::make_unexpected(persisted.error()); return std::monostate{}; @@ -533,8 +543,8 @@ score::crypto::Expected TrustStoreManager::AddMember( // Caller may retry via ImportCrlForMember. if (has_crl) { - const auto stored = resolved->handler->StoreCrl(*resolved->cfg, crl_bytes, crl_format); - if (stored) + const auto crl_stored = resolved->handler->StoreCrl(*resolved->cfg, crl_bytes, crl_format); + if (crl_stored) { CrlEntry entry{std::vector(crl_bytes.begin(), crl_bytes.end()), crl_format}; static_cast(m_stores[id].handler.get()) @@ -544,7 +554,7 @@ score::crypto::Expected TrustStoreManager::AddMember( m_member_states[id][resolved->slot.index].enabled = true; m_slot_cert_cache[resolved->slot.index] = cert; - static_cast(m_stores[id].handler.get())->NotifySlotUpdate(resolved->slot, cert); + static_cast(m_stores[id].handler.get())->NotifySlotUpdate(resolved->slot, std::move(cert)); if (const auto persisted = PersistState(id); !persisted) return score::crypto::make_unexpected(persisted.error()); return std::monostate{}; @@ -553,13 +563,14 @@ score::crypto::Expected TrustStoreManager::AddMember( } score::crypto::Expected TrustStoreManager::ImportCrlForMember( - TrustStoreId id, + TrustStoreHandle handle, CertSlotHandle slot, score::crypto::span crl_data, score::crypto::FormatType format, std::int64_t next_update_epoch_s) { std::lock_guard lock(m_mutex); + const TrustStoreId id = handle.index; if (id >= m_stores.size() || !slot.IsValid()) return score::crypto::make_unexpected(Error::kInvalidResourceId); if (crl_data.empty()) @@ -582,11 +593,12 @@ score::crypto::Expected TrustStoreManager::ImportCrlForMe return score::crypto::make_unexpected(Error::kInvalidResourceId); } -score::crypto::Expected TrustStoreManager::RemoveMember(TrustStoreId id, +score::crypto::Expected TrustStoreManager::RemoveMember(TrustStoreHandle handle, const std::vector& fingerprint, data_manager::ClientId client_id) { std::lock_guard lock(m_mutex); + const TrustStoreId id = handle.index; if (id >= m_stores.size()) return score::crypto::make_unexpected(Error::kInvalidResourceId); if (!AccessPolicyEnforcer::CheckTrustStoreWritePermission(m_stores[id].config, client_id).has_value()) @@ -618,11 +630,12 @@ score::crypto::Expected TrustStoreManager::RemoveMember(T return score::crypto::make_unexpected(Error::kInvalidArgument); } -score::crypto::Expected TrustStoreManager::EnableMember(TrustStoreId id, +score::crypto::Expected TrustStoreManager::EnableMember(TrustStoreHandle handle, CertSlotHandle slot, data_manager::ClientId client_id) { std::lock_guard lock(m_mutex); + const TrustStoreId id = handle.index; if (id >= m_stores.size() || !slot.IsValid()) return score::crypto::make_unexpected(Error::kInvalidResourceId); if (!AccessPolicyEnforcer::CheckTrustStoreWritePermission(m_stores[id].config, client_id).has_value()) @@ -641,11 +654,12 @@ score::crypto::Expected TrustStoreManager::EnableMember(T return std::monostate{}; } -score::crypto::Expected TrustStoreManager::DisableMember(TrustStoreId id, +score::crypto::Expected TrustStoreManager::DisableMember(TrustStoreHandle handle, CertSlotHandle slot, data_manager::ClientId client_id) { std::lock_guard lock(m_mutex); + const TrustStoreId id = handle.index; if (id >= m_stores.size() || !slot.IsValid()) return score::crypto::make_unexpected(Error::kInvalidResourceId); if (!AccessPolicyEnforcer::CheckTrustStoreWritePermission(m_stores[id].config, client_id).has_value()) @@ -662,10 +676,13 @@ score::crypto::Expected TrustStoreManager::DisableMember( return std::monostate{}; } -score::crypto::Expected -TrustStoreManager::AcknowledgeMemberUpdate(TrustStoreId id, CertSlotHandle slot, data_manager::ClientId client_id) +score::crypto::Expected TrustStoreManager::AcknowledgeMemberUpdate( + TrustStoreHandle handle, + CertSlotHandle slot, + data_manager::ClientId client_id) { std::lock_guard lock(m_mutex); + const TrustStoreId id = handle.index; if (id >= m_stores.size() || !slot.IsValid()) return score::crypto::make_unexpected(Error::kInvalidResourceId); if (!AccessPolicyEnforcer::CheckTrustStoreWritePermission(m_stores[id].config, client_id).has_value()) @@ -688,19 +705,20 @@ TrustStoreManager::AcknowledgeMemberUpdate(TrustStoreId id, CertSlotHandle slot, return std::monostate{}; } -const TrustStoreConfig* TrustStoreManager::GetStoreConfig(TrustStoreId id) const +const TrustStoreConfig* TrustStoreManager::GetStoreConfig(TrustStoreHandle handle) const { std::lock_guard lock(m_mutex); - return id < m_stores.size() ? &m_stores[id].config : nullptr; + return handle.index < m_stores.size() ? &m_stores[handle.index].config : nullptr; } // --------------------------------------------------------------------------- // Member snapshot (for ITrustStoreObject read-only view) // --------------------------------------------------------------------------- -std::vector TrustStoreManager::GetMembersSnapshot(TrustStoreId id) +std::vector TrustStoreManager::GetMembersSnapshot(TrustStoreHandle handle) { std::lock_guard lock(m_mutex); + const TrustStoreId id = handle.index; if (id >= m_stores.size() || !m_slot_registry) return {}; diff --git a/score/crypto/src/daemon/cert_management/truststore/trust_store_manager.hpp b/score/crypto/src/daemon/cert_management/truststore/trust_store_manager.hpp index bb0cf4210..d32cd6ec8 100644 --- a/score/crypto/src/daemon/cert_management/truststore/trust_store_manager.hpp +++ b/score/crypto/src/daemon/cert_management/truststore/trust_store_manager.hpp @@ -95,8 +95,8 @@ class TrustStoreManager // Store access // ----------------------------------------------------------------------- - [[nodiscard]] ITrustStoreHandler::Sptr GetStore(TrustStoreId id) const; - [[nodiscard]] TrustStoreId ResolveByName(const std::string& name) const; + [[nodiscard]] ITrustStoreHandler::Sptr GetStore(TrustStoreHandle handle) const; + [[nodiscard]] TrustStoreHandle ResolveByName(const std::string& name) const; void RegisterAppResource(uint32_t uid, const std::string& app_resource_id, const std::string& store_name); [[nodiscard]] score::crypto::Expected @@ -109,7 +109,7 @@ class TrustStoreManager /// @brief Return the set of trust store IDs that contain a given cert slot. /// /// Used by CertManagementService::SaveCertificate to fan out NotifyUpdate(). - [[nodiscard]] std::vector GetMembershipsForSlot(CertSlotHandle slot_handle) const; + [[nodiscard]] std::vector GetMembershipsForSlot(CertSlotHandle slot_handle) const; // ----------------------------------------------------------------------- // Runtime notifications and mutations @@ -143,7 +143,7 @@ class TrustStoreManager /// Called by CertManagementService::NotifySlotCertChanged() after StoreCertificate. /// Unchanged member slots retain their cached strong-refs; only the changed slot /// pays a reload cost on the next GetAnchors() call. - void NotifySlotChanged(TrustStoreId id, CertSlotHandle changed_slot); + void NotifySlotChanged(TrustStoreHandle handle, CertSlotHandle changed_slot); /// @brief Add a certificate to a trust store's runtime anchor set. /// @@ -164,7 +164,7 @@ class TrustStoreManager /// Write access to the trust store must be checked by CertManagementService /// before calling this method. [[nodiscard]] score::crypto::Expected AddMember( - TrustStoreId id, + TrustStoreHandle handle, CertObject::Sptr cert, data_manager::ClientId client_id, score::crypto::span crl_bytes = {}, @@ -179,7 +179,7 @@ class TrustStoreManager /// Returns kInvalidResourceId if @p slot is not a member of the trust store. /// Returns kUnsupportedOperation if the slot is not kExclusiveMutable. [[nodiscard]] score::crypto::Expected - ImportCrlForMember(TrustStoreId id, + ImportCrlForMember(TrustStoreHandle handle, CertSlotHandle slot, score::crypto::span crl_data, score::crypto::FormatType format, @@ -190,14 +190,14 @@ class TrustStoreManager /// Persists the change to the trust store descriptor and calls NotifyUpdate(). /// Write access must be checked before calling. [[nodiscard]] score::crypto::Expected - RemoveMember(TrustStoreId id, const std::vector& fingerprint, data_manager::ClientId client_id); + RemoveMember(TrustStoreHandle handle, const std::vector& fingerprint, data_manager::ClientId client_id); [[nodiscard]] score::crypto::Expected - EnableMember(TrustStoreId id, CertSlotHandle slot, data_manager::ClientId client_id); + EnableMember(TrustStoreHandle handle, CertSlotHandle slot, data_manager::ClientId client_id); [[nodiscard]] score::crypto::Expected - DisableMember(TrustStoreId id, CertSlotHandle slot, data_manager::ClientId client_id); + DisableMember(TrustStoreHandle handle, CertSlotHandle slot, data_manager::ClientId client_id); [[nodiscard]] score::crypto::Expected - AcknowledgeMemberUpdate(TrustStoreId id, CertSlotHandle slot, data_manager::ClientId client_id); + AcknowledgeMemberUpdate(TrustStoreHandle handle, CertSlotHandle slot, data_manager::ClientId client_id); // ----------------------------------------------------------------------- // Snapshot for read-only typed object access (ITrustStoreObject) @@ -223,14 +223,14 @@ class TrustStoreManager /// /// Certs are loaded via the shared cache where possible; fresh loads are taken for /// uncached slots. Non-const because it may populate handler and cert caches. - [[nodiscard]] std::vector GetMembersSnapshot(TrustStoreId id); + [[nodiscard]] std::vector GetMembersSnapshot(TrustStoreHandle handle); // ----------------------------------------------------------------------- // Configuration access // ----------------------------------------------------------------------- - /// @brief Return the TrustStoreConfig for a given store ID. - [[nodiscard]] const TrustStoreConfig* GetStoreConfig(TrustStoreId id) const; + /// @brief Return the TrustStoreConfig for a given store handle. + [[nodiscard]] const TrustStoreConfig* GetStoreConfig(TrustStoreHandle handle) const; private: struct MemberState @@ -243,9 +243,9 @@ class TrustStoreManager /// All pointers are non-null. Valid only while m_mutex is held. struct ResolvedMember { - CertSlotHandle slot; - ICertSlotHandler* handler; - const CertSlotConfig* cfg; + CertSlotHandle slot{}; + ICertSlotHandler* handler{nullptr}; + const CertSlotConfig* cfg{nullptr}; }; /// Returns nullopt if the member's slot name cannot be resolved or has no registered handler. [[nodiscard]] std::optional ResolveMember(const TrustStoreMemberConfig& member); diff --git a/score/crypto/src/daemon/common/storage/kv/kv_deployment_writer.cpp b/score/crypto/src/daemon/common/storage/kv/kv_deployment_writer.cpp index 38517a959..52e59a81b 100644 --- a/score/crypto/src/daemon/common/storage/kv/kv_deployment_writer.cpp +++ b/score/crypto/src/daemon/common/storage/kv/kv_deployment_writer.cpp @@ -35,7 +35,9 @@ score::crypto::Expected{reinterpret_cast(content.data()), content.size()}; return WriteFile(path, bytes); diff --git a/score/crypto/src/daemon/provider/score_provider/openssl/cert_management/openssl_cert_parser.cpp b/score/crypto/src/daemon/provider/score_provider/openssl/cert_management/openssl_cert_parser.cpp index e6b6a4661..52abf0eab 100644 --- a/score/crypto/src/daemon/provider/score_provider/openssl/cert_management/openssl_cert_parser.cpp +++ b/score/crypto/src/daemon/provider/score_provider/openssl/cert_management/openssl_cert_parser.cpp @@ -288,11 +288,8 @@ std::int64_t Asn1TimeToEpoch(const ASN1_TIME* asn1) struct tm t{}; if (ASN1_TIME_to_tm(asn1, &t) != 1) return 0; -#ifdef _WIN32 - return static_cast(_mkgmtime(&t)); -#else + return static_cast(timegm(&t)); -#endif } } // namespace diff --git a/score/crypto/src/daemon/provider/tests/cert_management/BUILD b/score/crypto/src/daemon/provider/tests/cert_management/BUILD index 51dccc98f..fa6ecbbc5 100644 --- a/score/crypto/src/daemon/provider/tests/cert_management/BUILD +++ b/score/crypto/src/daemon/provider/tests/cert_management/BUILD @@ -27,6 +27,7 @@ cc_test( "//score/crypto/src/daemon/cert_management:cert_management_headers", "//score/crypto/src/daemon/provider/score_provider/openssl:openssl_cert_management_headers", "//score/crypto/src/daemon/provider/score_provider/openssl:openssl_cert_parser_library", + "//third_party/openssl:openssl_shared", "@googletest//:gtest_main", ], ) diff --git a/score/crypto/src/daemon/provider/tests/cert_management/test_openssl_cert_parser.cpp b/score/crypto/src/daemon/provider/tests/cert_management/test_openssl_cert_parser.cpp index cb1f2cc3d..9b4469bce 100644 --- a/score/crypto/src/daemon/provider/tests/cert_management/test_openssl_cert_parser.cpp +++ b/score/crypto/src/daemon/provider/tests/cert_management/test_openssl_cert_parser.cpp @@ -15,10 +15,19 @@ #include +#include +#include +#include +#include +#include +#include + #include #include +#include #include #include +#include #include #include #include @@ -26,12 +35,17 @@ namespace { using score::crypto::FormatType; +using score::crypto::daemon::common::DaemonErrorCode; using score::crypto::daemon::common::ProviderId; using score::crypto::daemon::provider::score_provider::openssl::OpenSslCertParser; +// --------------------------------------------------------------------------- +// ParseCertificate / ParseCertificates tests +// --------------------------------------------------------------------------- + std::string ReadCertificateVector() { - std::ifstream input{"score/tests/test_vectors/certificate/certificate.pem"}; + std::ifstream input{"score/tests/test_vectors/certificate/basic/certificate.pem"}; return {std::istreambuf_iterator{input}, std::istreambuf_iterator{}}; } @@ -82,4 +96,384 @@ TEST(OpenSslCertParserTest, RejectsMalformedCertificate) EXPECT_FALSE(result.has_value()); } + +// --------------------------------------------------------------------------- +// ValidateCrl tests +// +// The fixture generates two independent self-signed CA + CRL bundles in-memory +// using the OpenSSL C API so that tests are fully hermetic — no committed +// private keys are required. +// +// CaA — positive tests: valid CRL signed by CaA, CaA cert as issuer. +// CaB — issuer-mismatch tests: CaA CRL presented with CaB cert as issuer. +// +// Test coverage: +// - DER CRL + DER issuer cert → success, epoch > 0 +// - PEM CRL + PEM issuer cert → success, epoch > 0 +// - DER CRL + PEM issuer cert (cross-format) → success +// - PEM CRL + DER issuer cert (cross-format) → success +// - nextUpdate epoch is strictly in the future → epoch > now +// - DER and PEM paths return identical epoch → consistency +// - Issuer DN mismatch (CaA CRL + CaB cert) → kInvalidArgument +// - Tampered CRL signature (byte flip in sig) → kOperationFailed +// - Malformed CRL bytes → kCertificateParsingFailed +// - Malformed issuer cert bytes → kInvalidArgument +// - Null CRL data pointer → kInvalidArgument +// - Zero CRL size → kInvalidArgument +// - Null issuer cert data pointer → kInvalidArgument +// - Zero issuer cert size → kInvalidArgument +// --------------------------------------------------------------------------- + +// RAII wrappers for OpenSSL objects used exclusively in this test file. +struct EvpPkeyDeleter +{ + void operator()(EVP_PKEY* p) const noexcept + { + EVP_PKEY_free(p); + } +}; +struct X509Deleter +{ + void operator()(X509* p) const noexcept + { + X509_free(p); + } +}; +struct CrlDeleter +{ + void operator()(X509_CRL* p) const noexcept + { + X509_CRL_free(p); + } +}; +struct BioDeleter +{ + void operator()(BIO* p) const noexcept + { + BIO_free(p); + } +}; +struct Asn1TimeDeleter +{ + void operator()(ASN1_TIME* p) const noexcept + { + ASN1_TIME_free(p); + } +}; + +using EvpPkeyPtr = std::unique_ptr; +using X509Ptr = std::unique_ptr; +using CrlPtr = std::unique_ptr; +using BioPtr = std::unique_ptr; +using Asn1TimePtr = std::unique_ptr; + +// Generate a 2048-bit RSA key pair. +EvpPkeyPtr GenerateRsaKey() +{ + EVP_PKEY_CTX* ctx = EVP_PKEY_CTX_new_id(EVP_PKEY_RSA, nullptr); + if (!ctx) + return {}; + EVP_PKEY_keygen_init(ctx); + EVP_PKEY_CTX_set_rsa_keygen_bits(ctx, 2048); + EVP_PKEY* raw = nullptr; + EVP_PKEY_keygen(ctx, &raw); + EVP_PKEY_CTX_free(ctx); + return EvpPkeyPtr{raw}; +} + +// Build a minimal self-signed CA certificate. Subject/issuer = CN=,O=Eclipse. +X509Ptr MakeSelfSignedCert(EVP_PKEY* pkey, const char* cn) +{ + X509Ptr cert{X509_new()}; + if (!cert) + return {}; + X509_set_version(cert.get(), 2); // version 3 + ASN1_INTEGER_set(X509_get_serialNumber(cert.get()), 1); + X509_gmtime_adj(X509_getm_notBefore(cert.get()), 0); + X509_gmtime_adj(X509_getm_notAfter(cert.get()), 365L * 24L * 3600L); + X509_set_pubkey(cert.get(), pkey); + X509_NAME* name = X509_get_subject_name(cert.get()); + X509_NAME_add_entry_by_txt(name, "CN", MBSTRING_ASC, reinterpret_cast(cn), -1, -1, 0); + X509_NAME_add_entry_by_txt(name, "O", MBSTRING_ASC, reinterpret_cast("Eclipse"), -1, -1, 0); + X509_set_issuer_name(cert.get(), name); + X509_sign(cert.get(), pkey, EVP_sha256()); + return cert; +} + +// Build and sign an empty CRL (no revoked entries, nextUpdate = now + 365 days). +CrlPtr MakeSignedCrl(X509* issuer, EVP_PKEY* issuer_key) +{ + CrlPtr crl{X509_CRL_new()}; + if (!crl) + return {}; + X509_CRL_set_version(crl.get(), 1); // 1 = CRL v2 in OpenSSL's enum + X509_CRL_set_issuer_name(crl.get(), X509_get_subject_name(issuer)); + { + Asn1TimePtr last{ASN1_TIME_new()}; + X509_gmtime_adj(last.get(), 0); +#if OPENSSL_VERSION_NUMBER >= 0x30000000L + X509_CRL_set1_lastUpdate(crl.get(), last.get()); +#else + X509_CRL_set_lastUpdate(crl.get(), last.get()); +#endif + } + { + Asn1TimePtr next{ASN1_TIME_new()}; + X509_gmtime_adj(next.get(), 365L * 24L * 3600L); +#if OPENSSL_VERSION_NUMBER >= 0x30000000L + X509_CRL_set1_nextUpdate(crl.get(), next.get()); +#else + X509_CRL_set_nextUpdate(crl.get(), next.get()); +#endif + } + X509_CRL_sign(crl.get(), issuer_key, EVP_sha256()); + return crl; +} + +// Serialise an X509 certificate to raw DER bytes. +std::vector X509ToDer(X509* cert) +{ + uint8_t* buf = nullptr; + const int len = i2d_X509(cert, &buf); + if (len <= 0) + return {}; + std::vector out(buf, buf + len); + OPENSSL_free(buf); + return out; +} + +// Serialise an X509 certificate to PEM bytes. +std::vector X509ToPem(X509* cert) +{ + BioPtr bio{BIO_new(BIO_s_mem())}; + PEM_write_bio_X509(bio.get(), cert); + char* data = nullptr; + const long len = BIO_get_mem_data(bio.get(), &data); + return {reinterpret_cast(data), reinterpret_cast(data) + len}; +} + +// Serialise an X509_CRL to raw DER bytes. +std::vector CrlToDer(X509_CRL* crl) +{ + uint8_t* buf = nullptr; + const int len = i2d_X509_CRL(crl, &buf); + if (len <= 0) + return {}; + std::vector out(buf, buf + len); + OPENSSL_free(buf); + return out; +} + +// Serialise an X509_CRL to PEM bytes. +std::vector CrlToPem(X509_CRL* crl) +{ + BioPtr bio{BIO_new(BIO_s_mem())}; + PEM_write_bio_X509_CRL(bio.get(), crl); + char* data = nullptr; + const long len = BIO_get_mem_data(bio.get(), &data); + return {reinterpret_cast(data), reinterpret_cast(data) + len}; +} + +// --------------------------------------------------------------------------- +// Fixture — generates two CA + CRL bundles once for the whole test suite. +// --------------------------------------------------------------------------- + +struct CaBundle +{ + std::vector cert_der; + std::vector cert_pem; + std::vector crl_der; + std::vector crl_pem; +}; + +class ValidateCrlTest : public ::testing::Test +{ + protected: + static void SetUpTestSuite() + { + // CA A — cert + CRL both generated; used for positive tests. + { + auto pkey = GenerateRsaKey(); + ASSERT_TRUE(pkey) << "RSA key generation for CA-A failed"; + auto cert = MakeSelfSignedCert(pkey.get(), "Test-CA-A"); + ASSERT_TRUE(cert) << "Self-signed cert for CA-A failed"; + auto crl = MakeSignedCrl(cert.get(), pkey.get()); + ASSERT_TRUE(crl) << "CRL generation for CA-A failed"; + s_ca_a.cert_der = X509ToDer(cert.get()); + s_ca_a.cert_pem = X509ToPem(cert.get()); + s_ca_a.crl_der = CrlToDer(crl.get()); + s_ca_a.crl_pem = CrlToPem(crl.get()); + } + // CA B — cert only; its cert is used as the wrong-issuer argument. + { + auto pkey = GenerateRsaKey(); + ASSERT_TRUE(pkey) << "RSA key generation for CA-B failed"; + auto cert = MakeSelfSignedCert(pkey.get(), "Test-CA-B"); + ASSERT_TRUE(cert) << "Self-signed cert for CA-B failed"; + s_ca_b.cert_der = X509ToDer(cert.get()); + s_ca_b.cert_pem = X509ToPem(cert.get()); + } + } + + // Convenience wrapper — calls ValidateCrl with vector data. + static score::crypto::Expected Validate(const std::vector& crl, + FormatType crl_fmt, + const std::vector& issuer, + FormatType issuer_fmt) + { + OpenSslCertParser parser{ProviderId{1U}}; + return parser.ValidateCrl(crl.data(), crl.size(), crl_fmt, issuer.data(), issuer.size(), issuer_fmt); + } + + static CaBundle s_ca_a; + static CaBundle s_ca_b; +}; + +CaBundle ValidateCrlTest::s_ca_a; +CaBundle ValidateCrlTest::s_ca_b; + +// --------------------------------------------------------------------------- +// Happy-path tests +// --------------------------------------------------------------------------- + +TEST_F(ValidateCrlTest, ValidDerCrl_DerIssuer_ReturnsNextUpdateEpoch) +{ + const auto result = Validate(s_ca_a.crl_der, FormatType::kDer, s_ca_a.cert_der, FormatType::kDer); + ASSERT_TRUE(result.has_value()); + EXPECT_GT(*result, 0); +} + +TEST_F(ValidateCrlTest, ValidPemCrl_PemIssuer_ReturnsNextUpdateEpoch) +{ + const auto result = Validate(s_ca_a.crl_pem, FormatType::kPem, s_ca_a.cert_pem, FormatType::kPem); + ASSERT_TRUE(result.has_value()); + EXPECT_GT(*result, 0); +} + +TEST_F(ValidateCrlTest, ValidDerCrl_PemIssuer_CrossFormat_ReturnsNextUpdateEpoch) +{ + const auto result = Validate(s_ca_a.crl_der, FormatType::kDer, s_ca_a.cert_pem, FormatType::kPem); + ASSERT_TRUE(result.has_value()); + EXPECT_GT(*result, 0); +} + +TEST_F(ValidateCrlTest, ValidPemCrl_DerIssuer_CrossFormat_ReturnsNextUpdateEpoch) +{ + const auto result = Validate(s_ca_a.crl_pem, FormatType::kPem, s_ca_a.cert_der, FormatType::kDer); + ASSERT_TRUE(result.has_value()); + EXPECT_GT(*result, 0); +} + +// nextUpdate was set to now + 365 days, so the epoch must be strictly in the future. +TEST_F(ValidateCrlTest, ValidCrl_NextUpdateIsInFuture) +{ + const auto result = Validate(s_ca_a.crl_der, FormatType::kDer, s_ca_a.cert_der, FormatType::kDer); + ASSERT_TRUE(result.has_value()); + const std::int64_t now_epoch = static_cast(std::time(nullptr)); + EXPECT_GT(*result, now_epoch); +} + +// DER and PEM paths must decode identically — same nextUpdate epoch. +TEST_F(ValidateCrlTest, ValidCrl_DerAndPemReturnSameEpoch) +{ + const auto der_result = Validate(s_ca_a.crl_der, FormatType::kDer, s_ca_a.cert_der, FormatType::kDer); + const auto pem_result = Validate(s_ca_a.crl_pem, FormatType::kPem, s_ca_a.cert_pem, FormatType::kPem); + ASSERT_TRUE(der_result.has_value()); + ASSERT_TRUE(pem_result.has_value()); + EXPECT_EQ(*der_result, *pem_result); +} + +// --------------------------------------------------------------------------- +// Negative tests — issuer mismatch and signature failure +// --------------------------------------------------------------------------- + +// CRL was signed by CA-A; CA-B cert has a different subject DN (Test-CA-B vs +// Test-CA-A). The issuer DN check fails before signature verification. +TEST_F(ValidateCrlTest, IssuerDnMismatch_ReturnsInvalidArgument) +{ + const auto result = Validate(s_ca_a.crl_der, FormatType::kDer, s_ca_b.cert_der, FormatType::kDer); + ASSERT_FALSE(result.has_value()); + EXPECT_EQ(result.error(), DaemonErrorCode::kInvalidArgument); +} + +// Flip a byte 16 bytes from the end of the DER — deep inside the 256-byte RSA +// signature — so the issuer DN check passes but X509_CRL_verify() fails. +TEST_F(ValidateCrlTest, TamperedCrlSignature_ReturnsOperationFailed) +{ + ASSERT_GE(s_ca_a.crl_der.size(), 32U); + auto tampered = s_ca_a.crl_der; + tampered[tampered.size() - 16U] ^= 0xFFU; + const auto result = Validate(tampered, FormatType::kDer, s_ca_a.cert_der, FormatType::kDer); + ASSERT_FALSE(result.has_value()); + EXPECT_EQ(result.error(), DaemonErrorCode::kOperationFailed); +} + +// --------------------------------------------------------------------------- +// Negative tests — parse failures +// --------------------------------------------------------------------------- + +TEST_F(ValidateCrlTest, MalformedCrlBytes_ReturnsCertificateParsingFailed) +{ + const std::vector garbage{0xDE, 0xAD, 0xBE, 0xEF, 0x00, 0x01}; + const auto result = Validate(garbage, FormatType::kDer, s_ca_a.cert_der, FormatType::kDer); + ASSERT_FALSE(result.has_value()); + EXPECT_EQ(result.error(), DaemonErrorCode::kCertificateParsingFailed); +} + +TEST_F(ValidateCrlTest, MalformedIssuerCertBytes_ReturnsInvalidArgument) +{ + const std::vector garbage{0xDE, 0xAD, 0xBE, 0xEF, 0x00, 0x01}; + const auto result = Validate(s_ca_a.crl_der, FormatType::kDer, garbage, FormatType::kDer); + ASSERT_FALSE(result.has_value()); + EXPECT_EQ(result.error(), DaemonErrorCode::kInvalidArgument); +} + +// --------------------------------------------------------------------------- +// Negative tests — null / zero-size inputs +// --------------------------------------------------------------------------- + +TEST_F(ValidateCrlTest, NullCrlData_ReturnsInvalidArgument) +{ + OpenSslCertParser parser{ProviderId{1U}}; + const auto result = parser.ValidateCrl(nullptr, + s_ca_a.crl_der.size(), + FormatType::kDer, + s_ca_a.cert_der.data(), + s_ca_a.cert_der.size(), + FormatType::kDer); + ASSERT_FALSE(result.has_value()); + EXPECT_EQ(result.error(), DaemonErrorCode::kInvalidArgument); +} + +TEST_F(ValidateCrlTest, ZeroCrlSize_ReturnsInvalidArgument) +{ + OpenSslCertParser parser{ProviderId{1U}}; + const auto result = parser.ValidateCrl( + s_ca_a.crl_der.data(), 0U, FormatType::kDer, s_ca_a.cert_der.data(), s_ca_a.cert_der.size(), FormatType::kDer); + ASSERT_FALSE(result.has_value()); + EXPECT_EQ(result.error(), DaemonErrorCode::kInvalidArgument); +} + +TEST_F(ValidateCrlTest, NullIssuerCertData_ReturnsInvalidArgument) +{ + OpenSslCertParser parser{ProviderId{1U}}; + const auto result = parser.ValidateCrl(s_ca_a.crl_der.data(), + s_ca_a.crl_der.size(), + FormatType::kDer, + nullptr, + s_ca_a.cert_der.size(), + FormatType::kDer); + ASSERT_FALSE(result.has_value()); + EXPECT_EQ(result.error(), DaemonErrorCode::kInvalidArgument); +} + +TEST_F(ValidateCrlTest, ZeroIssuerCertSize_ReturnsInvalidArgument) +{ + OpenSslCertParser parser{ProviderId{1U}}; + const auto result = parser.ValidateCrl( + s_ca_a.crl_der.data(), s_ca_a.crl_der.size(), FormatType::kDer, s_ca_a.cert_der.data(), 0U, FormatType::kDer); + ASSERT_FALSE(result.has_value()); + EXPECT_EQ(result.error(), DaemonErrorCode::kInvalidArgument); +} + } // namespace diff --git a/score/tests/test_vectors/certificate/BUILD b/score/tests/test_vectors/certificate/BUILD index d389c9e59..19a37c152 100644 --- a/score/tests/test_vectors/certificate/BUILD +++ b/score/tests/test_vectors/certificate/BUILD @@ -11,8 +11,69 @@ # SPDX-License-Identifier: Apache-2.0 # ******************************************************************************* +# --------------------------------------------------------------------------- +# Per-folder filegroup targets. +# +# Test targets should depend on the smallest set they actually use: +# +# :basic — primary RSA-2048 PKI (cert management, trust store, +# cert parser, integration tests) +# :algorithm_variety — one CA per key algorithm (slot handler variety tests) +# :pki_chain — three-level PKI for chain verification and OCSP tests +# +# The aggregating :certificate_test_vectors target is kept for backward +# compatibility; new test targets should prefer the specific sub-targets. +# --------------------------------------------------------------------------- + +filegroup( + name = "basic", + srcs = glob( + [ + "basic/**/*.pem", + "basic/**/*.der", + "basic/**/*.kv", + "basic/**/*.json", + ], + allow_empty = True, + ), + visibility = ["//visibility:public"], +) + +filegroup( + name = "algorithm_variety", + srcs = glob( + [ + "algorithm_variety/**/*.pem", + "algorithm_variety/**/*.kv", + "algorithm_variety/**/*.json", + ], + allow_empty = True, + ), + visibility = ["//visibility:public"], +) + +filegroup( + name = "pki_chain", + srcs = glob( + [ + "pki_chain/**/*.pem", + "pki_chain/**/*.der", + "pki_chain/**/*.kv", + "pki_chain/**/*.json", + "pki_chain/**/*.conf", + ], + allow_empty = True, + ), + visibility = ["//visibility:public"], +) + +# Aggregating target — backward compatible. filegroup( name = "certificate_test_vectors", - srcs = glob(["*.json", "*.kv", "*.pem"]), + srcs = [ + ":basic", + ":algorithm_variety", + ":pki_chain", + ], visibility = ["//visibility:public"], ) diff --git a/score/tests/test_vectors/certificate/README.md b/score/tests/test_vectors/certificate/README.md index 6d384f23e..de83e89a1 100644 --- a/score/tests/test_vectors/certificate/README.md +++ b/score/tests/test_vectors/certificate/README.md @@ -13,21 +13,72 @@ # Certificate Test Vectors -`certificate_manifest.json` is the inventory for certificate-management test -certificates. It records each certificate's purpose, subject, key algorithm, -private-key location, validity policy, and the metadata snapshot of the -checked-in public certificate. +This directory contains PKI test vectors for the certificate-management daemon +component. Certificates are organized into purpose-driven subfolders, each with +its own `manifest.json` inventory file. -Private keys are generated locally under `private/` and are ignored by Git. -Never commit them. The checked-in certificates use a long validity period -because their private keys are intentionally not stored in the public repository. +``` +certificate/ +├── generate_certificates.py # Shared generation script +├── basic/ # Primary RSA-2048 PKI — used by most tests +│ ├── manifest.json +│ ├── certificate.pem # Root CA (CN=cert-management-test) +│ ├── certificate_slot.kv +│ ├── certificate_updated.pem # Rotated CA (CN=cert-management-updated) +│ ├── certificate_updated_slot.kv +│ ├── certificate_leaf.pem # End-entity, signed by certificate CA +│ ├── certificate_leaf.der +│ ├── certificate_leaf.chain.pem +│ ├── certificate_leaf_slot.kv +│ ├── certificate.crl.pem # CRL issued by certificate, revokes leaf +│ ├── certificate.crl.der +│ ├── trust_store.kv # Empty trust-store state fixture +│ └── private/ # NOT committed — local keys only +│ +├── algorithm_variety/ # One self-signed CA per key algorithm +│ ├── manifest.json +│ ├── rsa_3072.pem, rsa_3072_slot.kv +│ ├── rsa_4096.pem, rsa_4096_slot.kv +│ ├── ec_p256.pem, ec_p256_slot.kv +│ ├── ec_p384.pem, ec_p384_slot.kv +│ ├── ec_p521.pem, ec_p521_slot.kv +│ ├── ed25519.pem, ed25519_slot.kv +│ ├── ed448.pem, ed448_slot.kv +│ ├── ml_dsa_44.pem, ml_dsa_44_slot.kv +│ ├── ml_dsa_65.pem, ml_dsa_65_slot.kv +│ ├── ml_dsa_87.pem, ml_dsa_87_slot.kv +│ └── private/ +│ +└── pki_chain/ # Future: root → intermediate → leaf + OCSP + ├── manifest.json + ├── ocsp_signer.ext.conf # Extension config for the OCSP signing cert + └── private/ +``` + +Private keys live under each folder's `private/` directory and are ignored by +Git. Never commit them. All PEM files carry a human-readable header (produced by +`openssl x509 -text` or `openssl crl -text`) before the `-----BEGIN ...-----` +block; this header is ignored by all conforming PEM parsers. + +--- ## Certificate inventory +### `basic/` + +| Name | Algorithm | Role | Purpose | +|------|-----------|------|---------| +| `certificate` | RSA-2048 | Root CA (self-signed) | Primary CA for slot and trust-store tests | +| `certificate_updated` | RSA-2048 | Root CA (self-signed) | Replacement CA for slot-rotation and anchor-invalidation tests | +| `certificate_leaf` | RSA-2048 | End-entity, issued by `certificate` | Signed leaf; subject of the CRL; demonstrates chain, DER, and revocation | + +Associated artifacts: `certificate.crl.pem/.der` (CRL revoking `certificate_leaf`), +`certificate_leaf.chain.pem` (leaf + root CA bundle). + +### `algorithm_variety/` + | Name | Algorithm | Purpose | |------|-----------|---------| -| `certificate` | RSA-2048 | Primary CA cert for file-backed slot and trust-store tests | -| `certificate_updated` | RSA-2048 | Replacement CA cert for slot-update and anchor-invalidation tests | | `rsa_3072` | RSA-3072 | RSA key-size variety | | `rsa_4096` | RSA-4096 | RSA key-size variety | | `ec_p256` | EC P-256 | ECDSA elliptic-curve algorithm tests | @@ -35,18 +86,21 @@ because their private keys are intentionally not stored in the public repository | `ec_p521` | EC P-521 | ECDSA elliptic-curve algorithm tests | | `ed25519` | Ed25519 | Edwards-curve algorithm tests | | `ed448` | Ed448 | Edwards-curve algorithm tests | -| `ml_dsa_44` | ML-DSA-44 | Post-quantum tests (NIST FIPS 204, security level 2) | -| `ml_dsa_65` | ML-DSA-65 | Post-quantum tests (NIST FIPS 204, security level 3) | -| `ml_dsa_87` | ML-DSA-87 | Post-quantum tests (NIST FIPS 204, security level 5) | +| `ml_dsa_44` | ML-DSA-44 | Post-quantum (NIST FIPS 204, level 2) | +| `ml_dsa_65` | ML-DSA-65 | Post-quantum (NIST FIPS 204, level 3) | +| `ml_dsa_87` | ML-DSA-87 | Post-quantum (NIST FIPS 204, level 5) | -All PEM files carry a human-readable header (produced by `openssl x509 -text`) -before the `-----BEGIN CERTIFICATE-----` block. This header is ignored by all -conforming PEM parsers and is present only for readability. +PQC support requires OpenSSL ≥ 3.5. -## Supported `key_algorithm` values +### `pki_chain/` (future) -The `generate_certificates.py` script accepts the following values for the -`key_algorithm` field in the manifest: +Three-level PKI for chain-verification and OCSP tests. No certificates are +committed yet. Generate in the order listed in `pki_chain/manifest.json` +under `_generation_order`. + +--- + +## Supported `key_algorithm` values | Family | Values | |--------|--------| @@ -55,40 +109,148 @@ The `generate_certificates.py` script accepts the following values for the | EdDSA | `Ed25519`, `Ed448` | | PQC | `ML-DSA-44`, `ML-DSA-65`, `ML-DSA-87` | -PQC support requires OpenSSL ≥ 3.5 (ML-DSA is in the OpenSSL default provider). +--- ## Generating and refreshing certificates -From the repository root, generate a certificate and its local key with: +The `generate_certificates.py` script is invoked from the repository root and +always requires `--manifest` pointing to the folder whose certificates you want +to work with. -```sh -python3 score/tests/test_vectors/certificate/generate_certificates.py generate -``` +### Actions -For example: +| Action | Description | +|--------|-------------| +| `generate ` | Create a new private key and certificate | +| `update ` | Re-sign the certificate reusing the existing local private key | +| `crl ` | Sign a CRL for ``; revokes entries in `crl.revoked` | +| `ocsp-req ` | Generate a DER OCSP request for `` against its issuer | +| `ocsp-resp ` | Generate a pre-computed DER OCSP response for `` | + +### Common flags + +| Flag | Description | +|------|-------------| +| `--manifest ` | Path to the folder's `manifest.json` (required) | +| `--cert-format pem\|der\|both` | Override the entry's `cert_formats` for one invocation | +| `--openssl ` | Path to the OpenSSL executable (default: `openssl`) | + +### Examples ```sh -python3 score/tests/test_vectors/certificate/generate_certificates.py generate ec_p256 -python3 score/tests/test_vectors/certificate/generate_certificates.py generate ml_dsa_65 +# Regenerate the primary CA (new key + cert): +python3 score/tests/test_vectors/certificate/generate_certificates.py \ + generate certificate --manifest score/tests/test_vectors/certificate/basic/manifest.json + +# Regenerate the leaf cert (reuses CA key, signs a new CSR): +python3 score/tests/test_vectors/certificate/generate_certificates.py \ + generate certificate_leaf \ + --manifest score/tests/test_vectors/certificate/basic/manifest.json + +# Generate the CA CRL that lists the leaf as revoked: +python3 score/tests/test_vectors/certificate/generate_certificates.py \ + crl certificate \ + --manifest score/tests/test_vectors/certificate/basic/manifest.json + +# Regenerate an algorithm-variety cert (update keeps the existing key): +python3 score/tests/test_vectors/certificate/generate_certificates.py \ + update ec_p256 \ + --manifest score/tests/test_vectors/certificate/algorithm_variety/manifest.json + +# Generate a cert in both PEM and DER (overrides manifest for this run): +python3 score/tests/test_vectors/certificate/generate_certificates.py \ + generate certificate_leaf --cert-format both \ + --manifest score/tests/test_vectors/certificate/basic/manifest.json + +# Generate an OCSP request for the pki_chain leaf (once certs exist): +python3 score/tests/test_vectors/certificate/generate_certificates.py \ + ocsp-req leaf \ + --manifest score/tests/test_vectors/certificate/pki_chain/manifest.json + +# Generate a pre-computed OCSP response: +python3 score/tests/test_vectors/certificate/generate_certificates.py \ + ocsp-resp leaf \ + --manifest score/tests/test_vectors/certificate/pki_chain/manifest.json ``` -Running `generate` creates a new local private key and a new self-signed public -certificate, then refreshes the manifest snapshot. Review and commit the public -PEM, the corresponding `_slot.kv` descriptor, and the manifest together. +After running `generate` or `update`, commit the PEM, any DER/chain/CRL/OCSP +artifacts, the `_slot.kv` descriptor, and the updated `manifest.json` +together. Do not commit private keys. -Only use `update` when the corresponding local private key is available and -preserving the existing key is intentional: +--- -```sh -python3 score/tests/test_vectors/certificate/generate_certificates.py update +## Manifest schema reference + +Each `manifest.json` follows schema version 2. All paths are resolved relative +to the manifest file's directory. + +### Entry fields + +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| `name` | string | yes | Unique identifier within the manifest | +| `subject` | string | yes | OpenSSL `-subj` value (`/CN=.../O=...`) | +| `key_algorithm` | string | yes | See supported values above | +| `purpose` | string | no | Human-readable description | +| `validity_days` | int | no | Overrides `defaults.validity_days` | +| `cert_dir` | string | no | Overrides `defaults.cert_dir` | +| `key_dir` | string | no | Overrides `defaults.key_dir` | +| `signed_by` | string | no | Name of the issuer CA entry — produces an issued cert instead of self-signed | +| `is_ca` | bool | no | `basicConstraints` CA flag. Defaults `true` for self-signed, `false` when `signed_by` is set | +| `cert_formats` | list | no | `["pem"]` (default), `["der"]`, or `["pem","der"]` | +| `chain_file` | bool | no | Write `.chain.pem` (leaf + all ancestors up to root) | +| `ext_file` | string | no | Path to a file containing a `[v3_ext]` OpenSSL extension stanza. Use only when you need non-default extensions (SANs, `OCSPSigning` EKU, etc.). The script adds the required `[req]` wrapper automatically. | +| `crl.revoked` | list | no | Cert names whose serials appear in the CRL signed by this entry | +| `ocsp.signer` | string | no | Name of the OCSP-signing cert entry (must carry `OCSPSigning` EKU) | +| `ocsp.status` | string | no | `"good"` (default) or `"revoked"` | +| `ocsp.validity_days` | int | no | OCSP response validity window in days (default 7) | +| `generated` | object | auto | Written by the script after generation. Do not edit by hand. | + +### When to use `ext_file` + +Most certificates do **not** need an `ext_file`. The script generates sensible +defaults from `is_ca`: + +- `is_ca: true` → `basicConstraints=critical,CA:TRUE` + `subjectKeyIdentifier` + `authorityKeyIdentifier` +- `is_ca: false` → same, with `CA:FALSE` + +Provide an `ext_file` only when you need extensions the script cannot infer, +such as: + +```ini +# Example: pki_chain/ocsp_signer.ext.conf +[v3_ext] +basicConstraints = critical,CA:FALSE +subjectKeyIdentifier = hash +authorityKeyIdentifier = keyid +extendedKeyUsage = OCSPSigning +noCheck = ignored ``` -Do not make tests depend on a private key being present. +The file must contain only the `[v3_ext]` section. Reference it from the +manifest as `"ext_file": "ocsp_signer.ext.conf"`. + +--- ## Adding a new certificate -1. Add an entry to `certificate_manifest.json` with `name`, `certificate_path`, - `private_key_path`, `purpose`, `subject`, `key_algorithm`, and `validity_days`. -2. Run `generate_certificates.py generate `. The script writes the PEM, - the `_slot.kv` KV descriptor, and updates the manifest snapshot. -3. Commit the PEM, the KV descriptor, and the updated manifest together. +1. Decide which folder it belongs to (`basic/`, `algorithm_variety/`, + `pki_chain/`, or a new folder for a new PKI context). +2. Add an entry to that folder's `manifest.json` with at minimum `name`, + `subject`, and `key_algorithm`. Add `signed_by` if it should be issued by an + existing CA rather than self-signed. +3. Run `generate_certificates.py generate --manifest /manifest.json`. +4. Commit the PEM, KV descriptor, and updated `manifest.json`. If `chain_file` + or DER output was requested, commit those too. Never commit private keys. + +## Adding a new PKI context (new folder) + +1. Create the folder and a `private/` subfolder (the `private/` subfolder stays + untracked; optionally add a `.gitignore` inside it). +2. Create `manifest.json` following the schema above with `"schema_version": 2` + and appropriate `defaults`. +3. If certs in this PKI have non-standard extensions, create the `.ext.conf` + sidecar file alongside the manifest. +4. Add a new `filegroup` target for the folder in `BUILD` and add it to the + `certificate_test_vectors` srcs list. +5. Generate certs in dependency order (issuers before leaves). diff --git a/score/tests/test_vectors/certificate/ec_p256.pem b/score/tests/test_vectors/certificate/algorithm_variety/ec_p256.pem similarity index 100% rename from score/tests/test_vectors/certificate/ec_p256.pem rename to score/tests/test_vectors/certificate/algorithm_variety/ec_p256.pem diff --git a/score/tests/test_vectors/certificate/algorithm_variety/ec_p256_slot.kv b/score/tests/test_vectors/certificate/algorithm_variety/ec_p256_slot.kv new file mode 100644 index 000000000..0b5c6ba5a --- /dev/null +++ b/score/tests/test_vectors/certificate/algorithm_variety/ec_p256_slot.kv @@ -0,0 +1,3 @@ +[certificate] +cert_path = score/tests/test_vectors/certificate/algorithm_variety/ec_p256.pem +cert_format = pem diff --git a/score/tests/test_vectors/certificate/ec_p384.pem b/score/tests/test_vectors/certificate/algorithm_variety/ec_p384.pem similarity index 100% rename from score/tests/test_vectors/certificate/ec_p384.pem rename to score/tests/test_vectors/certificate/algorithm_variety/ec_p384.pem diff --git a/score/tests/test_vectors/certificate/algorithm_variety/ec_p384_slot.kv b/score/tests/test_vectors/certificate/algorithm_variety/ec_p384_slot.kv new file mode 100644 index 000000000..b97d05bd7 --- /dev/null +++ b/score/tests/test_vectors/certificate/algorithm_variety/ec_p384_slot.kv @@ -0,0 +1,3 @@ +[certificate] +cert_path = score/tests/test_vectors/certificate/algorithm_variety/ec_p384.pem +cert_format = pem diff --git a/score/tests/test_vectors/certificate/ec_p521.pem b/score/tests/test_vectors/certificate/algorithm_variety/ec_p521.pem similarity index 100% rename from score/tests/test_vectors/certificate/ec_p521.pem rename to score/tests/test_vectors/certificate/algorithm_variety/ec_p521.pem diff --git a/score/tests/test_vectors/certificate/algorithm_variety/ec_p521_slot.kv b/score/tests/test_vectors/certificate/algorithm_variety/ec_p521_slot.kv new file mode 100644 index 000000000..193023740 --- /dev/null +++ b/score/tests/test_vectors/certificate/algorithm_variety/ec_p521_slot.kv @@ -0,0 +1,3 @@ +[certificate] +cert_path = score/tests/test_vectors/certificate/algorithm_variety/ec_p521.pem +cert_format = pem diff --git a/score/tests/test_vectors/certificate/ed25519.pem b/score/tests/test_vectors/certificate/algorithm_variety/ed25519.pem similarity index 100% rename from score/tests/test_vectors/certificate/ed25519.pem rename to score/tests/test_vectors/certificate/algorithm_variety/ed25519.pem diff --git a/score/tests/test_vectors/certificate/algorithm_variety/ed25519_slot.kv b/score/tests/test_vectors/certificate/algorithm_variety/ed25519_slot.kv new file mode 100644 index 000000000..85cdab6fe --- /dev/null +++ b/score/tests/test_vectors/certificate/algorithm_variety/ed25519_slot.kv @@ -0,0 +1,3 @@ +[certificate] +cert_path = score/tests/test_vectors/certificate/algorithm_variety/ed25519.pem +cert_format = pem diff --git a/score/tests/test_vectors/certificate/ed448.pem b/score/tests/test_vectors/certificate/algorithm_variety/ed448.pem similarity index 100% rename from score/tests/test_vectors/certificate/ed448.pem rename to score/tests/test_vectors/certificate/algorithm_variety/ed448.pem diff --git a/score/tests/test_vectors/certificate/algorithm_variety/ed448_slot.kv b/score/tests/test_vectors/certificate/algorithm_variety/ed448_slot.kv new file mode 100644 index 000000000..df01e18a4 --- /dev/null +++ b/score/tests/test_vectors/certificate/algorithm_variety/ed448_slot.kv @@ -0,0 +1,3 @@ +[certificate] +cert_path = score/tests/test_vectors/certificate/algorithm_variety/ed448.pem +cert_format = pem diff --git a/score/tests/test_vectors/certificate/certificate_manifest.json b/score/tests/test_vectors/certificate/algorithm_variety/manifest.json similarity index 64% rename from score/tests/test_vectors/certificate/certificate_manifest.json rename to score/tests/test_vectors/certificate/algorithm_variety/manifest.json index 15c594494..9eeb406e4 100644 --- a/score/tests/test_vectors/certificate/certificate_manifest.json +++ b/score/tests/test_vectors/certificate/algorithm_variety/manifest.json @@ -1,37 +1,15 @@ { "schema_version": 2, - "description": "Certificate-management test certificate inventory. Public certificates are checked in; private keys are generated locally and are not committed.", + "description": "One self-signed CA per supported key algorithm. Used by slot-handler algorithm-variety tests.", "defaults": { "cert_dir": ".", "key_dir": "private", "validity_days": 3650 }, "certificates": [ - { - "name": "certificate", - "purpose": "Initial CA certificate for file-backed slot and trust-store tests", - "subject": "/CN=cert-management-test/O=Eclipse", - "key_algorithm": "RSA-2048", - "generated": { - "not_before": "2026-08-24T22:20:18Z", - "not_after": "2036-08-21T22:20:18Z", - "sha256_fingerprint": "3E7A581BCAAB80F73535B10968F8C3E68517194B6A404CE5F58AACABE0A62D0C" - } - }, - { - "name": "certificate_updated", - "purpose": "Replacement CA certificate used to test slot updates and trust-store anchor invalidation", - "subject": "/CN=cert-management-updated/O=Eclipse", - "key_algorithm": "RSA-2048", - "generated": { - "not_before": "2026-08-24T22:20:18Z", - "not_after": "2036-08-21T22:20:18Z", - "sha256_fingerprint": "2324A755EB36B44DF2495504AF85A8ED3FE89A8328F01788905DB3A0E1EAED7E" - } - }, { "name": "rsa_3072", - "purpose": "RSA-3072 CA certificate for key-size variety tests", + "purpose": "RSA-3072 CA for key-size variety tests", "subject": "/CN=cert-mgmt-rsa-3072/O=Eclipse", "key_algorithm": "RSA-3072", "generated": { @@ -42,7 +20,7 @@ }, { "name": "rsa_4096", - "purpose": "RSA-4096 CA certificate for key-size variety tests", + "purpose": "RSA-4096 CA for key-size variety tests", "subject": "/CN=cert-mgmt-rsa-4096/O=Eclipse", "key_algorithm": "RSA-4096", "generated": { @@ -53,7 +31,7 @@ }, { "name": "ec_p256", - "purpose": "ECDSA P-256 CA certificate for elliptic-curve algorithm tests", + "purpose": "ECDSA P-256 CA for elliptic-curve algorithm tests", "subject": "/CN=cert-mgmt-ec-p256/O=Eclipse", "key_algorithm": "EC-P256", "generated": { @@ -64,7 +42,7 @@ }, { "name": "ec_p384", - "purpose": "ECDSA P-384 CA certificate for elliptic-curve algorithm tests", + "purpose": "ECDSA P-384 CA for elliptic-curve algorithm tests", "subject": "/CN=cert-mgmt-ec-p384/O=Eclipse", "key_algorithm": "EC-P384", "generated": { @@ -75,7 +53,7 @@ }, { "name": "ec_p521", - "purpose": "ECDSA P-521 CA certificate for elliptic-curve algorithm tests", + "purpose": "ECDSA P-521 CA for elliptic-curve algorithm tests", "subject": "/CN=cert-mgmt-ec-p521/O=Eclipse", "key_algorithm": "EC-P521", "generated": { @@ -86,7 +64,7 @@ }, { "name": "ed25519", - "purpose": "Ed25519 CA certificate for Edwards-curve algorithm tests", + "purpose": "Ed25519 CA for Edwards-curve algorithm tests", "subject": "/CN=cert-mgmt-ed25519/O=Eclipse", "key_algorithm": "Ed25519", "generated": { @@ -97,7 +75,7 @@ }, { "name": "ed448", - "purpose": "Ed448 CA certificate for Edwards-curve algorithm tests", + "purpose": "Ed448 CA for Edwards-curve algorithm tests", "subject": "/CN=cert-mgmt-ed448/O=Eclipse", "key_algorithm": "Ed448", "generated": { @@ -108,7 +86,7 @@ }, { "name": "ml_dsa_44", - "purpose": "ML-DSA-44 CA certificate for post-quantum algorithm tests (NIST FIPS 204 security level 2)", + "purpose": "ML-DSA-44 CA for post-quantum algorithm tests (NIST FIPS 204 level 2)", "subject": "/CN=cert-mgmt-ml-dsa-44/O=Eclipse", "key_algorithm": "ML-DSA-44", "generated": { @@ -119,7 +97,7 @@ }, { "name": "ml_dsa_65", - "purpose": "ML-DSA-65 CA certificate for post-quantum algorithm tests (NIST FIPS 204 security level 3)", + "purpose": "ML-DSA-65 CA for post-quantum algorithm tests (NIST FIPS 204 level 3)", "subject": "/CN=cert-mgmt-ml-dsa-65/O=Eclipse", "key_algorithm": "ML-DSA-65", "generated": { @@ -130,7 +108,7 @@ }, { "name": "ml_dsa_87", - "purpose": "ML-DSA-87 CA certificate for post-quantum algorithm tests (NIST FIPS 204 security level 5)", + "purpose": "ML-DSA-87 CA for post-quantum algorithm tests (NIST FIPS 204 level 5)", "subject": "/CN=cert-mgmt-ml-dsa-87/O=Eclipse", "key_algorithm": "ML-DSA-87", "generated": { diff --git a/score/tests/test_vectors/certificate/ml_dsa_44.pem b/score/tests/test_vectors/certificate/algorithm_variety/ml_dsa_44.pem similarity index 100% rename from score/tests/test_vectors/certificate/ml_dsa_44.pem rename to score/tests/test_vectors/certificate/algorithm_variety/ml_dsa_44.pem diff --git a/score/tests/test_vectors/certificate/algorithm_variety/ml_dsa_44_slot.kv b/score/tests/test_vectors/certificate/algorithm_variety/ml_dsa_44_slot.kv new file mode 100644 index 000000000..d5bab9f07 --- /dev/null +++ b/score/tests/test_vectors/certificate/algorithm_variety/ml_dsa_44_slot.kv @@ -0,0 +1,3 @@ +[certificate] +cert_path = score/tests/test_vectors/certificate/algorithm_variety/ml_dsa_44.pem +cert_format = pem diff --git a/score/tests/test_vectors/certificate/ml_dsa_65.pem b/score/tests/test_vectors/certificate/algorithm_variety/ml_dsa_65.pem similarity index 100% rename from score/tests/test_vectors/certificate/ml_dsa_65.pem rename to score/tests/test_vectors/certificate/algorithm_variety/ml_dsa_65.pem diff --git a/score/tests/test_vectors/certificate/algorithm_variety/ml_dsa_65_slot.kv b/score/tests/test_vectors/certificate/algorithm_variety/ml_dsa_65_slot.kv new file mode 100644 index 000000000..592e91a25 --- /dev/null +++ b/score/tests/test_vectors/certificate/algorithm_variety/ml_dsa_65_slot.kv @@ -0,0 +1,3 @@ +[certificate] +cert_path = score/tests/test_vectors/certificate/algorithm_variety/ml_dsa_65.pem +cert_format = pem diff --git a/score/tests/test_vectors/certificate/ml_dsa_87.pem b/score/tests/test_vectors/certificate/algorithm_variety/ml_dsa_87.pem similarity index 100% rename from score/tests/test_vectors/certificate/ml_dsa_87.pem rename to score/tests/test_vectors/certificate/algorithm_variety/ml_dsa_87.pem diff --git a/score/tests/test_vectors/certificate/algorithm_variety/ml_dsa_87_slot.kv b/score/tests/test_vectors/certificate/algorithm_variety/ml_dsa_87_slot.kv new file mode 100644 index 000000000..e0923007d --- /dev/null +++ b/score/tests/test_vectors/certificate/algorithm_variety/ml_dsa_87_slot.kv @@ -0,0 +1,3 @@ +[certificate] +cert_path = score/tests/test_vectors/certificate/algorithm_variety/ml_dsa_87.pem +cert_format = pem diff --git a/score/tests/test_vectors/certificate/private/.gitignore b/score/tests/test_vectors/certificate/algorithm_variety/private/.gitignore similarity index 100% rename from score/tests/test_vectors/certificate/private/.gitignore rename to score/tests/test_vectors/certificate/algorithm_variety/private/.gitignore diff --git a/score/tests/test_vectors/certificate/rsa_3072.pem b/score/tests/test_vectors/certificate/algorithm_variety/rsa_3072.pem similarity index 100% rename from score/tests/test_vectors/certificate/rsa_3072.pem rename to score/tests/test_vectors/certificate/algorithm_variety/rsa_3072.pem diff --git a/score/tests/test_vectors/certificate/algorithm_variety/rsa_3072_slot.kv b/score/tests/test_vectors/certificate/algorithm_variety/rsa_3072_slot.kv new file mode 100644 index 000000000..edc5597ed --- /dev/null +++ b/score/tests/test_vectors/certificate/algorithm_variety/rsa_3072_slot.kv @@ -0,0 +1,3 @@ +[certificate] +cert_path = score/tests/test_vectors/certificate/algorithm_variety/rsa_3072.pem +cert_format = pem diff --git a/score/tests/test_vectors/certificate/rsa_4096.pem b/score/tests/test_vectors/certificate/algorithm_variety/rsa_4096.pem similarity index 100% rename from score/tests/test_vectors/certificate/rsa_4096.pem rename to score/tests/test_vectors/certificate/algorithm_variety/rsa_4096.pem diff --git a/score/tests/test_vectors/certificate/algorithm_variety/rsa_4096_slot.kv b/score/tests/test_vectors/certificate/algorithm_variety/rsa_4096_slot.kv new file mode 100644 index 000000000..c78f3ee3c --- /dev/null +++ b/score/tests/test_vectors/certificate/algorithm_variety/rsa_4096_slot.kv @@ -0,0 +1,3 @@ +[certificate] +cert_path = score/tests/test_vectors/certificate/algorithm_variety/rsa_4096.pem +cert_format = pem diff --git a/score/tests/test_vectors/certificate/certificate.pem b/score/tests/test_vectors/certificate/basic/certificate.pem similarity index 100% rename from score/tests/test_vectors/certificate/certificate.pem rename to score/tests/test_vectors/certificate/basic/certificate.pem diff --git a/score/tests/test_vectors/certificate/basic/certificate_slot.kv b/score/tests/test_vectors/certificate/basic/certificate_slot.kv new file mode 100644 index 000000000..f3d3b4a2b --- /dev/null +++ b/score/tests/test_vectors/certificate/basic/certificate_slot.kv @@ -0,0 +1,3 @@ +[certificate] +cert_path = score/tests/test_vectors/certificate/basic/certificate.pem +cert_format = pem diff --git a/score/tests/test_vectors/certificate/certificate_updated.pem b/score/tests/test_vectors/certificate/basic/certificate_updated.pem similarity index 100% rename from score/tests/test_vectors/certificate/certificate_updated.pem rename to score/tests/test_vectors/certificate/basic/certificate_updated.pem diff --git a/score/tests/test_vectors/certificate/basic/certificate_updated_slot.kv b/score/tests/test_vectors/certificate/basic/certificate_updated_slot.kv new file mode 100644 index 000000000..27c5100d0 --- /dev/null +++ b/score/tests/test_vectors/certificate/basic/certificate_updated_slot.kv @@ -0,0 +1,3 @@ +[certificate] +cert_path = score/tests/test_vectors/certificate/basic/certificate_updated.pem +cert_format = pem diff --git a/score/tests/test_vectors/certificate/basic/manifest.json b/score/tests/test_vectors/certificate/basic/manifest.json new file mode 100644 index 000000000..fb52a754a --- /dev/null +++ b/score/tests/test_vectors/certificate/basic/manifest.json @@ -0,0 +1,55 @@ +{ + "schema_version": 2, + "description": "Primary RSA-2048 test PKI. Root CA + rotated CA + signed leaf. Used by cert-management, trust-store, slot-handler, integration, and cert-parser tests.", + "defaults": { + "cert_dir": ".", + "key_dir": "private", + "validity_days": 3650 + }, + "certificates": [ + { + "name": "certificate", + "purpose": "Root CA for basic slot and trust-store tests", + "subject": "/CN=cert-management-test/O=Eclipse", + "key_algorithm": "RSA-2048", + "crl": { + "_comment": "Run: python ../generate_certificates.py crl certificate --manifest manifest.json", + "revoked": ["certificate_leaf"] + }, + "generated": { + "not_before": "2026-08-24T22:20:18Z", + "not_after": "2036-08-21T22:20:18Z", + "sha256_fingerprint": "3E7A581BCAAB80F73535B10968F8C3E68517194B6A404CE5F58AACABE0A62D0C" + } + }, + { + "name": "certificate_updated", + "purpose": "Rotated CA — used to test slot updates and trust-store anchor invalidation", + "subject": "/CN=cert-management-updated/O=Eclipse", + "key_algorithm": "RSA-2048", + "generated": { + "not_before": "2026-08-24T22:20:18Z", + "not_after": "2036-08-21T22:20:18Z", + "sha256_fingerprint": "2324A755EB36B44DF2495504AF85A8ED3FE89A8328F01788905DB3A0E1EAED7E" + } + }, + { + "name": "certificate_leaf", + "purpose": "End-entity cert signed by 'certificate' CA; appears in certificate CRL revoked list", + "subject": "/CN=cert-mgmt-leaf/O=Eclipse", + "key_algorithm": "RSA-2048", + "signed_by": "certificate", + "is_ca": false, + "cert_formats": ["pem", "der"], + "chain_file": true, + "_usage": [ + "Step 1 — generate the CA first (if not already present):", + " python ../generate_certificates.py generate certificate --manifest manifest.json", + "Step 2 — generate the leaf (CSR signed by 'certificate'):", + " python ../generate_certificates.py generate certificate_leaf --manifest manifest.json", + "Step 3 — generate the CA CRL that revokes this leaf:", + " python ../generate_certificates.py crl certificate --manifest manifest.json" + ] + } + ] +} diff --git a/score/tests/test_vectors/certificate/basic/private/.gitignore b/score/tests/test_vectors/certificate/basic/private/.gitignore new file mode 100644 index 000000000..d6b7ef32c --- /dev/null +++ b/score/tests/test_vectors/certificate/basic/private/.gitignore @@ -0,0 +1,2 @@ +* +!.gitignore diff --git a/score/tests/test_vectors/certificate/trust_store.kv b/score/tests/test_vectors/certificate/basic/trust_store.kv similarity index 100% rename from score/tests/test_vectors/certificate/trust_store.kv rename to score/tests/test_vectors/certificate/basic/trust_store.kv diff --git a/score/tests/test_vectors/certificate/certificate_slot.kv b/score/tests/test_vectors/certificate/certificate_slot.kv deleted file mode 100644 index 5c3a0d5e9..000000000 --- a/score/tests/test_vectors/certificate/certificate_slot.kv +++ /dev/null @@ -1,3 +0,0 @@ -[certificate] -cert_path = score/tests/test_vectors/certificate/certificate.pem -cert_format = pem diff --git a/score/tests/test_vectors/certificate/certificate_updated_slot.kv b/score/tests/test_vectors/certificate/certificate_updated_slot.kv deleted file mode 100644 index 9366ea2dc..000000000 --- a/score/tests/test_vectors/certificate/certificate_updated_slot.kv +++ /dev/null @@ -1,3 +0,0 @@ -[certificate] -cert_path = score/tests/test_vectors/certificate/certificate_updated.pem -cert_format = pem diff --git a/score/tests/test_vectors/certificate/ec_p256_slot.kv b/score/tests/test_vectors/certificate/ec_p256_slot.kv deleted file mode 100644 index 7d4d4b241..000000000 --- a/score/tests/test_vectors/certificate/ec_p256_slot.kv +++ /dev/null @@ -1,3 +0,0 @@ -[certificate] -cert_path = score/tests/test_vectors/certificate/ec_p256.pem -cert_format = pem diff --git a/score/tests/test_vectors/certificate/ec_p384_slot.kv b/score/tests/test_vectors/certificate/ec_p384_slot.kv deleted file mode 100644 index 3bc1c1776..000000000 --- a/score/tests/test_vectors/certificate/ec_p384_slot.kv +++ /dev/null @@ -1,3 +0,0 @@ -[certificate] -cert_path = score/tests/test_vectors/certificate/ec_p384.pem -cert_format = pem diff --git a/score/tests/test_vectors/certificate/ec_p521_slot.kv b/score/tests/test_vectors/certificate/ec_p521_slot.kv deleted file mode 100644 index 3b7bbd26d..000000000 --- a/score/tests/test_vectors/certificate/ec_p521_slot.kv +++ /dev/null @@ -1,3 +0,0 @@ -[certificate] -cert_path = score/tests/test_vectors/certificate/ec_p521.pem -cert_format = pem diff --git a/score/tests/test_vectors/certificate/ed25519_slot.kv b/score/tests/test_vectors/certificate/ed25519_slot.kv deleted file mode 100644 index 664ad36d2..000000000 --- a/score/tests/test_vectors/certificate/ed25519_slot.kv +++ /dev/null @@ -1,3 +0,0 @@ -[certificate] -cert_path = score/tests/test_vectors/certificate/ed25519.pem -cert_format = pem diff --git a/score/tests/test_vectors/certificate/ed448_slot.kv b/score/tests/test_vectors/certificate/ed448_slot.kv deleted file mode 100644 index b23e952ef..000000000 --- a/score/tests/test_vectors/certificate/ed448_slot.kv +++ /dev/null @@ -1,3 +0,0 @@ -[certificate] -cert_path = score/tests/test_vectors/certificate/ed448.pem -cert_format = pem diff --git a/score/tests/test_vectors/certificate/generate_certificates.py b/score/tests/test_vectors/certificate/generate_certificates.py index 955668cc7..5ca8fdb57 100644 --- a/score/tests/test_vectors/certificate/generate_certificates.py +++ b/score/tests/test_vectors/certificate/generate_certificates.py @@ -21,22 +21,91 @@ EdDSA Ed25519, Ed448 PQC ML-DSA-44, ML-DSA-65, ML-DSA-87 (NIST FIPS 204, OpenSSL >= 3.5) -The key_algorithm field drives both the -newkey arguments and whether an -explicit message-digest flag (-sha256) is passed to openssl req. Ed25519, -Ed448, and ML-DSA variants carry their own inherent digest and must not -receive -sha256. - Manifest schema (v2) -------------------- -Paths are derived from the entry ``name`` field and the top-level ``defaults`` -block — they are not stored redundantly in each entry. Convention: - - certificate -> /.pem (default cert_dir = ".") - private key -> /.key.pem (default key_dir = "private") - slot KV -> /_slot.kv - -The ``generated`` block within each entry is written by this script after a -successful generation run and must not be edited by hand. +Each subfolder of the test-vector tree has its own ``manifest.json``. The +script is invoked with ``--manifest /manifest.json``; all paths inside +the manifest are resolved relative to that file's directory. + + certificate (PEM) -> /.pem + certificate (DER) -> /.der (if "der" in cert_formats) + certificate (chain) -> /.chain.pem (if chain_file = true) + private key -> /.key.pem + slot KV -> /_slot.kv + CRL (PEM) -> /.crl.pem + CRL (DER) -> /.crl.der + OCSP request -> /.ocsp.req.der + OCSP response -> /.ocsp.resp.der + +Manifest entry fields +--------------------- + name string Unique identifier within the manifest + subject string OpenSSL -subj value ("/CN=.../O=...") + key_algorithm string RSA-2048 / EC-P256 / Ed25519 / ML-DSA-44 … + purpose string (optional) human-readable description + validity_days int (optional) overrides defaults.validity_days + cert_dir string (optional) overrides defaults.cert_dir + key_dir string (optional) overrides defaults.key_dir + signed_by string (optional) name of issuer CA entry → issued cert path + is_ca bool (optional) basicConstraints CA flag. + Defaults true for self-signed, false when signed_by is set. + cert_formats list (optional) ["pem"] | ["der"] | ["pem","der"]. + Overridden per-invocation by --cert-format. + chain_file bool (optional) write .chain.pem (leaf + ancestors) + ext_file string (optional) path to a file containing a [v3_ext] OpenSSL + extension section. Use for non-standard extensions + (SANs, OCSPSigning EKU, specific key usage, etc.). + The file must contain only the [v3_ext] stanza — + boilerplate [req] wrapper is added by the script. + Most certs do NOT need an ext_file. + crl object (optional) CRL generation hints + revoked list (optional) cert names whose serials appear in the CRL + ocsp object (optional) OCSP generation hints + signer string Name of the OCSP-signing cert entry (must carry + extendedKeyUsage = OCSPSigning in its ext_file) + status string "good" | "revoked" (default "good") + validity_days int Response validity window in days (default 7) + +Certificate signing (signed_by) +-------------------------------- +Without ``signed_by``: self-signed via ``openssl req -x509``. +With ``signed_by``: CSR via ``openssl req -new``, then signed via + ``openssl x509 -req -CA ... -CAkey ...``. +Issuer PEM and private key must be on disk before signing the leaf. + +Extension customisation (ext_file) +------------------------------------ +Provide a file containing only the ``[v3_ext]`` OpenSSL stanza: + + [v3_ext] + basicConstraints = critical,CA:FALSE + subjectKeyIdentifier = hash + authorityKeyIdentifier = keyid + extendedKeyUsage = OCSPSigning + noCheck = ignored + +Reference it from the manifest entry as ``"ext_file": "ocsp_signer.ext.conf"``. +The script injects the boilerplate [req] wrapper automatically so the file stays +minimal. Omit ext_file for standard CA or leaf certs — the defaults are +sufficient for most test-vector purposes. + +Actions +-------- + generate Generate a new key + certificate. + update Re-sign the certificate reusing the existing private key. + crl Generate a CRL signed by ; revokes crl.revoked entries. + ocsp-req Generate a DER OCSP request for against its issuer. + ocsp-resp Generate a pre-computed DER OCSP response for . + +Example end-to-end (pki_chain folder): + + python generate_certificates.py generate root_ca --manifest pki_chain/manifest.json + python generate_certificates.py generate ocsp_signer --manifest pki_chain/manifest.json + python generate_certificates.py generate intermediate_ca --manifest pki_chain/manifest.json + python generate_certificates.py generate leaf --manifest pki_chain/manifest.json + python generate_certificates.py crl root_ca --manifest pki_chain/manifest.json + python generate_certificates.py ocsp-req leaf --manifest pki_chain/manifest.json + python generate_certificates.py ocsp-resp leaf --manifest pki_chain/manifest.json """ from __future__ import annotations @@ -46,6 +115,7 @@ import json import pathlib import subprocess +import tempfile from typing import Any # --------------------------------------------------------------------------- @@ -62,44 +132,25 @@ def newkey_args(key_algorithm: str) -> list[str]: - """Return the -newkey flag sequence for *openssl req -x509*. - - The returned list is ready to extend() into the openssl argument list - during key generation (not used for the ``update`` path which reuses an - existing key with -key). - """ alg = key_algorithm.upper() - if alg.startswith("RSA-"): bits = alg[4:] if not bits.isdigit(): - raise SystemExit(f"Invalid RSA key size in key_algorithm: {key_algorithm!r}") + raise SystemExit(f"Invalid RSA key size: {key_algorithm!r}") return ["-newkey", f"rsa:{bits}"] - if alg in _EC_CURVES: - curve = _EC_CURVES[alg] - return ["-newkey", "ec", "-pkeyopt", f"ec_paramgen_curve:{curve}"] - + return ["-newkey", "ec", "-pkeyopt", f"ec_paramgen_curve:{_EC_CURVES[alg]}"] if alg in _EDDSA_ALGS: - return ["-newkey", alg.lower()] # ed25519 / ed448 - + return ["-newkey", alg.lower()] if alg in _MLDSA_LEVELS: - return ["-newkey", alg.lower()] # ml-dsa-44 / ml-dsa-65 / ml-dsa-87 - + return ["-newkey", alg.lower()] raise SystemExit( f"Unknown key_algorithm: {key_algorithm!r}. " - f"Supported: RSA-, EC-P256/P384/P521, Ed25519, Ed448, " - f"ML-DSA-44/65/87." + "Supported: RSA-, EC-P256/P384/P521, Ed25519, Ed448, ML-DSA-44/65/87." ) def uses_separate_digest(key_algorithm: str) -> bool: - """True for RSA and EC where an explicit hash algorithm can be selected. - - Ed25519, Ed448, and ML-DSA carry their own inherent digest; passing - -sha256 to openssl req for these algorithms is incorrect and may produce - an error on strict OpenSSL builds. - """ alg = key_algorithm.upper() return alg.startswith("RSA-") or alg in _EC_CURVES @@ -114,6 +165,16 @@ def cert_path(root: pathlib.Path, defaults: dict[str, Any], entry: dict[str, Any return root / cert_dir / f"{entry['name']}.pem" +def cert_der_path(root: pathlib.Path, defaults: dict[str, Any], entry: dict[str, Any]) -> pathlib.Path: + cert_dir = entry.get("cert_dir", defaults.get("cert_dir", ".")) + return root / cert_dir / f"{entry['name']}.der" + + +def cert_chain_path(root: pathlib.Path, defaults: dict[str, Any], entry: dict[str, Any]) -> pathlib.Path: + cert_dir = entry.get("cert_dir", defaults.get("cert_dir", ".")) + return root / cert_dir / f"{entry['name']}.chain.pem" + + def key_path(root: pathlib.Path, defaults: dict[str, Any], entry: dict[str, Any]) -> pathlib.Path: key_dir = entry.get("key_dir", defaults.get("key_dir", "private")) return root / key_dir / f"{entry['name']}.key.pem" @@ -124,10 +185,48 @@ def slot_kv_path(root: pathlib.Path, defaults: dict[str, Any], entry: dict[str, return root / cert_dir / f"{entry['name']}_slot.kv" +def crl_pem_path(root: pathlib.Path, defaults: dict[str, Any], entry: dict[str, Any]) -> pathlib.Path: + cert_dir = entry.get("cert_dir", defaults.get("cert_dir", ".")) + return root / cert_dir / f"{entry['name']}.crl.pem" + + +def crl_der_path(root: pathlib.Path, defaults: dict[str, Any], entry: dict[str, Any]) -> pathlib.Path: + cert_dir = entry.get("cert_dir", defaults.get("cert_dir", ".")) + return root / cert_dir / f"{entry['name']}.crl.der" + + +def ocsp_req_path(root: pathlib.Path, defaults: dict[str, Any], entry: dict[str, Any]) -> pathlib.Path: + cert_dir = entry.get("cert_dir", defaults.get("cert_dir", ".")) + return root / cert_dir / f"{entry['name']}.ocsp.req.der" + + +def ocsp_resp_path(root: pathlib.Path, defaults: dict[str, Any], entry: dict[str, Any]) -> pathlib.Path: + cert_dir = entry.get("cert_dir", defaults.get("cert_dir", ".")) + return root / cert_dir / f"{entry['name']}.ocsp.resp.der" + + def entry_validity_days(defaults: dict[str, Any], entry: dict[str, Any]) -> int: return int(entry.get("validity_days", defaults.get("validity_days", 3650))) +def resolve_cert_formats( + defaults: dict[str, Any], + entry: dict[str, Any], + cli_override: str | None, +) -> list[str]: + if cli_override is not None: + return ["pem", "der"] if cli_override == "both" else [cli_override] + raw: list[str] = entry.get("cert_formats", defaults.get("cert_formats", ["pem"])) + seen: set[str] = set() + result: list[str] = [] + for fmt in raw: + fmt = fmt.lower() + if fmt not in seen: + seen.add(fmt) + result.append(fmt) + return result + + # --------------------------------------------------------------------------- # OpenSSL helpers # --------------------------------------------------------------------------- @@ -138,32 +237,22 @@ def run_openssl(openssl: str, arguments: list[str]) -> subprocess.CompletedProce def annotate_pem(openssl: str, certificate_path: pathlib.Path) -> None: - """Prepend human-readable certificate info to a PEM file. - - Runs ``openssl x509 -text`` which outputs the decoded fields followed by - the PEM block. The result is written back to the same file so the cert - remains machine-parseable while also being human-readable at a glance. - Most PEM parsers (OpenSSL, Python cryptography, etc.) skip the text header - and process only the base64 armored block. - """ + """Prepend human-readable x509 text to a PEM certificate file.""" result = run_openssl(openssl, ["x509", "-in", str(certificate_path), "-text"]) certificate_path.write_text(result.stdout, encoding="utf-8") +def annotate_crl_pem(openssl: str, crl_path: pathlib.Path) -> None: + """Prepend human-readable CRL text to a PEM CRL file.""" + result = run_openssl(openssl, ["crl", "-in", str(crl_path), "-text"]) + crl_path.write_text(result.stdout, encoding="utf-8") + + def parse_snapshot(openssl: str, certificate_path: pathlib.Path) -> dict[str, str]: result = run_openssl( openssl, - [ - "x509", - "-in", - str(certificate_path), - "-noout", - "-subject", - "-startdate", - "-enddate", - "-fingerprint", - "-sha256", - ], + ["x509", "-in", str(certificate_path), "-noout", + "-subject", "-startdate", "-enddate", "-fingerprint", "-sha256"], ) values: dict[str, str] = {} for line in result.stdout.splitlines(): @@ -175,11 +264,10 @@ def parse_date(value: str) -> str: parsed = datetime_module.datetime.strptime(value, "%b %d %H:%M:%S %Y GMT") return parsed.replace(tzinfo=datetime_module.timezone.utc).isoformat().replace("+00:00", "Z") - fingerprint: str = values["sha256 Fingerprint"].replace(":", "").upper() return { "not_before": parse_date(values["notBefore"]), "not_after": parse_date(values["notAfter"]), - "sha256_fingerprint": fingerprint, + "sha256_fingerprint": values["sha256 Fingerprint"].replace(":", "").upper(), } @@ -189,22 +277,22 @@ def parse_date(value: str) -> str: def load_manifest(path: pathlib.Path) -> dict[str, Any]: - with path.open(encoding="utf-8") as manifest_file: - return json.load(manifest_file) # type: ignore[no-any-return] + with path.open(encoding="utf-8") as f: + return json.load(f) # type: ignore[no-any-return] def save_manifest(path: pathlib.Path, manifest: dict[str, Any]) -> None: - with path.open("w", encoding="utf-8") as manifest_file: - json.dump(manifest, manifest_file, indent=2) - manifest_file.write("\n") + with path.open("w", encoding="utf-8") as f: + json.dump(manifest, f, indent=2) + f.write("\n") def find_certificate(manifest: dict[str, Any], name: str) -> dict[str, Any]: - for certificate in manifest["certificates"]: - if certificate["name"] == name: - return certificate # type: ignore[no-any-return] - names = ", ".join(entry["name"] for entry in manifest["certificates"]) - raise SystemExit(f"Unknown certificate '{name}'. Available names: {names}") + for entry in manifest["certificates"]: + if entry["name"] == name: + return entry # type: ignore[no-any-return] + names = ", ".join(e["name"] for e in manifest["certificates"]) + raise SystemExit(f"Unknown certificate '{name}'. Available: {names}") # --------------------------------------------------------------------------- @@ -213,7 +301,6 @@ def find_certificate(manifest: dict[str, Any], name: str) -> dict[str, Any]: def _find_workspace_root(start: pathlib.Path) -> pathlib.Path | None: - """Walk upward from *start* looking for a Bazel workspace marker file.""" path = start.resolve() for _ in range(12): for marker in ("MODULE.bazel", "WORKSPACE.bazel", "WORKSPACE", ".git"): @@ -230,95 +317,448 @@ def write_slot_kv( root: pathlib.Path, defaults: dict[str, Any], entry: dict[str, Any], + cert_format: str = "pem", ) -> pathlib.Path: - """Write a KV slot descriptor alongside the generated certificate. - - The cert_path inside the descriptor uses a workspace-relative path so that - tests can reference the descriptor directly from the Bazel runfiles root - without copying or modifying it at runtime. - - Workspace root discovery is done via marker-file search (MODULE.bazel, - WORKSPACE.bazel, WORKSPACE, .git) so the path is computed correctly - regardless of whether Python is invoked via a WSL executable, a Windows - native interpreter, or a Unix shell from any working directory. - """ kv_path = slot_kv_path(root, defaults, entry) - pem_path = cert_path(root, defaults, entry) - + target = ( + cert_path(root, defaults, entry) + if cert_format == "pem" + else cert_der_path(root, defaults, entry) + ) workspace_root = _find_workspace_root(root) if workspace_root is not None: try: - cert_path_str = pem_path.resolve().relative_to(workspace_root).as_posix() + cert_path_str = target.resolve().relative_to(workspace_root).as_posix() except ValueError: - cert_path_str = pem_path.name + cert_path_str = target.name else: - # Last-resort fallback: use the filename only; the test must locate it - # relative to its own runfiles root. - cert_path_str = pem_path.name - + cert_path_str = target.name kv_path.write_text( - f"[certificate]\ncert_path = {cert_path_str}\ncert_format = pem\n", + f"[certificate]\ncert_path = {cert_path_str}\ncert_format = {cert_format}\n", encoding="utf-8", ) return kv_path +# --------------------------------------------------------------------------- +# Extension file helpers +# --------------------------------------------------------------------------- + + +def _resolve_ext_file( + root: pathlib.Path, + entry: dict[str, Any], +) -> pathlib.Path | None: + """Return the resolved ext_file path for an entry, or None if not set.""" + raw = entry.get("ext_file") + if not raw: + return None + p = root / raw + if not p.exists(): + raise SystemExit(f"ext_file not found: {p}") + return p + + +def _build_ext_conf( + tmp: pathlib.Path, + is_ca: bool, + ext_file: pathlib.Path | None, +) -> pathlib.Path: + """Write a temp extension conf and return its path. + + If ext_file is provided, its [v3_ext] content is used verbatim (the script + adds only the [req] wrapper needed by openssl req -x509). + If absent, a minimal sensible default is generated from is_ca. + """ + conf_path = tmp / "extensions.conf" + if ext_file is not None: + user_content = ext_file.read_text(encoding="utf-8") + # Ensure the user section is named [v3_ext] (already required by docs) + conf_path.write_text(user_content, encoding="utf-8") + else: + bc = "CA:TRUE" if is_ca else "CA:FALSE" + conf_path.write_text( + "[v3_ext]\n" + f"basicConstraints = critical,{bc}\n" + "subjectKeyIdentifier = hash\n" + "authorityKeyIdentifier = keyid\n", + encoding="utf-8", + ) + return conf_path + + +def _build_req_conf(tmp: pathlib.Path, ext_conf: pathlib.Path) -> pathlib.Path: + """Wrap an extension conf in a minimal [req] section for openssl req -x509.""" + req_conf = tmp / "req.conf" + ext_content = ext_conf.read_text(encoding="utf-8") + req_conf.write_text( + "[req]\ndistinguished_name = _dn\nx509_extensions = v3_ext\n\n[_dn]\n\n" + + ext_content, + encoding="utf-8", + ) + return req_conf + + # --------------------------------------------------------------------------- # Certificate generation # --------------------------------------------------------------------------- +def _write_chain_file( + root: pathlib.Path, + defaults: dict[str, Any], + entry: dict[str, Any], + manifest: dict[str, Any], +) -> pathlib.Path: + chain = cert_chain_path(root, defaults, entry) + parts: list[str] = [cert_path(root, defaults, entry).read_text(encoding="utf-8")] + current = entry + while current.get("signed_by"): + parent = find_certificate(manifest, current["signed_by"]) + parent_pem = cert_path(root, defaults, parent) + if not parent_pem.exists(): + print(f" Warning: ancestor '{current['signed_by']}' not found; chain truncated.") + break + parts.append(parent_pem.read_text(encoding="utf-8")) + current = parent + chain.write_text("".join(parts), encoding="utf-8") + return chain + + def generate_certificate( openssl: str, root: pathlib.Path, defaults: dict[str, Any], entry: dict[str, Any], update: bool, + cert_formats: list[str], + manifest: dict[str, Any], ) -> tuple[pathlib.Path, pathlib.Path]: - certificate_path = cert_path(root, defaults, entry) - private_key_path = key_path(root, defaults, entry) - certificate_path.parent.mkdir(parents=True, exist_ok=True) - private_key_path.parent.mkdir(parents=True, exist_ok=True) + """Generate or update a certificate. - if update and not private_key_path.exists(): - raise SystemExit( - f"Cannot update '{entry['name']}': private key is missing at {private_key_path}" - ) + Self-signed path (no signed_by): openssl req -x509. + Issued path (signed_by present): openssl req -new → openssl x509 -req. + + ext_file (optional manifest field): + Provide a file with a [v3_ext] stanza to override the default extensions. + The script wraps it in the boilerplate required by each OpenSSL command. + Omit for standard CA or leaf certs. + """ + pem = cert_path(root, defaults, entry) + der = cert_der_path(root, defaults, entry) + private_key = key_path(root, defaults, entry) + pem.parent.mkdir(parents=True, exist_ok=True) + private_key.parent.mkdir(parents=True, exist_ok=True) + + if update and not private_key.exists(): + raise SystemExit(f"Cannot update '{entry['name']}': key missing at {private_key}") + + signed_by_name: str | None = entry.get("signed_by") + is_ca: bool = bool(entry.get("is_ca", signed_by_name is None)) + ext_file = _resolve_ext_file(root, entry) key_alg: str = entry["key_algorithm"] subject: str = entry["subject"] days: int = entry_validity_days(defaults, entry) - - digest_flags = ["-sha256"] if uses_separate_digest(key_alg) else [] - arguments = [ - "req", - "-x509", - "-new", - *digest_flags, - "-nodes", - "-days", - str(days), - "-subj", - subject, - "-out", - str(certificate_path), - "-addext", - "basicConstraints=critical,CA:TRUE", - "-addext", - "subjectKeyIdentifier=hash", - "-addext", - "authorityKeyIdentifier=keyid", - ] - if update: - arguments.extend(["-key", str(private_key_path)]) + leaf_digest = ["-sha256"] if uses_separate_digest(key_alg) else [] + + if signed_by_name is not None: + # ------------------------------------------------------------------ # + # Issued certificate: CSR → sign with issuer CA # + # ------------------------------------------------------------------ # + issuer_entry = find_certificate(manifest, signed_by_name) + issuer_cert = cert_path(root, defaults, issuer_entry) + issuer_key = key_path(root, defaults, issuer_entry) + issuer_digest = ["-sha256"] if uses_separate_digest(issuer_entry["key_algorithm"]) else [] + + for p, label in [(issuer_cert, "issuer cert"), (issuer_key, "issuer key")]: + if not p.exists(): + raise SystemExit(f"{label} for '{signed_by_name}' not found at {p}. " + f"Run 'generate {signed_by_name}' first.") + + with tempfile.TemporaryDirectory() as tmpdir: + tmp = pathlib.Path(tmpdir) + csr = tmp / "request.csr" + ext_conf = _build_ext_conf(tmp, is_ca, ext_file) + + if update: + csr_args = ["req", "-new", *leaf_digest, "-nodes", + "-key", str(private_key), "-subj", subject, "-out", str(csr)] + else: + csr_args = ["req", "-new", *leaf_digest, "-nodes", + *newkey_args(key_alg), "-keyout", str(private_key), + "-subj", subject, "-out", str(csr)] + run_openssl(openssl, csr_args) + + run_openssl(openssl, [ + "x509", "-req", *issuer_digest, + "-in", str(csr), + "-CA", str(issuer_cert), "-CAkey", str(issuer_key), "-CAcreateserial", + "-days", str(days), + "-extfile", str(ext_conf), "-extensions", "v3_ext", + "-out", str(pem), + ]) else: - arguments.extend([*newkey_args(key_alg), "-keyout", str(private_key_path)]) + # ------------------------------------------------------------------ # + # Self-signed: openssl req -x509 # + # ------------------------------------------------------------------ # + with tempfile.TemporaryDirectory() as tmpdir: + tmp = pathlib.Path(tmpdir) + ext_conf = _build_ext_conf(tmp, is_ca, ext_file) + req_conf = _build_req_conf(tmp, ext_conf) + + args = [ + "req", "-x509", "-new", *leaf_digest, "-nodes", + "-days", str(days), "-subj", subject, + "-config", str(req_conf), + "-out", str(pem), + ] + if update: + args.extend(["-key", str(private_key)]) + else: + args.extend([*newkey_args(key_alg), "-keyout", str(private_key)]) + run_openssl(openssl, args) + + annotate_pem(openssl, pem) + entry["generated"] = parse_snapshot(openssl, pem) + + if entry.get("chain_file", False): + chain = _write_chain_file(root, defaults, entry, manifest) + print(f" Wrote chain: {chain}") + + if "der" in cert_formats: + run_openssl(openssl, ["x509", "-in", str(pem), "-outform", "DER", "-out", str(der)]) + + if "pem" not in cert_formats: + pem.unlink() + + primary_format = "pem" if "pem" in cert_formats else "der" + primary = pem if primary_format == "pem" else der + kv = write_slot_kv(root, defaults, entry, cert_format=primary_format) + return primary, kv + + +# --------------------------------------------------------------------------- +# CRL generation +# --------------------------------------------------------------------------- - run_openssl(openssl, arguments) - annotate_pem(openssl, certificate_path) - entry["generated"] = parse_snapshot(openssl, certificate_path) - kv = write_slot_kv(root, defaults, entry) - return certificate_path, kv + +def _extract_cert_info( + openssl: str, + cert: pathlib.Path, +) -> tuple[str, str, str]: + """Return (serial_upper, expiry_YYMMDDZ, subject_slash_dn) from a PEM cert.""" + serial = run_openssl(openssl, ["x509", "-in", str(cert), "-noout", "-serial"]) + serial_hex = serial.stdout.strip().split("=", 1)[1].upper() + + enddate = run_openssl(openssl, ["x509", "-in", str(cert), "-noout", "-enddate"]) + enddate_str = " ".join(enddate.stdout.strip().split("=", 1)[1].strip().split()) + dt = datetime_module.datetime.strptime(enddate_str, "%b %d %H:%M:%S %Y GMT") + expiry = dt.strftime("%y%m%d%H%M%SZ") + + subj = run_openssl(openssl, ["x509", "-in", str(cert), "-noout", "-subject", "-nameopt", "compat"]) + subject = subj.stdout.strip().split("subject=", 1)[1].strip() + + return serial_hex, expiry, subject + + +def _add_revoked_entry( + index_txt: pathlib.Path, + revoked_cert: pathlib.Path, + openssl: str, +) -> None: + """Append an R (revoked) entry to an OpenSSL CA index.txt.""" + serial, expiry, subject = _extract_cert_info(openssl, revoked_cert) + revdate = datetime_module.datetime.now(datetime_module.timezone.utc).strftime("%y%m%d%H%M%SZ") + with index_txt.open("a", encoding="utf-8") as f: + f.write(f"R\t{expiry}\t{revdate},unspecified\t{serial}\tunknown\t{subject}\n") + + +def _add_valid_entry( + index_txt: pathlib.Path, + cert: pathlib.Path, + openssl: str, +) -> None: + """Append a V (valid) entry to an OpenSSL CA index.txt.""" + serial, expiry, subject = _extract_cert_info(openssl, cert) + with index_txt.open("a", encoding="utf-8") as f: + # V entry: empty revocation date field + f.write(f"V\t{expiry}\t\t{serial}\tunknown\t{subject}\n") + + +def generate_crl( + openssl: str, + root: pathlib.Path, + defaults: dict[str, Any], + entry: dict[str, Any], + manifest: dict[str, Any], +) -> tuple[pathlib.Path, pathlib.Path]: + """Generate a CA-signed CRL; revokes serials listed in crl.revoked.""" + ca_cert = cert_path(root, defaults, entry) + ca_key = key_path(root, defaults, entry) + out_pem = crl_pem_path(root, defaults, entry) + out_der = crl_der_path(root, defaults, entry) + + for p, label in [(ca_cert, "CA cert"), (ca_key, "CA key")]: + if not p.exists(): + raise SystemExit(f"{label} not found at {p}. Run 'generate {entry['name']}' first.") + + revoked_names: list[str] = entry.get("crl", {}).get("revoked", []) + validity_days = entry_validity_days(defaults, entry) + + with tempfile.TemporaryDirectory() as tmpdir: + tmp = pathlib.Path(tmpdir) + (tmp / "newcerts").mkdir() + index_txt = tmp / "index.txt" + index_txt.touch() + (tmp / "serial").write_text("01\n", encoding="utf-8") + + conf = ( + "[ca]\ndefault_ca = CA_default\n\n" + "[CA_default]\n" + f"dir = {tmp}\n" + "database = $dir/index.txt\n" + "new_certs_dir = $dir/newcerts\n" + "serial = $dir/serial\n" + f"private_key = {ca_key}\n" + f"certificate = {ca_cert}\n" + "default_md = sha256\n" + f"default_crl_days = {validity_days}\n" + "preserve = no\n" + "policy = policy_anything\n\n" + "[policy_anything]\n" + + "\n".join(f"{f} = optional" for f in [ + "countryName", "stateOrProvinceName", "localityName", + "organizationName", "organizationalUnitName", "commonName", "emailAddress", + ]) + "\n" + ) + conf_path = tmp / "openssl.conf" + conf_path.write_text(conf, encoding="utf-8") + + for name in revoked_names: + rev_entry = find_certificate(manifest, name) + rev_cert = cert_path(root, defaults, rev_entry) + if not rev_cert.exists(): + raise SystemExit(f"Revoked cert '{name}' not found at {rev_cert}.") + _add_revoked_entry(index_txt, rev_cert, openssl) + print(f" Added revoked entry: {name} ({rev_cert.name})") + + run_openssl(openssl, ["ca", "-gencrl", "-config", str(conf_path), "-out", str(out_pem)]) + + annotate_crl_pem(openssl, out_pem) + run_openssl(openssl, ["crl", "-in", str(out_pem), "-inform", "PEM", "-outform", "DER", "-out", str(out_der)]) + return out_pem, out_der + + +# --------------------------------------------------------------------------- +# OCSP generation +# --------------------------------------------------------------------------- + + +def generate_ocsp_request( + openssl: str, + root: pathlib.Path, + defaults: dict[str, Any], + entry: dict[str, Any], + manifest: dict[str, Any], +) -> pathlib.Path: + """Generate a DER OCSP request for against its issuer CA. + + Produces: /.ocsp.req.der + Requires: signed_by is set and both cert + issuer cert are on disk. + """ + signed_by_name = entry.get("signed_by") + if not signed_by_name: + raise SystemExit( + f"'{entry['name']}' has no 'signed_by' — OCSP request requires a known issuer." + ) + + pem = cert_path(root, defaults, entry) + issuer_pem = cert_path(root, defaults, find_certificate(manifest, signed_by_name)) + out = ocsp_req_path(root, defaults, entry) + + for p, label in [(pem, "cert"), (issuer_pem, "issuer cert")]: + if not p.exists(): + raise SystemExit(f"{label} not found at {p}.") + + run_openssl(openssl, [ + "ocsp", + "-issuer", str(issuer_pem), + "-cert", str(pem), + "-reqout", str(out), + ]) + return out + + +def generate_ocsp_response( + openssl: str, + root: pathlib.Path, + defaults: dict[str, Any], + entry: dict[str, Any], + manifest: dict[str, Any], +) -> pathlib.Path: + """Generate a pre-computed DER OCSP response for . + + Manifest fields read from entry.ocsp: + signer name of the OCSP-signing cert (must carry OCSPSigning EKU) + status "good" | "revoked" (default "good") + validity_days response validity window in days (default 7) + + Produces: /.ocsp.resp.der + + The response is generated offline (no running OCSP responder) by building a + temporary CA index.txt with the cert's status and calling + ``openssl ocsp -rsigner ... -index ...``. + """ + ocsp_cfg: dict[str, Any] = entry.get("ocsp", {}) + signer_name: str | None = ocsp_cfg.get("signer") + status: str = ocsp_cfg.get("status", "good") + validity_days: int = int(ocsp_cfg.get("validity_days", 7)) + + if not signer_name: + raise SystemExit( + f"'{entry['name']}' missing ocsp.signer — OCSP response requires a signing cert." + ) + signed_by_name = entry.get("signed_by") + if not signed_by_name: + raise SystemExit(f"'{entry['name']}' has no 'signed_by' — cannot determine issuer.") + + pem = cert_path(root, defaults, entry) + issuer_pem = cert_path(root, defaults, find_certificate(manifest, signed_by_name)) + signer_entry = find_certificate(manifest, signer_name) + signer_pem = cert_path(root, defaults, signer_entry) + signer_key = key_path(root, defaults, signer_entry) + out = ocsp_resp_path(root, defaults, entry) + + for p, label in [ + (pem, "cert"), (issuer_pem, "issuer"), (signer_pem, "OCSP signer"), (signer_key, "OCSP signer key"), + ]: + if not p.exists(): + raise SystemExit(f"{label} not found at {p}.") + + with tempfile.TemporaryDirectory() as tmpdir: + tmp = pathlib.Path(tmpdir) + index_txt = tmp / "index.txt" + index_txt.touch() + + if status == "revoked": + _add_revoked_entry(index_txt, pem, openssl) + else: + _add_valid_entry(index_txt, pem, openssl) + + run_openssl(openssl, [ + "ocsp", + "-issuer", str(issuer_pem), + "-cert", str(pem), + "-rsigner", str(signer_pem), + "-rkey", str(signer_key), + "-CA", str(issuer_pem), + "-ndays", str(validity_days), + "-index", str(index_txt), + "-respout", str(out), + ]) + + return out # --------------------------------------------------------------------------- @@ -327,32 +767,73 @@ def generate_certificate( def main() -> None: - parser = argparse.ArgumentParser(description=__doc__) - parser.add_argument("action", choices=["generate", "update"]) + parser = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter) + parser.add_argument( + "action", + choices=["generate", "update", "crl", "ocsp-req", "ocsp-resp"], + ) parser.add_argument("name", help="Manifest certificate name") parser.add_argument( "--manifest", type=pathlib.Path, default=pathlib.Path(__file__).with_name("certificate_manifest.json"), + help="Path to the manifest.json for the target folder (default: adjacent certificate_manifest.json)", ) parser.add_argument("--openssl", default="openssl", help="OpenSSL executable") + parser.add_argument( + "--cert-format", + dest="cert_format", + choices=["pem", "der", "both"], + default=None, + help=( + "Output format(s) for the certificate (generate/update only). " + "Overrides the entry's cert_formats manifest field. " + "'both' writes PEM and DER side by side." + ), + ) args = parser.parse_args() manifest_path = args.manifest.resolve() manifest = load_manifest(manifest_path) defaults: dict[str, Any] = manifest.get("defaults", {}) entry = find_certificate(manifest, args.name) - certificate_path, kv_path = generate_certificate( - args.openssl, - manifest_path.parent, - defaults, - entry, - update=args.action == "update", - ) - save_manifest(manifest_path, manifest) - print(f"Wrote {certificate_path}") - print(f"Wrote {kv_path}") - print(json.dumps(entry["generated"], indent=2)) + root = manifest_path.parent + + if args.action == "crl": + if args.cert_format is not None: + parser.error("--cert-format is only valid for generate/update") + pem_out, der_out = generate_crl(args.openssl, root, defaults, entry, manifest) + print(f"Wrote {pem_out}") + print(f"Wrote {der_out}") + + elif args.action == "ocsp-req": + if args.cert_format is not None: + parser.error("--cert-format is only valid for generate/update") + out = generate_ocsp_request(args.openssl, root, defaults, entry, manifest) + print(f"Wrote {out}") + + elif args.action == "ocsp-resp": + if args.cert_format is not None: + parser.error("--cert-format is only valid for generate/update") + out = generate_ocsp_response(args.openssl, root, defaults, entry, manifest) + print(f"Wrote {out}") + + else: # generate / update + cert_formats = resolve_cert_formats(defaults, entry, args.cert_format) + primary, kv = generate_certificate( + args.openssl, root, defaults, entry, + update=args.action == "update", + cert_formats=cert_formats, + manifest=manifest, + ) + save_manifest(manifest_path, manifest) + print(f"Wrote {primary}") + if "der" in cert_formats: + der = cert_der_path(root, defaults, entry) + if der.exists(): + print(f"Wrote {der}") + print(f"Wrote {kv}") + print(json.dumps(entry["generated"], indent=2)) if __name__ == "__main__": diff --git a/score/tests/test_vectors/certificate/ml_dsa_44_slot.kv b/score/tests/test_vectors/certificate/ml_dsa_44_slot.kv deleted file mode 100644 index 5ce349503..000000000 --- a/score/tests/test_vectors/certificate/ml_dsa_44_slot.kv +++ /dev/null @@ -1,3 +0,0 @@ -[certificate] -cert_path = score/tests/test_vectors/certificate/ml_dsa_44.pem -cert_format = pem diff --git a/score/tests/test_vectors/certificate/ml_dsa_65_slot.kv b/score/tests/test_vectors/certificate/ml_dsa_65_slot.kv deleted file mode 100644 index dc6c6423a..000000000 --- a/score/tests/test_vectors/certificate/ml_dsa_65_slot.kv +++ /dev/null @@ -1,3 +0,0 @@ -[certificate] -cert_path = score/tests/test_vectors/certificate/ml_dsa_65.pem -cert_format = pem diff --git a/score/tests/test_vectors/certificate/ml_dsa_87_slot.kv b/score/tests/test_vectors/certificate/ml_dsa_87_slot.kv deleted file mode 100644 index d8df213b3..000000000 --- a/score/tests/test_vectors/certificate/ml_dsa_87_slot.kv +++ /dev/null @@ -1,3 +0,0 @@ -[certificate] -cert_path = score/tests/test_vectors/certificate/ml_dsa_87.pem -cert_format = pem diff --git a/score/tests/test_vectors/certificate/pki_chain/manifest.json b/score/tests/test_vectors/certificate/pki_chain/manifest.json new file mode 100644 index 000000000..282e33136 --- /dev/null +++ b/score/tests/test_vectors/certificate/pki_chain/manifest.json @@ -0,0 +1,67 @@ +{ + "schema_version": 2, + "description": "Three-level PKI: root CA -> intermediate CA -> leaf. For chain-verification and OCSP tests. No certs committed yet — generate in order below.", + "defaults": { + "cert_dir": ".", + "key_dir": "private", + "validity_days": 3650 + }, + "certificates": [ + { + "name": "root_ca", + "purpose": "Self-signed root CA; issues intermediate_ca and the root CRL", + "subject": "/CN=Test-Root-CA/O=Eclipse", + "key_algorithm": "RSA-2048", + "crl": { + "revoked": ["intermediate_ca"] + } + }, + { + "name": "ocsp_signer", + "purpose": "OCSP signing certificate issued by root_ca; used to sign OCSP responses", + "subject": "/CN=Test-OCSP-Signer/O=Eclipse", + "key_algorithm": "RSA-2048", + "signed_by": "root_ca", + "is_ca": false, + "ext_file": "ocsp_signer.ext.conf", + "_note": "ext_file adds extendedKeyUsage = OCSPSigning — required for OCSP response signing" + }, + { + "name": "intermediate_ca", + "purpose": "Intermediate CA issued by root_ca; issues leaf and the intermediate CRL", + "subject": "/CN=Test-Intermediate-CA/O=Eclipse", + "key_algorithm": "RSA-2048", + "signed_by": "root_ca", + "is_ca": true, + "chain_file": true, + "crl": { + "revoked": ["leaf"] + } + }, + { + "name": "leaf", + "purpose": "End-entity cert issued by intermediate_ca; subject of OCSP request/response", + "subject": "/CN=Test-Leaf/O=Eclipse", + "key_algorithm": "RSA-2048", + "signed_by": "intermediate_ca", + "is_ca": false, + "cert_formats": ["pem", "der"], + "chain_file": true, + "ocsp": { + "signer": "ocsp_signer", + "status": "good", + "validity_days": 7 + } + } + ], + "_generation_order": [ + "python ../generate_certificates.py generate root_ca --manifest manifest.json", + "python ../generate_certificates.py generate ocsp_signer --manifest manifest.json", + "python ../generate_certificates.py generate intermediate_ca --manifest manifest.json", + "python ../generate_certificates.py generate leaf --manifest manifest.json", + "python ../generate_certificates.py crl root_ca --manifest manifest.json", + "python ../generate_certificates.py crl intermediate_ca --manifest manifest.json", + "python ../generate_certificates.py ocsp-req leaf --manifest manifest.json", + "python ../generate_certificates.py ocsp-resp leaf --manifest manifest.json" + ] +} diff --git a/score/tests/test_vectors/certificate/pki_chain/ocsp_signer.ext.conf b/score/tests/test_vectors/certificate/pki_chain/ocsp_signer.ext.conf new file mode 100644 index 000000000..8345c2430 --- /dev/null +++ b/score/tests/test_vectors/certificate/pki_chain/ocsp_signer.ext.conf @@ -0,0 +1,6 @@ +[v3_ext] +basicConstraints = critical,CA:FALSE +subjectKeyIdentifier = hash +authorityKeyIdentifier = keyid +extendedKeyUsage = OCSPSigning +noCheck = ignored diff --git a/score/tests/test_vectors/certificate/rsa_3072_slot.kv b/score/tests/test_vectors/certificate/rsa_3072_slot.kv deleted file mode 100644 index e73cf47f7..000000000 --- a/score/tests/test_vectors/certificate/rsa_3072_slot.kv +++ /dev/null @@ -1,3 +0,0 @@ -[certificate] -cert_path = score/tests/test_vectors/certificate/rsa_3072.pem -cert_format = pem diff --git a/score/tests/test_vectors/certificate/rsa_4096_slot.kv b/score/tests/test_vectors/certificate/rsa_4096_slot.kv deleted file mode 100644 index 41bb6d61d..000000000 --- a/score/tests/test_vectors/certificate/rsa_4096_slot.kv +++ /dev/null @@ -1,3 +0,0 @@ -[certificate] -cert_path = score/tests/test_vectors/certificate/rsa_4096.pem -cert_format = pem From 5f57e7492de55d82c35cb81c99962b9b8f60435c Mon Sep 17 00:00:00 2001 From: Athul Mallappallil Date: Tue, 8 Sep 2026 21:54:59 +0000 Subject: [PATCH 34/37] certslot manager impl --- score/crypto/src/daemon/cert_management/BUILD | 1 + .../cert_management_module.cpp | 8 +- .../core/cert_management_service.cpp | 67 ++- .../core/cert_management_service.hpp | 39 +- .../cert_management/core/cert_registry.cpp | 63 +-- .../cert_management/core/cert_registry.hpp | 8 +- .../architecture/cert_management_static.puml | 26 +- .../architecture/component_architecture.rst | 19 +- .../cert_management_dynamic.puml | 75 +++- .../docs/detailed_design/detailed_design.rst | 24 +- .../src/daemon/cert_management/docs/index.rst | 14 +- .../interfaces/access_policy.hpp | 11 +- .../query/cert_object_serializer.cpp | 13 +- .../query/cert_object_serializer.hpp | 13 +- .../slot/cert_slot_manager.cpp | 389 ++++++++++++++++++ .../slot/cert_slot_manager.hpp | 183 ++++++++ .../src/daemon/cert_management/tests/BUILD | 1 + .../tests/core/test_cert_registry.cpp | 35 +- ...est_certificate_management_integration.cpp | 135 +++++- .../query/test_cert_object_serializer.cpp | 100 +++-- .../service/test_cert_management_service.cpp | 47 ++- .../truststore/test_trust_store_manager.cpp | 82 ++-- .../config_driven_trust_store_catalog.cpp | 4 +- .../config_driven_trust_store_catalog.hpp | 11 +- .../truststore/trust_store_manager.cpp | 37 +- .../truststore/trust_store_manager.hpp | 15 +- 26 files changed, 1120 insertions(+), 300 deletions(-) create mode 100644 score/crypto/src/daemon/cert_management/slot/cert_slot_manager.cpp create mode 100644 score/crypto/src/daemon/cert_management/slot/cert_slot_manager.hpp diff --git a/score/crypto/src/daemon/cert_management/BUILD b/score/crypto/src/daemon/cert_management/BUILD index 95792fed6..37caf2df6 100644 --- a/score/crypto/src/daemon/cert_management/BUILD +++ b/score/crypto/src/daemon/cert_management/BUILD @@ -66,6 +66,7 @@ cc_library( "slot/deployment_loader.cpp", "slot/deployment_writer.cpp", "slot/file_backed_slot_handler.cpp", + "slot/cert_slot_manager.cpp", "slot/slot_registry.cpp", "truststore/trust_store_handler.cpp", "truststore/trust_store_manager.cpp", diff --git a/score/crypto/src/daemon/cert_management/cert_management_module.cpp b/score/crypto/src/daemon/cert_management/cert_management_module.cpp index 2c2f39c5b..6666f0131 100644 --- a/score/crypto/src/daemon/cert_management/cert_management_module.cpp +++ b/score/crypto/src/daemon/cert_management/cert_management_module.cpp @@ -12,6 +12,7 @@ ********************************************************************************/ #include "score/crypto/src/daemon/cert_management/cert_management_module.hpp" +#include "score/crypto/src/daemon/cert_management/slot/cert_slot_manager.hpp" #include "score/crypto/src/daemon/cert_management/slot/config_driven_slot_catalog.hpp" #include "score/crypto/src/daemon/cert_management/slot/file_backed_slot_handler.hpp" #include "score/crypto/src/daemon/cert_management/truststore/config_driven_trust_store_catalog.hpp" @@ -68,9 +69,12 @@ CertManagementModule::Sptr CertManagementModule::Create(data_manager::IDataManag return nullptr; return provider->GetCertSlotHandler(slot, cert_parser); }; - trust_catalog.Load(*module->m_trust_stores, module->m_slots, slot_handler_factory); + + auto slot_manager = std::make_shared(module->m_slots, std::move(slot_handler_factory)); + + trust_catalog.Load(*module->m_trust_stores, module->m_slots, slot_manager); module->m_service = std::make_shared( - std::move(data_manager), module->m_slots, module->m_trust_stores, std::move(slot_handler_factory)); + std::move(data_manager), module->m_slots, module->m_trust_stores, slot_manager); if (module->m_provider_manager) { module->m_provider_manager->ForEachProvider([&](const auto& /*id*/, const auto& provider) { diff --git a/score/crypto/src/daemon/cert_management/core/cert_management_service.cpp b/score/crypto/src/daemon/cert_management/core/cert_management_service.cpp index 376ddf823..5bdd4927d 100644 --- a/score/crypto/src/daemon/cert_management/core/cert_management_service.cpp +++ b/score/crypto/src/daemon/cert_management/core/cert_management_service.cpp @@ -22,11 +22,11 @@ using Error = common::DaemonErrorCode; CertManagementService::CertManagementService(data_manager::IDataManager::Sptr data_manager, CertSlotRegistry::Sptr slot_registry, TrustStoreManager::Sptr trust_store_manager, - CertSlotHandlerFactory slot_handler_factory) + CertSlotManager::Sptr slot_manager) : m_data_manager{std::move(data_manager)}, m_slot_registry{std::move(slot_registry)}, m_trust_stores{std::move(trust_store_manager)}, - m_slot_handler_factory{std::move(slot_handler_factory)} + m_slot_manager{std::move(slot_manager)} { } @@ -39,13 +39,6 @@ score::crypto::Expected CertManagementService::Regist auto entry = std::make_shared(std::move(object), params.slot_handle); auto id = params.slot_handle.IsValid() ? m_cert_registry.RegisterSlotCert(params.slot_handle, entry) : m_cert_registry.RegisterEphemeralCert(entry); - if (id == 0U) - { - id = m_cert_registry.FindSlotRegistryId(params.slot_handle); - entry = m_cert_registry.FindById(id); - if (!entry) - return score::crypto::make_unexpected(Error::kInternalError); - } auto captured = entry; auto node = std::make_shared(entry, id, params.client_id, [this](CertRegistryId rid) { m_cert_registry.Unregister(rid); @@ -56,24 +49,11 @@ score::crypto::Expected CertManagementService::Regist return CertDataNodeResult{*node_id, std::move(captured)}; } -score::crypto::Expected CertManagementService::LoadOrShare( - const CertRegistrationParams& params, - ICertSlotHandler& handler, - const CertSlotConfig& config) +score::crypto::Expected CertManagementService::Load(const CertRegistrationParams& params) { - auto existing = m_cert_registry.FindBySlot(params.slot_handle); - if (existing) - { - const auto id = m_cert_registry.FindSlotRegistryId(params.slot_handle); - auto node = std::make_shared(existing, id, params.client_id, [this](CertRegistryId rid) { - m_cert_registry.Unregister(rid); - }); - auto node_id = m_data_manager->addChildNode(params.client_id, params.parent_id, node); - if (!node_id) - return score::crypto::make_unexpected(Error::kInternalError); - return CertDataNodeResult{*node_id, std::move(existing)}; - } - auto loaded = handler.LoadCertificate(config); + if (!m_slot_manager) + return score::crypto::make_unexpected(Error::kInternalError); + auto loaded = m_slot_manager->LoadCertificate(params.slot_handle, params.client_id); if (!loaded) return score::crypto::make_unexpected(loaded.error()); return RegisterCertMaterial(params, std::move(*loaded)); @@ -107,22 +87,33 @@ score::crypto::Expected CertManagementService:: if (!m_data_manager || !m_slot_registry) return score::crypto::make_unexpected(Error::kInternalError); - // Dedup: same client resolving the same slot twice returns the cached node id. - auto& client_cache = m_cert_slot_node_cache[client_id]; - const auto it = client_cache.find(resource_name); - if (it != client_cache.end()) - return it->second; - auto handle_res = m_slot_registry->ResolveAppResource(resource_name, client_id); if (!handle_res.has_value()) return score::crypto::make_unexpected(handle_res.error()); - auto node = std::make_shared(handle_res.value(), m_slot_registry); + return ResolveCertSlot(handle_res.value(), client_id); +} + +score::crypto::Expected CertManagementService::ResolveCertSlot( + CertSlotHandle slot_handle, + data_manager::ClientId client_id) +{ + if (!m_data_manager || !m_slot_registry) + return score::crypto::make_unexpected(Error::kInternalError); + if (!m_slot_registry->GetConfig(slot_handle).has_value()) + return score::crypto::make_unexpected(Error::kInvalidResourceId); + + auto& client_cache = m_cert_slot_node_cache[client_id]; + const auto it = client_cache.find(slot_handle.index); + if (it != client_cache.end()) + return it->second; + + auto node = std::make_shared(slot_handle, m_slot_registry); auto node_id = m_data_manager->addNode(client_id, std::move(node)); if (!node_id.has_value()) return score::crypto::make_unexpected(Error::kInternalError); - client_cache[resource_name] = node_id.value(); + client_cache[slot_handle.index] = node_id.value(); return node_id.value(); } @@ -177,13 +168,7 @@ score::crypto::Expected CertManagementService::ResolveS if (!config_res.has_value()) return score::crypto::make_unexpected(config_res.error()); - if (!m_slot_handler_factory) - return score::crypto::make_unexpected(Error::kInternalError); - auto handler = m_slot_handler_factory(*config_res.value()); - if (!handler) - return score::crypto::make_unexpected(Error::kInternalError); - - return ResolvedCertSlot{handle, config_res.value(), std::move(handler)}; + return ResolvedCertSlot{handle, config_res.value()}; } score::crypto::Expected CertManagementService::ResolveCertForOperation( diff --git a/score/crypto/src/daemon/cert_management/core/cert_management_service.hpp b/score/crypto/src/daemon/cert_management/core/cert_management_service.hpp index 9b1136b3b..d45dc0d52 100644 --- a/score/crypto/src/daemon/cert_management/core/cert_management_service.hpp +++ b/score/crypto/src/daemon/cert_management/core/cert_management_service.hpp @@ -15,8 +15,8 @@ #include "score/crypto/src/api/common/types.hpp" #include "score/crypto/src/daemon/cert_management/core/cert_registry.hpp" -#include "score/crypto/src/daemon/cert_management/interfaces/i_cert_slot_handler.hpp" #include "score/crypto/src/daemon/cert_management/nodes/cert_data_node.hpp" +#include "score/crypto/src/daemon/cert_management/slot/cert_slot_manager.hpp" #include "score/crypto/src/daemon/cert_management/truststore/trust_store_manager.hpp" #include "score/crypto/src/daemon/data_manager/i_data_manager.hpp" @@ -33,12 +33,14 @@ struct CertDataNodeResult std::shared_ptr entry; }; -/// Resolved certificate slot — handler + config + handle returned by ResolveSlotForOperation. +/// Resolved certificate slot — handle + config returned by ResolveSlotForOperation. +/// +/// All slot backend operations must go through +/// CertManagementService::GetSlotManager() which enforces access policy centrally. struct ResolvedCertSlot { CertSlotHandle handle; const CertSlotConfig* config{nullptr}; - ICertSlotHandler::Sptr handler; }; class CertManagementService final @@ -49,7 +51,7 @@ class CertManagementService final CertManagementService(data_manager::IDataManager::Sptr data_manager, CertSlotRegistry::Sptr slot_registry, TrustStoreManager::Sptr trust_store_manager, - CertSlotHandlerFactory slot_handler_factory = {}); + CertSlotManager::Sptr slot_manager = {}); // ----------------------------------------------------------------------- // Existing cert lifecycle methods @@ -58,9 +60,13 @@ class CertManagementService final score::crypto::Expected RegisterCertMaterial( const CertRegistrationParams&, CertObject::Sptr); - score::crypto::Expected LoadOrShare(const CertRegistrationParams&, - ICertSlotHandler&, - const CertSlotConfig&); + /// Load a certificate from a slot and create a fresh CertEntry for this client. + /// + /// Each call produces an independent CertEntry so that per-client state + /// (e.g. session-scoped CRL) is never shared across clients. CertSlotManager + /// returns a cached CertObject when another client loaded the same slot recently, + /// avoiding a redundant disk read without sharing the wrapping CertEntry. + score::crypto::Expected Load(const CertRegistrationParams& params); score::crypto::Expected ReleaseCert(data_manager::ClientId, data_manager::DataNodeId); void CleanupClient(data_manager::ClientId); @@ -70,6 +76,11 @@ class CertManagementService final return m_trust_stores; } + CertSlotManager::Sptr GetSlotManager() const + { + return m_slot_manager; + } + // ----------------------------------------------------------------------- // Resource resolution — called by mediator resource resolvers // ----------------------------------------------------------------------- @@ -82,6 +93,10 @@ class CertManagementService final const std::string& resource_name, data_manager::ClientId client_id); + score::crypto::Expected ResolveCertSlot( + CertSlotHandle slot_handle, + data_manager::ClientId client_id); + /// Resolve an application trust store resource name to a client-scoped DataNodeId. /// /// Creates a TrustStoreDataNode under the client root in the data manager. @@ -141,13 +156,13 @@ class CertManagementService final data_manager::IDataManager::Sptr m_data_manager; CertSlotRegistry::Sptr m_slot_registry; TrustStoreManager::Sptr m_trust_stores; + CertSlotManager::Sptr m_slot_manager; CertRegistry m_cert_registry; - CertSlotHandlerFactory m_slot_handler_factory; - // Node-id dedup caches — keyed by (client_id, resource_name). - // Mirrors the key-slot dedup pattern so that resolving the same resource - // twice from the same client reuses the existing DataNodeId. - std::unordered_map> + // Slot node cache keyed by canonical slot identity, not app-resource name. + // Multiple resource names may map to one slot and must share one node ID + // within a client. + std::unordered_map> m_cert_slot_node_cache; std::unordered_map> m_trust_store_node_cache; diff --git a/score/crypto/src/daemon/cert_management/core/cert_registry.cpp b/score/crypto/src/daemon/cert_management/core/cert_registry.cpp index bf184d89c..bba1b7691 100644 --- a/score/crypto/src/daemon/cert_management/core/cert_registry.cpp +++ b/score/crypto/src/daemon/cert_management/core/cert_registry.cpp @@ -25,21 +25,11 @@ namespace constexpr std::string_view kLogPrefix = "[CERT_REGISTRY] "; } // namespace -CertRegistryId CertRegistry::RegisterSlotCert(CertSlotHandle slot_handle, std::shared_ptr cert_entry) +CertRegistryId CertRegistry::RegisterSlotCert(CertSlotHandle /*slot_handle*/, std::shared_ptr cert_entry) { const std::lock_guard lock(m_mutex); - - if (m_slot_to_id.count(slot_handle.index) != 0U) - { - score::mw::log::LogError() << kLogPrefix << "RegisterSlotCert: slot " << slot_handle.index - << " already registered"; - return 0U; - } - const CertRegistryId id = m_next_id++; m_certs.emplace(id, std::move(cert_entry)); - m_slot_to_id.emplace(slot_handle.index, id); - return id; } @@ -53,38 +43,6 @@ CertRegistryId CertRegistry::RegisterEphemeralCert(std::shared_ptr ce return id; } -std::shared_ptr CertRegistry::FindBySlot(CertSlotHandle slot_handle) const -{ - const std::lock_guard lock(m_mutex); - - const auto slot_it = m_slot_to_id.find(slot_handle.index); - if (slot_it == m_slot_to_id.end()) - { - return nullptr; - } - - const auto cert_it = m_certs.find(slot_it->second); - if (cert_it == m_certs.end()) - { - return nullptr; - } - - return cert_it->second; -} - -CertRegistryId CertRegistry::FindSlotRegistryId(CertSlotHandle slot_handle) const -{ - const std::lock_guard lock(m_mutex); - - const auto slot_it = m_slot_to_id.find(slot_handle.index); - if (slot_it == m_slot_to_id.end()) - { - return 0U; - } - - return slot_it->second; -} - std::shared_ptr CertRegistry::FindById(CertRegistryId id) const { const std::lock_guard lock(m_mutex); @@ -107,16 +65,6 @@ bool CertRegistry::Unregister(CertRegistryId id) { return false; } - - for (auto slot_it = m_slot_to_id.begin(); slot_it != m_slot_to_id.end(); ++slot_it) - { - if (slot_it->second == id) - { - m_slot_to_id.erase(slot_it); - break; - } - } - m_certs.erase(it); return true; } @@ -137,15 +85,6 @@ void CertRegistry::CleanupClient(data_manager::ClientId client_id) for (const auto id : to_remove) { - for (auto slot_it = m_slot_to_id.begin(); slot_it != m_slot_to_id.end(); ++slot_it) - { - if (slot_it->second == id) - { - m_slot_to_id.erase(slot_it); - break; - } - } - score::mw::log::LogDebug() << kLogPrefix << "CleanupClient: removing cert " << id << " (ref_count reached 0 after client " << client_id << " cleanup)"; m_certs.erase(id); diff --git a/score/crypto/src/daemon/cert_management/core/cert_registry.hpp b/score/crypto/src/daemon/cert_management/core/cert_registry.hpp index a42037e55..2cce2734e 100644 --- a/score/crypto/src/daemon/cert_management/core/cert_registry.hpp +++ b/score/crypto/src/daemon/cert_management/core/cert_registry.hpp @@ -56,8 +56,9 @@ class CertRegistry final /// Register a certificate that was loaded from a persistent slot. /// - /// Returns 0 if the slot is already registered (caller should call - /// FindBySlot() first to check). + /// Every call creates a new registry entry regardless of the slot; callers + /// that load the same slot for different clients each receive an independent + /// CertEntry so that per-client state (e.g. session CRL) cannot bleed. [[nodiscard]] CertRegistryId RegisterSlotCert(CertSlotHandle slot_handle, std::shared_ptr cert_entry); /// Register an ephemeral (non-slot) certificate. @@ -67,8 +68,6 @@ class CertRegistry final // Lookup // ------------------------------------------------------------------ - [[nodiscard]] std::shared_ptr FindBySlot(CertSlotHandle slot_handle) const; - [[nodiscard]] CertRegistryId FindSlotRegistryId(CertSlotHandle slot_handle) const; [[nodiscard]] std::shared_ptr FindById(CertRegistryId id) const; // ------------------------------------------------------------------ @@ -93,7 +92,6 @@ class CertRegistry final private: mutable std::mutex m_mutex; std::unordered_map> m_certs; - std::unordered_map m_slot_to_id; CertRegistryId m_next_id{1U}; }; diff --git a/score/crypto/src/daemon/cert_management/docs/architecture/cert_management_static.puml b/score/crypto/src/daemon/cert_management/docs/architecture/cert_management_static.puml index 0a73d7cfd..6e678d6bf 100644 --- a/score/crypto/src/daemon/cert_management/docs/architecture/cert_management_static.puml +++ b/score/crypto/src/daemon/cert_management/docs/architecture/cert_management_static.puml @@ -20,6 +20,7 @@ package "cert_management" { package "core" { [CertManagementService] [CertRegistry] + [CertEntry] } package "interfaces" { interface ICertSlotHandler @@ -33,6 +34,7 @@ package "cert_management" { [TrustStoreDataNode] } package "slot" { + [CertSlotManager] [CertSlotRegistry] [ConfigDrivenSlotCatalog] [FileBackedSlotHandler] @@ -66,20 +68,38 @@ package "daemon/mediator" { [Mediator] } +note right of [CertSlotManager] + Handler cache (Sptr, per slot) + CertObjectCache (weak_ptr, per slot) + Access policy enforcement +end note + +note right of [CertEntry] + Per-client (never shared) + Holds CertObject::Sptr + + session-scoped CRL +end note + [CertManagementService] -> [CertRegistry] +[CertManagementService] --> [CertSlotManager] [CertManagementService] --> [CertSlotRegistry] [CertManagementService] --> [TrustStoreManager] -[CertManagementService] -->[DataManager] -[CertManagementService] --> [AccessPolicyEnforcer] +[CertManagementService] --> [DataManager] +[CertRegistry] --> [CertEntry] +[CertEntry] --> [CertObject] [CertSlotDataNode] <.. [CertManagementService] : creates [CertDataNode] <.. [CertManagementService] : creates [TrustStoreDataNode] <.. [CertManagementService] : creates +[CertSlotManager] --> ICertSlotHandler +[CertSlotManager] --> [CertSlotRegistry] +[CertSlotManager] --> [AccessPolicyEnforcer] + [CertSlotRegistry] <-- [ConfigDrivenSlotCatalog] [ConfigDrivenTrustStoreCatalog] ---> [TrustStoreManager] [TrustStoreManager] --> [CertSlotRegistry] [TrustStoreManager] --> [TrustStoreHandler] -[TrustStoreManager] --> ICertSlotHandler +[TrustStoreManager] --> [CertSlotManager] : anchor loading [TrustStoreHandler] ..|> ITrustStoreHandler [FileBackedSlotHandler] ..|> ICertSlotHandler [FileBackedSlotHandler] --> [CrlHandler] diff --git a/score/crypto/src/daemon/cert_management/docs/architecture/component_architecture.rst b/score/crypto/src/daemon/cert_management/docs/architecture/component_architecture.rst index 969134cb5..f34b48ff5 100644 --- a/score/crypto/src/daemon/cert_management/docs/architecture/component_architecture.rst +++ b/score/crypto/src/daemon/cert_management/docs/architecture/component_architecture.rst @@ -71,8 +71,14 @@ Runtime boundaries Resource resolution is client-scoped through the Data Manager. A resolved certificate slot or trust store is represented by a lightweight DataNode. A -certificate loaded from a slot becomes a ``CertDataNode`` backed by a shared -``CertEntry``. Trust-store anchor content is loaded lazily and cached by +certificate loaded from a slot becomes a ``CertDataNode`` backed by a +per-client ``CertEntry``. Each ``Load`` call produces an independent +``CertEntry`` so that per-client state — such as a session-scoped CRL +associated via ``ImportCrl(persist=false)`` — cannot bleed across clients. +``CertSlotManager`` holds a weak-ptr cache of ``CertObject`` instances keyed +by slot: when the cache entry is live, multiple clients share the same parsed +bytes without redundant I/O; when it expires, the next load re-reads the slot. +Trust-store anchor content is loaded lazily and cached separately by ``TrustStoreManager``. The current provider boundary is intentionally narrow: @@ -116,9 +122,12 @@ Runtime flows During startup, the configuration adapter registers certificate slots and trust stores. A client resolves an application resource into a DataManager -node. Loading a certificate resolves its slot handler, reads the deployment -descriptor and payload, parses the bytes, and registers a shared -``CertEntry``. Repeated loads of the same slot share the registry entry. +node. Loading a certificate goes through ``CertSlotManager``, which checks +its weak-ptr ``CertObjectCache`` first: on a hit the parsed bytes are returned +without disk I/O; on a miss the slot handler reads and parses the certificate +and the result is stored in the cache. A fresh ``CertEntry`` is created for +each caller and registered in ``CertRegistry``; no entry is shared across +clients. Trust-store anchors are loaded lazily. ``TrustStoreManager`` resolves each typed member slot and caches the resulting ``CertObject`` through a weak diff --git a/score/crypto/src/daemon/cert_management/docs/detailed_design/cert_management_dynamic.puml b/score/crypto/src/daemon/cert_management/docs/detailed_design/cert_management_dynamic.puml index 2f4d4cc7c..5fa0a7fad 100644 --- a/score/crypto/src/daemon/cert_management/docs/detailed_design/cert_management_dynamic.puml +++ b/score/crypto/src/daemon/cert_management/docs/detailed_design/cert_management_dynamic.puml @@ -11,58 +11,113 @@ ' SPDX-License-Identifier: Apache-2.0 ' ******************************************************************************* -@startuml +@startuml cert_management_dynamic participant "Flat-config parser" as Config participant "ConfigDrivenSlotCatalog" as SlotCatalog participant "CertSlotRegistry" as Slots participant "ConfigDrivenTrustStoreCatalog" as TrustCatalog participant "TrustStoreManager" as Stores participant "CertManagementService" as Service +participant "CertSlotManager" as SlotMgr participant "FileBackedSlotHandler" as Handler participant "OpenSslCertParser" as Parser +participant "CertObjectCache\n(weak_ptr, per slot)" as Cache +participant "CertRegistry" as Registry participant "TrustStoreHandler" as StoreHandler participant "DataManager" as Data +== Startup: slot and trust-store registration == + Config -> SlotCatalog : CertificateConfig SlotCatalog -> Slots : RegisterSlot(config) Config -> TrustCatalog : CertificateConfig TrustCatalog -> Stores : Load(store configs, registry, factory) TrustCatalog -> Stores : RegisterAppResource() +== Slot resolution == + Service -> Slots : ResolveAppResource(resource, client) Slots --> Service : CertSlotHandle Service -> Data : addNode(CertSlotDataNode) Data --> Service : slot_node_id -Service -> Handler : LoadCertificate(slot config) +== Certificate load — first client (cache miss) == + +Service -> SlotMgr : Load(params) +SlotMgr -> Cache : lookup(slot.index) +Cache --> SlotMgr : miss (empty or expired) +SlotMgr -> Handler : LoadCertificate(slot config) Handler -> Parser : ParseCertificate(bytes, format) Parser --> Handler : CertObject -Handler --> Service : CertObject +Handler --> SlotMgr : CertObject +SlotMgr -> Cache : store weak_ptr(CertObject) +SlotMgr -> Registry : RegisterSlotCert(slot, new CertEntry) +Registry --> SlotMgr : cert_registry_id +SlotMgr --> Service : CertObject + CertEntry (per-client) Service -> Data : addChildNode(CertDataNode) +== Certificate load — second client (cache hit, shared bytes) == + +Service -> SlotMgr : Load(params, client_B) +SlotMgr -> Cache : lookup(slot.index) +Cache --> SlotMgr : hit — CertObject::Sptr (no I/O) +SlotMgr -> Registry : RegisterSlotCert(slot, new CertEntry) +note right of Registry + Independent CertEntry per client. + Both wrap the same CertObject ptr. + Session CRL on one entry is not + visible from the other entry. +end note +Registry --> SlotMgr : cert_registry_id_2 +SlotMgr --> Service : CertObject + CertEntry (per-client) + +== Trust store resolution and anchor loading == + Service -> Stores : ResolveAppResource(trust resource, client) Stores --> Service : TrustStoreHandle Service -> Stores : GetStore(handle) Stores --> Service : TrustStoreHandler Service -> StoreHandler : GetAnchors() StoreHandler -> Stores : lazy AnchorLoader -Stores -> Handler : LoadCertificate(slot config) -Handler -> Parser : ParseCertificate(bytes, format) -Parser --> Handler : CertObject -Handler --> Stores : CertObject +Stores -> SlotMgr : LoadCertificate(slot config) +SlotMgr -> Cache : lookup(slot.index) +Cache --> SlotMgr : hit or miss +alt cache miss + SlotMgr -> Handler : LoadCertificate(slot config) + Handler -> Parser : ParseCertificate(bytes, format) + Parser --> Handler : CertObject + Handler --> SlotMgr : CertObject + SlotMgr -> Cache : store weak_ptr(CertObject) +end +SlotMgr --> Stores : CertObject Stores -> StoreHandler : NotifySlotUpdate(slot, cert) StoreHandler --> Service : anchors -Service -> Handler : StoreCertificate(updated cert) +== Certificate store and cache invalidation == + +Service -> SlotMgr : StoreCertificate(slot, updated cert) +SlotMgr -> Handler : StoreCertificate(updated cert) Handler -> "Shared Deployment Storage" : atomic cert + metadata write +Handler --> SlotMgr : ok +SlotMgr -> Cache : erase(slot.index) +note right of Cache + Next Load will re-read from handler. + In-flight CertEntries holding the + old CertObject::Sptr remain valid + for their lifetime. +end note +SlotMgr --> Service : ok Service -> Stores : NotifySlotChanged(store, slot) Stores -> StoreHandler : InvalidateSlot(slot) Service -> StoreHandler : GetAnchors() StoreHandler -> Stores : reload changed slot -Stores -> Handler : LoadCertificate() +Stores -> SlotMgr : LoadCertificate() +SlotMgr -> Handler : LoadCertificate() Handler -> Parser : ParseCertificate() Parser --> Handler : updated CertObject -Handler --> Stores : updated CertObject +Handler --> SlotMgr : updated CertObject +SlotMgr -> Cache : store weak_ptr(updated CertObject) +SlotMgr --> Stores : updated CertObject Stores -> StoreHandler : NotifySlotUpdate() StoreHandler --> Service : updated anchors @enduml diff --git a/score/crypto/src/daemon/cert_management/docs/detailed_design/detailed_design.rst b/score/crypto/src/daemon/cert_management/docs/detailed_design/detailed_design.rst index dfde34b18..8fea8a82e 100644 --- a/score/crypto/src/daemon/cert_management/docs/detailed_design/detailed_design.rst +++ b/score/crypto/src/daemon/cert_management/docs/detailed_design/detailed_design.rst @@ -32,10 +32,13 @@ Implementation units access, slot operations, and trust-store update notifications. ``CertRegistry`` and ``CertEntry`` - Own live certificate entries and share immutable ``CertObject`` values - between clients and trust stores. ``CertEntry`` optionally holds a - session-scoped CRL (from ``ImportCrl`` with ``persist=false``) that is - never written to disk. + Own live certificate entries. Each ``Load`` call produces a fresh + ``CertEntry`` per client; entries are never shared across clients. + ``CertEntry`` holds a ``CertObject::Sptr`` (the immutable parsed bytes) and + an optional session-scoped CRL (from ``ImportCrl`` with ``persist=false``) + that is never written to disk. Because ``CertEntry`` is per-client, the + session CRL is isolated — one client's ``ImportCrl`` cannot be observed by + another client that loaded the same slot. ``CertSlotRegistry`` Stores immutable slot configuration and application resource mappings. @@ -80,10 +83,15 @@ Data and lifetime model * ``TrustStoreDataNode`` is a client-scoped reference to a manager-owned trust store. * ``CertObject`` is immutable and provider-neutral. -* Trust-store anchor contents are loaded on demand. A weak cache avoids - duplicate certificate objects across active stores. -* Per-client trust-store references prevent one client from evicting another - client's active anchor cache. +* ``CertSlotManager`` holds a weak-ptr cache of ``CertObject`` values keyed by + slot index. The cache avoids repeated disk reads when multiple clients open + the same slot in quick succession. Entries expire automatically when no + ``CertEntry`` holds a strong reference; ``StoreCertificate`` and + ``ClearSlot`` invalidate the entry explicitly so the next load reads fresh + bytes. +* Trust-store anchor contents are loaded on demand by ``TrustStoreManager`` + through a separate anchor cache. Per-client references prevent one client + from evicting another client's active anchor cache. Storage contract ---------------- diff --git a/score/crypto/src/daemon/cert_management/docs/index.rst b/score/crypto/src/daemon/cert_management/docs/index.rst index 40aa8b3f0..eb51df960 100644 --- a/score/crypto/src/daemon/cert_management/docs/index.rst +++ b/score/crypto/src/daemon/cert_management/docs/index.rst @@ -66,10 +66,16 @@ This drives several design decisions: file-backed handler so that all certificate operations are accessible without hardware. -* **CertObject as a value type** — parsed certificates are immutable - ``shared_ptr``-managed values. The same ``CertObject`` instance is - shared across trust stores and client handles via a weak-ptr cache in - ``TrustStoreManager``, avoiding redundant parses and copies. +* **CertObject as a shared immutable value** — parsed certificates are + immutable ``shared_ptr``-managed values. Two separate weak-ptr caches avoid + redundant disk reads and parses. ``CertSlotManager`` caches the + most-recently-loaded ``CertObject`` per slot so that multiple clients + opening the same slot in quick succession share the bytes without repeated + I/O. ``TrustStoreManager`` maintains a separate anchor cache for active + trust stores. Critically, each client receives its own independent + ``CertEntry`` wrapping the shared ``CertObject``, so per-client state such + as a session-scoped CRL cannot bleed across clients that have loaded the + same slot. * **Trust store membership by typed slot reference** — anchors are certificate slots, not raw file paths. This allows the daemon to track diff --git a/score/crypto/src/daemon/cert_management/interfaces/access_policy.hpp b/score/crypto/src/daemon/cert_management/interfaces/access_policy.hpp index 87b9137a3..5165f66ec 100644 --- a/score/crypto/src/daemon/cert_management/interfaces/access_policy.hpp +++ b/score/crypto/src/daemon/cert_management/interfaces/access_policy.hpp @@ -23,17 +23,16 @@ namespace score::crypto::daemon::cert_management /// UID-based read/write access control policy for a certificate slot or trust store. struct AccessPolicy { - /// UIDs permitted to read from this slot (LoadCertificate, GetSlotInfo, - /// CERT_EXPORT, CERT_PUBLIC_KEY, CERT_VERIFY). - /// - /// An empty list means no UID-based restriction (all clients may read). + /// Reserved for future UID-based read authorization. Certificate reads are + /// currently allowed after DataManager node ownership is established. std::vector allowed_uids; /// UIDs permitted to write to this slot (SaveCertificate, CRL_IMPORT, /// CERT_CLEAR, TRUST_STORE_ADD_CERT, TRUST_STORE_REMOVE_CERT). /// - /// A UID in allowed_uids but not in allowed_write_uids may read but not - /// write. An empty list means no UID-based write restriction. + /// Mutation authorization is explicit and default-deny: an empty list + /// grants no write permission. Trust-store-owned exclusive slots do not + /// use this list; their writes are authorized by TrustStoreManager. std::vector allowed_write_uids; }; diff --git a/score/crypto/src/daemon/cert_management/query/cert_object_serializer.cpp b/score/crypto/src/daemon/cert_management/query/cert_object_serializer.cpp index b21b5a566..b468d1324 100644 --- a/score/crypto/src/daemon/cert_management/query/cert_object_serializer.cpp +++ b/score/crypto/src/daemon/cert_management/query/cert_object_serializer.cpp @@ -35,19 +35,18 @@ common::ResponseParameters SerializeCertObject(const CertObject& cert) return out; } -score::crypto::Expected SerializeCertSlotInfo( - ICertSlotHandler& handler, - const CertSlotConfig& config) +score::crypto::Expected +SerializeCertSlotInfo(CertSlotManager& mgr, CertSlotHandle slot, data_manager::ClientId client_id) { - auto info_res = handler.GetSlotInfo(config); + auto info_res = mgr.GetSlotInfo(slot, client_id); if (!info_res.has_value()) return score::crypto::make_unexpected(info_res.error()); - const bool has_crl = info_res.value().has_crl; + const bool has_crl = mgr.HasCrl(slot); int64_t crl_next = 0; if (has_crl) { - auto nu = handler.GetCrlNextUpdate(config); + auto nu = mgr.GetCrlNextUpdate(slot, client_id); if (nu.has_value()) crl_next = nu.value(); } @@ -72,7 +71,7 @@ common::ResponseParameters SerializeTrustStoreMembers(const std::vector(nid_res.value()), &member}); diff --git a/score/crypto/src/daemon/cert_management/query/cert_object_serializer.hpp b/score/crypto/src/daemon/cert_management/query/cert_object_serializer.hpp index 85ff8fefd..3e8693a93 100644 --- a/score/crypto/src/daemon/cert_management/query/cert_object_serializer.hpp +++ b/score/crypto/src/daemon/cert_management/query/cert_object_serializer.hpp @@ -15,10 +15,11 @@ #define SCORE_CRYPTO_SRC_DAEMON_CERT_MANAGEMENT_QUERY_CERT_OBJECT_SERIALIZER_HPP #include "score/crypto/src/daemon/cert_management/interfaces/cert_object.hpp" -#include "score/crypto/src/daemon/cert_management/interfaces/i_cert_slot_handler.hpp" +#include "score/crypto/src/daemon/cert_management/slot/cert_slot_manager.hpp" #include "score/crypto/src/daemon/cert_management/truststore/trust_store_manager.hpp" #include "score/crypto/src/daemon/common/daemon_error.hpp" #include "score/crypto/src/daemon/common/types.hpp" +#include "score/crypto/src/daemon/data_manager/data_node.hpp" #include #include @@ -53,14 +54,14 @@ common::ResponseParameters SerializeCertObject(const CertObject& cert); /// /// Returns an error if GetSlotInfo fails. Both the mediator typed-object handler /// and the cert management executor use this function. -score::crypto::Expected SerializeCertSlotInfo( - ICertSlotHandler& handler, - const CertSlotConfig& config); +score::crypto::Expected +SerializeCertSlotInfo(CertSlotManager& mgr, CertSlotHandle slot, data_manager::ClientId client_id); /// @brief Serialize trust store member snapshot into IPC response parameters. /// -/// Resolves per-client slot DataNodeIds via @p service; members whose slot cannot -/// be resolved are silently omitted rather than failing the whole response. +/// Resolves per-client slot DataNodeIds from each member's canonical slot handle +/// via @p service; members whose slot cannot be resolved are silently omitted +/// rather than failing the whole response. /// /// Wire layout: [0] count N (uint64), then for each member i in [0, N) (7 params): /// [1+i*7+0] slot_node_id (uint64), [1+i*7+1] fingerprint (OwnedBuffer 32B), diff --git a/score/crypto/src/daemon/cert_management/slot/cert_slot_manager.cpp b/score/crypto/src/daemon/cert_management/slot/cert_slot_manager.cpp new file mode 100644 index 000000000..d021fbfff --- /dev/null +++ b/score/crypto/src/daemon/cert_management/slot/cert_slot_manager.cpp @@ -0,0 +1,389 @@ +/******************************************************************************** + * 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/cert_management/slot/cert_slot_manager.hpp" +#include "score/mw/log/logging.h" + +namespace score::crypto::daemon::cert_management +{ + +using Error = common::DaemonErrorCode; + +// --------------------------------------------------------------------------- +// Construction +// --------------------------------------------------------------------------- + +CertSlotManager::CertSlotManager(CertSlotRegistry::Sptr registry, CertSlotHandlerFactory factory) + : m_registry{std::move(registry)}, m_factory{std::move(factory)} +{ +} + +// --------------------------------------------------------------------------- +// Internal helpers +// --------------------------------------------------------------------------- + +score::crypto::Expected CertSlotManager::GetConfig(CertSlotHandle slot) const +{ + if (!m_registry) + return score::crypto::make_unexpected(Error::kInternalError); + const auto cfg = m_registry->GetConfig(slot); + if (!cfg.has_value()) + return score::crypto::make_unexpected(cfg.error()); + return cfg.value(); +} + +score::crypto::Expected CertSlotManager::ApplyWriteChecks(const CertSlotConfig& cfg, + data_manager::ClientId client_id) +{ + auto write_check = AccessPolicyEnforcer::CheckWritePermission(cfg, client_id); + if (!write_check.has_value()) + return write_check; + + // TODO(cert-slot-mgr): exclusive-slot guard — return kAccessDenied when the slot + // is a kExclusiveMutable member of any trust store. Requires a membership query + // callback to avoid a circular dependency with TrustStoreManager. + + return std::monostate{}; +} + +ICertSlotHandler::Sptr CertSlotManager::GetOrCreate(CertSlotHandle slot) +{ + // Must be called with m_mutex held. + const auto it = m_handlers.find(slot.index); + if (it != m_handlers.end()) + return it->second; + + if (!m_factory || !m_registry) + return nullptr; + const auto cfg = m_registry->GetConfig(slot); + if (!cfg.has_value()) + return nullptr; + auto handler = m_factory(*cfg.value()); + if (!handler) + return nullptr; + return m_handlers.emplace(slot.index, std::move(handler)).first->second; +} + +// --------------------------------------------------------------------------- +// Tier 3 — trust-store backend (friend-gated, no auth check) +// --------------------------------------------------------------------------- + +ICertSlotHandler::Sptr CertSlotManager::GetTrustStoreCertSlotHandler(CertSlotHandle slot) +{ + std::lock_guard lock(m_mutex); + return GetOrCreate(slot); +} + +// --------------------------------------------------------------------------- +// Tier 2 — provider extension escape hatch +// --------------------------------------------------------------------------- + +score::crypto::Expected +CertSlotManager::GetProviderCertSlotHandler(CertSlotHandle slot, data_manager::ClientId client_id, bool require_write) +{ + const auto cfg_res = GetConfig(slot); + if (!cfg_res.has_value()) + return score::crypto::make_unexpected(cfg_res.error()); + const CertSlotConfig& cfg = *cfg_res.value(); + + auto access_check = AccessPolicyEnforcer::CheckSlotAccess(cfg, client_id); + if (!access_check.has_value()) + return score::crypto::make_unexpected(access_check.error()); + + if (require_write) + { + auto write_res = ApplyWriteChecks(cfg, client_id); + if (!write_res.has_value()) + return score::crypto::make_unexpected(write_res.error()); + } + + ICertSlotHandler::Sptr handler; + { + std::lock_guard lock(m_mutex); + handler = GetOrCreate(slot); + } + if (!handler) + { + score::mw::log::LogError() << kLogPrefix << "GetProviderCertSlotHandler: no handler for slot " << slot.index; + return score::crypto::make_unexpected(Error::kInternalError); + } + return handler; +} + +// --------------------------------------------------------------------------- +// Tier 1 — reads +// --------------------------------------------------------------------------- + +score::crypto::Expected CertSlotManager::LoadCertificate(CertSlotHandle slot, + data_manager::ClientId client_id) +{ + const auto cfg_res = GetConfig(slot); + if (!cfg_res.has_value()) + return score::crypto::make_unexpected(cfg_res.error()); + const CertSlotConfig& cfg = *cfg_res.value(); + + auto access_check = AccessPolicyEnforcer::CheckSlotAccess(cfg, client_id); + if (!access_check.has_value()) + return score::crypto::make_unexpected(access_check.error()); + + ICertSlotHandler::Sptr handler; + { + std::lock_guard lock(m_mutex); + // Return cached CertObject if a strong ref is still alive — avoids disk re-read + // when multiple clients open the same slot cert concurrently. + const auto cache_it = m_cert_object_cache.find(slot.index); + if (cache_it != m_cert_object_cache.end()) + { + auto cached = cache_it->second.lock(); + if (cached) + return cached; + m_cert_object_cache.erase(cache_it); + } + handler = GetOrCreate(slot); + } + if (!handler) + return score::crypto::make_unexpected(Error::kInternalError); + + auto cert_obj = handler->LoadCertificate(cfg); + if (!cert_obj.has_value()) + return cert_obj; + + { + std::lock_guard lock(m_mutex); + m_cert_object_cache[slot.index] = cert_obj.value(); + } + return cert_obj; +} + +score::crypto::Expected CertSlotManager::GetSlotInfo( + CertSlotHandle slot, + data_manager::ClientId client_id) +{ + const auto cfg_res = GetConfig(slot); + if (!cfg_res.has_value()) + return score::crypto::make_unexpected(cfg_res.error()); + const CertSlotConfig& cfg = *cfg_res.value(); + + auto access_check = AccessPolicyEnforcer::CheckSlotAccess(cfg, client_id); + if (!access_check.has_value()) + return score::crypto::make_unexpected(access_check.error()); + + ICertSlotHandler::Sptr handler; + { + std::lock_guard lock(m_mutex); + handler = GetOrCreate(slot); + } + if (!handler) + return score::crypto::make_unexpected(Error::kInternalError); + return handler->GetSlotInfo(cfg); +} + +bool CertSlotManager::HasCrl(CertSlotHandle slot) +{ + const auto cfg_res = GetConfig(slot); + if (!cfg_res.has_value()) + return false; + + ICertSlotHandler::Sptr handler; + { + std::lock_guard lock(m_mutex); + handler = GetOrCreate(slot); + } + if (!handler) + return false; + return handler->HasCrl(*cfg_res.value()); +} + +score::crypto::Expected, Error> CertSlotManager::LoadCrl(CertSlotHandle slot, + data_manager::ClientId client_id) +{ + const auto cfg_res = GetConfig(slot); + if (!cfg_res.has_value()) + return score::crypto::make_unexpected(cfg_res.error()); + const CertSlotConfig& cfg = *cfg_res.value(); + + auto access_check = AccessPolicyEnforcer::CheckSlotAccess(cfg, client_id); + if (!access_check.has_value()) + return score::crypto::make_unexpected(access_check.error()); + + ICertSlotHandler::Sptr handler; + { + std::lock_guard lock(m_mutex); + handler = GetOrCreate(slot); + } + if (!handler) + return score::crypto::make_unexpected(Error::kInternalError); + return handler->LoadCrl(cfg); +} + +score::crypto::FormatType CertSlotManager::GetCrlFormat(CertSlotHandle slot) +{ + const auto cfg_res = GetConfig(slot); + if (!cfg_res.has_value()) + return score::crypto::FormatType::kDer; + + ICertSlotHandler::Sptr handler; + { + std::lock_guard lock(m_mutex); + handler = GetOrCreate(slot); + } + if (!handler) + return score::crypto::FormatType::kDer; + return handler->GetCrlFormat(*cfg_res.value()); +} + +score::crypto::Expected CertSlotManager::GetCrlNextUpdate(CertSlotHandle slot, + data_manager::ClientId client_id) +{ + const auto cfg_res = GetConfig(slot); + if (!cfg_res.has_value()) + return score::crypto::make_unexpected(cfg_res.error()); + const CertSlotConfig& cfg = *cfg_res.value(); + + auto access_check = AccessPolicyEnforcer::CheckSlotAccess(cfg, client_id); + if (!access_check.has_value()) + return score::crypto::make_unexpected(access_check.error()); + + ICertSlotHandler::Sptr handler; + { + std::lock_guard lock(m_mutex); + handler = GetOrCreate(slot); + } + if (!handler) + return score::crypto::make_unexpected(Error::kInternalError); + return handler->GetCrlNextUpdate(cfg); +} + +// --------------------------------------------------------------------------- +// Tier 1 — writes +// --------------------------------------------------------------------------- + +score::crypto::Expected CertSlotManager::StoreCertificate(CertSlotHandle slot, + data_manager::ClientId client_id, + const CertObject& cert) +{ + const auto cfg_res = GetConfig(slot); + if (!cfg_res.has_value()) + return score::crypto::make_unexpected(cfg_res.error()); + const CertSlotConfig& cfg = *cfg_res.value(); + + auto write_res = ApplyWriteChecks(cfg, client_id); + if (!write_res.has_value()) + { + score::mw::log::LogError() << kLogPrefix << "StoreCertificate: write access denied for slot " << slot.index; + return write_res; + } + + ICertSlotHandler::Sptr handler; + { + std::lock_guard lock(m_mutex); + handler = GetOrCreate(slot); + } + if (!handler) + return score::crypto::make_unexpected(Error::kInternalError); + + auto result = handler->StoreCertificate(cfg, cert); + if (result.has_value()) + InvalidateCertObjectCache(slot); + return result; +} + +score::crypto::Expected CertSlotManager::ClearSlot(CertSlotHandle slot, + data_manager::ClientId client_id) +{ + const auto cfg_res = GetConfig(slot); + if (!cfg_res.has_value()) + return score::crypto::make_unexpected(cfg_res.error()); + const CertSlotConfig& cfg = *cfg_res.value(); + + auto write_res = ApplyWriteChecks(cfg, client_id); + if (!write_res.has_value()) + { + score::mw::log::LogError() << kLogPrefix << "ClearSlot: write access denied for slot " << slot.index; + return write_res; + } + + ICertSlotHandler::Sptr handler; + { + std::lock_guard lock(m_mutex); + handler = GetOrCreate(slot); + } + if (!handler) + return score::crypto::make_unexpected(Error::kInternalError); + + auto result = handler->ClearSlot(cfg); + if (result.has_value()) + InvalidateCertObjectCache(slot); + return result; +} + +void CertSlotManager::InvalidateCertObjectCache(CertSlotHandle slot) +{ + std::lock_guard lock(m_mutex); + m_cert_object_cache.erase(slot.index); +} + +score::crypto::Expected CertSlotManager::ImportCrl(CertSlotHandle slot, + data_manager::ClientId client_id, + score::crypto::span crl_data, + score::crypto::FormatType format, + std::int64_t next_update_epoch_s) +{ + const auto cfg_res = GetConfig(slot); + if (!cfg_res.has_value()) + return score::crypto::make_unexpected(cfg_res.error()); + const CertSlotConfig& cfg = *cfg_res.value(); + + auto write_res = ApplyWriteChecks(cfg, client_id); + if (!write_res.has_value()) + { + score::mw::log::LogError() << kLogPrefix << "ImportCrl: write access denied for slot " << slot.index; + return write_res; + } + + ICertSlotHandler::Sptr handler; + { + std::lock_guard lock(m_mutex); + handler = GetOrCreate(slot); + } + if (!handler) + return score::crypto::make_unexpected(Error::kInternalError); + return handler->StoreCrl(cfg, crl_data, format, next_update_epoch_s); +} + +score::crypto::Expected CertSlotManager::DeleteCrl(CertSlotHandle slot, + data_manager::ClientId client_id) +{ + const auto cfg_res = GetConfig(slot); + if (!cfg_res.has_value()) + return score::crypto::make_unexpected(cfg_res.error()); + const CertSlotConfig& cfg = *cfg_res.value(); + + auto write_res = ApplyWriteChecks(cfg, client_id); + if (!write_res.has_value()) + { + score::mw::log::LogError() << kLogPrefix << "DeleteCrl: write access denied for slot " << slot.index; + return write_res; + } + + ICertSlotHandler::Sptr handler; + { + std::lock_guard lock(m_mutex); + handler = GetOrCreate(slot); + } + if (!handler) + return score::crypto::make_unexpected(Error::kInternalError); + return handler->ClearCrl(cfg); +} + +} // namespace score::crypto::daemon::cert_management diff --git a/score/crypto/src/daemon/cert_management/slot/cert_slot_manager.hpp b/score/crypto/src/daemon/cert_management/slot/cert_slot_manager.hpp new file mode 100644 index 000000000..f10e885f2 --- /dev/null +++ b/score/crypto/src/daemon/cert_management/slot/cert_slot_manager.hpp @@ -0,0 +1,183 @@ +/******************************************************************************** + * 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 + ********************************************************************************/ + +#ifndef SCORE_CRYPTO_SRC_DAEMON_CERT_MANAGEMENT_SLOT_CERT_SLOT_MANAGER_HPP +#define SCORE_CRYPTO_SRC_DAEMON_CERT_MANAGEMENT_SLOT_CERT_SLOT_MANAGER_HPP + +#include "score/crypto/src/api/common/types.hpp" +#include "score/crypto/src/daemon/cert_management/interfaces/cert_slot_config.hpp" +#include "score/crypto/src/daemon/cert_management/interfaces/cert_types.hpp" +#include "score/crypto/src/daemon/cert_management/interfaces/i_cert_slot_handler.hpp" +#include "score/crypto/src/daemon/cert_management/policy/access_policy_enforcer.hpp" +#include "score/crypto/src/daemon/cert_management/slot/slot_registry.hpp" +#include "score/crypto/src/daemon/common/daemon_error.hpp" +#include "score/crypto/src/daemon/data_manager/data_node.hpp" + +#include +#include +#include +#include +#include +#include + +namespace score::crypto::daemon::cert_management +{ + +class TrustStoreManager; + +/// @brief Central owner of certificate slot handler instances and the sole +/// enforcement point for slot-level access policy. +/// +/// All backend operations on a certificate slot must pass through this manager. +/// No caller outside CertSlotManager may hold or call a raw ICertSlotHandler. +/// +/// The API is divided into three tiers: +/// +/// Tier 1 — operation-oriented (executor / IPC handler path). +/// Authorization is unconditional inside each method. +/// Read ops call CheckSlotAccess; write ops also call CheckWritePermission. +/// +/// Tier 2 — GetProviderCertSlotHandler (public). +/// For provider extension dispatch that needs functionality beyond +/// ICertSlotHandler (e.g. custom PKCS#11 attributes). CertSlotManager +/// runs the same slot policy before returning the handler; the caller +/// dynamic_casts to its extended type. +/// +/// Tier 3 — GetTrustStoreCertSlotHandler (private, friend-gated). +/// For TrustStoreManager only, which has already enforced trust-store +/// write policy + membership/kind check before calling this method. +/// +/// Handler instances are created lazily on first access and cached per slot +/// (keyed by CertSlotHandle::index). This eliminates the per-call PKCS#11 session +/// overhead that existed when CertManagementService created a fresh handler on +/// every ResolveSlotForOperation call. +/// +/// Thread safety: m_handlers is guarded by m_mutex. Handlers themselves are not +/// thread-safe; serialization for concurrent backend calls is provided by the +/// DataNode locks in the data manager. +class CertSlotManager final +{ + friend class TrustStoreManager; + + public: + using Sptr = std::shared_ptr; + + CertSlotManager(CertSlotRegistry::Sptr registry, CertSlotHandlerFactory factory); + ~CertSlotManager() = default; + + CertSlotManager(const CertSlotManager&) = delete; + CertSlotManager& operator=(const CertSlotManager&) = delete; + CertSlotManager(CertSlotManager&&) = delete; + CertSlotManager& operator=(CertSlotManager&&) = delete; + + // ----------------------------------------------------------------------- + // Tier 1 — read operations (CheckSlotAccess inside) + // ----------------------------------------------------------------------- + + [[nodiscard]] score::crypto::Expected + LoadCertificate(CertSlotHandle slot, data_manager::ClientId client_id); + + [[nodiscard]] score::crypto::Expected + GetSlotInfo(CertSlotHandle slot, data_manager::ClientId client_id); + + /// Returns false when the slot has no handler or no CRL; never fails. + [[nodiscard]] bool HasCrl(CertSlotHandle slot); + + [[nodiscard]] score::crypto::Expected, score::crypto::daemon::common::DaemonErrorCode> LoadCrl( + CertSlotHandle slot, + data_manager::ClientId client_id); + + /// Returns kDer when the slot has no handler or no [crl] section; never fails. + [[nodiscard]] score::crypto::FormatType GetCrlFormat(CertSlotHandle slot); + + [[nodiscard]] score::crypto::Expected GetCrlNextUpdate( + CertSlotHandle slot, + data_manager::ClientId client_id); + + // ----------------------------------------------------------------------- + // Tier 1 — write operations (CheckSlotAccess + CheckWritePermission inside) + // ----------------------------------------------------------------------- + + [[nodiscard]] score::crypto::Expected + StoreCertificate(CertSlotHandle slot, data_manager::ClientId client_id, const CertObject& cert); + + [[nodiscard]] score::crypto::Expected ClearSlot( + CertSlotHandle slot, + data_manager::ClientId client_id); + + [[nodiscard]] score::crypto::Expected ImportCrl( + CertSlotHandle slot, + data_manager::ClientId client_id, + score::crypto::span crl_data, + score::crypto::FormatType format, + std::int64_t next_update_epoch_s); + + [[nodiscard]] score::crypto::Expected DeleteCrl( + CertSlotHandle slot, + data_manager::ClientId client_id); + + // ----------------------------------------------------------------------- + // Tier 2 — provider extension escape hatch + // + // CertSlotManager runs CheckSlotAccess (always) and optionally + // CheckWritePermission (when require_write=true) before returning the handler. + // The caller dynamic_casts to its extended provider type for custom operations. + // Must only be called from a provider extension dispatch, not from general + // executor handler code. + // ----------------------------------------------------------------------- + + [[nodiscard]] score::crypto::Expected + GetProviderCertSlotHandler(CertSlotHandle slot, data_manager::ClientId client_id, bool require_write = false); + + private: + // ----------------------------------------------------------------------- + // Tier 3 — trust-store backend seam (friend-gated) + // + // TrustStoreManager has already enforced trust-store write policy + + // membership/kind check before calling this method. No client_id — the + // trust store is the authority for exclusive-slot mutations. + // Returns nullptr if the slot is not found or the factory returns null. + // ----------------------------------------------------------------------- + [[nodiscard]] ICertSlotHandler::Sptr GetTrustStoreCertSlotHandler(CertSlotHandle slot); + + // Internal helpers + [[nodiscard]] score::crypto::Expected + GetConfig(CertSlotHandle slot) const; + + [[nodiscard]] score::crypto::Expected + ApplyWriteChecks(const CertSlotConfig& cfg, data_manager::ClientId client_id); + + /// Returns the cached handler or creates it via the factory. Must be called with m_mutex held. + [[nodiscard]] ICertSlotHandler::Sptr GetOrCreate(CertSlotHandle slot); + + /// Evict the CertObject cache entry for @p slot. Must be called after any + /// write that replaces the slot's certificate content (StoreCertificate, ClearSlot). + void InvalidateCertObjectCache(CertSlotHandle slot); + + CertSlotRegistry::Sptr m_registry; + CertSlotHandlerFactory m_factory; + std::unordered_map m_handlers; + /// Weak-pointer cache of parsed CertObjects keyed by slot index. + /// Avoids repeated disk reads when multiple clients open the same slot cert. + /// Entries are evicted automatically when no CertEntry holds a strong ref, + /// and explicitly invalidated on StoreCertificate / ClearSlot. + std::unordered_map> m_cert_object_cache; + mutable std::mutex m_mutex; + + static constexpr std::string_view kLogPrefix{"[CERT_SLOT_MANAGER] "}; +}; + +} // namespace score::crypto::daemon::cert_management + +#endif // SCORE_CRYPTO_SRC_DAEMON_CERT_MANAGEMENT_SLOT_CERT_SLOT_MANAGER_HPP diff --git a/score/crypto/src/daemon/cert_management/tests/BUILD b/score/crypto/src/daemon/cert_management/tests/BUILD index a67663c4c..334dafc19 100644 --- a/score/crypto/src/daemon/cert_management/tests/BUILD +++ b/score/crypto/src/daemon/cert_management/tests/BUILD @@ -12,6 +12,7 @@ cc_test( name = "test_cert_object_serializer", srcs = ["query/test_cert_object_serializer.cpp"], deps = [ + "//score/crypto/src/daemon/cert_management:cert_management", "//score/crypto/src/daemon/cert_management:cert_management_headers", "//score/crypto/src/daemon/cert_management:cert_object_serializer", "//score/crypto/src/daemon/common", diff --git a/score/crypto/src/daemon/cert_management/tests/core/test_cert_registry.cpp b/score/crypto/src/daemon/cert_management/tests/core/test_cert_registry.cpp index d0d8ac4a7..922d9d922 100644 --- a/score/crypto/src/daemon/cert_management/tests/core/test_cert_registry.cpp +++ b/score/crypto/src/daemon/cert_management/tests/core/test_cert_registry.cpp @@ -49,17 +49,30 @@ TEST(CertRegistryTest, RegistersFindsAndUnregistersEphemeralCertificate) EXPECT_FALSE(registry.FindById(id)); } -TEST(CertRegistryTest, RejectsDuplicateCertificateSlotRegistration) +// RegisterSlotCert creates independent entries per call — dedup of the +// underlying CertObject bytes is handled by CertSlotManager::CertObjectCache, +// not by the registry. Two clients loading the same slot each get their own +// CertEntry so that per-client state (e.g. session CRL) cannot bleed. +TEST(CertRegistryTest, RegisterSlotCert_TwoCallsProduceTwoIndependentEntries) { CertRegistry registry; const CertSlotHandle slot{7U}; - auto first = std::make_shared(MakeCertificate(), slot); - auto second = std::make_shared(MakeCertificate(), slot); + auto cert_obj = MakeCertificate(); + auto first = std::make_shared(cert_obj, slot); + auto second = std::make_shared(cert_obj, slot); - ASSERT_NE(registry.RegisterSlotCert(slot, first), 0U); - EXPECT_EQ(registry.RegisterSlotCert(slot, second), 0U); - EXPECT_EQ(registry.FindBySlot(slot), first); - EXPECT_EQ(registry.Size(), 1U); + const auto id1 = registry.RegisterSlotCert(slot, first); + const auto id2 = registry.RegisterSlotCert(slot, second); + + ASSERT_NE(id1, 0U); + ASSERT_NE(id2, 0U); + EXPECT_NE(id1, id2); + EXPECT_EQ(registry.Size(), 2U); + // Each ID resolves to its own CertEntry. + EXPECT_EQ(registry.FindById(id1), first); + EXPECT_EQ(registry.FindById(id2), second); + // Both entries wrap the same CertObject bytes. + EXPECT_EQ(registry.FindById(id1)->GetCertObject().get(), registry.FindById(id2)->GetCertObject().get()); } // Two ephemeral certs registered under two different client IDs. @@ -93,9 +106,9 @@ TEST(CertRegistryTest, CleanupClient_RemovesOnlyThatClientsEntries) EXPECT_EQ(registry.Size(), 1U); } -// A slot cert registered for one handle remains findable via both ID and slot -// after a CleanupClient for an unrelated client. -TEST(CertRegistryTest, FindBySlot_SlotCertPersistsAfterUnrelatedCleanup) +// A slot cert registered for one client remains findable by ID after an +// unrelated client's entries are cleaned up. +TEST(CertRegistryTest, SlotCertPersistsAfterUnrelatedClientCleanup) { using ClientId = score::crypto::daemon::data_manager::ClientId; static constexpr ClientId kClientA = 10U; @@ -115,7 +128,7 @@ TEST(CertRegistryTest, FindBySlot_SlotCertPersistsAfterUnrelatedCleanup) registry.CleanupClient(kClientB); - EXPECT_EQ(registry.FindBySlot(slot), slot_entry); EXPECT_EQ(registry.FindById(slot_id), slot_entry); + EXPECT_EQ(registry.Size(), 1U); } } // namespace diff --git a/score/crypto/src/daemon/cert_management/tests/integration/test_certificate_management_integration.cpp b/score/crypto/src/daemon/cert_management/tests/integration/test_certificate_management_integration.cpp index 05c2848a4..2c3c607c1 100644 --- a/score/crypto/src/daemon/cert_management/tests/integration/test_certificate_management_integration.cpp +++ b/score/crypto/src/daemon/cert_management/tests/integration/test_certificate_management_integration.cpp @@ -16,6 +16,7 @@ #include "score/crypto/src/daemon/cert_management/slot/file_backed_slot_handler.hpp" #include "score/crypto/src/daemon/cert_management/tests/test_environment.hpp" #include "score/crypto/src/daemon/cert_management/truststore/config_driven_trust_store_catalog.hpp" +#include "score/crypto/src/daemon/common/daemon_error.hpp" #include "score/crypto/src/daemon/common/storage/kv/kv_deployment_writer.hpp" #include "score/crypto/src/daemon/provider/score_provider/openssl/cert_management/openssl_cert_parser.hpp" @@ -33,10 +34,31 @@ namespace { namespace cert = score::crypto::daemon::cert_management; +namespace common = score::crypto::daemon::common; namespace config = score::crypto::daemon::config; namespace storage = score::crypto::daemon::common::storage; namespace openssl = score::crypto::daemon::provider::score_provider::openssl; +// --------------------------------------------------------------------------- +// Helper — build a ClientId from explicit pid/uid fields. +// Mirrors the union layout used by GetUidFromClientId in control_protocol.h. +// --------------------------------------------------------------------------- +score::crypto::daemon::data_manager::ClientId MakeClientId(std::uint32_t pid, std::uint32_t uid) +{ + union + { + score::crypto::daemon::data_manager::ClientId id; + struct + { + std::uint32_t process_id; + std::uint32_t user_id; + } parts; + } value{0U}; + value.parts.process_id = pid; + value.parts.user_id = uid; + return value.id; +} + class CertificateManagementIntegrationTest : public ::testing::Test { protected: @@ -92,10 +114,11 @@ class CertificateManagementIntegrationTest : public ::testing::Test slot_catalog.Load(*m_slot_registry); m_trust_store_manager = std::make_shared(); - cert::ConfigDrivenTrustStoreCatalog trust_store_catalog{m_config}; - trust_store_catalog.Load(*m_trust_store_manager, m_slot_registry, [this](const cert::CertSlotConfig&) { + m_slot_manager = std::make_shared(m_slot_registry, [this](const cert::CertSlotConfig&) { return std::make_shared(m_parser); }); + cert::ConfigDrivenTrustStoreCatalog trust_store_catalog{m_config}; + trust_store_catalog.Load(*m_trust_store_manager, m_slot_registry, m_slot_manager); } void TearDown() override @@ -117,6 +140,7 @@ class CertificateManagementIntegrationTest : public ::testing::Test config::CertificateConfig m_config; std::shared_ptr m_parser; cert::CertSlotRegistry::Sptr m_slot_registry; + cert::CertSlotManager::Sptr m_slot_manager; cert::TrustStoreManager::Sptr m_trust_store_manager; }; @@ -166,4 +190,111 @@ TEST_F(CertificateManagementIntegrationTest, LoadsPersistsUpdatesAndInvalidatesT (*initial_anchors)[0]->GetFingerprint().begin(), (*initial_anchors)[0]->GetFingerprint().end())); } +// =========================================================================== +// Access control — CertSlotManager +// +// The fixture configures the slot with allowed_write_uids={0}, so: +// MakeClientId(pid, 0) → UID 0 → write authorized +// MakeClientId(pid, 99) → UID 99 → write denied +// Reads are unconditionally permitted regardless of UID. +// =========================================================================== + +// Any UID can load a certificate; reads are unrestricted after resource resolution. +TEST_F(CertificateManagementIntegrationTest, LoadCertificate_IsUnrestrictedForAnyUid) +{ + const auto slot = m_slot_registry->ResolveAppResource("device_certificate", 0U); + ASSERT_TRUE(slot.has_value()); + + EXPECT_TRUE(m_slot_manager->LoadCertificate(*slot, MakeClientId(1U, 0U)).has_value()); + EXPECT_TRUE(m_slot_manager->LoadCertificate(*slot, MakeClientId(2U, 99U)).has_value()); +} + +// StoreCertificate succeeds when the caller's UID is in allowed_write_uids. +TEST_F(CertificateManagementIntegrationTest, StoreCertificate_GrantedForAuthorizedUid) +{ + const auto slot = m_slot_registry->ResolveAppResource("device_certificate", 0U); + ASSERT_TRUE(slot.has_value()); + const auto cert = m_slot_manager->LoadCertificate(*slot, MakeClientId(1U, 0U)); + ASSERT_TRUE(cert.has_value()); + + EXPECT_TRUE(m_slot_manager->StoreCertificate(*slot, MakeClientId(1U, 0U), **cert).has_value()); +} + +// StoreCertificate is denied when the caller's UID is not in allowed_write_uids. +TEST_F(CertificateManagementIntegrationTest, StoreCertificate_DeniedForUnauthorizedUid) +{ + const auto slot = m_slot_registry->ResolveAppResource("device_certificate", 0U); + ASSERT_TRUE(slot.has_value()); + const auto cert = m_slot_manager->LoadCertificate(*slot, MakeClientId(1U, 0U)); + ASSERT_TRUE(cert.has_value()); + + const auto result = m_slot_manager->StoreCertificate(*slot, MakeClientId(2U, 99U), **cert); + ASSERT_FALSE(result.has_value()); + EXPECT_EQ(result.error(), common::DaemonErrorCode::kAccessDenied); +} + +// ClearSlot is denied when the caller's UID is not in allowed_write_uids. +TEST_F(CertificateManagementIntegrationTest, ClearSlot_DeniedForUnauthorizedUid) +{ + const auto slot = m_slot_registry->ResolveAppResource("device_certificate", 0U); + ASSERT_TRUE(slot.has_value()); + + const auto result = m_slot_manager->ClearSlot(*slot, MakeClientId(2U, 99U)); + ASSERT_FALSE(result.has_value()); + EXPECT_EQ(result.error(), common::DaemonErrorCode::kAccessDenied); +} + +// A slot with an empty allowed_write_uids list denies writes from every UID, +// including UID 0. This is the default-deny invariant. +TEST_F(CertificateManagementIntegrationTest, WritesDeniedWhenAllowedWriteUidsIsEmpty) +{ + auto registry = std::make_shared(); + cert::CertSlotConfig cfg; + cfg.slot_name = "test/locked-slot"; + // access_policy.allowed_write_uids left empty — default-deny for all UIDs. + const auto locked_slot = registry->RegisterSlot(cfg); + + auto handler = std::make_shared(m_parser); + cert::CertSlotManager locked_mgr{registry, [&handler](const cert::CertSlotConfig&) { + return handler; + }}; + + const auto src_slot = m_slot_registry->ResolveAppResource("device_certificate", 0U); + ASSERT_TRUE(src_slot.has_value()); + const auto cert = m_slot_manager->LoadCertificate(*src_slot, MakeClientId(1U, 0U)); + ASSERT_TRUE(cert.has_value()); + + // Even UID 0 is denied because the allowlist is empty. + const auto result = locked_mgr.StoreCertificate(locked_slot, MakeClientId(1U, 0U), **cert); + ASSERT_FALSE(result.has_value()); + EXPECT_EQ(result.error(), common::DaemonErrorCode::kAccessDenied); +} + +// =========================================================================== +// Access control — TrustStoreManager +// +// The fixture configures the trust store with allowed_write_uids={0}. +// TrustStoreManager checks CheckTrustStoreWritePermission internally before +// any membership mutation. +// =========================================================================== + +// Trust-store membership mutations are denied for UIDs not in the store's +// allowed_write_uids, regardless of whether the member slot is valid. +TEST_F(CertificateManagementIntegrationTest, TrustStoreMutation_DeniedForUnauthorizedUid) +{ + const auto trust_store = m_trust_store_manager->ResolveAppResource("tls_roots", 0U); + ASSERT_TRUE(trust_store.has_value()); + const auto slot = m_slot_registry->ResolveAppResource("device_certificate", 0U); + ASSERT_TRUE(slot.has_value()); + + // UID 99 is not in the trust store's allowed_write_uids={0}. + const auto enable_result = m_trust_store_manager->EnableMember(*trust_store, *slot, MakeClientId(2U, 99U)); + ASSERT_FALSE(enable_result.has_value()); + EXPECT_EQ(enable_result.error(), common::DaemonErrorCode::kAccessDenied); + + const auto disable_result = m_trust_store_manager->DisableMember(*trust_store, *slot, MakeClientId(2U, 99U)); + ASSERT_FALSE(disable_result.has_value()); + EXPECT_EQ(disable_result.error(), common::DaemonErrorCode::kAccessDenied); +} + } // namespace diff --git a/score/crypto/src/daemon/cert_management/tests/query/test_cert_object_serializer.cpp b/score/crypto/src/daemon/cert_management/tests/query/test_cert_object_serializer.cpp index c2b9dd4dd..849111df3 100644 --- a/score/crypto/src/daemon/cert_management/tests/query/test_cert_object_serializer.cpp +++ b/score/crypto/src/daemon/cert_management/tests/query/test_cert_object_serializer.cpp @@ -19,7 +19,9 @@ // - Parameter values for known synthetic inputs // // These tests do NOT require OpenSSL. CertObject is constructed synthetically -// from a known CertChainMetadata. ICertSlotHandler is stubbed inline. +// from a known CertChainMetadata. ICertSlotHandler is stubbed inline and +// wrapped inside a minimal CertSlotManager so the serializer's access-policy +// path is exercised without a real storage backend. // // SerializeTrustStoreMembers requires a live CertManagementService for slot // resolution and is therefore covered by test_cert_management_service.cpp @@ -30,12 +32,15 @@ #include "score/crypto/src/daemon/cert_management/interfaces/cert_types.hpp" #include "score/crypto/src/daemon/cert_management/interfaces/i_cert_slot_handler.hpp" #include "score/crypto/src/daemon/cert_management/query/cert_object_serializer.hpp" +#include "score/crypto/src/daemon/cert_management/slot/cert_slot_manager.hpp" +#include "score/crypto/src/daemon/cert_management/slot/slot_registry.hpp" #include "score/crypto/src/daemon/common/daemon_error.hpp" #include "score/crypto/src/daemon/common/types.hpp" #include #include +#include #include #include @@ -80,12 +85,11 @@ cert::CertObject MakeSyntheticCert(bool is_ca = true) } // --------------------------------------------------------------------------- -// ICertSlotHandler stub — configurable inline +// ICertSlotHandler stub — configurable slot state and CRL metadata // --------------------------------------------------------------------------- struct SlotHandlerStub : public cert::ICertSlotHandler { - // Mutable so the stub can be used from const methods. mutable score::crypto::Expected slot_info_result{score::crypto::CertificateSlotInfo{score::crypto::CertificateSlotState::kOccupied}}; @@ -124,7 +128,7 @@ struct SlotHandlerStub : public cert::ICertSlotHandler bool HasCrl(const cert::CertSlotConfig&) override { - return slot_info_result->has_crl; + return slot_info_result.has_value() && slot_info_result->has_crl; } score::crypto::Expected, score::crypto::daemon::common::DaemonErrorCode> LoadCrl( @@ -290,47 +294,67 @@ TEST(SerializeCertObject, EmptySkidAndAkidEncodeAsEmptyBuffers) // =========================================================================== // SerializeCertSlotInfo +// +// CertSlotManager is constructed with a minimal CertSlotRegistry and a factory +// that returns a SlotHandlerStub. CheckSlotAccess is unconditionally permissive +// for reads, so no UID configuration is required. // =========================================================================== -TEST(SerializeCertSlotInfo, ProducesThreeParameters) +class SerializeCertSlotInfoTest : public ::testing::Test +{ + protected: + static constexpr score::crypto::daemon::data_manager::ClientId kClientId = 42U; + + void SetUp() override + { + auto registry = std::make_shared(); + cert::CertSlotConfig cfg; + cfg.slot_name = "test/serializer-slot"; + slot_handle_ = registry->RegisterSlot(cfg); + + stub_ = std::make_shared(); + auto stub_ptr = stub_; + mgr_ = std::make_unique(std::move(registry), [stub_ptr](const cert::CertSlotConfig&) { + return stub_ptr; + }); + } + + cert::CertSlotHandle slot_handle_{}; + std::shared_ptr stub_; + std::unique_ptr mgr_; +}; + +TEST_F(SerializeCertSlotInfoTest, ProducesThreeParameters) { - SlotHandlerStub stub; - cert::CertSlotConfig config{}; - const auto result = query::SerializeCertSlotInfo(stub, config); + const auto result = query::SerializeCertSlotInfo(*mgr_, slot_handle_, kClientId); ASSERT_TRUE(result.has_value()); EXPECT_EQ(result.value().size(), 3U); } -TEST(SerializeCertSlotInfo, Param0_SlotStateUint8_Occupied) +TEST_F(SerializeCertSlotInfoTest, Param0_SlotStateUint8_Occupied) { - SlotHandlerStub stub; - stub.slot_info_result = score::crypto::CertificateSlotInfo{score::crypto::CertificateSlotState::kOccupied}; - cert::CertSlotConfig config{}; - const auto result = query::SerializeCertSlotInfo(stub, config); + stub_->slot_info_result = score::crypto::CertificateSlotInfo{score::crypto::CertificateSlotState::kOccupied}; + const auto result = query::SerializeCertSlotInfo(*mgr_, slot_handle_, kClientId); ASSERT_TRUE(result.has_value()); const auto* val = GetParam(result.value(), 0U); ASSERT_NE(val, nullptr); EXPECT_EQ(*val, static_cast(score::crypto::CertificateSlotState::kOccupied)); } -TEST(SerializeCertSlotInfo, Param0_SlotStateUint8_Empty) +TEST_F(SerializeCertSlotInfoTest, Param0_SlotStateUint8_Empty) { - SlotHandlerStub stub; - stub.slot_info_result = score::crypto::CertificateSlotInfo{score::crypto::CertificateSlotState::kEmpty}; - cert::CertSlotConfig config{}; - const auto result = query::SerializeCertSlotInfo(stub, config); + stub_->slot_info_result = score::crypto::CertificateSlotInfo{score::crypto::CertificateSlotState::kEmpty}; + const auto result = query::SerializeCertSlotInfo(*mgr_, slot_handle_, kClientId); ASSERT_TRUE(result.has_value()); const auto* val = GetParam(result.value(), 0U); ASSERT_NE(val, nullptr); EXPECT_EQ(*val, static_cast(score::crypto::CertificateSlotState::kEmpty)); } -TEST(SerializeCertSlotInfo, NoCrl_Param1IsZero_Param2IsZero) +TEST_F(SerializeCertSlotInfoTest, NoCrl_Param1IsZero_Param2IsZero) { - SlotHandlerStub stub; - stub.slot_info_result->has_crl = false; - cert::CertSlotConfig config{}; - const auto result = query::SerializeCertSlotInfo(stub, config); + stub_->slot_info_result->has_crl = false; + const auto result = query::SerializeCertSlotInfo(*mgr_, slot_handle_, kClientId); ASSERT_TRUE(result.has_value()); const auto* has_crl = GetParam(result.value(), 1U); @@ -341,13 +365,11 @@ TEST(SerializeCertSlotInfo, NoCrl_Param1IsZero_Param2IsZero) EXPECT_EQ(*crl_next, 0U); } -TEST(SerializeCertSlotInfo, CrlPresent_Param1IsOne_Param2IsEpoch) +TEST_F(SerializeCertSlotInfoTest, CrlPresent_Param1IsOne_Param2IsEpoch) { - SlotHandlerStub stub; - stub.slot_info_result->has_crl = true; - stub.crl_next_update_value = 1700000000LL; - cert::CertSlotConfig config{}; - const auto result = query::SerializeCertSlotInfo(stub, config); + stub_->slot_info_result->has_crl = true; + stub_->crl_next_update_value = 1700000000LL; + const auto result = query::SerializeCertSlotInfo(*mgr_, slot_handle_, kClientId); ASSERT_TRUE(result.has_value()); const auto* has_crl = GetParam(result.value(), 1U); @@ -358,15 +380,13 @@ TEST(SerializeCertSlotInfo, CrlPresent_Param1IsOne_Param2IsEpoch) EXPECT_EQ(*crl_next, static_cast(1700000000ULL)); } -TEST(SerializeCertSlotInfo, CrlPresent_GetCrlNextUpdateFails_Param2IsZero) +TEST_F(SerializeCertSlotInfoTest, CrlPresent_GetCrlNextUpdateFails_Param2IsZero) { // HasCrl() returns true but GetCrlNextUpdate() is unavailable. - // The serializer should encode has_crl=1 and crl_next=0 (not an error). - SlotHandlerStub stub; - stub.slot_info_result->has_crl = true; - stub.crl_next_update_value = std::nullopt; // causes GetCrlNextUpdate to return error - cert::CertSlotConfig config{}; - const auto result = query::SerializeCertSlotInfo(stub, config); + // The serializer encodes has_crl=1 and crl_next=0 (not an error). + stub_->slot_info_result->has_crl = true; + stub_->crl_next_update_value = std::nullopt; + const auto result = query::SerializeCertSlotInfo(*mgr_, slot_handle_, kClientId); ASSERT_TRUE(result.has_value()); const auto* has_crl = GetParam(result.value(), 1U); @@ -377,13 +397,11 @@ TEST(SerializeCertSlotInfo, CrlPresent_GetCrlNextUpdateFails_Param2IsZero) EXPECT_EQ(*crl_next, 0U); } -TEST(SerializeCertSlotInfo, GetSlotInfoError_PropagatesError) +TEST_F(SerializeCertSlotInfoTest, GetSlotInfoError_PropagatesError) { - SlotHandlerStub stub; - stub.slot_info_result = + stub_->slot_info_result = score::crypto::make_unexpected(score::crypto::daemon::common::DaemonErrorCode::kInternalError); - cert::CertSlotConfig config{}; - const auto result = query::SerializeCertSlotInfo(stub, config); + const auto result = query::SerializeCertSlotInfo(*mgr_, slot_handle_, kClientId); ASSERT_FALSE(result.has_value()); EXPECT_EQ(result.error(), score::crypto::daemon::common::DaemonErrorCode::kInternalError); } diff --git a/score/crypto/src/daemon/cert_management/tests/service/test_cert_management_service.cpp b/score/crypto/src/daemon/cert_management/tests/service/test_cert_management_service.cpp index 92398a396..b01ff5ac4 100644 --- a/score/crypto/src/daemon/cert_management/tests/service/test_cert_management_service.cpp +++ b/score/crypto/src/daemon/cert_management/tests/service/test_cert_management_service.cpp @@ -23,15 +23,16 @@ // // Test subjects: // - Slot resolution and cert load via the executor call sequence -// (ResolveCertSlot → ResolveSlotForOperation → LoadOrShare) +// (ResolveCertSlot → ResolveSlotForOperation → Load) // - ResolveCertForOperation round-trip // - Cert node release and subsequent unresolvability -// - LoadOrShare deduplication — second call for the same slot shares the -// existing CertEntry without a second handler LoadCertificate call +// - Load creates independent CertEntries per client; shared CertObject bytes +// avoid repeated disk reads (CertObjectCache in CertSlotManager) // - Mediator-style client cleanup isolates clients — purging one leaves the other intact // - NotifySlotCertChanged propagates through TrustStoreManager anchor cache #include "score/crypto/src/daemon/cert_management/core/cert_management_service.hpp" +#include "score/crypto/src/daemon/cert_management/slot/cert_slot_manager.hpp" #include "score/crypto/src/daemon/cert_management/slot/file_backed_slot_handler.hpp" #include "score/crypto/src/daemon/cert_management/tests/test_environment.hpp" #include "score/crypto/src/daemon/cert_management/truststore/trust_store_manager.hpp" @@ -109,10 +110,12 @@ class CertManagementServiceTest : public ::testing::Test m_trust_store_manager = std::make_shared(); m_data_manager = std::make_shared(); - m_service = std::make_shared( - m_data_manager, m_registry, m_trust_store_manager, [this](const cert::CertSlotConfig&) { + auto slot_manager = std::make_shared( + m_registry, [this](const cert::CertSlotConfig&) -> cert::ICertSlotHandler::Sptr { return std::make_shared(m_parser); }); + m_service = std::make_shared( + m_data_manager, m_registry, m_trust_store_manager, slot_manager); } void TearDown() override @@ -124,7 +127,6 @@ class CertManagementServiceTest : public ::testing::Test } // Mirror the executor call sequence: resolve the slot node, then load the cert. - // Combines ResolveCertSlot → ResolveSlotForOperation → LoadOrShare. cert::CertDataNodeResult LoadCert(dm::ClientId client_id) { auto slot_res = m_service->ResolveCertSlot(std::string{kAppResource}, client_id); @@ -141,7 +143,7 @@ class CertManagementServiceTest : public ::testing::Test params.parent_id = slot_node_id; params.slot_handle = resolved->handle; - auto result = m_service->LoadOrShare(params, *resolved->handler, *resolved->config); + auto result = m_service->Load(params); if (!result.has_value()) return {}; return result.value(); @@ -206,10 +208,13 @@ TEST_F(CertManagementServiceTest, ReleaseCert_UnknownNodeId_ReturnsError) } // --------------------------------------------------------------------------- -// Deduplication — second call for the same slot must share the CertEntry +// Per-client CertEntry isolation — each Load call produces an independent +// CertEntry so that session CRL state cannot bleed across clients. +// The underlying CertObject bytes are shared via the CertObjectCache in +// CertSlotManager, avoiding repeated disk reads. // --------------------------------------------------------------------------- -TEST_F(CertManagementServiceTest, LoadOrShare_SecondCallSharesRegistryEntry) +TEST_F(CertManagementServiceTest, Load_SecondCallProducesIndependentCertEntry) { const auto result1 = LoadCert(kClientA); ASSERT_NE(result1.node_id, 0U); @@ -219,9 +224,11 @@ TEST_F(CertManagementServiceTest, LoadOrShare_SecondCallSharesRegistryEntry) ASSERT_NE(result2.node_id, 0U); ASSERT_NE(result2.entry, nullptr); - // Separate data-manager nodes but the same live CertEntry. + // Independent CertEntry objects — session CRL on one cannot affect the other. EXPECT_NE(result1.node_id, result2.node_id); - EXPECT_EQ(result1.entry.get(), result2.entry.get()); + EXPECT_NE(result1.entry.get(), result2.entry.get()); + // But the underlying CertObject bytes are the same shared object (cache hit). + EXPECT_EQ(result1.entry->GetCertObject().get(), result2.entry->GetCertObject().get()); } // --------------------------------------------------------------------------- @@ -238,6 +245,16 @@ TEST_F(CertManagementServiceTest, ResolveCertSlot_SameClientAndResource_ReturnsS EXPECT_EQ(id1.value(), id2.value()); } +TEST_F(CertManagementServiceTest, ResolveCertSlot_ByHandleSharesResourceNodeId) +{ + const auto by_resource = m_service->ResolveCertSlot(std::string{kAppResource}, kClientA); + const auto by_handle = m_service->ResolveCertSlot(m_slot_handle, kClientA); + + ASSERT_TRUE(by_resource.has_value()); + ASSERT_TRUE(by_handle.has_value()); + EXPECT_EQ(by_resource.value(), by_handle.value()); +} + TEST_F(CertManagementServiceTest, ResolveCertSlot_UnknownResource_ReturnsError) { EXPECT_FALSE(m_service->ResolveCertSlot("no_such_resource", kClientA).has_value()); @@ -275,9 +292,11 @@ TEST_F(CertManagementServiceTest, NotifySlotCertChanged_RefreshesAnchorSubjectIn ts_cfg.members.push_back( cert::TrustStoreMemberConfig{std::string{kSlotName}, cert::TrustStoreMemberKind::kSharedStatic}); - m_trust_store_manager->Load({ts_cfg}, m_registry, [this](const cert::CertSlotConfig&) { - return std::make_shared(m_parser); - }); + auto ts_slot_manager = std::make_shared( + m_registry, [this](const cert::CertSlotConfig&) -> cert::ICertSlotHandler::Sptr { + return std::make_shared(m_parser); + }); + m_trust_store_manager->Load({ts_cfg}, m_registry, ts_slot_manager); const auto ts_handle = m_trust_store_manager->ResolveByName("test-roots"); auto store = m_trust_store_manager->GetStore(ts_handle); diff --git a/score/crypto/src/daemon/cert_management/tests/truststore/test_trust_store_manager.cpp b/score/crypto/src/daemon/cert_management/tests/truststore/test_trust_store_manager.cpp index 48e048654..1d9ae29d3 100644 --- a/score/crypto/src/daemon/cert_management/tests/truststore/test_trust_store_manager.cpp +++ b/score/crypto/src/daemon/cert_management/tests/truststore/test_trust_store_manager.cpp @@ -31,6 +31,7 @@ // - AcknowledgeMemberUpdate — re-enables a disabled slot with fresh load // - ConditionalExternal — fingerprint mismatch auto-disables on reload; AcknowledgeMemberUpdate re-enables +#include "score/crypto/src/daemon/cert_management/slot/cert_slot_manager.hpp" #include "score/crypto/src/daemon/cert_management/slot/file_backed_slot_handler.hpp" #include "score/crypto/src/daemon/cert_management/tests/test_environment.hpp" #include "score/crypto/src/daemon/cert_management/truststore/trust_store_manager.hpp" @@ -115,12 +116,13 @@ class TrustStoreManagerTest : public ::testing::Test ASSERT_TRUE(storage::KvDeploymentWriter{}.Write(kv.string(), desc).has_value()); } - cert::CertSlotHandlerFactory MakeHandlerFactory() + cert::CertSlotManager::Sptr MakeSlotManager(cert::CertSlotRegistry::Sptr registry) { auto parser = m_parser; - return [parser](const cert::CertSlotConfig&) { + cert::CertSlotHandlerFactory factory = [parser](const cert::CertSlotConfig&) { return std::make_shared(parser); }; + return std::make_shared(std::move(registry), std::move(factory)); } // MakeSlotConfig — for tests that mutate state (overwrite cert file, AddMember, etc.) @@ -178,7 +180,7 @@ TEST_F(TrustStoreManagerTest, LoadsAnchorsLazilyAndBuildsSlotMembershipIndex) ts_cfg.members.push_back(cert::TrustStoreMemberConfig{"root-anchor", cert::TrustStoreMemberKind::kSharedStatic}); cert::TrustStoreManager manager; - manager.Load({ts_cfg}, registry, MakeHandlerFactory()); + manager.Load({ts_cfg}, registry, MakeSlotManager(registry)); // Membership index is built at Load() time without touching cert bytes. ASSERT_EQ(manager.GetMembershipsForSlot(slot).size(), 1U); @@ -226,7 +228,7 @@ TEST_F(TrustStoreManagerTest, PerClientAddRef_DoesNotEvictCacheWhileOtherClientH ts_cfg.members.push_back(cert::TrustStoreMemberConfig{"root-anchor", cert::TrustStoreMemberKind::kSharedStatic}); cert::TrustStoreManager manager; - manager.Load({ts_cfg}, registry, MakeHandlerFactory()); + manager.Load({ts_cfg}, registry, MakeSlotManager(registry)); const auto ts_handle = manager.ResolveByName("tls-roots"); auto store = manager.GetStore(ts_handle); @@ -282,7 +284,7 @@ TEST_F(TrustStoreManagerTest, CleanupClient_ReleasesAllRefsAndEvictsCache) ts_cfg.members.push_back(cert::TrustStoreMemberConfig{"root-anchor", cert::TrustStoreMemberKind::kSharedStatic}); cert::TrustStoreManager manager; - manager.Load({ts_cfg}, registry, MakeHandlerFactory()); + manager.Load({ts_cfg}, registry, MakeSlotManager(registry)); const auto ts_handle = manager.ResolveByName("tls-roots"); auto store = manager.GetStore(ts_handle); @@ -331,7 +333,7 @@ TEST_F(TrustStoreManagerTest, DisableAndReEnable_MemberTogglesAnchorVisibility) ts_cfg.members.push_back(cert::TrustStoreMemberConfig{"root-anchor", cert::TrustStoreMemberKind::kSharedStatic}); cert::TrustStoreManager manager; - manager.Load({ts_cfg}, registry, MakeHandlerFactory()); + manager.Load({ts_cfg}, registry, MakeSlotManager(registry)); const auto ts_handle = manager.ResolveByName("tls-roots"); auto store = manager.GetStore(ts_handle); @@ -377,7 +379,7 @@ TEST_F(TrustStoreManagerTest, TwoStores_OneSlot_BothGetMemberships) store_b.members.push_back(cert::TrustStoreMemberConfig{"shared-anchor", cert::TrustStoreMemberKind::kSharedStatic}); cert::TrustStoreManager manager; - manager.Load({store_a, store_b}, registry, MakeHandlerFactory()); + manager.Load({store_a, store_b}, registry, MakeSlotManager(registry)); // Both stores report membership for the shared slot. EXPECT_EQ(manager.GetMembershipsForSlot(slot).size(), 2U); @@ -414,7 +416,7 @@ TEST_F(TrustStoreManagerTest, AddMember_ToExclusiveSlot_AddsAnchorAndPersistsToD cert::TrustStoreMemberConfig{"empty-anchor", cert::TrustStoreMemberKind::kExclusiveMutable}); cert::TrustStoreManager manager; - manager.Load({ts_cfg}, registry, MakeHandlerFactory()); + manager.Load({ts_cfg}, registry, MakeSlotManager(registry)); const auto ts_handle = manager.ResolveByName("mutable-store"); auto store = manager.GetStore(ts_handle); @@ -463,7 +465,7 @@ TEST_F(TrustStoreManagerTest, RemoveMember_ByFingerprint_RemovesAnchor) cert::TrustStoreMemberConfig{"root-anchor", cert::TrustStoreMemberKind::kExclusiveMutable}); cert::TrustStoreManager manager; - manager.Load({ts_cfg}, registry, MakeHandlerFactory()); + manager.Load({ts_cfg}, registry, MakeSlotManager(registry)); const auto ts_handle = manager.ResolveByName("mutable-store"); auto store = manager.GetStore(ts_handle); @@ -503,7 +505,7 @@ TEST_F(TrustStoreManagerTest, AcknowledgeMemberUpdate_TransitionsDisabledMemberT ts_cfg.members.push_back(cert::TrustStoreMemberConfig{"root-anchor", cert::TrustStoreMemberKind::kSharedStatic}); cert::TrustStoreManager manager; - manager.Load({ts_cfg}, registry, MakeHandlerFactory()); + manager.Load({ts_cfg}, registry, MakeSlotManager(registry)); const auto ts_handle = manager.ResolveByName("cond-store"); auto store = manager.GetStore(ts_handle); @@ -570,7 +572,7 @@ TEST_F(TrustStoreManagerTest, ConditionalExternal_FingerprintMismatch_AutoDisabl cert::TrustStoreMemberConfig{"root-anchor", cert::TrustStoreMemberKind::kConditionalExternal}); cert::TrustStoreManager manager; - manager.Load({ts_cfg}, registry, MakeHandlerFactory()); + manager.Load({ts_cfg}, registry, MakeSlotManager(registry)); const auto ts_handle = manager.ResolveByName("cond-store"); auto store = manager.GetStore(ts_handle); @@ -634,7 +636,7 @@ TEST_F(TrustStoreManagerTest, AddMember_WithCrlBytes_StoresAndPersistsCrl) cert::TrustStoreMemberConfig{"empty-anchor", cert::TrustStoreMemberKind::kExclusiveMutable}); cert::TrustStoreManager manager; - manager.Load({ts_cfg}, registry, MakeHandlerFactory()); + manager.Load({ts_cfg}, registry, MakeSlotManager(registry)); const auto ts_handle = manager.ResolveByName("mutable-store"); @@ -674,7 +676,7 @@ TEST_F(TrustStoreManagerTest, AddMember_ExistingExclusiveMember_UpsertsCrl) cert::TrustStoreMemberConfig{"empty-anchor", cert::TrustStoreMemberKind::kExclusiveMutable}); cert::TrustStoreManager manager; - manager.Load({ts_cfg}, registry, MakeHandlerFactory()); + manager.Load({ts_cfg}, registry, MakeSlotManager(registry)); const auto ts_handle = manager.ResolveByName("mutable-store"); @@ -718,7 +720,7 @@ TEST_F(TrustStoreManagerTest, AddMember_SharedStaticMember_WithCrl_ReturnsUnsupp ts_cfg.members.push_back(cert::TrustStoreMemberConfig{"root-anchor", cert::TrustStoreMemberKind::kSharedStatic}); cert::TrustStoreManager manager; - manager.Load({ts_cfg}, registry, MakeHandlerFactory()); + manager.Load({ts_cfg}, registry, MakeSlotManager(registry)); const auto ts_handle = manager.ResolveByName("mixed-store"); @@ -753,7 +755,7 @@ TEST_F(TrustStoreManagerTest, AddMember_DeduplicationChecksAllMemberTypes) cert::TrustStoreMemberConfig{"empty-anchor", cert::TrustStoreMemberKind::kExclusiveMutable}); cert::TrustStoreManager manager; - manager.Load({ts_cfg}, registry, MakeHandlerFactory()); + manager.Load({ts_cfg}, registry, MakeSlotManager(registry)); const auto ts_handle = manager.ResolveByName("mixed-store"); @@ -788,7 +790,7 @@ TEST_F(TrustStoreManagerTest, ImportCrlForMember_WritesToExclusiveSlot) cert::TrustStoreMemberConfig{"empty-anchor", cert::TrustStoreMemberKind::kExclusiveMutable}); cert::TrustStoreManager manager; - manager.Load({ts_cfg}, registry, MakeHandlerFactory()); + manager.Load({ts_cfg}, registry, MakeSlotManager(registry)); const auto ts_handle = manager.ResolveByName("mutable-store"); @@ -802,8 +804,8 @@ TEST_F(TrustStoreManagerTest, ImportCrlForMember_WritesToExclusiveSlot) const std::vector crl{0xD0U, 0xD1U, 0xD2U}; const auto crl_span = score::crypto::span{crl.data(), crl.size()}; - ASSERT_TRUE( - manager.ImportCrlForMember(ts_handle, slot_handle, crl_span, score::crypto::FormatType::kDer).has_value()); + ASSERT_TRUE(manager.ImportCrlForMember(ts_handle, slot_handle, crl_span, score::crypto::FormatType::kDer, kClientA) + .has_value()); // Verify persistence: fresh handler must read the CRL. cert::FileBackedSlotHandler fresh_handler{m_parser}; @@ -814,6 +816,35 @@ TEST_F(TrustStoreManagerTest, ImportCrlForMember_WritesToExclusiveSlot) EXPECT_EQ(*loaded_crl, crl); } +TEST_F(TrustStoreManagerTest, ImportCrlForMember_DeniesUnauthorizedWriter) +{ + auto registry = std::make_shared(); + const auto slot_handle = registry->RegisterSlot(MakeSlotConfig("empty-anchor", m_empty_slot_kv)); + + cert::TrustStoreConfig ts_cfg; + ts_cfg.store_name = "mutable-store"; + ts_cfg.access_policy.allowed_write_uids = {0U}; + ts_cfg.members.push_back( + cert::TrustStoreMemberConfig{"empty-anchor", cert::TrustStoreMemberKind::kExclusiveMutable}); + + cert::TrustStoreManager manager; + manager.Load({ts_cfg}, registry, MakeSlotManager(registry)); + const auto ts_handle = manager.ResolveByName("mutable-store"); + + auto cert = ParseCert(m_root_cert); + ASSERT_NE(cert, nullptr); + ASSERT_TRUE(manager.AddMember(ts_handle, cert, kClientA).has_value()); + + const std::vector crl{0xD0U, 0xD1U, 0xD2U}; + const auto crl_span = score::crypto::span{crl.data(), crl.size()}; + const auto unauthorized = static_cast(1ULL << 32U) | 3U; + const auto result = + manager.ImportCrlForMember(ts_handle, slot_handle, crl_span, score::crypto::FormatType::kDer, unauthorized); + + ASSERT_FALSE(result.has_value()); + EXPECT_EQ(result.error(), Error::kAccessDenied); +} + // --------------------------------------------------------------------------- // AddMember — capacity exceeded: all exclusive slots are occupied by a // different cert. The trust store has no shared-static member and no empty @@ -832,7 +863,7 @@ TEST_F(TrustStoreManagerTest, AddMember_NoEmptyExclusiveSlot_ReturnsCapacityExce cert::TrustStoreMemberConfig{"root-anchor", cert::TrustStoreMemberKind::kExclusiveMutable}); cert::TrustStoreManager manager; - manager.Load({ts_cfg}, registry, MakeHandlerFactory()); + manager.Load({ts_cfg}, registry, MakeSlotManager(registry)); const auto ts_handle = manager.ResolveByName("full-store"); @@ -871,7 +902,7 @@ TEST_F(TrustStoreManagerTest, RemoveMember_WrongFingerprint_ReturnsInvalidArgume cert::TrustStoreMemberConfig{"root-anchor", cert::TrustStoreMemberKind::kExclusiveMutable}); cert::TrustStoreManager manager; - manager.Load({ts_cfg}, registry, MakeHandlerFactory()); + manager.Load({ts_cfg}, registry, MakeSlotManager(registry)); const auto ts_handle = manager.ResolveByName("mutable-store"); @@ -897,14 +928,15 @@ TEST_F(TrustStoreManagerTest, ImportCrlForMember_SharedStaticSlot_ReturnsUnsuppo ts_cfg.members.push_back(cert::TrustStoreMemberConfig{"root-anchor", cert::TrustStoreMemberKind::kSharedStatic}); cert::TrustStoreManager manager; - manager.Load({ts_cfg}, registry, MakeHandlerFactory()); + manager.Load({ts_cfg}, registry, MakeSlotManager(registry)); const auto ts_handle = manager.ResolveByName("shared-store"); const std::vector crl{0x01U, 0x02U, 0x03U}; const auto crl_span = score::crypto::span{crl.data(), crl.size()}; - const auto result = manager.ImportCrlForMember(ts_handle, slot_handle, crl_span, score::crypto::FormatType::kDer); + const auto result = + manager.ImportCrlForMember(ts_handle, slot_handle, crl_span, score::crypto::FormatType::kDer, kClientA); EXPECT_FALSE(result.has_value()); EXPECT_EQ(result.error(), score::crypto::daemon::common::DaemonErrorCode::kUnsupportedOperation); } @@ -927,7 +959,7 @@ TEST_F(TrustStoreManagerTest, EnableMember_UnregisteredSlot_ReturnsInvalidArgume ts_cfg.members.push_back(cert::TrustStoreMemberConfig{"root-anchor", cert::TrustStoreMemberKind::kSharedStatic}); cert::TrustStoreManager manager; - manager.Load({ts_cfg}, registry, MakeHandlerFactory()); + manager.Load({ts_cfg}, registry, MakeSlotManager(registry)); const auto ts_handle = manager.ResolveByName("tls-roots"); @@ -965,7 +997,7 @@ TEST_F(TrustStoreManagerTest, PersistState_DisabledMemberSurvivesReload) { cert::TrustStoreManager manager; - manager.Load({ts_cfg}, registry, MakeHandlerFactory()); + manager.Load({ts_cfg}, registry, MakeSlotManager(registry)); const auto ts_handle = manager.ResolveByName("persist-store"); auto store = manager.GetStore(ts_handle); @@ -980,7 +1012,7 @@ TEST_F(TrustStoreManagerTest, PersistState_DisabledMemberSurvivesReload) // Second manager instance — simulates daemon restart. cert::TrustStoreManager manager2; - manager2.Load({ts_cfg}, registry, MakeHandlerFactory()); + manager2.Load({ts_cfg}, registry, MakeSlotManager(registry)); const auto ts_handle2 = manager2.ResolveByName("persist-store"); auto store2 = manager2.GetStore(ts_handle2); diff --git a/score/crypto/src/daemon/cert_management/truststore/config_driven_trust_store_catalog.cpp b/score/crypto/src/daemon/cert_management/truststore/config_driven_trust_store_catalog.cpp index a830ae107..aeefd252f 100644 --- a/score/crypto/src/daemon/cert_management/truststore/config_driven_trust_store_catalog.cpp +++ b/score/crypto/src/daemon/cert_management/truststore/config_driven_trust_store_catalog.cpp @@ -24,7 +24,7 @@ ConfigDrivenTrustStoreCatalog::ConfigDrivenTrustStoreCatalog(const config::Certi void ConfigDrivenTrustStoreCatalog::Load(TrustStoreManager& manager, CertSlotRegistry::Sptr registry, - CertSlotHandlerFactory slot_handler_factory) + CertSlotManager::Sptr slot_manager) { const auto& entries = m_config.GetTrustStoreEntries(); @@ -54,7 +54,7 @@ void ConfigDrivenTrustStoreCatalog::Load(TrustStoreManager& manager, } score::mw::log::LogDebug() << kLogPrefix << "Loading " << configs.size() << " trust store(s) from configuration."; - manager.Load(std::move(configs), std::move(registry), std::move(slot_handler_factory)); + manager.Load(std::move(configs), std::move(registry), std::move(slot_manager)); for (const auto& mapping : m_config.GetAppTrustStoreEntries()) manager.RegisterAppResource(mapping.uid, mapping.app_resource_id, mapping.trust_store_name); } diff --git a/score/crypto/src/daemon/cert_management/truststore/config_driven_trust_store_catalog.hpp b/score/crypto/src/daemon/cert_management/truststore/config_driven_trust_store_catalog.hpp index aaad970bc..82d179329 100644 --- a/score/crypto/src/daemon/cert_management/truststore/config_driven_trust_store_catalog.hpp +++ b/score/crypto/src/daemon/cert_management/truststore/config_driven_trust_store_catalog.hpp @@ -14,6 +14,7 @@ #ifndef SCORE_CRYPTO_SRC_DAEMON_CERT_MANAGEMENT_TRUSTSTORE_CONFIG_DRIVEN_TRUST_STORE_CATALOG_HPP #define SCORE_CRYPTO_SRC_DAEMON_CERT_MANAGEMENT_TRUSTSTORE_CONFIG_DRIVEN_TRUST_STORE_CATALOG_HPP +#include "score/crypto/src/daemon/cert_management/slot/cert_slot_manager.hpp" #include "score/crypto/src/daemon/cert_management/slot/slot_registry.hpp" #include "score/crypto/src/daemon/cert_management/truststore/trust_store_manager.hpp" #include "score/crypto/src/daemon/config/inc/config.hpp" @@ -36,12 +37,10 @@ class ConfigDrivenTrustStoreCatalog final /// @brief Adapt config entries and populate the trust store manager. /// - /// @param manager The TrustStoreManager to populate. - /// @param registry Fully-populated CertSlotRegistry for member slot resolution. - /// @param factory Certificate factory for loading static anchor files (may be null). - void Load(TrustStoreManager& manager, - CertSlotRegistry::Sptr registry, - CertSlotHandlerFactory slot_handler_factory = {}); + /// @param manager The TrustStoreManager to populate. + /// @param registry Fully-populated CertSlotRegistry for member slot resolution. + /// @param slot_manager CertSlotManager for lazily-created, cached slot handlers (may be null). + void Load(TrustStoreManager& manager, CertSlotRegistry::Sptr registry, CertSlotManager::Sptr slot_manager = {}); private: const config::CertificateConfig& m_config; diff --git a/score/crypto/src/daemon/cert_management/truststore/trust_store_manager.cpp b/score/crypto/src/daemon/cert_management/truststore/trust_store_manager.cpp index 50f3b9d23..0a4bfeb51 100644 --- a/score/crypto/src/daemon/cert_management/truststore/trust_store_manager.cpp +++ b/score/crypto/src/daemon/cert_management/truststore/trust_store_manager.cpp @@ -44,7 +44,7 @@ std::optional> CopyFingerprint(score::crypto::span& configs, CertSlotRegistry::Sptr registry, - CertSlotHandlerFactory slot_handler_factory) + CertSlotManager::Sptr slot_manager) { std::lock_guard lock(m_mutex); m_stores.clear(); @@ -55,8 +55,7 @@ void TrustStoreManager::Load(const std::vector& configs, m_slot_cert_cache.clear(); m_client_ref_counts.clear(); m_slot_registry = std::move(registry); - m_slot_handler_factory = std::move(slot_handler_factory); - m_slot_handlers.clear(); // lazy cache — populated by GetOrCreateHandler() on first access + m_slot_manager = std::move(slot_manager); for (const auto& config : configs) { const TrustStoreId id = static_cast(m_stores.size()); @@ -83,7 +82,7 @@ void TrustStoreManager::Load(const std::vector& configs, } // --------------------------------------------------------------------------- -// State helpers (unchanged logic, no cert loading) +// State helpers // --------------------------------------------------------------------------- void TrustStoreManager::LoadState(TrustStoreId id) @@ -166,22 +165,15 @@ score::crypto::Expected TrustStoreManager::PersistState(T // Lazy anchor loading — called by TrustStoreHandler::EnsureLoaded() // --------------------------------------------------------------------------- -ICertSlotHandler* TrustStoreManager::GetOrCreateHandler(CertSlotHandle slot) +ICertSlotHandler* TrustStoreManager::GetHandler(CertSlotHandle slot) { // Must be called with m_mutex held. - auto it = m_slot_handlers.find(slot.index); - if (it != m_slot_handlers.end()) - return it->second.get(); - - if (!m_slot_handler_factory || !m_slot_registry) - return nullptr; - const auto cfg = m_slot_registry->GetConfig(slot); - if (!cfg) + // Delegate to CertSlotManager (friend access, no auth check). The returned Sptr + // is kept alive by CertSlotManager::m_handlers for the daemon lifetime — the raw + // pointer is safe for any call within this lock scope. + if (!m_slot_manager) return nullptr; - auto handler = m_slot_handler_factory(**cfg); - if (!handler) - return nullptr; - return m_slot_handlers.emplace(slot.index, std::move(handler)).first->second.get(); + return m_slot_manager->GetTrustStoreCertSlotHandler(slot).get(); } CertObject::Sptr TrustStoreManager::LoadOrGetCached(CertSlotHandle slot) @@ -191,7 +183,7 @@ CertObject::Sptr TrustStoreManager::LoadOrGetCached(CertSlotHandle slot) if (auto cert = weak.lock()) return cert; // another active trust store handler already holds it - auto* handler = GetOrCreateHandler(slot); + auto* handler = GetHandler(slot); const auto cfg = m_slot_registry ? m_slot_registry->GetConfig(slot) : nullptr; if (handler == nullptr || !cfg) return nullptr; @@ -266,7 +258,7 @@ void TrustStoreManager::LoadAnchorsIntoHandler(TrustStoreId id, TrustStoreHandle // Co-load the CRL for this slot into the handler's CRL cache. if (usable && cert) { - auto* slot_handler = GetOrCreateHandler(*slot); + auto* slot_handler = GetHandler(*slot); const auto cfg = m_slot_registry->GetConfig(*slot); if (slot_handler != nullptr && cfg && slot_handler->HasCrl(**cfg)) { @@ -438,7 +430,7 @@ std::optional TrustStoreManager::ResolveMembe const auto cfg = m_slot_registry->GetConfig(*slot); if (!cfg) return std::nullopt; - auto* handler = GetOrCreateHandler(*slot); + auto* handler = GetHandler(*slot); if (handler == nullptr) return std::nullopt; return ResolvedMember{*slot, handler, *cfg}; @@ -452,7 +444,7 @@ score::crypto::Expected TrustStoreMan const auto cfg = m_slot_registry->GetConfig(slot); if (!cfg) return score::crypto::make_unexpected(Error::kInvalidResourceId); - auto* handler = GetOrCreateHandler(slot); + auto* handler = GetHandler(slot); if (handler == nullptr) return score::crypto::make_unexpected(Error::kInvalidResourceId); return ResolvedBackend{*cfg, handler}; @@ -567,6 +559,7 @@ score::crypto::Expected TrustStoreManager::ImportCrlForMe CertSlotHandle slot, score::crypto::span crl_data, score::crypto::FormatType format, + data_manager::ClientId client_id, std::int64_t next_update_epoch_s) { std::lock_guard lock(m_mutex); @@ -575,6 +568,8 @@ score::crypto::Expected TrustStoreManager::ImportCrlForMe return score::crypto::make_unexpected(Error::kInvalidResourceId); if (crl_data.empty()) return score::crypto::make_unexpected(Error::kInvalidArgument); + if (!AccessPolicyEnforcer::CheckTrustStoreWritePermission(m_stores[id].config, client_id).has_value()) + return score::crypto::make_unexpected(Error::kAccessDenied); for (const auto& member : m_stores[id].config.members) { diff --git a/score/crypto/src/daemon/cert_management/truststore/trust_store_manager.hpp b/score/crypto/src/daemon/cert_management/truststore/trust_store_manager.hpp index d32cd6ec8..998fb74bf 100644 --- a/score/crypto/src/daemon/cert_management/truststore/trust_store_manager.hpp +++ b/score/crypto/src/daemon/cert_management/truststore/trust_store_manager.hpp @@ -23,6 +23,7 @@ #include "score/crypto/src/daemon/cert_management/interfaces/i_trust_store_handler.hpp" #include "score/crypto/src/daemon/cert_management/interfaces/trust_store_config.hpp" #include "score/crypto/src/daemon/cert_management/policy/access_policy_enforcer.hpp" +#include "score/crypto/src/daemon/cert_management/slot/cert_slot_manager.hpp" #include "score/crypto/src/daemon/cert_management/slot/slot_registry.hpp" #include "score/crypto/src/daemon/cert_management/truststore/trust_store_handler.hpp" #include "score/crypto/src/daemon/common/daemon_error.hpp" @@ -89,7 +90,7 @@ class TrustStoreManager /// - Does NOT load cert content — certs are loaded lazily on first GetAnchors() void Load(const std::vector& store_configs, CertSlotRegistry::Sptr slot_registry, - CertSlotHandlerFactory slot_handler_factory = {}); + CertSlotManager::Sptr slot_manager = {}); // ----------------------------------------------------------------------- // Store access @@ -183,6 +184,7 @@ class TrustStoreManager CertSlotHandle slot, score::crypto::span crl_data, score::crypto::FormatType format, + data_manager::ClientId client_id, std::int64_t next_update_epoch_s = 0); /// @brief Remove a certificate from a trust store by fingerprint. @@ -210,7 +212,7 @@ class TrustStoreManager struct MemberSnapshot { CertSlotHandle slot_handle{0U}; ///< Daemon-internal slot index (for enable/disable routing). - std::string slot_name; ///< Slot name (used by executor to call ResolveCertSlot). + std::string slot_name; ///< Stable diagnostic/configuration name. std::array fingerprint{}; ///< SHA-256 fingerprint of the member certificate. std::string subject; ///< RFC 4514 Subject DN. std::string issuer; ///< RFC 4514 Issuer DN. @@ -261,10 +263,10 @@ class TrustStoreManager [[nodiscard]] score::crypto::Expected ResolveSlotBackend( CertSlotHandle slot); - /// Returns the handler for @p slot, creating it via m_slot_handler_factory on first access. - /// Returns nullptr if the factory is absent, the slot has no config, or factory returns nullptr. + /// Returns the handler for @p slot from CertSlotManager (friend-gated, no auth check). + /// Returns nullptr if CertSlotManager is absent or has no config for the slot. /// Must be called with m_mutex held. - ICertSlotHandler* GetOrCreateHandler(CertSlotHandle slot); + ICertSlotHandler* GetHandler(CertSlotHandle slot); void LoadState(TrustStoreId id); score::crypto::Expected PersistState(TrustStoreId id) const; @@ -313,8 +315,7 @@ class TrustStoreManager std::unordered_map> m_client_ref_counts; CertSlotRegistry::Sptr m_slot_registry; - std::unordered_map m_slot_handlers; - CertSlotHandlerFactory m_slot_handler_factory; + CertSlotManager::Sptr m_slot_manager; static constexpr std::string_view kLogPrefix = "[TRUST_STORE_MANAGER] "; }; From 531676765d388e9cfa850dade279aa8e3ec9987e Mon Sep 17 00:00:00 2001 From: Athul Mallappallil Date: Wed, 9 Sep 2026 21:19:52 +0000 Subject: [PATCH 35/37] Min refactor of int. class and names --- .../cert_management_module.cpp | 12 +-- .../cert_management_module.hpp | 7 -- .../cert_management/core/cert_entry.hpp | 38 ++------- .../core/cert_management_service.cpp | 83 +++++-------------- .../core/cert_management_service.hpp | 24 +----- .../cert_management/core/cert_registry.cpp | 10 +-- .../cert_management/nodes/cert_data_node.hpp | 15 ++-- .../query/cert_object_serializer.cpp | 2 +- .../tests/core/test_cert_registry.cpp | 12 +-- .../service/test_cert_management_service.cpp | 47 +++-------- 10 files changed, 64 insertions(+), 186 deletions(-) diff --git a/score/crypto/src/daemon/cert_management/cert_management_module.cpp b/score/crypto/src/daemon/cert_management/cert_management_module.cpp index 6666f0131..d0b597061 100644 --- a/score/crypto/src/daemon/cert_management/cert_management_module.cpp +++ b/score/crypto/src/daemon/cert_management/cert_management_module.cpp @@ -26,10 +26,10 @@ CertManagementModule::Sptr CertManagementModule::Create(data_manager::IDataManag { auto module = Sptr(new CertManagementModule()); module->m_provider_manager = std::move(provider_manager); - module->m_slots = std::make_shared(); + auto slot_registry = std::make_shared(); ConfigDrivenSlotCatalog catalog{config}; - catalog.Load(*module->m_slots); - module->m_trust_stores = std::make_shared(); + catalog.Load(*slot_registry); + auto trust_store_manager = std::make_shared(); ConfigDrivenTrustStoreCatalog trust_catalog{config}; // Resolve the cert parser once at startup; injected into every FileBackedSlotHandler. // Contract: a provider that advertises kCertManagement must implement GetCertParser(). @@ -70,11 +70,11 @@ CertManagementModule::Sptr CertManagementModule::Create(data_manager::IDataManag return provider->GetCertSlotHandler(slot, cert_parser); }; - auto slot_manager = std::make_shared(module->m_slots, std::move(slot_handler_factory)); + auto slot_manager = std::make_shared(slot_registry, std::move(slot_handler_factory)); - trust_catalog.Load(*module->m_trust_stores, module->m_slots, slot_manager); + trust_catalog.Load(*trust_store_manager, slot_registry, slot_manager); module->m_service = std::make_shared( - std::move(data_manager), module->m_slots, module->m_trust_stores, slot_manager); + std::move(data_manager), slot_registry, trust_store_manager, slot_manager); if (module->m_provider_manager) { module->m_provider_manager->ForEachProvider([&](const auto& /*id*/, const auto& provider) { diff --git a/score/crypto/src/daemon/cert_management/cert_management_module.hpp b/score/crypto/src/daemon/cert_management/cert_management_module.hpp index f461a4df6..c5d559976 100644 --- a/score/crypto/src/daemon/cert_management/cert_management_module.hpp +++ b/score/crypto/src/daemon/cert_management/cert_management_module.hpp @@ -14,7 +14,6 @@ #define SCORE_CRYPTO_SRC_DAEMON_CERT_MANAGEMENT_CERT_MANAGEMENT_MODULE_HPP #include "score/crypto/src/daemon/cert_management/core/cert_management_service.hpp" -#include "score/crypto/src/daemon/cert_management/slot/slot_registry.hpp" #include "score/crypto/src/daemon/config/inc/config.hpp" #include "score/crypto/src/daemon/provider/provider_manager.hpp" @@ -27,10 +26,6 @@ class CertManagementModule final static Sptr Create(data_manager::IDataManager::Sptr, provider::ProviderManager::Sptr, const config::CertificateConfig&); - CertSlotRegistry::Sptr GetSlotRegistry() const - { - return m_slots; - } CertManagementService::Sptr GetService() const { return m_service; @@ -42,8 +37,6 @@ class CertManagementModule final private: CertManagementModule() = default; - CertSlotRegistry::Sptr m_slots; - TrustStoreManager::Sptr m_trust_stores; CertManagementService::Sptr m_service; provider::ProviderManager::Sptr m_provider_manager; }; diff --git a/score/crypto/src/daemon/cert_management/core/cert_entry.hpp b/score/crypto/src/daemon/cert_management/core/cert_entry.hpp index 99b926301..1c901688c 100644 --- a/score/crypto/src/daemon/cert_management/core/cert_entry.hpp +++ b/score/crypto/src/daemon/cert_management/core/cert_entry.hpp @@ -20,13 +20,10 @@ #include "score/crypto/src/daemon/cert_management/slot/slot_registry.hpp" #include "score/crypto/src/daemon/data_manager/data_node.hpp" -#include -#include #include #include #include #include -#include namespace score::crypto::daemon::cert_management { @@ -43,8 +40,9 @@ class CertEntry final : public std::enable_shared_from_this public: /// @param object Owning cert object (must not be nullptr). /// @param slot_handle Non-default only when cert was loaded from a slot. - CertEntry(CertObject::Sptr object, CertSlotHandle slot_handle = CertSlotHandle{}) - : m_object{std::move(object)}, m_slot_handle{slot_handle} + /// @param owner The client that owns this entry. + CertEntry(CertObject::Sptr object, CertSlotHandle slot_handle = CertSlotHandle{}, data_manager::ClientId owner = 0U) + : m_object{std::move(object)}, m_slot_handle{slot_handle}, m_owner_client_id{owner} { } @@ -113,43 +111,25 @@ class CertEntry final : public std::enable_shared_from_this } // ----------------------------------------------------------------------- - // Reference counting + // Ownership // ----------------------------------------------------------------------- - void AddRef(data_manager::ClientId client_id) + [[nodiscard]] data_manager::ClientId GetOwner() const noexcept { - const std::lock_guard lock(m_ref_mutex); - m_ref_count.fetch_add(1U, std::memory_order_relaxed); - m_referencing_clients.push_back(client_id); - } - - /// @return true when the reference count has reached zero. - bool Release(data_manager::ClientId client_id) - { - const std::lock_guard lock(m_ref_mutex); - auto it = std::find(m_referencing_clients.begin(), m_referencing_clients.end(), client_id); - if (it == m_referencing_clients.end()) - { - return false; - } - m_referencing_clients.erase(it); - const std::uint32_t prev = m_ref_count.fetch_sub(1U, std::memory_order_acq_rel); - return prev == 1U; + return m_owner_client_id; } - [[nodiscard]] std::uint32_t GetRefCount() const noexcept + [[nodiscard]] bool IsOwnedBy(data_manager::ClientId client_id) const noexcept { - return m_ref_count.load(std::memory_order_acquire); + return m_owner_client_id == client_id; } private: CertObject::Sptr m_object; CertSlotHandle m_slot_handle; + data_manager::ClientId m_owner_client_id{0U}; mutable std::mutex m_ref_mutex; - std::atomic m_ref_count{0U}; - std::vector m_referencing_clients; - std::optional> m_session_crl; score::crypto::FormatType m_session_crl_format{score::crypto::FormatType::kDer}; }; diff --git a/score/crypto/src/daemon/cert_management/core/cert_management_service.cpp b/score/crypto/src/daemon/cert_management/core/cert_management_service.cpp index 5bdd4927d..9c3f02ce0 100644 --- a/score/crypto/src/daemon/cert_management/core/cert_management_service.cpp +++ b/score/crypto/src/daemon/cert_management/core/cert_management_service.cpp @@ -25,7 +25,7 @@ CertManagementService::CertManagementService(data_manager::IDataManager::Sptr da CertSlotManager::Sptr slot_manager) : m_data_manager{std::move(data_manager)}, m_slot_registry{std::move(slot_registry)}, - m_trust_stores{std::move(trust_store_manager)}, + m_trust_store_manager{std::move(trust_store_manager)}, m_slot_manager{std::move(slot_manager)} { } @@ -36,17 +36,16 @@ score::crypto::Expected CertManagementService::Regist { if (!object || !m_data_manager) return score::crypto::make_unexpected(Error::kInvalidArgument); - auto entry = std::make_shared(std::move(object), params.slot_handle); + auto entry = std::make_shared(std::move(object), params.slot_handle, params.client_id); auto id = params.slot_handle.IsValid() ? m_cert_registry.RegisterSlotCert(params.slot_handle, entry) : m_cert_registry.RegisterEphemeralCert(entry); - auto captured = entry; auto node = std::make_shared(entry, id, params.client_id, [this](CertRegistryId rid) { m_cert_registry.Unregister(rid); }); auto node_id = m_data_manager->addChildNode(params.client_id, params.parent_id, node); if (!node_id) return score::crypto::make_unexpected(Error::kInternalError); - return CertDataNodeResult{*node_id, std::move(captured)}; + return CertDataNodeResult{*node_id, std::move(entry)}; } score::crypto::Expected CertManagementService::Load(const CertRegistrationParams& params) @@ -72,8 +71,8 @@ void CertManagementService::CleanupClient(data_manager::ClientId client_id) m_cert_registry.CleanupClient(client_id); m_cert_slot_node_cache.erase(client_id); m_trust_store_node_cache.erase(client_id); - if (m_trust_stores) - m_trust_stores->CleanupClient(client_id); + if (m_trust_store_manager) + m_trust_store_manager->CleanupClient(client_id); } // --------------------------------------------------------------------------- @@ -121,25 +120,24 @@ score::crypto::Expected CertManagementService:: const std::string& resource_name, data_manager::ClientId client_id) { - if (!m_data_manager || !m_trust_stores) + if (!m_data_manager || !m_trust_store_manager) return score::crypto::make_unexpected(Error::kInternalError); - // Dedup: same client resolving the same trust store twice returns the cached node id. + auto handle_res = m_trust_store_manager->ResolveAppResource(resource_name, client_id); + if (!handle_res.has_value()) + return score::crypto::make_unexpected(handle_res.error()); + auto& client_cache = m_trust_store_node_cache[client_id]; - const auto it = client_cache.find(resource_name); + const auto it = client_cache.find(handle_res.value().index); if (it != client_cache.end()) return it->second; - auto handle_res = m_trust_stores->ResolveAppResource(resource_name, client_id); - if (!handle_res.has_value()) - return score::crypto::make_unexpected(handle_res.error()); - - auto node = std::make_shared(handle_res.value(), m_trust_stores); + auto node = std::make_shared(handle_res.value(), m_trust_store_manager); auto node_id = m_data_manager->addNode(client_id, std::move(node)); if (!node_id.has_value()) return score::crypto::make_unexpected(Error::kInternalError); - client_cache[resource_name] = node_id.value(); + client_cache[handle_res.value().index] = node_id.value(); return node_id.value(); } @@ -175,21 +173,10 @@ score::crypto::Expected CertManagementService::ResolveC data_manager::ClientId client_id, data_manager::DataNodeId cert_node_id) { - if (!m_data_manager) - return score::crypto::make_unexpected(Error::kInternalError); - - auto acc_res = m_data_manager->getNodeAccessor(client_id, cert_node_id); - if (!acc_res.has_value()) - return score::crypto::make_unexpected(Error::kInvalidArgument); - - auto typed_res = std::move(acc_res).value().downCast(); - if (!typed_res.has_value()) - return score::crypto::make_unexpected(Error::kInvalidArgument); - - auto entry = typed_res.value()->GetCertEntry(); - if (!entry) - return score::crypto::make_unexpected(Error::kInternalError); - return entry->GetCertObject(); + auto entry_res = ResolveCertEntryForOperation(client_id, cert_node_id); + if (!entry_res.has_value()) + return score::crypto::make_unexpected(entry_res.error()); + return entry_res.value()->GetCertObject(); } score::crypto::Expected, Error> CertManagementService::ResolveCertEntryForOperation( @@ -233,41 +220,11 @@ score::crypto::Expected CertManagementService::ResolveT void CertManagementService::NotifySlotCertChanged(CertSlotHandle slot_handle) { - if (!m_trust_stores) + if (!m_trust_store_manager) return; - const auto memberships = m_trust_stores->GetMembershipsForSlot(slot_handle); + const auto memberships = m_trust_store_manager->GetMembershipsForSlot(slot_handle); for (const auto ts_handle : memberships) - m_trust_stores->NotifySlotChanged(ts_handle, slot_handle); -} - -// --------------------------------------------------------------------------- -// CRL helpers -// --------------------------------------------------------------------------- - -score::crypto::Expected CertManagementService::AttachSessionCrl( - data_manager::ClientId client_id, - data_manager::DataNodeId cert_node_id, - std::vector crl_bytes, - score::crypto::FormatType format) -{ - if (!m_data_manager) - return score::crypto::make_unexpected(Error::kInternalError); - if (crl_bytes.empty()) - return score::crypto::make_unexpected(Error::kInvalidArgument); - - auto acc_res = m_data_manager->getNodeAccessor(client_id, cert_node_id); - if (!acc_res.has_value()) - return score::crypto::make_unexpected(Error::kInvalidArgument); - - auto typed_res = std::move(acc_res).value().downCast(); - if (!typed_res.has_value()) - return score::crypto::make_unexpected(Error::kInvalidArgument); - - auto entry = typed_res.value()->GetCertEntry(); - if (!entry) - return score::crypto::make_unexpected(Error::kInternalError); - entry->AttachSessionCrl(std::move(crl_bytes), format); - return std::monostate{}; + m_trust_store_manager->NotifySlotChanged(ts_handle, slot_handle); } } // namespace score::crypto::daemon::cert_management diff --git a/score/crypto/src/daemon/cert_management/core/cert_management_service.hpp b/score/crypto/src/daemon/cert_management/core/cert_management_service.hpp index d45dc0d52..f0e02230b 100644 --- a/score/crypto/src/daemon/cert_management/core/cert_management_service.hpp +++ b/score/crypto/src/daemon/cert_management/core/cert_management_service.hpp @@ -73,7 +73,7 @@ class CertManagementService final TrustStoreManager::Sptr GetTrustStoreManager() const { - return m_trust_stores; + return m_trust_store_manager; } CertSlotManager::Sptr GetSlotManager() const @@ -121,7 +121,7 @@ class CertManagementService final /// Look up a CertDataNode by node_id and return the underlying CertEntry. /// /// Use instead of ResolveCertForOperation when the session CRL association - /// (CertEntry::GetSessionCrl) is also needed — e.g. in HandleTrustStoreAdd. + /// (CertEntry::GetSessionCrl) is also needed. score::crypto::Expected, common::DaemonErrorCode> ResolveCertEntryForOperation( data_manager::ClientId client_id, data_manager::DataNodeId cert_node_id); @@ -136,26 +136,10 @@ class CertManagementService final /// Called after a successful StoreCertificate to keep trust store anchors current. void NotifySlotCertChanged(CertSlotHandle slot_handle); - // ----------------------------------------------------------------------- - // CRL helpers — called by the cert management executor - // ----------------------------------------------------------------------- - - /// Attach a session-scoped CRL to an ephemeral or slot-loaded certificate. - /// - /// The CRL bytes must be pre-validated by the caller (signature, issuer match, - /// validity). This stores the bytes in-memory on the CertEntry; they are not - /// written to disk. Subsequent SaveCertificate / AddCertificateToTrustStore - /// with with_crl=true will propagate from this in-memory association. - score::crypto::Expected AttachSessionCrl( - data_manager::ClientId client_id, - data_manager::DataNodeId cert_node_id, - std::vector crl_bytes, - score::crypto::FormatType format); - private: data_manager::IDataManager::Sptr m_data_manager; CertSlotRegistry::Sptr m_slot_registry; - TrustStoreManager::Sptr m_trust_stores; + TrustStoreManager::Sptr m_trust_store_manager; CertSlotManager::Sptr m_slot_manager; CertRegistry m_cert_registry; @@ -164,7 +148,7 @@ class CertManagementService final // within a client. std::unordered_map> m_cert_slot_node_cache; - std::unordered_map> + std::unordered_map> m_trust_store_node_cache; }; } // namespace score::crypto::daemon::cert_management diff --git a/score/crypto/src/daemon/cert_management/core/cert_registry.cpp b/score/crypto/src/daemon/cert_management/core/cert_registry.cpp index bba1b7691..447115225 100644 --- a/score/crypto/src/daemon/cert_management/core/cert_registry.cpp +++ b/score/crypto/src/daemon/cert_management/core/cert_registry.cpp @@ -75,18 +75,16 @@ void CertRegistry::CleanupClient(data_manager::ClientId client_id) std::vector to_remove; - for (auto& [id, cert_entry] : m_certs) + for (const auto& [id, cert_entry] : m_certs) { - if (cert_entry->Release(client_id)) - { + if (cert_entry->IsOwnedBy(client_id)) to_remove.push_back(id); - } } for (const auto id : to_remove) { - score::mw::log::LogDebug() << kLogPrefix << "CleanupClient: removing cert " << id - << " (ref_count reached 0 after client " << client_id << " cleanup)"; + score::mw::log::LogDebug() << kLogPrefix << "CleanupClient: removing cert " << id << " owned by client " + << client_id; m_certs.erase(id); } } diff --git a/score/crypto/src/daemon/cert_management/nodes/cert_data_node.hpp b/score/crypto/src/daemon/cert_management/nodes/cert_data_node.hpp index f5106fb7c..b53eb34bb 100644 --- a/score/crypto/src/daemon/cert_management/nodes/cert_data_node.hpp +++ b/score/crypto/src/daemon/cert_management/nodes/cert_data_node.hpp @@ -46,25 +46,20 @@ class CertDataNode final : public data_manager::DataNode CertDataNode(std::shared_ptr cert_entry, CertRegistryId registry_id, data_manager::ClientId client_id, - UnregisterCallback on_last_release) + UnregisterCallback unregister_callback) : DataNode(false), m_cert_entry{std::move(cert_entry)}, m_registry_id{registry_id}, m_client_id{client_id}, - m_on_last_release{std::move(on_last_release)} + m_unregister_callback{std::move(unregister_callback)} { - m_cert_entry->AddRef(m_client_id); } ~CertDataNode() override { - if (m_cert_entry != nullptr) + if (m_cert_entry != nullptr && m_unregister_callback) { - const bool last = m_cert_entry->Release(m_client_id); - if (last && m_on_last_release) - { - m_on_last_release(m_registry_id); - } + m_unregister_callback(m_registry_id); } } @@ -87,7 +82,7 @@ class CertDataNode final : public data_manager::DataNode std::shared_ptr m_cert_entry; CertRegistryId m_registry_id; data_manager::ClientId m_client_id; - UnregisterCallback m_on_last_release; + UnregisterCallback m_unregister_callback; }; } // namespace score::crypto::daemon::cert_management diff --git a/score/crypto/src/daemon/cert_management/query/cert_object_serializer.cpp b/score/crypto/src/daemon/cert_management/query/cert_object_serializer.cpp index b468d1324..64bec5a52 100644 --- a/score/crypto/src/daemon/cert_management/query/cert_object_serializer.cpp +++ b/score/crypto/src/daemon/cert_management/query/cert_object_serializer.cpp @@ -42,7 +42,7 @@ SerializeCertSlotInfo(CertSlotManager& mgr, CertSlotHandle slot, data_manager::C if (!info_res.has_value()) return score::crypto::make_unexpected(info_res.error()); - const bool has_crl = mgr.HasCrl(slot); + const bool has_crl = info_res.value().has_crl; int64_t crl_next = 0; if (has_crl) { diff --git a/score/crypto/src/daemon/cert_management/tests/core/test_cert_registry.cpp b/score/crypto/src/daemon/cert_management/tests/core/test_cert_registry.cpp index 922d9d922..bb4237554 100644 --- a/score/crypto/src/daemon/cert_management/tests/core/test_cert_registry.cpp +++ b/score/crypto/src/daemon/cert_management/tests/core/test_cert_registry.cpp @@ -86,10 +86,8 @@ TEST(CertRegistryTest, CleanupClient_RemovesOnlyThatClientsEntries) CertRegistry registry; - auto entry_a = std::make_shared(MakeCertificate()); - auto entry_b = std::make_shared(MakeCertificate()); - entry_a->AddRef(kClientA); - entry_b->AddRef(kClientB); + auto entry_a = std::make_shared(MakeCertificate(), CertSlotHandle{}, kClientA); + auto entry_b = std::make_shared(MakeCertificate(), CertSlotHandle{}, kClientB); const auto id_a = registry.RegisterEphemeralCert(entry_a); const auto id_b = registry.RegisterEphemeralCert(entry_b); @@ -117,10 +115,8 @@ TEST(CertRegistryTest, SlotCertPersistsAfterUnrelatedClientCleanup) CertRegistry registry; const CertSlotHandle slot{3U}; - auto slot_entry = std::make_shared(MakeCertificate(), slot); - auto ephemeral_entry = std::make_shared(MakeCertificate()); - slot_entry->AddRef(kClientA); - ephemeral_entry->AddRef(kClientB); + auto slot_entry = std::make_shared(MakeCertificate(), slot, kClientA); + auto ephemeral_entry = std::make_shared(MakeCertificate(), CertSlotHandle{}, kClientB); const auto slot_id = registry.RegisterSlotCert(slot, slot_entry); ASSERT_NE(slot_id, 0U); diff --git a/score/crypto/src/daemon/cert_management/tests/service/test_cert_management_service.cpp b/score/crypto/src/daemon/cert_management/tests/service/test_cert_management_service.cpp index b01ff5ac4..7f65a304c 100644 --- a/score/crypto/src/daemon/cert_management/tests/service/test_cert_management_service.cpp +++ b/score/crypto/src/daemon/cert_management/tests/service/test_cert_management_service.cpp @@ -375,61 +375,36 @@ TEST_F(CertManagementServiceTest, RegisterCertMaterial_EphemeralPath_CreatesReso } // --------------------------------------------------------------------------- -// ResolveCertEntryForOperation — returns the CertEntry behind a cert node. -// Enables callers to access session CRL state alongside the CertObject. -// --------------------------------------------------------------------------- - -TEST_F(CertManagementServiceTest, ResolveCertEntryForOperation_ReturnsSameEntryAsLoaded) -{ - const auto result = LoadCert(kClientA); - ASSERT_NE(result.node_id, 0U); - ASSERT_NE(result.entry, nullptr); - - const auto entry_res = m_service->ResolveCertEntryForOperation(kClientA, result.node_id); - ASSERT_TRUE(entry_res.has_value()); - // Must be the exact same CertEntry (shared ownership), not a copy. - EXPECT_EQ(entry_res->get(), result.entry.get()); - // The resolved entry must expose the same cert object. - EXPECT_EQ((*entry_res)->GetCertObject()->GetSubject(), kSubjectInitial); -} - -TEST_F(CertManagementServiceTest, ResolveCertEntryForOperation_UnknownNode_ReturnsError) -{ - EXPECT_FALSE(m_service->ResolveCertEntryForOperation(kClientA, 999U).has_value()); -} - -// --------------------------------------------------------------------------- -// AttachSessionCrl — stores CRL bytes in-memory on CertEntry; no disk write. -// Subsequent GetSessionCrl() on the entry returns the attached bytes. +// ResolveCertEntryForOperation — session CRL attachment path. +// +// AttachSessionCrl() was removed from CertManagementService; the executor now +// calls ResolveCertEntryForOperation() directly and calls entry->AttachSessionCrl(). +// These tests verify the entry-level behaviour accessible via the service. // --------------------------------------------------------------------------- -TEST_F(CertManagementServiceTest, AttachSessionCrl_StoresInMemoryNotOnDisk) +TEST_F(CertManagementServiceTest, SessionCrl_StoresInMemoryNotOnDisk) { const auto result = LoadCert(kClientA); ASSERT_NE(result.node_id, 0U); - - // No CRL on disk before the call. EXPECT_FALSE(result.entry->HasSessionCrl()); const std::vector crl_bytes{0xC0U, 0xC1U, 0xC2U}; - ASSERT_TRUE( - m_service->AttachSessionCrl(kClientA, result.node_id, crl_bytes, score::crypto::FormatType::kDer).has_value()); + auto entry_res = m_service->ResolveCertEntryForOperation(kClientA, result.node_id); + ASSERT_TRUE(entry_res.has_value()); + entry_res.value()->AttachSessionCrl(crl_bytes, score::crypto::FormatType::kDer); - // CRL must be accessible in-memory through the entry. EXPECT_TRUE(result.entry->HasSessionCrl()); const auto session_crl = result.entry->GetSessionCrl(); ASSERT_TRUE(session_crl.has_value()); EXPECT_EQ(*session_crl, crl_bytes); EXPECT_EQ(result.entry->GetSessionCrlFormat(), score::crypto::FormatType::kDer); - // No CRL file must have been written to disk. EXPECT_FALSE(std::filesystem::exists(m_dir / "root_ca.crl")); } -TEST_F(CertManagementServiceTest, AttachSessionCrl_UnknownNode_ReturnsError) +TEST_F(CertManagementServiceTest, SessionCrl_UnknownNode_ReturnsError) { - const std::vector crl_bytes{0x01U}; - EXPECT_FALSE(m_service->AttachSessionCrl(kClientA, 999U, crl_bytes, score::crypto::FormatType::kDer).has_value()); + EXPECT_FALSE(m_service->ResolveCertEntryForOperation(kClientA, 999U).has_value()); } } // namespace From bde596982fa26dcf8628d3effb02ca14b6350e6d Mon Sep 17 00:00:00 2001 From: Athul Mallappallil Date: Thu, 10 Sep 2026 23:01:46 +0200 Subject: [PATCH 36/37] Fix coverage workflow --- tools/coverage/BUILD | 1 + 1 file changed, 1 insertion(+) diff --git a/tools/coverage/BUILD b/tools/coverage/BUILD index fbecbeb89..a8333941f 100644 --- a/tools/coverage/BUILD +++ b/tools/coverage/BUILD @@ -36,6 +36,7 @@ score_coverage_scope( "//score/crypto/src/backend:score_backend_defines", "//score/crypto/src/backend/score_provider:backend", "//score/crypto/src/daemon/provider:provider_manager_factory", + "//score/crypto/src/daemon/cert_management:cert_object_serializer", "//score/crypto/src/ipc/grpc_adapter:grpc_control_server", "//score/cryptoki:cryptoki_cdylib_wrapped", "//score/cryptoki:cryptoki_headers", From 61322e955d580e64e4447aae39585d5c475b0a5b Mon Sep 17 00:00:00 2001 From: Athul Mallappallil Date: Fri, 18 Sep 2026 21:10:32 +0000 Subject: [PATCH 37/37] crl managment support --- score/crypto/src/daemon/cert_management/BUILD | 1 + .../cert_management/core/cert_entry.hpp | 21 +- .../core/cert_management_service.cpp | 93 ++++++- .../core/cert_management_service.hpp | 34 ++- .../architecture/cert_management_static.puml | 35 ++- .../architecture/component_architecture.rst | 29 +- .../docs/architecture/design_decisions.rst | 40 ++- .../cert_management_dynamic.puml | 45 ++++ .../docs/detailed_design/detailed_design.rst | 21 +- .../src/daemon/cert_management/docs/index.rst | 10 +- .../interfaces/access_policy.hpp | 6 +- .../interfaces/cert_object.hpp | 2 +- .../cert_management/interfaces/cert_types.hpp | 14 +- .../interfaces/i_cert_slot_handler.cpp | 7 +- .../interfaces/i_cert_slot_handler.hpp | 16 +- .../query/cert_object_serializer.cpp | 19 +- .../query/cert_object_serializer.hpp | 25 +- .../slot/cert_slot_manager.cpp | 39 ++- .../slot/cert_slot_manager.hpp | 11 +- .../cert_management/slot/crl_handler.cpp | 63 ++++- .../cert_management/slot/crl_handler.hpp | 15 +- .../slot/file_backed_slot_handler.cpp | 70 +++-- .../slot/file_backed_slot_handler.hpp | 5 +- .../cert_management/slot/slot_registry.hpp | 2 +- ...est_certificate_management_integration.cpp | 84 ++++++ .../query/test_cert_object_serializer.cpp | 72 ++--- .../service/test_cert_management_service.cpp | 251 ++++++++++++++++++ .../slot/test_file_backed_slot_handler.cpp | 76 ++++-- .../truststore/test_trust_store_manager.cpp | 57 ++-- .../truststore/trust_store_manager.cpp | 130 +++++++-- .../truststore/trust_store_manager.hpp | 14 +- .../src/daemon/provider/cert_management/BUILD | 3 + .../cert_management_operations.hpp | 11 +- .../provider/cert_management/cert_types.hpp | 17 +- .../cert_management/i_cert_parser.hpp | 9 +- .../cert_management/openssl_cert_parser.cpp | 52 +++- .../cert_management/openssl_cert_parser.hpp | 2 +- .../test_openssl_cert_parser.cpp | 24 +- .../certificate/basic/certificate.crl.der | Bin 0 -> 437 bytes .../certificate/basic/certificate.crl.pem | 41 +++ .../basic/certificate_leaf.chain.pem | 153 +++++++++++ .../certificate/basic/certificate_leaf.der | Bin 0 -> 830 bytes .../certificate/basic/certificate_leaf.pem | 78 ++++++ .../basic/certificate_leaf_slot.kv | 3 + .../certificate/basic/manifest.json | 24 +- .../certificate/pki_chain/manifest.json | 2 +- 46 files changed, 1449 insertions(+), 277 deletions(-) create mode 100644 score/tests/test_vectors/certificate/basic/certificate.crl.der create mode 100644 score/tests/test_vectors/certificate/basic/certificate.crl.pem create mode 100644 score/tests/test_vectors/certificate/basic/certificate_leaf.chain.pem create mode 100644 score/tests/test_vectors/certificate/basic/certificate_leaf.der create mode 100644 score/tests/test_vectors/certificate/basic/certificate_leaf.pem create mode 100644 score/tests/test_vectors/certificate/basic/certificate_leaf_slot.kv diff --git a/score/crypto/src/daemon/cert_management/BUILD b/score/crypto/src/daemon/cert_management/BUILD index 37caf2df6..51829daf8 100644 --- a/score/crypto/src/daemon/cert_management/BUILD +++ b/score/crypto/src/daemon/cert_management/BUILD @@ -41,6 +41,7 @@ cc_library( visibility = ["//:__subpackages__"], deps = [ "//score/crypto/src/api/common:crypto_common", + "//score/crypto/src/api/types:types", "//score/crypto/src/api/config:context_configs", "//score/crypto/src/common:common_types", "//score/crypto/src/daemon/common", diff --git a/score/crypto/src/daemon/cert_management/core/cert_entry.hpp b/score/crypto/src/daemon/cert_management/core/cert_entry.hpp index 1c901688c..2e5decc46 100644 --- a/score/crypto/src/daemon/cert_management/core/cert_entry.hpp +++ b/score/crypto/src/daemon/cert_management/core/cert_entry.hpp @@ -14,7 +14,8 @@ #ifndef SCORE_CRYPTO_SRC_DAEMON_CERT_MANAGEMENT_CORE_CERT_ENTRY_HPP #define SCORE_CRYPTO_SRC_DAEMON_CERT_MANAGEMENT_CORE_CERT_ENTRY_HPP -#include "score/crypto/src/api/common/types.hpp" +#include "score/crypto/src/api/types/certificate.hpp" +#include "score/crypto/src/api/types/common.hpp" #include "score/crypto/src/daemon/cert_management/interfaces/cert_object.hpp" #include "score/crypto/src/daemon/cert_management/interfaces/cert_types.hpp" #include "score/crypto/src/daemon/cert_management/slot/slot_registry.hpp" @@ -69,17 +70,19 @@ class CertEntry final : public std::enable_shared_from_this // A session CRL is validated externally (signature, issuer match, validity) // before being attached. CertEntry stores the bytes verbatim; no re-validation // occurs. The association is session-lifetime — it does not survive a daemon - // restart and is never written to disk. This allows ImportCrl(persist=false) - // to associate a CRL with either an ephemeral kCertificate or a persistent - // kCertSlot without requiring write access to the slot's deployment descriptor. + // restart and is never written to disk. This allows ImportCrl() to associate + // a CRL with an ephemeral kCertificate without requiring slot write access. // ----------------------------------------------------------------------- /// Attach a previously-validated CRL to this entry for the current session. - void AttachSessionCrl(std::vector crl_bytes, score::crypto::FormatType format) + void AttachSessionCrl(std::vector crl_bytes, + score::crypto::FormatType format, + std::optional metadata = std::nullopt) { const std::lock_guard lock(m_ref_mutex); m_session_crl = std::move(crl_bytes); m_session_crl_format = format; + m_session_crl_metadata = std::move(metadata); } /// True when a session-scoped CRL has been attached via AttachSessionCrl(). @@ -102,12 +105,19 @@ class CertEntry final : public std::enable_shared_from_this return m_session_crl_format; } + [[nodiscard]] std::optional GetSessionCrlMetadata() const + { + const std::lock_guard lock(m_ref_mutex); + return m_session_crl_metadata; + } + /// Remove the session CRL association (e.g. after persisting to a slot). void ClearSessionCrl() { const std::lock_guard lock(m_ref_mutex); m_session_crl.reset(); m_session_crl_format = score::crypto::FormatType::kDer; + m_session_crl_metadata.reset(); } // ----------------------------------------------------------------------- @@ -132,6 +142,7 @@ class CertEntry final : public std::enable_shared_from_this mutable std::mutex m_ref_mutex; std::optional> m_session_crl; score::crypto::FormatType m_session_crl_format{score::crypto::FormatType::kDer}; + std::optional m_session_crl_metadata; }; } // namespace score::crypto::daemon::cert_management diff --git a/score/crypto/src/daemon/cert_management/core/cert_management_service.cpp b/score/crypto/src/daemon/cert_management/core/cert_management_service.cpp index 9c3f02ce0..b826a1b37 100644 --- a/score/crypto/src/daemon/cert_management/core/cert_management_service.cpp +++ b/score/crypto/src/daemon/cert_management/core/cert_management_service.cpp @@ -173,10 +173,49 @@ score::crypto::Expected CertManagementService::ResolveC data_manager::ClientId client_id, data_manager::DataNodeId cert_node_id) { - auto entry_res = ResolveCertEntryForOperation(client_id, cert_node_id); - if (!entry_res.has_value()) - return score::crypto::make_unexpected(entry_res.error()); - return entry_res.value()->GetCertObject(); + auto res = ResolveCertWithCrlMetadataForOperation(client_id, cert_node_id); + if (!res.has_value()) + return score::crypto::make_unexpected(res.error()); + return res.value().cert; +} + +score::crypto::Expected +CertManagementService::ResolveCertWithCrlMetadataForOperation(data_manager::ClientId client_id, + data_manager::DataNodeId cert_node_id) +{ + if (!m_data_manager) + return score::crypto::make_unexpected(Error::kInternalError); + + auto acc_res = m_data_manager->getNodeAccessor(client_id, cert_node_id); + if (!acc_res.has_value()) + return score::crypto::make_unexpected(Error::kInvalidArgument); + + auto accessor = std::move(acc_res).value(); + if (accessor->GetNodeType() == data_manager::DataNodeType::kCertData) + { + auto typed_res = std::move(accessor).downCast(); + if (!typed_res.has_value() || !typed_res.value()->GetCertEntry()) + return score::crypto::make_unexpected(Error::kInvalidArgument); + auto& entry = *typed_res.value()->GetCertEntry(); + + auto crl_metadata = entry.GetSessionCrlMetadata(); + if (!crl_metadata.has_value() && entry.GetSlotHandle().IsValid() && m_slot_manager) + crl_metadata = m_slot_manager->GetCrlMetadata(entry.GetSlotHandle()); + + return ResolvedCertWithCrlMetadata{entry.GetCertObject(), crl_metadata}; + } + + if (accessor->GetNodeType() != data_manager::DataNodeType::kCertSlot) + return score::crypto::make_unexpected(Error::kInvalidArgument); + auto slot_res = std::move(accessor).downCast(); + if (!slot_res.has_value() || !m_slot_manager) + return score::crypto::make_unexpected(Error::kInvalidArgument); + + const auto handle = slot_res.value()->GetSlotHandle(); + auto loaded = m_slot_manager->LoadCertificate(handle, client_id); + if (!loaded.has_value()) + return score::crypto::make_unexpected(loaded.error()); + return ResolvedCertWithCrlMetadata{std::move(*loaded), m_slot_manager->GetCrlMetadata(handle)}; } score::crypto::Expected, Error> CertManagementService::ResolveCertEntryForOperation( @@ -193,13 +232,57 @@ score::crypto::Expected, Error> CertManagementService auto typed_res = std::move(acc_res).value().downCast(); if (!typed_res.has_value()) return score::crypto::make_unexpected(Error::kInvalidArgument); - auto entry = typed_res.value()->GetCertEntry(); if (!entry) return score::crypto::make_unexpected(Error::kInternalError); return entry; } +score::crypto::Expected, Error> CertManagementService::ResolveCrlForOperation( + data_manager::ClientId client_id, + data_manager::DataNodeId cert_node_id) +{ + if (!m_slot_manager) + return score::crypto::make_unexpected(Error::kInternalError); + + std::optional source_slot; + + // Not a CertDataNode is not an error here — falls through to the direct-slot case below. + auto entry_res = ResolveCertEntryForOperation(client_id, cert_node_id); + if (entry_res.has_value()) + { + auto& entry = *entry_res.value(); + auto session_crl = entry.GetSessionCrl(); + if (session_crl.has_value()) + return ResolvedCrl{std::move(*session_crl), entry.GetSessionCrlFormat(), entry.GetSessionCrlMetadata()}; + if (entry.GetSlotHandle().IsValid()) + source_slot = entry.GetSlotHandle(); + } + + if (!source_slot.has_value()) + { + auto slot_res = ResolveSlotForOperation(client_id, cert_node_id); + if (slot_res.has_value()) + source_slot = slot_res.value().handle; + } + + if (!source_slot.has_value()) + return std::nullopt; + + const auto has_crl = m_slot_manager->HasCrl(*source_slot); + if (!has_crl.has_value()) + return score::crypto::make_unexpected(has_crl.error()); + if (!has_crl.value()) + return std::nullopt; + + auto crl = m_slot_manager->LoadCrl(*source_slot, client_id); + if (!crl.has_value()) + return score::crypto::make_unexpected(crl.error()); + + return ResolvedCrl{ + std::move(*crl), m_slot_manager->GetCrlFormat(*source_slot), m_slot_manager->GetCrlMetadata(*source_slot)}; +} + score::crypto::Expected CertManagementService::ResolveTrustStoreForOperation( data_manager::ClientId client_id, data_manager::DataNodeId ts_node_id) diff --git a/score/crypto/src/daemon/cert_management/core/cert_management_service.hpp b/score/crypto/src/daemon/cert_management/core/cert_management_service.hpp index f0e02230b..99e3804ba 100644 --- a/score/crypto/src/daemon/cert_management/core/cert_management_service.hpp +++ b/score/crypto/src/daemon/cert_management/core/cert_management_service.hpp @@ -13,7 +13,7 @@ #ifndef SCORE_CRYPTO_SRC_DAEMON_CERT_MANAGEMENT_CORE_CERT_MANAGEMENT_SERVICE_HPP #define SCORE_CRYPTO_SRC_DAEMON_CERT_MANAGEMENT_CORE_CERT_MANAGEMENT_SERVICE_HPP -#include "score/crypto/src/api/common/types.hpp" +#include "score/crypto/src/api/types/common.hpp" #include "score/crypto/src/daemon/cert_management/core/cert_registry.hpp" #include "score/crypto/src/daemon/cert_management/nodes/cert_data_node.hpp" #include "score/crypto/src/daemon/cert_management/slot/cert_slot_manager.hpp" @@ -22,6 +22,7 @@ #include #include +#include #include #include @@ -43,6 +44,22 @@ struct ResolvedCertSlot const CertSlotConfig* config{nullptr}; }; +/// CRL resolved for propagation to another cert resource, returned by ResolveCrlForOperation. +struct ResolvedCrl +{ + std::vector bytes; + score::crypto::FormatType format{score::crypto::FormatType::kDer}; + std::optional metadata; +}; + +/// Certificate plus its associated CRL metadata (if any), returned by +/// ResolveCertWithCrlMetadataForOperation. +struct ResolvedCertWithCrlMetadata +{ + CertObject::Sptr cert; + std::optional crl_metadata; +}; + class CertManagementService final { public: @@ -113,12 +130,17 @@ class CertManagementService final data_manager::ClientId client_id, data_manager::DataNodeId slot_node_id); - /// Look up a CertDataNode by node_id and return the underlying CertObject. + /// Look up a certificate or certificate-slot node and return its CertObject. score::crypto::Expected ResolveCertForOperation( data_manager::ClientId client_id, data_manager::DataNodeId cert_node_id); - /// Look up a CertDataNode by node_id and return the underlying CertEntry. + /// Look up a certificate or certificate-slot node and return its CertObject together with + /// the CRL metadata associated with it (session CRL, else entry-linked or direct slot CRL). + score::crypto::Expected + ResolveCertWithCrlMetadataForOperation(data_manager::ClientId client_id, data_manager::DataNodeId cert_node_id); + + /// Look up a certificate data node and return its CertEntry. /// /// Use instead of ResolveCertForOperation when the session CRL association /// (CertEntry::GetSessionCrl) is also needed. @@ -126,6 +148,12 @@ class CertManagementService final data_manager::ClientId client_id, data_manager::DataNodeId cert_node_id); + /// Resolves a `with_crl` propagation source: session CRL on the entry, else persistent CRL on + /// its slot (direct or entry-linked). std::nullopt (success) means no CRL is available. + score::crypto::Expected, common::DaemonErrorCode> ResolveCrlForOperation( + data_manager::ClientId client_id, + data_manager::DataNodeId cert_node_id); + /// Look up a TrustStoreDataNode by node_id and return the trust store handle. score::crypto::Expected ResolveTrustStoreForOperation( data_manager::ClientId client_id, diff --git a/score/crypto/src/daemon/cert_management/docs/architecture/cert_management_static.puml b/score/crypto/src/daemon/cert_management/docs/architecture/cert_management_static.puml index 6e678d6bf..f1d3975e6 100644 --- a/score/crypto/src/daemon/cert_management/docs/architecture/cert_management_static.puml +++ b/score/crypto/src/daemon/cert_management/docs/architecture/cert_management_static.puml @@ -59,9 +59,19 @@ package "daemon/common" { [Shared Deployment Storage] } +package "api/types" { + [common.hpp] + [certificate.hpp] + [key.hpp] +} + package "provider" { [OpenSslCertParser] - [Provider Context Handlers] + [Certificate Management Handler] + [Certificate Management Executor] + [Trust-Store Management Handler] + [Trust-Store Management Executor] + [Certificate Verification Handler] } package "daemon/mediator" { @@ -109,9 +119,28 @@ end note [Pkcs11CertSlotHandler] --> [CrlHandler] [OpenSslCertParser] ..|> ICertParser [OpenSslCertParser] --> [CertObject] -[Provider Context Handlers] --> [CertObject] +[Certificate Management Handler] --> [Certificate Management Executor] +[Trust-Store Management Handler] --> [Trust-Store Management Executor] +[Certificate Verification Handler] --> [TrustStoreManager] +[Certificate Management Executor] --> [CertManagementService] +[Trust-Store Management Executor] --> [CertManagementService] +[Certificate Verification Handler] --> [CertObject] +[Certificate Management Executor] --> [CertObject] [CertObjectSerializer] --> [CertManagementService] -[Provider Context Handlers] --> [CertObjectSerializer] +[Certificate Management Executor] --> [CertObjectSerializer] [Mediator] --> [CertObjectSerializer] +[certificate.hpp] --> [common.hpp] +[key.hpp] --> [common.hpp] +[CertManagementService] ..> [certificate.hpp] : public values + +note right of [Certificate Management Handler] + CERT:MANAGEMENT + Certificate and slot lifecycle +end note + +note right of [Trust-Store Management Handler] + CERT:TRUST_STORE + Membership curation and member CRLs +end note @enduml diff --git a/score/crypto/src/daemon/cert_management/docs/architecture/component_architecture.rst b/score/crypto/src/daemon/cert_management/docs/architecture/component_architecture.rst index f34b48ff5..495250200 100644 --- a/score/crypto/src/daemon/cert_management/docs/architecture/component_architecture.rst +++ b/score/crypto/src/daemon/cert_management/docs/architecture/component_architecture.rst @@ -60,9 +60,28 @@ The implementation is divided by responsibility: wire-format encoding for certificate, slot, and trust-store objects. Both the executor and the mediator's typed-object handlers depend on this module; changing the wire layout requires one edit. +* Public value definitions are owned by ``api/types``: ``common.hpp`` contains + cross-domain resource/provider types, ``certificate.hpp`` contains certificate, + CRL, OCSP, and verification types, and ``key.hpp`` contains key slot and + permission types. ``api/common`` contains shared utilities and guards, not + domain-specific type contracts. * ``provider/`` supplies parsing and provider-specific context handlers. The selected certificate-management provider must expose ``ICertParser``; certificate management does not require a particular provider. + The public certificate contexts route by scoped type: ``CERT:MANAGEMENT`` + owns certificate lifecycle operations, while ``CERT:TRUST_STORE`` owns + trust-store membership curation. Both handlers share the same + ``CertManagementService`` and certificate-management capability. + +Public certificate loading +-------------------------- + +Certificate contexts support two equivalent resource paths. Passing a resolved +``kCertSlot`` directly lets the context load and release the certificate for its +own operation. ``ICertificateManagementContext::LoadCertificate(slot)`` performs +an explicit load and returns a guarded ephemeral ``kCertificate`` resource. +Applications can retain that guard and reuse the loaded certificate across +multiple contexts; destroying the guard releases the client-owned data node. .. uml:: cert_management_static.puml @@ -74,7 +93,7 @@ certificate slot or trust store is represented by a lightweight DataNode. A certificate loaded from a slot becomes a ``CertDataNode`` backed by a per-client ``CertEntry``. Each ``Load`` call produces an independent ``CertEntry`` so that per-client state — such as a session-scoped CRL -associated via ``ImportCrl(persist=false)`` — cannot bleed across clients. +associated via session-scoped ``ImportCrl`` — cannot bleed across clients. ``CertSlotManager`` holds a weak-ptr cache of ``CertObject`` instances keyed by slot: when the cache entry is live, multiple clients share the same parsed bytes without redundant I/O; when it expires, the next load re-reads the slot. @@ -96,7 +115,11 @@ Architecture constraints * Certificate slots reference one storage backend selected at startup. * Trust stores reference certificate slots, never raw certificate paths. * CRLs are slot-scoped and are not independently resolved. +* CRL metadata is exposed through certificate views; CRL encoding format stays + internal to storage and provider decoding. * All trust-store mutation paths require trust-store write authorization. +* Trust-store mutations are dispatched through ``CERT:TRUST_STORE``; the + ``CERT:MANAGEMENT`` context retains certificate-slot and slot-CRL lifecycle. * Shared deployment writes are atomic; a partially written descriptor must not replace the previous valid descriptor. @@ -163,8 +186,8 @@ documented with full context, alternatives considered, and consequences in Current limitations -------------------- -The current implementation does not provide CRL validation during -verification, OCSP, or hardware-key CSR signing. Hardware CSR signing +CRL validation and CRL-based verification are implemented, including selected +CRL metadata reporting. OCSP remains deferred. Hardware-key CSR signing requires a cross-context service using ``Sign`` and ``GetPublicKeyDer`` without exporting private key material. Provider and daemon dispatch integration is outside this component's storage and lifecycle boundary. diff --git a/score/crypto/src/daemon/cert_management/docs/architecture/design_decisions.rst b/score/crypto/src/daemon/cert_management/docs/architecture/design_decisions.rst index 54270005b..6c00f374a 100644 --- a/score/crypto/src/daemon/cert_management/docs/architecture/design_decisions.rst +++ b/score/crypto/src/daemon/cert_management/docs/architecture/design_decisions.rst @@ -60,7 +60,9 @@ Three representations were considered for trust-store membership: ``CertSlotRegistry`` resolves each name to a ``CertSlotHandle`` at startup. The ``TrustStoreManager`` maintains a reverse index (slot → stores) so that a single ``NotifySlotChanged(slot)`` call after ``StoreCertificate`` invalidates - every affected trust store's anchor cache in O(1). + every affected trust store's anchor cache. Conditional-external members are + disabled and persisted until ``AcknowledgeMemberUpdate`` records the new + fingerprint. Decision -------- @@ -161,10 +163,22 @@ Decision -------- CRL co-location with the cert slot (option 3) was selected. ``ICertSlotHandler`` -is extended with ``HasCrl``, ``LoadCrl``, ``StoreCrl``, ``ClearCrl``, and -``GetCrlNextUpdate`` — implemented by the composed ``CrlHandler``. The -``crl_next_update`` epoch is written by ``StoreCrl`` and exposed through the -descriptor so the daemon can evaluate CRL freshness without loading the full DER. +is extended with ``HasCrl``, ``LoadCrl``, ``StoreCrl``, ``ClearCrl``, +``GetCrlNextUpdate``, and ``GetCrlMetadata`` — implemented by the composed +``CrlHandler``. Validated CRLs persist their fingerprint, issuer fingerprint, +``thisUpdate``, ``nextUpdate``, and ``cRLNumber`` in the descriptor. The public +``ICertificateObject`` exposes this as optional ``CrlMetadata``; encoding format +remains an internal persistence detail. Persisted ``CrlMetadata`` is populated +only through ``ImportCrl``/``StoreCrl``; a CRL placed into a slot's ``[crl]`` +section out of band (only ``crl_path``/``crl_format`` set, no fingerprint +fields) has no cached metadata and ``GetCrlMetadata`` returns no value for it. +This does not affect revocation checking: ``OpenSslCertVerificationHandler`` +never reads the descriptor's cached ``CrlMetadata`` — it re-parses each raw +CRL and recomputes fingerprint, issuer fingerprint, ``thisUpdate``, +``nextUpdate``, and ``cRLNumber`` directly from the CRL bytes on every +``DoVerify()`` call. The cached descriptor fields exist solely to answer +inspection queries (``ICertificateObject::GetCrlMetadata``) without +deserialising the CRL. For PKCS#11 token slots (where no filesystem cert path exists), ``CrlHandler`` stores CRL data in the deployment filesystem using the same ``crl_path`` @@ -181,8 +195,7 @@ Consequences cross-registry coordination. * The verification handler walks trust-store member slots and calls ``HasCrl`` per slot — no separate CRL lookup service. -* ``crl_next_update`` in the descriptor enables freshness checks without - deserialising the DER. +* CRL metadata is available without deserialising the CRL during normal reads. **Negative:** @@ -191,6 +204,10 @@ Consequences * CRL data is not independently addressable — no ``kCrl`` API resource type. Applications use ``ImportCrl(cert_resource_id, ...)`` rather than ``ImportCrl(crl_resource_id, ...)``. +* A CRL written to a slot's ``[crl]`` section out of band (bypassing + ``ImportCrl``/``StoreCrl``) has no cached ``CrlMetadata``; inspection + queries report no metadata for it even though the CRL is fully usable for + revocation checking. There is no on-demand derivation path. --- @@ -286,6 +303,15 @@ Consequences * Misconfigured ``writers`` sets are caught at runtime, not at compile time. * Long-term refactoring to a shared ACL component will require updating enforcement sites in both ``key_management`` and ``cert_management``. +* ``CertSlotManager``'s slot-write enforcement is keyed purely off + ``allowed_write_uids``; it has no notion of trust-store ownership. A + ``kExclusiveMutable`` trust-store member slot must therefore be configured + with an empty ``allowed_write_uids`` — the exclusive slot is meant to be + written only through the trust store's own write-access gate. A non-empty + list on such a slot lets any listed UID write to it directly, bypassing + that gate and the trust store's cache/notification bookkeeping. This is + currently a configuration invariant rather than one enforced by + ``CertSlotManager`` itself. --- diff --git a/score/crypto/src/daemon/cert_management/docs/detailed_design/cert_management_dynamic.puml b/score/crypto/src/daemon/cert_management/docs/detailed_design/cert_management_dynamic.puml index 5fa0a7fad..96e876080 100644 --- a/score/crypto/src/daemon/cert_management/docs/detailed_design/cert_management_dynamic.puml +++ b/score/crypto/src/daemon/cert_management/docs/detailed_design/cert_management_dynamic.puml @@ -21,6 +21,9 @@ participant "CertManagementService" as Service participant "CertSlotManager" as SlotMgr participant "FileBackedSlotHandler" as Handler participant "OpenSslCertParser" as Parser +participant "Certificate Management Handler" as CertHandler +participant "Trust-Store Management Handler" as TrustHandler +participant "Trust-Store Management Executor" as TrustExecutor participant "CertObjectCache\n(weak_ptr, per slot)" as Cache participant "CertRegistry" as Registry participant "TrustStoreHandler" as StoreHandler @@ -56,6 +59,38 @@ Registry --> SlotMgr : cert_registry_id SlotMgr --> Service : CertObject + CertEntry (per-client) Service -> Data : addChildNode(CertDataNode) +== Public guarded load == + +"CertificateManagementContext" -> Service : CERT_LOAD(slot_node_id) +Service -> SlotMgr : Load(params, client) +SlotMgr --> Service : CertObject + CertEntry +Service -> Data : addChildNode(CertDataNode) +Data --> "CertificateManagementContext" : certificate_node_id +"CertificateManagementContext" --> "Application" : CryptoResourceGuard(kCertificate) +note right of "Application" + The guard can be passed to multiple + certificate contexts. Its destruction + releases the client-owned CertDataNode. +end note + +== Slot-direct context use == + +"CertificateVerificationContext" -> Service : SetCertificate(kCertSlot) +note right of Service + The context may resolve/load the slot + internally for one operation. Use + CERT_LOAD when the loaded certificate + should be shared across contexts. +end note + +== Trust-store management routing == + +"TrustStoreManagementContext" -> CertHandler : CTX_CREATE(CERT:TRUST_STORE) +CertHandler -> TrustHandler : Create trust-store handler +TrustHandler -> TrustExecutor : Add/remove/enable/disable/acknowledge +TrustExecutor -> Service : TrustStoreManager operation +Service --> "TrustStoreManagementContext" : result + == Certificate load — second client (cache hit, shared bytes) == Service -> SlotMgr : Load(params, client_B) @@ -109,6 +144,16 @@ end note SlotMgr --> Service : ok Service -> Stores : NotifySlotChanged(store, slot) Stores -> StoreHandler : InvalidateSlot(slot) +alt conditional-external member + Stores -> Stores : Disable member and persist state + note right of Stores + accepted_fingerprint remains unchanged. + AcknowledgeMemberUpdate records the + replacement fingerprint and re-enables the member. + end note +else other member kind + Stores -> Stores : Retain member enablement state +end Service -> StoreHandler : GetAnchors() StoreHandler -> Stores : reload changed slot Stores -> SlotMgr : LoadCertificate() diff --git a/score/crypto/src/daemon/cert_management/docs/detailed_design/detailed_design.rst b/score/crypto/src/daemon/cert_management/docs/detailed_design/detailed_design.rst index 8fea8a82e..3986ec300 100644 --- a/score/crypto/src/daemon/cert_management/docs/detailed_design/detailed_design.rst +++ b/score/crypto/src/daemon/cert_management/docs/detailed_design/detailed_design.rst @@ -35,7 +35,7 @@ Implementation units Own live certificate entries. Each ``Load`` call produces a fresh ``CertEntry`` per client; entries are never shared across clients. ``CertEntry`` holds a ``CertObject::Sptr`` (the immutable parsed bytes) and - an optional session-scoped CRL (from ``ImportCrl`` with ``persist=false``) + an optional session-scoped CRL from ``ImportCrl`` that is never written to disk. Because ``CertEntry`` is per-client, the session CRL is isolated — one client's ``ImportCrl`` cannot be observed by another client that loaded the same slot. @@ -107,18 +107,21 @@ Trust-store update contract After a successful slot certificate update, the service obtains the reverse membership list and calls ``TrustStoreManager::NotifySlotChanged``. The manager -invalidates the affected ``TrustStoreHandler`` cache. The next ``GetAnchors`` -operation reloads the slot and reconstructs its ``CertObject`` through the -injected parser. +invalidates the affected ``TrustStoreHandler`` cache. For a +``kConditionalExternal`` member, the manager also disables the member and +persists its existing accepted fingerprint. The member remains unavailable +until ``AcknowledgeMemberUpdate`` records the replacement fingerprint and +re-enables it. Other member kinds retain their enablement state. The next +``GetAnchors`` operation reloads the slot and reconstructs its ``CertObject`` +through the injected parser. Provider boundary and scope ---------------------------- The core component does not depend on OpenSSL or PKCS#11 concrete types. -OpenSSL currently supplies parsing and verification implementations; PKCS#11 -supplies a read-only certificate-slot backend. Hardware-key CSR generation, -CRL validation, OCSP, and mediator dispatch are outside this component's -storage and lifecycle scope. Hardware CSR signing must use a cross-context -service without exporting private key material. +OpenSSL supplies parsing and verification implementations; PKCS#11 supplies +certificate-slot storage. Hardware-key CSR signing uses a cross-context +service without exporting private key material. OCSP remains a provider-boundary +extension. .. uml:: cert_management_dynamic.puml diff --git a/score/crypto/src/daemon/cert_management/docs/index.rst b/score/crypto/src/daemon/cert_management/docs/index.rst index eb51df960..51281a35c 100644 --- a/score/crypto/src/daemon/cert_management/docs/index.rst +++ b/score/crypto/src/daemon/cert_management/docs/index.rst @@ -91,13 +91,6 @@ This drives several design decisions: application's verification context cannot evict another application's active anchor cache. -Backwards Compatibility Impact -------------------------------- - -The component introduces no changes to existing certificate-management -callers. The certificate-management, certificate-verification, and -CSR-generation context interfaces are defined in ``score/crypto/src/api/``. - Security Impact --------------- @@ -111,7 +104,8 @@ Security Impact * Trust store mutations (``AddMember``, ``RemoveMember``, ``DisableMember``, ``EnableMember``) carry a separate write permission check on the trust store policy, independent of the member slot's - write policy. + write policy, and are dispatched through the ``CERT:TRUST_STORE`` context + handler. Safety Impact ------------- diff --git a/score/crypto/src/daemon/cert_management/interfaces/access_policy.hpp b/score/crypto/src/daemon/cert_management/interfaces/access_policy.hpp index 5165f66ec..2cc5be4f1 100644 --- a/score/crypto/src/daemon/cert_management/interfaces/access_policy.hpp +++ b/score/crypto/src/daemon/cert_management/interfaces/access_policy.hpp @@ -31,8 +31,10 @@ struct AccessPolicy /// CERT_CLEAR, TRUST_STORE_ADD_CERT, TRUST_STORE_REMOVE_CERT). /// /// Mutation authorization is explicit and default-deny: an empty list - /// grants no write permission. Trust-store-owned exclusive slots do not - /// use this list; their writes are authorized by TrustStoreManager. + /// grants no write permission. Trust-store-owned exclusive slots must be + /// configured with an empty list — see the "Mutation Default-Deny with + /// Explicit Writer UID" design decision (design_decisions.rst) for why, + /// and the TODO in CertSlotManager::ApplyWriteChecks for enforcement status. std::vector allowed_write_uids; }; diff --git a/score/crypto/src/daemon/cert_management/interfaces/cert_object.hpp b/score/crypto/src/daemon/cert_management/interfaces/cert_object.hpp index a1859cb80..fcaf02544 100644 --- a/score/crypto/src/daemon/cert_management/interfaces/cert_object.hpp +++ b/score/crypto/src/daemon/cert_management/interfaces/cert_object.hpp @@ -14,7 +14,7 @@ #ifndef SCORE_CRYPTO_SRC_DAEMON_CERT_MANAGEMENT_INTERFACES_CERT_OBJECT_HPP #define SCORE_CRYPTO_SRC_DAEMON_CERT_MANAGEMENT_INTERFACES_CERT_OBJECT_HPP -#include "score/crypto/src/api/common/types.hpp" +#include "score/crypto/src/api/types/common.hpp" #include "score/crypto/src/common/types.hpp" #include "score/crypto/src/daemon/cert_management/interfaces/cert_types.hpp" diff --git a/score/crypto/src/daemon/cert_management/interfaces/cert_types.hpp b/score/crypto/src/daemon/cert_management/interfaces/cert_types.hpp index c9a7d9723..d8a5976de 100644 --- a/score/crypto/src/daemon/cert_management/interfaces/cert_types.hpp +++ b/score/crypto/src/daemon/cert_management/interfaces/cert_types.hpp @@ -14,7 +14,7 @@ #ifndef SCORE_CRYPTO_SRC_DAEMON_CERT_MANAGEMENT_INTERFACES_CERT_TYPES_HPP #define SCORE_CRYPTO_SRC_DAEMON_CERT_MANAGEMENT_INTERFACES_CERT_TYPES_HPP -#include "score/crypto/src/api/common/types.hpp" +#include "score/crypto/src/api/types/common.hpp" #include "score/crypto/src/common/types.hpp" #include "score/crypto/src/daemon/common/types.hpp" @@ -199,10 +199,22 @@ inline constexpr std::string_view kCrlPath = "crl_path"; /// Encoding of the CRL file: "pem" or "der". inline constexpr std::string_view kCrlFormat = "crl_format"; +/// Hex-encoded SHA-256 fingerprint of the CRL DER encoding. +inline constexpr std::string_view kCrlFingerprint = "crl_fingerprint"; + +/// Hex-encoded SHA-256 fingerprint of the CRL issuer certificate. +inline constexpr std::string_view kCrlIssuerFingerprint = "crl_issuer_fingerprint"; + +/// Unix epoch seconds corresponding to the CRL's thisUpdate field. +inline constexpr std::string_view kCrlThisUpdate = "crl_this_update"; + /// ISO-8601 UTC timestamp of the CRL's nextUpdate field. /// Written when the CRL is imported; read by the daemon to schedule refresh. inline constexpr std::string_view kCrlNextUpdate = "crl_next_update"; +/// Issuer-assigned CRL revision; zero when cRLNumber is absent. +inline constexpr std::string_view kCrlNumber = "crl_number"; + // ---- [certificate_metadata] section -------------------------------------- /// RFC 4514 canonical Subject DN string. diff --git a/score/crypto/src/daemon/cert_management/interfaces/i_cert_slot_handler.cpp b/score/crypto/src/daemon/cert_management/interfaces/i_cert_slot_handler.cpp index 827bfde3d..35d34b2e0 100644 --- a/score/crypto/src/daemon/cert_management/interfaces/i_cert_slot_handler.cpp +++ b/score/crypto/src/daemon/cert_management/interfaces/i_cert_slot_handler.cpp @@ -39,7 +39,7 @@ score::crypto::Expected /*crl_data*/, score::crypto::FormatType /*format*/, - std::int64_t /*next_update_epoch_s*/) + std::optional /*metadata*/) { return score::crypto::make_unexpected(score::crypto::daemon::common::DaemonErrorCode::kUnsupportedOperation); } @@ -61,4 +61,9 @@ score::crypto::FormatType ICertSlotHandler::GetCrlFormat(const CertSlotConfig& / return score::crypto::FormatType::kDer; } +std::optional ICertSlotHandler::GetCrlMetadata(const CertSlotConfig& /*slot*/) +{ + return std::nullopt; +} + } // namespace score::crypto::daemon::cert_management diff --git a/score/crypto/src/daemon/cert_management/interfaces/i_cert_slot_handler.hpp b/score/crypto/src/daemon/cert_management/interfaces/i_cert_slot_handler.hpp index 7693b570e..d73652b80 100644 --- a/score/crypto/src/daemon/cert_management/interfaces/i_cert_slot_handler.hpp +++ b/score/crypto/src/daemon/cert_management/interfaces/i_cert_slot_handler.hpp @@ -14,7 +14,8 @@ #ifndef SCORE_CRYPTO_SRC_DAEMON_CERT_MANAGEMENT_INTERFACES_I_CERT_SLOT_HANDLER_HPP #define SCORE_CRYPTO_SRC_DAEMON_CERT_MANAGEMENT_INTERFACES_I_CERT_SLOT_HANDLER_HPP -#include "score/crypto/src/api/common/types.hpp" +#include "score/crypto/src/api/types/certificate.hpp" +#include "score/crypto/src/api/types/common.hpp" #include "score/crypto/src/common/types.hpp" #include "score/crypto/src/daemon/cert_management/interfaces/cert_object.hpp" #include "score/crypto/src/daemon/cert_management/interfaces/cert_slot_config.hpp" @@ -26,6 +27,7 @@ #include #include #include +#include #include #include @@ -101,11 +103,12 @@ class ICertSlotHandler score::crypto::daemon::common::DaemonErrorCode> GetSlotInfo(const CertSlotConfig& slot) = 0; - /// True when the slot has an associated CRL stored in its [crl] section. + /// Check whether the slot has an associated CRL stored in its [crl] section. /// /// For file-backed slots: checks existence of the crl_path key in the /// deployment descriptor and verifies the file is present. - [[nodiscard]] virtual bool HasCrl(const CertSlotConfig& slot) = 0; + [[nodiscard]] virtual score::crypto::Expected HasCrl( + const CertSlotConfig& slot) = 0; // ----------------------------------------------------------------------- // Optional — defaulted to kUnsupportedOperation @@ -145,7 +148,7 @@ class ICertSlotHandler /// /// crl_data points to caller-owned memory valid for the duration of this call. /// The implementation must copy the bytes and write them to the configured path. - /// Updates cert_deployment_keys::kCrlNextUpdate in the deployment descriptor. + /// Updates CRL metadata keys in the deployment descriptor when metadata is provided. /// /// Consistency model: the operation is two steps — write CRL file, then write /// descriptor. Each step is individually atomic (temp-file + rename). If the @@ -159,7 +162,7 @@ class ICertSlotHandler StoreCrl(const CertSlotConfig& slot, score::crypto::span crl_data, score::crypto::FormatType format, - std::int64_t next_update_epoch_s = 0); + std::optional metadata = std::nullopt); /// Remove the CRL from the slot's [crl] section. /// @@ -185,6 +188,9 @@ class ICertSlotHandler /// /// Default: returns kDer. [[nodiscard]] virtual score::crypto::FormatType GetCrlFormat(const CertSlotConfig& slot); + + /// Return metadata for the stored CRL, when available. + [[nodiscard]] virtual std::optional GetCrlMetadata(const CertSlotConfig& slot); }; /// Factory function type for creating a slot handler from a slot configuration. diff --git a/score/crypto/src/daemon/cert_management/query/cert_object_serializer.cpp b/score/crypto/src/daemon/cert_management/query/cert_object_serializer.cpp index 64bec5a52..1ceec184c 100644 --- a/score/crypto/src/daemon/cert_management/query/cert_object_serializer.cpp +++ b/score/crypto/src/daemon/cert_management/query/cert_object_serializer.cpp @@ -19,7 +19,8 @@ namespace score::crypto::daemon::cert_management::query { -common::ResponseParameters SerializeCertObject(const CertObject& cert) +common::ResponseParameters SerializeCertObject(const CertObject& cert, + std::optional crl_metadata) { const auto& meta = cert.GetChainMetadata(); common::ResponseParameters out; @@ -32,6 +33,13 @@ common::ResponseParameters SerializeCertObject(const CertObject& cert) out.push_back(common::OwnedBuffer{meta.akid.begin(), meta.akid.end()}); out.push_back(common::OwnedString{meta.serial_number_hex}); out.push_back(common::OwnedBuffer{meta.fingerprint.begin(), meta.fingerprint.end()}); + out.push_back(static_cast(crl_metadata.has_value() ? 1U : 0U)); + const auto crl = crl_metadata.value_or(score::crypto::CrlMetadata{}); + out.push_back(common::OwnedBuffer{crl.fingerprint.begin(), crl.fingerprint.end()}); + out.push_back(common::OwnedBuffer{crl.issuer_fingerprint.begin(), crl.issuer_fingerprint.end()}); + out.push_back(static_cast(crl.this_update)); + out.push_back(static_cast(crl.next_update)); + out.push_back(crl.crl_number); return out; } @@ -43,18 +51,9 @@ SerializeCertSlotInfo(CertSlotManager& mgr, CertSlotHandle slot, data_manager::C return score::crypto::make_unexpected(info_res.error()); const bool has_crl = info_res.value().has_crl; - int64_t crl_next = 0; - if (has_crl) - { - auto nu = mgr.GetCrlNextUpdate(slot, client_id); - if (nu.has_value()) - crl_next = nu.value(); - } - common::ResponseParameters out; out.push_back(static_cast(info_res.value().state)); out.push_back(static_cast(has_crl ? 1U : 0U)); - out.push_back(static_cast(crl_next)); return out; } diff --git a/score/crypto/src/daemon/cert_management/query/cert_object_serializer.hpp b/score/crypto/src/daemon/cert_management/query/cert_object_serializer.hpp index 3e8693a93..064449a74 100644 --- a/score/crypto/src/daemon/cert_management/query/cert_object_serializer.hpp +++ b/score/crypto/src/daemon/cert_management/query/cert_object_serializer.hpp @@ -22,6 +22,7 @@ #include "score/crypto/src/daemon/data_manager/data_node.hpp" #include +#include #include // Forward-declare to avoid pulling in cert_management_service.hpp from the header. @@ -35,25 +36,27 @@ namespace score::crypto::daemon::cert_management::query /// @brief Serialize a CertObject's chain metadata into IPC response parameters. /// -/// Wire layout (9 params, indices 0–8): +/// Wire layout (15 params, indices 0–14): /// [0] subject (OwnedString), [1] issuer (OwnedString), /// [2] not_before_epoch_s (uint64), [3] not_after_epoch_s (uint64), /// [4] is_ca (uint8), [5] skid (OwnedBuffer), [6] akid (OwnedBuffer), -/// [7] serial_number_hex (OwnedString), [8] SHA-256 fingerprint (OwnedBuffer 32B) +/// [7] serial_number_hex (OwnedString), [8] SHA-256 fingerprint (OwnedBuffer 32B), +/// [9] has_crl (uint8), [10] CRL fingerprint (OwnedBuffer 32B), +/// [11] issuer fingerprint (OwnedBuffer 32B), [12] thisUpdate (int64 encoded as uint64), +/// [13] nextUpdate (int64 encoded as uint64), [14] cRLNumber (uint64). /// -/// This is the canonical layout for GET_CERTIFICATE_OBJECT and CERT_GET_METADATA. -/// Both the mediator typed-object handler and the cert management executor use this -/// function so the format is defined exactly once. -common::ResponseParameters SerializeCertObject(const CertObject& cert); +/// This is the canonical layout for the mediator's GET_CERTIFICATE_OBJECT op, +/// defined exactly once here. +common::ResponseParameters SerializeCertObject(const CertObject& cert, + std::optional crl_metadata = std::nullopt); /// @brief Serialize certificate slot state + CRL metadata into IPC response parameters. /// -/// Wire layout (3 params): -/// [0] slot state (uint8, CertificateSlotState), [1] has_crl (uint8), -/// [2] crl_next_update_epoch (uint64, 0 if no CRL) +/// Wire layout (2 params): +/// [0] slot state (uint8, CertificateSlotState), [1] has_crl (uint8) /// -/// Returns an error if GetSlotInfo fails. Both the mediator typed-object handler -/// and the cert management executor use this function. +/// Returns an error if GetSlotInfo fails. Used by the mediator's +/// GET_CERT_SLOT_OBJECT handler. score::crypto::Expected SerializeCertSlotInfo(CertSlotManager& mgr, CertSlotHandle slot, data_manager::ClientId client_id); diff --git a/score/crypto/src/daemon/cert_management/slot/cert_slot_manager.cpp b/score/crypto/src/daemon/cert_management/slot/cert_slot_manager.cpp index d021fbfff..b517a24a4 100644 --- a/score/crypto/src/daemon/cert_management/slot/cert_slot_manager.cpp +++ b/score/crypto/src/daemon/cert_management/slot/cert_slot_manager.cpp @@ -51,7 +51,9 @@ score::crypto::Expected CertSlotManager::ApplyWriteChecks // TODO(cert-slot-mgr): exclusive-slot guard — return kAccessDenied when the slot // is a kExclusiveMutable member of any trust store. Requires a membership query - // callback to avoid a circular dependency with TrustStoreManager. + // callback to avoid a circular dependency with TrustStoreManager. Until this + // lands, correctness relies on a configuration invariant documented on + // AccessPolicy::allowed_write_uids (access_policy.hpp) and in design_decisions.rst. return std::monostate{}; } @@ -188,11 +190,11 @@ score::crypto::Expected CertSlotManag return handler->GetSlotInfo(cfg); } -bool CertSlotManager::HasCrl(CertSlotHandle slot) +score::crypto::Expected CertSlotManager::HasCrl(CertSlotHandle slot) { const auto cfg_res = GetConfig(slot); if (!cfg_res.has_value()) - return false; + return score::crypto::make_unexpected(cfg_res.error()); ICertSlotHandler::Sptr handler; { @@ -200,7 +202,7 @@ bool CertSlotManager::HasCrl(CertSlotHandle slot) handler = GetOrCreate(slot); } if (!handler) - return false; + return score::crypto::make_unexpected(Error::kInternalError); return handler->HasCrl(*cfg_res.value()); } @@ -242,6 +244,22 @@ score::crypto::FormatType CertSlotManager::GetCrlFormat(CertSlotHandle slot) return handler->GetCrlFormat(*cfg_res.value()); } +std::optional CertSlotManager::GetCrlMetadata(CertSlotHandle slot) +{ + const auto cfg_res = GetConfig(slot); + if (!cfg_res.has_value()) + return std::nullopt; + + ICertSlotHandler::Sptr handler; + { + std::lock_guard lock(m_mutex); + handler = GetOrCreate(slot); + } + if (!handler) + return std::nullopt; + return handler->GetCrlMetadata(*cfg_res.value()); +} + score::crypto::Expected CertSlotManager::GetCrlNextUpdate(CertSlotHandle slot, data_manager::ClientId client_id) { @@ -333,11 +351,12 @@ void CertSlotManager::InvalidateCertObjectCache(CertSlotHandle slot) m_cert_object_cache.erase(slot.index); } -score::crypto::Expected CertSlotManager::ImportCrl(CertSlotHandle slot, - data_manager::ClientId client_id, - score::crypto::span crl_data, - score::crypto::FormatType format, - std::int64_t next_update_epoch_s) +score::crypto::Expected CertSlotManager::ImportCrl( + CertSlotHandle slot, + data_manager::ClientId client_id, + score::crypto::span crl_data, + score::crypto::FormatType format, + std::optional metadata) { const auto cfg_res = GetConfig(slot); if (!cfg_res.has_value()) @@ -358,7 +377,7 @@ score::crypto::Expected CertSlotManager::ImportCrl(CertSl } if (!handler) return score::crypto::make_unexpected(Error::kInternalError); - return handler->StoreCrl(cfg, crl_data, format, next_update_epoch_s); + return handler->StoreCrl(cfg, crl_data, format, std::move(metadata)); } score::crypto::Expected CertSlotManager::DeleteCrl(CertSlotHandle slot, diff --git a/score/crypto/src/daemon/cert_management/slot/cert_slot_manager.hpp b/score/crypto/src/daemon/cert_management/slot/cert_slot_manager.hpp index f10e885f2..e81e249d2 100644 --- a/score/crypto/src/daemon/cert_management/slot/cert_slot_manager.hpp +++ b/score/crypto/src/daemon/cert_management/slot/cert_slot_manager.hpp @@ -14,7 +14,8 @@ #ifndef SCORE_CRYPTO_SRC_DAEMON_CERT_MANAGEMENT_SLOT_CERT_SLOT_MANAGER_HPP #define SCORE_CRYPTO_SRC_DAEMON_CERT_MANAGEMENT_SLOT_CERT_SLOT_MANAGER_HPP -#include "score/crypto/src/api/common/types.hpp" +#include "score/crypto/src/api/types/certificate.hpp" +#include "score/crypto/src/api/types/common.hpp" #include "score/crypto/src/daemon/cert_management/interfaces/cert_slot_config.hpp" #include "score/crypto/src/daemon/cert_management/interfaces/cert_types.hpp" #include "score/crypto/src/daemon/cert_management/interfaces/i_cert_slot_handler.hpp" @@ -91,8 +92,8 @@ class CertSlotManager final score::crypto::daemon::common::DaemonErrorCode> GetSlotInfo(CertSlotHandle slot, data_manager::ClientId client_id); - /// Returns false when the slot has no handler or no CRL; never fails. - [[nodiscard]] bool HasCrl(CertSlotHandle slot); + [[nodiscard]] score::crypto::Expected HasCrl( + CertSlotHandle slot); [[nodiscard]] score::crypto::Expected, score::crypto::daemon::common::DaemonErrorCode> LoadCrl( CertSlotHandle slot, @@ -101,6 +102,8 @@ class CertSlotManager final /// Returns kDer when the slot has no handler or no [crl] section; never fails. [[nodiscard]] score::crypto::FormatType GetCrlFormat(CertSlotHandle slot); + [[nodiscard]] std::optional GetCrlMetadata(CertSlotHandle slot); + [[nodiscard]] score::crypto::Expected GetCrlNextUpdate( CertSlotHandle slot, data_manager::ClientId client_id); @@ -121,7 +124,7 @@ class CertSlotManager final data_manager::ClientId client_id, score::crypto::span crl_data, score::crypto::FormatType format, - std::int64_t next_update_epoch_s); + std::optional metadata = std::nullopt); [[nodiscard]] score::crypto::Expected DeleteCrl( CertSlotHandle slot, diff --git a/score/crypto/src/daemon/cert_management/slot/crl_handler.cpp b/score/crypto/src/daemon/cert_management/slot/crl_handler.cpp index ca64105f1..aab1225c5 100644 --- a/score/crypto/src/daemon/cert_management/slot/crl_handler.cpp +++ b/score/crypto/src/daemon/cert_management/slot/crl_handler.cpp @@ -15,9 +15,12 @@ #include "score/crypto/src/daemon/cert_management/slot/deployment_loader.hpp" #include "score/crypto/src/daemon/cert_management/slot/deployment_writer.hpp" +#include "score/crypto/src/daemon/common/hex.hpp" #include "score/crypto/src/daemon/common/storage/file_io.hpp" #include +#include +#include namespace score::crypto::daemon::cert_management { @@ -43,13 +46,16 @@ std::string CrlHandler::FormatName(score::crypto::FormatType format) return format == score::crypto::FormatType::kDer ? "der" : "pem"; } -bool CrlHandler::HasCrl(const CertSlotConfig& slot) const +score::crypto::Expected CrlHandler::HasCrl(const CertSlotConfig& slot) const { auto descriptor = LoadDescriptor(slot); if (!descriptor) - return false; + return score::crypto::make_unexpected(descriptor.error()); const auto path = descriptor->Get("crl", "crl_path"); - return !path.empty() && file_io::FileExists(path); + const auto format = descriptor->Get("crl", "crl_format"); + if (path.empty() || format.empty()) + return false; + return file_io::FileExists(path); } score::crypto::Expected, Error> CrlHandler::LoadCrl(const CertSlotConfig& slot) const @@ -66,7 +72,7 @@ score::crypto::Expected, Error> CrlHandler::LoadCrl(co score::crypto::Expected CrlHandler::StoreCrl(const CertSlotConfig& slot, score::crypto::span data, score::crypto::FormatType format, - std::int64_t next_update_epoch_s) + std::optional metadata) { if (data.empty()) return score::crypto::make_unexpected(Error::kInvalidArgument); @@ -91,10 +97,20 @@ score::crypto::Expected CrlHandler::StoreCrl(const CertSl auto result = file_io::WriteFile(path, data); if (!result) return result; + descriptor->RemoveSection("crl"); descriptor->Set("crl", "crl_path", path); descriptor->Set("crl", "crl_format", FormatName(format)); - if (next_update_epoch_s != 0) - descriptor->Set("crl", "crl_next_update", std::to_string(next_update_epoch_s)); + if (metadata.has_value()) + { + descriptor->Set( + "crl", "crl_fingerprint", common::EncodeHex({metadata->fingerprint.data(), metadata->fingerprint.size()})); + descriptor->Set("crl", + "crl_issuer_fingerprint", + common::EncodeHex({metadata->issuer_fingerprint.data(), metadata->issuer_fingerprint.size()})); + descriptor->Set("crl", "crl_this_update", std::to_string(metadata->this_update)); + descriptor->Set("crl", "crl_next_update", std::to_string(metadata->next_update)); + descriptor->Set("crl", "crl_number", std::to_string(metadata->crl_number)); + } return SaveDescriptor(slot, *descriptor); } @@ -104,7 +120,7 @@ score::crypto::Expected CrlHandler::ClearCrl(const CertSl if (!descriptor) return score::crypto::make_unexpected(descriptor.error()); const auto path = descriptor->Get("crl", "crl_path"); - if (!path.empty() && file_io::FileExists(path)) + if (!path.empty()) { auto remove_result = file_io::RemoveFile(path); if (!remove_result) @@ -140,4 +156,37 @@ score::crypto::FormatType CrlHandler::GetCrlFormat(const CertSlotConfig& slot) c return (fmt == "pem") ? score::crypto::FormatType::kPem : score::crypto::FormatType::kDer; } +std::optional CrlHandler::GetCrlMetadata(const CertSlotConfig& slot) const +{ + const auto has_crl = HasCrl(slot); + if (!has_crl || !has_crl.value()) + return std::nullopt; + + const auto descriptor = LoadDescriptor(slot); + if (!descriptor) + return std::nullopt; + + const auto fingerprint = common::DecodeHex(descriptor->Get("crl", "crl_fingerprint")); + const auto issuer_fingerprint = common::DecodeHex(descriptor->Get("crl", "crl_issuer_fingerprint")); + if (!fingerprint.has_value() || fingerprint->size() != 32U || !issuer_fingerprint.has_value() || + issuer_fingerprint->size() != 32U) + return std::nullopt; + + score::crypto::CrlMetadata metadata; + std::copy(fingerprint->begin(), fingerprint->end(), metadata.fingerprint.begin()); + std::copy(issuer_fingerprint->begin(), issuer_fingerprint->end(), metadata.issuer_fingerprint.begin()); + + const auto parse_integer = [&descriptor](std::string_view key, auto& result) { + const auto value = descriptor->Get("crl", std::string{key}); + if (value.empty()) + return false; + const auto [end, ec] = std::from_chars(value.data(), value.data() + value.size(), result); + return ec == std::errc{} && end == value.data() + value.size(); + }; + if (!parse_integer("crl_this_update", metadata.this_update) || + !parse_integer("crl_next_update", metadata.next_update) || !parse_integer("crl_number", metadata.crl_number)) + return std::nullopt; + return metadata; +} + } // namespace score::crypto::daemon::cert_management diff --git a/score/crypto/src/daemon/cert_management/slot/crl_handler.hpp b/score/crypto/src/daemon/cert_management/slot/crl_handler.hpp index 2a2b436db..246f0fcce 100644 --- a/score/crypto/src/daemon/cert_management/slot/crl_handler.hpp +++ b/score/crypto/src/daemon/cert_management/slot/crl_handler.hpp @@ -14,13 +14,15 @@ #ifndef SCORE_CRYPTO_SRC_DAEMON_CERT_MANAGEMENT_SLOT_CRL_HANDLER_HPP #define SCORE_CRYPTO_SRC_DAEMON_CERT_MANAGEMENT_SLOT_CRL_HANDLER_HPP -#include "score/crypto/src/api/common/types.hpp" +#include "score/crypto/src/api/types/certificate.hpp" +#include "score/crypto/src/api/types/common.hpp" #include "score/crypto/src/common/types.hpp" #include "score/crypto/src/daemon/cert_management/interfaces/cert_slot_config.hpp" #include "score/crypto/src/daemon/cert_management/interfaces/cert_types.hpp" #include "score/crypto/src/daemon/common/daemon_error.hpp" #include +#include #include #include @@ -49,8 +51,8 @@ class CrlHandler final CrlHandler(CrlHandler&&) = delete; CrlHandler& operator=(CrlHandler&&) = delete; - /// Return true if the slot's deployment descriptor references an existing CRL file. - [[nodiscard]] bool HasCrl(const CertSlotConfig& slot) const; + /// Check whether the slot's deployment descriptor references an existing CRL file. + [[nodiscard]] score::crypto::Expected HasCrl(const CertSlotConfig& slot) const; /// Load raw CRL bytes from the file referenced by the descriptor's [crl] section. /// Returns kResourceNotAllocated if no CRL is configured. @@ -59,12 +61,12 @@ class CrlHandler final /// Write @p data to the CRL file and update the descriptor's [crl] section. /// Derives the CRL path from the deployment path if not already set. - /// When @p next_update_epoch_s is non-zero, caches the value in the descriptor. + /// When @p metadata is present, caches the validated CRL metadata in the descriptor. [[nodiscard]] score::crypto::Expected StoreCrl( const CertSlotConfig& slot, score::crypto::span data, score::crypto::FormatType format, - std::int64_t next_update_epoch_s = 0); + std::optional metadata = std::nullopt); /// Remove the CRL file and erase the [crl] section from the descriptor. [[nodiscard]] score::crypto::Expected ClearCrl(const CertSlotConfig& slot); @@ -78,6 +80,9 @@ class CrlHandler final /// Returns kDer when the key is absent or the descriptor cannot be loaded. [[nodiscard]] score::crypto::FormatType GetCrlFormat(const CertSlotConfig& slot) const; + /// Return metadata cached in the descriptor, or no value when unavailable. + [[nodiscard]] std::optional GetCrlMetadata(const CertSlotConfig& slot) const; + private: static std::string FormatName(score::crypto::FormatType format); }; diff --git a/score/crypto/src/daemon/cert_management/slot/file_backed_slot_handler.cpp b/score/crypto/src/daemon/cert_management/slot/file_backed_slot_handler.cpp index 49ef2e7fd..d6b4c4a60 100644 --- a/score/crypto/src/daemon/cert_management/slot/file_backed_slot_handler.cpp +++ b/score/crypto/src/daemon/cert_management/slot/file_backed_slot_handler.cpp @@ -81,8 +81,13 @@ score::crypto::Expected FileBackedSl if (!descriptor) return score::crypto::make_unexpected(descriptor.error()); const auto path = ResolvePath(*descriptor, "certificate", "cert_path"); - return (!path.empty() && file_io::FileExists(path)) ? score::crypto::CertificateSlotState::kOccupied - : score::crypto::CertificateSlotState::kEmpty; + if (path.empty()) + return score::crypto::CertificateSlotState::kEmpty; + const auto exists = file_io::FileExists(path); + if (!exists) + return score::crypto::make_unexpected(exists.error()); + return exists.value() ? score::crypto::CertificateSlotState::kOccupied + : score::crypto::CertificateSlotState::kEmpty; } score::crypto::Expected FileBackedSlotHandler::GetSlotInfo( @@ -93,11 +98,14 @@ score::crypto::Expected FileBackedSlo return score::crypto::make_unexpected(state.error()); score::crypto::CertificateSlotInfo info{}; info.state = *state; - info.has_crl = HasCrl(slot); + const auto has_crl = m_crl.HasCrl(slot); + if (!has_crl) + return score::crypto::make_unexpected(has_crl.error()); + info.has_crl = has_crl.value(); return info; } -bool FileBackedSlotHandler::HasCrl(const CertSlotConfig& slot) +score::crypto::Expected FileBackedSlotHandler::HasCrl(const CertSlotConfig& slot) { return m_crl.HasCrl(slot); } @@ -119,17 +127,20 @@ score::crypto::Expected FileBackedSlotHandler::StoreCerti if (!result) return result; // Clear any stale CRL: it was issued for the previous CA key and is no longer - // valid for the incoming certificate. Preserve crl_path so that a future - // StoreCrl call re-uses the same location — this is essential for backends - // (e.g. PKCS#11) where cert_path is absent and the CRL path cannot be - // re-derived from the certificate file. File removal is best-effort; a - // stale CRL file will be overwritten by the next StoreCrl. + // valid for the incoming certificate. Metadata invalidation happens + // unconditionally so the descriptor never reports a stale CRL as current, + // even if the physical file removal below fails. Preserve crl_path so that + // a future StoreCrl call re-uses the same location — this is essential for + // backends (e.g. PKCS#11) where cert_path is absent and the CRL path cannot + // be re-derived from the certificate file. File removal is best-effort: a + // leftover stale CRL file will be overwritten by the next StoreCrl. const auto old_crl_path = descriptor->Get("crl", "crl_path"); - if (!old_crl_path.empty()) - static_cast(file_io::RemoveFile(old_crl_path)); descriptor->RemoveSection("crl"); if (!old_crl_path.empty()) + { descriptor->Set("crl", "crl_path", old_crl_path); + static_cast(file_io::RemoveFile(old_crl_path)); + } descriptor->Set("certificate", "cert_format", cert.GetFormat() == score::crypto::FormatType::kDer ? "der" : "pem"); descriptor->Set("certificate_metadata", "subject", std::string(cert.GetSubject())); descriptor->Set("certificate_metadata", "issuer", std::string(cert.GetIssuer())); @@ -145,7 +156,7 @@ score::crypto::Expected FileBackedSlotHandler::ClearSlot( if (!descriptor) return score::crypto::make_unexpected(descriptor.error()); const auto cert = descriptor->Get("certificate", "cert_path"); - if (!cert.empty() && file_io::FileExists(cert)) + if (!cert.empty()) { if (const auto rm = file_io::RemoveFile(cert); !rm) return rm; @@ -153,7 +164,7 @@ score::crypto::Expected FileBackedSlotHandler::ClearSlot( // Preserve crl_path so a future StoreCrl re-uses the same location. // Only the file and volatile metadata (format, next_update) are discarded. const auto crl_path = descriptor->Get("crl", "crl_path"); - if (!crl_path.empty() && file_io::FileExists(crl_path)) + if (!crl_path.empty()) { if (const auto rm = file_io::RemoveFile(crl_path); !rm) return rm; @@ -170,12 +181,13 @@ score::crypto::Expected, Error> FileBackedSlotHandler::Load return m_crl.LoadCrl(slot); } -score::crypto::Expected FileBackedSlotHandler::StoreCrl(const CertSlotConfig& slot, - score::crypto::span data, - score::crypto::FormatType format, - std::int64_t next_update_epoch_s) +score::crypto::Expected FileBackedSlotHandler::StoreCrl( + const CertSlotConfig& slot, + score::crypto::span data, + score::crypto::FormatType format, + std::optional metadata) { - return m_crl.StoreCrl(slot, data, format, next_update_epoch_s); + return m_crl.StoreCrl(slot, data, format, std::move(metadata)); } score::crypto::Expected FileBackedSlotHandler::ClearCrl(const CertSlotConfig& slot) @@ -192,4 +204,26 @@ score::crypto::FormatType FileBackedSlotHandler::GetCrlFormat(const CertSlotConf { return m_crl.GetCrlFormat(slot); } + +std::optional FileBackedSlotHandler::GetCrlMetadata(const CertSlotConfig& slot) +{ + if (const auto metadata = m_crl.GetCrlMetadata(slot); metadata.has_value()) + return metadata; + const auto has_crl = m_crl.HasCrl(slot); + if (!m_parser || !has_crl || !has_crl.value()) + return std::nullopt; + + const auto certificate = LoadCertificate(slot); + const auto crl = m_crl.LoadCrl(slot); + if (!certificate.has_value() || !crl.has_value()) + return std::nullopt; + + const auto result = m_parser->ValidateCrl(crl->data(), + crl->size(), + m_crl.GetCrlFormat(slot), + certificate.value()->GetRawBytes().data(), + certificate.value()->GetRawBytes().size(), + certificate.value()->GetFormat()); + return result.has_value() ? std::optional{result.value()} : std::nullopt; +} } // namespace score::crypto::daemon::cert_management diff --git a/score/crypto/src/daemon/cert_management/slot/file_backed_slot_handler.hpp b/score/crypto/src/daemon/cert_management/slot/file_backed_slot_handler.hpp index f972e62e9..7f1dc7a80 100644 --- a/score/crypto/src/daemon/cert_management/slot/file_backed_slot_handler.hpp +++ b/score/crypto/src/daemon/cert_management/slot/file_backed_slot_handler.hpp @@ -41,7 +41,7 @@ class FileBackedSlotHandler final : public ICertSlotHandler const CertSlotConfig&) override; score::crypto::Expected GetSlotInfo( const CertSlotConfig&) override; - bool HasCrl(const CertSlotConfig&) override; + score::crypto::Expected HasCrl(const CertSlotConfig&) override; score::crypto::Expected StoreCertificate(const CertSlotConfig&, const CertObject&) override; score::crypto::Expected ClearSlot(const CertSlotConfig&) override; @@ -50,10 +50,11 @@ class FileBackedSlotHandler final : public ICertSlotHandler const CertSlotConfig&, score::crypto::span, score::crypto::FormatType, - std::int64_t next_update_epoch_s = 0) override; + std::optional metadata = std::nullopt) override; score::crypto::Expected ClearCrl(const CertSlotConfig&) override; score::crypto::Expected GetCrlNextUpdate(const CertSlotConfig&) override; score::crypto::FormatType GetCrlFormat(const CertSlotConfig&) override; + std::optional GetCrlMetadata(const CertSlotConfig&) override; private: using Handler = CrlHandler; diff --git a/score/crypto/src/daemon/cert_management/slot/slot_registry.hpp b/score/crypto/src/daemon/cert_management/slot/slot_registry.hpp index b91886033..3ff109200 100644 --- a/score/crypto/src/daemon/cert_management/slot/slot_registry.hpp +++ b/score/crypto/src/daemon/cert_management/slot/slot_registry.hpp @@ -94,7 +94,7 @@ class CertSlotRegistry : public std::enable_shared_from_this /// @brief Return a snapshot of all registered slot configs. /// - /// Used by services that need to iterate all slots (e.g., DeleteExpiredCrls). + /// Used by services that need to iterate all registered slots. std::vector GetAllHandles() const; /// @brief Register an application-local resource ID mapping. diff --git a/score/crypto/src/daemon/cert_management/tests/integration/test_certificate_management_integration.cpp b/score/crypto/src/daemon/cert_management/tests/integration/test_certificate_management_integration.cpp index 2c3c607c1..01da1abf8 100644 --- a/score/crypto/src/daemon/cert_management/tests/integration/test_certificate_management_integration.cpp +++ b/score/crypto/src/daemon/cert_management/tests/integration/test_certificate_management_integration.cpp @@ -244,6 +244,90 @@ TEST_F(CertificateManagementIntegrationTest, ClearSlot_DeniedForUnauthorizedUid) EXPECT_EQ(result.error(), common::DaemonErrorCode::kAccessDenied); } +// ImportCrl succeeds when the caller's UID is in allowed_write_uids, and the +// stored CRL becomes visible via HasCrl. +TEST_F(CertificateManagementIntegrationTest, ImportCrl_GrantedForAuthorizedUid_StoresCrl) +{ + const auto slot = m_slot_registry->ResolveAppResource("device_certificate", 0U); + ASSERT_TRUE(slot.has_value()); + ASSERT_FALSE(m_slot_manager->HasCrl(*slot).value()); + + const std::vector crl_bytes{0xC0U, 0xC1U, 0xC2U}; + const auto result = + m_slot_manager->ImportCrl(*slot, MakeClientId(1U, 0U), crl_bytes, score::crypto::FormatType::kDer); + ASSERT_TRUE(result.has_value()); + EXPECT_TRUE(m_slot_manager->HasCrl(*slot).value()); +} + +// ImportCrl is denied when the caller's UID is not in allowed_write_uids; no +// CRL is stored. +TEST_F(CertificateManagementIntegrationTest, ImportCrl_DeniedForUnauthorizedUid) +{ + const auto slot = m_slot_registry->ResolveAppResource("device_certificate", 0U); + ASSERT_TRUE(slot.has_value()); + + const std::vector crl_bytes{0xC0U, 0xC1U, 0xC2U}; + const auto result = + m_slot_manager->ImportCrl(*slot, MakeClientId(2U, 99U), crl_bytes, score::crypto::FormatType::kDer); + ASSERT_FALSE(result.has_value()); + EXPECT_EQ(result.error(), common::DaemonErrorCode::kAccessDenied); + EXPECT_FALSE(m_slot_manager->HasCrl(*slot).value()); +} + +// DeleteCrl is denied when the caller's UID is not in allowed_write_uids; a +// previously-stored CRL survives the denied attempt. +TEST_F(CertificateManagementIntegrationTest, DeleteCrl_DeniedForUnauthorizedUid) +{ + const auto slot = m_slot_registry->ResolveAppResource("device_certificate", 0U); + ASSERT_TRUE(slot.has_value()); + const std::vector crl_bytes{0xC0U, 0xC1U, 0xC2U}; + ASSERT_TRUE( + m_slot_manager->ImportCrl(*slot, MakeClientId(1U, 0U), crl_bytes, score::crypto::FormatType::kDer).has_value()); + + const auto result = m_slot_manager->DeleteCrl(*slot, MakeClientId(2U, 99U)); + ASSERT_FALSE(result.has_value()); + EXPECT_EQ(result.error(), common::DaemonErrorCode::kAccessDenied); + EXPECT_TRUE(m_slot_manager->HasCrl(*slot).value()); +} + +// DeleteCrl succeeds when the caller's UID is in allowed_write_uids. +TEST_F(CertificateManagementIntegrationTest, DeleteCrl_GrantedForAuthorizedUid_RemovesCrl) +{ + const auto slot = m_slot_registry->ResolveAppResource("device_certificate", 0U); + ASSERT_TRUE(slot.has_value()); + const std::vector crl_bytes{0xC0U, 0xC1U, 0xC2U}; + ASSERT_TRUE( + m_slot_manager->ImportCrl(*slot, MakeClientId(1U, 0U), crl_bytes, score::crypto::FormatType::kDer).has_value()); + + const auto result = m_slot_manager->DeleteCrl(*slot, MakeClientId(1U, 0U)); + ASSERT_TRUE(result.has_value()); + EXPECT_FALSE(m_slot_manager->HasCrl(*slot).value()); +} + +// StoreCertificate invalidates the CertObjectCache: a certificate loaded +// before a write must not be returned again after the slot content changes. +TEST_F(CertificateManagementIntegrationTest, StoreCertificate_InvalidatesCertObjectCache) +{ + const auto slot = m_slot_registry->ResolveAppResource("device_certificate", 0U); + ASSERT_TRUE(slot.has_value()); + + const auto before = m_slot_manager->LoadCertificate(*slot, MakeClientId(1U, 0U)); + ASSERT_TRUE(before.has_value()); + EXPECT_EQ((*before)->GetSubject(), "CN=cert-management-test,O=Eclipse"); + + const auto updated_pem = ReadFile(m_directory / "certificate_updated.pem"); + ASSERT_FALSE(updated_pem.empty()); + const auto updated = m_parser->ParseCertificate( + reinterpret_cast(updated_pem.data()), updated_pem.size(), score::crypto::FormatType::kPem); + ASSERT_TRUE(updated.has_value()); + ASSERT_TRUE(m_slot_manager->StoreCertificate(*slot, MakeClientId(1U, 0U), **updated).has_value()); + + const auto after = m_slot_manager->LoadCertificate(*slot, MakeClientId(1U, 0U)); + ASSERT_TRUE(after.has_value()); + // A stale cache would still report the pre-write subject here. + EXPECT_EQ((*after)->GetSubject(), "CN=cert-management-updated,O=Eclipse"); +} + // A slot with an empty allowed_write_uids list denies writes from every UID, // including UID 0. This is the default-deny invariant. TEST_F(CertificateManagementIntegrationTest, WritesDeniedWhenAllowedWriteUidsIsEmpty) diff --git a/score/crypto/src/daemon/cert_management/tests/query/test_cert_object_serializer.cpp b/score/crypto/src/daemon/cert_management/tests/query/test_cert_object_serializer.cpp index 849111df3..68c1bfd47 100644 --- a/score/crypto/src/daemon/cert_management/tests/query/test_cert_object_serializer.cpp +++ b/score/crypto/src/daemon/cert_management/tests/query/test_cert_object_serializer.cpp @@ -126,7 +126,8 @@ struct SlotHandlerStub : public cert::ICertSlotHandler return slot_info_result; } - bool HasCrl(const cert::CertSlotConfig&) override + score::crypto::Expected HasCrl( + const cert::CertSlotConfig&) override { return slot_info_result.has_value() && slot_info_result->has_crl; } @@ -141,7 +142,7 @@ struct SlotHandlerStub : public cert::ICertSlotHandler const cert::CertSlotConfig&, score::crypto::span, score::crypto::FormatType, - std::int64_t) override + std::optional) override { return score::crypto::make_unexpected(score::crypto::daemon::common::DaemonErrorCode::kUnsupportedOperation); } @@ -171,10 +172,10 @@ struct SlotHandlerStub : public cert::ICertSlotHandler // SerializeCertObject // =========================================================================== -TEST(SerializeCertObject, ProducesNineParameters) +TEST(SerializeCertObject, ProducesFifteenParameters) { const auto cert = MakeSyntheticCert(); - EXPECT_EQ(query::SerializeCertObject(cert).size(), 9U); + EXPECT_EQ(query::SerializeCertObject(cert).size(), 15U); } TEST(SerializeCertObject, Param0_SubjectString) @@ -274,6 +275,37 @@ TEST(SerializeCertObject, Param8_Fingerprint32ByteBuffer) EXPECT_EQ(byte, 0x5AU); } +TEST(SerializeCertObject, CrlMetadataUsesTypedTail) +{ + const auto cert = MakeSyntheticCert(); + score::crypto::CrlMetadata metadata; + metadata.fingerprint.fill(0xA1U); + metadata.issuer_fingerprint.fill(0xB2U); + metadata.this_update = 1700000000LL; + metadata.next_update = 1730000000LL; + metadata.crl_number = 7U; + + const auto params = query::SerializeCertObject(cert, metadata); + const auto* has_crl = GetParam(params, 9U); + const auto* crl_fp = GetParam(params, 10U); + const auto* issuer_fp = GetParam(params, 11U); + const auto* this_update = GetParam(params, 12U); + const auto* next_update = GetParam(params, 13U); + const auto* crl_number = GetParam(params, 14U); + ASSERT_NE(has_crl, nullptr); + ASSERT_NE(crl_fp, nullptr); + ASSERT_NE(issuer_fp, nullptr); + ASSERT_NE(this_update, nullptr); + ASSERT_NE(next_update, nullptr); + ASSERT_NE(crl_number, nullptr); + EXPECT_EQ(*has_crl, 1U); + EXPECT_EQ(crl_fp->size(), 32U); + EXPECT_EQ(issuer_fp->size(), 32U); + EXPECT_EQ(*this_update, static_cast(metadata.this_update)); + EXPECT_EQ(*next_update, static_cast(metadata.next_update)); + EXPECT_EQ(*crl_number, metadata.crl_number); +} + TEST(SerializeCertObject, EmptySkidAndAkidEncodeAsEmptyBuffers) { cert::CertChainMetadata meta; @@ -324,11 +356,11 @@ class SerializeCertSlotInfoTest : public ::testing::Test std::unique_ptr mgr_; }; -TEST_F(SerializeCertSlotInfoTest, ProducesThreeParameters) +TEST_F(SerializeCertSlotInfoTest, ProducesTwoParameters) { const auto result = query::SerializeCertSlotInfo(*mgr_, slot_handle_, kClientId); ASSERT_TRUE(result.has_value()); - EXPECT_EQ(result.value().size(), 3U); + EXPECT_EQ(result.value().size(), 2U); } TEST_F(SerializeCertSlotInfoTest, Param0_SlotStateUint8_Occupied) @@ -351,50 +383,26 @@ TEST_F(SerializeCertSlotInfoTest, Param0_SlotStateUint8_Empty) EXPECT_EQ(*val, static_cast(score::crypto::CertificateSlotState::kEmpty)); } -TEST_F(SerializeCertSlotInfoTest, NoCrl_Param1IsZero_Param2IsZero) +TEST_F(SerializeCertSlotInfoTest, NoCrl_Param1IsZero) { stub_->slot_info_result->has_crl = false; const auto result = query::SerializeCertSlotInfo(*mgr_, slot_handle_, kClientId); ASSERT_TRUE(result.has_value()); const auto* has_crl = GetParam(result.value(), 1U); - const auto* crl_next = GetParam(result.value(), 2U); ASSERT_NE(has_crl, nullptr); - ASSERT_NE(crl_next, nullptr); EXPECT_EQ(*has_crl, 0U); - EXPECT_EQ(*crl_next, 0U); -} - -TEST_F(SerializeCertSlotInfoTest, CrlPresent_Param1IsOne_Param2IsEpoch) -{ - stub_->slot_info_result->has_crl = true; - stub_->crl_next_update_value = 1700000000LL; - const auto result = query::SerializeCertSlotInfo(*mgr_, slot_handle_, kClientId); - ASSERT_TRUE(result.has_value()); - - const auto* has_crl = GetParam(result.value(), 1U); - const auto* crl_next = GetParam(result.value(), 2U); - ASSERT_NE(has_crl, nullptr); - ASSERT_NE(crl_next, nullptr); - EXPECT_EQ(*has_crl, 1U); - EXPECT_EQ(*crl_next, static_cast(1700000000ULL)); } -TEST_F(SerializeCertSlotInfoTest, CrlPresent_GetCrlNextUpdateFails_Param2IsZero) +TEST_F(SerializeCertSlotInfoTest, CrlPresent_Param1IsOne) { - // HasCrl() returns true but GetCrlNextUpdate() is unavailable. - // The serializer encodes has_crl=1 and crl_next=0 (not an error). stub_->slot_info_result->has_crl = true; - stub_->crl_next_update_value = std::nullopt; const auto result = query::SerializeCertSlotInfo(*mgr_, slot_handle_, kClientId); ASSERT_TRUE(result.has_value()); const auto* has_crl = GetParam(result.value(), 1U); - const auto* crl_next = GetParam(result.value(), 2U); ASSERT_NE(has_crl, nullptr); - ASSERT_NE(crl_next, nullptr); EXPECT_EQ(*has_crl, 1U); - EXPECT_EQ(*crl_next, 0U); } TEST_F(SerializeCertSlotInfoTest, GetSlotInfoError_PropagatesError) diff --git a/score/crypto/src/daemon/cert_management/tests/service/test_cert_management_service.cpp b/score/crypto/src/daemon/cert_management/tests/service/test_cert_management_service.cpp index 7f65a304c..6833e33b6 100644 --- a/score/crypto/src/daemon/cert_management/tests/service/test_cert_management_service.cpp +++ b/score/crypto/src/daemon/cert_management/tests/service/test_cert_management_service.cpp @@ -103,6 +103,9 @@ class CertManagementServiceTest : public ::testing::Test slot_cfg.storage_backend = "DEFAULT"; slot_cfg.deployment_path = m_descriptor_path.string(); slot_cfg.deployment_format = "kv"; + // uid=0 matches kClientA/kClientB (see ClientId layout note above) so tests + // below can exercise slot-CRL write paths (ImportCrl) as well as reads. + slot_cfg.access_policy.allowed_write_uids = {0U}; m_slot_handle = m_registry->RegisterSlot(slot_cfg); // Map uid=0 app resource "root_ca" → slot "test/root-ca" m_registry->RegisterAppResource(0U, std::string{kAppResource}, std::string{kSlotName}); @@ -407,4 +410,252 @@ TEST_F(CertManagementServiceTest, SessionCrl_UnknownNode_ReturnsError) EXPECT_FALSE(m_service->ResolveCertEntryForOperation(kClientA, 999U).has_value()); } +// --------------------------------------------------------------------------- +// ResolveCertWithCrlMetadataForOperation +// +// This is the function behind GET_CERTIFICATE_OBJECT (mediator typed-object +// query): it must report CRL metadata from the session association when +// present, and otherwise fall back to the slot's persisted CRL metadata when +// the entry was loaded from a slot. Ephemeral (non-slot) certs have no +// fallback. Regression coverage for a prior inconsistency where the CERT:MANAGEMENT +// executor's now-removed CERT_GET_METADATA path only checked the session CRL and +// silently dropped persisted slot CRL metadata. +// --------------------------------------------------------------------------- + +score::crypto::CrlMetadata MakeCrlMetadata(std::uint8_t seed) +{ + score::crypto::CrlMetadata metadata; + metadata.fingerprint.fill(seed); + metadata.issuer_fingerprint.fill(static_cast(seed + 1U)); + metadata.this_update = 1000 + seed; + metadata.next_update = 2000 + seed; + metadata.crl_number = 3000U + seed; + return metadata; +} + +TEST_F(CertManagementServiceTest, ResolveCertWithCrlMetadata_NoCrlAnywhere_ReturnsNoMetadata) +{ + const auto result = LoadCert(kClientA); + ASSERT_NE(result.node_id, 0U); + + const auto resolved = m_service->ResolveCertWithCrlMetadataForOperation(kClientA, result.node_id); + ASSERT_TRUE(resolved.has_value()); + EXPECT_EQ(resolved->cert->GetSubject(), kSubjectInitial); + EXPECT_FALSE(resolved->crl_metadata.has_value()); +} + +TEST_F(CertManagementServiceTest, ResolveCertWithCrlMetadata_SlotHasPersistedCrl_FallsBackToSlotMetadata) +{ + const auto result = LoadCert(kClientA); + ASSERT_NE(result.node_id, 0U); + ASSERT_FALSE(result.entry->HasSessionCrl()); + + const std::vector crl_bytes{0xC0U, 0xC1U, 0xC2U}; + const auto slot_metadata = MakeCrlMetadata(0x11U); + ASSERT_TRUE(m_service->GetSlotManager() + ->ImportCrl(m_slot_handle, kClientA, crl_bytes, score::crypto::FormatType::kDer, slot_metadata) + .has_value()); + + const auto resolved = m_service->ResolveCertWithCrlMetadataForOperation(kClientA, result.node_id); + ASSERT_TRUE(resolved.has_value()); + ASSERT_TRUE(resolved->crl_metadata.has_value()); + EXPECT_EQ(resolved->crl_metadata->fingerprint, slot_metadata.fingerprint); + EXPECT_EQ(resolved->crl_metadata->crl_number, slot_metadata.crl_number); +} + +TEST_F(CertManagementServiceTest, ResolveCertWithCrlMetadata_SessionCrlPresent_TakesPrecedenceOverSlotCrl) +{ + const auto result = LoadCert(kClientA); + ASSERT_NE(result.node_id, 0U); + + // Persist a CRL on the slot itself... + const std::vector slot_crl_bytes{0xC0U, 0xC1U, 0xC2U}; + const auto slot_metadata = MakeCrlMetadata(0x11U); + ASSERT_TRUE(m_service->GetSlotManager() + ->ImportCrl(m_slot_handle, kClientA, slot_crl_bytes, score::crypto::FormatType::kDer, slot_metadata) + .has_value()); + + // ...but attach a *different* session-scoped CRL to this entry. + const std::vector session_crl_bytes{0xD0U, 0xD1U}; + const auto session_metadata = MakeCrlMetadata(0x22U); + result.entry->AttachSessionCrl(session_crl_bytes, score::crypto::FormatType::kDer, session_metadata); + + const auto resolved = m_service->ResolveCertWithCrlMetadataForOperation(kClientA, result.node_id); + ASSERT_TRUE(resolved.has_value()); + ASSERT_TRUE(resolved->crl_metadata.has_value()); + // Session metadata wins — the slot's persisted CRL must not leak through. + EXPECT_EQ(resolved->crl_metadata->fingerprint, session_metadata.fingerprint); + EXPECT_EQ(resolved->crl_metadata->crl_number, session_metadata.crl_number); +} + +TEST_F(CertManagementServiceTest, ResolveCertWithCrlMetadata_EphemeralCert_NoSlotFallback_ReturnsNoMetadata) +{ + std::ifstream file(m_cert_path, std::ios::binary); + ASSERT_TRUE(file.is_open()); + const std::vector bytes{std::istreambuf_iterator(file), {}}; + auto parsed = m_parser->ParseCertificate(bytes.data(), bytes.size(), score::crypto::FormatType::kPem); + ASSERT_TRUE(parsed.has_value()); + + const auto slot_node_id = m_service->ResolveCertSlot(std::string{kAppResource}, kClientA); + ASSERT_TRUE(slot_node_id.has_value()); + + cert::CertRegistrationParams params; + params.client_id = kClientA; + params.parent_id = slot_node_id.value(); + params.slot_handle = cert::CertSlotHandle{}; // ephemeral — no slot backing + const auto registered = m_service->RegisterCertMaterial(params, *parsed); + ASSERT_TRUE(registered.has_value()); + + const auto resolved = m_service->ResolveCertWithCrlMetadataForOperation(kClientA, registered->node_id); + ASSERT_TRUE(resolved.has_value()); + EXPECT_FALSE(resolved->crl_metadata.has_value()); +} + +TEST_F(CertManagementServiceTest, ResolveCertWithCrlMetadata_SlotNodeNotYetLoaded_ReadsCrlFromSlot) +{ + const std::vector crl_bytes{0xC0U, 0xC1U, 0xC2U}; + const auto slot_metadata = MakeCrlMetadata(0x33U); + ASSERT_TRUE(m_service->GetSlotManager() + ->ImportCrl(m_slot_handle, kClientA, crl_bytes, score::crypto::FormatType::kDer, slot_metadata) + .has_value()); + + // Resolve the bare slot node (kCertSlot), never Load()-ed into a CertEntry. + const auto slot_node_id = m_service->ResolveCertSlot(std::string{kAppResource}, kClientA); + ASSERT_TRUE(slot_node_id.has_value()); + + const auto resolved = m_service->ResolveCertWithCrlMetadataForOperation(kClientA, slot_node_id.value()); + ASSERT_TRUE(resolved.has_value()); + EXPECT_EQ(resolved->cert->GetSubject(), kSubjectInitial); + ASSERT_TRUE(resolved->crl_metadata.has_value()); + EXPECT_EQ(resolved->crl_metadata->crl_number, slot_metadata.crl_number); +} + +TEST_F(CertManagementServiceTest, ResolveCertWithCrlMetadata_UnknownNode_ReturnsError) +{ + EXPECT_FALSE(m_service->ResolveCertWithCrlMetadataForOperation(kClientA, 999U).has_value()); +} + +// --------------------------------------------------------------------------- +// ResolveCrlForOperation +// +// This is the `with_crl` propagation source resolver: it returns the actual +// CRL bytes (not just metadata) to copy into a destination slot/trust-store +// member via SaveCertificateWithCrl / AddCertificateToTrustStore(with_crl). +// Same session-vs-slot precedence as ResolveCertWithCrlMetadataForOperation, +// but had zero prior test coverage. +// --------------------------------------------------------------------------- + +TEST_F(CertManagementServiceTest, ResolveCrl_NoCrlAnywhere_ReturnsNulloptSuccess) +{ + const auto result = LoadCert(kClientA); + ASSERT_NE(result.node_id, 0U); + + const auto resolved = m_service->ResolveCrlForOperation(kClientA, result.node_id); + ASSERT_TRUE(resolved.has_value()); + EXPECT_FALSE(resolved->has_value()); +} + +TEST_F(CertManagementServiceTest, ResolveCrl_SlotHasPersistedCrl_NoSessionCrl_ReturnsSlotCrlBytes) +{ + const auto result = LoadCert(kClientA); + ASSERT_NE(result.node_id, 0U); + + const std::vector crl_bytes{0xC0U, 0xC1U, 0xC2U}; + const auto slot_metadata = MakeCrlMetadata(0x11U); + ASSERT_TRUE(m_service->GetSlotManager() + ->ImportCrl(m_slot_handle, kClientA, crl_bytes, score::crypto::FormatType::kDer, slot_metadata) + .has_value()); + + const auto resolved = m_service->ResolveCrlForOperation(kClientA, result.node_id); + ASSERT_TRUE(resolved.has_value()); + ASSERT_TRUE(resolved->has_value()); + EXPECT_EQ((*resolved)->bytes, crl_bytes); + EXPECT_EQ((*resolved)->format, score::crypto::FormatType::kDer); + ASSERT_TRUE((*resolved)->metadata.has_value()); + EXPECT_EQ((*resolved)->metadata->crl_number, slot_metadata.crl_number); +} + +TEST_F(CertManagementServiceTest, ResolveCrl_SessionCrlPresent_TakesPrecedenceOverSlotCrl) +{ + const auto result = LoadCert(kClientA); + ASSERT_NE(result.node_id, 0U); + + // Persist a different CRL on the slot itself... + const std::vector slot_crl_bytes{0xC0U, 0xC1U, 0xC2U}; + ASSERT_TRUE( + m_service->GetSlotManager() + ->ImportCrl( + m_slot_handle, kClientA, slot_crl_bytes, score::crypto::FormatType::kDer, MakeCrlMetadata(0x11U)) + .has_value()); + + // ...but attach a session-scoped CRL to this entry. + const std::vector session_crl_bytes{0xD0U, 0xD1U}; + const auto session_metadata = MakeCrlMetadata(0x22U); + result.entry->AttachSessionCrl(session_crl_bytes, score::crypto::FormatType::kPem, session_metadata); + + const auto resolved = m_service->ResolveCrlForOperation(kClientA, result.node_id); + ASSERT_TRUE(resolved.has_value()); + ASSERT_TRUE(resolved->has_value()); + // Session CRL wins — the slot's persisted CRL must not leak through. + EXPECT_EQ((*resolved)->bytes, session_crl_bytes); + EXPECT_EQ((*resolved)->format, score::crypto::FormatType::kPem); + ASSERT_TRUE((*resolved)->metadata.has_value()); + EXPECT_EQ((*resolved)->metadata->crl_number, session_metadata.crl_number); +} + +TEST_F(CertManagementServiceTest, ResolveCrl_EphemeralCert_NoSessionCrl_ReturnsNulloptSuccess) +{ + std::ifstream file(m_cert_path, std::ios::binary); + ASSERT_TRUE(file.is_open()); + const std::vector bytes{std::istreambuf_iterator(file), {}}; + auto parsed = m_parser->ParseCertificate(bytes.data(), bytes.size(), score::crypto::FormatType::kPem); + ASSERT_TRUE(parsed.has_value()); + + const auto slot_node_id = m_service->ResolveCertSlot(std::string{kAppResource}, kClientA); + ASSERT_TRUE(slot_node_id.has_value()); + + cert::CertRegistrationParams params; + params.client_id = kClientA; + params.parent_id = slot_node_id.value(); + params.slot_handle = cert::CertSlotHandle{}; // ephemeral — no slot backing, no fallback + const auto registered = m_service->RegisterCertMaterial(params, *parsed); + ASSERT_TRUE(registered.has_value()); + + // No session CRL attached either: must resolve to "no CRL" success, not an + // error, even though the ephemeral entry has no slot to fall back to. + const auto resolved = m_service->ResolveCrlForOperation(kClientA, registered->node_id); + ASSERT_TRUE(resolved.has_value()); + EXPECT_FALSE(resolved->has_value()); +} + +TEST_F(CertManagementServiceTest, ResolveCrl_BareSlotNodeNotYetLoaded_ReadsCrlFromSlot) +{ + const std::vector crl_bytes{0xC0U, 0xC1U, 0xC2U}; + ASSERT_TRUE( + m_service->GetSlotManager() + ->ImportCrl(m_slot_handle, kClientA, crl_bytes, score::crypto::FormatType::kDer, MakeCrlMetadata(0x33U)) + .has_value()); + + const auto slot_node_id = m_service->ResolveCertSlot(std::string{kAppResource}, kClientA); + ASSERT_TRUE(slot_node_id.has_value()); + + const auto resolved = m_service->ResolveCrlForOperation(kClientA, slot_node_id.value()); + ASSERT_TRUE(resolved.has_value()); + ASSERT_TRUE(resolved->has_value()); + EXPECT_EQ((*resolved)->bytes, crl_bytes); +} + +// A completely unknown node_id resolves neither to a CertDataNode nor a +// CertSlotDataNode, so both internal lookups fail and there is no source to +// report — this is treated as "no CRL available", not an error. Documents +// the actual (perhaps surprising) contract rather than asserting it is ideal; +// callers reach this function only after already resolving cert_node_id +// earlier in the same operation, so an unknown id should not occur in practice. +TEST_F(CertManagementServiceTest, ResolveCrl_UnknownNode_ReturnsNulloptNotError) +{ + const auto resolved = m_service->ResolveCrlForOperation(kClientA, 999U); + ASSERT_TRUE(resolved.has_value()); + EXPECT_FALSE(resolved->has_value()); +} + } // namespace diff --git a/score/crypto/src/daemon/cert_management/tests/slot/test_file_backed_slot_handler.cpp b/score/crypto/src/daemon/cert_management/tests/slot/test_file_backed_slot_handler.cpp index 89b7162fa..d26fb232b 100644 --- a/score/crypto/src/daemon/cert_management/tests/slot/test_file_backed_slot_handler.cpp +++ b/score/crypto/src/daemon/cert_management/tests/slot/test_file_backed_slot_handler.cpp @@ -58,14 +58,14 @@ class FakeParser final : public provider::ICertParser return std::vector{*parsed}; } - score::crypto::Expected ValidateCrl(const std::uint8_t*, - std::size_t, - score::crypto::FormatType, - const std::uint8_t*, - std::size_t, - score::crypto::FormatType) override + score::crypto::Expected ValidateCrl(const std::uint8_t*, + std::size_t, + score::crypto::FormatType, + const std::uint8_t*, + std::size_t, + score::crypto::FormatType) override { - return 0; + return score::crypto::CrlMetadata{}; } }; @@ -127,12 +127,37 @@ TEST_F(FileBackedSlotHandlerTest, StoresLoadsAndClearsCrl) const std::vector crl{4U, 5U, 6U}; const auto crl_span = score::crypto::span{crl.data(), crl.size()}; ASSERT_TRUE(m_handler->StoreCrl(m_slot, crl_span, score::crypto::FormatType::kDer).has_value()); - EXPECT_TRUE(m_handler->HasCrl(m_slot)); + ASSERT_TRUE(m_handler->HasCrl(m_slot).has_value()); + EXPECT_TRUE(m_handler->HasCrl(m_slot).value()); ASSERT_TRUE(m_handler->LoadCrl(m_slot).has_value()); EXPECT_EQ(*m_handler->LoadCrl(m_slot), crl); ASSERT_TRUE(m_handler->ClearCrl(m_slot).has_value()); - EXPECT_FALSE(m_handler->HasCrl(m_slot)); + ASSERT_TRUE(m_handler->HasCrl(m_slot).has_value()); + EXPECT_FALSE(m_handler->HasCrl(m_slot).value()); +} + +TEST_F(FileBackedSlotHandlerTest, DerivesMetadataForPreconfiguredCrlWithoutCachedMetadata) +{ + cert::CertChainMetadata certificate_metadata; + certificate_metadata.subject_canonical = "CN=file-test"; + certificate_metadata.issuer_canonical = "CN=file-test"; + auto certificate = std::make_shared( + std::move(certificate_metadata), std::vector{1U, 2U, 3U}, score::crypto::FormatType::kDer); + ASSERT_TRUE(m_handler->StoreCertificate(m_slot, *certificate).has_value()); + + const std::vector crl{4U, 5U, 6U}; + ASSERT_TRUE(m_handler + ->StoreCrl(m_slot, + score::crypto::span{crl.data(), crl.size()}, + score::crypto::FormatType::kDer) + .has_value()); + + const auto metadata = m_handler->GetCrlMetadata(m_slot); + ASSERT_TRUE(metadata.has_value()); + EXPECT_EQ(metadata->this_update, 0); + EXPECT_EQ(metadata->next_update, 0); + EXPECT_EQ(metadata->crl_number, 0U); } // Storing a new certificate must invalidate any existing CRL: the CRL was @@ -154,14 +179,16 @@ TEST_F(FileBackedSlotHandlerTest, StoreCertificate_ClearsExistingCrl) score::crypto::span{crl_bytes.data(), crl_bytes.size()}, score::crypto::FormatType::kDer) .has_value()); - ASSERT_TRUE(m_handler->HasCrl(m_slot)); + ASSERT_TRUE(m_handler->HasCrl(m_slot).has_value()); + ASSERT_TRUE(m_handler->HasCrl(m_slot).value()); // Storing a new cert must invalidate the stale CRL. // crl_path is preserved in the descriptor (for future StoreCrl re-use) // but the CRL file is removed, so HasCrl() must return false. ASSERT_TRUE(m_handler->StoreCertificate(m_slot, *certificate).has_value()); - EXPECT_FALSE(m_handler->HasCrl(m_slot)); // file gone → FileExists false + ASSERT_TRUE(m_handler->HasCrl(m_slot).has_value()); + EXPECT_FALSE(m_handler->HasCrl(m_slot).value()); EXPECT_FALSE(std::filesystem::exists(m_crl)); // crl_path is preserved in the descriptor so a subsequent StoreCrl re-uses @@ -172,7 +199,8 @@ TEST_F(FileBackedSlotHandlerTest, StoreCertificate_ClearsExistingCrl) score::crypto::span{new_crl.data(), new_crl.size()}, score::crypto::FormatType::kDer) .has_value()); - EXPECT_TRUE(m_handler->HasCrl(m_slot)); + ASSERT_TRUE(m_handler->HasCrl(m_slot).has_value()); + EXPECT_TRUE(m_handler->HasCrl(m_slot).value()); EXPECT_TRUE(std::filesystem::exists(m_crl)); } @@ -182,7 +210,8 @@ TEST_F(FileBackedSlotHandlerTest, StoreCertificate_ClearsExistingCrl) // detection doesn't go unnoticed. TEST_F(FileBackedSlotHandlerTest, HasCrl_FalseForFreshSlot) { - EXPECT_FALSE(m_handler->HasCrl(m_slot)); + ASSERT_TRUE(m_handler->HasCrl(m_slot).has_value()); + EXPECT_FALSE(m_handler->HasCrl(m_slot).value()); } // GetSlotState on a fresh slot must report kEmpty without a prior store. @@ -247,19 +276,18 @@ TEST_F(FileBackedSlotHandlerTest, ClearCrl_IsIdempotent) ASSERT_TRUE(m_handler->ClearCrl(m_slot).has_value()); // Second call must succeed — CRL file is already gone. ASSERT_TRUE(m_handler->ClearCrl(m_slot).has_value()); - EXPECT_FALSE(m_handler->HasCrl(m_slot)); + ASSERT_TRUE(m_handler->HasCrl(m_slot).has_value()); + EXPECT_FALSE(m_handler->HasCrl(m_slot).value()); } -// StoreCrl with next_update_epoch_s == 0 must NOT write a crl_next_update key -// to the descriptor. Subsequent GetCrlNextUpdate must return kResourceNotAllocated. -TEST_F(FileBackedSlotHandlerTest, StoreCrl_ZeroNextUpdate_DoesNotWriteNextUpdateKey) +// StoreCrl without metadata must not write a crl_next_update key to the descriptor. +TEST_F(FileBackedSlotHandlerTest, StoreCrl_WithoutMetadata_DoesNotWriteNextUpdateKey) { const std::vector crl{0x01U, 0x02U}; ASSERT_TRUE(m_handler ->StoreCrl(m_slot, score::crypto::span{crl.data(), crl.size()}, - score::crypto::FormatType::kDer, - 0 /* next_update_epoch_s = 0 */) + score::crypto::FormatType::kDer) .has_value()); const auto nu = m_handler->GetCrlNextUpdate(m_slot); @@ -267,17 +295,19 @@ TEST_F(FileBackedSlotHandlerTest, StoreCrl_ZeroNextUpdate_DoesNotWriteNextUpdate EXPECT_EQ(nu.error(), Error::kResourceNotAllocated); } -// StoreCrl with a non-zero next_update_epoch_s must persist the epoch and -// GetCrlNextUpdate must read it back. -TEST_F(FileBackedSlotHandlerTest, StoreCrl_NonZeroNextUpdate_RoundTrips) +// StoreCrl with validated metadata must persist nextUpdate and expose it +// through the existing freshness query. +TEST_F(FileBackedSlotHandlerTest, StoreCrl_MetadataPersistsNextUpdate) { constexpr std::int64_t kEpoch = 1800000000LL; const std::vector crl{0x03U, 0x04U}; + score::crypto::CrlMetadata metadata; + metadata.next_update = kEpoch; ASSERT_TRUE(m_handler ->StoreCrl(m_slot, score::crypto::span{crl.data(), crl.size()}, score::crypto::FormatType::kDer, - kEpoch) + metadata) .has_value()); const auto nu = m_handler->GetCrlNextUpdate(m_slot); diff --git a/score/crypto/src/daemon/cert_management/tests/truststore/test_trust_store_manager.cpp b/score/crypto/src/daemon/cert_management/tests/truststore/test_trust_store_manager.cpp index 1d9ae29d3..dad27276f 100644 --- a/score/crypto/src/daemon/cert_management/tests/truststore/test_trust_store_manager.cpp +++ b/score/crypto/src/daemon/cert_management/tests/truststore/test_trust_store_manager.cpp @@ -29,7 +29,7 @@ // - AddMember — stores a cert into an empty exclusive slot, persists to disk // - RemoveMember — removes anchor by real SHA-256 fingerprint // - AcknowledgeMemberUpdate — re-enables a disabled slot with fresh load -// - ConditionalExternal — fingerprint mismatch auto-disables on reload; AcknowledgeMemberUpdate re-enables +// - ConditionalExternal — slot updates disable members until acknowledged #include "score/crypto/src/daemon/cert_management/slot/cert_slot_manager.hpp" #include "score/crypto/src/daemon/cert_management/slot/file_backed_slot_handler.hpp" @@ -502,7 +502,9 @@ TEST_F(TrustStoreManagerTest, AcknowledgeMemberUpdate_TransitionsDisabledMemberT cert::TrustStoreConfig ts_cfg; ts_cfg.store_name = "cond-store"; ts_cfg.access_policy.allowed_write_uids = {0U}; - ts_cfg.members.push_back(cert::TrustStoreMemberConfig{"root-anchor", cert::TrustStoreMemberKind::kSharedStatic}); + ts_cfg.conditional_slot_initialization = cert::ConditionalSlotInitialization::kEnableAndAcceptCurrent; + ts_cfg.members.push_back( + cert::TrustStoreMemberConfig{"root-anchor", cert::TrustStoreMemberKind::kConditionalExternal}); cert::TrustStoreManager manager; manager.Load({ts_cfg}, registry, MakeSlotManager(registry)); @@ -542,9 +544,7 @@ TEST_F(TrustStoreManagerTest, AcknowledgeMemberUpdate_TransitionsDisabledMemberT } // --------------------------------------------------------------------------- -// ConditionalExternal — fingerprint mismatch triggers auto-disable; the entire -// scenario from initial acceptance through rotation, mismatch detection, -// and re-acknowledgement is exercised here. +// ConditionalExternal members require acknowledgment after a slot update. // // Steps: // 1. kConditionalExternal member with kEnableAndAcceptCurrent init policy: @@ -562,7 +562,10 @@ TEST_F(TrustStoreManagerTest, AcknowledgeMemberUpdate_TransitionsDisabledMemberT TEST_F(TrustStoreManagerTest, ConditionalExternal_FingerprintMismatch_AutoDisablesUntilAcknowledged) { auto registry = std::make_shared(); - const auto slot = registry->RegisterSlot(MakeSlotConfig("root-anchor", m_root_slot_kv)); + auto slot_config = MakeSlotConfig("root-anchor", m_root_slot_kv); + slot_config.access_policy.allowed_uids = {0U}; + slot_config.access_policy.allowed_write_uids = {0U}; + const auto slot = registry->RegisterSlot(slot_config); cert::TrustStoreConfig ts_cfg; ts_cfg.store_name = "cond-store"; @@ -571,8 +574,9 @@ TEST_F(TrustStoreManagerTest, ConditionalExternal_FingerprintMismatch_AutoDisabl ts_cfg.members.push_back( cert::TrustStoreMemberConfig{"root-anchor", cert::TrustStoreMemberKind::kConditionalExternal}); + auto slot_manager = MakeSlotManager(registry); cert::TrustStoreManager manager; - manager.Load({ts_cfg}, registry, MakeSlotManager(registry)); + manager.Load({ts_cfg}, registry, slot_manager); const auto ts_handle = manager.ResolveByName("cond-store"); auto store = manager.GetStore(ts_handle); @@ -589,28 +593,23 @@ TEST_F(TrustStoreManagerTest, ConditionalExternal_FingerprintMismatch_AutoDisabl // 'initial' drops here; the only remaining strong ref is inside TrustStoreHandler::m_slots. } - // Step 2: Rotate cert on disk without acknowledgement. - ASSERT_TRUE(std::filesystem::copy_file("score/tests/test_vectors/certificate/basic/certificate_updated.pem", - m_root_cert, - std::filesystem::copy_options::overwrite_existing)); - - // Step 3: ReleaseRef — last client ref gone → MaybeEvictAnchorCache → ClearAnchorCache - // (m_loaded = false, m_slots cleared, weak_ptr in m_slot_cert_cache expires). - manager.ReleaseRef(ts_handle, kClientA); + // Step 2: Update the member slot through the certificate-slot write path. + const auto updated = ParseCert("score/tests/test_vectors/certificate/basic/certificate_updated.pem"); + ASSERT_NE(updated, nullptr); + ASSERT_TRUE(slot_manager->StoreCertificate(slot, kClientA, *updated).has_value()); + manager.NotifySlotChanged(ts_handle, slot); - // Step 4: GetAnchors() triggers LoadAnchorsIntoHandler which loads cert B from disk, - // compares fingerprint(B) with the recorded fingerprint(A), detects mismatch, - // sets state.enabled = false, and calls NotifySlotUpdate with nullptr. + // Step 3: The direct slot update disables the conditional member until it is acknowledged. { - auto after_rotate = store->GetAnchors(); - ASSERT_TRUE(after_rotate.has_value()); - EXPECT_EQ(after_rotate->size(), 0U); + auto after_update = store->GetAnchors(); + ASSERT_TRUE(after_update.has_value()); + EXPECT_EQ(after_update->size(), 0U); } - // Step 5: Acknowledge — fresh load of cert B records new fingerprint and re-enables. + // Step 4: Acknowledge the update and load the replacement certificate. ASSERT_TRUE(manager.AcknowledgeMemberUpdate(ts_handle, slot, kClientA).has_value()); - // Step 6: GetAnchors() returns cert B without another reload cycle (m_loaded is + // Step 5: GetAnchors() returns cert B without another reload cycle (m_loaded is // still true; AcknowledgeMemberUpdate updated m_slots directly via NotifySlotUpdate). auto after_ack = store->GetAnchors(); ASSERT_TRUE(after_ack.has_value()); @@ -652,7 +651,8 @@ TEST_F(TrustStoreManagerTest, AddMember_WithCrlBytes_StoresAndPersistsCrl) cert::FileBackedSlotHandler fresh_handler{m_parser}; const auto cfg = MakeSlotConfig("empty-anchor", m_empty_slot_kv); ASSERT_TRUE(fresh_handler.LoadCertificate(cfg).has_value()); - ASSERT_TRUE(fresh_handler.HasCrl(cfg)); + ASSERT_TRUE(fresh_handler.HasCrl(cfg).has_value()); + ASSERT_TRUE(fresh_handler.HasCrl(cfg).value()); const auto loaded_crl = fresh_handler.LoadCrl(cfg); ASSERT_TRUE(loaded_crl.has_value()); EXPECT_EQ(*loaded_crl, crl); @@ -688,7 +688,8 @@ TEST_F(TrustStoreManagerTest, AddMember_ExistingExclusiveMember_UpsertsCrl) { cert::FileBackedSlotHandler fh{m_parser}; const auto cfg = MakeSlotConfig("empty-anchor", m_empty_slot_kv); - EXPECT_FALSE(fh.HasCrl(cfg)); + ASSERT_TRUE(fh.HasCrl(cfg).has_value()); + EXPECT_FALSE(fh.HasCrl(cfg).value()); } // Second add: same cert, with CRL — upsert path. @@ -698,7 +699,8 @@ TEST_F(TrustStoreManagerTest, AddMember_ExistingExclusiveMember_UpsertsCrl) cert::FileBackedSlotHandler fresh_handler{m_parser}; const auto cfg = MakeSlotConfig("empty-anchor", m_empty_slot_kv); - ASSERT_TRUE(fresh_handler.HasCrl(cfg)); + ASSERT_TRUE(fresh_handler.HasCrl(cfg).has_value()); + ASSERT_TRUE(fresh_handler.HasCrl(cfg).value()); const auto loaded_crl = fresh_handler.LoadCrl(cfg); ASSERT_TRUE(loaded_crl.has_value()); EXPECT_EQ(*loaded_crl, crl); @@ -810,7 +812,8 @@ TEST_F(TrustStoreManagerTest, ImportCrlForMember_WritesToExclusiveSlot) // Verify persistence: fresh handler must read the CRL. cert::FileBackedSlotHandler fresh_handler{m_parser}; const auto cfg = MakeSlotConfig("empty-anchor", m_empty_slot_kv); - ASSERT_TRUE(fresh_handler.HasCrl(cfg)); + ASSERT_TRUE(fresh_handler.HasCrl(cfg).has_value()); + ASSERT_TRUE(fresh_handler.HasCrl(cfg).value()); const auto loaded_crl = fresh_handler.LoadCrl(cfg); ASSERT_TRUE(loaded_crl.has_value()); EXPECT_EQ(*loaded_crl, crl); diff --git a/score/crypto/src/daemon/cert_management/truststore/trust_store_manager.cpp b/score/crypto/src/daemon/cert_management/truststore/trust_store_manager.cpp index 0a4bfeb51..6f1eb790d 100644 --- a/score/crypto/src/daemon/cert_management/truststore/trust_store_manager.cpp +++ b/score/crypto/src/daemon/cert_management/truststore/trust_store_manager.cpp @@ -190,7 +190,10 @@ CertObject::Sptr TrustStoreManager::LoadOrGetCached(CertSlotHandle slot) auto loaded = handler->LoadCertificate(**cfg); if (!loaded) + { + score::mw::log::LogError() << kLogPrefix << "Failed to load certificate for trust-store slot"; return nullptr; + } weak = *loaded; // cache as weak_ptr; expires when all handler caches drop their strong ref return std::move(*loaded); @@ -260,13 +263,25 @@ void TrustStoreManager::LoadAnchorsIntoHandler(TrustStoreId id, TrustStoreHandle { auto* slot_handler = GetHandler(*slot); const auto cfg = m_slot_registry->GetConfig(*slot); - if (slot_handler != nullptr && cfg && slot_handler->HasCrl(**cfg)) + if (slot_handler != nullptr && cfg) { - auto crl_res = slot_handler->LoadCrl(**cfg); - if (crl_res) + const auto has_crl = slot_handler->HasCrl(**cfg); + if (!has_crl) + { + score::mw::log::LogError() << kLogPrefix << "Failed to inspect CRL for trust-store slot"; + } + else if (has_crl.value()) { - CrlEntry entry{std::move(*crl_res), slot_handler->GetCrlFormat(**cfg)}; - handler.NotifyCrlUpdate(*slot, std::move(entry)); + auto crl_res = slot_handler->LoadCrl(**cfg); + if (!crl_res) + { + score::mw::log::LogError() << kLogPrefix << "Failed to load CRL for trust-store slot"; + } + else + { + CrlEntry entry{std::move(*crl_res), slot_handler->GetCrlFormat(**cfg)}; + handler.NotifyCrlUpdate(*slot, std::move(entry)); + } } } } @@ -406,14 +421,39 @@ void TrustStoreManager::CleanupClient(data_manager::ClientId client_id) void TrustStoreManager::NotifySlotChanged(TrustStoreHandle handle, CertSlotHandle changed_slot) { std::lock_guard lock(m_mutex); - if (!handle.IsValid() || handle.index >= m_stores.size()) + if (!handle.IsValid() || handle.index >= m_stores.size() || !m_slot_registry) return; + + bool is_conditional_member = false; + for (const auto& member : m_stores[handle.index].config.members) + { + const auto slot = m_slot_registry->ResolveSlotInternal(member.slot_name); + if (slot.has_value() && *slot == changed_slot) + { + is_conditional_member = member.kind == TrustStoreMemberKind::kConditionalExternal; + break; + } + } + // Evict the changed slot from the shared cache so LoadOrGetCached does fresh I/O. m_slot_cert_cache.erase(changed_slot.index); // Tell the handler to drop just this slot and mark itself for reload. auto* h = static_cast(m_stores[handle.index].handler.get()); if (h != nullptr) + { h->InvalidateSlot(changed_slot); + if (is_conditional_member) + h->NotifySlotUpdate(changed_slot, nullptr); + } + + if (is_conditional_member) + { + m_member_states[handle.index][changed_slot.index].enabled = false; + if (const auto persisted = PersistState(handle.index); !persisted) + { + score::mw::log::LogError() << kLogPrefix << "Failed to persist conditional member state after slot update"; + } + } } // --------------------------------------------------------------------------- @@ -455,7 +495,8 @@ score::crypto::Expected TrustStoreManager::AddMember( CertObject::Sptr cert, data_manager::ClientId client_id, score::crypto::span crl_bytes, - score::crypto::FormatType crl_format) + score::crypto::FormatType crl_format, + std::optional crl_metadata) { std::lock_guard lock(m_mutex); const TrustStoreId id = handle.index; @@ -475,7 +516,11 @@ score::crypto::Expected TrustStoreManager::AddMember( continue; const auto current = resolved->handler->LoadCertificate(*resolved->cfg); if (!current) - continue; + { + if (current.error() == Error::kResourceNotAllocated || current.error() == Error::kKeySlotEmpty) + continue; + return score::crypto::make_unexpected(current.error()); + } const auto& current_fp = (*current)->GetFingerprint(); if (current_fp.size() != cert_fp.size() || !std::equal(current_fp.begin(), current_fp.end(), cert_fp.begin())) continue; @@ -486,7 +531,8 @@ score::crypto::Expected TrustStoreManager::AddMember( // Upsert CRL if provided — trust store owns this exclusive slot. if (has_crl) { - const auto stored = resolved->handler->StoreCrl(*resolved->cfg, crl_bytes, crl_format); + const auto stored = + resolved->handler->StoreCrl(*resolved->cfg, crl_bytes, crl_format, std::move(crl_metadata)); if (stored) { CrlEntry entry{std::vector(crl_bytes.begin(), crl_bytes.end()), crl_format}; @@ -531,17 +577,15 @@ score::crypto::Expected TrustStoreManager::AddMember( if (!stored) return score::crypto::make_unexpected(stored.error()); - // Best-effort CRL write — cert already persisted; CRL failure is non-fatal. - // Caller may retry via ImportCrlForMember. if (has_crl) { - const auto crl_stored = resolved->handler->StoreCrl(*resolved->cfg, crl_bytes, crl_format); - if (crl_stored) - { - CrlEntry entry{std::vector(crl_bytes.begin(), crl_bytes.end()), crl_format}; - static_cast(m_stores[id].handler.get()) - ->NotifyCrlUpdate(resolved->slot, std::move(entry)); - } + const auto crl_stored = + resolved->handler->StoreCrl(*resolved->cfg, crl_bytes, crl_format, std::move(crl_metadata)); + if (!crl_stored) + return score::crypto::make_unexpected(crl_stored.error()); + CrlEntry entry{std::vector(crl_bytes.begin(), crl_bytes.end()), crl_format}; + static_cast(m_stores[id].handler.get()) + ->NotifyCrlUpdate(resolved->slot, std::move(entry)); } m_member_states[id][resolved->slot.index].enabled = true; @@ -560,11 +604,11 @@ score::crypto::Expected TrustStoreManager::ImportCrlForMe score::crypto::span crl_data, score::crypto::FormatType format, data_manager::ClientId client_id, - std::int64_t next_update_epoch_s) + std::optional metadata) { std::lock_guard lock(m_mutex); const TrustStoreId id = handle.index; - if (id >= m_stores.size() || !slot.IsValid()) + if (id >= m_stores.size() || !slot.IsValid() || !m_slot_registry) return score::crypto::make_unexpected(Error::kInvalidResourceId); if (crl_data.empty()) return score::crypto::make_unexpected(Error::kInvalidArgument); @@ -578,7 +622,7 @@ score::crypto::Expected TrustStoreManager::ImportCrlForMe continue; if (member.kind != TrustStoreMemberKind::kExclusiveMutable) return score::crypto::make_unexpected(Error::kUnsupportedOperation); - const auto stored = resolved->handler->StoreCrl(*resolved->cfg, crl_data, format, next_update_epoch_s); + const auto stored = resolved->handler->StoreCrl(*resolved->cfg, crl_data, format, std::move(metadata)); if (!stored) return score::crypto::make_unexpected(stored.error()); CrlEntry entry{std::vector(crl_data.begin(), crl_data.end()), format}; @@ -588,6 +632,34 @@ score::crypto::Expected TrustStoreManager::ImportCrlForMe return score::crypto::make_unexpected(Error::kInvalidResourceId); } +score::crypto::Expected TrustStoreManager::DeleteCrlForMember(TrustStoreHandle handle, + CertSlotHandle slot, + data_manager::ClientId client_id) +{ + std::lock_guard lock(m_mutex); + const TrustStoreId id = handle.index; + if (id >= m_stores.size() || !slot.IsValid() || !m_slot_registry) + return score::crypto::make_unexpected(Error::kInvalidResourceId); + if (!AccessPolicyEnforcer::CheckTrustStoreWritePermission(m_stores[id].config, client_id).has_value()) + return score::crypto::make_unexpected(Error::kAccessDenied); + + for (const auto& member : m_stores[id].config.members) + { + const auto resolved = ResolveMember(member); + if (!resolved || resolved->slot.index != slot.index) + continue; + if (member.kind != TrustStoreMemberKind::kExclusiveMutable) + return score::crypto::make_unexpected(Error::kUnsupportedOperation); + + const auto cleared = resolved->handler->ClearCrl(*resolved->cfg); + if (!cleared) + return score::crypto::make_unexpected(cleared.error()); + static_cast(m_stores[id].handler.get())->NotifyCrlUpdate(slot, std::nullopt); + return std::monostate{}; + } + return score::crypto::make_unexpected(Error::kInvalidResourceId); +} + score::crypto::Expected TrustStoreManager::RemoveMember(TrustStoreHandle handle, const std::vector& fingerprint, data_manager::ClientId client_id) @@ -631,7 +703,7 @@ score::crypto::Expected TrustStoreManager::EnableMember(T { std::lock_guard lock(m_mutex); const TrustStoreId id = handle.index; - if (id >= m_stores.size() || !slot.IsValid()) + if (id >= m_stores.size() || !slot.IsValid() || !m_slot_registry) return score::crypto::make_unexpected(Error::kInvalidResourceId); if (!AccessPolicyEnforcer::CheckTrustStoreWritePermission(m_stores[id].config, client_id).has_value()) return score::crypto::make_unexpected(Error::kAccessDenied); @@ -678,10 +750,22 @@ score::crypto::Expected TrustStoreManager::AcknowledgeMem { std::lock_guard lock(m_mutex); const TrustStoreId id = handle.index; - if (id >= m_stores.size() || !slot.IsValid()) + if (id >= m_stores.size() || !slot.IsValid() || !m_slot_registry) return score::crypto::make_unexpected(Error::kInvalidResourceId); if (!AccessPolicyEnforcer::CheckTrustStoreWritePermission(m_stores[id].config, client_id).has_value()) return score::crypto::make_unexpected(Error::kAccessDenied); + bool is_conditional_member = false; + for (const auto& member : m_stores[id].config.members) + { + const auto member_slot = m_slot_registry->ResolveSlotInternal(member.slot_name); + if (member_slot.has_value() && *member_slot == slot) + { + is_conditional_member = member.kind == TrustStoreMemberKind::kConditionalExternal; + break; + } + } + if (!is_conditional_member) + return score::crypto::make_unexpected(Error::kUnsupportedOperation); // Fresh load to capture the new cert and update the shared cache. const auto resolved = ResolveSlotBackend(slot); if (!resolved) diff --git a/score/crypto/src/daemon/cert_management/truststore/trust_store_manager.hpp b/score/crypto/src/daemon/cert_management/truststore/trust_store_manager.hpp index 998fb74bf..36db3c14b 100644 --- a/score/crypto/src/daemon/cert_management/truststore/trust_store_manager.hpp +++ b/score/crypto/src/daemon/cert_management/truststore/trust_store_manager.hpp @@ -14,7 +14,8 @@ #ifndef SCORE_CRYPTO_SRC_DAEMON_CERT_MANAGEMENT_TRUSTSTORE_TRUST_STORE_MANAGER_HPP #define SCORE_CRYPTO_SRC_DAEMON_CERT_MANAGEMENT_TRUSTSTORE_TRUST_STORE_MANAGER_HPP -#include "score/crypto/src/api/common/types.hpp" +#include "score/crypto/src/api/types/certificate.hpp" +#include "score/crypto/src/api/types/common.hpp" #include "score/crypto/src/common/types.hpp" #include "score/crypto/src/daemon/cert_management/interfaces/cert_object.hpp" #include "score/crypto/src/daemon/cert_management/interfaces/cert_slot_config.hpp" @@ -33,6 +34,7 @@ #include #include #include +#include #include #include #include @@ -160,7 +162,7 @@ class TrustStoreManager /// - kExclusiveMutable match: CRL is stored/updated; cert is re-enabled. /// - kSharedStatic / kConditionalExternal match: trust store does not own /// these slots; if crl_bytes is non-empty, kUnsupportedOperation is - /// returned. Callers should use ImportCrl directly on the slot resource. + /// returned. Callers should use ImportCrlToSlot on the slot resource. /// /// Write access to the trust store must be checked by CertManagementService /// before calling this method. @@ -169,7 +171,8 @@ class TrustStoreManager CertObject::Sptr cert, data_manager::ClientId client_id, score::crypto::span crl_bytes = {}, - score::crypto::FormatType crl_format = score::crypto::FormatType::kDer); + score::crypto::FormatType crl_format = score::crypto::FormatType::kDer, + std::optional crl_metadata = std::nullopt); /// @brief Import a CRL to the exclusive trust store slot identified by @p slot. /// @@ -185,7 +188,10 @@ class TrustStoreManager score::crypto::span crl_data, score::crypto::FormatType format, data_manager::ClientId client_id, - std::int64_t next_update_epoch_s = 0); + std::optional metadata = std::nullopt); + + [[nodiscard]] score::crypto::Expected + DeleteCrlForMember(TrustStoreHandle handle, CertSlotHandle slot, data_manager::ClientId client_id); /// @brief Remove a certificate from a trust store by fingerprint. /// diff --git a/score/crypto/src/daemon/provider/cert_management/BUILD b/score/crypto/src/daemon/provider/cert_management/BUILD index 5daf42b41..b6b6985c1 100644 --- a/score/crypto/src/daemon/provider/cert_management/BUILD +++ b/score/crypto/src/daemon/provider/cert_management/BUILD @@ -20,6 +20,7 @@ cc_library( visibility = ["//:__subpackages__"], deps = [ "//score/crypto/src/api/common:crypto_common", + "//score/crypto/src/api/types:types", "//score/crypto/src/common:common_types", "//score/crypto/src/daemon/common", ], @@ -36,6 +37,8 @@ cc_library( deps = [ ":cert_parser_headers", "//score/crypto/src/api/common:crypto_common", + "//score/crypto/src/api/types:types", + "//score/crypto/src/api/types:certificate_types", "//score/crypto/src/daemon/cert_management:cert_management_headers", ], ) diff --git a/score/crypto/src/daemon/provider/cert_management/cert_management_operations.hpp b/score/crypto/src/daemon/provider/cert_management/cert_management_operations.hpp index 94f47107a..01b3cfc13 100644 --- a/score/crypto/src/daemon/provider/cert_management/cert_management_operations.hpp +++ b/score/crypto/src/daemon/provider/cert_management/cert_management_operations.hpp @@ -23,19 +23,16 @@ using OperationAction = common::OperationAction; inline constexpr OperationAction CERT_PARSE = 0x10U; inline constexpr OperationAction CERT_PARSE_CHAIN = 0x11U; inline constexpr OperationAction CERT_SAVE = 0x12U; -inline constexpr OperationAction CERT_GET_METADATA = 0x13U; inline constexpr OperationAction CERT_LOAD = 0x20U; inline constexpr OperationAction CERT_EXPORT = 0x30U; inline constexpr OperationAction CERT_GET_EXPORT_SIZE = 0x31U; inline constexpr OperationAction CERT_CONVERT = 0x32U; inline constexpr OperationAction CERT_GET_CONVERT_SIZE = 0x33U; inline constexpr OperationAction CERT_CLEAR = 0x40U; -inline constexpr OperationAction CERT_SLOT_INFO = 0x50U; inline constexpr OperationAction CERT_PUBLIC_KEY = 0x60U; -inline constexpr OperationAction CERT_DELETE_EXPIRED = 0x70U; inline constexpr OperationAction CRL_IMPORT = 0x80U; inline constexpr OperationAction CRL_DELETE = 0x81U; -inline constexpr OperationAction CRL_DELETE_EXPIRED = 0x82U; +inline constexpr OperationAction CRL_IMPORT_TO_SLOT = 0x82U; inline constexpr OperationAction OCSP_REQUEST = 0x90U; inline constexpr OperationAction TRUST_STORE_ADD_CERT = 0xC0U; inline constexpr OperationAction TRUST_STORE_REMOVE_CERT = 0xC1U; @@ -44,10 +41,8 @@ inline constexpr OperationAction TRUST_STORE_DISABLE_CERT = 0xC3U; inline constexpr OperationAction TRUST_STORE_ACK_UPDATE = 0xC4U; inline constexpr OperationAction TRUST_STORE_REMOVE_CERT_BY_ID = 0xC5U; // remove by cert node_id (lib resolves fp) inline constexpr OperationAction TRUST_STORE_IMPORT_CRL_FOR_MEMBER = 0xC6U; // fingerprint + CRL bytes → exclusive slot -inline constexpr OperationAction TRUST_STORE_GET_INFO = 0xC7U; // read-only snapshot: member list + state -// Enable/disable now fingerprint-based at the IPC layer (no slot_node_id round-trip needed from lib side): -// TRUST_STORE_ENABLE_CERT (0xC2): [0]=ts_node_id, [1]=fingerprint_bytes -// TRUST_STORE_DISABLE_CERT (0xC3): [0]=ts_node_id, [1]=fingerprint_bytes +inline constexpr OperationAction TRUST_STORE_DELETE_CRL_FOR_MEMBER = 0xC8U; // clear CRL from exclusive slot +// TRUST_STORE_ENABLE_CERT / TRUST_STORE_DISABLE_CERT: [0]=ts_node_id, [1]=slot_node_id inline constexpr OperationAction CERT_RELEASE = 0xF0U; diff --git a/score/crypto/src/daemon/provider/cert_management/cert_types.hpp b/score/crypto/src/daemon/provider/cert_management/cert_types.hpp index 879b305f5..080b5ede7 100644 --- a/score/crypto/src/daemon/provider/cert_management/cert_types.hpp +++ b/score/crypto/src/daemon/provider/cert_management/cert_types.hpp @@ -14,7 +14,8 @@ #ifndef SCORE_CRYPTO_SRC_DAEMON_PROVIDER_CERT_MANAGEMENT_CERT_TYPES_HPP #define SCORE_CRYPTO_SRC_DAEMON_PROVIDER_CERT_MANAGEMENT_CERT_TYPES_HPP -#include "score/crypto/src/api/common/types.hpp" +#include "score/crypto/src/api/types/certificate.hpp" +#include "score/crypto/src/api/types/common.hpp" #include "score/crypto/src/daemon/cert_management/interfaces/cert_object.hpp" #include "score/crypto/src/daemon/cert_management/interfaces/cert_types.hpp" #include "score/crypto/src/daemon/key_management/interfaces/key_types.hpp" @@ -32,11 +33,7 @@ using ::score::crypto::daemon::cert_management::CertObject; using ::score::crypto::daemon::cert_management::CertSlotHandle; using ::score::crypto::daemon::cert_management::TrustStoreHandle; -enum class ChainTerminationPolicy : std::uint8_t -{ - kRootRequired = 0, - kTrustStoreTerminated = 1 -}; +using ::score::crypto::ChainTerminationPolicy; enum class CertVerifyErrorCode : std::uint16_t { kNone = 0, @@ -51,13 +48,7 @@ enum class CertVerifyErrorCode : std::uint16_t kUnknownError }; -enum class RevocationCheckPolicy : std::uint8_t -{ - kNone = 0, - kCrlOnly, - kOcspOnly, - kOcspWithCrlFallback -}; +using ::score::crypto::RevocationCheckPolicy; /// Verification result. The established chain is returned as neutral CertObjects /// (leaf-first, terminating anchor last), never as provider-bound handles. diff --git a/score/crypto/src/daemon/provider/cert_management/i_cert_parser.hpp b/score/crypto/src/daemon/provider/cert_management/i_cert_parser.hpp index 02ac6f372..067cc94e1 100644 --- a/score/crypto/src/daemon/provider/cert_management/i_cert_parser.hpp +++ b/score/crypto/src/daemon/provider/cert_management/i_cert_parser.hpp @@ -14,7 +14,8 @@ #ifndef SCORE_CRYPTO_SRC_DAEMON_PROVIDER_CERT_MANAGEMENT_I_CERT_PARSER_HPP #define SCORE_CRYPTO_SRC_DAEMON_PROVIDER_CERT_MANAGEMENT_I_CERT_PARSER_HPP -#include "score/crypto/src/api/common/types.hpp" +#include "score/crypto/src/api/types/certificate.hpp" +#include "score/crypto/src/api/types/common.hpp" #include "score/crypto/src/common/types.hpp" #include "score/crypto/src/daemon/common/daemon_error.hpp" @@ -61,9 +62,9 @@ class ICertParser /// 2. The CRL issuer DN equals the issuer cert's subject DN. /// 3. The CRL signature verifies against the issuer cert's public key. /// - /// @return The CRL's nextUpdate field as Unix epoch seconds on success. - /// 0 is returned when the CRL omits the optional nextUpdate field. - [[nodiscard]] virtual score::crypto::Expected ValidateCrl( + /// @return Metadata extracted from the validated CRL. Optional timestamps + /// and cRLNumber are zero when the corresponding fields are absent. + [[nodiscard]] virtual score::crypto::Expected ValidateCrl( const std::uint8_t* crl_data, std::size_t crl_size, score::crypto::FormatType crl_format, diff --git a/score/crypto/src/daemon/provider/score_provider/openssl/cert_management/openssl_cert_parser.cpp b/score/crypto/src/daemon/provider/score_provider/openssl/cert_management/openssl_cert_parser.cpp index 52abf0eab..8a887e28a 100644 --- a/score/crypto/src/daemon/provider/score_provider/openssl/cert_management/openssl_cert_parser.cpp +++ b/score/crypto/src/daemon/provider/score_provider/openssl/cert_management/openssl_cert_parser.cpp @@ -19,7 +19,9 @@ #include #include +#include #include +#include #include #include #include @@ -291,9 +293,47 @@ std::int64_t Asn1TimeToEpoch(const ASN1_TIME* asn1) return static_cast(timegm(&t)); } + +bool Sha256CertificateDigest(const X509* certificate, std::array& output) +{ + unsigned int digest_size = 0U; + return X509_digest(certificate, EVP_sha256(), output.data(), &digest_size) == 1 && digest_size == output.size(); +} + +bool Sha256CrlDigest(const X509_CRL* crl, std::array& output) +{ + unsigned int digest_size = 0U; + return X509_CRL_digest(crl, EVP_sha256(), output.data(), &digest_size) == 1 && digest_size == output.size(); +} + +std::uint64_t CrlNumber(const X509_CRL* crl) +{ + auto* number = static_cast(X509_CRL_get_ext_d2i(crl, NID_crl_number, nullptr, nullptr)); + if (number == nullptr) + return 0U; + + BIGNUM* big_number = ASN1_INTEGER_to_BN(number, nullptr); + ASN1_INTEGER_free(number); + if (big_number == nullptr || BN_num_bits(big_number) > 64) + { + BN_free(big_number); + return 0U; + } + + char* hex = BN_bn2hex(big_number); + BN_free(big_number); + if (hex == nullptr) + return 0U; + + std::uint64_t result = 0U; + const auto* end = hex + std::strlen(hex); + const auto [parsed_end, ec] = std::from_chars(hex, end, result, 16); + OPENSSL_free(hex); + return ec == std::errc{} && parsed_end == end ? result : 0U; +} } // namespace -score::crypto::Expected OpenSslCertParser::ValidateCrl( +score::crypto::Expected OpenSslCertParser::ValidateCrl( const std::uint8_t* crl_data, std::size_t crl_size, score::crypto::FormatType crl_format, @@ -348,8 +388,14 @@ score::crypto::Expected OpenSslCertParser return score::crypto::make_unexpected(Error::kOperationFailed); } - // 5. Extract nextUpdate (optional field — 0 when absent). - return Asn1TimeToEpoch(X509_CRL_get0_nextUpdate(crl.get())); + score::crypto::CrlMetadata metadata; + if (!Sha256CrlDigest(crl.get(), metadata.fingerprint) || + !Sha256CertificateDigest(issuer_x509.get(), metadata.issuer_fingerprint)) + return score::crypto::make_unexpected(Error::kInternalError); + metadata.this_update = Asn1TimeToEpoch(X509_CRL_get0_lastUpdate(crl.get())); + metadata.next_update = Asn1TimeToEpoch(X509_CRL_get0_nextUpdate(crl.get())); + metadata.crl_number = CrlNumber(crl.get()); + return metadata; } } // namespace score::crypto::daemon::provider::score_provider::openssl diff --git a/score/crypto/src/daemon/provider/score_provider/openssl/cert_management/openssl_cert_parser.hpp b/score/crypto/src/daemon/provider/score_provider/openssl/cert_management/openssl_cert_parser.hpp index 5bc6c6b7e..016e849fb 100644 --- a/score/crypto/src/daemon/provider/score_provider/openssl/cert_management/openssl_cert_parser.hpp +++ b/score/crypto/src/daemon/provider/score_provider/openssl/cert_management/openssl_cert_parser.hpp @@ -48,7 +48,7 @@ class OpenSslCertParser final : public cert_management::ICertParser common::DaemonErrorCode> ParseCertificates(const std::uint8_t* bytes, std::size_t size, score::crypto::FormatType format) override; - [[nodiscard]] score::crypto::Expected ValidateCrl( + [[nodiscard]] score::crypto::Expected ValidateCrl( const std::uint8_t* crl_data, std::size_t crl_size, score::crypto::FormatType crl_format, diff --git a/score/crypto/src/daemon/provider/tests/cert_management/test_openssl_cert_parser.cpp b/score/crypto/src/daemon/provider/tests/cert_management/test_openssl_cert_parser.cpp index 9b4469bce..2e1573deb 100644 --- a/score/crypto/src/daemon/provider/tests/cert_management/test_openssl_cert_parser.cpp +++ b/score/crypto/src/daemon/provider/tests/cert_management/test_openssl_cert_parser.cpp @@ -316,10 +316,11 @@ class ValidateCrlTest : public ::testing::Test } // Convenience wrapper — calls ValidateCrl with vector data. - static score::crypto::Expected Validate(const std::vector& crl, - FormatType crl_fmt, - const std::vector& issuer, - FormatType issuer_fmt) + static score::crypto::Expected Validate( + const std::vector& crl, + FormatType crl_fmt, + const std::vector& issuer, + FormatType issuer_fmt) { OpenSslCertParser parser{ProviderId{1U}}; return parser.ValidateCrl(crl.data(), crl.size(), crl_fmt, issuer.data(), issuer.size(), issuer_fmt); @@ -340,28 +341,28 @@ TEST_F(ValidateCrlTest, ValidDerCrl_DerIssuer_ReturnsNextUpdateEpoch) { const auto result = Validate(s_ca_a.crl_der, FormatType::kDer, s_ca_a.cert_der, FormatType::kDer); ASSERT_TRUE(result.has_value()); - EXPECT_GT(*result, 0); + EXPECT_GT(result->next_update, 0); } TEST_F(ValidateCrlTest, ValidPemCrl_PemIssuer_ReturnsNextUpdateEpoch) { const auto result = Validate(s_ca_a.crl_pem, FormatType::kPem, s_ca_a.cert_pem, FormatType::kPem); ASSERT_TRUE(result.has_value()); - EXPECT_GT(*result, 0); + EXPECT_GT(result->next_update, 0); } TEST_F(ValidateCrlTest, ValidDerCrl_PemIssuer_CrossFormat_ReturnsNextUpdateEpoch) { const auto result = Validate(s_ca_a.crl_der, FormatType::kDer, s_ca_a.cert_pem, FormatType::kPem); ASSERT_TRUE(result.has_value()); - EXPECT_GT(*result, 0); + EXPECT_GT(result->next_update, 0); } TEST_F(ValidateCrlTest, ValidPemCrl_DerIssuer_CrossFormat_ReturnsNextUpdateEpoch) { const auto result = Validate(s_ca_a.crl_pem, FormatType::kPem, s_ca_a.cert_der, FormatType::kDer); ASSERT_TRUE(result.has_value()); - EXPECT_GT(*result, 0); + EXPECT_GT(result->next_update, 0); } // nextUpdate was set to now + 365 days, so the epoch must be strictly in the future. @@ -370,7 +371,7 @@ TEST_F(ValidateCrlTest, ValidCrl_NextUpdateIsInFuture) const auto result = Validate(s_ca_a.crl_der, FormatType::kDer, s_ca_a.cert_der, FormatType::kDer); ASSERT_TRUE(result.has_value()); const std::int64_t now_epoch = static_cast(std::time(nullptr)); - EXPECT_GT(*result, now_epoch); + EXPECT_GT(result->next_update, now_epoch); } // DER and PEM paths must decode identically — same nextUpdate epoch. @@ -380,7 +381,10 @@ TEST_F(ValidateCrlTest, ValidCrl_DerAndPemReturnSameEpoch) const auto pem_result = Validate(s_ca_a.crl_pem, FormatType::kPem, s_ca_a.cert_pem, FormatType::kPem); ASSERT_TRUE(der_result.has_value()); ASSERT_TRUE(pem_result.has_value()); - EXPECT_EQ(*der_result, *pem_result); + EXPECT_EQ(der_result->next_update, pem_result->next_update); + EXPECT_EQ(der_result->this_update, pem_result->this_update); + EXPECT_EQ(der_result->fingerprint, pem_result->fingerprint); + EXPECT_EQ(der_result->issuer_fingerprint, pem_result->issuer_fingerprint); } // --------------------------------------------------------------------------- diff --git a/score/tests/test_vectors/certificate/basic/certificate.crl.der b/score/tests/test_vectors/certificate/basic/certificate.crl.der new file mode 100644 index 0000000000000000000000000000000000000000..d16d1da9e838423357cab28ca1bb326b73504333 GIT binary patch literal 437 zcmXqLV%%uZIE#sq(SVnYQ>)FR?K>|cBR4C9fuW(SfixR)C<`->NOEdXiEeIUUSfJ` zZfahMZb@o!iJ^c2A4r;uhut+fC$pe9Rh-wz%)rvX!pOkb#MCfKoYxq{HHC5wObv{g zL~={k{GF7wyL2X7ZU62w5xX~CR7pnDXTW2?1vFSzl!cj#kpazF%uS4p4D;Ik)tA1S zxhVI`OkeM5bJibHw=k7t@Gd-2apUI7>i7vC7tTNU!eaFS5vId4-+6A@Aobd3u?Am7 zeT`Jn|8g~pJxtG<%Y0Q{%~>dxa`xIJSFfdCmM@xrFwrtW@x%um&z-A7p2i3i@19wt ztN+D*L@AJa9Rw#d7f228tmH>2%JTd)4&bIqEM7p-UE-|}|Km7juV3SRm*CjH&C zpiTJB_GzwDOt$(=b5wo(W8#PH{pv#RJOr9!*Grn+UUPr)l0b8npm4FctfaE4nnw}= z8z+{|IVE$>E7nDA(l!}gfp4KR_FB_*;n@8JsUPeZ4 zRt5t@Ls*0AF)}iok-SjRT0XnPZm$0B9m1@)PdZOm*vNDERKuakC%1Rax~cYA z=audIMbrO(+~=FSG_vLY#677FkAiy+-FcnRaN^?c#ld0y&yBpIc8XgpZY-^v^V?#l z`-4AwCZ}AFGUvYkE%N4?f0w4)ByQciq%h^*H!kiArM*VtcAkq9ANi>K_Mf=lUtVy< zm4ki}DLTxuf=ha)sxO?&5aK>d_LiqMPe=2qe_Zmt>++H=_CMz6*}xcnRkG!ZjK_45 zBBuODi{m^U-!EIz`XP7HE9Z|~$*Hltg&M4S{?pdj3QaNkS@m9M_L};KEdMRezkBd> zJ(uvF%!Hij(){ZU+FxcksFW+c->)NG)X2ol$iTQbz`)Od2NGK`1j;mw5^oudu$*Nl2&GsFc51HnJBydzhG}_y;_g=DoRLAcUE=N^m6}%Fg<2=hd8CJjvwm8ae!n97L)XoK=Zr0y58OPr zG`|fy%deN%Wb^!YM)T|v!^=}^FIcAW7{vTKw3TI3X~z{1+v z9=)*1v}&J$h3vN<$sz71tvZC>m_FVvv!*0x##~oX9#NfBlT5?@{OhbU{(AD<*@e5< zem>Q;d9>^GhaEiYqYK{M5Zd{IS@rpc&a25gR~%dV`{wkji)NO0*ph#7q&T{~$i1E+ z*?Q#Fo|X&eLf-AIOnB#*=9oEynO$M;yR5QlydQP74vTiz1itb8vr^4#XPTer-O`(e cr}%b+N?WJzzVlW>b!UvM3db&aH4EEP0AN^8;Q#;t literal 0 HcmV?d00001 diff --git a/score/tests/test_vectors/certificate/basic/certificate_leaf.pem b/score/tests/test_vectors/certificate/basic/certificate_leaf.pem new file mode 100644 index 000000000..fc83a656f --- /dev/null +++ b/score/tests/test_vectors/certificate/basic/certificate_leaf.pem @@ -0,0 +1,78 @@ +Certificate: + Data: + Version: 3 (0x2) + Serial Number: + 6d:74:ac:fd:92:6a:bb:75:99:06:7d:8f:bb:cc:58:bb:b2:d1:24:63 + Signature Algorithm: sha256WithRSAEncryption + Issuer: CN = cert-management-test, O = Eclipse + Validity + Not Before: Sep 8 20:34:45 2026 GMT + Not After : Sep 5 20:34:45 2036 GMT + Subject: CN = cert-mgmt-leaf, O = Eclipse + Subject Public Key Info: + Public Key Algorithm: rsaEncryption + Public-Key: (2048 bit) + Modulus: + 00:cc:19:d0:78:85:77:9b:74:3e:9d:2f:bb:b8:13: + 05:db:c9:43:97:20:b1:0c:c3:ca:80:c2:93:c9:b7: + 8a:9a:d9:26:f3:2c:ea:3d:ef:a2:97:ff:f1:be:4d: + 6d:a5:59:84:ff:91:bc:65:40:e2:53:8c:c2:dc:eb: + 60:80:c8:d1:fb:a3:53:56:8f:e7:32:4a:5a:b9:17: + 38:a3:81:75:7e:9c:fb:38:b9:47:e0:fc:bc:93:64: + d7:5a:37:0b:df:f6:59:d9:ac:fe:d2:97:3c:61:b5: + bd:a4:71:64:fe:f6:0a:0b:d0:75:8d:32:17:3e:49: + a3:61:e2:4c:24:fb:4f:91:bf:4f:1f:11:a8:d4:c1: + 4e:58:64:2c:03:1d:11:a4:8c:95:27:a1:9d:00:54: + 47:9a:1d:da:49:2b:0c:88:83:ca:fe:0a:1f:8d:ae: + 6e:62:d1:8f:e3:08:8c:b0:01:57:d5:19:84:d4:1c: + 48:97:14:72:02:6f:e2:a3:5e:49:41:ef:a6:a4:85: + f0:6d:92:ea:43:f1:0a:63:65:5d:0d:71:28:05:2e: + 4f:96:ac:3d:12:94:32:f9:7a:ef:12:9b:ac:7f:e1: + 04:ff:38:cf:ee:e0:e5:af:0a:13:bc:69:60:6c:97: + 1b:0f:af:30:87:e9:68:40:24:77:22:ef:bf:2c:1b: + 72:81 + Exponent: 65537 (0x10001) + X509v3 extensions: + X509v3 Basic Constraints: critical + CA:FALSE + X509v3 Subject Key Identifier: + 42:C4:8D:ED:7D:81:52:07:0C:86:48:0F:BC:6D:38:86:A9:1E:8E:E3 + X509v3 Authority Key Identifier: + 91:1D:AF:FF:11:8D:65:7F:26:8C:4B:D3:96:C6:B1:1A:63:8A:35:78 + Signature Algorithm: sha256WithRSAEncryption + Signature Value: + 1d:87:18:dd:fc:24:7c:85:2d:a1:2a:c8:ce:48:67:57:57:05: + f2:ae:67:df:78:6b:f0:8a:d9:50:e7:01:84:29:e0:46:49:b4: + 83:ed:56:cd:0f:2e:61:82:3c:e7:fb:68:83:9b:74:31:d3:95: + 7d:d0:39:66:0c:30:5c:fa:c2:42:e8:b3:f7:16:c5:74:b6:81: + 80:df:a7:80:5e:b5:72:42:5d:20:04:05:85:87:5b:a1:82:35: + 7a:be:30:38:1d:f6:f8:63:54:47:c9:3a:88:12:ec:35:e3:b7: + 1c:ac:74:6c:98:9d:45:15:0c:15:2c:ca:92:35:56:fc:fe:89: + 7e:33:f5:c9:ce:cd:a1:ba:06:f9:e5:2d:3c:e2:ba:eb:f0:b8: + 0c:af:5b:70:ee:d8:12:b9:e8:03:25:e7:f0:89:d5:63:b9:a8: + c6:a5:fb:d9:97:7a:d1:36:39:dc:06:63:fa:08:64:41:44:e8: + 6d:d7:68:19:85:c4:ea:bc:84:d0:ce:54:ee:bd:79:60:ee:41: + 66:41:69:98:03:07:20:bd:ee:6a:76:96:0d:f1:2d:2a:c3:15: + 8b:7c:51:ec:4d:fc:a9:26:4a:b9:66:4e:15:dd:75:d9:31:ca: + 0e:b8:55:1b:3b:67:bb:dc:ed:18:25:b9:5c:1d:24:08:ba:1f: + 26:38:3d:75 +-----BEGIN CERTIFICATE----- +MIIDOjCCAiKgAwIBAgIUbXSs/ZJqu3WZBn2Pu8xYu7LRJGMwDQYJKoZIhvcNAQEL +BQAwMTEdMBsGA1UEAwwUY2VydC1tYW5hZ2VtZW50LXRlc3QxEDAOBgNVBAoMB0Vj +bGlwc2UwHhcNMjYwOTA4MjAzNDQ1WhcNMzYwOTA1MjAzNDQ1WjArMRcwFQYDVQQD +DA5jZXJ0LW1nbXQtbGVhZjEQMA4GA1UECgwHRWNsaXBzZTCCASIwDQYJKoZIhvcN +AQEBBQADggEPADCCAQoCggEBAMwZ0HiFd5t0Pp0vu7gTBdvJQ5cgsQzDyoDCk8m3 +iprZJvMs6j3vopf/8b5NbaVZhP+RvGVA4lOMwtzrYIDI0fujU1aP5zJKWrkXOKOB +dX6c+zi5R+D8vJNk11o3C9/2Wdms/tKXPGG1vaRxZP72CgvQdY0yFz5Jo2HiTCT7 +T5G/Tx8RqNTBTlhkLAMdEaSMlSehnQBUR5od2kkrDIiDyv4KH42ubmLRj+MIjLAB +V9UZhNQcSJcUcgJv4qNeSUHvpqSF8G2S6kPxCmNlXQ1xKAUuT5asPRKUMvl67xKb +rH/hBP84z+7g5a8KE7xpYGyXGw+vMIfpaEAkdyLvvywbcoECAwEAAaNQME4wDAYD +VR0TAQH/BAIwADAdBgNVHQ4EFgQUQsSN7X2BUgcMhkgPvG04hqkejuMwHwYDVR0j +BBgwFoAUkR2v/xGNZX8mjEvTlsaxGmOKNXgwDQYJKoZIhvcNAQELBQADggEBAB2H +GN38JHyFLaEqyM5IZ1dXBfKuZ994a/CK2VDnAYQp4EZJtIPtVs0PLmGCPOf7aIOb +dDHTlX3QOWYMMFz6wkLos/cWxXS2gYDfp4BetXJCXSAEBYWHW6GCNXq+MDgd9vhj +VEfJOogS7DXjtxysdGyYnUUVDBUsypI1Vvz+iX4z9cnOzaG6BvnlLTziuuvwuAyv +W3Du2BK56AMl5/CJ1WO5qMal+9mXetE2OdwGY/oIZEFE6G3XaBmFxOq8hNDOVO69 +eWDuQWZBaZgDByC97mp2lg3xLSrDFYt8UexN/KkmSrlmThXdddkxyg64VRs7Z7vc +7RgluVwdJAi6HyY4PXU= +-----END CERTIFICATE----- diff --git a/score/tests/test_vectors/certificate/basic/certificate_leaf_slot.kv b/score/tests/test_vectors/certificate/basic/certificate_leaf_slot.kv new file mode 100644 index 000000000..34c43b263 --- /dev/null +++ b/score/tests/test_vectors/certificate/basic/certificate_leaf_slot.kv @@ -0,0 +1,3 @@ +[certificate] +cert_path = score/tests/test_vectors/certificate/basic/certificate_leaf.pem +cert_format = pem diff --git a/score/tests/test_vectors/certificate/basic/manifest.json b/score/tests/test_vectors/certificate/basic/manifest.json index fb52a754a..3f0843f2c 100644 --- a/score/tests/test_vectors/certificate/basic/manifest.json +++ b/score/tests/test_vectors/certificate/basic/manifest.json @@ -14,7 +14,9 @@ "key_algorithm": "RSA-2048", "crl": { "_comment": "Run: python ../generate_certificates.py crl certificate --manifest manifest.json", - "revoked": ["certificate_leaf"] + "revoked": [ + "certificate_leaf" + ] }, "generated": { "not_before": "2026-08-24T22:20:18Z", @@ -24,7 +26,7 @@ }, { "name": "certificate_updated", - "purpose": "Rotated CA — used to test slot updates and trust-store anchor invalidation", + "purpose": "Rotated CA - used to test slot updates and trust-store anchor invalidation", "subject": "/CN=cert-management-updated/O=Eclipse", "key_algorithm": "RSA-2048", "generated": { @@ -40,16 +42,24 @@ "key_algorithm": "RSA-2048", "signed_by": "certificate", "is_ca": false, - "cert_formats": ["pem", "der"], + "cert_formats": [ + "pem", + "der" + ], "chain_file": true, "_usage": [ - "Step 1 — generate the CA first (if not already present):", + "Step 1 - generate the CA first (if not already present):", " python ../generate_certificates.py generate certificate --manifest manifest.json", - "Step 2 — generate the leaf (CSR signed by 'certificate'):", + "Step 2 - generate the leaf (CSR signed by 'certificate'):", " python ../generate_certificates.py generate certificate_leaf --manifest manifest.json", - "Step 3 — generate the CA CRL that revokes this leaf:", + "Step 3 - generate the CA CRL that revokes this leaf:", " python ../generate_certificates.py crl certificate --manifest manifest.json" - ] + ], + "generated": { + "not_before": "2026-09-08T20:34:45Z", + "not_after": "2036-09-05T20:34:45Z", + "sha256_fingerprint": "C86DF79F450B9014F0D1C4EA28028CC01E36D991C272F86B7C030F0FDEE3F321" + } } ] } diff --git a/score/tests/test_vectors/certificate/pki_chain/manifest.json b/score/tests/test_vectors/certificate/pki_chain/manifest.json index 282e33136..fff596ada 100644 --- a/score/tests/test_vectors/certificate/pki_chain/manifest.json +++ b/score/tests/test_vectors/certificate/pki_chain/manifest.json @@ -1,6 +1,6 @@ { "schema_version": 2, - "description": "Three-level PKI: root CA -> intermediate CA -> leaf. For chain-verification and OCSP tests. No certs committed yet — generate in order below.", + "description": "Three-level PKI: root CA -> intermediate CA -> leaf. Used by integration chain and CRL verification tests.", "defaults": { "cert_dir": ".", "key_dir": "private",