Skip to content

Commit

Permalink
Add issuer check to JWT authorization
Browse files Browse the repository at this point in the history
  • Loading branch information
maxtropets committed May 24, 2024
1 parent b9c2499 commit b2bc164
Show file tree
Hide file tree
Showing 16 changed files with 491 additions and 81 deletions.
10 changes: 6 additions & 4 deletions doc/audit/builtin_maps.rst
Original file line number Diff line number Diff line change
Expand Up @@ -363,7 +363,7 @@ JWT issuers.
:project: CCF
:members:

``jwt.public_signing_keys``
``jwt.public_signing_key_certs``
~~~~~~~~~~~~~~~~~~~~~~~~~~~

JWT signing keys.
Expand All @@ -372,14 +372,16 @@ JWT signing keys.

**Value** JWT public key or certificate, represented as a DER-encoded string.

``jwt.public_signing_key_issuer``
``jwt.public_signing_key_issuers``
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

JWT signing key to Issuer mapping.
JWT signing key to Issuers and Constraints mapping.

**Key** JWT Key ID, represented as a string.

**Value** JWT issuer URL, represented as a string.
**Value** List of Issuers and Constraints used to validate the Issuer during authorization. represented as JSON.

See :cpp:struct:`ccf::JwtIssuerWithConstraint`.

``constitution``
~~~~~~~~~~~~~~~~
Expand Down
32 changes: 29 additions & 3 deletions doc/schemas/gov_openapi.json
Original file line number Diff line number Diff line change
Expand Up @@ -394,6 +394,26 @@
],
"type": "object"
},
"JwtIssuerWithConstraint": {
"properties": {
"constraint": {
"$ref": "#/components/schemas/string"
},
"issuer": {
"$ref": "#/components/schemas/string"
}
},
"required": [
"issuer"
],
"type": "object"
},
"JwtIssuerWithConstraint_array": {
"items": {
"$ref": "#/components/schemas/JwtIssuerWithConstraint"
},
"type": "array"
},
"KeyIdInfo": {
"properties": {
"cert": {
Expand Down Expand Up @@ -1262,6 +1282,12 @@
},
"type": "object"
},
"string_to_JwtIssuerWithConstraint_array": {
"additionalProperties": {
"$ref": "#/components/schemas/JwtIssuerWithConstraint_array"
},
"type": "object"
},
"string_to_KeyIdInfo": {
"additionalProperties": {
"$ref": "#/components/schemas/KeyIdInfo"
Expand Down Expand Up @@ -1335,7 +1361,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.1.6"
"version": "4.1.7"
},
"openapi": "3.0.0",
"paths": {
Expand Down Expand Up @@ -1744,14 +1770,14 @@
}
}
},
"/gov/kv/jwt/public_signing_key_issuer": {
"/gov/kv/jwt/public_signing_key_issuers": {
"get": {
"responses": {
"200": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/string_to_string"
"$ref": "#/components/schemas/string_to_JwtIssuerWithConstraint_array"
}
}
},
Expand Down
4 changes: 2 additions & 2 deletions include/ccf/common_auth_policies.h
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,8 @@ namespace ccf
std::make_shared<MemberCertAuthnPolicy>();

/** Authenticate using JWT, validating the token using the
* @c public:ccf.gov.jwt.public_signing_key_issuer and
* @c public:ccf.gov.jwt.public_signing_keys tables */
* @c public:ccf.gov.jwt.public_signing_key_issuers and
* @c public:ccf.gov.jwt.public_signing_key_certs tables */
static std::shared_ptr<JwtAuthnPolicy> jwt_auth_policy =
std::make_shared<JwtAuthnPolicy>();

Expand Down
3 changes: 2 additions & 1 deletion include/ccf/crypto/jwk.h
Original file line number Diff line number Diff line change
Expand Up @@ -27,12 +27,13 @@ namespace crypto
JsonWebKeyType kty;
std::optional<std::string> kid = std::nullopt;
std::optional<std::vector<std::string>> x5c = std::nullopt;
std::optional<std::string> issuer = std::nullopt;

bool operator==(const JsonWebKey&) const = default;
};
DECLARE_JSON_TYPE_WITH_OPTIONAL_FIELDS(JsonWebKey);
DECLARE_JSON_REQUIRED_FIELDS(JsonWebKey, kty);
DECLARE_JSON_OPTIONAL_FIELDS(JsonWebKey, kid, x5c);
DECLARE_JSON_OPTIONAL_FIELDS(JsonWebKey, kid, x5c, issuer);

enum class JsonWebKeyECCurve
{
Expand Down
30 changes: 24 additions & 6 deletions include/ccf/service/tables/jwt.h
Original file line number Diff line number Diff line change
Expand Up @@ -58,18 +58,36 @@ namespace ccf
using JwtKeyId = std::string;
using Cert = std::vector<uint8_t>;

struct JwtIssuerWithConstraint
{
JwtIssuer issuer;
std::optional<JwtIssuer> constraint;
};
DECLARE_JSON_TYPE_WITH_OPTIONAL_FIELDS(JwtIssuerWithConstraint);
DECLARE_JSON_REQUIRED_FIELDS(JwtIssuerWithConstraint, issuer);
DECLARE_JSON_OPTIONAL_FIELDS(JwtIssuerWithConstraint, constraint);

using JwtIssuers = ServiceMap<JwtIssuer, JwtIssuerMetadata>;
using JwtPublicSigningKeys = kv::RawCopySerialisedMap<JwtKeyId, Cert>;
using JwtPublicSigningKeyIssuer =
kv::RawCopySerialisedMap<JwtKeyId, JwtIssuer>;
using JwtPublicSigningKeyIssuers =
ServiceMap<JwtKeyId, std::vector<JwtIssuerWithConstraint>>;

namespace Tables
{
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";

namespace Legacy
{
static constexpr auto JWT_PUBLIC_SIGNING_KEYS =
"public:ccf.gov.jwt.public_signing_key";
static constexpr auto JWT_PUBLIC_SIGNING_KEY_ISSUER =
"public:ccf.gov.jwt.public_signing_key_issuer";
}

static constexpr auto JWT_PUBLIC_SIGNING_KEY_CERTS =
"public:ccf.gov.jwt.public_signing_key_certs";
static constexpr auto JWT_PUBLIC_SIGNING_KEY_ISSUERS =
"public:ccf.gov.jwt.public_signing_key_issuers";
}

struct JsonWebKeySet
Expand Down
107 changes: 102 additions & 5 deletions src/endpoints/authentication/jwt_auth.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,94 @@

#include "ccf/endpoints/authentication/jwt_auth.h"

#include "ccf/ds/nonstd.h"
#include "ccf/pal/locking.h"
#include "ccf/rpc_context.h"
#include "ccf/service/tables/jwt.h"
#include "ds/lru.h"
#include "enclave/enclave_time.h"
#include "http/http_jwt.h"

namespace
{
static const std::string multitenancy_indicator{"{tenantid}"};
static const std::string microsoft_entra_domain{"login.microsoftonline.com"};

std::optional<std::string_view> first_non_empty_chunk(
const std::vector<std::string_view>& chunks)
{
for (auto chunk : chunks)
{
if (!chunk.empty())
{
return chunk;
}
}
return std::nullopt;
}

bool validate_issuer(
const http::JwtVerifier::Token& token, std::string issuer)
{
LOG_INFO_FMT(
"Verify token.iss {} and token.tid {} against published key issuer {}",
token.payload_typed.iss,
token.payload_typed.tid,
issuer);

const bool is_microsoft_entra =
issuer.find(microsoft_entra_domain) != std::string::npos;
if (!is_microsoft_entra)
{
return token.payload_typed.iss == issuer;
}

// Specify tenant if working with multi-tenant endpoint.
const auto pos = issuer.find(multitenancy_indicator);
if (pos != std::string::npos && token.payload_typed.tid)
{
issuer.replace(
pos, multitenancy_indicator.size(), *token.payload_typed.tid);
}

// Step 1. Verify the token issuer against the key issuer.
if (token.payload_typed.iss != issuer)
{
return false;
}

// Step 2. Verify that token.tid is served as a part of token.iss. According
// to the documentation, we only accept this format:
//
// https://domain.com/tenant_id/something_else
//
// Here url.path == "/tenant_id/something_else".

const auto url = http::parse_url_full(token.payload_typed.iss);
const auto tenant_id = first_non_empty_chunk(nonstd::split(url.path, "/"));

return (
tenant_id && token.payload_typed.tid &&
*token.payload_typed.tid == tenant_id.value());
}

bool validate_issuers(
const http::JwtVerifier::Token& token,
const std::vector<ccf::JwtIssuerWithConstraint>& issuers,
std::string& validated_issuer)
{
return std::any_of(issuers.begin(), issuers.end(), [&](const auto& issuer) {
if (issuer.constraint && validate_issuer(token, *issuer.constraint))
{
validated_issuer = issuer.issuer;
return true;
}
return false;
});
}

}

namespace ccf
{
struct VerifiersCache
Expand Down Expand Up @@ -58,11 +139,20 @@ namespace ccf
{
auto& token = token_opt.value();
auto keys =
tx.ro<JwtPublicSigningKeys>(ccf::Tables::JWT_PUBLIC_SIGNING_KEYS);
auto key_issuers = tx.ro<JwtPublicSigningKeyIssuer>(
ccf::Tables::JWT_PUBLIC_SIGNING_KEY_ISSUER);
tx.ro<JwtPublicSigningKeys>(ccf::Tables::JWT_PUBLIC_SIGNING_KEY_CERTS);
auto key_issuers = tx.ro<JwtPublicSigningKeyIssuers>(
ccf::Tables::JWT_PUBLIC_SIGNING_KEY_ISSUERS);
const auto key_id = token.header_typed.kid;
const auto token_key = keys->get(key_id);
auto token_key = keys->get(key_id);
const auto issuers = key_issuers->get(key_id);
std::string validated_issuer{};

if (!token_key.has_value())
{
auto fallback_keys = tx.ro<JwtPublicSigningKeys>(
ccf::Tables::Legacy::JWT_PUBLIC_SIGNING_KEYS);
token_key = fallback_keys->get(key_id);
}

if (!token_key.has_value())
{
Expand Down Expand Up @@ -96,10 +186,17 @@ namespace ccf
time_now,
token.payload_typed.exp);
}
else if (
!issuers ||
!validate_issuers(token, *issuers, std::ref(validated_issuer)))
{
error_reason =
fmt::format("Kid {} failed issuer validation", key_id);
}
else
{
auto identity = std::make_unique<JwtAuthnIdentity>();
identity->key_issuer = key_issuers->get(key_id).value();
identity->key_issuer = validated_issuer;
identity->header = std::move(token.header);
identity->payload = std::move(token.payload);
return identity;
Expand Down
7 changes: 5 additions & 2 deletions src/http/http_jwt.h
Original file line number Diff line number Diff line change
Expand Up @@ -32,9 +32,12 @@ namespace http
{
size_t nbf;
size_t exp;
std::string iss;
std::optional<std::string> tid;
};
DECLARE_JSON_TYPE(JwtPayload)
DECLARE_JSON_REQUIRED_FIELDS(JwtPayload, nbf, exp)
DECLARE_JSON_TYPE_WITH_OPTIONAL_FIELDS(JwtPayload)
DECLARE_JSON_REQUIRED_FIELDS(JwtPayload, nbf, exp, iss);
DECLARE_JSON_OPTIONAL_FIELDS(JwtPayload, tid)

class JwtVerifier
{
Expand Down
12 changes: 6 additions & 6 deletions src/node/gov/handlers/service_state.h
Original file line number Diff line number Diff line change
Expand Up @@ -466,10 +466,10 @@ namespace ccf::gov::endpoints

auto jwt_keys_handle =
ctx.tx.template ro<ccf::JwtPublicSigningKeys>(
ccf::Tables::JWT_PUBLIC_SIGNING_KEYS);
ccf::Tables::JWT_PUBLIC_SIGNING_KEY_CERTS);
auto jwt_key_issuers_handle =
ctx.tx.template ro<ccf::JwtPublicSigningKeyIssuer>(
ccf::Tables::JWT_PUBLIC_SIGNING_KEY_ISSUER);
ctx.tx.template ro<ccf::JwtPublicSigningKeyIssuers>(
ccf::Tables::JWT_PUBLIC_SIGNING_KEY_ISSUERS);

jwt_keys_handle->foreach(
[&keys, jwt_key_issuers_handle](
Expand All @@ -480,10 +480,10 @@ namespace ccf::gov::endpoints
const auto cert_pem = crypto::cert_der_to_pem(cert);
key_info["certificate"] = cert_pem.str();

const auto issuer = jwt_key_issuers_handle->get(kid);
if (issuer.has_value())
const auto issuers = jwt_key_issuers_handle->get(kid);
if (issuers && !issuers->empty())
{
key_info["issuer"] = issuer.value();
key_info["issuer"] = issuers->front().issuer;
}
else
{
Expand Down
Loading

0 comments on commit b2bc164

Please sign in to comment.