Skip to content

Commit

Permalink
Extend set_unprotected_header() to allow setting an empty header, and…
Browse files Browse the repository at this point in the history
… verify_receipt() to check claim_digest (#6607)
  • Loading branch information
achamayou authored Nov 4, 2024
1 parent 79ffcdb commit 09669ad
Show file tree
Hide file tree
Showing 8 changed files with 172 additions and 58 deletions.
11 changes: 10 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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]

Expand Down
43 changes: 26 additions & 17 deletions include/ccf/crypto/cose.h
Original file line number Diff line number Diff line change
Expand Up @@ -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<InArray, AtKey>;
}

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<uint8_t>& value;
};

using Type = std::variant<Empty, Value>;
}

/**
* 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<uint8_t> set_unprotected_header(
const std::span<const uint8_t>& cose_input,
int64_t key,
pos::Type position,
const std::vector<uint8_t> value);
const std::span<const uint8_t>& cose_input, const desc::Type& descriptor);
}
2 changes: 1 addition & 1 deletion python/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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="[email protected]" },
]
Expand Down
6 changes: 5 additions & 1 deletion python/src/ccf/cose.py
Original file line number Diff line number Diff line change
Expand Up @@ -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/
Expand Down Expand Up @@ -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
Expand Down
8 changes: 5 additions & 3 deletions samples/apps/logging/logging.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
77 changes: 51 additions & 26 deletions src/crypto/cose.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,7 @@
namespace ccf::cose::edit
{
std::vector<uint8_t> set_unprotected_header(
const std::span<const uint8_t>& cose_input,
int64_t key,
pos::Type pos,
const std::vector<uint8_t> value)
const std::span<const uint8_t>& cose_input, const desc::Type& descriptor)
{
UsefulBufC buf{cose_input.data(), cose_input.size()};

Expand Down Expand Up @@ -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<desc::Empty>(descriptor))
{
// Nothing to do
}
else if (std::holds_alternative<desc::Value>(descriptor))
{
auto& [pos, key, value] = std::get<desc::Value>(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
Expand All @@ -108,24 +120,37 @@ namespace ccf::cose::edit
QCBOREncode_AddBytes(&ectx, phdr);
QCBOREncode_OpenMap(&ectx);

if (std::holds_alternative<pos::InArray>(pos))
if (std::holds_alternative<desc::Empty>(descriptor))
{
QCBOREncode_OpenArrayInMapN(&ectx, key);
QCBOREncode_AddBytes(&ectx, {value.data(), value.size()});
QCBOREncode_CloseArray(&ectx);
// Nothing to do
}
else if (std::holds_alternative<pos::AtKey>(pos))
else if (std::holds_alternative<desc::Value>(descriptor))
{
QCBOREncode_OpenMapInMapN(&ectx, key);
auto subkey = std::get<pos::AtKey>(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<desc::Value>(descriptor);

if (std::holds_alternative<pos::InArray>(pos))
{
QCBOREncode_OpenArrayInMapN(&ectx, key);
QCBOREncode_AddBytes(&ectx, {value.data(), value.size()});
QCBOREncode_CloseArray(&ectx);
}
else if (std::holds_alternative<pos::AtKey>(pos))
{
QCBOREncode_OpenMapInMapN(&ectx, key);
auto subkey = std::get<pos::AtKey>(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);
Expand Down
74 changes: 66 additions & 8 deletions src/crypto/test/cose.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}
}

Expand All @@ -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);
}
}
}

Expand All @@ -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<uint8_t> ref(1024);
{
Expand Down Expand Up @@ -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<uint8_t> 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<uint8_t> 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);
}
}
}
9 changes: 8 additions & 1 deletion tests/e2e_logging.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand All @@ -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"
)
Expand Down

0 comments on commit 09669ad

Please sign in to comment.