Skip to content

Commit

Permalink
WIP raw key JWT auth .
Browse files Browse the repository at this point in the history
  • Loading branch information
maxtropets committed Dec 3, 2024
1 parent 04a550f commit 08e056f
Show file tree
Hide file tree
Showing 15 changed files with 330 additions and 245 deletions.
56 changes: 3 additions & 53 deletions doc/schemas/gov_openapi.json
Original file line number Diff line number Diff line change
Expand Up @@ -291,27 +291,6 @@
},
"type": "object"
},
"KeyIdInfo": {
"properties": {
"cert": {
"$ref": "#/components/schemas/Pem"
},
"issuer": {
"$ref": "#/components/schemas/string"
}
},
"required": [
"issuer",
"cert"
],
"type": "object"
},
"KeyIdInfo_array": {
"items": {
"$ref": "#/components/schemas/KeyIdInfo"
},
"type": "array"
},
"MDType": {
"enum": [
"NONE",
Expand Down Expand Up @@ -808,10 +787,12 @@
},
"issuer": {
"$ref": "#/components/schemas/string"
},
"public_key": {
"$ref": "#/components/schemas/base64string"
}
},
"required": [
"cert",
"issuer"
],
"type": "object"
Expand Down Expand Up @@ -1222,12 +1203,6 @@
},
"type": "object"
},
"string_to_KeyIdInfo_array": {
"additionalProperties": {
"$ref": "#/components/schemas/KeyIdInfo_array"
},
"type": "object"
},
"string_to_OpenIDJWKMetadata_array": {
"additionalProperties": {
"$ref": "#/components/schemas/OpenIDJWKMetadata_array"
Expand Down Expand Up @@ -1473,31 +1448,6 @@
}
]
},
"/gov/jwt_keys/all": {
"get": {
"deprecated": true,
"description": "This endpoint is deprecated from 5.0.0. It is replaced by POST /gov/service/jwk",
"operationId": "GetGovJwtKeysAll",
"responses": {
"200": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/string_to_KeyIdInfo_array"
}
}
},
"description": "Default response description"
},
"default": {
"$ref": "#/components/responses/default"
}
},
"x-ccf-forwarding": {
"$ref": "#/components/x-ccf-forwarding/always"
}
}
},
"/gov/kv/constitution": {
"get": {
"deprecated": true,
Expand Down
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
7 changes: 7 additions & 0 deletions include/ccf/crypto/rsa_public_key.h
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,13 @@ namespace ccf::crypto
MDType md_type = MDType::NONE,
size_t salt_legth = 0) = 0;

virtual bool verify_pkcs1(
const uint8_t* contents,
size_t contents_size,
const uint8_t* signature,
size_t signature_size,
MDType md_type = MDType::NONE) = 0;

struct Components
{
std::vector<uint8_t> n;
Expand Down
4 changes: 2 additions & 2 deletions include/ccf/endpoints/authentication/jwt_auth.h
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ namespace ccf
nlohmann::json payload;
};

struct VerifiersCache;
struct PublicKeysCache;

bool validate_issuer(
const std::string& iss,
Expand All @@ -28,7 +28,7 @@ namespace ccf
{
protected:
static const OpenAPISecuritySchema security_schema;
std::unique_ptr<VerifiersCache> verifiers;
std::unique_ptr<PublicKeysCache> keys_cache;

public:
static constexpr auto SECURITY_SCHEME_NAME = "jwt";
Expand Down
9 changes: 5 additions & 4 deletions include/ccf/service/tables/jwt.h
Original file line number Diff line number Diff line change
Expand Up @@ -37,16 +37,17 @@ 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<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, public_key, constraint);

using JwtIssuers = ServiceMap<JwtIssuer, JwtIssuerMetadata>;
using JwtPublicSigningKeys =
Expand Down Expand Up @@ -75,7 +76,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
25 changes: 16 additions & 9 deletions samples/constitutions/default/actions.js
Original file line number Diff line number Diff line change
Expand Up @@ -130,15 +130,22 @@ function checkJwks(value, field) {
for (const [i, jwk] of value.keys.entries()) {
checkType(jwk.kid, "string", `${field}.keys[${i}].kid`);
checkType(jwk.kty, "string", `${field}.keys[${i}].kty`);
checkType(jwk.x5c, "array", `${field}.keys[${i}].x5c`);
checkLength(jwk.x5c, 1, null, `${field}.keys[${i}].x5c`);
for (const [j, b64der] of jwk.x5c.entries()) {
checkType(b64der, "string", `${field}.keys[${i}].x5c[${j}]`);
const pem =
"-----BEGIN CERTIFICATE-----\n" +
b64der +
"\n-----END CERTIFICATE-----";
checkX509CertBundle(pem, `${field}.keys[${i}].x5c[${j}]`);
if (jwk.x5c) {
checkType(jwk.x5c, "array", `${field}.keys[${i}].x5c`);
checkLength(jwk.x5c, 1, null, `${field}.keys[${i}].x5c`);
for (const [j, b64der] of jwk.x5c.entries()) {
checkType(b64der, "string", `${field}.keys[${i}].x5c[${j}]`);
const pem =
"-----BEGIN CERTIFICATE-----\n" +
b64der +
"\n-----END CERTIFICATE-----";
checkX509CertBundle(pem, `${field}.keys[${i}].x5c[${j}]`);
}
} else if (jwk.n && jwk.e) {
checkType(jwk.n, "string", `${field}.keys[${i}].n`);
checkType(jwk.e, "string", `${field}.keys[${i}].e`);
} else {
throw new Error("JWK must contain either x5c or n and e");
}
}
}
Expand Down
16 changes: 16 additions & 0 deletions src/crypto/openssl/rsa_public_key.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -208,6 +208,22 @@ namespace ccf::crypto
pctx, signature, signature_size, hash.data(), hash.size()) == 1;
}

bool RSAPublicKey_OpenSSL::verify_pkcs1(
const uint8_t* contents,
size_t contents_size,
const uint8_t* signature,
size_t signature_size,
MDType md_type)
{
auto hash = OpenSSLHashProvider().Hash(contents, contents_size, md_type);
Unique_EVP_PKEY_CTX pctx(key);
CHECK1(EVP_PKEY_verify_init(pctx));
CHECK1(EVP_PKEY_CTX_set_rsa_padding(pctx, RSA_PKCS1_PADDING));
CHECK1(EVP_PKEY_CTX_set_signature_md(pctx, get_md_type(md_type)));
return EVP_PKEY_verify(
pctx, signature, signature_size, hash.data(), hash.size()) == 1;
}

std::vector<uint8_t> RSAPublicKey_OpenSSL::bn_bytes(const BIGNUM* bn)
{
std::vector<uint8_t> r(BN_num_bytes(bn));
Expand Down
7 changes: 7 additions & 0 deletions src/crypto/openssl/rsa_public_key.h
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,13 @@ namespace ccf::crypto
MDType md_type = MDType::NONE,
size_t salt_length = 0) override;

virtual bool verify_pkcs1(
const uint8_t* contents,
size_t contents_size,
const uint8_t* signature,
size_t signature_size,
MDType md_type = MDType::NONE) override;

virtual Components components() const override;

static std::vector<uint8_t> bn_bytes(const BIGNUM* bn);
Expand Down
52 changes: 34 additions & 18 deletions src/endpoints/authentication/jwt_auth.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

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

#include "ccf/crypto/rsa_key_pair.h"
#include "ccf/ds/nonstd.h"
#include "ccf/pal/locking.h"
#include "ccf/rpc_context.h"
Expand Down Expand Up @@ -82,34 +83,32 @@ namespace ccf
return tenant_id && tid && *tid == *tenant_id;
}

struct VerifiersCache
struct PublicKeysCache
{
static constexpr size_t DEFAULT_MAX_VERIFIERS = 10;
static constexpr size_t DEFAULT_MAX_KEYS = 10;

using DER = std::vector<uint8_t>;
ccf::pal::Mutex verifiers_lock;
LRU<DER, ccf::crypto::VerifierPtr> verifiers;
ccf::pal::Mutex keys_lock;
LRU<DER, ccf::crypto::RSAPublicKeyPtr> keys;

VerifiersCache(size_t max_verifiers = DEFAULT_MAX_VERIFIERS) :
verifiers(max_verifiers)
{}
PublicKeysCache(size_t max_keys = DEFAULT_MAX_KEYS) : keys(max_keys) {}

ccf::crypto::VerifierPtr get_verifier(const DER& der)
ccf::crypto::RSAPublicKeyPtr get_key(const DER& der)
{
std::lock_guard<ccf::pal::Mutex> guard(verifiers_lock);
std::lock_guard<ccf::pal::Mutex> guard(keys_lock);

auto it = verifiers.find(der);
if (it == verifiers.end())
auto it = keys.find(der);
if (it == keys.end())
{
it = verifiers.insert(der, ccf::crypto::make_unique_verifier(der));
it = keys.insert(der, ccf::crypto::make_rsa_public_key(der));
}

return it->second;
}
};

JwtAuthnPolicy::JwtAuthnPolicy() :
verifiers(std::make_unique<VerifiersCache>())
keys_cache(std::make_unique<PublicKeysCache>())
{}

JwtAuthnPolicy::~JwtAuthnPolicy() = default;
Expand Down Expand Up @@ -141,11 +140,14 @@ namespace ccf
auto fallback_issuers = tx.ro<Tables::Legacy::JwtPublicSigningKeyIssuer>(
ccf::Tables::Legacy::JWT_PUBLIC_SIGNING_KEY_ISSUER);

auto fallback_key = fallback_keys->get(key_id);
if (fallback_key)
auto fallback_cert = fallback_keys->get(key_id);
if (fallback_cert)
{
// Legacy keys are stored as certs, new approach is raw keys, so
// conversion is needed to implicitly work futher down the code.
auto verifier = ccf::crypto::make_unique_verifier(*fallback_cert);
token_keys = std::vector<OpenIDJWKMetadata>{OpenIDJWKMetadata{
.cert = *fallback_key,
.public_key = verifier->public_key_der(),
.issuer = *fallback_issuers->get(key_id),
.constraint = std::nullopt}};
}
Expand All @@ -160,8 +162,22 @@ namespace ccf

for (const auto& metadata : *token_keys)
{
auto verifier = verifiers->get_verifier(metadata.cert);
if (!::http::JwtVerifier::validate_token_signature(token, verifier))
if (!metadata.public_key.has_value())
{
error_reason =
fmt::format("Missing public key for a given kid: {}", key_id);
continue;
}

const auto pubkey = keys_cache->get_key(metadata.public_key.value());
// Obsolote PKCS1 padding is chosen for JWT, as explained in details here:
// https://github.com/microsoft/CCF/issues/6601#issuecomment-2512059875.
if (!pubkey->verify_pkcs1(
(uint8_t*)token.signed_content.data(),
token.signed_content.size(),
token.signature.data(),
token.signature.size(),
ccf::crypto::MDType::SHA256))
{
error_reason = "Signature verification failed";
continue;
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 @@ -578,11 +578,13 @@ 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();

if (metadata.public_key.has_value())
{
info["publicKey"] = ccf::crypto::make_rsa_public_key(
metadata.public_key.value())
->public_key_pem()
.str();
}
info["issuer"] = metadata.issuer;
info["constraint"] = metadata.constraint;

Expand Down
Loading

0 comments on commit 08e056f

Please sign in to comment.