diff --git a/CMakeLists.txt b/CMakeLists.txt index 6f600245a758..44146af74d75 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -884,6 +884,11 @@ if(BUILD_TESTS) ${CMAKE_CURRENT_SOURCE_DIR}/src/node/test/node_info_json.cpp ) + add_unit_test( + identity_types_test + ${CMAKE_CURRENT_SOURCE_DIR}/src/node/test/identity_types.cpp + ) + add_unit_test(tls_test ${CMAKE_CURRENT_SOURCE_DIR}/src/tls/test/main.cpp) target_link_libraries(tls_test PRIVATE ${CMAKE_THREAD_LIBS_INIT}) diff --git a/src/node/test/identity_types.cpp b/src/node/test/identity_types.cpp new file mode 100644 index 000000000000..74f3d96e5b70 --- /dev/null +++ b/src/node/test/identity_types.cpp @@ -0,0 +1,95 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the Apache 2.0 License. + +#include "service/tables/identity_types.h" + +#include "ccf/kv/unit.h" + +#define DOCTEST_CONFIG_IMPLEMENT_WITH_MAIN +#include +#include +#include +#include +#include +#include +#include +#include + +using IdentityTypeSerialiser = + ccf::kv::serialisers::BlitSerialiser; + +static constexpr std::array IDENTITY_TYPES = { + ccf::IdentityType::CLASSICAL, ccf::IdentityType::PQ}; + +TEST_CASE("CLASSICAL shares the serialised key of a single-Value table") +{ + // ServiceValue is a Map with a single entry, whose key is 8 null bytes. A + // table keyed by IdentityType is therefore serialised identically to a + // ServiceValue for as long as CLASSICAL is its only entry. + REQUIRE( + IdentityTypeSerialiser::to_serialised(ccf::IdentityType::CLASSICAL) == + ccf::kv::serialisers::ZeroBlitUnitCreator::get()); + + REQUIRE( + IdentityTypeSerialiser::to_serialised(ccf::IdentityType::PQ) != + ccf::kv::serialisers::ZeroBlitUnitCreator::get()); +} + +TEST_CASE("IdentityType serialisation round-trips") +{ + for (const auto identity_type : IDENTITY_TYPES) + { + const auto serialised = + IdentityTypeSerialiser::to_serialised(identity_type); + REQUIRE(serialised.size() == sizeof(uint64_t)); + REQUIRE( + IdentityTypeSerialiser::from_serialised(serialised) == identity_type); + } +} + +TEST_CASE("Unknown identity types are rejected") +{ + const auto unknown = + ccf::kv::serialisers::BlitSerialiser::to_serialised( + std::numeric_limits::max()); + REQUIRE_THROWS_AS( + IdentityTypeSerialiser::from_serialised(unknown), std::logic_error); +} + +TEST_CASE("IdentityType names are distinct and stable") +{ + // These names will appear in the ledger, so they must not change. + REQUIRE(nlohmann::json(ccf::IdentityType::CLASSICAL) == "CLASSICAL"); + REQUIRE(nlohmann::json(ccf::IdentityType::PQ) == "PQ"); + + std::set names; + for (const auto identity_type : IDENTITY_TYPES) + { + const nlohmann::json name = identity_type; + REQUIRE(names.insert(name.get()).second); + } + REQUIRE(names.size() == IDENTITY_TYPES.size()); +} + +TEST_CASE("Identity round-trips through JSON") +{ + const ccf::Identity identity{ + ccf::IdentityKind::X509_SPKI_DER, std::vector{1, 2, 3, 4}}; + + const nlohmann::json j = identity; + REQUIRE(j["kind"] == "X509_SPKI_DER"); + + REQUIRE(j.get() == identity); +} + +TEST_CASE("Identities round-trips through JSON") +{ + const ccf::Identities identities{ + {ccf::IdentityType::CLASSICAL, + {ccf::IdentityKind::X509_CERT_DER, std::vector{1, 2}}}, + {ccf::IdentityType::PQ, + {ccf::IdentityKind::X509_SPKI_DER, std::vector{3, 4}}}}; + + const nlohmann::json j = identities; + REQUIRE(j.get() == identities); +} diff --git a/src/service/tables/identity_types.h b/src/service/tables/identity_types.h new file mode 100644 index 000000000000..5b215c441387 --- /dev/null +++ b/src/service/tables/identity_types.h @@ -0,0 +1,81 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the Apache 2.0 License. +#pragma once + +#include "ccf/ds/json.h" +#include "ccf/kv/serialisers/blit_serialiser.h" + +#include +#include +#include +#include + +namespace ccf +{ + enum class IdentityType : uint64_t + { + CLASSICAL = 0, + PQ = 1, + }; + + DECLARE_JSON_ENUM( + IdentityType, + {{IdentityType::CLASSICAL, "CLASSICAL"}, {IdentityType::PQ, "PQ"}}); + + enum class IdentityKind : uint8_t + { + X509_CERT_DER = 0, + X509_SPKI_DER = 1, + }; + + DECLARE_JSON_ENUM( + IdentityKind, + {{IdentityKind::X509_CERT_DER, "X509_CERT_DER"}, + {IdentityKind::X509_SPKI_DER, "X509_SPKI_DER"}}); + + using IdentityValue = std::vector; + + struct Identity + { + IdentityKind kind; + IdentityValue value; + + bool operator==(const Identity&) const = default; + }; + + DECLARE_JSON_TYPE(Identity); + DECLARE_JSON_REQUIRED_FIELDS(Identity, kind, value); + + using Identities = std::map; +} + +namespace ccf::kv::serialisers +{ + // IdentityType is used as a KV key by tables which were previously a single + // Value. CLASSICAL is 0, so it serialises to the same bytes as the unit key + // of those tables, keeping their serialised form unchanged. + template <> + struct BlitSerialiser + { + static SerialisedEntry to_serialised(const ccf::IdentityType& identity_type) + { + return BlitSerialiser::to_serialised( + static_cast(identity_type)); + } + + static ccf::IdentityType from_serialised(const SerialisedEntry& data) + { + const auto value = BlitSerialiser::from_serialised(data); + switch (value) + { + case static_cast(ccf::IdentityType::CLASSICAL): + return ccf::IdentityType::CLASSICAL; + case static_cast(ccf::IdentityType::PQ): + return ccf::IdentityType::PQ; + default: + throw std::logic_error( + fmt::format("Unknown identity type: {}", value)); + } + } + }; +}