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

Unauthenticated handlers #962

Merged
merged 21 commits into from
Mar 19, 2020
39 changes: 39 additions & 0 deletions src/apps/logging/logging.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ namespace ccfapp
static constexpr auto LOG_GET_PUBLIC = "LOG_get_pub";

static constexpr auto LOG_RECORD_PREFIX_CERT = "LOG_record_prefix_cert";
static constexpr auto LOG_RECORD_ANONYMOUS_CALLER = "LOG_record_anonymous";
};

// SNIPPET: table_definition
Expand Down Expand Up @@ -195,6 +196,42 @@ namespace ccfapp
};
// SNIPPET_END: log_record_prefix_cert

auto log_record_anonymous = [this](RequestArgs& args) {
jumaffre marked this conversation as resolved.
Show resolved Hide resolved
const auto& cert_data = args.rpc_ctx->session->caller_cert;

const auto body_j =
nlohmann::json::parse(args.rpc_ctx->get_request_body());

if (args.caller_id != INVALID_ID)
{
args.rpc_ctx->set_response_status(HTTP_STATUS_BAD_REQUEST);
args.rpc_ctx->set_response_header(
http::headers::CONTENT_TYPE, http::headervalues::contenttype::TEXT);
args.rpc_ctx->set_response_body(
"Only anonymous callers can record anonymous messages");
return;
}

const auto in = body_j.get<LoggingRecord::In>();
if (in.msg.empty())
{
args.rpc_ctx->set_response_status(HTTP_STATUS_BAD_REQUEST);
args.rpc_ctx->set_response_header(
http::headers::CONTENT_TYPE, http::headervalues::contenttype::TEXT);
args.rpc_ctx->set_response_body("Cannot record an empty log message");
return;
}

const auto log_line = fmt::format("Anonymous: {}", in.msg);
auto view = args.tx.get_view(records);
view->put(in.id, log_line);

args.rpc_ctx->set_response_status(HTTP_STATUS_OK);
args.rpc_ctx->set_response_header(
http::headers::CONTENT_TYPE, http::headervalues::contenttype::JSON);
args.rpc_ctx->set_response_body(nlohmann::json(true).dump());
};

install(Procs::LOG_RECORD, json_adapter(record), Write)
.set_auto_schema<LoggingRecord::In, bool>();
// SNIPPET_START: install_get
Expand All @@ -211,6 +248,8 @@ namespace ccfapp
.set_result_schema(get_public_result_schema);

install(Procs::LOG_RECORD_PREFIX_CERT, log_record_prefix_cert, Write);
install(Procs::LOG_RECORD_ANONYMOUS_CALLER, log_record_anonymous, Write)
.set_caller_auth_disabled(true);
jumaffre marked this conversation as resolved.
Show resolved Hide resolved

nwt.signatures.set_global_hook([this, &notifier](
kv::Version version,
Expand Down
2 changes: 1 addition & 1 deletion src/enclave/rpccontext.h
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ namespace enclave
{
size_t client_session_id = InvalidSessionId;
std::vector<uint8_t> caller_cert = {};
bool is_forwarded = false;
bool forward = false;
jumaffre marked this conversation as resolved.
Show resolved Hide resolved

//
// Only set in the case of a forwarded RPC
Expand Down
129 changes: 55 additions & 74 deletions src/node/rpc/frontend.h
Original file line number Diff line number Diff line change
Expand Up @@ -218,66 +218,25 @@ namespace ccf

Store::Tx tx;

// Retrieve id of caller
std::optional<CallerId> caller_id;
if (ctx->is_create_request)
jumaffre marked this conversation as resolved.
Show resolved Hide resolved
{
caller_id = INVALID_ID;
}
else
{
caller_id = handlers.valid_caller(tx, ctx->session->caller_cert);
}

if (!caller_id.has_value())
{
ctx->set_response_status(HTTP_STATUS_FORBIDDEN);
ctx->set_response_body(invalid_caller_error_message());
return ctx->serialise_response();
}

const auto signed_request = ctx->get_signed_request();
if (signed_request.has_value())
{
if (
!ctx->is_create_request &&
!verify_client_signature(
ctx->session->caller_cert,
caller_id.value(),
signed_request.value()))
{
set_response_unauthorized(ctx);
return ctx->serialise_response();
}

// Client signature is only recorded on the primary
if (
consensus == nullptr || consensus->is_primary() ||
ctx->is_create_request)
{
record_client_signature(
tx, caller_id.value(), signed_request.value());
}
}
auto caller_id = handlers.get_caller_id(tx, ctx->session->caller_cert);

if (consensus != nullptr && consensus->type() == ConsensusType::PBFT)
{
auto rep = process_if_local_node_rpc(ctx, tx, caller_id.value());
auto rep = process_if_local_node_rpc(ctx, tx, caller_id);
if (rep.has_value())
{
return rep.value();
jumaffre marked this conversation as resolved.
Show resolved Hide resolved
}
kv::TxHistory::RequestID reqid;

update_history();
reqid = {caller_id.value(),
ctx->session->client_session_id,
ctx->get_request_index()};
reqid = {
caller_id, ctx->session->client_session_id, ctx->get_request_index()};
if (history)
{
if (!history->add_request(
reqid,
caller_id.value(),
caller_id,
ctx->session->caller_cert,
ctx->get_serialised_request()))
{
Expand All @@ -302,7 +261,7 @@ namespace ccf
}
else
{
auto rep = process_command(ctx, tx, caller_id.value());
auto rep = process_command(ctx, tx, caller_id);

// If necessary, forward the RPC to the current primary
if (!rep.has_value())
Expand All @@ -314,7 +273,7 @@ namespace ccf
if (
primary_id != NoNode && cmd_forwarder &&
cmd_forwarder->forward_command(
ctx, primary_id, caller_id.value(), get_cert_to_forward(ctx)))
ctx, primary_id, caller_id, get_cert_to_forward(ctx)))
{
// Indicate that the RPC has been forwarded to primary
LOG_DEBUG_FMT("RPC forwarded to primary {}", primary_id);
Expand Down Expand Up @@ -403,22 +362,6 @@ namespace ccf

Store::Tx tx;

if (!lookup_forwarded_caller_cert(ctx, tx))
{
ctx->set_response_status(HTTP_STATUS_FORBIDDEN);
ctx->set_response_body(invalid_caller_error_message());
return ctx->serialise_response();
}

// Store client signature. It is assumed that the forwarder node has
// already verified the client signature.
const auto signed_request = ctx->get_signed_request();
if (signed_request.has_value())
{
record_client_signature(
tx, ctx->session->fwd->caller_id, signed_request.value());
}

auto rep = process_command(ctx, tx, ctx->session->fwd->caller_id);
if (!rep.has_value())
{
Expand Down Expand Up @@ -459,27 +402,65 @@ namespace ccf
return ctx->serialise_response();
}

if (
handler->require_client_signature &&
!ctx->get_signed_request().has_value())
if (!handler->caller_auth_disabled && handlers.has_certs())
{
// Only if handler requires auth.
// If a request is forwarded, check that the caller is known. Otherwise,
// only check that the caller id is valid.
if (
(ctx->session->fwd.has_value() &&
jumaffre marked this conversation as resolved.
Show resolved Hide resolved
!lookup_forwarded_caller_cert(ctx, tx)) ||
caller_id == INVALID_ID)
{
ctx->set_response_status(HTTP_STATUS_FORBIDDEN);
ctx->set_response_body(invalid_caller_error_message());
return ctx->serialise_response();
}
}

bool is_primary = (consensus == nullptr) || consensus->is_primary() ||
ctx->is_create_request;

const auto signed_request = ctx->get_signed_request();
if (handler->require_client_signature && !signed_request.has_value())
{
set_response_unauthorized(
ctx, fmt::format("'{}' RPC must be signed", method));
return ctx->serialise_response();
}

update_history();
// By default, signed requests are verified and recorded, even on
// handlers that do not require client signatures
if (signed_request.has_value())
{
// For forwarded requests (Raft only), skip verification as it is
// assumed that the verification was done by the forwarder node.
if (
(!ctx->is_create_request &&
(!(consensus->type() == ConsensusType::RAFT) ||
!ctx->session->fwd.has_value())) &&
!verify_client_signature(
ctx->session->caller_cert, caller_id, signed_request.value()))
{
set_response_unauthorized(ctx);
return ctx->serialise_response();
}

bool is_primary = (consensus == nullptr) || consensus->is_primary() ||
ctx->is_create_request;
if (is_primary)
{
record_client_signature(tx, caller_id, signed_request.value());
}
}

update_history();

if (!is_primary && consensus->type() == ConsensusType::RAFT)
{
switch (handler->read_write)
{
case HandlerRegistry::Read:
{
if (ctx->session->is_forwarded)
if (ctx->session->forward)
{
return forward_or_redirect_json(ctx);
}
Expand All @@ -488,7 +469,7 @@ namespace ccf

case HandlerRegistry::Write:
{
ctx->session->is_forwarded = true;
ctx->session->forward = true;
return forward_or_redirect_json(ctx);
}

Expand All @@ -498,10 +479,10 @@ namespace ccf
ctx->get_request_header(http::headers::CCF_READ_ONLY);
if (!read_only_it.has_value() || (read_only_it.value() != "true"))
{
ctx->session->is_forwarded = true;
ctx->session->forward = true;
return forward_or_redirect_json(ctx);
}
else if (ctx->session->is_forwarded)
else if (ctx->session->forward)
{
return forward_or_redirect_json(ctx);
}
Expand Down
43 changes: 34 additions & 9 deletions src/node/rpc/handlerregistry.h
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ namespace ccf
std::string method;
HandleFunction func;
ReadWrite read_write = Write;
HandlerRegistry* registry;

nlohmann::json params_schema = nullptr;

Expand Down Expand Up @@ -92,6 +93,24 @@ namespace ccf
return *this;
}

// If true, caller does not need to be authenticated
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This sounds like exactly the same thing as require_client_signature. The comments around its use-point also talk about requiring auth. Convince me these are different?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The two flags are different. require_client_signature indicates that the RPC requires a signed command (i.e. use scurl.sh) while caller_auth_disabled indicates that the client does not need to be authenticated (i.e. not specifying --key and --cert to curl is OK), even on a frontend that has (through its registry) a certs table. In practice however, I imagine that the two are not completely orthogonal (e.g. using proxies).

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Both could be considered authentication, if we had a mechanism to convey intended identity on signed messages?

bool caller_auth_disabled = false;

Handler& set_caller_auth_disabled(bool v)
{
if (v && registry->certs == nullptr)
jumaffre marked this conversation as resolved.
Show resolved Hide resolved
{
LOG_INFO_FMT(
"Disabling caller auth on {} handler has no effect since its "
"registry does not have certificates table",
method);
return *this;
}

caller_auth_disabled = v;
return *this;
}

// If true, request is executed without consensus (PBFT only)
bool execute_locally = false;

Expand Down Expand Up @@ -139,6 +158,7 @@ namespace ccf
handler.method = method;
handler.func = f;
handler.read_write = read_write;
handler.registry = this;
return handler;
}

Expand All @@ -153,7 +173,7 @@ namespace ccf
*/
Handler& set_default(HandleFunction f, ReadWrite read_write)
{
default_handler = {"", f, read_write};
default_handler = {"", f, read_write, this};
return default_handler.value();
}

Expand Down Expand Up @@ -190,23 +210,28 @@ namespace ccf

virtual void tick(std::chrono::milliseconds elapsed, size_t tx_count) {}

virtual std::optional<CallerId> valid_caller(
bool has_certs()
{
return certs != nullptr;
}

virtual CallerId get_caller_id(
jumaffre marked this conversation as resolved.
Show resolved Hide resolved
Store::Tx& tx, const std::vector<uint8_t>& caller)
{
if (certs == nullptr)
if (certs == nullptr || caller.empty())
{
return INVALID_ID;
}

if (caller.empty())
{
return {};
}

auto certs_view = tx.get_view(*certs);
auto caller_id = certs_view->get(caller);

return caller_id;
if (!caller_id.has_value())
{
return INVALID_ID;
}

return caller_id.value();
}

void set_consensus(kv::Consensus* c)
Expand Down
3 changes: 2 additions & 1 deletion src/node/rpc/memberfrontend.h
Original file line number Diff line number Diff line change
Expand Up @@ -885,7 +885,8 @@ namespace ccf
LOG_INFO_FMT("Created service");
return make_success(true);
};
install(MemberProcs::CREATE, json_adapter(create), Write);
install(MemberProcs::CREATE, json_adapter(create), Write)
.set_caller_auth_disabled(true);
}
};

Expand Down
Loading