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 <[email protected]>
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 <[email protected]>
Authored: Wed Jan 25 14:59:18 2017 -0800
Committer: Todd Lipcon <[email protected]>
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

Reply via email to