From 1bf76ddd7eadb31bf66772171c3788b67bed802f Mon Sep 17 00:00:00 2001 From: Amaury Chamayou Date: Thu, 24 Oct 2024 16:03:29 +0100 Subject: [PATCH] Add API to allow setting unprotected headers (#6586) Co-authored-by: Max --- CHANGELOG.md | 1 + CMakeLists.txt | 7 + cddl/ccf-receipt.cddl | 50 +++++ cmake/crypto.cmake | 1 + doc/build_apps/api.rst | 6 + doc/schemas/app_openapi.json | 26 ++- include/ccf/crypto/cose.h | 47 +++++ samples/apps/logging/logging.cpp | 50 ++++- src/crypto/cose.cpp | 146 ++++++++++++++ src/crypto/test/cose.cpp | 219 +++++++++++++++++++++ src/node/historical_queries_adapter.cpp | 7 +- src/node/test/historical_queries.cpp | 2 +- tests/e2e_logging.py | 53 +++++ tests/perf-system/submitter/CMakeLists.txt | 2 + 14 files changed, 611 insertions(+), 6 deletions(-) create mode 100644 cddl/ccf-receipt.cddl create mode 100644 include/ccf/crypto/cose.h create mode 100644 src/crypto/cose.cpp create mode 100644 src/crypto/test/cose.cpp diff --git a/CHANGELOG.md b/CHANGELOG.md index 9d666ea52a2d..19fcc9a4babf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0. ### Changed - Set VMPL value when creating SNP attestations, and check VMPL value is in guest range when verifiying attestation, since recent [updates allow host-initiated attestations](https://www.amd.com/content/dam/amd/en/documents/epyc-technical-docs/programmer-references/56860.pdf) (#6583). +- Added ccf::cose::edit::set_unprotected_header() API, to allow easy injection of proofs in signatures, and of receipts in signed statements (#6586). ## [6.0.0-dev2] diff --git a/CMakeLists.txt b/CMakeLists.txt index 806d9102833a..65992691c8b3 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -864,6 +864,13 @@ if(BUILD_TESTS) ) target_link_libraries(base64_test PRIVATE ${CMAKE_THREAD_LIBS_INIT}) + add_unit_test( + cose_test ${CMAKE_CURRENT_SOURCE_DIR}/src/crypto/test/cose.cpp + ) + target_link_libraries( + cose_test PRIVATE ${CMAKE_THREAD_LIBS_INIT} ccfcrypto.host qcbor.host + ) + add_unit_test(pem_test ${CMAKE_CURRENT_SOURCE_DIR}/src/crypto/test/pem.cpp) target_link_libraries(pem_test PRIVATE ${CMAKE_THREAD_LIBS_INIT}) diff --git a/cddl/ccf-receipt.cddl b/cddl/ccf-receipt.cddl new file mode 100644 index 000000000000..c943a09dfec8 --- /dev/null +++ b/cddl/ccf-receipt.cddl @@ -0,0 +1,50 @@ +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 = { + &(vdp: 396) => verifiable-proofs +} + +inclusion-proofs = [ + bstr .cbor ccf-inclusion-proof ] + +verifiable-proofs = { + &(inclusion-proof: -1) => inclusion-proofs +} + +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 +} + +ccf-inclusion-proof = { + &(leaf: 1) => ccf-leaf + &(path: 2) => [+ ccf-proof-element] +} + +ccf-leaf = [ + internal-transaction-hash: bstr .size 32 ; a string of HASH_SIZE(32) bytes + internal-evidence: tstr .size (1..1024) ; a string of at most 1024 bytes + data-hash: bstr .size 32 ; a string of HASH_SIZE(32) bytes +] + +ccf-proof-element = [ + left: bool ; position of the element + hash: bstr .size 32 ; hash of the proof element (string of HASH_SIZE(32) bytes) +] diff --git a/cmake/crypto.cmake b/cmake/crypto.cmake index c5de9c2fc2d0..ef63e162f998 100644 --- a/cmake/crypto.cmake +++ b/cmake/crypto.cmake @@ -15,6 +15,7 @@ set(CCFCRYPTO_SRC ${CCF_DIR}/src/crypto/hmac.cpp ${CCF_DIR}/src/crypto/pem.cpp ${CCF_DIR}/src/crypto/ecdsa.cpp + ${CCF_DIR}/src/crypto/cose.cpp ${CCF_DIR}/src/crypto/openssl/symmetric_key.cpp ${CCF_DIR}/src/crypto/openssl/public_key.cpp ${CCF_DIR}/src/crypto/openssl/key_pair.cpp diff --git a/doc/build_apps/api.rst b/doc/build_apps/api.rst index 0bd0ea9004eb..0354d9f7cca4 100644 --- a/doc/build_apps/api.rst +++ b/doc/build_apps/api.rst @@ -158,3 +158,9 @@ HTTP Entity Tags Matching .. doxygenclass:: ccf::http::Matcher :project: CCF :members: + +COSE +---- + +.. doxygenfunction:: ccf::cose::edit::set_unprotected_header + :project: CCF \ No newline at end of file diff --git a/doc/schemas/app_openapi.json b/doc/schemas/app_openapi.json index b225066f1fc0..0b6561c9e9c0 100644 --- a/doc/schemas/app_openapi.json +++ b/doc/schemas/app_openapi.json @@ -295,7 +295,7 @@ "info": { "description": "This CCF sample app implements a simple logging application, securely recording messages at client-specified IDs. It demonstrates most of the features available to CCF apps.", "title": "CCF Sample Logging App", - "version": "2.4.3" + "version": "2.5.0" }, "openapi": "3.0.0", "paths": { @@ -1273,6 +1273,30 @@ } } }, + "/app/log/public/cose_receipt": { + "get": { + "operationId": "GetAppLogPublicCoseReceipt", + "responses": { + "204": { + "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/cose_signature": { "get": { "operationId": "GetAppLogPublicCoseSignature", diff --git a/include/ccf/crypto/cose.h b/include/ccf/crypto/cose.h new file mode 100644 index 000000000000..b75e5f9db5ee --- /dev/null +++ b/include/ccf/crypto/cose.h @@ -0,0 +1,47 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the Apache 2.0 License. +#pragma once + +#include +#include +#include +#include + +namespace ccf::cose::edit +{ + namespace pos + { + struct InArray + {}; + + struct AtKey + { + int64_t key; + }; + + using Type = std::variant; + } + + /** + * Set the unprotected header of a COSE_Sign1 message, to a map containing + * @p key and depending on the value of @p position, either an array + * containing + * @p value, or a map with key @p subkey and value @p value. + * + * Useful to add a proof to a signature to turn it into a receipt, or to + * add a receipt to a signed statement to turn it into a transparent + * statement. + * + * @param cose_input The COSE_Sign1 message to edit. + * @param key The key at which to insert either an array or a map. + * @param position Either InArray or AtKey, to determine whether to insert an + * array or a map. + * + * @return The COSE_Sign1 message with the new unprotected header. + */ + std::vector set_unprotected_header( + const std::span& cose_input, + int64_t key, + pos::Type position, + const std::vector value); +} \ No newline at end of file diff --git a/samples/apps/logging/logging.cpp b/samples/apps/logging/logging.cpp index f6bb2ca40423..8a3fe63dcc09 100644 --- a/samples/apps/logging/logging.cpp +++ b/samples/apps/logging/logging.cpp @@ -7,6 +7,7 @@ // CCF #include "ccf/app_interface.h" #include "ccf/common_auth_policies.h" +#include "ccf/crypto/cose.h" #include "ccf/crypto/verifier.h" #include "ccf/ds/hash.h" #include "ccf/endpoints/authentication/all_of_auth.h" @@ -458,7 +459,7 @@ namespace loggingapp "recording messages at client-specified IDs. It demonstrates most of " "the features available to CCF apps."; - openapi_info.document_version = "2.4.3"; + openapi_info.document_version = "2.5.0"; index_per_public_key = std::make_shared( PUBLIC_RECORDS, context, 10000, 20); @@ -2038,6 +2039,53 @@ namespace loggingapp .set_auto_schema() .set_forwarding_required(ccf::endpoints::ForwardingRequired::Never) .install(); + + auto get_cose_receipt = [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; + } + auto proof = describe_merkle_proof_v1(*historical_state->receipt); + if (!proof.has_value()) + { + ctx.rpc_ctx->set_error( + HTTP_STATUS_NOT_FOUND, + ccf::errors::ResourceNotFound, + "No merkle proof available for this transaction"); + return; + } + + size_t vdp = 396; + auto inclusion_proof = ccf::cose::edit::pos::AtKey{-1}; + + auto cose_receipt = ccf::cose::edit::set_unprotected_header( + *signature, vdp, inclusion_proof, *proof); + + ctx.rpc_ctx->set_response_status(HTTP_STATUS_OK); + ctx.rpc_ctx->set_response_header( + ccf::http::headers::CONTENT_TYPE, + ccf::http::headervalues::contenttype::COSE); + ctx.rpc_ctx->set_response_body(cose_receipt); + }; + make_read_only_endpoint( + "/log/public/cose_receipt", + HTTP_GET, + ccf::historical::read_only_adapter_v4( + get_cose_receipt, context, is_tx_committed), + auth_policies) + .set_auto_schema() + .set_forwarding_required(ccf::endpoints::ForwardingRequired::Never) + .install(); } }; } diff --git a/src/crypto/cose.cpp b/src/crypto/cose.cpp new file mode 100644 index 000000000000..f4ad8c67aded --- /dev/null +++ b/src/crypto/cose.cpp @@ -0,0 +1,146 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the Apache 2.0 License. + +#include "ccf/crypto/cose.h" + +#include +#include +#include +#include +#include + +namespace ccf::cose::edit +{ + std::vector set_unprotected_header( + const std::span& cose_input, + int64_t key, + pos::Type pos, + const std::vector value) + { + UsefulBufC buf{cose_input.data(), cose_input.size()}; + + QCBORError err; + QCBORDecodeContext ctx; + QCBORDecode_Init(&ctx, buf, QCBOR_DECODE_MODE_NORMAL); + + size_t pos_start = 0; + size_t pos_end = 0; + + QCBORDecode_EnterArray(&ctx, nullptr); + err = QCBORDecode_GetError(&ctx); + if (err != QCBOR_SUCCESS) + { + throw std::logic_error("Failed to parse COSE_Sign1 outer array"); + } + + auto tag = QCBORDecode_GetNthTagOfLast(&ctx, 0); + if (tag != CBOR_TAG_COSE_SIGN1) + { + throw std::logic_error("Failed to parse COSE_Sign1 tag"); + } + + QCBORItem item; + err = QCBORDecode_GetNext(&ctx, &item); + if (err != QCBOR_SUCCESS || item.uDataType != QCBOR_TYPE_BYTE_STRING) + { + throw std::logic_error( + "Failed to parse COSE_Sign1 protected header as bstr"); + } + UsefulBufC phdr = {item.val.string.ptr, item.val.string.len}; + + // Skip unprotected header + QCBORDecode_VGetNextConsume(&ctx, &item); + + err = QCBORDecode_PartialFinish(&ctx, &pos_start); + if (err != QCBOR_ERR_ARRAY_OR_MAP_UNCONSUMED) + { + throw std::logic_error("Failed to find start of payload"); + } + QCBORDecode_VGetNextConsume(&ctx, &item); + err = QCBORDecode_PartialFinish(&ctx, &pos_end); + if (err != QCBOR_ERR_ARRAY_OR_MAP_UNCONSUMED) + { + throw std::logic_error("Failed to find end of payload"); + } + UsefulBufC payload = {cose_input.data() + pos_start, pos_end - pos_start}; + + // QCBORDecode_PartialFinish() before and after should allow constructing a + // span of the encoded payload, which can perhaps then be passed to + // QCBOREncode_AddEncoded and would allow blindly copying the payload + // without parsing it. + + err = QCBORDecode_GetNext(&ctx, &item); + if (err != QCBOR_SUCCESS && item.uDataType != QCBOR_TYPE_BYTE_STRING) + { + throw std::logic_error("Failed to parse COSE_Sign1 signature"); + } + UsefulBufC signature = {item.val.string.ptr, item.val.string.len}; + + QCBORDecode_ExitArray(&ctx); + err = QCBORDecode_Finish(&ctx); + if (err != QCBOR_SUCCESS) + { + throw std::logic_error("Failed to parse COSE_Sign1"); + } + + // Maximum expected size of the additional map, sub-map is the + // worst-case scenario + const size_t additional_map_size = QCBOR_HEAD_BUFFER_SIZE + // map + QCBOR_HEAD_BUFFER_SIZE + // key + sizeof(key) + // key + QCBOR_HEAD_BUFFER_SIZE + // submap + QCBOR_HEAD_BUFFER_SIZE + // subkey + sizeof(pos::AtKey::key) + // subkey + QCBOR_HEAD_BUFFER_SIZE + // value + value.size(); // value + + // We add one extra QCBOR_HEAD_BUFFER_SIZE, because we parse and re-encode + // the protected header bstr, which involves variable integer encoding, just + // in case the library does not pick the most compact encoding. + std::vector output( + cose_input.size() + additional_map_size + QCBOR_HEAD_BUFFER_SIZE); + UsefulBuf output_buf{output.data(), output.size()}; + + QCBOREncodeContext ectx; + QCBOREncode_Init(&ectx, output_buf); + QCBOREncode_AddTag(&ectx, CBOR_TAG_COSE_SIGN1); + QCBOREncode_OpenArray(&ectx); + QCBOREncode_AddBytes(&ectx, phdr); + QCBOREncode_OpenMap(&ectx); + + if (std::holds_alternative(pos)) + { + QCBOREncode_OpenArrayInMapN(&ectx, key); + QCBOREncode_AddBytes(&ectx, {value.data(), value.size()}); + QCBOREncode_CloseArray(&ectx); + } + else if (std::holds_alternative(pos)) + { + QCBOREncode_OpenMapInMapN(&ectx, key); + auto subkey = std::get(pos).key; + QCBOREncode_OpenArrayInMapN(&ectx, subkey); + QCBOREncode_AddBytes(&ectx, {value.data(), value.size()}); + QCBOREncode_CloseArray(&ectx); + QCBOREncode_CloseMap(&ectx); + } + else + { + throw std::logic_error("Invalid COSE_Sign1 edit operation"); + } + + QCBOREncode_CloseMap(&ectx); + QCBOREncode_AddEncoded(&ectx, payload); + QCBOREncode_AddBytes(&ectx, signature); + QCBOREncode_CloseArray(&ectx); + + UsefulBufC cose_output; + err = QCBOREncode_Finish(&ectx, &cose_output); + if (err != QCBOR_SUCCESS) + { + throw std::logic_error("Failed to encode COSE_Sign1"); + } + output.resize(cose_output.len); + output.shrink_to_fit(); + return output; + }; +} \ No newline at end of file diff --git a/src/crypto/test/cose.cpp b/src/crypto/test/cose.cpp new file mode 100644 index 000000000000..c3304d0cb7b7 --- /dev/null +++ b/src/crypto/test/cose.cpp @@ -0,0 +1,219 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the Apache 2.0 License. +#define DOCTEST_CONFIG_IMPLEMENT_WITH_MAIN +#include "ccf/crypto/cose.h" + +#include "crypto/openssl/cose_sign.h" +#include "crypto/openssl/cose_verifier.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +static const std::vector keys = { + 42, std::numeric_limits::min(), std::numeric_limits::max()}; + +static const std::vector positions = { + ccf::cose::edit::pos::AtKey{42}, + ccf::cose::edit::pos::AtKey{std::numeric_limits::min()}, + ccf::cose::edit::pos::AtKey{std::numeric_limits::max()}, + ccf::cose::edit::pos::InArray{}}; + +const std::vector value = {1, 2, 3, 4}; + +enum class PayloadType +{ + Detached, + Flat, + NestedCBOR // Useful to test the payload transfer +}; + +struct Signer +{ + ccf::crypto::KeyPair_OpenSSL kp; + std::vector payload; + bool detached_payload = false; + + Signer(PayloadType type) : kp(ccf::crypto::CurveID::SECP384R1) + { + switch (type) + { + case PayloadType::Detached: + detached_payload = true; + payload = {'p', 'a', 'y', 'l', 'o', 'a', 'd'}; + break; + case PayloadType::Flat: + payload = {'p', 'a', 'y', 'l', 'o', 'a', 'd'}; + break; + case PayloadType::NestedCBOR: + { + payload.resize(1024); + QCBOREncodeContext ctx; + QCBOREncode_Init(&ctx, {payload.data(), payload.size()}); + QCBOREncode_OpenArray(&ctx); + QCBOREncode_AddInt64(&ctx, 1); + QCBOREncode_OpenArray(&ctx); + QCBOREncode_AddInt64(&ctx, 2); + QCBOREncode_AddInt64(&ctx, 3); + QCBOREncode_CloseArray(&ctx); + QCBOREncode_CloseArray(&ctx); + UsefulBufC result; + QCBOREncode_Finish(&ctx, &result); + payload.resize(result.len); + payload.shrink_to_fit(); + } + break; + } + } + + std::vector make_cose_sign1() + { + const auto pheaders = { + ccf::crypto::cose_params_int_bytes(300, value), + ccf::crypto::cose_params_int_int(301, 34)}; + + return ccf::crypto::cose_sign1(kp, pheaders, payload, detached_payload); + }; + + void verify(const std::vector& cose_sign1) + { + auto verifier = + ccf::crypto::make_cose_verifier_from_key(kp.public_key_pem()); + if (detached_payload) + { + verifier->verify_detached(cose_sign1, payload); + } + else + { + std::span payload_; + REQUIRE(verifier->verify(cose_sign1, payload_)); + std::vector payload_copy(payload_.begin(), payload_.end()); + REQUIRE(payload == payload_copy); + } + }; +}; + +TEST_CASE("Verification and payload invariant") +{ + for (auto type : + {PayloadType::Detached, PayloadType::Flat, PayloadType::NestedCBOR}) + { + Signer signer(type); + auto csp = signer.make_cose_sign1(); + signer.verify(csp); + + for (const auto& key : keys) + { + for (const auto& position : positions) + { + auto csp_set = + ccf::cose::edit::set_unprotected_header(csp, key, position, value); + + signer.verify(csp_set); + } + } + } +} + +TEST_CASE("Idempotence") +{ + for (auto type : + {PayloadType::Detached, PayloadType::Flat, PayloadType::NestedCBOR}) + { + Signer signer(type); + auto csp = signer.make_cose_sign1(); + + for (const auto& key : keys) + { + for (const auto& position : positions) + { + auto csp_set_once = + ccf::cose::edit::set_unprotected_header(csp, key, position, value); + + auto csp_set_twice = ccf::cose::edit::set_unprotected_header( + csp_set_once, key, position, value); + REQUIRE(csp_set_once == csp_set_twice); + } + } + } +} + +TEST_CASE("Check unprotected header") +{ + for (auto type : + {PayloadType::Detached, PayloadType::Flat, PayloadType::NestedCBOR}) + { + Signer signer(type); + auto csp = signer.make_cose_sign1(); + + for (const auto& key : keys) + { + for (const auto& position : positions) + { + auto csp_set = + ccf::cose::edit::set_unprotected_header(csp, key, position, value); + + std::vector ref(1024); + { + // Create expected reference value for the unprotected header + UsefulBuf ref_buf{ref.data(), ref.size()}; + QCBOREncodeContext ctx; + QCBOREncode_Init(&ctx, ref_buf); + QCBOREncode_OpenMap(&ctx); + + if (std::holds_alternative(position)) + { + QCBOREncode_OpenArrayInMapN(&ctx, key); + QCBOREncode_AddBytes(&ctx, {value.data(), value.size()}); + QCBOREncode_CloseArray(&ctx); + } + else if (std::holds_alternative( + position)) + { + QCBOREncode_OpenMapInMapN(&ctx, key); + auto subkey = std::get(position).key; + QCBOREncode_OpenArrayInMapN(&ctx, subkey); + QCBOREncode_AddBytes(&ctx, {value.data(), value.size()}); + QCBOREncode_CloseArray(&ctx); + QCBOREncode_CloseMap(&ctx); + } + QCBOREncode_CloseMap(&ctx); + UsefulBufC ref_buf_c; + QCBOREncode_Finish(&ctx, &ref_buf_c); + ref.resize(ref_buf_c.len); + ref.shrink_to_fit(); + } + + size_t uhdr_start, uhdr_end; + QCBORError err; + QCBORItem item; + QCBORDecodeContext ctx; + UsefulBufC buf{csp_set.data(), csp_set.size()}; + QCBORDecode_Init(&ctx, buf, QCBOR_DECODE_MODE_NORMAL); + QCBORDecode_EnterArray(&ctx, nullptr); + QCBORDecode_GetNthTagOfLast(&ctx, 0); + // Protected header + QCBORDecode_VGetNextConsume(&ctx, &item); + // Unprotected header + QCBORDecode_PartialFinish(&ctx, &uhdr_start); + QCBORDecode_VGetNextConsume(&ctx, &item); + QCBORDecode_PartialFinish(&ctx, &uhdr_end); + std::vector uhdr{ + csp_set.data() + uhdr_start, csp_set.data() + uhdr_end}; + REQUIRE(uhdr == ref); + // Payload + QCBORDecode_VGetNextConsume(&ctx, &item); + // Signature + QCBORDecode_VGetNextConsume(&ctx, &item); + QCBORDecode_ExitArray(&ctx); + err = QCBORDecode_Finish(&ctx); + REQUIRE(err == QCBOR_SUCCESS); + } + } + } +} \ No newline at end of file diff --git a/src/node/historical_queries_adapter.cpp b/src/node/historical_queries_adapter.cpp index e0dda512c2f8..3a932bde61af 100644 --- a/src/node/historical_queries_adapter.cpp +++ b/src/node/historical_queries_adapter.cpp @@ -202,9 +202,10 @@ namespace ccf std::optional> describe_merkle_proof_v1( const TxReceiptImpl& receipt) { - constexpr size_t buf_size = 2048; + constexpr size_t buf_size = 2048; // TBD: calculate why this is enough std::vector underlying_buffer(buf_size); - q_useful_buf buffer{underlying_buffer.data(), buf_size}; + UsefulBuf buffer{underlying_buffer.data(), underlying_buffer.size()}; + assert(buffer.len == buf_size); QCBOREncodeContext ctx; QCBOREncode_Init(&ctx, buffer); @@ -232,7 +233,7 @@ namespace ccf QCBOREncode_CloseMap(&ctx); - struct q_useful_buf_c result; + UsefulBufC result; auto qerr = QCBOREncode_Finish(&ctx, &result); if (qerr) { diff --git a/src/node/test/historical_queries.cpp b/src/node/test/historical_queries.cpp index 5be0c1e14725..ffe1d825cc9f 100644 --- a/src/node/test/historical_queries.cpp +++ b/src/node/test/historical_queries.cpp @@ -1978,7 +1978,7 @@ TEST_CASE("Valid merkle proof from receipts") REQUIRE_EQ( ccf::ds::to_hex(decoded.claims_digest), historical_state->receipt->claims_digest.value() - .hex_str()); // HEX as workaround emmpy claims (set flag). + .hex_str()); // HEX as workaround empty claims (set flag). auto it = decoded.path.begin(); for (const auto& node : *historical_state->receipt->path) diff --git a/tests/e2e_logging.py b/tests/e2e_logging.py index a5918066c6f0..ce9918e38ad6 100644 --- a/tests/e2e_logging.py +++ b/tests/e2e_logging.py @@ -1011,6 +1011,58 @@ def test_cose_signature_schema(network, args): return network +@reqs.description("Check COSE receipt CDDL schema") +def test_cose_receipt_schema(network, args): + primary, _ = network.find_nodes() + + with primary.client("user0") as client: + r = client.get("/commit") + assert r.status_code == http.HTTPStatus.OK + last_txid = TxID.from_str(r.body.json()["transaction_id"]) + + for seqno in range(last_txid.seqno, last_txid.seqno - 10, -1): + txid = f"{last_txid.view}.{seqno}" + LOG.debug(f"Trying to get COSE receipt for txid {txid}") + max_retries = 10 + found_proof = False + for _ in range(max_retries): + r = client.get( + "/log/public/cose_receipt", + headers={infra.clients.CCF_TX_ID_HEADER: txid}, + log_capture=[], # Do not emit raw binary to stdout + ) + if r.status_code == http.HTTPStatus.OK: + cbor_proof = r.body.data() + cbor_proof_filename = os.path.join( + network.common_dir, f"receipt_{txid}.cose" + ) + with open(cbor_proof_filename, "wb") as f: + f.write(cbor_proof) + subprocess.run( + ["cddl", "../cddl/ccf-receipt.cddl", "v", cbor_proof_filename], + check=True, + ) + found_proof = True + LOG.debug(f"Checked COSE receipt for txid {txid}") + break + elif r.status_code == http.HTTPStatus.ACCEPTED: + LOG.debug(f"Transaction {txid} accepted, retrying") + time.sleep(0.1) + elif r.status_code == http.HTTPStatus.NOT_FOUND: + LOG.debug(f"Transaction {txid} is a signature") + break + else: + assert ( + False + ), f"Failed to get receipt for txid {txid} after {max_retries} retries" + if found_proof: + break + else: + assert False, "Failed to find a non-signature in the last 10 transactions" + + 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): @@ -2194,6 +2246,7 @@ def run_main_tests(network, args): if args.package == "samples/apps/logging/liblogging": test_cbor_merkle_proof(network, args) test_cose_signature_schema(network, args) + test_cose_receipt_schema(network, args) # HTTP2 doesn't support forwarding if not args.http2: diff --git a/tests/perf-system/submitter/CMakeLists.txt b/tests/perf-system/submitter/CMakeLists.txt index 45e9f9c45d45..eea0e855f602 100644 --- a/tests/perf-system/submitter/CMakeLists.txt +++ b/tests/perf-system/submitter/CMakeLists.txt @@ -19,6 +19,7 @@ set(CCFCRYPTO_SRC ${CCF_DIR}/src/crypto/key_wrap.cpp ${CCF_DIR}/src/crypto/hmac.cpp ${CCF_DIR}/src/crypto/pem.cpp + ${CCF_DIR}/src/crypto/cose.cpp ${CCF_DIR}/src/crypto/openssl/symmetric_key.cpp ${CCF_DIR}/src/crypto/openssl/public_key.cpp ${CCF_DIR}/src/crypto/openssl/key_pair.cpp @@ -32,6 +33,7 @@ set(CCFCRYPTO_SRC add_library(stdcxxccfcrypto.host STATIC "${CCFCRYPTO_SRC}") target_link_libraries(stdcxxccfcrypto.host PUBLIC crypto) target_link_libraries(stdcxxccfcrypto.host PUBLIC ssl) +target_link_libraries(stdcxxccfcrypto.host PUBLIC qcbor.host) target_link_libraries( submit PRIVATE stdcxxhttp_parser.host stdcxxccfcrypto.host arrow parquet