CCF
Loading...
Searching...
No Matches
network_identity_subsystem.h
Go to the documentation of this file.
1// Copyright (c) Microsoft Corporation. All rights reserved.
2// Licensed under the Apache 2.0 License.
3#pragma once
4
8#include "node/identity.h"
11
12#include <atomic>
13
14namespace ccf
15{
16 static std::string format_epoch(const std::optional<ccf::TxID>& epoch_end)
17 {
18 return epoch_end.has_value() ? epoch_end->to_str() : "null";
19 }
20
21 static bool is_self_endorsement(const ccf::CoseEndorsement& endorsement)
22 {
23 return !endorsement.previous_version.has_value();
24 }
25
26 static bool is_ill_formed(const ccf::CoseEndorsement& endorsement)
27 {
28 return endorsement.endorsement_epoch_end.has_value() &&
29 endorsement.endorsement_epoch_end->seqno <
31 }
32
33 static void validate_fetched_endorsement(
34 const ccf::CoseEndorsement& endorsement)
35 {
37 "Validating fetched endorsement from {} to {}",
38 endorsement.endorsement_epoch_begin.to_str(),
39 format_epoch(endorsement.endorsement_epoch_end));
40
41 if (!is_self_endorsement(endorsement))
42 {
43 const auto [from, to] =
45
46 const auto from_txid = ccf::TxID::from_str(from);
47 if (!from_txid.has_value())
48 {
49 throw std::logic_error(fmt::format(
50 "Cannot parse COSE endorsement header: {}",
51 ccf::cose::header::custom::TX_RANGE_BEGIN));
52 }
53
54 const auto to_txid = ccf::TxID::from_str(to);
55 if (!to_txid.has_value())
56 {
57 throw std::logic_error(fmt::format(
58 "Cannot parse COSE endorsement header: {}",
59 ccf::cose::header::custom::TX_RANGE_END));
60 }
61
62 if (!endorsement.endorsement_epoch_end.has_value())
63 {
64 throw std::logic_error(
65 "COSE endorsement does not contain epoch end in the table entry");
66 }
67 if (
68 endorsement.endorsement_epoch_begin != *from_txid ||
69 *endorsement.endorsement_epoch_end != *to_txid)
70 {
71 throw std::logic_error(fmt::format(
72 "COSE endorsement fetched but range is invalid, epoch begin {}, "
73 "epoch end {}, header epoch begin: {}, header epoch end: {}",
74 endorsement.endorsement_epoch_begin.to_str(),
75 endorsement.endorsement_epoch_end->to_str(),
76 from,
77 to));
78 }
79 }
80 }
81
82 static void validate_chain_integrity(
83 const ccf::CoseEndorsement& newer, const ccf::CoseEndorsement& older)
84 {
85 if (!older.endorsement_epoch_end.has_value())
86 {
87 throw std::logic_error(fmt::format(
88 "COSE endorsement chain integrity is violated, previous endorsement "
89 "from {} does not have an epoch end",
91 }
92
93 if (
94 newer.endorsement_epoch_begin.view - aft::starting_view_change !=
95 older.endorsement_epoch_end->view ||
97 older.endorsement_epoch_end->seqno)
98 {
99 throw std::logic_error(fmt::format(
100 "COSE endorsement chain integrity is violated, previous endorsement "
101 "epoch end {} is not chained with newer endorsement epoch begin {}",
102 older.endorsement_epoch_end->to_str(),
104 }
105 }
106
108 {
109 protected:
111 const std::unique_ptr<NetworkIdentity>& network_identity;
112 std::shared_ptr<historical::StateCacheImpl> historical_cache;
113 std::map<SeqNo, CoseEndorsement> endorsements;
114 std::map<SeqNo, ccf::crypto::ECPublicKeyPtr> trusted_keys;
115 std::optional<TxID> current_service_from;
117 std::atomic<FetchStatus> fetch_status{FetchStatus::Retry};
118 bool has_predecessors{false};
119
120 public:
122 AbstractNodeState& node_state_,
123 const std::unique_ptr<NetworkIdentity>& network_identity_,
124 std::shared_ptr<ccf::historical::StateCacheImpl> historical_cache_) :
125 node_state(node_state_),
126 network_identity(network_identity_),
127 historical_cache(std::move(historical_cache_))
128 {
129 fetch_first();
130 }
131
132 [[nodiscard]] FetchStatus endorsements_fetching_status() const override
133 {
134 return fetch_status.load();
135 }
136
137 const std::unique_ptr<NetworkIdentity>& get() override
138 {
139 return network_identity;
140 }
141
142 [[nodiscard]] std::optional<CoseEndorsementsChain>
144 {
145 if (fetch_status.load() != FetchStatus::Done)
146 {
147 throw IdentityHistoryNotFetched(fmt::format(
148 "COSE endorsements chain requested for seqno {} but identity "
149 "history fetching has not been completed yet",
150 seqno));
151 }
152
153 if (!current_service_from.has_value())
154 {
156 "Unset current_service_from when fetching endorsements chain");
157 return std::nullopt;
158 }
159
160 if (!has_predecessors || seqno >= current_service_from->seqno)
161 {
162 return CoseEndorsementsChain{};
163 }
164
165 auto it = endorsements.upper_bound(seqno);
166 if (it == endorsements.begin())
167 {
169 "No endorsements found for seqno {}, earliest endorsed is {}",
170 seqno,
172 return {};
173 }
174
176 for (--it; it != endorsements.end(); ++it)
177 {
178 result.push_back(it->second.endorsement);
179 }
180 std::reverse(result.begin(), result.end());
181 return result;
182 }
183
185 ccf::SeqNo seqno) const override
186 {
187 if (fetch_status.load() != FetchStatus::Done)
188 {
189 throw IdentityHistoryNotFetched(fmt::format(
190 "Trusted key requested for seqno {} but identity history "
191 "fetching has not been completed yet",
192 seqno));
193 }
194 if (trusted_keys.empty())
195 {
196 throw std::logic_error(fmt::format(
197 "No trusted keys fetched when requested one for seqno {}", seqno));
198 }
199 auto it = trusted_keys.upper_bound(seqno);
200 if (it == trusted_keys.begin())
201 {
202 // The earliest known trusted seqno is greater than the requested one.
203 return nullptr;
204 }
205 const auto& [key_seqno, key_ptr] = *(--it);
206 if (key_seqno > seqno)
207 {
208 throw std::logic_error(fmt::format(
209 "Resolved trusted key for {} with wrong starting seqno {}",
210 seqno,
211 key_seqno));
212 }
213 return key_ptr;
214 }
215
216 [[nodiscard]] TrustedKeys get_trusted_keys() const override
217 {
218 if (fetch_status.load() != FetchStatus::Done)
219 {
221 "Trusted keys requested but identity history fetching has not "
222 "completed yet");
223 }
224 return trusted_keys;
225 }
226
227 private:
228 void retry_first_fetch()
229 {
230 using namespace std::chrono_literals;
231 static constexpr auto retry_after = 1s;
233 ccf::tasks::make_basic_task([this]() { this->fetch_first(); }),
234 retry_after);
235 }
236
237 void fail_fetching(const std::string& err = "")
238 {
239 if (!err.empty())
240 {
241 LOG_FAIL_FMT("Failed fetching network identity: {}", err);
242 }
244
245 // The caller may want to re-capture this, but by default it's supposed to
246 // fail the node startup early. This is purely reading, so there's no risk
247 // of corruption, but the endorsement chain is essential for the node to
248 // produce receipts for the past epochs, which is a must-have
249 // functionality.
250 throw std::runtime_error("Failed fetching network identity: " + err);
251 }
252
253 void complete_fetching()
254 {
255 if (!current_service_from.has_value())
256 {
257 fail_fetching("Unset current_service_from when completing fetching");
258 return; // to silence clang-tidy unchecked optional
259 }
260
261 if (!endorsements.empty())
262 {
263 auto next = endorsements.begin();
264 auto prev = next++;
265 try
266 {
267 while (next != endorsements.end())
268 {
269 validate_chain_integrity(next->second, prev->second);
270 ++prev;
271 ++next;
272 }
273 }
274 catch (const std::exception& e)
275 {
276 fail_fetching(e.what());
277 }
278
279 const auto& last = prev->second;
280 if (!last.endorsement_epoch_end.has_value())
281 {
282 fail_fetching(fmt::format(
283 "The last fetched endorsement at {} has no epoch end",
284 last.endorsement_epoch_begin.seqno));
285 return; // to silence clang-tidy unchecked optional
286 }
287
288 if (
289 current_service_from->view - aft::starting_view_change !=
290 last.endorsement_epoch_end->view ||
291 current_service_from->seqno - 1 != last.endorsement_epoch_end->seqno)
292 {
293 fail_fetching(fmt::format(
294 "COSE endorsement chain integrity is violated, the current "
295 "service start at {} is not chained with previous endorsement "
296 "ending at {}",
297 current_service_from->to_str(),
298 last.endorsement_epoch_end->to_str()));
299 }
300 }
301
302 try
303 {
304 build_trusted_key_chain();
305 }
306 catch (const std::exception& e)
307 {
308 fail_fetching(e.what());
309 }
310
312 }
313
314 void fetch_first()
315 {
317 {
319 "Retry fetching network identity as node is not part of the network "
320 "yet");
321 retry_first_fetch();
322 return;
323 }
324
325 auto store = node_state.get_store();
326 auto tx = store->create_read_only_tx();
327
328 if (!current_service_from.has_value())
329 {
330 auto* service_info_handle =
331 tx.template ro<ccf::Service>(ccf::Tables::SERVICE);
332 auto service_info = service_info_handle->get();
333 if (
334 !service_info ||
335 !service_info->current_service_create_txid.has_value())
336 {
338 "Retrying fetching network identity as current service create txid "
339 "is not yet available");
340 retry_first_fetch();
341 return;
342 }
343
344 if (service_info->status != ServiceStatus::OPEN)
345 {
346 // It can happen that node advances its internal state machine to
347 // part-of-network, but the service opening tx has not been replicated
348 // yet. This will cause the first fetched endorsement to be obsolete,
349 // but waiting for ServiceStatus::OPEN is sufficient, as it's supposed
350 // to arrive in the same TX that the previous identity endorsement.
352 "Retrying fetching network identity as service is not yet open");
353 retry_first_fetch();
354 return;
355 }
356
357 current_service_from = service_info->current_service_create_txid;
358 }
359
360 auto* previous_identity_endorsement =
362 ccf::Tables::PREVIOUS_SERVICE_IDENTITY_ENDORSEMENT);
363
364 auto endorsement = previous_identity_endorsement->get();
365 if (!endorsement.has_value())
366 {
368 "Retrying fetching network identity as there is no previous service "
369 "identity endorsement yet");
370 retry_first_fetch();
371 return;
372 }
373
374 if (is_self_endorsement(endorsement.value()))
375 {
376 if (
377 current_service_from->seqno !=
378 endorsement->endorsement_epoch_begin.seqno)
379 {
380 fail_fetching(fmt::format(
381 "The first fetched endorsement is a self-endorsement with seqno {} "
382 "which is different from current_service_create_txid {}",
383 endorsement->endorsement_epoch_begin.seqno,
384 current_service_from->seqno));
385 }
386
388 "The very first service endorsement is self-signed at {}, no "
389 "endorsement chain will be preloaded",
390 current_service_from->seqno);
391
392 has_predecessors = false;
393 complete_fetching();
394 return;
395 }
396
397 has_predecessors = true;
399 process_endorsement(endorsement.value());
400 }
401
402 void process_endorsement(const ccf::CoseEndorsement& endorsement)
403 {
404 if (is_ill_formed(endorsement))
405 {
406 // For double-sealed cases, which could have happened in the past. We
407 // mark with failed logs, but skip intentionally if there are other
408 // endorsements that follow. The overall chain integrity will be checked
409 // at the end and will fail anyway if it's not intact.
410 if (endorsement.previous_version.has_value())
411 {
413 "Fetched endorsement for {} - {} is ill-formed but has a "
414 "predecessor, so skipping this entry",
415 endorsement.endorsement_epoch_begin.to_str(),
416 format_epoch(endorsement.endorsement_epoch_end));
417 fetch_next_at(endorsement.previous_version.value());
418 return;
419 }
420 fail_fetching(fmt::format(
421 "Found an ill-formed endorsement for {} - {} which has no "
422 "predecessor",
423 endorsement.endorsement_epoch_begin.to_str(),
424 format_epoch(endorsement.endorsement_epoch_end)));
425 }
426
427 const auto from = endorsement.endorsement_epoch_begin.seqno;
428 if (is_self_endorsement(endorsement))
429 {
430 if (endorsements.find(from) == endorsements.end())
431 {
432 fail_fetching(fmt::format(
433 "Fetched self-endorsement with seqno {} which has not been seen",
434 from));
435 }
436 LOG_INFO_FMT("Got self-endorsement at {}, stopping fetching", from);
437 complete_fetching();
438 return;
439 }
440
441 if (from >= earliest_endorsed_seq)
442 {
443 fail_fetching(fmt::format(
444 "Fetched service endorsement with seqno {} which is greater than "
445 "the earliest known in the chain {}",
446 from,
448 }
449
450 if (!endorsement.endorsement_epoch_end.has_value())
451 {
452 fail_fetching(
453 fmt::format("Fetched endorsement at {} has no epoch end", from));
454 return; // to silence clang-tidy unchecked optional
455 }
456
458 if (endorsements.find(from) != endorsements.end())
459 {
460 fail_fetching(fmt::format(
461 "Fetched service endorsement with seqno {} which already exists",
462 from));
463 }
464
466 "Fetched service endorsement from {} to {}",
467 from,
468 endorsement.endorsement_epoch_end->seqno);
469 endorsements.insert({from, endorsement});
470
471 if (endorsement.previous_version.has_value())
472 {
473 fetch_next_at(endorsement.previous_version.value());
474 return;
475 }
476
477 complete_fetching();
478 }
479
480 void build_trusted_key_chain()
481 {
482 if (!current_service_from.has_value())
483 {
484 throw std::logic_error(
485 "Attempting to build trusted key chain but no current service "
486 "created seqno fetched");
487 }
488
489 std::span<const uint8_t> previous_key_der{};
490 for (const auto& [seqno, endorsement] : endorsements)
491 {
492 auto verifier =
494 std::span<uint8_t> endorsed_key;
495 if (!verifier->verify(endorsement.endorsement, endorsed_key))
496 {
497 throw std::logic_error(fmt::format(
498 "COSE endorsement chain integrity is violated, endorsement from {} "
499 "to {} failed signature verification",
500 endorsement.endorsement_epoch_begin.to_str(),
501 format_epoch(endorsement.endorsement_epoch_end)));
502 }
503
505 "Adding trusted seq {} key {}",
506 endorsement.endorsement_epoch_begin.seqno,
507 ccf::crypto::b64_from_raw(endorsed_key));
508 trusted_keys.insert(
509 {endorsement.endorsement_epoch_begin.seqno,
510 ccf::crypto::make_ec_public_key(endorsed_key)});
511
512 if (
513 !previous_key_der.empty() &&
514 !std::equal(
515 previous_key_der.begin(),
516 previous_key_der.end(),
517 endorsed_key.begin(),
518 endorsed_key.end()))
519 {
520 throw std::logic_error(fmt::format(
521 "Endorsement from {} to {} over public key {} doesn't chain with "
522 "the previous endorsement with key {}",
523 endorsement.endorsement_epoch_begin.seqno,
524 format_epoch(endorsement.endorsement_epoch_end),
525 ccf::ds::to_hex(endorsed_key),
526 ccf::ds::to_hex(previous_key_der)));
527 }
528
529 previous_key_der = endorsement.endorsing_key;
530 }
531
532 const auto& current_pkey =
533 network_identity->get_key_pair()->public_key_der();
534 if (
535 !previous_key_der.empty() &&
536 !std::equal(
537 previous_key_der.begin(),
538 previous_key_der.end(),
539 current_pkey.begin(),
540 current_pkey.end()))
541 {
542 throw std::logic_error(fmt::format(
543 "Current service identity public key {} does not match the last "
544 "endorsing key {}",
545 ccf::ds::to_hex(current_pkey),
546 ccf::ds::to_hex(previous_key_der)));
547 }
548
550 "Adding trusted seq {} key {}",
552 ccf::crypto::b64_from_raw(current_pkey));
553 trusted_keys.insert(
554 {current_service_from->seqno,
555 ccf::crypto::make_ec_public_key(current_pkey)});
556 }
557
558 void fetch_next_at(ccf::SeqNo seq)
559 {
560 auto state = historical_cache->get_state_at(
563 seq);
564 if (!state)
565 {
566 retry_fetch_next(seq);
567 return;
568 }
569
570 if (!state->store)
571 {
572 fail_fetching(fmt::format(
573 "Fetched historical state with seqno {} with missing store", seq));
574 }
575 auto htx = state->store->create_read_only_tx();
576 const auto endorsement =
577 htx
578 .template ro<ccf::PreviousServiceIdentityEndorsement>(
579 ccf::Tables::PREVIOUS_SERVICE_IDENTITY_ENDORSEMENT)
580 ->get();
581
582 if (!endorsement.has_value())
583 {
584 fail_fetching(
585 fmt::format("Fetched COSE endorsement for {} is invalid", seq));
586 return; // to silence clang-tidy unchecked optional
587 }
588
589 try
590 {
591 validate_fetched_endorsement(endorsement.value());
592 }
593 catch (const std::exception& e)
594 {
595 fail_fetching(e.what());
596 }
597
598 process_endorsement(endorsement.value());
599 }
600
601 void retry_fetch_next(ccf::SeqNo seq)
602 {
603 using namespace std::chrono_literals;
604 static constexpr auto retry_after = 100ms;
607 [this, seq]() { this->fetch_next_at(seq); }),
608 retry_after);
609 }
610 };
611}
Definition node_interface.h:23
virtual bool is_part_of_network() const =0
virtual std::shared_ptr< ccf::kv::Store > get_store()=0
Definition network_identity_interface.h:53
Definition network_identity_subsystem.h:108
ccf::crypto::ECPublicKeyPtr get_trusted_identity_for(ccf::SeqNo seqno) const override
Definition network_identity_subsystem.h:184
std::map< SeqNo, CoseEndorsement > endorsements
Definition network_identity_subsystem.h:113
FetchStatus endorsements_fetching_status() const override
Returns the current status of endorsement fetching.
Definition network_identity_subsystem.h:132
std::optional< CoseEndorsementsChain > get_cose_endorsements_chain(ccf::SeqNo seqno) const override
Definition network_identity_subsystem.h:143
const std::unique_ptr< NetworkIdentity > & network_identity
Definition network_identity_subsystem.h:111
std::atomic< FetchStatus > fetch_status
Definition network_identity_subsystem.h:117
SeqNo earliest_endorsed_seq
Definition network_identity_subsystem.h:116
AbstractNodeState & node_state
Definition network_identity_subsystem.h:110
std::shared_ptr< historical::StateCacheImpl > historical_cache
Definition network_identity_subsystem.h:112
const std::unique_ptr< NetworkIdentity > & get() override
Returns a reference to the current network identity.
Definition network_identity_subsystem.h:137
std::optional< TxID > current_service_from
Definition network_identity_subsystem.h:115
bool has_predecessors
Definition network_identity_subsystem.h:118
std::map< SeqNo, ccf::crypto::ECPublicKeyPtr > trusted_keys
Definition network_identity_subsystem.h:114
NetworkIdentitySubsystem(AbstractNodeState &node_state_, const std::unique_ptr< NetworkIdentity > &network_identity_, std::shared_ptr< ccf::historical::StateCacheImpl > historical_cache_)
Definition network_identity_subsystem.h:121
TrustedKeys get_trusted_keys() const override
Definition network_identity_subsystem.h:216
Definition value.h:32
#define LOG_INFO_FMT
Definition internal_logger.h:15
#define LOG_FAIL_FMT
Definition internal_logger.h:16
std::string b64_from_raw(const uint8_t *data, size_t size)
Definition base64.cpp:41
std::shared_ptr< ECPublicKey > ECPublicKeyPtr
Definition ec_public_key.h:158
COSEVerifierUniquePtr make_cose_verifier_from_key(const Pem &public_key)
Definition cose_verifier.cpp:291
ECPublicKeyPtr make_ec_public_key(const Pem &pem)
Definition ec_public_key.cpp:331
COSEEndorsementValidity extract_cose_endorsement_validity(std::span< const uint8_t > cose_msg)
Definition cose_verifier.cpp:302
std::pair< RequestNamespace, RequestHandle > CompoundHandle
Definition historical_queries.h:38
Task make_basic_task(Ts &&... ts)
Definition basic_task.h:33
void add_delayed_task(Task task, std::chrono::milliseconds delay)
Definition task_system.cpp:70
Definition app_interface.h:13
FetchStatus
Status of the network identity endorsement fetching process.
Definition network_identity_interface.h:26
@ Retry
Fetching should be retried.
@ Failed
Fetching failed.
@ Done
Fetching completed successfully.
std::map< ccf::SeqNo, ccf::crypto::ECPublicKeyPtr > TrustedKeys
Definition network_identity_interface.h:34
std::vector< RawCoseEndorsement > CoseEndorsementsChain
An ordered chain of raw COSE endorsements.
Definition network_identity_interface.h:22
uint64_t SeqNo
Definition tx_id.h:36
STL namespace.
Definition previous_service_identity.h:18
std::optional< ccf::TxID > endorsement_epoch_end
Definition previous_service_identity.h:31
std::optional< ccf::kv::Version > previous_version
Definition previous_service_identity.h:35
std::vector< uint8_t > endorsement
COSE-sign of the a previous service identity's public key.
Definition previous_service_identity.h:20
std::vector< uint8_t > endorsing_key
Service key at the moment of endorsing.
Definition previous_service_identity.h:23
ccf::TxID endorsement_epoch_begin
The transaction ID when the endorsed service was created.
Definition previous_service_identity.h:26
Definition network_identity_interface.h:39
SeqNo seqno
Definition tx_id.h:46
View view
Definition tx_id.h:45
std::string to_str() const
Definition tx_id.h:48
static std::optional< TxID > from_str(const std::string_view &sv)
Definition tx_id.h:53