diff --git a/cddl/ccf-cose-endorsement-service-identity.cddl b/cddl/ccf-cose-endorsement-service-identity.cddl new file mode 100644 index 000000000..4243f8cac --- /dev/null +++ b/cddl/ccf-cose-endorsement-service-identity.cddl @@ -0,0 +1,28 @@ +ccf-cose-endorsement-tagged = #6.18(ccf-cose-endorsement) + +ccf-cose-endorsement = [ + phdr : bstr .cbor protected-headers, ; bstr-wrapped protected headers + uhdr : unprotected-headers, ; unwrappeed (plain map) unprotected headers + payload : bstr, ; previous servide identity public key + signature : bstr ; COSE-signature +] + +unprotected-headers = { + ; empty map instead of 'nil'. QCBOR library verifier doesn't support 'nil'. +} + +protected-headers = { + &(alg: 1) => int, ; signing algoritm ID, as per RFC8152 + &(cwt: 15) => cwt-map, ; CWT claims, as per RFC8392 + &(ccf: "ccf.v1") => ccf-map ; a set of CCF-specific parameters +} + +cwt-map = { + &(iat: 6) => int ; "issued at", number of seconds since the epoch +} + +ccf-map = { + &(endorsed-from: "epoch.start.txid") => tstr, ; first committed TxID in the endorsed epoch + ? &(endorsed-to: "epoch.end.txid") => tstr, ; last committed TxID in the endorsed epoch + ? &(last-endorsed-root: "epoch.end.merkle.root") => bstr .size 32 ; Merkle tree root of the last TxID in the endorsed epoch (string of HASH_SIZE(32) bytes) +} diff --git a/cddl/ccf-merkle-tree-cose-signature.cddl b/cddl/ccf-merkle-tree-cose-signature.cddl new file mode 100644 index 000000000..f73d4dd15 --- /dev/null +++ b/cddl/ccf-merkle-tree-cose-signature.cddl @@ -0,0 +1,28 @@ +ccf-cose-root-signature-tagged = #6.18(ccf-cose-root-signature) + +ccf-cose-root-signature = [ + phdr : bstr .cbor protected-headers, ; bstr-wrapped protected headers + uhdr : unprotected-headers, ; unwrappeed (plain map) unprotected headers + payload : nil, ; signed Merkle tree root hash, *detached* payload + signature : bstr ; COSE-signature +] + +unprotected-headers = { + ; empty map instead of 'nil'. QCBOR library verifier doesn't support 'nil'. +} + +protected-headers = { + &(alg: 1) => int, ; signing algoritm ID, as per RFC8152 + &(kid: 4) => bstr, ; signing key hash + &(cwt: 15) => cwt-map, ; CWT claims, as per RFC8392 + &(vds: 395) => int, ; verifiable data structure, as per COSE Receipts (draft) RFC (https://datatracker.ietf.org/doc/draft-ietf-cose-merkle-tree-proofs/) + "ccf.v1" => ccf-map ; a set of CCF-specific parameters +} + +cwt-map = { + &(iat: 6) => int ; "issued at", number of seconds since the epoch +} + +ccf-map = { + &(last-signed-txid: "txid") => tstr ; last committed transaction ID this COSE-signature signs +} diff --git a/doc/schemas/app_openapi.json b/doc/schemas/app_openapi.json index 70942e22f..b225066f1 100644 --- a/doc/schemas/app_openapi.json +++ b/doc/schemas/app_openapi.json @@ -109,6 +109,17 @@ ], "type": "object" }, + "LoggingGetCoseSignature__Out": { + "properties": { + "cose_signature": { + "$ref": "#/components/schemas/base64string" + } + }, + "required": [ + "cose_signature" + ], + "type": "object" + }, "LoggingGetHistoricalRange__Entry": { "properties": { "id": { @@ -1262,6 +1273,37 @@ } } }, + "/app/log/public/cose_signature": { + "get": { + "operationId": "GetAppLogPublicCoseSignature", + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/LoggingGetCoseSignature__Out" + } + } + }, + "description": "Default response description" + }, + "default": { + "$ref": "#/components/responses/default" + } + }, + "security": [ + { + "jwt": [] + }, + { + "user_cose_sign1": [] + } + ], + "x-ccf-forwarding": { + "$ref": "#/components/x-ccf-forwarding/never" + } + } + }, "/app/log/public/count": { "get": { "operationId": "GetAppLogPublicCount", diff --git a/include/ccf/receipt.h b/include/ccf/receipt.h index 96a58eeee..4fc7b21aa 100644 --- a/include/ccf/receipt.h +++ b/include/ccf/receipt.h @@ -148,9 +148,12 @@ namespace ccf const TxReceiptImpl& in); using SerialisedCoseEndorsement = std::vector; + using SerialisedCoseSignature = std::vector; using SerialisedCoseEndorsements = std::vector; std::optional describe_cose_endorsements_v1( const TxReceiptImpl& in); + std::optional describe_cose_signature_v1( + const TxReceiptImpl& receipt); // Manual JSON serializers are specified for these types as they are not // trivial POD structs diff --git a/samples/apps/logging/logging.cpp b/samples/apps/logging/logging.cpp index b3f8afc6d..f6bb2ca40 100644 --- a/samples/apps/logging/logging.cpp +++ b/samples/apps/logging/logging.cpp @@ -2002,6 +2002,42 @@ namespace loggingapp .set_auto_schema() .set_forwarding_required(ccf::endpoints::ForwardingRequired::Never) .install(); + + auto get_cose_signature = [this]( + ccf::endpoints::ReadOnlyEndpointContext& ctx, + ccf::historical::StatePtr historical_state) { + auto historical_tx = historical_state->store->create_read_only_tx(); + + assert(historical_state->receipt); + auto signature = describe_cose_signature_v1(*historical_state->receipt); + if (!signature.has_value()) + { + ctx.rpc_ctx->set_error( + HTTP_STATUS_NOT_FOUND, + ccf::errors::ResourceNotFound, + "No COSE signature available for this transaction"); + return; + } + + LoggingGetCoseSignature::Out response{ + .cose_signature = signature.value()}; + + nlohmann::json j_response = response; + ctx.rpc_ctx->set_response_status(HTTP_STATUS_OK); + ctx.rpc_ctx->set_response_header( + ccf::http::headers::CONTENT_TYPE, + ccf::http::headervalues::contenttype::JSON); + ctx.rpc_ctx->set_response_body(j_response.dump()); + }; + make_read_only_endpoint( + "/log/public/cose_signature", + HTTP_GET, + ccf::historical::read_only_adapter_v4( + get_cose_signature, context, is_tx_committed), + auth_policies) + .set_auto_schema() + .set_forwarding_required(ccf::endpoints::ForwardingRequired::Never) + .install(); } }; } diff --git a/samples/apps/logging/logging_schema.h b/samples/apps/logging/logging_schema.h index b908061bf..f8d912dc1 100644 --- a/samples/apps/logging/logging_schema.h +++ b/samples/apps/logging/logging_schema.h @@ -108,6 +108,16 @@ namespace loggingapp DECLARE_JSON_TYPE(LoggingGetCoseEndorsements::Out); DECLARE_JSON_REQUIRED_FIELDS(LoggingGetCoseEndorsements::Out, endorsements); + struct LoggingGetCoseSignature + { + struct Out + { + ccf::SerialisedCoseSignature cose_signature; + }; + }; + DECLARE_JSON_TYPE(LoggingGetCoseSignature::Out); + DECLARE_JSON_REQUIRED_FIELDS(LoggingGetCoseSignature::Out, cose_signature); + // Public record/get // Manual schemas, verified then parsed in handler static const std::string j_record_public_in = R"!!!( diff --git a/src/crypto/openssl/cose_sign.cpp b/src/crypto/openssl/cose_sign.cpp index fe678e771..5b6f42118 100644 --- a/src/crypto/openssl/cose_sign.cpp +++ b/src/crypto/openssl/cose_sign.cpp @@ -13,7 +13,7 @@ namespace static constexpr size_t extra_size_for_seq_tag = 1 + 8; // type + size size_t estimate_buffer_size( - const std::vector& protected_headers, + const ccf::crypto::COSEHeadersArray& protected_headers, std::span payload) { size_t result = @@ -28,7 +28,7 @@ namespace protected_headers.end(), result, [](auto result, const auto& factory) { - return result + factory.estimated_size(); + return result + factory->estimated_size(); }); return result + payload.size(); @@ -37,7 +37,7 @@ namespace void encode_protected_headers( t_cose_sign1_sign_ctx* ctx, QCBOREncodeContext* encode_ctx, - const std::vector& protected_headers) + const ccf::crypto::COSEHeadersArray& protected_headers) { QCBOREncode_BstrWrap(encode_ctx); QCBOREncode_OpenMap(encode_ctx); @@ -50,7 +50,7 @@ namespace // Caller-provided headers follow for (const auto& factory : protected_headers) { - factory.apply(encode_ctx); + factory->apply(encode_ctx); } QCBOREncode_CloseMap(encode_ctx); @@ -67,7 +67,7 @@ namespace void encode_parameters_custom( struct t_cose_sign1_sign_ctx* me, QCBOREncodeContext* cbor_encode, - const std::vector& protected_headers) + const ccf::crypto::COSEHeadersArray& protected_headers) { encode_protected_headers(me, cbor_encode, protected_headers); @@ -94,57 +94,91 @@ 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; - } + COSEMapIntKey::COSEMapIntKey(int64_t key_) : key(key_) {} - 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); + void COSEMapIntKey::apply(QCBOREncodeContext* ctx) const + { + QCBOREncode_AddInt64(ctx, key); } - COSEParametersFactory cose_params_int_int(int64_t key, int64_t value) + size_t COSEMapIntKey::estimated_size() const + { + return sizeof(key) + extra_size_for_int_tag; + } + + COSEMapStringKey::COSEMapStringKey(const std::string& key_) : key(key_) {} + + void COSEMapStringKey::apply(QCBOREncodeContext* ctx) const + { + QCBOREncode_AddSZString(ctx, key.c_str()); + } + + size_t COSEMapStringKey::estimated_size() const + { + return key.size() + extra_size_for_seq_tag; + } + + COSEParametersMap::COSEParametersMap( + std::shared_ptr key_, + const std::vector>& factories_) : + key(std::move(key_)), + factories(factories_) + {} + + void COSEParametersMap::apply(QCBOREncodeContext* ctx) const + { + key->apply(ctx); + QCBOREncode_OpenMap(ctx); + for (const auto& f : factories) + { + f->apply(ctx); + } + QCBOREncode_CloseMap(ctx); + } + + size_t COSEParametersMap::estimated_size() const + { + size_t value = key->estimated_size() + extra_size_for_seq_tag; + std::accumulate( + factories.begin(), + factories.end(), + value, + [](auto value, const auto& factory) { + return value + factory->estimated_size(); + }); + return value; + } + + std::shared_ptr cose_params_int_int( + int64_t key, int64_t value) { const size_t args_size = sizeof(key) + sizeof(value) + extra_size_for_int_tag + extra_size_for_int_tag; - return COSEParametersFactory( + return std::make_shared( [=](QCBOREncodeContext* ctx) { QCBOREncode_AddInt64ToMapN(ctx, key, value); }, args_size); } - COSEParametersFactory cose_params_int_string( + std::shared_ptr cose_params_int_string( int64_t key, const std::string& value) { const size_t args_size = sizeof(key) + value.size() + extra_size_for_int_tag + extra_size_for_seq_tag; - return COSEParametersFactory( + return std::make_shared( [=](QCBOREncodeContext* ctx) { QCBOREncode_AddSZStringToMapN(ctx, key, value.data()); }, args_size); } - COSEParametersFactory cose_params_string_int( + std::shared_ptr cose_params_string_int( const std::string& key, int64_t value) { const size_t args_size = key.size() + sizeof(value) + extra_size_for_seq_tag + extra_size_for_int_tag; - return COSEParametersFactory( + return std::make_shared( [=](QCBOREncodeContext* ctx) { QCBOREncode_AddSZString(ctx, key.data()); QCBOREncode_AddInt64(ctx, value); @@ -152,12 +186,12 @@ namespace ccf::crypto args_size); } - COSEParametersFactory cose_params_string_string( + std::shared_ptr cose_params_string_string( const std::string& key, const std::string& value) { const size_t args_size = key.size() + value.size() + extra_size_for_seq_tag + extra_size_for_seq_tag; - return COSEParametersFactory( + return std::make_shared( [=](QCBOREncodeContext* ctx) { QCBOREncode_AddSZString(ctx, key.data()); QCBOREncode_AddSZString(ctx, value.data()); @@ -165,26 +199,26 @@ namespace ccf::crypto args_size); } - COSEParametersFactory cose_params_int_bytes( + std::shared_ptr cose_params_int_bytes( int64_t key, const std::vector& value) { const size_t args_size = sizeof(key) + value.size() + +extra_size_for_int_tag + extra_size_for_seq_tag; q_useful_buf_c buf{value.data(), value.size()}; - return COSEParametersFactory( + return std::make_shared( [=](QCBOREncodeContext* ctx) { QCBOREncode_AddBytesToMapN(ctx, key, buf); }, args_size); } - COSEParametersFactory cose_params_string_bytes( + std::shared_ptr cose_params_string_bytes( const std::string& key, const std::vector& value) { const size_t args_size = key.size() + value.size() + extra_size_for_seq_tag + extra_size_for_seq_tag; q_useful_buf_c buf{value.data(), value.size()}; - return COSEParametersFactory( + return std::make_shared( [=](QCBOREncodeContext* ctx) { QCBOREncode_AddSZString(ctx, key.data()); QCBOREncode_AddBytes(ctx, buf); @@ -194,7 +228,8 @@ namespace ccf::crypto std::vector cose_sign1( const KeyPair_OpenSSL& key, - const std::vector& protected_headers, + const std::vector>& + protected_headers, std::span payload, bool detached_payload) { diff --git a/src/crypto/openssl/cose_sign.h b/src/crypto/openssl/cose_sign.h index 582a27e2a..af7567961 100644 --- a/src/crypto/openssl/cose_sign.h +++ b/src/crypto/openssl/cose_sign.h @@ -30,32 +30,114 @@ namespace ccf::crypto * omitted. */ static constexpr int64_t COSE_PHEADER_KEY_IAT = 6; + // CCF headers nested map key. + static const std::string COSE_PHEADER_KEY_CCF = "ccf.v1"; // CCF-specific: last signed TxID. - static const std::string COSE_PHEADER_KEY_TXID = "ccf.txid"; + static const std::string COSE_PHEADER_KEY_TXID = "txid"; // CCF-specific: first TX in the range. - static const std::string COSE_PHEADER_KEY_RANGE_BEGIN = "ccf.epoch.begin"; + static const std::string COSE_PHEADER_KEY_RANGE_BEGIN = "epoch.start.txid"; // CCF-specific: last TX included in the range. - static const std::string COSE_PHEADER_KEY_RANGE_END = "ccf.epoch.end"; - // CCF-specific: Merkle root hash. - static const std::string COSE_PHEADER_KEY_MERKLE_ROOT = "ccf.merkle.root"; + static const std::string COSE_PHEADER_KEY_RANGE_END = "epoch.end.txid"; + // CCF-specific: last signed Merkle root hash in the range. + static const std::string COSE_PHEADER_KEY_EPOCH_LAST_MERKLE_ROOT = + "epoch.end.merkle.root"; - using CWTMap = std::unordered_map; + class COSEMapKey + { + public: + virtual void apply(QCBOREncodeContext* ctx) const = 0; + virtual size_t estimated_size() const = 0; + + virtual ~COSEMapKey() = default; + }; + + class COSEMapIntKey : public COSEMapKey + { + public: + COSEMapIntKey(int64_t key_); + ~COSEMapIntKey() override = default; + + void apply(QCBOREncodeContext* ctx) const override; + size_t estimated_size() const override; + + private: + int64_t key; + }; + + class COSEMapStringKey : public COSEMapKey + { + public: + COSEMapStringKey(const std::string& key_); + ~COSEMapStringKey() override = default; + + void apply(QCBOREncodeContext* ctx) const override; + size_t estimated_size() const override; + + private: + std::string key; + }; class COSEParametersFactory { + public: + virtual void apply(QCBOREncodeContext* ctx) const = 0; + virtual size_t estimated_size() const = 0; + + virtual ~COSEParametersFactory() = default; + }; + + class COSEParametersMap : public COSEParametersFactory + { + public: + COSEParametersMap( + std::shared_ptr key_, + const std::vector>& factories_); + + void apply(QCBOREncodeContext* ctx) const override; + size_t estimated_size() const override; + + virtual ~COSEParametersMap() = default; + + private: + std::shared_ptr key; + std::vector> factories{}; + }; + + std::shared_ptr cose_params_int_int( + int64_t key, int64_t value); + + std::shared_ptr cose_params_int_string( + int64_t key, const std::string& value); + + std::shared_ptr cose_params_string_int( + const std::string& key, int64_t value); + + std::shared_ptr cose_params_string_string( + const std::string& key, const std::string& value); + + std::shared_ptr cose_params_int_bytes( + int64_t key, const std::vector& value); + + std::shared_ptr cose_params_string_bytes( + const std::string& key, const std::vector& value); + + class COSEParametersPair : public COSEParametersFactory + { public: template - COSEParametersFactory(Callable&& impl, size_t args_size) : + COSEParametersPair(Callable&& impl, size_t args_size) : impl(std::forward(impl)), args_size{args_size} {} - void apply(QCBOREncodeContext* ctx) const + virtual ~COSEParametersPair() = default; + + void apply(QCBOREncodeContext* ctx) const override { impl(ctx); } - size_t estimated_size() const + size_t estimated_size() const override { return args_size; } @@ -65,24 +147,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( - int64_t key, const std::string& value); - - COSEParametersFactory cose_params_string_int( - const std::string& key, int64_t value); - - COSEParametersFactory cose_params_string_string( - const std::string& key, const std::string& value); - - COSEParametersFactory cose_params_int_bytes( - int64_t key, const std::vector& value); - - COSEParametersFactory cose_params_string_bytes( - const std::string& key, const std::vector& value); + using COSEHeadersArray = + std::vector>; struct COSESignError : public std::runtime_error { @@ -101,7 +167,8 @@ namespace ccf::crypto */ std::vector cose_sign1( const KeyPair_OpenSSL& key, - const std::vector& protected_headers, + const std::vector>& + protected_headers, std::span payload, bool detached_payload = true); } diff --git a/src/crypto/openssl/cose_verifier.cpp b/src/crypto/openssl/cose_verifier.cpp index fbf5a8248..fa989684f 100644 --- a/src/crypto/openssl/cose_verifier.cpp +++ b/src/crypto/openssl/cose_verifier.cpp @@ -262,7 +262,8 @@ namespace ccf::crypto throw std::logic_error("Failed to parse COSE_Sign1 as bstr"); } - QCBORDecode_EnterMap(&ctx, NULL); + QCBORDecode_EnterMap(&ctx, NULL); // phdr + QCBORDecode_EnterMapFromMapSZ(&ctx, "ccf.v1"); // phdr["ccf.v1"] qcbor_result = QCBORDecode_GetError(&ctx); if (qcbor_result != QCBOR_SUCCESS) @@ -318,7 +319,8 @@ namespace ccf::crypto // Complete decode to ensure well-formed CBOR. - QCBORDecode_ExitMap(&ctx); + QCBORDecode_ExitMap(&ctx); // phdr["ccf.v1"] + QCBORDecode_ExitMap(&ctx); // phdr QCBORDecode_ExitBstrWrapped(&ctx); qcbor_result = QCBORDecode_GetError(&ctx); diff --git a/src/node/historical_queries_adapter.cpp b/src/node/historical_queries_adapter.cpp index 4e097ac33..e0dda512c 100644 --- a/src/node/historical_queries_adapter.cpp +++ b/src/node/historical_queries_adapter.cpp @@ -254,6 +254,12 @@ namespace ccf { return receipt.cose_endorsements; } + + std::optional describe_cose_signature_v1( + const TxReceiptImpl& receipt) + { + return receipt.cose_signature; + } } namespace ccf::historical diff --git a/src/node/history.h b/src/node/history.h index b57efe44b..2f8962d30 100644 --- a/src/node/history.h +++ b/src/node/history.h @@ -378,6 +378,23 @@ namespace ccf ccf::get_enclave_time()) .count(); + auto ccf_headers = + std::static_pointer_cast( + std::make_shared( + std::make_shared( + ccf::crypto::COSE_PHEADER_KEY_CCF), + ccf::crypto::COSEHeadersArray{ + ccf::crypto::cose_params_string_string( + ccf::crypto::COSE_PHEADER_KEY_TXID, txid.str())})); + + auto cwt_headers = + std::static_pointer_cast( + std::make_shared( + std::make_shared( + ccf::crypto::COSE_PHEADER_KEY_CWT), + ccf::crypto::COSEHeadersArray{ccf::crypto::cose_params_int_int( + ccf::crypto::COSE_PHEADER_KEY_IAT, time_since_epoch)})); + const auto pheaders = { // Key digest ccf::crypto::cose_params_int_bytes( @@ -385,12 +402,10 @@ namespace ccf // VDS ccf::crypto::cose_params_int_int( ccf::crypto::COSE_PHEADER_KEY_VDS, vds_merkle_tree), - // TxID - ccf::crypto::cose_params_string_string( - 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}})}; + // CWT claims + cwt_headers, + // CCF headers + ccf_headers}; auto cose_sign = crypto::cose_sign1(service_kp, pheaders, root_hash); diff --git a/src/service/internal_tables_access.h b/src/service/internal_tables_access.h index 7c1efb3b8..91415fa4d 100644 --- a/src/service/internal_tables_access.h +++ b/src/service/internal_tables_access.h @@ -394,7 +394,6 @@ namespace ccf ccf::Tables::PREVIOUS_SERVICE_IDENTITY_ENDORSEMENT); ccf::CoseEndorsement endorsement{}; - std::vector pheaders{}; std::vector key_to_endorse{}; std::vector previous_root{}; @@ -441,28 +440,44 @@ namespace ccf key_to_endorse = endorsement.endorsing_key; } - pheaders.push_back(ccf::crypto::cose_params_string_string( + std::vector> + ccf_headers_arr{}; + ccf_headers_arr.push_back(ccf::crypto::cose_params_string_string( ccf::crypto::COSE_PHEADER_KEY_RANGE_BEGIN, endorsement.endorsement_epoch_begin.to_str())); if (endorsement.endorsement_epoch_end) { - pheaders.push_back(ccf::crypto::cose_params_string_string( + ccf_headers_arr.push_back(ccf::crypto::cose_params_string_string( ccf::crypto::COSE_PHEADER_KEY_RANGE_END, endorsement.endorsement_epoch_end->to_str())); } if (!previous_root.empty()) { - pheaders.push_back(ccf::crypto::cose_params_string_bytes( - ccf::crypto::COSE_PHEADER_KEY_MERKLE_ROOT, previous_root)); + ccf_headers_arr.push_back(ccf::crypto::cose_params_string_bytes( + ccf::crypto::COSE_PHEADER_KEY_EPOCH_LAST_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}})); + + auto cwt_headers = + std::static_pointer_cast( + std::make_shared( + std::make_shared( + ccf::crypto::COSE_PHEADER_KEY_CWT), + ccf::crypto::COSEHeadersArray{ccf::crypto::cose_params_int_int( + ccf::crypto::COSE_PHEADER_KEY_IAT, time_since_epoch)})); + + auto ccf_headers = + std::static_pointer_cast( + std::make_shared( + std::make_shared( + ccf::crypto::COSE_PHEADER_KEY_CCF), + ccf_headers_arr)); + + ccf::crypto::COSEHeadersArray pheaders{cwt_headers, ccf_headers}; try { diff --git a/tests/e2e_logging.py b/tests/e2e_logging.py index 48e25b784..a5918066c 100644 --- a/tests/e2e_logging.py +++ b/tests/e2e_logging.py @@ -38,6 +38,7 @@ import copy import programmability import e2e_common_endpoints import subprocess +import base64 from loguru import logger as LOG @@ -962,6 +963,54 @@ def test_cbor_merkle_proof(network, args): return network +@reqs.description("Check COSE signature CDDL model") +def test_cose_signature_schema(network, args): + primary, _ = network.find_nodes() + + with primary.client("user0") as client: + r = client.get("/commit") + assert r.status_code == http.HTTPStatus.OK + txid = TxID.from_str(r.body.json()["transaction_id"]) + max_retries = 10 + for _ in range(max_retries): + response = client.get( + "/log/public/cose_signature", + headers={infra.clients.CCF_TX_ID_HEADER: f"{txid.view}.{txid.seqno}"}, + ) + + if response.status_code == http.HTTPStatus.OK: + signature = response.body.json()["cose_signature"] + signature = base64.b64decode(signature) + signature_filename = os.path.join( + network.common_dir, f"cose_signature_{txid}.cose" + ) + with open(signature_filename, "wb") as f: + f.write(signature) + subprocess.run( + [ + "cddl", + "../cddl/ccf-merkle-tree-cose-signature.cddl", + "v", + signature_filename, + ], + check=True, + ) + LOG.debug(f"Checked COSE signature schema for txid {txid}") + break + elif response.status_code == http.HTTPStatus.ACCEPTED: + LOG.debug(f"Transaction {txid} accepted, retrying") + time.sleep(0.1) + else: + LOG.error(f"Failed to get COSE signature for txid {txid}") + break + else: + assert ( + False + ), f"Failed to get receipt for txid {txid} after {max_retries} retries" + + return network + + @reqs.description("Read range of historical state") @reqs.supports_methods("/app/log/public", "/app/log/public/historical/range") def test_historical_query_range(network, args): @@ -2144,6 +2193,7 @@ def run_main_tests(network, args): test_record_count(network, args) if args.package == "samples/apps/logging/liblogging": test_cbor_merkle_proof(network, args) + test_cose_signature_schema(network, args) # HTTP2 doesn't support forwarding if not args.http2: diff --git a/tests/recovery.py b/tests/recovery.py index e366add20..e09f96ab0 100644 --- a/tests/recovery.py +++ b/tests/recovery.py @@ -9,6 +9,7 @@ import infra.crypto import suite.test_requirements as reqs import ccf.ledger import os +import subprocess import json from infra.runner import ConcurrentRunner from distutils.dir_util import copy_tree @@ -71,10 +72,10 @@ def verify_endorsements_chain(primary, endorsements, pubkey): validate_cose_sign1(cose_sign1=endorsement, pubkey=pubkey) cose_msg = Sign1Message.decode(endorsement) - last_tx = ccf.tx_id.TxID.from_str(cose_msg.phdr["ccf.epoch.end"]) + last_tx = ccf.tx_id.TxID.from_str(cose_msg.phdr["ccf.v1"]["epoch.end.txid"]) receipt = primary.get_receipt(last_tx.view, last_tx.seqno) root_from_receipt = bytes.fromhex(receipt.json()["leaf"]) - root_from_headers = cose_msg.phdr["ccf.merkle.root"] + root_from_headers = cose_msg.phdr["ccf.v1"]["epoch.end.merkle.root"] assert root_from_receipt == root_from_headers CWT_KEY = 15 @@ -88,6 +89,19 @@ def verify_endorsements_chain(primary, endorsements, pubkey): time.time() - cose_msg.phdr[CWT_KEY][IAT_CWT_LABEL] < last_five_minutes ), cose_msg.phdr + endorsement_filename = "prev_service_identoty_endorsement.cose" + with open(endorsement_filename, "wb") as f: + f.write(endorsement) + subprocess.run( + [ + "cddl", + "../cddl/ccf-cose-endorsement-service-identity.cddl", + "v", + endorsement_filename, + ], + check=True, + ) + next_key_bytes = cose_msg.payload pubkey = serialization.load_der_public_key(next_key_bytes, default_backend())