You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@kudu.apache.org by to...@apache.org on 2017/01/26 21:45:49 UTC

[1/3] kudu git commit: KUDU-1760. Add test coverage of reading pre-REINSERT after ALTER

Repository: kudu
Updated Branches:
  refs/heads/master 0a4541bba -> 7b01d8132


KUDU-1760. Add test coverage of reading pre-REINSERT after ALTER

This adds a test for the case where an ALTER TABLE adds a column, and
then we read the old history of that row from prior to the addition of
the column. We make sure that the default value of the new column is
returned.

Change-Id: Id7d44f6e2fe4d3cae03e0024c70e9f2ae84b614c
Reviewed-on: http://gerrit.cloudera.org:8080/5488
Tested-by: Kudu Jenkins
Reviewed-by: Todd Lipcon <to...@apache.org>


Project: http://git-wip-us.apache.org/repos/asf/kudu/repo
Commit: http://git-wip-us.apache.org/repos/asf/kudu/commit/5d12bdb9
Tree: http://git-wip-us.apache.org/repos/asf/kudu/tree/5d12bdb9
Diff: http://git-wip-us.apache.org/repos/asf/kudu/diff/5d12bdb9

Branch: refs/heads/master
Commit: 5d12bdb904388f0e6a50b41b60138f3dff2efe00
Parents: 0a4541b
Author: Todd Lipcon <to...@apache.org>
Authored: Tue Dec 13 14:42:38 2016 +0700
Committer: Todd Lipcon <to...@apache.org>
Committed: Thu Jan 26 21:15:01 2017 +0000

----------------------------------------------------------------------
 src/kudu/integration-tests/alter_table-test.cc | 47 +++++++++++++++++++++
 src/kudu/tablet/compaction.cc                  |  1 -
 2 files changed, 47 insertions(+), 1 deletion(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/kudu/blob/5d12bdb9/src/kudu/integration-tests/alter_table-test.cc
----------------------------------------------------------------------
diff --git a/src/kudu/integration-tests/alter_table-test.cc b/src/kudu/integration-tests/alter_table-test.cc
index 56c0e7b..144c48a 100644
--- a/src/kudu/integration-tests/alter_table-test.cc
+++ b/src/kudu/integration-tests/alter_table-test.cc
@@ -53,6 +53,7 @@ DECLARE_bool(enable_maintenance_manager);
 DECLARE_int32(heartbeat_interval_ms);
 DECLARE_int32(flush_threshold_mb);
 DECLARE_bool(use_hybrid_clock);
+DECLARE_bool(scanner_allow_snapshot_scans_with_logical_timestamps);
 
 namespace kudu {
 
@@ -60,6 +61,7 @@ using client::CountTableRows;
 using client::KuduClient;
 using client::KuduClientBuilder;
 using client::KuduColumnSchema;
+using client::KuduDelete;
 using client::KuduError;
 using client::KuduInsert;
 using client::KuduRowResult;
@@ -95,6 +97,7 @@ class AlterTableTest : public KuduTest {
     CHECK_OK(b.Build(&schema_));
 
     FLAGS_use_hybrid_clock = false;
+    FLAGS_scanner_allow_snapshot_scans_with_logical_timestamps = true;
   }
 
   void SetUp() override {
@@ -208,6 +211,7 @@ class AlterTableTest : public KuduTest {
   void VerifyRows(int start_row, int num_rows, VerifyPattern pattern);
 
   void InsertRows(int start_row, int num_rows);
+  void DeleteRow(int row_key);
 
   Status InsertRowsSequential(const string& table_name, int start_row, int num_rows);
 
@@ -451,6 +455,19 @@ void AlterTableTest::InsertRows(int start_row, int num_rows) {
   FlushSessionOrDie(session);
 }
 
+void AlterTableTest::DeleteRow(int row_key) {
+  shared_ptr<KuduSession> session = client_->NewSession();
+  shared_ptr<KuduTable> table;
+  CHECK_OK(session->SetFlushMode(KuduSession::MANUAL_FLUSH));
+  session->SetTimeoutMillis(15 * 1000);
+  CHECK_OK(client_->OpenTable(kTableName, &table));
+
+  unique_ptr<KuduDelete> del(table->NewDelete());
+  CHECK_OK(del->mutable_row()->SetInt32(0, bswap_32(row_key)));
+  CHECK_OK(session->Apply(del.release()));
+  FlushSessionOrDie(session);
+}
+
 Status AlterTableTest::InsertRowsSequential(const string& table_name, int start_row, int num_rows) {
   shared_ptr<KuduSession> session = client_->NewSession();
   shared_ptr<KuduTable> table;
@@ -852,6 +869,36 @@ TEST_F(AlterTableTest, TestMajorCompactDeltasAfterAddUpdateRemoveColumn) {
             JoinStrings(rows, "\n"));
 }
 
+// Test that, if we have history of previous versions of a row stored as REINSERTs,
+// and those REINSERTs were written prior to adding a new column, that reading the
+// old versions of the row will return the default value for the new column.
+// See KUDU-1760.
+TEST_F(AlterTableTest, TestReadHistoryAfterAlter) {
+  FLAGS_enable_maintenance_manager = false;
+
+  InsertRows(0, 1);
+  DeleteRow(0);
+  InsertRows(0, 1);
+  DeleteRow(0);
+  InsertRows(0, 1);
+  uint64_t ts1 = client_->GetLatestObservedTimestamp();
+  ASSERT_OK(tablet_peer_->tablet()->Flush());
+  ASSERT_OK(AddNewI32Column(kTableName, "c2", 12345));
+
+  shared_ptr<KuduTable> table;
+  ASSERT_OK(client_->OpenTable(kTableName, &table));
+  KuduScanner scanner(table.get());
+  ASSERT_OK(scanner.SetReadMode(KuduScanner::READ_AT_SNAPSHOT));
+  // TODO(KUDU-1813): why do we need the '- 1' below? this seems odd that the
+  // "latest observed" timestamp is one higher than the thing we wrote, and the
+  // Delete then is assigned that timestamp.
+  ASSERT_OK(scanner.SetSnapshotRaw(ts1 - 1));
+  vector<string> row_strings;
+  client::ScanToStrings(&scanner, &row_strings);
+  ASSERT_EQ(1, row_strings.size());
+  ASSERT_EQ("(int32 c0=0, int32 c1=0, int32 c2=12345)", row_strings[0]);
+}
+
 // Thread which inserts rows into the table.
 // After each batch of rows is inserted, next_idx_ is updated
 // to communicate how much data has been written (and should now be

http://git-wip-us.apache.org/repos/asf/kudu/blob/5d12bdb9/src/kudu/tablet/compaction.cc
----------------------------------------------------------------------
diff --git a/src/kudu/tablet/compaction.cc b/src/kudu/tablet/compaction.cc
index 45f5772..afe4b88 100644
--- a/src/kudu/tablet/compaction.cc
+++ b/src/kudu/tablet/compaction.cc
@@ -1016,7 +1016,6 @@ Status ApplyMutationsAndGenerateUndos(const MvccSnapshot& snap,
 
         // 2 - Apply the changes of the reinsert to the latest version of the row
         // capturing the old row while we're at it.
-        // TODO(dralves) Make Reinserts set defaults on the dest row. See KUDU-1760.
         undo_encoder.SetToReinsert();
         RETURN_NOT_OK_LOG(redo_decoder.MutateRowAndCaptureChanges(
             dst_row, static_cast<Arena*>(nullptr), &undo_encoder), ERROR,


[2/3] kudu git commit: security: generate certs on the tserver, sign them on the master

Posted by to...@apache.org.
security: generate certs on the tserver, sign them on the master

This adds a bit of plumbing for the self-hosted PKI:

* Servers (both TS and Master) have a new ServerCertManager instance
  which generate a private key on startup. They also generate a CSR and
  adopt a signed cert once provided. This is also a convenient place to
  stash the set of CA certs and plumb them through to the SSL library,
  though that isn't implemented yet.

* Similarly, the master has a MasterCertAuthority instance which generates
  a key and self-signed CA cert on startup. It can then sign certs
  provided by other servers. This may change a bit in the future as the
  CA cert will have to be loaded from the system tablet if it's
  available, rather than generated on startup.

* When the TS heartbeats, it checks if the cert manager has a signed
  cert yet. If not, it sends the CSR to the master in DER format.

* If the master gets a heartbeat with a CSR, it signs it and returns the
  signed cert in the heartbeat response. The tablet server then adopts
  this as its cert.

A number of items are left as follow-ons. I noted them with "TODO(PKI)"
so that they'll be easy to grep for before we call this feature done.
In particular:

* Currently the master doesn't yet sign its own cert. This is going to
  have some interaction with the storage of certs in the catalog table,
  so want to wait until that code is integrated before figuring out
  where to plug this in.

* The built-in PKI stuff should have a flag to disable it. Again I
  wasn't sure the best place to put it for now, and it's nice to get the
  test coverage of this new code all the time. We can add this flag at
  the same point when we add the appropriate flags to configure your own
  PKI.

* Various other questions and vague thoughts that we can address as we
  go.

Note that this doesn't add any actual functionality, since the resulting
certs aren't actually attached to the RPC system in any way.

Change-Id: I3eb8ab4edc17e2fa1a54e0123a06dabc59a0489b
Reviewed-on: http://gerrit.cloudera.org:8080/5766
Reviewed-by: Dan Burkert <da...@apache.org>
Tested-by: Todd Lipcon <to...@apache.org>


Project: http://git-wip-us.apache.org/repos/asf/kudu/repo
Commit: http://git-wip-us.apache.org/repos/asf/kudu/commit/1ee00311
Tree: http://git-wip-us.apache.org/repos/asf/kudu/tree/1ee00311
Diff: http://git-wip-us.apache.org/repos/asf/kudu/diff/1ee00311

Branch: refs/heads/master
Commit: 1ee003113ce32fa1379e7e0656516f41b839dcd3
Parents: 5d12bdb
Author: Todd Lipcon <to...@apache.org>
Authored: Sun Jan 22 16:07:42 2017 -0800
Committer: Todd Lipcon <to...@apache.org>
Committed: Thu Jan 26 21:44:36 2017 +0000

----------------------------------------------------------------------
 src/kudu/integration-tests/registration-test.cc |  14 +-
 src/kudu/master/CMakeLists.txt                  |  16 ++-
 src/kudu/master/master.cc                       |   9 ++
 src/kudu/master/master.h                        |   6 +-
 src/kudu/master/master.proto                    |   8 ++
 src/kudu/master/master_cert_authority.cc        | 128 +++++++++++++++++++
 src/kudu/master/master_cert_authority.h         |  75 +++++++++++
 src/kudu/master/master_service.cc               |  16 +++
 src/kudu/security/CMakeLists.txt                |   1 +
 src/kudu/security/server_cert_manager.cc        | 115 +++++++++++++++++
 src/kudu/security/server_cert_manager.h         |  94 ++++++++++++++
 src/kudu/server/server_base.cc                  |   6 +
 src/kudu/server/server_base.h                   |  13 +-
 src/kudu/tserver/CMakeLists.txt                 |   1 +
 src/kudu/tserver/heartbeater.cc                 |  20 +++
 15 files changed, 511 insertions(+), 11 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/kudu/blob/1ee00311/src/kudu/integration-tests/registration-test.cc
----------------------------------------------------------------------
diff --git a/src/kudu/integration-tests/registration-test.cc b/src/kudu/integration-tests/registration-test.cc
index 12ea54a..52a0272 100644
--- a/src/kudu/integration-tests/registration-test.cc
+++ b/src/kudu/integration-tests/registration-test.cc
@@ -25,11 +25,12 @@
 #include "kudu/fs/fs_manager.h"
 #include "kudu/gutil/gscoped_ptr.h"
 #include "kudu/integration-tests/mini_cluster.h"
-#include "kudu/master/mini_master.h"
+#include "kudu/master/master-test-util.h"
 #include "kudu/master/master.h"
 #include "kudu/master/master.pb.h"
-#include "kudu/master/master-test-util.h"
+#include "kudu/master/mini_master.h"
 #include "kudu/master/ts_descriptor.h"
+#include "kudu/security/server_cert_manager.h"
 #include "kudu/tserver/mini_tablet_server.h"
 #include "kudu/tserver/tablet_server.h"
 #include "kudu/util/curl_util.h"
@@ -198,4 +199,13 @@ TEST_F(RegistrationTest, TestTabletReports) {
   // the TS, and verifies that the master notices the issue.
 }
 
+// Check that after the tablet server registers, it gets a signed cert
+// from the master.
+TEST_F(RegistrationTest, TestTSGetsSignedX509Certificate) {
+  MiniTabletServer* ts = cluster_->mini_tablet_server(0);
+  AssertEventually([&](){
+      ASSERT_TRUE(ts->server()->cert_manager()->has_signed_cert());
+    }, MonoDelta::FromSeconds(10));
+}
+
 } // namespace kudu

http://git-wip-us.apache.org/repos/asf/kudu/blob/1ee00311/src/kudu/master/CMakeLists.txt
----------------------------------------------------------------------
diff --git a/src/kudu/master/CMakeLists.txt b/src/kudu/master/CMakeLists.txt
index f97140d..3d3e18d 100644
--- a/src/kudu/master/CMakeLists.txt
+++ b/src/kudu/master/CMakeLists.txt
@@ -35,6 +35,7 @@ ADD_EXPORTABLE_LIBRARY(master_proto
 set(MASTER_SRCS
   catalog_manager.cc
   master.cc
+  master_cert_authority.cc
   master_options.cc
   master_service.cc
   master-path-handlers.cc
@@ -46,17 +47,18 @@ set(MASTER_SRCS
 
 add_library(master ${MASTER_SRCS})
 target_link_libraries(master
+  gutil
+  krpc
   kudu_common
-  tablet
+  kudu_util
+  master_proto
+  rpc_header_proto
+  security
   server_common
   server_process
-  krpc
-  gutil
-  kudu_util
+  tablet
   tserver
-  tserver_service_proto
-  master_proto
-  rpc_header_proto)
+  tserver_service_proto)
 
 set(MASTER_RPC_SRCS
   master_rpc.cc)

http://git-wip-us.apache.org/repos/asf/kudu/blob/1ee00311/src/kudu/master/master.cc
----------------------------------------------------------------------
diff --git a/src/kudu/master/master.cc b/src/kudu/master/master.cc
index 8d592ce..93873fc 100644
--- a/src/kudu/master/master.cc
+++ b/src/kudu/master/master.cc
@@ -28,6 +28,7 @@
 #include "kudu/common/wire_protocol.h"
 #include "kudu/gutil/strings/substitute.h"
 #include "kudu/master/catalog_manager.h"
+#include "kudu/master/master_cert_authority.h"
 #include "kudu/master/master_service.h"
 #include "kudu/master/master.proxy.h"
 #include "kudu/master/master-path-handlers.h"
@@ -94,6 +95,14 @@ Status Master::Init() {
 
   RETURN_NOT_OK(ServerBase::Init());
 
+  // TODO(PKI): the CA should not be initialized if built-in PKI is disabled
+  // by some flag.
+  cert_authority_.reset(new MasterCertAuthority(fs_manager_->uuid()));
+  // TODO(PKI): master should sign its own cert, but it has to be done
+  // after the catalog manager is loaded. Not clear the right place to
+  // put it, so punting on it at the moment.
+  RETURN_NOT_OK(cert_authority_->Init());
+
   RETURN_NOT_OK(path_handlers_->Register(web_server_.get()));
 
   state_ = kInitialized;

http://git-wip-us.apache.org/repos/asf/kudu/blob/1ee00311/src/kudu/master/master.h
----------------------------------------------------------------------
diff --git a/src/kudu/master/master.h b/src/kudu/master/master.h
index 753bea8..0c1fceb 100644
--- a/src/kudu/master/master.h
+++ b/src/kudu/master/master.h
@@ -47,8 +47,9 @@ class ServicePool;
 namespace master {
 
 class CatalogManager;
-class TSManager;
+class MasterCertAuthority;
 class MasterPathHandlers;
+class TSManager;
 
 class Master : public server::ServerBase {
  public:
@@ -74,6 +75,8 @@ class Master : public server::ServerBase {
 
   std::string ToString() const;
 
+  MasterCertAuthority* cert_authority() { return cert_authority_.get(); }
+
   TSManager* ts_manager() { return ts_manager_.get(); }
 
   CatalogManager* catalog_manager() { return catalog_manager_.get(); }
@@ -118,6 +121,7 @@ class Master : public server::ServerBase {
 
   MasterState state_;
 
+  std::unique_ptr<MasterCertAuthority> cert_authority_;
   gscoped_ptr<TSManager> ts_manager_;
   gscoped_ptr<CatalogManager> catalog_manager_;
   gscoped_ptr<MasterPathHandlers> path_handlers_;

http://git-wip-us.apache.org/repos/asf/kudu/blob/1ee00311/src/kudu/master/master.proto
----------------------------------------------------------------------
diff --git a/src/kudu/master/master.proto b/src/kudu/master/master.proto
index 25672ef..e17e73c 100644
--- a/src/kudu/master/master.proto
+++ b/src/kudu/master/master.proto
@@ -251,6 +251,10 @@ message TSHeartbeatRequestPB {
   // The number of tablets that are BOOTSTRAPPING or RUNNING.
   // Used by the master to determine load when creating new tablet replicas.
   optional int32 num_live_tablets = 4;
+
+  // If the tablet server needs its certificate signed, the CSR
+  // in DER format.
+  optional bytes csr_der = 5;
 }
 
 message TSHeartbeatResponsePB {
@@ -274,6 +278,10 @@ message TSHeartbeatResponsePB {
 
   // Specify whether or not the node is the leader master.
   optional bool leader_master = 6;
+
+  // If the heartbeat request had a CSR, then the successfully
+  // signed certificate will be returned in DER format.
+  optional bytes signed_cert_der = 7;
 }
 
 //////////////////////////////

http://git-wip-us.apache.org/repos/asf/kudu/blob/1ee00311/src/kudu/master/master_cert_authority.cc
----------------------------------------------------------------------
diff --git a/src/kudu/master/master_cert_authority.cc b/src/kudu/master/master_cert_authority.cc
new file mode 100644
index 0000000..54816eb
--- /dev/null
+++ b/src/kudu/master/master_cert_authority.cc
@@ -0,0 +1,128 @@
+// 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.
+
+#include "kudu/master/master_cert_authority.h"
+
+#include <gflags/gflags.h>
+#include <memory>
+#include <string>
+
+#include "kudu/security/ca/cert_management.h"
+#include "kudu/security/openssl_util.h"
+#include "kudu/util/flag_tags.h"
+
+using std::make_shared;
+using std::string;
+
+using kudu::security::Cert;
+using kudu::security::CertSignRequest;
+using kudu::security::Key;
+using kudu::security::ca::CaCertRequestGenerator;
+using kudu::security::ca::CertSigner;
+
+DEFINE_int32(master_ca_rsa_key_length_bits, 2048,
+             "The number of bits to use for the master's self-signed "
+             "certificate authority private key.");
+TAG_FLAG(master_ca_rsa_key_length_bits, experimental);
+
+namespace kudu {
+namespace master {
+
+namespace {
+
+CaCertRequestGenerator::Config PrepareCaConfig(const string& server_uuid) {
+  // TODO(aserbin): do we actually have to set all these fields given we
+  // aren't using a web browser to connect?
+  return {
+    "US",                     // country
+    "CA",                     // state
+    "San Francisco",          // locality
+    "ASF",                    // org
+    "The Kudu Project",       // unit
+    server_uuid,              // uuid
+    "Self-signed master CA",  // comment
+    {},                       // hostnames
+    {},                       // ips
+  };
+}
+
+} // anonymous namespace
+
+MasterCertAuthority::MasterCertAuthority(string server_uuid)
+    : server_uuid_(std::move(server_uuid)) {
+}
+
+MasterCertAuthority::~MasterCertAuthority() {
+}
+
+Status MasterCertAuthority::Init() {
+  CHECK(!ca_private_key_);
+
+  // Create a key for the self-signed CA.
+  auto key = make_shared<Key>();
+  RETURN_NOT_OK(GeneratePrivateKey(FLAGS_master_ca_rsa_key_length_bits,
+                                   key.get()));
+
+  // Generate a CSR for the CA.
+  CertSignRequest ca_csr;
+  {
+    CaCertRequestGenerator gen(PrepareCaConfig(server_uuid_));
+    RETURN_NOT_OK(gen.Init());
+    RETURN_NOT_OK(gen.GenerateRequest(*key, &ca_csr));
+  }
+
+  // Self-sign the CA's CSR.
+  auto ca_cert = make_shared<Cert>();
+  {
+    CertSigner ca_signer;
+    RETURN_NOT_OK(ca_signer.InitForSelfSigning(key));
+    RETURN_NOT_OK(ca_signer.Sign(ca_csr, ca_cert.get()));
+  }
+
+  // Initialize our signer with the new CA.
+  auto signer = make_shared<CertSigner>();
+  RETURN_NOT_OK(signer->Init(ca_cert, key));
+
+  cert_signer_ = std::move(signer);
+  ca_cert_ = std::move(ca_cert);
+  ca_private_key_ = std::move(key);
+  return Status::OK();
+}
+
+Status MasterCertAuthority::SignServerCSR(const string& csr_der, string* cert_der) {
+  CHECK(cert_signer_) << "not initialized";
+
+  // TODO(PKI): before signing, should we somehow verify the CSR's
+  // hostname/server_uuid matches what we think is the hostname? can the signer
+  // modify the CSR to add fields, etc, indicating when/where it was signed?
+  // maybe useful for debugging.
+
+  CertSignRequest csr;
+  RETURN_NOT_OK_PREPEND(csr.FromString(csr_der, security::DataFormat::DER),
+                        "could not parse CSR");
+  Cert cert;
+  RETURN_NOT_OK_PREPEND(cert_signer_->Sign(csr, &cert),
+                        "failed to sign cert");
+
+  RETURN_NOT_OK_PREPEND(cert.ToString(cert_der, security::DataFormat::DER),
+                        "failed to signed cert as DER format");
+  return Status::OK();
+}
+
+
+} // namespace master
+} // namespace kudu

http://git-wip-us.apache.org/repos/asf/kudu/blob/1ee00311/src/kudu/master/master_cert_authority.h
----------------------------------------------------------------------
diff --git a/src/kudu/master/master_cert_authority.h b/src/kudu/master/master_cert_authority.h
new file mode 100644
index 0000000..c3430c5
--- /dev/null
+++ b/src/kudu/master/master_cert_authority.h
@@ -0,0 +1,75 @@
+// 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.
+#pragma once
+
+#include "kudu/gutil/macros.h"
+#include "kudu/util/status.h"
+
+#include <memory>
+#include <string>
+
+namespace kudu {
+
+namespace security {
+class Cert;
+class Key;
+namespace ca {
+class CertSigner;
+} // namespace ca
+} // namespace security
+
+namespace master {
+
+// Implements the X509 certificate-authority functionality of the Master.
+//
+// This is used in the "built-in PKI" mode of operation. The master generates
+// its own self-signed CA certificate, and then signs CSRs provided by tablet
+// in their heartbeats.
+//
+// This class is thread-safe after initialization.
+class MasterCertAuthority {
+ public:
+  explicit MasterCertAuthority(std::string server_uuid);
+  virtual ~MasterCertAuthority();
+
+  // Initializes the MasterCertAuthority by generating a new private key
+  // and self-signed CA certificate.
+  //
+  // Must be called exactly once before any other functions.
+  Status Init();
+
+  // Sign the given CSR 'csr_der' provided by a server in the cluster.
+  //
+  // The CSR should be provided in the DER format.
+  // The resulting certificate, also in DER format, is returned in 'cert_der'.
+  //
+  // REQUIRES: Init() must be called first.
+  Status SignServerCSR(const std::string& csr_der, std::string* cert_der);
+
+ private:
+  // The UUID of the master. This is used as a field in the certificate.
+  const std::string server_uuid_;
+
+  std::shared_ptr<security::ca::CertSigner> cert_signer_;
+  std::shared_ptr<security::Key> ca_private_key_;
+  std::shared_ptr<security::Cert> ca_cert_;
+
+  DISALLOW_COPY_AND_ASSIGN(MasterCertAuthority);
+};
+
+} // namespace master
+} // namespace kudu

http://git-wip-us.apache.org/repos/asf/kudu/blob/1ee00311/src/kudu/master/master_service.cc
----------------------------------------------------------------------
diff --git a/src/kudu/master/master_service.cc b/src/kudu/master/master_service.cc
index 9a83ff5..c6ec327 100644
--- a/src/kudu/master/master_service.cc
+++ b/src/kudu/master/master_service.cc
@@ -25,6 +25,7 @@
 #include "kudu/common/wire_protocol.h"
 #include "kudu/master/catalog_manager.h"
 #include "kudu/master/master.h"
+#include "kudu/master/master_cert_authority.h"
 #include "kudu/master/ts_descriptor.h"
 #include "kudu/master/ts_manager.h"
 #include "kudu/rpc/rpc_context.h"
@@ -140,6 +141,21 @@ void MasterServiceImpl::TSHeartbeat(const TSHeartbeatRequestPB* req,
     }
   }
 
+  // 6. If the heartbeat has a CSR, sign their cert.
+  // TODO(PKI): should this be done only by leaders or all masters?
+  if (req->has_csr_der()) {
+    string cert;
+    Status s = server_->cert_authority()->SignServerCSR(req->csr_der(), &cert);
+    if (!s.ok()) {
+      rpc->RespondFailure(s.CloneAndPrepend("invalid CSR"));
+      return;
+    }
+    LOG(INFO) << "Signed X509 certificate for tserver " << rpc->requestor_string();
+    resp->mutable_signed_cert_der()->swap(cert);
+  }
+
+  // 7. Send any active CA certs which the TS doesn't have.
+
   rpc->RespondSuccess();
 }
 

http://git-wip-us.apache.org/repos/asf/kudu/blob/1ee00311/src/kudu/security/CMakeLists.txt
----------------------------------------------------------------------
diff --git a/src/kudu/security/CMakeLists.txt b/src/kudu/security/CMakeLists.txt
index d49f6eb..be2e320 100644
--- a/src/kudu/security/CMakeLists.txt
+++ b/src/kudu/security/CMakeLists.txt
@@ -34,6 +34,7 @@ set(SECURITY_SRCS
   init.cc
   openssl_util.cc
   ${PORTED_X509_CHECK_HOST_CC}
+  server_cert_manager.cc
   tls_context.cc
   tls_handshake.cc
   tls_socket.cc)

http://git-wip-us.apache.org/repos/asf/kudu/blob/1ee00311/src/kudu/security/server_cert_manager.cc
----------------------------------------------------------------------
diff --git a/src/kudu/security/server_cert_manager.cc b/src/kudu/security/server_cert_manager.cc
new file mode 100644
index 0000000..650ae47
--- /dev/null
+++ b/src/kudu/security/server_cert_manager.cc
@@ -0,0 +1,115 @@
+// 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.
+
+#include "kudu/security/server_cert_manager.h"
+
+#include <memory>
+#include <string>
+
+#include <boost/optional/optional.hpp>
+#include <gflags/gflags.h>
+
+#include "kudu/util/flag_tags.h"
+#include "kudu/security/ca/cert_management.h"
+#include "kudu/security/openssl_util.h"
+
+using std::string;
+using std::unique_ptr;
+
+DEFINE_int32(server_rsa_key_length_bits, 2048,
+             "The number of bits to use for the server's private RSA key. This is used "
+             "for TLS connections to and from clients and other servers.");
+TAG_FLAG(server_rsa_key_length_bits, experimental);
+
+namespace kudu {
+namespace security {
+
+using ca::CertRequestGenerator;
+
+ServerCertManager::ServerCertManager(string server_uuid)
+    : server_uuid_(std::move(server_uuid)) {
+}
+
+ServerCertManager::~ServerCertManager() {
+}
+
+Status ServerCertManager::Init() {
+  MutexLock lock(lock_);
+  CHECK(!key_);
+
+  unique_ptr<Key> key(new Key());
+  RETURN_NOT_OK_PREPEND(GeneratePrivateKey(FLAGS_server_rsa_key_length_bits, key.get()),
+                        "could not generate private key");
+
+  // TODO(aserbin): do these fields actually have to be set?
+  const CertRequestGenerator::Config config = {
+    "US",               // country
+    "CA",               // state
+    "San Francisco",    // locality
+    "ASF",              // org
+    "The Kudu Project", // unit
+    server_uuid_,       // uuid
+    "",                 // comment
+    {"localhost"},      // hostnames TODO(PKI): use real hostnames
+    {"127.0.0.1"},      // ips
+  };
+
+  CertRequestGenerator gen(config);
+  CertSignRequest csr;
+  RETURN_NOT_OK_PREPEND(gen.Init(), "could not initialize CSR generator");
+  RETURN_NOT_OK_PREPEND(gen.GenerateRequest(*key, &csr), "could not generate CSR");
+  RETURN_NOT_OK_PREPEND(csr.ToString(&csr_der_, DataFormat::DER),
+                        "unable to output DER format CSR");
+  key_.swap(key);
+  return Status::OK();
+}
+
+
+Status ServerCertManager::AdoptSignedCert(const string& cert_der) {
+  MutexLock lock(lock_);
+  // If we already have a cert, then no need to adopt a new one.
+  // We heartbeat to multiple masters in parallel, and so we might
+  // get multiple valid signed certs from different masters.
+  if (signed_cert_) {
+    return Status::OK();
+  }
+  unique_ptr<Cert> new_cert(new Cert());
+  RETURN_NOT_OK_PREPEND(new_cert->FromString(cert_der, DataFormat::DER),
+                        "could not parse DER data");
+
+  LOG(INFO) << "Adopting new signed X509 certificate";
+  signed_cert_ = std::move(new_cert);
+  // No longer need to get a signed cert, so we can forget our CSR.
+  csr_der_.clear();
+  return Status::OK();
+}
+
+// Return a new CSR, if this tablet server's cert has not yet been
+// signed. If the cert is already signed, returns null.
+boost::optional<string> ServerCertManager::GetCSRIfNecessary() const {
+  MutexLock lock(lock_);
+  if (signed_cert_) return boost::none;
+  return csr_der_;
+}
+
+bool ServerCertManager::has_signed_cert() const {
+  MutexLock lock(lock_);
+  return signed_cert_ != nullptr;
+}
+
+} // namespace security
+} // namespace kudu

http://git-wip-us.apache.org/repos/asf/kudu/blob/1ee00311/src/kudu/security/server_cert_manager.h
----------------------------------------------------------------------
diff --git a/src/kudu/security/server_cert_manager.h b/src/kudu/security/server_cert_manager.h
new file mode 100644
index 0000000..916bd2d
--- /dev/null
+++ b/src/kudu/security/server_cert_manager.h
@@ -0,0 +1,94 @@
+// 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.
+#pragma once
+
+#include <memory>
+#include <string>
+
+#include <boost/optional/optional_fwd.hpp>
+
+#include "kudu/gutil/macros.h"
+#include "kudu/util/mutex.h"
+#include "kudu/util/status.h"
+
+namespace kudu {
+namespace security {
+
+class Key;
+class Cert;
+class CertSignRequest;
+
+// Manages the X509 certificate used by a server when the built-in
+// PKI infrastructure is enabled.
+//
+// This generates an X509 certificate for the server, provides a CSR which can be
+// transferred to the master, and then stores the signed certificate once provided.
+//
+// Note that the master, in addition to acting as a CA, also needs its own cert
+// (signed by the CA cert) which it uses for its RPC server. This handles the latter.
+//
+// This class is thread-safe after initialization.
+class ServerCertManager {
+ public:
+  // Construct a ServertCertManager.
+  explicit ServerCertManager(std::string server_uuid);
+  ~ServerCertManager();
+
+  // Generate a private key and CSR.
+  //
+  // This must be called exactly once before any methods below.
+  Status Init();
+
+  // Return a new CSR in DER format, if this server's cert has not yet been
+  // signed. If the cert is already signed, returns boost::none;
+  boost::optional<std::string> GetCSRIfNecessary() const;
+
+  // Adopt the given signed cert (provided in DER format) as the cert for
+  // this server.
+  //
+  // This has no effect if the instance already has a signed cert.
+  Status AdoptSignedCert(const std::string& cert_der);
+
+  // TODO(PKI): some code to register a callback when the cert is adopted,
+  // so that it can set it on the SSLFactory?
+
+  // Return true if we currently have a signed certificate.
+  // TODO(PKI): should this check validity?
+  bool has_signed_cert() const;
+
+ private:
+  const std::string server_uuid_;
+
+  // Protects the below variables.
+  mutable Mutex lock_;
+
+  // The CSR for this server. If the cert has already been
+  // signed, set to empty.
+  std::string csr_der_;
+
+  // The signed cert for this server. Only set once the cert
+  // has been signed.
+  std::unique_ptr<Cert> signed_cert_;
+
+  // The keypair associated with this server's cert.
+  std::unique_ptr<Key> key_;
+
+  DISALLOW_COPY_AND_ASSIGN(ServerCertManager);
+};
+
+} // namespace security
+} // namespace kudu

http://git-wip-us.apache.org/repos/asf/kudu/blob/1ee00311/src/kudu/server/server_base.cc
----------------------------------------------------------------------
diff --git a/src/kudu/server/server_base.cc b/src/kudu/server/server_base.cc
index 2831855..088250f 100644
--- a/src/kudu/server/server_base.cc
+++ b/src/kudu/server/server_base.cc
@@ -32,6 +32,7 @@
 #include "kudu/gutil/walltime.h"
 #include "kudu/rpc/messenger.h"
 #include "kudu/security/init.h"
+#include "kudu/security/server_cert_manager.h"
 #include "kudu/server/default-path-handlers.h"
 #include "kudu/server/generic_service.h"
 #include "kudu/server/glog_metrics.h"
@@ -185,6 +186,11 @@ Status ServerBase::Init() {
   }
   RETURN_NOT_OK_PREPEND(s, "Failed to load FS layout");
 
+  // Initialize the cert manager.
+  // TODO(PKI): make built-in PKI enabled/disabled based on a flag.
+  cert_manager_.reset(new security::ServerCertManager(fs_manager_->uuid()));
+  RETURN_NOT_OK(cert_manager_->Init());
+
   // Create the Messenger.
   rpc::MessengerBuilder builder(name_);
 

http://git-wip-us.apache.org/repos/asf/kudu/blob/1ee00311/src/kudu/server/server_base.h
----------------------------------------------------------------------
diff --git a/src/kudu/server/server_base.h b/src/kudu/server/server_base.h
index 2ad6978..57cadd2 100644
--- a/src/kudu/server/server_base.h
+++ b/src/kudu/server/server_base.h
@@ -46,6 +46,10 @@ class Messenger;
 class ServiceIf;
 } // namespace rpc
 
+namespace security {
+class ServerCertManager;
+} // namespace security
+
 namespace server {
 class Clock;
 
@@ -71,8 +75,10 @@ class ServerBase {
 
   FsManager* fs_manager() { return fs_manager_.get(); }
 
+  security::ServerCertManager* cert_manager() { return cert_manager_.get(); }
+
   // Return the instance identifier of this server.
-  // This may not be called until after the server is Initted.
+  // This may not be called until after the server is Started.
   const NodeInstancePB& instance_pb() const;
 
   const std::shared_ptr<MemTracker>& mem_tracker() const { return mem_tracker_; }
@@ -107,6 +113,11 @@ class ServerBase {
   gscoped_ptr<FsManager> fs_manager_;
   gscoped_ptr<RpcServer> rpc_server_;
   gscoped_ptr<Webserver> web_server_;
+
+  // Manager for the SSL certificate associated with this server, if built-in
+  // PKI is enabled.
+  std::unique_ptr<security::ServerCertManager> cert_manager_;
+
   std::shared_ptr<rpc::Messenger> messenger_;
   scoped_refptr<rpc::ResultTracker> result_tracker_;
   bool is_first_run_;

http://git-wip-us.apache.org/repos/asf/kudu/blob/1ee00311/src/kudu/tserver/CMakeLists.txt
----------------------------------------------------------------------
diff --git a/src/kudu/tserver/CMakeLists.txt b/src/kudu/tserver/CMakeLists.txt
index 7cb4655..10b88ce 100644
--- a/src/kudu/tserver/CMakeLists.txt
+++ b/src/kudu/tserver/CMakeLists.txt
@@ -131,6 +131,7 @@ target_link_libraries(tserver
   log
   consensus
   krpc
+  security
   server_common
   server_process
   tablet)

http://git-wip-us.apache.org/repos/asf/kudu/blob/1ee00311/src/kudu/tserver/heartbeater.cc
----------------------------------------------------------------------
diff --git a/src/kudu/tserver/heartbeater.cc b/src/kudu/tserver/heartbeater.cc
index e8aca16..5edd83d 100644
--- a/src/kudu/tserver/heartbeater.cc
+++ b/src/kudu/tserver/heartbeater.cc
@@ -17,6 +17,7 @@
 
 #include "kudu/tserver/heartbeater.h"
 
+#include <boost/optional.hpp>
 #include <gflags/gflags.h>
 #include <glog/logging.h>
 #include <memory>
@@ -29,6 +30,7 @@
 #include "kudu/master/master.h"
 #include "kudu/master/master_rpc.h"
 #include "kudu/master/master.proxy.h"
+#include "kudu/security/server_cert_manager.h"
 #include "kudu/server/webserver.h"
 #include "kudu/tserver/tablet_server.h"
 #include "kudu/tserver/tablet_server_options.h"
@@ -362,6 +364,17 @@ Status Heartbeater::Thread::DoHeartbeat() {
                           "Unable to set up registration");
   }
 
+  // Check with the TS cert manager if it has a cert that needs signing.
+  // if so, send the CSR in the heartbeat for the master to sign.
+  boost::optional<string> csr = server_->cert_manager()->GetCSRIfNecessary();
+  if (csr != boost::none) {
+    req.mutable_csr_der()->swap(*csr);
+    VLOG(1) << "Sending a CSR to the master in the next heartbeat";
+  }
+
+  // TODO(PKI): send the version number of the latest CA cert which we know about.
+  // The response should include new CA certs.
+
   if (send_full_tablet_report_) {
     LOG(INFO) << Substitute(
         "Master $0 was elected leader, sending a full tablet report...",
@@ -406,6 +419,13 @@ Status Heartbeater::Thread::DoHeartbeat() {
 
   last_hb_response_.Swap(&resp);
 
+  // If we have a new signed certificate from the master, adopt it.
+  if (last_hb_response_.has_signed_cert_der()) {
+    RETURN_NOT_OK_PREPEND(
+        server_->cert_manager()->AdoptSignedCert(last_hb_response_.signed_cert_der()),
+        "could not adopt master-signed X509 cert");
+  }
+
   MarkTabletReportAcknowledged(req.tablet_report());
   return Status::OK();
 }


[3/3] kudu git commit: security: initial work on token creation and verification

Posted by to...@apache.org.
security: initial work on token creation and verification

This adds classes for managing Token Signing Keys (TSKs) on the
signer (masters) and verifiers (all servers). The new classes aren't
hooked up with the actual servers as of yet, nor do they actually use
real signature routines (blocked on another in-flight patch for that).

A unit test is included which drives the APIs using a stubbed-out
"signature" algorithm.

Change-Id: Iaf53ae50082d69028315952ac0732af6a83ffdbe
Reviewed-on: http://gerrit.cloudera.org:8080/5796
Reviewed-by: Dan Burkert <da...@apache.org>
Tested-by: Kudu Jenkins


Project: http://git-wip-us.apache.org/repos/asf/kudu/repo
Commit: http://git-wip-us.apache.org/repos/asf/kudu/commit/7b01d813
Tree: http://git-wip-us.apache.org/repos/asf/kudu/tree/7b01d813
Diff: http://git-wip-us.apache.org/repos/asf/kudu/diff/7b01d813

Branch: refs/heads/master
Commit: 7b01d81325e77447325315f5ebd166f52e6d8bc8
Parents: 1ee0031
Author: Todd Lipcon <to...@apache.org>
Authored: Wed Jan 25 14:59:18 2017 -0800
Committer: Todd Lipcon <to...@apache.org>
Committed: Thu Jan 26 21:44:43 2017 +0000

----------------------------------------------------------------------
 src/kudu/security/CMakeLists.txt       |  41 +++++-
 src/kudu/security/token-test.cc        | 208 ++++++++++++++++++++++++++++
 src/kudu/security/token.proto          |  82 +++++++++++
 src/kudu/security/token_signer.cc      | 100 +++++++++++++
 src/kudu/security/token_signer.h       | 124 +++++++++++++++++
 src/kudu/security/token_signing_key.cc |  68 +++++++++
 src/kudu/security/token_signing_key.h  |  77 ++++++++++
 src/kudu/security/token_verifier.cc    | 120 ++++++++++++++++
 src/kudu/security/token_verifier.h     | 106 ++++++++++++++
 9 files changed, 924 insertions(+), 2 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/kudu/blob/7b01d813/src/kudu/security/CMakeLists.txt
----------------------------------------------------------------------
diff --git a/src/kudu/security/CMakeLists.txt b/src/kudu/security/CMakeLists.txt
index be2e320..b3ec6a7 100644
--- a/src/kudu/security/CMakeLists.txt
+++ b/src/kudu/security/CMakeLists.txt
@@ -18,12 +18,37 @@
 # See the comment in krb5_realm_override.cc for details on this library's usage.
 # The top-level CMakeLists sets a ${KRB5_REALM_OVERRIDE} variable which should
 # be linked first into all Kudu binaries.
+
+##############################
+# krb5_realm_override
+##############################
+
 add_library(krb5_realm_override STATIC krb5_realm_override.cc)
 target_link_libraries(krb5_realm_override glog)
 if(NOT APPLE)
   target_link_libraries(krb5_realm_override dl)
 endif()
 
+##############################
+# token_proto
+##############################
+
+KRPC_GENERATE(
+  TOKEN_PROTO_SRCS TOKEN_PROTO_HDRS TOKEN_PROTO_TGTS
+  SOURCE_ROOT ${CMAKE_CURRENT_SOURCE_DIR}/../..
+  BINARY_ROOT ${CMAKE_CURRENT_BINARY_DIR}/../..
+  PROTO_FILES token.proto)
+set(TOKEN_PROTO_LIBS protobuf)
+ADD_EXPORTABLE_LIBRARY(token_proto
+  SRCS ${TOKEN_PROTO_SRCS}
+  DEPS ${TOKEN_PROTO_LIBS}
+  NONLINK_DEPS ${TOKEN_PROTO_TGTS})
+
+
+##############################
+# security
+##############################
+
 # Fall back to using the ported functionality if we're using an older version of OpenSSL.
 if (${OPENSSL_VERSION} VERSION_LESS "1.0.2")
   set(PORTED_X509_CHECK_HOST_CC "x509_check_host.cc")
@@ -37,12 +62,18 @@ set(SECURITY_SRCS
   server_cert_manager.cc
   tls_context.cc
   tls_handshake.cc
-  tls_socket.cc)
+  tls_socket.cc
+  token_verifier.cc
+  token_signer.cc
+  token_signing_key.cc
+  )
 
 set(SECURITY_LIBS
   gutil
-  krb5
   kudu_util
+  token_proto
+
+  krb5
   openssl_crypto
   openssl_ssl)
 
@@ -50,6 +81,11 @@ ADD_EXPORTABLE_LIBRARY(security
   SRCS ${SECURITY_SRCS}
   DEPS ${SECURITY_LIBS})
 
+
+##############################
+# security-test
+##############################
+
 if (NOT NO_TESTS)
   set(SECURITY_TEST_SRCS
     test/mini_kdc.cc
@@ -70,4 +106,5 @@ if (NOT NO_TESTS)
   ADD_KUDU_TEST(test/cert_management-test)
   ADD_KUDU_TEST(test/mini_kdc-test)
   ADD_KUDU_TEST(tls_handshake-test)
+  ADD_KUDU_TEST(token-test)
 endif()

http://git-wip-us.apache.org/repos/asf/kudu/blob/7b01d813/src/kudu/security/token-test.cc
----------------------------------------------------------------------
diff --git a/src/kudu/security/token-test.cc b/src/kudu/security/token-test.cc
new file mode 100644
index 0000000..d99cc85
--- /dev/null
+++ b/src/kudu/security/token-test.cc
@@ -0,0 +1,208 @@
+// 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.
+
+#include "kudu/util/test_util.h"
+
+#include "kudu/gutil/walltime.h"
+#include "kudu/security/token.pb.h"
+#include "kudu/security/token_signer.h"
+#include "kudu/security/token_verifier.h"
+
+DECLARE_int32(token_signing_key_num_rsa_bits);
+DECLARE_int64(token_signing_key_validity_seconds);
+
+
+namespace kudu {
+namespace security {
+
+namespace {
+
+SignedTokenPB MakeUnsignedToken(int64_t expiration) {
+  SignedTokenPB ret;
+  TokenPB token;
+  token.set_expire_unix_epoch_seconds(expiration);
+  CHECK(token.SerializeToString(ret.mutable_token_data()));
+  return ret;
+}
+
+SignedTokenPB MakeIncompatibleToken() {
+  SignedTokenPB ret;
+  TokenPB token;
+  token.set_expire_unix_epoch_seconds(WallTime_Now() + 100);
+  token.add_incompatible_features(TokenPB::Feature_MAX + 1);
+  CHECK(token.SerializeToString(ret.mutable_token_data()));
+  return ret;
+}
+
+} // anonymous namespace
+
+class TokenTest : public KuduTest {
+  void SetUp() override {
+    KuduTest::SetUp();
+    // Set the keylength smaller to make tests run faster.
+    FLAGS_token_signing_key_num_rsa_bits = 512;
+  }
+};
+
+TEST_F(TokenTest, TestSigner) {
+  SignedTokenPB token = MakeUnsignedToken(WallTime_Now());
+
+  const int kStartingSeqNum = 123;
+  TokenSigner signer(kStartingSeqNum);
+
+  // Should start off with no signing keys.
+  ASSERT_TRUE(signer.GetTokenSigningPublicKeys(0).empty());
+
+  // Trying to sign a token when there is no TSK should give
+  // an error.
+  Status s = signer.SignToken(&token);
+  ASSERT_TRUE(s.IsIllegalState());
+
+  // Rotate the TSK once.
+  ASSERT_OK(signer.RotateSigningKey());
+
+  // We should see the key now if we request TSKs starting at a
+  // lower sequence number.
+  ASSERT_EQ(signer.GetTokenSigningPublicKeys(0).size(), 1);
+  // We should not see the key if we ask for the sequence number
+  // that it is assigned.
+  ASSERT_EQ(signer.GetTokenSigningPublicKeys(kStartingSeqNum).size(), 0);
+
+  // We should be able to sign a token, even though we have
+  // just one key.
+  ASSERT_OK(signer.SignToken(&token));
+  ASSERT_TRUE(token.has_signature());
+  ASSERT_EQ(token.signing_key_seq_num(), kStartingSeqNum);
+
+  // Rotate again and check that we return the right keys.
+  ASSERT_OK(signer.RotateSigningKey());
+  ASSERT_EQ(signer.GetTokenSigningPublicKeys(0).size(), 2);
+  ASSERT_EQ(signer.GetTokenSigningPublicKeys(kStartingSeqNum).size(), 1);
+  ASSERT_EQ(signer.GetTokenSigningPublicKeys(kStartingSeqNum + 1).size(), 0);
+
+  // We still use the original key for signing (we always use the second-to-latest
+  // key).
+  token = MakeUnsignedToken(WallTime_Now());
+  ASSERT_OK(signer.SignToken(&token));
+  ASSERT_TRUE(token.has_signature());
+  ASSERT_EQ(token.signing_key_seq_num(), kStartingSeqNum);
+
+  // If we rotate one more time, we should start using the second key.
+  ASSERT_OK(signer.RotateSigningKey());
+  token = MakeUnsignedToken(WallTime_Now());
+  ASSERT_OK(signer.SignToken(&token));
+  ASSERT_TRUE(token.has_signature());
+  ASSERT_EQ(token.signing_key_seq_num(), kStartingSeqNum + 1);
+}
+
+// Test that the TokenSigner can export its public keys in protobuf form.
+TEST_F(TokenTest, TestExportKeys) {
+  // Test that the exported public keys don't contain private key material,
+  // and have an appropriate expiration.
+  TokenSigner signer(1);
+  ASSERT_OK(signer.RotateSigningKey());
+  auto keys = signer.GetTokenSigningPublicKeys(0);
+  ASSERT_EQ(keys.size(), 1);
+  const TokenSigningPublicKeyPB& key = keys[0];
+  ASSERT_TRUE(key.has_rsa_key_der());
+  ASSERT_EQ(key.key_seq_num(), 1);
+  ASSERT_TRUE(key.has_expire_unix_epoch_seconds());
+  ASSERT_GT(key.expire_unix_epoch_seconds(), WallTime_Now());
+}
+
+// Test that the TokenVerifier can import keys exported by the TokenSigner
+// and then verify tokens signed by it.
+TEST_F(TokenTest, TestEndToEnd_Valid) {
+  TokenSigner signer(1);
+  ASSERT_OK(signer.RotateSigningKey());
+
+  // Make and sign a token.
+  SignedTokenPB token = MakeUnsignedToken(WallTime_Now() + 600);
+  ASSERT_OK(signer.SignToken(&token));
+
+  // Try to verify it.
+  TokenVerifier verifier;
+  verifier.ImportPublicKeys(signer.GetTokenSigningPublicKeys(0));
+  ASSERT_EQ(VerificationResult::VALID, verifier.VerifyTokenSignature(token));
+}
+
+// Test all of the possible cases covered by token verification.
+// See VerificationResult.
+TEST_F(TokenTest, TestEndToEnd_InvalidCases) {
+  TokenSigner signer(1);
+  ASSERT_OK(signer.RotateSigningKey());
+
+  TokenVerifier verifier;
+  verifier.ImportPublicKeys(signer.GetTokenSigningPublicKeys(0));
+
+  // Make and sign a token, but corrupt the data in it.
+  {
+    SignedTokenPB token = MakeUnsignedToken(WallTime_Now() + 600);
+    ASSERT_OK(signer.SignToken(&token));
+    token.set_token_data("xyz");
+    ASSERT_EQ(VerificationResult::INVALID_TOKEN, verifier.VerifyTokenSignature(token));
+  }
+
+  // Make and sign a token, but corrupt the signature.
+  {
+    SignedTokenPB token = MakeUnsignedToken(WallTime_Now() + 600);
+    ASSERT_OK(signer.SignToken(&token));
+    token.set_signature("xyz");
+    ASSERT_EQ(VerificationResult::INVALID_SIGNATURE, verifier.VerifyTokenSignature(token));
+  }
+
+  // Make and sign a token, but set it to be already expired.
+  {
+    SignedTokenPB token = MakeUnsignedToken(WallTime_Now() - 10);
+    ASSERT_OK(signer.SignToken(&token));
+    ASSERT_EQ(VerificationResult::EXPIRED_TOKEN, verifier.VerifyTokenSignature(token));
+  }
+
+  // Make and sign a token which uses an incompatible feature flag.
+  {
+    SignedTokenPB token = MakeIncompatibleToken();
+    ASSERT_OK(signer.SignToken(&token));
+    ASSERT_EQ(VerificationResult::INCOMPATIBLE_FEATURE, verifier.VerifyTokenSignature(token));
+  }
+
+  // Rotate to a new key, but don't inform the verifier of it yet. When we
+  // verify, we expect the verifier to complain the key is unknown.
+  ASSERT_OK(signer.RotateSigningKey());
+  ASSERT_OK(signer.RotateSigningKey());
+  {
+    SignedTokenPB token = MakeUnsignedToken(WallTime_Now() + 600);
+    ASSERT_OK(signer.SignToken(&token));
+    ASSERT_EQ(VerificationResult::UNKNOWN_SIGNING_KEY, verifier.VerifyTokenSignature(token));
+  }
+
+  // Rotate to a signing key which is already expired, and inform the verifier
+  // of all of the current keys. The verifier should recognize the key but
+  // know that it's expired.
+  FLAGS_token_signing_key_validity_seconds = -10;
+  ASSERT_OK(signer.RotateSigningKey());
+  ASSERT_OK(signer.RotateSigningKey());
+  verifier.ImportPublicKeys(signer.GetTokenSigningPublicKeys(
+      verifier.GetMaxKnownKeySequenceNumber()));
+  {
+    SignedTokenPB token = MakeUnsignedToken(WallTime_Now() + 600);
+    ASSERT_OK(signer.SignToken(&token));
+    ASSERT_EQ(VerificationResult::EXPIRED_SIGNING_KEY, verifier.VerifyTokenSignature(token));
+  }
+}
+
+} // namespace security
+} // namespace kudu

http://git-wip-us.apache.org/repos/asf/kudu/blob/7b01d813/src/kudu/security/token.proto
----------------------------------------------------------------------
diff --git a/src/kudu/security/token.proto b/src/kudu/security/token.proto
new file mode 100644
index 0000000..79e5490
--- /dev/null
+++ b/src/kudu/security/token.proto
@@ -0,0 +1,82 @@
+// 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 kudu.security;
+
+option java_package = "org.apache.kudu.security";
+
+message AuthnTokenPB {
+};
+
+message AuthzTokenPB {
+};
+
+message TokenPB {
+  // The time at which this token expires, in seconds since the
+  // unix epoch.
+  optional int64 expire_unix_epoch_seconds = 1;
+
+  enum Feature {
+    // Protobuf doesn't let us define a enum with no values,
+    // so we've got this placeholder in here for now. When we add
+    // the first real feature flag, we can remove this.
+    UNUSED_PLACEHOLDER = 999;
+  };
+
+  // List of incompatible features used by this token. If a feature
+  // is listed in the token and a server verifying/authorizing the token
+  // sees an UNKNOWN value in this list, it should reject the token.
+  //
+  // This allows us to safely add "restrictive" content to tokens
+  // and have a "default deny" policy on servers that may not understand
+  // them.
+  //
+  // We use an int32 here but the values correspond to the 'Feature' enum
+  // above. This is to deal with protobuf's odd handling of unknown enum
+  // values (see KUDU-1850).
+  repeated int32 incompatible_features = 2;
+
+  oneof token {
+    AuthnTokenPB authn = 3;
+    AuthzTokenPB authz = 4;
+  }
+};
+
+message SignedTokenPB {
+  // The actual token data. This is a serialized TokenPB protobuf. However, we use a
+  // 'bytes' field, since protobuf doesn't guarantee that if two implementations serialize
+  // a protobuf, they'll necessary get bytewise identical results, particularly in the
+  // presence of unknown fields.
+  optional bytes token_data = 1;
+
+  // The cryptographic signature of 'token_contents'.
+  optional bytes signature = 2;
+
+  // The sequence number of the key which produced 'signature'.
+  optional int64 signing_key_seq_num = 3;
+};
+
+// A public key corresponding to the private key used to sign tokens.
+message TokenSigningPublicKeyPB {
+  optional int64 key_seq_num = 1;
+
+  // The public key material, in DER format.
+  optional bytes rsa_key_der = 2;
+
+  // The time at which signatures made by this key should no longer
+  // be valid.
+  optional int64 expire_unix_epoch_seconds = 3;
+};
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/kudu/blob/7b01d813/src/kudu/security/token_signer.cc
----------------------------------------------------------------------
diff --git a/src/kudu/security/token_signer.cc b/src/kudu/security/token_signer.cc
new file mode 100644
index 0000000..e40c86b
--- /dev/null
+++ b/src/kudu/security/token_signer.cc
@@ -0,0 +1,100 @@
+// 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.
+
+#include "kudu/security/token_signer.h"
+
+#include <map>
+#include <memory>
+#include <string>
+
+#include <gflags/gflags.h>
+
+#include "kudu/gutil/walltime.h"
+#include "kudu/security/openssl_util.h"
+#include "kudu/security/token.pb.h"
+#include "kudu/security/token_signing_key.h"
+#include "kudu/util/flag_tags.h"
+#include "kudu/util/locks.h"
+
+DEFINE_int32(token_signing_key_num_rsa_bits, 2048,
+             "number of bits used for token signing keys");
+// TODO(PKI) is 1024 enough for TSKs since they rotate frequently?
+// maybe it would verify faster?
+DEFINE_int64(token_signing_key_validity_seconds, 60 * 60 * 24 * 7,
+             "number of seconds that a token signing key is valid for");
+// TODO(PKI): add flag tags
+
+using std::lock_guard;
+using std::string;
+using std::unique_ptr;
+
+namespace kudu {
+namespace security {
+
+TokenSigner::TokenSigner(int64_t next_seq_num)
+    : next_seq_num_(next_seq_num) {
+}
+
+TokenSigner::~TokenSigner() {
+}
+
+Status TokenSigner::RotateSigningKey() {
+  Key key;
+  RETURN_NOT_OK_PREPEND(GeneratePrivateKey(FLAGS_token_signing_key_num_rsa_bits, &key),
+                        "could not generate new RSA token-signing key");
+  int64_t expire = WallTime_Now() + FLAGS_token_signing_key_validity_seconds;
+  lock_guard<RWMutex> l(lock_);
+  int64_t seq = next_seq_num_++;
+  unique_ptr<TokenSigningPrivateKey> new_tsk(
+      new TokenSigningPrivateKey(seq, expire, std::move(key)));
+  keys_by_seq_[seq] = std::move(new_tsk);
+  return Status::OK();
+}
+
+Status TokenSigner::SignToken(SignedTokenPB* token) const {
+  CHECK(token);
+  shared_lock<RWMutex> l(lock_);
+  if (keys_by_seq_.empty()) {
+    return Status::IllegalState("must generate a key before signing");
+  }
+  // If there is more than one key available, we use the second-latest key,
+  // since the latest one may not have yet propagated to other servers, etc.
+  auto it = keys_by_seq_.end();
+  --it;
+  if (it != keys_by_seq_.begin()) {
+    --it;
+  }
+  const auto& tsk = it->second;
+  return tsk->Sign(token);
+}
+
+std::vector<TokenSigningPublicKeyPB> TokenSigner::GetTokenSigningPublicKeys(
+    int64_t after_sequence_number) const {
+  vector<TokenSigningPublicKeyPB> ret;
+  shared_lock<RWMutex> l(lock_);
+  for (auto it = keys_by_seq_.upper_bound(after_sequence_number);
+       it != keys_by_seq_.end();
+       ++it) {
+    ret.emplace_back();
+    it->second->ExportPublicKeyPB(&ret.back());
+  }
+  return ret;
+}
+
+
+} // namespace security
+} // namespace kudu

http://git-wip-us.apache.org/repos/asf/kudu/blob/7b01d813/src/kudu/security/token_signer.h
----------------------------------------------------------------------
diff --git a/src/kudu/security/token_signer.h b/src/kudu/security/token_signer.h
new file mode 100644
index 0000000..2824be0
--- /dev/null
+++ b/src/kudu/security/token_signer.h
@@ -0,0 +1,124 @@
+// 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.
+#pragma once
+
+#include "kudu/gutil/macros.h"
+
+#include <map>
+#include <memory>
+#include <vector>
+
+#include "kudu/util/rw_mutex.h"
+
+namespace kudu {
+namespace security {
+
+class SignedTokenPB;
+class TokenSigningPrivateKey;
+class TokenSigningPublicKeyPB;
+
+// Class responsible for managing Token Signing Keys (TSKs) and signing tokens.
+//
+// This class manages a set of private TSKs, each identified by a sequence number.
+// Callers can export their public TSK counterparts, optionally transfer them
+// to another node, and then import them into a TokenVerifier.
+//
+// The class provides the ability to rotate the current TSK. This generates a new
+// key pair and assigns it a sequence number. Note that, when signing tokens,
+// the most recent key is not used. Rather, the second-most-recent key is used.
+// This ensures that there is plenty of time to transmit the public key for the
+// new TSK to all TokenVerifiers (eg on other servers, via heartbeats), before
+// the new key enters usage.
+//
+// On a fresh instance, with only one key, there is no "second most recent"
+// key. Thus, we fall back to signing tokens with the one available key.
+//
+//
+// Key rotation schedules and validity periods
+// ===========================================
+// The TokenSigner does not automatically handle the rotation of keys.
+// Rotation must be performed by the caller using the 'RotateSigningKey()'
+// method. Typically, key rotation is performed much more frequently than
+// the validity period of the key, so that at any given point in time
+// there are several valid keys.
+//
+// For example, consider a validity period of 4 days and a rotation interval of
+// 1 day:
+//
+// Day      1    2    3    4    5    6    7    8
+// ------------------------------------------------
+// Key 1:   <AAAAAAAAA==========>
+// Key 2:        <====AAAAA=========>
+// Key 3:             <====AAAAA========>
+// Key 4:                  <====AAAAA==========>
+//                               .............
+// 'A' indicates the 'Originator Usage Period' (the period in which the key
+// is being used to sign tokens).
+//
+// '<...>' indicates the 'Recipient Usage Period' (the period in which the
+// verifier will consider the key valid).
+//
+// When configuring the rotation and validity, consider the following constraint:
+//
+//   max_token_validity < tsk_validity_period - 2 * tsk_rotation_interval
+//
+// In the example above, this means that no token may be issued with a validity
+// longer than 2 days, without risking that the signing key would expire before
+// the token.
+//
+// TODO(PKI): should we try to enforce this constraint in code?
+//
+// NOTE: one other result of the above is that the first key (Key 1) is actually
+// active for longer than the rest. This has some potential security implications,
+// so it's worth considering rolling twice at startup.
+//
+// This class is thread-safe.
+class TokenSigner {
+ public:
+  // Create a new TokenSigner. It will start assigning key sequence numbers
+  // at 'next_seq_num'.
+  //
+  // NOTE: this does not initialize an initial key. Call 'RotateSigningKey()'
+  // to initialize the first key.
+  explicit TokenSigner(int64_t next_seq_num);
+  ~TokenSigner();
+
+  // Sign the given token using the current TSK.
+  Status SignToken(SignedTokenPB* token) const;
+
+  // Returns the set of valid public keys with sequence numbers greater
+  // than 'after_sequence_number'.
+  std::vector<TokenSigningPublicKeyPB> GetTokenSigningPublicKeys(
+      int64_t after_sequence_number) const;
+
+  // Rotate to a new token-signing key.
+  //
+  // See class documentation for more information.
+  Status RotateSigningKey();
+
+ private:
+  // Protects following fields.
+  mutable RWMutex lock_;
+  std::map<int64_t, std::unique_ptr<TokenSigningPrivateKey>> keys_by_seq_;
+
+  int64_t next_seq_num_;
+
+  DISALLOW_COPY_AND_ASSIGN(TokenSigner);
+};
+
+} // namespace security
+} // namespace kudu

http://git-wip-us.apache.org/repos/asf/kudu/blob/7b01d813/src/kudu/security/token_signing_key.cc
----------------------------------------------------------------------
diff --git a/src/kudu/security/token_signing_key.cc b/src/kudu/security/token_signing_key.cc
new file mode 100644
index 0000000..5971e63
--- /dev/null
+++ b/src/kudu/security/token_signing_key.cc
@@ -0,0 +1,68 @@
+// 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.
+
+#include "kudu/security/token_signing_key.h"
+
+#include <glog/logging.h>
+
+#include "kudu/security/token.pb.h"
+#include "kudu/util/status.h"
+
+namespace kudu {
+namespace security {
+
+TokenSigningPublicKey::TokenSigningPublicKey(const TokenSigningPublicKeyPB& pb)
+    : pb_(pb) {
+}
+
+TokenSigningPublicKey::~TokenSigningPublicKey() {
+}
+
+bool TokenSigningPublicKey::VerifySignature(const SignedTokenPB& token) const {
+  CHECK(pb_.has_rsa_key_der());
+  // TODO(PKI): add real signatures!
+  return token.signature() == "signed:" + token.token_data();
+}
+
+TokenSigningPrivateKey::TokenSigningPrivateKey(
+    int64_t key_seq_num, int64_t expire_time, Key key)
+    : key_(std::move(key)),
+      key_seq_num_(key_seq_num),
+      expire_time_(expire_time) {
+}
+
+TokenSigningPrivateKey::~TokenSigningPrivateKey() {
+}
+
+Status TokenSigningPrivateKey::Sign(SignedTokenPB* token) const {
+  token->set_signature("signed:" + token->token_data());
+  token->set_signing_key_seq_num(key_seq_num_);
+  return Status::OK();
+}
+
+void TokenSigningPrivateKey::ExportPublicKeyPB(TokenSigningPublicKeyPB* pb) {
+  pb->Clear();
+  // TODO(PKI): implement me! depends on https://gerrit.cloudera.org/#/c/5783/
+  // though we probably would want to export this once and cache it in DER
+  // format.
+  pb->set_key_seq_num(key_seq_num_);
+  pb->set_rsa_key_der("TODO");
+  pb->set_expire_unix_epoch_seconds(expire_time_);
+}
+
+} // namespace security
+} // namespace kudu

http://git-wip-us.apache.org/repos/asf/kudu/blob/7b01d813/src/kudu/security/token_signing_key.h
----------------------------------------------------------------------
diff --git a/src/kudu/security/token_signing_key.h b/src/kudu/security/token_signing_key.h
new file mode 100644
index 0000000..38fa795
--- /dev/null
+++ b/src/kudu/security/token_signing_key.h
@@ -0,0 +1,77 @@
+// 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.
+#pragma once
+
+#include "kudu/gutil/macros.h"
+#include "kudu/security/openssl_util.h"
+#include "kudu/security/token.pb.h"
+#include "kudu/util/status.h"
+
+namespace kudu {
+namespace security {
+
+// Wrapper around a TokenSigningPublicKeyPB that provides useful functionality
+// verify tokens.
+//
+// Like the underlying OpenSSL types, this can either represent a
+// public/private keypair, or a standalone public key. Attempts to
+// call Sign() without a private key present will result in an error.
+//
+// This class is thread-safe.
+class TokenSigningPublicKey {
+ public:
+  explicit TokenSigningPublicKey(const TokenSigningPublicKeyPB& pb);
+  ~TokenSigningPublicKey();
+
+  const TokenSigningPublicKeyPB& pb() const {
+    return pb_;
+  }
+
+  // Verify the signature in a given token.
+  // NOTE: this does _not_ verify the expiration.
+  bool VerifySignature(const SignedTokenPB& token) const;
+ private:
+  TokenSigningPublicKeyPB pb_;
+
+  // TODO(PKI): parse the underlying PB DER data into an EVP_PKEY
+  // and store that instead.
+  DISALLOW_COPY_AND_ASSIGN(TokenSigningPublicKey);
+};
+
+// Contains a private key used to sign tokens, along with its sequence
+// number and expiration date.
+class TokenSigningPrivateKey {
+ public:
+  TokenSigningPrivateKey(int64_t key_seq_num, int64_t expire_time, Key key);
+  ~TokenSigningPrivateKey();
+
+  // Sign a token, and store the signature and signing key's sequence number.
+  Status Sign(SignedTokenPB* token) const;
+
+  // Export the public-key portion of this signing key.
+  void ExportPublicKeyPB(TokenSigningPublicKeyPB* pb);
+ private:
+  Key key_;
+  int64_t key_seq_num_;
+  int64_t expire_time_;
+
+  DISALLOW_COPY_AND_ASSIGN(TokenSigningPrivateKey);
+};
+
+
+} // namespace security
+} // namespace kudu

http://git-wip-us.apache.org/repos/asf/kudu/blob/7b01d813/src/kudu/security/token_verifier.cc
----------------------------------------------------------------------
diff --git a/src/kudu/security/token_verifier.cc b/src/kudu/security/token_verifier.cc
new file mode 100644
index 0000000..51bd398
--- /dev/null
+++ b/src/kudu/security/token_verifier.cc
@@ -0,0 +1,120 @@
+// 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.
+
+#include "kudu/security/token_verifier.h"
+
+#include <mutex>
+#include <string>
+#include <vector>
+
+#include "kudu/gutil/map-util.h"
+#include "kudu/gutil/walltime.h"
+#include "kudu/security/token.pb.h"
+#include "kudu/security/token_signing_key.h"
+#include "kudu/util/locks.h"
+
+using std::lock_guard;
+using std::string;
+using std::unique_ptr;
+using std::vector;
+
+namespace kudu {
+namespace security {
+
+TokenVerifier::TokenVerifier() {
+}
+
+TokenVerifier::~TokenVerifier() {
+}
+
+int64_t TokenVerifier::GetMaxKnownKeySequenceNumber() const {
+  shared_lock<RWMutex> l(lock_);
+  if (keys_by_seq_.empty()) {
+    return -1;
+  }
+
+  return keys_by_seq_.rbegin()->first;
+}
+
+// Import a set of public keys provided by the token signer (typically
+// running on another node).
+void TokenVerifier::ImportPublicKeys(const vector<TokenSigningPublicKeyPB>& public_keys) {
+  // Do the copy construction outside of the lock, to avoid holding the
+  // lock while doing lots of allocation.
+  vector<unique_ptr<TokenSigningPublicKey>> tsks;
+  for (const auto& pb : public_keys) {
+    // Sanity check the key.
+    CHECK(pb.has_rsa_key_der());
+    CHECK(pb.has_key_seq_num());
+    CHECK(pb.has_expire_unix_epoch_seconds());
+    tsks.emplace_back(new TokenSigningPublicKey { pb });
+  }
+
+  lock_guard<RWMutex> l(lock_);
+  for (auto&& tsk_ptr : tsks) {
+    keys_by_seq_.emplace(tsk_ptr->pb().key_seq_num(), std::move(tsk_ptr));
+  }
+}
+
+// Verify the signature on the given token.
+VerificationResult TokenVerifier::VerifyTokenSignature(
+    const SignedTokenPB& signed_token) const {
+  if (!signed_token.has_signature() ||
+      !signed_token.has_signing_key_seq_num() ||
+      !signed_token.has_token_data()) {
+    return VerificationResult::INVALID_TOKEN;
+  }
+
+  // TODO(perf): should we return the deserialized TokenPB here
+  // since callers are probably going to need it, anyway?
+  TokenPB token;
+  if (!token.ParseFromString(signed_token.token_data()) ||
+      !token.has_expire_unix_epoch_seconds()) {
+    return VerificationResult::INVALID_TOKEN;
+  }
+
+  int64_t now = WallTime_Now();
+  if (token.expire_unix_epoch_seconds() < now) {
+    return VerificationResult::EXPIRED_TOKEN;
+  }
+
+  for (auto flag : token.incompatible_features()) {
+    if (!TokenPB::Feature_IsValid(flag)) {
+      return VerificationResult::INCOMPATIBLE_FEATURE;
+    }
+  }
+
+  {
+    shared_lock<RWMutex> l(lock_);
+    auto* tsk = FindPointeeOrNull(keys_by_seq_, signed_token.signing_key_seq_num());
+    if (!tsk) {
+      return VerificationResult::UNKNOWN_SIGNING_KEY;
+    }
+    if (tsk->pb().expire_unix_epoch_seconds() < now) {
+      return VerificationResult::EXPIRED_SIGNING_KEY;
+    }
+    if (!tsk->VerifySignature(signed_token)) {
+      return VerificationResult::INVALID_SIGNATURE;
+    }
+  }
+
+  return VerificationResult::VALID;
+}
+
+} // namespace security
+} // namespace kudu
+

http://git-wip-us.apache.org/repos/asf/kudu/blob/7b01d813/src/kudu/security/token_verifier.h
----------------------------------------------------------------------
diff --git a/src/kudu/security/token_verifier.h b/src/kudu/security/token_verifier.h
new file mode 100644
index 0000000..d068416
--- /dev/null
+++ b/src/kudu/security/token_verifier.h
@@ -0,0 +1,106 @@
+// 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.
+#pragma once
+
+#include <map>
+#include <vector>
+
+#include "kudu/gutil/macros.h"
+#include "kudu/util/rw_mutex.h"
+
+namespace kudu {
+namespace security {
+
+class TokenSigningPublicKey;
+class TokenSigningPublicKeyPB;
+class SignedTokenPB;
+enum class VerificationResult;
+
+// Class responsible for verifying tokens provided to a server.
+//
+// This class manages a set of public keys, each identified by a sequence
+// number. It exposes the latest known sequence number, which can be sent
+// to a 'TokenSigner' running on another node. That node can then
+// export public keys, which are transferred back to this node and imported
+// into the 'TokenVerifier'.
+//
+// Each signed token also includes the key sequence number that signed it,
+// so this class can look up the correct key and verify the token's
+// validity and expiration.
+//
+// Note that this class does not perform any "business logic" around the
+// content of a token. It only verifies that the token has a valid signature
+// and is not yet expired. Any business rules around authorization or
+// authentication are left up to callers.
+//
+// This class is thread-safe.
+class TokenVerifier {
+ public:
+  TokenVerifier();
+  ~TokenVerifier();
+
+  // Return the highest key sequence number known by this instance.
+  //
+  // If no keys are known, returns -1.
+  int64_t GetMaxKnownKeySequenceNumber() const;
+
+  // Import a set of public keys provided by a TokenSigner instance (which might
+  // be running on a remote node). If any public keys already exist with matching key
+  // sequence numbers, they are replaced by the new keys.
+  void ImportPublicKeys(const std::vector<TokenSigningPublicKeyPB>& public_keys);
+
+  // Verify the signature on the given token.
+  VerificationResult VerifyTokenSignature(const SignedTokenPB& signed_token) const;
+
+  // TODO(PKI): should expire out old key versions at some point. eg only
+  // keep old key versions until their expiration is an hour or two in the past?
+  // Not sure where we'll call this from, and likely the "slow leak" isn't
+  // critical for first implementation.
+  // void ExpireOldKeys();
+
+ private:
+  // Lock protecting keys_by_seq_
+  mutable RWMutex lock_;
+  std::map<int64_t, std::unique_ptr<TokenSigningPublicKey>> keys_by_seq_;
+
+  DISALLOW_COPY_AND_ASSIGN(TokenVerifier);
+};
+
+// Result of a token verification.
+enum class VerificationResult {
+  // The signature is valid and the token is not expired.
+  VALID,
+  // The token itself is invalid (e.g. missing its signature or data,
+  // can't be deserialized, etc).
+  INVALID_TOKEN,
+  // The signature is invalid (i.e. cryptographically incorrect).
+  INVALID_SIGNATURE,
+  // The signature is valid, but the token has already expired.
+  EXPIRED_TOKEN,
+  // The signature is valid, but the signing key is no longer valid.
+  EXPIRED_SIGNING_KEY,
+  // The signing key used to sign this token is not available.
+  UNKNOWN_SIGNING_KEY,
+  // The token uses an incompatible feature which isn't supported by this
+  // version of the server. We reject the token to give a "default deny"
+  // policy.
+  INCOMPATIBLE_FEATURE
+};
+
+
+} // namespace security
+} // namespace kudu