From 09669ad8361bd250fbf71ee9196bc7533e515d4f Mon Sep 17 00:00:00 2001 From: Amaury Chamayou Date: Mon, 4 Nov 2024 11:47:28 +0000 Subject: [PATCH] Extend set_unprotected_header() to allow setting an empty header, and verify_receipt() to check claim_digest (#6607) --- CHANGELOG.md | 11 ++++- include/ccf/crypto/cose.h | 43 +++++++++++------- python/pyproject.toml | 2 +- python/src/ccf/cose.py | 6 ++- samples/apps/logging/logging.cpp | 8 ++-- src/crypto/cose.cpp | 77 +++++++++++++++++++++----------- src/crypto/test/cose.cpp | 74 ++++++++++++++++++++++++++---- tests/e2e_logging.py | 9 +++- 8 files changed, 172 insertions(+), 58 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index de56f581d9e6..233b64b7c6e6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,15 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). +## [6.0.0-dev5] + +[6.0.0-dev5]: https://github.com/microsoft/CCF/releases/tag/6.0.0-dev5 + +### Added + +- Updated `ccf::cose::edit::set_unprotected_header()` API, to allow removing the unprotected header altogether (#6607). +- Updated `ccf.cose.verify_receipt()` to support checking the claim_digest against a reference value (#6607). + ## [6.0.0-dev4] [6.0.0-dev4]: https://github.com/microsoft/CCF/releases/tag/6.0.0-dev4 @@ -24,7 +33,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). +- 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/include/ccf/crypto/cose.h b/include/ccf/crypto/cose.h index 3777130b2df7..a06677b8cfa0 100644 --- a/include/ccf/crypto/cose.h +++ b/include/ccf/crypto/cose.h @@ -16,34 +16,43 @@ namespace ccf::cose::edit struct AtKey { - /// @brief The key at which to insert the value. + /// @brief The sub-key at which to insert the value. int64_t key; }; using Type = std::variant; } + namespace desc + { + struct Empty + {}; + + struct Value + { + /// @brief The type of position at which to insert the value. + pos::Type position; + /// @brief The top-level key at which to insert the value. + int64_t key; + /// @brief The value to insert in the unprotected header. + const std::vector& value; + }; + + 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. + * Set the unprotected header of a COSE_Sign1 message, according to a + * descriptor. * - * Useful to add a proof to a signature to turn it into a receipt, or to + * Useful to add a proof to a signature to turn it into a receipt, to * add a receipt to a signed statement to turn it into a transparent - * statement. + * statement, or simply to strip the unprotected header from a COSE Sign1. * * @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. - * @param value The value to insert either in the array or the map. - * - * @return The COSE_Sign1 message with the new unprotected header. + * @param descriptor An object describing whether and how to set the + * unprotected header. */ std::vector set_unprotected_header( - const std::span& cose_input, - int64_t key, - pos::Type position, - const std::vector value); + const std::span& cose_input, const desc::Type& descriptor); } \ No newline at end of file diff --git a/python/pyproject.toml b/python/pyproject.toml index ffeba41bd035..9698f2c56836 100644 --- a/python/pyproject.toml +++ b/python/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "ccf" -version = "6.0.0-dev4" +version = "6.0.0-dev5" authors = [ { name="CCF Team", email="CCF-Sec@microsoft.com" }, ] diff --git a/python/src/ccf/cose.py b/python/src/ccf/cose.py index 4c9d3821153c..fb9c2713b6b0 100644 --- a/python/src/ccf/cose.py +++ b/python/src/ccf/cose.py @@ -201,7 +201,9 @@ def validate_cose_sign1(pubkey, cose_sign1, payload=None): raise ValueError("signature is invalid") -def verify_receipt(receipt_bytes: bytes, key: CertificatePublicKeyTypes): +def verify_receipt( + receipt_bytes: bytes, key: CertificatePublicKeyTypes, claim_digest: bytes +): """ Verify a COSE Sign1 receipt as defined in https://datatracker.ietf.org/doc/draft-ietf-cose-merkle-tree-proofs/, using the CCF tree algorithm defined in https://datatracker.ietf.org/doc/draft-birkholz-cose-receipts-ccf-profile/ @@ -239,6 +241,8 @@ def verify_receipt(receipt_bytes: bytes, key: CertificatePublicKeyTypes): accumulator = sha256(accumulator + digest).digest() if not receipt.verify_signature(accumulator): raise ValueError("Signature verification failed") + if claim_digest != leaf[2]: + raise ValueError(f"Claim digest mismatch: {leaf[2]!r} != {claim_digest!r}") _SIGN_DESCRIPTION = """Create and sign a COSE Sign1 message for CCF governance diff --git a/samples/apps/logging/logging.cpp b/samples/apps/logging/logging.cpp index 8a3fe63dcc09..bd0afec84ff4 100644 --- a/samples/apps/logging/logging.cpp +++ b/samples/apps/logging/logging.cpp @@ -2065,11 +2065,13 @@ namespace loggingapp return; } - size_t vdp = 396; + int64_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); + ccf::cose::edit::desc::Value desc{inclusion_proof, vdp, *proof}; + + auto cose_receipt = + ccf::cose::edit::set_unprotected_header(*signature, desc); ctx.rpc_ctx->set_response_status(HTTP_STATUS_OK); ctx.rpc_ctx->set_response_header( diff --git a/src/crypto/cose.cpp b/src/crypto/cose.cpp index f4ad8c67aded..ca3b166b7a2e 100644 --- a/src/crypto/cose.cpp +++ b/src/crypto/cose.cpp @@ -12,10 +12,7 @@ namespace ccf::cose::edit { std::vector set_unprotected_header( - const std::span& cose_input, - int64_t key, - pos::Type pos, - const std::vector value) + const std::span& cose_input, const desc::Type& descriptor) { UsefulBufC buf{cose_input.data(), cose_input.size()}; @@ -83,16 +80,31 @@ namespace ccf::cose::edit 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 + size_t additional_map_size = 0; + + if (std::holds_alternative(descriptor)) + { + // Nothing to do + } + else if (std::holds_alternative(descriptor)) + { + auto& [pos, key, value] = std::get(descriptor); + + // Maximum expected size of the additional map, sub-map is the + // worst-case scenario + 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 + } + else + { + throw std::logic_error("Invalid COSE_Sign1 edit descriptor"); + } // We add one extra QCBOR_HEAD_BUFFER_SIZE, because we parse and re-encode // the protected header bstr, which involves variable integer encoding, just @@ -108,24 +120,37 @@ namespace ccf::cose::edit QCBOREncode_AddBytes(&ectx, phdr); QCBOREncode_OpenMap(&ectx); - if (std::holds_alternative(pos)) + if (std::holds_alternative(descriptor)) { - QCBOREncode_OpenArrayInMapN(&ectx, key); - QCBOREncode_AddBytes(&ectx, {value.data(), value.size()}); - QCBOREncode_CloseArray(&ectx); + // Nothing to do } - else if (std::holds_alternative(pos)) + else if (std::holds_alternative(descriptor)) { - 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); + auto& [pos, key, value] = std::get(descriptor); + + 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"); + } } else { - throw std::logic_error("Invalid COSE_Sign1 edit operation"); + throw std::logic_error("Invalid COSE_Sign1 edit descriptor"); } QCBOREncode_CloseMap(&ectx); diff --git a/src/crypto/test/cose.cpp b/src/crypto/test/cose.cpp index c3304d0cb7b7..5457ba652cf6 100644 --- a/src/crypto/test/cose.cpp +++ b/src/crypto/test/cose.cpp @@ -111,12 +111,18 @@ TEST_CASE("Verification and payload invariant") { for (const auto& position : positions) { - auto csp_set = - ccf::cose::edit::set_unprotected_header(csp, key, position, value); + ccf::cose::edit::desc::Value desc{position, key, value}; + auto csp_set = ccf::cose::edit::set_unprotected_header(csp, desc); signer.verify(csp_set); } } + + { + auto csp_set_empty = ccf::cose::edit::set_unprotected_header( + csp, ccf::cose::edit::desc::Empty{}); + signer.verify(csp_set_empty); + } } } @@ -132,14 +138,23 @@ TEST_CASE("Idempotence") { for (const auto& position : positions) { - auto csp_set_once = - ccf::cose::edit::set_unprotected_header(csp, key, position, value); + ccf::cose::edit::desc::Value desc{position, key, value}; + auto csp_set_once = ccf::cose::edit::set_unprotected_header(csp, desc); - auto csp_set_twice = ccf::cose::edit::set_unprotected_header( - csp_set_once, key, position, value); + auto csp_set_twice = + ccf::cose::edit::set_unprotected_header(csp_set_once, desc); REQUIRE(csp_set_once == csp_set_twice); } } + + { + auto csp_set_empty = ccf::cose::edit::set_unprotected_header( + csp, ccf::cose::edit::desc::Empty{}); + auto csp_set_twice_empty = ccf::cose::edit::set_unprotected_header( + csp_set_empty, ccf::cose::edit::desc::Empty{}); + + REQUIRE(csp_set_empty == csp_set_twice_empty); + } } } @@ -155,8 +170,8 @@ TEST_CASE("Check unprotected header") { for (const auto& position : positions) { - auto csp_set = - ccf::cose::edit::set_unprotected_header(csp, key, position, value); + ccf::cose::edit::desc::Value desc{position, key, value}; + auto csp_set = ccf::cose::edit::set_unprotected_header(csp, desc); std::vector ref(1024); { @@ -215,5 +230,48 @@ TEST_CASE("Check unprotected header") REQUIRE(err == QCBOR_SUCCESS); } } + + { + auto csp_set_empty = ccf::cose::edit::set_unprotected_header( + csp, ccf::cose::edit::desc::Empty{}); + + 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); + 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_empty.data(), csp_set_empty.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_empty.data() + uhdr_start, csp_set_empty.data() + uhdr_end}; + REQUIRE(uhdr == ref); + // Payload + QCBORDecode_VGetNextConsume(&ctx, &item); + // Signature + QCBORDecode_VGetNextConsume(&ctx, &item); + QCBORDecode_ExitArray(&ctx); + err = QCBORDecode_Finish(&ctx); + } } } \ No newline at end of file diff --git a/tests/e2e_logging.py b/tests/e2e_logging.py index 5b3b3f834d74..878e5a799f15 100644 --- a/tests/e2e_logging.py +++ b/tests/e2e_logging.py @@ -1015,6 +1015,12 @@ def test_cose_signature_schema(network, args): def test_cose_receipt_schema(network, args): primary, _ = network.find_nodes() + # Make sure the last transaction does not contain application claims + member = network.consortium.get_any_active_member() + r = member.update_ack_state_digest(primary) + with primary.client() as client: + client.wait_for_commit(r) + service_cert_path = os.path.join(network.common_dir, "service_cert.pem") service_cert = load_pem_x509_certificate( open(service_cert_path, "rb").read(), default_backend() @@ -1037,9 +1043,10 @@ def test_cose_receipt_schema(network, args): 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() - ccf.cose.verify_receipt(cbor_proof, service_key) + ccf.cose.verify_receipt(cbor_proof, service_key, b"\0" * 32) cbor_proof_filename = os.path.join( network.common_dir, f"receipt_{txid}.cose" )