Skip to content

Commit

Permalink
Add API to allow setting unprotected headers (#6586)
Browse files Browse the repository at this point in the history
Co-authored-by: Max <[email protected]>
  • Loading branch information
achamayou and maxtropets authored Oct 24, 2024
1 parent da1b2ad commit 1bf76dd
Show file tree
Hide file tree
Showing 14 changed files with 611 additions and 6 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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]

Expand Down
7 changes: 7 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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})

Expand Down
50 changes: 50 additions & 0 deletions cddl/ccf-receipt.cddl
Original file line number Diff line number Diff line change
@@ -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)
]
1 change: 1 addition & 0 deletions cmake/crypto.cmake
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
6 changes: 6 additions & 0 deletions doc/build_apps/api.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
26 changes: 25 additions & 1 deletion doc/schemas/app_openapi.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
Expand Down Expand Up @@ -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",
Expand Down
47 changes: 47 additions & 0 deletions include/ccf/crypto/cose.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the Apache 2.0 License.
#pragma once

#include <cstdint>
#include <span>
#include <variant>
#include <vector>

namespace ccf::cose::edit
{
namespace pos
{
struct InArray
{};

struct AtKey
{
int64_t key;
};

using Type = std::variant<InArray, AtKey>;
}

/**
* 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<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);
}
50 changes: 49 additions & 1 deletion samples/apps/logging/logging.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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<RecordsIndexingStrategy>(
PUBLIC_RECORDS, context, 10000, 20);
Expand Down Expand Up @@ -2038,6 +2039,53 @@ namespace loggingapp
.set_auto_schema<void, LoggingGetCoseSignature::Out>()
.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<void, void>()
.set_forwarding_required(ccf::endpoints::ForwardingRequired::Never)
.install();
}
};
}
Expand Down
146 changes: 146 additions & 0 deletions src/crypto/cose.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the Apache 2.0 License.

#include "ccf/crypto/cose.h"

#include <optional>
#include <qcbor/qcbor_decode.h>
#include <qcbor/qcbor_encode.h>
#include <qcbor/qcbor_spiffy_decode.h>
#include <stdexcept>

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)
{
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<uint8_t> 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::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");
}

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;
};
}
Loading

0 comments on commit 1bf76dd

Please sign in to comment.