Skip to content

Commit

Permalink
WIP
Browse files Browse the repository at this point in the history
  • Loading branch information
maxtropets committed Nov 19, 2024
1 parent f1aed3e commit 6c2a7f6
Show file tree
Hide file tree
Showing 8 changed files with 192 additions and 117 deletions.
18 changes: 16 additions & 2 deletions include/ccf/crypto/jwk.h
Original file line number Diff line number Diff line change
Expand Up @@ -27,13 +27,27 @@ namespace ccf::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, issuer);
DECLARE_JSON_OPTIONAL_FIELDS(JsonWebKey, kid, x5c);

struct JsonWebKeyExtended
{
JsonWebKeyType kty;
std::optional<std::string> kid = std::nullopt;
std::optional<std::vector<std::string>> x5c = std::nullopt;
std::optional<std::string> n = std::nullopt;
std::optional<std::string> e = std::nullopt;
std::optional<std::string> issuer = std::nullopt;

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

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

struct OpenIDJWKMetadata
{
Cert cert;
std::optional<Cert> cert;
std::optional<PublicKey> public_key;
JwtIssuer issuer;
std::optional<JwtIssuer> constraint;
};
DECLARE_JSON_TYPE_WITH_OPTIONAL_FIELDS(OpenIDJWKMetadata);
DECLARE_JSON_REQUIRED_FIELDS(OpenIDJWKMetadata, cert, issuer);
DECLARE_JSON_OPTIONAL_FIELDS(OpenIDJWKMetadata, constraint);
DECLARE_JSON_REQUIRED_FIELDS(OpenIDJWKMetadata, issuer);
DECLARE_JSON_OPTIONAL_FIELDS(OpenIDJWKMetadata, cert, public_key, constraint);

using JwtIssuers = ServiceMap<JwtIssuer, JwtIssuerMetadata>;
using JwtPublicSigningKeys =
Expand Down Expand Up @@ -75,7 +77,7 @@ namespace ccf

struct JsonWebKeySet
{
std::vector<ccf::crypto::JsonWebKey> keys;
std::vector<ccf::crypto::JsonWebKeyExtended> keys;

bool operator!=(const JsonWebKeySet& rhs) const
{
Expand Down
2 changes: 1 addition & 1 deletion src/endpoints/authentication/jwt_auth.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -160,7 +160,7 @@ namespace ccf

for (const auto& metadata : *token_keys)
{
auto verifier = verifiers->get_verifier(metadata.cert);
auto verifier = verifiers->get_verifier(metadata.cert.value());
if (!::http::JwtVerifier::validate_token_signature(token, verifier))
{
error_reason = "Signature verification failed";
Expand Down
27 changes: 23 additions & 4 deletions src/node/gov/handlers/service_state.h
Original file line number Diff line number Diff line change
Expand Up @@ -578,10 +578,29 @@ namespace ccf::gov::endpoints
{
auto info = nlohmann::json::object();

// cert is stored as DER - convert to PEM for API
const auto cert_pem =
ccf::crypto::cert_der_to_pem(metadata.cert);
info["certificate"] = cert_pem.str();
std::string key{}, value{};
if (metadata.public_key.has_value())
{
key = "publicKey";
value = ccf::crypto::make_rsa_public_key(
metadata.public_key.value())
->public_key_pem()
.str();
}
else if (metadata.cert.has_value())
{
key = "certificate";
value =
ccf::crypto::cert_der_to_pem(metadata.cert.value()).str();
}
else
{
// This must not happen, but we intentionally ignore it here
// as this is just a key reporting endpoint, this situation
// must be prevented at the time of storing the new keys.
}

info[key] = value;

info["issuer"] = metadata.issuer;
info["constraint"] = metadata.constraint;
Expand Down
181 changes: 113 additions & 68 deletions src/node/rpc/jwt_management.h
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
// Licensed under the Apache 2.0 License.
#pragma once

#include "ccf/crypto/rsa_key_pair.h"
#include "ccf/crypto/verifier.h"
#include "ccf/ds/hex.h"
#include "ccf/service/tables/jwt.h"
Expand All @@ -12,6 +13,67 @@
#include <set>
#include <sstream>

namespace
{
std::pair<std::vector<uint8_t>, bool> try_parse_jwk(
const ccf::crypto::JsonWebKeyExtended& jwk)
{
const auto& kid = jwk.kid.value();
if (
jwk.e.has_value() && !jwk.e->empty() && jwk.n.has_value() &&
!jwk.n->empty())
{
std::vector<uint8_t> der;
ccf::crypto::JsonWebKeyRSAPublic data;
data.kty = ccf::crypto::JsonWebKeyType::RSA;
data.kid = jwk.kid;
data.n = jwk.n.value();
data.e = jwk.e.value();
try
{
const auto pubkey = ccf::crypto::make_rsa_public_key(data);
der = pubkey->public_key_der();
}
catch (const std::invalid_argument& exc)
{
throw std::logic_error(
fmt::format("Failed to construct RSA public key: {}", exc.what()));
}
return {der, false};
}
else if (jwk.x5c.has_value() && !jwk.x5c->empty())
{
auto& der_base64 = jwk.x5c.value()[0];
ccf::Cert der;
try
{
der = ccf::crypto::raw_from_b64(der_base64);
}
catch (const std::invalid_argument& e)
{
throw std::logic_error(
fmt::format("Could not parse x5c of key id {}: {}", kid, e.what()));
}
try
{
ccf::crypto::make_unique_verifier(der); // throws
}
catch (std::invalid_argument& exc)
{
throw std::logic_error(fmt::format(
"JWKS kid {} has an invalid X.509 certificate: {}", kid, exc.what()));
}

return {der, true};
}
else
{
throw std::logic_error(
fmt::format("JWKS kid {} has neither x5c or RSA public key", kid));
}
}
}

namespace ccf
{
static void legacy_remove_jwt_public_signing_keys(
Expand All @@ -37,8 +99,8 @@ namespace ccf
const std::string& issuer, const std::string& constraint)
{
// Only accept key constraints for the same (sub)domain. This is to avoid
// setting keys from issuer A which will be used to validate iss claims for
// issuer B, so this doesn't make sense (at least for now).
// setting keys from issuer A which will be used to validate iss claims
// for issuer B, so this doesn't make sense (at least for now).

const auto issuer_domain = ::http::parse_url_full(issuer).host;
const auto constraint_domain = ::http::parse_url_full(constraint).host;
Expand All @@ -48,13 +110,13 @@ namespace ccf
return false;
}

// Either constraint's domain == issuer's domain or it is a subdomain, e.g.:
// limited.facebook.com
// Either constraint's domain == issuer's domain or it is a subdomain,
// e.g.: limited.facebook.com
// .facebook.com
//
// It may make sense to support vice-versa too, but we haven't found any
// instances of that so far, so leaveing it only-way only for facebook-like
// cases.
// instances of that so far, so leaveing it only-way only for
// facebook-like cases.
if (issuer_domain != constraint_domain)
{
const auto pattern = "." + constraint_domain;
Expand All @@ -68,8 +130,8 @@ namespace ccf
ccf::kv::Tx& tx, std::string issuer)
{
// Unlike resetting JWT keys for a particular issuer, removing keys can be
// safely done on both table revisions, as soon as the application shouldn't
// use them anyway after being ask about that explicitly.
// safely done on both table revisions, as soon as the application
// shouldn't use them anyway after being ask about that explicitly.
legacy_remove_jwt_public_signing_keys(tx, issuer);

auto keys =
Expand Down Expand Up @@ -113,74 +175,46 @@ namespace ccf
LOG_FAIL_FMT("{}: JWKS has no keys", log_prefix);
return false;
}
std::map<std::string, std::vector<uint8_t>> new_keys;
using DerValue = std::pair<std::vector<uint8_t>, bool /* is cert */>;
std::map<std::string, DerValue> new_keys;
std::map<std::string, JwtIssuer> issuer_constraints;
for (auto& jwk : jwks.keys)
{
if (!jwk.kid.has_value())
{
LOG_FAIL_FMT("No kid for JWT signing key");
return false;
}

if (!jwk.x5c.has_value() && jwk.x5c->empty())
{
LOG_FAIL_FMT("{}: JWKS is invalid (empty x5c)", log_prefix);
return false;
}

auto& der_base64 = jwk.x5c.value()[0];
ccf::Cert der;
auto const& kid = jwk.kid.value();
try
{
der = ccf::crypto::raw_from_b64(der_base64);
}
catch (const std::invalid_argument& e)
{
LOG_FAIL_FMT(
"{}: Could not parse x5c of key id {}: {}",
log_prefix,
kid,
e.what());
return false;
}

try
{
ccf::crypto::make_unique_verifier(
(std::vector<uint8_t>)der); // throws on error
}
catch (std::invalid_argument& exc)
try
{
for (auto& jwk : jwks.keys)
{
LOG_FAIL_FMT(
"{}: JWKS kid {} has an invalid X.509 certificate: {}",
log_prefix,
kid,
exc.what());
return false;
}
if (!jwk.kid.has_value())
{
throw(std::logic_error("Missing kid for JWT signing key"));
}

LOG_INFO_FMT("{}: Storing JWT signing key with kid {}", log_prefix, kid);
new_keys.emplace(kid, der);
const auto& kid = jwk.kid.value();
auto [der, is_cert] = try_parse_jwk(jwk);

if (jwk.issuer)
{
if (!check_issuer_constraint(issuer, *jwk.issuer))
if (jwk.issuer)
{
LOG_FAIL_FMT(
"{}: JWKS kid {} with issuer constraint {} fails validation "
"against issuer {}",
log_prefix,
kid,
*jwk.issuer,
issuer);
return false;
if (!check_issuer_constraint(issuer, *jwk.issuer))
{
throw std::logic_error(fmt::format(
"JWKS kid {} with issuer constraint {} fails validation "
"against "
"issuer {}",
kid,
*jwk.issuer,
issuer));
}

issuer_constraints.emplace(kid, *jwk.issuer);
}

issuer_constraints.emplace(kid, *jwk.issuer);
new_keys.emplace(kid, DerValue{std::move(der), is_cert});
}
}
catch (const std::exception& exc)
{
LOG_FAIL_FMT("{}: {}", log_prefix, exc.what());
return false;
}

if (new_keys.empty())
{
Expand All @@ -201,9 +235,20 @@ namespace ccf
return true;
});

for (auto& [kid, der] : new_keys)
for (auto& [kid, data] : new_keys)
{
OpenIDJWKMetadata value{der, issuer, std::nullopt};
OpenIDJWKMetadata value{std::nullopt, std::nullopt, issuer, std::nullopt};

const auto& [der, is_cert] = data;
if (is_cert)
{
value.cert = der;
}
else
{
value.public_key = der;
}

const auto it = issuer_constraints.find(kid);
if (it != issuer_constraints.end())
{
Expand Down
3 changes: 2 additions & 1 deletion src/node/rpc/member_frontend.h
Original file line number Diff line number Diff line change
Expand Up @@ -1108,7 +1108,8 @@ namespace ccf
for (const auto& metadata : v)
{
info.push_back(KeyIdInfo{
metadata.issuer, ccf::crypto::cert_der_to_pem(metadata.cert)});
metadata.issuer,
ccf::crypto::cert_der_to_pem(metadata.cert.value())});
}
kmap.emplace(k, std::move(info));
return true;
Expand Down
Loading

0 comments on commit 6c2a7f6

Please sign in to comment.