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 c5e3b4b4a..6aff07582 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) }; @@ -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/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/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 diff --git a/score/crypto/docs/architecture/key_management_class_diagram.puml b/score/crypto/docs/architecture/key_management_class_diagram.puml index 2d340d86f..c0f4f1c2f 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 { @@ -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 .. - .. opens with ios::trunc .. + .. delegates to file_io::WriteFile (atomic write via score::filesystem) .. } } 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** 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/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 new file mode 100644 index 000000000..c3047b02d --- /dev/null +++ b/score/crypto/src/api/certificate/BUILD @@ -0,0 +1,27 @@ +# ******************************************************************************* +# 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 = "ocsp_export", + hdrs = [ + "i_ocsp_request_export.hpp", + ], + includes = ["."], + visibility = ["//visibility:public"], + deps = [ + "@score_baselibs//score/language/futurecpp", + "@score_baselibs//score/result", + ], +) 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/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 ebeb4918c..000000000 --- a/score/crypto/src/api/common/types.hpp +++ /dev/null @@ -1,442 +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 - kVerificationTrustStore, ///< Named group of trusted CA certificates used for certificate chain - ///< verification. - kKey, ///< Key material (generated / loaded / derived / imported) - kCertificate, ///< Parsed or stored certificate object - kCrl, ///< Certificate Revocation List — shares the same numeric id - ///< as the issuer certificate resource (differentiated by type field) - kSecureObject, ///< Secure storage entry - kDataObject ///< Generic data blob -}; - -/// @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 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 Information about a certificate slot and its contents. -/// -/// Returned by ICertificateManagementContext::GetCertificateSlotInfo(). -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 -}; - -/// @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 5ff3b764a..6dbe92095 100644 --- a/score/crypto/src/api/config/BUILD +++ b/score/crypto/src/api/config/BUILD @@ -27,5 +27,22 @@ cc_library( visibility = ["//visibility:public"], deps = [ "//score/crypto/src/api/common:crypto_common", + "//score/crypto/src/api/types:types", + ], +) + +cc_library( + name = "cert_context_configs", + hdrs = [ + "certificate_context_config.hpp", + "certificate_verification_context_config.hpp", + "trust_store_management_context_config.hpp", + ], + includes = ["."], + visibility = ["//visibility:public"], + 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/future/config/certificate_context_config.hpp b/score/crypto/src/api/config/certificate_context_config.hpp similarity index 82% 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..a0b94b39c 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" @@ -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); @@ -67,4 +61,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 84% 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..0eac7dd5f 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,11 +11,12 @@ * 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" +#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); @@ -86,4 +81,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/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/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 6f905b7b3..8b4b6af7c 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", @@ -48,6 +50,28 @@ cc_library( ], ) +cc_library( + name = "cert_contexts", + hdrs = [ + "i_certificate_management_context.hpp", + "i_certificate_verification_context.hpp", + "i_trust_store_management_context.hpp", + ], + includes = ["."], + visibility = ["//visibility:public"], + deps = [ + ":context_bases", + "//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", + "@score_baselibs//score/result", + ], +) + cc_library( name = "crypto_contexts_impl", srcs = [ @@ -67,6 +91,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/future/contexts/i_certificate_management_context.hpp b/score/crypto/src/api/contexts/i_certificate_management_context.hpp similarity index 57% 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..2521e7b3f 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/crypto/src/api/types/certificate.hpp" +#include "score/crypto/src/api/types/common.hpp" #include "score/result/result.h" #include "score/span.hpp" @@ -26,6 +26,7 @@ #include #include #include +#include #include namespace score @@ -36,25 +37,21 @@ 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 -/// (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**, **CRL management**, and **trust store management** -/// are all 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 { @@ -74,37 +71,33 @@ 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. /// - /// @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, @@ -168,48 +161,11 @@ 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 occupancy, algorithm, and provider binding - 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; - // ---- 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 +179,64 @@ class ICertificateManagementContext : public IContext virtual score::Result> LoadCertificatePublicKey( const CryptoResourceId& cert) = 0; - /// @brief Constructs an OCSP request for a certificate's revocation status. + // ---- Persistence with CRL propagation ---- + + /// @brief Copies a certificate to a persistent slot and propagates its CRL. + /// + /// 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). /// - /// 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(). + /// No CRL re-validation occurs — the daemon reuses the CRL it already accepted. /// - /// @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 - virtual score::Result GetOcspRequestData(const CryptoResourceId& cert, - const CryptoResourceId& issuer_cert) = 0; + /// @param cert CryptoResourceId of the certificate to save (type = kCertificate or kCertSlot) + /// @param target_slot Handle to the target slot (type = kCertSlot) + virtual score::Result SaveCertificateWithCrl(const CryptoResourceId& cert, + const CryptoResourceId& target_slot) = 0; + + // ---- CRL management ---- + + /// @brief Imports a session-scoped CRL for a `kCertificate` resource. + /// + /// 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 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) = 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) + /// @return std::monostate on success, error if no CRL is present or access is denied + virtual score::Result DeleteCrl(const CryptoResourceId& cert_slot) = 0; + + // ---- 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; protected: ICertificateManagementContext() = default; @@ -243,4 +246,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/contexts/i_certificate_verification_context.hpp b/score/crypto/src/api/contexts/i_certificate_verification_context.hpp new file mode 100644 index 000000000..51d91dd89 --- /dev/null +++ b/score/crypto/src/api/contexts/i_certificate_verification_context.hpp @@ -0,0 +1,210 @@ +/******************************************************************************** + * 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_CERTIFICATE_VERIFICATION_CONTEXT_HPP +#define SCORE_CRYPTO_SRC_API_CONTEXTS_I_CERTIFICATE_VERIFICATION_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 + +namespace score +{ + +namespace crypto +{ + +/// @brief Builder-style context for certificate and chain verification. +/// +/// Created via ICryptoContext::CreateCertificateVerificationContext(). +/// Follows the same create-configure-execute pattern as other contexts +/// for uniformity. Configure the verification parameters via setter +/// methods, then call Verify() to execute. +/// +/// @par Example — single certificate verification +/// @code +/// auto ctx = crypto_context->CreateCertificateVerificationContext(config).value(); +/// ctx->SetCertificate(leaf_cert); +/// ctx->SetVerificationTrustStore(system_trust_store); +/// ctx->SetRevocationCheckPolicy(RevocationCheckPolicy::kCrlOnly); +/// auto result = ctx->Verify(); +/// @endcode +/// +/// @par Example — chain verification with additional untrusted certificates +/// @code +/// // ext_ca is a kCertificate from ParseCertificate() — not persisted. +/// std::array extra = {ext_ca.Id()}; +/// auto ctx = crypto_context->CreateCertificateVerificationContext(config).value(); +/// ctx->SetCertificateChain(chain); +/// ctx->SetVerificationTrustStore(system_trust_store); +/// ctx->SetAdditionalCertificates(extra); // untrusted chain-building inputs +/// auto result = ctx->Verify(); +/// @endcode +class ICertificateVerificationContext : public IContext +{ + public: + using Uptr = std::unique_ptr; + + ~ICertificateVerificationContext() override = default; + + ICertificateVerificationContext(const ICertificateVerificationContext&) = delete; + ICertificateVerificationContext& operator=(const ICertificateVerificationContext&) = delete; + ICertificateVerificationContext(ICertificateVerificationContext&&) = default; + ICertificateVerificationContext& operator=(ICertificateVerificationContext&&) = default; + + // ---- Configuration setters (call before Verify()) ---- + + /// @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 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 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. + /// + /// The trust store is a manifest-configured named group of persistent certificate + /// 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 = kCertificateTrustStore) + /// @return std::monostate on success, error if handle is invalid + virtual score::Result SetVerificationTrustStore(const CryptoResourceId& trust_store) = 0; + + /// @brief Sets explicit trusted certificates for this verification context. + /// + /// 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 configured-anchor verification. + /// + /// 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. + /// + /// 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 the additional certificates configured 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 ---- +#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 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 + 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 + /// @return std::monostate on success + /// @note Default: current system time. Use this for testing or for + /// verifying certificates at a specific point in time. + virtual score::Result SetVerificationTime(int64_t epoch_seconds) = 0; + + /// @brief Sets the revocation checking strategy. + /// @param policy The revocation check policy to apply + /// @return std::monostate on success + /// @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. + /// @return Verification result indicating validity or failure reason + /// @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. + /// @return Number of certificates, or an error before Verify(). + virtual score::Result GetVerifiedChainCertificateCount() const = 0; + + /// @brief Returns the encoded size of the verified chain. + /// + /// 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; +}; + +} // namespace crypto + +} // namespace score + +#endif // SCORE_CRYPTO_SRC_API_CONTEXTS_I_CERTIFICATE_VERIFICATION_CONTEXT_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/i_trust_store_management_context.hpp b/score/crypto/src/api/contexts/i_trust_store_management_context.hpp new file mode 100644 index 000000000..d9905d8d1 --- /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 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 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 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 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 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/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/certificate/BUILD b/score/crypto/src/api/future/certificate/BUILD index 877c2b40e..2cf8d569d 100644 --- a/score/crypto/src/api/future/certificate/BUILD +++ b/score/crypto/src/api/future/certificate/BUILD @@ -17,12 +17,11 @@ load("@rules_cc//cc:defs.bzl", "cc_library") + # 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/certificate/cert_types.hpp b/score/crypto/src/api/future/certificate/cert_types.hpp deleted file mode 100644 index 9d14deb7b..000000000 --- a/score/crypto/src/api/future/certificate/cert_types.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_API_FUTURE_CERTIFICATE_CERT_TYPES_HPP -#define SCORE_CRYPTO_SRC_API_FUTURE_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 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_FUTURE_CERTIFICATE_CERT_TYPES_HPP diff --git a/score/crypto/src/api/future/config/BUILD b/score/crypto/src/api/future/config/BUILD index bf631ee2c..d9b0d65c9 100644 --- a/score/crypto/src/api/future/config/BUILD +++ b/score/crypto/src/api/future/config/BUILD @@ -22,8 +22,6 @@ load("@rules_cc//cc:defs.bzl", "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/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/BUILD b/score/crypto/src/api/future/contexts/BUILD index b425f9caf..226c95608 100644 --- a/score/crypto/src/api/future/contexts/BUILD +++ b/score/crypto/src/api/future/contexts/BUILD @@ -62,18 +62,15 @@ load("@rules_cc//cc:defs.bzl", "cc_library") # ], # ) -# -- Certificate management + verification + CSR -- +# -- 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/contexts/i_certificate_verification_context.hpp b/score/crypto/src/api/future/contexts/i_certificate_verification_context.hpp deleted file mode 100644 index 6dea3ea90..000000000 --- a/score/crypto/src/api/future/contexts/i_certificate_verification_context.hpp +++ /dev/null @@ -1,155 +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_FUTURE_CONTEXTS_I_CERTIFICATE_VERIFICATION_CONTEXT_HPP -#define SCORE_CRYPTO_SRC_API_FUTURE_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/result/result.h" -#include "score/span.hpp" - -#include -#include - -namespace score -{ - -namespace crypto -{ - -/// @brief Builder-style context for certificate and chain verification. -/// -/// Created via ICryptoContext::CreateCertificateVerificationContext(). -/// Follows the same create-configure-execute pattern as other contexts -/// for uniformity. Configure the verification parameters via setter -/// methods, then call Verify() to execute. -/// -/// @par Example — single certificate verification -/// @code -/// 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); -/// auto result = ctx->Verify(); -/// @endcode -/// -/// @par Example — chain verification with additional trust anchors -/// @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 -/// auto result = ctx->Verify(); -/// @endcode -class ICertificateVerificationContext : public IContext -{ - public: - using Uptr = std::unique_ptr; - - ~ICertificateVerificationContext() override = default; - - ICertificateVerificationContext(const ICertificateVerificationContext&) = delete; - ICertificateVerificationContext& operator=(const ICertificateVerificationContext&) = delete; - ICertificateVerificationContext(ICertificateVerificationContext&&) = default; - ICertificateVerificationContext& operator=(ICertificateVerificationContext&&) = default; - - // ---- Configuration setters (call before Verify()) ---- - - /// @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(). - 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(). - virtual score::Result SetCertificateChain(score::cpp::span chain) = 0; - - /// @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. - /// Empty slots in the store are silently skipped at verification time. - /// - /// @param trust_store Handle to the verification trust store - /// (type = kVerificationTrustStore) - /// @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. - /// - /// 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. - /// - /// 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). - /// - /// Replaces any previously set additional anchors on this context (set semantics). - /// - /// @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; - - /// @brief Overrides the verification time. - /// @param epoch_seconds Verification time as seconds since Unix epoch - /// @return std::monostate on success - /// @note Default: current system time. Use this for testing or for - /// verifying certificates at a specific point in time. - virtual score::Result SetVerificationTime(int64_t epoch_seconds) = 0; - - /// @brief Sets the revocation checking strategy. - /// @param policy The revocation check policy to apply - /// @return std::monostate on success - /// @note Overrides the default policy set in the config. - virtual score::Result SetRevocationCheckPolicy(RevocationCheckPolicy policy) = 0; - - // ---- Execution ---- - - /// @brief Executes the configured certificate verification. - /// @return Verification result indicating validity or failure reason - /// @note At minimum, a certificate (or chain) and trust anchor must be set. - virtual score::Result Verify() = 0; - - protected: - ICertificateVerificationContext() = default; -}; - -} // namespace crypto - -} // namespace score - -#endif // SCORE_CRYPTO_SRC_API_FUTURE_CONTEXTS_I_CERTIFICATE_VERIFICATION_CONTEXT_HPP 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/future/objects/BUILD b/score/crypto/src/api/future/objects/BUILD index 2b0c6ec0f..b31ce8318 100644 --- a/score/crypto/src/api/future/objects/BUILD +++ b/score/crypto/src/api/future/objects/BUILD @@ -17,11 +17,10 @@ load("@rules_cc//cc:defs.bzl", "cc_library") + # 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/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/BUILD b/score/crypto/src/api/objects/BUILD index 0b45142c5..929d9898b 100644 --- a/score/crypto/src/api/objects/BUILD +++ b/score/crypto/src/api/objects/BUILD @@ -27,6 +27,25 @@ 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", + ], +) + +cc_library( + name = "cert_objects", + hdrs = [ + "i_cert_slot_object.hpp", + "i_certificate_object.hpp", + "i_trust_store_object.hpp", + ], + includes = ["."], + visibility = ["//visibility:public"], + 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/future/objects/i_cert_slot_object.hpp b/score/crypto/src/api/objects/i_cert_slot_object.hpp similarity index 83% 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..24aedd0da 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" @@ -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; }; @@ -51,4 +54,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 70% 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..4cf72d4ae 100644 --- a/score/crypto/src/api/future/objects/i_certificate_object.hpp +++ b/score/crypto/src/api/objects/i_certificate_object.hpp @@ -11,17 +11,20 @@ * 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/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 #include -#include +#include namespace score { @@ -31,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 @@ -76,9 +76,21 @@ 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 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. @@ -105,4 +117,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 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 new file mode 100644 index 000000000..6c49e2ffa --- /dev/null +++ b/score/crypto/src/api/objects/i_trust_store_object.hpp @@ -0,0 +1,88 @@ +/******************************************************************************** + * 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/objects/i_crypto_object.hpp" +#include "score/crypto/src/api/types/certificate.hpp" +#include "score/span.hpp" + +#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 +/// ITrustStoreManagementContext — not through this object. +/// +/// Obtained via ICryptoContext::GetTrustStoreObject(). +class ITrustStoreObject : public ICryptoObject +{ + public: + using Uptr = std::unique_ptr; + + ~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; + + // ---- 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]] 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]] virtual const MemberInfo* FindMemberByFingerprint( + score::cpp::span fingerprint) const noexcept = 0; + + /// @brief Returns the slot IDs of all enabled trust store members. + [[nodiscard]] virtual std::vector GetEnabledMemberSlotIds() const = 0; + + /// @brief Returns the slot IDs of all disabled trust store members. + [[nodiscard]] virtual std::vector GetDisabledMemberSlotIds() const = 0; + + protected: + ITrustStoreObject() = default; +}; + +} // namespace crypto + +} // namespace score + +#endif // SCORE_CRYPTO_SRC_API_OBJECTS_I_TRUST_STORE_OBJECT_HPP diff --git a/score/crypto/src/api/src/crypto_context_impl.cpp b/score/crypto/src/api/src/crypto_context_impl.cpp index 1afc8e301..14f5aa67b 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" @@ -128,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/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..566624b63 --- /dev/null +++ b/score/crypto/src/api/types/certificate.hpp @@ -0,0 +1,144 @@ +/******************************************************************************** + * 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 +#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, + 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 = kSha256FingerprintSize; + 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}; +}; + +/// 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 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/cert_management/BUILD b/score/crypto/src/daemon/cert_management/BUILD new file mode 100644 index 000000000..51829daf8 --- /dev/null +++ b/score/crypto/src/daemon/cert_management/BUILD @@ -0,0 +1,87 @@ +# ******************************************************************************* +# 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: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([ + "interfaces/*.hpp", + "core/*.hpp", + "nodes/*.hpp", + "policy/*.hpp", + "slot/*.hpp", + "truststore/*.hpp", + "cert_management_module.hpp", + ]), + includes = ["."], + 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", + "//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/cert_slot_manager.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..d0b597061 --- /dev/null +++ b/score/crypto/src/daemon/cert_management/cert_management_module.cpp @@ -0,0 +1,86 @@ +/******************************************************************************** + * 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/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" +#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); + auto slot_registry = std::make_shared(); + ConfigDrivenSlotCatalog catalog{config}; + 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(). + // 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); + }; + + auto slot_manager = std::make_shared(slot_registry, std::move(slot_handler_factory)); + + trust_catalog.Load(*trust_store_manager, slot_registry, slot_manager); + module->m_service = std::make_shared( + 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) { + 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..c5d559976 --- /dev/null +++ b/score/crypto/src/daemon/cert_management/cert_management_module.hpp @@ -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 + ********************************************************************************/ +#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/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&); + CertManagementService::Sptr GetService() const + { + return m_service; + } + provider::ProviderManager::Sptr GetProviderManager() const + { + return m_provider_manager; + } + + private: + CertManagementModule() = default; + 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..2e5decc46 --- /dev/null +++ b/score/crypto/src/daemon/cert_management/core/cert_entry.hpp @@ -0,0 +1,150 @@ +/******************************************************************************** + * 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/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" +#include "score/crypto/src/daemon/data_manager/data_node.hpp" + +#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. + /// @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} + { + } + + ~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; + } + + // ----------------------------------------------------------------------- + // 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() 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, + 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(). + [[nodiscard]] bool HasSessionCrl() const + { + 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 + { + const std::lock_guard lock(m_ref_mutex); + 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(); + } + + // ----------------------------------------------------------------------- + // Ownership + // ----------------------------------------------------------------------- + + [[nodiscard]] data_manager::ClientId GetOwner() const noexcept + { + return m_owner_client_id; + } + + [[nodiscard]] bool IsOwnedBy(data_manager::ClientId client_id) const noexcept + { + 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::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 + +#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..b826a1b37 --- /dev/null +++ b/score/crypto/src/daemon/cert_management/core/cert_management_service.cpp @@ -0,0 +1,313 @@ +/******************************************************************************** + * 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, + CertSlotManager::Sptr slot_manager) + : m_data_manager{std::move(data_manager)}, + m_slot_registry{std::move(slot_registry)}, + m_trust_store_manager{std::move(trust_store_manager)}, + m_slot_manager{std::move(slot_manager)} +{ +} + +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, params.client_id); + auto id = params.slot_handle.IsValid() ? m_cert_registry.RegisterSlotCert(params.slot_handle, entry) + : m_cert_registry.RegisterEphemeralCert(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(entry)}; +} + +score::crypto::Expected CertManagementService::Load(const CertRegistrationParams& params) +{ + 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)); +} + +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_store_manager) + m_trust_store_manager->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); + + auto handle_res = m_slot_registry->ResolveAppResource(resource_name, client_id); + if (!handle_res.has_value()) + return score::crypto::make_unexpected(handle_res.error()); + + 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[slot_handle.index] = 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_store_manager) + return score::crypto::make_unexpected(Error::kInternalError); + + 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(handle_res.value().index); + if (it != client_cache.end()) + return it->second; + + 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[handle_res.value().index] = 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()); + + return ResolvedCertSlot{handle, config_res.value()}; +} + +score::crypto::Expected CertManagementService::ResolveCertForOperation( + data_manager::ClientId client_id, + data_manager::DataNodeId cert_node_id) +{ + 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( + 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, 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) +{ + 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_store_manager) + return; + const auto memberships = m_trust_store_manager->GetMembershipsForSlot(slot_handle); + for (const auto ts_handle : memberships) + 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 new file mode 100644 index 000000000..99e3804ba --- /dev/null +++ b/score/crypto/src/daemon/cert_management/core/cert_management_service.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_CORE_CERT_MANAGEMENT_SERVICE_HPP +#define SCORE_CRYPTO_SRC_DAEMON_CERT_MANAGEMENT_CORE_CERT_MANAGEMENT_SERVICE_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" +#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 +#include + +namespace score::crypto::daemon::cert_management +{ +struct CertDataNodeResult +{ + data_manager::DataNodeId node_id{}; + std::shared_ptr entry; +}; + +/// 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}; +}; + +/// 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: + using Sptr = std::shared_ptr; + + CertManagementService(data_manager::IDataManager::Sptr data_manager, + CertSlotRegistry::Sptr slot_registry, + TrustStoreManager::Sptr trust_store_manager, + CertSlotManager::Sptr slot_manager = {}); + + // ----------------------------------------------------------------------- + // Existing cert lifecycle methods + // ----------------------------------------------------------------------- + + score::crypto::Expected RegisterCertMaterial( + const CertRegistrationParams&, + CertObject::Sptr); + /// 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); + + TrustStoreManager::Sptr GetTrustStoreManager() const + { + return m_trust_store_manager; + } + + CertSlotManager::Sptr GetSlotManager() const + { + return m_slot_manager; + } + + // ----------------------------------------------------------------------- + // 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); + + 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. + 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 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 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. + score::crypto::Expected, common::DaemonErrorCode> ResolveCertEntryForOperation( + 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, + 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_store_manager; + CertSlotManager::Sptr m_slot_manager; + CertRegistry m_cert_registry; + + // 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; +}; +} // 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..447115225 --- /dev/null +++ b/score/crypto/src/daemon/cert_management/core/cert_registry.cpp @@ -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 + ********************************************************************************/ + +#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); + const CertRegistryId id = m_next_id++; + m_certs.emplace(id, std::move(cert_entry)); + 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::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; + } + 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 (const auto& [id, cert_entry] : m_certs) + { + 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 << " owned by client " + << client_id; + 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..2cce2734e --- /dev/null +++ b/score/crypto/src/daemon/cert_management/core/cert_registry.hpp @@ -0,0 +1,100 @@ +/******************************************************************************** + * 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. + /// + /// 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. + [[nodiscard]] CertRegistryId RegisterEphemeralCert(std::shared_ptr cert_entry); + + // ------------------------------------------------------------------ + // Lookup + // ------------------------------------------------------------------ + + [[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; + 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/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..f1d3975e6 --- /dev/null +++ b/score/crypto/src/daemon/cert_management/docs/architecture/cert_management_static.puml @@ -0,0 +1,146 @@ +' ******************************************************************************* +' 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 cert_management_static +top to bottom direction +skinparam componentStyle rectangle +skinparam packageStyle rectangle + +package "cert_management" { + package "core" { + [CertManagementService] + [CertRegistry] + [CertEntry] + } + package "interfaces" { + interface ICertSlotHandler + interface ITrustStoreHandler + interface ICertParser + [CertObject] + } + package "nodes" { + [CertSlotDataNode] + [CertDataNode] + [TrustStoreDataNode] + } + package "slot" { + [CertSlotManager] + [CertSlotRegistry] + [ConfigDrivenSlotCatalog] + [FileBackedSlotHandler] + [Pkcs11CertSlotHandler] + [CrlHandler] + } + package "truststore" { + [TrustStoreManager] + [TrustStoreHandler] + [ConfigDrivenTrustStoreCatalog] + } + package "policy" { + [AccessPolicyEnforcer] + } + package "query" { + [CertObjectSerializer] + } +} + +package "daemon/common" { + [DataManager] + [Shared Deployment Storage] +} + +package "api/types" { + [common.hpp] + [certificate.hpp] + [key.hpp] +} + +package "provider" { + [OpenSslCertParser] + [Certificate Management Handler] + [Certificate Management Executor] + [Trust-Store Management Handler] + [Trust-Store Management Executor] + [Certificate Verification Handler] +} + +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] +[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] --> [CertSlotManager] : anchor loading +[TrustStoreHandler] ..|> ITrustStoreHandler +[FileBackedSlotHandler] ..|> ICertSlotHandler +[FileBackedSlotHandler] --> [CrlHandler] +[FileBackedSlotHandler] ---> ICertParser +[Shared Deployment Storage] <-- [FileBackedSlotHandler] +[Pkcs11CertSlotHandler] ..|> ICertSlotHandler +[Pkcs11CertSlotHandler] --> [CrlHandler] +[OpenSslCertParser] ..|> ICertParser +[OpenSslCertParser] --> [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] +[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 new file mode 100644 index 000000000..495250200 --- /dev/null +++ b/score/crypto/src/daemon/cert_management/docs/architecture/component_architecture.rst @@ -0,0 +1,193 @@ +.. + # ******************************************************************************* + # 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 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 + (``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. +* 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 + +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 +per-client ``CertEntry``. Each ``Load`` call produces an independent +``CertEntry`` so that per-client state — such as a session-scoped CRL +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. +Trust-store anchor content is loaded lazily and cached separately 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. +* 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. + +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 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 +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 +---------------- + +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 +-------------------- + +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 new file mode 100644 index 000000000..6c00f374a --- /dev/null +++ b/score/crypto/src/daemon/cert_management/docs/architecture/design_decisions.rst @@ -0,0 +1,483 @@ +.. + # ******************************************************************************* + # 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. Conditional-external members are + disabled and persisted until ``AcknowledgeMemberUpdate`` records the new + fingerprint. + +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``, +``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`` +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 metadata is available without deserialising the CRL during normal reads. + +**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, ...)``. +* 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. + +--- + +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``. +* ``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. + +--- + +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 new file mode 100644 index 000000000..42812e250 --- /dev/null +++ b/score/crypto/src/daemon/cert_management/docs/architecture/index.rst @@ -0,0 +1,24 @@ +.. + # ******************************************************************************* + # ******************************************************************************* + # 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_architecture: + +Architecture +============ + +.. toctree:: + + component_architecture + design_decisions 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 new file mode 100644 index 000000000..96e876080 --- /dev/null +++ b/score/crypto/src/daemon/cert_management/docs/detailed_design/cert_management_dynamic.puml @@ -0,0 +1,168 @@ +' ******************************************************************************* +' 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 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 "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 +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 + +== 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 --> 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) + +== 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) +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 -> 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 + +== 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) +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() +SlotMgr -> Handler : LoadCertificate() +Handler -> Parser : ParseCertificate() +Parser --> Handler : 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 new file mode 100644 index 000000000..3986ec300 --- /dev/null +++ b/score/crypto/src/daemon/cert_management/docs/detailed_design/detailed_design.rst @@ -0,0 +1,127 @@ +.. + # ******************************************************************************* + # 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 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. 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`` + 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. + +``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 +----------------------- + +* ``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. +* ``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 +---------------- + +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. 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 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/detailed_design/index.rst b/score/crypto/src/daemon/cert_management/docs/detailed_design/index.rst new file mode 100644 index 000000000..6381f9eb2 --- /dev/null +++ b/score/crypto/src/daemon/cert_management/docs/detailed_design/index.rst @@ -0,0 +1,22 @@ +.. + # ******************************************************************************* + # 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_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..51281a35c --- /dev/null +++ b/score/crypto/src/daemon/cert_management/docs/index.rst @@ -0,0 +1,143 @@ +.. + # ******************************************************************************* + # 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 +================================ + +.. 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 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 + 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. + +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, and are dispatched through the ``CERT:TRUST_STORE`` context + handler. + +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..433992fd6 --- /dev/null +++ b/score/crypto/src/daemon/cert_management/docs/requirements/index.rst @@ -0,0 +1,22 @@ +.. + # ******************************************************************************* + # 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_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..1f4bb59ef --- /dev/null +++ b/score/crypto/src/daemon/cert_management/docs/requirements/requirements.rst @@ -0,0 +1,111 @@ +.. + # ******************************************************************************* + # 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 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 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..2cc5be4f1 --- /dev/null +++ b/score/crypto/src/daemon/cert_management/interfaces/access_policy.hpp @@ -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 + ********************************************************************************/ + +#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 +{ + /// 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). + /// + /// Mutation authorization is explicit and default-deny: an empty list + /// 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; +}; + +} // 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..fcaf02544 --- /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/types/common.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..d8a5976de --- /dev/null +++ b/score/crypto/src/daemon/cert_management/interfaces/cert_types.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_INTERFACES_CERT_TYPES_HPP +#define SCORE_CRYPTO_SRC_DAEMON_CERT_MANAGEMENT_INTERFACES_CERT_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" + +#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; + + /// 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; + + /// 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}; +}; + +/// 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 +// --------------------------------------------------------------------------- + +/// 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"; + +/// 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. +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"; + +/// 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". +/// 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..35d34b2e0 --- /dev/null +++ b/score/crypto/src/daemon/cert_management/interfaces/i_cert_slot_handler.cpp @@ -0,0 +1,69 @@ +/******************************************************************************** + * 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*/, + std::optional /*metadata*/) +{ + 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); +} + +score::crypto::FormatType ICertSlotHandler::GetCrlFormat(const CertSlotConfig& /*slot*/) +{ + 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 new file mode 100644 index 000000000..d73652b80 --- /dev/null +++ b/score/crypto/src/daemon/cert_management/interfaces/i_cert_slot_handler.hpp @@ -0,0 +1,205 @@ +/******************************************************************************** + * 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/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" +#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 +#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; + + /// 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 score::crypto::Expected 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 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 + /// 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, + std::optional metadata = std::nullopt); + + /// 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); + + /// 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); + + /// 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. +/// +/// 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..d38b0dd28 --- /dev/null +++ b/score/crypto/src/daemon/cert_management/interfaces/i_trust_store_handler.hpp @@ -0,0 +1,162 @@ +/******************************************************************************** + * 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 +#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). + /// + /// 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; + + // ----------------------------------------------------------------------- + // 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; + + // ----------------------------------------------------------------------- + // 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 + // ----------------------------------------------------------------------- + + /// 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..b53eb34bb --- /dev/null +++ b/score/crypto/src/daemon/cert_management/nodes/cert_data_node.hpp @@ -0,0 +1,90 @@ +/******************************************************************************** + * 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 unregister_callback) + : DataNode(false), + m_cert_entry{std::move(cert_entry)}, + m_registry_id{registry_id}, + m_client_id{client_id}, + m_unregister_callback{std::move(unregister_callback)} + { + } + + ~CertDataNode() override + { + if (m_cert_entry != nullptr && m_unregister_callback) + { + m_unregister_callback(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_unregister_callback; +}; + +} // 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..efeebddd6 --- /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) : 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/query/cert_object_serializer.cpp b/score/crypto/src/daemon/cert_management/query/cert_object_serializer.cpp new file mode 100644 index 000000000..1ceec184c --- /dev/null +++ b/score/crypto/src/daemon/cert_management/query/cert_object_serializer.cpp @@ -0,0 +1,94 @@ +/******************************************************************************** + * 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, + std::optional crl_metadata) +{ + 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(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()}); + 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; +} + +score::crypto::Expected +SerializeCertSlotInfo(CertSlotManager& mgr, CertSlotHandle slot, data_manager::ClientId client_id) +{ + 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; + common::ResponseParameters out; + out.push_back(static_cast(info_res.value().state)); + out.push_back(static_cast(has_crl ? 1U : 0U)); + 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_handle, 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(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..064449a74 --- /dev/null +++ b/score/crypto/src/daemon/cert_management/query/cert_object_serializer.hpp @@ -0,0 +1,80 @@ +/******************************************************************************** + * 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/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 +#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 (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), +/// [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 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 (2 params): +/// [0] slot state (uint8, CertificateSlotState), [1] has_crl (uint8) +/// +/// 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); + +/// @brief Serialize trust store member snapshot into IPC response parameters. +/// +/// 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), +/// [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/slot/cert_slot_manager.cpp b/score/crypto/src/daemon/cert_management/slot/cert_slot_manager.cpp new file mode 100644 index 000000000..b517a24a4 --- /dev/null +++ b/score/crypto/src/daemon/cert_management/slot/cert_slot_manager.cpp @@ -0,0 +1,408 @@ +/******************************************************************************** + * 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. 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{}; +} + +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); +} + +score::crypto::Expected CertSlotManager::HasCrl(CertSlotHandle slot) +{ + const auto cfg_res = GetConfig(slot); + if (!cfg_res.has_value()) + return score::crypto::make_unexpected(cfg_res.error()); + + ICertSlotHandler::Sptr handler; + { + std::lock_guard lock(m_mutex); + handler = GetOrCreate(slot); + } + if (!handler) + return score::crypto::make_unexpected(Error::kInternalError); + 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()); +} + +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) +{ + 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::optional metadata) +{ + 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, std::move(metadata)); +} + +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..e81e249d2 --- /dev/null +++ b/score/crypto/src/daemon/cert_management/slot/cert_slot_manager.hpp @@ -0,0 +1,186 @@ +/******************************************************************************** + * 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/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" +#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); + + [[nodiscard]] score::crypto::Expected 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]] std::optional GetCrlMetadata(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::optional metadata = std::nullopt); + + [[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/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..aab1225c5 --- /dev/null +++ b/score/crypto/src/daemon/cert_management/slot/crl_handler.cpp @@ -0,0 +1,192 @@ +/******************************************************************************** + * 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/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/hex.hpp" +#include "score/crypto/src/daemon/common/storage/file_io.hpp" + +#include +#include +#include + +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"; +} + +score::crypto::Expected CrlHandler::HasCrl(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"); + 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 +{ + 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, + std::optional metadata) +{ + 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->RemoveSection("crl"); + descriptor->Set("crl", "crl_path", path); + descriptor->Set("crl", "crl_format", FormatName(format)); + 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); +} + +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); + 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 +{ + 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; +} + +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 new file mode 100644 index 000000000..246f0fcce --- /dev/null +++ b/score/crypto/src/daemon/cert_management/slot/crl_handler.hpp @@ -0,0 +1,92 @@ +/******************************************************************************** + * 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_CRL_HANDLER_HPP +#define SCORE_CRYPTO_SRC_DAEMON_CERT_MANAGEMENT_SLOT_CRL_HANDLER_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 + +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; + + /// 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. + [[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. + /// 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::optional metadata = std::nullopt); + + /// 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; + + /// 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; + + /// 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); +}; + +} // 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..d6b4c4a60 --- /dev/null +++ b/score/crypto/src/daemon/cert_management/slot/file_backed_slot_handler.cpp @@ -0,0 +1,229 @@ +/******************************************************************************** + * 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"); + 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( + const CertSlotConfig& slot) +{ + auto state = GetSlotState(slot); + if (!state) + return score::crypto::make_unexpected(state.error()); + score::crypto::CertificateSlotInfo info{}; + info.state = *state; + 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; +} + +score::crypto::Expected 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; + // Clear any stale CRL: it was issued for the previous CA key and is no longer + // 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"); + 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())); + 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()) + { + 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()) + { + 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); + 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, + std::optional metadata) +{ + return m_crl.StoreCrl(slot, data, format, std::move(metadata)); +} + +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); +} + +score::crypto::FormatType FileBackedSlotHandler::GetCrlFormat(const CertSlotConfig& slot) +{ + 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 new file mode 100644 index 000000000..7f1dc7a80 --- /dev/null +++ b/score/crypto/src/daemon/cert_management/slot/file_backed_slot_handler.hpp @@ -0,0 +1,65 @@ +/******************************************************************************** + * 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_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; + score::crypto::Expected 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, + 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; + 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..87796fa15 --- /dev/null +++ b/score/crypto/src/daemon/cert_management/slot/slot_registry.cpp @@ -0,0 +1,141 @@ +/******************************************************************************** + * 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 +{ + // RegisterSlot is called only at startup (ConfigDrivenSlotCatalog::Populate), + // before any concurrent executor threads start. m_name_index and m_registry are + // read-only at runtime, so no lock is needed here. + 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); + 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{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; +} + +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..3ff109200 --- /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 registered slots. + 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..334dafc19 --- /dev/null +++ b/score/crypto/src/daemon/cert_management/tests/BUILD @@ -0,0 +1,123 @@ +# ******************************************************************************* +# Copyright (c) 2026 Contributors to the Eclipse Foundation +# SPDX-License-Identifier: Apache-2.0 +# ******************************************************************************* + +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", + "//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"], + 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", "test_environment.hpp"], + 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", "test_environment.hpp"], + 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", "test_environment.hpp"], + data = [ + "//score/tests/config:logging.json", + "//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", "test_environment.hpp"], + 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..05cca0344 --- /dev/null +++ b/score/crypto/src/daemon/cert_management/tests/core/test_access_policy_enforcer.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/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..bb4237554 --- /dev/null +++ b/score/crypto/src/daemon/cert_management/tests/core/test_cert_registry.cpp @@ -0,0 +1,130 @@ +/******************************************************************************** + * 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" + +#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)); +} + +// 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 cert_obj = MakeCertificate(); + auto first = std::make_shared(cert_obj, slot); + auto second = std::make_shared(cert_obj, slot); + + 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. +// 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(), 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); + 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 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; + static constexpr ClientId kClientB = 20U; + + CertRegistry registry; + const CertSlotHandle slot{3U}; + + 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); + ASSERT_NE(registry.RegisterEphemeralCert(ephemeral_entry), 0U); + + registry.CleanupClient(kClientB); + + 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 new file mode 100644 index 000000000..01da1abf8 --- /dev/null +++ b/score/crypto/src/daemon/cert_management/tests/integration/test_certificate_management_integration.cpp @@ -0,0 +1,384 @@ +/******************************************************************************** + * 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" +#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" + +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +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: + void SetUp() override + { + 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"; + 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/basic/certificate.pem", + m_certificate_path, + std::filesystem::copy_options::overwrite_existing)); + 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)); + + 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/basic/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(); + 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 + { + 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::CertSlotManager::Sptr m_slot_manager; + 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); + 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, *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())); +} +// =========================================================================== +// 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); +} + +// 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) +{ + 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 new file mode 100644 index 000000000..68c1bfd47 --- /dev/null +++ b/score/crypto/src/daemon/cert_management/tests/query/test_cert_object_serializer.cpp @@ -0,0 +1,417 @@ +/******************************************************************************* + * 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 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 +// (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/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 + +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 slot state and CRL metadata +// --------------------------------------------------------------------------- + +struct SlotHandlerStub : public cert::ICertSlotHandler +{ + mutable score::crypto::Expected + slot_info_result{score::crypto::CertificateSlotInfo{score::crypto::CertificateSlotState::kOccupied}}; + + 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; + } + + score::crypto::Expected HasCrl( + const cert::CertSlotConfig&) override + { + return slot_info_result.has_value() && slot_info_result->has_crl; + } + + 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, + std::optional) 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, ProducesFifteenParameters) +{ + const auto cert = MakeSyntheticCert(); + EXPECT_EQ(query::SerializeCertObject(cert).size(), 15U); +} + +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, 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; + 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 +// +// 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. +// =========================================================================== + +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, ProducesTwoParameters) +{ + const auto result = query::SerializeCertSlotInfo(*mgr_, slot_handle_, kClientId); + ASSERT_TRUE(result.has_value()); + EXPECT_EQ(result.value().size(), 2U); +} + +TEST_F(SerializeCertSlotInfoTest, Param0_SlotStateUint8_Occupied) +{ + 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_F(SerializeCertSlotInfoTest, Param0_SlotStateUint8_Empty) +{ + 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_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); + ASSERT_NE(has_crl, nullptr); + EXPECT_EQ(*has_crl, 0U); +} + +TEST_F(SerializeCertSlotInfoTest, CrlPresent_Param1IsOne) +{ + stub_->slot_info_result->has_crl = true; + const auto result = query::SerializeCertSlotInfo(*mgr_, slot_handle_, kClientId); + ASSERT_TRUE(result.has_value()); + + const auto* has_crl = GetParam(result.value(), 1U); + ASSERT_NE(has_crl, nullptr); + EXPECT_EQ(*has_crl, 1U); +} + +TEST_F(SerializeCertSlotInfoTest, GetSlotInfoError_PropagatesError) +{ + stub_->slot_info_result = + score::crypto::make_unexpected(score::crypto::daemon::common::DaemonErrorCode::kInternalError); + const auto result = query::SerializeCertSlotInfo(*mgr_, slot_handle_, kClientId); + 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 new file mode 100644 index 000000000..6833e33b6 --- /dev/null +++ b/score/crypto/src/daemon/cert_management/tests/service/test_cert_management_service.cpp @@ -0,0 +1,661 @@ +/******************************************************************************* + * 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. +// +// 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/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 +// parser tests. +// +// Test subjects: +// - Slot resolution and cert load via the executor call sequence +// (ResolveCertSlot → ResolveSlotForOperation → Load) +// - ResolveCertForOperation round-trip +// - Cert node release and subsequent unresolvability +// - 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" +#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 + { + 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( + 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/basic/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"; + // 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}); + + m_trust_store_manager = std::make_shared(); + m_data_manager = std::make_shared(); + + 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 + { + 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); + } + + // Mirror the executor call sequence: resolve the slot node, then load the cert. + 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->Load(params); + 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()); +} + +// --------------------------------------------------------------------------- +// 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, Load_SecondCallProducesIndependentCertEntry) +{ + 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); + + // Independent CertEntry objects — session CRL on one cannot affect the other. + EXPECT_NE(result1.node_id, result2.node_id); + 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()); +} + +// --------------------------------------------------------------------------- +// 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_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()); +} + +// --------------------------------------------------------------------------- +// 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}); + + 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); + 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()); +} + +// --------------------------------------------------------------------------- +// 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, SessionCrl_StoresInMemoryNotOnDisk) +{ + const auto result = LoadCert(kClientA); + ASSERT_NE(result.node_id, 0U); + EXPECT_FALSE(result.entry->HasSessionCrl()); + + const std::vector crl_bytes{0xC0U, 0xC1U, 0xC2U}; + 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); + + 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); + + EXPECT_FALSE(std::filesystem::exists(m_dir / "root_ca.crl")); +} + +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 new file mode 100644 index 000000000..d26fb232b --- /dev/null +++ b/score/crypto/src/daemon/cert_management/tests/slot/test_file_backed_slot_handler.cpp @@ -0,0 +1,549 @@ +/******************************************************************************** + * 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/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" + +#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}; + } + + 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 score::crypto::CrlMetadata{}; + } +}; + +class FileBackedSlotHandlerTest : public ::testing::Test +{ + protected: + void SetUp() override + { + 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"; + 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()); + 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()); + 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 +// 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).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()); + + 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 + // 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()); + ASSERT_TRUE(m_handler->HasCrl(m_slot).has_value()); + EXPECT_TRUE(m_handler->HasCrl(m_slot).value()); + 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 +// detection doesn't go unnoticed. +TEST_F(FileBackedSlotHandlerTest, HasCrl_FalseForFreshSlot) +{ + 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. +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()); + ASSERT_TRUE(m_handler->HasCrl(m_slot).has_value()); + EXPECT_FALSE(m_handler->HasCrl(m_slot).value()); +} + +// 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) + .has_value()); + + const auto nu = m_handler->GetCrlNextUpdate(m_slot); + EXPECT_FALSE(nu.has_value()); + EXPECT_EQ(nu.error(), Error::kResourceNotAllocated); +} + +// 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, + metadata) + .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. +// --------------------------------------------------------------------------- + +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 = 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); + std::filesystem::create_directories(m_dir); + + ASSERT_TRUE(std::filesystem::copy_file("score/tests/test_vectors/certificate/basic/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()); +} + +// --------------------------------------------------------------------------- +// 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/basic/certificate.pem", + "CN=cert-management-test,O=Eclipse"}, + AlgorithmVarietyParam{"score/tests/test_vectors/certificate/algorithm_variety/rsa_3072.pem", + "CN=cert-mgmt-rsa-3072,O=Eclipse"}, + 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/algorithm_variety/ec_p256.pem", + "CN=cert-mgmt-ec-p256,O=Eclipse"}, + AlgorithmVarietyParam{"score/tests/test_vectors/certificate/algorithm_variety/ec_p384.pem", + "CN=cert-mgmt-ec-p384,O=Eclipse"}, + 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/algorithm_variety/ed25519.pem", + "CN=cert-mgmt-ed25519,O=Eclipse"}, + 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/algorithm_variety/ml_dsa_44.pem", + "CN=cert-mgmt-ml-dsa-44,O=Eclipse"}, + 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/algorithm_variety/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 new file mode 100644 index 000000000..dad27276f --- /dev/null +++ b/score/crypto/src/daemon/cert_management/tests/truststore/test_trust_store_manager.cpp @@ -0,0 +1,1030 @@ +/******************************************************************************* + * 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. +// +// 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/basic/ 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 +// - 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" +#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" + +#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 = cert::test::TempDirectory("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/basic/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::CertSlotManager::Sptr MakeSlotManager(cert::CertSlotRegistry::Sptr registry) + { + auto parser = m_parser; + 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.) + // 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, MakeSlotManager(registry)); + + // Membership index is built at Load() time without touching cert bytes. + ASSERT_EQ(manager.GetMembershipsForSlot(slot).size(), 1U); + EXPECT_EQ(manager.GetMembershipsForSlot(slot)[0], cert::TrustStoreHandle{0U}); + + // Anchors are loaded lazily on the first GetAnchors() call. + auto store = manager.GetStore(cert::TrustStoreHandle{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(cert::TrustStoreHandle{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, MakeSlotManager(registry)); + + 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 + // 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); + } + + 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/basic/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, MakeSlotManager(registry)); + + 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. + 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/basic/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, MakeSlotManager(registry)); + + const auto ts_handle = manager.ResolveByName("tls-roots"); + auto store = manager.GetStore(ts_handle); + 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_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_handle, 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, MakeSlotManager(registry)); + + // Both stores report membership for the shared slot. + EXPECT_EQ(manager.GetMembershipsForSlot(slot).size(), 2U); + + 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); + 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, MakeSlotManager(registry)); + + 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. + 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_handle, 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, MakeSlotManager(registry)); + + 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. + 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_handle, 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.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)); + + const auto ts_handle = manager.ResolveByName("cond-store"); + auto store = manager.GetStore(ts_handle); + 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/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_handle, 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_handle, 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); +} + +// --------------------------------------------------------------------------- +// ConditionalExternal members require acknowledgment after a slot update. +// +// 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(); + 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"; + 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}); + + auto slot_manager = MakeSlotManager(registry); + cert::TrustStoreManager manager; + manager.Load({ts_cfg}, registry, slot_manager); + + 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: 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 3: The direct slot update disables the conditional member until it is acknowledged. + { + auto after_update = store->GetAnchors(); + ASSERT_TRUE(after_update.has_value()); + EXPECT_EQ(after_update->size(), 0U); + } + + // Step 4: Acknowledge the update and load the replacement certificate. + ASSERT_TRUE(manager.AcknowledgeMemberUpdate(ts_handle, slot, kClientA).has_value()); + + // 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()); + 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. +// --------------------------------------------------------------------------- + +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, MakeSlotManager(registry)); + + const auto ts_handle = 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_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}; + const auto cfg = MakeSlotConfig("empty-anchor", m_empty_slot_kv); + ASSERT_TRUE(fresh_handler.LoadCertificate(cfg).has_value()); + 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); + 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, MakeSlotManager(registry)); + + 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_handle, cert, kClientA).has_value()); + { + cert::FileBackedSlotHandler fh{m_parser}; + const auto cfg = MakeSlotConfig("empty-anchor", m_empty_slot_kv); + ASSERT_TRUE(fh.HasCrl(cfg).has_value()); + EXPECT_FALSE(fh.HasCrl(cfg).value()); + } + + // 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_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); + 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); +} + +// --------------------------------------------------------------------------- +// 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, MakeSlotManager(registry)); + + 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); + + const std::vector crl{0x01U, 0x02U}; + const auto crl_span = score::crypto::span{crl.data(), crl.size()}; + + 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); +} + +// --------------------------------------------------------------------------- +// 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, MakeSlotManager(registry)); + + 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_handle, 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(); + 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); + + // First add the cert without CRL. + 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_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}; + const auto cfg = MakeSlotConfig("empty-anchor", m_empty_slot_kv); + 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); +} + +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 +// 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, MakeSlotManager(registry)); + + 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, MakeSlotManager(registry)); + + 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, 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, kClientA); + 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, MakeSlotManager(registry)); + + 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, MakeSlotManager(registry)); + + 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, MakeSlotManager(registry)); + + 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/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..aeefd252f --- /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, + CertSlotManager::Sptr slot_manager) +{ + 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_manager)); + 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..82d179329 --- /dev/null +++ b/score/crypto/src/daemon/cert_management/truststore/config_driven_trust_store_catalog.hpp @@ -0,0 +1,53 @@ +/******************************************************************************** + * 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/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" + +#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 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; + + 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..e965a9b1b --- /dev/null +++ b/score/crypto/src/daemon/cert_management/truststore/trust_store_handler.cpp @@ -0,0 +1,121 @@ +/******************************************************************************** + * 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); +} + +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; +} + +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..2d5a0ab20 --- /dev/null +++ b/score/crypto/src/daemon/cert_management/truststore/trust_store_handler.hpp @@ -0,0 +1,65 @@ +/******************************************************************************** + * 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; + + 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; + + void InvalidateSlot(CertSlotHandle slot); + void ClearAnchorCache(); + + private: + void EnsureLoaded(); + std::vector All() const; + + TrustStoreHandle m_handle{}; + std::unordered_map m_slots; + std::unordered_map m_crls; + 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..6f1eb790d --- /dev/null +++ b/score/crypto/src/daemon/cert_management/truststore/trust_store_manager.cpp @@ -0,0 +1,845 @@ +/******************************************************************************** + * 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, + CertSlotManager::Sptr slot_manager) +{ + 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_manager = std::move(slot_manager); + 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 +// --------------------------------------------------------------------------- + +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::GetHandler(CertSlotHandle slot) +{ + // Must be called with m_mutex held. + // 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; + return m_slot_manager->GetTrustStoreCertSlotHandler(slot).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 = GetHandler(slot); + const auto cfg = m_slot_registry ? m_slot_registry->GetConfig(slot) : nullptr; + if (handler == nullptr || !cfg) + return nullptr; + + 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); +} + +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 = GetHandler(*slot); + const auto cfg = m_slot_registry->GetConfig(*slot); + if (slot_handler != nullptr && cfg) + { + 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()) + { + 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)); + } + } + } + } + } +} + +// --------------------------------------------------------------------------- +// Store access +// --------------------------------------------------------------------------- + +ITrustStoreHandler::Sptr TrustStoreManager::GetStore(TrustStoreHandle handle) const +{ + std::lock_guard lock(m_mutex); + if (!handle.IsValid()) + return nullptr; + return handle.index < m_stores.size() ? m_stores[handle.index].handler : nullptr; +} + +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() ? TrustStoreHandle{} : TrustStoreHandle{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); + 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; +} + +// --------------------------------------------------------------------------- +// 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) != 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 != nullptr) + 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(TrustStoreHandle handle, CertSlotHandle changed_slot) +{ + std::lock_guard lock(m_mutex); + 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"; + } + } +} + +// --------------------------------------------------------------------------- +// 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 = GetHandler(*slot); + if (handler == nullptr) + 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 = GetHandler(slot); + if (handler == nullptr) + return score::crypto::make_unexpected(Error::kInvalidResourceId); + return ResolvedBackend{*cfg, handler}; +} + +score::crypto::Expected TrustStoreManager::AddMember( + TrustStoreHandle handle, + CertObject::Sptr cert, + data_manager::ClientId client_id, + score::crypto::span crl_bytes, + score::crypto::FormatType crl_format, + std::optional crl_metadata) +{ + 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()) + 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) + { + 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; + + // 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, std::move(crl_metadata)); + 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, std::move(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()); + + if (has_crl) + { + 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; + m_slot_cert_cache[resolved->slot.index] = 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{}; + } + return score::crypto::make_unexpected(Error::kTrustStoreCapacityExceeded); +} + +score::crypto::Expected TrustStoreManager::ImportCrlForMember( + TrustStoreHandle handle, + CertSlotHandle slot, + score::crypto::span crl_data, + score::crypto::FormatType format, + data_manager::ClientId client_id, + std::optional metadata) +{ + 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 (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) + { + 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, std::move(metadata)); + 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::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) +{ + 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()) + 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(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); + // 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(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()) + 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( + 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); + 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) + 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(TrustStoreHandle handle) const +{ + std::lock_guard lock(m_mutex); + return handle.index < m_stores.size() ? &m_stores[handle.index].config : nullptr; +} + +// --------------------------------------------------------------------------- +// Member snapshot (for ITrustStoreObject read-only view) +// --------------------------------------------------------------------------- + +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 {}; + + 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 new file mode 100644 index 000000000..36db3c14b --- /dev/null +++ b/score/crypto/src/daemon/cert_management/truststore/trust_store_manager.hpp @@ -0,0 +1,331 @@ +/******************************************************************************** + * 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/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" +#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/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" +#include "score/crypto/src/daemon/data_manager/data_node.hpp" + +#include +#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, + CertSlotManager::Sptr slot_manager = {}); + + // ----------------------------------------------------------------------- + // Store access + // ----------------------------------------------------------------------- + + [[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 + 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(TrustStoreHandle handle, CertSlotHandle changed_slot); + + /// @brief Add a certificate to a trust store's runtime anchor set. + /// + /// 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 ImportCrlToSlot on the slot resource. + /// + /// Write access to the trust store must be checked by CertManagementService + /// before calling this method. + [[nodiscard]] score::crypto::Expected AddMember( + TrustStoreHandle handle, + CertObject::Sptr cert, + data_manager::ClientId client_id, + score::crypto::span crl_bytes = {}, + 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. + /// + /// 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 @p slot is not a member of the trust store. + /// Returns kUnsupportedOperation if the slot is not kExclusiveMutable. + [[nodiscard]] score::crypto::Expected + ImportCrlForMember(TrustStoreHandle handle, + CertSlotHandle slot, + score::crypto::span crl_data, + score::crypto::FormatType format, + data_manager::ClientId client_id, + 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. + /// + /// Persists the change to the trust store descriptor and calls NotifyUpdate(). + /// Write access must be checked before calling. + [[nodiscard]] score::crypto::Expected + RemoveMember(TrustStoreHandle handle, const std::vector& fingerprint, data_manager::ClientId client_id); + + [[nodiscard]] score::crypto::Expected + EnableMember(TrustStoreHandle handle, CertSlotHandle slot, data_manager::ClientId client_id); + [[nodiscard]] score::crypto::Expected + DisableMember(TrustStoreHandle handle, CertSlotHandle slot, data_manager::ClientId client_id); + [[nodiscard]] score::crypto::Expected + AcknowledgeMemberUpdate(TrustStoreHandle handle, 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; ///< 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. + 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(TrustStoreHandle handle); + + // ----------------------------------------------------------------------- + // Configuration access + // ----------------------------------------------------------------------- + + /// @brief Return the TrustStoreConfig for a given store handle. + [[nodiscard]] const TrustStoreConfig* GetStoreConfig(TrustStoreHandle handle) 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{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); + + /// 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 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* GetHandler(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; + CertSlotManager::Sptr m_slot_manager; + + 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/BUILD b/score/crypto/src/daemon/common/BUILD index 1cfebd98d..b5888cf68 100644 --- a/score/crypto/src/daemon/common/BUILD +++ b/score/crypto/src/daemon/common/BUILD @@ -18,12 +18,14 @@ cc_library( hdrs = [ "actors.hpp", "daemon_error.hpp", + "hex.hpp", "secure_memory.hpp", "types.hpp", ], 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/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/common/hex.hpp b/score/crypto/src/daemon/common/hex.hpp new file mode 100644 index 000000000..c97f32eb1 --- /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/src/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 diff --git a/score/crypto/src/daemon/key_management/slot/deployment/BUILD b/score/crypto/src/daemon/common/storage/BUILD similarity index 54% rename from score/crypto/src/daemon/key_management/slot/deployment/BUILD rename to score/crypto/src/daemon/common/storage/BUILD index 4039b8e1a..a9c43b26d 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,21 @@ 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 = "file_io", + srcs = ["file_io.cpp"], + hdrs = ["file_io.hpp"], + visibility = ["//:__subpackages__"], + deps = [ + "//score/crypto/src/common:common_types", + "//score/crypto/src/daemon/common", + "@score_baselibs//score/filesystem", + ], +) + cc_library( name = "kv_deployment", srcs = [ @@ -45,5 +54,36 @@ cc_library( "kv/kv_deployment_writer.hpp", ], visibility = ["//:__subpackages__"], - deps = [":deployment_iface"], + deps = [ + ":deployment_iface", + ":file_io", + "@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.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..2c8812367 --- /dev/null +++ b/score/crypto/src/daemon/common/storage/deployment_descriptor.hpp @@ -0,0 +1,87 @@ +/******************************************************************************** + * 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 +#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 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 kEmptyString; + } + const auto kit = sit->second.find(key); + 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). + [[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..be3fe0e55 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,33 @@ * 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/file_io.cpp b/score/crypto/src/daemon/common/storage/file_io.cpp new file mode 100644 index 000000000..5db4c4548 --- /dev/null +++ b/score/crypto/src/daemon/common/storage/file_io.cpp @@ -0,0 +1,125 @@ +/******************************************************************************** + * 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 "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 +{ + +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(open_result.error() == + score::filesystem::ErrorCode::kFileOrDirectoryDoesNotExist + ? DaemonErrorCode::kResourceNotAllocated + : DaemonErrorCode::kInternalError); + } + + 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); + stream.seekg(0, std::ios::beg); + std::vector data(static_cast(size)); + stream.read(reinterpret_cast(data.data()), static_cast(data.size())); + if (!stream) + 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); + + const score::filesystem::Path target_path{path}; + score::filesystem::StandardFilesystem fs{}; + const auto parent = target_path.ParentPath(); + if (!parent.Empty()) + { + if (!fs.CreateDirectories(parent).has_value()) + return score::crypto::make_unexpected(DaemonErrorCode::kInternalError); + } + + score::filesystem::FileFactory factory{}; + 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); + + 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{}; +} + +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}); + 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) +{ + if (path.empty()) + return score::crypto::make_unexpected(DaemonErrorCode::kInvalidArgument); + score::filesystem::StandardFilesystem fs{}; + const auto result = fs.Remove(score::filesystem::Path{path}); + 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{}; +} + +} // 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..844e4db71 --- /dev/null +++ b/score/crypto/src/daemon/common/storage/file_io.hpp @@ -0,0 +1,69 @@ +/******************************************************************************** + * 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_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 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); + +/// 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); + +/// Check whether @p path refers to an existing regular file. +/// +/// 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. +/// +/// 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/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..0524bfe36 --- /dev/null +++ b/score/crypto/src/daemon/common/storage/kv/kv_deployment_loader.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/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 + +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) +{ + 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(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(stream, 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)); + + auto& section_map = descriptor.sections[current_section]; + if (section_map.find(key) != section_map.end()) + { + score::mw::log::LogError() << kLogPrefix << "Duplicate key in descriptor: " << path + << " section=" << current_section << " key=" << key; + return score::crypto::make_unexpected(DaemonErrorCode::kInvalidArgument); + } + section_map[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..5dbb9c324 --- /dev/null +++ b/score/crypto/src/daemon/common/storage/kv/kv_deployment_loader.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 + ********************************************************************************/ + +#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. +/// - A key that appears more than once within the same section is rejected: +/// Load() logs the descriptor path, section, and key, then returns +/// DaemonErrorCode::kInvalidArgument. +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/common/storage/kv/kv_deployment_writer.cpp b/score/crypto/src/daemon/common/storage/kv/kv_deployment_writer.cpp new file mode 100644 index 000000000..52e59a81b --- /dev/null +++ b/score/crypto/src/daemon/common/storage/kv/kv_deployment_writer.cpp @@ -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 + ********************************************************************************/ + +#include "score/crypto/src/daemon/common/storage/kv/kv_deployment_writer.hpp" + +#include "score/crypto/src/daemon/common/storage/file_io.hpp" + +#include + +namespace score::crypto::daemon::common::storage +{ + +score::crypto::Expected KvDeploymentWriter::Write( + const std::string& path, + const DeploymentDescriptor& descriptor) +{ + std::ostringstream oss; + for (const auto& [section, entries] : descriptor.sections) + { + oss << '[' << section << ']' << '\n'; + for (const auto& [key, value] : entries) + { + oss << key << " = " << value << '\n'; + } + oss << '\n'; + } + + std::string content = oss.str(); + if (content.empty()) + content = "# empty deployment descriptor\n"; + const auto bytes = + score::crypto::span{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 new file mode 100644 index 000000000..153bfa863 --- /dev/null +++ b/score/crypto/src/daemon/common/storage/kv/kv_deployment_writer.hpp @@ -0,0 +1,39 @@ +/******************************************************************************** + * 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. +/// 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: + [[nodiscard]] score::crypto::Expected Write( + const std::string& path, + const DeploymentDescriptor& descriptor) override; +}; + +} // 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/common/storage/tests/test_deployment_descriptor.cpp b/score/crypto/src/daemon/common/storage/tests/test_deployment_descriptor.cpp new file mode 100644 index 000000000..dc1b8ed7a --- /dev/null +++ b/score/crypto/src/daemon/common/storage/tests/test_deployment_descriptor.cpp @@ -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 + ********************************************************************************/ + +#include "score/crypto/src/daemon/common/storage/deployment_descriptor.hpp" + +#include + +#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 diff --git a/score/crypto/src/daemon/common/types.hpp b/score/crypto/src/daemon/common/types.hpp index 9f3bdfdf0..77f435e92 100644 --- a/score/crypto/src/daemon/common/types.hpp +++ b/score/crypto/src/daemon/common/types.hpp @@ -180,6 +180,52 @@ 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 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. +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/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/inc/config.hpp b/score/crypto/src/daemon/config/inc/config.hpp index cec1841d4..c896c268d 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", @@ -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,18 +294,18 @@ 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). + /// @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. @@ -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; }; @@ -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; }; /** @@ -395,7 +538,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/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/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/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/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/key_management/BUILD b/score/crypto/src/daemon/key_management/BUILD index 09d5232e0..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,9 +111,11 @@ 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", "//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", ], 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/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); } 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.cpp b/score/crypto/src/daemon/key_management/slot/deployment/kv/kv_deployment_writer.cpp deleted file mode 100644 index dd4f6a0bb..000000000 --- a/score/crypto/src/daemon/key_management/slot/deployment/kv/kv_deployment_writer.cpp +++ /dev/null @@ -1,56 +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_writer.hpp" - -#include "score/mw/log/logging.h" -#include - -#include - -namespace score::crypto::daemon::key_management -{ - -score::crypto::Expected KvDeploymentWriter::Write( - const std::string& path, - const SlotDeploymentInfo& info) -{ - 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'; - } - - file << "\n[key]\n"; - for (const auto& [key, value] : info.key_properties) - { - file << key << '=' << value << '\n'; - } - - if (!file.good()) - { - score::mw::log::LogError() << kLogPrefix << "Write error for deployment descriptor:" << path; - return score::crypto::make_unexpected(score::crypto::daemon::common::DaemonErrorCode::kInternalError); - } - - return std::monostate{}; -} - -} // namespace score::crypto::daemon::key_management 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); } 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..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 @@ -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,38 @@ 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; + 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()) - { + 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; + 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) +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 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/mediator/src/mediator_impl.cpp b/score/crypto/src/daemon/mediator/src/mediator_impl.cpp index b4cd5d8ab..e69d96f5b 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); @@ -329,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) { @@ -343,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; } @@ -413,8 +446,10 @@ bool MediatorImpl::HandleContextCreationOperation(const score::crypto::daemon::c return false; } - const std::string_view provider_selection = - has_key_binding ? " (key-affinity resolved)" : " (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/cert_management/BUILD b/score/crypto/src/daemon/provider/cert_management/BUILD new file mode 100644 index 000000000..b6b6985c1 --- /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 +# ******************************************************************************* + +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/api/types:types", + "//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/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 new file mode 100644 index 000000000..01b3cfc13 --- /dev/null +++ b/score/crypto/src/daemon/provider/cert_management/cert_management_operations.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_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_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_PUBLIC_KEY = 0x60U; +inline constexpr OperationAction CRL_IMPORT = 0x80U; +inline constexpr OperationAction CRL_DELETE = 0x81U; +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; +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_IMPORT_CRL_FOR_MEMBER = 0xC6U; // fingerprint + CRL bytes → exclusive slot +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; + +// 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..080b5ede7 --- /dev/null +++ b/score/crypto/src/daemon/provider/cert_management/cert_types.hpp @@ -0,0 +1,94 @@ +/******************************************************************************** + * 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_PROVIDER_CERT_MANAGEMENT_CERT_TYPES_HPP +#define SCORE_CRYPTO_SRC_DAEMON_PROVIDER_CERT_MANAGEMENT_CERT_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" + +#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; + +using ::score::crypto::ChainTerminationPolicy; +enum class CertVerifyErrorCode : std::uint16_t +{ + kNone = 0, + kExpired, + kNotYetValid, + kRevoked, + kNoRootFound, + kChainIncomplete, + kSignatureInvalid, + kInvalidPurpose, + kUnknownAlgorithm, + kUnknownError +}; + +using ::score::crypto::RevocationCheckPolicy; + +/// 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; + RevocationCheckPolicy revocation_policy{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..067cc94e1 --- /dev/null +++ b/score/crypto/src/daemon/provider/cert_management/i_cert_parser.hpp @@ -0,0 +1,78 @@ +/******************************************************************************** + * 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_PROVIDER_CERT_MANAGEMENT_I_CERT_PARSER_HPP +#define SCORE_CRYPTO_SRC_DAEMON_PROVIDER_CERT_MANAGEMENT_I_CERT_PARSER_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" + +#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; + + /// 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 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, + 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 + +#endif // SCORE_CRYPTO_SRC_DAEMON_PROVIDER_CERT_MANAGEMENT_I_CERT_PARSER_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/i_provider.hpp b/score/crypto/src/daemon/provider/i_provider.hpp index b7dc53b57..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. @@ -147,6 +191,26 @@ class IProvider { return nullptr; } + + // ----------------------------------------------------------------------- + // Capability advertisement + // ----------------------------------------------------------------------- + + /// @brief Report the functional capabilities this provider offers. + /// + /// 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() + { + return common::ProviderCapability::kNone; + } }; } // namespace score::crypto::daemon::provider 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/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 38fead7c8..01a596c4b 100644 --- a/score/crypto/src/daemon/provider/provider_manager.hpp +++ b/score/crypto/src/daemon/provider/provider_manager.hpp @@ -160,6 +160,38 @@ 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 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. + * + * 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. + * @return The selected provider, or nullptr if none offers the capability. + */ + [[nodiscard]] std::shared_ptr GetProviderForCapability( + common::ProviderCapability capability, + 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/BUILD b/score/crypto/src/daemon/provider/score_provider/openssl/BUILD index 1ba4c2fd0..45a768dfe 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", @@ -58,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", @@ -92,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 new file mode 100644 index 000000000..8a887e28a --- /dev/null +++ b/score/crypto/src/daemon/provider/score_provider/openssl/cert_management/openssl_cert_parser.cpp @@ -0,0 +1,401 @@ +/******************************************************************************** + * 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 +#include +#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); + } + + // 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) + { + 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; +} + +// --------------------------------------------------------------------------- +// 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; + + 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( + 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); + } + + 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 new file mode 100644 index 000000000..016e849fb --- /dev/null +++ b/score/crypto/src/daemon/provider/score_provider/openssl/cert_management/openssl_cert_parser.hpp @@ -0,0 +1,65 @@ +/******************************************************************************** + * 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_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; + + [[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; +}; + +} // 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/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) 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..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 @@ -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(); @@ -68,11 +69,28 @@ std::shared_ptr<::score::crypto::daemon::provider::handler::ICryptoHandlerFactor return std::make_shared(m_factory, GetKeySlotHandler({}), m_keyManagementService); } +void OpenSSL::SetCertManagementService( + std::shared_ptr<::score::crypto::daemon::cert_management::CertManagementService> service) +{ + m_certManagementService = std::move(service); +} + +common::ProviderCapability OpenSSL::GetProviderCapabilities() +{ + return common::ProviderCapability::kCrypto | common::ProviderCapability::kKeyManagement | + common::ProviderCapability::kCertManagement; +} + 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 07d82a1d8..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 @@ -41,12 +41,20 @@ 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( 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; @@ -60,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/src/provider_manager.cpp b/score/crypto/src/daemon/provider/src/provider_manager.cpp index 78f3eb632..bb50a9d38 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,59 @@ 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 +{ + 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..e1b53cf3c 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,122 @@ 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"); +} 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..fa6ecbbc5 --- /dev/null +++ b/score/crypto/src/daemon/provider/tests/cert_management/BUILD @@ -0,0 +1,33 @@ +# ******************************************************************************* +# 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: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", + "//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 new file mode 100644 index 000000000..2e1573deb --- /dev/null +++ b/score/crypto/src/daemon/provider/tests/cert_management/test_openssl_cert_parser.cpp @@ -0,0 +1,483 @@ +/******************************************************************************** + * 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 + +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +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/basic/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_view 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()); +} + +// --------------------------------------------------------------------------- +// 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->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->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->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->next_update, 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->next_update, 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->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); +} + +// --------------------------------------------------------------------------- +// 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/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/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/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/score/tests/test_vectors/certificate/BUILD b/score/tests/test_vectors/certificate/BUILD new file mode 100644 index 000000000..19a37c152 --- /dev/null +++ b/score/tests/test_vectors/certificate/BUILD @@ -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 +# ******************************************************************************* + +# --------------------------------------------------------------------------- +# 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 = [ + ":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 new file mode 100644 index 000000000..de83e89a1 --- /dev/null +++ b/score/tests/test_vectors/certificate/README.md @@ -0,0 +1,256 @@ + + +# Certificate Test Vectors + +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. + +``` +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 | +|------|-----------|---------| +| `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 (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) | + +PQC support requires OpenSSL ≥ 3.5. + +### `pki_chain/` (future) + +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 | +|--------|--------| +| 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` | + +--- + +## Generating and refreshing certificates + +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. + +### Actions + +| 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 +# 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 +``` + +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. + +--- + +## 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 +``` + +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. 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/algorithm_variety/ec_p256.pem b/score/tests/test_vectors/certificate/algorithm_variety/ec_p256.pem new file mode 100644 index 000000000..a18702ca4 --- /dev/null +++ b/score/tests/test_vectors/certificate/algorithm_variety/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/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/algorithm_variety/ec_p384.pem b/score/tests/test_vectors/certificate/algorithm_variety/ec_p384.pem new file mode 100644 index 000000000..d6ae27365 --- /dev/null +++ b/score/tests/test_vectors/certificate/algorithm_variety/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/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/algorithm_variety/ec_p521.pem b/score/tests/test_vectors/certificate/algorithm_variety/ec_p521.pem new file mode 100644 index 000000000..2ee427c1e --- /dev/null +++ b/score/tests/test_vectors/certificate/algorithm_variety/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/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/algorithm_variety/ed25519.pem b/score/tests/test_vectors/certificate/algorithm_variety/ed25519.pem new file mode 100644 index 000000000..18fe57803 --- /dev/null +++ b/score/tests/test_vectors/certificate/algorithm_variety/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/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/algorithm_variety/ed448.pem b/score/tests/test_vectors/certificate/algorithm_variety/ed448.pem new file mode 100644 index 000000000..ba9fea6ae --- /dev/null +++ b/score/tests/test_vectors/certificate/algorithm_variety/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/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/algorithm_variety/manifest.json b/score/tests/test_vectors/certificate/algorithm_variety/manifest.json new file mode 100644 index 000000000..9eeb406e4 --- /dev/null +++ b/score/tests/test_vectors/certificate/algorithm_variety/manifest.json @@ -0,0 +1,121 @@ +{ + "schema_version": 2, + "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": "rsa_3072", + "purpose": "RSA-3072 CA 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 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 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 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 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 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 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 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": { + "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 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": { + "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 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": { + "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/algorithm_variety/ml_dsa_44.pem b/score/tests/test_vectors/certificate/algorithm_variety/ml_dsa_44.pem new file mode 100644 index 000000000..f941ea0cb --- /dev/null +++ b/score/tests/test_vectors/certificate/algorithm_variety/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/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/algorithm_variety/ml_dsa_65.pem b/score/tests/test_vectors/certificate/algorithm_variety/ml_dsa_65.pem new file mode 100644 index 000000000..96f312966 --- /dev/null +++ b/score/tests/test_vectors/certificate/algorithm_variety/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/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/algorithm_variety/ml_dsa_87.pem b/score/tests/test_vectors/certificate/algorithm_variety/ml_dsa_87.pem new file mode 100644 index 000000000..8a6387323 --- /dev/null +++ b/score/tests/test_vectors/certificate/algorithm_variety/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/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/algorithm_variety/private/.gitignore b/score/tests/test_vectors/certificate/algorithm_variety/private/.gitignore new file mode 100644 index 000000000..d6b7ef32c --- /dev/null +++ b/score/tests/test_vectors/certificate/algorithm_variety/private/.gitignore @@ -0,0 +1,2 @@ +* +!.gitignore diff --git a/score/tests/test_vectors/certificate/algorithm_variety/rsa_3072.pem b/score/tests/test_vectors/certificate/algorithm_variety/rsa_3072.pem new file mode 100644 index 000000000..e9ae2104e --- /dev/null +++ b/score/tests/test_vectors/certificate/algorithm_variety/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/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/algorithm_variety/rsa_4096.pem b/score/tests/test_vectors/certificate/algorithm_variety/rsa_4096.pem new file mode 100644 index 000000000..4ff0b73c3 --- /dev/null +++ b/score/tests/test_vectors/certificate/algorithm_variety/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/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/basic/certificate.crl.der b/score/tests/test_vectors/certificate/basic/certificate.crl.der new file mode 100644 index 000000000..d16d1da9e Binary files /dev/null and b/score/tests/test_vectors/certificate/basic/certificate.crl.der differ diff --git a/score/tests/test_vectors/certificate/basic/certificate.crl.pem b/score/tests/test_vectors/certificate/basic/certificate.crl.pem new file mode 100644 index 000000000..2d3909292 --- /dev/null +++ b/score/tests/test_vectors/certificate/basic/certificate.crl.pem @@ -0,0 +1,41 @@ +Certificate Revocation List (CRL): + Version 2 (0x1) + Signature Algorithm: sha256WithRSAEncryption + Issuer: CN = cert-management-test, O = Eclipse + Last Update: Sep 8 20:34:51 2026 GMT + Next Update: Sep 5 20:34:51 2036 GMT +Revoked Certificates: + Serial Number: 6D74ACFD926ABB7599067D8FBBCC58BBB2D12463 + Revocation Date: Sep 8 20:34:51 2026 GMT + CRL entry extensions: + X509v3 CRL Reason Code: + Unspecified + Signature Algorithm: sha256WithRSAEncryption + Signature Value: + 9e:87:4f:27:a5:ea:99:a2:6d:f4:99:4d:4b:96:9c:af:c2:27: + 38:35:19:00:4b:71:c8:78:d8:d9:a9:7b:5f:90:f1:a1:9f:c1: + e8:38:ab:c0:14:02:c3:99:ee:49:b2:b0:1a:eb:4c:a3:28:0e: + 78:7f:7c:1a:72:ff:77:26:38:bc:02:e6:83:76:4d:24:ea:9c: + a1:16:64:cd:d6:92:45:4a:a5:f4:a7:a2:9f:c1:61:39:60:21: + c8:f0:2c:49:b9:ab:54:e5:5c:10:73:bb:99:72:2d:2f:f3:39: + 22:ce:95:40:f8:97:29:42:b4:1f:44:35:50:96:de:dd:68:86: + d4:86:8d:2f:a3:ce:83:29:e3:a2:af:04:0f:b4:ed:94:d4:f9: + 11:cc:70:e9:4f:81:62:fd:b2:a0:86:13:dc:b7:96:45:94:34: + b5:4c:96:41:25:eb:f8:91:f0:b7:8f:27:12:ee:48:10:83:5d: + af:19:36:db:ac:df:93:a4:51:37:24:52:57:16:5e:6a:62:76: + 7a:7c:e2:18:50:b1:91:75:9c:ca:1c:ce:4a:5d:44:26:92:b6: + 1c:2d:10:f6:55:98:0f:8c:0b:af:fa:9c:7d:40:20:8c:e9:94: + 20:29:0a:b9:0b:b9:34:f0:18:0e:ed:e7:60:86:2b:5d:85:df: + c8:2c:5c:df +-----BEGIN X509 CRL----- +MIIBsTCBmgIBATANBgkqhkiG9w0BAQsFADAxMR0wGwYDVQQDDBRjZXJ0LW1hbmFn +ZW1lbnQtdGVzdDEQMA4GA1UECgwHRWNsaXBzZRcNMjYwOTA4MjAzNDUxWhcNMzYw +OTA1MjAzNDUxWjA1MDMCFG10rP2Sart1mQZ9j7vMWLuy0SRjFw0yNjA5MDgyMDM0 +NTFaMAwwCgYDVR0VBAMKAQAwDQYJKoZIhvcNAQELBQADggEBAJ6HTyel6pmibfSZ +TUuWnK/CJzg1GQBLcch42Nmpe1+Q8aGfweg4q8AUAsOZ7kmysBrrTKMoDnh/fBpy +/3cmOLwC5oN2TSTqnKEWZM3WkkVKpfSnop/BYTlgIcjwLEm5q1TlXBBzu5lyLS/z +OSLOlUD4lylCtB9ENVCW3t1ohtSGjS+jzoMp46KvBA+07ZTU+RHMcOlPgWL9sqCG +E9y3lkWUNLVMlkEl6/iR8LePJxLuSBCDXa8ZNtus35OkUTckUlcWXmpidnp84hhQ +sZF1nMoczkpdRCaSthwtEPZVmA+MC6/6nH1AIIzplCApCrkLuTTwGA7t52CGK12F +38gsXN8= +-----END X509 CRL----- diff --git a/score/tests/test_vectors/certificate/basic/certificate.pem b/score/tests/test_vectors/certificate/basic/certificate.pem new file mode 100644 index 000000000..26aaf5cb3 --- /dev/null +++ b/score/tests/test_vectors/certificate/basic/certificate.pem @@ -0,0 +1,75 @@ +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 +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/basic/certificate_leaf.chain.pem b/score/tests/test_vectors/certificate/basic/certificate_leaf.chain.pem new file mode 100644 index 000000000..65feedb73 --- /dev/null +++ b/score/tests/test_vectors/certificate/basic/certificate_leaf.chain.pem @@ -0,0 +1,153 @@ +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----- +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 +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/basic/certificate_leaf.der b/score/tests/test_vectors/certificate/basic/certificate_leaf.der new file mode 100644 index 000000000..d92812ac3 Binary files /dev/null and b/score/tests/test_vectors/certificate/basic/certificate_leaf.der differ 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/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/basic/certificate_updated.pem b/score/tests/test_vectors/certificate/basic/certificate_updated.pem new file mode 100644 index 000000000..721d8ab95 --- /dev/null +++ b/score/tests/test_vectors/certificate/basic/certificate_updated.pem @@ -0,0 +1,75 @@ +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 +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/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..3f0843f2c --- /dev/null +++ b/score/tests/test_vectors/certificate/basic/manifest.json @@ -0,0 +1,65 @@ +{ + "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" + ], + "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/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/basic/trust_store.kv b/score/tests/test_vectors/certificate/basic/trust_store.kv new file mode 100644 index 000000000..4ad069426 --- /dev/null +++ b/score/tests/test_vectors/certificate/basic/trust_store.kv @@ -0,0 +1 @@ +[trust_store_state] 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..5ca8fdb57 --- /dev/null +++ b/score/tests/test_vectors/certificate/generate_certificates.py @@ -0,0 +1,840 @@ +#!/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. + +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) + +Manifest schema (v2) +-------------------- +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 + +import argparse +import datetime as datetime_module +import json +import pathlib +import subprocess +import tempfile +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]: + alg = key_algorithm.upper() + if alg.startswith("RSA-"): + bits = alg[4:] + if not bits.isdigit(): + raise SystemExit(f"Invalid RSA key size: {key_algorithm!r}") + return ["-newkey", f"rsa:{bits}"] + if alg in _EC_CURVES: + return ["-newkey", "ec", "-pkeyopt", f"ec_paramgen_curve:{_EC_CURVES[alg]}"] + if alg in _EDDSA_ALGS: + return ["-newkey", alg.lower()] + if alg in _MLDSA_LEVELS: + return ["-newkey", alg.lower()] + raise SystemExit( + f"Unknown key_algorithm: {key_algorithm!r}. " + "Supported: RSA-, EC-P256/P384/P521, Ed25519, Ed448, ML-DSA-44/65/87." + ) + + +def uses_separate_digest(key_algorithm: str) -> bool: + 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 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" + + +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 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 +# --------------------------------------------------------------------------- + + +def run_openssl(openssl: str, arguments: list[str]) -> subprocess.CompletedProcess[str]: + return subprocess.run([openssl, *arguments], check=True, capture_output=True, text=True) + + +def annotate_pem(openssl: str, certificate_path: pathlib.Path) -> None: + """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"], + ) + 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: 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") + + return { + "not_before": parse_date(values["notBefore"]), + "not_after": parse_date(values["notAfter"]), + "sha256_fingerprint": values["sha256 Fingerprint"].replace(":", "").upper(), + } + + +# --------------------------------------------------------------------------- +# Manifest I/O +# --------------------------------------------------------------------------- + + +def load_manifest(path: pathlib.Path) -> dict[str, Any]: + 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 f: + json.dump(manifest, f, indent=2) + f.write("\n") + + +def find_certificate(manifest: dict[str, Any], name: str) -> dict[str, Any]: + 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}") + + +# --------------------------------------------------------------------------- +# Slot KV descriptor +# --------------------------------------------------------------------------- + + +def _find_workspace_root(start: pathlib.Path) -> pathlib.Path | None: + 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], + cert_format: str = "pem", +) -> pathlib.Path: + kv_path = slot_kv_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 = target.resolve().relative_to(workspace_root).as_posix() + except ValueError: + cert_path_str = target.name + else: + cert_path_str = target.name + kv_path.write_text( + 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]: + """Generate or update a certificate. + + 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) + 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: + # ------------------------------------------------------------------ # + # 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 +# --------------------------------------------------------------------------- + + +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 + + +# --------------------------------------------------------------------------- +# Entry point +# --------------------------------------------------------------------------- + + +def main() -> None: + 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) + 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__": + main() 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..fff596ada --- /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. Used by integration chain and CRL verification tests.", + "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/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", diff --git a/tools/coverage/BUILD b/tools/coverage/BUILD index a5ae2a252..a8333941f 100644 --- a/tools/coverage/BUILD +++ b/tools/coverage/BUILD @@ -26,12 +26,17 @@ score_coverage_scope( visibility = ["//visibility:private"], deps = [ "//score/crypto/src/api:crypto_stack", + "//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", "//score/crypto/src/api/future/common:future_common", "//score/crypto/src/backend:active_pkcs11_backend", "//score/crypto/src/backend:pkcs11_backend_defines", "//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",