Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .changes/pq-endorsement-chain-fetching.added.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
`ccf::NetworkIdentitySubsystemInterface` fetches and validates the endorsement chain of the PQ service identities with the CLASSICAL one, failing identity history fetching if either is inconsistent, and serves the chain and the trusted service keys of each identity type with `get_cose_endorsements_chain(seqno, identity_type)`, `get_trusted_key_der_for(seqno, identity_type)` and `get_trusted_keys_der(identity_type)`.
`ccf::IdentityType` is now public, in `ccf/service/identity_type.h`; see the [C++ API reference](doc/build_apps/api.rst#network-identity).
3 changes: 3 additions & 0 deletions doc/build_apps/api.rst
Original file line number Diff line number Diff line change
Expand Up @@ -212,6 +212,9 @@ Network Identity
.. doxygentypedef:: ccf::TrustedKeys
:project: CCF

.. doxygentypedef:: ccf::TrustedKeysDer
:project: CCF

.. doxygenclass:: ccf::NetworkIdentitySubsystemInterface
:project: CCF
:members:
1 change: 1 addition & 0 deletions doc/releases/migration_7_x_to_8_x.rst
Original file line number Diff line number Diff line change
Expand Up @@ -60,3 +60,4 @@ Nodes that join a service never create one.
After the recovery that introduces the identity and before the first renewal, install the 8.x default constitution with ``set_constitution``, or port its ``set_pq_service_certificate_validity`` action into the custom constitution.
- When a service with an ML-DSA identity opens, it endorses the ML-DSA identity of the previous service with its own, as it does for the classical identity (see :ref:`audit/builtin_maps:``previous_service_identity_endorsement```).
A service whose previous service had no ML-DSA identity, such as the one that introduces it, endorses its own ML-DSA identity, which starts the chain.
- Nodes fetch and validate the ML-DSA endorsement chain with the classical one, and applications read the endorsement chain and trusted keys of either identity type through :cpp:class:`ccf::NetworkIdentitySubsystemInterface`.
46 changes: 46 additions & 0 deletions include/ccf/network_identity_interface.h
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

#include "ccf/crypto/ec_public_key.h"
#include "ccf/node_subsystem_interface.h"
#include "ccf/service/identity_type.h"
#include "ccf/tx_id.h"

#include <exception>
Expand Down Expand Up @@ -62,6 +63,11 @@ namespace ccf
/// network identity keys over the history of the service.
using TrustedKeys = std::map<ccf::SeqNo, ccf::crypto::ECPublicKeyPtr>;

/// Map from sequence number to the DER-encoded SubjectPublicKeyInfo of the
/// trusted service key of one identity type which signed from that sequence
/// number on.
using TrustedKeysDer = std::map<ccf::SeqNo, std::vector<uint8_t>>;

/// Exception thrown when identity data is requested before the
/// asynchronous identity-history-fetching process has completed.
struct IdentityHistoryNotFetched : public std::exception
Expand Down Expand Up @@ -103,6 +109,21 @@ namespace ccf
[[nodiscard]] virtual std::optional<CoseEndorsementsChain>
get_cose_endorsements_chain(ccf::SeqNo seqno) const = 0;

/// Returns the COSE endorsements chain of the service identities of the
/// given type for the given sequence number, newest endorsement first, or
/// std::nullopt if the chain is not available for it. The CLASSICAL chain
/// is the one returned by get_cose_endorsements_chain(seqno). The PQ chain
/// is validated as the CLASSICAL one is, and fetching fails if either is
/// inconsistent. It starts at the latest service which had a PQ identity
/// when the service before it, if any, did not, so it is std::nullopt for
/// earlier sequence numbers and for a service without a PQ identity.
///
/// @throws IdentityHistoryNotFetched if identity history fetching has not
/// completed.
[[nodiscard]] virtual std::optional<CoseEndorsementsChain>
get_cose_endorsements_chain(
ccf::SeqNo seqno, IdentityType identity_type) const = 0;

/// Returns the trusted EC public key that was active at the given
/// sequence number, or nullptr if the sequence number precedes the
/// earliest known trusted key.
Expand All @@ -114,11 +135,36 @@ namespace ccf
[[nodiscard]] virtual ccf::crypto::ECPublicKeyPtr get_trusted_identity_for(
ccf::SeqNo seqno) const = 0;

/// Returns the DER-encoded SubjectPublicKeyInfo of the trusted service
/// key of the given type that was active at the given sequence number, or
/// std::nullopt if there is none. The CLASSICAL key is the one returned by
/// get_trusted_identity_for(seqno). There is no PQ key before the PQ chain
/// of get_cose_endorsements_chain(seqno, identity_type) starts, nor for a
/// service without a PQ identity.
///
/// @throws IdentityHistoryNotFetched if identity history fetching has not
/// completed.
/// @throws std::logic_error for CLASSICAL, as get_trusted_identity_for.
[[nodiscard]] virtual std::optional<std::vector<uint8_t>>
get_trusted_key_der_for(
ccf::SeqNo seqno, IdentityType identity_type) const = 0;

/// Returns all trusted network identity keys as a map from sequence
/// number to EC public key.
///
/// @throws IdentityHistoryNotFetched if identity history fetching has not
/// completed.
[[nodiscard]] virtual TrustedKeys get_trusted_keys() const = 0;

/// Returns all trusted service keys of the given type, as DER-encoded
/// SubjectPublicKeyInfo, by the sequence number from which each signed.
/// The CLASSICAL keys are those returned by get_trusted_keys(). The PQ
/// keys are those of the PQ chain, and are empty for a service without a
/// PQ identity.
///
/// @throws IdentityHistoryNotFetched if identity history fetching has not
/// completed.
[[nodiscard]] virtual TrustedKeysDer get_trusted_keys_der(
IdentityType identity_type) const = 0;
};
}
58 changes: 58 additions & 0 deletions include/ccf/service/identity_type.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the Apache 2.0 License.
#pragma once

#include "ccf/ds/json.h"

#include <cstdint>
#include <fmt/format.h>

namespace ccf
{
/// Cryptographic family of an identity.
// Preserve the existing 64-bit C++ representation.
// NOLINTNEXTLINE(performance-enum-size)
enum class IdentityType : uint64_t
{
/// Classical cryptographic identity.
CLASSICAL = 0,
/// Post-quantum cryptographic identity.
PQ = 1,
};

DECLARE_JSON_ENUM(
IdentityType,
{{IdentityType::CLASSICAL, "CLASSICAL"}, {IdentityType::PQ, "PQ"}});
}

FMT_BEGIN_NAMESPACE

template <>
struct formatter<ccf::IdentityType>
{
template <typename ParseContext>
constexpr auto parse(ParseContext& ctx)
{
return ctx.begin();
}

template <typename FormatContext>
auto format(const ccf::IdentityType& identity_type, FormatContext& ctx) const
-> decltype(ctx.out())
{
switch (identity_type)
{
case ccf::IdentityType::CLASSICAL:
{
return format_to(ctx.out(), "CLASSICAL");
}
case ccf::IdentityType::PQ:
{
return format_to(ctx.out(), "PQ");
}
}
return format_to(ctx.out(), "{}", static_cast<uint64_t>(identity_type));
}
};

FMT_END_NAMESPACE
25 changes: 19 additions & 6 deletions src/node/rpc/network_identity_accessors.h
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,26 @@

namespace ccf
{
// The current service's identity, with the create-txid and endorsement read
// The current service's identity, with the create-txid and endorsements read
// together from the same KV transaction.
struct CurrentServiceIdentity
{
std::optional<TxID> create_txid;

std::optional<CoseEndorsement> endorsement;

// The PQ endorsement written with the CLASSICAL one, by the opening of a
// service with a PQ identity
std::optional<CoseEndorsement> pq_endorsement;
};

// The previous service identity endorsements written by one transaction
struct WrittenEndorsements
{
CoseEndorsement classical;

// Written by the opening of a service with a PQ identity
std::optional<CoseEndorsement> pq;
};

struct INodeStateAccessor
Expand All @@ -26,9 +39,9 @@ namespace ccf

[[nodiscard]] virtual bool is_part_of_network() const = 0;

// Read the current service create-txid and previous-identity endorsement
// Read the current service create-txid and previous-identity endorsements
// together, only when the service status is OPEN. This guarantees they
// belong to the same service, because the endorsement is written in the
// belong to the same service, because the endorsements are written in the
// same transaction that opens the service.
virtual CurrentServiceIdentity get_current_service_identity() = 0;
};
Expand All @@ -37,10 +50,10 @@ namespace ccf
{
virtual ~IHistoricalStateAccessor() = default;

// Endorsement entry at the given historical kv version, or nullopt
// Endorsements written at the given historical kv version, or nullopt
// if the historical state is not yet loaded. Implementations may
// throw on hard errors.
virtual std::optional<CoseEndorsement> get_endorsement_at(SeqNo) = 0;
// throw on hard errors, including a missing CLASSICAL endorsement.
virtual std::optional<WrittenEndorsements> get_endorsements_at(SeqNo) = 0;
};

struct TaskScheduler
Expand Down
37 changes: 25 additions & 12 deletions src/node/rpc/network_identity_accessors_impl.h
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,9 @@

namespace ccf
{
// Read the service record and the previous-identity endorsement from the
// Read the service record and the previous-identity endorsements from the
// same read-only transaction (one KV snapshot), and only report the
// create-txid when the service is OPEN. The endorsement is written in the
// create-txid when the service is OPEN. The endorsements are written in the
// same transaction that opens the service, so this guarantees they belong to
// the same service.
inline CurrentServiceIdentity read_current_service_identity(
Expand Down Expand Up @@ -58,9 +58,20 @@ namespace ccf
}

result.create_txid = service_info->current_service_create_txid;
result.endorsement = tx.ro<ccf::PreviousServiceIdentityEndorsement>(
ccf::Tables::PREVIOUS_SERVICE_IDENTITY_ENDORSEMENT)
->get(ccf::IdentityType::CLASSICAL);
auto* endorsements = tx.ro<ccf::PreviousServiceIdentityEndorsement>(
ccf::Tables::PREVIOUS_SERVICE_IDENTITY_ENDORSEMENT);
result.endorsement = endorsements->get(ccf::IdentityType::CLASSICAL);
// A PQ endorsement belongs to the current service only if the transaction
// which opened it wrote the PQ endorsement with the CLASSICAL one
const auto classical_version =
endorsements->get_version_of_previous_write(ccf::IdentityType::CLASSICAL);
if (
classical_version.has_value() &&
classical_version ==
endorsements->get_version_of_previous_write(ccf::IdentityType::PQ))
{
result.pq_endorsement = endorsements->get(ccf::IdentityType::PQ);
}
return result;
}

Expand Down Expand Up @@ -108,7 +119,7 @@ namespace ccf
historical_cache(std::move(historical_cache_))
{}

std::optional<CoseEndorsement> get_endorsement_at(SeqNo seq) override
std::optional<WrittenEndorsements> get_endorsements_at(SeqNo seq) override
{
auto state = historical_cache->get_state_at(
ccf::historical::CompoundHandle{
Expand All @@ -125,20 +136,22 @@ namespace ccf
"missing",
seq));
}
// The historical store holds the writes of the transaction at seq only
auto htx = state->store->create_read_only_tx();
auto endorsement =
htx
.template ro<ccf::PreviousServiceIdentityEndorsement>(
ccf::Tables::PREVIOUS_SERVICE_IDENTITY_ENDORSEMENT)
->get(ccf::IdentityType::CLASSICAL);
auto* endorsements =
htx.template ro<ccf::PreviousServiceIdentityEndorsement>(
ccf::Tables::PREVIOUS_SERVICE_IDENTITY_ENDORSEMENT);
auto endorsement = endorsements->get(ccf::IdentityType::CLASSICAL);
if (!endorsement.has_value())
{
throw std::runtime_error(fmt::format(
"COSE endorsement entry for seqno {} is missing from its "
"historical state",
seq));
}
return endorsement;
return WrittenEndorsements{
.classical = std::move(endorsement.value()),
.pq = endorsements->get(ccf::IdentityType::PQ)};
}
};

Expand Down
Loading