diff --git a/CMakeLists.txt b/CMakeLists.txt index cf6c7eb0a36e..8d4ff526fa65 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -787,6 +787,7 @@ if(BUILD_TESTS) add_unit_test( history_test ${CMAKE_CURRENT_SOURCE_DIR}/src/node/test/history.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/src/enclave/enclave_time.cpp ) target_link_libraries( history_test PRIVATE ccfcrypto.host http_parser.host ccf_kv.host @@ -841,6 +842,7 @@ if(BUILD_TESTS) add_unit_test( snapshot_test ${CMAKE_CURRENT_SOURCE_DIR}/src/node/test/snapshot.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/src/enclave/enclave_time.cpp ) target_link_libraries(snapshot_test PRIVATE ccf_kv.host) @@ -1008,6 +1010,7 @@ if(BUILD_TESTS) add_picobench( history_bench SRCS src/node/test/history_bench.cpp src/enclave/thread_local.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/src/enclave/enclave_time.cpp LINK_LIBS ccf_kv.host ) diff --git a/src/crypto/openssl/cose_sign.cpp b/src/crypto/openssl/cose_sign.cpp index 3f4254aae112..fe678e771d90 100644 --- a/src/crypto/openssl/cose_sign.cpp +++ b/src/crypto/openssl/cose_sign.cpp @@ -94,6 +94,28 @@ namespace ccf::crypto } } + COSEParametersFactory cose_params_cwt_map_int_int(const CWTMap& m) + { + size_t args_size = extra_size_for_seq_tag; + for (const auto& [key, value] : m) + { + args_size += sizeof(key) + sizeof(value) + extra_size_for_int_tag + + extra_size_for_int_tag; + } + + return COSEParametersFactory( + [=](QCBOREncodeContext* ctx) { + QCBOREncode_OpenMapInMapN(ctx, COSE_PHEADER_KEY_CWT); + for (const auto& [key, value] : m) + { + QCBOREncode_AddInt64(ctx, key); + QCBOREncode_AddInt64(ctx, value); + } + QCBOREncode_CloseMap(ctx); + }, + args_size); + } + COSEParametersFactory cose_params_int_int(int64_t key, int64_t value) { const size_t args_size = sizeof(key) + sizeof(value) + diff --git a/src/crypto/openssl/cose_sign.h b/src/crypto/openssl/cose_sign.h index 06eb68483584..582a27e2a820 100644 --- a/src/crypto/openssl/cose_sign.h +++ b/src/crypto/openssl/cose_sign.h @@ -16,8 +16,20 @@ namespace ccf::crypto static constexpr int64_t COSE_PHEADER_KEY_ALG = 1; // Standardised: hash of the signing key. static constexpr int64_t COSE_PHEADER_KEY_ID = 4; + // Standardised: CWT claims map. + static constexpr int64_t COSE_PHEADER_KEY_CWT = 15; // Standardised: verifiable data structure. static constexpr int64_t COSE_PHEADER_KEY_VDS = 395; + // Standardised: issued at CWT claim. Value is **PLAIN INTEGER**, as per + // https://www.rfc-editor.org/rfc/rfc8392#section-2. Quote: + /* The "NumericDate" term in this specification has the same meaning and + * processing rules as the JWT "NumericDate" term defined in Section 2 of + * [RFC7519], except that it is represented as a CBOR numericdate (from + * Section 2.4.1 of [RFC7049]) instead of a JSON number. The encoding is + * modified so that the leading tag 1 (epoch-based date/time) MUST be + * omitted. + */ + static constexpr int64_t COSE_PHEADER_KEY_IAT = 6; // CCF-specific: last signed TxID. static const std::string COSE_PHEADER_KEY_TXID = "ccf.txid"; // CCF-specific: first TX in the range. @@ -27,6 +39,8 @@ namespace ccf::crypto // CCF-specific: Merkle root hash. static const std::string COSE_PHEADER_KEY_MERKLE_ROOT = "ccf.merkle.root"; + using CWTMap = std::unordered_map; + class COSEParametersFactory { public: @@ -51,6 +65,8 @@ namespace ccf::crypto size_t args_size{}; }; + COSEParametersFactory cose_params_cwt_map_int_int(const CWTMap& m); + COSEParametersFactory cose_params_int_int(int64_t key, int64_t value); COSEParametersFactory cose_params_int_string( diff --git a/src/node/history.h b/src/node/history.h index 988f3fc84108..b57efe44b79b 100644 --- a/src/node/history.h +++ b/src/node/history.h @@ -11,6 +11,7 @@ #include "crypto/openssl/hash.h" #include "crypto/openssl/key_pair.h" #include "ds/thread_messaging.h" +#include "enclave/enclave_time.h" #include "endian.h" #include "kv/kv_types.h" #include "kv/store.h" @@ -372,6 +373,11 @@ namespace ccf std::vector kid(SHA256_DIGEST_LENGTH); SHA256(service_key_der.data(), service_key_der.size(), kid.data()); + const auto time_since_epoch = + std::chrono::duration_cast( + ccf::get_enclave_time()) + .count(); + const auto pheaders = { // Key digest ccf::crypto::cose_params_int_bytes( @@ -381,7 +387,11 @@ namespace ccf ccf::crypto::COSE_PHEADER_KEY_VDS, vds_merkle_tree), // TxID ccf::crypto::cose_params_string_string( - ccf::crypto::COSE_PHEADER_KEY_TXID, txid.str())}; + ccf::crypto::COSE_PHEADER_KEY_TXID, txid.str()), + // iat + ccf::crypto::cose_params_cwt_map_int_int(ccf::crypto::CWTMap{ + {ccf::crypto::COSE_PHEADER_KEY_IAT, time_since_epoch}})}; + auto cose_sign = crypto::cose_sign1(service_kp, pheaders, root_hash); signatures->put(sig_value); diff --git a/src/service/internal_tables_access.h b/src/service/internal_tables_access.h index ccc52e66f7d4..7c1efb3b82cc 100644 --- a/src/service/internal_tables_access.h +++ b/src/service/internal_tables_access.h @@ -13,6 +13,7 @@ #include "ccf/tx.h" #include "consensus/aft/raft_types.h" #include "crypto/openssl/cose_sign.h" +#include "enclave/enclave_time.h" #include "node/ledger_secrets.h" #include "node/uvm_endorsements.h" #include "service/tables/governance_history.h" @@ -455,6 +456,14 @@ namespace ccf ccf::crypto::COSE_PHEADER_KEY_MERKLE_ROOT, previous_root)); } + const auto time_since_epoch = + std::chrono::duration_cast( + ccf::get_enclave_time()) + .count(); + pheaders.push_back( + ccf::crypto::cose_params_cwt_map_int_int(ccf::crypto::CWTMap{ + {ccf::crypto::COSE_PHEADER_KEY_IAT, time_since_epoch}})); + try { endorsement.endorsement = cose_sign1( diff --git a/tests/recovery.py b/tests/recovery.py index 1f696af8d7c1..e366add20a77 100644 --- a/tests/recovery.py +++ b/tests/recovery.py @@ -77,6 +77,17 @@ def verify_endorsements_chain(primary, endorsements, pubkey): root_from_headers = cose_msg.phdr["ccf.merkle.root"] assert root_from_receipt == root_from_headers + CWT_KEY = 15 + IAT_CWT_LABEL = 6 + assert ( + CWT_KEY in cose_msg.phdr and IAT_CWT_LABEL in cose_msg.phdr[CWT_KEY] + ), cose_msg.phdr + + last_five_minutes = 5 * 60 + assert ( + time.time() - cose_msg.phdr[CWT_KEY][IAT_CWT_LABEL] < last_five_minutes + ), cose_msg.phdr + next_key_bytes = cose_msg.payload pubkey = serialization.load_der_public_key(next_key_bytes, default_backend())