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

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

commit f3d693d3fd8533a8533bd63b4711374aa8e61cfa
Author: jasonmfehr <[email protected]>
AuthorDate: Wed Jun 25 20:51:11 2025 -0700

    IMPALA-13237: [Patch 2 of 5] - Add OpenSSL Utility Function to Validate PEM 
Bundles
    
    Adds a new function ValidatePemBundle(const string& bundle) to the
    openssl utilities. This function asserts that all certificates in a
    string containing a bundle of X.509 PEM-encoded certificates are
    valid. If even one certificate is invalid, then validation fails and
    the function returns false.
    
    Certificates can be invalid for these reasons:
      1. Structure is invalid/PEM encoding is incorrect.
      2. notBefore time is in the future.
      3. notAfter time is in the past.
    
    Testing accomplished by the tests in openssl-util-test.cc
    successfully passing locally and in a build.
    
    Generated-by: Github Copilot (GPT-4.1)
    Change-Id: I1e9f12e854b679ccf3e94a38c2688a7431460a7a
    Reviewed-on: http://gerrit.cloudera.org:8080/23097
    Reviewed-by: Impala Public Jenkins <[email protected]>
    Reviewed-by: Jason Fehr <[email protected]>
    Reviewed-by: Alexey Serbin <[email protected]>
    Tested-by: Impala Public Jenkins <[email protected]>
---
 be/src/testutil/future-cert.pem  |  24 +++++
 be/src/testutil/future-key.pem   |  28 +++++
 be/src/util/openssl-util-test.cc | 222 +++++++++++++++++++++++++++++++++++++++
 be/src/util/openssl-util.cc      |  62 +++++++++++
 be/src/util/openssl-util.h       |   5 +
 5 files changed, 341 insertions(+)

diff --git a/be/src/testutil/future-cert.pem b/be/src/testutil/future-cert.pem
new file mode 100644
index 000000000..401744296
--- /dev/null
+++ b/be/src/testutil/future-cert.pem
@@ -0,0 +1,24 @@
+-----BEGIN CERTIFICATE-----
+MIID+zCCAeOgAwIBAgICEAAwDQYJKoZIhvcNAQELBQAwGDEWMBQGA1UEAwwNSW1w
+YWxhVGVzdGluZzAiGA8yMDk5MTIzMDAwMDAwMFoYDzIwOTkxMjMxMjM1OTU5WjAU
+MRIwEAYDVQQDDAlsb2NhbGhvc3QwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEK
+AoIBAQC7hXd0/0rQ16BpCk6qL7/I4jqG1ntWPNlmD/1Fcv2LB3rEVL+h9inR8yyN
+Ht59qXSxnQHTdoGqdLC3/H6q7bOQb9dBBYEeZiH00RdTrmzb4uJL7YGagIhINqJc
+YOyP1Ek9ZFHi5YDqdqri0kEGid4CiOz1RTjrbjfwI5kdvpEbzcF4HJFr9eMh1+KI
+v6rHd3i36CV296FaGkvpxYmtNjjXTtoXmZtiw+t7nC7kHZ2tQdPCU7+0SUYn8p7e
+ifAD0Bjs8/Tei27JqdkpOptIxl0Jtg3j8bK1nVW7BrbVMAiVAOIi/bdlSUB7uKC4
+QCxqDvLHCKyKCT4cFHMYLQTWyOg1AgMBAAGjTzBNMAkGA1UdEwQCMAAwCwYDVR0P
+BAQDAgWgMB0GA1UdJQQWMBQGCCsGAQUFBwMBBggrBgEFBQcDAjAUBgNVHREEDTAL
+gglsb2NhbGhvc3QwDQYJKoZIhvcNAQELBQADggIBAAQQzPDedcBDa9zw0Pg3G0a3
+CjeDi/T4bw2jF1f6wwk1o9sHJGN33hrU1v3lz43ba4v8SOgTxuwRAzlBCTudqRnw
+5ysf+RfbaF4MaCuT7b/gzARwsdD70gRcaa4zBVMfteIysw8RaQlv4TU22g33OeT9
+eAyoYIODjP47NtEdt0MMFc4elG38ZGcPxVIfymriesNCUsCDW64s0BJACZzjDDih
+CQbrzYi/VMgZAsngs8v3yN+Novlbax8FoJquMJVtDMFp5ihHO7uikO2f4pqGz+eK
+KRkkTkedTFE8KeIAx2rYsEIrsRWkgusfGRYtMmNgU791ChL9eR0B90D8/g4e4Aa1
+mN9jPYKR42Yr7+OUuiSNkd0dEdAOge2blPPFsKdsndFTtTRBagwg8+NfEIWqVUJv
+x8GHQ995jLWBWvCxF/AsMjgmGD+wC5ya4RRnX3FEGffpPDxZffJJiFn97ItbNGtp
+KL9fMy+MA6kvpCQGJ1VPXRCo17v6S2+k/e/IlSVZXOcTghqCOpjJaURechqECVnc
+lWBEALlAORmFshico4mYk3Acaohe3MjpAhkoKGbsUYdE95mZ38Kqyt74mWYz2zGc
+dXmgAuScijk/TtVuL9HW9y8RpIfhmfjoiNEM1JC7Tkl62I6RV/h5XPUDT8wtdz3t
+co9ID1TWIjeBcX3Xijks
+-----END CERTIFICATE-----
diff --git a/be/src/testutil/future-key.pem b/be/src/testutil/future-key.pem
new file mode 100644
index 000000000..dff8fa970
--- /dev/null
+++ b/be/src/testutil/future-key.pem
@@ -0,0 +1,28 @@
+-----BEGIN PRIVATE KEY-----
+MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQC7hXd0/0rQ16Bp
+Ck6qL7/I4jqG1ntWPNlmD/1Fcv2LB3rEVL+h9inR8yyNHt59qXSxnQHTdoGqdLC3
+/H6q7bOQb9dBBYEeZiH00RdTrmzb4uJL7YGagIhINqJcYOyP1Ek9ZFHi5YDqdqri
+0kEGid4CiOz1RTjrbjfwI5kdvpEbzcF4HJFr9eMh1+KIv6rHd3i36CV296FaGkvp
+xYmtNjjXTtoXmZtiw+t7nC7kHZ2tQdPCU7+0SUYn8p7eifAD0Bjs8/Tei27Jqdkp
+OptIxl0Jtg3j8bK1nVW7BrbVMAiVAOIi/bdlSUB7uKC4QCxqDvLHCKyKCT4cFHMY
+LQTWyOg1AgMBAAECggEBAIonvub334sWEhzRdztzinzaD/yfsKbMle7YAOVV3u67
+R/u15KnSzuTboAsonws9+beR0XKWcNnAtyIWaMxN7GmRvKkvAP4AiSvxkJoafCFU
+ex0tkrrdz4pElkIxNpiQ8pTEgIAGToBvhV8vQLEsieCcONhLc7AguB24XGrJp+cu
+4WOcIFIG4B5JlnFIMg36SCymlhSFsIlBVoHIc5EwJrJHTwhBSS0KLv1X0PT9hKjB
+Ozu3GT2+QmWsMjjaCYkQvfmqOkZ3nQNt0xyjxJ3b3ce1H4GYD0R/rKp6SkhFodnT
+VY51/UtpI/sbNLV9Dzg9hulHyGhsViBT5FkVHBuFxoECgYEA3puv424Udny4Dedv
+8yU+UJmRhoAM+uePF56GVDKzCbQKlfYeqODxdw41kprh5+NKr3xXqunE4HW2Ksgq
+iBcoV+aZTaiMSuZ1EKwAvbhL1HfbNBM46g8UhdEmF88SosDzZv0NWk/AmLXa/wnR
+bHC20FRx2D27Y8m6HV0WIMq5m8kCgYEA16Zr2ljd1DJhfHmeFXeHsv5B6Fkx7JZg
+Br5+89pPT3IV64bUtmm2T9M53vWTi1cHrxLh8rSiIUJEIZrAL5nj4iODis9XMqyq
+bkzgySzMlqzbuXrqyY87i7gLZLW8zlKCQThUGv42pdd4+uYtvnuJC1vMM1RRFYfO
+opqnpjkNhw0CgYAsUCWlSmZ8Z7tj2Sxp5IIRHWDgu2DaGXpco0vuOuF1DwgEbigQ
+SicHIib79iT+OjrF0CTLexmb7RLEVXBSAvoe9WKTGPhEeLfZB8Rl/+fodauHJERy
+I2pccRP6LVhyWaKaX8lmrvWR8LGVj2jHA3EA665182P4K1IHQl6DTJfCMQKBgHJG
+rXaqivrMq95TcFM94ToYBIME6TDPSxmnzwyYDlkyQq1p6MS1ZjgM3ejpyAMZ3meH
+IRcFY7kuLX6FS/ytlxYZ7bYcQ8AFwdbrhF73D391IIa8vTiMX2id4eO/bssC7CV1
+nRQkzH67nU+PtIPxYOEVBzPYJZO8QOLoA5lROHrtAoGBAI9QMBCv4vruXlgWadVP
+HrtH7IdB4BuRuAQBsJUnWi6ruS3DjvZUU0XCJQmy+/y8c7AMR73f2KUYHu8aESy/
+MjFv8SPBZHb/6HzH6HlPqjL7hHKvgvvEYovvtsdbz0oN3RRY//Agc8ECbu4Khl5f
+nWnr74srNeq5u+gC01RvdiUD
+-----END PRIVATE KEY-----
diff --git a/be/src/util/openssl-util-test.cc b/be/src/util/openssl-util-test.cc
index 66925e10e..384536f9f 100644
--- a/be/src/util/openssl-util-test.cc
+++ b/be/src/util/openssl-util-test.cc
@@ -22,11 +22,18 @@
 #include <openssl/rand.h>
 
 #include "common/init.h"
+#include "common/status.h"
+#include "gutil/strings/substitute.h"
+#include "kudu/util/env.h"
+#include "kudu/util/faststring.h"
+#include "kudu/util/status.h"
 #include "testutil/gtest-util.h"
 #include "util/openssl-util.h"
 
 using std::uniform_int_distribution;
 using std::mt19937_64;
+using strings::Substitute;
+using std::string;
 
 namespace impala {
 
@@ -241,5 +248,220 @@ TEST_F(OpenSSLUtilTest, RandSeeding) {
     ASSERT_OK(key.InitializeRandom(AES_BLOCK_SIZE, 
key.GetSupportedDefaultMode()));
   }
 }
+
+///
+/// ValidatePemBundle tests
+///
+/// Constants defining paths to test files.
+static const string& EXPIRED_CERT = "bad-cert";
+static const string& FUTURE_CERT = "future-cert";
+static const string& INVALID_CERT = "invalid-server-cert";
+static const string& VALID_CERT_1 = "server-cert";
+static const string& VALID_CERT_2 = "wildcardCA";
+
+/// Constants and functions defining expected error messages.
+static const string& MSG_NEW_BIO_ERR = "OpenSSL error in PEM_read_bio_X509 
unexpected "
+    "error '$0' while reading PEM bundle";
+static const string& MSG_NONE_VALID = "PEM bundle contains no valid 
certificates";
+static string _msg_time(int invalid_notbefore_cnt, int invalid_notafter_cnt) {
+  return Substitute("PEM bundle contains $0 invalid certificate(s) with 
notBefore in the "
+      "future and $1 invalid certificate(s) with notAfter in the past",
+      invalid_notbefore_cnt, invalid_notafter_cnt);
+}
+
+/// Constants used in the tests
+static const string& INVALID_CERT_TEXT = "-----BEGIN CERTIFICATE-----\nnot a 
cert";
+
+/// Helper function to read a certificate from the be/src/testutil directory.
+static string read_cert(const string& cert_name) {
+  kudu::faststring contents;
+  kudu::Status s = kudu::ReadFileToString(kudu::Env::Default(),
+      Substitute("$0/be/src/testutil/$1.pem", getenv("IMPALA_HOME"), 
cert_name),
+      &contents);
+
+  EXPECT_TRUE(s.ok())<< "Certificate '" << cert_name << "' could not be read: "
+      << s.ToString();
+  EXPECT_FALSE(contents.ToString().empty()) << "Certificate '" << cert_name
+      << "' is empty. Please check the test data.";
+
+  string cert = contents.ToString();
+  if (cert.back() != '\n') {
+    cert.push_back('\n');
+  }
+
+  return cert;
+} // function read_cert
+
+/// Asserts a PEM bundle containing one valid certificate is valid.
+TEST_F(OpenSSLUtilTest, ValidatePemBundleHappyPathOneCert) {
+  ASSERT_OK(ValidatePemBundle(Substitute("$0", read_cert(VALID_CERT_1))));
+}
+
+/// Asserts a PEM bundle containing two valid certificates is valid.
+TEST_F(OpenSSLUtilTest, ValidatePemBundleHappyPath) {
+  ASSERT_OK(ValidatePemBundle(Substitute("$0$1", read_cert(VALID_CERT_1),
+      read_cert(VALID_CERT_2))));
+}
+
+/// Asserts an empty PEM bundle is invalid.
+TEST_F(OpenSSLUtilTest, ValidatePemBundleEmpty) {
+  ASSERT_ERROR_MSG(ValidatePemBundle(""), "bundle is empty");
+}
+
+/// Asserts a bundle containing a single expired certificate is invalid.
+TEST_F(OpenSSLUtilTest, ValidatePemBundleExpired) {
+  ASSERT_ERROR_MSG(ValidatePemBundle(read_cert(EXPIRED_CERT)), _msg_time(0, 
1));
+}
+
+/// Asserts a bundle containing a single future dated certificate is invalid.
+TEST_F(OpenSSLUtilTest, ValidatePemBundleFutureDated) {
+  ASSERT_ERROR_MSG(ValidatePemBundle(read_cert(FUTURE_CERT)), _msg_time(1, 0));
+}
+
+/// Asserts a bundle containing a single invalid certificate is invalid.
+TEST_F(OpenSSLUtilTest, ValidatePemBundleNotACert) {
+  ASSERT_ERROR_MSG(ValidatePemBundle(INVALID_CERT_TEXT), MSG_NONE_VALID);
+}
+
+/// Asserts a bundle is invalid if it contains one expired and two non-expired
+/// certificates where the expired cert changes position in the bundle.
+TEST_F(OpenSSLUtilTest, ValidatePemBundleThreeCertsOneExpired) {
+  // Expired cert is first in the bundle.
+  ASSERT_ERROR_MSG(ValidatePemBundle(Substitute("$0$1$2", 
read_cert(EXPIRED_CERT),
+      read_cert(VALID_CERT_1), read_cert(VALID_CERT_2))), _msg_time(0, 1));
+
+  // Expired cert is in the middle of the bundle.
+  ASSERT_ERROR_MSG(ValidatePemBundle(Substitute("$0$1$2", 
read_cert(VALID_CERT_1),
+      read_cert(EXPIRED_CERT), read_cert(VALID_CERT_2))), _msg_time(0, 1));
+
+  // Expired cert is last in the bundle.
+  ASSERT_ERROR_MSG(ValidatePemBundle(Substitute("$0$1$2", 
read_cert(VALID_CERT_1),
+      read_cert(VALID_CERT_2), read_cert(EXPIRED_CERT))), _msg_time(0, 1));
+}
+
+/// Asserts a bundle is invalid if it contains one future dated and two valid
+/// certificates where the expired cert changes position in the bundle.
+TEST_F(OpenSSLUtilTest, ValidatePemBundleThreeCertsOneFutureDated) {
+  // Expired cert is first in the bundle.
+  ASSERT_ERROR_MSG(ValidatePemBundle(Substitute("$0$1$2", 
read_cert(FUTURE_CERT),
+      read_cert(VALID_CERT_1), read_cert(VALID_CERT_2))), _msg_time(1, 0));
+
+  // Expired cert is in the middle of the bundle.
+  ASSERT_ERROR_MSG(ValidatePemBundle(Substitute("$0$1$2", 
read_cert(VALID_CERT_1),
+      read_cert(FUTURE_CERT), read_cert(VALID_CERT_2))), _msg_time(1, 0));
+
+  // Expired cert is last in the bundle.
+  ASSERT_ERROR_MSG(ValidatePemBundle(Substitute("$0$1$2", 
read_cert(VALID_CERT_1),
+      read_cert(VALID_CERT_2), read_cert(FUTURE_CERT))), _msg_time(1, 0));
+}
+
+/// Asserts a bundle is invalid if it contains two expired and one non-expired
+/// certificate no matter the position of the expired cert in the bundle.
+TEST_F(OpenSSLUtilTest, ValidatePemBundleThreeCertsTwoExpired) {
+  // First two certs are expired.
+  ASSERT_ERROR_MSG(ValidatePemBundle(Substitute("$0$1$2", 
read_cert(EXPIRED_CERT),
+      read_cert(EXPIRED_CERT), read_cert(VALID_CERT_2))), _msg_time(0, 2));
+
+  // Last two certs are expired.
+  ASSERT_ERROR_MSG(ValidatePemBundle(Substitute("$0$1$2", 
read_cert(VALID_CERT_2),
+      read_cert(EXPIRED_CERT), read_cert(EXPIRED_CERT))), _msg_time(0, 2));
+}
+
+/// Asserts a bundle is invalid if it contains two future dated and one valid
+/// certificate no matter the position of the future dated cert in the bundle.
+TEST_F(OpenSSLUtilTest, ValidatePemBundleThreeCertsTwoFutureDated) {
+  // First two certs are expired.
+  ASSERT_ERROR_MSG(ValidatePemBundle(Substitute("$0$1$2", 
read_cert(FUTURE_CERT),
+      read_cert(FUTURE_CERT), read_cert(VALID_CERT_2))), _msg_time(2, 0));
+
+  // Last two certs are expired.
+  ASSERT_ERROR_MSG(ValidatePemBundle(Substitute("$0$1$2", 
read_cert(VALID_CERT_2),
+      read_cert(FUTURE_CERT), read_cert(FUTURE_CERT))), _msg_time(2, 0));
+}
+
+/// Asserts a bundle is invalid if it contains one valid and two invalid 
certificates
+/// where the expired cert changes position in the bundle.
+TEST_F(OpenSSLUtilTest, ValidatePemBundleThreeCertsOneInvalid) {
+  // Invalid cert is first in the bundle.
+  ASSERT_ERROR_MSG(ValidatePemBundle(Substitute("$0$1$2", INVALID_CERT_TEXT,
+      read_cert(VALID_CERT_1), read_cert(VALID_CERT_2))), MSG_NONE_VALID);
+
+  // Invalid cert is in the middle of the bundle.
+  EXPECT_STR_CONTAINS(ValidatePemBundle(Substitute("$0$1$2", 
read_cert(VALID_CERT_1),
+      INVALID_CERT_TEXT, read_cert(VALID_CERT_2))).GetDetail(),
+      Substitute(MSG_NEW_BIO_ERR, 123));
+
+  // Invalid cert is last in the bundle.
+  EXPECT_STR_CONTAINS(ValidatePemBundle(Substitute("$0$1$2", 
read_cert(VALID_CERT_1),
+      read_cert(VALID_CERT_2), INVALID_CERT_TEXT)).GetDetail(),
+      Substitute(MSG_NEW_BIO_ERR, 112));
+}
+
+/// Asserts a bundle is invalid if it contains one expired and one non-expired 
certificate
+/// no matter the position of the expired cert in the bundle.
+TEST_F(OpenSSLUtilTest, ValidatePemBundleTwoCertsExpired) {
+  // Expired cert is first in the bundle.
+  ASSERT_ERROR_MSG(ValidatePemBundle(Substitute("$0$1", 
read_cert(EXPIRED_CERT),
+      read_cert(VALID_CERT_1))), _msg_time(0, 1));
+
+  // Expired cert is last in the bundle.
+  ASSERT_ERROR_MSG(ValidatePemBundle(Substitute("$0$1", 
read_cert(VALID_CERT_1),
+      read_cert(EXPIRED_CERT))), _msg_time(0, 1));
+}
+
+/// Asserts a bundle is invalid if it contains one future dated and one valid 
certificate
+/// no matter the position of the future dated cert in the bundle.
+TEST_F(OpenSSLUtilTest, ValidatePemBundleTwoCertsFutureDated) {
+  // Expired cert is first in the bundle.
+  ASSERT_ERROR_MSG(ValidatePemBundle(Substitute("$0$1", read_cert(FUTURE_CERT),
+      read_cert(VALID_CERT_1))), _msg_time(1, 0));
+
+  // Expired cert is last in the bundle.
+  ASSERT_ERROR_MSG(ValidatePemBundle(Substitute("$0$1", 
read_cert(VALID_CERT_1),
+      read_cert(FUTURE_CERT))), _msg_time(1, 0));
+}
+
+/// Asserts a bundle is invalid if it contains one valid and one invalid 
certificate
+/// no matter the position of the expired cert in the bundle.
+TEST_F(OpenSSLUtilTest, ValidatePemBundleTwoCertsNotCertFirst) {
+  // Invalid cert is first in the bundle.
+  ASSERT_ERROR_MSG(ValidatePemBundle(Substitute("$0$1", INVALID_CERT_TEXT,
+      read_cert(VALID_CERT_1))), MSG_NONE_VALID);
+
+  // Invalid cert is last in the bundle.
+  EXPECT_STR_CONTAINS(ValidatePemBundle(Substitute("$0$1", 
read_cert(VALID_CERT_1),
+      INVALID_CERT_TEXT)).GetDetail(), Substitute(MSG_NEW_BIO_ERR, 112));
+}
+
+/// Asserts a bundle is invalid if it contains two valid, one expired, and one 
future
+/// dated certificate no matter the position of the certs in the bundle.
+TEST_F(OpenSSLUtilTest, ValidatePemBundleFourCerts) {
+  // Expired and future dated certs are first in the bundle.
+  ASSERT_ERROR_MSG(ValidatePemBundle(Substitute("$0$1", 
read_cert(EXPIRED_CERT),
+      read_cert(FUTURE_CERT), read_cert(VALID_CERT_1), 
read_cert(VALID_CERT_2))),
+      _msg_time(1, 1));
+
+  // Expired and future dated certs are last in the bundle.
+  ASSERT_ERROR_MSG(ValidatePemBundle(Substitute("$0$1$2$3", 
read_cert(VALID_CERT_1),
+      read_cert(VALID_CERT_2), read_cert(EXPIRED_CERT), 
read_cert(FUTURE_CERT))),
+      _msg_time(1, 1));
+
+  // Expired and future dated certs are intermixed in the bundle.
+  ASSERT_ERROR_MSG(ValidatePemBundle(Substitute("$0$1$2$3", 
read_cert(VALID_CERT_1),
+      read_cert(FUTURE_CERT), read_cert(VALID_CERT_2), 
read_cert(EXPIRED_CERT))),
+      _msg_time(1, 1));
+}
+
+/// Asserts a bundle is invalid if it contains one expired and one future dated
+/// certificate no matter the position of the certs in the bundle.
+TEST_F(OpenSSLUtilTest, ValidatePemBundleOneExpiredOneFutureDated) {
+  // Expired cert is first in the bundle.
+  ASSERT_ERROR_MSG(ValidatePemBundle(Substitute("$0$1", 
read_cert(EXPIRED_CERT),
+      read_cert(FUTURE_CERT))), _msg_time(1, 1));
+
+  // Future dated cert is first in the bundle.
+  ASSERT_ERROR_MSG(ValidatePemBundle(Substitute("$0$1", read_cert(FUTURE_CERT),
+      read_cert(EXPIRED_CERT))), _msg_time(1, 1));
 }
 
+} // namespace impala
diff --git a/be/src/util/openssl-util.cc b/be/src/util/openssl-util.cc
index d814d6d8c..bb65e21e8 100644
--- a/be/src/util/openssl-util.cc
+++ b/be/src/util/openssl-util.cc
@@ -25,19 +25,24 @@
 #include <iostream>
 
 #include <glog/logging.h>
+#include <openssl/bio.h>
 #include <openssl/err.h>
 #include <openssl/evp.h>
+#include <openssl/pem.h>
 #include <openssl/rand.h>
 #include <openssl/sha.h>
 #include <openssl/tls1.h>
+#include <openssl/x509.h>
 
 #include "common/atomic.h"
+#include "common/compiler-util.h"
 #include "common/status.h"
 #include "gutil/port.h" // ATTRIBUTE_WEAK
 #include "gutil/strings/substitute.h"
 
 #include "common/names.h"
 #include "cpu-info.h"
+#include "kudu/util/openssl_util.h"
 
 DECLARE_string(ssl_client_ca_certificate);
 DECLARE_string(ssl_server_certificate);
@@ -152,6 +157,63 @@ void SeedOpenSSLRNG() {
   RAND_load_file("/dev/urandom", RNG_RESEED_BYTES);
 }
 
+Status ValidatePemBundle(const string& bundle) {
+  SCOPED_OPENSSL_NO_PENDING_ERRORS;
+
+  if (UNLIKELY(bundle.empty())) {
+    return Status("bundle is empty");
+  }
+
+  BIO* bio = BIO_new_mem_buf(bundle.data(), bundle.size());
+  if (UNLIKELY(ERR_peek_error() != 0 || bio == nullptr)) {
+    Status ret = OpenSSLErr("BIO_new_mem_buf", Substitute("error '$0' creating 
BIO from "
+        "PEM bundle", ERR_GET_REASON(ERR_peek_error())));
+    ERR_clear_error();
+    return ret;
+  }
+
+  X509* cert = nullptr;
+  int cert_count = 0;
+  int invalid_notbefore_cnt = 0;
+  int invalid_notafter_cnt = 0;
+  while ((cert = PEM_read_bio_X509(bio, nullptr, nullptr, nullptr)) != 
nullptr) {
+    cert_count++;
+    // Check certificate validity (notBefore and notAfter)
+    if (UNLIKELY(X509_cmp_current_time(X509_get0_notBefore(cert)) > 0)) {
+      invalid_notbefore_cnt++;
+    }
+
+    if (UNLIKELY(X509_cmp_current_time(X509_get0_notAfter(cert)) < 0)) {
+      invalid_notafter_cnt++;
+    }
+
+    X509_free(cert);
+  }
+  BIO_free(bio);
+
+  Status ret;
+  if (UNLIKELY(invalid_notbefore_cnt > 0 || invalid_notafter_cnt > 0)) {
+    ret = Status(Substitute(
+        "PEM bundle contains $0 invalid certificate(s) with notBefore in the 
future "
+        "and $1 invalid certificate(s) with notAfter in the past",
+        invalid_notbefore_cnt, invalid_notafter_cnt));
+  } else if (UNLIKELY(cert_count == 0)) {
+    ret = Status("PEM bundle contains no valid certificates");
+  } else if (UNLIKELY(ERR_GET_REASON(ERR_peek_error()) != 
PEM_R_NO_START_LINE)) {
+    // The final PEM_read_bio_X509 always sets the openssl error to 
PEM_R_NO_START_LINE,
+    // if the ssl error is set to anything else, return it.
+    ret = OpenSSLErr("PEM_read_bio_X509", Substitute("unexpected error '$0' 
while "
+        "reading PEM bundle", ERR_GET_REASON(ERR_peek_error())));
+  } else {
+    ret = Status::OK();
+  }
+
+  // Clear the error left by the final PEM_read_bio_X509 call when it returns 
nullptr.
+  ERR_clear_error();
+
+  return ret;
+} // function ValidatePemBundle
+
 void IntegrityHash::Compute(const uint8_t* data, int64_t len) {
   // Explicitly ignore the return value from SHA256(); it can't fail.
   (void)SHA256(data, len, hash_);
diff --git a/be/src/util/openssl-util.h b/be/src/util/openssl-util.h
index 88f2c4d1d..b52c0710a 100644
--- a/be/src/util/openssl-util.h
+++ b/be/src/util/openssl-util.h
@@ -70,6 +70,11 @@ inline bool IsFIPSMode() {
 #endif
 };
 
+/// Validates a PEM-encoded certificate bundle by ensuring the given string 
contains only
+/// certificates with a valid X.509 structure and that each certificate is 
useable based
+/// its notBefore/notAfter fields. Does not validate each certificate's 
signature chain.
+Status ValidatePemBundle(const string& bundle);
+
 // Enum of all the AES modes that are currently supported. INVALID is used 
whenever
 // user inputs a wrong AES Mode, for example, AES_128_CTF etc.
 enum class AES_CIPHER_MODE {

Reply via email to