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

COSE back-endorsements for previous service identities #6510

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
55 commits
Select commit Hold shift + click to select a range
8bc7b3d
WIP COSE back-endorsements interface
maxtropets Sep 27, 2024
f56e188
Merge branch 'main' into f/6500-cose-service-identity-endorsements
maxtropets Sep 30, 2024
a777234
Store the endorsement in KV
maxtropets Sep 30, 2024
41da476
Merge branch 'main' into f/6500-cose-service-identity-endorsements
maxtropets Sep 30, 2024
cb3ed93
Sign key instead of cert
maxtropets Sep 30, 2024
ab92cf2
Merge branch 'f/6500-cose-service-identity-endorsements' of github.co…
maxtropets Sep 30, 2024
117a7b6
Populate txreceiptimpl with cose SI endorsements chain
maxtropets Sep 30, 2024
e84679e
Fix endorsement chain
maxtropets Oct 1, 2024
199c0e8
Err handling at create service
maxtropets Oct 1, 2024
affb9f7
Merge branch 'main' into f/6500-cose-service-identity-endorsements
maxtropets Oct 2, 2024
6619854
Fix iterators corner case
maxtropets Oct 2, 2024
25a9a60
Sign raw key not pem
maxtropets Oct 3, 2024
fd8aeab
Do endorsements when opening service, not creating
maxtropets Oct 4, 2024
4611b09
Polish receipts
maxtropets Oct 4, 2024
6123a3e
Merge branch 'main' into f/6500-cose-service-identity-endorsements
maxtropets Oct 4, 2024
90b7175
Fixups + logging cose endpoint
maxtropets Oct 4, 2024
edde321
Merge branch 'main' into f/6500-cose-service-identity-endorsements
maxtropets Oct 4, 2024
0f35629
Fixup result to avoid being stuck when no endorsements
maxtropets Oct 5, 2024
043bf58
Fixup prev identity ptr
maxtropets Oct 5, 2024
0f1deb6
till-from in fetch check and format
maxtropets Oct 5, 2024
3e909bb
cose endorsement endpoint fix path
maxtropets Oct 5, 2024
200b9b0
Fixup ids of seek and testcover
maxtropets Oct 5, 2024
a646ac1
More asserts
maxtropets Oct 5, 2024
61e6bba
Enable disabled test
maxtropets Oct 6, 2024
b994681
Trace logs
maxtropets Oct 7, 2024
d5731c3
Convert new table to internal and document
maxtropets Oct 7, 2024
540405c
Merge branch 'main' into f/6500-cose-service-identity-endorsements
maxtropets Oct 7, 2024
68f140a
Format
maxtropets Oct 7, 2024
babc082
Type alias + error handling
maxtropets Oct 7, 2024
3c0efba
Schema JSON for http sample
maxtropets Oct 7, 2024
f47e91d
Err handling for COSE unpack
maxtropets Oct 7, 2024
b113e72
Merge branch 'main' into f/6500-cose-service-identity-endorsements
maxtropets Oct 7, 2024
5448ae2
Attempting debug crash
maxtropets Oct 7, 2024
816860d
Inclusive TX range + validation
maxtropets Oct 7, 2024
16a0cd5
Format
maxtropets Oct 7, 2024
ac2ea73
Merge branch 'main' into f/6500-cose-service-identity-endorsements
maxtropets Oct 7, 2024
930acd9
Fix iterators bug
maxtropets Oct 8, 2024
b7b93ef
Merge branch 'main' into f/6500-cose-service-identity-endorsements
maxtropets Oct 8, 2024
6edf5b1
Named struct to return validity
maxtropets Oct 8, 2024
37c26d5
Schema test fix
maxtropets Oct 8, 2024
83a3381
Rename from->from_txid and to
maxtropets Oct 8, 2024
2f8a0f7
Style
maxtropets Oct 8, 2024
1f81bab
Consistent naming
maxtropets Oct 8, 2024
2b2de0b
Fix doc
maxtropets Oct 8, 2024
ba6d13f
View change from raft.h
maxtropets Oct 8, 2024
00cdbd3
Consistent epoch naming
maxtropets Oct 8, 2024
399646b
Better error handling
maxtropets Oct 8, 2024
8d85f29
Merge branch 'main' into f/6500-cose-service-identity-endorsements
maxtropets Oct 8, 2024
2f035e9
Typo
maxtropets Oct 8, 2024
b5a08f2
Changelog + release num
maxtropets Oct 8, 2024
6d612d6
Changelog
maxtropets Oct 8, 2024
d11794f
Merge branch 'main' into f/6500-cose-service-identity-endorsements
achamayou Oct 9, 2024
3b12223
Update CHANGELOG.md
achamayou Oct 9, 2024
45ba87e
Update CHANGELOG.md
achamayou Oct 9, 2024
1e20c58
Update pyproject.toml
achamayou Oct 9, 2024
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
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,14 @@ 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-dev2]

[6.0.0-dev2]: https://github.com/microsoft/CCF/releases/tag/6.0.0-dev2

### Added

- Introduced `ccf::describe_cose_endorsements_v1(receipt)` for COSE-endorsements chain of previous service identities (#6500).

## [6.0.0-dev1]

[6.0.0-dev1]: https://github.com/microsoft/CCF/releases/tag/6.0.0-dev1
Expand Down
10 changes: 9 additions & 1 deletion doc/audit/builtin_maps.rst
Original file line number Diff line number Diff line change
Expand Up @@ -509,4 +509,12 @@ Evidence inserted in the ledger by a primary producing a snapshot to establish p

Used to persist submitted shares during a recovery.

While the contents themselves are encrypted, the table is public so as to be accessible by nodes bootstrapping a recovery service.
While the contents themselves are encrypted, the table is public so as to be accessible by nodes bootstrapping a recovery service.


``previous_service_identity_endorsement``
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

**Key** Sentinel value 0, represented as a little-endian 64-bit unsigned integer.

**Value** Endorsed COSE sign1 for the interface, represented as a DER-encoded string.
52 changes: 52 additions & 0 deletions doc/schemas/app_openapi.json
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,17 @@
],
"type": "object"
},
"LoggingGetCoseEndorsements__Out": {
"properties": {
"endorsements": {
"$ref": "#/components/schemas/base64string_array"
}
},
"required": [
"endorsements"
],
"type": "object"
},
"LoggingGetHistoricalRange__Entry": {
"properties": {
"id": {
Expand Down Expand Up @@ -215,6 +226,16 @@
],
"type": "string"
},
"base64string": {
"format": "base64",
"type": "string"
},
"base64string_array": {
"items": {
"$ref": "#/components/schemas/base64string"
},
"type": "array"
},
"boolean": {
"type": "boolean"
},
Expand Down Expand Up @@ -1210,6 +1231,37 @@
}
}
},
"/app/log/public/cose_endorsements": {
"get": {
"operationId": "GetAppLogPublicCoseEndorsements",
"responses": {
"200": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/LoggingGetCoseEndorsements__Out"
}
}
},
"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/count": {
"get": {
"operationId": "GetAppLogPublicCount",
Expand Down
8 changes: 8 additions & 0 deletions include/ccf/crypto/cose_verifier.h
Original file line number Diff line number Diff line change
Expand Up @@ -27,4 +27,12 @@ namespace ccf::crypto
COSEVerifierUniquePtr make_cose_verifier_from_cert(
const std::vector<uint8_t>& cert);
COSEVerifierUniquePtr make_cose_verifier_from_key(const Pem& public_key);

struct COSEEndorsementValidity
{
std::string from_txid{};
std::string to_txid{};
};
COSEEndorsementValidity extract_cose_endorsement_validity(
std::span<const uint8_t> cose_msg);
}
15 changes: 15 additions & 0 deletions include/ccf/historical_queries_utils.h
Original file line number Diff line number Diff line change
Expand Up @@ -25,4 +25,19 @@ namespace ccf::historical
AbstractStateCache& state_cache,
std::shared_ptr<NetworkIdentitySubsystemInterface>
network_identity_subsystem);

// Modifies the receipt stored in state to include historical service
// endorsements, where required. If the state talks about a different service
// identity, which is known to be a predecessor of this service (via disaster
// recoveries), then an endorsement chain of all service identities preceding
// the current one will be created. This may need to use the state_cache to
// request additional historical entries to construct this endorsement, and
// may read from the current/latest state via tx. Returns true if the
// operation is complete, though it may still have failed to produce an
// endorsement. Returns false if additional entries have been requested, in
// which case the caller should retry later.
bool populate_cose_service_endorsements(
ccf::kv::ReadOnlyTx& tx,
ccf::historical::StatePtr& state,
AbstractStateCache& state_cache);
}
5 changes: 5 additions & 0 deletions include/ccf/receipt.h
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,11 @@ namespace ccf
std::optional<std::vector<uint8_t>> describe_merkle_proof_v1(
const TxReceiptImpl& in);

using SerialisedCoseEndorsement = std::vector<uint8_t>;
using SerialisedCoseEndorsements = std::vector<SerialisedCoseEndorsement>;
std::optional<SerialisedCoseEndorsements> describe_cose_endorsements_v1(
const TxReceiptImpl& in);

// Manual JSON serializers are specified for these types as they are not
// trivial POD structs

Expand Down
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-dev1"
version = "6.0.0-dev2"
authors = [
{ name="CCF Team", email="[email protected]" },
]
Expand Down
14 changes: 6 additions & 8 deletions python/src/ccf/cose.py
Original file line number Diff line number Diff line change
Expand Up @@ -174,16 +174,14 @@ def create_cose_sign1_finish(
return msg.encode(sign=False)


def validate_cose_sign1(payload: bytes, cert_pem: Pem, cose_sign1: bytes):
cert = load_pem_x509_certificate(cert_pem.encode("ascii"), default_backend())
if not isinstance(cert.public_key(), EllipticCurvePublicKey):
raise NotImplementedError("unsupported key type")

key = cert.public_key()
cose_key = from_cryptography_eckey_obj(key)
def validate_cose_sign1(pubkey, cose_sign1, payload=None):
cose_key = from_cryptography_eckey_obj(pubkey)
msg = Sign1Message.decode(cose_sign1)
msg.key = cose_key
msg.payload = payload

if payload:
# Detached payload
msg.payload = payload

if not msg.verify_signature():
raise ValueError("signature is invalid")
Expand Down
5 changes: 4 additions & 1 deletion python/src/ccf/ledger.py
Original file line number Diff line number Diff line change
Expand Up @@ -571,8 +571,11 @@ def _verify_root_signature(self, tx_info: TxBundleInfo):

def _verify_root_cose_signature(self, root, cose_sign1):
try:
cert = load_pem_x509_certificate(
self.service_cert.encode("ascii"), default_backend()
)
validate_cose_sign1(
payload=root, cert_pem=self.service_cert, cose_sign1=cose_sign1
cose_sign1=cose_sign1, pubkey=cert.public_key(), payload=root
)
except Exception as exc:
raise InvalidRootCoseSignatureException(
Expand Down
41 changes: 41 additions & 0 deletions samples/apps/logging/logging.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1961,6 +1961,47 @@ namespace loggingapp
.set_auto_schema<void, void>()
.set_forwarding_required(ccf::endpoints::ForwardingRequired::Never)
.install();

auto get_cose_endorsements =
[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 endorsements =
describe_cose_endorsements_v1(*historical_state->receipt);
if (!endorsements.has_value())
{
ctx.rpc_ctx->set_error(
HTTP_STATUS_NOT_FOUND,
ccf::errors::ResourceNotFound,
"No COSE endorsements available for this transaction");
return;
}
LoggingGetCoseEndorsements::Out response{
.endorsements = ccf::SerialisedCoseEndorsements{}};
for (const auto& endorsement : *endorsements)
{
response.endorsements->push_back(endorsement);
}

nlohmann::json j_response = response;
ctx.rpc_ctx->set_response_status(HTTP_STATUS_OK);
ctx.rpc_ctx->set_response_header(
ccf::http::headers::CONTENT_TYPE,
ccf::http::headervalues::contenttype::JSON);
ctx.rpc_ctx->set_response_body(j_response.dump());
};
make_read_only_endpoint(
"/log/public/cose_endorsements",
HTTP_GET,
ccf::historical::read_only_adapter_v4(
get_cose_endorsements, context, is_tx_committed),
auth_policies)
.set_auto_schema<void, LoggingGetCoseEndorsements::Out>()
.set_forwarding_required(ccf::endpoints::ForwardingRequired::Never)
.install();
}
};
}
Expand Down
10 changes: 10 additions & 0 deletions samples/apps/logging/logging_schema.h
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,16 @@ namespace loggingapp
DECLARE_JSON_OPTIONAL_FIELDS_WITH_RENAMES(
LoggingGetHistoricalRange::Out, next_link, "@nextLink");

struct LoggingGetCoseEndorsements
{
struct Out
{
std::optional<ccf::SerialisedCoseEndorsements> endorsements;
};
};
DECLARE_JSON_TYPE(LoggingGetCoseEndorsements::Out);
DECLARE_JSON_REQUIRED_FIELDS(LoggingGetCoseEndorsements::Out, endorsements);

// Public record/get
// Manual schemas, verified then parsed in handler
static const std::string j_record_public_in = R"!!!(
Expand Down
34 changes: 29 additions & 5 deletions src/crypto/openssl/cose_sign.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,8 @@ namespace

namespace ccf::crypto
{
std::optional<int> key_to_cose_alg_id(ccf::crypto::PublicKey_OpenSSL& key)
std::optional<int> key_to_cose_alg_id(
const ccf::crypto::PublicKey_OpenSSL& key)
{
const auto cid = key.get_curve_id();
switch (cid)
Expand Down Expand Up @@ -155,10 +156,25 @@ namespace ccf::crypto
args_size);
}

COSEParametersFactory cose_params_string_bytes(
const std::string& key, const std::vector<uint8_t>& value)
{
const size_t args_size = key.size() + value.size() +
extra_size_for_seq_tag + extra_size_for_seq_tag;
q_useful_buf_c buf{value.data(), value.size()};
return COSEParametersFactory(
[=](QCBOREncodeContext* ctx) {
QCBOREncode_AddSZString(ctx, key.data());
QCBOREncode_AddBytes(ctx, buf);
},
args_size);
}

std::vector<uint8_t> cose_sign1(
KeyPair_OpenSSL& key,
const KeyPair_OpenSSL& key,
const std::vector<COSEParametersFactory>& protected_headers,
std::span<const uint8_t> payload)
std::span<const uint8_t> payload,
bool detached_payload)
{
const auto buf_size = estimate_buffer_size(protected_headers, payload);
std::vector<uint8_t> underlying_buffer(buf_size);
Expand Down Expand Up @@ -188,8 +204,16 @@ namespace ccf::crypto

encode_parameters_custom(&sign_ctx, &cbor_encode, protected_headers);

// Mark empty payload manually.
QCBOREncode_AddNULL(&cbor_encode);
if (detached_payload)
{
// Mark empty payload explicitly.
QCBOREncode_AddNULL(&cbor_encode);
}
else
{
UsefulBufC payload_buffer{payload.data(), payload.size()};
QCBOREncode_AddBytes(&cbor_encode, payload_buffer);
}

// If payload is empty - we still want to sign. Putting NULL_Q_USEFUL_BUF_C,
// however, makes t_cose think that the payload is included into the
Expand Down
17 changes: 13 additions & 4 deletions src/crypto/openssl/cose_sign.h
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,11 @@ namespace ccf::crypto
// Standardised: verifiable data structure
static constexpr int64_t COSE_PHEADER_KEY_VDS = 395;
// CCF-specific: last signed TxID
static constexpr const char* COSE_PHEADER_KEY_TXID = "ccf.txid";
static const std::string COSE_PHEADER_KEY_TXID = "ccf.txid";
// CCF-specific: first TX in the range.
static const std::string COSE_PHEADER_KEY_RANGE_BEGIN = "ccf.epoch.begin";
// CCF-specific: last TX included in the range.
static const std::string COSE_PHEADER_KEY_RANGE_END = "ccf.epoch.end";

class COSEParametersFactory
{
Expand Down Expand Up @@ -59,12 +63,16 @@ namespace ccf::crypto
COSEParametersFactory cose_params_int_bytes(
int64_t key, const std::vector<uint8_t>& value);

COSEParametersFactory cose_params_string_bytes(
const std::string& key, const std::vector<uint8_t>& value);

struct COSESignError : public std::runtime_error
{
COSESignError(const std::string& msg) : std::runtime_error(msg) {}
};

std::optional<int> key_to_cose_alg_id(ccf::crypto::PublicKey_OpenSSL& key);
std::optional<int> key_to_cose_alg_id(
const ccf::crypto::PublicKey_OpenSSL& key);

/* Sign a cose_sign1 payload with custom protected headers as strings, where
- key: integer label to be assigned in a COSE value
Expand All @@ -74,7 +82,8 @@ namespace ccf::crypto
https://www.iana.org/assignments/cose/cose.xhtml#header-parameters.
*/
std::vector<uint8_t> cose_sign1(
KeyPair_OpenSSL& key,
const KeyPair_OpenSSL& key,
const std::vector<COSEParametersFactory>& protected_headers,
std::span<const uint8_t> payload);
std::span<const uint8_t> payload,
bool detached_payload = true);
}
Loading