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 28 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
95 changes: 95 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -93,3 +93,98 @@ jobs:
build/workspace/*/err
if-no-files-found: ignore
if: success() || failure()

scan_build:
maxtropets marked this conversation as resolved.
Show resolved Hide resolved
name: "Scan build"
runs-on: [self-hosted, 1ES.Pool=gha-virtual-ccf-sub]
container:
image: ghcr.io/microsoft/ccf/ci/default:build-25-07-2024

steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: "Run scan"
run: |
set -x
mkdir build
cd build
../scripts/scan-build.sh

long-asan:
name: ASAN
runs-on: [self-hosted, 1ES.Pool=gha-virtual-ccf-sub]
container:
image: ghcr.io/microsoft/ccf/ci/default:build-25-07-2024

steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: "Install deps"
run: |
sudo apt-get -y update
sudo apt install ansible -y
cd getting_started/setup_vm
ansible-playbook ccf-extended-testing.yml

- name: "Build"
run: |
git config --global --add safe.directory "$GITHUB_WORKSPACE"
mkdir build
cd build
cmake -GNinja -DCOMPILE_TARGET=virtual -DCMAKE_BUILD_TYPE=Debug -DLONG_TESTS=ON -DLVI_MITIGATIONS=OFF -DSAN=ON ..
ninja

- name: "Test"
run: |
set +x
cd build
./tests.sh --output-on-failure --timeout 1600 -LE "benchmark"

- name: "Upload logs"
if: success() || failure()
uses: actions/upload-artifact@v4
with:
name: logs-asan
path: |
build/workspace/*/*.config.json
build/workspace/*/out
build/workspace/*/err
if-no-files-found: ignore

long-tsan:
name: TSAN
runs-on: [self-hosted, 1ES.Pool=gha-virtual-ccf-sub]
container:
image: ghcr.io/microsoft/ccf/ci/default:build-25-07-2024

steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0

- name: "Build"
run: |
git config --global --add safe.directory /__w/CCF/CCF
mkdir build
cd build
cmake -GNinja -DCOMPILE_TARGET=virtual -DCMAKE_BUILD_TYPE=Debug -DLONG_TESTS=ON -DLVI_MITIGATIONS=OFF -DTSAN=ON -DWORKER_THREADS=2 ..
ninja

- name: "Test"
run: |
set +x
cd build
./tests.sh --output-on-failure --timeout 1600 -LE "benchmark"

- name: "Upload logs"
if: success() || failure()
uses: actions/upload-artifact@v4
with:
name: logs-tsan
path: |
build/workspace/*/*.config.json
build/workspace/*/out
build/workspace/*/err
if-no-files-found: ignore
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["sig"])
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
109 changes: 93 additions & 16 deletions src/crypto/openssl/cose_sign.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,11 @@
#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.

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 +24,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 +34,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 +64,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 +79,83 @@ 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::SECP256K1:
maxtropets marked this conversation as resolved.
Show resolved Hide resolved
case ccf::crypto::CurveID::SECP256R1:
std::cout << "Return ES256" << std::endl;
maxtropets marked this conversation as resolved.
Show resolved Hide resolved
return T_COSE_ALGORITHM_ES256;
case ccf::crypto::CurveID::SECP384R1:
std::cout << "Return ES384" << std::endl;
maxtropets marked this conversation as resolved.
Show resolved Hide resolved
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);
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, std::string_view value)
maxtropets marked this conversation as resolved.
Show resolved Hide resolved
{
const size_t args_size = sizeof(key) + value.size();
return COSEParametersFactory(
[=](QCBOREncodeContext* ctx) {
QCBOREncode_AddSZStringToMapN(ctx, key, value.data());
},
args_size);
}

COSEParametersFactory cose_params_string_int(
std::string_view key, int64_t value)
maxtropets marked this conversation as resolved.
Show resolved Hide resolved
{
const size_t args_size = key.size() + sizeof(value);
return COSEParametersFactory(
[=](QCBOREncodeContext* ctx) {
QCBOREncode_AddSZString(ctx, key.data());
QCBOREncode_AddInt64(ctx, value);
},
args_size);
}

COSEParametersFactory cose_params_string_string(
std::string_view key, std::string_view value)
maxtropets marked this conversation as resolved.
Show resolved Hide resolved
{
const size_t args_size = key.size() + value.size();
return COSEParametersFactory(
[=](QCBOREncodeContext* ctx) {
QCBOREncode_AddSZString(ctx, key.data());
QCBOREncode_AddSZString(ctx, value.data());
},
args_size);
}

COSEParametersFactory cose_params_int_bytes(
int64_t key, std::span<const uint8_t> value)
maxtropets marked this conversation as resolved.
Show resolved Hide resolved
{
const size_t args_size = sizeof(key) + value.size();
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 +165,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
Loading
Loading