Skip to content

Commit

Permalink
COSE endorsements shall include previous epoch root (#6555)
Browse files Browse the repository at this point in the history
  • Loading branch information
maxtropets authored Oct 11, 2024
1 parent 5f78120 commit 0292e12
Show file tree
Hide file tree
Showing 6 changed files with 60 additions and 11 deletions.
10 changes: 9 additions & 1 deletion doc/audit/builtin_maps.rst
Original file line number Diff line number Diff line change
Expand Up @@ -517,4 +517,12 @@ While the contents themselves are encrypted, the table is public so as to be acc

**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.
**Value** Endorsed COSE sign1 for the interface, represented as a DER-encoded string.


``previous_service_last_signed_root``
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

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

**Value** Last signed Merkle root of previous service instance, represented as a hex-encoded string.
10 changes: 6 additions & 4 deletions src/crypto/openssl/cose_sign.h
Original file line number Diff line number Diff line change
Expand Up @@ -12,18 +12,20 @@

namespace ccf::crypto
{
// Standardised field: algorithm used to sign
// Standardised field: algorithm used to sign.
static constexpr int64_t COSE_PHEADER_KEY_ALG = 1;
// Standardised: hash of the signing key
// Standardised: hash of the signing key.
static constexpr int64_t COSE_PHEADER_KEY_ID = 4;
// Standardised: verifiable data structure
// Standardised: verifiable data structure.
static constexpr int64_t COSE_PHEADER_KEY_VDS = 395;
// CCF-specific: last signed TxID
// CCF-specific: last signed 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";
// CCF-specific: Merkle root hash.
static const std::string COSE_PHEADER_KEY_MERKLE_ROOT = "ccf.merkle.root";

class COSEParametersFactory
{
Expand Down
29 changes: 27 additions & 2 deletions src/service/internal_tables_access.h
Original file line number Diff line number Diff line change
Expand Up @@ -350,9 +350,19 @@ namespace ccf
ccf::Tables::PREVIOUS_SERVICE_IDENTITY);
previous_service_identity->put(prev_service_info->cert);

auto last_signed_root = tx.wo<ccf::PreviousServiceLastSignedRoot>(
ccf::Tables::PREVIOUS_SERVICE_LAST_SIGNED_ROOT);
auto sigs = tx.ro<ccf::Signatures>(ccf::Tables::SIGNATURES);
if (!sigs->has())
{
throw std::logic_error(
"Previous service doesn't have any signed transactions");
}
last_signed_root->put(sigs->get()->root);

// Record number of recoveries for service. If the value does
// not exist in the table (i.e. pre 2.x ledger), assume it is the first
// recovery.
// not exist in the table (i.e. pre 2.x ledger), assume it is the
// first recovery.
recovery_count = prev_service_info->recovery_count.value_or(0) + 1;
}

Expand Down Expand Up @@ -404,6 +414,21 @@ namespace ccf
previous_identity_endorsement->get_version_of_previous_write();

key_to_endorse = prev_endorsement->endorsing_key;

auto previous_service_last_signed_root =
tx.ro<ccf::PreviousServiceLastSignedRoot>(
ccf::Tables::PREVIOUS_SERVICE_LAST_SIGNED_ROOT);
if (!previous_service_last_signed_root->has())
{
LOG_FAIL_FMT(
"Failed to sign previous service identity: no last signed root");
return false;
}

const auto root = previous_service_last_signed_root->get().value();
pheaders.push_back(ccf::crypto::cose_params_string_bytes(
ccf::crypto::COSE_PHEADER_KEY_MERKLE_ROOT,
std::vector<uint8_t>(root.h.begin(), root.h.end())));
}
else
{
Expand Down
2 changes: 2 additions & 0 deletions src/service/network_tables.h
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,8 @@ namespace ccf
const Service service = {Tables::SERVICE};
const PreviousServiceIdentity previous_service_identity = {
Tables::PREVIOUS_SERVICE_IDENTITY};
const PreviousServiceLastSignedRoot previous_service_last_signed_root = {
Tables::PREVIOUS_SERVICE_LAST_SIGNED_ROOT};
const PreviousServiceIdentityEndorsement
previous_service_identity_endorsement = {
Tables::PREVIOUS_SERVICE_IDENTITY_ENDORSEMENT};
Expand Down
4 changes: 4 additions & 0 deletions src/service/tables/previous_service_identity.h
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ namespace ccf
{
using PreviousServiceIdentity = ServiceValue<ccf::crypto::Pem>;

using PreviousServiceLastSignedRoot = ServiceValue<ccf::crypto::Sha256Hash>;

struct CoseEndorsement
{
/// COSE-sign of the a previous service identity's public key.
Expand Down Expand Up @@ -45,6 +47,8 @@ namespace ccf
{
static constexpr auto PREVIOUS_SERVICE_IDENTITY =
"public:ccf.gov.service.previous_service_identity";
static constexpr auto PREVIOUS_SERVICE_LAST_SIGNED_ROOT =
"public:ccf.internal.previous_service_last_signed_root";
static constexpr auto PREVIOUS_SERVICE_IDENTITY_ENDORSEMENT =
"public:ccf.internal.previous_service_identity_endorsement";
}
Expand Down
16 changes: 12 additions & 4 deletions tests/recovery.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,10 +66,18 @@ def query_endorsements_chain(node, txid):
return response


def verify_endorsements_chain(endorsements, pubkey):
def verify_endorsements_chain(primary, endorsements, pubkey):
for endorsement in endorsements:
validate_cose_sign1(cose_sign1=endorsement, pubkey=pubkey)
next_key_bytes = Sign1Message.decode(endorsement).payload

cose_msg = Sign1Message.decode(endorsement)
last_tx = ccf.tx_id.TxID.from_str(cose_msg.phdr["ccf.epoch.end"])
receipt = primary.get_receipt(last_tx.view, last_tx.seqno)
root_from_receipt = bytes.fromhex(receipt.json()["leaf"])
root_from_headers = cose_msg.phdr["ccf.merkle.root"]
assert root_from_receipt == root_from_headers

next_key_bytes = cose_msg.payload
pubkey = serialization.load_der_public_key(next_key_bytes, default_backend())


Expand Down Expand Up @@ -366,7 +374,7 @@ def test_recover_service_with_wrong_identity(network, args):
base64.b64decode(x) for x in response.body.json()["endorsements"]
]
assert len(endorsements) == 2 # 2 recoveries behind
verify_endorsements_chain(endorsements, cert.public_key())
verify_endorsements_chain(primary, endorsements, cert.public_key())

for tx in txids[1:4]:
response = query_endorsements_chain(primary, tx)
Expand All @@ -375,7 +383,7 @@ def test_recover_service_with_wrong_identity(network, args):
base64.b64decode(x) for x in response.body.json()["endorsements"]
]
assert len(endorsements) == 1 # 1 recovery behind
verify_endorsements_chain(endorsements, cert.public_key())
verify_endorsements_chain(primary, endorsements, cert.public_key())

for tx in txids[4:]:
response = query_endorsements_chain(primary, tx)
Expand Down

0 comments on commit 0292e12

Please sign in to comment.