Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add API to allow setting unprotected headers #6586

Merged
merged 22 commits into from
Oct 24, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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)
maxtropets marked this conversation as resolved.
Show resolved Hide resolved

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};
achamayou marked this conversation as resolved.
Show resolved Hide resolved

// 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