зеркало из https://github.com/microsoft/CCF.git
JWT auth proposal types and kv maps (#1851)
This commit is contained in:
Родитель
efc3527b57
Коммит
70b09e53cf
|
@ -716,6 +716,12 @@ if(BUILD_TESTS)
|
|||
${CCF_DIR}/src/runtime_config/gov_ca_cert_mismatch.lua
|
||||
CONSENSUS cft
|
||||
)
|
||||
|
||||
add_e2e_test(
|
||||
NAME jwt_test
|
||||
PYTHON_SCRIPT ${CMAKE_SOURCE_DIR}/tests/jwt_test.py
|
||||
CONSENSUS cft
|
||||
)
|
||||
endif()
|
||||
|
||||
if(BUILD_SMALLBANK)
|
||||
|
|
|
@ -435,6 +435,35 @@ def update_ca_cert(cert_name, cert_path, skip_checks=False, **kwargs):
|
|||
return build_proposal("update_ca_cert", args, **kwargs)
|
||||
|
||||
|
||||
@cli_proposal
|
||||
def set_jwt_issuer(json_path: str, **kwargs):
|
||||
with open(json_path) as f:
|
||||
obj = json.load(f)
|
||||
args = {
|
||||
"issuer": obj["issuer"],
|
||||
"key_filter": obj.get("key_filter", "all"),
|
||||
"key_policy": obj.get("key_policy"),
|
||||
"jwks": obj.get("jwks"),
|
||||
}
|
||||
return build_proposal("set_jwt_issuer", args, **kwargs)
|
||||
|
||||
|
||||
@cli_proposal
|
||||
def remove_jwt_issuer(issuer: str, **kwargs):
|
||||
args = {"issuer": issuer}
|
||||
return build_proposal("remove_jwt_issuer", args, **kwargs)
|
||||
|
||||
|
||||
@cli_proposal
|
||||
def set_jwt_public_signing_keys(issuer: str, jwks_path: str, **kwargs):
|
||||
with open(jwks_path) as f:
|
||||
jwks = json.load(f)
|
||||
if "keys" not in jwks:
|
||||
raise ValueError("not a JWKS document")
|
||||
args = {"issuer": issuer, "jwks": jwks}
|
||||
return build_proposal("set_jwt_public_signing_keys", args, **kwargs)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
parser = argparse.ArgumentParser()
|
||||
|
||||
|
|
|
@ -26,6 +26,10 @@ namespace kv::serialisers
|
|||
std::memcpy(s.data(), (uint8_t*)&t, sizeof(t));
|
||||
return s;
|
||||
}
|
||||
else if constexpr (std::is_same_v<T, std::string>)
|
||||
{
|
||||
return SerialisedEntry(t.begin(), t.end());
|
||||
}
|
||||
else
|
||||
{
|
||||
static_assert(
|
||||
|
@ -51,6 +55,10 @@ namespace kv::serialisers
|
|||
}
|
||||
return *(T*)rep.data();
|
||||
}
|
||||
else if constexpr (std::is_same_v<T, std::string>)
|
||||
{
|
||||
return T(rep.begin(), rep.end());
|
||||
}
|
||||
else
|
||||
{
|
||||
static_assert(
|
||||
|
|
|
@ -88,6 +88,11 @@ namespace ccf
|
|||
static constexpr auto SNAPSHOT_EVIDENCE =
|
||||
"public:ccf.gov.snapshot_evidence";
|
||||
static constexpr auto CA_CERT_DERS = "public:ccf.gov.ca_cert_ders";
|
||||
static constexpr auto JWT_ISSUERS = "public:ccf.gov.jwt_issuers";
|
||||
static constexpr auto JWT_PUBLIC_SIGNING_KEYS =
|
||||
"public:ccf.gov.jwt_public_signing_keys";
|
||||
static constexpr auto JWT_PUBLIC_SIGNING_KEY_ISSUER =
|
||||
"public:ccf.gov.jwt_public_signing_key_issuer";
|
||||
static constexpr auto ENDPOINTS = "public:ccf.gov.endpoints";
|
||||
|
||||
static constexpr auto SIGNATURES = "public:ccf.internal.signatures";
|
||||
|
|
|
@ -0,0 +1,61 @@
|
|||
// Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
// Licensed under the Apache 2.0 License.
|
||||
#pragma once
|
||||
#include "ds/json.h"
|
||||
#include "entities.h"
|
||||
#include "kv/map.h"
|
||||
|
||||
#include <msgpack/msgpack.hpp>
|
||||
#include <optional>
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
MSGPACK_DEFINE(sgx_claims);
|
||||
};
|
||||
|
||||
DECLARE_JSON_TYPE(JwtIssuerKeyPolicy);
|
||||
DECLARE_JSON_REQUIRED_FIELDS(JwtIssuerKeyPolicy, sgx_claims);
|
||||
|
||||
enum class JwtIssuerKeyFilter
|
||||
{
|
||||
All,
|
||||
SGX
|
||||
};
|
||||
|
||||
DECLARE_JSON_ENUM(
|
||||
JwtIssuerKeyFilter,
|
||||
{{JwtIssuerKeyFilter::All, "all"}, {JwtIssuerKeyFilter::SGX, "sgx"}});
|
||||
|
||||
struct JwtIssuerMetadata
|
||||
{
|
||||
JwtIssuerKeyFilter key_filter;
|
||||
std::optional<JwtIssuerKeyPolicy> key_policy;
|
||||
|
||||
MSGPACK_DEFINE(key_filter, key_policy);
|
||||
};
|
||||
|
||||
DECLARE_JSON_TYPE_WITH_OPTIONAL_FIELDS(JwtIssuerMetadata);
|
||||
DECLARE_JSON_REQUIRED_FIELDS(JwtIssuerMetadata, key_filter);
|
||||
DECLARE_JSON_OPTIONAL_FIELDS(JwtIssuerMetadata, key_policy);
|
||||
|
||||
using JwtIssuer = std::string;
|
||||
using JwtKeyId = std::string;
|
||||
|
||||
using JwtIssuers = kv::Map<JwtIssuer, JwtIssuerMetadata>;
|
||||
using JwtPublicSigningKeys = kv::RawCopySerialisedMap<JwtKeyId, Cert>;
|
||||
using JwtPublicSigningKeyIssuer =
|
||||
kv::RawCopySerialisedMap<JwtKeyId, JwtIssuer>;
|
||||
}
|
||||
|
||||
MSGPACK_ADD_ENUM(ccf::JwtIssuerKeyFilter);
|
|
@ -13,6 +13,7 @@
|
|||
#include "consensus/aft/revealed_nonces.h"
|
||||
#include "entities.h"
|
||||
#include "governance_history.h"
|
||||
#include "jwt.h"
|
||||
#include "kv/map.h"
|
||||
#include "kv/store.h"
|
||||
#include "members.h"
|
||||
|
@ -60,6 +61,10 @@ namespace ccf
|
|||
|
||||
CACertDERs ca_certs;
|
||||
|
||||
JwtIssuers jwt_issuers;
|
||||
JwtPublicSigningKeys jwt_public_signing_keys;
|
||||
JwtPublicSigningKeyIssuer jwt_public_signing_key_issuer;
|
||||
|
||||
//
|
||||
// User tables
|
||||
//
|
||||
|
@ -118,6 +123,9 @@ namespace ccf
|
|||
submitted_shares(Tables::SUBMITTED_SHARES),
|
||||
config(Tables::CONFIGURATION),
|
||||
ca_certs(Tables::CA_CERT_DERS),
|
||||
jwt_issuers(Tables::JWT_ISSUERS),
|
||||
jwt_public_signing_keys(Tables::JWT_PUBLIC_SIGNING_KEYS),
|
||||
jwt_public_signing_key_issuer(Tables::JWT_PUBLIC_SIGNING_KEY_ISSUER),
|
||||
users(Tables::USERS),
|
||||
user_certs(Tables::USER_CERT_DERS),
|
||||
user_client_signatures(Tables::USER_CLIENT_SIGNATURES),
|
||||
|
@ -154,6 +162,9 @@ namespace ccf
|
|||
std::ref(member_client_signatures),
|
||||
std::ref(config),
|
||||
std::ref(ca_certs),
|
||||
std::ref(jwt_issuers),
|
||||
std::ref(jwt_public_signing_keys),
|
||||
std::ref(jwt_public_signing_key_issuer),
|
||||
std::ref(users),
|
||||
std::ref(user_certs),
|
||||
std::ref(user_client_signatures),
|
||||
|
|
|
@ -6,6 +6,7 @@
|
|||
#include "lua_interp/lua_json.h"
|
||||
#include "lua_interp/tx_script_runner.h"
|
||||
#include "node/genesis_gen.h"
|
||||
#include "node/jwt.h"
|
||||
#include "node/members.h"
|
||||
#include "node/nodes.h"
|
||||
#include "node/quote.h"
|
||||
|
@ -31,6 +32,20 @@
|
|||
|
||||
namespace ccf
|
||||
{
|
||||
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;
|
||||
}
|
||||
|
||||
class MemberTsr : public lua::TxScriptRunner
|
||||
{
|
||||
void setup_environment(
|
||||
|
@ -54,20 +69,6 @@ namespace ccf
|
|||
return 1;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
static int lua_verify_cert_and_get_claims(lua_State* l)
|
||||
{
|
||||
LOG_INFO_FMT("lua_verify_cert_and_get_claims");
|
||||
|
@ -170,6 +171,57 @@ namespace ccf
|
|||
DECLARE_JSON_TYPE(DeployJsApp)
|
||||
DECLARE_JSON_REQUIRED_FIELDS(DeployJsApp, bundle)
|
||||
|
||||
struct JsonWebKey
|
||||
{
|
||||
std::vector<std::string> x5c;
|
||||
std::string kid;
|
||||
std::string kty;
|
||||
|
||||
bool operator==(const JsonWebKey& rhs) const
|
||||
{
|
||||
return x5c == rhs.x5c && kid == rhs.kid && kty == rhs.kty;
|
||||
}
|
||||
};
|
||||
DECLARE_JSON_TYPE(JsonWebKey)
|
||||
DECLARE_JSON_REQUIRED_FIELDS(JsonWebKey, x5c, kid, kty)
|
||||
|
||||
struct JsonWebKeySet
|
||||
{
|
||||
std::vector<JsonWebKey> keys;
|
||||
|
||||
bool operator!=(const JsonWebKeySet& rhs) const
|
||||
{
|
||||
return keys != rhs.keys;
|
||||
}
|
||||
};
|
||||
DECLARE_JSON_TYPE(JsonWebKeySet)
|
||||
DECLARE_JSON_REQUIRED_FIELDS(JsonWebKeySet, keys)
|
||||
|
||||
struct SetJwtIssuer : public ccf::JwtIssuerMetadata
|
||||
{
|
||||
std::string issuer;
|
||||
std::optional<JsonWebKeySet> jwks;
|
||||
};
|
||||
DECLARE_JSON_TYPE_WITH_BASE_AND_OPTIONAL_FIELDS(
|
||||
SetJwtIssuer, ccf::JwtIssuerMetadata)
|
||||
DECLARE_JSON_REQUIRED_FIELDS(SetJwtIssuer, issuer)
|
||||
DECLARE_JSON_OPTIONAL_FIELDS(SetJwtIssuer, jwks)
|
||||
|
||||
struct RemoveJwtIssuer
|
||||
{
|
||||
std::string issuer;
|
||||
};
|
||||
DECLARE_JSON_TYPE(RemoveJwtIssuer)
|
||||
DECLARE_JSON_REQUIRED_FIELDS(RemoveJwtIssuer, issuer)
|
||||
|
||||
struct SetJwtPublicSigningKeys
|
||||
{
|
||||
std::string issuer;
|
||||
JsonWebKeySet jwks;
|
||||
};
|
||||
DECLARE_JSON_TYPE(SetJwtPublicSigningKeys)
|
||||
DECLARE_JSON_REQUIRED_FIELDS(SetJwtPublicSigningKeys, issuer, jwks)
|
||||
|
||||
class MemberEndpoints : public CommonEndpointRegistry
|
||||
{
|
||||
private:
|
||||
|
@ -330,6 +382,157 @@ namespace ccf
|
|||
return tx_modules->remove(name);
|
||||
}
|
||||
|
||||
void remove_jwt_keys(kv::Tx& tx, std::string issuer)
|
||||
{
|
||||
auto keys = tx.get_view(this->network.jwt_public_signing_keys);
|
||||
auto key_issuer =
|
||||
tx.get_view(this->network.jwt_public_signing_key_issuer);
|
||||
|
||||
key_issuer->foreach(
|
||||
[&issuer, &keys, &key_issuer](const auto& k, const auto& v) {
|
||||
if (v == issuer)
|
||||
{
|
||||
keys->remove(k);
|
||||
key_issuer->remove(k);
|
||||
}
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
bool set_jwt_public_signing_keys(
|
||||
kv::Tx& tx,
|
||||
ObjectId proposal_id,
|
||||
std::string issuer,
|
||||
const JwtIssuerMetadata& issuer_metadata,
|
||||
const JsonWebKeySet& jwks)
|
||||
{
|
||||
auto keys = tx.get_view(this->network.jwt_public_signing_keys);
|
||||
auto key_issuer =
|
||||
tx.get_view(this->network.jwt_public_signing_key_issuer);
|
||||
|
||||
// add keys
|
||||
if (jwks.keys.empty())
|
||||
{
|
||||
LOG_FAIL_FMT("Proposal {}: JWKS has no keys", proposal_id);
|
||||
return false;
|
||||
}
|
||||
std::map<std::string, std::vector<uint8_t>> new_keys;
|
||||
for (auto& jwk : jwks.keys)
|
||||
{
|
||||
if (keys->has(jwk.kid) && key_issuer->get(jwk.kid).value() != issuer)
|
||||
{
|
||||
LOG_FAIL_FMT(
|
||||
"Proposal {}: key id {} already added for different issuer",
|
||||
proposal_id,
|
||||
jwk.kid);
|
||||
return false;
|
||||
}
|
||||
if (jwk.x5c.empty())
|
||||
{
|
||||
LOG_FAIL_FMT("Proposal {}: JWKS is invalid (empty x5c)", proposal_id);
|
||||
return false;
|
||||
}
|
||||
|
||||
auto& der_base64 = jwk.x5c[0];
|
||||
auto der = tls::raw_from_b64(der_base64);
|
||||
|
||||
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)
|
||||
{
|
||||
oe_verifier_initialize();
|
||||
oe_verify_attestation_certificate_with_evidence(
|
||||
der.data(),
|
||||
der.size(),
|
||||
oe_verify_attestation_certificate_with_evidence_cb,
|
||||
&claims);
|
||||
}
|
||||
|
||||
if (
|
||||
issuer_metadata.key_filter == JwtIssuerKeyFilter::SGX &&
|
||||
claims.empty())
|
||||
{
|
||||
LOG_INFO_FMT(
|
||||
"Proposal {}: Skipping JWT signing key with kid {} (not OE "
|
||||
"attested)",
|
||||
proposal_id,
|
||||
jwk.kid);
|
||||
continue;
|
||||
}
|
||||
|
||||
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(
|
||||
"Proposal {}: JWKS kid {} is missing the {} SGX claim",
|
||||
proposal_id,
|
||||
jwk.kid,
|
||||
claim_name);
|
||||
return false;
|
||||
}
|
||||
auto& actual_claim_val = claims[claim_name];
|
||||
auto actual_claim_val_hex =
|
||||
fmt::format("{:02x}", fmt::join(actual_claim_val, ""));
|
||||
if (expected_claim_val_hex != actual_claim_val_hex)
|
||||
{
|
||||
LOG_FAIL_FMT(
|
||||
"Proposal {}: JWKS kid {} has a mismatching {} SGX claim",
|
||||
proposal_id,
|
||||
jwk.kid,
|
||||
claim_name);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
try
|
||||
{
|
||||
tls::check_is_cert(der);
|
||||
}
|
||||
catch (std::exception& exc)
|
||||
{
|
||||
LOG_FAIL_FMT(
|
||||
"Proposal {}: JWKS kid {} has an invalid X.509 certificate: "
|
||||
"{}",
|
||||
proposal_id,
|
||||
jwk.kid,
|
||||
exc.what());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
LOG_INFO_FMT(
|
||||
"Proposal {}: Storing JWT signing key with kid {}",
|
||||
proposal_id,
|
||||
jwk.kid);
|
||||
new_keys.emplace(jwk.kid, der);
|
||||
}
|
||||
if (new_keys.empty())
|
||||
{
|
||||
LOG_FAIL_FMT(
|
||||
"Proposal {}: no keys left after applying filter", proposal_id);
|
||||
return false;
|
||||
}
|
||||
|
||||
remove_jwt_keys(tx, issuer);
|
||||
for (auto& [kid, der] : new_keys)
|
||||
{
|
||||
keys->put(kid, der);
|
||||
key_issuer->put(kid, issuer);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void remove_endpoints(kv::Tx& tx)
|
||||
{
|
||||
auto endpoints_view =
|
||||
|
@ -523,6 +726,60 @@ namespace ccf
|
|||
users_view->put(parsed.user_id, user_info.value());
|
||||
return true;
|
||||
}},
|
||||
{"set_jwt_issuer",
|
||||
[this](ObjectId proposal_id, kv::Tx& tx, const nlohmann::json& args) {
|
||||
const auto parsed = args.get<SetJwtIssuer>();
|
||||
auto issuers = tx.get_view(this->network.jwt_issuers);
|
||||
|
||||
bool success = true;
|
||||
if (parsed.jwks.has_value())
|
||||
{
|
||||
success = set_jwt_public_signing_keys(
|
||||
tx, proposal_id, parsed.issuer, parsed, parsed.jwks.value());
|
||||
}
|
||||
if (success)
|
||||
{
|
||||
issuers->put(parsed.issuer, parsed);
|
||||
}
|
||||
|
||||
return success;
|
||||
}},
|
||||
{"remove_jwt_issuer",
|
||||
[this](ObjectId proposal_id, kv::Tx& tx, const nlohmann::json& args) {
|
||||
const auto parsed = args.get<RemoveJwtIssuer>();
|
||||
const auto issuer = parsed.issuer;
|
||||
auto issuers = tx.get_view(this->network.jwt_issuers);
|
||||
|
||||
if (!issuers->remove(issuer))
|
||||
{
|
||||
LOG_FAIL_FMT(
|
||||
"Proposal {}: {} is not a valid issuer", proposal_id, issuer);
|
||||
return false;
|
||||
}
|
||||
|
||||
remove_jwt_keys(tx, issuer);
|
||||
|
||||
return true;
|
||||
}},
|
||||
{"set_jwt_public_signing_keys",
|
||||
[this](ObjectId proposal_id, kv::Tx& tx, const nlohmann::json& args) {
|
||||
const auto parsed = args.get<SetJwtPublicSigningKeys>();
|
||||
|
||||
auto issuers = tx.get_view(this->network.jwt_issuers);
|
||||
auto issuer_metadata_ = issuers->get(parsed.issuer);
|
||||
if (!issuer_metadata_.has_value())
|
||||
{
|
||||
LOG_FAIL_FMT(
|
||||
"Proposal {}: {} is not a valid issuer",
|
||||
proposal_id,
|
||||
parsed.issuer);
|
||||
return false;
|
||||
}
|
||||
auto& issuer_metadata = issuer_metadata_.value();
|
||||
|
||||
return set_jwt_public_signing_keys(
|
||||
tx, proposal_id, parsed.issuer, issuer_metadata, parsed.jwks);
|
||||
}},
|
||||
// accept a node
|
||||
{"trust_node",
|
||||
[this](ObjectId proposal_id, kv::Tx& tx, const nlohmann::json& args) {
|
||||
|
|
|
@ -27,7 +27,10 @@ namespace ccf
|
|||
Tables::MODULES,
|
||||
Tables::SERVICE,
|
||||
Tables::CONFIGURATION,
|
||||
Tables::CA_CERT_DERS}},
|
||||
Tables::CA_CERT_DERS,
|
||||
Tables::JWT_ISSUERS,
|
||||
Tables::JWT_PUBLIC_SIGNING_KEYS,
|
||||
Tables::JWT_PUBLIC_SIGNING_KEY_ISSUER}},
|
||||
|
||||
{MEMBER_CAN_PROPOSE,
|
||||
{Tables::USERS,
|
||||
|
@ -38,7 +41,10 @@ namespace ccf
|
|||
Tables::APP_SCRIPTS,
|
||||
Tables::MODULES,
|
||||
Tables::CONFIGURATION,
|
||||
Tables::CA_CERT_DERS}},
|
||||
Tables::CA_CERT_DERS,
|
||||
Tables::JWT_ISSUERS,
|
||||
Tables::JWT_PUBLIC_SIGNING_KEYS,
|
||||
Tables::JWT_PUBLIC_SIGNING_KEY_ISSUER}},
|
||||
|
||||
{USER_APP_CAN_READ_ONLY,
|
||||
{Tables::MEMBERS,
|
||||
|
@ -50,5 +56,8 @@ namespace ccf
|
|||
Tables::APP_SCRIPTS,
|
||||
Tables::MODULES,
|
||||
Tables::GOV_HISTORY,
|
||||
Tables::CA_CERT_DERS}}};
|
||||
Tables::CA_CERT_DERS,
|
||||
Tables::JWT_ISSUERS,
|
||||
Tables::JWT_PUBLIC_SIGNING_KEYS,
|
||||
Tables::JWT_PUBLIC_SIGNING_KEY_ISSUER}}};
|
||||
}
|
|
@ -881,4 +881,18 @@ namespace tls
|
|||
mbedtls_x509_crt_free(&c);
|
||||
return tls::Pem(data, len);
|
||||
}
|
||||
|
||||
inline void check_is_cert(CBuffer der)
|
||||
{
|
||||
mbedtls_x509_crt cert;
|
||||
mbedtls_x509_crt_init(&cert);
|
||||
int rc = mbedtls_x509_crt_parse(&cert, der.p, der.n);
|
||||
mbedtls_x509_crt_free(&cert);
|
||||
if (rc != 0)
|
||||
{
|
||||
throw std::runtime_error(fmt::format(
|
||||
"Failed to parse certificate, mbedtls_x509_crt_parse: {}",
|
||||
tls::error_string(rc)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -328,6 +328,23 @@ class Consortium:
|
|||
# Large apps take a long time to process - wait longer than normal for commit
|
||||
return self.vote_using_majority(remote_node, proposal, careful_vote, timeout=10)
|
||||
|
||||
def set_jwt_issuer(self, remote_node, json_path):
|
||||
proposal_body, careful_vote = self.make_proposal("set_jwt_issuer", json_path)
|
||||
proposal = self.get_any_active_member().propose(remote_node, proposal_body)
|
||||
return self.vote_using_majority(remote_node, proposal, careful_vote)
|
||||
|
||||
def remove_jwt_issuer(self, remote_node, issuer):
|
||||
proposal_body, careful_vote = self.make_proposal("remove_jwt_issuer", issuer)
|
||||
proposal = self.get_any_active_member().propose(remote_node, proposal_body)
|
||||
return self.vote_using_majority(remote_node, proposal, careful_vote)
|
||||
|
||||
def set_jwt_public_signing_keys(self, remote_node, issuer, jwks_path):
|
||||
proposal_body, careful_vote = self.make_proposal(
|
||||
"set_jwt_public_signing_keys", issuer, jwks_path
|
||||
)
|
||||
proposal = self.get_any_active_member().propose(remote_node, proposal_body)
|
||||
return self.vote_using_majority(remote_node, proposal, careful_vote)
|
||||
|
||||
def accept_recovery(self, remote_node):
|
||||
proposal_body, careful_vote = self.make_proposal("accept_recovery")
|
||||
proposal = self.get_any_active_member().propose(remote_node, proposal_body)
|
||||
|
|
|
@ -5,16 +5,23 @@ from typing import Tuple, Optional
|
|||
import base64
|
||||
from enum import IntEnum
|
||||
import secrets
|
||||
import datetime
|
||||
|
||||
import coincurve
|
||||
from coincurve._libsecp256k1 import ffi, lib # pylint: disable=no-name-in-module
|
||||
from coincurve.context import GLOBAL_CONTEXT
|
||||
|
||||
from cryptography.exceptions import InvalidSignature
|
||||
from cryptography.x509 import load_der_x509_certificate
|
||||
from cryptography import x509
|
||||
from cryptography.x509.oid import NameOID
|
||||
from cryptography.x509 import (
|
||||
load_pem_x509_certificate,
|
||||
load_der_x509_certificate,
|
||||
)
|
||||
from cryptography.hazmat.primitives.asymmetric import ec, rsa, padding
|
||||
from cryptography.hazmat.primitives.serialization import (
|
||||
load_pem_private_key,
|
||||
load_pem_public_key,
|
||||
Encoding,
|
||||
PrivateFormat,
|
||||
PublicFormat,
|
||||
|
@ -23,6 +30,8 @@ from cryptography.hazmat.primitives.serialization import (
|
|||
from cryptography.hazmat.primitives import hashes
|
||||
from cryptography.hazmat.backends import default_backend
|
||||
|
||||
import jwt
|
||||
|
||||
RECOMMENDED_RSA_PUBLIC_EXPONENT = 65537
|
||||
|
||||
|
||||
|
@ -91,7 +100,7 @@ def verify_recover_secp256k1_bc(
|
|||
|
||||
def verify_request_sig(raw_cert, sig, req, request_body, md):
|
||||
try:
|
||||
cert = load_der_x509_certificate(raw_cert, backend=default_backend())
|
||||
cert = x509.load_der_x509_certificate(raw_cert, backend=default_backend())
|
||||
|
||||
digest = (
|
||||
hashes.SHA256()
|
||||
|
@ -140,6 +149,28 @@ def generate_rsa_keypair(key_size: int) -> Tuple[str, str]:
|
|||
return priv_pem, pub_pem
|
||||
|
||||
|
||||
def generate_cert(priv_key_pem: str) -> str:
|
||||
priv = load_pem_private_key(priv_key_pem.encode("ascii"), None, default_backend())
|
||||
pub = priv.public_key()
|
||||
subject = issuer = x509.Name(
|
||||
[
|
||||
x509.NameAttribute(NameOID.COMMON_NAME, u"dummy"),
|
||||
]
|
||||
)
|
||||
cert = (
|
||||
x509.CertificateBuilder()
|
||||
.subject_name(subject)
|
||||
.issuer_name(issuer)
|
||||
.public_key(pub)
|
||||
.serial_number(x509.random_serial_number())
|
||||
.not_valid_before(datetime.datetime.utcnow())
|
||||
.not_valid_after(datetime.datetime.utcnow() + datetime.timedelta(days=10))
|
||||
.sign(priv, hashes.SHA256(), default_backend())
|
||||
)
|
||||
|
||||
return cert.public_bytes(Encoding.PEM).decode("ascii")
|
||||
|
||||
|
||||
def unwrap_key_rsa_oaep(
|
||||
wrapped_key: bytes, wrapping_key_priv_pem: str, label: Optional[bytes] = None
|
||||
) -> bytes:
|
||||
|
@ -155,3 +186,30 @@ def unwrap_key_rsa_oaep(
|
|||
),
|
||||
)
|
||||
return unwrapped
|
||||
|
||||
|
||||
def pub_key_pem_to_der(pem: str) -> bytes:
|
||||
cert = load_pem_public_key(pem.encode("ascii"), default_backend())
|
||||
return cert.public_bytes(Encoding.DER, PublicFormat.SubjectPublicKeyInfo)
|
||||
|
||||
|
||||
def create_jwt(body_claims: dict, key_priv_pem: str, key_id: str) -> str:
|
||||
return jwt.encode(
|
||||
body_claims, key_priv_pem, algorithm="RS256", headers={"kid": key_id}
|
||||
).decode("ascii")
|
||||
|
||||
|
||||
def cert_pem_to_der(pem: str) -> bytes:
|
||||
cert = load_pem_x509_certificate(pem.encode("ascii"), default_backend())
|
||||
return cert.public_bytes(Encoding.DER)
|
||||
|
||||
|
||||
def cert_der_to_pem(der: bytes) -> str:
|
||||
cert = load_der_x509_certificate(der, default_backend())
|
||||
return cert.public_bytes(Encoding.PEM).decode("ascii")
|
||||
|
||||
|
||||
def are_certs_equal(pem1: str, pem2: str) -> bool:
|
||||
cert1 = load_pem_x509_certificate(pem1.encode(), default_backend())
|
||||
cert2 = load_pem_x509_certificate(pem2.encode(), default_backend())
|
||||
return cert1 == cert2
|
||||
|
|
|
@ -111,6 +111,12 @@ def cli_args(add=lambda x: None, parser=None, accept_unknown=False):
|
|||
parser.add_argument("-s", "--app-script", help="Path to app script")
|
||||
parser.add_argument("-j", "--js-app-script", help="Path to js app script")
|
||||
parser.add_argument("--js-app-bundle", help="Path to js app bundle")
|
||||
parser.add_argument(
|
||||
"--jwt-issuer",
|
||||
help="Path to JSON file with JWT issuer definition",
|
||||
action="append",
|
||||
default=[],
|
||||
)
|
||||
parser.add_argument(
|
||||
"-o",
|
||||
"--network-only",
|
||||
|
|
|
@ -381,6 +381,9 @@ class Network:
|
|||
remote_node=primary, app_bundle_path=args.js_app_bundle
|
||||
)
|
||||
|
||||
for path in args.jwt_issuer:
|
||||
self.consortium.set_jwt_issuer(remote_node=primary, json_path=path)
|
||||
|
||||
self.consortium.add_users(primary, initial_users)
|
||||
LOG.info("Initial set of users added")
|
||||
|
||||
|
|
|
@ -0,0 +1,270 @@
|
|||
# Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
# Licensed under the Apache 2.0 License.
|
||||
import os
|
||||
import tempfile
|
||||
import json
|
||||
import base64
|
||||
import infra.network
|
||||
import infra.path
|
||||
import infra.proc
|
||||
import infra.net
|
||||
import infra.e2e_args
|
||||
import suite.test_requirements as reqs
|
||||
|
||||
from loguru import logger as LOG
|
||||
|
||||
this_dir = os.path.dirname(__file__)
|
||||
|
||||
|
||||
def create_jwks(kid, cert_pem, test_invalid_is_key=False):
|
||||
der_b64 = base64.b64encode(
|
||||
infra.crypto.cert_pem_to_der(cert_pem)
|
||||
if not test_invalid_is_key
|
||||
else infra.crypto.pub_key_pem_to_der(cert_pem)
|
||||
).decode("ascii")
|
||||
return {"keys": [{"kty": "RSA", "kid": kid, "x5c": [der_b64]}]}
|
||||
|
||||
|
||||
@reqs.description("JWT without key policy")
|
||||
def test_jwt_without_key_policy(network, args):
|
||||
primary, _ = network.find_nodes()
|
||||
|
||||
key_priv_pem, key_pub_pem = infra.crypto.generate_rsa_keypair(2048)
|
||||
cert_pem = infra.crypto.generate_cert(key_priv_pem)
|
||||
kid = "my_kid"
|
||||
issuer = "my_issuer"
|
||||
|
||||
LOG.info("Try to add JWT signing key without matching issuer")
|
||||
with tempfile.NamedTemporaryFile(prefix="ccf", mode="w+") as jwks_fp:
|
||||
json.dump(create_jwks(kid, cert_pem), jwks_fp)
|
||||
jwks_fp.flush()
|
||||
try:
|
||||
network.consortium.set_jwt_public_signing_keys(
|
||||
primary, issuer, jwks_fp.name
|
||||
)
|
||||
except infra.proposal.ProposalNotAccepted:
|
||||
pass
|
||||
else:
|
||||
assert False, "Proposal should not have been created"
|
||||
|
||||
LOG.info("Add JWT issuer")
|
||||
with tempfile.NamedTemporaryFile(prefix="ccf", mode="w+") as metadata_fp:
|
||||
json.dump({"issuer": issuer}, metadata_fp)
|
||||
metadata_fp.flush()
|
||||
network.consortium.set_jwt_issuer(primary, metadata_fp.name)
|
||||
|
||||
LOG.info("Try to add a public key instead of a certificate")
|
||||
with tempfile.NamedTemporaryFile(prefix="ccf", mode="w+") as jwks_fp:
|
||||
json.dump(create_jwks(kid, key_pub_pem, test_invalid_is_key=True), jwks_fp)
|
||||
jwks_fp.flush()
|
||||
try:
|
||||
network.consortium.set_jwt_public_signing_keys(
|
||||
primary, issuer, jwks_fp.name
|
||||
)
|
||||
except infra.proposal.ProposalNotAccepted:
|
||||
pass
|
||||
else:
|
||||
assert False, "Proposal should not have been created"
|
||||
|
||||
LOG.info("Add JWT signing key with matching issuer")
|
||||
with tempfile.NamedTemporaryFile(prefix="ccf", mode="w+") as jwks_fp:
|
||||
json.dump(create_jwks(kid, cert_pem), jwks_fp)
|
||||
jwks_fp.flush()
|
||||
network.consortium.set_jwt_public_signing_keys(primary, issuer, jwks_fp.name)
|
||||
|
||||
LOG.info("Check if JWT signing key was stored correctly")
|
||||
with primary.client(
|
||||
f"member{network.consortium.get_any_active_member().member_id}"
|
||||
) as c:
|
||||
r = c.post(
|
||||
"/gov/read", {"table": "public:ccf.gov.jwt_public_signing_keys", "key": kid}
|
||||
)
|
||||
assert r.status_code == 200, r.status_code
|
||||
# Note that /gov/read returns all data as JSON.
|
||||
# Here, the stored data is a uint8 array, therefore it
|
||||
# is returned as an array of integers.
|
||||
cert_kv_der = bytes(r.body.json())
|
||||
cert_kv_pem = infra.crypto.cert_der_to_pem(cert_kv_der)
|
||||
assert infra.crypto.are_certs_equal(
|
||||
cert_pem, cert_kv_pem
|
||||
), "stored cert not equal to input cert"
|
||||
|
||||
LOG.info("Remove JWT issuer")
|
||||
network.consortium.remove_jwt_issuer(primary, issuer)
|
||||
|
||||
LOG.info("Check if JWT signing key was deleted")
|
||||
with primary.client(
|
||||
f"member{network.consortium.get_any_active_member().member_id}"
|
||||
) as c:
|
||||
r = c.post(
|
||||
"/gov/read", {"table": "public:ccf.gov.jwt_public_signing_keys", "key": kid}
|
||||
)
|
||||
assert r.status_code == 400, r.status_code
|
||||
|
||||
LOG.info("Add JWT issuer with initial keys")
|
||||
with tempfile.NamedTemporaryFile(prefix="ccf", mode="w+") as metadata_fp:
|
||||
json.dump({"issuer": issuer, "jwks": create_jwks(kid, cert_pem)}, metadata_fp)
|
||||
metadata_fp.flush()
|
||||
network.consortium.set_jwt_issuer(primary, metadata_fp.name)
|
||||
|
||||
LOG.info("Check if JWT signing key was stored correctly")
|
||||
with primary.client(
|
||||
f"member{network.consortium.get_any_active_member().member_id}"
|
||||
) as c:
|
||||
r = c.post(
|
||||
"/gov/read", {"table": "public:ccf.gov.jwt_public_signing_keys", "key": kid}
|
||||
)
|
||||
assert r.status_code == 200, r.status_code
|
||||
cert_kv_der = bytes(r.body.json())
|
||||
cert_kv_pem = infra.crypto.cert_der_to_pem(cert_kv_der)
|
||||
assert infra.crypto.are_certs_equal(
|
||||
cert_pem, cert_kv_pem
|
||||
), "stored cert not equal to input cert"
|
||||
|
||||
return network
|
||||
|
||||
|
||||
@reqs.description("JWT with SGX key policy")
|
||||
def test_jwt_with_sgx_key_policy(network, args):
|
||||
primary, _ = network.find_nodes()
|
||||
|
||||
oe_cert_path = os.path.join(this_dir, "ca_cert.pem")
|
||||
with open(oe_cert_path) as f:
|
||||
oe_cert_pem = f.read()
|
||||
|
||||
kid = "my_kid"
|
||||
issuer = "my_issuer"
|
||||
|
||||
matching_key_policy = {
|
||||
"sgx_claims": {
|
||||
"signer_id": "ca9ad7331448980aa28890ce73e433638377f179ab4456b2fe237193193a8d0a",
|
||||
"attributes": "0300000000000000",
|
||||
}
|
||||
}
|
||||
|
||||
mismatching_key_policy = {
|
||||
"sgx_claims": {
|
||||
"signer_id": "da9ad7331448980aa28890ce73e433638377f179ab4456b2fe237193193a8d0a",
|
||||
"attributes": "0300000000000000",
|
||||
}
|
||||
}
|
||||
|
||||
LOG.info("Add JWT issuer with SGX key policy")
|
||||
with tempfile.NamedTemporaryFile(prefix="ccf", mode="w+") as metadata_fp:
|
||||
json.dump({"issuer": issuer, "key_policy": matching_key_policy}, metadata_fp)
|
||||
metadata_fp.flush()
|
||||
network.consortium.set_jwt_issuer(primary, metadata_fp.name)
|
||||
|
||||
LOG.info("Try to add a non-OE-attested cert")
|
||||
key_priv_pem, _ = infra.crypto.generate_rsa_keypair(2048)
|
||||
non_oe_cert_pem = infra.crypto.generate_cert(key_priv_pem)
|
||||
with tempfile.NamedTemporaryFile(prefix="ccf", mode="w+") as jwks_fp:
|
||||
json.dump(create_jwks(kid, non_oe_cert_pem), jwks_fp)
|
||||
jwks_fp.flush()
|
||||
try:
|
||||
network.consortium.set_jwt_public_signing_keys(
|
||||
primary, issuer, jwks_fp.name
|
||||
)
|
||||
except infra.proposal.ProposalNotAccepted:
|
||||
pass
|
||||
else:
|
||||
assert False, "Proposal should not have been created"
|
||||
|
||||
LOG.info("Add an OE-attested cert with matching claims")
|
||||
with tempfile.NamedTemporaryFile(prefix="ccf", mode="w+") as jwks_fp:
|
||||
json.dump(create_jwks(kid, oe_cert_pem), jwks_fp)
|
||||
jwks_fp.flush()
|
||||
network.consortium.set_jwt_public_signing_keys(primary, issuer, jwks_fp.name)
|
||||
|
||||
LOG.info("Update JWT issuer with mismatching SGX key policy")
|
||||
with tempfile.NamedTemporaryFile(prefix="ccf", mode="w+") as metadata_fp:
|
||||
json.dump(
|
||||
{
|
||||
"issuer": issuer,
|
||||
"key_policy": mismatching_key_policy,
|
||||
},
|
||||
metadata_fp,
|
||||
)
|
||||
metadata_fp.flush()
|
||||
network.consortium.set_jwt_issuer(primary, metadata_fp.name)
|
||||
|
||||
LOG.info("Try to add an OE-attested cert with mismatching claims")
|
||||
with tempfile.NamedTemporaryFile(prefix="ccf", mode="w+") as jwks_fp:
|
||||
json.dump(create_jwks(kid, non_oe_cert_pem), jwks_fp)
|
||||
jwks_fp.flush()
|
||||
try:
|
||||
network.consortium.set_jwt_public_signing_keys(
|
||||
primary, issuer, jwks_fp.name
|
||||
)
|
||||
except infra.proposal.ProposalNotAccepted:
|
||||
pass
|
||||
else:
|
||||
assert False, "Proposal should not have been created"
|
||||
|
||||
return network
|
||||
|
||||
|
||||
@reqs.description("JWT with SGX key filter")
|
||||
def test_jwt_with_sgx_key_filter(network, args):
|
||||
primary, _ = network.find_nodes()
|
||||
|
||||
oe_cert_path = os.path.join(this_dir, "ca_cert.pem")
|
||||
with open(oe_cert_path) as f:
|
||||
oe_cert_pem = f.read()
|
||||
oe_kid = "oe_kid"
|
||||
|
||||
key_priv_pem, _ = infra.crypto.generate_rsa_keypair(2048)
|
||||
non_oe_cert_pem = infra.crypto.generate_cert(key_priv_pem)
|
||||
non_oe_kid = "non_oe_kid"
|
||||
|
||||
issuer = "my_issuer"
|
||||
|
||||
LOG.info("Add JWT issuer with SGX key filter")
|
||||
with tempfile.NamedTemporaryFile(prefix="ccf", mode="w+") as metadata_fp:
|
||||
json.dump({"issuer": issuer, "key_filter": "sgx"}, metadata_fp)
|
||||
metadata_fp.flush()
|
||||
network.consortium.set_jwt_issuer(primary, metadata_fp.name)
|
||||
|
||||
LOG.info("Add multiple certs (1 SGX, 1 non-SGX)")
|
||||
with tempfile.NamedTemporaryFile(prefix="ccf", mode="w+") as jwks_fp:
|
||||
oe_jwks = create_jwks(oe_kid, oe_cert_pem)
|
||||
non_oe_jwks = create_jwks(non_oe_kid, non_oe_cert_pem)
|
||||
jwks = {"keys": non_oe_jwks["keys"] + oe_jwks["keys"]}
|
||||
json.dump(jwks, jwks_fp)
|
||||
jwks_fp.flush()
|
||||
network.consortium.set_jwt_public_signing_keys(primary, issuer, jwks_fp.name)
|
||||
|
||||
LOG.info("Check that only SGX cert was added")
|
||||
with primary.client(
|
||||
f"member{network.consortium.get_any_active_member().member_id}"
|
||||
) as c:
|
||||
r = c.post(
|
||||
"/gov/read",
|
||||
{"table": "public:ccf.gov.jwt_public_signing_keys", "key": non_oe_kid},
|
||||
)
|
||||
assert r.status_code == 400, r.status_code
|
||||
r = c.post(
|
||||
"/gov/read",
|
||||
{"table": "public:ccf.gov.jwt_public_signing_keys", "key": oe_kid},
|
||||
)
|
||||
assert r.status_code == 200, r.status_code
|
||||
|
||||
return network
|
||||
|
||||
|
||||
def run(args):
|
||||
with infra.network.network(
|
||||
args.nodes, args.binary_dir, args.debug_nodes, args.perf_nodes, pdb=args.pdb
|
||||
) as network:
|
||||
network.start_and_join(args)
|
||||
network = test_jwt_without_key_policy(network, args)
|
||||
network = test_jwt_with_sgx_key_policy(network, args)
|
||||
network = test_jwt_with_sgx_key_filter(network, args)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
args = infra.e2e_args.cli_args()
|
||||
args.package = "liblogging"
|
||||
args.nodes = infra.e2e_args.max_nodes(args, f=0)
|
||||
run(args)
|
|
@ -1,5 +1,6 @@
|
|||
# Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
# Licensed under the Apache 2.0 License.
|
||||
import base64
|
||||
import tempfile
|
||||
import http
|
||||
import subprocess
|
||||
|
@ -324,6 +325,62 @@ def test_npm_app(network, args):
|
|||
assert {"id": 42, "msg": "Saluton!"} in body, body
|
||||
assert {"id": 43, "msg": "Bonjour!"} in body, body
|
||||
|
||||
r = c.get("/app/jwt")
|
||||
assert r.status_code == http.HTTPStatus.UNAUTHORIZED, r.status_code
|
||||
body = r.body.json()
|
||||
assert body["msg"] == "authorization header missing", r.body
|
||||
|
||||
r = c.get("/app/jwt", headers={"authorization": "Bearer not-a-jwt"})
|
||||
assert r.status_code == http.HTTPStatus.UNAUTHORIZED, r.status_code
|
||||
body = r.body.json()
|
||||
assert body["msg"].startswith("malformed jwt:"), r.body
|
||||
|
||||
jwt_key_priv_pem, _ = infra.crypto.generate_rsa_keypair(2048)
|
||||
jwt_cert_pem = infra.crypto.generate_cert(jwt_key_priv_pem)
|
||||
|
||||
jwt_kid = "my_key_id"
|
||||
jwt = infra.crypto.create_jwt({}, jwt_key_priv_pem, jwt_kid)
|
||||
r = c.get("/app/jwt", headers={"authorization": "Bearer " + jwt})
|
||||
assert r.status_code == http.HTTPStatus.UNAUTHORIZED, r.status_code
|
||||
body = r.body.json()
|
||||
assert body["msg"].startswith("token signing key not found"), r.body
|
||||
|
||||
LOG.info("Store JWT signing keys")
|
||||
|
||||
issuer = "https://example.issuer"
|
||||
with tempfile.NamedTemporaryFile(prefix="ccf", mode="w+") as metadata_fp:
|
||||
jwt_cert_der = infra.crypto.cert_pem_to_der(jwt_cert_pem)
|
||||
der_b64 = base64.b64encode(jwt_cert_der).decode("ascii")
|
||||
data = {
|
||||
"issuer": issuer,
|
||||
"jwks": {"keys": [{"kty": "RSA", "kid": jwt_kid, "x5c": [der_b64]}]},
|
||||
}
|
||||
json.dump(data, metadata_fp)
|
||||
metadata_fp.flush()
|
||||
network.consortium.set_jwt_issuer(primary, metadata_fp.name)
|
||||
|
||||
LOG.info("Calling jwt endpoint after storing keys")
|
||||
with primary.client("user0") as c:
|
||||
jwt_mismatching_key_priv_pem, _ = infra.crypto.generate_rsa_keypair(2048)
|
||||
jwt = infra.crypto.create_jwt({}, jwt_mismatching_key_priv_pem, jwt_kid)
|
||||
r = c.get("/app/jwt", headers={"authorization": "Bearer " + jwt})
|
||||
assert r.status_code == http.HTTPStatus.UNAUTHORIZED, r.status_code
|
||||
body = r.body.json()
|
||||
assert body["msg"] == "jwt validation failed", r.body
|
||||
|
||||
jwt = infra.crypto.create_jwt({}, jwt_key_priv_pem, jwt_kid)
|
||||
r = c.get("/app/jwt", headers={"authorization": "Bearer " + jwt})
|
||||
assert r.status_code == http.HTTPStatus.UNAUTHORIZED, r.status_code
|
||||
body = r.body.json()
|
||||
assert body["msg"] == "jwt invalid, sub claim missing", r.body
|
||||
|
||||
user_id = "user0"
|
||||
jwt = infra.crypto.create_jwt({"sub": user_id}, jwt_key_priv_pem, jwt_kid)
|
||||
r = c.get("/app/jwt", headers={"authorization": "Bearer " + jwt})
|
||||
assert r.status_code == http.HTTPStatus.OK, r.status_code
|
||||
body = r.body.json()
|
||||
assert body["userId"] == user_id, r.body
|
||||
|
||||
return network
|
||||
|
||||
|
||||
|
|
|
@ -1,5 +1,60 @@
|
|||
{
|
||||
"endpoints": {
|
||||
"/jwt": {
|
||||
"get": {
|
||||
"js_module": "src/endpoints/jwt.js",
|
||||
"js_function": "jwt",
|
||||
"forwarding_required": "always",
|
||||
"execute_locally": false,
|
||||
"require_client_signature": false,
|
||||
"require_client_identity": false,
|
||||
"readonly": true,
|
||||
"openapi": {
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Ok",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"properties": {
|
||||
"userId": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"userId"
|
||||
],
|
||||
"type": "object",
|
||||
"additionalProperties": false
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"401": {
|
||||
"description": "Unauthorized",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"properties": {
|
||||
"msg": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"msg"
|
||||
],
|
||||
"type": "object",
|
||||
"additionalProperties": false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"security": [],
|
||||
"parameters": []
|
||||
}
|
||||
}
|
||||
},
|
||||
"/crypto": {
|
||||
"get": {
|
||||
"js_module": "src/endpoints/crypto.js",
|
||||
|
|
|
@ -7,8 +7,9 @@
|
|||
"type": "module",
|
||||
"dependencies": {
|
||||
"js-base64": "^3.5.2",
|
||||
"jsrsasign": "^8.0.22",
|
||||
"jsrsasign-util": "^1.0.0",
|
||||
"jsrsasign": "^10.0.4",
|
||||
"jsrsasign-util": "^1.0.2",
|
||||
"jwt-decode": "^3.0.0",
|
||||
"lodash-es": "^4.17.15",
|
||||
"protobufjs": "^6.10.1"
|
||||
},
|
||||
|
@ -16,7 +17,8 @@
|
|||
"@rollup/plugin-commonjs": "^14.0.0",
|
||||
"@rollup/plugin-node-resolve": "^8.4.0",
|
||||
"@rollup/plugin-typescript": "^5.0.2",
|
||||
"@types/jsrsasign": "^8.0.5",
|
||||
"@types/jsrsasign": "^8.0.7",
|
||||
"@types/jwt-decode": "^2.2.1",
|
||||
"@types/lodash-es": "^4.17.3",
|
||||
"del-cli": "^3.0.1",
|
||||
"http-server": "^0.12.3",
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
export * from './jwt'
|
||||
export * from './crypto'
|
||||
export * from './partition'
|
||||
export * from './proto'
|
||||
|
|
|
@ -0,0 +1,88 @@
|
|||
import { KJUR, KEYUTIL, ArrayBuffertohex } from 'jsrsasign'
|
||||
import jwt_decode from 'jwt-decode'
|
||||
import { Base64 } from 'js-base64'
|
||||
|
||||
import * as ccf from '../types/ccf'
|
||||
|
||||
interface JwtResponse {
|
||||
userId: string
|
||||
}
|
||||
|
||||
interface ErrorResponse {
|
||||
msg: string
|
||||
}
|
||||
|
||||
interface HeaderClaims {
|
||||
kid: string
|
||||
}
|
||||
|
||||
interface BodyClaims {
|
||||
sub: string
|
||||
}
|
||||
|
||||
export function jwt(request: ccf.Request): ccf.Response<JwtResponse | ErrorResponse> {
|
||||
const authHeader = request.headers['authorization']
|
||||
if (!authHeader) {
|
||||
return unauthorized('authorization header missing')
|
||||
}
|
||||
const parts = authHeader.split(' ', 2)
|
||||
if (parts.length !== 2 || parts[0] !== 'Bearer') {
|
||||
return unauthorized('unexpected authentication type')
|
||||
}
|
||||
const token = parts[1]
|
||||
|
||||
// Extract header claims to select the correct signing key.
|
||||
// We use jwt_decode() instead of jsrsasign's parse() as the latter does unnecessary work.
|
||||
let headerClaims: HeaderClaims
|
||||
try {
|
||||
headerClaims = jwt_decode(token, { header: true }) as HeaderClaims
|
||||
} catch (e) {
|
||||
return unauthorized(`malformed jwt: ${e.message}`)
|
||||
}
|
||||
const signingKeyId = headerClaims.kid
|
||||
if (!signingKeyId) {
|
||||
return unauthorized('kid missing in header claims')
|
||||
}
|
||||
|
||||
// Get the stored signing key to validate the token.
|
||||
const keysMap = new ccf.TypedKVMap(ccf.kv['public:ccf.gov.jwt_public_signing_keys'], ccf.string, ccf.typedArray(Uint8Array))
|
||||
const publicKeyDer = keysMap.get(signingKeyId)
|
||||
if (publicKeyDer === undefined) {
|
||||
return unauthorized(`token signing key not found: ${signingKeyId}`)
|
||||
}
|
||||
// jsrsasign can only load X.509 certs from PEM strings
|
||||
const publicKeyB64 = Base64.fromUint8Array(publicKeyDer)
|
||||
const publicKeyPem = "-----BEGIN CERTIFICATE-----\n" + publicKeyB64 + "\n-----END CERTIFICATE-----";
|
||||
const publicKey = KEYUTIL.getKey(publicKeyPem)
|
||||
|
||||
// Validate the token signature.
|
||||
const valid = KJUR.jws.JWS.verifyJWT(token, <any>publicKey, <any>{
|
||||
alg: ['RS256'],
|
||||
// No trusted time, disable time validation.
|
||||
verifyAt: Date.parse('2020-01-01T00:00:00') / 1000,
|
||||
gracePeriod: 10 * 365 * 24 * 60 * 60
|
||||
})
|
||||
if (!valid) {
|
||||
return unauthorized('jwt validation failed')
|
||||
}
|
||||
|
||||
// Custom body claims validation, app-specific.
|
||||
const claims = jwt_decode(token) as BodyClaims
|
||||
if (!claims.sub) {
|
||||
return unauthorized('jwt invalid, sub claim missing')
|
||||
}
|
||||
return {
|
||||
body: {
|
||||
userId: claims.sub
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function unauthorized(msg: string): ccf.Response<ErrorResponse> {
|
||||
return {
|
||||
statusCode: 401,
|
||||
body: {
|
||||
msg: msg
|
||||
}
|
||||
}
|
||||
}
|
|
@ -4,6 +4,7 @@
|
|||
"target": "ES2020",
|
||||
"module": "ES2020",
|
||||
"moduleResolution": "node",
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"noImplicitAny": false,
|
||||
"removeComments": true,
|
||||
"preserveConstEnums": true,
|
||||
|
|
|
@ -5,4 +5,5 @@ coincurve
|
|||
psutil
|
||||
cimetrics>=0.2.1
|
||||
pynacl
|
||||
openapi-spec-validator
|
||||
openapi-spec-validator
|
||||
PyJWT
|
Загрузка…
Ссылка в новой задаче