Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[WIP] [DRAFT] COSE back-endorsements #6510

Draft
wants to merge 12 commits into
base: main
Choose a base branch
from
3 changes: 3 additions & 0 deletions include/ccf/crypto/cose_verifier.h
Original file line number Diff line number Diff line change
Expand Up @@ -27,4 +27,7 @@ namespace ccf::crypto
COSEVerifierUniquePtr make_cose_verifier_from_cert(
const std::vector<uint8_t>& cert);
COSEVerifierUniquePtr make_cose_verifier_from_key(const Pem& public_key);

std::pair<std::string, std::string> extract_cose_endorsement_validity(
std::span<const uint8_t> cose_msg);
}
15 changes: 15 additions & 0 deletions include/ccf/historical_queries_utils.h
Original file line number Diff line number Diff line change
Expand Up @@ -25,4 +25,19 @@ namespace ccf::historical
AbstractStateCache& state_cache,
std::shared_ptr<NetworkIdentitySubsystemInterface>
network_identity_subsystem);

// Modifies the receipt stored in state to include historical service
// endorsements, where required. If the state talks about a different service
// identity, which is known to be a predecessor of this service (via disaster
// recoveries), then an endorsement chain of all service identities preceding
// the current one will be created. This may need to use the state_cache to
// request additional historical entries to construct this endorsement, and
// may read from the current/latest state via tx. Returns true if the
// operation is complete, though it may still have failed to produce an
// endorsement. Returns false if additional entries have been requested, in
// which case the caller should retry later.
bool populate_cose_service_endorsements(
ccf::kv::ReadOnlyTx& tx,
ccf::historical::StatePtr& state,
AbstractStateCache& state_cache);
}
5 changes: 3 additions & 2 deletions src/crypto/openssl/cose_sign.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,8 @@ namespace

namespace ccf::crypto
{
std::optional<int> key_to_cose_alg_id(ccf::crypto::PublicKey_OpenSSL& key)
std::optional<int> key_to_cose_alg_id(
const ccf::crypto::PublicKey_OpenSSL& key)
{
const auto cid = key.get_curve_id();
switch (cid)
Expand Down Expand Up @@ -156,7 +157,7 @@ namespace ccf::crypto
}

std::vector<uint8_t> cose_sign1(
KeyPair_OpenSSL& key,
const KeyPair_OpenSSL& key,
const std::vector<COSEParametersFactory>& protected_headers,
std::span<const uint8_t> payload)
{
Expand Down
5 changes: 3 additions & 2 deletions src/crypto/openssl/cose_sign.h
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,8 @@ namespace ccf::crypto
COSESignError(const std::string& msg) : std::runtime_error(msg) {}
};

std::optional<int> key_to_cose_alg_id(ccf::crypto::PublicKey_OpenSSL& key);
std::optional<int> key_to_cose_alg_id(
const ccf::crypto::PublicKey_OpenSSL& key);

/* Sign a cose_sign1 payload with custom protected headers as strings, where
- key: integer label to be assigned in a COSE value
Expand All @@ -74,7 +75,7 @@ namespace ccf::crypto
https://www.iana.org/assignments/cose/cose.xhtml#header-parameters.
*/
std::vector<uint8_t> cose_sign1(
KeyPair_OpenSSL& key,
const KeyPair_OpenSSL& key,
const std::vector<COSEParametersFactory>& protected_headers,
std::span<const uint8_t> payload);
}
90 changes: 90 additions & 0 deletions src/crypto/openssl/cose_verifier.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,11 @@

namespace
{
static std::string qcbor_buf_to_string(const UsefulBufC& buf)
{
return std::string(reinterpret_cast<const char*>(buf.ptr), buf.len);
}

static std::optional<int> extract_algorithm_from_header(
std::span<const uint8_t> cose_msg)
{
Expand Down Expand Up @@ -223,4 +228,89 @@ namespace ccf::crypto
{
return std::make_unique<COSEKeyVerifier_OpenSSL>(public_key);
}

std::pair<std::string, std::string> extract_cose_endorsement_validity(
std::span<const uint8_t> cose_msg)
{
UsefulBufC msg{cose_msg.data(), cose_msg.size()};
QCBORError qcbor_result;

QCBORDecodeContext ctx;
QCBORDecode_Init(&ctx, msg, QCBOR_DECODE_MODE_NORMAL);

QCBORDecode_EnterArray(&ctx, nullptr);
qcbor_result = QCBORDecode_GetError(&ctx);
if (qcbor_result != QCBOR_SUCCESS)
{
LOG_DEBUG_FMT("Failed to parse COSE_Sign1 outer array");
return {};
}

const uint64_t tag = QCBORDecode_GetNthTagOfLast(&ctx, 0);
if (tag != CBOR_TAG_COSE_SIGN1)
{
LOG_DEBUG_FMT("Failed to parse COSE_Sign1 tag");
return {};
}

struct q_useful_buf_c protected_parameters;
QCBORDecode_EnterBstrWrapped(
&ctx, QCBOR_TAG_REQUIREMENT_NOT_A_TAG, &protected_parameters);
QCBORDecode_EnterMap(&ctx, NULL);

enum
{
FROM_INDEX,
TILL_INDEX,
END_INDEX
};
QCBORItem header_items[END_INDEX + 1];

header_items[FROM_INDEX].label.string = UsefulBufC{"from", 4};
header_items[FROM_INDEX].uLabelType = QCBOR_TYPE_TEXT_STRING;
header_items[FROM_INDEX].uDataType = QCBOR_TYPE_TEXT_STRING;

header_items[TILL_INDEX].label.string = UsefulBufC{"till", 4};
header_items[TILL_INDEX].uLabelType = QCBOR_TYPE_TEXT_STRING;
header_items[TILL_INDEX].uDataType = QCBOR_TYPE_TEXT_STRING;

header_items[END_INDEX].uLabelType = QCBOR_TYPE_NONE;

QCBORDecode_GetItemsInMap(&ctx, header_items);
qcbor_result = QCBORDecode_GetError(&ctx);
if (qcbor_result != QCBOR_SUCCESS)
{
LOG_DEBUG_FMT("Failed to decode protected header");
return {};
}

if (header_items[FROM_INDEX].uDataType == QCBOR_TYPE_NONE)
{
LOG_DEBUG_FMT("Failed to retrieve (missing) 'from' parameter");
return {};
}

if (header_items[TILL_INDEX].uDataType == QCBOR_TYPE_NONE)
{
LOG_DEBUG_FMT("Failed to retrieve (missing) 'till' parameter");
return {};
}

const auto from = qcbor_buf_to_string(header_items[FROM_INDEX].val.string);
const auto till = qcbor_buf_to_string(header_items[TILL_INDEX].val.string);

// Complete decode to ensure well-formed CBOR.

QCBORDecode_ExitMap(&ctx);
QCBORDecode_ExitBstrWrapped(&ctx);

qcbor_result = QCBORDecode_GetError(&ctx);
if (qcbor_result != QCBOR_SUCCESS)
{
LOG_DEBUG_FMT("Failed to decode protected header: {}", qcbor_result);
return {};
}

return {from, till};
}
}
7 changes: 6 additions & 1 deletion src/node/historical_queries_adapter.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -544,7 +544,12 @@ namespace ccf::historical
if (
historical_state == nullptr ||
(!populate_service_endorsements(
args.tx, historical_state, state_cache, network_identity_subsystem)))
args.tx,
historical_state,
state_cache,
network_identity_subsystem)) ||
!populate_cose_service_endorsements(
args.tx, historical_state, state_cache))
{
auto reason = fmt::format(
"Historical transaction {} is not currently available.",
Expand Down
128 changes: 128 additions & 0 deletions src/node/historical_queries_utils.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,27 @@

#include "ccf/historical_queries_utils.h"

#include "ccf/crypto/cose_verifier.h"
#include "ccf/rpc_context.h"
#include "ccf/service/tables/service.h"
#include "kv/kv_types.h"
#include "node/identity.h"
#include "node/tx_receipt_impl.h"
#include "service/tables/previous_service_identity.h"

namespace ccf
{
static std::map<ccf::crypto::Pem, std::vector<ccf::crypto::Pem>>
service_endorsement_cache;

struct BoundedEndorsement
{
ccf::SeqNo from{};
ccf::SeqNo till{};
std::vector<uint8_t> endorsement{};
};
static std::vector<BoundedEndorsement> cose_endorsements;

namespace historical
{
std::optional<ServiceInfo> find_previous_service_identity(
Expand Down Expand Up @@ -145,5 +155,123 @@ namespace ccf

return true;
}

bool populate_cose_service_endorsements(
ccf::kv::ReadOnlyTx& tx,
ccf::historical::StatePtr& state,
AbstractStateCache& state_cache)
{
const auto service_info = tx.template ro<Service>(Tables::SERVICE)->get();
const auto service_start = service_info->current_service_create_txid;
if (!service_start)
{
LOG_FAIL_FMT("Service start txid not found");
return true;
}

const auto target_seq = state->transaction_id.seqno;
if (service_start->seqno <= target_seq)
{
LOG_TRACE_FMT(
"Target seqno {} belongs to current service started at {}",
target_seq,
service_start->seqno);
return true;
}

if (cose_endorsements.empty())
{
const auto endorsement =
tx.template ro<PreviousServiceIdentityEndorsement>(
Tables::PREVIOUS_SERVICE_IDENTITY_ENDORSEMENT)
->get();
if (!endorsement)
{
LOG_FAIL_FMT("Endorsed identity not found");
return true;
}

const auto [from, till] =
ccf::crypto::extract_cose_endorsement_validity(*endorsement);

const auto from_id = TxID::from_str(from);
const auto till_id = TxID::from_str(till);

if (!from_id || !till_id)
{
LOG_FAIL_FMT("Can't parse COSE endorsement validity");
return true;
}

cose_endorsements.push_back(
{from_id->seqno, till_id->seqno, *endorsement});
}

while (cose_endorsements.back().from > target_seq)
{
auto earliest_seqno = cose_endorsements.back().from;
auto hstate = state_cache.get_state_at(earliest_seqno, earliest_seqno);
if (!hstate)
{
return false; // retry later
}
auto htx = hstate->store->create_read_only_tx();
const auto endorsement =
htx
.template ro<PreviousServiceIdentityEndorsement>(
Tables::PREVIOUS_SERVICE_IDENTITY_ENDORSEMENT)
->get();

if (!endorsement)
{
LOG_DEBUG_FMT(
"No COSE endorsement for service identity befire {}, can't provide "
"a full endorsement chain",
earliest_seqno);
return true;
}

const auto [from, till] =
ccf::crypto::extract_cose_endorsement_validity(*endorsement);

const auto from_id = TxID::from_str(from);
const auto till_id = TxID::from_str(till);

if (!from_id || !till_id)
{
LOG_FAIL_FMT("Can't parse COSE endorsement validity");
return true;
}

cose_endorsements.push_back(
{from_id->seqno, till_id->seqno, *endorsement});
}

const auto final_endorsement = std::upper_bound(
cose_endorsements.begin(),
cose_endorsements.end(),
target_seq,
[](const auto& seq, const auto& endorsement) {
return endorsement.from <= seq;
});

if (final_endorsement == cose_endorsements.end())
{
LOG_FAIL_FMT(
"Error during COSE endorsement chain reconstruction, seqno {} not "
"found",
target_seq);
return true;
}

std::vector<std::vector<uint8_t>> endorsements;
for (auto it = cose_endorsements.begin(); it <= final_endorsement; ++it)
{
endorsements.push_back(it->endorsement);
}

state->receipt->cose_endorsements = endorsements;
return true;
}
}
}
17 changes: 15 additions & 2 deletions src/node/rpc/node_frontend.h
Original file line number Diff line number Diff line change
Expand Up @@ -1509,8 +1509,21 @@ namespace ccf
"Service is already created.");
}

InternalTablesAccess::create_service(
ctx.tx, in.service_cert, in.create_txid, in.service_data, recovering);
try
{
InternalTablesAccess::create_service(
ctx.tx,
in.service_cert,
*this->network.identity->get_key_pair(),
in.create_txid,
in.service_data,
recovering);
}
catch (const std::logic_error& e)
{
return make_error(
HTTP_STATUS_FORBIDDEN, ccf::errors::InternalError, e.what());
}

// Retire all nodes, in case there are any (i.e. post recovery)
InternalTablesAccess::retire_active_nodes(ctx.tx);
Expand Down
5 changes: 4 additions & 1 deletion src/node/rpc/test/frontend_test.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -486,7 +486,10 @@ void prepare_callers(NetworkState& network)
init_network(network);

InternalTablesAccess::create_service(
tx, network.identity->cert, ccf::TxID{2, 1});
tx,
network.identity->cert,
*network.identity->get_key_pair(),
ccf::TxID{2, 1});
user_id = InternalTablesAccess::add_user(tx, {user_caller});
member_id = InternalTablesAccess::add_member(tx, member_cert);
invalid_member_id = InternalTablesAccess::add_member(tx, invalid_caller);
Expand Down
10 changes: 8 additions & 2 deletions src/node/rpc/test/node_frontend_test.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,10 @@ TEST_CASE("Add a node to an opening service")
}

InternalTablesAccess::create_service(
gen_tx, network.identity->cert, ccf::TxID{2, 1});
gen_tx,
network.identity->cert,
*network.identity->get_key_pair(),
ccf::TxID{2, 1});
REQUIRE(gen_tx.commit() == ccf::kv::CommitResult::SUCCESS);
auto tx = network.tables->create_tx();

Expand Down Expand Up @@ -199,7 +202,10 @@ TEST_CASE("Add a node to an open service")
up_to_ledger_secret_seqno, make_ledger_secret());

InternalTablesAccess::create_service(
gen_tx, network.identity->cert, ccf::TxID{2, 1});
gen_tx,
network.identity->cert,
*network.identity->get_key_pair(),
ccf::TxID{2, 1});
InternalTablesAccess::init_configuration(gen_tx, {1});
InternalTablesAccess::activate_member(
gen_tx,
Expand Down
Loading