common/protobuf/kudu/security/token-test.cc (634 lines of code) (raw):

// 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