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 signatures over merkle root in the ledger #6453

Merged
merged 36 commits into from
Sep 11, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
937c621
WIP
maxtropets Aug 27, 2024
e3a71ad
Cose signature simplified. Check tests
maxtropets Aug 28, 2024
f5da99b
Key fixup
maxtropets Aug 28, 2024
400bba6
Support variable COSE header types
maxtropets Aug 29, 2024
f51b3b1
Sign VDS + txid
maxtropets Aug 29, 2024
14818e7
Change node key to service key for COSE
maxtropets Aug 29, 2024
2bed88b
Change key setting in history
maxtropets Aug 30, 2024
baabe3d
Doc rst WIP
maxtropets Aug 30, 2024
35bacc0
Fixup keys in tests
maxtropets Aug 30, 2024
6de9314
Fixup change deser
maxtropets Aug 30, 2024
49537db
Verify COSE in history->verify
maxtropets Sep 5, 2024
d89b7c5
Format checks
maxtropets Sep 5, 2024
246e3d5
Optimise key creation
maxtropets Sep 5, 2024
ec538fa
Rollback public key caching
maxtropets Sep 6, 2024
c046403
FIx history test (mock service key)
maxtropets Sep 6, 2024
ec59604
Merge branch 'main' into f/cose-sign-merkle-root
maxtropets Sep 6, 2024
7ef7f27
Change default curve to es384
maxtropets Sep 10, 2024
5272c14
Add cose sig verification to python ledger checker
maxtropets Sep 10, 2024
69fa97d
Fix linter
maxtropets Sep 10, 2024
45ec618
Use correct alg id in signing
maxtropets Sep 10, 2024
1055180
Cache verifier
maxtropets Sep 10, 2024
cbd46f2
Improve alg. verification in cpp code
maxtropets Sep 10, 2024
08a800c
Format fix
maxtropets Sep 10, 2024
d35115e
Pass kid as key hash
maxtropets Sep 10, 2024
34f21f3
Update doc
maxtropets Sep 10, 2024
d3dcf18
Merge branch 'main' into f/cose-sign-merkle-root
maxtropets Sep 10, 2024
270a646
Redundant spaces
maxtropets Sep 10, 2024
63a30b4
Long test (removeme)
maxtropets Sep 10, 2024
3b9f986
Typos and logs
maxtropets Sep 11, 2024
bd94818
FIx ASAN
maxtropets Sep 11, 2024
da541bb
Remove SECP256K1 support
maxtropets Sep 11, 2024
77c11dc
COSE sig as bytes instead of JSON(bytes)
maxtropets Sep 11, 2024
bb3adc1
Improved estimated arg size
maxtropets Sep 11, 2024
c00f963
Merge branch 'main' into f/cose-sign-merkle-root
maxtropets Sep 11, 2024
a668cf8
Revert "Long test (removeme)"
maxtropets Sep 11, 2024
e90e33f
Cose as JSON in python parser
maxtropets Sep 11, 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
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);
maxtropets marked this conversation as resolved.
Show resolved Hide resolved
}

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