680 std::optional<ccf::MemberCOSESign1AuthnIdentity> cose_auth_id =
682 std::optional<MemberId> member_id = std::nullopt;
688 auto params = nlohmann::json::parse(
689 cose_auth_id.has_value() ? cose_auth_id->content :
690 ctx.rpc_ctx->get_request_body());
693 const auto ma = mas->get(member_id.value());
698 HTTP_STATUS_FORBIDDEN,
699 ccf::errors::AuthorizationFailed,
701 "No ACK record exists for caller {}.", member_id.value()));
706 if (ma->state_digest != digest.state_digest)
710 HTTP_STATUS_BAD_REQUEST,
711 ccf::errors::StateDigestMismatch,
712 "Submitted state digest is not valid.");
716 auto sig = ctx.tx.rw(this->network.
signatures);
717 const auto s = sig->get();
718 if (cose_auth_id.has_value())
720 std::vector<uint8_t> cose_sign1 = {
721 cose_auth_id->envelope.begin(), cose_auth_id->envelope.end()};
724 mas->put(member_id.value(),
MemberAck({}, cose_sign1));
728 mas->put(member_id.value(),
MemberAck(s->root, cose_sign1));
737 catch (
const std::logic_error& e)
741 HTTP_STATUS_FORBIDDEN,
742 ccf::errors::AuthorizationFailed,
743 fmt::format(
"Error activating new member: {}", e.what()));
748 if (!service_status.has_value())
752 HTTP_STATUS_INTERNAL_SERVER_ERROR,
753 ccf::errors::InternalError,
754 "No service currently available.");
759 auto member_info =
members->get(member_id.value());
768 share_manager.shuffle_recovery_shares(ctx.tx);
770 catch (
const std::logic_error& e)
774 HTTP_STATUS_INTERNAL_SERVER_ERROR,
775 ccf::errors::InternalError,
776 fmt::format(
"Error issuing new recovery shares: {}", e.what()));
780 ctx.rpc_ctx->set_response_status(HTTP_STATUS_NO_CONTENT);
785 "Provide a member endorsement of a service state digest")
787 .set_openapi_deprecated_replaced(
788 "5.0.0",
"POST /gov/members/state-digests/{memberId}:ack")
794 if (!member_id.has_value())
798 HTTP_STATUS_FORBIDDEN,
799 ccf::errors::AuthorizationFailed,
800 "Caller is a not a valid member id");
805 auto sig = ctx.tx.rw(this->network.
signatures);
806 auto ma = mas->get(member_id.
value());
811 HTTP_STATUS_FORBIDDEN,
812 ccf::errors::AuthorizationFailed,
814 "No ACK record exists for caller {}.", member_id.
value()));
821 ma->state_digest = s->root.hex_str();
822 mas->put(member_id.
value(), ma.value());
825 j[
"state_digest"] = ma->state_digest;
827 ctx.rpc_ctx->set_response_header(
828 ccf::http::headers::CONTENT_TYPE,
829 http::headervalues::contenttype::JSON);
830 ctx.rpc_ctx->set_response_body(j.dump());
831 ctx.rpc_ctx->set_response_status(HTTP_STATUS_OK);
835 "/ack/update_state_digest",
840 .set_openapi_summary(
841 "Update and fetch a service state digest, for the purpose of member "
843 .set_openapi_deprecated_replaced(
844 "5.0.0",
"POST /gov/members/state-digests/{memberId}:update")
847 auto get_encrypted_recovery_share =
850 if (!member_id.has_value())
854 HTTP_STATUS_FORBIDDEN,
855 ccf::errors::AuthorizationFailed,
856 "Member is unknown.");
859 if (!check_member_active(ctx.tx, member_id.
value()))
863 HTTP_STATUS_FORBIDDEN,
864 ccf::errors::AuthorizationFailed,
865 "Only active members are given recovery shares.");
869 auto encrypted_share =
872 if (!encrypted_share.has_value())
876 HTTP_STATUS_NOT_FOUND,
877 ccf::errors::ResourceNotFound,
879 "Recovery share not found for member {}.", member_id->
value()));
885 ctx.rpc_ctx->set_response_header(
886 ccf::http::headers::CONTENT_TYPE,
887 http::headervalues::contenttype::JSON);
888 ctx.rpc_ctx->set_response_body(nlohmann::json(rec_share).dump());
889 ctx.rpc_ctx->set_response_status(HTTP_STATUS_OK);
895 get_encrypted_recovery_share,
898 .set_openapi_summary(
"A member's recovery share")
899 .set_openapi_deprecated_replaced(
900 "5.0.0",
"GET /gov/recovery/encrypted-shares/{memberId}")
903 auto get_encrypted_recovery_share_for_member =
905 std::string error_msg;
907 if (!get_member_id_from_path(
908 ctx.rpc_ctx->get_request_path_params(), member_id, error_msg))
912 HTTP_STATUS_BAD_REQUEST,
913 ccf::errors::InvalidResourceName,
914 std::move(error_msg));
918 auto encrypted_share =
921 if (!encrypted_share.has_value())
925 HTTP_STATUS_NOT_FOUND,
926 ccf::errors::ResourceNotFound,
928 "Recovery share not found for member {}.", member_id));
934 ctx.rpc_ctx->set_response_header(
935 ccf::http::headers::CONTENT_TYPE,
936 http::headervalues::contenttype::JSON);
937 ctx.rpc_ctx->set_response_body(nlohmann::json(rec_share).dump());
938 ctx.rpc_ctx->set_response_status(HTTP_STATUS_OK);
942 "/encrypted_recovery_share/{member_id}",
944 get_encrypted_recovery_share_for_member,
945 ccf::no_auth_required)
947 .set_openapi_summary(
"A member's recovery share")
948 .set_openapi_deprecated_replaced(
949 "5.0.0",
"GET /gov/recovery/encrypted-shares/{memberId}")
952 auto submit_recovery_share = [
this](
956 if (!member_id.has_value())
960 HTTP_STATUS_FORBIDDEN,
961 ccf::errors::AuthorizationFailed,
962 "Member is unknown.");
965 if (!check_member_active(ctx.tx, member_id.
value()))
969 HTTP_STATUS_FORBIDDEN,
970 errors::AuthorizationFailed,
971 "Member is not active.");
975 const auto* cose_auth_id =
977 auto params = nlohmann::json::parse(
978 cose_auth_id ? cose_auth_id->content :
979 ctx.rpc_ctx->get_request_body());
987 HTTP_STATUS_FORBIDDEN,
988 errors::ServiceNotWaitingForRecoveryShares,
989 "Service is not waiting for recovery shares.");
994 if (node_operation ==
nullptr)
996 throw std::logic_error(
997 "Unexpected: Could not access NodeOperation subsystem");
1000 if (node_operation->is_reading_private_ledger())
1004 HTTP_STATUS_FORBIDDEN,
1005 errors::NodeAlreadyRecovering,
1006 "Node is already recovering private ledger.");
1010 std::string share = params[
"share"];
1012 OPENSSL_cleanse(
const_cast<char*
>(share.data()), share.size());
1014 size_t submitted_shares_count = 0;
1018 ctx.tx, member_id.
value(), raw_recovery_share);
1020 catch (
const std::exception& e)
1022 constexpr auto error_msg =
"Error submitting recovery shares.";
1027 HTTP_STATUS_INTERNAL_SERVER_ERROR,
1028 errors::InternalError,
1032 OPENSSL_cleanse(raw_recovery_share.data(), raw_recovery_share.size());
1035 submitted_shares_count <
1041 "{}/{} recovery shares successfully submitted.",
1042 submitted_shares_count,
1044 ctx.rpc_ctx->set_response_header(
1045 ccf::http::headers::CONTENT_TYPE,
1046 http::headervalues::contenttype::JSON);
1047 ctx.rpc_ctx->set_response_body(nlohmann::json(recovery_share).dump());
1048 ctx.rpc_ctx->set_response_status(HTTP_STATUS_OK);
1053 "Reached recovery threshold {}",
1058 node_operation->initiate_private_recovery(ctx.tx);
1060 catch (
const std::exception& e)
1064 constexpr auto error_msg =
"Failed to initiate private recovery.";
1068 ctx.rpc_ctx->set_apply_writes(
true);
1071 HTTP_STATUS_INTERNAL_SERVER_ERROR,
1072 errors::InternalError,
1078 "{}/{} recovery shares successfully submitted. End of recovery "
1079 "procedure initiated.",
1080 submitted_shares_count,
1082 ctx.rpc_ctx->set_response_header(
1083 ccf::http::headers::CONTENT_TYPE,
1084 http::headervalues::contenttype::JSON);
1085 ctx.rpc_ctx->set_response_body(nlohmann::json(recovery_share).dump());
1086 ctx.rpc_ctx->set_response_status(HTTP_STATUS_OK);
1091 submit_recovery_share,
1094 .set_openapi_summary(
1095 "Provide a recovery share for the purpose of completing a service "
1097 .set_openapi_deprecated_replaced(
1098 "5.0.0",
"POST /gov/recovery/members/{memberId}:recover")
1101 using JWTKeyMap = std::map<JwtKeyId, std::vector<KeyIdInfo>>;
1103 auto get_jwt_keys = [
this](
auto& ctx, nlohmann::json&& body) {
1106 keys->foreach([&kmap](
const auto& k,
const auto& v) {
1107 std::vector<KeyIdInfo> info;
1108 for (
const auto& metadata : v)
1113 kmap.emplace(k, std::move(info));
1120 "/jwt_keys/all", HTTP_GET,
json_adapter(get_jwt_keys), no_auth_required)
1122 .set_openapi_deprecated_replaced(
"5.0.0",
"POST /gov/service/jwk")
1126 std::optional<ccf::MemberCOSESign1AuthnIdentity> cose_auth_id =
1128 std::optional<MemberId> member_id = std::nullopt;
1138 HTTP_STATUS_INTERNAL_SERVER_ERROR,
1139 ccf::errors::InternalError,
1140 "No consensus available.");
1144 std::vector<uint8_t> request_digest;
1145 if (cose_auth_id.has_value())
1147 std::span<const uint8_t> sig = cose_auth_id->signature;
1152 auto root_at_read = ctx.tx.get_root_at_read_version();
1153 if (!root_at_read.has_value())
1157 HTTP_STATUS_INTERNAL_SERVER_ERROR,
1158 ccf::errors::InternalError,
1159 "Proposal failed to bind to state.");
1167 std::vector<uint8_t> acc(
1168 root_at_read.value().h.begin(), root_at_read.value().h.end());
1169 acc.insert(acc.end(), request_digest.begin(), request_digest.end());
1171 proposal_id = proposal_digest.
hex_str();
1173 auto constitution = ctx.tx.ro(network.
constitution)->get();
1174 if (!constitution.has_value())
1178 HTTP_STATUS_INTERNAL_SERVER_ERROR,
1179 ccf::errors::InternalError,
1180 "No constitution is set - proposals cannot be evaluated");
1184 auto validate_script = constitution.value();
1188 auto validate_func =
context.get_exported_function(
1191 fmt::format(
"{}[0]", ccf::Tables::CONSTITUTION));
1193 const std::span<const uint8_t> proposal_body =
1194 cose_auth_id.has_value() ? cose_auth_id->content :
1195 ctx.rpc_ctx->get_request_body();
1197 auto body =
reinterpret_cast<const char*
>(proposal_body.data());
1198 auto body_len = proposal_body.size();
1200 auto proposal =
context.new_string_len(body, body_len);
1201 auto val =
context.call_with_rt_options(
1207 if (val.is_exception())
1209 auto [reason, trace] =
context.error_message();
1210 if (
context.interrupt_data.request_timed_out)
1212 reason =
"Operation took too long to complete.";
1216 HTTP_STATUS_INTERNAL_SERVER_ERROR,
1217 ccf::errors::InternalError,
1219 "Failed to execute validation: {} {}",
1221 trace.value_or(
"")));
1229 HTTP_STATUS_INTERNAL_SERVER_ERROR,
1230 ccf::errors::InternalError,
1231 "Validation failed to return an object");
1235 std::string description;
1236 auto desc = val[
"description"];
1239 description =
context.to_str(desc).value_or(
"");
1242 auto valid = val[
"valid"];
1243 if (!valid.is_true())
1247 HTTP_STATUS_BAD_REQUEST,
1248 ccf::errors::ProposalFailedToValidate,
1249 fmt::format(
"Proposal failed to validate: {}", description));
1258 if (pm->has(proposal_id))
1262 HTTP_STATUS_INTERNAL_SERVER_ERROR,
1263 ccf::errors::InternalError,
1264 "Proposal ID collision.");
1267 pm->put(proposal_id, {proposal_body.begin(), proposal_body.end()});
1273 if (cose_auth_id.has_value())
1275 record_cose_governance_history(
1276 ctx.tx, member_id.value(), cose_auth_id->envelope);
1278 std::string min_created_at =
"";
1288 if (cose_auth_id->protected_header.gov_msg_created_at > 9'999'999'999)
1292 HTTP_STATUS_BAD_REQUEST,
1293 ccf::errors::InvalidCreatedAt,
1294 "Header parameter created_at value is too large");
1297 std::string created_at_str = fmt::format(
1298 "{:0>10}", cose_auth_id->protected_header.gov_msg_created_at);
1299 const auto acceptable = is_proposal_submission_acceptable(
1304 colliding_proposal_id,
1312 HTTP_STATUS_BAD_REQUEST,
1313 ccf::errors::ProposalCreatedTooLongAgo,
1315 "Proposal created too long ago, created_at must be greater "
1324 HTTP_STATUS_BAD_REQUEST,
1325 ccf::errors::ProposalReplay,
1327 "Proposal submission replay, already exists as proposal {}",
1328 colliding_proposal_id));
1334 throw std::runtime_error(
1335 "Invalid ProposalSubmissionStatus value");
1339 auto rv = resolve_proposal(
1340 ctx.tx, proposal_id, proposal_body, constitution.value());
1348 HTTP_STATUS_INTERNAL_SERVER_ERROR,
1349 ccf::errors::InternalError,
1350 fmt::format(
"{}", rv.failure));
1357 {member_id.value(), rv.state, {}, {}, std::nullopt, rv.failure});
1358 ctx.rpc_ctx->set_response_header(
1359 ccf::http::headers::CONTENT_TYPE,
1360 http::headervalues::contenttype::JSON);
1361 ctx.rpc_ctx->set_response_body(nlohmann::json(rv).dump());
1362 ctx.rpc_ctx->set_response_status(HTTP_STATUS_OK);
1373 .set_openapi_deprecated_replaced(
1374 "5.0.0",
"POST /gov/members/proposals:create")
1377 using AllOpenProposals = std::map<ProposalId, jsgov::ProposalInfo>;
1378 auto get_open_proposals_js =
1381 jsgov::Tables::PROPOSALS_INFO);
1382 AllOpenProposals response;
1383 proposal_info->foreach(
1388 response[pid] = pinfo;
1399 ccf::no_auth_required)
1401 .set_openapi_summary(
1402 "Proposed changes to the service pending resolution")
1406 auto get_proposal_js = [
this](
1414 if (!get_proposal_id_from_path(
1415 ctx.
rpc_ctx->get_request_path_params(), proposal_id,
error))
1418 HTTP_STATUS_BAD_REQUEST, ccf::errors::InvalidResourceName,
error);
1422 auto p = pm->get(proposal_id);
1427 HTTP_STATUS_NOT_FOUND,
1428 ccf::errors::ProposalNotFound,
1429 fmt::format(
"Proposal {} does not exist.", proposal_id));
1434 auto pi_ = pi->get(proposal_id);
1439 HTTP_STATUS_INTERNAL_SERVER_ERROR,
1440 ccf::errors::InternalError,
1442 "No proposal info associated with {} exists.", proposal_id));
1449 "/proposals/{proposal_id}",
1452 ccf::no_auth_required)
1454 .set_openapi_summary(
1455 "Information about a proposed change to the service")
1456 .set_openapi_deprecated_replaced(
1457 "5.0.0",
"GET /gov/members/proposals/{proposalId}")
1461 std::optional<ccf::MemberCOSESign1AuthnIdentity> cose_auth_id =
1463 std::optional<MemberId> member_id = std::nullopt;
1471 if (!get_proposal_id_from_path(
1472 ctx.
rpc_ctx->get_request_path_params(), proposal_id,
error))
1476 HTTP_STATUS_BAD_REQUEST,
1477 ccf::errors::InvalidResourceName,
1482 if (cose_auth_id.has_value())
1484 if (!(cose_auth_id->protected_header.gov_msg_proposal_id
1486 cose_auth_id->protected_header.gov_msg_proposal_id.value() ==
1491 HTTP_STATUS_BAD_REQUEST,
1492 ccf::errors::InvalidResourceName,
1493 "Authenticated proposal id does not match URL");
1500 auto pi_ = pi->get(proposal_id);
1506 HTTP_STATUS_BAD_REQUEST,
1507 ccf::errors::ProposalNotFound,
1508 fmt::format(
"Proposal {} does not exist.", proposal_id));
1512 if (member_id.value() != pi_->proposer_id)
1516 HTTP_STATUS_FORBIDDEN,
1517 ccf::errors::AuthorizationFailed,
1519 "Proposal {} can only be withdrawn by proposer {}, not caller "
1523 member_id.value()));
1531 HTTP_STATUS_BAD_REQUEST,
1532 ccf::errors::ProposalNotOpen,
1534 "Proposal {} is currently in state {} - only {} proposals can be "
1543 pi->put(proposal_id, pi_.value());
1545 remove_all_other_non_open_proposals(ctx.
tx, proposal_id);
1546 if (cose_auth_id.has_value())
1548 record_cose_governance_history(
1549 ctx.
tx, member_id.value(), cose_auth_id->envelope);
1552 ctx.
rpc_ctx->set_response_header(
1553 ccf::http::headers::CONTENT_TYPE,
1554 http::headervalues::contenttype::JSON);
1555 ctx.
rpc_ctx->set_response_body(nlohmann::json(pi_.value()).dump());
1556 ctx.
rpc_ctx->set_response_status(HTTP_STATUS_OK);
1560 "/proposals/{proposal_id}/withdraw",
1565 .set_openapi_summary(
"Withdraw a proposed change to the service")
1566 .set_openapi_deprecated_replaced(
1567 "5.0.0",
"POST /gov/members/proposals/{proposalId}:withdraw")
1570 auto get_proposal_actions_js =
1574 if (!get_proposal_id_from_path(
1575 ctx.
rpc_ctx->get_request_path_params(), proposal_id,
error))
1579 HTTP_STATUS_BAD_REQUEST,
1580 ccf::errors::InvalidResourceName,
1587 auto p = pm->get(proposal_id);
1593 HTTP_STATUS_NOT_FOUND,
1594 ccf::errors::ProposalNotFound,
1595 fmt::format(
"Proposal {} does not exist.", proposal_id));
1599 ctx.
rpc_ctx->set_response_status(HTTP_STATUS_OK);
1600 ctx.
rpc_ctx->set_response_header(
1601 ccf::http::headers::CONTENT_TYPE,
1602 http::headervalues::contenttype::JSON);
1603 ctx.
rpc_ctx->set_response_body(std::move(p.value()));
1607 "/proposals/{proposal_id}/actions",
1609 get_proposal_actions_js,
1610 ccf::no_auth_required)
1612 .set_openapi_summary(
1613 "Actions contained in a proposed change to the service")
1614 .set_openapi_deprecated_replaced(
1615 "5.0.0",
"GET /gov/members/proposals/{proposalId}/actions")
1619 std::optional<ccf::MemberCOSESign1AuthnIdentity> cose_auth_id =
1621 std::optional<MemberId> member_id = std::nullopt;
1629 if (!get_proposal_id_from_path(
1630 ctx.
rpc_ctx->get_request_path_params(), proposal_id,
error))
1634 HTTP_STATUS_BAD_REQUEST,
1635 ccf::errors::InvalidResourceName,
1639 if (cose_auth_id.has_value())
1641 if (!(cose_auth_id->protected_header.gov_msg_proposal_id
1643 cose_auth_id->protected_header.gov_msg_proposal_id.value() ==
1648 HTTP_STATUS_BAD_REQUEST,
1649 ccf::errors::InvalidResourceName,
1650 "Authenticated proposal id does not match URL");
1656 if (!constitution.has_value())
1660 HTTP_STATUS_INTERNAL_SERVER_ERROR,
1661 ccf::errors::InternalError,
1662 "No constitution is set - proposals cannot be evaluated");
1668 auto pi_ = pi->get(proposal_id);
1673 HTTP_STATUS_NOT_FOUND,
1674 ccf::errors::ProposalNotFound,
1675 fmt::format(
"Could not find proposal {}.", proposal_id));
1683 HTTP_STATUS_BAD_REQUEST,
1684 ccf::errors::ProposalNotOpen,
1686 "Proposal {} is currently in state {} - only {} proposals can "
1695 auto p = pm->get(proposal_id);
1701 HTTP_STATUS_NOT_FOUND,
1702 ccf::errors::ProposalNotFound,
1703 fmt::format(
"Proposal {} does not exist.", proposal_id));
1707 if (pi_->ballots.find(member_id.value()) != pi_->ballots.end())
1711 HTTP_STATUS_BAD_REQUEST,
1712 ccf::errors::VoteAlreadyExists,
1713 "Vote already submitted.");
1718 auto params = nlohmann::json::parse(
1719 cose_auth_id.has_value() ? cose_auth_id->content :
1720 ctx.
rpc_ctx->get_request_body());
1724 const auto options_handle =
1726 context.runtime().set_runtime_options(
1727 options_handle->get(),
1729 auto ballot_func =
context.get_exported_function(
1730 params[
"ballot"],
"vote",
"body[\"ballot\"]");
1733 pi_->ballots[member_id.value()] = params[
"ballot"];
1734 pi->put(proposal_id, pi_.value());
1736 if (cose_auth_id.has_value())
1738 record_cose_governance_history(
1739 ctx.
tx, member_id.value(), cose_auth_id->envelope);
1742 auto rv = resolve_proposal(
1743 ctx.
tx, proposal_id, p.value(), constitution.value());
1750 HTTP_STATUS_INTERNAL_SERVER_ERROR,
1751 ccf::errors::InternalError,
1752 fmt::format(
"{}", rv.failure));
1757 pi_.value().state = rv.state;
1758 pi_.value().final_votes = rv.votes;
1759 pi_.value().vote_failures = rv.vote_failures;
1760 pi_.value().failure = rv.failure;
1761 pi->put(proposal_id, pi_.value());
1762 ctx.
rpc_ctx->set_response_header(
1763 ccf::http::headers::CONTENT_TYPE,
1764 http::headervalues::contenttype::JSON);
1765 ctx.
rpc_ctx->set_response_body(nlohmann::json(rv).dump());
1766 ctx.
rpc_ctx->set_response_status(HTTP_STATUS_OK);
1771 "/proposals/{proposal_id}/ballots",
1776 .set_openapi_summary(
1777 "Submit a ballot for a proposed change to the service")
1778 .set_openapi_deprecated_replaced(
1780 "POST /gov/members/proposals/{proposalId}/ballots/{memberId}:submit")
1787 if (!get_proposal_id_from_path(
1788 ctx.
rpc_ctx->get_request_path_params(), proposal_id,
error))
1791 HTTP_STATUS_BAD_REQUEST, ccf::errors::InvalidResourceName,
error);
1795 if (!get_member_id_from_path(
1796 ctx.
rpc_ctx->get_request_path_params(), vote_member_id,
error))
1799 HTTP_STATUS_BAD_REQUEST, ccf::errors::InvalidResourceName,
error);
1803 jsgov::Tables::PROPOSALS_INFO);
1804 auto pi_ = pi->get(proposal_id);
1808 HTTP_STATUS_NOT_FOUND,
1809 ccf::errors::ProposalNotFound,
1810 fmt::format(
"Proposal {} does not exist.", proposal_id));
1813 const auto vote_it = pi_->ballots.find(vote_member_id);
1814 if (vote_it == pi_->ballots.end())
1817 HTTP_STATUS_NOT_FOUND,
1818 ccf::errors::VoteNotFound,
1820 "Member {} has not voted for proposal {}.",
1828 "/proposals/{proposal_id}/ballots/{member_id}",
1831 ccf::no_auth_required)
1833 .set_openapi_summary(
1834 "Ballot for a given member about a proposed change to the service")
1835 .set_openapi_deprecated_replaced(
1836 "5.0.0",
"GET /gov/members/proposals/{proposalId}/ballots/{memberId}")
1839 using AllMemberDetails = std::map<ccf::MemberId, FullMemberDetails>;
1840 auto get_all_members =
1845 auto member_public_encryption_keys =
1847 ccf::Tables::MEMBER_ENCRYPTION_PUBLIC_KEYS);
1849 AllMemberDetails response;
1852 [&response, member_certs, member_public_encryption_keys](
1853 const auto& k,
const auto& v) {
1858 const auto cert = member_certs->get(k);
1859 if (cert.has_value())
1861 md.
cert = cert.value();
1864 const auto public_encryption_key =
1865 member_public_encryption_keys->get(k);
1866 if (public_encryption_key.has_value())
1881 ccf::no_auth_required)
1883 .set_openapi_deprecated_replaced(
"5.0.0",
"GET /gov/service/members")
1886 add_kv_wrapper_endpoints();