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

Remove SGX JWT key filter and policy support #6450

Merged
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
2 changes: 1 addition & 1 deletion .snpcc_canary
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,4 @@
O \ o | /
/-xXx--//-----x=x--/-xXx--/---x---->>>--/
...
/\/\(-_-)
/\/\d(-_-)b
12 changes: 12 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,18 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/)
and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html).

## [6.0.0-dev0]

[6.0.0-dev0]: https://github.com/microsoft/CCF/releases/tag/6.0.0-dev0

### Changed

- The `set_jwt_issuer` governance action has been updated, and no longer accepts `key_filter` or `key_policy` arguments (#6450).

### Removed

- SGX Platform support.

## [5.0.4]

[5.0.4]: https://github.com/microsoft/CCF/releases/tag/ccf-5.0.4
Expand Down
42 changes: 0 additions & 42 deletions doc/build_apps/auth/jwt.rst
Original file line number Diff line number Diff line change
Expand Up @@ -137,48 +137,6 @@ Token signing keys are stored in the ``public:ccf.gov.jwt.public_signing_keys``

If an application uses multiple token issuers, then the ``public:ccf.gov.jwt.public_signing_key_issuer`` kv map which maps key IDs to issuers can be used to determine the issuer that a key belongs to.

Advanced issuer configuration
-----------------------------

CCF has special support for IdPs that issue tokens within SGX enclaves, for example MAA (`Microsoft Azure Attestation <https://docs.microsoft.com/en-us/azure/attestation/>`_).
The goal is to validate that a token has indeed been issued from an SGX enclave that has certain properties.
CCF supports the approach taken by MAA where the token signing key and certificate are generated inside the enclave and the certificate embeds evidence from the enclave platform in an X.509 extension (see Open Enclave's `oe_get_attestation_certificate_with_evidence() <https://openenclave.io/apidocs/v0.12/attester_8h_a2d7a05a906935c74a089d3b1240fad64.html#a2d7a05a906935c74a089d3b1240fad64>`_ for details).
In this model it is sufficient to validate the evidence of the signing certificates when storing them in CCF.
After the signing certificates have been stored, token validation follows the same methods as described in earlier sections.

CCF validates embedded SGX evidence if a key policy is given in the issuer metadata:

.. code-block:: json

{
"actions": [
{
"name": "set_jwt_issuer",
"args": {
"issuer": "https://shareduks.uks.attest.azure.net",
"key_filter": "sgx",
"key_policy": {
"sgx_claims": {
"signer_id": "5e5410aaf99a32e32df2a97d579e65f8310f274816ec4f34cedeeb1be410a526",
"attributes": "0300000000000000"
}
},
"auto_refresh": false
}
}
]
}

All claims contained in ``key_policy.sgx_claims`` must be identical to the ones embedded in the certificate.
Any attempt to add a certificate with mismatching claims in a ``set_jwt_public_signing_keys`` proposal for that issuer would result in failure.

.. note::

See Open Enclave's `oe_verify_evidence() <https://openenclave.io/apidocs/v0.12/verifier_8h_a5ad1a6314d2fe5b3470cb3a25c4c39df.html#a5ad1a6314d2fe5b3470cb3a25c4c39df>`_ for a list of available claim names and their meaning. Note that all claim values must be given hex-encoded.

Some IdPs, like MAA, advertise a mix of SGX and non-SGX signing certificates.
In this case, ``key_filter`` must be set to ``sgx`` such that only those certificates are stored which contain SGX evidence.

Extracting JWT metrics
----------------------

Expand Down
22 changes: 2 additions & 20 deletions doc/schemas/gov_openapi.json
Original file line number Diff line number Diff line change
Expand Up @@ -305,22 +305,10 @@
},
"JwtIssuerKeyFilter": {
"enum": [
"all",
"sgx"
"all"
],
"type": "string"
},
"JwtIssuerKeyPolicy": {
"properties": {
"sgx_claims": {
"$ref": "#/components/schemas/string_to_string"
}
},
"required": [
"sgx_claims"
],
"type": "object"
},
"JwtIssuerMetadata": {
"properties": {
"auto_refresh": {
Expand All @@ -331,14 +319,8 @@
},
"key_filter": {
"$ref": "#/components/schemas/JwtIssuerKeyFilter"
},
"key_policy": {
"$ref": "#/components/schemas/JwtIssuerKeyPolicy"
}
},
"required": [
"key_filter"
],
"type": "object"
},
"KeyIdInfo": {
Expand Down Expand Up @@ -1351,7 +1333,7 @@
"info": {
"description": "This API is used to submit and query proposals which affect CCF's public governance tables.",
"title": "CCF Governance API",
"version": "4.3.0"
"version": "4.4.0"
},
"openapi": "3.0.0",
"paths": {
Expand Down
32 changes: 6 additions & 26 deletions include/ccf/service/tables/jwt.h
Original file line number Diff line number Diff line change
Expand Up @@ -12,47 +12,27 @@

namespace ccf
{
struct JwtIssuerKeyPolicy
{
/** OE claim name -> hex-encoded claim value
See openenclave/attestation/verifier.h */
std::optional<std::map<std::string, std::string>> sgx_claims;

bool operator!=(const JwtIssuerKeyPolicy& rhs) const
{
return rhs.sgx_claims != sgx_claims;
}
};

DECLARE_JSON_TYPE(JwtIssuerKeyPolicy);
DECLARE_JSON_REQUIRED_FIELDS(JwtIssuerKeyPolicy, sgx_claims);

enum class JwtIssuerKeyFilter
{
All,
SGX
All
};

DECLARE_JSON_ENUM(
JwtIssuerKeyFilter,
{{JwtIssuerKeyFilter::All, "all"}, {JwtIssuerKeyFilter::SGX, "sgx"}});
DECLARE_JSON_ENUM(JwtIssuerKeyFilter, {{JwtIssuerKeyFilter::All, "all"}});

struct JwtIssuerMetadata
{
/// JWT issuer key filter
JwtIssuerKeyFilter key_filter;
/// Optional Key Policy
std::optional<JwtIssuerKeyPolicy> key_policy;
/// JWT issuer key filter, kept for compatibility with existing ledgers
JwtIssuerKeyFilter key_filter = JwtIssuerKeyFilter::All;
/// Optional CA bundle name used for authentication when auto-refreshing
std::optional<std::string> ca_cert_bundle_name;
/// Whether to auto-refresh keys from the issuer
bool auto_refresh = false;
};

DECLARE_JSON_TYPE_WITH_OPTIONAL_FIELDS(JwtIssuerMetadata);
DECLARE_JSON_REQUIRED_FIELDS(JwtIssuerMetadata, key_filter);
DECLARE_JSON_REQUIRED_FIELDS(JwtIssuerMetadata);
DECLARE_JSON_OPTIONAL_FIELDS(
JwtIssuerMetadata, key_policy, ca_cert_bundle_name, auto_refresh);
JwtIssuerMetadata, key_filter, ca_cert_bundle_name, auto_refresh);

using JwtIssuer = std::string;
using JwtKeyId = std::string;
Expand Down
16 changes: 0 additions & 16 deletions samples/constitutions/default/actions.js
Original file line number Diff line number Diff line change
Expand Up @@ -884,22 +884,6 @@ const actions = new Map([
checkType(args.issuer, "string", "issuer");
checkType(args.auto_refresh, "boolean?", "auto_refresh");
checkType(args.ca_cert_bundle_name, "string?", "ca_cert_bundle_name");
checkEnum(args.key_filter, ["all", "sgx"], "key_filter");
checkType(args.key_policy, "object?", "key_policy");
if (args.key_policy) {
checkType(
args.key_policy.sgx_claims,
"object?",
"key_policy.sgx_claims",
);
if (args.key_policy.sgx_claims) {
for (const [name, value] of Object.entries(
args.key_policy.sgx_claims,
)) {
checkType(value, "string", `key_policy.sgx_claims["${name}"]`);
}
}
}
checkType(args.jwks, "object?", "jwks");
if (args.jwks) {
checkJwks(args.jwks, "jwks");
Expand Down
6 changes: 0 additions & 6 deletions src/node/gov/handlers/service_state.h
Original file line number Diff line number Diff line change
Expand Up @@ -546,14 +546,8 @@ namespace ccf::gov::endpoints
const ccf::JwtIssuerMetadata& metadata) {
auto jwt_issuer = nlohmann::json::object();

jwt_issuer["keyFilter"] = metadata.key_filter;
jwt_issuer["autoRefresh"] = metadata.auto_refresh;

if (metadata.key_policy.has_value())
{
jwt_issuer["keyPolicy"] = metadata.key_policy.value();
}

if (metadata.ca_cert_bundle_name.has_value())
{
jwt_issuer["caCertBundleName"] =
Expand Down
105 changes: 9 additions & 96 deletions src/node/rpc/jwt_management.h
Original file line number Diff line number Diff line change
Expand Up @@ -9,17 +9,8 @@
#include "ccf/tx.h"
#include "http/http_jwt.h"

#ifdef SGX_ATTESTATION_VERIFICATION
# include <openenclave/attestation/verifier.h>
#endif

#include <set>
#include <sstream>
#if defined(INSIDE_ENCLAVE) && !defined(VIRTUAL_ENCLAVE)
# include <openenclave/enclave.h>
#elif defined(SGX_ATTESTATION_VERIFICATION)
# include <openenclave/host_verify.h>
#endif

namespace ccf
{
Expand Down Expand Up @@ -107,22 +98,6 @@ namespace ccf
});
}

#ifdef SGX_ATTESTATION_VERIFICATION
static oe_result_t oe_verify_attestation_certificate_with_evidence_cb(
oe_claim_t* claims, size_t claims_length, void* arg)
{
auto claims_map = (std::map<std::string, std::vector<uint8_t>>*)arg;
for (size_t i = 0; i < claims_length; i++)
{
std::string claim_name(claims[i].name);
std::vector<uint8_t> claim_value(
claims[i].value, claims[i].value + claims[i].value_size);
claims_map->emplace(std::move(claim_name), std::move(claim_value));
}
return OE_OK;
}
#endif

static bool set_jwt_public_signing_keys(
ccf::kv::Tx& tx,
const std::string& log_prefix,
Expand Down Expand Up @@ -171,83 +146,21 @@ namespace ccf
return false;
}

std::map<std::string, std::vector<uint8_t>> claims;
bool has_key_policy_sgx_claims = issuer_metadata.key_policy.has_value() &&
issuer_metadata.key_policy.value().sgx_claims.has_value() &&
!issuer_metadata.key_policy.value().sgx_claims.value().empty();
if (
issuer_metadata.key_filter == JwtIssuerKeyFilter::SGX ||
has_key_policy_sgx_claims)
try
{
#ifdef SGX_ATTESTATION_VERIFICATION
oe_verify_attestation_certificate_with_evidence(
der.data(),
der.size(),
oe_verify_attestation_certificate_with_evidence_cb,
&claims);
#else
LOG_FAIL_FMT("{}: SGX claims not supported", log_prefix);
return false;
#endif
ccf::crypto::make_unique_verifier(
(std::vector<uint8_t>)der); // throws on error
}

if (
issuer_metadata.key_filter == JwtIssuerKeyFilter::SGX && claims.empty())
catch (std::invalid_argument& exc)
{
LOG_INFO_FMT(
"{}: Skipping JWT signing key with kid {} (not OE "
"attested)",
LOG_FAIL_FMT(
"{}: JWKS kid {} has an invalid X.509 certificate: {}",
log_prefix,
kid);
continue;
kid,
exc.what());
return false;
}

if (has_key_policy_sgx_claims)
{
for (auto& [claim_name, expected_claim_val_hex] :
issuer_metadata.key_policy.value().sgx_claims.value())
{
if (claims.find(claim_name) == claims.end())
{
LOG_FAIL_FMT(
"{}: JWKS kid {} is missing the {} SGX claim",
log_prefix,
kid,
claim_name);
return false;
}
auto& actual_claim_val = claims[claim_name];
auto actual_claim_val_hex = ds::to_hex(actual_claim_val);
if (expected_claim_val_hex != actual_claim_val_hex)
{
LOG_FAIL_FMT(
"{}: JWKS kid {} has a mismatching {} SGX claim: {} != {}",
log_prefix,
kid,
claim_name,
expected_claim_val_hex,
actual_claim_val_hex);
return false;
}
}
}
else
{
try
{
ccf::crypto::make_unique_verifier(
(std::vector<uint8_t>)der); // throws on error
}
catch (std::invalid_argument& exc)
{
LOG_FAIL_FMT(
"{}: JWKS kid {} has an invalid X.509 certificate: {}",
log_prefix,
kid,
exc.what());
return false;
}
}
LOG_INFO_FMT("{}: Storing JWT signing key with kid {}", log_prefix, kid);
new_keys.emplace(kid, der);

Expand Down
2 changes: 1 addition & 1 deletion src/node/rpc/member_frontend.h
Original file line number Diff line number Diff line change
Expand Up @@ -599,7 +599,7 @@ namespace ccf
openapi_info.description =
"This API is used to submit and query proposals which affect CCF's "
"public governance tables.";
openapi_info.document_version = "4.3.0";
openapi_info.document_version = "4.4.0";
}

static std::optional<MemberId> get_caller_member_id(
Expand Down
5 changes: 3 additions & 2 deletions tests/infra/consortium.py
Original file line number Diff line number Diff line change
Expand Up @@ -610,9 +610,10 @@ def refresh_js_app_bytecode_cache(self, remote_node):
def set_jwt_issuer(self, remote_node, json_path):
obj = slurp_json(json_path)
args = {
# Key filter is no longer used, but kept for compatibility with
# lts_compatibility tests.
"key_filter": "all",
"issuer": obj["issuer"],
"key_filter": obj.get("key_filter", "all"),
"key_policy": obj.get("key_policy"),
"ca_cert_bundle_name": obj.get("ca_cert_bundle_name"),
"auto_refresh": obj.get("auto_refresh", False),
"jwks": obj.get("jwks"),
Expand Down
Loading