You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@impala.apache.org by jo...@apache.org on 2023/02/23 04:01:36 UTC

[impala] 01/02: IMPALA-11922 Verify JWKS URL server TLS certificate by default.

This is an automated email from the ASF dual-hosted git repository.

joemcdonnell pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/impala.git

commit a77e4aaaf6c517add897c546a4ca75353e8296a5
Author: jasonmfehr <jf...@cloudera.com>
AuthorDate: Mon Feb 13 21:12:48 2023 -0800

    IMPALA-11922 Verify JWKS URL server TLS certificate by default.
    
    **** BREAKING CHANGE ****
    If using JWT authentication to the Impala engine and the
    JWKS is retrieved from a URL, Impala now verifies the
    server's TLS certificate.  Before, Impala did not verify
    the trust chain nor did it verify the CN/SAN.
    
    JWT Auth has an option to specify the location of the
    JSON Web Key Set (JWKS) using a URL. If that URL is
    accessed over HTTPS, the TLS certificate presented by the
    server is not verified.
    
    This means that Impala only requires the server to return
    a TLS certificate, whether or not Impala trusts the signing
    certificate chain.
    
    The implications of this setup is that a fully secure chain
    of trust cannot be established throughout the entire JWT
    authentication lifecycle and thus creates an attack vector
    where a bad actor could trick Impala into trusting an
    actor-controlled JWKS. The bad actor can then generate
    a JWT with any claims they chose and Impala will accept it.
    
    This change introduces:
      1. verification of JWKS server TLS certificate by default
      2. jwks_verify_server_certificate Impala startup flag
      3. jwks_ca_certificate Impala startup flag
    
    1. While previously, the JWKS URL was always called without
       verifying its TLS certificate, the default is to now to
       verify that cert. Thus, any cases where the JWKS was
       retrieved from an untrusted URL will now cause Impala
       to fail to start.
    
    2. The new flag jwks_verify_server_certificate controls
       whether or not Impala verifies the TLS certificate
       presented by the JWKS server. It defaults to "false"
       meaning that the certificate will be verified. Setting
       this value to "false" will restore the previous behavior
       where untrusted TLS certificates are accepted.
    
    3. The new flag jwks_ca_certificate enables specifying
       a PEM certificate bundle that contains certificates
       to trust when calling to the JWKS URL.
    
    Testing was achieved in the front-end Java custom cluster
    tests. An existing test was modified and three new tests
    were created. The following test cases are covered:
      1. Insecurely retrieve a JWKS from a server with an
         untrusted TLS certificate. This test case is expected
         to pass.
      2. Securely retrieve a JWKS from a server with an
         untrusted TLS certificate. This test case is expected
         to fail. The Impala coordinator logs are checked to
         ensure the cause was an untrusted certificate
         presented by the JWKS server.
      3. Retrieve a JWKS from a server where the root CA is
         trusted, but the cert contains the wrong CN. This
         test is expected to fail. The Impala logs are checked
         to ensure the cause was a certificate with an
         incorrect CN.
      4. Securely retrieve a JWKS from a server with a trusted
         TLS certificate. This test case is expected to pass.
    
    Change-Id: I5f1e887fae39b5fb82fa9a40352e4b507b7d8d35
    Reviewed-on: http://gerrit.cloudera.org:8080/19503
    Reviewed-by: Impala Public Jenkins <im...@cloudera.com>
    Tested-by: Impala Public Jenkins <im...@cloudera.com>
---
 be/src/kudu/util/curl_util.cc                      |  22 +-
 be/src/kudu/util/curl_util.h                       |  10 +
 be/src/rpc/authentication.cc                       |  12 +
 be/src/service/impala-server.cc                    |   7 +-
 be/src/util/jwt-util-internal.h                    |  14 +-
 be/src/util/jwt-util-test.cc                       |  46 ++--
 be/src/util/jwt-util.cc                            |  28 ++-
 be/src/util/jwt-util.h                             |   7 +-
 fe/pom.xml                                         |  12 +
 .../apache/impala/customcluster/JwtHttpTest.java   | 193 +++++++++++++--
 .../org/apache/impala/testutil/X509CertChain.java  | 266 +++++++++++++++++++++
 11 files changed, 563 insertions(+), 54 deletions(-)

diff --git a/be/src/kudu/util/curl_util.cc b/be/src/kudu/util/curl_util.cc
index 8e0756e1e..8eb3ae1c1 100644
--- a/be/src/kudu/util/curl_util.cc
+++ b/be/src/kudu/util/curl_util.cc
@@ -86,11 +86,8 @@ EasyCurl::EasyCurl()
   curl_ = curl_easy_init();
   CHECK(curl_) << "Could not init curl";
 
-  // Set the error buffer to enhance error messages with more details, when
-  // available.
+  // Ensure the curl error buffer is large enough.
   static_assert(kErrBufSize >= CURL_ERROR_SIZE, "kErrBufSize is too small");
-  const auto code = curl_easy_setopt(curl_, CURLOPT_ERRORBUFFER, errbuf_);
-  CHECK_EQ(CURLE_OK, code);
 }
 
 EasyCurl::~EasyCurl() {
@@ -115,14 +112,29 @@ Status EasyCurl::DoRequest(const string& url,
                            const vector<string>& headers) {
   CHECK_NOTNULL(dst)->clear();
 
+  // Reset all options to default values to ensure settings do not leak
+  // across calls.
+  curl_easy_reset(curl_);
+
+  // Set the error buffer to enhance error messages with more details, when
+  // available.
+  CURL_RETURN_NOT_OK(curl_easy_setopt(curl_, CURLOPT_ERRORBUFFER, errbuf_));
+
   // Mark the error buffer as cleared.
   errbuf_[0] = 0;
 
-  if (!verify_peer_) {
+  if (verify_peer_) {
+    CURL_RETURN_NOT_OK(curl_easy_setopt(curl_, CURLOPT_SSL_VERIFYHOST, 2));
+    CURL_RETURN_NOT_OK(curl_easy_setopt(curl_, CURLOPT_SSL_VERIFYPEER, 1));
+  } else {
     CURL_RETURN_NOT_OK(curl_easy_setopt(curl_, CURLOPT_SSL_VERIFYHOST, 0));
     CURL_RETURN_NOT_OK(curl_easy_setopt(curl_, CURLOPT_SSL_VERIFYPEER, 0));
   }
 
+  if (!ca_certificates_.empty()) {
+    CURL_RETURN_NOT_OK(curl_easy_setopt(curl_, CURLOPT_CAINFO, ca_certificates_.c_str()));
+  }
+
   switch (auth_type_) {
     case CurlAuthType::SPNEGO:
       CURL_RETURN_NOT_OK(curl_easy_setopt(
diff --git a/be/src/kudu/util/curl_util.h b/be/src/kudu/util/curl_util.h
index 378168814..977e59834 100644
--- a/be/src/kudu/util/curl_util.h
+++ b/be/src/kudu/util/curl_util.h
@@ -71,6 +71,12 @@ class EasyCurl {
     verify_peer_ = verify;
   }
 
+ // Sets a file path for a PEM bundle of certificates to trust when making a request over
+ // HTTPS.  Can be either CA certificates or the actual server certificate.
+  void set_ca_certificates(const std::string& ca_certificates) {
+    ca_certificates_ = ca_certificates;
+  }
+
   void set_return_headers(bool v) {
     return_headers_ = v;
   }
@@ -150,6 +156,10 @@ class EasyCurl {
   // Whether to verify the server certificate.
   bool verify_peer_ = true;
 
+  // File path to a pem encoded bundle of certs to trust when calling to a server
+  // over https
+  std::string ca_certificates_;
+
   // Whether to return the HTTP headers with the response.
   bool return_headers_ = false;
 
diff --git a/be/src/rpc/authentication.cc b/be/src/rpc/authentication.cc
index 5001f7184..b11565243 100644
--- a/be/src/rpc/authentication.cc
+++ b/be/src/rpc/authentication.cc
@@ -166,6 +166,18 @@ DEFINE_string(jwks_file_path, "",
     "File path of the pre-installed JSON Web Key Set (JWKS) for JWT verification");
 // This specifies the URL for JWKS to be downloaded.
 DEFINE_string(jwks_url, "", "URL of the JSON Web Key Set (JWKS) for JWT verification");
+// Enables retrieving the JWKS URL without verifying the presented TLS certificate
+// from the server.
+DEFINE_bool(jwks_verify_server_certificate, true,
+    "Specifies if the TLS certificate of the JWKS server is verified when retrieving "
+    "the JWKS from the specified JWKS URL.  A certificate is considered valid if a "
+    "trust chain can be established for it, and if the certificate has a common name or "
+    "SAN that matches the server's hostname. This should only be set to false for "
+    "development / testing.");
+// Enables defining a custom pem bundle file containing root certificates to trust.
+DEFINE_string(jwks_ca_certificate, "", "File path of a pem bundle of root ca "
+    "certificates that will be trusted when retrieving the JWKS from the "
+    "specified JWKS URL.");
 DEFINE_int32(jwks_update_frequency_s, 60,
     "(Advanced) The time in seconds to wait between downloading JWKS from the specified "
     "URL.");
diff --git a/be/src/service/impala-server.cc b/be/src/service/impala-server.cc
index 95fd16362..c0add43c2 100644
--- a/be/src/service/impala-server.cc
+++ b/be/src/service/impala-server.cc
@@ -379,6 +379,8 @@ DECLARE_bool(jwt_token_auth);
 DECLARE_bool(jwt_validate_signature);
 DECLARE_string(jwks_file_path);
 DECLARE_string(jwks_url);
+DECLARE_bool(jwks_verify_server_certificate);
+DECLARE_string(jwks_ca_certificate);
 
 namespace {
 using namespace impala;
@@ -2951,10 +2953,11 @@ Status ImpalaServer::Start(int32_t beeswax_port, int32_t hs2_port,
     // Load JWKS from file if validation for signature of JWT token is enabled.
     if (FLAGS_jwt_token_auth && FLAGS_jwt_validate_signature) {
       if (!FLAGS_jwks_file_path.empty()) {
-        RETURN_IF_ERROR(JWTHelper::GetInstance()->Init(FLAGS_jwks_file_path, true));
+        RETURN_IF_ERROR(JWTHelper::GetInstance()->Init(FLAGS_jwks_file_path));
       } else if (!FLAGS_jwks_url.empty()) {
         if (TestInfo::is_test()) sleep(1);
-        RETURN_IF_ERROR(JWTHelper::GetInstance()->Init(FLAGS_jwks_url, false));
+        RETURN_IF_ERROR(JWTHelper::GetInstance()->Init(FLAGS_jwks_url,
+            FLAGS_jwks_verify_server_certificate, FLAGS_jwks_ca_certificate, false));
       } else {
         LOG(ERROR) << "JWKS file is not specified when the validation of JWT signature "
                    << " is enabled.";
diff --git a/be/src/util/jwt-util-internal.h b/be/src/util/jwt-util-internal.h
index 67de411c8..bf62a02dc 100644
--- a/be/src/util/jwt-util-internal.h
+++ b/be/src/util/jwt-util-internal.h
@@ -262,7 +262,9 @@ class JWKSSnapshot {
   /// checksum of JWKS object is changed. If no keys were given in the URL, the internal
   /// maps will be empty.
   Status LoadKeysFromUrl(
-      const std::string& jwks_url, uint64_t cur_jwks_hash, bool* is_changed);
+      const std::string& jwks_url, bool jwks_verify_server_certificate,
+      const std::string& jwks_ca_certificate, uint64_t cur_jwks_hash,
+      bool* is_changed);
 
   /// Look up the key ID in the internal key maps and returns the key if the lookup was
   /// successful, otherwise return nullptr.
@@ -343,7 +345,8 @@ class JWKSMgr {
   /// the internal maps will be empty.
   /// If the given jwks_uri is a URL, start a working thread which will periodically
   /// checks the JWKS URL for updates. This provides support for key rotation.
-  Status Init(const std::string& jwks_uri, bool is_local_file);
+  Status Init(const std::string& jwks_uri, bool jwks_verify_server_certificate,
+      const std::string& jwks_ca_certificate, bool is_local_file);
 
   /// Returns a read only snapshot of the current JWKS. This function should be called
   /// after calling Init().
@@ -365,6 +368,13 @@ class JWKSMgr {
   /// JWKS URI.
   std::string jwks_uri_;
 
+  /// JWKS insecure TLS
+  bool jwks_verify_server_certificate_;
+
+  /// File path to PEM certificate bundle of certs to trust when retrieving the JWKS
+  /// from the specified URL.
+  std::string jwks_ca_certificate_;
+
   /// The snapshot of the current JWKS. When the checksum of downloaded JWKS json object
   /// has been changed, the public keys will be reloaded and the content of this pointer
   /// will be atomically swapped.
diff --git a/be/src/util/jwt-util-test.cc b/be/src/util/jwt-util-test.cc
index f3db824a2..25745c1a6 100644
--- a/be/src/util/jwt-util-test.cc
+++ b/be/src/util/jwt-util-test.cc
@@ -405,7 +405,7 @@ TEST(JwtUtilTest, LoadJwksFile) {
       rsa_pub_key_jwk_n, rsa_pub_key_jwk_e, kid_2, "RS256", rsa_invalid_pub_key_jwk_n,
       rsa_pub_key_jwk_e));
   JWTHelper jwt_helper;
-  Status status = jwt_helper.Init(jwks_file.Filename(), true);
+  Status status = jwt_helper.Init(jwks_file.Filename());
   EXPECT_OK(status);
   JWKSSnapshotPtr jwks = jwt_helper.GetJWKS();
   ASSERT_FALSE(jwks->IsEmpty());
@@ -436,7 +436,7 @@ TEST(JwtUtilTest, LoadInvalidJwksFiles) {
       "  ]"
       "}"));
   JWTHelper jwt_helper;
-  Status status = jwt_helper.Init(jwks_file->Filename(), true);
+  Status status = jwt_helper.Init(jwks_file->Filename());
   ASSERT_FALSE(status.ok());
   ASSERT_TRUE(status.msg().msg().find("parsing key #0") != std::string::npos)
       << " Actual error: " << status.msg().msg();
@@ -455,7 +455,7 @@ TEST(JwtUtilTest, LoadInvalidJwksFiles) {
       "      \"n\": \"sttddbg-_yjXzcFpbMJB1fIFam9lQBeXWbTqzJwbuFbspHMsRowa8FaPw\","
       "      \"e\": \"AQAB\""
       "}"));
-  status = jwt_helper.Init(jwks_file->Filename(), true);
+  status = jwt_helper.Init(jwks_file->Filename());
   ASSERT_FALSE(status.ok());
   ASSERT_TRUE(status.GetDetail().find("Missing a comma or ']' after an array element")
       != std::string::npos)
@@ -465,7 +465,7 @@ TEST(JwtUtilTest, LoadInvalidJwksFiles) {
   jwks_file.reset(new TempTestDataFile(
       Substitute(jwks_rsa_file_format, "", "RS256", rsa_pub_key_jwk_n, rsa_pub_key_jwk_e,
           "", "RS256", rsa_invalid_pub_key_jwk_n, rsa_pub_key_jwk_e)));
-  status = jwt_helper.Init(jwks_file->Filename(), true);
+  status = jwt_helper.Init(jwks_file->Filename());
   ASSERT_FALSE(status.ok());
   ASSERT_TRUE(status.msg().msg().find("parsing key #0") != std::string::npos)
       << " Actual error: " << status.msg().msg();
@@ -476,7 +476,7 @@ TEST(JwtUtilTest, LoadInvalidJwksFiles) {
   // JWKS with empty key value.
   jwks_file.reset(new TempTestDataFile(
       Substitute(jwks_rsa_file_format, kid_1, "RS256", "", "", kid_2, "RS256", "", "")));
-  status = jwt_helper.Init(jwks_file->Filename(), true);
+  status = jwt_helper.Init(jwks_file->Filename());
   ASSERT_FALSE(status.ok());
   ASSERT_TRUE(status.msg().msg().find("parsing key #0") != std::string::npos)
       << " Actual error: " << status.msg().msg();
@@ -492,7 +492,7 @@ TEST(JwtUtilTest, VerifyJwtHS256) {
   TempTestDataFile jwks_file(
       Substitute(jwks_hs_file_format, kid_1, "HS256", shared_secret));
   JWTHelper jwt_helper;
-  Status status = jwt_helper.Init(jwks_file.Filename(), true);
+  Status status = jwt_helper.Init(jwks_file.Filename());
   EXPECT_OK(status);
   JWKSSnapshotPtr jwks = jwt_helper.GetJWKS();
   EXPECT_OK(status);
@@ -532,7 +532,7 @@ TEST(JwtUtilTest, VerifyJwtHS384) {
   TempTestDataFile jwks_file(
       Substitute(jwks_hs_file_format, kid_1, "HS384", shared_secret));
   JWTHelper jwt_helper;
-  Status status = jwt_helper.Init(jwks_file.Filename(), true);
+  Status status = jwt_helper.Init(jwks_file.Filename());
   EXPECT_OK(status);
   JWKSSnapshotPtr jwks = jwt_helper.GetJWKS();
   EXPECT_OK(status);
@@ -572,7 +572,7 @@ TEST(JwtUtilTest, VerifyJwtHS512) {
   TempTestDataFile jwks_file(
       Substitute(jwks_hs_file_format, kid_1, "HS512", shared_secret));
   JWTHelper jwt_helper;
-  Status status = jwt_helper.Init(jwks_file.Filename(), true);
+  Status status = jwt_helper.Init(jwks_file.Filename());
   EXPECT_OK(status);
   JWKSSnapshotPtr jwks = jwt_helper.GetJWKS();
   EXPECT_OK(status);
@@ -610,7 +610,7 @@ TEST(JwtUtilTest, VerifyJwtRS256) {
       rsa_pub_key_jwk_n, rsa_pub_key_jwk_e, kid_2, "RS256", rsa_invalid_pub_key_jwk_n,
       rsa_pub_key_jwk_e));
   JWTHelper jwt_helper;
-  Status status = jwt_helper.Init(jwks_file.Filename(), true);
+  Status status = jwt_helper.Init(jwks_file.Filename());
   EXPECT_OK(status);
   JWKSSnapshotPtr jwks = jwt_helper.GetJWKS();
   ASSERT_EQ(2, jwks->GetRSAPublicKeyNum());
@@ -664,7 +664,7 @@ TEST(JwtUtilTest, VerifyJwtRS384) {
       rsa_pub_key_jwk_n, rsa_pub_key_jwk_e, kid_2, "RS384", rsa_invalid_pub_key_jwk_n,
       rsa_pub_key_jwk_e));
   JWTHelper jwt_helper;
-  Status status = jwt_helper.Init(jwks_file.Filename(), true);
+  Status status = jwt_helper.Init(jwks_file.Filename());
   EXPECT_OK(status);
   JWKSSnapshotPtr jwks = jwt_helper.GetJWKS();
   ASSERT_EQ(2, jwks->GetRSAPublicKeyNum());
@@ -702,7 +702,7 @@ TEST(JwtUtilTest, VerifyJwtRS512) {
       rsa512_pub_key_jwk_n, rsa512_pub_key_jwk_e, kid_2, "RS512",
       rsa512_invalid_pub_key_jwk_n, rsa512_pub_key_jwk_e));
   JWTHelper jwt_helper;
-  Status status = jwt_helper.Init(jwks_file.Filename(), true);
+  Status status = jwt_helper.Init(jwks_file.Filename());
   EXPECT_OK(status);
   JWKSSnapshotPtr jwks = jwt_helper.GetJWKS();
   ASSERT_EQ(2, jwks->GetRSAPublicKeyNum());
@@ -740,7 +740,7 @@ TEST(JwtUtilTest, VerifyJwtPS256) {
       rsa1024_pub_key_jwk_n, rsa1024_pub_key_jwk_e, kid_2, "PS256",
       rsa_invalid_pub_key_jwk_n, rsa_pub_key_jwk_e));
   JWTHelper jwt_helper;
-  Status status = jwt_helper.Init(jwks_file.Filename(), true);
+  Status status = jwt_helper.Init(jwks_file.Filename());
   EXPECT_OK(status);
   JWKSSnapshotPtr jwks = jwt_helper.GetJWKS();
   ASSERT_EQ(2, jwks->GetRSAPublicKeyNum());
@@ -778,7 +778,7 @@ TEST(JwtUtilTest, VerifyJwtPS384) {
       rsa2048_pub_key_jwk_n, rsa2048_pub_key_jwk_e, kid_2, "PS384",
       rsa_invalid_pub_key_jwk_n, rsa_pub_key_jwk_e));
   JWTHelper jwt_helper;
-  Status status = jwt_helper.Init(jwks_file.Filename(), true);
+  Status status = jwt_helper.Init(jwks_file.Filename());
   EXPECT_OK(status);
   JWKSSnapshotPtr jwks = jwt_helper.GetJWKS();
   ASSERT_EQ(2, jwks->GetRSAPublicKeyNum());
@@ -816,7 +816,7 @@ TEST(JwtUtilTest, VerifyJwtPS512) {
       rsa4096_pub_key_jwk_n, rsa4096_pub_key_jwk_e, kid_2, "PS512",
       rsa_invalid_pub_key_jwk_n, rsa_pub_key_jwk_e));
   JWTHelper jwt_helper;
-  Status status = jwt_helper.Init(jwks_file.Filename(), true);
+  Status status = jwt_helper.Init(jwks_file.Filename());
   EXPECT_OK(status);
   JWKSSnapshotPtr jwks = jwt_helper.GetJWKS();
   ASSERT_EQ(2, jwks->GetRSAPublicKeyNum());
@@ -853,7 +853,7 @@ TEST(JwtUtilTest, VerifyJwtES256) {
   TempTestDataFile jwks_file(Substitute(jwks_ec_file_format, kid_1, "P-256",
       ecdsa256_pub_key_jwk_x, ecdsa256_pub_key_jwk_y));
   JWTHelper jwt_helper;
-  Status status = jwt_helper.Init(jwks_file.Filename(), true);
+  Status status = jwt_helper.Init(jwks_file.Filename());
   EXPECT_OK(status);
   JWKSSnapshotPtr jwks = jwt_helper.GetJWKS();
   ASSERT_EQ(1, jwks->GetECPublicKeyNum());
@@ -898,7 +898,7 @@ TEST(JwtUtilTest, VerifyJwtES384) {
   TempTestDataFile jwks_file(Substitute(jwks_ec_file_format, kid_1, "P-384",
       ecdsa384_pub_key_jwk_x, ecdsa384_pub_key_jwk_y));
   JWTHelper jwt_helper;
-  Status status = jwt_helper.Init(jwks_file.Filename(), true);
+  Status status = jwt_helper.Init(jwks_file.Filename());
   EXPECT_OK(status);
   JWKSSnapshotPtr jwks = jwt_helper.GetJWKS();
   ASSERT_EQ(1, jwks->GetECPublicKeyNum());
@@ -935,7 +935,7 @@ TEST(JwtUtilTest, VerifyJwtES512) {
   TempTestDataFile jwks_file(Substitute(jwks_ec_file_format, kid_1, "P-521",
       ecdsa521_pub_key_jwk_x, ecdsa521_pub_key_jwk_y));
   JWTHelper jwt_helper;
-  Status status = jwt_helper.Init(jwks_file.Filename(), true);
+  Status status = jwt_helper.Init(jwks_file.Filename());
   EXPECT_OK(status);
   JWKSSnapshotPtr jwks = jwt_helper.GetJWKS();
   ASSERT_EQ(1, jwks->GetECPublicKeyNum());
@@ -993,7 +993,7 @@ TEST(JwtUtilTest, VerifyJwtFailMismatchingAlgorithms) {
       rsa_pub_key_jwk_n, rsa_pub_key_jwk_e, kid_2, "RS256", rsa_invalid_pub_key_jwk_n,
       rsa_pub_key_jwk_e));
   JWTHelper jwt_helper;
-  Status status = jwt_helper.Init(jwks_file.Filename(), true);
+  Status status = jwt_helper.Init(jwks_file.Filename());
   EXPECT_OK(status);
 
   // Create a JWT token, but set mismatching algorithm.
@@ -1022,7 +1022,7 @@ TEST(JwtUtilTest, VerifyJwtFailKeyNotFound) {
       rsa_pub_key_jwk_n, rsa_pub_key_jwk_e, kid_2, "RS256", rsa_invalid_pub_key_jwk_n,
       rsa_pub_key_jwk_e));
   JWTHelper jwt_helper;
-  Status status = jwt_helper.Init(jwks_file.Filename(), true);
+  Status status = jwt_helper.Init(jwks_file.Filename());
   EXPECT_OK(status);
 
   // Create a JWT token with a key ID which can not be found in JWKS.
@@ -1050,7 +1050,7 @@ TEST(JwtUtilTest, VerifyJwtTokenWithoutKeyId) {
       rsa_pub_key_jwk_n, rsa_pub_key_jwk_e, kid_2, "RS256", rsa_invalid_pub_key_jwk_n,
       rsa_pub_key_jwk_e));
   JWTHelper jwt_helper;
-  Status status = jwt_helper.Init(jwks_file.Filename(), true);
+  Status status = jwt_helper.Init(jwks_file.Filename());
   EXPECT_OK(status);
 
   // Create a JWT token without key ID.
@@ -1071,7 +1071,7 @@ TEST(JwtUtilTest, VerifyJwtFailTokenWithoutKeyId) {
       rsa_pub_key_jwk_n, rsa_pub_key_jwk_e, kid_2, "RS256", rsa_invalid_pub_key_jwk_n,
       rsa_pub_key_jwk_e));
   JWTHelper jwt_helper;
-  Status status = jwt_helper.Init(jwks_file.Filename(), true);
+  Status status = jwt_helper.Init(jwks_file.Filename());
   EXPECT_OK(status);
 
   // Create a JWT token without key ID.
@@ -1091,7 +1091,7 @@ TEST(JwtUtilTest, VerifyJwtFailTokenWithoutSignature) {
       rsa_pub_key_jwk_n, rsa_pub_key_jwk_e, kid_2, "RS256", rsa_invalid_pub_key_jwk_n,
       rsa_pub_key_jwk_e));
   JWTHelper jwt_helper;
-  Status status = jwt_helper.Init(jwks_file.Filename(), true);
+  Status status = jwt_helper.Init(jwks_file.Filename());
   EXPECT_OK(status);
 
   // Create a JWT token without signature.
@@ -1113,7 +1113,7 @@ TEST(JwtUtilTest, VerifyJwtFailExpiredToken) {
       rsa_pub_key_jwk_n, rsa_pub_key_jwk_e, kid_2, "RS256", rsa_invalid_pub_key_jwk_n,
       rsa_pub_key_jwk_e));
   JWTHelper jwt_helper;
-  Status status = jwt_helper.Init(jwks_file.Filename(), true);
+  Status status = jwt_helper.Init(jwks_file.Filename());
   EXPECT_OK(status);
 
   // Create a JWT token and sign it with RS256.
diff --git a/be/src/util/jwt-util.cc b/be/src/util/jwt-util.cc
index 7dc2758c9..63e031c4b 100644
--- a/be/src/util/jwt-util.cc
+++ b/be/src/util/jwt-util.cc
@@ -547,14 +547,17 @@ Status JWKSSnapshot::LoadKeysFromFile(const string& jwks_file_path) {
 
 // Download JWKS from the given URL with Kudu's EasyCurl wrapper.
 Status JWKSSnapshot::LoadKeysFromUrl(
-    const std::string& jwks_url, uint64_t cur_jwks_checksum, bool* is_changed) {
+    const std::string& jwks_url, bool jwks_verify_server_certificate,
+    const std::string& jwks_ca_certificate, uint64_t cur_jwks_checksum,
+    bool* is_changed) {
   kudu::EasyCurl curl;
   kudu::faststring dst;
   Status status;
 
   curl.set_timeout(
       kudu::MonoDelta::FromMilliseconds(FLAGS_jwks_pulling_timeout_s * 1000));
-  curl.set_verify_peer(false);
+  curl.set_verify_peer(jwks_verify_server_certificate);
+  curl.set_ca_certificates(jwks_ca_certificate);
   // TODO support CurlAuthType by calling kudu::EasyCurl::set_auth().
   KUDU_RETURN_IF_ERROR(curl.FetchURL(jwks_url, &dst),
       Substitute("Error downloading JWKS from '$0'", jwks_url));
@@ -647,9 +650,12 @@ JWKSMgr::~JWKSMgr() {
   if (jwks_update_thread_ != nullptr) jwks_update_thread_->Join();
 }
 
-Status JWKSMgr::Init(const std::string& jwks_uri, bool is_local_file) {
+Status JWKSMgr::Init(const std::string& jwks_uri, bool jwks_verify_server_certificate,
+    const std::string& jwks_ca_certificate, bool is_local_file) {
   Status status;
   jwks_uri_ = jwks_uri;
+  jwks_verify_server_certificate_ = jwks_verify_server_certificate;
+  jwks_ca_certificate_ = jwks_ca_certificate;
   std::shared_ptr<JWKSSnapshot> new_jwks = std::make_shared<JWKSSnapshot>();
   if (is_local_file) {
     status = new_jwks->LoadKeysFromFile(jwks_uri);
@@ -671,7 +677,8 @@ Status JWKSMgr::Init(const std::string& jwks_uri, bool is_local_file) {
     }
 
     bool is_changed = false;
-    status = new_jwks->LoadKeysFromUrl(jwks_uri, current_jwks_checksum_, &is_changed);
+    status = new_jwks->LoadKeysFromUrl(jwks_uri, jwks_verify_server_certificate,
+        jwks_ca_certificate, current_jwks_checksum_, &is_changed);
     if (!status.ok()) {
       LOG(ERROR) << "Failed to load JWKS: " << status;
       return status;
@@ -700,7 +707,8 @@ void JWKSMgr::UpdateJWKSThread() {
     new_jwks = std::make_shared<JWKSSnapshot>();
     bool is_changed = false;
     Status status =
-        new_jwks->LoadKeysFromUrl(jwks_uri_, current_jwks_checksum_, &is_changed);
+        new_jwks->LoadKeysFromUrl(jwks_uri_, jwks_verify_server_certificate_,
+            jwks_ca_certificate_, current_jwks_checksum_, &is_changed);
     if (!status.ok()) {
       LOG(WARNING) << "Failed to update JWKS: " << status;
     } else if (is_changed) {
@@ -742,9 +750,15 @@ void JWTHelper::TokenDeleter::operator()(JWTHelper::JWTDecodedToken* token) cons
   if (token != nullptr) delete token;
 };
 
-Status JWTHelper::Init(const std::string& jwks_uri, bool is_local_file) {
+Status JWTHelper::Init(const std::string& jwks_file_path) {
+  return Init(jwks_file_path, false, "", true);
+}
+
+Status JWTHelper::Init(const std::string& jwks_uri, bool jwks_verify_server_certificate,
+    const std::string& jwks_ca_certificate, bool is_local_file) {
   jwks_mgr_.reset(new JWKSMgr());
-  RETURN_IF_ERROR(jwks_mgr_->Init(jwks_uri, is_local_file));
+  RETURN_IF_ERROR(jwks_mgr_->Init(jwks_uri, jwks_verify_server_certificate,
+      jwks_ca_certificate, is_local_file));
   if (!initialized_) initialized_ = true;
   return Status::OK();
 }
diff --git a/be/src/util/jwt-util.h b/be/src/util/jwt-util.h
index 5d1ba6158..ce777b0ba 100644
--- a/be/src/util/jwt-util.h
+++ b/be/src/util/jwt-util.h
@@ -54,9 +54,14 @@ class JWTHelper {
   /// Return the single instance.
   static JWTHelper* GetInstance() { return jwt_helper_; }
 
+  /// Load JWKS from a given local JSON file. Returns an error if problems were
+  /// encountered.
+  Status Init(const std::string& jwks_file_path);
+
   /// Load JWKS from a given local JSON file or URL. Returns an error if problems were
   /// encountered.
-  Status Init(const std::string& jwks_uri, bool is_local_file);
+  Status Init(const std::string& jwks_uri, bool jwks_verify_server_certificate,
+      const std::string& jwks_ca_certificate, bool is_local_file);
 
   /// Decode the given JWT token. The decoding result is stored in decoded_token_.
   /// Return Status::OK if the decoding is successful.
diff --git a/fe/pom.xml b/fe/pom.xml
index 4ae1c3fda..00c2eae0d 100644
--- a/fe/pom.xml
+++ b/fe/pom.xml
@@ -583,6 +583,18 @@ under the License.
         </exclusion>
       </exclusions>
     </dependency>
+    <dependency>
+      <groupId>org.bouncycastle</groupId>
+      <artifactId>bcprov-jdk18on</artifactId>
+      <version>1.72</version>
+      <scope>test</scope>
+    </dependency>
+    <dependency>
+      <groupId>org.bouncycastle</groupId>
+      <artifactId>bcpkix-jdk18on</artifactId>
+      <version>1.72</version>
+      <scope>test</scope>
+    </dependency>
   </dependencies>
 
   <reporting>
diff --git a/fe/src/test/java/org/apache/impala/customcluster/JwtHttpTest.java b/fe/src/test/java/org/apache/impala/customcluster/JwtHttpTest.java
index 86f3c81d6..4a72c3e39 100644
--- a/fe/src/test/java/org/apache/impala/customcluster/JwtHttpTest.java
+++ b/fe/src/test/java/org/apache/impala/customcluster/JwtHttpTest.java
@@ -18,9 +18,11 @@
 package org.apache.impala.customcluster;
 
 import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertThat;
 import static org.junit.Assert.fail;
 
 import java.io.File;
+import java.io.FileWriter;
 import java.io.IOException;
 import java.nio.file.Files;
 import java.nio.file.Path;
@@ -32,16 +34,24 @@ import java.util.Map;
 
 import org.apache.hive.service.rpc.thrift.*;
 import org.apache.impala.testutil.WebClient;
+import org.apache.impala.testutil.X509CertChain;
 import org.apache.thrift.transport.THttpClient;
 import org.apache.thrift.protocol.TBinaryProtocol;
 import org.junit.After;
 import org.junit.Test;
 
+import static org.hamcrest.core.IsCollectionContaining.hasItem;
+import static org.hamcrest.core.StringContains.containsString;
+
 /**
  * Tests that hiveserver2 operations over the http interface work as expected when
  * JWT authentication is being used.
  */
 public class JwtHttpTest {
+  private static final String CA_CERT = "cacert.pem";
+  private static final String SERVER_CERT = "server-cert.pem";
+  private static final String SERVER_KEY = "server-key.pem";
+
   WebClient client_ = new WebClient();
 
   /* Since we don't have Java version of JWT library, we use pre-calculated JWT token.
@@ -62,18 +72,28 @@ public class JwtHttpTest {
    */
   boolean createJWKSForWebServer_ = false;
 
-  public void setUp(String extraArgs) throws Exception {
+  private void setUp(String extraArgs) throws Exception {
     int ret = CustomClusterRunner.StartImpalaCluster(extraArgs);
     assertEquals(ret, 0);
   }
 
-  public void setUp(String impaladArgs, String catalogdArgs, String statestoredArgs)
+  /**
+   * Helper method to start a JWT auth enabled Impala cluster.
+   *
+   * @param impaladArgs startup flags to send to the impala coordinator/executors
+   * @param catalogdArgs startup flags to send to the impala catalog
+   * @param statestoredArgs startup flags to send to the statestore
+   * @param expectedRetCode expected exit code for the start impala cluster command,
+   *                        if the cluster is expected to start successfully, set to 0
+   */
+  private void setUp(String impaladArgs, String catalogdArgs,
+      String statestoredArgs, int expectedRetCode)
       throws Exception {
     if (createJWKSForWebServer_) createTempJWKSInWebServerRootDir("jwks_rs256.json");
 
     int ret = CustomClusterRunner.StartImpalaCluster(
         impaladArgs, catalogdArgs, statestoredArgs);
-    assertEquals(ret, 0);
+    assertEquals(expectedRetCode, ret);
   }
 
   @After
@@ -117,7 +137,7 @@ public class JwtHttpTest {
 
   /**
    * Executes 'query' and fetches the results. Expects there to be exactly one string
-   * returned, which be be equal to 'expectedResult'.
+   * returned, which will be equal to 'expectedResult'.
    */
   static TOperationHandle execAndFetch(TCLIService.Iface client,
       TSessionHandle sessionHandle, String query, String expectedResult)
@@ -275,7 +295,7 @@ public class JwtHttpTest {
             + "--jwt_validate_signature=true --jwks_url=%s "
             + "--jwks_update_frequency_s=1 --jwt_allow_without_tls=true",
         jwksHttpUrl);
-    setUp(impaladJwtArgs, "", statestoreWebserverArgs);
+    setUp(impaladJwtArgs, "", statestoreWebserverArgs, 0);
 
     THttpClient transport = new THttpClient("http://localhost:28000");
     Map<String, String> headers = new HashMap<String, String>();
@@ -322,20 +342,22 @@ public class JwtHttpTest {
    * Web server when downloading JWKS.
    */
   @Test
-  public void testJwtAuthWithJwksHttpsUrl() throws Exception {
+  public void testJwtAuthWithInsecureJwksHttpsUrl() throws Exception {
     createJWKSForWebServer_ = true;
-    String certDir = new File(System.getenv("IMPALA_HOME"), "be/src/testutil").getPath();
+    String certDir = setupServerAndRootCerts("testJwtAuthWithInsecureJwksHttpsUrl",
+        "testJwtAuthWithInsecureJwksHttpsUrl Root", "localhostlocalhost");
     String statestoreWebserverArgs =
-        String.format("--webserver_certificate_file=%s/server-cert.pem "
-                + "--webserver_private_key_file=%s/server-key.pem "
-                + "--webserver_interface=localhost --webserver_port=25010 "
-                + "--hostname=localhost ",
-            certDir, certDir);
+        String.format("--webserver_certificate_file=%s "
+            + "--webserver_private_key_file=%s "
+            + "--webserver_interface=localhost --webserver_port=25010 "
+            + "--hostname=localhost ",
+            Paths.get(certDir, SERVER_CERT), Paths.get(certDir, SERVER_KEY));
     String jwksHttpUrl = "https://localhost:25010/www/temp_jwks.json";
     String impaladJwtArgs = String.format("--jwt_token_auth=true "
-            + "--jwt_validate_signature=true --jwks_url=%s --jwt_allow_without_tls=true ",
+        + "--jwt_validate_signature=true --jwks_url=%s "
+        + "--jwt_allow_without_tls=true --jwks_verify_server_certificate=false ",
         jwksHttpUrl);
-    setUp(impaladJwtArgs, "", statestoreWebserverArgs);
+    setUp(impaladJwtArgs, "", statestoreWebserverArgs, 0);
 
     THttpClient transport = new THttpClient("http://localhost:28000");
     Map<String, String> headers = new HashMap<String, String>();
@@ -359,4 +381,147 @@ public class JwtHttpTest {
     // Two more successful authentications - for the Exec() and the Fetch().
     verifyJwtAuthMetrics(3, 0);
   }
+
+  /**
+   * Tests that the Impala coordinator fails to start because the TLS certificate
+   * returned by the JWKS server is not trusted.
+   *
+   * In this test, the TLS certificate has the correct CN but its issuing CA certificate
+   * is not trusted.
+   */
+  @Test
+  public void testJwtAuthWithUntrustedJwksHttpsUrl() throws Exception {
+    createJWKSForWebServer_ = true;
+    String certDir = setupServerAndRootCerts("testJwtAuthWithUntrustedJwksHttpsUrl",
+        "testJwtAuthWithUntrustedJwksHttpsUrl Root", "localhost");
+    Path logDir = Files.createTempDirectory("testJwtAuthWithUntrustedJwksHttpsUrl");
+    String statestoreWebserverArgs =
+        String.format("--webserver_certificate_file=%s "
+            + "--webserver_private_key_file=%s "
+            + "--webserver_interface=localhost --webserver_port=25010 "
+            + "--hostname=localhost ",
+            Paths.get(certDir, SERVER_CERT), Paths.get(certDir, SERVER_KEY));
+    String jwksHttpUrl = "https://localhost:25010/www/temp_jwks.json";
+    String impaladJwtArgs = String.format("--jwt_token_auth=true "
+        + "--jwt_validate_signature=true --jwks_url=%s "
+        + "--jwt_allow_without_tls=true --log_dir=%s ",
+        jwksHttpUrl, logDir.toAbsolutePath());
+    String expectedErrString = String.format("Impalad services did not start correctly, "
+        + "exiting.  Error: Error downloading JWKS from '%s': Network error: curl "
+        + "error: SSL peer certificate or SSH remote key was not OK: SSL certificate "
+        + "problem: unable to get local issuer certificate", jwksHttpUrl);
+
+    // cluster start will fail because the TLS cert returned by the
+    // JWKS server is not trusted
+    setUp(impaladJwtArgs, "", statestoreWebserverArgs, 1);
+
+    // check in the impalad logs that the server startup failed for the expected reason
+    List<String> logLines = Files.readAllLines(logDir.resolve("impalad.ERROR"));
+
+    assertThat(String.format("Impalad startup failed but not for the expected reason. "
+        + "See logs in the '%s' folder for details.", logDir), logLines,
+        hasItem(containsString(expectedErrString)));
+  }
+
+  /**
+   * Tests that the Impala coordinator fails to start because the TLS certificate
+   * returned by the JWKS server is not valid.
+   *
+   * In this test, the TLS certificate has an incorrect CN which means the certificate is
+   * not valid even though its issuing CA certificate is trusted.
+   */
+  @Test
+  public void testJwtAuthWithTrustedJwksHttpsUrlInvalidCN() throws Exception {
+    createJWKSForWebServer_ = true;
+    String certCN = "notvalid";
+    String certDir = setupServerAndRootCerts(
+        "testJwtAuthWithTrustedJwksHttpsUrlInvalidCN",
+        "testJwtAuthWithTrustedJwksHttpsUrlInvalidCN Root", certCN);
+    Path logDir = Files.createTempDirectory(
+        "testJwtAuthWithTrustedJwksHttpsUrlInvalidCN");
+    String statestoreWebserverArgs =
+        String.format("--webserver_certificate_file=%s "
+            + "--webserver_private_key_file=%s "
+            + "--webserver_interface=localhost --webserver_port=25010 "
+            + "--hostname=localhost ",
+            Paths.get(certDir, SERVER_CERT), Paths.get(certDir, SERVER_KEY));
+    String jwksHttpUrl = "https://localhost:25010/www/temp_jwks.json";
+    String impaladJwtArgs = String.format("--jwt_token_auth=true "
+        + "--jwt_validate_signature=true --jwks_url=%s "
+        + "--jwt_allow_without_tls=true --log_dir=%s --jwks_ca_certificate=%s ",
+        jwksHttpUrl, logDir.toAbsolutePath(), Paths.get(certDir, CA_CERT));
+    String expectedErrString = String.format("Impalad services did not start correctly, "
+        + "exiting.  Error: Error downloading JWKS from '%s': Network error: curl "
+        + "error: SSL peer certificate or SSH remote key was not OK: SSL: "
+        + "certificate subject name '%s' does not match target host name '%s'",
+        jwksHttpUrl, certCN, "localhost");
+
+    // cluster start will fail because the TLS cert returned by the
+    // JWKS server is not trusted
+    setUp(impaladJwtArgs, "", statestoreWebserverArgs, 1);
+
+    // check in the impalad logs that the server startup failed for the expected reason
+    List<String> logLines = Files.readAllLines(logDir.resolve("impalad.ERROR"));
+    assertThat(String.format("Impalad startup failed but not for the expected reason. "
+        + "See logs in the '%s' folder for details.", logDir), logLines,
+        hasItem(containsString(expectedErrString)));
+  }
+
+  /**
+   * Tests that the Impala coordinator successfully starts since the TLS certificate
+   * returned by the JWKS server is trusted.
+   */
+  @Test
+  public void testJwtAuthWithTrustedJwksHttpsUrl() throws Exception {
+    createJWKSForWebServer_ = true;
+    String certDir = setupServerAndRootCerts("testJwtAuthWithTrustedJwksHttpsUrl",
+        "testJwtAuthWithTrustedJwksHttpsUrl Root", "localhost");
+    String statestoreWebserverArgs =
+        String.format("--webserver_certificate_file=%s "
+            + "--webserver_private_key_file=%s "
+            + "--webserver_interface=localhost --webserver_port=25010 "
+            + "--hostname=localhost ",
+            Paths.get(certDir, SERVER_CERT), Paths.get(certDir, SERVER_KEY));
+    String jwksHttpUrl = "https://localhost:25010/www/temp_jwks.json";
+    String impaladJwtArgs = String.format("--jwt_token_auth=true "
+        + "--jwt_validate_signature=true --jwks_url=%s "
+        + "--jwt_allow_without_tls=true --jwks_ca_certificate=%s ",
+        jwksHttpUrl, Paths.get(certDir, CA_CERT));
+
+    // cluster start will succeed because the TLS cert returned by the
+    // JWKS server is trusted.
+    setUp(impaladJwtArgs, "", statestoreWebserverArgs, 0);
+  }
+
+  /**
+   * Generates new CA root certificate and server certificate/private key.  All three are
+   * written to a new, unique temporary folder.
+   *
+   * @param testName used as a prefix for the temp folder name
+   * @param rootCaCertCN CN of the generated self-signed root cert
+   * @param rootLeafCertCN CN of the leaf cert that is signed by the root cert
+   *
+   * @return path to the temporary folder
+   */
+  private String setupServerAndRootCerts(String testName, String rootCaCertCN,
+      String rootLeafCertCN) throws Exception {
+    Path certDir = Files.createTempDirectory(testName);
+    Path rootCACert = certDir.resolve(Paths.get(CA_CERT));
+    Path serverCert = certDir.resolve(Paths.get(SERVER_CERT));
+    Path serverKey = certDir.resolve(Paths.get(SERVER_KEY));
+    FileWriter rootCACertWriter = new FileWriter(rootCACert.toFile());
+    FileWriter serverCertWriter = new FileWriter(serverCert.toFile());
+    FileWriter serverKeyWriter = new FileWriter(serverKey.toFile());
+
+    X509CertChain certChain = new X509CertChain(rootCaCertCN, rootLeafCertCN);
+
+    certChain.writeLeafCertAsPem(serverCertWriter);
+    certChain.writeLeafPrivateKeyAsPem(serverKeyWriter);
+    certChain.writeRootCertAsPem(rootCACertWriter);
+    rootCACertWriter.close();
+    serverCertWriter.close();
+    serverKeyWriter.close();
+
+    return certDir.toString();
+  }
 }
diff --git a/fe/src/test/java/org/apache/impala/testutil/X509CertChain.java b/fe/src/test/java/org/apache/impala/testutil/X509CertChain.java
new file mode 100644
index 000000000..b80aade08
--- /dev/null
+++ b/fe/src/test/java/org/apache/impala/testutil/X509CertChain.java
@@ -0,0 +1,266 @@
+// 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.
+
+package org.apache.impala.testutil;
+
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.io.StringWriter;
+import java.io.Writer;
+import java.security.InvalidKeyException;
+import java.security.KeyPair;
+import java.security.KeyPairGenerator;
+import java.security.NoSuchAlgorithmException;
+import java.security.NoSuchProviderException;
+import java.security.PrivateKey;
+import java.security.Security;
+import java.security.Signature;
+import java.security.SignatureException;
+import java.security.cert.CertificateEncodingException;
+import java.security.cert.CertificateException;
+import java.security.cert.CertificateFactory;
+import java.security.cert.X509Certificate;
+import java.util.Date;
+
+import org.bouncycastle.asn1.ASN1EncodableVector;
+import org.bouncycastle.asn1.ASN1Encoding;
+import org.bouncycastle.asn1.ASN1Integer;
+import org.bouncycastle.asn1.DERBitString;
+import org.bouncycastle.asn1.DERNull;
+import org.bouncycastle.asn1.DERSequence;
+import org.bouncycastle.asn1.pkcs.PKCSObjectIdentifiers;
+import org.bouncycastle.asn1.x500.X500Name;
+import org.bouncycastle.asn1.x509.AlgorithmIdentifier;
+import org.bouncycastle.asn1.x509.BasicConstraints;
+import org.bouncycastle.asn1.x509.ExtendedKeyUsage;
+import org.bouncycastle.asn1.x509.Extension;
+import org.bouncycastle.asn1.x509.ExtensionsGenerator;
+import org.bouncycastle.asn1.x509.KeyPurposeId;
+import org.bouncycastle.asn1.x509.KeyUsage;
+import org.bouncycastle.asn1.x509.SubjectPublicKeyInfo;
+import org.bouncycastle.asn1.x509.TBSCertificate;
+import org.bouncycastle.asn1.x509.Time;
+import org.bouncycastle.asn1.x509.V3TBSCertificateGenerator;
+import org.bouncycastle.cert.X509CertificateHolder;
+import org.bouncycastle.jce.provider.BouncyCastleProvider;
+import org.bouncycastle.openssl.MiscPEMGenerator;
+import org.bouncycastle.util.io.pem.PemObject;
+import org.bouncycastle.util.io.pem.PemWriter;
+
+/**
+ * Stateful class that generates X509 CA and leaf certificates.
+ */
+public class X509CertChain {
+
+  private static final String SHA256_WITH_RSA = "SHA256withRSA";
+  private static final AlgorithmIdentifier SIGNATURE_SHA256_RSA = new
+      AlgorithmIdentifier(PKCSObjectIdentifiers.sha256WithRSAEncryption,
+      DERNull.INSTANCE);
+
+  private static final KeyUsage KEY_USAGE_CERT_SIGN = new KeyUsage(KeyUsage.keyCertSign |
+      KeyUsage.cRLSign);
+  private static final KeyUsage KEY_USAGE_SERVER_AUTH = new KeyUsage(
+      KeyUsage.digitalSignature | KeyUsage.keyEncipherment);
+  private static final BasicConstraints CONSTRAINT_CA = new BasicConstraints(true);
+
+  private final KeyPair rootCaKp_;
+  private final KeyPair leafKp_;
+  private final X509Certificate rootCert_;
+  private final X509Certificate leafCert_;
+
+  public X509CertChain(String rootCaCertCN, String rootLeafCertCN)
+      throws NoSuchAlgorithmException, NoSuchProviderException,
+      InvalidKeyException, SignatureException, IOException, CertificateException  {
+    Security.addProvider(new BouncyCastleProvider());
+
+    KeyPairGenerator kpg = KeyPairGenerator.getInstance("RSA", "BC");
+    kpg.initialize(2048);
+    this.rootCaKp_ = kpg.generateKeyPair();
+    this.leafKp_ = kpg.generateKeyPair();
+
+    rootCert_ = generateRootCACert(rootCaCertCN, this.rootCaKp_);
+    leafCert_ = generateLeafCert(rootLeafCertCN, this.leafKp_, this.rootCert_,
+        this.rootCaKp_.getPrivate());
+  }
+
+  /**
+   * Generates a string representation of the PEM-encoded root certificate.
+   */
+  public String rootCertAsPemString() throws CertificateEncodingException, IOException {
+    return this.certToPem(this.rootCert_);
+  }
+
+  /**
+   * Generates a string representation of the PEM-encoded leaf certificate.
+   */
+  public String leafCertAsPemString() throws CertificateEncodingException, IOException {
+    return this.certToPem(this.leafCert_);
+  }
+
+  /**
+   * Writes the PEM encoded root certificate to the provided java.io.Writer. The writer
+   * is not flushed after the cert is written to it.
+   *
+   * @param w java.io.Writer where the PEM-encoded root certificate will be written.
+   */
+  public void writeRootCertAsPem(Writer w)
+      throws CertificateEncodingException, IOException {
+    this.certToPem(this.rootCert_, w);
+  }
+
+  /**
+   * Writes the PEM-encoded leaf certificate to the provided java.io.Writer. The writer
+   * is not flushed after the cert is written to it.
+   *
+   * @param w java.io.Writer where the PEM encoded leaf certificate will be written.
+   */
+  public void writeLeafCertAsPem(Writer w)
+      throws CertificateEncodingException, IOException {
+    this.certToPem(this.leafCert_, w);
+  }
+
+  /**
+   * Writes the PEM-encoded RSA private key of the leaf certificate to the provided
+   * java.io.Writer.  The writer is not fluished after the cert is written to it.
+   *
+   * @param w java.io.Writer where the PEM encoded leaf private key will be written.
+   */
+  public void writeLeafPrivateKeyAsPem(Writer w) throws IOException {
+    PemObject o = new PemObject("RSA PRIVATE KEY",
+        this.leafKp_.getPrivate().getEncoded());
+    PemWriter pw = new PemWriter(w);
+
+    pw.writeObject(o);
+    pw.close();
+  }
+
+  public X509Certificate getRootCert() {
+    return this.rootCert_;
+  }
+
+  public X509Certificate getLeafCert() {
+    return this.leafCert_;
+  }
+
+  private X509Certificate generateRootCACert(String commonName, KeyPair kp)
+      throws NoSuchAlgorithmException, NoSuchProviderException, InvalidKeyException,
+      SignatureException, IOException, CertificateException {
+    V3TBSCertificateGenerator gen = new V3TBSCertificateGenerator();
+    X500Name cn = new X500Name(String.format("CN=%s", commonName));
+    ExtensionsGenerator extGenerator = new ExtensionsGenerator();
+    SubjectPublicKeyInfo subj;
+
+    subj = SubjectPublicKeyInfo.getInstance(kp.getPublic().getEncoded());
+
+    extGenerator.addExtension(Extension.keyUsage, false, KEY_USAGE_CERT_SIGN);
+    extGenerator.addExtension(Extension.basicConstraints, false, CONSTRAINT_CA);
+
+    // set the certificate start to an hour ago and the certificate end to an hour
+    // from now
+    gen.setStartDate(new Time(new Date(System.currentTimeMillis() - 60 * 1000)));
+    gen.setEndDate(new Time(new Date(System.currentTimeMillis() + 60 * 60 * 1000)));
+
+    gen.setSerialNumber(new ASN1Integer(1));
+    gen.setIssuer(cn);
+    gen.setSubject(cn);
+    gen.setSignature(SIGNATURE_SHA256_RSA);
+    gen.setSubjectPublicKeyInfo(subj);
+    gen.setExtensions(extGenerator.generate());
+
+    TBSCertificate tbsCert = gen.generateTBSCertificate();
+    Signature sig = Signature.getInstance(SHA256_WITH_RSA, "BC");
+
+    sig.initSign(kp.getPrivate());
+    sig.update(gen.generateTBSCertificate().getEncoded(ASN1Encoding.DER));
+
+    ASN1EncodableVector v = new ASN1EncodableVector();
+
+    v.add(tbsCert);
+    v.add(SIGNATURE_SHA256_RSA);
+    v.add(new DERBitString(sig.sign()));
+
+    return (X509Certificate)CertificateFactory.getInstance("X.509", "BC")
+        .generateCertificate(new ByteArrayInputStream(new DERSequence(v)
+        .getEncoded(ASN1Encoding.DER)));
+  }
+
+  private X509Certificate generateLeafCert(String commonName, KeyPair kp, X509Certificate
+      issuerCert, PrivateKey issuerPrivateKey) throws IOException,
+      NoSuchAlgorithmException, NoSuchProviderException, InvalidKeyException,
+      SignatureException, CertificateException {
+    V3TBSCertificateGenerator gen = new V3TBSCertificateGenerator();
+    X500Name cn = new X500Name(String.format("CN=%s", commonName));
+    ExtensionsGenerator extGenerator = new ExtensionsGenerator();
+    SubjectPublicKeyInfo subj;
+    X500Name issuerSubj;
+
+    issuerSubj = new X500Name(issuerCert.getSubjectX500Principal().getName());
+
+    subj = SubjectPublicKeyInfo.getInstance(kp.getPublic().getEncoded());
+
+    extGenerator.addExtension(Extension.keyUsage, false, KEY_USAGE_SERVER_AUTH);
+    extGenerator.addExtension(Extension.extendedKeyUsage, false, new ExtendedKeyUsage(
+        new KeyPurposeId[]{KeyPurposeId.id_kp_serverAuth,
+        KeyPurposeId.id_kp_clientAuth}));
+
+    // set the certificate start to an hour ago and the certificate end to an hour
+    // from now
+    gen.setStartDate(new Time(new Date(System.currentTimeMillis() - 60 * 1000)));
+    gen.setEndDate(new Time(new Date(System.currentTimeMillis() + 60 * 60 * 1000)));
+
+    gen.setSerialNumber(new ASN1Integer(2));
+    gen.setIssuer(issuerSubj);
+    gen.setSubject(cn);
+    gen.setSignature(SIGNATURE_SHA256_RSA);
+    gen.setSubjectPublicKeyInfo(subj);
+    gen.setExtensions(extGenerator.generate());
+
+    TBSCertificate leafCert = gen.generateTBSCertificate();
+    Signature sig = Signature.getInstance(SHA256_WITH_RSA, "BC");
+
+    sig.initSign(issuerPrivateKey);
+    sig.update(gen.generateTBSCertificate().getEncoded(ASN1Encoding.DER));
+
+    ASN1EncodableVector v = new ASN1EncodableVector();
+
+    v.add(leafCert);
+    v.add(SIGNATURE_SHA256_RSA);
+    v.add(new DERBitString(sig.sign()));
+
+    return (java.security.cert.X509Certificate)CertificateFactory
+        .getInstance("X.509", "BC").generateCertificate(new ByteArrayInputStream(
+        new DERSequence(v).getEncoded(ASN1Encoding.DER)));
+  }
+
+  private void certToPem(X509Certificate cert, Writer writer) throws IOException,
+      CertificateEncodingException {
+    X509CertificateHolder bundle = new X509CertificateHolder(cert.getEncoded());
+    PemWriter pw = new PemWriter(writer);
+    pw.writeObject(new MiscPEMGenerator(bundle));
+    pw.close();
+  }
+
+  private String certToPem(X509Certificate cert) throws IOException,
+      CertificateEncodingException {
+    StringWriter pemOut = new StringWriter();
+    this.certToPem(cert, pemOut);
+    pemOut.flush();
+
+    return pemOut.toString();
+  }
+
+}