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

formatting #152

Closed
wants to merge 27 commits into from
Closed
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
4 changes: 3 additions & 1 deletion .github/workflows/build-test-virtual.yml
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
name: "Build/test (virtual)"
on: [push]
on:
workflow_dispatch:
push:
jobs:
build-test:
runs-on: ubuntu-20.04
Expand Down
23 changes: 9 additions & 14 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,23 +1,21 @@
# scitt-ccf-ledger
# contract-ledger

[![Build/test (virtual)](https://github.com/microsoft/scitt-ccf-ledger/actions/workflows/build-test-virtual.yml/badge.svg)](https://github.com/microsoft/scitt-ccf-ledger/actions/workflows/build-test-virtual.yml)
This repository contains the source code for contract-ledger, an application
that runs on top of [CCF](https://ccf.dev/) implementing standards developed within the [DEPA Training cycle](https://github.com/kapilvgit/depa-training/). Its purpose is to provide registry for contracts. contracts-ledger achieves this by allowing signed contracts to be submitted to a secure immutable ledger, and returning receipts which prove contracts have been stored and registration policies applied.

This repository contains the source code for scitt-ccf-ledger, an application
that runs on top of [CCF](https://ccf.dev/) implementing draft standards developed within the [IETF SCITT WG](https://datatracker.ietf.org/wg/scitt/about/). Its purpose is to provide provenance for artefacts in digital supply chains, increasing trust in those artefacts. scitt-ccf-ledger achieves this by allowing signed claims about artefacts to be submitted to a secure immutable ledger, and returning receipts which prove claims have been stored and registration policies applied.

This research project is at an early stage and is open sourced to facilitate academic collaborations. We are keen to engage in research collaborations on this project, please do reach out to discuss this by opening an issue.
This project is at an early stage and is open sourced to facilitate academic collaborations. We are keen to engage in research collaborations on this project, please do reach out to discuss this by opening an issue.

## Getting Started

The instructions below guide you through building and deploying a local instance of scitt-ccf-ledger for development and testing purposes.
The instructions below guide you through building and deploying a local instance of contract-ledger for development and testing purposes.

Being a CCF application, scitt-ccf-ledger runs in SGX enclaves. However, for testing purposes, it also supports running on non-SGX hardware in what is called *virtual* mode.
Being a CCF application, contract-ledger runs in SGX enclaves. However, for testing purposes, it also supports running on non-SGX hardware in what is called *virtual* mode.

All instructions below assume Linux as the operating system.

### Using Docker

Use the following commands to start a single-node CCF network with the scitt-ccf-ledger application setup for development purposes.
Use the following commands to start a single-node CCF network with the contract-ledger application setup for development purposes.

Note: `PLATFORM` should be set to `sgx` or `virtual` to select the type of build.

Expand All @@ -31,15 +29,12 @@ The node is now reachable at https://127.0.0.1:8000/.

Note that `run-dev.sh` configures the network in a way that is not suitable for production, in particular it generates an ad-hoc governance member key pair and it disables API authentication.

See the `demo/` folder on how to interact with the application.
See the `demo/contract` folder on how to interact with the application.

### Development setup

See [DEVELOPMENT.md](DEVELOPMENT.md) for instructions on building, running, and testing scitt-ccf-ledger without Docker.
See [DEVELOPMENT.md](DEVELOPMENT.md) for instructions on building, running, and testing contract-ledger without Docker.

## Contributing

This project welcomes contributions and suggestions. Please see the [Contribution guidelines](CONTRIBUTING.md).

### Trademarks
This project may contain trademarks or logos for projects, products, or services. Authorized use of Microsoft trademarks or logos is subject to and must follow Microsoft’s Trademark & Brand Guidelines. Use of Microsoft trademarks or logos in modified versions of this project must not cause confusion or imply Microsoft sponsorship. Any use of third-party trademarks or logos are subject to those third-party’s policies.
269 changes: 267 additions & 2 deletions app/src/cose.h
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,12 @@ namespace scitt::cose
NOTARY_HEADER_PARAM_AUTHENTIC_SIGNING_TIME,
NOTARY_HEADER_PARAM_EXPIRY};

// Temporary assignments for contract service
static constexpr int64_t CONTRACT_HEADER_PARAM_PARTICIPANT_INFO = 491;

static const std::set<std::variant<int64_t, std::string>>
CONTRACT_HEADER_PARAMS{CONTRACT_HEADER_PARAM_PARTICIPANT_INFO};

struct COSEDecodeError : public std::runtime_error
{
COSEDecodeError(const std::string& msg) : std::runtime_error(msg) {}
Expand Down Expand Up @@ -98,6 +104,9 @@ namespace scitt::cose
std::optional<int64_t> notary_authentic_signing_time;
std::optional<int64_t> notary_expiry;

// Contract object headers
std::optional<std::vector<std::string>> participant_info;

bool is_present(const std::variant<int64_t, std::string>& label) const
{
// Helper function checking if a known label has a value in the protected
Expand Down Expand Up @@ -149,6 +158,14 @@ namespace scitt::cose
{
return true;
}
if (
label ==
std::variant<int64_t, std::string>(
CONTRACT_HEADER_PARAM_PARTICIPANT_INFO) and
participant_info.has_value())
{
return true;
}
if (
label ==
std::variant<int64_t, std::string>(
Expand Down Expand Up @@ -303,6 +320,7 @@ namespace scitt::cose
NOTARY_SIGNING_TIME_INDEX,
NOTARY_AUTHENTIC_SIGNING_TIME_INDEX,
NOTARY_EXPIRY_INDEX,
CONTRACT_PARTICIPANT_INFO_INDEX,
END_INDEX,
};
QCBORItem header_items[END_INDEX + 1];
Expand Down Expand Up @@ -359,6 +377,11 @@ namespace scitt::cose
header_items[NOTARY_EXPIRY_INDEX].uLabelType = QCBOR_TYPE_TEXT_STRING;
header_items[NOTARY_EXPIRY_INDEX].uDataType = QCBOR_TYPE_DATE_EPOCH;

header_items[CONTRACT_PARTICIPANT_INFO_INDEX].label.int64 =
CONTRACT_HEADER_PARAM_PARTICIPANT_INFO;
header_items[CONTRACT_PARTICIPANT_INFO_INDEX].uLabelType = QCBOR_TYPE_INT64;
header_items[CONTRACT_PARTICIPANT_INFO_INDEX].uDataType = QCBOR_TYPE_ARRAY;

header_items[END_INDEX].uLabelType = QCBOR_TYPE_NONE;

QCBORDecode_GetItemsInMap(&ctx, header_items);
Expand Down Expand Up @@ -469,6 +492,45 @@ namespace scitt::cose
parsed.notary_expiry =
header_items[NOTARY_EXPIRY_INDEX].val.epochDate.nSeconds;
}
// Contract headers
if (
header_items[CONTRACT_PARTICIPANT_INFO_INDEX].uDataType !=
QCBOR_TYPE_NONE)
{
parsed.participant_info = std::vector<std::string>();
QCBORItem participantItem = header_items[CONTRACT_PARTICIPANT_INFO_INDEX];
QCBORDecode_EnterArrayFromMapN(
&ctx, CONTRACT_HEADER_PARAM_PARTICIPANT_INFO);
while (true)
{
auto result = QCBORDecode_GetNext(&ctx, &participantItem);
if (result == QCBOR_ERR_NO_MORE_ITEMS)
{
break;
}
if (result != QCBOR_SUCCESS)
{
throw COSEDecodeError("Item in participant info is not well-formed.");
}
if (participantItem.uDataType == QCBOR_TYPE_TEXT_STRING)
{
parsed.participant_info->push_back(
std::string(cbor::as_string(participantItem.val.string)));
}
else
{
throw COSEDecodeError(
"Next item in participant info was not of type text string");
}
}
QCBORDecode_ExitArray(&ctx);
if (parsed.participant_info->empty())
{
throw COSEDecodeError(
"Cannot have participant info array of length 0 in COSE protected "
"header.");
}
}

QCBORDecode_ExitMap(&ctx);
QCBORDecode_ExitBstrWrapped(&ctx);
Expand Down Expand Up @@ -545,9 +607,9 @@ namespace scitt::cose
}

uint64_t tag = QCBORDecode_GetNthTagOfLast(&ctx, 0);
if (tag != CBOR_TAG_COSE_SIGN1)
if (tag != CBOR_TAG_COSE_SIGN1 && tag != CBOR_TAG_COSE_SIGN)
{
throw COSEDecodeError("COSE_Sign1 is not tagged");
throw COSEDecodeError("COSE_Sign1 or COSE_Sign is not tagged");
}

auto phdr = decode_protected_header(ctx);
Expand Down Expand Up @@ -611,6 +673,77 @@ namespace scitt::cose
return {phdrs, payload, signature};
}

/**
* Extract the bstr fields from a COSE Sign.
*
* Returns an array containing the protected headers, the payload and the
* signatures.
*/
static std::vector<std::span<const uint8_t>> extract_sign_fields(
std::span<const uint8_t> cose_sign)
{
std::vector<std::span<const uint8_t>> sign_fields;
QCBORDecodeContext ctx;
QCBORDecode_Init(
&ctx, cbor::from_bytes(cose_sign), QCBOR_DECODE_MODE_NORMAL);

QCBORDecode_EnterArray(&ctx, nullptr);

UsefulBufC bstr_item;
// protected headers
QCBORDecode_GetByteString(&ctx, &bstr_item);
auto phdrs = cbor::as_span(bstr_item);
sign_fields.push_back(phdrs);

QCBORItem item;
// skip unprotected header
QCBORDecode_VGetNextConsume(&ctx, &item);

// payload
QCBORDecode_GetByteString(&ctx, &bstr_item);
auto payload = cbor::as_span(bstr_item);
sign_fields.push_back(payload);

// signatures
QCBORDecode_EnterArray(&ctx, nullptr);

while (true)
{
QCBORDecode_EnterArray(&ctx, nullptr);
auto uErr = QCBORDecode_GetAndResetError(&ctx);
if (uErr != QCBOR_SUCCESS)
{
break;
}

// signature protected headers
QCBORDecode_GetByteString(&ctx, &bstr_item);
auto signature_phdrs = cbor::as_span(bstr_item);
sign_fields.push_back(signature_phdrs);

// skip unproctected headers
QCBORDecode_VGetNextConsume(&ctx, &item);

// signature
QCBORDecode_GetByteString(&ctx, &bstr_item);
auto signature = cbor::as_span(bstr_item);
sign_fields.push_back(signature);

QCBORDecode_ExitArray(&ctx);
}

QCBORDecode_ExitArray(&ctx);

QCBORDecode_ExitArray(&ctx);
auto error = QCBORDecode_Finish(&ctx);
if (error)
{
throw std::runtime_error("Failed to decode COSE_Sign");
}

return sign_fields;
}

/**
* Verify the signature of a COSE Sign1 message using the given public key.
*
Expand Down Expand Up @@ -787,4 +920,136 @@ namespace scitt::cose

return encoder.finish();
}

/**
* Compute the digest of the TBS for a countersignature over a COSE Sign.
*
* The following structure is hashed incrementally to avoid
* serializing it in full. This avoids excessive memory usage
* for larger payloads.
*
* Countersign_structure = [
* context: "CounterSignatureV2",
* body_protected: empty_or_serialized_map,
* sign_protected: empty_or_serialized_map,
* external_aad: bstr,
* payload: bstr,
* other_fields: [
* signatures: bstr
* ]
* ]
*/
static crypto::Sha256Hash create_countersign_tbs_hash_cose_sign(
std::span<const uint8_t> cose_sign, std::span<const uint8_t> sign_protected)
{
auto sign_fields = extract_sign_fields(cose_sign);
auto body_protected = sign_fields[0];
auto payload = sign_fields[1];
std::vector<uint8_t> signatures;
for (size_t i = 2; i < sign_fields.size(); i++)
{
signatures.insert(
signatures.end(), sign_fields[i].begin(), sign_fields[i].end());
}

// Hash the Countersign_structure incrementally.
cbor::hasher hash;
hash.open_array(6);
hash.add_text("CounterSignatureV2");

// body_protected: The protected header of the target message.
hash.add_bytes(body_protected);
// sign_protected: The protected header of the countersigner.
hash.add_bytes(sign_protected);
// external_aad: always empty.
hash.add_bytes({});
// payload: The payload of the target message.
hash.add_bytes(payload);

// other_fields: Array holding the signatures of the target message.
hash.open_array(1);
hash.add_bytes(signatures);

return hash.finalise();
}

static std::vector<uint8_t> embed_receipt_cose_sign(
const std::vector<uint8_t>& cose_sign, const std::vector<uint8_t>& receipt)
{
// t_cose doesn't support modifying the unprotected header yet.
// The following code is a low-level workaround.

// Extract fields from the COSE_Sign1 message.
auto sign_fields = extract_sign_fields(cose_sign);
auto protected_header = sign_fields[0];
auto payload = sign_fields[1];

// Decode unprotected header.
// TODO: This is a temporary solution to carry over Notary's x5chain
// parameter. Ideally, the full unprotected header should be preserved
// but that is more tricky to do in QCBOR.
UnprotectedHeader uhdr = std::get<1>(cose::decode_headers(cose_sign));
auto x5chain = uhdr.x5chain;

// Serialize COSE_Sign with new unprotected header.
cbor::encoder encoder;

QCBOREncode_AddTag(encoder, CBOR_TAG_COSE_SIGN);

QCBOREncode_OpenArray(encoder);

QCBOREncode_AddBytes(encoder, cbor::from_bytes(protected_header));

// unprotected header
QCBOREncode_OpenMap(encoder);
QCBOREncode_OpenArrayInMapN(encoder, COSE_HEADER_PARAM_SCITT_RECEIPTS);
QCBOREncode_AddEncoded(encoder, cbor::from_bytes(receipt));
QCBOREncode_CloseArray(encoder);
if (x5chain.has_value())
{
auto certs = x5chain.value();
if (certs.size() == 1)
{
// To obey the IETF COSE X509 draft;
// A single cert MUST be serialized as a single bstr.
QCBOREncode_AddBytesToMapN(
encoder, COSE_HEADER_PARAM_X5CHAIN, cbor::from_bytes(certs[0]));
}
else
{
// And multiple certs MUST be serialized as an array of bstrs.
QCBOREncode_OpenArrayInMapN(encoder, COSE_HEADER_PARAM_X5CHAIN);
for (auto& cert : certs)
{
QCBOREncode_AddBytes(encoder, cbor::from_bytes(cert));
}
QCBOREncode_CloseArray(encoder);
}
}
QCBOREncode_CloseMap(encoder);

QCBOREncode_AddBytes(encoder, cbor::from_bytes(payload));

// Signatures
QCBOREncode_OpenArray(encoder);
for (size_t i = 2; i < sign_fields.size(); i += 2)
{
QCBOREncode_OpenArray(encoder);
// protected headers
QCBOREncode_AddBytes(encoder, cbor::from_bytes(sign_fields[i]));

// Empty map with unprotected headers
QCBOREncode_OpenMap(encoder);
QCBOREncode_CloseMap(encoder);

// signature
QCBOREncode_AddBytes(encoder, cbor::from_bytes(sign_fields[i + 1]));
QCBOREncode_CloseArray(encoder);
}
QCBOREncode_CloseArray(encoder);

QCBOREncode_CloseArray(encoder);

return encoder.finish();
}
}
Loading
Loading