Skip to content

Commit

Permalink
COSE signatures over merkle root in the ledger (#6453)
Browse files Browse the repository at this point in the history
  • Loading branch information
maxtropets authored Sep 11, 2024
1 parent 4093777 commit b08724a
Show file tree
Hide file tree
Showing 21 changed files with 612 additions and 111 deletions.
13 changes: 13 additions & 0 deletions doc/audit/builtin_maps.rst
Original file line number Diff line number Diff line change
Expand Up @@ -471,6 +471,19 @@ Signatures emitted by the primary node at regular interval, over the root of the
:project: CCF
:members:

``cose_signatures``
~~~~~~~~~~~~~~

COSE signatures emitted by the primary node over the root of the Merkle Tree at that sequence number.

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

**Value**

.. doxygenstruct:: ccf::CoseSignature
:project: CCF
:members:

``recovery_shares``
~~~~~~~~~~~~~~~~~~~

Expand Down
2 changes: 2 additions & 0 deletions include/ccf/crypto/cose_verifier.h
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ namespace ccf::crypto
virtual bool verify(
const std::span<const uint8_t>& buf,
std::span<uint8_t>& authned_content) const = 0;
virtual bool verify_detached(
std::span<const uint8_t> buf, std::span<const uint8_t> payload) const = 0;
virtual ~COSEVerifier() = default;
};

Expand Down
15 changes: 15 additions & 0 deletions python/src/ccf/cose.py
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,21 @@ 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)
msg = Sign1Message.decode(cose_sign1)
msg.key = cose_key
msg.payload = payload

if not msg.verify_signature():
raise ValueError("signature is invalid")


_SIGN_DESCRIPTION = """Create and sign a COSE Sign1 message for CCF governance
Note that this tool writes binary COSE Sign1 to standard output.
Expand Down
27 changes: 27 additions & 0 deletions python/src/ccf/ledger.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@

from ccf.merkletree import MerkleTree
from ccf.tx_id import TxID
from ccf.cose import validate_cose_sign1
import ccf.receipt
from hashlib import sha256
import functools
Expand All @@ -31,6 +32,7 @@

# Public table names as defined in CCF
SIGNATURE_TX_TABLE_NAME = "public:ccf.internal.signatures"
COSE_SIGNATURE_TX_TABLE_NAME = "public:ccf.internal.cose_signatures"
NODES_TABLE_NAME = "public:ccf.gov.nodes.info"
ENDORSED_NODE_CERTIFICATES_TABLE_NAME = "public:ccf.gov.nodes.endorsed_certificates"
SERVICE_INFO_TABLE_NAME = "public:ccf.gov.service.info"
Expand Down Expand Up @@ -389,6 +391,7 @@ def __init__(self, accept_deprecated_entry_types: bool = True):
self.last_verified_view = 0

self.service_status = None
self.service_cert = None

def last_verified_txid(self) -> TxID:
return TxID(self.last_verified_view, self.last_verified_seqno)
Expand Down Expand Up @@ -509,6 +512,14 @@ def add_transaction(self, transaction):
else:
assert self.service_status is None, self.service_status
self.service_status = updated_status
self.service_cert = updated_service_json["cert"]

if COSE_SIGNATURE_TX_TABLE_NAME in tables:
cose_signature_table = tables[COSE_SIGNATURE_TX_TABLE_NAME]
cose_signature = cose_signature_table.get(WELL_KNOWN_SINGLETON_TABLE_KEY)
signature = json.loads(cose_signature)
cose_sign1 = base64.b64decode(signature)
self._verify_root_cose_signature(self.merkle.get_merkle_root(), cose_sign1)

# Checks complete, add this transaction to tree
self.merkle.add_leaf(transaction.get_tx_digest(), False)
Expand Down Expand Up @@ -558,6 +569,18 @@ def _verify_root_signature(self, tx_info: TxBundleInfo):
+ f"\nRoot: {tx_info.existing_root.hex()}"
) from InvalidSignature

def _verify_root_cose_signature(self, root, cose_sign1):
try:
validate_cose_sign1(
payload=root, cert_pem=self.service_cert, cose_sign1=cose_sign1
)
except Exception as exc:
raise InvalidRootCoseSignatureException(
"Signature verification failed:"
+ f"\nCertificate: {self.service_cert}"
+ f"\nRoot: {root}"
) from exc

def _verify_merkle_root(self, merkletree: MerkleTree, existing_root: bytes):
"""Verify item 3, by comparing the roots from the merkle tree that's maintained by this class and from the one extracted from the ledger"""
root = merkletree.get_merkle_root()
Expand Down Expand Up @@ -1061,6 +1084,10 @@ class InvalidRootSignatureException(Exception):
"""Signature of the MerkleRoot doesn't match with the signature that's reported in the signature's table"""


class InvalidRootCoseSignatureException(Exception):
"""COSE signature of the MerkleRoot doesn't pass COSE verification"""


class CommitIdRangeException(Exception):
"""Missing ledger chunk in the ledger directory"""

Expand Down
112 changes: 97 additions & 15 deletions src/crypto/openssl/cose_sign.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,14 @@
#include "ccf/ds/logger.h"

#include <openssl/evp.h>
#include <t_cose/t_cose_sign1_sign.h>

namespace
{
constexpr int64_t COSE_HEADER_PARAM_ALG =
1; // Duplicate of t_cose::COSE_HEADER_PARAM_ALG to keep it compatible.
static constexpr size_t extra_size_for_int_tag = 1; // type
static constexpr size_t extra_size_for_seq_tag = 1 + 8; // type + size

size_t estimate_buffer_size(
const ccf::crypto::COSEProtectedHeaders& protected_headers,
const std::vector<ccf::crypto::COSEParametersFactory>& protected_headers,
std::span<const uint8_t> payload)
{
size_t result =
Expand All @@ -28,8 +27,8 @@ namespace
protected_headers.begin(),
protected_headers.end(),
result,
[](auto result, const auto& kv) {
return result + sizeof(kv.first) + kv.second.size();
[](auto result, const auto& factory) {
return result + factory.estimated_size();
});

return result + payload.size();
Expand All @@ -38,20 +37,20 @@ namespace
void encode_protected_headers(
t_cose_sign1_sign_ctx* ctx,
QCBOREncodeContext* encode_ctx,
const ccf::crypto::COSEProtectedHeaders& protected_headers)
const std::vector<ccf::crypto::COSEParametersFactory>& protected_headers)
{
QCBOREncode_BstrWrap(encode_ctx);
QCBOREncode_OpenMap(encode_ctx);

// This's what the t_cose implementation of `encode_protected_parameters`
// sets unconditionally.
QCBOREncode_AddInt64ToMapN(
encode_ctx, COSE_HEADER_PARAM_ALG, ctx->cose_algorithm_id);
encode_ctx, ccf::crypto::COSE_PHEADER_KEY_ALG, ctx->cose_algorithm_id);

// Caller-provided headers follow
for (const auto& [label, value] : protected_headers)
for (const auto& factory : protected_headers)
{
QCBOREncode_AddSZStringToMapN(encode_ctx, label, value.c_str());
factory.apply(encode_ctx);
}

QCBOREncode_CloseMap(encode_ctx);
Expand All @@ -68,7 +67,7 @@ namespace
void encode_parameters_custom(
struct t_cose_sign1_sign_ctx* me,
QCBOREncodeContext* cbor_encode,
const ccf::crypto::COSEProtectedHeaders& protected_headers)
const std::vector<ccf::crypto::COSEParametersFactory>& protected_headers)
{
QCBOREncode_AddTag(cbor_encode, CBOR_TAG_COSE_SIGN1);
QCBOREncode_OpenArray(cbor_encode);
Expand All @@ -83,9 +82,85 @@ namespace

namespace ccf::crypto
{
std::optional<int> key_to_cose_alg_id(ccf::crypto::PublicKey_OpenSSL& key)
{
const auto cid = key.get_curve_id();
switch (cid)
{
case ccf::crypto::CurveID::SECP256R1:
return T_COSE_ALGORITHM_ES256;
case ccf::crypto::CurveID::SECP384R1:
return T_COSE_ALGORITHM_ES384;
default:
return std::nullopt;
}
}

COSEParametersFactory cose_params_int_int(int64_t key, int64_t value)
{
const size_t args_size = sizeof(key) + sizeof(value) +
extra_size_for_int_tag + extra_size_for_int_tag;
return COSEParametersFactory(
[=](QCBOREncodeContext* ctx) {
QCBOREncode_AddInt64ToMapN(ctx, key, value);
},
args_size);
}

COSEParametersFactory cose_params_int_string(
int64_t key, const std::string& value)
{
const size_t args_size = sizeof(key) + value.size() +
extra_size_for_int_tag + extra_size_for_seq_tag;
return COSEParametersFactory(
[=](QCBOREncodeContext* ctx) {
QCBOREncode_AddSZStringToMapN(ctx, key, value.data());
},
args_size);
}

COSEParametersFactory cose_params_string_int(
const std::string& key, int64_t value)
{
const size_t args_size = key.size() + sizeof(value) +
extra_size_for_seq_tag + extra_size_for_int_tag;
return COSEParametersFactory(
[=](QCBOREncodeContext* ctx) {
QCBOREncode_AddSZString(ctx, key.data());
QCBOREncode_AddInt64(ctx, value);
},
args_size);
}

COSEParametersFactory cose_params_string_string(
const std::string& key, const std::string& value)
{
const size_t args_size = key.size() + value.size() +
extra_size_for_seq_tag + extra_size_for_seq_tag;
return COSEParametersFactory(
[=](QCBOREncodeContext* ctx) {
QCBOREncode_AddSZString(ctx, key.data());
QCBOREncode_AddSZString(ctx, value.data());
},
args_size);
}

COSEParametersFactory cose_params_int_bytes(
int64_t key, const std::vector<uint8_t>& value)
{
const size_t args_size = sizeof(key) + value.size() +
+extra_size_for_int_tag + extra_size_for_seq_tag;
q_useful_buf_c buf{value.data(), value.size()};
return COSEParametersFactory(
[=](QCBOREncodeContext* ctx) {
QCBOREncode_AddBytesToMapN(ctx, key, buf);
},
args_size);
}

std::vector<uint8_t> cose_sign1(
EVP_PKEY* key,
const COSEProtectedHeaders& protected_headers,
KeyPair_OpenSSL& key,
const std::vector<COSEParametersFactory>& protected_headers,
std::span<const uint8_t> payload)
{
const auto buf_size = estimate_buffer_size(protected_headers, payload);
Expand All @@ -95,11 +170,18 @@ namespace ccf::crypto
QCBOREncode_Init(&cbor_encode, signed_cose_buffer);

t_cose_sign1_sign_ctx sign_ctx;
t_cose_sign1_sign_init(&sign_ctx, 0, T_COSE_ALGORITHM_ES256);
const auto algorithm_id = key_to_cose_alg_id(key);
if (!algorithm_id.has_value())
{
throw ccf::crypto::COSESignError(fmt::format("Unsupported key type"));
}

t_cose_sign1_sign_init(&sign_ctx, 0, *algorithm_id);

EVP_PKEY* evp_key = key;
t_cose_key signing_key;
signing_key.crypto_lib = T_COSE_CRYPTO_LIB_OPENSSL;
signing_key.k.key_ptr = key;
signing_key.k.key_ptr = evp_key;

t_cose_sign1_set_signing_key(&sign_ctx, signing_key, NULL_Q_USEFUL_BUF_C);

Expand Down
56 changes: 53 additions & 3 deletions src/crypto/openssl/cose_sign.h
Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,69 @@
// Licensed under the Apache 2.0 License.
#pragma once

#include "crypto/openssl/key_pair.h"

#include <openssl/ossl_typ.h>
#include <span>
#include <string>
#include <t_cose/t_cose_sign1_sign.h>
#include <unordered_map>

namespace ccf::crypto
{
// Standardised field: algorithm used to sign
static constexpr int64_t COSE_PHEADER_KEY_ALG = 1;
// Standardised: hash of the signing key
static constexpr int64_t COSE_PHEADER_KEY_ID = 4;
// 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";

class COSEParametersFactory
{
public:
template <typename Callable>
COSEParametersFactory(Callable&& impl, size_t args_size) :
impl(std::forward<Callable>(impl)),
args_size{args_size}
{}

void apply(QCBOREncodeContext* ctx) const
{
impl(ctx);
}

size_t estimated_size() const
{
return args_size;
}

private:
std::function<void(QCBOREncodeContext*)> impl{};
size_t args_size{};
};

COSEParametersFactory cose_params_int_int(int64_t key, int64_t value);

COSEParametersFactory cose_params_int_string(
int64_t key, const std::string& value);

COSEParametersFactory cose_params_string_int(
const std::string& key, int64_t value);

COSEParametersFactory cose_params_string_string(
const std::string& key, const std::string& value);

COSEParametersFactory cose_params_int_bytes(
int64_t key, const std::vector<uint8_t>& value);

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

using COSEProtectedHeaders = std::unordered_map<int64_t, std::string>;
std::optional<int> key_to_cose_alg_id(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 @@ -24,7 +74,7 @@ namespace ccf::crypto
https://www.iana.org/assignments/cose/cose.xhtml#header-parameters.
*/
std::vector<uint8_t> cose_sign1(
EVP_PKEY* key,
const COSEProtectedHeaders& protected_headers,
KeyPair_OpenSSL& key,
const std::vector<COSEParametersFactory>& protected_headers,
std::span<const uint8_t> payload);
}
Loading

0 comments on commit b08724a

Please sign in to comment.