Skip to content

Commit

Permalink
JWT WIP
Browse files Browse the repository at this point in the history
  • Loading branch information
maxtropets committed May 22, 2024
1 parent b9c2499 commit 8d2f033
Show file tree
Hide file tree
Showing 13 changed files with 459 additions and 55 deletions.
9 changes: 9 additions & 0 deletions doc/audit/builtin_maps.rst
Original file line number Diff line number Diff line change
Expand Up @@ -381,6 +381,15 @@ JWT signing key to Issuer mapping.

**Value** JWT issuer URL, represented as a string.

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

JWT signing key to CHANGE-ME mapping.

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

**Value** CHANGE-ME.

``constitution``
~~~~~~~~~~~~~~~~

Expand Down
50 changes: 49 additions & 1 deletion 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 @@ -1766,6 +1792,28 @@
}
}
},
"/gov/kv/jwt/public_signing_key_issuers": {
"get": {
"responses": {
"200": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/string_to_JwtIssuerWithConstraint_array"
}
}
},
"description": "Default response description"
},
"default": {
"$ref": "#/components/responses/default"
}
},
"x-ccf-forwarding": {
"$ref": "#/components/x-ccf-forwarding/sometimes"
}
}
},
"/gov/kv/jwt/public_signing_keys": {
"get": {
"responses": {
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
14 changes: 14 additions & 0 deletions include/ccf/service/tables/jwt.h
Original file line number Diff line number Diff line change
Expand Up @@ -58,10 +58,21 @@ 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
{
Expand All @@ -70,6 +81,9 @@ namespace ccf
"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 JWT_PUBLIC_SIGNING_KEY_ISSUERS =
"public:ccf.gov.jwt.public_signing_key_issuers";

}

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

#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)
{
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 == tenant_id.value());
}

bool validate_issuers(
const http::JwtVerifier::Token& token,
const std::vector<ccf::JwtIssuerWithConstraint>& issuers)
{
return std::any_of(issuers.begin(), issuers.end(), [&](const auto& issuer) {
return issuer.constraint.has_value() &&
validate_issuer(token, issuer.constraint.value());
});
}

}

namespace ccf
{
struct VerifiersCache
Expand Down Expand Up @@ -59,10 +133,11 @@ 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);
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);
const auto issuers = key_issuers->get(key_id);

if (!token_key.has_value())
{
Expand Down Expand Up @@ -96,10 +171,24 @@ namespace ccf
time_now,
token.payload_typed.exp);
}
else if (issuers->empty())
{
error_reason = fmt::format(
"Issuer validation failure for kid {} - issuer not found",
key_id);
}
else if (!validate_issuers(token, issuers.value()))
{
error_reason = fmt::format(
"Token issuer for kid {} failed validation agains the key issuer",
key_id);
}
else
{
auto identity = std::make_unique<JwtAuthnIdentity>();
identity->key_issuer = key_issuers->get(key_id).value();
// TODO(#same-kid-different-issuer) back-propagate the proper issuer
// we checked against in case there are many.
identity->key_issuer = issuers->front().issuer;
identity->header = std::move(token.header);
identity->payload = std::move(token.payload);
return identity;
Expand Down
4 changes: 3 additions & 1 deletion src/http/http_jwt.h
Original file line number Diff line number Diff line change
Expand Up @@ -32,9 +32,11 @@ namespace http
{
size_t nbf;
size_t exp;
std::string iss;
std::string tid;
};
DECLARE_JSON_TYPE(JwtPayload)
DECLARE_JSON_REQUIRED_FIELDS(JwtPayload, nbf, exp)
DECLARE_JSON_REQUIRED_FIELDS(JwtPayload, nbf, exp, iss, tid)

class JwtVerifier
{
Expand Down
12 changes: 7 additions & 5 deletions src/node/gov/handlers/service_state.h
Original file line number Diff line number Diff line change
Expand Up @@ -468,8 +468,8 @@ namespace ccf::gov::endpoints
ctx.tx.template ro<ccf::JwtPublicSigningKeys>(
ccf::Tables::JWT_PUBLIC_SIGNING_KEYS);
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,12 @@ 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())
// TODO(#same-kid-different-issuer) we must either populate all
// issuers or choose one. To be discussed later on.
const auto issuers = jwt_key_issuers_handle->get(kid);
if (issuers.has_value() && !issuers->empty())
{
key_info["issuer"] = issuer.value();
key_info["issuer"] = issuers->front().issuer;
}
else
{
Expand Down
30 changes: 27 additions & 3 deletions src/node/jwt_key_auto_refresh.h
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,7 @@ namespace ccf

void handle_jwt_jwks_response(
const std::string& issuer,
const std::optional<std::string>& issuer_constraint,
http_status status,
std::vector<uint8_t>&& data)
{
Expand Down Expand Up @@ -173,6 +174,20 @@ namespace ccf

// call internal endpoint to update keys
auto msg = SetJwtPublicSigningKeys{issuer, jwks};

// For each key we leave the specified issuer constraint or set a common
// one otherwise (if present).
if (issuer_constraint.has_value())
{
for (auto& key : jwks.keys)
{
if (!key.issuer.has_value())
{
key.issuer = issuer_constraint;
}
}
}

send_refresh_jwt_keys(msg);
}

Expand Down Expand Up @@ -201,9 +216,10 @@ namespace ccf
issuer);

std::string jwks_url_str;
nlohmann::json metadata;
try
{
auto metadata = nlohmann::json::parse(data);
metadata = nlohmann::json::parse(data);
jwks_url_str = metadata.at("jwks_uri").get<std::string>();
}
catch (const std::exception& e)
Expand Down Expand Up @@ -235,6 +251,13 @@ namespace ccf
auto ca_cert = std::make_shared<tls::Cert>(
ca, std::nullopt, std::nullopt, jwks_url.host);

std::optional<std::string> issuer_constraint{std::nullopt};
const auto constraint = metadata.find("issuer");
if (constraint != metadata.end())
{
issuer_constraint = *constraint;
}

LOG_DEBUG_FMT(
"JWT key auto-refresh: Requesting JWKS at https://{}:{}{}",
jwks_url.host,
Expand All @@ -246,9 +269,10 @@ namespace ccf
http_client->connect(
std::string(jwks_url.host),
std::string(jwks_url_port),
[this, issuer](
[this, issuer, issuer_constraint](
http_status status, http::HeaderMap&&, std::vector<uint8_t>&& data) {
handle_jwt_jwks_response(issuer, status, std::move(data));
handle_jwt_jwks_response(
issuer, issuer_constraint, status, std::move(data));
return true;
});
http::Request r(jwks_url.path, HTTP_GET);
Expand Down
Loading

0 comments on commit 8d2f033

Please sign in to comment.