summaryrefslogtreecommitdiffstats
path: root/chrome/browser/internal_auth.cc
diff options
context:
space:
mode:
authordilmah@chromium.org <dilmah@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98>2011-05-18 11:58:44 +0000
committerdilmah@chromium.org <dilmah@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98>2011-05-18 11:58:44 +0000
commitc6e584c20129f8745e6fc9170a220eb58e13e172 (patch)
tree6491e890f845af7443f6be07d15d9e60c89ec998 /chrome/browser/internal_auth.cc
parent37e7790801761dc99be00d69f102b7319f2d6a8e (diff)
downloadchromium_src-c6e584c20129f8745e6fc9170a220eb58e13e172.zip
chromium_src-c6e584c20129f8745e6fc9170a220eb58e13e172.tar.gz
chromium_src-c6e584c20129f8745e6fc9170a220eb58e13e172.tar.bz2
Private API for extensions like ssh-client that need access to websocket-to-tcp proxy.
Access to TCP is obtained in following way: (1) extension requests authentication token via call to private API like: chrome.webSocketProxyPrivate.getPassportForTCP('netbsd.org', 25, callback); if API validates this request then extension obtains some string token (in callback). (2) open websocket connection to local websocket-to-tcp proxy ws://127.0.0.1:10101/tcpproxy (3) pass header containing hostname, port and token obtained at step (1) (4) communicate (in base64 encoding at this moment). Proxy (running in chrome process) verifies those tokens by calls to InternalAuthVerification::VerifyPassport Passports are one-time; no passport can be reused. Passports expire in short period of time (20 seconds). BUG=chromium-os:9667 TEST=unit_test,apitest Review URL: http://codereview.chromium.org/6683060 git-svn-id: svn://svn.chromium.org/chrome/trunk/src@85757 0039d316-1c4b-4281-b951-d872f2087c98
Diffstat (limited to 'chrome/browser/internal_auth.cc')
-rw-r--r--chrome/browser/internal_auth.cc480
1 files changed, 480 insertions, 0 deletions
diff --git a/chrome/browser/internal_auth.cc b/chrome/browser/internal_auth.cc
new file mode 100644
index 0000000..268ee1b
--- /dev/null
+++ b/chrome/browser/internal_auth.cc
@@ -0,0 +1,480 @@
+// Copyright (c) 2011 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "chrome/browser/internal_auth.h"
+
+#include <algorithm>
+#include <deque>
+
+#include "base/base64.h"
+#include "base/lazy_instance.h"
+#include "base/rand_util.h"
+#include "base/synchronization/lock.h"
+#include "base/string_number_conversions.h"
+#include "base/string_split.h"
+#include "base/string_util.h"
+#include "base/threading/thread_checker.h"
+#include "base/time.h"
+#include "base/timer.h"
+#include "base/values.h"
+#include "content/browser/browser_thread.h"
+#include "crypto/hmac.h"
+
+namespace {
+
+typedef std::map<std::string, std::string> VarValueMap;
+
+// Size of a tick in microseconds. This determines upper bound for average
+// number of passports generated per time unit. This bound equals to
+// (kMicrosecondsPerSecond / TickUs) calls per second.
+const int64 kTickUs = 10000;
+
+// Verification window size in ticks; that means any passport expires in
+// (kVerificationWindowTicks * TickUs / kMicrosecondsPerSecond) seconds.
+const int kVerificationWindowTicks = 2000;
+
+// Generation window determines how well we are able to cope with bursts of
+// GeneratePassport calls those exceed upper bound on average speed.
+const int kGenerationWindowTicks = 20;
+
+// Makes no sense to compare other way round.
+COMPILE_ASSERT(kGenerationWindowTicks <= kVerificationWindowTicks,
+ makes_no_sense_to_have_generation_window_larger_than_verification_one);
+// We are not optimized for high value of kGenerationWindowTicks.
+COMPILE_ASSERT(kGenerationWindowTicks < 30, too_large_generation_window);
+
+// Regenerate key after this number of ticks.
+const int kKeyRegenerationSoftTicks = 500000;
+// Reject passports if key has not been regenerated in that number of ticks.
+const int kKeyRegenerationHardTicks = kKeyRegenerationSoftTicks * 2;
+
+// Limit for number of accepted var=value pairs. Feel free to bump this limit
+// higher once needed.
+const size_t kVarsLimit = 16;
+
+// Limit for length of caller-supplied strings. Feel free to bump this limit
+// higher once needed.
+const size_t kStringLengthLimit = 512;
+
+// Character used as a separator for construction of message to take HMAC of.
+// It is critical to validate all caller-supplied data (used to construct
+// message) to be clear of this separator because it could allow attacks.
+const char kItemSeparator = '\n';
+
+// Character used for var=value separation.
+const char kVarValueSeparator = '=';
+
+const size_t kKeySizeInBytes = 128 / 8;
+const int kHMACSizeInBytes = 256 / 8;
+
+// Length of base64 string required to encode given number of raw octets.
+#define BASE64_PER_RAW(X) (X > 0 ? ((X - 1) / 3 + 1) * 4 : 0)
+
+// Size of decimal string representing 64-bit tick.
+const size_t kTickStringLength = 20;
+
+// A passport consists of 2 parts: HMAC and tick.
+const size_t kPassportSize =
+ BASE64_PER_RAW(kHMACSizeInBytes) + kTickStringLength;
+
+int64 GetCurrentTick() {
+ int64 tick = base::Time::Now().ToInternalValue() / kTickUs;
+ if (tick < kVerificationWindowTicks ||
+ tick < kKeyRegenerationHardTicks ||
+ tick > kint64max - kKeyRegenerationHardTicks) {
+ return 0;
+ }
+ return tick;
+}
+
+bool IsDomainSane(const std::string& domain) {
+ return !domain.empty() &&
+ domain.size() <= kStringLengthLimit &&
+ IsStringUTF8(domain) &&
+ domain.find_first_of(kItemSeparator) == std::string::npos;
+}
+
+bool IsVarSane(const std::string& var) {
+ static const char kAllowedChars[] =
+ "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
+ "abcdefghijklmnopqrstuvwxyz"
+ "0123456789"
+ "_";
+ COMPILE_ASSERT(
+ sizeof(kAllowedChars) == 26 + 26 + 10 + 1 + 1, some_mess_with_chars);
+ // We must not allow kItemSeparator in anything used as an input to construct
+ // message to sign.
+ DCHECK(std::find(kAllowedChars, kAllowedChars + arraysize(kAllowedChars),
+ kItemSeparator) == kAllowedChars + arraysize(kAllowedChars));
+ DCHECK(std::find(kAllowedChars, kAllowedChars + arraysize(kAllowedChars),
+ kVarValueSeparator) == kAllowedChars + arraysize(kAllowedChars));
+ return !var.empty() &&
+ var.size() <= kStringLengthLimit &&
+ IsStringASCII(var) &&
+ var.find_first_not_of(kAllowedChars) == std::string::npos &&
+ !IsAsciiDigit(var[0]);
+}
+
+bool IsValueSane(const std::string& value) {
+ return value.size() <= kStringLengthLimit &&
+ IsStringUTF8(value) &&
+ value.find_first_of(kItemSeparator) == std::string::npos;
+}
+
+bool IsVarValueMapSane(const VarValueMap& map) {
+ if (map.size() > kVarsLimit)
+ return false;
+ for (VarValueMap::const_iterator it = map.begin(); it != map.end(); ++it) {
+ const std::string& var = it->first;
+ const std::string& value = it->second;
+ if (!IsVarSane(var) || !IsValueSane(value))
+ return false;
+ }
+ return true;
+}
+
+void ConvertVarValueMapToBlob(const VarValueMap& map, std::string* out) {
+ out->clear();
+ DCHECK(IsVarValueMapSane(map));
+ for (VarValueMap::const_iterator it = map.begin(); it != map.end(); ++it)
+ *out += it->first + kVarValueSeparator + it->second + kItemSeparator;
+}
+
+void CreatePassport(
+ const std::string& domain,
+ const VarValueMap& map,
+ int64 tick,
+ const crypto::HMAC* engine,
+ std::string* out) {
+ DCHECK(engine);
+ DCHECK(out);
+ DCHECK(IsDomainSane(domain));
+ DCHECK(IsVarValueMapSane(map));
+
+ out->clear();
+ std::string result(kPassportSize, '0');
+
+ std::string blob;
+ blob = domain + kItemSeparator;
+ std::string tmp;
+ ConvertVarValueMapToBlob(map, &tmp);
+ blob += tmp + kItemSeparator + base::Uint64ToString(tick);
+
+ std::string hmac;
+ unsigned char* hmac_data = reinterpret_cast<unsigned char*>(
+ WriteInto(&hmac, kHMACSizeInBytes + 1));
+ if (!engine->Sign(blob, hmac_data, kHMACSizeInBytes)) {
+ NOTREACHED();
+ return;
+ }
+ std::string hmac_base64;
+ if (!base::Base64Encode(hmac, &hmac_base64)) {
+ NOTREACHED();
+ return;
+ }
+ if (hmac_base64.size() != BASE64_PER_RAW(kHMACSizeInBytes)) {
+ NOTREACHED();
+ return;
+ }
+ DCHECK(hmac_base64.size() < result.size());
+ std::copy(hmac_base64.begin(), hmac_base64.end(), result.begin());
+
+ std::string tick_decimal = base::Uint64ToString(tick);
+ DCHECK(tick_decimal.size() <= kTickStringLength);
+ std::copy(
+ tick_decimal.begin(),
+ tick_decimal.end(),
+ result.begin() + kPassportSize - tick_decimal.size());
+
+ out->swap(result);
+}
+
+} // namespace
+
+namespace browser {
+
+class InternalAuthVerificationService {
+ public:
+ InternalAuthVerificationService()
+ : key_change_tick_(0),
+ dark_tick_(0) {
+ }
+
+ bool VerifyPassport(
+ const std::string& passport,
+ const std::string& domain,
+ const VarValueMap& map) {
+ int64 current_tick = GetCurrentTick();
+ int64 tick = PreVerifyPassport(passport, domain, current_tick);
+ if (tick == 0)
+ return false;
+ if (!IsVarValueMapSane(map))
+ return false;
+ std::string reference_passport;
+ CreatePassport(domain, map, tick, engine_.get(), &reference_passport);
+ if (passport != reference_passport) {
+ // Consider old key.
+ if (key_change_tick_ + get_verification_window_ticks() < tick) {
+ return false;
+ }
+ if (old_key_.empty() || old_engine_ == NULL)
+ return false;
+ CreatePassport(domain, map, tick, old_engine_.get(), &reference_passport);
+ if (passport != reference_passport)
+ return false;
+ }
+
+ // Record used tick to prevent reuse.
+ std::deque<int64>::iterator it = std::lower_bound(
+ used_ticks_.begin(), used_ticks_.end(), tick);
+ DCHECK(it == used_ticks_.end() || *it != tick);
+ used_ticks_.insert(it, tick);
+
+ // Consider pruning |used_ticks_|.
+ if (used_ticks_.size() > 50) {
+ dark_tick_ = std::max(dark_tick_,
+ current_tick - get_verification_window_ticks());
+ used_ticks_.erase(
+ used_ticks_.begin(),
+ std::lower_bound(used_ticks_.begin(), used_ticks_.end(),
+ dark_tick_ + 1));
+ }
+ return true;
+ }
+
+ void ChangeKey(const std::string& key) {
+ old_key_.swap(key_);
+ key_.clear();
+ old_engine_.swap(engine_);
+ engine_.reset(NULL);
+
+ if (key.size() != kKeySizeInBytes)
+ return;
+ engine_.reset(new crypto::HMAC(crypto::HMAC::SHA256));
+ engine_->Init(key);
+ key_ = key;
+ key_change_tick_ = GetCurrentTick();
+ }
+
+ private:
+ static int get_verification_window_ticks() {
+ return InternalAuthVerification::get_verification_window_ticks();
+ }
+
+ // Returns tick bound to given passport on success or zero on failure.
+ int64 PreVerifyPassport(
+ const std::string& passport,
+ const std::string& domain,
+ int64 current_tick) {
+ if (passport.size() != kPassportSize ||
+ !IsStringASCII(passport) ||
+ !IsDomainSane(domain) ||
+ current_tick <= dark_tick_ ||
+ current_tick > key_change_tick_ + kKeyRegenerationHardTicks ||
+ key_.empty() ||
+ engine_ == NULL) {
+ return 0;
+ }
+
+ // Passport consists of 2 parts: first hmac and then tick.
+ std::string tick_decimal =
+ passport.substr(BASE64_PER_RAW(kHMACSizeInBytes));
+ DCHECK(tick_decimal.size() == kTickStringLength);
+ int64 tick = 0;
+ if (!base::StringToInt64(tick_decimal, &tick) ||
+ tick <= dark_tick_ ||
+ tick > key_change_tick_ + kKeyRegenerationHardTicks ||
+ tick < current_tick - get_verification_window_ticks() ||
+ std::binary_search(used_ticks_.begin(), used_ticks_.end(), tick)) {
+ return 0;
+ }
+ return tick;
+ }
+
+ // Current key.
+ std::string key_;
+
+ // We keep previous key in order to be able to verify passports during
+ // regeneration time. Keys are regenerated on a regular basis.
+ std::string old_key_;
+
+ // Corresponding HMAC engines.
+ scoped_ptr<crypto::HMAC> engine_;
+ scoped_ptr<crypto::HMAC> old_engine_;
+
+ // Tick at a time of recent key regeneration.
+ int64 key_change_tick_;
+
+ // Keeps track of ticks of successfully verified passports to prevent their
+ // reuse. Size of this container is kept reasonably low by purging outdated
+ // ticks.
+ std::deque<int64> used_ticks_;
+
+ // Some ticks before |dark_tick_| were purged from |used_ticks_| container.
+ // That means that we must not trust any tick less than or equal to dark tick.
+ int64 dark_tick_;
+
+ DISALLOW_COPY_AND_ASSIGN(InternalAuthVerificationService);
+};
+
+} // namespace browser
+
+namespace {
+
+static base::LazyInstance<browser::InternalAuthVerificationService>
+ g_verification_service(base::LINKER_INITIALIZED);
+static base::LazyInstance<base::Lock> g_verification_service_lock(
+ base::LINKER_INITIALIZED);
+
+} // namespace
+
+namespace browser {
+
+class InternalAuthGenerationService : public base::ThreadChecker {
+ public:
+ InternalAuthGenerationService() : key_regeneration_tick_(0) {
+ GenerateNewKey();
+ }
+
+ void GenerateNewKey() {
+ DCHECK(CalledOnValidThread());
+ if (!timer_.IsRunning()) {
+ timer_.Start(
+ base::TimeDelta::FromMicroseconds(
+ kKeyRegenerationSoftTicks * kTickUs),
+ this,
+ &InternalAuthGenerationService::GenerateNewKey);
+ }
+
+ engine_.reset(new crypto::HMAC(crypto::HMAC::SHA256));
+ std::string key = base::RandBytesAsString(kKeySizeInBytes);
+ engine_->Init(key);
+ key_regeneration_tick_ = GetCurrentTick();
+ g_verification_service.Get().ChangeKey(key);
+ std::fill(key.begin(), key.end(), 0);
+ }
+
+ // Returns zero on failure.
+ int64 GetUnusedTick(const std::string& domain) {
+ DCHECK(CalledOnValidThread());
+ if (engine_ == NULL) {
+ NOTREACHED();
+ return 0;
+ }
+ if (!IsDomainSane(domain))
+ return 0;
+
+ int64 current_tick = GetCurrentTick();
+ if (!used_ticks_.empty() && used_ticks_.back() > current_tick)
+ current_tick = used_ticks_.back();
+ if (current_tick > key_regeneration_tick_ + kKeyRegenerationHardTicks)
+ return 0;
+
+ // Forget outdated ticks if any.
+ used_ticks_.erase(
+ used_ticks_.begin(),
+ std::lower_bound(used_ticks_.begin(), used_ticks_.end(),
+ current_tick - kGenerationWindowTicks + 1));
+ DCHECK(used_ticks_.size() <= kGenerationWindowTicks + 0u);
+ if (used_ticks_.size() >= kGenerationWindowTicks + 0u) {
+ // Average speed of GeneratePassport calls exceeds limit.
+ return 0;
+ }
+ for (int64 tick = current_tick;
+ tick > current_tick - kGenerationWindowTicks;
+ --tick) {
+ int idx = static_cast<int>(used_ticks_.size()) -
+ static_cast<int>(current_tick - tick + 1);
+ if (idx < 0 || used_ticks_[idx] != tick) {
+ DCHECK(used_ticks_.end() ==
+ std::find(used_ticks_.begin(), used_ticks_.end(), tick));
+ return tick;
+ }
+ }
+ NOTREACHED();
+ return 0;
+ }
+
+ std::string GeneratePassport(
+ const std::string& domain, const VarValueMap& map, int64 tick) {
+ DCHECK(CalledOnValidThread());
+ if (tick == 0) {
+ tick = GetUnusedTick(domain);
+ if (tick == 0)
+ return std::string();
+ }
+ if (!IsVarValueMapSane(map))
+ return std::string();
+
+ std::string result;
+ CreatePassport(domain, map, tick, engine_.get(), &result);
+ used_ticks_.insert(
+ std::lower_bound(used_ticks_.begin(), used_ticks_.end(), tick), tick);
+ return result;
+ }
+
+ private:
+ static int get_verification_window_ticks() {
+ return InternalAuthVerification::get_verification_window_ticks();
+ }
+
+ scoped_ptr<crypto::HMAC> engine_;
+ base::RepeatingTimer<InternalAuthGenerationService> timer_;
+ int64 key_regeneration_tick_;
+ std::deque<int64> used_ticks_;
+
+ DISALLOW_COPY_AND_ASSIGN(InternalAuthGenerationService);
+};
+
+} // namespace browser
+
+namespace {
+
+static base::LazyInstance<browser::InternalAuthGenerationService>
+ g_generation_service(base::LINKER_INITIALIZED);
+
+} // namespace
+
+namespace browser {
+
+// static
+bool InternalAuthVerification::VerifyPassport(
+ const std::string& passport,
+ const std::string& domain,
+ const VarValueMap& var_value_map) {
+ base::AutoLock alk(g_verification_service_lock.Get());
+ return g_verification_service.Get().VerifyPassport(
+ passport, domain, var_value_map);
+}
+
+// static
+void InternalAuthVerification::ChangeKey(const std::string& key) {
+ base::AutoLock alk(g_verification_service_lock.Get());
+ g_verification_service.Get().ChangeKey(key);
+};
+
+// static
+int InternalAuthVerification::get_verification_window_ticks() {
+ int candidate = kVerificationWindowTicks;
+ if (verification_window_seconds_ > 0)
+ candidate = verification_window_seconds_ *
+ base::Time::kMicrosecondsPerSecond / kTickUs;
+ return std::max(1, std::min(candidate, kVerificationWindowTicks));
+}
+
+int InternalAuthVerification::verification_window_seconds_ = 0;
+
+// static
+std::string InternalAuthGeneration::GeneratePassport(
+ const std::string& domain, const VarValueMap& var_value_map) {
+ return g_generation_service.Get().GeneratePassport(domain, var_value_map, 0);
+}
+
+// static
+void InternalAuthGeneration::GenerateNewKey() {
+ g_generation_service.Get().GenerateNewKey();
+}
+
+} // namespace browser
+