Add issuer and subject to COSE signatures (#6637)

This commit is contained in:
Amaury Chamayou 2024-11-14 11:11:08 +00:00 коммит произвёл GitHub
Родитель 999fa17089
Коммит 31ceb7b93c
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: B5690EEEBB952194
25 изменённых файлов: 211 добавлений и 38 удалений

Просмотреть файл

@ -12,7 +12,8 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
### Added
- Added a `ccf::any_cert_auth_policy` (C++), or `any_cert` (JS/TS), implementing TLS client certificate authentication, but without checking for the presence of the certificate in the governance user or member tables. This enables applications wanting to do so to perform user management in application space, using application tables (#6608).
- Added OpenAPI support for `std::unordered_set`.
- Added OpenAPI support for `std::unordered_set` (#6634).
- Added ["cose_signatures"](https://microsoft.github.io/CCF/main/operations/configuration.html#command-start-cose-signatures) entry in the configuration, which allows setting "issuer" and "subject" at network start or recovery time (#6637).
## [6.0.0-dev5]

Просмотреть файл

@ -20,6 +20,8 @@ protected-headers = {
}
cwt-map = {
&(iss: 1) => tstr, ; "issuer", string
&(sub: 2) => tstr, ; "subject", string
&(iat: 6) => int ; "issued at", number of seconds since the epoch
}

Просмотреть файл

@ -26,6 +26,8 @@ protected-headers = {
}
cwt-map = {
&(iss: 1) => tstr, ; "issuer", string
&(sub: 2) => tstr, ; "subject", string
&(iat: 6) => int ; "issued at", number of seconds since the epoch
}

Просмотреть файл

@ -296,6 +296,19 @@
"default": "CN=CCF Service",
"description": "Subject name to include in service certificate. Can only be set once on service start."
},
"cose_signatures": {
"type": "object",
"properties": {
"issuer": {
"type": "string",
"description": "Issuer, set in CWT_Claims of COSE ledger signatures. Can only be set once on service start."
},
"subject": {
"type": "string",
"description": "Subject, set in CWT_Claims of COSE ledger signatures. Can only be set once on service start."
}
}
},
"members": {
"type": "array",
"items": {

Просмотреть файл

@ -8,8 +8,8 @@
struct COSESignaturesConfig
{
std::string issuer;
std::string subject;
std::string issuer = "";
std::string subject = "";
bool operator==(const COSESignaturesConfig& other) const = default;
};

Просмотреть файл

@ -4,6 +4,7 @@
#include "ccf/crypto/curve.h"
#include "ccf/ds/unit_strings.h"
#include "ccf/node/cose_signatures_config.h"
#include "ccf/pal/attestation_sev_snp_endorsements.h"
#include "ccf/service/consensus_config.h"
#include "ccf/service/node_info_network.h"
@ -86,6 +87,7 @@ struct StartupConfig : CCFConfig
// Only if starting or recovering
size_t initial_service_certificate_validity_days = 1;
std::string service_subject_name = "CN=CCF Service";
COSESignaturesConfig cose_signatures;
nlohmann::json service_data = nullptr;

Просмотреть файл

@ -243,6 +243,7 @@ def verify_receipt(
raise ValueError("Signature verification failed")
if claim_digest != leaf[2]:
raise ValueError(f"Claim digest mismatch: {leaf[2]!r} != {claim_digest!r}")
return receipt.phdr
_SIGN_DESCRIPTION = """Create and sign a COSE Sign1 message for CCF governance

Просмотреть файл

@ -51,7 +51,11 @@
"maximum_node_certificate_validity_days": 365
},
"initial_service_certificate_validity_days": 1,
"service_subject_name": "CN=A Sample CCF Service"
"service_subject_name": "CN=A Sample CCF Service",
"cose_signatures": {
"issuer": "service.example.com",
"subject": "ledger.signature"
}
}
},
"ledger": {

Просмотреть файл

@ -117,6 +117,7 @@ DECLARE_JSON_REQUIRED_FIELDS(
snapshot_tx_interval,
initial_service_certificate_validity_days,
service_subject_name,
cose_signatures,
service_data,
node_data,
start,

Просмотреть файл

@ -30,6 +30,19 @@ namespace ccf::crypto
* omitted.
*/
static constexpr int64_t COSE_PHEADER_KEY_IAT = 6;
// Standardised: issuer CWT claim.
// https://www.iana.org/assignments/cose/cose.xhtml#header-parameters
/* The "iss" (issuer) claim identifies the principal that issued the CWT.
* The "iss" value is a case-sensitive string containing a StringOrURI value.
*/
static constexpr int64_t COSE_PHEADER_KEY_ISS = 1;
// Standardised: subject CWT claim.
// https://www.iana.org/assignments/cose/cose.xhtml#header-parameters
/* The "sub" (subject) claim identifies the principal that is the subject of
* the CWT. The claims in a CWT are normally statements about the subject.
* The "sub" value is a case-sensitive string containing a StringOrURI value.
*/
static constexpr int64_t COSE_PHEADER_KEY_SUB = 2;
// CCF headers nested map key.
static const std::string COSE_PHEADER_KEY_CCF = "ccf.v1";
// CCF-specific: last signed TxID.

Просмотреть файл

@ -144,6 +144,7 @@ namespace host
ccf::ServiceConfiguration service_configuration;
size_t initial_service_certificate_validity_days = 1;
std::string service_subject_name = "CN=CCF Service";
COSESignaturesConfig cose_signatures;
bool operator==(const Start&) const = default;
};
@ -209,7 +210,8 @@ namespace host
CCHostConfig::Command::Start,
service_configuration,
initial_service_certificate_validity_days,
service_subject_name);
service_subject_name,
cose_signatures);
DECLARE_JSON_TYPE_WITH_OPTIONAL_FIELDS(CCHostConfig::Command::Join);
DECLARE_JSON_REQUIRED_FIELDS(CCHostConfig::Command::Join, target_rpc_address);

Просмотреть файл

@ -640,6 +640,7 @@ int main(int argc, char** argv)
config.command.start.initial_service_certificate_validity_days;
startup_config.service_subject_name =
config.command.start.service_subject_name;
startup_config.cose_signatures = config.command.start.cose_signatures;
LOG_INFO_FMT(
"Creating new node: new network (with {} initial member(s) and {} "
"member(s) required for recovery)",

Просмотреть файл

@ -10,6 +10,7 @@
#include "ccf/kv/get_name.h"
#include "ccf/kv/hooks.h"
#include "ccf/kv/version.h"
#include "ccf/node/cose_signatures_config.h"
#include "ccf/tx_id.h"
#include "crypto/openssl/key_pair.h"
#include "enclave/consensus_type.h"
@ -424,8 +425,9 @@ namespace ccf::kv
virtual std::vector<uint8_t> serialise_tree(size_t to) = 0;
virtual void set_endorsed_certificate(const ccf::crypto::Pem& cert) = 0;
virtual void start_signature_emit_timer() = 0;
virtual void set_service_kp(
std::shared_ptr<ccf::crypto::KeyPair_OpenSSL>) = 0;
virtual void set_service_signing_identity(
std::shared_ptr<ccf::crypto::KeyPair_OpenSSL> keypair,
const COSESignaturesConfig& cose_signatures) = 0;
};
class Consensus : public ConfigurableConsensus

Просмотреть файл

@ -196,8 +196,9 @@ namespace ccf
void start_signature_emit_timer() override {}
void set_service_kp(
std::shared_ptr<ccf::crypto::KeyPair_OpenSSL> service_kp_) override
void set_service_signing_identity(
std::shared_ptr<ccf::crypto::KeyPair_OpenSSL> service_kp_,
const COSESignaturesConfig& cose_signatures) override
{
std::ignore = std::move(service_kp_);
}
@ -322,6 +323,7 @@ namespace ccf
ccf::crypto::KeyPair& node_kp;
ccf::crypto::KeyPair_OpenSSL& service_kp;
ccf::crypto::Pem& endorsed_cert;
const COSESignaturesConfig& cose_signatures_config;
public:
MerkleTreeHistoryPendingTx(
@ -331,14 +333,16 @@ namespace ccf
const NodeId& id_,
ccf::crypto::KeyPair& node_kp_,
ccf::crypto::KeyPair_OpenSSL& service_kp_,
ccf::crypto::Pem& endorsed_cert_) :
ccf::crypto::Pem& endorsed_cert_,
const COSESignaturesConfig& cose_signatures_config_) :
txid(txid_),
store(store_),
history(history_),
id(id_),
node_kp(node_kp_),
service_kp(service_kp_),
endorsed_cert(endorsed_cert_)
endorsed_cert(endorsed_cert_),
cose_signatures_config(cose_signatures_config_)
{}
ccf::kv::PendingTxInfo call() override
@ -392,8 +396,16 @@ namespace ccf
std::make_shared<ccf::crypto::COSEParametersMap>(
std::make_shared<ccf::crypto::COSEMapIntKey>(
ccf::crypto::COSE_PHEADER_KEY_CWT),
ccf::crypto::COSEHeadersArray{ccf::crypto::cose_params_int_int(
ccf::crypto::COSE_PHEADER_KEY_IAT, time_since_epoch)}));
ccf::crypto::COSEHeadersArray{
ccf::crypto::cose_params_int_int(
ccf::crypto::COSE_PHEADER_KEY_IAT, time_since_epoch),
ccf::crypto::cose_params_int_string(
ccf::crypto::COSE_PHEADER_KEY_ISS,
cose_signatures_config.issuer),
ccf::crypto::cose_params_int_string(
ccf::crypto::COSE_PHEADER_KEY_SUB,
cose_signatures_config.subject),
}));
const auto pheaders = {
// Key digest
@ -568,6 +580,7 @@ namespace ccf
ccf::kv::Term term_of_next_version;
std::optional<ccf::crypto::Pem> endorsed_cert = std::nullopt;
COSESignaturesConfig cose_signatures_config;
public:
HashedTxHistory(
@ -589,10 +602,16 @@ namespace ccf
}
}
void set_service_kp(
std::shared_ptr<ccf::crypto::KeyPair_OpenSSL> service_kp_) override
void set_service_signing_identity(
std::shared_ptr<ccf::crypto::KeyPair_OpenSSL> service_kp_,
const COSESignaturesConfig& cose_signatures_config_) override
{
service_kp = std::move(service_kp_);
cose_signatures_config = cose_signatures_config_;
LOG_INFO_FMT(
"Setting service signing identity to iss: {} sub: {}",
cose_signatures_config.issuer,
cose_signatures_config.subject);
}
void start_signature_emit_timer() override
@ -860,7 +879,14 @@ namespace ccf
store.commit(
txid,
std::make_unique<MerkleTreeHistoryPendingTx<T>>(
txid, store, *this, id, node_kp, *service_kp, endorsed_cert.value()),
txid,
store,
*this,
id,
node_kp,
*service_kp,
endorsed_cert.value(),
cose_signatures_config),
true);
}

Просмотреть файл

@ -3,6 +3,7 @@
#pragma once
#include "ccf/crypto/curve.h"
#include "ccf/node/cose_signatures_config.h"
#include "crypto/certs.h"
#include "crypto/openssl/key_pair.h"
@ -24,6 +25,7 @@ namespace ccf
ccf::crypto::Pem cert;
std::optional<IdentityType> type = IdentityType::REPLICATED;
std::string subject_name = "CN=CCF Service";
COSESignaturesConfig cose_signatures_config;
std::shared_ptr<ccf::crypto::KeyPair_OpenSSL> kp{};
std::shared_ptr<ccf::crypto::KeyPair_OpenSSL> get_key_pair()
@ -39,12 +41,16 @@ namespace ccf
bool operator==(const NetworkIdentity& other) const
{
return cert == other.cert && priv_key == other.priv_key &&
type == other.type && subject_name == other.subject_name;
type == other.type && subject_name == other.subject_name &&
cose_signatures_config == other.cose_signatures_config;
}
NetworkIdentity(const std::string& subject_name_) :
NetworkIdentity(
const std::string& subject_name_,
const COSESignaturesConfig& cose_signatures_config_) :
type(IdentityType::REPLICATED),
subject_name(subject_name_)
subject_name(subject_name_),
cose_signatures_config(cose_signatures_config_)
{}
NetworkIdentity() = default;
@ -68,8 +74,9 @@ namespace ccf
const std::string& subject_name_,
ccf::crypto::CurveID curve_id,
const std::string& valid_from,
size_t validity_period_days) :
NetworkIdentity(subject_name_)
size_t validity_period_days,
const COSESignaturesConfig& cose_signatures_config_) :
NetworkIdentity(subject_name_, cose_signatures_config_)
{
auto identity_key_pair =
std::make_shared<ccf::crypto::KeyPair_OpenSSL>(curve_id);
@ -84,7 +91,7 @@ namespace ccf
}
ReplicatedNetworkIdentity(const NetworkIdentity& other) :
NetworkIdentity(other.subject_name)
NetworkIdentity(other.subject_name, other.cose_signatures_config)
{
if (type != other.type)
{

Просмотреть файл

@ -8,6 +8,7 @@
#include "ccf/crypto/verifier.h"
#include "ccf/ds/logger.h"
#include "ccf/js/core/context.h"
#include "ccf/node/cose_signatures_config.h"
#include "ccf/pal/attestation.h"
#include "ccf/pal/locking.h"
#include "ccf/pal/platform.h"
@ -501,11 +502,13 @@ namespace ccf
config.service_subject_name,
curve_id,
config.startup_host_time,
config.initial_service_certificate_validity_days);
config.initial_service_certificate_validity_days,
config.cose_signatures);
network.ledger_secrets->init();
history->set_service_kp(network.identity->get_key_pair());
history->set_service_signing_identity(
network.identity->get_key_pair(), config.cose_signatures);
setup_consensus(
ServiceStatus::OPENING,
@ -540,9 +543,11 @@ namespace ccf
ccf::crypto::get_subject_name(previous_service_identity_cert),
curve_id,
config.startup_host_time,
config.initial_service_certificate_validity_days);
config.initial_service_certificate_validity_days,
config.cose_signatures);
history->set_service_kp(network.identity->get_key_pair());
history->set_service_signing_identity(
network.identity->get_key_pair(), config.cose_signatures);
LOG_INFO_FMT("Created recovery node {}", self);
return {self_signed_node_cert, network.identity->cert};
@ -654,12 +659,20 @@ namespace ccf
// Set network secrets, node id and become part of network.
if (resp.node_status == NodeStatus::TRUSTED)
{
if (!resp.network_info.has_value())
{
throw std::logic_error("Expected network info in join response");
}
network.identity = std::make_unique<ReplicatedNetworkIdentity>(
resp.network_info->identity);
network.ledger_secrets->init_from_map(
std::move(resp.network_info->ledger_secrets));
history->set_service_kp(network.identity->get_key_pair());
history->set_service_signing_identity(
network.identity->get_key_pair(),
resp.network_info->cose_signatures_config.value_or(
COSESignaturesConfig{}));
ccf::crypto::Pem n2n_channels_cert;
if (!resp.network_info->endorsed_certificate.has_value())

Просмотреть файл

@ -3,6 +3,7 @@
#pragma once
#include "ccf/ds/json_schema.h"
#include "ccf/node/cose_signatures_config.h"
#include "ccf/node_startup_state.h"
#include "ccf/pal/mem.h"
#include "ccf/service/node_info_network.h"
@ -14,7 +15,6 @@
#include "enclave/interface.h"
#include "node/identity.h"
#include "node/ledger_secrets.h"
#include "node/rpc/cose_signatures_config.h"
#include "node/uvm_endorsements.h"
#include <nlohmann/json.hpp>

Просмотреть файл

@ -123,7 +123,8 @@ std::unique_ptr<ccf::NetworkIdentity> make_test_network_ident()
"CN=CCF test network",
ccf::crypto::service_identity_curve_choice,
valid_from,
2);
2,
COSESignaturesConfig{});
}
void init_network(NetworkState& network)

Просмотреть файл

@ -62,7 +62,7 @@ TestState create_and_init_state(bool initialise_ledger_rekey = true)
auto h =
std::make_shared<ccf::MerkleTxHistory>(*ts.kv_store, node_id, *ts.node_kp);
h->set_endorsed_certificate({});
h->set_service_kp(ts.service_kp);
h->set_service_signing_identity(ts.service_kp, COSESignaturesConfig{});
ts.kv_store->set_history(h);
ts.kv_store->initialise_term(2);

Просмотреть файл

@ -87,7 +87,8 @@ TEST_CASE("Check signature verification")
std::make_shared<ccf::MerkleTxHistory>(
primary_store, ccf::kv::test::PrimaryNodeId, *node_kp);
primary_history->set_endorsed_certificate(self_signed);
primary_history->set_service_kp(service_kp);
primary_history->set_service_signing_identity(
service_kp, COSESignaturesConfig{});
primary_store.set_history(primary_history);
primary_store.initialise_term(store_term);
@ -97,7 +98,8 @@ TEST_CASE("Check signature verification")
std::make_shared<ccf::MerkleTxHistory>(
backup_store, ccf::kv::test::FirstBackupNodeId, *node_kp);
backup_history->set_endorsed_certificate(self_signed);
backup_history->set_service_kp(service_kp);
backup_history->set_service_signing_identity(
service_kp, COSESignaturesConfig{});
backup_store.set_history(backup_history);
backup_store.initialise_term(store_term);
@ -163,7 +165,8 @@ TEST_CASE("Check signing works across rollback")
std::make_shared<ccf::MerkleTxHistory>(
primary_store, ccf::kv::test::PrimaryNodeId, *node_kp);
primary_history->set_endorsed_certificate(self_signed);
primary_history->set_service_kp(service_kp);
primary_history->set_service_signing_identity(
service_kp, COSESignaturesConfig{});
primary_store.set_history(primary_history);
primary_store.initialise_term(store_term);
@ -172,7 +175,8 @@ TEST_CASE("Check signing works across rollback")
std::make_shared<ccf::MerkleTxHistory>(
backup_store, ccf::kv::test::FirstBackupNodeId, *node_kp);
backup_history->set_endorsed_certificate(self_signed);
backup_history->set_service_kp(service_kp);
backup_history->set_service_signing_identity(
service_kp, COSESignaturesConfig{});
backup_store.set_history(backup_history);
backup_store.set_encryptor(encryptor);
backup_store.initialise_term(store_term);

Просмотреть файл

@ -34,7 +34,8 @@ TEST_CASE("Snapshot with merkle tree" * doctest::test_suite("snapshot"))
auto source_history = std::make_shared<ccf::MerkleTxHistory>(
source_store, source_node_id, *source_node_kp);
source_history->set_endorsed_certificate({});
source_history->set_service_kp(service_kp);
source_history->set_service_signing_identity(
service_kp, COSESignaturesConfig{});
source_store.set_history(source_history);
source_store.initialise_term(2);
@ -102,7 +103,8 @@ TEST_CASE("Snapshot with merkle tree" * doctest::test_suite("snapshot"))
auto target_history = std::make_shared<ccf::MerkleTxHistory>(
target_store, ccf::kv::test::PrimaryNodeId, *target_node_kp);
target_history->set_endorsed_certificate({});
target_history->set_service_kp(service_kp);
target_history->set_service_signing_identity(
service_kp, COSESignaturesConfig{});
target_store.set_history(target_history);
}

Просмотреть файл

@ -39,7 +39,12 @@
"maximum_service_certificate_validity_days": {{ maximum_service_certificate_validity_days }}
},
"initial_service_certificate_validity_days": {{ initial_service_cert_validity_days }},
"service_subject_name": {{ service_subject_name|tojson }}
"service_subject_name": {{ service_subject_name|tojson }},
"cose_signatures":
{
"issuer": {{ cose_signatures_issuer|tojson }},
"subject": {{ cose_signatures_subject|tojson }}
}
},
"join":
{

Просмотреть файл

@ -1053,7 +1053,11 @@ def test_cose_receipt_schema(network, args):
if r.status_code == http.HTTPStatus.OK:
cbor_proof = r.body.data()
ccf.cose.verify_receipt(cbor_proof, service_key, b"\0" * 32)
receipt_phdr = ccf.cose.verify_receipt(
cbor_proof, service_key, b"\0" * 32
)
assert receipt_phdr[15][1] == "service.example.com"
assert receipt_phdr[15][2] == "ledger.signature"
cbor_proof_filename = os.path.join(
network.common_dir, f"receipt_{txid}.cose"
)

Просмотреть файл

@ -8,6 +8,8 @@ import infra.logging_app as app
import infra.e2e_args
import infra.network
import ccf.ledger
from ccf.tx_id import TxID
import base64
import suite.test_requirements as reqs
import infra.crypto
import ipaddress
@ -19,9 +21,11 @@ import json
import subprocess
import time
import http
import copy
import infra.snp as snp
from cryptography import x509
from cryptography.hazmat.backends import default_backend
from pycose.messages import Sign1Message
from loguru import logger as LOG
@ -589,6 +593,64 @@ def run_service_subject_name_check(args):
assert cert.subject.rfc4514_string() == "CN=This test service", cert
def run_cose_signatures_config_check(args):
nargs = copy.deepcopy(args)
nargs.nodes = infra.e2e_args.max_nodes(nargs, f=0)
with infra.network.network(
nargs.nodes,
nargs.binary_dir,
nargs.debug_nodes,
nargs.perf_nodes,
pdb=nargs.pdb,
) as network:
network.start_and_open(
nargs,
cose_signatures_issuer="test.issuer.example.com",
cose_signatures_subject="test.subject",
)
for node in network.get_joined_nodes():
with node.client("user0") as client:
r = client.get("/commit")
assert r.status_code == http.HTTPStatus.OK
txid = TxID.from_str(r.body.json()["transaction_id"])
max_retries = 10
for _ in range(max_retries):
response = client.get(
"/log/public/cose_signature",
headers={
infra.clients.CCF_TX_ID_HEADER: f"{txid.view}.{txid.seqno}"
},
)
if response.status_code == http.HTTPStatus.OK:
signature = response.body.json()["cose_signature"]
signature = base64.b64decode(signature)
signature_filename = os.path.join(
network.common_dir, f"cose_signature_{txid}.cose"
)
with open(signature_filename, "wb") as f:
f.write(signature)
sig = Sign1Message.decode(signature)
assert sig.phdr[15][1] == "test.issuer.example.com"
assert sig.phdr[15][2] == "test.subject"
LOG.debug(
"Checked COSE signature schema for issuer and subject"
)
break
elif response.status_code == http.HTTPStatus.ACCEPTED:
LOG.debug(f"Transaction {txid} accepted, retrying")
time.sleep(0.1)
else:
LOG.error(f"Failed to get COSE signature for txid {txid}")
break
else:
assert (
False
), f"Failed to get receipt for txid {txid} after {max_retries} retries"
def run(args):
run_max_uncommitted_tx_count(args)
run_file_operations(args)
@ -599,3 +661,4 @@ def run(args):
run_preopen_readiness_check(args)
run_sighup_check(args)
run_service_subject_name_check(args)
run_cose_signatures_config_check(args)

Просмотреть файл

@ -341,6 +341,8 @@ class CCFRemote(object):
snp_uvm_endorsements_file=None,
service_subject_name="CN=CCF Test Service",
historical_cache_soft_limit=None,
cose_signatures_issuer="service.example.com",
cose_signatures_subject="ledger.signature",
**kwargs,
):
"""
@ -536,6 +538,8 @@ class CCFRemote(object):
snp_uvm_endorsements_file=snp_uvm_endorsements_file,
service_subject_name=service_subject_name,
historical_cache_soft_limit=historical_cache_soft_limit,
cose_signatures_issuer=cose_signatures_issuer,
cose_signatures_subject=cose_signatures_subject,
**kwargs,
)