| // 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 |