| // 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 <cstdint> |
| #include <deque> |
| #include <memory> |
| #include <ostream> |
| #include <string> |
| #include <thread> |
| #include <utility> |
| #include <vector> |
| |
| #include <glog/logging.h> |
| #include <gtest/gtest.h> |
| |
| #include "kudu/gutil/strings/substitute.h" |
| #include "kudu/gutil/walltime.h" |
| #include "kudu/security/crypto.h" |
| #include "kudu/security/token.pb.h" |
| #include "kudu/security/token_signer.h" |
| #include "kudu/security/token_signing_key.h" |
| #include "kudu/security/token_verifier.h" |
| #include "kudu/util/countdown_latch.h" |
| #include "kudu/util/logging.h" |
| #include "kudu/util/monotime.h" |
| #include "kudu/util/openssl_util.h" |
| #include "kudu/util/pb_util.h" |
| #include "kudu/util/status.h" |
| #include "kudu/util/test_macros.h" |
| #include "kudu/util/test_util.h" |
| |
| using kudu::pb_util::SecureDebugString; |
| using std::make_shared; |
| using std::string; |
| using std::thread; |
| using std::unique_ptr; |
| using std::vector; |
| using strings::Substitute; |
| |
| namespace kudu { |
| namespace security { |
| |
| namespace { |
| |
| // Dummy variables to use when their values don't matter much. |
| const int kNumBits = UseLargeKeys() ? 2048 : 512; |
| const int64_t kTokenValiditySeconds = 10; |
| const char kUser[] = "user"; |
| |
| // Repeatedly signs tokens and attempts to rotate TSKs until the active TSK's |
| // sequence number passes `seq_num`, returning the last token signed by the TSK |
| // at `seq_num`. This token is roughly the last possible token signed in the |
| // TSK's activity interval. |
| // The TokenGenerator 'generate_token' is a lambda that fills in a |
| // SignedTokenPB and returns a Status. |
| template <class TokenGenerator> |
| Status SignUntilRotatePast(TokenSigner* signer, TokenGenerator generate_token, |
| const string& token_type, int64_t seq_num, |
| SignedTokenPB* last_signed_by_tsk) { |
| SignedTokenPB last_signed; |
| RETURN_NOT_OK_PREPEND(generate_token(&last_signed), |
| Substitute("Failed to generate first $0 token", token_type)); |
| DCHECK_EQ(seq_num, last_signed.signing_key_seq_num()) |
| << Substitute("Unexpected starting seq_num for $0 token", token_type); |
| |
| auto cur_seq_num = seq_num; |
| while (cur_seq_num == seq_num) { |
| SleepFor(MonoDelta::FromMilliseconds(50)); |
| KLOG_EVERY_N_SECS(INFO, 1) << |
| Substitute("Generating $0 token for activity interval $1", token_type, seq_num); |
| RETURN_NOT_OK_PREPEND(signer->TryRotateKey(), "Failed to attempt to rotate key"); |
| SignedTokenPB signed_token; |
| RETURN_NOT_OK_PREPEND(generate_token(&signed_token), |
| Substitute("Failed to generate $0 token", token_type)); |
| // We want to return the last token signed by the `seq_num` TSK, so only |
| // update it when appropriate. |
| cur_seq_num = signed_token.signing_key_seq_num(); |
| if (cur_seq_num == seq_num) { |
| last_signed = std::move(signed_token); |
| } |
| } |
| *last_signed_by_tsk = std::move(last_signed); |
| return Status::OK(); |
| } |
| |
| 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; |
| } |
| |
| // Generate public key as a string in DER format for tests. |
| Status GeneratePublicKeyStrDer(string* ret) { |
| PrivateKey private_key; |
| RETURN_NOT_OK(GeneratePrivateKey(kNumBits, &private_key)); |
| PublicKey public_key; |
| RETURN_NOT_OK(private_key.GetPublicKey(&public_key)); |
| string public_key_str_der; |
| RETURN_NOT_OK(public_key.ToString(&public_key_str_der, DataFormat::DER)); |
| *ret = public_key_str_der; |
| return Status::OK(); |
| } |
| |
| // Generate token signing key with the specified parameters. |
| Status GenerateTokenSigningKey(int64_t seq_num, |
| int64_t expire_time_seconds, |
| unique_ptr<TokenSigningPrivateKey>* tsk) { |
| { |
| unique_ptr<PrivateKey> private_key(new PrivateKey); |
| RETURN_NOT_OK(GeneratePrivateKey(kNumBits, private_key.get())); |
| tsk->reset(new TokenSigningPrivateKey( |
| seq_num, expire_time_seconds, std::move(private_key))); |
| } |
| return Status::OK(); |
| } |
| |
| void CheckAndAddNextKey(int iter_num, |
| TokenSigner* signer, |
| int64_t* key_seq_num) { |
| ASSERT_NE(nullptr, signer); |
| ASSERT_NE(nullptr, key_seq_num); |
| int64_t seq_num; |
| { |
| unique_ptr<TokenSigningPrivateKey> key; |
| ASSERT_OK(signer->CheckNeedKey(&key)); |
| ASSERT_NE(nullptr, key.get()); |
| seq_num = key->key_seq_num(); |
| } |
| |
| for (int i = 0; i < iter_num; ++i) { |
| unique_ptr<TokenSigningPrivateKey> key; |
| ASSERT_OK(signer->CheckNeedKey(&key)); |
| ASSERT_NE(nullptr, key.get()); |
| ASSERT_EQ(seq_num, key->key_seq_num()); |
| if (i + 1 == iter_num) { |
| // Finally, add the key to the TokenSigner. |
| ASSERT_OK(signer->AddKey(std::move(key))); |
| } |
| } |
| *key_seq_num = seq_num; |
| } |
| |
| } // anonymous namespace |
| |
| class TokenTest : public KuduTest { |
| }; |
| |
| TEST_F(TokenTest, TestInit) { |
| TokenSigner signer(kTokenValiditySeconds, kTokenValiditySeconds, 10); |
| const TokenVerifier& verifier(signer.verifier()); |
| |
| SignedTokenPB token = MakeUnsignedToken(WallTime_Now()); |
| Status s = signer.SignToken(&token); |
| ASSERT_TRUE(s.IsIllegalState()) << s.ToString(); |
| |
| static const int64_t kKeySeqNum = 100; |
| PrivateKey private_key; |
| ASSERT_OK(GeneratePrivateKey(kNumBits, &private_key)); |
| string private_key_str_der; |
| ASSERT_OK(private_key.ToString(&private_key_str_der, DataFormat::DER)); |
| TokenSigningPrivateKeyPB pb; |
| pb.set_rsa_key_der(private_key_str_der); |
| pb.set_key_seq_num(kKeySeqNum); |
| pb.set_expire_unix_epoch_seconds(WallTime_Now() + 120); |
| |
| ASSERT_OK(signer.ImportKeys({pb})); |
| vector<TokenSigningPublicKeyPB> public_keys(verifier.ExportKeys()); |
| ASSERT_EQ(1, public_keys.size()); |
| ASSERT_EQ(kKeySeqNum, public_keys[0].key_seq_num()); |
| |
| // It should be possible to sign tokens once the signer is initialized. |
| ASSERT_OK(signer.SignToken(&token)); |
| ASSERT_TRUE(token.has_signature()); |
| } |
| |
| // Verify that TokenSigner does not allow 'holes' in the sequence numbers |
| // of the generated keys. The idea is to not allow sequences like '1, 5, 6'. |
| // In general, calling the CheckNeedKey() method multiple times and then calling |
| // the AddKey() method once should advance the key sequence number only by 1 |
| // regardless of number CheckNeedKey() calls. |
| // |
| // This is to make sure that the sequence numbers are not sparse in case if |
| // running scenarios CheckNeedKey()-try-to-store-key-AddKey() over and over |
| // again, given that the 'try-to-store-key' part can fail sometimes. |
| TEST_F(TokenTest, TestTokenSignerNonSparseSequenceNumbers) { |
| static const int kIterNum = 3; |
| static const int64_t kAuthnTokenValiditySeconds = 1; |
| static const int64_t kAuthzTokenValiditySeconds = 1; |
| static const int64_t kKeyRotationSeconds = 1; |
| |
| TokenSigner signer(kAuthnTokenValiditySeconds, kAuthzTokenValiditySeconds, kKeyRotationSeconds); |
| |
| int64_t seq_num_first_key; |
| NO_FATALS(CheckAndAddNextKey(kIterNum, &signer, &seq_num_first_key)); |
| |
| SleepFor(MonoDelta::FromSeconds(kKeyRotationSeconds + 1)); |
| |
| int64_t seq_num_second_key; |
| NO_FATALS(CheckAndAddNextKey(kIterNum, &signer, &seq_num_second_key)); |
| |
| ASSERT_EQ(seq_num_first_key + 1, seq_num_second_key); |
| } |
| |
| // Verify the behavior of the TokenSigner::ImportKeys() method. In general, |
| // it should tolerate mix of expired and non-expired keys, even if their |
| // sequence numbers are intermixed: keys with greater sequence numbers could |
| // be already expired but keys with lesser sequence numbers could be still |
| // valid. The idea is to correctly import TSKs generated with different |
| // validity period settings. This is to address scenarios when the system |
| // was run with long authn token validity interval and then switched to |
| // a shorter one. |
| // |
| // After importing keys, the TokenSigner should contain only the valid ones. |
| // In addition, the sequence number of the very first key generated after the |
| // import should be greater than any sequence number the TokenSigner has seen |
| // during the import. |
| TEST_F(TokenTest, TestTokenSignerAddKeyAfterImport) { |
| static const int64_t kKeyRotationSeconds = 8; |
| |
| TokenSigner signer(kTokenValiditySeconds, kTokenValiditySeconds, kKeyRotationSeconds); |
| const int64_t key_validity_seconds = signer.key_validity_seconds_; |
| const TokenVerifier& verifier(signer.verifier()); |
| |
| static const int64_t kExpiredKeySeqNum = 100; |
| static const int64_t kKeySeqNum = kExpiredKeySeqNum - 1; |
| { |
| // First, try to import already expired key to check that internal key |
| // sequence number advances correspondingly. |
| PrivateKey private_key; |
| ASSERT_OK(GeneratePrivateKey(kNumBits, &private_key)); |
| string private_key_str_der; |
| ASSERT_OK(private_key.ToString(&private_key_str_der, DataFormat::DER)); |
| TokenSigningPrivateKeyPB pb; |
| pb.set_rsa_key_der(private_key_str_der); |
| pb.set_key_seq_num(kExpiredKeySeqNum); |
| pb.set_expire_unix_epoch_seconds(WallTime_Now() - 1); |
| |
| ASSERT_OK(signer.ImportKeys({pb})); |
| |
| // Check the result of importing keys: there should be no keys because |
| // the only one we tried to import was already expired. |
| vector<TokenSigningPublicKeyPB> public_keys(verifier.ExportKeys()); |
| ASSERT_TRUE(public_keys.empty()); |
| } |
| |
| { |
| // Now import valid (not yet expired) key, but with sequence number less |
| // than of the expired key. |
| PrivateKey private_key; |
| ASSERT_OK(GeneratePrivateKey(kNumBits, &private_key)); |
| string private_key_str_der; |
| ASSERT_OK(private_key.ToString(&private_key_str_der, DataFormat::DER)); |
| TokenSigningPrivateKeyPB pb; |
| pb.set_rsa_key_der(private_key_str_der); |
| pb.set_key_seq_num(kKeySeqNum); |
| // Set the TSK's expiration time: make the key valid but past its activity |
| // interval. |
| pb.set_expire_unix_epoch_seconds( |
| WallTime_Now() + (key_validity_seconds - 2 * kKeyRotationSeconds - 1)); |
| |
| ASSERT_OK(signer.ImportKeys({pb})); |
| |
| // Check the result of importing keys. The lower sequence number is |
| // accepted, even though we previously imported a key with a higher |
| // sequence number that was expired. |
| vector<TokenSigningPublicKeyPB> public_keys(verifier.ExportKeys()); |
| ASSERT_EQ(1, public_keys.size()); |
| ASSERT_EQ(kKeySeqNum, public_keys[0].key_seq_num()); |
| |
| // The newly imported key should be used to sign tokens. |
| SignedTokenPB token = MakeUnsignedToken(WallTime_Now()); |
| ASSERT_OK(signer.SignToken(&token)); |
| ASSERT_TRUE(token.has_signature()); |
| ASSERT_TRUE(token.has_signing_key_seq_num()); |
| EXPECT_EQ(kKeySeqNum, token.signing_key_seq_num()); |
| } |
| |
| { |
| unique_ptr<TokenSigningPrivateKey> key; |
| ASSERT_OK(signer.CheckNeedKey(&key)); |
| ASSERT_NE(nullptr, key.get()); |
| ASSERT_EQ(kExpiredKeySeqNum + 1, key->key_seq_num()); |
| ASSERT_OK(signer.AddKey(std::move(key))); |
| bool has_rotated = false; |
| ASSERT_OK(signer.TryRotateKey(&has_rotated)); |
| ASSERT_TRUE(has_rotated); |
| } |
| { |
| // Check the result of generating the new key: the identifier of the new key |
| // should be +1 increment from the identifier of the expired imported key. |
| vector<TokenSigningPublicKeyPB> public_keys(verifier.ExportKeys()); |
| ASSERT_EQ(2, public_keys.size()); |
| EXPECT_EQ(kKeySeqNum, public_keys[0].key_seq_num()); |
| EXPECT_EQ(kExpiredKeySeqNum + 1, public_keys[1].key_seq_num()); |
| } |
| |
| // At this point the new key should be used to sign tokens. |
| SignedTokenPB token = MakeUnsignedToken(WallTime_Now()); |
| ASSERT_OK(signer.SignToken(&token)); |
| ASSERT_TRUE(token.has_signature()); |
| ASSERT_TRUE(token.has_signing_key_seq_num()); |
| EXPECT_EQ(kExpiredKeySeqNum + 1, token.signing_key_seq_num()); |
| } |
| |
| // The AddKey() method should not allow to add a key with the sequence number |
| // less or equal to the sequence number of the most 'recent' key. |
| TEST_F(TokenTest, TestAddKeyConstraints) { |
| { |
| // If a signer has not created a TSK yet, it will create a key, and will |
| // happily accept the generated key. |
| TokenSigner signer(kTokenValiditySeconds, kTokenValiditySeconds, 1); |
| unique_ptr<TokenSigningPrivateKey> key; |
| ASSERT_OK(signer.CheckNeedKey(&key)); |
| ASSERT_NE(nullptr, key.get()); |
| ASSERT_OK(signer.AddKey(std::move(key))); |
| } |
| { |
| // If the key sequence number added to the signer isn't monotonically |
| // increasing, the signer will complain. |
| TokenSigner signer(kTokenValiditySeconds, kTokenValiditySeconds, 1); |
| unique_ptr<TokenSigningPrivateKey> key; |
| ASSERT_OK(signer.CheckNeedKey(&key)); |
| ASSERT_NE(nullptr, key.get()); |
| const int64_t key_seq_num = key->key_seq_num(); |
| key->key_seq_num_ = key_seq_num - 1; |
| Status s = signer.AddKey(std::move(key)); |
| ASSERT_TRUE(s.IsInvalidArgument()) << s.ToString(); |
| ASSERT_STR_CONTAINS(s.ToString(), |
| ": invalid key sequence number, should be at least "); |
| } |
| { |
| // Test importing expired keys. The signer should be OK with it. |
| TokenSigner signer(kTokenValiditySeconds, kTokenValiditySeconds, 1); |
| static const int64_t kKeySeqNum = 100; |
| PrivateKey private_key; |
| ASSERT_OK(GeneratePrivateKey(kNumBits, &private_key)); |
| string private_key_str_der; |
| ASSERT_OK(private_key.ToString(&private_key_str_der, DataFormat::DER)); |
| TokenSigningPrivateKeyPB pb; |
| pb.set_rsa_key_der(private_key_str_der); |
| pb.set_key_seq_num(kKeySeqNum); |
| // Make the key already expired. |
| pb.set_expire_unix_epoch_seconds(WallTime_Now() - 1); |
| ASSERT_OK(signer.ImportKeys({pb})); |
| |
| // Generated keys thereafter are expected to have higher sequence numbers |
| // than the imported expired keys. |
| unique_ptr<TokenSigningPrivateKey> key; |
| ASSERT_OK(signer.CheckNeedKey(&key)); |
| ASSERT_NE(nullptr, key.get()); |
| const int64_t key_seq_num = key->key_seq_num(); |
| ASSERT_GT(key_seq_num, kKeySeqNum); |
| key->key_seq_num_ = kKeySeqNum; |
| Status s = signer.AddKey(std::move(key)); |
| ASSERT_TRUE(s.IsInvalidArgument()) << s.ToString(); |
| ASSERT_STR_CONTAINS(s.ToString(), |
| ": invalid key sequence number, should be at least "); |
| } |
| } |
| |
| TEST_F(TokenTest, TestGenerateAuthnTokenNoUserName) { |
| TokenSigner signer(kTokenValiditySeconds, kTokenValiditySeconds, 10); |
| SignedTokenPB signed_token_pb; |
| const Status& s = signer.GenerateAuthnToken("", &signed_token_pb); |
| EXPECT_TRUE(s.IsInvalidArgument()) << s.ToString(); |
| ASSERT_STR_CONTAINS(s.ToString(), "no username provided for authn token"); |
| } |
| |
| TEST_F(TokenTest, TestGenerateAuthzToken) { |
| // We cannot generate tokens with no username associated with it. |
| auto verifier(make_shared<TokenVerifier>()); |
| TokenSigner signer(kTokenValiditySeconds, kTokenValiditySeconds, 10, verifier); |
| TablePrivilegePB table_privilege; |
| SignedTokenPB signed_token_pb; |
| Status s = signer.GenerateAuthzToken("", table_privilege, &signed_token_pb); |
| EXPECT_TRUE(s.IsInvalidArgument()) << s.ToString(); |
| ASSERT_STR_CONTAINS(s.ToString(), "no username provided for authz token"); |
| |
| // Generated tokens will have the specified privileges. |
| const string kAuthorized = "authzed"; |
| unique_ptr<TokenSigningPrivateKey> key; |
| ASSERT_OK(signer.CheckNeedKey(&key)); |
| ASSERT_NE(nullptr, key.get()); |
| ASSERT_OK(signer.AddKey(std::move(key))); |
| ASSERT_OK(signer.GenerateAuthzToken(kAuthorized, |
| table_privilege, |
| &signed_token_pb)); |
| ASSERT_TRUE(signed_token_pb.has_token_data()); |
| TokenPB token_pb; |
| ASSERT_EQ(TokenVerificationResult::VALID, |
| verifier->VerifyTokenSignature(signed_token_pb, &token_pb)); |
| ASSERT_TRUE(token_pb.has_authz()); |
| ASSERT_EQ(kAuthorized, token_pb.authz().username()); |
| ASSERT_TRUE(token_pb.authz().has_table_privilege()); |
| ASSERT_EQ(SecureDebugString(table_privilege), |
| SecureDebugString(token_pb.authz().table_privilege())); |
| } |
| |
| TEST_F(TokenTest, TestIsCurrentKeyValid) { |
| // This test sleeps for a key validity period, so set it up to be short. |
| static const int64_t kShortTokenValiditySeconds = 1; |
| static const int64_t kKeyRotationSeconds = 1; |
| |
| TokenSigner signer(kShortTokenValiditySeconds, kShortTokenValiditySeconds, kKeyRotationSeconds); |
| static const int64_t key_validity_seconds = signer.key_validity_seconds_; |
| |
| EXPECT_FALSE(signer.IsCurrentKeyValid()); |
| { |
| unique_ptr<TokenSigningPrivateKey> key; |
| ASSERT_OK(signer.CheckNeedKey(&key)); |
| // No keys are available yet, so should be able to add. |
| ASSERT_NE(nullptr, key.get()); |
| ASSERT_OK(signer.AddKey(std::move(key))); |
| } |
| EXPECT_TRUE(signer.IsCurrentKeyValid()); |
| SleepFor(MonoDelta::FromSeconds(key_validity_seconds)); |
| // The key should expire after its validity interval. |
| EXPECT_FALSE(signer.IsCurrentKeyValid()); |
| |
| // Anyway, current implementation allows to use an expired key to sign tokens. |
| SignedTokenPB token = MakeUnsignedToken(WallTime_Now()); |
| EXPECT_OK(signer.SignToken(&token)); |
| } |
| |
| TEST_F(TokenTest, TestTokenSignerAddKeys) { |
| { |
| TokenSigner signer(kTokenValiditySeconds, kTokenValiditySeconds, 10); |
| unique_ptr<TokenSigningPrivateKey> key; |
| ASSERT_OK(signer.CheckNeedKey(&key)); |
| // No keys are available yet, so should be able to add. |
| ASSERT_NE(nullptr, key.get()); |
| ASSERT_OK(signer.AddKey(std::move(key))); |
| |
| ASSERT_OK(signer.CheckNeedKey(&key)); |
| // It's not time to add next key yet. |
| ASSERT_EQ(nullptr, key.get()); |
| } |
| |
| { |
| // Special configuration for TokenSigner: rotation interval is zero, |
| // so should be able to add two keys right away. |
| TokenSigner signer(kTokenValiditySeconds, kTokenValiditySeconds, 0); |
| unique_ptr<TokenSigningPrivateKey> key; |
| ASSERT_OK(signer.CheckNeedKey(&key)); |
| // No keys are available yet, so should be able to add. |
| ASSERT_NE(nullptr, key.get()); |
| ASSERT_OK(signer.AddKey(std::move(key))); |
| |
| // Should be able to add next key right away. |
| ASSERT_OK(signer.CheckNeedKey(&key)); |
| ASSERT_NE(nullptr, key.get()); |
| ASSERT_OK(signer.AddKey(std::move(key))); |
| |
| // Active key and next key are already in place: no need for a new key. |
| ASSERT_OK(signer.CheckNeedKey(&key)); |
| ASSERT_EQ(nullptr, key.get()); |
| } |
| |
| if (AllowSlowTests()) { |
| // Special configuration for TokenSigner: short interval for key rotation. |
| // It should not need next key right away, but should need next key after |
| // the rotation interval. |
| static const int64_t kKeyRotationIntervalSeconds = 8; |
| TokenSigner signer(kTokenValiditySeconds, kTokenValiditySeconds, kKeyRotationIntervalSeconds); |
| unique_ptr<TokenSigningPrivateKey> key; |
| ASSERT_OK(signer.CheckNeedKey(&key)); |
| // No keys are available yet, so should be able to add. |
| ASSERT_NE(nullptr, key.get()); |
| ASSERT_OK(signer.AddKey(std::move(key))); |
| |
| // Should not need next key right away. |
| ASSERT_OK(signer.CheckNeedKey(&key)); |
| ASSERT_EQ(nullptr, key.get()); |
| |
| SleepFor(MonoDelta::FromSeconds(kKeyRotationIntervalSeconds)); |
| |
| // Should need next key after the rotation interval. |
| ASSERT_OK(signer.CheckNeedKey(&key)); |
| ASSERT_NE(nullptr, key.get()); |
| ASSERT_OK(signer.AddKey(std::move(key))); |
| |
| // Active key and next key are already in place: no need for a new key. |
| ASSERT_OK(signer.CheckNeedKey(&key)); |
| ASSERT_EQ(nullptr, key.get()); |
| } |
| } |
| |
| // Test how key rotation works. |
| TEST_F(TokenTest, TestTokenSignerSignVerifyExport) { |
| // Key rotation interval 0 allows adding 2 keys in a row with no delay. |
| TokenSigner signer(kTokenValiditySeconds, kTokenValiditySeconds, 0); |
| const TokenVerifier& verifier(signer.verifier()); |
| |
| // Should start off with no signing keys. |
| ASSERT_TRUE(verifier.ExportKeys().empty()); |
| |
| // Trying to sign a token when there is no TSK should give an error. |
| SignedTokenPB token = MakeUnsignedToken(WallTime_Now()); |
| Status s = signer.SignToken(&token); |
| ASSERT_TRUE(s.IsIllegalState()) << s.ToString(); |
| |
| // Generate and set a new key. |
| int64_t signing_key_seq_num; |
| { |
| unique_ptr<TokenSigningPrivateKey> key; |
| ASSERT_OK(signer.CheckNeedKey(&key)); |
| ASSERT_NE(nullptr, key.get()); |
| signing_key_seq_num = key->key_seq_num(); |
| ASSERT_GT(signing_key_seq_num, -1); |
| ASSERT_OK(signer.AddKey(std::move(key))); |
| } |
| |
| // We should see the key now if we request TSKs starting at a |
| // lower sequence number. |
| ASSERT_EQ(1, verifier.ExportKeys().size()); |
| // We should not see the key if we ask for the sequence number |
| // that it is assigned. |
| ASSERT_EQ(0, verifier.ExportKeys(signing_key_seq_num).size()); |
| |
| // We should be able to sign a token now. |
| ASSERT_OK(signer.SignToken(&token)); |
| ASSERT_TRUE(token.has_signature()); |
| ASSERT_EQ(signing_key_seq_num, token.signing_key_seq_num()); |
| |
| // Set next key and check that we return the right keys. |
| int64_t next_signing_key_seq_num; |
| { |
| unique_ptr<TokenSigningPrivateKey> key; |
| ASSERT_OK(signer.CheckNeedKey(&key)); |
| ASSERT_NE(nullptr, key.get()); |
| next_signing_key_seq_num = key->key_seq_num(); |
| ASSERT_GT(next_signing_key_seq_num, signing_key_seq_num); |
| ASSERT_OK(signer.AddKey(std::move(key))); |
| } |
| ASSERT_EQ(2, verifier.ExportKeys().size()); |
| ASSERT_EQ(1, verifier.ExportKeys(signing_key_seq_num).size()); |
| ASSERT_EQ(0, verifier.ExportKeys(next_signing_key_seq_num).size()); |
| |
| // The first key should be used for signing: the next one is saved |
| // for the next rotation. |
| { |
| SignedTokenPB token = MakeUnsignedToken(WallTime_Now()); |
| ASSERT_OK(signer.SignToken(&token)); |
| ASSERT_TRUE(token.has_signature()); |
| ASSERT_EQ(signing_key_seq_num, token.signing_key_seq_num()); |
| } |
| } |
| |
| // Test that the TokenSigner can export its public keys in protobuf form |
| // via bound TokenVerifier. |
| TEST_F(TokenTest, TestExportKeys) { |
| // Test that the exported public keys don't contain private key material, |
| // and have an appropriate expiration. |
| const int64_t key_exp_seconds = 30; |
| const int64_t key_rotation_seconds = 10; |
| TokenSigner signer(key_exp_seconds - 2 * key_rotation_seconds, 0, |
| key_rotation_seconds); |
| int64_t key_seq_num; |
| { |
| unique_ptr<TokenSigningPrivateKey> key; |
| ASSERT_OK(signer.CheckNeedKey(&key)); |
| ASSERT_NE(nullptr, key.get()); |
| key_seq_num = key->key_seq_num(); |
| ASSERT_OK(signer.AddKey(std::move(key))); |
| } |
| const TokenVerifier& verifier(signer.verifier()); |
| auto keys = verifier.ExportKeys(); |
| ASSERT_EQ(1, keys.size()); |
| const TokenSigningPublicKeyPB& key = keys[0]; |
| ASSERT_TRUE(key.has_rsa_key_der()); |
| ASSERT_EQ(key_seq_num, key.key_seq_num()); |
| ASSERT_TRUE(key.has_expire_unix_epoch_seconds()); |
| const int64_t now = WallTime_Now(); |
| ASSERT_GT(key.expire_unix_epoch_seconds(), now); |
| ASSERT_LE(key.expire_unix_epoch_seconds(), now + key_exp_seconds); |
| } |
| |
| // 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(kTokenValiditySeconds, kTokenValiditySeconds, 10); |
| { |
| unique_ptr<TokenSigningPrivateKey> key; |
| ASSERT_OK(signer.CheckNeedKey(&key)); |
| ASSERT_NE(nullptr, key.get()); |
| ASSERT_OK(signer.AddKey(std::move(key))); |
| } |
| |
| // Make and sign a token. |
| SignedTokenPB signed_token = MakeUnsignedToken(WallTime_Now() + 600); |
| ASSERT_OK(signer.SignToken(&signed_token)); |
| |
| // Try to verify it. |
| TokenVerifier verifier; |
| ASSERT_OK(verifier.ImportKeys(signer.verifier().ExportKeys())); |
| TokenPB token; |
| ASSERT_EQ(TokenVerificationResult::VALID, verifier.VerifyTokenSignature(signed_token, &token)); |
| } |
| |
| // Test all of the possible cases covered by token verification. |
| // See TokenVerificationResult. |
| TEST_F(TokenTest, TestEndToEnd_InvalidCases) { |
| // Key rotation interval 0 allows adding 2 keys in a row with no delay. |
| TokenSigner signer(kTokenValiditySeconds, kTokenValiditySeconds, 0); |
| { |
| unique_ptr<TokenSigningPrivateKey> key; |
| ASSERT_OK(signer.CheckNeedKey(&key)); |
| ASSERT_NE(nullptr, key.get()); |
| ASSERT_OK(signer.AddKey(std::move(key))); |
| } |
| |
| TokenVerifier verifier; |
| ASSERT_OK(verifier.ImportKeys(signer.verifier().ExportKeys())); |
| |
| // Make and sign a token, but corrupt the data in it. |
| { |
| SignedTokenPB signed_token = MakeUnsignedToken(WallTime_Now() + 600); |
| ASSERT_OK(signer.SignToken(&signed_token)); |
| signed_token.set_token_data("xyz"); |
| TokenPB token; |
| ASSERT_EQ(TokenVerificationResult::INVALID_TOKEN, |
| verifier.VerifyTokenSignature(signed_token, &token)); |
| } |
| |
| // Make and sign a token, but corrupt the signature. |
| { |
| SignedTokenPB signed_token = MakeUnsignedToken(WallTime_Now() + 600); |
| ASSERT_OK(signer.SignToken(&signed_token)); |
| signed_token.set_signature("xyz"); |
| TokenPB token; |
| ASSERT_EQ(TokenVerificationResult::INVALID_SIGNATURE, |
| verifier.VerifyTokenSignature(signed_token, &token)); |
| } |
| |
| // Make and sign a token, but set it to be already expired. |
| { |
| SignedTokenPB signed_token = MakeUnsignedToken(WallTime_Now() - 10); |
| ASSERT_OK(signer.SignToken(&signed_token)); |
| TokenPB token; |
| ASSERT_EQ(TokenVerificationResult::EXPIRED_TOKEN, |
| verifier.VerifyTokenSignature(signed_token, &token)); |
| } |
| |
| // Make and sign a token which uses an incompatible feature flag. |
| { |
| SignedTokenPB signed_token = MakeIncompatibleToken(); |
| ASSERT_OK(signer.SignToken(&signed_token)); |
| TokenPB token; |
| ASSERT_EQ(TokenVerificationResult::INCOMPATIBLE_FEATURE, |
| verifier.VerifyTokenSignature(signed_token, &token)); |
| } |
| |
| // Set a new signing key, but don't inform the verifier of it yet. When we |
| // verify, we expect the verifier to complain the key is unknown. |
| { |
| { |
| unique_ptr<TokenSigningPrivateKey> key; |
| ASSERT_OK(signer.CheckNeedKey(&key)); |
| ASSERT_NE(nullptr, key.get()); |
| ASSERT_OK(signer.AddKey(std::move(key))); |
| bool has_rotated = false; |
| ASSERT_OK(signer.TryRotateKey(&has_rotated)); |
| ASSERT_TRUE(has_rotated); |
| } |
| SignedTokenPB signed_token = MakeUnsignedToken(WallTime_Now() + 600); |
| ASSERT_OK(signer.SignToken(&signed_token)); |
| TokenPB token; |
| ASSERT_EQ(TokenVerificationResult::UNKNOWN_SIGNING_KEY, |
| verifier.VerifyTokenSignature(signed_token, &token)); |
| } |
| |
| // Set a new 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. |
| { |
| { |
| unique_ptr<TokenSigningPrivateKey> tsk; |
| ASSERT_OK(GenerateTokenSigningKey(100, WallTime_Now() - 1, &tsk)); |
| // This direct access is necessary because AddKey() does not allow to add |
| // an expired key. |
| TokenSigningPublicKeyPB tsk_public_pb; |
| tsk->ExportPublicKeyPB(&tsk_public_pb); |
| ASSERT_OK(verifier.ImportKeys({tsk_public_pb})); |
| signer.tsk_deque_.push_front(std::move(tsk)); |
| } |
| |
| SignedTokenPB signed_token = MakeUnsignedToken(WallTime_Now() + 600); |
| // Current implementation allows to use an expired key to sign tokens. |
| ASSERT_OK(signer.SignToken(&signed_token)); |
| TokenPB token; |
| ASSERT_EQ(TokenVerificationResult::EXPIRED_SIGNING_KEY, |
| verifier.VerifyTokenSignature(signed_token, &token)); |
| } |
| } |
| |
| // Test functionality of the TokenVerifier::ImportKeys() method. |
| TEST_F(TokenTest, TestTokenVerifierImportKeys) { |
| TokenVerifier verifier; |
| |
| // An attempt to import no keys is fine. |
| ASSERT_OK(verifier.ImportKeys({})); |
| ASSERT_TRUE(verifier.ExportKeys().empty()); |
| |
| TokenSigningPublicKeyPB tsk_public_pb; |
| const auto exp_time = WallTime_Now() + 600; |
| tsk_public_pb.set_key_seq_num(100500); |
| tsk_public_pb.set_expire_unix_epoch_seconds(exp_time); |
| string public_key_str_der; |
| ASSERT_OK(GeneratePublicKeyStrDer(&public_key_str_der)); |
| tsk_public_pb.set_rsa_key_der(public_key_str_der); |
| |
| ASSERT_OK(verifier.ImportKeys({ tsk_public_pb })); |
| { |
| const auto& exported_tsks_public_pb = verifier.ExportKeys(); |
| ASSERT_EQ(1, exported_tsks_public_pb.size()); |
| EXPECT_EQ(tsk_public_pb.SerializeAsString(), |
| exported_tsks_public_pb[0].SerializeAsString()); |
| } |
| |
| // Re-importing the same key again is fine, and the total number |
| // of exported keys should not increase. |
| ASSERT_OK(verifier.ImportKeys({ tsk_public_pb })); |
| { |
| const auto& exported_tsks_public_pb = verifier.ExportKeys(); |
| ASSERT_EQ(1, exported_tsks_public_pb.size()); |
| EXPECT_EQ(tsk_public_pb.SerializeAsString(), |
| exported_tsks_public_pb[0].SerializeAsString()); |
| } |
| } |
| |
| // Test using different token validity intervals. |
| TEST_F(TokenTest, TestVaryingTokenValidityIntervals) { |
| constexpr int kShortValiditySeconds = 2; |
| const int kLongValiditySeconds = kShortValiditySeconds * 3; |
| auto verifier(make_shared<TokenVerifier>()); |
| TokenSigner signer(kLongValiditySeconds, kShortValiditySeconds, 10, verifier); |
| unique_ptr<TokenSigningPrivateKey> key; |
| ASSERT_OK(signer.CheckNeedKey(&key)); |
| ASSERT_NE(nullptr, key.get()); |
| ASSERT_OK(signer.AddKey(std::move(key))); |
| |
| const TablePrivilegePB table_privilege; |
| SignedTokenPB signed_authn; |
| SignedTokenPB signed_authz; |
| ASSERT_OK(signer.GenerateAuthnToken(kUser, &signed_authn)); |
| ASSERT_OK(signer.GenerateAuthzToken(kUser, table_privilege, &signed_authz)); |
| TokenPB authn_token; |
| TokenPB authz_token; |
| ASSERT_EQ(TokenVerificationResult::VALID, |
| verifier->VerifyTokenSignature(signed_authn, &authn_token)); |
| ASSERT_EQ(TokenVerificationResult::VALID, |
| verifier->VerifyTokenSignature(signed_authz, &authz_token)); |
| |
| // Wait for the authz validity interval to pass and verify its expiration. |
| SleepFor(MonoDelta::FromSeconds(1 + kShortValiditySeconds)); |
| EXPECT_EQ(TokenVerificationResult::VALID, |
| verifier->VerifyTokenSignature(signed_authn, &authn_token)); |
| EXPECT_EQ(TokenVerificationResult::EXPIRED_TOKEN, |
| verifier->VerifyTokenSignature(signed_authz, &authz_token)); |
| |
| // Wait for the authn validity interval to pass and verify its expiration. |
| SleepFor(MonoDelta::FromSeconds(kLongValiditySeconds - kShortValiditySeconds)); |
| EXPECT_EQ(TokenVerificationResult::EXPIRED_TOKEN, |
| verifier->VerifyTokenSignature(signed_authn, &authn_token)); |
| EXPECT_EQ(TokenVerificationResult::EXPIRED_TOKEN, |
| verifier->VerifyTokenSignature(signed_authz, &authz_token)); |
| } |
| |
| // Test to check the invariant that all tokens signed within a TSK's activity |
| // interval must be expired by the end of the TSK's validity interval. |
| TEST_F(TokenTest, TestKeyValidity) { |
| SKIP_IF_SLOW_NOT_ALLOWED(); |
| // Note: this test's runtime is roughly the length of a key-validity |
| // interval, which is determined by the token validity intervals and the key |
| // rotation interval. |
| const int kShortValiditySeconds = 2; |
| const int kLongValiditySeconds = 6; |
| const int kKeyRotationSeconds = 5; |
| auto verifier(make_shared<TokenVerifier>()); |
| TokenSigner signer(kLongValiditySeconds, kShortValiditySeconds, kKeyRotationSeconds, verifier); |
| unique_ptr<TokenSigningPrivateKey> key; |
| ASSERT_OK(signer.CheckNeedKey(&key)); |
| ASSERT_NE(nullptr, key.get()); |
| ASSERT_OK(signer.AddKey(std::move(key))); |
| |
| // First, start a countdown for the first TSK's validity interval. Any token |
| // signed during the first TSK's activity interval must be expired once this |
| // latch counts down. |
| vector<thread> threads; |
| CountDownLatch first_tsk_validity_latch(1); |
| const double key_validity_seconds = signer.key_validity_seconds_; |
| threads.emplace_back([&first_tsk_validity_latch, key_validity_seconds] { |
| SleepFor(MonoDelta::FromSeconds(key_validity_seconds)); |
| LOG(INFO) << Substitute("First TSK's validity interval of $0 secs has finished!", |
| key_validity_seconds); |
| first_tsk_validity_latch.CountDown(); |
| }); |
| |
| // Set up a second TSK so our threads can rotate TSKs when the time comes. |
| while (true) { |
| KLOG_EVERY_N_SECS(INFO, 1) << "Waiting for a second key..."; |
| unique_ptr<TokenSigningPrivateKey> tsk; |
| ASSERT_OK(signer.CheckNeedKey(&tsk)); |
| if (tsk) { |
| LOG(INFO) << "Added second key!"; |
| ASSERT_OK(signer.AddKey(std::move(tsk))); |
| break; |
| } |
| SleepFor(MonoDelta::FromMilliseconds(50)); |
| } |
| |
| // Utility lambda to check that the token is expired. |
| const auto verify_expired = [&verifier] (const SignedTokenPB& signed_token, |
| const string& token_type) { |
| TokenPB token_pb; |
| const auto result = verifier->VerifyTokenSignature(signed_token, &token_pb); |
| const auto expire_secs = token_pb.expire_unix_epoch_seconds(); |
| ASSERT_EQ(TokenVerificationResult::EXPIRED_TOKEN, result) |
| << Substitute("validation result '$0': $1 token expires at $2, now $3", |
| TokenVerificationResultToString(result), token_type, |
| expire_secs, WallTime_Now()); |
| }; |
| |
| // Create a thread that repeatedly signs new authn tokens, returning the |
| // final one signed by TSK with seq_num 0. At the end of the key validity |
| // period, this token will not be valid. |
| vector<SignedTokenPB> tsks(2); |
| vector<Status> results(2); |
| threads.emplace_back([&] { |
| results[0] = SignUntilRotatePast(&signer, |
| [&] (SignedTokenPB* signed_token) { |
| return signer.GenerateAuthnToken(kUser, signed_token); |
| }, |
| "authn", 0, &tsks[0]); |
| first_tsk_validity_latch.Wait(); |
| }); |
| |
| // Do the same for authz tokens. |
| threads.emplace_back([&] { |
| results[1] = SignUntilRotatePast(&signer, |
| [&] (SignedTokenPB* signed_token) { |
| return signer.GenerateAuthzToken(kUser, TablePrivilegePB(), signed_token); |
| }, |
| "authz", 0, &tsks[1]); |
| first_tsk_validity_latch.Wait(); |
| }); |
| |
| for (auto& t : threads) { |
| t.join(); |
| } |
| EXPECT_OK(results[0]); |
| EXPECT_OK(results[1]); |
| NO_FATALS(verify_expired(tsks[0], "authn")); |
| NO_FATALS(verify_expired(tsks[1], "authz")); |
| } |
| |
| } // namespace security |
| } // namespace kudu |