From 66a9946580728452ef636752c751ece600201cda Mon Sep 17 00:00:00 2001 From: Amaury Chamayou Date: Thu, 20 Oct 2022 09:53:59 +0000 Subject: [PATCH 01/23] Reads of public tables do not require authentication --- src/node/rpc/member_frontend.h | 51 +++------------------------------- 1 file changed, 4 insertions(+), 47 deletions(-) diff --git a/src/node/rpc/member_frontend.h b/src/node/rpc/member_frontend.h index 7355f1a5af30..38005517b159 100644 --- a/src/node/rpc/member_frontend.h +++ b/src/node/rpc/member_frontend.h @@ -948,29 +948,13 @@ namespace ccf "/proposals", HTTP_GET, json_read_only_adapter(get_open_proposals_js), - member_cert_or_sig) + ccf::no_auth_required) .set_auto_schema() .install(); auto get_proposal_js = [this]( endpoints::ReadOnlyEndpointContext& ctx, nlohmann::json&&) { - const auto member_id = get_caller_member_id(ctx); - if (!member_id.has_value()) - { - return make_error( - HTTP_STATUS_FORBIDDEN, - ccf::errors::AuthorizationFailed, - "Member is unknown."); - } - if (!check_member_active(ctx.tx, member_id.value())) - { - return make_error( - HTTP_STATUS_FORBIDDEN, - ccf::errors::AuthorizationFailed, - "Member is not active."); - } - // Take expand=ballots, return eg. "ballots": 3 if not set // or "ballots": list of ballots in full if passed @@ -1014,7 +998,7 @@ namespace ccf "/proposals/{proposal_id}", HTTP_GET, json_read_only_adapter(get_proposal_js), - member_cert_or_sig) + ccf::no_auth_required) .set_auto_schema() .install(); @@ -1097,17 +1081,6 @@ namespace ccf auto get_proposal_actions_js = [this](ccf::endpoints::ReadOnlyEndpointContext& ctx) { - const auto& caller_identity = - ctx.get_caller(); - if (!check_member_active(ctx.tx, caller_identity.member_id)) - { - ctx.rpc_ctx->set_error( - HTTP_STATUS_FORBIDDEN, - ccf::errors::AuthorizationFailed, - "Member is not active."); - return; - } - ProposalId proposal_id; std::string error; if (!get_proposal_id_from_path( @@ -1143,7 +1116,7 @@ namespace ccf "/proposals/{proposal_id}/actions", HTTP_GET, get_proposal_actions_js, - member_cert_or_sig) + ccf::no_auth_required) .set_auto_schema() .install(); @@ -1278,22 +1251,6 @@ namespace ccf auto get_vote_js = [this](endpoints::ReadOnlyEndpointContext& ctx, nlohmann::json&&) { - const auto member_id = get_caller_member_id(ctx); - if (!member_id.has_value()) - { - return make_error( - HTTP_STATUS_FORBIDDEN, - ccf::errors::AuthorizationFailed, - "Member is unknown."); - } - if (!check_member_active(ctx.tx, member_id.value())) - { - return make_error( - HTTP_STATUS_FORBIDDEN, - ccf::errors::AuthorizationFailed, - "Member is not active."); - } - std::string error; ProposalId proposal_id; if (!get_proposal_id_from_path( @@ -1340,7 +1297,7 @@ namespace ccf "/proposals/{proposal_id}/ballots/{member_id}", HTTP_GET, json_read_only_adapter(get_vote_js), - member_cert_or_sig) + ccf::no_auth_required) .set_auto_schema() .install(); From 9d5d4c69c3e0fe7f76fe6e913c83c6c7c3da4a62 Mon Sep 17 00:00:00 2001 From: Amaury Chamayou Date: Fri, 21 Oct 2022 09:01:09 +0000 Subject: [PATCH 02/23] wip --- src/node/rpc/member_frontend.h | 113 ++++++++++++++++++++++++--------- 1 file changed, 84 insertions(+), 29 deletions(-) diff --git a/src/node/rpc/member_frontend.h b/src/node/rpc/member_frontend.h index 38005517b159..42b13dfef6b1 100644 --- a/src/node/rpc/member_frontend.h +++ b/src/node/rpc/member_frontend.h @@ -397,7 +397,11 @@ namespace ccf static std::optional get_caller_member_id( endpoints::CommandEndpointContext& ctx) { - if ( + if (const auto* cose_ident = + ctx.try_get_caller()) + { + return cose_ident->member_id; + } else if ( const auto* sig_ident = ctx.try_get_caller()) { @@ -418,13 +422,19 @@ namespace ccf { CommonEndpointRegistry::init_handlers(); - const AuthnPolicies member_sig_only = {member_signature_auth_policy}; + const AuthnPolicies member_sig_only = {member_signature_auth_policy, + member_cose_sign1_auth_policy}; const AuthnPolicies member_cert_or_sig = { - member_cert_auth_policy, member_signature_auth_policy}; + member_cert_auth_policy, + member_signature_auth_policy}; //! A member acknowledges state - auto ack = [this](auto& ctx, nlohmann::json&& params) { + auto ack = [this](ccf::endpoints::EndpointContext& ctx) { + // TODO + auto params = nlohmann::json::parse(ctx.rpc_ctx->get_request_body()); + // Branch on content type + // Expand MemberAck to contain a COSE_Sign1 const auto& caller_identity = ctx.template get_caller(); const auto& signed_request = caller_identity.signed_request; @@ -433,21 +443,23 @@ namespace ccf const auto ma = mas->get(caller_identity.member_id); if (!ma) { - return make_error( + ctx.rpc_ctx->set_error( HTTP_STATUS_FORBIDDEN, ccf::errors::AuthorizationFailed, fmt::format( "No ACK record exists for caller {}.", caller_identity.member_id)); + return; } const auto digest = params.get(); if (ma->state_digest != digest.state_digest) { - return make_error( + ctx.rpc_ctx->set_error( HTTP_STATUS_BAD_REQUEST, ccf::errors::StateDigestMismatch, "Submitted state digest is not valid."); + return; } auto sig = ctx.tx.rw(this->network.signatures); @@ -470,19 +482,21 @@ namespace ccf } catch (const std::logic_error& e) { - return make_error( + ctx.rpc_ctx->set_error( HTTP_STATUS_FORBIDDEN, ccf::errors::AuthorizationFailed, fmt::format("Error activating new member: {}", e.what())); + return; } auto service_status = g.get_service_status(); if (!service_status.has_value()) { - return make_error( + ctx.rpc_ctx->set_error( HTTP_STATUS_INTERNAL_SERVER_ERROR, ccf::errors::InternalError, "No service currently available."); + return; } auto members = ctx.tx.rw(this->network.member_info); @@ -499,27 +513,32 @@ namespace ccf } catch (const std::logic_error& e) { - return make_error( + ctx.rpc_ctx->set_error( HTTP_STATUS_INTERNAL_SERVER_ERROR, ccf::errors::InternalError, fmt::format("Error issuing new recovery shares: {}", e.what())); + return; } } - return make_success(); + ctx.rpc_ctx->set_response_status(HTTP_STATUS_OK); + return; }; - make_endpoint("/ack", HTTP_POST, json_adapter(ack), member_sig_only) + make_endpoint("/ack", HTTP_POST, ack, member_sig_only) .set_auto_schema() .install(); //! A member asks for a fresher state digest - auto update_state_digest = [this](auto& ctx, nlohmann::json&&) { + auto update_state_digest = [this](ccf::endpoints::EndpointContext& ctx) { + // TODO + // Branch on content type const auto member_id = get_caller_member_id(ctx); if (!member_id.has_value()) { - return make_error( + ctx.rpc_ctx->set_error( HTTP_STATUS_FORBIDDEN, ccf::errors::AuthorizationFailed, "Caller is a not a valid member id"); + return; } auto mas = ctx.tx.rw(this->network.member_acks); @@ -527,11 +546,12 @@ namespace ccf auto ma = mas->get(member_id.value()); if (!ma) { - return make_error( + ctx.rpc_ctx->set_error( HTTP_STATUS_FORBIDDEN, ccf::errors::AuthorizationFailed, fmt::format( "No ACK record exists for caller {}.", member_id.value())); + return; } auto s = sig->get(); @@ -543,31 +563,39 @@ namespace ccf nlohmann::json j; j["state_digest"] = ma->state_digest; - return make_success(j); + ctx.rpc_ctx->set_response_header( + http::headers::CONTENT_TYPE, http::headervalues::contenttype::JSON); + ctx.rpc_ctx->set_response_body(j.dump()); + ctx.rpc_ctx->set_response_status(HTTP_STATUS_OK); + return; }; make_endpoint( "/ack/update_state_digest", HTTP_POST, - json_adapter(update_state_digest), + update_state_digest, member_cert_or_sig) .set_auto_schema() .install(); - auto get_encrypted_recovery_share = [this](auto& ctx, nlohmann::json&&) { + auto get_encrypted_recovery_share = [this](ccf::endpoints::EndpointContext& ctx) { + // TODO + // Branch on content type const auto member_id = get_caller_member_id(ctx); if (!member_id.has_value()) { - return make_error( + ctx.rpc_ctx->set_error( HTTP_STATUS_FORBIDDEN, ccf::errors::AuthorizationFailed, "Member is unknown."); + return; } if (!check_member_active(ctx.tx, member_id.value())) { - return make_error( + ctx.rpc_ctx->set_error( HTTP_STATUS_FORBIDDEN, ccf::errors::AuthorizationFailed, "Only active members are given recovery shares."); + return; } auto encrypted_share = @@ -575,50 +603,62 @@ namespace ccf if (!encrypted_share.has_value()) { - return make_error( + ctx.rpc_ctx->set_error( HTTP_STATUS_NOT_FOUND, ccf::errors::ResourceNotFound, fmt::format( "Recovery share not found for member {}.", member_id->value())); + return; } - return make_success( - GetRecoveryShare::Out{crypto::b64_from_raw(encrypted_share.value())}); + auto rec_share = + GetRecoveryShare::Out{crypto::b64_from_raw(encrypted_share.value())}; + ctx.rpc_ctx->set_response_header( + http::headers::CONTENT_TYPE, http::headervalues::contenttype::JSON); + ctx.rpc_ctx->set_response_body(nlohmann::json(rec_share).dump()); + ctx.rpc_ctx->set_response_status(HTTP_STATUS_OK); + return; }; make_endpoint( "/recovery_share", HTTP_GET, - json_adapter(get_encrypted_recovery_share), + get_encrypted_recovery_share, member_cert_or_sig) .set_auto_schema() .install(); - auto submit_recovery_share = [this](auto& ctx, nlohmann::json&& params) { + auto submit_recovery_share = [this](ccf::endpoints::EndpointContext& ctx) { + // TODO + // Branch on content type + // Only active members can submit their shares for recovery const auto member_id = get_caller_member_id(ctx); if (!member_id.has_value()) { - return make_error( + ctx.rpc_ctx->set_error( HTTP_STATUS_FORBIDDEN, ccf::errors::AuthorizationFailed, "Member is unknown."); + return; } if (!check_member_active(ctx.tx, member_id.value())) { - return make_error( + ctx.rpc_ctx->set_error( HTTP_STATUS_FORBIDDEN, errors::AuthorizationFailed, "Member is not active."); + return; } GenesisGenerator g(this->network, ctx.tx); if ( g.get_service_status() != ServiceStatus::WAITING_FOR_RECOVERY_SHARES) { - return make_error( + ctx.rpc_ctx->set_error( HTTP_STATUS_FORBIDDEN, errors::ServiceNotWaitingForRecoveryShares, "Service is not waiting for recovery shares."); + return; } auto node_operation = context.get_subsystem(); @@ -630,10 +670,11 @@ namespace ccf if (node_operation->is_reading_private_ledger()) { - return make_error( + ctx.rpc_ctx->set_error( HTTP_STATUS_FORBIDDEN, errors::NodeAlreadyRecovering, "Node is already recovering private ledger."); + return; } const auto in = params.get(); @@ -651,10 +692,11 @@ namespace ccf constexpr auto error_msg = "Error submitting recovery shares."; LOG_FAIL_FMT(error_msg); LOG_DEBUG_FMT("Error: {}", e.what()); - return make_error( + ctx.rpc_ctx->set_error( HTTP_STATUS_INTERNAL_SERVER_ERROR, errors::InternalError, error_msg); + return; } OPENSSL_cleanse(raw_recovery_share.data(), raw_recovery_share.size()); @@ -734,6 +776,10 @@ namespace ccf #pragma clang diagnostic ignored "-Wc99-extensions" auto post_proposals_js = [this](ccf::endpoints::EndpointContext& ctx) { + // TODO + // generic identity fetch + // store envelope in voting history table + const auto& caller_identity = ctx.get_caller(); if (!check_member_active(ctx.tx, caller_identity.member_id)) @@ -1004,6 +1050,12 @@ namespace ccf auto withdraw_js = [this]( endpoints::EndpointContext& ctx, nlohmann::json&&) { + + // TODO + // generic identity fetch + // remove json wrapper + // store envelope in voting history table + const auto& caller_identity = ctx.template get_caller(); if (!check_member_active(ctx.tx, caller_identity.member_id)) @@ -1121,6 +1173,9 @@ namespace ccf .install(); auto vote_js = [this](ccf::endpoints::EndpointContext& ctx) { + // TODO + // generic identity fetch + // store envelope in voting history table const auto& caller_identity = ctx.get_caller(); if (!check_member_active(ctx.tx, caller_identity.member_id)) From b80ec6916ba495e3e6d6ee1d8033b1b207014f35 Mon Sep 17 00:00:00 2001 From: Amaury Chamayou Date: Fri, 21 Oct 2022 13:40:16 +0000 Subject: [PATCH 03/23] finish removing the wrappers --- src/node/rpc/member_frontend.h | 32 +++++++++++++++-------- tests/governance.py | 46 +++++++++++++++++----------------- 2 files changed, 45 insertions(+), 33 deletions(-) diff --git a/src/node/rpc/member_frontend.h b/src/node/rpc/member_frontend.h index 42b13dfef6b1..566e59b659ff 100644 --- a/src/node/rpc/member_frontend.h +++ b/src/node/rpc/member_frontend.h @@ -520,7 +520,7 @@ namespace ccf return; } } - ctx.rpc_ctx->set_response_status(HTTP_STATUS_OK); + ctx.rpc_ctx->set_response_status(HTTP_STATUS_NO_CONTENT); return; }; make_endpoint("/ack", HTTP_POST, ack, member_sig_only) @@ -631,6 +631,8 @@ namespace ccf // TODO // Branch on content type + auto params = nlohmann::json::parse(ctx.rpc_ctx->get_request_body()); + // Only active members can submit their shares for recovery const auto member_id = get_caller_member_id(ctx); if (!member_id.has_value()) @@ -677,9 +679,9 @@ namespace ccf return; } - const auto in = params.get(); - auto raw_recovery_share = crypto::raw_from_b64(in.share); - OPENSSL_cleanse(const_cast(in.share.data()), in.share.size()); + std::string share = params["share"]; + auto raw_recovery_share = crypto::raw_from_b64(share); + OPENSSL_cleanse(const_cast(share.data()), share.size()); size_t submitted_shares_count = 0; try @@ -704,10 +706,15 @@ namespace ccf { // The number of shares required to re-assemble the secret has not yet // been reached - return make_success(SubmitRecoveryShare::Out{fmt::format( + auto recovery_share = SubmitRecoveryShare::Out{fmt::format( "{}/{} recovery shares successfully submitted.", submitted_shares_count, - g.get_recovery_threshold())}); + g.get_recovery_threshold())}; + ctx.rpc_ctx->set_response_header( + http::headers::CONTENT_TYPE, http::headervalues::contenttype::JSON); + ctx.rpc_ctx->set_response_body(nlohmann::json(recovery_share).dump()); + ctx.rpc_ctx->set_response_status(HTTP_STATUS_OK); + return; } LOG_DEBUG_FMT( @@ -726,22 +733,27 @@ namespace ccf LOG_DEBUG_FMT("Error: {}", e.what()); share_manager.clear_submitted_recovery_shares(ctx.tx); ctx.rpc_ctx->set_apply_writes(true); - return make_error( + ctx.rpc_ctx->set_error( HTTP_STATUS_INTERNAL_SERVER_ERROR, errors::InternalError, error_msg); + return; } - return make_success(SubmitRecoveryShare::Out{fmt::format( + auto recovery_share = SubmitRecoveryShare::Out{fmt::format( "{}/{} recovery shares successfully submitted. End of recovery " "procedure initiated.", submitted_shares_count, - g.get_recovery_threshold())}); + g.get_recovery_threshold())}; + ctx.rpc_ctx->set_response_header( + http::headers::CONTENT_TYPE, http::headervalues::contenttype::JSON); + ctx.rpc_ctx->set_response_body(nlohmann::json(recovery_share).dump()); + ctx.rpc_ctx->set_response_status(HTTP_STATUS_OK); }; make_endpoint( "/recovery_share", HTTP_POST, - json_adapter(submit_recovery_share), + submit_recovery_share, member_cert_or_sig) .set_auto_schema() .install(); diff --git a/tests/governance.py b/tests/governance.py index 0beaf935971b..64f6f34fb3f5 100644 --- a/tests/governance.py +++ b/tests/governance.py @@ -688,14 +688,14 @@ def add(parser): infra.log_capture.COLORS = False - cr.add( - "session_auth", - gov, - package="samples/apps/logging/liblogging", - nodes=infra.e2e_args.max_nodes(cr.args, f=0), - initial_user_count=3, - authenticate_session=True, - ) + # cr.add( + # "session_auth", + # gov, + # package="samples/apps/logging/liblogging", + # nodes=infra.e2e_args.max_nodes(cr.args, f=0), + # initial_user_count=3, + # authenticate_session=True, + # ) cr.add( "session_noauth", @@ -706,20 +706,20 @@ def add(parser): authenticate_session=False, ) - cr.add( - "js", - js_gov, - package="samples/apps/logging/liblogging", - nodes=infra.e2e_args.max_nodes(cr.args, f=0), - initial_user_count=3, - authenticate_session=True, - ) - - cr.add( - "history", - governance_history.run, - package="samples/apps/logging/liblogging", - nodes=infra.e2e_args.max_nodes(cr.args, f=0), - ) + # cr.add( + # "js", + # js_gov, + # package="samples/apps/logging/liblogging", + # nodes=infra.e2e_args.max_nodes(cr.args, f=0), + # initial_user_count=3, + # authenticate_session=True, + # ) + + # cr.add( + # "history", + # governance_history.run, + # package="samples/apps/logging/liblogging", + # nodes=infra.e2e_args.max_nodes(cr.args, f=0), + # ) cr.run(2) From ce21efe63d3d527d4204b12bf80dadc23a2ead13 Mon Sep 17 00:00:00 2001 From: Amaury Chamayou Date: Fri, 21 Oct 2022 14:37:21 +0000 Subject: [PATCH 04/23] wip --- src/node/rpc/member_frontend.h | 62 +++++++++++++++++++++++++++------- 1 file changed, 49 insertions(+), 13 deletions(-) diff --git a/src/node/rpc/member_frontend.h b/src/node/rpc/member_frontend.h index 566e59b659ff..dc5bdff40ee6 100644 --- a/src/node/rpc/member_frontend.h +++ b/src/node/rpc/member_frontend.h @@ -418,6 +418,38 @@ namespace ccf return std::nullopt; } + bool authnz_active_member(ccf::endpoints::EndpointContext& ctx, std::optional& member_id) + { + if (const auto* cose_ident = + ctx.try_get_caller()) + { + member_id = cose_ident->member_id; + } else if ( + const auto* sig_ident = + ctx.try_get_caller()) + { + member_id = sig_ident->member_id; + } else { + ctx.rpc_ctx->set_error( + HTTP_STATUS_FORBIDDEN, + ccf::errors::AuthorizationFailed, + "Caller is a not a valid member id"); + + return false; + } + + if (!check_member_active(ctx.tx, member_id.value())) + { + ctx.rpc_ctx->set_error( + HTTP_STATUS_FORBIDDEN, + ccf::errors::AuthorizationFailed, + "Member is not active."); + return false; + } + + return true; + } + void init_handlers() override { CommonEndpointRegistry::init_handlers(); @@ -529,8 +561,6 @@ namespace ccf //! A member asks for a fresher state digest auto update_state_digest = [this](ccf::endpoints::EndpointContext& ctx) { - // TODO - // Branch on content type const auto member_id = get_caller_member_id(ctx); if (!member_id.has_value()) { @@ -1060,22 +1090,21 @@ namespace ccf .set_auto_schema() .install(); - auto withdraw_js = [this]( - endpoints::EndpointContext& ctx, nlohmann::json&&) { + auto withdraw_js = [this](ccf::endpoints::EndpointContext& ctx) { // TODO // generic identity fetch - // remove json wrapper // store envelope in voting history table const auto& caller_identity = ctx.template get_caller(); if (!check_member_active(ctx.tx, caller_identity.member_id)) { - return make_error( + ctx.rpc_ctx->set_error( HTTP_STATUS_FORBIDDEN, ccf::errors::AuthorizationFailed, "Member is not active."); + return; } ProposalId proposal_id; @@ -1083,8 +1112,9 @@ namespace ccf if (!get_proposal_id_from_path( ctx.rpc_ctx->get_request_path_params(), proposal_id, error)) { - return make_error( - HTTP_STATUS_BAD_REQUEST, ccf::errors::InvalidResourceName, error); + ctx.rpc_ctx->set_error( + HTTP_STATUS_BAD_REQUEST, ccf::errors::InvalidResourceName, std::move(error)); + return; } auto pi = @@ -1093,15 +1123,16 @@ namespace ccf if (!pi_) { - return make_error( + ctx.rpc_ctx->set_error( HTTP_STATUS_BAD_REQUEST, ccf::errors::ProposalNotFound, fmt::format("Proposal {} does not exist.", proposal_id)); + return; } if (caller_identity.member_id != pi_->proposer_id) { - return make_error( + ctx.rpc_ctx->set_error( HTTP_STATUS_FORBIDDEN, ccf::errors::AuthorizationFailed, fmt::format( @@ -1110,11 +1141,12 @@ namespace ccf proposal_id, pi_->proposer_id, caller_identity.member_id)); + return; } if (pi_->state != ProposalState::OPEN) { - return make_error( + ctx.rpc_ctx->set_error( HTTP_STATUS_BAD_REQUEST, ccf::errors::ProposalNotOpen, fmt::format( @@ -1123,6 +1155,7 @@ namespace ccf proposal_id, pi_->state, ProposalState::OPEN)); + return; } pi_->state = ProposalState::WITHDRAWN; @@ -1132,13 +1165,16 @@ namespace ccf record_voting_history( ctx.tx, caller_identity.member_id, caller_identity.signed_request); - return make_success(pi_.value()); + ctx.rpc_ctx->set_response_header( + http::headers::CONTENT_TYPE, http::headervalues::contenttype::JSON); + ctx.rpc_ctx->set_response_body(nlohmann::json(pi_.value()).dump()); + ctx.rpc_ctx->set_response_status(HTTP_STATUS_OK); }; make_endpoint( "/proposals/{proposal_id}/withdraw", HTTP_POST, - json_adapter(withdraw_js), + withdraw_js, member_sig_only) .set_auto_schema() .install(); From 6d8797a53db6a2dfb8efe5780b4601b6ca8bef0d Mon Sep 17 00:00:00 2001 From: Amaury Chamayou Date: Fri, 21 Oct 2022 16:01:16 +0000 Subject: [PATCH 05/23] more wip --- src/node/rpc/member_frontend.h | 166 +++++++++++++++++------- src/service/network_tables.h | 2 + src/service/tables/governance_history.h | 5 + 3 files changed, 125 insertions(+), 48 deletions(-) diff --git a/src/node/rpc/member_frontend.h b/src/node/rpc/member_frontend.h index dc5bdff40ee6..ddf9dcbd566f 100644 --- a/src/node/rpc/member_frontend.h +++ b/src/node/rpc/member_frontend.h @@ -361,6 +361,12 @@ namespace ccf governance_history->put(caller_id, {signed_request}); } + void record_cose_governance_history(kv::Tx& tx, const MemberId& caller_id, const std::span& cose_sign1) + { + auto cose_governance_history = tx.rw(network.cose_governance_history); + cose_governance_history->put(caller_id, {cose_sign1.begin(), cose_sign1.end()}); + } + bool get_proposal_id_from_path( const ccf::PathParams& params, ProposalId& proposal_id, @@ -418,17 +424,23 @@ namespace ccf return std::nullopt; } - bool authnz_active_member(ccf::endpoints::EndpointContext& ctx, std::optional& member_id) + bool authnz_active_member( + ccf::endpoints::EndpointContext& ctx, + std::optional& member_id, + std::optional& sig_auth_id, + std::optional& cose_auth_id) { if (const auto* cose_ident = ctx.try_get_caller()) { member_id = cose_ident->member_id; + cose_auth_id = *cose_ident; } else if ( const auto* sig_ident = ctx.try_get_caller()) { member_id = sig_ident->member_id; + sig_auth_id = *sig_ident; } else { ctx.rpc_ctx->set_error( HTTP_STATUS_FORBIDDEN, @@ -818,18 +830,11 @@ namespace ccf #pragma clang diagnostic ignored "-Wc99-extensions" auto post_proposals_js = [this](ccf::endpoints::EndpointContext& ctx) { - // TODO - // generic identity fetch - // store envelope in voting history table - - const auto& caller_identity = - ctx.get_caller(); - if (!check_member_active(ctx.tx, caller_identity.member_id)) + std::optional sig_auth_id = std::nullopt; + std::optional cose_auth_id = std::nullopt; + std::optional member_id = std::nullopt; + if (!authnz_active_member(ctx, member_id, sig_auth_id, cose_auth_id)) { - ctx.rpc_ctx->set_error( - HTTP_STATUS_FORBIDDEN, - ccf::errors::AuthorizationFailed, - "Member is not active."); return; } @@ -842,6 +847,22 @@ namespace ccf return; } + if (cose_auth_id.has_value()) + { + if (!(cose_auth_id->protected_header.gov_msg_type.has_value() && + cose_auth_id->protected_header.gov_msg_type.value() == "proposal")) + { + ctx.rpc_ctx->set_error( + HTTP_STATUS_BAD_REQUEST, + ccf::errors::InvalidResourceName, + "Unexpected message type"); + return; + } + } + + // TODO: handle cose_auth_id + auto request_digest = sig_auth_id->request_digest; + ProposalId proposal_id; if (consensus->type() == ConsensusType::CFT) { @@ -863,15 +884,15 @@ namespace ccf root_at_read.value().h.begin(), root_at_read.value().h.end()); acc.insert( acc.end(), - caller_identity.request_digest.begin(), - caller_identity.request_digest.end()); + request_digest.begin(), + request_digest.end()); const crypto::Sha256Hash proposal_digest(acc); proposal_id = proposal_digest.hex_str(); } else { proposal_id = fmt::format( - "{:02x}", fmt::join(caller_identity.request_digest, "")); + "{:02x}", fmt::join(request_digest, "")); } auto constitution = ctx.tx.ro(network.constitution)->get(); @@ -971,10 +992,17 @@ namespace ccf ctx.tx.rw(jsgov::Tables::PROPOSALS_INFO); pi->put( proposal_id, - {caller_identity.member_id, ccf::ProposalState::OPEN, {}}); + {member_id.value(), ccf::ProposalState::OPEN, {}}); + if (sig_auth_id.has_value()) + { record_voting_history( - ctx.tx, caller_identity.member_id, caller_identity.signed_request); + ctx.tx, member_id.value(), sig_auth_id->signed_request); + } + if (cose_auth_id.has_value()) + { + record_cose_governance_history(ctx.tx, member_id.value(), cose_auth_id->envelope); + } auto rv = resolve_proposal( ctx.tx, @@ -996,7 +1024,7 @@ namespace ccf { pi->put( proposal_id, - {caller_identity.member_id, + {member_id.value(), rv.state, {}, {}, @@ -1091,19 +1119,11 @@ namespace ccf .install(); auto withdraw_js = [this](ccf::endpoints::EndpointContext& ctx) { - - // TODO - // generic identity fetch - // store envelope in voting history table - - const auto& caller_identity = - ctx.template get_caller(); - if (!check_member_active(ctx.tx, caller_identity.member_id)) + std::optional sig_auth_id = std::nullopt; + std::optional cose_auth_id = std::nullopt; + std::optional member_id = std::nullopt; + if (!authnz_active_member(ctx, member_id, sig_auth_id, cose_auth_id)) { - ctx.rpc_ctx->set_error( - HTTP_STATUS_FORBIDDEN, - ccf::errors::AuthorizationFailed, - "Member is not active."); return; } @@ -1117,6 +1137,28 @@ namespace ccf return; } + if (cose_auth_id.has_value()) + { + if (!(cose_auth_id->protected_header.gov_msg_type.has_value() && + cose_auth_id->protected_header.gov_msg_type.value() == "withdrawal")) + { + ctx.rpc_ctx->set_error( + HTTP_STATUS_BAD_REQUEST, + ccf::errors::InvalidResourceName, + "Unexpected message type"); + return; + } + if (!(cose_auth_id->protected_header.gov_msg_proposal_id.has_value() && + cose_auth_id->protected_header.gov_msg_proposal_id.value() == proposal_id)) + { + ctx.rpc_ctx->set_error( + HTTP_STATUS_BAD_REQUEST, + ccf::errors::InvalidResourceName, + "Authenticated proposal id does not match URL"); + return; + } + } + auto pi = ctx.tx.rw(jsgov::Tables::PROPOSALS_INFO); auto pi_ = pi->get(proposal_id); @@ -1130,7 +1172,7 @@ namespace ccf return; } - if (caller_identity.member_id != pi_->proposer_id) + if (member_id.value() != pi_->proposer_id) { ctx.rpc_ctx->set_error( HTTP_STATUS_FORBIDDEN, @@ -1140,7 +1182,7 @@ namespace ccf "{}.", proposal_id, pi_->proposer_id, - caller_identity.member_id)); + member_id.value())); return; } @@ -1162,8 +1204,15 @@ namespace ccf pi->put(proposal_id, pi_.value()); remove_all_other_non_open_proposals(ctx.tx, proposal_id); - record_voting_history( - ctx.tx, caller_identity.member_id, caller_identity.signed_request); + if (sig_auth_id.has_value()) + { + record_voting_history( + ctx.tx, member_id.value(), sig_auth_id->signed_request); + } + if (cose_auth_id.has_value()) + { + record_cose_governance_history(ctx.tx, member_id.value(), cose_auth_id->envelope); + } ctx.rpc_ctx->set_response_header( http::headers::CONTENT_TYPE, http::headervalues::contenttype::JSON); @@ -1221,17 +1270,11 @@ namespace ccf .install(); auto vote_js = [this](ccf::endpoints::EndpointContext& ctx) { - // TODO - // generic identity fetch - // store envelope in voting history table - const auto& caller_identity = - ctx.get_caller(); - if (!check_member_active(ctx.tx, caller_identity.member_id)) + std::optional sig_auth_id = std::nullopt; + std::optional cose_auth_id = std::nullopt; + std::optional member_id = std::nullopt; + if (!authnz_active_member(ctx, member_id, sig_auth_id, cose_auth_id)) { - ctx.rpc_ctx->set_error( - HTTP_STATUS_FORBIDDEN, - ccf::errors::AuthorizationFailed, - "Member is not active."); return; } @@ -1246,6 +1289,27 @@ namespace ccf std::move(error)); return; } + if (cose_auth_id.has_value()) + { + if (!(cose_auth_id->protected_header.gov_msg_type.has_value() && + cose_auth_id->protected_header.gov_msg_type.value() == "ballot")) + { + ctx.rpc_ctx->set_error( + HTTP_STATUS_BAD_REQUEST, + ccf::errors::InvalidResourceName, + "Unexpected message type"); + return; + } + if (!(cose_auth_id->protected_header.gov_msg_proposal_id.has_value() && + cose_auth_id->protected_header.gov_msg_proposal_id.value() == proposal_id)) + { + ctx.rpc_ctx->set_error( + HTTP_STATUS_BAD_REQUEST, + ccf::errors::InvalidResourceName, + "Authenticated proposal id does not match URL"); + return; + } + } auto constitution = ctx.tx.ro(network.constitution)->get(); if (!constitution.has_value()) @@ -1295,7 +1359,7 @@ namespace ccf return; } - if (pi_->ballots.find(caller_identity.member_id) != pi_->ballots.end()) + if (pi_->ballots.find(member_id.value()) != pi_->ballots.end()) { ctx.rpc_ctx->set_error( HTTP_STATUS_BAD_REQUEST, @@ -1314,12 +1378,18 @@ namespace ccf context.function(params["ballot"], "vote", "body[\"ballot\"]"); } - pi_->ballots[caller_identity.member_id] = params["ballot"]; + pi_->ballots[member_id.value()] = params["ballot"]; pi->put(proposal_id, pi_.value()); - // Do we still need to do this? + if (sig_auth_id.has_value()) + { record_voting_history( - ctx.tx, caller_identity.member_id, caller_identity.signed_request); + ctx.tx, member_id.value(), sig_auth_id->signed_request); + } + if (cose_auth_id.has_value()) + { + record_cose_governance_history(ctx.tx, member_id.value(), cose_auth_id->envelope); + } auto rv = resolve_proposal( ctx.tx, proposal_id, p.value(), constitution.value()); diff --git a/src/service/network_tables.h b/src/service/network_tables.h index d2cf9acdb60c..3610ed3c983b 100644 --- a/src/service/network_tables.h +++ b/src/service/network_tables.h @@ -63,6 +63,7 @@ namespace ccf SecurityPolicies security_policies; MemberAcks member_acks; GovernanceHistory governance_history; + COSEGovernanceHistory cose_governance_history; RecoveryShares shares; EncryptedLedgerSecretsInfo encrypted_ledger_secrets; EncryptedSubmittedShares encrypted_submitted_shares; @@ -121,6 +122,7 @@ namespace ccf security_policies(Tables::SECURITY_POLICIES), member_acks(Tables::MEMBER_ACKS), governance_history(Tables::GOV_HISTORY), + cose_governance_history(Tables::COSE_GOV_HISTORY), shares(Tables::SHARES), encrypted_ledger_secrets(Tables::ENCRYPTED_PAST_LEDGER_SECRET), encrypted_submitted_shares(Tables::ENCRYPTED_SUBMITTED_SHARES), diff --git a/src/service/tables/governance_history.h b/src/service/tables/governance_history.h index 08d7c28707a4..794b49d1b2ec 100644 --- a/src/service/tables/governance_history.h +++ b/src/service/tables/governance_history.h @@ -13,4 +13,9 @@ namespace ccf { static constexpr auto GOV_HISTORY = "public:ccf.gov.history"; } + using COSEGovernanceHistory = ServiceMap>; + namespace Tables + { + static constexpr auto COSE_GOV_HISTORY = "public:ccf.gov.cose_history"; + } } \ No newline at end of file From 46bf90b9b4dd1669eedaf475d3b8c0593be90cf5 Mon Sep 17 00:00:00 2001 From: Amaury Chamayou Date: Fri, 21 Oct 2022 16:15:02 +0000 Subject: [PATCH 06/23] wip --- src/node/rpc/member_frontend.h | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/src/node/rpc/member_frontend.h b/src/node/rpc/member_frontend.h index ddf9dcbd566f..866411ede667 100644 --- a/src/node/rpc/member_frontend.h +++ b/src/node/rpc/member_frontend.h @@ -5,6 +5,7 @@ #include "ccf/common_endpoint_registry.h" #include "ccf/crypto/base64.h" #include "ccf/crypto/key_pair.h" +#include "ccf/crypto/sha256.h" #include "ccf/ds/nonstd.h" #include "ccf/json_handler.h" #include "ccf/node/quote.h" @@ -860,8 +861,17 @@ namespace ccf } } - // TODO: handle cose_auth_id - auto request_digest = sig_auth_id->request_digest; + std::vector request_digest; + if (sig_auth_id.has_value()) + { + request_digest = sig_auth_id->request_digest; + } + if (cose_auth_id.has_value()) + { + // This isn't right, instead the digest of the COSE Sign1 + // TBS should be used here. + request_digest = crypto::sha256({cose_auth_id->envelope.begin(), cose_auth_id->envelope.end()}); + } ProposalId proposal_id; if (consensus->type() == ConsensusType::CFT) From 39911f4907fa55cef0adf7f76518cdfcc6d2ed52 Mon Sep 17 00:00:00 2001 From: Amaury Chamayou Date: Sat, 22 Oct 2022 16:04:46 +0000 Subject: [PATCH 07/23] wip --- src/node/rpc/member_frontend.h | 33 ++++++++++++++++++++++++++++----- 1 file changed, 28 insertions(+), 5 deletions(-) diff --git a/src/node/rpc/member_frontend.h b/src/node/rpc/member_frontend.h index 866411ede667..fac1d4555eaf 100644 --- a/src/node/rpc/member_frontend.h +++ b/src/node/rpc/member_frontend.h @@ -621,8 +621,6 @@ namespace ccf .install(); auto get_encrypted_recovery_share = [this](ccf::endpoints::EndpointContext& ctx) { - // TODO - // Branch on content type const auto member_id = get_caller_member_id(ctx); if (!member_id.has_value()) { @@ -641,6 +639,20 @@ namespace ccf return; } + if (const auto* cose_auth_id = + ctx.try_get_caller()) + { + if (!(cose_auth_id->protected_header.gov_msg_type.has_value() && + cose_auth_id->protected_header.gov_msg_type.value() == "encrypted_recovery_share")) + { + ctx.rpc_ctx->set_error( + HTTP_STATUS_BAD_REQUEST, + ccf::errors::InvalidResourceName, + "Unexpected message type"); + return; + } + } + auto encrypted_share = share_manager.get_encrypted_share(ctx.tx, member_id.value()); @@ -671,9 +683,6 @@ namespace ccf .install(); auto submit_recovery_share = [this](ccf::endpoints::EndpointContext& ctx) { - // TODO - // Branch on content type - auto params = nlohmann::json::parse(ctx.rpc_ctx->get_request_body()); // Only active members can submit their shares for recovery @@ -695,6 +704,20 @@ namespace ccf return; } + if (const auto* cose_auth_id = + ctx.try_get_caller()) + { + if (!(cose_auth_id->protected_header.gov_msg_type.has_value() && + cose_auth_id->protected_header.gov_msg_type.value() == "recovery_share")) + { + ctx.rpc_ctx->set_error( + HTTP_STATUS_BAD_REQUEST, + ccf::errors::InvalidResourceName, + "Unexpected message type"); + return; + } + } + GenesisGenerator g(this->network, ctx.tx); if ( g.get_service_status() != ServiceStatus::WAITING_FOR_RECOVERY_SHARES) From 94d1629653b6f928f35a4121c37592df8a8a4cf6 Mon Sep 17 00:00:00 2001 From: Amaury Chamayou Date: Mon, 24 Oct 2022 16:20:58 +0000 Subject: [PATCH 08/23] last TODO, fmt --- include/ccf/service/tables/members.h | 12 +- src/node/rpc/member_frontend.h | 362 +++++++++++++++------------ 2 files changed, 213 insertions(+), 161 deletions(-) diff --git a/include/ccf/service/tables/members.h b/include/ccf/service/tables/members.h index a112234531ea..a4dbe7614ee8 100644 --- a/include/ccf/service/tables/members.h +++ b/include/ccf/service/tables/members.h @@ -109,6 +109,9 @@ namespace ccf /// Signed request containing the last state digest. std::optional signed_req = std::nullopt; + /// COSE Sign1 containing the last state digest + std::optional> cose_sign1_req = std::nullopt; + MemberAck() {} MemberAck(const crypto::Sha256Hash& root) : StateDigest(root) {} @@ -117,6 +120,13 @@ namespace ccf StateDigest(root), signed_req(signed_req_) {} + + MemberAck( + const crypto::Sha256Hash& root, + const std::vector& cose_sign1_req_) : + StateDigest(root), + cose_sign1_req(cose_sign1_req_) + {} }; DECLARE_JSON_TYPE_WITH_BASE_AND_OPTIONAL_FIELDS(MemberAck, StateDigest) #pragma clang diagnostic push @@ -124,7 +134,7 @@ namespace ccf #pragma clang diagnostic ignored "-Wgnu-zero-variadic-macro-arguments" DECLARE_JSON_REQUIRED_FIELDS(MemberAck) #pragma clang diagnostic pop - DECLARE_JSON_OPTIONAL_FIELDS(MemberAck, signed_req) + DECLARE_JSON_OPTIONAL_FIELDS(MemberAck, signed_req, cose_sign1_req) using MemberAcks = ServiceMap; namespace Tables { diff --git a/src/node/rpc/member_frontend.h b/src/node/rpc/member_frontend.h index fac1d4555eaf..68d3367a52dd 100644 --- a/src/node/rpc/member_frontend.h +++ b/src/node/rpc/member_frontend.h @@ -362,10 +362,14 @@ namespace ccf governance_history->put(caller_id, {signed_request}); } - void record_cose_governance_history(kv::Tx& tx, const MemberId& caller_id, const std::span& cose_sign1) + void record_cose_governance_history( + kv::Tx& tx, + const MemberId& caller_id, + const std::span& cose_sign1) { auto cose_governance_history = tx.rw(network.cose_governance_history); - cose_governance_history->put(caller_id, {cose_sign1.begin(), cose_sign1.end()}); + cose_governance_history->put( + caller_id, {cose_sign1.begin(), cose_sign1.end()}); } bool get_proposal_id_from_path( @@ -404,11 +408,13 @@ namespace ccf static std::optional get_caller_member_id( endpoints::CommandEndpointContext& ctx) { - if (const auto* cose_ident = - ctx.try_get_caller()) + if ( + const auto* cose_ident = + ctx.try_get_caller()) { return cose_ident->member_id; - } else if ( + } + else if ( const auto* sig_ident = ctx.try_get_caller()) { @@ -431,18 +437,22 @@ namespace ccf std::optional& sig_auth_id, std::optional& cose_auth_id) { - if (const auto* cose_ident = - ctx.try_get_caller()) + if ( + const auto* cose_ident = + ctx.try_get_caller()) { member_id = cose_ident->member_id; cose_auth_id = *cose_ident; - } else if ( + } + else if ( const auto* sig_ident = ctx.try_get_caller()) { member_id = sig_ident->member_id; sig_auth_id = *sig_ident; - } else { + } + else + { ctx.rpc_ctx->set_error( HTTP_STATUS_FORBIDDEN, ccf::errors::AuthorizationFailed, @@ -467,33 +477,35 @@ namespace ccf { CommonEndpointRegistry::init_handlers(); - const AuthnPolicies member_sig_only = {member_signature_auth_policy, - member_cose_sign1_auth_policy}; + const AuthnPolicies member_sig_only = { + member_signature_auth_policy, member_cose_sign1_auth_policy}; const AuthnPolicies member_cert_or_sig = { - member_cert_auth_policy, - member_signature_auth_policy}; + member_cert_auth_policy, member_signature_auth_policy}; //! A member acknowledges state auto ack = [this](ccf::endpoints::EndpointContext& ctx) { - // TODO + std::optional sig_auth_id = + std::nullopt; + std::optional cose_auth_id = + std::nullopt; + std::optional member_id = std::nullopt; + if (!authnz_active_member(ctx, member_id, sig_auth_id, cose_auth_id)) + { + return; + } + auto params = nlohmann::json::parse(ctx.rpc_ctx->get_request_body()); - // Branch on content type - // Expand MemberAck to contain a COSE_Sign1 - const auto& caller_identity = - ctx.template get_caller(); - const auto& signed_request = caller_identity.signed_request; auto mas = ctx.tx.rw(this->network.member_acks); - const auto ma = mas->get(caller_identity.member_id); + const auto ma = mas->get(member_id.value()); if (!ma) { ctx.rpc_ctx->set_error( HTTP_STATUS_FORBIDDEN, ccf::errors::AuthorizationFailed, fmt::format( - "No ACK record exists for caller {}.", - caller_identity.member_id)); + "No ACK record exists for caller {}.", member_id.value())); return; } @@ -509,21 +521,40 @@ namespace ccf auto sig = ctx.tx.rw(this->network.signatures); const auto s = sig->get(); - if (!s) + if (sig_auth_id.has_value()) { - mas->put(caller_identity.member_id, MemberAck({}, signed_request)); + if (!s) + { + mas->put( + member_id.value(), MemberAck({}, sig_auth_id->signed_request)); + } + else + { + mas->put( + member_id.value(), + MemberAck(s->root, sig_auth_id->signed_request)); + } } - else + + if (cose_auth_id.has_value()) { - mas->put( - caller_identity.member_id, MemberAck(s->root, signed_request)); + std::vector cose_sign1 = { + cose_auth_id->envelope.begin(), cose_auth_id->envelope.end()}; + if (!s) + { + mas->put(member_id.value(), MemberAck({}, cose_sign1)); + } + else + { + mas->put(member_id.value(), MemberAck(s->root, cose_sign1)); + } } // update member status to ACTIVE GenesisGenerator g(this->network, ctx.tx); try { - g.activate_member(caller_identity.member_id); + g.activate_member(member_id.value()); } catch (const std::logic_error& e) { @@ -545,10 +576,10 @@ namespace ccf } auto members = ctx.tx.rw(this->network.member_info); - auto member_info = members->get(caller_identity.member_id); + auto member_info = members->get(member_id.value()); if ( service_status.value() == ServiceStatus::OPEN && - g.is_recovery_member(caller_identity.member_id)) + g.is_recovery_member(member_id.value())) { // When the service is OPEN and the new active member is a recovery // member, all recovery members are allocated new recovery shares @@ -607,8 +638,8 @@ namespace ccf j["state_digest"] = ma->state_digest; ctx.rpc_ctx->set_response_header( - http::headers::CONTENT_TYPE, http::headervalues::contenttype::JSON); - ctx.rpc_ctx->set_response_body(j.dump()); + http::headers::CONTENT_TYPE, http::headervalues::contenttype::JSON); + ctx.rpc_ctx->set_response_body(j.dump()); ctx.rpc_ctx->set_response_status(HTTP_STATUS_OK); return; }; @@ -620,30 +651,33 @@ namespace ccf .set_auto_schema() .install(); - auto get_encrypted_recovery_share = [this](ccf::endpoints::EndpointContext& ctx) { - const auto member_id = get_caller_member_id(ctx); - if (!member_id.has_value()) - { - ctx.rpc_ctx->set_error( - HTTP_STATUS_FORBIDDEN, - ccf::errors::AuthorizationFailed, - "Member is unknown."); - return; - } - if (!check_member_active(ctx.tx, member_id.value())) - { - ctx.rpc_ctx->set_error( - HTTP_STATUS_FORBIDDEN, - ccf::errors::AuthorizationFailed, - "Only active members are given recovery shares."); - return; - } + auto get_encrypted_recovery_share = + [this](ccf::endpoints::EndpointContext& ctx) { + const auto member_id = get_caller_member_id(ctx); + if (!member_id.has_value()) + { + ctx.rpc_ctx->set_error( + HTTP_STATUS_FORBIDDEN, + ccf::errors::AuthorizationFailed, + "Member is unknown."); + return; + } + if (!check_member_active(ctx.tx, member_id.value())) + { + ctx.rpc_ctx->set_error( + HTTP_STATUS_FORBIDDEN, + ccf::errors::AuthorizationFailed, + "Only active members are given recovery shares."); + return; + } - if (const auto* cose_auth_id = - ctx.try_get_caller()) - { - if (!(cose_auth_id->protected_header.gov_msg_type.has_value() && - cose_auth_id->protected_header.gov_msg_type.value() == "encrypted_recovery_share")) + if ( + const auto* cose_auth_id = + ctx.try_get_caller()) + { + if (!(cose_auth_id->protected_header.gov_msg_type.has_value() && + cose_auth_id->protected_header.gov_msg_type.value() == + "encrypted_recovery_share")) { ctx.rpc_ctx->set_error( HTTP_STATUS_BAD_REQUEST, @@ -651,29 +685,29 @@ namespace ccf "Unexpected message type"); return; } - } + } - auto encrypted_share = - share_manager.get_encrypted_share(ctx.tx, member_id.value()); + auto encrypted_share = + share_manager.get_encrypted_share(ctx.tx, member_id.value()); - if (!encrypted_share.has_value()) - { - ctx.rpc_ctx->set_error( - HTTP_STATUS_NOT_FOUND, - ccf::errors::ResourceNotFound, - fmt::format( - "Recovery share not found for member {}.", member_id->value())); + if (!encrypted_share.has_value()) + { + ctx.rpc_ctx->set_error( + HTTP_STATUS_NOT_FOUND, + ccf::errors::ResourceNotFound, + fmt::format( + "Recovery share not found for member {}.", member_id->value())); return; - } + } - auto rec_share = - GetRecoveryShare::Out{crypto::b64_from_raw(encrypted_share.value())}; - ctx.rpc_ctx->set_response_header( + auto rec_share = GetRecoveryShare::Out{ + crypto::b64_from_raw(encrypted_share.value())}; + ctx.rpc_ctx->set_response_header( http::headers::CONTENT_TYPE, http::headervalues::contenttype::JSON); ctx.rpc_ctx->set_response_body(nlohmann::json(rec_share).dump()); - ctx.rpc_ctx->set_response_status(HTTP_STATUS_OK); - return; - }; + ctx.rpc_ctx->set_response_status(HTTP_STATUS_OK); + return; + }; make_endpoint( "/recovery_share", HTTP_GET, @@ -682,7 +716,8 @@ namespace ccf .set_auto_schema() .install(); - auto submit_recovery_share = [this](ccf::endpoints::EndpointContext& ctx) { + auto submit_recovery_share = [this]( + ccf::endpoints::EndpointContext& ctx) { auto params = nlohmann::json::parse(ctx.rpc_ctx->get_request_body()); // Only active members can submit their shares for recovery @@ -704,18 +739,20 @@ namespace ccf return; } - if (const auto* cose_auth_id = + if ( + const auto* cose_auth_id = ctx.try_get_caller()) { if (!(cose_auth_id->protected_header.gov_msg_type.has_value() && - cose_auth_id->protected_header.gov_msg_type.value() == "recovery_share")) - { - ctx.rpc_ctx->set_error( - HTTP_STATUS_BAD_REQUEST, - ccf::errors::InvalidResourceName, - "Unexpected message type"); - return; - } + cose_auth_id->protected_header.gov_msg_type.value() == + "recovery_share")) + { + ctx.rpc_ctx->set_error( + HTTP_STATUS_BAD_REQUEST, + ccf::errors::InvalidResourceName, + "Unexpected message type"); + return; + } } GenesisGenerator g(this->network, ctx.tx); @@ -812,15 +849,12 @@ namespace ccf submitted_shares_count, g.get_recovery_threshold())}; ctx.rpc_ctx->set_response_header( - http::headers::CONTENT_TYPE, http::headervalues::contenttype::JSON); + http::headers::CONTENT_TYPE, http::headervalues::contenttype::JSON); ctx.rpc_ctx->set_response_body(nlohmann::json(recovery_share).dump()); ctx.rpc_ctx->set_response_status(HTTP_STATUS_OK); }; make_endpoint( - "/recovery_share", - HTTP_POST, - submit_recovery_share, - member_cert_or_sig) + "/recovery_share", HTTP_POST, submit_recovery_share, member_cert_or_sig) .set_auto_schema() .install(); @@ -854,8 +888,10 @@ namespace ccf #pragma clang diagnostic ignored "-Wc99-extensions" auto post_proposals_js = [this](ccf::endpoints::EndpointContext& ctx) { - std::optional sig_auth_id = std::nullopt; - std::optional cose_auth_id = std::nullopt; + std::optional sig_auth_id = + std::nullopt; + std::optional cose_auth_id = + std::nullopt; std::optional member_id = std::nullopt; if (!authnz_active_member(ctx, member_id, sig_auth_id, cose_auth_id)) { @@ -874,14 +910,15 @@ namespace ccf if (cose_auth_id.has_value()) { if (!(cose_auth_id->protected_header.gov_msg_type.has_value() && - cose_auth_id->protected_header.gov_msg_type.value() == "proposal")) - { - ctx.rpc_ctx->set_error( - HTTP_STATUS_BAD_REQUEST, - ccf::errors::InvalidResourceName, - "Unexpected message type"); - return; - } + cose_auth_id->protected_header.gov_msg_type.value() == + "proposal")) + { + ctx.rpc_ctx->set_error( + HTTP_STATUS_BAD_REQUEST, + ccf::errors::InvalidResourceName, + "Unexpected message type"); + return; + } } std::vector request_digest; @@ -893,7 +930,8 @@ namespace ccf { // This isn't right, instead the digest of the COSE Sign1 // TBS should be used here. - request_digest = crypto::sha256({cose_auth_id->envelope.begin(), cose_auth_id->envelope.end()}); + request_digest = crypto::sha256( + {cose_auth_id->envelope.begin(), cose_auth_id->envelope.end()}); } ProposalId proposal_id; @@ -915,17 +953,13 @@ namespace ccf // request digest. std::vector acc( root_at_read.value().h.begin(), root_at_read.value().h.end()); - acc.insert( - acc.end(), - request_digest.begin(), - request_digest.end()); + acc.insert(acc.end(), request_digest.begin(), request_digest.end()); const crypto::Sha256Hash proposal_digest(acc); proposal_id = proposal_digest.hex_str(); } else { - proposal_id = fmt::format( - "{:02x}", fmt::join(request_digest, "")); + proposal_id = fmt::format("{:02x}", fmt::join(request_digest, "")); } auto constitution = ctx.tx.ro(network.constitution)->get(); @@ -1023,18 +1057,17 @@ namespace ccf auto pi = ctx.tx.rw(jsgov::Tables::PROPOSALS_INFO); - pi->put( - proposal_id, - {member_id.value(), ccf::ProposalState::OPEN, {}}); + pi->put(proposal_id, {member_id.value(), ccf::ProposalState::OPEN, {}}); if (sig_auth_id.has_value()) { - record_voting_history( - ctx.tx, member_id.value(), sig_auth_id->signed_request); + record_voting_history( + ctx.tx, member_id.value(), sig_auth_id->signed_request); } if (cose_auth_id.has_value()) { - record_cose_governance_history(ctx.tx, member_id.value(), cose_auth_id->envelope); + record_cose_governance_history( + ctx.tx, member_id.value(), cose_auth_id->envelope); } auto rv = resolve_proposal( @@ -1057,12 +1090,7 @@ namespace ccf { pi->put( proposal_id, - {member_id.value(), - rv.state, - {}, - {}, - std::nullopt, - rv.failure}); + {member_id.value(), rv.state, {}, {}, std::nullopt, rv.failure}); ctx.rpc_ctx->set_response_header( http::headers::CONTENT_TYPE, http::headervalues::contenttype::JSON); ctx.rpc_ctx->set_response_body(nlohmann::json(rv).dump()); @@ -1152,8 +1180,10 @@ namespace ccf .install(); auto withdraw_js = [this](ccf::endpoints::EndpointContext& ctx) { - std::optional sig_auth_id = std::nullopt; - std::optional cose_auth_id = std::nullopt; + std::optional sig_auth_id = + std::nullopt; + std::optional cose_auth_id = + std::nullopt; std::optional member_id = std::nullopt; if (!authnz_active_member(ctx, member_id, sig_auth_id, cose_auth_id)) { @@ -1166,30 +1196,35 @@ namespace ccf ctx.rpc_ctx->get_request_path_params(), proposal_id, error)) { ctx.rpc_ctx->set_error( - HTTP_STATUS_BAD_REQUEST, ccf::errors::InvalidResourceName, std::move(error)); + HTTP_STATUS_BAD_REQUEST, + ccf::errors::InvalidResourceName, + std::move(error)); return; } if (cose_auth_id.has_value()) { if (!(cose_auth_id->protected_header.gov_msg_type.has_value() && - cose_auth_id->protected_header.gov_msg_type.value() == "withdrawal")) - { - ctx.rpc_ctx->set_error( - HTTP_STATUS_BAD_REQUEST, - ccf::errors::InvalidResourceName, - "Unexpected message type"); - return; - } - if (!(cose_auth_id->protected_header.gov_msg_proposal_id.has_value() && - cose_auth_id->protected_header.gov_msg_proposal_id.value() == proposal_id)) - { - ctx.rpc_ctx->set_error( - HTTP_STATUS_BAD_REQUEST, - ccf::errors::InvalidResourceName, - "Authenticated proposal id does not match URL"); - return; - } + cose_auth_id->protected_header.gov_msg_type.value() == + "withdrawal")) + { + ctx.rpc_ctx->set_error( + HTTP_STATUS_BAD_REQUEST, + ccf::errors::InvalidResourceName, + "Unexpected message type"); + return; + } + if (!(cose_auth_id->protected_header.gov_msg_proposal_id + .has_value() && + cose_auth_id->protected_header.gov_msg_proposal_id.value() == + proposal_id)) + { + ctx.rpc_ctx->set_error( + HTTP_STATUS_BAD_REQUEST, + ccf::errors::InvalidResourceName, + "Authenticated proposal id does not match URL"); + return; + } } auto pi = @@ -1244,11 +1279,12 @@ namespace ccf } if (cose_auth_id.has_value()) { - record_cose_governance_history(ctx.tx, member_id.value(), cose_auth_id->envelope); + record_cose_governance_history( + ctx.tx, member_id.value(), cose_auth_id->envelope); } ctx.rpc_ctx->set_response_header( - http::headers::CONTENT_TYPE, http::headervalues::contenttype::JSON); + http::headers::CONTENT_TYPE, http::headervalues::contenttype::JSON); ctx.rpc_ctx->set_response_body(nlohmann::json(pi_.value()).dump()); ctx.rpc_ctx->set_response_status(HTTP_STATUS_OK); }; @@ -1303,8 +1339,10 @@ namespace ccf .install(); auto vote_js = [this](ccf::endpoints::EndpointContext& ctx) { - std::optional sig_auth_id = std::nullopt; - std::optional cose_auth_id = std::nullopt; + std::optional sig_auth_id = + std::nullopt; + std::optional cose_auth_id = + std::nullopt; std::optional member_id = std::nullopt; if (!authnz_active_member(ctx, member_id, sig_auth_id, cose_auth_id)) { @@ -1325,23 +1363,26 @@ namespace ccf if (cose_auth_id.has_value()) { if (!(cose_auth_id->protected_header.gov_msg_type.has_value() && - cose_auth_id->protected_header.gov_msg_type.value() == "ballot")) - { - ctx.rpc_ctx->set_error( - HTTP_STATUS_BAD_REQUEST, - ccf::errors::InvalidResourceName, - "Unexpected message type"); - return; - } - if (!(cose_auth_id->protected_header.gov_msg_proposal_id.has_value() && - cose_auth_id->protected_header.gov_msg_proposal_id.value() == proposal_id)) - { - ctx.rpc_ctx->set_error( - HTTP_STATUS_BAD_REQUEST, - ccf::errors::InvalidResourceName, - "Authenticated proposal id does not match URL"); - return; - } + cose_auth_id->protected_header.gov_msg_type.value() == + "ballot")) + { + ctx.rpc_ctx->set_error( + HTTP_STATUS_BAD_REQUEST, + ccf::errors::InvalidResourceName, + "Unexpected message type"); + return; + } + if (!(cose_auth_id->protected_header.gov_msg_proposal_id + .has_value() && + cose_auth_id->protected_header.gov_msg_proposal_id.value() == + proposal_id)) + { + ctx.rpc_ctx->set_error( + HTTP_STATUS_BAD_REQUEST, + ccf::errors::InvalidResourceName, + "Authenticated proposal id does not match URL"); + return; + } } auto constitution = ctx.tx.ro(network.constitution)->get(); @@ -1416,12 +1457,13 @@ namespace ccf if (sig_auth_id.has_value()) { - record_voting_history( - ctx.tx, member_id.value(), sig_auth_id->signed_request); + record_voting_history( + ctx.tx, member_id.value(), sig_auth_id->signed_request); } if (cose_auth_id.has_value()) { - record_cose_governance_history(ctx.tx, member_id.value(), cose_auth_id->envelope); + record_cose_governance_history( + ctx.tx, member_id.value(), cose_auth_id->envelope); } auto rv = resolve_proposal( From 8893a9a009206948ada21dfcb55b893a15e909f4 Mon Sep 17 00:00:00 2001 From: Amaury Chamayou Date: Mon, 24 Oct 2022 17:14:15 +0000 Subject: [PATCH 09/23] duh --- src/node/rpc/member_frontend.h | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/node/rpc/member_frontend.h b/src/node/rpc/member_frontend.h index cc23cddd9f65..71fb1b9ef4a4 100644 --- a/src/node/rpc/member_frontend.h +++ b/src/node/rpc/member_frontend.h @@ -435,7 +435,8 @@ namespace ccf ccf::endpoints::EndpointContext& ctx, std::optional& member_id, std::optional& sig_auth_id, - std::optional& cose_auth_id) + std::optional& cose_auth_id, + bool must_be_active=true) { if ( const auto* cose_ident = @@ -461,7 +462,7 @@ namespace ccf return false; } - if (!check_member_active(ctx.tx, member_id.value())) + if (must_be_active && !check_member_active(ctx.tx, member_id.value())) { ctx.rpc_ctx->set_error( HTTP_STATUS_FORBIDDEN, @@ -490,7 +491,7 @@ namespace ccf std::optional cose_auth_id = std::nullopt; std::optional member_id = std::nullopt; - if (!authnz_active_member(ctx, member_id, sig_auth_id, cose_auth_id)) + if (!authnz_active_member(ctx, member_id, sig_auth_id, cose_auth_id, false)) { return; } From a0b1759e2f035df9b61915bc0b52e1921339cb38 Mon Sep 17 00:00:00 2001 From: Amaury Chamayou Date: Mon, 24 Oct 2022 18:59:58 +0000 Subject: [PATCH 10/23] fix --- doc/schemas/gov_openapi.json | 39 ++++++++++++++++------------------ src/node/rpc/member_frontend.h | 7 +++--- 2 files changed, 22 insertions(+), 24 deletions(-) diff --git a/doc/schemas/gov_openapi.json b/doc/schemas/gov_openapi.json index a5d336a90955..697922145644 100644 --- a/doc/schemas/gov_openapi.json +++ b/doc/schemas/gov_openapi.json @@ -463,6 +463,11 @@ } }, "securitySchemes": { + "member_cose_sign1": { + "description": "Request payload must be a COSE Sign1 document, with expected protected headers.Signer must be a member identity registered with this service.", + "scheme": "cose_sign1", + "type": "http" + }, "member_signature": { "description": "Request must be signed according to the HTTP Signature scheme. The signer must be a member identity registered with this service.", "scheme": "signature", @@ -487,7 +492,7 @@ "info": { "description": "This API is used to submit and query proposals which affect CCF's public governance tables.", "title": "CCF Governance API", - "version": "2.9.8" + "version": "2.10.0" }, "openapi": "3.0.0", "paths": { @@ -514,6 +519,9 @@ "security": [ { "member_signature": [] + }, + { + "member_cose_sign1": [] } ], "x-ccf-forwarding": { @@ -729,11 +737,6 @@ "$ref": "#/components/responses/default" } }, - "security": [ - { - "member_signature": [] - } - ], "x-ccf-forwarding": { "$ref": "#/components/x-ccf-forwarding/sometimes" } @@ -767,6 +770,9 @@ "security": [ { "member_signature": [] + }, + { + "member_cose_sign1": [] } ], "x-ccf-forwarding": { @@ -791,11 +797,6 @@ "$ref": "#/components/responses/default" } }, - "security": [ - { - "member_signature": [] - } - ], "x-ccf-forwarding": { "$ref": "#/components/x-ccf-forwarding/sometimes" } @@ -828,11 +829,6 @@ "$ref": "#/components/responses/default" } }, - "security": [ - { - "member_signature": [] - } - ], "x-ccf-forwarding": { "$ref": "#/components/x-ccf-forwarding/sometimes" } @@ -888,6 +884,9 @@ "security": [ { "member_signature": [] + }, + { + "member_cose_sign1": [] } ], "x-ccf-forwarding": { @@ -912,11 +911,6 @@ "$ref": "#/components/responses/default" } }, - "security": [ - { - "member_signature": [] - } - ], "x-ccf-forwarding": { "$ref": "#/components/x-ccf-forwarding/sometimes" } @@ -970,6 +964,9 @@ "security": [ { "member_signature": [] + }, + { + "member_cose_sign1": [] } ], "x-ccf-forwarding": { diff --git a/src/node/rpc/member_frontend.h b/src/node/rpc/member_frontend.h index 71fb1b9ef4a4..322ac0ae02e4 100644 --- a/src/node/rpc/member_frontend.h +++ b/src/node/rpc/member_frontend.h @@ -402,7 +402,7 @@ namespace ccf openapi_info.description = "This API is used to submit and query proposals which affect CCF's " "public governance tables."; - openapi_info.document_version = "2.9.8"; + openapi_info.document_version = "2.10.0"; } static std::optional get_caller_member_id( @@ -436,7 +436,7 @@ namespace ccf std::optional& member_id, std::optional& sig_auth_id, std::optional& cose_auth_id, - bool must_be_active=true) + bool must_be_active = true) { if ( const auto* cose_ident = @@ -491,7 +491,8 @@ namespace ccf std::optional cose_auth_id = std::nullopt; std::optional member_id = std::nullopt; - if (!authnz_active_member(ctx, member_id, sig_auth_id, cose_auth_id, false)) + if (!authnz_active_member( + ctx, member_id, sig_auth_id, cose_auth_id, false)) { return; } From e6c3f39215cc4fbc5c4d9406108c23b33cf0b529 Mon Sep 17 00:00:00 2001 From: Amaury Chamayou Date: Mon, 24 Oct 2022 19:17:50 +0000 Subject: [PATCH 11/23] fix membership test --- tests/memberclient.py | 23 ++++++++++++----------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/tests/memberclient.py b/tests/memberclient.py index 89ec0ccb5fcb..b5e161857766 100644 --- a/tests/memberclient.py +++ b/tests/memberclient.py @@ -24,17 +24,18 @@ def test_missing_signature_header(network, args): www_auth = "www-authenticate" assert www_auth in r.headers, r.headers auth_header = r.headers[www_auth] - assert auth_header.startswith("Signature"), auth_header - elements = { - e[0].strip(): e[1] - for e in (element.split("=") for element in auth_header.split(",")) - } - assert "headers" in elements, elements - required_headers = elements["headers"] - assert required_headers.startswith('"'), required_headers - assert required_headers.endswith('"'), required_headers - assert "(request-target)" in required_headers, required_headers - assert "digest" in required_headers, required_headers + if auth_header != 'COSE-SIGN1 realm="Signed request access"': + assert auth_header.startswith("Signature"), auth_header + elements = { + e[0].strip(): e[1] + for e in (element.split("=") for element in auth_header.split(",")) + } + assert "headers" in elements, elements + required_headers = elements["headers"] + assert required_headers.startswith('"'), required_headers + assert required_headers.endswith('"'), required_headers + assert "(request-target)" in required_headers, required_headers + assert "digest" in required_headers, required_headers return network From 2aa310e2e358f7164244a811293e1bdd3cca0f73 Mon Sep 17 00:00:00 2001 From: Amaury Chamayou Date: Mon, 24 Oct 2022 20:02:15 +0000 Subject: [PATCH 12/23] . --- tests/governance.py | 46 ++++++++++++++++++++++----------------------- 1 file changed, 23 insertions(+), 23 deletions(-) diff --git a/tests/governance.py b/tests/governance.py index 7b4a12caebf1..f7822a522881 100644 --- a/tests/governance.py +++ b/tests/governance.py @@ -689,14 +689,14 @@ def add(parser): infra.log_capture.COLORS = False - # cr.add( - # "session_auth", - # gov, - # package="samples/apps/logging/liblogging", - # nodes=infra.e2e_args.max_nodes(cr.args, f=0), - # initial_user_count=3, - # authenticate_session=True, - # ) + cr.add( + "session_auth", + gov, + package="samples/apps/logging/liblogging", + nodes=infra.e2e_args.max_nodes(cr.args, f=0), + initial_user_count=3, + authenticate_session=True, + ) cr.add( "session_noauth", @@ -707,20 +707,20 @@ def add(parser): authenticate_session=False, ) - # cr.add( - # "js", - # js_gov, - # package="samples/apps/logging/liblogging", - # nodes=infra.e2e_args.max_nodes(cr.args, f=0), - # initial_user_count=3, - # authenticate_session=True, - # ) - - # cr.add( - # "history", - # governance_history.run, - # package="samples/apps/logging/liblogging", - # nodes=infra.e2e_args.max_nodes(cr.args, f=0), - # ) + cr.add( + "js", + js_gov, + package="samples/apps/logging/liblogging", + nodes=infra.e2e_args.max_nodes(cr.args, f=0), + initial_user_count=3, + authenticate_session=True, + ) + + cr.add( + "history", + governance_history.run, + package="samples/apps/logging/liblogging", + nodes=infra.e2e_args.max_nodes(cr.args, f=0), + ) cr.run(2) From 9adfd978cf9d57dbff1e45a363dd238834b08831 Mon Sep 17 00:00:00 2001 From: Amaury Chamayou Date: Tue, 25 Oct 2022 07:56:35 +0000 Subject: [PATCH 13/23] missing endpoints --- doc/schemas/gov_openapi.json | 11 ++++++++++- src/node/rpc/member_frontend.h | 35 ++++++++++++++++++++++++++++++++-- 2 files changed, 43 insertions(+), 3 deletions(-) diff --git a/doc/schemas/gov_openapi.json b/doc/schemas/gov_openapi.json index 697922145644..80dae96f3aac 100644 --- a/doc/schemas/gov_openapi.json +++ b/doc/schemas/gov_openapi.json @@ -492,7 +492,7 @@ "info": { "description": "This API is used to submit and query proposals which affect CCF's public governance tables.", "title": "CCF Governance API", - "version": "2.10.0" + "version": "2.11.0" }, "openapi": "3.0.0", "paths": { @@ -549,6 +549,9 @@ "security": [ { "member_signature": [] + }, + { + "member_cose_sign1": [] } ], "x-ccf-forwarding": { @@ -1026,6 +1029,9 @@ "security": [ { "member_signature": [] + }, + { + "member_cose_sign1": [] } ], "x-ccf-forwarding": { @@ -1061,6 +1067,9 @@ "security": [ { "member_signature": [] + }, + { + "member_cose_sign1": [] } ], "x-ccf-forwarding": { diff --git a/src/node/rpc/member_frontend.h b/src/node/rpc/member_frontend.h index 322ac0ae02e4..35915e10b085 100644 --- a/src/node/rpc/member_frontend.h +++ b/src/node/rpc/member_frontend.h @@ -402,7 +402,7 @@ namespace ccf openapi_info.description = "This API is used to submit and query proposals which affect CCF's " "public governance tables."; - openapi_info.document_version = "2.10.0"; + openapi_info.document_version = "2.11.0"; } static std::optional get_caller_member_id( @@ -482,7 +482,9 @@ namespace ccf member_signature_auth_policy, member_cose_sign1_auth_policy}; const AuthnPolicies member_cert_or_sig = { - member_cert_auth_policy, member_signature_auth_policy}; + member_cert_auth_policy, + member_signature_auth_policy, + member_cose_sign1_auth_policy}; //! A member acknowledges state auto ack = [this](ccf::endpoints::EndpointContext& ctx) { @@ -497,6 +499,19 @@ namespace ccf return; } + if (cose_auth_id.has_value()) + { + if (!(cose_auth_id->protected_header.gov_msg_type.has_value() && + cose_auth_id->protected_header.gov_msg_type.value() == "ack")) + { + ctx.rpc_ctx->set_error( + HTTP_STATUS_BAD_REQUEST, + ccf::errors::InvalidResourceName, + "Unexpected message type"); + return; + } + } + auto params = nlohmann::json::parse(ctx.rpc_ctx->get_request_body()); auto mas = ctx.tx.rw(this->network.member_acks); @@ -617,6 +632,22 @@ namespace ccf return; } + if ( + const auto* cose_auth_id = + ctx.try_get_caller()) + { + if (!(cose_auth_id->protected_header.gov_msg_type.has_value() && + cose_auth_id->protected_header.gov_msg_type.value() == + "state_digest")) + { + ctx.rpc_ctx->set_error( + HTTP_STATUS_BAD_REQUEST, + ccf::errors::InvalidResourceName, + "Unexpected message type"); + return; + } + } + auto mas = ctx.tx.rw(this->network.member_acks); auto sig = ctx.tx.rw(this->network.signatures); auto ma = mas->get(member_id.value()); From 7f161b989e0a16bbad980c0286a65cd47dc78950 Mon Sep 17 00:00:00 2001 From: Amaury Chamayou Date: Tue, 25 Oct 2022 10:10:32 +0000 Subject: [PATCH 14/23] test positive ack flow --- src/node/rpc/member_frontend.h | 2 +- tests/governance.py | 112 +++++++++++++++++++++------------ 2 files changed, 73 insertions(+), 41 deletions(-) diff --git a/src/node/rpc/member_frontend.h b/src/node/rpc/member_frontend.h index 35915e10b085..113007e75574 100644 --- a/src/node/rpc/member_frontend.h +++ b/src/node/rpc/member_frontend.h @@ -512,7 +512,7 @@ namespace ccf } } - auto params = nlohmann::json::parse(ctx.rpc_ctx->get_request_body()); + auto params = nlohmann::json::parse(cose_auth_id.has_value() ? cose_auth_id->content : ctx.rpc_ctx->get_request_body()); auto mas = ctx.tx.rw(this->network.member_acks); const auto ma = mas->get(member_id.value()); diff --git a/tests/governance.py b/tests/governance.py index f7822a522881..42bbd8829850 100644 --- a/tests/governance.py +++ b/tests/governance.py @@ -628,6 +628,37 @@ def test_cose_auth(network, args): ) assert r.status_code == 401 +@reqs.description("Test COSE ack") +def test_cose_ack(network, args): + primary, _ = network.find_primary() + identity = network.identity("member0") + signed_statement = signing.create_cose_sign1( + b"", + open(identity.key, encoding="utf-8").read(), + open(identity.cert, encoding="utf-8").read(), + {"ccf.gov.msg.type": "state_digest"}, + ) + with primary.client() as c: + r = c.post( + "/gov/ack/update_state_digest", + body=signed_statement, + headers={"content-type": "application/cose"}, + ) + assert r.status_code == 200 + + signed_state_digest = signing.create_cose_sign1( + r.body.data(), + open(identity.key, encoding="utf-8").read(), + open(identity.cert, encoding="utf-8").read(), + {"ccf.gov.msg.type": "ack"}, + ) + with primary.client() as c: + r = c.post( + "/gov/ack", + body=signed_state_digest, + headers={"content-type": "application/cose"}, + ) + assert r.status_code == 204 def gov(args): for node in args.nodes: @@ -638,23 +669,24 @@ def gov(args): ) as network: network.start_and_open(args) network.consortium.set_authenticate_session(args.authenticate_session) - test_create_endpoint(network, args) - test_consensus_status(network, args) - test_member_data(network, args) - network = test_all_members(network, args) - test_quote(network, args) - test_user(network, args) - test_jinja_templates(network, args) - test_no_quote(network, args) - test_node_data(network, args) - test_ack_state_digest_update(network, args) - test_invalid_client_signature(network, args) - test_each_node_cert_renewal(network, args) - test_binding_proposal_to_service_identity(network, args) - test_all_nodes_cert_renewal(network, args) - test_service_cert_renewal(network, args) - test_service_cert_renewal_extended(network, args) - test_cose_auth(network, args) + # test_create_endpoint(network, args) + # test_consensus_status(network, args) + # test_member_data(network, args) + # network = test_all_members(network, args) + # test_quote(network, args) + # test_user(network, args) + # test_jinja_templates(network, args) + # test_no_quote(network, args) + # test_node_data(network, args) + # test_ack_state_digest_update(network, args) + # test_invalid_client_signature(network, args) + # test_each_node_cert_renewal(network, args) + # test_binding_proposal_to_service_identity(network, args) + # test_all_nodes_cert_renewal(network, args) + # test_service_cert_renewal(network, args) + # test_service_cert_renewal_extended(network, args) + # test_cose_auth(network, args) + test_cose_ack(network, args) def js_gov(args): @@ -689,14 +721,14 @@ def add(parser): infra.log_capture.COLORS = False - cr.add( - "session_auth", - gov, - package="samples/apps/logging/liblogging", - nodes=infra.e2e_args.max_nodes(cr.args, f=0), - initial_user_count=3, - authenticate_session=True, - ) + # cr.add( + # "session_auth", + # gov, + # package="samples/apps/logging/liblogging", + # nodes=infra.e2e_args.max_nodes(cr.args, f=0), + # initial_user_count=3, + # authenticate_session=True, + # ) cr.add( "session_noauth", @@ -707,20 +739,20 @@ def add(parser): authenticate_session=False, ) - cr.add( - "js", - js_gov, - package="samples/apps/logging/liblogging", - nodes=infra.e2e_args.max_nodes(cr.args, f=0), - initial_user_count=3, - authenticate_session=True, - ) - - cr.add( - "history", - governance_history.run, - package="samples/apps/logging/liblogging", - nodes=infra.e2e_args.max_nodes(cr.args, f=0), - ) + # cr.add( + # "js", + # js_gov, + # package="samples/apps/logging/liblogging", + # nodes=infra.e2e_args.max_nodes(cr.args, f=0), + # initial_user_count=3, + # authenticate_session=True, + # ) + + # cr.add( + # "history", + # governance_history.run, + # package="samples/apps/logging/liblogging", + # nodes=infra.e2e_args.max_nodes(cr.args, f=0), + # ) cr.run(2) From bf0197ca890c5e8781079402ab9a1e352081f2e0 Mon Sep 17 00:00:00 2001 From: Amaury Chamayou Date: Tue, 25 Oct 2022 10:33:30 +0000 Subject: [PATCH 15/23] fix --- src/node/rpc/member_frontend.h | 21 +++++++++++++-------- tests/governance.py | 4 +++- 2 files changed, 16 insertions(+), 9 deletions(-) diff --git a/src/node/rpc/member_frontend.h b/src/node/rpc/member_frontend.h index 113007e75574..0c38e35fbfe0 100644 --- a/src/node/rpc/member_frontend.h +++ b/src/node/rpc/member_frontend.h @@ -467,7 +467,7 @@ namespace ccf ctx.rpc_ctx->set_error( HTTP_STATUS_FORBIDDEN, ccf::errors::AuthorizationFailed, - "Member is not active."); + fmt::format("Member {} is not active.", member_id.value())); return false; } @@ -512,7 +512,9 @@ namespace ccf } } - auto params = nlohmann::json::parse(cose_auth_id.has_value() ? cose_auth_id->content : ctx.rpc_ctx->get_request_body()); + auto params = nlohmann::json::parse( + cose_auth_id.has_value() ? cose_auth_id->content : + ctx.rpc_ctx->get_request_body()); auto mas = ctx.tx.rw(this->network.member_acks); const auto ma = mas->get(member_id.value()); @@ -751,8 +753,6 @@ namespace ccf auto submit_recovery_share = [this]( ccf::endpoints::EndpointContext& ctx) { - auto params = nlohmann::json::parse(ctx.rpc_ctx->get_request_body()); - // Only active members can submit their shares for recovery const auto member_id = get_caller_member_id(ctx); if (!member_id.has_value()) @@ -772,9 +772,8 @@ namespace ccf return; } - if ( - const auto* cose_auth_id = - ctx.try_get_caller()) + const auto* cose_auth_id = ctx.try_get_caller(); + if (cose_auth_id) { if (!(cose_auth_id->protected_header.gov_msg_type.has_value() && cose_auth_id->protected_header.gov_msg_type.value() == @@ -788,6 +787,10 @@ namespace ccf } } + auto params = nlohmann::json::parse( + cose_auth_id ? cose_auth_id->content : + ctx.rpc_ctx->get_request_body()); + GenesisGenerator g(this->network, ctx.tx); if ( g.get_service_status() != ServiceStatus::WAITING_FOR_RECOVERY_SHARES) @@ -1476,7 +1479,9 @@ namespace ccf } // Validate vote - auto params = nlohmann::json::parse(ctx.rpc_ctx->get_request_body()); + auto params = nlohmann::json::parse( + cose_auth_id.has_value() ? cose_auth_id->content : + ctx.rpc_ctx->get_request_body()); { js::Runtime rt(&ctx.tx); diff --git a/tests/governance.py b/tests/governance.py index 42bbd8829850..095743c8be9c 100644 --- a/tests/governance.py +++ b/tests/governance.py @@ -628,6 +628,7 @@ def test_cose_auth(network, args): ) assert r.status_code == 401 + @reqs.description("Test COSE ack") def test_cose_ack(network, args): primary, _ = network.find_primary() @@ -645,7 +646,7 @@ def test_cose_ack(network, args): headers={"content-type": "application/cose"}, ) assert r.status_code == 200 - + signed_state_digest = signing.create_cose_sign1( r.body.data(), open(identity.key, encoding="utf-8").read(), @@ -660,6 +661,7 @@ def test_cose_ack(network, args): ) assert r.status_code == 204 + def gov(args): for node in args.nodes: node.rpc_interfaces.update(infra.interfaces.make_secondary_interface()) From dbbfb425128e77a84336e54ee82280eb2f723462 Mon Sep 17 00:00:00 2001 From: Amaury Chamayou Date: Tue, 25 Oct 2022 16:36:55 +0000 Subject: [PATCH 16/23] proposal --- src/node/rpc/member_frontend.h | 16 ++-- tests/governance.py | 153 ++++++++++++++++++++++++--------- 2 files changed, 123 insertions(+), 46 deletions(-) diff --git a/src/node/rpc/member_frontend.h b/src/node/rpc/member_frontend.h index 0c38e35fbfe0..f9108b5d424d 100644 --- a/src/node/rpc/member_frontend.h +++ b/src/node/rpc/member_frontend.h @@ -772,7 +772,8 @@ namespace ccf return; } - const auto* cose_auth_id = ctx.try_get_caller(); + const auto* cose_auth_id = + ctx.try_get_caller(); if (cose_auth_id) { if (!(cose_auth_id->protected_header.gov_msg_type.has_value() && @@ -1030,9 +1031,12 @@ namespace ccf auto validate_func = context.function( validate_script, "validate", "public:ccf.gov.constitution[0]"); - auto body = - reinterpret_cast(ctx.rpc_ctx->get_request_body().data()); - auto body_len = ctx.rpc_ctx->get_request_body().size(); + const std::span proposal_body = + cose_auth_id.has_value() ? cose_auth_id->content : + ctx.rpc_ctx->get_request_body(); + + auto body = reinterpret_cast(proposal_body.data()); + auto body_len = proposal_body.size(); auto proposal = context.new_string_len(body, body_len); auto val = context.call(validate_func, {proposal}); @@ -1089,7 +1093,7 @@ namespace ccf "Proposal ID collision."); return; } - pm->put(proposal_id, ctx.rpc_ctx->get_request_body()); + pm->put(proposal_id, {proposal_body.begin(), proposal_body.end()}); auto pi = ctx.tx.rw(jsgov::Tables::PROPOSALS_INFO); @@ -1109,7 +1113,7 @@ namespace ccf auto rv = resolve_proposal( ctx.tx, proposal_id, - ctx.rpc_ctx->get_request_body(), + {proposal_body.begin(), proposal_body.end()}, constitution.value()); if (rv.state == ProposalState::FAILED) diff --git a/tests/governance.py b/tests/governance.py index 095743c8be9c..bc5aac1f8dec 100644 --- a/tests/governance.py +++ b/tests/governance.py @@ -662,6 +662,78 @@ def test_cose_ack(network, args): assert r.status_code == 204 +@reqs.description("Test COSE proposal") +def test_cose_proposal(network, args): + primary, _ = network.find_primary() + identity = network.identity("member0") + other_identity = network.identity("member1") + + new_user_local_id = "alice" + new_user = network.create_user(new_user_local_id, args.participants_curve) + template_loader = jinja2.ChoiceLoader( + [ + jinja2.FileSystemLoader(args.jinja_templates_path), + jinja2.FileSystemLoader(os.path.dirname(new_user.cert_path)), + ] + ) + template_env = jinja2.Environment( + loader=template_loader, undefined=jinja2.StrictUndefined + ) + proposal_template = template_env.get_template("set_user_proposal.json.jinja") + proposal_body = proposal_template.render(cert=os.path.basename(new_user.cert_path)) + + signed_statement = signing.create_cose_sign1( + proposal_body.encode(), + open(identity.key, encoding="utf-8").read(), + open(identity.cert, encoding="utf-8").read(), + {"ccf.gov.msg.type": "proposal"}, + ) + with primary.client() as c: + r = c.post( + "/gov/proposals", + body=signed_statement, + headers={"content-type": "application/cose"}, + ) + assert r.status_code == 200 + proposal_id = r.body.json()["proposal_id"] + + def vote(body): + return {"ballot": f"export function vote (proposal, proposer_id) {{ {body} }}"} + + ballot_yes = vote("return true") + + signed_ballot0 = signing.create_cose_sign1( + json.dumps(ballot_yes).encode(), + open(identity.key, encoding="utf-8").read(), + open(identity.cert, encoding="utf-8").read(), + {"ccf.gov.msg.type": "ballot", "ccf.gov.msg.proposal_id": proposal_id}, + ) + + with primary.client() as c: + r = c.post( + f"/gov/proposals/{proposal_id}/ballots", + body=signed_ballot0, + headers={"content-type": "application/cose"}, + ) + assert r.status_code == 200 + + signed_ballot1 = signing.create_cose_sign1( + json.dumps(ballot_yes).encode(), + open(other_identity.key, encoding="utf-8").read(), + open(other_identity.cert, encoding="utf-8").read(), + {"ccf.gov.msg.type": "ballot", "ccf.gov.msg.proposal_id": proposal_id}, + ) + + with primary.client() as c: + r = c.post( + f"/gov/proposals/{proposal_id}/ballots", + body=signed_ballot1, + headers={"content-type": "application/cose"}, + ) + assert r.status_code == 200 + assert r.body.json()["state"] == "Accepted" + + def gov(args): for node in args.nodes: node.rpc_interfaces.update(infra.interfaces.make_secondary_interface()) @@ -671,24 +743,25 @@ def gov(args): ) as network: network.start_and_open(args) network.consortium.set_authenticate_session(args.authenticate_session) - # test_create_endpoint(network, args) - # test_consensus_status(network, args) - # test_member_data(network, args) - # network = test_all_members(network, args) - # test_quote(network, args) - # test_user(network, args) - # test_jinja_templates(network, args) - # test_no_quote(network, args) - # test_node_data(network, args) - # test_ack_state_digest_update(network, args) - # test_invalid_client_signature(network, args) - # test_each_node_cert_renewal(network, args) - # test_binding_proposal_to_service_identity(network, args) - # test_all_nodes_cert_renewal(network, args) - # test_service_cert_renewal(network, args) - # test_service_cert_renewal_extended(network, args) - # test_cose_auth(network, args) + test_create_endpoint(network, args) + test_consensus_status(network, args) + test_member_data(network, args) + network = test_all_members(network, args) + test_quote(network, args) + test_user(network, args) + test_jinja_templates(network, args) + test_no_quote(network, args) + test_node_data(network, args) + test_ack_state_digest_update(network, args) + test_invalid_client_signature(network, args) + test_each_node_cert_renewal(network, args) + test_binding_proposal_to_service_identity(network, args) + test_all_nodes_cert_renewal(network, args) + test_service_cert_renewal(network, args) + test_service_cert_renewal_extended(network, args) + test_cose_auth(network, args) test_cose_ack(network, args) + test_cose_proposal(network, args) def js_gov(args): @@ -723,14 +796,14 @@ def add(parser): infra.log_capture.COLORS = False - # cr.add( - # "session_auth", - # gov, - # package="samples/apps/logging/liblogging", - # nodes=infra.e2e_args.max_nodes(cr.args, f=0), - # initial_user_count=3, - # authenticate_session=True, - # ) + cr.add( + "session_auth", + gov, + package="samples/apps/logging/liblogging", + nodes=infra.e2e_args.max_nodes(cr.args, f=0), + initial_user_count=3, + authenticate_session=True, + ) cr.add( "session_noauth", @@ -741,20 +814,20 @@ def add(parser): authenticate_session=False, ) - # cr.add( - # "js", - # js_gov, - # package="samples/apps/logging/liblogging", - # nodes=infra.e2e_args.max_nodes(cr.args, f=0), - # initial_user_count=3, - # authenticate_session=True, - # ) - - # cr.add( - # "history", - # governance_history.run, - # package="samples/apps/logging/liblogging", - # nodes=infra.e2e_args.max_nodes(cr.args, f=0), - # ) + cr.add( + "js", + js_gov, + package="samples/apps/logging/liblogging", + nodes=infra.e2e_args.max_nodes(cr.args, f=0), + initial_user_count=3, + authenticate_session=True, + ) + + cr.add( + "history", + governance_history.run, + package="samples/apps/logging/liblogging", + nodes=infra.e2e_args.max_nodes(cr.args, f=0), + ) cr.run(2) From 65b33615fd2e073bac2be84b9f0b64785f5cc6f1 Mon Sep 17 00:00:00 2001 From: Amaury Chamayou Date: Tue, 25 Oct 2022 16:56:32 +0000 Subject: [PATCH 17/23] changelog --- CHANGELOG.md | 4 ++++ tests/governance.py | 51 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 55 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 57b7bbcefb0e..44254fbfc1f0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0. ## Unreleased +### Experimental + +- Governance endpoints now support [COSE Sign1](https://www.rfc-editor.org/rfc/rfc8152#page-18) input, as well as signed HTTP requests (#4392). + ### Removed - The functions `starts_with`, `ends_with`, `remove_prefix`, and `remove_suffix`, and the type `remove_cvref` have been removed from `nonstd::`. The C++20 equivalents should be used instead. diff --git a/tests/governance.py b/tests/governance.py index bc5aac1f8dec..266daf63f9b9 100644 --- a/tests/governance.py +++ b/tests/governance.py @@ -734,6 +734,56 @@ def vote(body): assert r.body.json()["state"] == "Accepted" +@reqs.description("Test COSE withdraw") +def test_cose_withdrawal(network, args): + primary, _ = network.find_primary() + identity = network.identity("member0") + + new_user_local_id = "alice" + new_user = network.create_user(new_user_local_id, args.participants_curve) + template_loader = jinja2.ChoiceLoader( + [ + jinja2.FileSystemLoader(args.jinja_templates_path), + jinja2.FileSystemLoader(os.path.dirname(new_user.cert_path)), + ] + ) + template_env = jinja2.Environment( + loader=template_loader, undefined=jinja2.StrictUndefined + ) + proposal_template = template_env.get_template("set_user_proposal.json.jinja") + proposal_body = proposal_template.render(cert=os.path.basename(new_user.cert_path)) + + signed_statement = signing.create_cose_sign1( + proposal_body.encode(), + open(identity.key, encoding="utf-8").read(), + open(identity.cert, encoding="utf-8").read(), + {"ccf.gov.msg.type": "proposal"}, + ) + with primary.client() as c: + r = c.post( + "/gov/proposals", + body=signed_statement, + headers={"content-type": "application/cose"}, + ) + assert r.status_code == 200 + proposal_id = r.body.json()["proposal_id"] + + signed_withdrawal = signing.create_cose_sign1( + b"", + open(identity.key, encoding="utf-8").read(), + open(identity.cert, encoding="utf-8").read(), + {"ccf.gov.msg.type": "withdrawal", "ccf.gov.msg.proposal_id": proposal_id}, + ) + + with primary.client() as c: + r = c.post( + f"/gov/proposals/{proposal_id}/withdraw", + body=signed_withdrawal, + headers={"content-type": "application/cose"}, + ) + assert r.status_code == 200 + + def gov(args): for node in args.nodes: node.rpc_interfaces.update(infra.interfaces.make_secondary_interface()) @@ -762,6 +812,7 @@ def gov(args): test_cose_auth(network, args) test_cose_ack(network, args) test_cose_proposal(network, args) + test_cose_withdrawal(network, args) def js_gov(args): From 4766e4537ffb0cf3eb9f1340beebc586dd383de7 Mon Sep 17 00:00:00 2001 From: Amaury Chamayou Date: Tue, 25 Oct 2022 20:03:09 +0000 Subject: [PATCH 18/23] more tests --- tests/governance.py | 18 +++- tests/infra/clients.py | 43 ++++++++- tests/infra/member.py | 2 + tests/infra/node.py | 7 +- tests/infra/signing.py | 198 +++++++++++++++++++++++++++++++++++++++++ 5 files changed, 260 insertions(+), 8 deletions(-) create mode 100644 tests/infra/signing.py diff --git a/tests/governance.py b/tests/governance.py index 266daf63f9b9..390f5197e81f 100644 --- a/tests/governance.py +++ b/tests/governance.py @@ -809,10 +809,11 @@ def gov(args): test_all_nodes_cert_renewal(network, args) test_service_cert_renewal(network, args) test_service_cert_renewal_extended(network, args) - test_cose_auth(network, args) - test_cose_ack(network, args) - test_cose_proposal(network, args) - test_cose_withdrawal(network, args) + if args.authenticate_session != "COSE": + test_cose_auth(network, args) + test_cose_ack(network, args) + test_cose_proposal(network, args) + test_cose_withdrawal(network, args) def js_gov(args): @@ -847,6 +848,15 @@ def add(parser): infra.log_capture.COLORS = False + cr.add( + "session_coseauth", + gov, + package="samples/apps/logging/liblogging", + nodes=infra.e2e_args.max_nodes(cr.args, f=0), + initial_user_count=3, + authenticate_session="COSE", + ) + cr.add( "session_auth", gov, diff --git a/tests/infra/clients.py b/tests/infra/clients.py index c48c48bab3de..2c8168002981 100644 --- a/tests/infra/clients.py +++ b/tests/infra/clients.py @@ -28,6 +28,7 @@ import infra.commit from infra.log_capture import flush_info +import infra.signing class HttpSig(httpx.Auth): @@ -87,6 +88,7 @@ def truncate(string: str, max_len: int = 256): CONTENT_TYPE_TEXT = "text/plain" CONTENT_TYPE_JSON = "application/json" CONTENT_TYPE_BINARY = "application/octet-stream" +CONTENT_TYPE_COSE = "application/cose" @dataclass @@ -304,6 +306,7 @@ def __init__( ca=None, session_auth=None, signing_auth=None, + cose_signing_auth=None, common_headers=None, **kwargs, ): @@ -425,6 +428,7 @@ def __init__( ca: str, session_auth: Optional[Identity] = None, signing_auth: Optional[Identity] = None, + cose_signing_auth: Optional[Identity] = None, common_headers: Optional[dict] = None, **kwargs, ): @@ -432,6 +436,7 @@ def __init__( self.ca = ca self.session_auth = session_auth self.signing_auth = signing_auth + self.cose_signing_auth = cose_signing_auth self.common_headers = common_headers self.key_id = None cert = None @@ -442,8 +447,9 @@ def __init__( self.protocol = kwargs.get("protocol") kwargs.pop("protocol") self.session = httpx.Client(verify=self.ca, cert=cert, **kwargs) - if self.signing_auth: - with open(self.signing_auth.cert, encoding="utf-8") as cert_file: + sig_auth = signing_auth or cose_signing_auth + if sig_auth: + with open(sig_auth.cert, encoding="utf-8") as cert_file: self.key_id = ( x509.load_pem_x509_certificate( cert_file.read().encode(), default_backend() @@ -503,6 +509,29 @@ def request( if not "content-type" in request.headers and len(request.body) > 0: extra_headers["content-type"] = content_type + if self.cose_signing_auth is not None: + key = open(self.cose_signing_auth.key, encoding="utf-8").read() + cert = open(self.cose_signing_auth.cert, encoding="utf-8").read() + phdr = {} + if request.path.endswith("gov/ack/update_state_digest"): + phdr["ccf.gov.msg.type"] = "state_digest" + elif request.path.endswith("gov/ack"): + phdr["ccf.gov.msg.type"] = "ack" + elif request.path.endswith("gov/proposals"): + phdr["ccf.gov.msg.type"] = "proposal" + elif request.path.endswith("/ballots"): + pid = request.path.split("/")[-2] + phdr["ccf.gov.msg.type"] = "ballot" + phdr["ccf.gov.msg.proposal_id"] = pid + elif request.path.endswith("/withdraw"): + pid = request.path.split("/")[-2] + phdr["ccf.gov.msg.type"] = "withdrawal" + phdr["ccf.gov.msg.proposal_id"] = pid + request_body = infra.signing.create_cose_sign1( + request_body or b"", key, cert, phdr + ) + extra_headers["content-type"] = CONTENT_TYPE_COSE + try: response = self.session.request( request.http_verb, @@ -571,6 +600,7 @@ def __init__( ca: str, session_auth: Optional[Identity] = None, signing_auth: Optional[Identity] = None, + cose_signing_auth: Optional[Identity] = None, connection_timeout: int = DEFAULT_CONNECTION_TIMEOUT_SEC, description: Optional[str] = None, curl: bool = False, @@ -584,6 +614,7 @@ def __init__( self.is_connected = False self.auth = bool(session_auth) self.sign = bool(signing_auth) + self.cose = bool(cose_signing_auth) impl_type = CCFClient.default_impl_type @@ -591,7 +622,13 @@ def __init__( impl_type = CurlClient self.client_impl = impl_type( - self.hostname, ca, session_auth, signing_auth, common_headers, **kwargs + self.hostname, + ca, + session_auth, + signing_auth, + cose_signing_auth, + common_headers, + **kwargs, ) def _response(self, response: Response) -> Response: diff --git a/tests/infra/member.py b/tests/infra/member.py index 67d4da8368e4..08696b8db163 100644 --- a/tests/infra/member.py +++ b/tests/infra/member.py @@ -112,6 +112,8 @@ def __init__( LOG.info(f"Member {self.local_id} created: {self.service_id}") def auth(self, write=False): + if self.authenticate_session == "COSE": + return (None, None, self.local_id) if self.authenticate_session: if write: return (self.local_id, self.local_id) diff --git a/tests/infra/node.py b/tests/infra/node.py index b7d4deb7fe78..f271674ff504 100644 --- a/tests/infra/node.py +++ b/tests/infra/node.py @@ -552,6 +552,9 @@ def session_auth(self, name=None): def signing_auth(self, name=None): return {"signing_auth": self.identity(name)} + def cose_signing_auth(self, name=None): + return {"cose_signing_auth": self.identity(name)} + def get_public_rpc_host( self, interface_name=infra.interfaces.PRIMARY_RPC_INTERFACE ): @@ -597,6 +600,7 @@ def client( self, identity=None, signing_identity=None, + cose_signing_identity=None, interface_name=infra.interfaces.PRIMARY_RPC_INTERFACE, verify_ca=True, **kwargs, @@ -627,9 +631,10 @@ def client( akwargs["http2"] = True akwargs.update(self.session_auth(identity)) akwargs.update(self.signing_auth(signing_identity)) + akwargs.update(self.cose_signing_auth(cose_signing_identity)) akwargs[ "description" - ] = f"[{self.local_node_id}|{identity or ''}|{signing_identity or ''}]" + ] = f"[{self.local_node_id}|{identity or ''}|{signing_identity or ''}|{cose_signing_identity or ''}]" akwargs.update(kwargs) if self.curl: diff --git a/tests/infra/signing.py b/tests/infra/signing.py new file mode 100644 index 000000000000..c6c887d73434 --- /dev/null +++ b/tests/infra/signing.py @@ -0,0 +1,198 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the Apache 2.0 License. + +from typing import Optional + +import cbor2 +import cose.headers +from cose.keys.ec2 import EC2Key +from cose.keys.curves import P256, P384, P521 +from cose.keys.keyparam import EC2KpCurve, EC2KpX, EC2KpY, EC2KpD +from cose.messages import Sign1Message +from cryptography.hazmat.primitives.asymmetric import ec +from cryptography.hazmat.primitives.asymmetric.ec import ( + EllipticCurvePrivateKey, + EllipticCurvePublicKey, +) +from cryptography.hazmat.backends import default_backend +from cryptography.x509 import load_pem_x509_certificate +from cryptography.hazmat.primitives import hashes +from cryptography.hazmat.primitives.serialization import load_pem_private_key + +Pem = str + + +def from_cryptography_eckey_obj(ext_key) -> EC2Key: + """ + Returns an initialized COSE Key object of type EC2Key. + :param ext_key: Python cryptography key. + :return: an initialized EC key + """ + if hasattr(ext_key, "private_numbers"): + priv_nums = ext_key.private_numbers() + pub_nums = priv_nums.public_numbers + else: + priv_nums = None + pub_nums = ext_key.public_numbers() + + if pub_nums.curve.name == "secp256r1": + curve = P256 + elif pub_nums.curve.name == "secp384r1": + curve = P384 + elif pub_nums.curve.name == "secp521r1": + curve = P521 + else: + raise NotImplementedError("unsupported curve") + + cose_key = {} + if pub_nums: + cose_key.update( + { + EC2KpCurve: curve, + EC2KpX: pub_nums.x.to_bytes(curve.size, "big"), + EC2KpY: pub_nums.y.to_bytes(curve.size, "big"), + } + ) + if priv_nums: + cose_key.update( + { + EC2KpD: priv_nums.private_value.to_bytes(curve.size, "big"), + } + ) + return EC2Key.from_dict(cose_key) + + +def default_algorithm_for_key(key) -> str: + """ + Get the default algorithm for a given key, based on its + type and parameters. + """ + if isinstance(key, EllipticCurvePublicKey): + if isinstance(key.curve, ec.SECP256R1): + return "ES256" + elif isinstance(key.curve, ec.SECP384R1): + return "ES384" + elif isinstance(key.curve, ec.SECP521R1): + return "ES512" + else: + raise NotImplementedError("unsupported curve") + else: + raise NotImplementedError("unsupported key type") + + +def get_priv_key_type(priv_pem: str) -> str: + key = load_pem_private_key(priv_pem.encode("ascii"), None, default_backend()) + if isinstance(key, EllipticCurvePrivateKey): + return "ec" + raise NotImplementedError("unsupported key type") + + +def cert_fingerprint(cert_pem: Pem): + cert = load_pem_x509_certificate(cert_pem.encode("ascii"), default_backend()) + return cert.fingerprint(hashes.SHA256()).hex().encode("utf-8") + + +def create_cose_sign1( + payload: bytes, + key_priv_pem: Pem, + cert_pem: Pem, + additional_headers: Optional[dict] = None, +) -> bytes: + key_type = get_priv_key_type(key_priv_pem) + + cert = load_pem_x509_certificate(cert_pem.encode("ascii"), default_backend()) + alg = default_algorithm_for_key(cert.public_key()) + kid = cert_fingerprint(cert_pem) + + headers = {cose.headers.Algorithm: alg, cose.headers.KID: kid} + headers.update(additional_headers or {}) + msg = Sign1Message(phdr=headers, payload=payload) + + key = load_pem_private_key(key_priv_pem.encode("ascii"), None, default_backend()) + if key_type == "ec": + cose_key = from_cryptography_eckey_obj(key) + else: + raise NotImplementedError("unsupported key type") + msg.key = cose_key + + return msg.encode() + + +def get_cert_key_type(cert_pem: str) -> str: + cert = load_pem_x509_certificate(cert_pem.encode("ascii"), default_backend()) + if isinstance(cert.public_key(), EllipticCurvePublicKey): + return "ec" + raise NotImplementedError("unsupported key type") + + +def verify_cose_sign1(buf: bytes, cert_pem: str): + key_type = get_cert_key_type(cert_pem) + cert = load_pem_x509_certificate(cert_pem.encode("ascii"), default_backend()) + key = cert.public_key() + if key_type == "ec": + cose_key = from_cryptography_eckey_obj(key) + else: + raise NotImplementedError("unsupported key type") + msg = Sign1Message.decode(buf) + msg.key = cose_key + if not msg.verify_signature(): + raise ValueError("signature is invalid") + return msg + + +def detach_content(msg: bytes): + m = cbor2.loads(msg) + content = m.value[2] + m.value[2] = None + return content, cbor2.dumps(m) + + +def attach_content(content, detached_envelope): + m = cbor2.loads(detached_envelope) + m.value[2] = content + return cbor2.dumps(m) + + +PRIV = """-----BEGIN EC PARAMETERS----- +BgUrgQQAIg== +-----END EC PARAMETERS----- +-----BEGIN EC PRIVATE KEY----- +MIGkAgEBBDDMwIszb3ZmKpeHq/vPoz6qnxheI89T2IZpKFQHJwQrvuaFFLDUKK9Z +jKRMshAeALagBwYFK4EEACKhZANiAAQ38JreTF2uKVaTKBd7fAkIy2bg5U6T0O+H +wcxJOLgqK+fwidnVlPG+GQUwIj6ik7Xp/0Ig7RVSAyAjcpYWL4dHU5gJ/g9PruHz +cnmFtP88dARPH2EKy0n/iGh9yXD3bXw= +-----END EC PRIVATE KEY----- +""" + +PUB = """-----BEGIN CERTIFICATE----- +MIIBtjCCATygAwIBAgIUJCUauYlNsJ76zOUomey4cF7F+pUwCgYIKoZIzj0EAwMw +EjEQMA4GA1UEAwwHbWVtYmVyMDAeFw0yMjA5MDYxMzQ2NDlaFw0yMzA5MDYxMzQ2 +NDlaMBIxEDAOBgNVBAMMB21lbWJlcjAwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAAQ3 +8JreTF2uKVaTKBd7fAkIy2bg5U6T0O+HwcxJOLgqK+fwidnVlPG+GQUwIj6ik7Xp +/0Ig7RVSAyAjcpYWL4dHU5gJ/g9PruHzcnmFtP88dARPH2EKy0n/iGh9yXD3bXyj +UzBRMB0GA1UdDgQWBBTpme2NGI1y3OY8XYT5XwcuuvG55jAfBgNVHSMEGDAWgBTp +me2NGI1y3OY8XYT5XwcuuvG55jAPBgNVHRMBAf8EBTADAQH/MAoGCCqGSM49BAMD +A2gAMGUCMDg1QddcE5YFrcHmFvyXW2s7LaV0NYx24lwImrgWXQTOv7iNXAfrogzP +CQxyHqkSxgIxANmkmLCojf5NCvwxI5tf37i6zGQ0c9zR0P9b4FtcznEtrbzmXfdJ +b2H04E57XZmVdg== +-----END CERTIFICATE----- +""" + +if __name__ == "__main__": + signed_statement = create_cose_sign1( + b"governance js here", PRIV, PUB, {"ccf_governance_action": "proposal"} + ) + msg = verify_cose_sign1(signed_statement, PUB) + assert msg.phdr[cose.headers.KID] == cert_fingerprint(PUB), ( + msg.phdr[cose.headers.KID], + cert_fingerprint(PUB), + ) + content, detached_envelope = detach_content(signed_statement) + signed_statement = attach_content(content, detached_envelope) + msg = verify_cose_sign1(signed_statement, PUB) + signed_statement = create_cose_sign1( + b"governance js here", + PRIV, + PUB, + {"ccf_governance_action": "proposal", "ccf_governance_proposal_id": "12345"}, + ) From ad88e1e2795aebef1ea666f899f2a68b733e5986 Mon Sep 17 00:00:00 2001 From: Amaury Chamayou Date: Tue, 25 Oct 2022 21:02:25 +0000 Subject: [PATCH 19/23] suite --- CMakeLists.txt | 1 + tests/e2e_suite.py | 5 +++++ tests/governance.py | 4 ++++ tests/suite/test_suite.py | 2 ++ 4 files changed, 12 insertions(+) diff --git a/CMakeLists.txt b/CMakeLists.txt index e0bd157655bc..1cd742193490 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -605,6 +605,7 @@ if(BUILD_TESTS) 200 --test-suite all + --jinja-templates-path ${CMAKE_SOURCE_DIR}/samples/templates ) add_e2e_test( diff --git a/tests/e2e_suite.py b/tests/e2e_suite.py index d5f1f26c9648..84c2ca743091 100644 --- a/tests/e2e_suite.py +++ b/tests/e2e_suite.py @@ -217,6 +217,11 @@ def add(parser): help="If set, tests execution is skipped", default=False, ) + parser.add_argument( + "--jinja-templates-path", + help="Path to directory containing sample Jinja templates", + required=True, + ) args = infra.e2e_args.cli_args(add) args.package = "samples/apps/logging/liblogging" diff --git a/tests/governance.py b/tests/governance.py index 390f5197e81f..9a3eae679db3 100644 --- a/tests/governance.py +++ b/tests/governance.py @@ -627,6 +627,7 @@ def test_cose_auth(network, args): headers={"content-type": "application/cose"}, ) assert r.status_code == 401 + return network @reqs.description("Test COSE ack") @@ -660,6 +661,7 @@ def test_cose_ack(network, args): headers={"content-type": "application/cose"}, ) assert r.status_code == 204 + return network @reqs.description("Test COSE proposal") @@ -732,6 +734,7 @@ def vote(body): ) assert r.status_code == 200 assert r.body.json()["state"] == "Accepted" + return network @reqs.description("Test COSE withdraw") @@ -782,6 +785,7 @@ def test_cose_withdrawal(network, args): headers={"content-type": "application/cose"}, ) assert r.status_code == 200 + return network def gov(args): diff --git a/tests/suite/test_suite.py b/tests/suite/test_suite.py index e45bcf660769..be0c19c4ace3 100644 --- a/tests/suite/test_suite.py +++ b/tests/suite/test_suite.py @@ -123,6 +123,8 @@ # governance governance.test_each_node_cert_renewal, governance.test_service_cert_renewal, + governance.test_cose_ack, + governance.test_cose_withdrawal, # e2e_operations: e2e_operations.test_forced_ledger_chunk, e2e_operations.test_forced_snapshot, From 9c2b4356f22a95b13b83176d981c08b3376fcaf8 Mon Sep 17 00:00:00 2001 From: Amaury Chamayou Date: Tue, 25 Oct 2022 21:18:29 +0000 Subject: [PATCH 20/23] . --- CMakeLists.txt | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 1cd742193490..e8481204b7b3 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -579,8 +579,15 @@ if(BUILD_TESTS) PYTHON_SCRIPT ${CMAKE_SOURCE_DIR}/tests/e2e_suite.py CONSENSUS cft LABEL suite - ADDITIONAL_ARGS --test-duration 150 --test-suite rekey_recovery - --test-suite membership_recovery + ADDITIONAL_ARGS + --test-duration + 150 + --test-suite + rekey_recovery + --test-suite + membership_recovery + --jinja-templates-path + ${CMAKE_SOURCE_DIR}/samples/templates ) add_e2e_test( @@ -588,7 +595,9 @@ if(BUILD_TESTS) PYTHON_SCRIPT ${CMAKE_SOURCE_DIR}/tests/e2e_suite.py CONSENSUS cft LABEL suite - ADDITIONAL_ARGS --test-duration 200 --test-suite reconfiguration + ADDITIONAL_ARGS + --test-duration 200 --test-suite reconfiguration --jinja-templates-path + ${CMAKE_SOURCE_DIR}/samples/templates ) add_e2e_test( @@ -605,7 +614,8 @@ if(BUILD_TESTS) 200 --test-suite all - --jinja-templates-path ${CMAKE_SOURCE_DIR}/samples/templates + --jinja-templates-path + ${CMAKE_SOURCE_DIR}/samples/templates ) add_e2e_test( From 1dc4cdc1c4ff5ffaafae025d700f7c9e9d28ca95 Mon Sep 17 00:00:00 2001 From: Amaury Chamayou Date: Tue, 25 Oct 2022 22:14:56 +0000 Subject: [PATCH 21/23] doc --- doc/audit/builtin_maps.rst | 11 ++++++++++- tests/governance_history.py | 3 +++ 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/doc/audit/builtin_maps.rst b/doc/audit/builtin_maps.rst index ab602bbce5f2..59162d3a6186 100644 --- a/doc/audit/builtin_maps.rst +++ b/doc/audit/builtin_maps.rst @@ -365,7 +365,7 @@ Service constitution: JavaScript module, exporting ``validate()``, ``resolve()`` ``history`` ~~~~~~~~~~~ -Governance history of the service, captures all governance requests submitted by members. +Governance history of the service, captures signed governance requests submitted by members. **Key** Member ID: SHA-256 fingerprint of the member certificate, represented as a hex-encoded string. @@ -373,6 +373,15 @@ Governance history of the service, captures all governance requests submitted by See :cpp:struct:`ccf::SignedReq` +``cose_history`` +~~~~~~~~~~~~~~~~ + +Governance history of the service, captures all COSE Sign 1 governance requests submitted by members. + +**Key** Member ID: SHA-256 fingerprint of the member certificate, represented as a hex-encoded string. + +**Value** COSE Sign1 + ``public:ccf.internal.`` ------------------------ diff --git a/tests/governance_history.py b/tests/governance_history.py index 43cda9c0eeda..94f02a602457 100644 --- a/tests/governance_history.py +++ b/tests/governance_history.py @@ -16,6 +16,7 @@ import suite.test_requirements as reqs import ccf.read_ledger import infra.logging_app as app +import governance def check_operations(ledger, operations): @@ -205,6 +206,8 @@ def run(args): (new_member_proposal.proposal_id, member.service_id, "withdraw") ) + governance.test_cose_proposal(network, args) + # Force ledger flush of all transactions so far network.get_latest_ledger_public_state() ledger = ccf.ledger.Ledger(ledger_directories) From cef6a1e6356103efa34570836700a48a6cff2e10 Mon Sep 17 00:00:00 2001 From: Amaury Chamayou Date: Wed, 26 Oct 2022 14:39:17 +0100 Subject: [PATCH 22/23] Update doc/schemas/gov_openapi.json Co-authored-by: Dominic Ayre --- doc/schemas/gov_openapi.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/schemas/gov_openapi.json b/doc/schemas/gov_openapi.json index 80dae96f3aac..bda0754e2d5c 100644 --- a/doc/schemas/gov_openapi.json +++ b/doc/schemas/gov_openapi.json @@ -464,7 +464,7 @@ }, "securitySchemes": { "member_cose_sign1": { - "description": "Request payload must be a COSE Sign1 document, with expected protected headers.Signer must be a member identity registered with this service.", + "description": "Request payload must be a COSE Sign1 document, with expected protected headers. Signer must be a member identity registered with this service.", "scheme": "cose_sign1", "type": "http" }, From e761722b93b4d61f4b249caf98ba03bb0ef805b5 Mon Sep 17 00:00:00 2001 From: Amaury Chamayou Date: Wed, 26 Oct 2022 15:03:19 +0000 Subject: [PATCH 23/23] schema, schema... --- doc/schemas/app_openapi.json | 2 +- doc/schemas/gov_openapi.json | 2 +- src/endpoints/authentication/cose_auth.cpp | 2 +- src/node/rpc/member_frontend.h | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/doc/schemas/app_openapi.json b/doc/schemas/app_openapi.json index c2f0a9650203..81f0ace0d533 100644 --- a/doc/schemas/app_openapi.json +++ b/doc/schemas/app_openapi.json @@ -306,7 +306,7 @@ "type": "http" }, "member_cose_sign1": { - "description": "Request payload must be a COSE Sign1 document, with expected protected headers.Signer must be a member identity registered with this service.", + "description": "Request payload must be a COSE Sign1 document, with expected protected headers. Signer must be a member identity registered with this service.", "scheme": "cose_sign1", "type": "http" }, diff --git a/doc/schemas/gov_openapi.json b/doc/schemas/gov_openapi.json index bda0754e2d5c..ffa1e523fd69 100644 --- a/doc/schemas/gov_openapi.json +++ b/doc/schemas/gov_openapi.json @@ -492,7 +492,7 @@ "info": { "description": "This API is used to submit and query proposals which affect CCF's public governance tables.", "title": "CCF Governance API", - "version": "2.11.0" + "version": "2.12.0" }, "openapi": "3.0.0", "paths": { diff --git a/src/endpoints/authentication/cose_auth.cpp b/src/endpoints/authentication/cose_auth.cpp index c5f4b042ebb0..a65d6e470e4c 100644 --- a/src/endpoints/authentication/cose_auth.cpp +++ b/src/endpoints/authentication/cose_auth.cpp @@ -240,6 +240,6 @@ namespace ccf {"scheme", "cose_sign1"}, {"description", "Request payload must be a COSE Sign1 document, with expected " - "protected headers." + "protected headers. " "Signer must be a member identity registered with this service."}}); } diff --git a/src/node/rpc/member_frontend.h b/src/node/rpc/member_frontend.h index f9108b5d424d..9a932a936a1f 100644 --- a/src/node/rpc/member_frontend.h +++ b/src/node/rpc/member_frontend.h @@ -402,7 +402,7 @@ namespace ccf openapi_info.description = "This API is used to submit and query proposals which affect CCF's " "public governance tables."; - openapi_info.document_version = "2.11.0"; + openapi_info.document_version = "2.12.0"; } static std::optional get_caller_member_id(