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

did:x509 issuer support in IETF profile #206

Merged
merged 31 commits into from
Aug 8, 2024
Merged
Show file tree
Hide file tree
Changes from 26 commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
1d7e3ad
Minimal demo of JS eval
eddyashton Jul 23, 2024
688c453
Confirming function access, return value handling
eddyashton Jul 23, 2024
a56c5f0
Convert COSE protected headers to a JSON object, pass to exported JS …
eddyashton Jul 24, 2024
3209756
Load policy script from configuration
eddyashton Jul 24, 2024
bcd64f7
Convert to PEM before inserting into JS
eddyashton Jul 24, 2024
cacbc15
Testing
eddyashton Jul 24, 2024
520b664
Cosntitution validation of new field
eddyashton Jul 24, 2024
32ffc95
Minor cleanups
eddyashton Jul 24, 2024
6f138a9
Remove debug logging
eddyashton Jul 24, 2024
588ef9a
Placate mypy
eddyashton Jul 25, 2024
2957528
Implement PR suggestions: Document config entry, return failure reaso…
eddyashton Jul 25, 2024
048d539
Cleaner debug logging
eddyashton Jul 25, 2024
256496a
Update trivial_false test with new return type
eddyashton Jul 26, 2024
df801e1
Different formatter -_-
eddyashton Jul 26, 2024
6924b88
Merge branch 'main' of https://github.com/microsoft/scitt-ccf-ledger …
eddyashton Jul 26, 2024
2180d5d
Remove redundant breaks
eddyashton Jul 26, 2024
e73a4f6
Update app/src/policy_engine.h
eddyashton Jul 26, 2024
e3453ff
Update test/test_configuration.py
eddyashton Jul 26, 2024
511d2fc
wip
achamayou Jul 30, 2024
bf31fde
wip
achamayou Jul 30, 2024
1970079
more wip
achamayou Aug 2, 2024
bdf2249
Add EKU check to test
achamayou Aug 5, 2024
bb1d36a
DID model mismatch
achamayou Aug 6, 2024
65ca9ce
key comparison
achamayou Aug 6, 2024
da6231d
fmt
achamayou Aug 7, 2024
088a6f0
Merge branch 'main' into didx509
achamayou Aug 7, 2024
f9e8343
Merge branch 'main' into didx509
achamayou Aug 7, 2024
ea80e71
Split out didx509 validation
achamayou Aug 7, 2024
ea968a4
rename inject_eku
achamayou Aug 7, 2024
3b1cc10
Remove stale comment
achamayou Aug 7, 2024
0249632
Expand tests
achamayou Aug 8, 2024
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 app/constitution/scitt.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ actions.set("set_scitt_configuration",
checkType(alg, "string", `configuration.policy.accepted_did_issuers[${i}]`);
}
}
checkType(args.configuration.policy.policy_script, "string?", "configuration.policy.policy_script");
}

checkType(args.configuration.authentication, "object?", "configuration.authentication");
Expand Down
2 changes: 2 additions & 0 deletions app/src/constants.h
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@ namespace scitt
const std::string NoPrefixTree = "NoPrefixTree";
const std::string NotFound = "NotFound";
const std::string OperationExpired = "OperationExpired";
const std::string PolicyError = "PolicyError";
const std::string PolicyFailed = "PolicyFailed";
} // namespace errors

} // namespace scitt
50 changes: 50 additions & 0 deletions app/src/did/document.h
Original file line number Diff line number Diff line change
Expand Up @@ -199,3 +199,53 @@ namespace scitt::did
return method.public_key_jwk.value();
}
}

// Alternative DID document spec imported from CCF/src/node/did.h
// Unlike scitt::did::DidDocument, this expects a single string for
// assertion_method and leaves the JWK parsing to specific sub-type to the
// caller based on the kty, rather than expose a single merged type where every
// field is optional. This is needed now for compatibility with didx509cpp, but
// the types should be merged eventually if they are still both needed.
namespace scitt::did::alt
{
// From https://www.w3.org/TR/did-core.
// Note that the types defined in this file do not exhaustively cover
// all fields and types from the spec.
struct DIDDocumentVerificationMethod
{
std::string id;
std::string type;
std::string controller;
std::optional<nlohmann::json> public_key_jwk = std::nullopt;

bool operator==(const DIDDocumentVerificationMethod&) const = default;
};
DECLARE_JSON_TYPE_WITH_OPTIONAL_FIELDS(DIDDocumentVerificationMethod);
DECLARE_JSON_REQUIRED_FIELDS(
DIDDocumentVerificationMethod, id, type, controller);
DECLARE_JSON_OPTIONAL_FIELDS_WITH_RENAMES(
DIDDocumentVerificationMethod, public_key_jwk, "publicKeyJwk");

struct DIDDocument
{
std::string id;
std::string context;
std::string type;
std::vector<DIDDocumentVerificationMethod> verification_method = {};
nlohmann::json assertion_method = {};

bool operator==(const DIDDocument&) const = default;
};
DECLARE_JSON_TYPE_WITH_OPTIONAL_FIELDS(DIDDocument);
DECLARE_JSON_REQUIRED_FIELDS(DIDDocument, id);
DECLARE_JSON_OPTIONAL_FIELDS_WITH_RENAMES(
DIDDocument,
context,
"@context",
type,
"type",
verification_method,
"verificationMethod",
assertion_method,
"assertionMethod");
}
11 changes: 10 additions & 1 deletion app/src/kv_types.h
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
#pragma once
#include "did/document.h"
#include "odata_error.h"
#include "policy_engine.h"
#include "signature_algorithms.h"

#include <ccf/crypto/hash_provider.h>
Expand Down Expand Up @@ -116,6 +117,11 @@ namespace scitt
*/
std::optional<std::vector<std::string>> accepted_did_issuers;

/**
* Script defining executable policy to be applied to each incoming entry.
*/
std::optional<PolicyScript> policy_script;

std::vector<std::string> get_accepted_algorithms() const
{
if (accepted_algorithms.has_value())
Expand Down Expand Up @@ -169,7 +175,10 @@ namespace scitt
DECLARE_JSON_TYPE_WITH_OPTIONAL_FIELDS(Configuration::Policy);
DECLARE_JSON_REQUIRED_FIELDS(Configuration::Policy);
DECLARE_JSON_OPTIONAL_FIELDS(
Configuration::Policy, accepted_algorithms, accepted_did_issuers);
Configuration::Policy,
accepted_algorithms,
accepted_did_issuers,
policy_script);

DECLARE_JSON_TYPE_WITH_OPTIONAL_FIELDS(Configuration::Authentication::JWT);
DECLARE_JSON_REQUIRED_FIELDS(Configuration::Authentication::JWT);
Expand Down
28 changes: 27 additions & 1 deletion app/src/main.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
#include "http_error.h"
#include "kv_types.h"
#include "operations_endpoints.h"
#include "policy_engine.h"
#include "receipt.h"
#include "service_endpoints.h"
#include "tracing.h"
Expand Down Expand Up @@ -211,10 +212,12 @@ namespace scitt
.value_or(Configuration{});

ClaimProfile claim_profile;
cose::ProtectedHeader phdr;
cose::UnprotectedHeader uhdr;
try
{
SCITT_DEBUG("Verify submitted claim");
claim_profile = verifier->verify_claim(
std::tie(claim_profile, phdr, uhdr) = verifier->verify_claim(
body, ctx.tx, host_time, DID_RESOLUTION_CACHE_EXPIRY, cfg);
}
catch (const did::DIDMethodNotSupportedError& e)
Expand Down Expand Up @@ -248,6 +251,29 @@ namespace scitt
#endif
}

if (cfg.policy.policy_script.has_value())
{
const auto policy_violation_reason = check_for_policy_violations(
cfg.policy.policy_script.value(),
"configured_policy",
claim_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 check passed");
}
else
{
SCITT_DEBUG("No policy applied");
}

// TODO: Apply further acceptance policies.
achamayou marked this conversation as resolved.
Show resolved Hide resolved

auto service = ctx.tx.template ro<ccf::Service>(ccf::Tables::SERVICE);
Expand Down
229 changes: 229 additions & 0 deletions app/src/policy_engine.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,229 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

#pragma once

#include "cose.h"
#include "http_error.h"
#include "profiles.h"
#include "tracing.h"

#include <ccf/js/common_context.h>
#include <string>

namespace scitt
{
using PolicyScript = std::string;

namespace js
{
static inline ccf::js::core::JSWrappedValue claim_profile_to_js_val(
ccf::js::core::Context& ctx, ClaimProfile claim_profile)
{
switch (claim_profile)
{
case ClaimProfile::IETF:
{
return ctx.new_string("IETF");
}
case ClaimProfile::X509:
{
return ctx.new_string("X509");
}
case ClaimProfile::Notary:
{
return ctx.new_string("Notary");
}
default:
{
throw std::logic_error("Unhandled ClaimProfile value");
}
}
}

static inline ccf::js::core::JSWrappedValue protected_headers_to_js_val(
ccf::js::core::Context& ctx, const scitt::cose::ProtectedHeader& phdr)
{
auto obj = ctx.new_obj();

// Vanilla SCITT protected header parameters
{
if (phdr.alg.has_value())
{
obj.set_int64("alg", phdr.alg.value());
}

if (phdr.crit.has_value())
{
auto crit_array = ctx.new_array();
size_t i = 0;

for (const auto& e : phdr.crit.value())
{
if (std::holds_alternative<int64_t>(e))
{
crit_array.set_at_index(
i++,
ccf::js::core::JSWrappedValue(
ctx, JS_NewInt64(ctx, std::get<int64_t>(e))));
}
else if (std::holds_alternative<std::string>(e))
{
crit_array.set_at_index(
i++, ctx.new_string(std::get<std::string>(e)));
}
}

obj.set("crit", std::move(crit_array));
}

if (phdr.kid.has_value())
{
obj.set("kid", ctx.new_string(phdr.kid.value()));
}

if (phdr.issuer.has_value())
{
obj.set("issuer", ctx.new_string(phdr.issuer.value()));
}

if (phdr.feed.has_value())
{
obj.set("feed", ctx.new_string(phdr.feed.value()));
}

if (phdr.cty.has_value())
{
if (std::holds_alternative<int64_t>(phdr.cty.value()))
{
obj.set_int64("cty", std::get<int64_t>(phdr.cty.value()));
}
else if (std::holds_alternative<std::string>(phdr.cty.value()))
{
obj.set(
"cty", ctx.new_string(std::get<std::string>(phdr.cty.value())));
}
}

if (phdr.x5chain.has_value())
{
auto x5_array = ctx.new_array();
size_t i = 0;

for (const auto& der_cert : phdr.x5chain.value())
{
auto pem = ccf::crypto::cert_der_to_pem(der_cert);
x5_array.set_at_index(i++, ctx.new_string(pem.str()));
}

obj.set("x5chain", std::move(x5_array));
}
}

// Extra Notary protected header parameters.
{
if (phdr.notary_signing_scheme.has_value())
{
obj.set(
"notary_signing_scheme",
ctx.new_string(phdr.notary_signing_scheme.value()));
}

if (phdr.notary_signing_time.has_value())
{
obj.set_int64(
"notary_signing_time", phdr.notary_signing_time.value());
}

if (phdr.notary_authentic_signing_time.has_value())
{
obj.set_int64(
"notary_authentic_signing_time",
phdr.notary_authentic_signing_time.value());
}

if (phdr.notary_expiry.has_value())
{
obj.set_int64("notary_expiry", phdr.notary_expiry.value());
}
}

return obj;
}

static inline std::optional<std::string> apply_js_policy(
const PolicyScript& script,
const std::string& policy_name,
ClaimProfile claim_profile,
const scitt::cose::ProtectedHeader& phdr)
{
// Allow the policy to access common globals (including shims for
// builtins) like "console", "ccf.crypto"
ccf::js::CommonContext interpreter(ccf::js::TxAccess::APP_RO);

ccf::js::core::JSWrappedValue apply_func;
try
{
apply_func =
interpreter.get_exported_function(script, "apply", policy_name);
}
catch (const std::exception& e)
{
throw BadRequestError(
scitt::errors::PolicyError,
fmt::format("Invalid policy module: {}", e.what()));
}

auto profile_val = claim_profile_to_js_val(interpreter, claim_profile);
auto phdr_val = protected_headers_to_js_val(interpreter, phdr);

const auto result = interpreter.call_with_rt_options(
apply_func,
{profile_val, phdr_val},
std::nullopt, // TODO: add runtime limits (heap, stack, time)
ccf::js::core::RuntimeLimitsPolicy::NONE);

if (result.is_exception())
{
auto [reason, trace] = interpreter.error_message();

throw BadRequestError(
scitt::errors::PolicyError,
fmt::format(
"Error while applying policy: {}\n{}",
reason,
trace.value_or("<no trace>")));
}

if (result.is_str())
{
return interpreter.to_str(result);
}

// Note this does JS-style truthy conversion, so lots of truthy values may
// become true here
if (result.is_true())
{
return std::nullopt;
}

throw BadRequestError(
scitt::errors::PolicyError,
fmt::format(
"Unexpected return value from policy: {}",
interpreter.to_str(result)));
}
}

// Returns nullopt for success, else a string describing why the policy was
// refused. May also throw if given invalid policies, or policy execution
// throws.
static inline std::optional<std::string> check_for_policy_violations(
const PolicyScript& script,
const std::string& policy_name,
ClaimProfile claim_profile,
const cose::ProtectedHeader& phdr)
{
return js::apply_js_policy(script, policy_name, claim_profile, phdr);
}
}
Loading
Loading