blob: b391d5e2e8feb7de707b51478b91868b0152d4ef [file] [log] [blame]
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
//
// Copied from Impala and adapted to Kudu.
#include "kudu/util/jwt-util.h"
#include <openssl/bn.h>
#include <openssl/crypto.h>
#include <openssl/ec.h>
#include <openssl/obj_mac.h>
#include <openssl/pem.h>
#include <openssl/ssl.h>
#include <openssl/x509.h>
#include <sys/stat.h>
#include <cerrno>
#include <cstdint>
#include <cstdio>
#include <cstring>
#include <exception>
#include <functional>
#include <mutex>
#include <ostream>
#include <stdexcept>
#include <type_traits>
#include <typeinfo>
#include <unordered_map>
#include <utility>
#include <vector>
#include <gflags/gflags.h>
#include <glog/logging.h>
#include <jwt-cpp/jwt.h>
#include <jwt-cpp/traits/kazuho-picojson/defaults.h>
#include <jwt-cpp/traits/kazuho-picojson/traits.h>
#include <rapidjson/document.h>
#include <rapidjson/error/en.h>
#include <rapidjson/filereadstream.h>
#include <rapidjson/rapidjson.h>
#include "kudu/gutil/map-util.h"
#include "kudu/gutil/port.h"
#include "kudu/gutil/ref_counted.h"
#include "kudu/gutil/strings/escaping.h"
#include "kudu/gutil/strings/split.h"
#include "kudu/gutil/strings/substitute.h"
#include "kudu/util/curl_util.h"
#include "kudu/util/faststring.h"
#include "kudu/util/hash_util.h"
#include "kudu/util/jwt-util-internal.h"
#include "kudu/util/monotime.h"
#include "kudu/util/openssl_util.h"
#include "kudu/util/openssl_util_bio.h"
#include "kudu/util/promise.h"
#include "kudu/util/status.h"
#include "kudu/util/string_case.h"
#include "kudu/util/thread.h"
using kudu::security::DataFormat;
using kudu::security::GetOpenSSLErrors;
using kudu::security::ToString;
using kudu::security::c_unique_ptr;
using kudu::security::ssl_make_unique;
using rapidjson::Document;
using rapidjson::Value;
using std::make_shared;
using std::shared_ptr;
using std::string;
using std::unique_ptr;
using strings::Substitute;
using strings::WebSafeBase64Unescape;
DEFINE_int32(jwks_update_frequency_s, 60,
"The time in seconds to wait between downloading JWKS from the specified URL.");
DEFINE_int32(jwks_pulling_timeout_s, 10,
"The time in seconds for connection timed out when pulling JWKS from the specified URL.");
static bool ValidateBiggerThanZero(const char* name, const int32_t val) {
if (val <= 0) {
LOG(ERROR) << Substitute("Invalid value for $0 flag: $1", name, val);
return false;
}
return true;
}
DEFINE_validator(jwks_update_frequency_s, &ValidateBiggerThanZero);
DEFINE_validator(jwks_pulling_timeout_s, &ValidateBiggerThanZero);
namespace kudu {
namespace security {
template<> struct SslTypeTraits<BIGNUM> {
static constexpr auto kFreeFunc = &BN_free;
};
// Need this function because of template instantiation, but it's never used.
int WriteDerFuncNotImplementedEC(BIO* /*ununsed*/, EC_KEY* /*unused*/) {
LOG(DFATAL) << "this should never be called";
return -1;
}
template<> struct SslTypeTraits<EC_KEY> {
static constexpr auto kFreeFunc = &EC_KEY_free;
static constexpr auto kWritePemFunc = &PEM_write_bio_EC_PUBKEY;
static constexpr auto kWriteDerFunc = &WriteDerFuncNotImplementedEC;
};
// Need this function because of template instantiation, but it's never used.
int WriteDerNotImplementedRSA(BIO* /*unused*/, RSA* /*unused*/) {
LOG(DFATAL) << "this should never be called";
return -1;
}
template<> struct SslTypeTraits<RSA> {
static constexpr auto kFreeFunc = &RSA_free;
static constexpr auto kWritePemFunc = &PEM_write_bio_RSA_PUBKEY;
static constexpr auto kWriteDerFunc = &WriteDerNotImplementedRSA;
};
} // namespace security
// JWK Set (JSON Web Key Set) is JSON data structure that represents a set of JWKs.
// This class parses JWKS file.
class JWKSetParser {
public:
explicit JWKSetParser(JWKSSnapshot* jwks) : jwks_(jwks) {}
// Perform the parsing and populate JWKS's internal map. Return error status if
// encountering any error.
Status Parse(const Document& rules_doc) {
bool found_keys = false;
for (Value::ConstMemberIterator member = rules_doc.MemberBegin();
member != rules_doc.MemberEnd(); ++member) {
if (strcmp("keys", member->name.GetString()) == 0) {
found_keys = true;
RETURN_NOT_OK(ParseKeys(member->value));
} else {
return Status::InvalidArgument(
Substitute(
"Unexpected property '$0' must be removed", member->name.GetString()));
}
}
if (!found_keys) {
return Status::InvalidArgument("An array of keys is required");
}
return Status::OK();
}
private:
JWKSSnapshot* jwks_;
static string NameOfTypeOfJsonValue(const Value& value) {
switch (value.GetType()) {
case rapidjson::kNullType:
return "Null";
case rapidjson::kFalseType:
case rapidjson::kTrueType:
return "Bool";
case rapidjson::kObjectType:
return "Object";
case rapidjson::kArrayType:
return "Array";
case rapidjson::kStringType:
return "String";
case rapidjson::kNumberType:
if (value.IsInt()) return "Integer";
if (value.IsDouble()) return "Float";
default:
DCHECK(false);
return "Unknown";
}
}
// Parse an array of keys.
Status ParseKeys(const Value& keys) {
if (!keys.IsArray()) {
return Status::InvalidArgument(
Substitute(
"'keys' must be of type Array but is a '$0'", NameOfTypeOfJsonValue(keys)));
}
if (keys.Size() == 0) {
return Status::InvalidArgument(Substitute("'keys' must be a non empty Array"));
}
for (rapidjson::SizeType key_idx = 0; key_idx < keys.Size(); ++key_idx) {
const Value& key = keys[key_idx];
if (!key.IsObject()) {
return Status::InvalidArgument(
Substitute("parsing key #$0, key should be a JSON Object but is a '$1'.",
key_idx, NameOfTypeOfJsonValue(key)));
}
Status status = ParseKey(key);
if (!status.ok()) {
Status parse_status = Status::InvalidArgument(Substitute("parsing key #$0, ", key_idx));
return parse_status.CloneAndAppend(status.message());
}
}
return Status::OK();
}
// Parse a public key and populate JWKS's internal map.
Status ParseKey(const Value& json_key) {
std::unordered_map<std::string, std::string> kv_map;
string key;
string value;
for (Value::ConstMemberIterator member = json_key.MemberBegin();
member != json_key.MemberEnd(); ++member) {
key = string(member->name.GetString());
RETURN_NOT_OK(ReadKeyProperty(key, json_key, &value, /*required*/ false));
if (!EmplaceIfNotPresent(&kv_map, key, value)) {
LOG(WARNING) << "Duplicate property of JWK: " << key;
}
}
const auto* key_type = FindOrNull(kv_map, "kty");
if (!key_type) return Status::InvalidArgument("'kty' property is required");
const auto* key_id = FindOrNull(kv_map, "kid");
if (!key_id) return Status::InvalidArgument("'kid' property is required");
if (key_id->empty()) {
return Status::InvalidArgument(Substitute("'kid' property must be a non-empty string"));
}
string key_type_lower;
ToLowerCase(*key_type, &key_type_lower);
if (key_type_lower == "oct") {
unique_ptr<JWTPublicKey> jwt_pub_key;
RETURN_NOT_OK(HSJWTPublicKeyBuilder::CreateJWKPublicKey(kv_map, &jwt_pub_key));
jwks_->AddHSKey(*key_id, std::move(jwt_pub_key));
} else if (key_type_lower == "rsa") {
unique_ptr<JWTPublicKey> jwt_pub_key;
RETURN_NOT_OK(RSAJWTPublicKeyBuilder::CreateJWKPublicKey(kv_map, &jwt_pub_key));
jwks_->AddRSAPublicKey(*key_id, std::move(jwt_pub_key));
} else if (key_type_lower == "ec") {
unique_ptr<JWTPublicKey> jwt_pub_key;
RETURN_NOT_OK(ECJWTPublicKeyBuilder::CreateJWKPublicKey(kv_map, &jwt_pub_key));
jwks_->AddECPublicKey(*key_id, std::move(jwt_pub_key));
} else {
return Status::InvalidArgument(Substitute("Unsupported kty: '$0'", key_type));
}
return Status::OK();
}
// Reads a key property of the given name and assigns the property value to the out
// parameter. A true return value indicates success.
template <typename T>
Status ReadKeyProperty(
const string& name, const Value& json_key, T* value, bool required = true) {
const Value& json_value = json_key[name.c_str()];
if (json_value.IsNull()) {
if (required) {
return Status::InvalidArgument(Substitute("'$0' property is required and cannot be null",
name));
}
return Status::OK();
}
return ValidateTypeAndExtractValue(name, json_value, value);
}
// Extract a value stored in a rapidjson::Value and assign it to the out parameter.
// The type will be validated before extraction. A true return value indicates success.
// The name parameter is only used to generate an error message upon failure.
#define EXTRACT_VALUE(json_type, cpp_type) \
Status ValidateTypeAndExtractValue( \
const string& name, const Value& json_value, cpp_type* value) { \
if (!json_value.Is##json_type()) { \
return Status::InvalidArgument( \
Substitute("'$0' property must be of type " #json_type " but is a $1", name, \
NameOfTypeOfJsonValue(json_value))); \
} \
*value = json_value.Get##json_type(); \
return Status::OK(); \
}
EXTRACT_VALUE(String, string)
// EXTRACT_VALUE(Bool, bool)
};
namespace {
// Utility function to handle exceptions from the jwt-cpp library.
Status HandleEx(const char* const msg, const std::exception& e) {
const auto err = GetOpenSSLErrors();
return Status::NotAuthorized(err.empty()
? Substitute("$0: $1", msg, e.what())
: Substitute("$0: $1 ($2)", msg, e.what(), err));
}
} // anonymous namespace
//
// JWTPublicKey member functions.
//
// Verify JWT's signature for the given decoded token with jwt-cpp API.
Status JWTPublicKey::Verify(
const DecodedJWT& decoded_jwt, const std::string& algorithm) const {
SCOPED_OPENSSL_NO_PENDING_ERRORS;
// Verify if algorithms are matching.
if (algorithm_ != algorithm) {
return Status::NotAuthorized(
Substitute("JWT algorithm '$0' is not matching with JWK algorithm '$1'",
algorithm, algorithm_));
}
try {
// Call jwt-cpp API to verify token's signature.
verifier_.verify(decoded_jwt);
} catch (const std::exception& e) {
return HandleEx("JWT verification failed", e);
}
return Status::OK();
}
// Create a JWKPublicKey of HS from the JWK.
Status HSJWTPublicKeyBuilder::CreateJWKPublicKey(
const JsonKVMap& kv_map, unique_ptr<JWTPublicKey>* pub_key_out) {
SCOPED_OPENSSL_NO_PENDING_ERRORS;
// Octet Sequence keys for HS256, HS384 or HS512.
// JWK Sample:
// {
// "kty":"oct",
// "alg":"HS256",
// "k":"f83OJ3D2xF1Bg8vub9tLe1gHMzV76e8Tus9uPHvRVEU",
// "kid":"Id that can be uniquely Identified"
// }
auto it_alg = kv_map.find("alg");
if (it_alg == kv_map.end()) return Status::InvalidArgument("'alg' property is required");
string algorithm;
ToLowerCase(it_alg->second, &algorithm);
if (algorithm.empty()) {
return Status::InvalidArgument(Substitute("'alg' property must be a non-empty string"));
}
auto it_k = kv_map.find("k");
if (it_k == kv_map.end()) return Status::InvalidArgument("'k' property is required");
if (it_k->second.empty()) {
return Status::InvalidArgument(Substitute("'k' property must be a non-empty string"));
}
unique_ptr<JWTPublicKey> jwt_pub_key;
try {
if (algorithm == "hs256") {
jwt_pub_key.reset(new HS256JWTPublicKey(algorithm, it_k->second));
} else if (algorithm == "hs384") {
jwt_pub_key.reset(new HS384JWTPublicKey(algorithm, it_k->second));
} else if (algorithm == "hs512") {
jwt_pub_key.reset(new HS512JWTPublicKey(algorithm, it_k->second));
} else {
return Status::InvalidArgument(Substitute("Invalid 'alg' property value: '$0'", algorithm));
}
} catch (const std::exception& e) {
return HandleEx("failed to initialize verifier", e);
}
*pub_key_out = std::move(jwt_pub_key);
return Status::OK();
}
// Create a JWKPublicKey of RSA from the JWK.
Status RSAJWTPublicKeyBuilder::CreateJWKPublicKey(
const JsonKVMap& kv_map, unique_ptr<JWTPublicKey>* pub_key_out) {
SCOPED_OPENSSL_NO_PENDING_ERRORS;
// JWK Sample:
// {
// "kty":"RSA",
// "alg":"RS256",
// "n":"sttddbg-_yjXzcFpbMJB1fI9...Q_QDhvqXx8eQ1r9smM",
// "e":"AQAB",
// "kid":"Id that can be uniquely Identified"
// }
auto it_alg = kv_map.find("alg");
if (it_alg == kv_map.end()) return Status::InvalidArgument("'alg' property is required");
string algorithm;
ToLowerCase(it_alg->second, &algorithm);
if (algorithm.empty()) {
return Status::InvalidArgument(Substitute("'alg' property must be a non-empty string"));
}
auto it_n = kv_map.find("n");
auto it_e = kv_map.find("e");
if (it_n == kv_map.end() || it_e == kv_map.end()) {
return Status::InvalidArgument("'n' and 'e' properties are required");
}
if (it_n->second.empty() || it_e->second.empty()) {
return Status::InvalidArgument("'n' and 'e' properties must be a non-empty string");
}
// Converts public key to PEM encoded form.
string pub_key;
RETURN_NOT_OK_PREPEND(ConvertJwkToPem(it_n->second, it_e->second, pub_key),
Substitute("invalid public key 'n':'$0', 'e':'$1'",
it_n->second, it_e->second));
unique_ptr<JWTPublicKey> jwt_pub_key;
try {
if (algorithm == "rs256") {
jwt_pub_key.reset(new RS256JWTPublicKey(algorithm, pub_key));
} else if (algorithm == "rs384") {
jwt_pub_key.reset(new RS384JWTPublicKey(algorithm, pub_key));
} else if (algorithm == "rs512") {
jwt_pub_key.reset(new RS512JWTPublicKey(algorithm, pub_key));
} else if (algorithm == "ps256") {
jwt_pub_key.reset(new PS256JWTPublicKey(algorithm, pub_key));
} else if (algorithm == "ps384") {
jwt_pub_key.reset(new PS384JWTPublicKey(algorithm, pub_key));
} else if (algorithm == "ps512") {
jwt_pub_key.reset(new PS512JWTPublicKey(algorithm, pub_key));
} else {
return Status::InvalidArgument(Substitute("Invalid 'alg' property value: '$0'", algorithm));
}
} catch (const std::exception& e) {
return HandleEx("failed to initialize verifier", e);
}
*pub_key_out = std::move(jwt_pub_key);
return Status::OK();
}
// Convert JWK's RSA public key to PEM format using OpenSSL API.
Status RSAJWTPublicKeyBuilder::ConvertJwkToPem(
const string& base64_n, const string& base64_e, string& pub_key) {
SCOPED_OPENSSL_NO_PENDING_ERRORS;
string str_n;
if (!WebSafeBase64Unescape(base64_n, &str_n)) {
return Status::InvalidArgument("malformed 'n' key component");
}
string str_e;
if (!WebSafeBase64Unescape(base64_e, &str_e)) {
return Status::InvalidArgument("malformed 'e' key component");
}
auto mod = ssl_make_unique(BN_bin2bn(
reinterpret_cast<const unsigned char*>(str_n.c_str()),
static_cast<int>(str_n.size()),
nullptr));
auto exp = ssl_make_unique(BN_bin2bn(
reinterpret_cast<const unsigned char*>(str_e.c_str()),
static_cast<int>(str_e.size()),
nullptr));
auto rsa = ssl_make_unique(RSA_new());
#if OPENSSL_VERSION_NUMBER < 0x10100000L
rsa->n = mod.release();
rsa->e = exp.release();
#else
// RSA_set0_key is a new API introduced in OpenSSL version 1.1
OPENSSL_RET_NOT_OK(RSA_set0_key(
rsa.get(), mod.release(), exp.release(), nullptr), "failed to set RSA key");
#endif
return ToString(&pub_key, DataFormat::PEM, rsa.get());
}
// Create a JWKPublicKey of EC (ES256, ES384 or ES512) from the JWK.
Status ECJWTPublicKeyBuilder::CreateJWKPublicKey(
const JsonKVMap& kv_map, unique_ptr<JWTPublicKey>* pub_key_out) {
SCOPED_OPENSSL_NO_PENDING_ERRORS;
// JWK Sample:
// {
// "kty":"EC",
// "crv":"P-256",
// "x":"f83OJ3D2xF1Bg8vub9tLe1gHMzV76e8Tus9uPHvRVEU",
// "y":"x_FEzRu9m36HLN_tue659LNpXW6pCyStikYjKIWI5a0",
// "kid":"Id that can be uniquely Identified"
// }
string algorithm;
int eccgrp;
auto it_crv = kv_map.find("crv");
if (it_crv != kv_map.end()) {
string curve;
ToUpperCase(it_crv->second, &curve);
if (curve == "P-256") {
algorithm = "es256";
eccgrp = NID_X9_62_prime256v1;
} else if (curve == "P-384") {
algorithm = "es384";
eccgrp = NID_secp384r1;
} else if (curve == "P-521") {
algorithm = "es512";
eccgrp = NID_secp521r1;
} else {
return Status::NotSupported(Substitute("Unsupported crv: '$0'", curve));
}
} else {
auto it_alg = kv_map.find("alg");
if (it_alg == kv_map.end()) {
return Status::InvalidArgument("'alg' or 'crv' property is required");
}
ToLowerCase(it_alg->second, &algorithm);
if (algorithm.empty()) {
return Status::InvalidArgument(Substitute("'alg' property must be a non-empty string"));
}
if (algorithm == "es256") {
// ECDSA using P-256 and SHA-256 (OBJ_txt2nid("prime256v1")).
eccgrp = NID_X9_62_prime256v1;
} else if (algorithm == "es384") {
// ECDSA using P-384 and SHA-384 (OBJ_txt2nid("secp384r1")).
eccgrp = NID_secp384r1;
} else if (algorithm == "es512") {
// ECDSA using P-521 and SHA-512 (OBJ_txt2nid("secp521r1")).
eccgrp = NID_secp521r1;
} else {
return Status::NotSupported(Substitute("Unsupported alg: '$0'", algorithm));
}
}
auto it_x = kv_map.find("x");
auto it_y = kv_map.find("y");
if (it_x == kv_map.end() || it_y == kv_map.end()) {
return Status::InvalidArgument("'x' and 'y' properties are required");
}
if (it_x->second.empty() || it_y->second.empty()) {
return Status::InvalidArgument("'x' and 'y' properties must be a non-empty string");
}
// Convert the public key into PEM format.
string pub_key;
RETURN_NOT_OK_PREPEND(ConvertJwkToPem(eccgrp, it_x->second, it_y->second, pub_key),
Substitute("invalid public key 'x':'$0', 'y':'$1'",
it_x->second, it_y->second));
JWTPublicKey* jwt_pub_key = nullptr;
try {
if (algorithm == "es256") {
jwt_pub_key = new ES256JWTPublicKey(algorithm, pub_key);
} else if (algorithm == "es384") {
jwt_pub_key = new ES384JWTPublicKey(algorithm, pub_key);
} else {
DCHECK(algorithm == "es512");
jwt_pub_key = new ES512JWTPublicKey(algorithm, pub_key);
}
} catch (const std::exception& e) {
return HandleEx("failed to initialize verifier", e);
}
pub_key_out->reset(jwt_pub_key);
return Status::OK();
}
// Convert JWK's EC public key to PEM format using OpenSSL API.
Status ECJWTPublicKeyBuilder::ConvertJwkToPem(int eccgrp,
const string& base64_x,
const string& base64_y,
string& pub_key) {
SCOPED_OPENSSL_NO_PENDING_ERRORS;
string ascii_x;
if (!WebSafeBase64Unescape(base64_x, &ascii_x)) {
return Status::InvalidArgument("malformed 'x' key component");
}
string ascii_y;
if (!WebSafeBase64Unescape(base64_y, &ascii_y)) {
return Status::InvalidArgument("malformed 'y' key component");
}
auto x = ssl_make_unique(BN_bin2bn(
reinterpret_cast<const unsigned char*>(ascii_x.c_str()),
static_cast<int>(ascii_x.size()),
nullptr));
auto y = ssl_make_unique(BN_bin2bn(
reinterpret_cast<const unsigned char*>(ascii_y.c_str()),
static_cast<int>(ascii_y.size()),
nullptr));
auto ec_key = ssl_make_unique(EC_KEY_new_by_curve_name(eccgrp));
OPENSSL_RET_IF_NULL(ec_key, "failed to create EC key");
EC_KEY_set_asn1_flag(ec_key.get(), OPENSSL_EC_NAMED_CURVE);
OPENSSL_RET_NOT_OK(EC_KEY_set_public_key_affine_coordinates(
ec_key.get(), x.get(), y.get()), "failed to set public key");
return ToString(&pub_key, DataFormat::PEM, ec_key.get());
}
//
// JWKSSnapshot member functions.
//
// Load JWKS from the given local json file.
Status JWKSSnapshot::LoadKeysFromFile(const string& jwks_file_path) {
hs_key_map_.clear();
rsa_pub_key_map_.clear();
// Read the file.
FILE* jwks_file = fopen(jwks_file_path.c_str(), "r");
if (jwks_file == nullptr) {
return Status::RuntimeError(
Substitute("Could not open JWKS file '$0'; $1", jwks_file_path, strerror(errno)));
}
// Check for an empty file and ignore it.
struct stat jwks_file_stats;
if (fstat(fileno(jwks_file), &jwks_file_stats)) {
fclose(jwks_file);
return Status::RuntimeError(
Substitute("Error reading JWKS file '$0'; $1", jwks_file_path, strerror(errno)));
}
if (jwks_file_stats.st_size == 0) {
fclose(jwks_file);
return Status::OK();
}
char readBuffer[65536];
rapidjson::FileReadStream stream(jwks_file, readBuffer, sizeof(readBuffer));
Document jwks_doc;
jwks_doc.ParseStream(stream);
fclose(jwks_file);
if (jwks_doc.HasParseError()) {
return Status::InvalidArgument(GetParseError_En(jwks_doc.GetParseError()));
}
if (!jwks_doc.IsObject()) {
return Status::InvalidArgument("root element must be a JSON Object");
}
if (!jwks_doc.HasMember("keys")) {
return Status::InvalidArgument("keys is required");
}
JWKSetParser jwks_parser(this);
return jwks_parser.Parse(jwks_doc);
}
// Download JWKS from the given URL with Kudu's EasyCurl wrapper.
Status JWKSSnapshot::LoadKeysFromUrl(
const std::string& jwks_url, bool jwks_verify_server_certificate, uint64_t cur_jwks_checksum,
bool* is_changed) {
kudu::EasyCurl curl;
kudu::faststring dst;
*is_changed = false;
curl.set_timeout(
kudu::MonoDelta::FromMilliseconds(static_cast<int64_t>(FLAGS_jwks_pulling_timeout_s) * 1000));
curl.set_verify_peer(jwks_verify_server_certificate);
// TODO support CurlAuthType by calling kudu::EasyCurl::set_auth().
RETURN_NOT_OK_PREPEND(curl.FetchURL(jwks_url, &dst),
Substitute("Error downloading JWKS from '$0'", jwks_url));
if (dst.size() > 0) {
// Verify if the checksum of the downloaded JWKS has been changed.
jwks_checksum_ = HashUtil::FastHash64(dst.data(), dst.size(), /*seed*/ 0xcafebeef);
if (jwks_checksum_ == cur_jwks_checksum) {
return Status::OK();
}
// Append '\0' so that the in-memory object could be parsed as StringStream.
dst.push_back('\0');
#ifndef NDEBUG
VLOG(3) << "JWKS: " << dst.data();
#endif
// Parse in-memory JWKS JSON object as StringStream.
Document jwks_doc;
jwks_doc.Parse(reinterpret_cast<char*>(dst.data()));
if (jwks_doc.HasParseError()) {
return Status::InvalidArgument(GetParseError_En(jwks_doc.GetParseError()));
}
if (!jwks_doc.IsObject()) {
return Status::InvalidArgument("root element must be a JSON Object");
}
if (!jwks_doc.HasMember("keys")) {
return Status::InvalidArgument("keys is required");
}
// Load and initialize public keys.
JWKSetParser jwks_parser(this);
RETURN_NOT_OK(jwks_parser.Parse(jwks_doc));
}
*is_changed = true;
return Status::OK();
}
void JWKSSnapshot::AddHSKey(const std::string& key_id,
unique_ptr<JWTPublicKey> jwk_pub_key) {
if (hs_key_map_.find(key_id) == hs_key_map_.end()) {
hs_key_map_[key_id] = std::move(jwk_pub_key);
} else {
LOG(WARNING) << "Duplicate key ID of JWK for HS key: " << key_id;
}
}
void JWKSSnapshot::AddRSAPublicKey(const std::string& key_id,
unique_ptr<JWTPublicKey> jwk_pub_key) {
if (rsa_pub_key_map_.find(key_id) == rsa_pub_key_map_.end()) {
rsa_pub_key_map_[key_id] = std::move(jwk_pub_key);
} else {
LOG(WARNING) << "Duplicate key ID of JWK for RSA public key: " << key_id;
}
}
void JWKSSnapshot::AddECPublicKey(const std::string& key_id,
unique_ptr<JWTPublicKey> jwk_pub_key) {
if (ec_pub_key_map_.find(key_id) == ec_pub_key_map_.end()) {
ec_pub_key_map_[key_id] = std::move(jwk_pub_key);
} else {
LOG(WARNING) << "Duplicate key ID of JWK for EC public key: " << key_id;
}
}
const JWTPublicKey* JWKSSnapshot::LookupHSKey(const std::string& kid) const {
return FindPointeeOrNull(hs_key_map_, kid);
}
const JWTPublicKey* JWKSSnapshot::LookupRSAPublicKey(const std::string& kid) const {
return FindPointeeOrNull(rsa_pub_key_map_, kid);
}
const JWTPublicKey* JWKSSnapshot::LookupECPublicKey(const std::string& kid) const {
return FindPointeeOrNull(ec_pub_key_map_, kid);
}
//
// JWKSMgr member functions.
//
JWKSMgr::~JWKSMgr() {
shut_down_promise_.Set(true);
if (jwks_update_thread_ != nullptr) jwks_update_thread_->Join();
}
Status JWKSMgr::Init(const std::string& jwks_uri, bool jwks_verify_server_certificate,
bool is_local_file) {
jwks_uri_ = jwks_uri;
jwks_verify_server_certificate_ = jwks_verify_server_certificate;
std::shared_ptr<JWKSSnapshot> new_jwks = std::make_shared<JWKSSnapshot>();
if (is_local_file) {
RETURN_NOT_OK_PREPEND(new_jwks->LoadKeysFromFile(jwks_uri), "Failed to load JWKS");
SetJWKSSnapshot(new_jwks);
} else {
bool is_changed = false;
RETURN_NOT_OK_PREPEND(new_jwks->LoadKeysFromUrl(jwks_uri, jwks_verify_server_certificate,
current_jwks_checksum_,
&is_changed),
"Failed to load JWKS");
DCHECK(is_changed);
if (is_changed) SetJWKSSnapshot(new_jwks);
// Start a working thread to periodically check the JWKS URL for updates.
RETURN_NOT_OK(Thread::Create("JWT", "JWKS-mgr",
[this] { return UpdateJWKSThread(); }, &jwks_update_thread_));
}
// This is only a warning as JWKS information might be changing over time, if for some reason,
// the file which URI is pointing at becomes empty, it'll still be downloaded, but no keys will be
// verified successfully (due to no public keys in the JWKS to do so).
// Since the UpdateJWKSThread is still alive, if the JWKS file/endpoint is fixed, then the
// verification will be successful again.
if (new_jwks->IsEmpty()) LOG(WARNING) << "JWKS file is empty.";
return Status::OK();
}
void JWKSMgr::UpdateJWKSThread() {
std::shared_ptr<JWKSSnapshot> new_jwks;
const MonoDelta &timeout = MonoDelta::FromSeconds(FLAGS_jwks_update_frequency_s);
while (true) {
if (shut_down_promise_.WaitFor(timeout) != nullptr) {
// Shutdown has happened, stop updating JWKS.
break;
}
new_jwks = std::make_shared<JWKSSnapshot>();
bool is_changed = false;
Status status =
new_jwks->LoadKeysFromUrl(jwks_uri_, jwks_verify_server_certificate_,
current_jwks_checksum_, &is_changed);
if (!status.ok()) {
LOG(WARNING) << "Failed to update JWKS: " << status.ToString();
} else if (is_changed) {
SetJWKSSnapshot(new_jwks);
if (new_jwks->IsEmpty()) {
LOG(WARNING) << "New JWKS snapshot is empty.";
}
}
new_jwks.reset();
}
// The promise must be set to true.
DCHECK(shut_down_promise_.Get());
}
JWKSSnapshotPtr JWKSMgr::GetJWKSSnapshot() const {
std::lock_guard<std::mutex> l(current_jwks_lock_);
DCHECK(current_jwks_.get() != nullptr);
JWKSSnapshotPtr jwks = current_jwks_;
return jwks;
}
void JWKSMgr::SetJWKSSnapshot(const JWKSSnapshotPtr& new_jwks) {
std::lock_guard<std::mutex> l(current_jwks_lock_);
DCHECK(new_jwks.get() != nullptr);
current_jwks_ = new_jwks;
current_jwks_checksum_ = new_jwks->GetChecksum();
}
//
// JWTHelper member functions.
//
JWTHelper::~JWTHelper() {
}
struct JWTHelper::JWTDecodedToken {
explicit JWTDecodedToken(DecodedJWT decoded_jwt) : decoded_jwt_(std::move(decoded_jwt)) {}
DecodedJWT decoded_jwt_;
};
JWTHelper* JWTHelper::jwt_helper_ = new JWTHelper();
void JWTHelper::TokenDeleter::operator()(JWTHelper::JWTDecodedToken* token) const {
delete token;
}
Status JWTHelper::Init(const std::string& jwks_file_path) {
return Init(jwks_file_path,
/*jwks_verify_server_certificate*/ false,
/*is_local_file*/ true);
}
Status JWTHelper::Init(const std::string& jwks_uri, bool jwks_verify_server_certificate,
bool is_local_file) {
jwks_mgr_.reset(new JWKSMgr());
RETURN_NOT_OK(jwks_mgr_->Init(jwks_uri, jwks_verify_server_certificate,
is_local_file));
if (!initialized_) initialized_ = true;
return Status::OK();
}
JWKSSnapshotPtr JWTHelper::GetJWKS() const {
DCHECK(initialized_);
return jwks_mgr_->GetJWKSSnapshot();
}
// Decode the given JWT token.
Status JWTHelper::Decode(const string& token, UniqueJWTDecodedToken& decoded_token_out) {
SCOPED_OPENSSL_NO_PENDING_ERRORS;
try {
// Call jwt-cpp API to decode the JWT token with default jwt::json_traits
// (jwt::picojson_traits).
decoded_token_out.reset(new JWTDecodedToken(jwt::decode(token)));
#ifndef NDEBUG
std::stringstream msg;
msg << "JWT token header: ";
for (auto& [key, val] : decoded_token_out.get()->decoded_jwt_.get_header_claims()) {
msg << key << "=" << val.to_json().serialize() << ";";
}
msg << " JWT token payload: ";
for (auto& [key, val] : decoded_token_out.get()->decoded_jwt_.get_payload_claims()) {
msg << key << "=" << val.to_json().serialize() << ";";
}
VLOG(3) << msg.str();
#endif
} catch (const std::invalid_argument& e) {
return HandleEx("token is not in correct format", e);
} catch (const std::runtime_error& e) {
return HandleEx("base64 decoding failed or invalid JSON", e);
} catch (const std::exception& e) {
return HandleEx("unexpected error while decoding JWT", e);
}
return Status::OK();
}
// Validate the token's signature with public key.
Status JWTHelper::Verify(const JWTDecodedToken* decoded_token) const {
SCOPED_OPENSSL_NO_PENDING_ERRORS;
DCHECK(initialized_);
const auto& decoded_jwt = decoded_token->decoded_jwt_;
if (decoded_jwt.get_signature().empty()) {
// Don't accept JWT without a signature.
return Status::NotAuthorized("Unsecured JWT");
}
if (jwks_mgr_ == nullptr) {
// Skip to signature validation if JWKS file or url is not specified.
return Status::OK();
}
JWKSSnapshotPtr jwks = GetJWKS();
if (jwks->IsEmpty()) {
return Status::NotAuthorized("Verification failed, no matching valid key");
}
try {
string algorithm;
ToLowerCase(decoded_jwt.get_algorithm(), &algorithm);
string prefix = algorithm.substr(0, 2);
if (decoded_jwt.has_key_id()) {
// Get key id from token's header and use it to retrieve the public key from JWKS.
std::string key_id = decoded_jwt.get_key_id();
const JWTPublicKey* pub_key = nullptr;
if (prefix == "hs") {
pub_key = jwks->LookupHSKey(key_id);
} else if (prefix == "rs" || prefix == "ps") {
pub_key = jwks->LookupRSAPublicKey(key_id);
} else if (prefix == "es") {
pub_key = jwks->LookupECPublicKey(key_id);
} else {
return Status::NotAuthorized(
Substitute("Unsupported cryptographic algorithm '$0' for JWT", algorithm));
}
if (pub_key == nullptr) {
return Status::NotAuthorized("Invalid JWK ID in the JWT token");
}
// Use the public key to verify the token's signature.
RETURN_NOT_OK(pub_key->Verify(decoded_jwt, algorithm));
} else {
// According to RFC 7517 (JSON Web Key), 'kid' is OPTIONAL so it's possible there
// is no key id in the token's header. In this case, get all of public keys from
// JWKS for the family of algorithms.
const JWKSSnapshot::JWTPublicKeyMap* key_map = nullptr;
if (prefix == "hs") {
key_map = jwks->GetAllHSKeys();
} else if (prefix == "rs" || prefix == "ps") {
key_map = jwks->GetAllRSAPublicKeys();
} else if (prefix == "es") {
key_map = jwks->GetAllECPublicKeys();
} else {
return Status::NotAuthorized(
Substitute("Unsupported cryptographic algorithm '$0' for JWT", algorithm));
}
if (key_map->empty()) {
return Status::NotAuthorized("Verification failed, no matching key");
}
Status status;
// Try each key with matching algorithm util the signature is verified.
for (const auto& key : *key_map) {
status = key.second->Verify(decoded_jwt, algorithm);
if (status.ok()) return status;
}
return status;
}
} catch (const std::bad_cast& e) {
return HandleEx("claim was present but not a string", e);
} catch (const jwt::error::claim_not_present_exception& e) {
return HandleEx("claim not present in JWT token", e);
} catch (const std::exception& e) {
return HandleEx("token verification failed", e);
}
return Status::OK();
}
Status JWTHelper::GetCustomClaimUsername(const JWTDecodedToken* decoded_token,
const string& jwt_custom_claim_username, string& username) {
SCOPED_OPENSSL_NO_PENDING_ERRORS;
DCHECK(!jwt_custom_claim_username.empty());
const auto& decoded_jwt = decoded_token->decoded_jwt_;
try {
// Get value of custom claim 'username' from the token payload.
if (!decoded_jwt.has_payload_claim(jwt_custom_claim_username)) {
return Status::NotAuthorized(
Substitute("Claim '$0' was not present", jwt_custom_claim_username));
}
username = decoded_jwt.get_payload_claim(jwt_custom_claim_username).to_json().to_str();
if (username.empty()) {
return Status::NotAuthorized(
Substitute("Claim '$0' is empty", jwt_custom_claim_username));
}
} catch (const std::runtime_error& e) {
return HandleEx(Substitute("claim '$0' was not present",
jwt_custom_claim_username).c_str(), e);
} catch (const std::exception& e) {
return HandleEx("unexpected error while processing JWT claim", e);
}
return Status::OK();
}
Status KeyBasedJwtVerifier::Init() {
return jwt_->Init(jwks_uri_, /*jwks_verify_server_certificate*/ false, is_local_file_);
}
Status KeyBasedJwtVerifier::VerifyToken(const string& bytes_raw, string* subject) const {
JWTHelper::UniqueJWTDecodedToken decoded_token;
RETURN_NOT_OK(JWTHelper::Decode(bytes_raw, decoded_token));
RETURN_NOT_OK(jwt_->Verify(decoded_token.get()));
if (!decoded_token->decoded_jwt_.has_subject()) {
return Status::InvalidArgument("token does not include subject");
}
*subject = decoded_token->decoded_jwt_.get_subject();
return Status::OK();
}
Status PerAccountKeyBasedJwtVerifier::JWTHelperForToken(const JWTHelper::JWTDecodedToken& token,
JWTHelper** helper) const {
if (!token.decoded_jwt_.has_issuer()) {
return Status::InvalidArgument("Expected token to have 'issuer' field");
}
// Parse the account ID from the 'iss' field of the JWT. If we already have a
// JWTHelper for it, use it.
const auto& issuer = token.decoded_jwt_.get_issuer();
std::vector<string> issuer_pieces = strings::Split(issuer, "/");
if (issuer_pieces.empty()) {
return Status::InvalidArgument("cannot parse 'issuer' field");
}
const auto& account_id = issuer_pieces.back();
{
const std::lock_guard<simple_spinlock> l(jwt_by_account_id_map_lock_);
const auto* unique_helper = FindOrNull(jwt_by_account_id_, account_id);
if (unique_helper) {
*helper = unique_helper->get();
return Status::OK();
}
}
// Otherwise, use the OIDC Discovery Endpoint to determine what 'jwks_uri' to
// use.
kudu::EasyCurl curl;
kudu::faststring dst;
const auto discovery_endpoint = Substitute("$0?accountId=$1", oidc_uri_, account_id);
curl.set_timeout(
kudu::MonoDelta::FromSeconds(static_cast<int64_t>(FLAGS_jwks_pulling_timeout_s)));
curl.set_verify_peer(false);
RETURN_NOT_OK_PREPEND(curl.FetchURL(discovery_endpoint, &dst),
Substitute("Error downloading contents of Discovery Endpoint from '$0'", discovery_endpoint));
string jwks_uri;
if (dst.size() <= 0) {
return Status::RuntimeError("Discovery Endpoint returned an empty document");
}
dst.push_back('\0');
Document endpoint_doc;
endpoint_doc.Parse(reinterpret_cast<char*>(dst.data()));
#define RETURN_INVALID_IF(stmt, msg) \
if (PREDICT_FALSE(stmt)) { \
return Status::InvalidArgument(msg); \
}
RETURN_INVALID_IF(endpoint_doc.HasParseError(), GetParseError_En(endpoint_doc.GetParseError()));
RETURN_INVALID_IF(!endpoint_doc.IsObject(), "root element must be a JSON Object");
auto jwks_uri_member = endpoint_doc.FindMember("jwks_uri");
RETURN_INVALID_IF(jwks_uri_member == endpoint_doc.MemberEnd(), "jwks_uri is required");
RETURN_INVALID_IF(!jwks_uri_member->value.IsString(), "jwks_uri must be a string");
jwks_uri = string(jwks_uri_member->value.GetString());
#undef RETURN_INVALID_IF
// TODO(zchovan): this implementation expects there to be a small number of
// accounts, as it creates a JWKS refresh thread for each account. Group the
// refreshes into a single thread or threadpool.
auto new_helper = std::make_shared<JWTHelper>();
RETURN_NOT_OK_PREPEND(new_helper->Init(jwks_uri,
jwks_verify_server_certificate_,
/*is_local_file*/ false),
"Error initializing JWT helper");
{
const std::lock_guard<simple_spinlock> l(jwt_by_account_id_map_lock_);
LookupOrEmplace(&jwt_by_account_id_, account_id, std::move(new_helper));
*helper = FindPointeeOrNull(jwt_by_account_id_, account_id);
}
return Status::OK();
}
Status PerAccountKeyBasedJwtVerifier::Init() {
for (auto& [account_id, verifier] : jwt_by_account_id_) {
RETURN_NOT_OK(verifier->Init(Substitute("$0?accountId=$1", oidc_uri_, account_id),
jwks_verify_server_certificate_,
/*is_local_file*/ false));
}
return Status::OK();
}
Status PerAccountKeyBasedJwtVerifier::VerifyToken(const string& bytes_raw, string* subject) const {
JWTHelper::UniqueJWTDecodedToken decoded_token;
RETURN_NOT_OK(JWTHelper::Decode(bytes_raw, decoded_token));
JWTHelper* jwt;
RETURN_NOT_OK(JWTHelperForToken(*decoded_token, &jwt));
RETURN_NOT_OK(jwt->Verify(decoded_token.get()));
if (!decoded_token->decoded_jwt_.has_subject()) {
return Status::InvalidArgument("token does not include subject");
}
*subject = decoded_token->decoded_jwt_.get_subject();
return Status::OK();
}
} // namespace kudu