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

Mandate that signed payloads are endpoint-specific and timestamped in Programmability sample #6285

Merged
merged 18 commits into from
Jun 26, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
- `::consensus` is now `ccf::consensus`
- `::tls` is now `ccf::tls`
- The `programmability` sample app now demonstrates how applications can define their own extensions, creating bindings between C++ and JS state, and allowing JS endpoints to call functions implemented in C++.
- Introduce `DynamicJSEndpointRegistry::record_action_for_audit_v1` and `DynamicJSEndpointRegistry::check_action_not_replayed_v1` to allow an application making use of the programmability feature to easily implement auditability, and protect users allowed to update the application against replay attacks (#6285).
- Endpoints now support a `ToBackup` redirection strategy, for requests which should never be executed on a primary. These must also be read-only. These are configured similar to `ToPrimary` endpoints, with a `to_backup` object (specifying by-role or statically-addressed targets) in each node's configuration.

### Removed
Expand Down
3 changes: 3 additions & 0 deletions doc/build_apps/api.rst
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,9 @@ Policies
.. doxygenvariable:: ccf::jwt_auth_policy
:project: CCF

.. doxygenvariable:: ccf::TypedUserCOSESign1AuthnPolicy
:project: CCF

Identities
~~~~~~~~~~

Expand Down
54 changes: 54 additions & 0 deletions include/ccf/base_endpoint_registry.h
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,47 @@ namespace ccf
}
}

/** Lists possible reasons for an ApiResult::InvalidArgs being return in @c
* ccf::BaseEndpointRegistry
*/
enum class InvalidArgsReason
{
NoReason = 0,
/** Views start at 1 (one) in CCF */
ViewSmallerThanOne,
/** Action has already been applied on this instance */
ActionAlreadyApplied,
/** Action created_at is older than the median of recent action */
StaleActionCreatedTimestamp,
};

constexpr char const* invalid_args_reason_to_str(InvalidArgsReason reason)
{
switch (reason)
{
case InvalidArgsReason::NoReason:
{
return "NoReason";
}
case InvalidArgsReason::ViewSmallerThanOne:
{
return "ViewSmallerThanOne";
}
case InvalidArgsReason::ActionAlreadyApplied:
{
return "ActionAlreadyApplied";
}
case InvalidArgsReason::StaleActionCreatedTimestamp:
{
return "StaleActionCreatedTimestamp";
}
default:
{
return "Unhandled InvalidArgsReason";
}
}
}

/** Extends the basic @ref ccf::endpoints::EndpointRegistry with helper API
* methods for retrieving core CCF properties.
*
Expand Down Expand Up @@ -96,6 +137,19 @@ namespace ccf
ApiResult get_view_history_v1(
std::vector<ccf::TxID>& history, ccf::View since = 1);

/** Get the history of the consensus view changes.
*
* Returns the history of view changes since the given view, which defaults
* to the start of time.
*
* A view change is characterised by the first sequence number in the new
* view.
*/
ApiResult get_view_history_v2(
std::vector<ccf::TxID>& history,
ccf::View since,
ccf::InvalidArgsReason& reason);
achamayou marked this conversation as resolved.
Show resolved Hide resolved

/** Get the status of a transaction by ID, provided as a view+seqno pair.
*
* Note that this value is the node's local understanding of the status
Expand Down
62 changes: 58 additions & 4 deletions include/ccf/endpoints/authentication/cose_auth.h
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,12 @@ namespace ccf
uint64_t gov_msg_created_at;
};

struct TimestampedProtectedHeader : ProtectedHeader
{
std::optional<std::string> msg_type;
std::optional<uint64_t> msg_created_at;
};

struct COSESign1AuthnIdentity : public AuthnIdentity
{
/** COSE Content */
Expand Down Expand Up @@ -83,15 +89,15 @@ namespace ccf
crypto::Pem user_cert;

/** COSE Protected Header */
ProtectedHeader protected_header;
TimestampedProtectedHeader protected_header;

UserCOSESign1AuthnIdentity(
const std::span<const uint8_t>& content_,
const std::span<const uint8_t>& envelope_,
const std::span<const uint8_t>& signature_,
const UserId& user_id_,
const crypto::Pem& user_cert_,
const ProtectedHeader& protected_header_) :
const TimestampedProtectedHeader& protected_header_) :
COSESign1AuthnIdentity(content_, envelope_, signature_),
user_id(user_id_),
user_cert(user_cert_),
Expand Down Expand Up @@ -163,16 +169,32 @@ namespace ccf
};

/** User COSE Sign1 Authentication Policy
*
* Allows parametrising two optional protected header entries
* which are exposed to the endpoint if present.
*/
class UserCOSESign1AuthnPolicy : public AuthnPolicy
{
std::string msg_type_name;
std::string msg_created_at_name;

protected:
static const OpenAPISecuritySchema security_schema;

virtual std::unique_ptr<UserCOSESign1AuthnIdentity> _authenticate(
kv::ReadOnlyTx& tx,
const std::shared_ptr<ccf::RpcContext>& ctx,
std::string& error_reason);

public:
static constexpr auto SECURITY_SCHEME_NAME = "user_cose_sign1";

UserCOSESign1AuthnPolicy();
UserCOSESign1AuthnPolicy(
const std::string& msg_type_name_ = "ccf.msg.type",
const std::string& msg_created_at_name_ = "ccf.msg.created_at") :
msg_type_name(msg_type_name_),
msg_created_at_name(msg_created_at_name_)
{}
~UserCOSESign1AuthnPolicy();

std::unique_ptr<AuthnIdentity> authenticate(
Expand All @@ -195,4 +217,36 @@ namespace ccf
return SECURITY_SCHEME_NAME;
}
};
}

/** Typed User COSE Sign1 Authentication Policy
*
* Extends UserCOSESign1AuthPolicy, to require that a specific message
* type is present in the corresponding protected header.
*/
class TypedUserCOSESign1AuthnPolicy : public UserCOSESign1AuthnPolicy
{
private:
std::string expected_msg_type;

public:
static constexpr auto SECURITY_SCHEME_NAME = "typed_user_cose_sign1";

TypedUserCOSESign1AuthnPolicy(
const std::string& expected_msg_type_,
const std::string& msg_type_name_ = "ccf.msg.type",
const std::string& msg_created_at_name_ = "ccf.msg.created_at") :
UserCOSESign1AuthnPolicy(msg_type_name_, msg_created_at_name_),
expected_msg_type(expected_msg_type_)
{}

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;
}
};
}
32 changes: 32 additions & 0 deletions include/ccf/js/audit_format.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the Apache 2.0 License.

#pragma once
#include "ccf/ds/json.h"

#include <vector>

namespace ccf
{
enum class ActionFormat
{
COSE = 0,
JSON = 1
};
DECLARE_JSON_ENUM(
ActionFormat, {{ActionFormat::COSE, "COSE"}, {ActionFormat::JSON, "JSON"}});

struct AuditInfo
{
ActionFormat format;
// Deliberately a string and not a ccf::UserId to allow extended usage, for
// example with OpenID
std::string user_id;
// Format left to the application, Verb + URL with some of kind of
// versioning is recommended
std::string action_name;
};

DECLARE_JSON_TYPE(AuditInfo)
DECLARE_JSON_REQUIRED_FIELDS(AuditInfo, format, user_id, action_name)
}
26 changes: 26 additions & 0 deletions include/ccf/js/registry.h
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
// CCF
#include "ccf/app_interface.h"
#include "ccf/endpoint.h"
#include "ccf/js/audit_format.h"
#include "ccf/js/bundle.h"
#include "ccf/js/core/context.h"
#include "ccf/js/interpreter_cache_interface.h"
Expand Down Expand Up @@ -51,6 +52,9 @@ namespace ccf::js
std::string modules_quickjs_version_map;
std::string modules_quickjs_bytecode_map;
std::string runtime_options_map;
std::string recent_actions_map;
std::string audit_input_map;
std::string audit_info_map;

ccf::js::NamespaceRestriction namespace_restriction;

Expand Down Expand Up @@ -127,6 +131,28 @@ namespace ccf::js
ccf::ApiResult get_js_runtime_options_v1(
ccf::JSRuntimeOptions& options, kv::ReadOnlyTx& tx);

/**
* Record action details by storing them in KV maps using a common format,
* for the purposes of offline audit using the ledger.
*/
ccf::ApiResult record_action_for_audit_v1(
kv::Tx& tx,
ccf::ActionFormat format,
const std::string& user_id,
const std::string& action_name,
const std::vector<uint8_t>& action_body);

/**
* Check an action is not being replayed, by looking it up
* in the history of recent actions. To place an upper bound on the history
* size, an authenticated timestamp (@p created_at) is required.
*/
ccf::ApiResult check_action_not_replayed_v1(
kv::Tx& tx,
uint64_t created_at,
const std::span<const uint8_t> action,
ccf::InvalidArgsReason& reason);

/// \defgroup Overrides for base EndpointRegistry functions, looking up JS
/// endpoints before delegating to base implementation.
///@{
Expand Down
40 changes: 0 additions & 40 deletions samples/apps/programmability/audit_info.h

This file was deleted.

Loading