This is an automated email from the ASF dual-hosted git repository. alexey pushed a commit to branch master in repository https://gitbox.apache.org/repos/asf/kudu.git
The following commit(s) were added to refs/heads/master by this push: new 98fa157bb jwt: add test for fetching JWKS via URL 98fa157bb is described below commit 98fa157bb72939c285094152d83f822431347e7a Author: Andrew Wong <aw...@cloudera.com> AuthorDate: Wed Apr 20 15:31:43 2022 -0700 jwt: add test for fetching JWKS via URL Co-authored-by: Zoltan Chovan <zcho...@cloudera.com> Change-Id: Ief272c813b62e789d747a88e1f3be8c406eed3f8 Reviewed-on: http://gerrit.cloudera.org:8080/18468 Tested-by: Alexey Serbin <ale...@apache.org> Reviewed-by: Alexey Serbin <ale...@apache.org> Reviewed-by: Wenzhe Zhou <wz...@cloudera.com> --- src/kudu/util/CMakeLists.txt | 3 +- src/kudu/util/jwt-util-internal.h | 22 +-- src/kudu/util/jwt-util-test.cc | 113 +++++++++---- src/kudu/util/jwt-util.cc | 323 +++++++++++++++++++------------------- 4 files changed, 260 insertions(+), 201 deletions(-) diff --git a/src/kudu/util/CMakeLists.txt b/src/kudu/util/CMakeLists.txt index 3a1fab523..5223d9ef6 100644 --- a/src/kudu/util/CMakeLists.txt +++ b/src/kudu/util/CMakeLists.txt @@ -629,7 +629,8 @@ endif() ADD_KUDU_TEST(jwt-util-test) if(NOT NO_TESTS) target_link_libraries(jwt-util-test - kudu_jwt_util) + kudu_jwt_util + server_process) endif() ####################################### diff --git a/src/kudu/util/jwt-util-internal.h b/src/kudu/util/jwt-util-internal.h index 49b7b3dc9..38c499d24 100644 --- a/src/kudu/util/jwt-util-internal.h +++ b/src/kudu/util/jwt-util-internal.h @@ -19,6 +19,7 @@ #pragma once +#include <memory> #include <mutex> #include <string> #include <unordered_map> @@ -44,8 +45,8 @@ typedef std::unordered_map<std::string, std::string> JsonKVMap; // This class represent cryptographic public key for JSON Web Token (JWT) verification. class JWTPublicKey { public: - JWTPublicKey(std::string algorithm, std::string pub_key) - : verifier_(jwt::verify()), algorithm_(std::move(algorithm)), public_key_(std::move(pub_key)) {} + JWTPublicKey(std::string algorithm, std::string pub_key) + : verifier_(jwt::verify()), algorithm_(std::move(algorithm)), public_key_(std::move(pub_key)) {} // Verify the given decoded token. Status Verify(const DecodedJWT& decoded_jwt, const std::string& algorithm) const; @@ -205,13 +206,15 @@ class ES512JWTPublicKey : public JWTPublicKey { // Construct a JWKPublicKey of HS from the JWK. class HSJWTPublicKeyBuilder { public: - static Status CreateJWKPublicKey(const JsonKVMap& kv_map, JWTPublicKey** pub_key_out); + static Status CreateJWKPublicKey(const JsonKVMap& kv_map, + std::unique_ptr<JWTPublicKey>* pub_key_out); }; // Construct a JWKPublicKey of RSA from the JWK. class RSAJWTPublicKeyBuilder { public: - static Status CreateJWKPublicKey(const JsonKVMap& kv_map, JWTPublicKey** pub_key_out); + static Status CreateJWKPublicKey(const JsonKVMap& kv_map, + std::unique_ptr<JWTPublicKey>* pub_key_out); private: // Convert public key of RSA from JWK format to PEM encoded format by using OpenSSL @@ -223,7 +226,8 @@ class RSAJWTPublicKeyBuilder { // Construct a JWKPublicKey of EC from the JWK. class ECJWTPublicKeyBuilder { public: - static Status CreateJWKPublicKey(const JsonKVMap& kv_map, JWTPublicKey** pub_key_out); + static Status CreateJWKPublicKey(const JsonKVMap& kv_map, + std::unique_ptr<JWTPublicKey>* pub_key_out); private: // Convert public key of EC from JWK format to PEM encoded format by using OpenSSL @@ -238,7 +242,7 @@ class ECJWTPublicKeyBuilder { // and updates it atomically when the public keys in JWKS are changed. Clients can obtain // an immutable copy. Class instances can be created through the implicitly-defined // default and copy constructors. -class JWKSSnapshot { +class JWKSSnapshot final { public: JWKSSnapshot() = default; JWKSSnapshot(const JWKSSnapshot&) = default; @@ -288,11 +292,11 @@ class JWKSSnapshot { // Following two functions are called inside Init(). // Add a RSA public key. - void AddRSAPublicKey(const std::string& key_id, JWTPublicKey* jwk_pub_key); + void AddRSAPublicKey(const std::string& key_id, std::unique_ptr<JWTPublicKey> jwk_pub_key); // Add a HS key. - void AddHSKey(const std::string& key_id, JWTPublicKey* jwk_pub_key); + void AddHSKey(const std::string& key_id, std::unique_ptr<JWTPublicKey> jwk_pub_key); // Add an EC public key. - void AddECPublicKey(const std::string& key_id, JWTPublicKey* jwk_pub_key); + void AddECPublicKey(const std::string& key_id, std::unique_ptr<JWTPublicKey> jwk_pub_key); // Note: According to section 4.5 of RFC 7517 (JSON Web Key), different keys might use // the same "kid" value is if they have different "kty" (key type) values but are diff --git a/src/kudu/util/jwt-util-test.cc b/src/kudu/util/jwt-util-test.cc index b035ad0ad..f9b4534f5 100644 --- a/src/kudu/util/jwt-util-test.cc +++ b/src/kudu/util/jwt-util-test.cc @@ -19,16 +19,13 @@ #include "kudu/util/jwt-util.h" -#include <errno.h> -#include <stdlib.h> -#include <string.h> - #include <chrono> -#include <cstdio> // file stuff +#include <functional> #include <iostream> #include <memory> #include <string> #include <type_traits> +#include <vector> #include <gtest/gtest.h> #include <jwt-cpp/jwt.h> @@ -36,15 +33,21 @@ #include <jwt-cpp/traits/kazuho-picojson/traits.h> #include "kudu/gutil/strings/substitute.h" +#include "kudu/server/webserver.h" +#include "kudu/server/webserver_options.h" #include "kudu/util/env.h" #include "kudu/util/jwt-util-internal.h" +#include "kudu/util/net/sockaddr.h" #include "kudu/util/slice.h" #include "kudu/util/status.h" #include "kudu/util/test_macros.h" +#include "kudu/util/web_callback_registry.h" namespace kudu { using std::string; +using std::unique_ptr; +using std::vector; using strings::Substitute; const std::string kRsaPrivKeyPem = R"(-----BEGIN PRIVATE KEY----- @@ -367,27 +370,21 @@ class TempTestDataFile { // Creates a temporary file with the specified contents. explicit TempTestDataFile(const std::string& contents); - ~TempTestDataFile() { Delete(); } - /// Returns the absolute path to the file. const std::string& Filename() const { return name_; } private: std::string name_; - bool deleted_; - - // Delete this temporary file - void Delete(); + std::unique_ptr<WritableFile> tmp_file_; }; TempTestDataFile::TempTestDataFile(const std::string& contents) - : name_("/tmp/jwks_XXXXXX"), deleted_(false) { - std::unique_ptr<WritableFile> tmp_file; + : name_("/tmp/jwks_XXXXXX") { string created_filename; WritableFileOptions opts; opts.is_sensitive = false; Status status; - status = Env::Default()->NewTempWritableFile(opts, &name_[0], &created_filename, &tmp_file); + status = Env::Default()->NewTempWritableFile(opts, &name_[0], &created_filename, &tmp_file_); if (!status.ok()) { std::cerr << Substitute("Error creating temp file: $0", created_filename); } @@ -400,15 +397,6 @@ TempTestDataFile::TempTestDataFile(const std::string& contents) name_ = created_filename; } -void TempTestDataFile::Delete() { - if (deleted_) return; - deleted_ = true; - if (remove(name_.c_str()) != 0) { - std::cout << "Error deleting temp file; " << strerror(errno) << std::endl; - abort(); - } -} - TEST(JwtUtilTest, LoadJwksFile) { // Load JWKS from file. TempTestDataFile jwks_file(Substitute(kJwksRsaFileFormat, kKid1, "RS256", @@ -1061,8 +1049,11 @@ TEST(JwtUtilTest, VerifyJwtTokenWithoutKeyId) { // Create a JWT token without key ID. auto token = - jwt::create().set_issuer("auth0").set_type("JWS").set_algorithm("RS256").sign( - jwt::algorithm::rs256(kRsaPubKeyPem, kRsaPrivKeyPem, "", "")); + jwt::create() + .set_issuer("auth0") + .set_type("JWS") + .set_algorithm("RS256") + .sign(jwt::algorithm::rs256(kRsaPubKeyPem, kRsaPrivKeyPem, "", "")); // Verify the token by trying each key in JWK set and there is one matched key. JWTHelper::UniqueJWTDecodedToken decoded_token; status = JWTHelper::Decode(token, decoded_token); @@ -1082,8 +1073,11 @@ TEST(JwtUtilTest, VerifyJwtFailTokenWithoutKeyId) { // Create a JWT token without key ID. auto token = - jwt::create().set_issuer("auth0").set_type("JWS").set_algorithm("RS512").sign( - jwt::algorithm::rs512(kRsa512PubKeyPem, kRsa512PrivKeyPem, "", "")); + jwt::create() + .set_issuer("auth0") + .set_type("JWS") + .set_algorithm("RS512") + .sign(jwt::algorithm::rs512(kRsa512PubKeyPem, kRsa512PrivKeyPem, "", "")); // Verify the token by trying each key in JWK set, but there is no matched key. JWTHelper::UniqueJWTDecodedToken decoded_token; status = JWTHelper::Decode(token, decoded_token); @@ -1145,4 +1139,69 @@ TEST(JwtUtilTest, VerifyJwtFailExpiredToken) { << " Actual error: " << status.ToString(); } +namespace { + +void JWKSHandler(const Webserver::WebRequest& /*req*/, + Webserver::PrerenderedWebResponse* resp) { + resp->output << + Substitute(kJwksRsaFileFormat, kKid1, "RS256", + kRsaPubKeyJwkN, kRsaPubKeyJwkE, kKid2, "RS256", kRsaInvalidPubKeyJwkN, + kRsaPubKeyJwkE); + resp->status_code = HttpStatusCode::Ok; +} + +class JWKSMockServer { + public: + Status Start() { + WebserverOptions opts; + opts.port = 0; + opts.bind_interface = "127.0.0.1"; + webserver_.reset(new Webserver(opts)); + webserver_->RegisterPrerenderedPathHandler("/jwks", "JWKS", JWKSHandler, + /*is_styled*/false, /*is_on_nav_bar*/false); + RETURN_NOT_OK(webserver_->Start()); + vector<Sockaddr> addrs; + RETURN_NOT_OK(webserver_->GetBoundAddresses(&addrs)); + url_ = Substitute("http://$0/jwks", addrs[0].ToString()); + return Status::OK(); + } + + const string& url() const { + return url_; + } + private: + unique_ptr<Webserver> webserver_; + string url_; +}; + +} // anonymous namespace + +TEST(JwtUtilTest, VerifyJWKSUrl) { + JWKSMockServer jwks_server; + ASSERT_OK(jwks_server.Start()); + + JWTHelper jwt_helper; + ASSERT_OK(jwt_helper.Init(jwks_server.url(), false)); + auto encoded_token = + jwt::create() + .set_issuer("auth0") + .set_type("JWS") + .set_algorithm("RS256") + .set_key_id(kKid1) + .set_payload_claim("username", picojson::value("impala")) + .sign(jwt::algorithm::rs256(kRsaPubKeyPem, kRsaPrivKeyPem, "", "")); + ASSERT_EQ( + "eyJhbGciOiJSUzI1NiIsImtpZCI6InB1YmxpYzpjNDI0YjY3Yi1mZTI4LTQ1ZDctYjAxNS1mNzlkYTUwYj" + "ViMjEiLCJ0eXAiOiJKV1MifQ.eyJpc3MiOiJhdXRoMCIsInVzZXJuYW1lIjoiaW1wYWxhIn0.OW5H2SClL" + "lsotsCarTHYEbqlbRh43LFwOyo9WubpNTwE7hTuJDsnFoVrvHiWI02W69TZNat7DYcC86A_ogLMfNXagHj" + "lMFJaRnvG5Ekag8NRuZNJmHVqfX-qr6x7_8mpOdU554kc200pqbpYLhhuK4Qf7oT7y9mOrtNrUKGDCZ0Q2" + "y_mizlbY6SMg4RWqSz0RQwJbRgXIWSgcbZd0GbD_MQQ8x7WRE4nluU-5Fl4N2Wo8T9fNTuxALPiuVeIczO" + "25b5n4fryfKasSgaZfmk0CoOJzqbtmQxqiK9QNSJAiH2kaqMwLNgAdgn8fbd-lB1RAEGeyPH8Px8ipqcKs" + "Pk0bg", + encoded_token); + JWTHelper::UniqueJWTDecodedToken decoded_token; + ASSERT_OK(jwt_helper.Decode(encoded_token, decoded_token)); + ASSERT_OK(jwt_helper.Verify(decoded_token.get())); +} + } // namespace kudu diff --git a/src/kudu/util/jwt-util.cc b/src/kudu/util/jwt-util.cc index 31cc30069..bf512fd1b 100644 --- a/src/kudu/util/jwt-util.cc +++ b/src/kudu/util/jwt-util.cc @@ -19,7 +19,6 @@ #include "kudu/util/jwt-util.h" -#include <errno.h> #include <openssl/bn.h> #include <openssl/crypto.h> #include <openssl/ec.h> @@ -27,13 +26,16 @@ #include <openssl/pem.h> #include <openssl/ssl.h> #include <openssl/x509.h> -#include <stdint.h> -#include <stdio.h> + #include <sys/stat.h> +#include <cerrno> +#include <cstdint> +#include <cstdio> #include <cstring> #include <exception> #include <functional> +#include <iterator> #include <mutex> #include <ostream> #include <stdexcept> @@ -42,7 +44,6 @@ #include <unordered_map> #include <utility> -#include <boost/algorithm/string/case_conv.hpp> #include <gflags/gflags.h> #include <glog/logging.h> #include <jwt-cpp/jwt.h> @@ -68,6 +69,7 @@ #include "kudu/util/thread.h" using std::string; +using std::unique_ptr; using strings::Substitute; DEFINE_int32(jwks_update_frequency_s, 60, @@ -163,7 +165,7 @@ class JWKSetParser { Status status = ParseKey(key); if (!status.ok()) { Status parse_status = Status::InvalidArgument(Substitute("parsing key #$0, ", key_idx)); - return parse_status.CloneAndAppend(status.ToString()); + return parse_status.CloneAndAppend(status.message()); } } return Status::OK(); @@ -178,41 +180,37 @@ class JWKSetParser { member != json_key.MemberEnd(); ++member) { key = string(member->name.GetString()); RETURN_NOT_OK(ReadKeyProperty(key, json_key, &value, /*required*/ false)); - if (kv_map.find(key) == kv_map.end()) { - kv_map.insert(make_pair(key, value)); - } else { + if (!EmplaceIfNotPresent(&kv_map, key, value)) { LOG(WARNING) << "Duplicate property of JWK: " << key; } } - auto it_kty = kv_map.find("kty"); - if (it_kty == kv_map.end()) return Status::InvalidArgument("'kty' property is required"); - auto it_kid = kv_map.find("kid"); - if (it_kid == kv_map.end()) return Status::InvalidArgument("'kid' property is required"); - string key_id = it_kid->second; - if (key_id.empty()) { + 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")); } - Status status; - string key_type; - ToLowerCase(it_kty->second, &key_type); - if (key_type == "oct") { - JWTPublicKey* jwt_pub_key; - status = HSJWTPublicKeyBuilder::CreateJWKPublicKey(kv_map, &jwt_pub_key); - if (status.ok()) jwks_->AddHSKey(key_id, jwt_pub_key); - } else if (key_type == "rsa") { - JWTPublicKey* jwt_pub_key; - status = RSAJWTPublicKeyBuilder::CreateJWKPublicKey(kv_map, &jwt_pub_key); - if (status.ok()) jwks_->AddRSAPublicKey(key_id, jwt_pub_key); - } else if (key_type == "ec") { - JWTPublicKey* jwt_pub_key; - status = ECJWTPublicKeyBuilder::CreateJWKPublicKey(kv_map, &jwt_pub_key); - if (status.ok()) jwks_->AddECPublicKey(key_id, jwt_pub_key); + 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; + return Status::OK(); } // Reads a key property of the given name and assigns the property value to the out @@ -264,25 +262,24 @@ Status JWTPublicKey::Verify( algorithm, algorithm_)); } - Status status; try { // Call jwt-cpp API to verify token's signature. verifier_.verify(decoded_jwt); } catch (const jwt::error::rsa_exception& e) { - status = Status::NotAuthorized(Substitute("RSA error: $0", e.what())); + return Status::NotAuthorized(Substitute("RSA error: $0", e.what())); } catch (const jwt::error::token_verification_exception& e) { - status = Status::NotAuthorized( + return Status::NotAuthorized( Substitute("Verification failed, error: $0", e.what())); } catch (const std::exception& e) { - status = Status::NotAuthorized( + return Status::NotAuthorized( Substitute("Verification failed, error: $0", e.what())); } - return status; + return Status::OK(); } // Create a JWKPublicKey of HS from the JWK. Status HSJWTPublicKeyBuilder::CreateJWKPublicKey( - const JsonKVMap& kv_map, JWTPublicKey** pub_key_out) { + const JsonKVMap& kv_map, unique_ptr<JWTPublicKey>* pub_key_out) { // Octet Sequence keys for HS256, HS384 or HS512. // JWK Sample: // { @@ -305,30 +302,28 @@ Status HSJWTPublicKeyBuilder::CreateJWKPublicKey( return Status::InvalidArgument(Substitute("'k' property must be a non-empty string")); } - Status status; - JWTPublicKey* jwt_pub_key = nullptr; + unique_ptr<JWTPublicKey> jwt_pub_key; try { if (algorithm == "hs256") { - jwt_pub_key = new HS256JWTPublicKey(algorithm, it_k->second); + jwt_pub_key.reset(new HS256JWTPublicKey(algorithm, it_k->second)); } else if (algorithm == "hs384") { - jwt_pub_key = new HS384JWTPublicKey(algorithm, it_k->second); + jwt_pub_key.reset(new HS384JWTPublicKey(algorithm, it_k->second)); } else if (algorithm == "hs512") { - jwt_pub_key = new HS512JWTPublicKey(algorithm, it_k->second); + 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) { - status = Status::NotAuthorized( + return Status::NotAuthorized( Substitute("Failed to initialize verifier, error: $0", e.what())); } - if (!status.ok()) return status; - *pub_key_out = jwt_pub_key; + *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, JWTPublicKey** pub_key_out) { + const JsonKVMap& kv_map, unique_ptr<JWTPublicKey>* pub_key_out) { // JWK Sample: // { // "kty":"RSA", @@ -339,7 +334,8 @@ Status RSAJWTPublicKeyBuilder::CreateJWKPublicKey( // } auto it_alg = kv_map.find("alg"); if (it_alg == kv_map.end()) return Status::InvalidArgument("'alg' property is required"); - string algorithm = boost::algorithm::to_lower_copy(it_alg->second); + string algorithm; + ToLowerCase(it_alg->second, &algorithm); if (algorithm.empty()) { return Status::InvalidArgument(Substitute("'alg' property must be a non-empty string")); } @@ -359,34 +355,31 @@ Status RSAJWTPublicKeyBuilder::CreateJWKPublicKey( Substitute("Invalid public key 'n':'$0', 'e':'$1'", it_n->second, it_e->second)); } - Status status; - JWTPublicKey* jwt_pub_key = nullptr; + unique_ptr<JWTPublicKey> jwt_pub_key; try { if (algorithm == "rs256") { - jwt_pub_key = new RS256JWTPublicKey(algorithm, pub_key); + jwt_pub_key.reset(new RS256JWTPublicKey(algorithm, pub_key)); } else if (algorithm == "rs384") { - jwt_pub_key = new RS384JWTPublicKey(algorithm, pub_key); + jwt_pub_key.reset(new RS384JWTPublicKey(algorithm, pub_key)); } else if (algorithm == "rs512") { - jwt_pub_key = new RS512JWTPublicKey(algorithm, pub_key); + jwt_pub_key.reset(new RS512JWTPublicKey(algorithm, pub_key)); } else if (algorithm == "ps256") { - jwt_pub_key = new PS256JWTPublicKey(algorithm, pub_key); + jwt_pub_key.reset(new PS256JWTPublicKey(algorithm, pub_key)); } else if (algorithm == "ps384") { - jwt_pub_key = new PS384JWTPublicKey(algorithm, pub_key); + jwt_pub_key.reset(new PS384JWTPublicKey(algorithm, pub_key)); } else if (algorithm == "ps512") { - jwt_pub_key = new PS512JWTPublicKey(algorithm, pub_key); + jwt_pub_key.reset(new PS512JWTPublicKey(algorithm, pub_key)); } else { return Status::InvalidArgument(Substitute("Invalid 'alg' property value: '$0'", algorithm)); } } catch (const jwt::error::rsa_exception& e) { - status = Status::NotAuthorized(Substitute("RSA error: $0", e.what())); - RETURN_NOT_OK(status); + return Status::NotAuthorized(Substitute("RSA error: $0", e.what())); } catch (const std::exception& e) { - status = Status::NotAuthorized( + return Status::NotAuthorized( Substitute("Failed to initialize verifier, error: $0", e.what())); - RETURN_NOT_OK(status); } - *pub_key_out = jwt_pub_key; + *pub_key_out = std::move(jwt_pub_key); return Status::OK(); } @@ -398,38 +391,42 @@ bool RSAJWTPublicKeyBuilder::ConvertJwkToPem( string str_e; if (!strings::WebSafeBase64Unescape(base64_n, &str_n)) return false; if (!strings::WebSafeBase64Unescape(base64_e, &str_e)) return false; - BIGNUM* modul = BN_bin2bn(reinterpret_cast<const unsigned char*>(str_n.c_str()), - static_cast<int>(str_n.size()), - nullptr); - BIGNUM* expon = BN_bin2bn(reinterpret_cast<const unsigned char*>(str_e.c_str()), - static_cast<int>(str_e.size()), - nullptr); - - RSA* rsa = RSA_new(); + security::c_unique_ptr<BIGNUM> modul { + BN_bin2bn(reinterpret_cast<const unsigned char*>(str_n.c_str()), + static_cast<int>(str_n.size()), + nullptr), + &BN_free + }; + security::c_unique_ptr<BIGNUM> expon { + BN_bin2bn(reinterpret_cast<const unsigned char*>(str_e.c_str()), + static_cast<int>(str_e.size()), + nullptr), + &BN_free + }; + + security::c_unique_ptr<RSA> rsa { RSA_new(), &RSA_free }; #if OPENSSL_VERSION_NUMBER < 0x10100000L - rsa->n = modul; - rsa->e = expon; + rsa->n = modul.get(); + rsa->e = expon.get(); #else // RSA_set0_key is a new API introduced in OpenSSL version 1.1 - RSA_set0_key(rsa, modul, expon, nullptr); + RSA_set0_key(rsa.get(), modul.release(), expon.release(), nullptr); #endif - unsigned char desc[1024]; - memset(desc, 0, 1024); + unsigned char desc[1024] = { 0 }; auto bio = security::ssl_make_unique(BIO_new(BIO_s_mem())); - PEM_write_bio_RSA_PUBKEY(bio.get(), rsa); - if (BIO_read(bio.get(), desc, 1024) > 0) { + PEM_write_bio_RSA_PUBKEY(bio.get(), rsa.get()); + if (BIO_read(bio.get(), desc, std::size(desc) - 1) > 0) { pub_key = reinterpret_cast<char*>(desc); // Remove last '\n'. if (pub_key.length() > 0 && pub_key[pub_key.length() - 1] == '\n') pub_key.pop_back(); } - RSA_free(rsa); return !pub_key.empty(); } // Create a JWKPublicKey of EC (ES256, ES384 or ES512) from the JWK. Status ECJWTPublicKeyBuilder::CreateJWKPublicKey( - const JsonKVMap& kv_map, JWTPublicKey** pub_key_out) { + const JsonKVMap& kv_map, unique_ptr<JWTPublicKey>* pub_key_out) { // JWK Sample: // { // "kty":"EC", @@ -442,7 +439,8 @@ Status ECJWTPublicKeyBuilder::CreateJWKPublicKey( int eccgrp; auto it_crv = kv_map.find("crv"); if (it_crv != kv_map.end()) { - string curve = boost::algorithm::to_upper_copy(it_crv->second); + string curve; + ToUpperCase(it_crv->second, &curve); if (curve == "P-256") { algorithm = "es256"; eccgrp = NID_X9_62_prime256v1; @@ -460,7 +458,7 @@ Status ECJWTPublicKeyBuilder::CreateJWKPublicKey( if (it_alg == kv_map.end()) { return Status::InvalidArgument("'alg' or 'crv' property is required"); } - algorithm = boost::algorithm::to_lower_copy(it_alg->second); + ToLowerCase(it_alg->second, &algorithm); if (algorithm.empty()) { return Status::InvalidArgument(Substitute("'alg' property must be a non-empty string")); } @@ -493,7 +491,6 @@ Status ECJWTPublicKeyBuilder::CreateJWKPublicKey( Substitute("Invalid public key 'x':'$0', 'y':'$1'", it_x->second, it_y->second)); } - Status status; JWTPublicKey* jwt_pub_key = nullptr; try { if (algorithm == "es256") { @@ -505,13 +502,13 @@ Status ECJWTPublicKeyBuilder::CreateJWKPublicKey( jwt_pub_key = new ES512JWTPublicKey(algorithm, pub_key); } } catch (const jwt::error::ecdsa_exception& e) { - status = Status::NotAuthorized(Substitute("EC error: $0", e.what())); + return Status::NotAuthorized(Substitute("EC error: $0", e.what())); } catch (const std::exception& e) { - status = Status::NotAuthorized( + return Status::NotAuthorized( Substitute("Failed to initialize verifier, error: $0", e.what())); } - if (!status.ok()) return status; - *pub_key_out = jwt_pub_key; + + pub_key_out->reset(jwt_pub_key); return Status::OK(); } @@ -523,23 +520,27 @@ bool ECJWTPublicKeyBuilder::ConvertJwkToPem(int eccgrp, const std::string& base6 string ascii_y; if (!strings::WebSafeBase64Unescape(base64_x, &ascii_x)) return false; if (!strings::WebSafeBase64Unescape(base64_y, &ascii_y)) return false; - BIGNUM* x = BN_bin2bn(reinterpret_cast<const unsigned char*>(ascii_x.c_str()), - static_cast<int>(ascii_x.size()), - nullptr); - BIGNUM* y = BN_bin2bn(reinterpret_cast<const unsigned char*>(ascii_y.c_str()), - static_cast<int>(ascii_y.size()), - nullptr); - - BIO* bio = nullptr; - EC_KEY* ecKey = EC_KEY_new_by_curve_name(eccgrp); - EC_KEY_set_asn1_flag(ecKey, OPENSSL_EC_NAMED_CURVE); - if (EC_KEY_set_public_key_affine_coordinates(ecKey, x, y) == 0) goto cleanup; - - unsigned char desc[1024]; - memset(desc, 0, 1024); - bio = BIO_new(BIO_s_mem()); - if (PEM_write_bio_EC_PUBKEY(bio, ecKey) != 0) { - if (BIO_read(bio, desc, 1024) > 0) { + security::c_unique_ptr<BIGNUM> x { + BN_bin2bn(reinterpret_cast<const unsigned char*>(ascii_x.c_str()), + static_cast<int>(ascii_x.size()), + nullptr), + &BN_free + }; + security::c_unique_ptr<BIGNUM> y { + BN_bin2bn(reinterpret_cast<const unsigned char*>(ascii_y.c_str()), + static_cast<int>(ascii_y.size()), + nullptr), + &BN_free + }; + + security::c_unique_ptr<EC_KEY> ecKey { EC_KEY_new_by_curve_name(eccgrp), &EC_KEY_free }; + EC_KEY_set_asn1_flag(ecKey.get(), OPENSSL_EC_NAMED_CURVE); + if (EC_KEY_set_public_key_affine_coordinates(ecKey.get(), x.get(), y.get()) == 0) return false; + + unsigned char desc[1024] = { 0 }; + auto bio = security::ssl_make_unique(BIO_new(BIO_s_mem())); + if (PEM_write_bio_EC_PUBKEY(bio.get(), ecKey.get()) != 0) { + if (BIO_read(bio.get(), desc, std::size(desc) - 1) > 0) { pub_key = reinterpret_cast<char*>(desc); // Remove last '\n'. if (pub_key.length() > 0 && pub_key[pub_key.length() - 1] == '\n') { @@ -548,11 +549,6 @@ bool ECJWTPublicKeyBuilder::ConvertJwkToPem(int eccgrp, const std::string& base6 } } -cleanup: - if (bio != nullptr) BIO_free(bio); - EC_KEY_free(ecKey); - BN_free(x); - BN_free(y); return !pub_key.empty(); } @@ -607,7 +603,7 @@ Status JWKSSnapshot::LoadKeysFromUrl( const std::string& jwks_url, uint64_t cur_jwks_checksum, bool* is_changed) { kudu::EasyCurl curl; kudu::faststring dst; - Status status; + *is_changed = false; curl.set_timeout( kudu::MonoDelta::FromMilliseconds(static_cast<int64_t>(FLAGS_jwks_pulling_timeout_s) * 1000)); @@ -618,8 +614,9 @@ Status JWKSSnapshot::LoadKeysFromUrl( 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(); - *is_changed = true; + 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 @@ -629,39 +626,46 @@ Status JWKSSnapshot::LoadKeysFromUrl( Document jwks_doc; jwks_doc.Parse(reinterpret_cast<char*>(dst.data())); if (jwks_doc.HasParseError()) { - status = Status::InvalidArgument(GetParseError_En(jwks_doc.GetParseError())); - } else if (!jwks_doc.IsObject()) { - status = Status::InvalidArgument("root element must be a JSON Object"); - } else if (!jwks_doc.HasMember("keys")) { - status = Status::InvalidArgument("keys is required"); - } else { - // Load and initialize public keys. - JWKSetParser jwks_parser(this); - status = jwks_parser.Parse(jwks_doc); + 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)); } - return status; + + *is_changed = true; + return Status::OK(); } -void JWKSSnapshot::AddHSKey(const std::string& key_id, JWTPublicKey* jwk_pub_key) { +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].reset(jwk_pub_key); + 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, JWTPublicKey* jwk_pub_key) { +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].reset(jwk_pub_key); + 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, JWTPublicKey* jwk_pub_key) { +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].reset(jwk_pub_key); + 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; } @@ -689,28 +693,20 @@ JWKSMgr::~JWKSMgr() { } Status JWKSMgr::Init(const std::string& jwks_uri, bool is_local_file) { - Status status; jwks_uri_ = jwks_uri; std::shared_ptr<JWKSSnapshot> new_jwks = std::make_shared<JWKSSnapshot>(); if (is_local_file) { - status = new_jwks->LoadKeysFromFile(jwks_uri); - if (!status.ok()) { - LOG(ERROR) << "Failed to load JWKS: " << status.ToString(); - return status; - } + RETURN_NOT_OK_PREPEND(new_jwks->LoadKeysFromFile(jwks_uri), "Failed to load JWKS"); SetJWKSSnapshot(new_jwks); } else { bool is_changed = false; - status = new_jwks->LoadKeysFromUrl(jwks_uri, current_jwks_checksum_, &is_changed); - if (!status.ok()) { - LOG(ERROR) << "Failed to load JWKS: " << status.ToString(); - return status; - } + RETURN_NOT_OK_PREPEND(new_jwks->LoadKeysFromUrl(jwks_uri, 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("impala-server", "JWKS-mgr", + RETURN_NOT_OK(Thread::Create("JWT", "JWKS-mgr", [this] { return UpdateJWKSThread(); }, &jwks_update_thread_)); } @@ -733,7 +729,6 @@ void JWKSMgr::UpdateJWKSThread() { break; } - new_jwks = std::make_shared<JWKSSnapshot>(); bool is_changed = false; Status status = @@ -742,6 +737,9 @@ void JWKSMgr::UpdateJWKSThread() { 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(); } @@ -792,7 +790,6 @@ JWKSSnapshotPtr JWTHelper::GetJWKS() const { // Decode the given JWT token. Status JWTHelper::Decode(const string& token, UniqueJWTDecodedToken& decoded_token_out) { - Status status; try { // Call jwt-cpp API to decode the JWT token with default jwt::json_traits // (jwt::picojson_traits). @@ -810,18 +807,17 @@ Status JWTHelper::Decode(const string& token, UniqueJWTDecodedToken& decoded_tok VLOG(3) << msg.str(); #endif } catch (const std::invalid_argument& e) { - status = Status::NotAuthorized( + return Status::NotAuthorized( Substitute("Token is not in correct format, error: $0", e.what())); } catch (const std::runtime_error& e) { - status = Status::NotAuthorized( + return Status::NotAuthorized( Substitute("Base64 decoding failed or invalid json, error: $0", e.what())); } - return status; + return Status::OK(); } // Validate the token's signature with public key. Status JWTHelper::Verify(const JWTDecodedToken* decoded_token) const { - Status status; DCHECK(initialized_); const auto& decoded_jwt = decoded_token->decoded_jwt_; @@ -841,8 +837,8 @@ Status JWTHelper::Verify(const JWTDecodedToken* decoded_token) const { } try { - string algorithm = - boost::algorithm::to_lower_copy(decoded_jwt.get_algorithm()); + 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. @@ -863,7 +859,7 @@ Status JWTHelper::Verify(const JWTDecodedToken* decoded_token) const { return Status::NotAuthorized("Invalid JWK ID in the JWT token"); } // Use the public key to verify the token's signature. - status = pub_key->Verify(decoded_jwt, algorithm); + 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 @@ -882,52 +878,51 @@ Status JWTHelper::Verify(const JWTDecodedToken* decoded_token) const { 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) { - status = Status::NotAuthorized( + return Status::NotAuthorized( Substitute("Claim was present but not a string, error: $0", e.what())); } catch (const jwt::error::claim_not_present_exception& e) { - status = Status::NotAuthorized( + return Status::NotAuthorized( Substitute("Claim not present in JWT token, error $0", e.what())); } catch (const std::exception& e) { - status = Status::NotAuthorized( + return Status::NotAuthorized( Substitute("Token verification failed, error: $0", e.what())); } - return status; + return Status::OK(); } Status JWTHelper::GetCustomClaimUsername(const JWTDecodedToken* decoded_token, const string& jwt_custom_claim_username, string& username) { DCHECK(!jwt_custom_claim_username.empty()); - Status status; 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)) { - // Assume the claim data type of 'username' is string. - username.assign( - decoded_jwt.get_payload_claim(jwt_custom_claim_username) - .to_json() - .to_str()); - if (username.empty()) { - status = Status::NotAuthorized( - Substitute("Claim '$0' is empty", jwt_custom_claim_username)); - } - } else { - status = Status::NotAuthorized( + 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) { - status = Status::NotAuthorized( + return Status::NotAuthorized( Substitute("Claim '$0' was not present, error: $1", jwt_custom_claim_username, e.what())); } - return status; + return Status::OK(); } } // namespace kudu