Skip to content

Rego policy support #260

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

Open
wants to merge 18 commits into
base: main
Choose a base branch
from
Open
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
17 changes: 17 additions & 0 deletions DEVELOPMENT.md
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,8 @@ export PLATFORM=virtual

The application expects the [configuration](docs/configuration.md) to be submitted via the CCF proposals, for that you could use the CLI.

To set a JavaScript policy:

```sh
echo <<< EOL
{
Expand All @@ -113,6 +115,21 @@ echo <<< EOL
}
EOL >> test-config.json;

To set a Rego policy:

```sh
echo <<< EOL
{
"policy": {
"policyRego": "\npackage policy\n\nissuer_allowed if {\n input.phdr.cwt.iss == "did:x509:0:sha256:HnwZ4lezuxq_GVcl_Sk7YWW170qAD0DZBLXilXet0jg::eku:1.3.6.1.4.1.311.10.3.13"\n}\n\nseconds_since_epoch := time.now_ns() / 1000000000\n\niat_in_the_past if {\n input.phdr.cwt.iat < seconds_since_epoch\n}\n\nsvn_undefined if {\n not input.phdr.cwt.svn\n}\n\nsvn_positive if {\n input.phdr.cwt.svn >= 0\n}\n\nallow if {\n issuer_allowed\n iat_in_the_past\n svn_undefined\n}\n\nallow if {\n issuer_allowed\n iat_in_the_past\n svn_positive\n}\n"
},
"authentication": {
"allowUnauthenticated": true
}
}
EOL >> test-config.json
```

./pyscitt.sh governance propose_configuration -k --url https://localhost:8000 --member-key workspace/member0_privk.pem --member-cert workspace/member0_cert.pem --configuration test-config.json
```

Expand Down
20 changes: 20 additions & 0 deletions app/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@

cmake_minimum_required(VERSION 3.16)

include(FetchContent)

set(CMAKE_EXPORT_COMPILE_COMMANDS ON)

if((NOT CMAKE_CXX_COMPILER)
Expand Down Expand Up @@ -31,6 +33,22 @@ set(CMAKE_GENERATED_COMMENT
)
configure_file(src/generated/constants.h.in src/generated/constants.h @ONLY)

# Conflicts with CCF snmalloc harcoding
set(TRIESTE_USE_SNMALLOC OFF)

FetchContent_Declare(
regocpp
GIT_REPOSITORY https://github.com/microsoft/rego-cpp
GIT_TAG 5a6b47653fa212de6cafe1f871c6a6511e59f7b5
)
set(CMAKE_POLICY_DEFAULT_CMP0077 NEW)
FetchContent_MakeAvailable(regocpp)

set_property(TARGET rego PROPERTY POSITION_INDEPENDENT_CODE ON)
set_property(TARGET yaml PROPERTY POSITION_INDEPENDENT_CODE ON)
set_property(TARGET json PROPERTY POSITION_INDEPENDENT_CODE ON)
set_property(TARGET re2 PROPERTY POSITION_INDEPENDENT_CODE ON)

# add CCF dependencies
# add linking options
# add SAN options if CCF is built with them
Expand All @@ -45,6 +63,7 @@ if (COMPILE_TARGET STREQUAL "virtual")
elseif (COMPILE_TARGET STREQUAL "snp")
set(SCITT_TARGET scitt.snp)
endif ()
target_link_system_libraries(${SCITT_TARGET} PRIVATE regocpp::rego)

# The INCLUDE_DIRS argument of the add_ccf_app macro adds directories with
# -isystem, which silences any warnings in them, which is undesirable for our
Expand Down Expand Up @@ -120,6 +139,7 @@ if (BUILD_TESTS)
rapidcheck_gtest
t_cose.host
qcbor.host
regocpp::rego

ccf_endpoints.host
ccf_kv.host
Expand Down
1 change: 1 addition & 0 deletions app/constitution/scitt.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ actions.set("set_scitt_configuration",
}
}
checkType(args.configuration.policy.policyScript, "string?", "configuration.policy.policyScript");
checkType(args.configuration.policy.policyRego, "string?", "configuration.policy.policyRego");
}

checkType(args.configuration.authentication, "object?", "configuration.authentication");
Expand Down
48 changes: 3 additions & 45 deletions app/src/cose.h
Original file line number Diff line number Diff line change
Expand Up @@ -522,8 +522,8 @@ namespace scitt::cose
return parsed;
}

static std::tuple<ProtectedHeader, UnprotectedHeader> decode_headers(
const std::vector<uint8_t>& cose_sign1)
[[maybe_unused]] static std::tuple<ProtectedHeader, UnprotectedHeader>
decode_headers(const std::vector<uint8_t>& cose_sign1)
{
QCBORError qcbor_result;

Expand Down Expand Up @@ -563,55 +563,13 @@ namespace scitt::cose
{}
};

/**
* Extract the bstr fields from a COSE Sign1.
*
* Returns an array containing the protected headers, the payload and the
* signature.
*/
static std::array<std::span<const uint8_t>, 3> extract_sign1_fields(
std::span<const uint8_t> cose_sign1)
{
QCBORDecodeContext ctx;
QCBORDecode_Init(
&ctx, cbor::from_bytes(cose_sign1), QCBOR_DECODE_MODE_NORMAL);

QCBORDecode_EnterArray(&ctx, nullptr);

UsefulBufC bstr_item;
// protected headers
QCBORDecode_GetByteString(&ctx, &bstr_item);
auto phdrs = cbor::as_span(bstr_item);

QCBORItem item;
// skip unprotected header
QCBORDecode_VGetNextConsume(&ctx, &item);

// payload
QCBORDecode_GetByteString(&ctx, &bstr_item);
auto payload = cbor::as_span(bstr_item);

// signature
QCBORDecode_GetByteString(&ctx, &bstr_item);
auto signature = cbor::as_span(bstr_item);

QCBORDecode_ExitArray(&ctx);
auto error = QCBORDecode_Finish(&ctx);
if (error)
{
throw std::runtime_error("Failed to decode COSE_Sign1");
}

return {phdrs, payload, signature};
}

/**
* Verify the signature of a COSE Sign1 message using the given public key.
*
* Beyond the basic verification of key usage and the signature
* itself, no particular validation of the message is done.
*/
static void verify(
[[maybe_unused]] static void verify(
const std::vector<uint8_t>& cose_sign1,
const PublicKey& key,
bool allow_unknown_crit = false)
Expand Down
90 changes: 0 additions & 90 deletions app/src/did/document.h
Original file line number Diff line number Diff line change
Expand Up @@ -108,96 +108,6 @@ namespace scitt::did
DIDAssertionMethodError(msg)
{}
};

static DidVerificationMethod find_assertion_method_in_did_document(
const DidDocument& did_doc,
const std::optional<std::string>& assertion_method_id)
{
std::unordered_map<std::string, const DidVerificationMethod*> id_to_method;
if (did_doc.verification_method.has_value())
{
for (auto& m : did_doc.verification_method.value())
{
id_to_method[m.id] = &m;
}
}

const DidVerificationMethod* method = nullptr;
std::optional<std::string> method_id;

if (assertion_method_id.has_value())
{
for (auto& m : did_doc.assertion_method)
{
if (std::holds_alternative<std::string>(m))
{
if (std::get<std::string>(m) == *assertion_method_id)
{
method_id = *assertion_method_id;
break;
}
}
else
{
if (std::get<DidVerificationMethod>(m).id == *assertion_method_id)
{
method = &std::get<DidVerificationMethod>(m);
break;
}
}
}
}
else
{
if (did_doc.assertion_method.size() != 1)
{
throw DIDAssertionMethodNotFoundError(
"DID document must have exactly one assertion method if no assertion "
"method id is provided");
}
auto& m = did_doc.assertion_method[0];
if (std::holds_alternative<std::string>(m))
{
method_id = std::get<std::string>(m);
}
else
{
method = &std::get<DidVerificationMethod>(m);
}
}

if (method_id.has_value())
{
if (!contains(id_to_method, method_id.value()))
{
throw DIDAssertionMethodNotFoundError(
"DID document assertion method references unknown verification "
"method");
}
method = id_to_method[*method_id];
}

if (!method)
{
throw DIDAssertionMethodNotFoundError(
"DID document assertion method not found");
}
return *method;
}

static Jwk find_assertion_method_jwk_in_did_document(
const DidDocument& did_doc,
const std::optional<std::string>& assertion_method_id)
{
auto method =
find_assertion_method_in_did_document(did_doc, assertion_method_id);
if (!method.public_key_jwk.has_value())
{
throw DIDAssertionMethodUnsupportedError(
"DID document assertion method is missing publicKeyJwk");
}
return method.public_key_jwk.value();
}
}

// Alternative DID document spec imported from CCF/src/node/did.h
Expand Down
10 changes: 5 additions & 5 deletions app/src/http_error.h
Original file line number Diff line number Diff line change
Expand Up @@ -121,24 +121,24 @@ namespace scitt
* Create an adapter around an existing EndpointFunction to handle thrown
* HTTPError exceptions.
*/
static ccf::endpoints::EndpointFunction error_adapter(
[[maybe_unused]] static ccf::endpoints::EndpointFunction error_adapter(
ccf::endpoints::EndpointFunction fn)
{
return generic_error_adapter<
ccf::endpoints::EndpointFunction,
ccf::endpoints::EndpointContext>(fn);
}

static ccf::endpoints::ReadOnlyEndpointFunction error_read_only_adapter(
ccf::endpoints::ReadOnlyEndpointFunction fn)
[[maybe_unused]] static ccf::endpoints::ReadOnlyEndpointFunction
error_read_only_adapter(ccf::endpoints::ReadOnlyEndpointFunction fn)
{
return generic_error_adapter<
ccf::endpoints::ReadOnlyEndpointFunction,
ccf::endpoints::ReadOnlyEndpointContext>(fn);
}

static ccf::endpoints::CommandEndpointFunction error_command_adapter(
ccf::endpoints::CommandEndpointFunction fn)
[[maybe_unused]] static ccf::endpoints::CommandEndpointFunction
error_command_adapter(ccf::endpoints::CommandEndpointFunction fn)
{
return generic_error_adapter<
ccf::endpoints::CommandEndpointFunction,
Expand Down
9 changes: 8 additions & 1 deletion app/src/kv_types.h
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,11 @@ namespace scitt
*/
std::optional<PolicyScript> policy_script;

/**
* Rego policy to be applied to each incoming entry.
*/
std::optional<PolicyRego> policy_rego;

std::vector<std::string> get_accepted_algorithms() const
{
if (accepted_algorithms.has_value())
Expand Down Expand Up @@ -155,7 +160,9 @@ namespace scitt
accepted_algorithms,
"acceptedAlgorithms",
policy_script,
"policyScript");
"policyScript",
policy_rego,
"policyRego");

DECLARE_JSON_TYPE_WITH_OPTIONAL_FIELDS(Configuration::Authentication::JWT);
DECLARE_JSON_REQUIRED_FIELDS(Configuration::Authentication::JWT);
Expand Down
51 changes: 37 additions & 14 deletions app/src/main.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -277,23 +277,46 @@ namespace scitt
throw BadRequestError(errors::InvalidInput, e.what());
}

if (cfg.policy.policy_script.has_value())
if (
cfg.policy.policy_script.has_value() ||
cfg.policy.policy_rego.has_value())
{
const auto policy_violation_reason = check_for_policy_violations(
cfg.policy.policy_script.value(),
"configured_policy",
signed_statement_profile,
phdr);
if (policy_violation_reason.has_value())
if (cfg.policy.policy_script.has_value())
{
SCITT_DEBUG(
"Policy check failed: {}", policy_violation_reason.value());
throw BadRequestError(
errors::PolicyFailed,
fmt::format(
"Policy was not met: {}", policy_violation_reason.value()));
const auto policy_violation_reason = check_for_policy_violations_js(
cfg.policy.policy_script.value(),
"configured_policy",
signed_statement_profile,
phdr);
if (policy_violation_reason.has_value())
{
SCITT_DEBUG(
"Policy check failed: {}", policy_violation_reason.value());
throw BadRequestError(
errors::PolicyFailed,
fmt::format(
"Policy was not met: {}", policy_violation_reason.value()));
}
}
SCITT_DEBUG("Policy script check passed");
if (cfg.policy.policy_rego.has_value())
{
const auto policy_violation_reason =
check_for_policy_violations_rego(
cfg.policy.policy_rego.value(), signed_statement_profile, phdr);
if (policy_violation_reason.has_value())
{
SCITT_DEBUG(
"Policy check failed (rego): {}",
policy_violation_reason.value());
throw BadRequestError(
errors::PolicyFailed,
fmt::format(
"Policy was not met (rego): {}",
policy_violation_reason.value()));
}
}
SCITT_DEBUG("Policy check passed");
SCITT_DEBUG("Policy check passed (rego)");
}
else
{
Expand Down
Loading
Loading