Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement Azure-compliant governance interface #5660

Merged
merged 79 commits into from
Sep 27, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
79 commits
Select commit Hold shift + click to select a range
e446fe5
Skeleton: New dir, insert new registry into hierarchy
eddyashton May 18, 2023
2b18f33
PoC initial endpoint
eddyashton May 18, 2023
17e7c7c
Make get_path_param a non-member
eddyashton May 18, 2023
cda62d5
Add api_version_adapter
eddyashton May 19, 2023
b81a3e6
Merge branch 'main' of github.com:microsoft/CCF into impl_new_gov_fro…
eddyashton May 19, 2023
5678e50
Add commit endpoint
eddyashton May 19, 2023
610358a
Update TypeScript, pin versions
eddyashton May 19, 2023
63764a5
Juggling some files around, adding more placeholder handlers
eddyashton May 19, 2023
06bee3d
Version all Azure.Core use
eddyashton May 22, 2023
41ac6d3
Add descriptions to recovery API
eddyashton May 22, 2023
7655265
Skeleton state-digests endpoints
eddyashton May 22, 2023
c3842d6
Implement ack endpoints
eddyashton May 22, 2023
218899b
Workarounds and hacks to get a basic end-to-end test
eddyashton May 22, 2023
7a34d77
Bite the bullet on adapter naming
eddyashton May 23, 2023
760c6d6
Merge branch 'main' of github.com:microsoft/CCF into impl_new_gov_fro…
eddyashton May 23, 2023
74e360c
Hide new API from OpenAPI
eddyashton May 23, 2023
dafc146
Remove unneeded TODO
eddyashton May 23, 2023
fef08f3
Merge branch 'main' of github.com:microsoft/CCF into impl_new_gov_fro…
eddyashton Jun 1, 2023
ac889da
Merge branch 'main' of github.com:microsoft/CCF into impl_new_gov_fro…
eddyashton Jun 13, 2023
ed4387d
Expanding skeleton
eddyashton Jun 15, 2023
7ab5603
Progress? Or tedium?
eddyashton Jun 15, 2023
03ac864
Merge branch 'main' of github.com:microsoft/CCF into impl_new_gov_fro…
eddyashton Sep 4, 2023
230415c
Resync'd ballot submit endpoint
eddyashton Sep 4, 2023
0a160fa
get_actions
eddyashton Sep 4, 2023
6d94a16
Validation
eddyashton Sep 4, 2023
c5d2c23
Long road to a create endpoint
eddyashton Sep 5, 2023
05ed32f
Denser, trivial TODOs
eddyashton Sep 5, 2023
4e701dc
Bodges for PoC e2e testing
eddyashton Sep 5, 2023
4f64a7f
Progress towards test
eddyashton Sep 5, 2023
9be8e19
Merge branch 'main' of github.com:microsoft/CCF into impl_new_gov_fro…
eddyashton Sep 5, 2023
bf578ea
Hmmmm
eddyashton Sep 5, 2023
a9defcc
Record COSE history, active-member-only auth
eddyashton Sep 6, 2023
0cde5df
Some deduplication
eddyashton Sep 6, 2023
a7cecb2
Distinct details namespace
eddyashton Sep 6, 2023
8aebb06
Line flowing
eddyashton Sep 6, 2023
080a78c
SImplify handling of resolve result
eddyashton Sep 6, 2023
25cb89e
Log errors
eddyashton Sep 6, 2023
c158f09
De-centralise the root-capture decision
eddyashton Sep 7, 2023
97384c2
Recovery endpoints
eddyashton Sep 7, 2023
0b63717
More bodges for PoC e2e testing
eddyashton Sep 7, 2023
5c4fbbc
Start on service_state
eddyashton Sep 7, 2023
a7246f5
More endpoints, everything but JWK?
eddyashton Sep 12, 2023
305ad6b
JWK endpoint
eddyashton Sep 13, 2023
7b79249
Rename API version, working towards initial test, OpenAPI version inc…
eddyashton Sep 13, 2023
0fa65cb
Scrap this
eddyashton Sep 14, 2023
827f05c
Hide for consistency
eddyashton Sep 15, 2023
94da681
Reinvent wheel, run self over
eddyashton Sep 15, 2023
a68c697
Remove unnecessary JSON adapter, add client wrapper
eddyashton Sep 15, 2023
1d478a6
Oops - keep hiding the hidden endpoints
eddyashton Sep 15, 2023
3d2b985
Merge branch 'main' of github.com:microsoft/CCF into impl_new_gov_fro…
eddyashton Sep 15, 2023
f21369d
Format, remove vestigial files
eddyashton Sep 15, 2023
46b90cd
Unpicking temporary bodges, will retest via member client
eddyashton Sep 15, 2023
178e9e5
Progress towards a plugin replacement test
eddyashton Sep 15, 2023
6bf53c0
Merge branch 'main' of github.com:microsoft/CCF into impl_new_gov_fro…
eddyashton Sep 18, 2023
2e20d9e
More tests for new API
eddyashton Sep 18, 2023
b05721a
Match COSE header endpoints by regex, fix ballot submission endpoint
eddyashton Sep 18, 2023
5e13aaf
Remove spammy logging
eddyashton Sep 18, 2023
e4e98b3
Rename tests
eddyashton Sep 18, 2023
cc473cb
Wipe out the small TODOs
eddyashton Sep 18, 2023
b6c9c3d
Restore requirements check
eddyashton Sep 18, 2023
7aff82e
Branch in submit_recovery_share.sh to use new API, based on env var
eddyashton Sep 18, 2023
c3e7f59
Oops
eddyashton Sep 19, 2023
8774192
Oops
eddyashton Sep 19, 2023
a8d6ded
Merge branch 'main' of github.com:microsoft/CCF into impl_new_gov_fro…
eddyashton Sep 19, 2023
6319da5
Remove a TODO by reworking towards idempotence
eddyashton Sep 19, 2023
23f1988
Remove TODOs tracked by PR comments
eddyashton Sep 19, 2023
dea64b1
Take a stance on remaining TODOs
eddyashton Sep 19, 2023
10665eb
Format and lint
eddyashton Sep 19, 2023
c6906c3
Avoid unnecessary breaking change, by keeping old instance-local APIs
eddyashton Sep 25, 2023
1d6d8ba
(Mostly) remove env var API version, replace with CLI arg passed through
eddyashton Sep 25, 2023
bb66260
Minor
eddyashton Sep 26, 2023
14921b2
submit_recovery_share.sh takes CLI arg rather than env var
eddyashton Sep 26, 2023
f08ed2d
Merge branch 'main' of github.com:microsoft/CCF into impl_new_gov_fro…
eddyashton Sep 26, 2023
771c95a
Format
eddyashton Sep 26, 2023
5f35c02
Base response format on API guidance
eddyashton Sep 26, 2023
e96d8c3
Format
eddyashton Sep 26, 2023
be0e008
Weird compile failure
eddyashton Sep 26, 2023
6966996
Fix governance tests
eddyashton Sep 27, 2023
cfca822
Apply suggestions from code review
eddyashton Sep 27, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 19 additions & 3 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -1217,9 +1217,16 @@ if(BUILD_TESTS)
endif()

add_e2e_test(
NAME recovery_test_cft
NAME recovery_test_cft_api_0
PYTHON_SCRIPT ${CMAKE_SOURCE_DIR}/tests/recovery.py
ADDITIONAL_ARGS ${ADDITIONAL_RECOVERY_ARGS}
ADDITIONAL_ARGS ${ADDITIONAL_RECOVERY_ARGS} --gov-api-version "classic"
)

add_e2e_test(
NAME recovery_test_cft_api_1
PYTHON_SCRIPT ${CMAKE_SOURCE_DIR}/tests/recovery.py
ADDITIONAL_ARGS ${ADDITIONAL_RECOVERY_ARGS} --gov-api-version
"2023-06-01-preview"
)

add_e2e_test(
Expand Down Expand Up @@ -1375,8 +1382,17 @@ if(BUILD_TESTS)
)

add_e2e_test(
NAME membership PYTHON_SCRIPT ${CMAKE_SOURCE_DIR}/tests/membership.py
NAME membership_api_0
PYTHON_SCRIPT ${CMAKE_SOURCE_DIR}/tests/membership.py
ADDITIONAL_ARGS --gov-api-version "classic"
)

add_e2e_test(
NAME membership_api_1
PYTHON_SCRIPT ${CMAKE_SOURCE_DIR}/tests/membership.py
ADDITIONAL_ARGS --gov-api-version "2023-06-01-preview"
)

set(PARTITIONS_TEST_ARGS
# Higher snapshot interval as the test currently assumes that no
# transactions
Expand Down
77 changes: 50 additions & 27 deletions include/ccf/endpoint_registry.h
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,51 @@ namespace ccf::endpoints
void default_locally_committed_func(
CommandEndpointContext& ctx, const TxID& tx_id);

template <typename T>
eddyashton marked this conversation as resolved.
Show resolved Hide resolved
inline bool get_path_param(
const ccf::PathParams& params,
const std::string& param_name,
T& value,
std::string& error)
{
const auto it = params.find(param_name);
if (it == params.end())
{
error = fmt::format("No parameter named '{}' in path", param_name);
return false;
}

const auto param_s = it->second;
const auto [p, ec] =
std::from_chars(param_s.data(), param_s.data() + param_s.size(), value);
if (ec != std::errc())
{
error = fmt::format(
"Unable to parse path parameter '{}' as a {}", param_s, param_name);
return false;
}

return true;
}

template <>
inline bool get_path_param(
const ccf::PathParams& params,
const std::string& param_name,
std::string& value,
std::string& error)
{
const auto it = params.find(param_name);
if (it == params.end())
{
error = fmt::format("No parameter named '{}' in path", param_name);
return false;
}

value = it->second;
return true;
}

/** The EndpointRegistry records the user-defined endpoints for a given
* CCF application.
*
Expand Down Expand Up @@ -83,24 +128,8 @@ namespace ccf::endpoints
T& value,
std::string& error)
{
const auto it = params.find(param_name);
if (it == params.end())
{
error = fmt::format("No parameter named '{}' in path", param_name);
return false;
}

const auto param_s = it->second;
const auto [p, ec] =
std::from_chars(param_s.data(), param_s.data() + param_s.size(), value);
if (ec != std::errc())
{
error = fmt::format(
"Unable to parse path parameter '{}' as a {}", param_s, param_name);
return false;
}

return true;
return ccf::endpoints::get_path_param<T>(
params, param_name, value, error);
}

template <>
Expand All @@ -110,15 +139,7 @@ namespace ccf::endpoints
std::string& value,
std::string& error)
{
const auto it = params.find(param_name);
if (it == params.end())
{
error = fmt::format("No parameter named '{}' in path", param_name);
return false;
}

value = it->second;
return true;
return ccf::endpoints::get_path_param(params, param_name, value, error);
}

protected:
Expand Down Expand Up @@ -245,6 +266,8 @@ namespace ccf::endpoints
virtual std::set<RESTVerb> get_allowed_verbs(
kv::Tx&, const ccf::RpcContext& rpc_ctx);

virtual bool request_needs_root(const ccf::RpcContext& rpc_ctx);

virtual void report_ambiguous_templated_path(
const std::string& path,
const std::vector<EndpointDefinitionPtr>& matches);
Expand Down
23 changes: 23 additions & 0 deletions include/ccf/endpoints/authentication/cose_auth.h
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,29 @@ namespace ccf
}
};

/** Active Member COSE Sign1 Authentication Policy
*
* Extends MemberCOSESign1AuthPolicy, to also require that the signer's state
* is Active
*/
class ActiveMemberCOSESign1AuthnPolicy : public MemberCOSESign1AuthnPolicy
{
public:
static constexpr auto SECURITY_SCHEME_NAME = "active_member_cose_sign1";

using MemberCOSESign1AuthnPolicy::MemberCOSESign1AuthnPolicy;

std::unique_ptr<AuthnIdentity> authenticate(
kv::ReadOnlyTx& tx,
const std::shared_ptr<ccf::RpcContext>& ctx,
std::string& error_reason) override;

std::string get_security_scheme_name() override
{
return SECURITY_SCHEME_NAME;
}
};

/** User COSE Sign1 Authentication Policy
*/
class UserCOSESign1AuthnPolicy : public AuthnPolicy
Expand Down
1 change: 1 addition & 0 deletions include/ccf/http_consts.h
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ namespace http
static constexpr auto OCTET_STREAM = "application/octet-stream";
static constexpr auto GRPC = "application/grpc";
static constexpr auto COSE = "application/cose";
static constexpr auto JAVASCRIPT = "text/javascript";
}
}

Expand Down
2 changes: 2 additions & 0 deletions include/ccf/odata_error.h
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,8 @@ namespace ccf
ERROR(ProposalCreatedTooLongAgo)
ERROR(InvalidCreatedAt)
ERROR(JSException)
ERROR(MissingApiVersionParameter)
ERROR(UnsupportedApiVersionValue)

// node-to-node (/join and /create):
ERROR(ConsensusTypeMismatch)
Expand Down
2 changes: 1 addition & 1 deletion include/ccf/rpc_context.h
Original file line number Diff line number Diff line change
Expand Up @@ -151,7 +151,7 @@ namespace ccf
}

virtual void set_response_json(
nlohmann::json& body, http_status status) = 0;
const nlohmann::json& body, http_status status) = 0;

/// Construct error response, formatted according to the request content
/// type (either JSON OData-formatted or gRPC error)
Expand Down
24 changes: 21 additions & 3 deletions python/utils/submit_recovery_share.sh
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ set -e
function usage()
{
echo "Usage:"""
echo " $0 https://<node-address> --member-enc-privk /path/to/member_enc_privk.pem --cert /path/to/member_cert.pem [CURL_OPTIONS]"
echo " $0 https://<node-address> --member-enc-privk /path/to/member_enc_privk.pem --api-version api_version --cert /path/to/member_cert.pem [CURL_OPTIONS]"
echo "Retrieves the encrypted recovery share for a given member, decrypts the share and submits it for recovery."
echo ""
echo "A sufficient number of recovery shares must be submitted by members to initiate the end of recovery procedure."
Expand All @@ -25,6 +25,7 @@ fi
node_rpc_address=$1
shift

api_version="classic"
while [ "$1" != "" ]; do
case $1 in
-h|-\?|--help)
Expand All @@ -34,6 +35,9 @@ while [ "$1" != "" ]; do
--member-enc-privk)
member_enc_privk="$2"
;;
--api-version)
api_version="$2"
;;
*)
break
esac
Expand Down Expand Up @@ -67,9 +71,23 @@ fi
# Compute member ID, as the SHA-256 fingerprint of the signing certificate
member_id=$(openssl x509 -in "$cert" -noout -fingerprint -sha256 | cut -d "=" -f 2 | sed 's/://g' | awk '{print tolower($0)}')

if [ "${api_version}" == "classic" ]; then
get_share_path="gov/encrypted_recovery_share/${member_id}"
share_field="encrypted_share"
submit_share_path="gov/recovery_share"
else
get_share_path="gov/recovery/encrypted-shares/${member_id}?api-version=${api_version}"
share_field="encryptedShare"
submit_share_path="gov/recovery/members/${member_id}:recover?api-version=${api_version}"
fi

# First, retrieve the encrypted recovery share
encrypted_share=$(curl -sS --fail -X GET "${node_rpc_address}"/gov/encrypted_recovery_share/"${member_id}" "${@}" | jq -r '.encrypted_share')
encrypted_share=$(curl -sS --fail -X GET "${node_rpc_address}/${get_share_path}" "${@}" | jq -r ".${share_field}")

# Then, decrypt encrypted share with member private key submit decrypted recovery share
# Note: all in one line so that the decrypted recovery share is not exposed
echo "${encrypted_share}" | openssl base64 -d | openssl pkeyutl -inkey "${member_enc_privk}" -decrypt -pkeyopt rsa_padding_mode:oaep -pkeyopt rsa_oaep_md:sha256 | openssl base64 -A | jq -R '{share: (.)}' | curl -i -sS --fail -H "Content-Type: application/json" -X POST "${node_rpc_address}"/gov/recovery_share "${@}" -d @-
echo "${encrypted_share}" \
| openssl base64 -d \
| openssl pkeyutl -inkey "${member_enc_privk}" -decrypt -pkeyopt rsa_padding_mode:oaep -pkeyopt rsa_oaep_md:sha256 \
| openssl base64 -A | jq -R '{share: (.)}' \
| curl -i -sS --fail -H "Content-Type: application/json" -X POST "${node_rpc_address}/${submit_share_path}" "${@}" -d @-
32 changes: 32 additions & 0 deletions src/endpoints/authentication/cose_auth.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -373,6 +373,38 @@ namespace ccf
"protected headers. "
"Signer must be a member identity registered with this service."}});

std::unique_ptr<AuthnIdentity> ActiveMemberCOSESign1AuthnPolicy::authenticate(
kv::ReadOnlyTx& tx,
const std::shared_ptr<ccf::RpcContext>& ctx,
std::string& error_reason)
{
auto ident =
MemberCOSESign1AuthnPolicy::authenticate(tx, ctx, error_reason);
if (ident != nullptr)
{
auto cose_ident =
dynamic_cast<const MemberCOSESign1AuthnIdentity*>(ident.get());
if (cose_ident == nullptr)
{
error_reason = "Unexpected Identity type";
return nullptr;
}

const auto member_id = cose_ident->member_id;

auto member_info_handle =
tx.template ro<ccf::MemberInfo>(ccf::Tables::MEMBER_INFO);
const auto member = member_info_handle->get(member_id);
if (!member.has_value() || member->status != ccf::MemberStatus::ACTIVE)
{
error_reason = "Signer is not an ACTIVE member";
return nullptr;
}
}

return ident;
}

UserCOSESign1AuthnPolicy::UserCOSESign1AuthnPolicy() = default;
UserCOSESign1AuthnPolicy::~UserCOSESign1AuthnPolicy() = default;

Expand Down
5 changes: 5 additions & 0 deletions src/endpoints/endpoint_registry.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -535,6 +535,11 @@ namespace ccf::endpoints
return verbs;
}

bool EndpointRegistry::request_needs_root(const ccf::RpcContext& rpc_ctx)
{
return false;
}

void EndpointRegistry::report_ambiguous_templated_path(
const std::string& path, const std::vector<EndpointDefinitionPtr>& matches)
{
Expand Down
12 changes: 12 additions & 0 deletions src/node/gov/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
This folder implements a new governance API.
This API is defined in the root-level `typespec-ccf` folder, with a TypeSpec source generating an OpenAPI definition.

It aims to expose all of the functionality of the existing API, defined in `src/node/rpc/member_frontend.h`, but with naming and schemas compliant with Azure API standards.

For a transition period (at least the 4.x release), both APIs will be offered. The plan is to eventually deprecate the old API.

Implementation notes:

- All endpoints validate and process an `api-version` parameter, modifying their behaviour accordingly.
- To present both under the `/gov` prefix, the old frontend is a subclass of the new frontend.
- The frontend implementation is split into distinct components which can be more easily moved around, rather than a single monolithic frontend implementation.
79 changes: 79 additions & 0 deletions src/node/gov/api_version.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the Apache 2.0 License.
#pragma once

#include "ccf/http_query.h"
#include "ccf/json_handler.h"

#include <string>

namespace ccf::gov::endpoints
{
enum class ApiVersion
{
preview_v1,
};

static constexpr std::pair<ApiVersion, char const*> api_version_strings[] = {
{ApiVersion::preview_v1, "2023-06-01-preview"}};

// Extracts api-version from query parameter, and passes this to the given
// functor. Will return error responses for missing and unknown api-versions.
// This means handler functors can safely provide a default implementation
// without validating the given API version, so long as the behaviour is the
// same for *all* accepted versions.
template <typename Fn>
auto api_version_adapter(Fn&& f)
{
return [f](auto& ctx) {
const auto param_name = "api-version";
const auto parsed_query =
http::parse_query(ctx.rpc_ctx->get_request_query());
const auto qit = parsed_query.find(param_name);
if (qit == parsed_query.end())
{
ctx.rpc_ctx->set_error(
HTTP_STATUS_BAD_REQUEST,
ccf::errors::MissingApiVersionParameter,
fmt::format(
"The api-version query parameter (?{}=) is required for all "
"requests.",
param_name));
return;
}

const auto it = std::find_if(
std::begin(api_version_strings),
std::end(api_version_strings),
[&qit](const auto& p) { return p.second == qit->second; });
if (it == std::end(api_version_strings))
{
auto message = fmt::format(
"Unsupported api-version '{}'. The supported api-versions are: ",
qit->second);
auto first = true;
for (const auto& p : api_version_strings)
{
if (first)
{
message += p.second;
first = false;
}
else
{
message += fmt::format(", {}", p.second);
}
}
ctx.rpc_ctx->set_error(
HTTP_STATUS_BAD_REQUEST,
ccf::errors::UnsupportedApiVersionValue,
std::move(message));
return;
}

const ApiVersion api_version = it->first;
f(ctx, api_version);
return;
};
}
}
Loading